Compare commits
40
Commits
7d058df54d
...
9bbddc08e6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9bbddc08e6 | ||
|
|
b0be1fd012 | ||
|
|
21303346b1 | ||
|
|
bd7531e5b7 | ||
|
|
a0184ac002 | ||
|
|
4d438ccab4 | ||
|
|
48e6144568 | ||
|
|
e028503c9e | ||
|
|
fcb3d78cc4 | ||
|
|
b976a3caa8 | ||
|
|
cc6a4cd125 | ||
|
|
6d7f98c55b | ||
|
|
d80ddcabea | ||
|
|
4c44b61a9d | ||
|
|
9eb7bebe80 | ||
|
|
1f634f5877 | ||
|
|
f78d673e02 | ||
|
|
a5d405b083 | ||
|
|
405ce756a2 | ||
|
|
31190ec826 | ||
|
|
9b3c714390 | ||
|
|
2e76b122a2 | ||
|
|
14d75223e2 | ||
|
|
bc3e82bcbf | ||
|
|
10d88da558 | ||
|
|
4dde45b6be | ||
|
|
00eeb2503a | ||
|
|
4f70de3aa9 | ||
|
|
486cf2e355 | ||
|
|
21841c0022 | ||
|
|
d1f7f02fa0 | ||
|
|
fbbab3e7d6 | ||
|
|
7248b5c398 | ||
|
|
96de18ea7b | ||
|
|
198bd9a64c | ||
|
|
9324faa32f | ||
|
|
769c25170f | ||
|
|
9e073356b2 | ||
|
|
14ff124674 | ||
|
|
f959aa24cc |
+38
-34
@@ -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 {
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Executable
BIN
Binary file not shown.
|
After Width: | Height: | Size: 2.5 KiB |
Generated
+16
-21
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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": "сохранить"
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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";
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
@@ -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">
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -32,7 +32,6 @@ export default class SettingsView extends React.PureComponent {
|
||||
|
||||
handleServerAddressChange(name) {
|
||||
this.setState({serverAddress: name});
|
||||
this.props.onServerAddressChange(name);
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
@@ -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
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 ?
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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'});
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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} × {this.props.content.height} px; {bytesToHumanSize(this.props.content.size)}</div>
|
||||
<div>{width} × {height} px; {bytesToHumanSize(this.props.content.size)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,6 +1,6 @@
|
||||
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
|
||||
|
||||
module.exports = config = {
|
||||
module.exports = {
|
||||
entry: [
|
||||
'./src/index.js',
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user