Compare commits

...
40 Commits
Author SHA1 Message Date
or-else 9bbddc08e6 make broken image not clickable 2019-01-25 17:03:50 +03:00
or-else b0be1fd012 show sock broken_image when the image is invalid 2019-01-25 16:49:24 +03:00
or-else 21303346b1 0.15.12-rc3 2019-01-25 14:24:51 +03:00
or-else bd7531e5b7 fix for tinode/chat#212 2019-01-25 14:24:43 +03:00
or-else a0184ac002 add cosmetic space between words 2019-01-22 13:40:09 +03:00
or-else 4d438ccab4 use locale for dates too 2019-01-14 12:42:26 +03:00
or-else 48e6144568 bum version and rebuild 2019-01-13 14:47:42 +03:00
or-else e028503c9e add "broken_image.png" as fallback 2019-01-13 14:46:46 +03:00
or-else fcb3d78cc4 rebuild 2019-01-06 14:06:46 -08:00
or-else b976a3caa8 0.15.11 2019-01-06 14:02:45 -08:00
or-else cc6a4cd125 rebuild 2018-12-27 16:10:40 +03:00
or-else 6d7f98c55b 0.15.11-rc4 2018-12-27 16:08:31 +03:00
or-else d80ddcabea pass Enter event otherwise crash 2018-12-25 11:08:57 +03:00
or-else 4c44b61a9d bump version of a dependency 2018-12-23 14:49:26 +03:00
or-else 9eb7bebe80 bump version and rebuild 2018-12-17 12:08:27 +03:00
or-else 1f634f5877 remove unnecessary check 2018-12-17 12:06:33 +03:00
or-else f78d673e02 add "reconnect now" option 2018-12-17 12:02:47 +03:00
or-else a5d405b083 add handling og onAutoreconnectIteration 2018-12-17 10:05:37 +03:00
or-else 405ce756a2 allow avatar removal 2018-12-15 14:24:05 +03:00
or-else 31190ec826 rebuild 2018-12-15 12:59:51 +03:00
or-else 9b3c714390 js2016 changes 2018-12-15 12:11:22 +03:00
or-else 2e76b122a2 keep focus on message editing firld whenever possible 2018-12-15 12:05:03 +03:00
or-else 14d75223e2 remove debug logging 2018-12-13 22:22:32 +03:00
or-else bc3e82bcbf bug fixes: crash in editing group member permissions, fnd not working sometimes, var -> let, const 2018-12-13 15:42:54 +03:00
or-else 10d88da558 bump version and rebuild 2018-12-05 13:58:19 +03:00
or-else 4dde45b6be 0.15.10 2018-12-05 13:54:44 +03:00
or-else 00eeb2503a css fix 2018-12-01 15:51:21 +03:00
or-else 4f70de3aa9 make topic title required 2018-12-01 11:34:42 +03:00
or-else 486cf2e355 rebuild 2018-11-30 21:37:11 +03:00
or-else 21841c0022 add missing import 2018-11-30 21:36:02 +03:00
or-else d1f7f02fa0 correctlu display messages from missing users 2018-11-24 18:23:29 +03:00
or-else fbbab3e7d6 fixes in settings view and message deletion 2018-11-21 11:50:04 +03:00
or-else 7248b5c398 keeping scroll position on vertical resize works now 2018-11-19 16:05:49 +03:00
or-else 96de18ea7b 0.15.10-rc1 2018-11-17 21:34:20 +03:00
or-else 198bd9a64c keep message view in the same scroll position on vertical resize 2018-11-17 21:34:13 +03:00
or-else 9324faa32f rebuild 2018-11-17 12:39:44 +03:00
or-else 769c25170f bug fix, add maps to package 2018-11-13 12:32:46 +03:00
or-else 9e073356b2 remove unused code for form name 2018-11-11 21:25:00 +03:00
or-else 14ff124674 strip formatting from button text 2018-11-11 16:59:39 +03:00
or-else f959aa24cc new drafty formatting seems to work as expected 2018-11-09 14:59:48 +03:00
39 changed files with 640 additions and 375 deletions
+38 -34
View File
@@ -511,6 +511,10 @@ table.permission-editor td.checkbox {
cursor: pointer;
}
.image-clickable {
cursor: pointer;
}
/* END of top-level layout */
/* Sidepanel styles */
@@ -548,38 +552,19 @@ table.permission-editor td.checkbox {
flex-shrink: 0;
}
.bot-form, .bot-form.vlist {
.bot-form {
display: flex;
flex-direction: column;
overflow: hidden;
border: 1px solid #ccc;
border-radius: 0.25rem;
background-color: #eee;
min-width: 8rem;
}
.bot-form.hlist {
flex-direction: row;
line-height: 2rem;
}
.bot-form div {
min-height: 2rem;
vertical-align: middle;
}
/* Nested bot forms should have not border */
.bot-form .bot-form {
border: none;
}
/*
Form element with non-interactive content
needs padding.
*/
.bot-form div.dft {
padding: 0.25rem 1rem;
min-height: 2rem;
vertical-align: middle;
}
.bot-form button {
display: inline-block;
margin: 0;
width: 100%;
background-color: #eef;
@@ -589,7 +574,9 @@ needs padding.
height: 2rem;
line-height: 2rem;
}
.bot-form > div > button {
width: auto;
}
.bot-form button:focus, button:hover, button:active {
outline: 0;
background-color: #e3f2fd;
@@ -779,8 +766,8 @@ i.search {
display: flex;
flex-direction: column;
justify-content: space-around;
width: 3rem;
height: 3rem;
font-size: 2.4rem;
line-height: 3rem;
}
.contact-box .text-box {
display: flex;
@@ -885,6 +872,7 @@ i.search {
/* Styling of avatar upload control */
.avatar-upload {
display: flex;
position: relative;
flex-direction: column;
flex-shrink: 0;
width: 7rem;
@@ -894,6 +882,19 @@ i.search {
padding: 0;
}
.avatar-upload .clear-avatar {
position: absolute;
font-size: 1rem;
top: 0;
right: 0;
opacity: 0;
background-image: radial-gradient(#fff 40%, transparent 75%);
}
.avatar-upload:hover .clear-avatar {
opacity: 1;
}
.avatar-upload .avatar-box {
position: relative;
width: 7rem;
@@ -902,8 +903,6 @@ i.search {
.avatar-upload .avatar-box > .material-icons {
position: relative;
width: 7rem;
height: 7rem;
font-size: 4rem;
line-height: 7rem;
vertical-align: middle;
@@ -1003,6 +1002,11 @@ i.search {
margin-right: 0.35rem;
}
#topic-caption-panel .avatar-box > .material-icons {
font-size: 2.4rem;
line-height: 3rem;
}
#hide-message-view {
display: block;
margin-right: 0.35rem;
@@ -1306,8 +1310,8 @@ i.search {
* (group chat only)
*/
.chat-box .author {
font-size: 80%;
color:#777;
font-size: 80%;
color:#777;
}
/* Inline image styles */
@@ -1316,7 +1320,6 @@ i.search {
max-height: 24rem;
overflow: hidden;
border-radius: 0.1rem;
cursor: pointer;
display: block;
}
@@ -1684,16 +1687,16 @@ ul.tabbar li.active > a:after {
font-size: 2.4rem;
}
#self-avatar.material-icons, .avatar-box .material-icons {
#self-avatar .material-icons {
font-size: 2.4rem;
width: 3rem;
height: 3rem;
line-height: 3rem;
}
.avatar-box .material-icons {
color: rgba(250, 250, 250, 0.8); /* #fafafa, gray 50 */
background-color: #c0c0c0;
border-radius: 50%;
width: inherit;
height: inherit;
}
.chip-input {
@@ -1778,7 +1781,8 @@ ul.tabbar li.active > a:after {
}
.chip .avatar-box .material-icons {
font-size: 1.8rem;
font-size: 1.4rem;
line-height: 1.8rem;
}
.chip .spacer {
+1 -1
View File
File diff suppressed because one or more lines are too long
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

+16 -21
View File
@@ -1,6 +1,6 @@
{
"name": "tinode-webapp",
"version": "0.15.9-rc1",
"version": "0.15.12-rc3",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -1775,26 +1775,26 @@
}
},
"browserslist": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.3.3.tgz",
"integrity": "sha512-6h84UD1mmHeuQ9IucX6yzBc+KBYcBBTLYt2CXtY7GYCra6iE5kOm7oM+zuGw/0tjGtbJxjm58OvxSBmogEMCRQ==",
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.4.1.tgz",
"integrity": "sha512-pEBxEXg7JwaakBXjATYw/D1YZh4QUSCX/Mnd/wnqSRPPSi1U39iDhDoKGoBUcraKdxDlrYqJxSI5nNvD+dWP2A==",
"dev": true,
"requires": {
"caniuse-lite": "^1.0.30000898",
"electron-to-chromium": "^1.3.81",
"node-releases": "^1.0.0-alpha.15"
"caniuse-lite": "^1.0.30000929",
"electron-to-chromium": "^1.3.103",
"node-releases": "^1.1.3"
},
"dependencies": {
"caniuse-lite": {
"version": "1.0.30000899",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000899.tgz",
"integrity": "sha512-enC3zKfUCJxxwvUIsBkbHd54CtJw1KtIWvrK0JZxWD/fEN2knHaai45lndJ4xXAkyRAPyk60J3yagkKDWhfeMA==",
"version": "1.0.30000932",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000932.tgz",
"integrity": "sha512-4bghJFItvzz8m0T3lLZbacmEY9X1Z2AtIzTr7s7byqZIOumASfr4ynDx7rtm0J85nDmx8vsgR6vnaSoeU8Oh0A==",
"dev": true
},
"node-releases": {
"version": "1.0.0-alpha.15",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.0.0-alpha.15.tgz",
"integrity": "sha512-hKG6hd/g6a9OV/ARt2qrxbRhe/4WEMFohTLOB9PNyTYvvI59gICZFzt9/mMgpYUTts06qXlN8H6UjfbIRdnW8A==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.3.tgz",
"integrity": "sha512-6VrvH7z6jqqNFY200kdB6HdzkgM96Oaj9v3dqGfgp6mF+cHmU4wyQKZ2/WPDRVoR0Jz9KqbamaBN0ZhdUaysUQ==",
"dev": true,
"requires": {
"semver": "^5.3.0"
@@ -2780,9 +2780,9 @@
}
},
"electron-to-chromium": {
"version": "1.3.82",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.82.tgz",
"integrity": "sha512-NI4nB2IWGcU4JVT1AE8kBb/dFor4zjLHMLsOROPahppeHrR0FG5uslxMmkp/thO1MvPjM2xhlKoY29/I60s0ew==",
"version": "1.3.107",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.107.tgz",
"integrity": "sha512-EEbV1rhZE6DfbZg/Dt85fyXd8S0oDNXvwgUqkQvxsVXmhNfHxDXRZXUURu/y2J6HSgk+58a56Qqk20U9X7KEbQ==",
"dev": true
},
"elliptic": {
@@ -8105,11 +8105,6 @@
"integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=",
"dev": true
},
"tinode-sdk": {
"version": "0.15.8-rc1",
"resolved": "https://registry.npmjs.org/tinode-sdk/-/tinode-sdk-0.15.8-rc1.tgz",
"integrity": "sha512-139xdnrkBb6mPU4NiLuvMCZ2Ji/1WmP5WhHI7utdMirewq3l5jnQ0cF86iYBbOMaYhhnLLLsbbm4KjleP+1zHA=="
},
"to-arraybuffer": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz",
+6 -3
View File
@@ -1,14 +1,14 @@
{
"name": "tinode-webapp",
"description": "Tinode messenger for the web",
"version": "0.15.9-rc2",
"version": "0.15.12-rc3",
"devDependencies": {
"@babel/core": "^7.1.2",
"@babel/preset-env": "^7.1.0",
"@babel/preset-react": "^7.0.0",
"babel-loader": "^8.0.4",
"babel-plugin-react-intl": "^3.0.1",
"browserslist": "^4.3.3",
"browserslist": "^4.4.1",
"cssnano": "^4.0.5",
"webpack": "^4.23.1",
"webpack-cli": "^3.1.2"
@@ -33,7 +33,9 @@
"src/lib/*.js",
"src/i18n/*.json",
"umd/index.prod.js",
"umd/index.prod.js.map",
"umd/index.dev.js",
"umd/index.dev.js.map",
"audio/msg.mp3",
"css/base.css",
"css/base.min.css",
@@ -44,6 +46,7 @@
"img/logo96.png",
"img/logo192.png",
"img/badge96.png",
"img/broken_image.png",
"index-dev.html",
"index.html",
"manifest.json",
@@ -76,6 +79,6 @@
"react": "^16.5.2",
"react-dom": "^16.5.2",
"react-intl": "^2.7.1",
"tinode-sdk": "^0.15.8-rc1"
"tinode-sdk": "^0.15.11-rc2"
}
}
+3
View File
@@ -32,6 +32,9 @@ export const REM_SIZE = 13;
// a square of this size.
export const AVATAR_SIZE = 128;
// Size of the broken_image shown in MessagesView
export const BROKEN_IMAGE_SIZE = 32;
// Number of chat messages to fetch in one call.
export const MESSAGES_PAGE = 24;
+5 -1
View File
@@ -1,5 +1,5 @@
{
"contacts_not_found": "You have no chats<br />¯\\\\_(ツ)_/¯",
"contacts_not_found": "You have no chats<br />¯∖_(ツ)_/¯",
"full_name_prompt": "Full name, e.g. John Doe",
"email_prompt": "Email, e.g. jdoe@example.com",
"button_sign_up": "Sign up",
@@ -64,12 +64,16 @@
"sidepanel_title_cred": "Confirm Credentials",
"sidepanel_title_reset": "Reset Password",
"update_available": "Update available. <a href=\"\">Reload</a>.",
"reconnect_countdown": "Disconnected. Reconnecting in {seconds}s…",
"reconnect_now": "Try now",
"phone_dative": "phone",
"email_dative": "email",
"enter_confirmation_code_prompt": "Enter confirmation code sent to you by {method}:",
"numeric_confirmation_code_prompt": "Numbers only",
"button_confirm": "Confirm",
"save_attachment": "save",
"invalid_content": "invalid content",
"user_not_found": "Not found",
"badge_you": "you",
"badge_owner": "owner",
"menu_item_info": "Info",
+6 -2
View File
@@ -1,5 +1,5 @@
{
"contacts_not_found": "Чатов нет<br />¯\\\\_(ツ)_/¯",
"contacts_not_found": "Чатов нет<br />¯∖_(ツ)_/¯",
"full_name_prompt": "Полное имя, напр. Иван Петров",
"email_prompt": "Email, напр. ivan@example.com",
"button_sign_up": "Создать счет",
@@ -118,5 +118,9 @@
"button_create": "Создать",
"badge_you": "вы",
"badge_owner": "влад.",
"update_available": "Есть новая версия приложения. <a href=\"\">Обновить</a>."
"update_available": "Есть новая версия приложения. <a href=\"\">Обновить</a>.",
"user_not_found": "Не найден",
"reconnect_countdown": "Нет связи. Подключение через {seconds}с…",
"reconnect_now": "Подключить сейчас.",
"save_attachment": "сохранить"
}
+7 -1
View File
@@ -15,7 +15,13 @@ export function makeImageUrl(photo) {
// Returns an object which contains destination sizes, source sizes, and offsets
// into source (when making square images).
export function fitImageSize(width, height, maxWidth, maxHeight, forceSquare) {
if (!width || !height || !maxWidth || !maxHeight) {
// Sanitize input
width = width | 0;
height = height | 0;
maxWidth = maxWidth | 0;
maxHeight = maxHeight | 0;
if (width <= 0 || height <= 0 || maxWidth <= 0 || maxHeight <= 0) {
return null;
}
+9 -9
View File
@@ -1,7 +1,7 @@
// Short representation of time in the past.
export function shortDateFormat(then) {
var locale = window.navigator.userLanguage || window.navigator.language;
var now = new Date();
export function shortDateFormat(then, locale) {
locale = locale || window.navigator.userLanguage || window.navigator.language;
const now = new Date();
if (then.getFullYear() == now.getFullYear()) {
if (then.getMonth() == now.getMonth() && then.getDate() == now.getDate()) {
return then.toLocaleTimeString(locale, {hour12: false, hour: '2-digit', minute: '2-digit'});
@@ -20,19 +20,19 @@ export function bytesToHumanSize(bytes) {
return '0 Bytes';
}
var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB'];
var bucket = Math.min(Math.floor(Math.log2(bytes) / 10) | 0, sizes.length-1);
var count = bytes / Math.pow(1024, bucket);
var round = bucket > 0 ? (count < 3 ? 2 : (count < 30 ? 1 : 0)) : 0;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB'];
const bucket = Math.min(Math.floor(Math.log2(bytes) / 10) | 0, sizes.length-1);
const count = bytes / Math.pow(1024, bucket);
const round = bucket > 0 ? (count < 3 ? 2 : (count < 30 ? 1 : 0)) : 0;
return count.toFixed(round) + ' ' + sizes[bucket];
}
// Get 32 bit integer hash value for a string. Ideally it should produce the same value
// as Java's String#hash().
export function stringHash(value) {
var hash = 0;
let hash = 0;
value = '' + value;
for (var i = 0; i < value.length; i++) {
for (let i = 0; i < value.length; i++) {
hash = ((hash << 5) - hash) + value.charCodeAt(i);
hash = hash & hash; // Convert to 32bit integer
}
+11 -3
View File
@@ -1,6 +1,6 @@
{
"en": {
"contacts_not_found": "You have no chats<br />¯\\\\_(ツ)_/¯",
"contacts_not_found": "You have no chats<br />¯∖_(ツ)_/¯",
"full_name_prompt": "Full name, e.g. John Doe",
"email_prompt": "Email, e.g. jdoe@example.com",
"button_sign_up": "Sign up",
@@ -65,12 +65,16 @@
"sidepanel_title_cred": "Confirm Credentials",
"sidepanel_title_reset": "Reset Password",
"update_available": "Update available. <a href=\"\">Reload</a>.",
"reconnect_countdown": "Disconnected. Reconnecting in {seconds}s…",
"reconnect_now": "Try now",
"phone_dative": "phone",
"email_dative": "email",
"enter_confirmation_code_prompt": "Enter confirmation code sent to you by {method}:",
"numeric_confirmation_code_prompt": "Numbers only",
"button_confirm": "Confirm",
"save_attachment": "save",
"invalid_content": "invalid content",
"user_not_found": "Not found",
"badge_you": "you",
"badge_owner": "owner",
"menu_item_info": "Info",
@@ -122,7 +126,7 @@
"title_manage_tags": "Manage tags"
},
"ru": {
"contacts_not_found": "Чатов нет<br />¯\\\\_(ツ)_/¯",
"contacts_not_found": "Чатов нет<br />¯∖_(ツ)_/¯",
"full_name_prompt": "Полное имя, напр. Иван Петров",
"email_prompt": "Email, напр. ivan@example.com",
"button_sign_up": "Создать счет",
@@ -241,6 +245,10 @@
"button_create": "Создать",
"badge_you": "вы",
"badge_owner": "влад.",
"update_available": "Есть новая версия приложения. <a href=\"\">Обновить</a>."
"update_available": "Есть новая версия приложения. <a href=\"\">Обновить</a>.",
"user_not_found": "Не найден",
"reconnect_countdown": "Нет связи. Подключение через {seconds}с…",
"reconnect_now": "Подключить сейчас.",
"save_attachment": "сохранить"
}
}
+1 -1
View File
@@ -1,3 +1,3 @@
// This is a generated file. Don't edit.
export const PACKAGE_VERSION = "0.15.9-rc2";
export const PACKAGE_VERSION = "0.15.12-rc3";
+1 -1
View File
@@ -32,7 +32,7 @@ export default class ContactsView extends React.Component {
render() {
return (
<FormattedHTMLMessage id="contacts_not_found"
defaultMesage="You have no chats<br />¯\\\\_(ツ)_/¯"
defaultMesage="You have no chats<br />¯∖_(ツ)_/¯"
description="HTML message shown in ContactList when no contacts are found">{
(no_contacts) => <ContactList
connected={this.props.connected}
+1
View File
@@ -9,6 +9,7 @@ import PermissionsEditor from '../widgets/permissions-editor.jsx';
import TagManager from '../widgets/tag-manager.jsx';
import { makeImageUrl } from '../lib/blob-helpers.js';
import { vcard } from '../lib/utils.js';
export default class EditAccountView extends React.Component {
constructor(props) {
+12 -8
View File
@@ -16,6 +16,7 @@ import PermissionsEditor from '../widgets/permissions-editor.jsx';
import TagManager from '../widgets/tag-manager.jsx';
import { makeImageUrl } from '../lib/blob-helpers.js';
import { vcard } from '../lib/utils.js';
const messages = defineMessages({
requested: {
@@ -96,8 +97,8 @@ class InfoView extends React.Component {
}
// No need to separately handle component mount.
UNSAFE_componentWillReceiveProps(props) {
var topic = this.props.tinode.getTopic(props.topic);
componentDidUpdate(props) {
let topic = this.props.tinode.getTopic(props.topic);
if (!topic) {
return;
}
@@ -136,17 +137,17 @@ class InfoView extends React.Component {
}
resetSubs(topic, props) {
var newState = {contactList: []};
const newState = {contactList: []};
if (topic.getType() == 'p2p') {
// Fetch the other party in the p2p conversation.
// Topic may not be ready yet, so check if user is found.
var user2 = topic.subscriber(props.topic);
const user2 = topic.subscriber(props.topic);
if (user2) {
newState.modeGiven2 = user2.acs.getGiven();
newState.modeWant2 = user2.acs.getWant();
}
} else {
topic.subscribers(function(sub) {
topic.subscribers((sub) => {
newState.contactList.push(sub);
}, this);
}
@@ -155,12 +156,13 @@ class InfoView extends React.Component {
}
resetDesc(topic, props) {
var defacs = topic.getDefaultAccess() || {};
var acs = topic.getAccessMode();
const defacs = topic.getDefaultAccess() || {};
const acs = topic.getAccessMode();
this.setState({
owner: acs && acs.isOwner(),
admin: acs && acs.isAdmin(),
sharer: acs && (acs.isAdmin() || acs.isSharer()),
sharer: acs && acs.isSharer(),
muted: acs && acs.isMuted(),
fullName: topic.public ? topic.public.fn : undefined,
@@ -215,6 +217,7 @@ class InfoView extends React.Component {
}
handleFullNameUpdate(fn) {
fn = fn.trim();
if (this.state.fullName !== fn) {
this.setState({fullName: fn});
this.props.onTopicDescUpdate(this.props.topic,
@@ -448,6 +451,7 @@ class InfoView extends React.Component {
placeholder={this.state.groupTopic ? "Group name" : <i>Unknown</i>}
readOnly={!this.state.owner}
value={this.state.fullName}
required={true}
onFinished={this.handleFullNameUpdate} /></div>
<div>
<label className="small">
+1 -2
View File
@@ -17,8 +17,7 @@ export default class LogoView extends React.PureComponent {
<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>
<p><FormattedMessage id="label_server" defaultMessage="Server:" /> {this.props.serverVersion} ({this.props.serverAddress})</p>
</div>
</div>
);
+18 -13
View File
@@ -55,7 +55,7 @@ class MessagesView extends React.Component {
this.propsChange = this.propsChange.bind(this);
this.leave = this.leave.bind(this);
this.handleScrollReference = this.handleScrollReference.bind(this);
this.fetchMoreMessages = this.fetchMoreMessages.bind(this);
this.handleScrollEvent = this.handleScrollEvent.bind(this);
this.handleDescChange = this.handleDescChange.bind(this);
this.handleSubsUpdated = this.handleSubsUpdated.bind(this);
this.handleNewMessage = this.handleNewMessage.bind(this);
@@ -69,26 +69,29 @@ class MessagesView extends React.Component {
this.handleBackNavigation = this.handleBackNavigation.bind(this);
}
// Scroll last message into view on component update e.g. on message received.
// Scroll last message into view on component update e.g. on message received
// or vertical shrinking.
componentDidUpdate(prevProps, prevState) {
if (this.messagesScroller &&
(prevState.title != this.state.title ||
prevState.messages.length != this.state.messages.length)) {
this.messagesScroller.scrollTop = this.messagesScroller.scrollHeight - this.state.scrollPosition;
if (this.messagesScroller) {
if (prevState.title != this.state.title || prevState.messages.length != this.state.messages.length) {
this.messagesScroller.scrollTop = this.messagesScroller.scrollHeight - this.state.scrollPosition;
} else if (prevProps.viewportHeight > this.props.viewportHeight) {
this.messagesScroller.scrollTop += prevProps.viewportHeight - this.props.viewportHeight;
}
}
}
componentDidMount() {
this.propsChange(this.props);
if (this.messagesScroller) {
this.messagesScroller.addEventListener('scroll', this.fetchMoreMessages);
this.messagesScroller.addEventListener('scroll', this.handleScrollEvent);
}
}
componentWillUnmount() {
this.leave(this.state.topic);
if (this.messagesScroller) {
this.messagesScroller.removeEventListener('scroll', this.fetchMoreMessages);
this.messagesScroller.removeEventListener('scroll', this.handleScrollEvent);
}
}
@@ -227,18 +230,19 @@ class MessagesView extends React.Component {
handleScrollReference(node) {
if (node) {
node.addEventListener('scroll', this.fetchMoreMessages);
node.addEventListener('scroll', this.handleScrollEvent);
this.messagesScroller = node;
}
}
// Get older messages
fetchMoreMessages(event) {
handleScrollEvent(event) {
this.setState({scrollPosition: event.target.scrollHeight - event.target.scrollTop});
if (event.target.scrollTop <= 0) {
let newState = {scrollPosition: event.target.scrollHeight - event.target.scrollTop};
this.setState((prevState, props) => {
let newState = {};
if (!prevState.fetchingMessages) {
var topic = this.props.tinode.getTopic(this.state.topic);
let topic = this.props.tinode.getTopic(this.state.topic);
if (topic && topic.isSubscribed() && topic.msgHasMoreMessages()) {
newState.fetchingMessages = true;
topic.getMessagesPage(MESSAGES_PAGE).catch((err) => {
@@ -467,7 +471,8 @@ class MessagesView extends React.Component {
if (cont.online) {
lastSeen = formatMessage(messages.online_now);
} else if (cont.seen) {
lastSeen = formatMessage(messages.last_seen) + ": " + shortDateFormat(cont.seen.when);
lastSeen = formatMessage(messages.last_seen) + ": " +
shortDateFormat(cont.seen.when, this.props.intl.locale);
// TODO: also handle user agent in c.seen.ua
}
}
+3 -10
View File
@@ -8,14 +8,14 @@ import NewTopicGroup from '../widgets/new-topic-group.jsx';
import SearchContacts from '../widgets/search-contacts.jsx';
import HashNavigation from '../lib/navigation.js';
import { vcard } from '../lib/utils.js';
export default class NewTopicView extends React.Component {
constructor(props) {
super(props);
this.state = {
tabSelected: 'p2p',
contactList: props.searchResults
tabSelected: 'p2p'
};
this.handleTabClick = this.handleTabClick.bind(this);
@@ -28,12 +28,6 @@ export default class NewTopicView extends React.Component {
this.props.onInitFind();
}
static getDerivedStateFromProps(nextProps, prevState) {
return {
contactList: nextProps.searchResults
};
}
handleTabClick(e) {
e.preventDefault();
HashNavigation.navigateTo(HashNavigation.addUrlParam(window.location.hash, 'tab', e.currentTarget.dataset.id));
@@ -92,10 +86,9 @@ export default class NewTopicView extends React.Component {
<FormattedMessage id="search_for_contacts" defaultMessage="Use search to find contacts"
description="Prompt in search field.">{
(search_for_contacts) => <ContactList
contacts={this.state.contactList}
contacts={this.props.searchResults}
myUserId={this.props.myUserId}
emptyListMessage={search_for_contacts}
topicSelected={this.state.selectedContact}
showOnline={false}
showUnread={false}
showContextMenu={false}
-1
View File
@@ -32,7 +32,6 @@ export default class SettingsView extends React.PureComponent {
handleServerAddressChange(name) {
this.setState({serverAddress: name});
this.props.onServerAddressChange(name);
}
render() {
+2 -1
View File
@@ -96,6 +96,8 @@ class SidepanelView extends React.Component {
<ErrorPanel
level={this.props.errorLevel}
text={this.props.errorText}
action={this.props.errorAction}
actionText={this.props.errorActionText}
onClearError={this.props.onError} />
{view === 'login' ?
@@ -114,7 +116,6 @@ class SidepanelView extends React.Component {
<SettingsView
transport={this.props.transport}
serverAddress={this.props.serverAddress}
onServerAddressChange={this.props.onGlobalSettings}
onCancel={this.props.onCancel}
onUpdate={this.props.onGlobalSettings} /> :
+97 -26
View File
@@ -29,6 +29,16 @@ const messages = defineMessages({
id: 'update_available',
defaultMessage: 'Update available. <a href="">Reload</a>.',
description: 'Message shown when an app update is available.'
},
reconnect_countdown: {
id: 'reconnect_countdown',
defaultMessage: 'Disconnected. Reconnecting in {seconds}s…',
description: 'Message shown when an app update is available.'
},
reconnect_now: {
id: 'reconnect_now',
defaultMessage: 'Try now',
description: 'Prompt for reconnecting now'
}
});
@@ -48,6 +58,7 @@ class TinodeWeb extends React.Component {
this.handleError = this.handleError.bind(this);
this.handleLoginRequest = this.handleLoginRequest.bind(this);
this.handleConnected = this.handleConnected.bind(this);
this.handleAutoreconnectIteration = this.handleAutoreconnectIteration.bind(this);
this.doLogin = this.doLogin.bind(this);
this.handleCredentialsRequest = this.handleCredentialsRequest.bind(this);
this.handleLoginSuccessful = this.handleLoginSuccessful.bind(this);
@@ -112,6 +123,8 @@ class TinodeWeb extends React.Component {
errorText: '',
errorLevel: null,
errorAction: undefined,
errorActionText: null,
sidePanelSelected: 'login',
sidePanelTitle: null,
@@ -156,11 +169,15 @@ class TinodeWeb extends React.Component {
// Window/tab visible or invisible for pausing timers.
document.addEventListener('visibilitychange', this.handleVisibilityEvent);
this.setState({viewportWidth: document.documentElement.clientWidth});
this.setState({
viewportWidth: document.documentElement.clientWidth,
viewportHeight: document.documentElement.clientHeight
});
this.tinode = TinodeWeb.tnSetup(this.state.serverAddress, this.state.transport);
this.tinode.onConnect = this.handleConnected;
this.tinode.onDisconnect = this.handleDisconnect;
this.tinode.onAutoreconnectIteration = this.handleAutoreconnectIteration;
// Initialize desktop alerts.
if (this.state.desktopAlertsEnabled) {
@@ -230,7 +247,10 @@ class TinodeWeb extends React.Component {
handleResize() {
var mobile = document.documentElement.clientWidth <= MEDIA_BREAKPOINT;
this.setState({viewportWidth: document.documentElement.clientWidth});
this.setState({
viewportWidth: document.documentElement.clientWidth,
viewportHeight: document.documentElement.clientHeight
});
if (this.state.displayMobile != mobile) {
this.setState({displayMobile: mobile});
}
@@ -316,8 +336,8 @@ class TinodeWeb extends React.Component {
this.handleAppVisibility(!document.hidden, this.readTimerCallback);
}
handleError(err, level) {
this.setState({errorText: err, errorLevel: level});
handleError(err, level, action, actionText) {
this.setState({errorText: err, errorLevel: level, errorAction: action, errorActionText: actionText});
}
// User clicked Login button in the side panel.
@@ -345,6 +365,58 @@ class TinodeWeb extends React.Component {
this.doLogin(this.state.login, this.state.password, {meth: this.state.credMethod, resp: this.state.credCode});
}
// Called for each auto-reconnect iteration.
handleAutoreconnectIteration(sec, prom) {
clearInterval(this.reconnectCountdown);
if (sec < 0) {
// Clear error
this.handleError();
return;
}
if (prom) {
// Reconnecting now
prom.then(() => {
// Reconnected: clear error
this.handleError();
}).catch((err) => {
this.handleError(err.message, 'err');
});
return;
}
const {formatHTMLMessage} = this.props.intl;
let count = sec / 1000;
count = count | count;
this.reconnectCountdown = setInterval(() => {
this.handleError(
formatHTMLMessage(messages.reconnect_countdown, {seconds: count}),
'warn',
() => {
clearInterval(this.reconnectCountdown);
this.tinode.reconnect();
},
formatHTMLMessage(messages.reconnect_now)
);
count -= 1;
}, 1000);
}
// Connection lost
handleDisconnect(err) {
this.setState({
connected: false,
topicSelectedOnline: false,
dialogSelected: null,
errorText: err && err.message ? err.message : "Disconnected",
errorLevel: err && err.message ? 'err' : 'warn',
loginDisabled: false,
contextMenuVisible: false,
serverVersion: "no connection"
});
}
doLogin(login, password, cred) {
if (this.tinode.isAuthenticated()) {
// Already logged in. Go to default screen.
@@ -427,19 +499,6 @@ class TinodeWeb extends React.Component {
HashNavigation.navigateTo(HashNavigation.setUrlSidePanel(window.location.hash, 'contacts'));
}
handleDisconnect(err) {
this.setState({
connected: false,
topicSelectedOnline: false,
dialogSelected: null,
errorText: err && err.message ? err.message : "Disconnected",
errorLevel: err && err.message ? 'err' : 'warn',
loginDisabled: false,
contextMenuVisible: false,
serverVersion: "no connection"
});
}
tnMeMetaDesc(desc) {
if (desc && desc.public) {
this.setState({
@@ -546,7 +605,7 @@ class TinodeWeb extends React.Component {
// Sending "received" notifications
tnData(data) {
let topic = this.tinode.getTopic(data.topic);
const topic = this.tinode.getTopic(data.topic);
if (topic.msgStatus(data) >= Tinode.MESSAGE_STATUS_SENT) {
clearTimeout(this.receivedTimer);
this.receivedTimer = setTimeout(() => {
@@ -558,11 +617,11 @@ class TinodeWeb extends React.Component {
/* Fnd topic: find contacts by tokens */
tnInitFind() {
let fnd = this.tinode.getFndTopic();
const fnd = this.tinode.getFndTopic();
fnd.onSubsUpdated = this.tnFndSubsUpdated;
if (fnd.isSubscribed()) {
this.tnFndSubsUpdated();
} else {
fnd.onSubsUpdated = this.tnFndSubsUpdated;
fnd.subscribe(fnd.startMetaQuery().withSub().withTags().build()).catch((err) => {
this.handleError(err.message, 'err');
});
@@ -570,7 +629,7 @@ class TinodeWeb extends React.Component {
}
tnFndSubsUpdated() {
let foundContacts = [];
const foundContacts = [];
// Don't attempt to create P2P topics which already exist. Server will reject the duplicates.
this.tinode.getFndTopic().contacts((s) => {
foundContacts.push(s);
@@ -585,7 +644,7 @@ class TinodeWeb extends React.Component {
@param query {Array} is an array of contacts to search for
*/
handleSearchContacts(query) {
var fnd = this.tinode.getFndTopic();
const fnd = this.tinode.getFndTopic();
fnd.setMeta({desc: {public: query}}).then((ctrl) => {
return fnd.getMeta(fnd.startMetaQuery().withSub().build());
}).catch((err) => {
@@ -842,9 +901,9 @@ class TinodeWeb extends React.Component {
}
handleTopicUpdateRequest(topicName, pub, priv, permissions) {
var topic = this.tinode.getTopic(topicName);
const topic = this.tinode.getTopic(topicName);
if (topic) {
var params = {};
const params = {};
if (pub) {
params.public = pub;
}
@@ -862,9 +921,9 @@ class TinodeWeb extends React.Component {
}
handleChangePermissions(topicName, mode, uid) {
var topic = this.tinode.getTopic(topicName);
const topic = this.tinode.getTopic(topicName);
if (topic) {
var am = topic.getAccessMode();
const am = topic.getAccessMode();
if (uid) {
am.updateGiven(mode);
mode = am.getGiven();
@@ -1063,8 +1122,12 @@ class TinodeWeb extends React.Component {
login={this.state.login}
myUserId={this.state.myUserId}
loginDisabled={this.state.loginDisabled}
errorText={this.state.errorText}
errorLevel={this.state.errorLevel}
errorAction={this.state.errorAction}
errorActionText={this.state.errorActionText}
topicSelected={this.state.topicSelected}
chatList={this.state.chatList}
credMethod={this.state.credMethod}
@@ -1108,14 +1171,19 @@ class TinodeWeb extends React.Component {
acs={this.state.topicSelectedAcs}
displayMobile={this.state.displayMobile}
viewportWidth={this.state.viewportWidth}
viewportHeight={this.state.viewportHeight}
hideSelf={this.state.displayMobile &&
(this.state.mobilePanel !== 'topic-view' || this.state.showInfoPanel)}
topic={this.state.topicSelected}
myUserId={this.state.myUserId}
serverVersion={this.state.serverVersion}
serverAddress={this.state.serverAddress}
errorText={this.state.errorText}
errorLevel={this.state.errorLevel}
errorAction={this.state.errorAction}
errorActionText={this.state.errorActionText}
newGroupTopicParams={this.state.newGroupTopicParams}
onHideMessagesView={this.handleHideMessagesView}
@@ -1134,8 +1202,11 @@ class TinodeWeb extends React.Component {
topic={this.state.topicSelected}
searchableContacts={this.state.searchableContacts}
myUserId={this.state.myUserId}
errorText={this.state.errorText}
errorLevel={this.state.errorLevel}
errorAction={this.state.errorAction}
errorActionText={this.state.errorActionText}
onTopicDescUpdate={this.handleTopicUpdateRequest}
onCancel={this.handleHideInfoView}
+3 -1
View File
@@ -1,4 +1,5 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import FileProgress from './file-progress.jsx';
import { bytesToHumanSize } from '../lib/strformat.js';
@@ -73,7 +74,8 @@ export default class Attachment extends React.Component {
onCancel={this.handleCancel} />
:
<div><a href={url} download={this.props.filename} onClick={helperFunc} >
<i className="material-icons">file_download</i> save
<i className="material-icons">file_download</i> <FormattedMessage id="save_attachment"
defaultMessage="save" description="Call to save an attachment" />
</a></div>
}
</div>
+10
View File
@@ -14,6 +14,7 @@ export default class AvatarUpload extends React.Component {
};
this.handleFileUpload = this.handleFileUpload.bind(this);
this.handleClear = this.handleClear.bind(this);
}
static getDerivedStateFromProps(nextProps, prevState) {
@@ -41,12 +42,21 @@ export default class AvatarUpload extends React.Component {
e.target.value = '';
}
handleClear() {
this.props.onImageChanged(null);
}
render() {
// Randomize id value in case more than one AvatarUpload is shown
// at the same time.
var randId = "file-input-avatar-" + (Math.random() + '').substr(2);
return (
<div className="avatar-upload">
{this.props.readOnly || !this.state.dataUrl ?
null :
<a href="javascript:;" className="clear-avatar" onClick={this.handleClear}>
<i className="material-icons">clear</i>
</a>}
{this.state.dataUrl ?
<img src={this.state.dataUrl} className="preview" /> :
this.props.readOnly && this.props.uid ?
+27 -9
View File
@@ -50,9 +50,6 @@ export default class ChatMessage extends React.Component {
if (e.target.dataset.act == 'url') {
data.ref = '' + e.target.dataset.ref;
}
if (e.target.dataset.formname) {
data.name = '' + e.target.dataset.formname;
}
const text = e.target.dataset.title || 'unknown';
this.props.onFormResponse(e.target.dataset.act, text, data);
}
@@ -133,7 +130,15 @@ export default class ChatMessage extends React.Component {
</a>
</span>
</div>
{fullDisplay ? <div className="author">{this.props.userName}</div> : null}
{fullDisplay ?
<div className="author">
<FormattedMessage id="user_not_found" defaultMessage="Not found"
description="In place of a user's full name when the user is not found.">{
(notFound) => {return this.props.userName || <i>{notFound}</i>}
}</FormattedMessage>
</div>
: null
}
</div>
</li>
);
@@ -141,7 +146,7 @@ export default class ChatMessage extends React.Component {
};
// Convert Drafty object to a tree of React elements.
import { REM_SIZE } from '../config.js';
import { BROKEN_IMAGE_SIZE, REM_SIZE } from '../config.js';
import { fitImageSize } from '../lib/blob-helpers.js';
function draftyFormatter(style, data, values, key) {
@@ -154,24 +159,37 @@ function draftyFormatter(style, data, values, key) {
// Additional processing for images
if (data) {
attr.className = 'inline-image';
attr.onClick = this.handlePreviewImage;
let dim = fitImageSize(data.width, data.height,
Math.min(this.props.viewportWidth - REM_SIZE * 4, REM_SIZE * 36), REM_SIZE * 24, false);
attr.style = dim ? { width: dim.dstWidth + 'px', height: dim.dstHeight + 'px' } : null;
dim = dim || {dstWidth: BROKEN_IMAGE_SIZE, dstHeight: BROKEN_IMAGE_SIZE};
attr.style = { width: dim.dstWidth + 'px', height: dim.dstHeight + 'px' };
if (attr.src) {
attr.onClick = this.handlePreviewImage;
attr.className += ' image-clickable';
} else {
attr.src = 'img/broken_image.png';
}
}
break;
case 'BN':
// Button
attr.onClick = this.handleFormButtonClick;
let inner = React.Children.map(values, (child) => {
return typeof child == 'string' ? child : undefined;
});
if (!inner || inner.length == 0) {
inner = [attr.name]
}
// Get text which will be sent back when the button is clicked.
attr['data-title'] = inner.join('');
break;
case 'FM':
// Form
attr.className = 'bot-form' + ((data && data.layout) ? ' ' + data.layout : '');
attr.className = 'bot-form';
// console.log(data, attr);
break;
case 'FE':
// Form element formatting is dependent on element content.
attr.className = data && data.dftp ? data.dftp : '';
break;
}
return React.createElement(el, attr, values);
+5 -5
View File
@@ -27,11 +27,11 @@ class ContactList extends React.Component {
const contactNodes = [];
if (this.props.contacts && this.props.contacts.length > 0) {
this.props.contacts.map((c) => {
var key = c.topic ? c.topic : c.user;
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) {
var content = [key];
let content = [key];
if (c.private && c.private.comment) {
content.push(('' + c.private.comment).toLowerCase());
}
@@ -43,10 +43,10 @@ class ContactList extends React.Component {
}
}
var selected = showCheckmark ?
let selected = showCheckmark ?
(this.props.topicSelected.indexOf(key) > -1) :
(this.props.topicSelected === key);
var badges = [];
let badges = [];
if (this.props.showMode) {
if (key === this.props.myUserId) {
badges.push({name: formatMessage(messages.badge_you), color: 'green'});
@@ -55,7 +55,7 @@ class ContactList extends React.Component {
badges.push({name: formatMessage(messages.badge_owner), color: 'blue'});
}
}
var comment = Array.isArray(c.private) ?
let comment = Array.isArray(c.private) ?
c.private.join(',') : (c.private ? c.private.comment : null);
contactNodes.push(
+3 -3
View File
@@ -36,9 +36,9 @@ export default class Contact extends React.Component {
// FIXME: this is probably wrong for RTL languages.
title = title.substring(0, 28) + '...';
}
let online = this.props.now ? 'online' : 'offline';
let avatar = this.props.avatar ? this.props.avatar : true;
let badges = this.props.badges.slice();
const online = this.props.now ? 'online' : 'offline';
const avatar = this.props.avatar ? this.props.avatar : true;
const badges = this.props.badges ? this.props.badges.slice() : [];
if (this.props.showMode && this.props.acs) {
badges.push({name: this.props.acs.getMode(), key: 'mode'});
}
+7 -3
View File
@@ -170,15 +170,19 @@ class ContextMenu extends React.Component {
console.log("Topic not found: ", params.topicName);
return;
}
// We don't know if the message is still pending (e.g. attachment is being uploaded),
// so try cancelling first. No harm if we can't cancel.
if (topic.cancelSend(params.seq)) {
return Promise.resolve();
// The message can be cancelled if transmission to the server has not
// started yet or if the message send has failed.
if (!all && topic.cancelSend(params.seq)) {
return;
}
// Can't cancel. Delete instead.
var promise = all ?
let promise = all ?
topic.delMessagesAll(hard) :
topic.delMessagesList([params.seq], hard);
promise.catch((err) => {
if (errorHandler) {
errorHandler(err.message, 'err');
+8 -1
View File
@@ -33,7 +33,14 @@ export default class ErrorPanel extends React.PureComponent {
return (
<div className={className}>
<div className="icon"><i className="material-icons">{level}</i></div>
<span dangerouslySetInnerHTML={{__html: this.props.text}} />
<span>
<span dangerouslySetInnerHTML={{__html: this.props.text}} />
{this.props.action ?
<a href="javascript:;" onClick={this.props.action}>
{this.props.actionText}
</a>
: null}
</span>
<div className="cancel"><MenuCancel onCancel={this.hide} /></div>
</div>
);
+2 -12
View File
@@ -8,21 +8,11 @@ import { makeImageUrl } from '../lib/blob-helpers.js';
export default class GroupSubs extends React.Component {
constructor(props) {
super(props);
this.state = {
onlineSubs: props.subscribers || []
};
}
static getDerivedStateFromProps(nextProps, prevState) {
return {
onlineSubs: nextProps.subscribers ? nextProps.subscribers : []
};
}
render() {
var usersOnline = [];
this.state.onlineSubs.map((sub) => {
const usersOnline = [];
(this.props.subscribers || []).map((sub) => {
usersOnline.push(
<div className="avatar-box" key={sub.user}>
<LetterTile
+9 -7
View File
@@ -23,19 +23,21 @@ export default class ImagePreview extends React.PureComponent {
if (!this.props.content) {
return null;
}
var instance = this;
var dim = fitImageSize(this.props.content.width, this.props.content.height,
const instance = this;
const dim = fitImageSize(this.props.content.width, this.props.content.height,
this.state.width, this.state.height, false);
var size = dim ? { width: dim.dstWidth + 'px', height: dim.dstHeight + 'px' } :
const size = dim ? { width: dim.dstWidth + 'px', height: dim.dstHeight + 'px' } :
((this.props.content.width > this.props.content.height) ? {width: '100%'} : {height: '100%'});
size.maxWidth = '100%';
size.maxHeight = '100%';
var filename = this.props.content.filename;
var maxlength = (this.props.content.width / REM_SIZE) | 0;
let filename = this.props.content.filename;
const maxlength = (this.props.content.width / REM_SIZE) | 0;
if (filename.length > maxlength) {
filename = filename.slice(0, maxlength-2) + '...' + filename.slice(2-maxlength);
}
const width = this.props.content.width || '-';
const height = this.props.content.height || '-';
return (
<div id="image-preview" onClick={this.props.onClose}>
<div id="image-preview-caption-panel">
@@ -52,7 +54,7 @@ export default class ImagePreview extends React.PureComponent {
<div>
<div><b><FormattedMessage id="label_file_name" defaultMessage="File name:"
description="Label for a file name" /></b></div>
<div><span title={this.props.content.filename}>{filename}</span></div>
<div><span title={this.props.content.filename}>{filename ? filename : '-'}</span></div>
</div>
<div>
<div><b><FormattedMessage id="label_content_type" defaultMessage="Content type:"
@@ -62,7 +64,7 @@ export default class ImagePreview extends React.PureComponent {
<div>
<div><b><FormattedMessage id="label_size" defaultMessage="Size:"
description="Label for file size" /></b></div>
<div>{this.props.content.width} &times; {this.props.content.height} px; {bytesToHumanSize(this.props.content.size)}</div>
<div>{width} &times; {height} px; {bytesToHumanSize(this.props.content.size)}</div>
</div>
</div>
</div>
+7 -2
View File
@@ -43,7 +43,7 @@ export default class InPlaceEdit extends React.Component {
this.setState({value: this.props.value, active: false});
} else if (e.keyCode === 13) {
// Enter pressed
this.handleEditingFinished();
this.handleEditingFinished(e);
}
}
@@ -54,7 +54,12 @@ export default class InPlaceEdit extends React.Component {
}
}
handleEditingFinished() {
handleEditingFinished(event) {
if (this.props.required && !event.target.checkValidity()) {
// Empty input
this.setState({value: this.props.value, active: false});
return;
}
this.setState({active: false});
let value = this.state.value.trim();
if ((value || this.props.value) && (value !== this.props.value)) {
+3 -1
View File
@@ -18,7 +18,9 @@ export default class LetterTile extends React.PureComponent {
<i className="material-icons">person</i>;
}
} else if (this.props.avatar) {
avatar = <img className="avatar" alt="avatar" src={this.props.avatar} />;
// If avatar image is invalid, show a placeholder.
avatar = <img className="avatar" alt="avatar" src={this.props.avatar}
onError={(e)=>{e.target.onerror = null; e.target.src="/img/broken_image.png"}} />;
} else {
avatar = null;
}
+1 -1
View File
@@ -28,7 +28,7 @@ class ReceivedMarker extends React.PureComponent {
} else if (this.props.received == Tinode.MESSAGE_STATUS_FAILED) {
timestamp = formatMessage(messages.failed);
} else {
timestamp = shortDateFormat(this.props.timestamp);
timestamp = shortDateFormat(this.props.timestamp, this.props.intl.locale);
}
let marker = null;
+20 -17
View File
@@ -49,11 +49,15 @@ class SendMessage extends React.PureComponent {
}
componentDidMount() {
this.pasteFile.addEventListener('paste', this.handlePasteEvent, false);
this.messageEditArea.addEventListener('paste', this.handlePasteEvent, false);
}
componentWillUnmount() {
this.pasteFile.removeEventListener('paste', this.handlePasteEvent, false)
this.messageEditArea.removeEventListener('paste', this.handlePasteEvent, false);
}
componentDidUpdate() {
this.messageEditArea.focus();
}
handlePasteEvent(e) {
@@ -78,7 +82,7 @@ class SendMessage extends React.PureComponent {
handleAttachImage(e) {
if (e.target.files && e.target.files.length > 0) {
let file = e.target.files[0];
const file = e.target.files[0];
// Check if the uploaded file is indeed an image and if it isn't too large.
if (file.size > MAX_INBAND_ATTACHMENT_SIZE || SUPPORTED_IMAGE_FORMATS.indexOf(file.type) < 0) {
// Convert image for size or format.
@@ -114,21 +118,21 @@ class SendMessage extends React.PureComponent {
handleAttachFile(e) {
const {formatMessage} = this.props.intl;
if (e.target.files && e.target.files.length > 0) {
var file = e.target.files[0];
const file = e.target.files[0];
if (file.size > MAX_EXTERN_ATTACHMENT_SIZE) {
// Too large.
this.props.onError(formatMessage(messages.file_attachment_too_large,
{size: bytesToHumanSize(file.size), limit: bytesToHumanSize(MAX_EXTERN_ATTACHMENT_SIZE)}), 'err');
} else if (file.size > MAX_INBAND_ATTACHMENT_SIZE) {
// Too large to send inband - uploading out of band and sending as a link.
let uploader = this.props.tinode.getLargeFileHelper();
const uploader = this.props.tinode.getLargeFileHelper();
if (!uploader) {
this.props.onError(formatMessage(messages.cannot_initiate_upload));
return;
}
// Format data and initiate upload.
let uploadCompletionPromise = uploader.upload(file);
let msg = Drafty.attachFile(null, file.type, null, file.name, file.size, uploadCompletionPromise);
const uploadCompletionPromise = uploader.upload(file);
const msg = Drafty.attachFile(null, file.type, null, file.name, file.size, uploadCompletionPromise);
// Pass data and the uploader to the TinodeWeb.
this.props.sendMessage(msg, uploadCompletionPromise, uploader);
} else {
@@ -146,7 +150,7 @@ class SendMessage extends React.PureComponent {
}
handleSend() {
var message = this.state.message.trim();
const message = this.state.message.trim();
if (message) {
this.props.sendMessage(this.state.message.trim());
this.setState({message: ''});
@@ -168,10 +172,10 @@ class SendMessage extends React.PureComponent {
}
handleMessageTyping(e) {
var newState = {message: e.target.value};
var now = new Date().getTime();
const newState = {message: e.target.value};
const now = new Date().getTime();
if (now - this.state.keypressTimestamp > KEYPRESS_DELAY) {
var topic = this.props.tinode.getTopic(this.props.topic);
const topic = this.props.tinode.getTopic(this.props.topic);
if (topic.isSubscribed()) {
topic.noteKeyPress();
}
@@ -185,32 +189,31 @@ class SendMessage extends React.PureComponent {
const prompt = this.props.disabled ?
formatMessage(messages.messaging_disabled) :
formatMessage(messages.type_new_message);
let instance = this;
return (
<div id="send-message-panel">
{this.props.disabled ?
<i className="material-icons disabled">photo</i> :
<a href="javascript:;" onClick={function(e) {instance.attachImage.click();}} title="Add image">
<a href="javascript:;" onClick={(e) => {this.attachImage.click();}} title="Add image">
<i className="material-icons secondary">photo</i>
</a>}
{this.props.disabled ?
<i className="material-icons disabled">attach_file</i> :
<a href="javascript:;" onClick={function(e) {instance.attachFile.click();}} title="Attach file">
<a href="javascript:;" onClick={(e) => {this.attachFile.click();}} title="Attach file">
<i className="material-icons secondary">attach_file</i>
</a>}
<textarea id="sendMessage" placeholder={prompt}
disabled={this.props.disabled} value={this.state.message}
onChange={this.handleMessageTyping} onKeyPress={this.handleKeyPress}
ref={function(ref) {instance.pasteFile = ref;}}
ref={(ref) => {this.messageEditArea = ref;}}
autoFocus />
{this.props.disabled ?
<i className="material-icons disabled">send</i> :
<a href="javascript:;" onClick={this.handleSend} title="Send">
<i className="material-icons">send</i>
</a>}
<input type="file" ref={function(ref) {instance.attachFile = ref;}}
<input type="file" ref={(ref) => {this.attachFile = ref;}}
onChange={this.handleAttachFile} style={{display: 'none'}} />
<input type="file" ref={function(ref) {instance.attachImage = ref;}} accept="image/*"
<input type="file" ref={(ref) => {this.attachImage = ref;}} accept="image/*"
onChange={this.handleAttachImage} style={{display: 'none'}} />
</div>
);
+293 -171
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
+1 -1
View File
@@ -1,6 +1,6 @@
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
module.exports = config = {
module.exports = {
entry: [
'./src/index.js',
],