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