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 |
+36
-32
@@ -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;
|
||||||
@@ -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 {
|
||||||
|
|||||||
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",
|
"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
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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": "сохранить"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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";
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
+17
-12
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 ?
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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'});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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>
|
||||||
<span dangerouslySetInnerHTML={{__html: this.props.text}} />
|
<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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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} × {this.props.content.height} px; {bytesToHumanSize(this.props.content.size)}</div>
|
<div>{width} × {height} px; {bytesToHumanSize(this.props.content.size)}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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)) {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
+292
-170
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');
|
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
|
||||||
|
|
||||||
module.exports = config = {
|
module.exports = {
|
||||||
entry: [
|
entry: [
|
||||||
'./src/index.js',
|
'./src/index.js',
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user