Compare commits

..
44 Commits
Author SHA1 Message Date
or-else 0598a42164 adding support for delayed removal of deleted topics 2022-02-19 20:58:35 -08:00
or-else 9a811708ba merge with master 2022-02-18 15:32:17 -08:00
or-else bb63cb204b rebuilt with fix 2022-02-18 10:12:01 -08:00
or-else fb45c442a8 crash in message preview on empty topic 2022-02-17 16:07:59 -08:00
or-else e5545f5f14 bump version to 0.18.0 2022-02-15 17:20:37 -08:00
or-else 24d75dfbbc minor visual tweaks 2022-02-15 17:18:28 -08:00
or-else 84a78ce6b5 added date badges (dividers) in messages view 2022-02-10 14:19:51 -08:00
or-else c8756c3a01 webpack updated to v10 2022-02-10 09:55:37 -08:00
or-else e3cb8a7291 merge conflicts resolved 2022-02-10 09:40:37 -08:00
or-else 3c11cbbdf1 adding vscode config to gitignore 2022-02-10 09:33:44 -08:00
or-else 9657a1e61e bump version to 18.0-rc3 2022-02-07 16:52:15 -08:00
or-else 728b3a3a1b crash on missing topic parameters, ranamed var for clarity 2022-01-28 10:17:14 -08:00
or-else 41bf6f3941 rebuilt with updated dependencies 2022-01-27 15:27:05 -08:00
or-else a02be831f2 minor fixes around remote avatars 2022-01-27 15:17:42 -08:00
or-else 5cfcd5789f removed unused imports, stripped src from IM before sending 2022-01-15 20:35:43 -08:00
or-else 28c34bcf7d rebuilt with the sdk fix 2022-01-12 19:15:38 -08:00
or-else e87a36421c fix crash on image with no data 2022-01-12 17:27:39 -08:00
or-else 479752036d rebuilt with the sdk fix 2021-12-29 17:38:43 -08:00
or-else c2d95725e4 fix missing context 2021-12-21 11:04:18 -08:00
or-else 9acca449e3 everything but images seems to be correct 2021-12-20 13:50:14 -08:00
or-else bd2def7882 formatting of forwarded messages previews 2021-12-20 12:27:02 -08:00
or-else b201f019ef reply formatting works correctly 2021-12-19 17:42:19 -08:00
or-else 1d59d29519 use new method for replies 2021-12-19 11:51:24 -08:00
or-else 9582a9f2cd simplify a bit and remove debug logging 2021-12-19 10:59:08 -08:00
or-else 363104755e completely remove quotes in previews 2021-12-19 10:51:16 -08:00
or-else ab7160170d refactoring split formatters - transformers 2021-12-19 10:05:16 -08:00
or-else afcb8d8616 some formatting fixes 2021-12-11 21:49:03 -08:00
or-else 69cf5817f0 rebuilt with the SDK fix 2021-12-05 20:18:25 -08:00
or-else 572fc66a69 sdk fix for attachment rendering in previews, bumped version 2021-12-02 12:00:09 -08:00
or-else 6a877620e6 fix for #90 2021-11-30 18:13:48 -08:00
or-else c7a60b9608 visual tweaks of forwarded replies 2021-11-25 14:57:08 -08:00
or-else 3feca32d13 strip mentioned name from the forwarded preview 2021-11-24 18:55:44 -08:00
or-else 081eb8723d bump version to 0.18.0-rc1 and rebuild 2021-11-21 13:29:08 -08:00
or-else a1034654a9 added [go to latest message] button 2021-11-21 11:06:21 -08:00
or-else faa3f60b55 adding [go to the latest message] button 2021-11-15 13:10:31 -08:00
or-else 31345eb298 rebuilt 2021-11-14 16:24:05 -08:00
or-else a9332495cf bump version to -beta2 and rebuild 2021-11-14 16:23:34 -08:00
or-else e4c5da3178 roll back to firebase 8, otherwise crash 2021-11-14 16:22:13 -08:00
or-else f3c6581fa5 rebuilt 2021-11-10 14:46:44 -08:00
or-else d6cf419266 emphesize that this is not a standaline app 2021-11-10 14:15:49 -08:00
or-else 939804eae0 move push setup to a separate file 2021-11-10 14:11:53 -08:00
or-else 2fc8826403 further shorten forwarded preview 2021-11-09 13:45:50 -08:00
or-else 298bca4042 replace screenshot with a more informative one 2021-11-09 10:58:30 -08:00
or-else f5d3b3af0b rebuilt with the sdk 2021-11-09 10:31:34 -08:00
50 changed files with 8418 additions and 10208 deletions
+2 -1
View File
@@ -1,3 +1,4 @@
.DS_Store .DS_Store
node_modules node_modules
src/i18n/src/ src/i18n/srci/
webapp.code-workspace.code-workspace
+4 -14
View File
@@ -16,13 +16,14 @@ For demo access and other instructions [see here](https://github.com/tinode/chat
## Installing and running ## Installing and running
This is NOT a standalone app, this is just a frontend, a client. It requires a [backend](https://github.com/tinode/chat/). See [installation instructions](https://github.com/tinode/chat/blob/master/INSTALL.md). This is _NOT_ a standalone app, this is just a frontend, a client. It requires a [backend](https://github.com/tinode/chat/). See [installation instructions](https://github.com/tinode/chat/blob/master/INSTALL.md). See instructions for [enabling push notifications](push.md).
## Getting support ## Getting support
* Read [client-side](http://tinode.github.io/js-api/) and [server-side](https://github.com/tinode/chat/blob/master/docs/API.md) API documentation. * Read [client-side](http://tinode.github.io/js-api/) and [server-side](https://github.com/tinode/chat/blob/master/docs/API.md) API documentation.
* For support, general questions, discussions post to [https://groups.google.com/d/forum/tinode](https://groups.google.com/d/forum/tinode). * For support, general questions, discussions post to [https://groups.google.com/d/forum/tinode](https://groups.google.com/d/forum/tinode).
* For bugs and feature requests [open an issue](https://github.com/tinode/webchat/issues/new). * For bugs and feature requests [open an issue](https://github.com/tinode/webchat/issues/new).
* Use https://tinode.co/contact for commercial inquiries.
## Internationalization ## Internationalization
@@ -43,6 +44,7 @@ More translations are welcome. See [instructions](https://github.com/tinode/chat
## Not done yet ## Not done yet
* Voice or video messages, video or audio calling.
* End-to-End encryption. * End-to-End encryption.
* Emoji support is weak. * Emoji support is weak.
* Mentions, hashtags. * Mentions, hashtags.
@@ -55,18 +57,6 @@ More translations are welcome. See [instructions](https://github.com/tinode/chat
* [Fonts](https://fonts.google.com/) and [icons](https://google.github.io/material-design-icons/#icon-font-for-the-web) from Google under [license](https://developers.google.com/terms) which permits commercial and non-commercial use. * [Fonts](https://fonts.google.com/) and [icons](https://google.github.io/material-design-icons/#icon-font-for-the-web) from Google under [license](https://developers.google.com/terms) which permits commercial and non-commercial use.
* Push notifications built with https://firebase.google.com/. * Push notifications built with https://firebase.google.com/.
## Push notifications
If you want to use the app with your own server and want web push notification to work you have to set them up:
* Register at https://firebase.google.com/, set up the project if you have not done so already.
* Follow instructions to create a web application https://support.google.com/firebase/answer/9326094 in your project.
* Follow instructions at https://support.google.com/firebase/answer/7015592 to get a Firebase configuration object ("Firebase SDK snippet").
* Locate `firebase-init.js` in the root folder of your copy of TinodeWeb app. Copy-paste the following keys from the configuration object to `firebase-init.js`: `apiKey`, `messagingSenderId`, `projectId`, `appId` (you may copy all keys).
* Copy `Web Push certificate` (Project Settings -> Cloud Messaging -> Web configuration -> Web Push certificates) to `messagingVapidKey` field in `firebase-init.js`.
* Double check that `firebase-init.js` contains the following keys: `apiKey`, `messagingSenderId`, `projectId`, `appId`, `messagingVapidKey`. The file may contain other optional keys.
* Copy Google-provided server key to `tinode.conf`, see details [here](https://github.com/tinode/chat/blob/master/docs/faq.md#q-how-to-setup-fcm-push-notifications).
## Responsive design ## Responsive design
### Desktop screenshot ### Desktop screenshot
@@ -78,5 +68,5 @@ If you want to use the app with your own server and want web push notification t
### Mobile screenshots ### Mobile screenshots
<p align="center"> <p align="center">
<kbd><img src="web-mob-contacts.png" alt="Mobile web: contacts" width=250 /></kbd> <kbd><img src="web-mob-chat.png" alt="Mobile web: chat" width=250 /></kbd> <kbd><img src="web-mob-info.png" alt="Mobile web: topic info" width=250 /></kbd> <kbd><img src="web-mob-new-chat.png" alt="Mobile web: start new chat" width=250 /></kbd> <kbd><img src="web-mob-acc-settings.png" alt="Mobile web: account settings" width=250 /></kbd> <kbd><img src="web-mob-avatar.png" alt="Mobile web: change avatar" width=250 /></kbd> <kbd><img src="web-mob-contacts.png" alt="Mobile web: contacts" width=250 /></kbd> <kbd><img src="web-mob-chat.png" alt="Mobile web: chat" width=250 /></kbd> <kbd><img src="web-mob-info.png" alt="Mobile web: topic info" width=250 /></kbd> <kbd><img src="web-mob-forward.png" alt="Mobile web: forward message" width=250 /></kbd> <kbd><img src="web-mob-acc-settings.png" alt="Mobile web: account settings" width=250 /></kbd> <kbd><img src="web-mob-avatar.png" alt="Mobile web: change avatar" width=250 /></kbd>
</p> </p>
+129 -43
View File
@@ -46,36 +46,6 @@ a.gray:focus {
color: #999; color: #999;
} }
.flat-button {
display: flex;
align-items: center;
margin: 0.15rem 0.35rem;
height: 2em;
line-height: 2em;
font-weight: normal;
text-align: center;
vertical-align: middle;
touch-action: manipulation;
white-space: nowrap;
text-transform: uppercase;
}
.flat-button:hover,
.flat-button:focus {
text-decoration: none;
}
.flat-button.faux {
display: inline-block;
height: 1.4em;
line-height: 1.4em;
text-transform: none;
padding: 0 .35rem;
margin: 0.15rem;
border: 1px solid #999;
border-radius: 3px;
background-color: #eee;
}
form { form {
font-size: 1.1rem; font-size: 1.1rem;
padding: 0.5rem 0.75rem; padding: 0.5rem 0.75rem;
@@ -147,6 +117,7 @@ button.round.small {
line-height: 2rem; line-height: 2rem;
box-shadow: 0 2px 0.35rem 0 rgba(0, 0, 0, 0.25); box-shadow: 0 2px 0.35rem 0 rgba(0, 0, 0, 0.25);
} }
button:focus, button:hover, button:active { button:focus, button:hover, button:active {
color: #fff; color: #fff;
} }
@@ -252,6 +223,7 @@ textarea {
textarea.inplace-edit { textarea.inplace-edit {
height: unset; height: unset;
font-size: unset;
} }
/* END of input and buttons */ /* END of input and buttons */
@@ -265,6 +237,7 @@ textarea.inplace-edit {
::-webkit-scrollbar-track { ::-webkit-scrollbar-track {
-webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.2); -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.2);
box-shadow: inset 0 0 6px rgba(0,0,0,0.2);
background-color: rgba(255,255,255,0.1); background-color: rgba(255,255,255,0.1);
} }
@@ -432,6 +405,7 @@ div.trusted-badge {
font-size: 120%; font-size: 120%;
} }
/* Other universal style components */
.small { .small {
font-size: 90%; font-size: 90%;
} }
@@ -461,6 +435,76 @@ div.trusted-badge {
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.flat-button {
display: flex;
align-items: center;
margin: 0.15rem 0.35rem;
height: 2em;
line-height: 2em;
font-weight: normal;
text-align: center;
vertical-align: middle;
touch-action: manipulation;
white-space: nowrap;
text-transform: uppercase;
}
.flat-button:hover,
.flat-button:focus {
text-decoration: none;
}
.flat-button.faux {
display: inline-block;
height: 1.4em;
line-height: 1.4em;
text-transform: none;
padding: 0 .35rem;
margin: 0.15rem;
border: 1px solid #999;
border-radius: 3px;
background-color: #eee;
}
.action-button {
display: block;
position: absolute;
color: #999;
font-size: 1.8rem;
background-color: #fafafa;
right: 1rem;
bottom: 1rem;
border-radius: 50%;
width: 3.25rem;
height: 3.25rem;
min-width: unset;
min-height: unset;
padding: 0;
margin: 0;
outline: none; /* otherwise a square outline is shown */
box-shadow: 0 2px 0.5rem 0 rgba(0, 0, 0, 0.25);
transition: all 150ms;
z-index: 2;
}
.action-button:focus, .action-button:hover {
color: #888;
background-color: #f0f0f0;
}
.action-button:active {
color: #777;
background-color: #eee;
}
.action-button.hidden {
width: 1px;
height: 1px;
font-size: 1px;
line-height: 1px;
right: 2.625rem;
bottom: 2.625rem;
opacity: 0;
}
/* Element which can take focus but shows no outline */ /* Element which can take focus but shows no outline */
.group-focus { .group-focus {
outline: none; outline: none;
@@ -963,6 +1007,10 @@ i.search {
height: 2rem; height: 2rem;
} }
.avatar.disabled {
opacity: 0.5;
}
.contact-box .avatar-box > .material-icons { .contact-box .avatar-box > .material-icons {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -1043,7 +1091,6 @@ img.channel {
width: 1.5rem; width: 1.5rem;
height: 1.5rem; height: 1.5rem;
font-size: 1.5rem; font-size: 1.5rem;
line-height: 1.5rem;
color: #0097A7; color: #0097A7;
position: absolute; position: absolute;
right: 0.05rem; right: 0.05rem;
@@ -1052,6 +1099,18 @@ img.channel {
overflow: visible; overflow: visible;
border: 0.0px solid transparent; border: 0.0px solid transparent;
} }
.contact-box .deleted.material-icons {
width: 1.5rem;
height: 1.5rem;
font-size: 1.5rem;
color: gray;
position: absolute;
right: 0.05rem;
bottom: 0.05rem;
background-color: white;
}
/* END of avatar box */ /* END of avatar box */
#add-topic { #add-topic {
@@ -1434,7 +1493,6 @@ img.channel {
text-decoration: none; text-decoration: none;
display: block; display: block;
text-align: center; text-align: center;
vertical-align: middle;
} }
#image-preview { #image-preview {
@@ -1515,12 +1573,6 @@ img.channel {
justify-content: flex-end; justify-content: flex-end;
} }
.chat-box .center {
justify-content: center;
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}
.bubble { .bubble {
position: relative; position: relative;
display: flex; display: flex;
@@ -1530,6 +1582,7 @@ img.channel {
padding: 0.75rem 0.5rem 0.75rem 0.5rem; padding: 0.75rem 0.5rem 0.75rem 0.5rem;
margin: 0; margin: 0;
text-align: left; text-align: left;
color: #212121;
border-radius: 0.5rem; border-radius: 0.5rem;
box-shadow: 0 1px 0.5px rgba(0, 0, 0, 0.12); box-shadow: 0 1px 0.5px rgba(0, 0, 0, 0.12);
transition: ease-out background-color 0.5s; transition: ease-out background-color 0.5s;
@@ -1582,11 +1635,28 @@ img.channel {
border-bottom-right-radius: 0; border-bottom-right-radius: 0;
} }
.center .bubble { .chat-box .meta {
justify-content: center;
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}
.chat-box .meta .bubble {
color: #666;
min-width: unset;
}
.meta .bubble.deleted {
background-color: #e3f2fd; background-color: #e3f2fd;
padding: 0.75rem 0.75rem 0.5rem 0.75rem; padding: 0.75rem 0.75rem 0.5rem 0.75rem;
} }
.meta .bubble.date {
font-size: smaller;
padding: 0.5rem 0.5rem 0.5rem 0.5rem;
background-color: rgb(244 244 244 / 66%);
}
/* Left triangle placed bottom left flush. */ /* Left triangle placed bottom left flush. */
.left .bubble.tip::before { .left .bubble.tip::before {
content: ''; content: '';
@@ -1649,7 +1719,6 @@ img.channel {
.bubble div.message-content { .bubble div.message-content {
position: relative; position: relative;
color: #212121;
margin: 0; margin: 0;
width: 100%; width: 100%;
max-width: 36rem; max-width: 36rem;
@@ -1959,6 +2028,10 @@ ul.tabbar li.active > a {
color: white; color: white;
} }
.lettertile.disabled {
opacity: 0.5;
}
/* Colors of letter tiles */ /* Colors of letter tiles */
.lt-bg-0 { .lt-bg-0 {
background-color: #ef9a9a; background-color: #ef9a9a;
@@ -2254,7 +2327,7 @@ ul.tabbar li.active > a {
.chip > a { .chip > a {
display: inline-block; display: inline-block;
color: #ccc; color: #eee;
background-color: #aaa; background-color: #aaa;
border-radius: 50%; border-radius: 50%;
margin: 0.3rem; margin: 0.3rem;
@@ -2268,7 +2341,7 @@ ul.tabbar li.active > a {
.chip > a:focus, .chip > a:focus,
.chip > a:hover { .chip > a:hover {
text-decoration: none; text-decoration: none;
color: #ddd; color: #fff;
background-color: #bbb; background-color: #bbb;
} }
@@ -2471,7 +2544,11 @@ ul.tabbar li.active > a {
} }
.chat-box .bubble .reply-quote { .chat-box .bubble .reply-quote {
margin: -0.25rem -0.75rem 0.35rem -0.75rem; margin: 0.15rem -0.75rem 0.3rem -0.75rem;
}
.chat-box .bubble .reply-quote:first-child {
margin-top: -0.25rem;
} }
.chat-box .left .reply-quote { .chat-box .left .reply-quote {
@@ -2487,6 +2564,10 @@ ul.tabbar li.active > a {
margin-bottom: 0.1rem; margin-bottom: 0.1rem;
} }
.reply-quote img.inline-image::before {
content: '';
}
#reply-quote-preview { #reply-quote-preview {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -2519,6 +2600,11 @@ ul.tabbar li.active > a {
margin-bottom: 0.35rem; margin-bottom: 0.35rem;
} }
.forwarding-quote {
font-size: 200%;
line-height: 200%;
}
.mention { .mention {
font-weight: 500; font-weight: 500;
} }
+1 -1
View File
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -34,8 +34,8 @@
<!-- React-Intl for translations --> <!-- React-Intl for translations -->
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-intl@5/react-intl.iife.js"></script> <script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-intl@5/react-intl.iife.js"></script>
<!-- Firebase for push notifications --> <!-- Firebase for push notifications -->
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@9/firebase-app.js"></script> <script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@8/firebase-app.js"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@9/firebase-messaging.js"></script> <script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@8/firebase-messaging.js"></script>
</head> </head>
<body> <body>
+2 -2
View File
@@ -25,8 +25,8 @@
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react@17.0.2/umd/react.production.min.js"></script> <script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react@17.0.2/umd/react.production.min.js"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-dom@17.0.2/umd/react-dom.production.min.js"></script> <script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-dom@17.0.2/umd/react-dom.production.min.js"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-intl@5.20.7/react-intl.iife.min.js"></script> <script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-intl@5.20.7/react-intl.iife.min.js"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@9.4.0/firebase-app.js"></script> <script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@8.10.0/firebase-app.js"></script>
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@9.4.0/firebase-messaging.js"></script> <script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@8.10.0/firebase-messaging.js"></script>
</head> </head>
<body> <body>
<noscript><div id="noscript-message"><div id="noscript-title">Javascript support is required.</div> <noscript><div id="noscript-message"><div id="noscript-title">Javascript support is required.</div>
+2882 -3234
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -1,7 +1,7 @@
{ {
"name": "tinode-webapp", "name": "tinode-webapp",
"description": "Tinode messenger for the web", "description": "Tinode messenger for the web",
"version": "0.18.0-beta1", "version": "0.18.2-beta1",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/tinode/example-react-js.git" "url": "https://github.com/tinode/example-react-js.git"
@@ -66,11 +66,11 @@
"author": "Tinode Authors <info@tinode.co>", "author": "Tinode Authors <info@tinode.co>",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"firebase": "^9.4.0", "firebase": "^8.10.0",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-intl": "^5.20.2", "react-intl": "^5.20.2",
"tinode-sdk": "^0.18.0-beta1" "tinode-sdk": "^0.18.1"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.14.5", "@babel/core": "^7.14.5",
@@ -80,7 +80,7 @@
"babel-loader": "^8.2.2", "babel-loader": "^8.2.2",
"babel-plugin-formatjs": "^10.2.19", "babel-plugin-formatjs": "^10.2.19",
"browserslist": "^4.16.5", "browserslist": "^4.16.5",
"copy-webpack-plugin": "^9.0.0", "copy-webpack-plugin": "^10.0.0",
"cssnano": "^5.0.6", "cssnano": "^5.0.6",
"glob": "^7.1.7", "glob": "^7.1.7",
"mkdirp": "^1.0.4", "mkdirp": "^1.0.4",
+11
View File
@@ -0,0 +1,11 @@
# Push notifications
If you want to use the app with your own server and want web push notification to work you have to set them up:
* Register at https://firebase.google.com/, set up the project if you have not done so already.
* Follow instructions to create a web application https://support.google.com/firebase/answer/9326094 in your project.
* Follow instructions at https://support.google.com/firebase/answer/7015592 to get a Firebase configuration object ("Firebase SDK snippet").
* Locate `firebase-init.js` in the root folder of your copy of TinodeWeb app. Copy-paste the following keys from the configuration object to `firebase-init.js`: `apiKey`, `messagingSenderId`, `projectId`, `appId` (you may copy all keys).
* Copy `Web Push certificate` (Project Settings -> Cloud Messaging -> Web configuration -> Web Push certificates) to `messagingVapidKey` field in `firebase-init.js`.
* Double check that `firebase-init.js` contains the following keys: `apiKey`, `messagingSenderId`, `projectId`, `appId`, `messagingVapidKey`. The file may contain other optional keys.
* Copy Google-provided server key to `tinode.conf`, see details [here](https://github.com/tinode/chat/blob/master/docs/faq.md#q-how-to-setup-fcm-push-notifications).
+1 -1
View File
@@ -96,7 +96,7 @@ export const MESSAGE_PREVIEW_LENGTH = 80;
// Length of a quote in quoted reply (for outgoing messages). // Length of a quote in quoted reply (for outgoing messages).
export const QUOTED_REPLY_LENGTH = 30; export const QUOTED_REPLY_LENGTH = 30;
// Length of a preview of a forwarded message. // Length of a preview of a forwarded message.
export const FORWARDED_PREVIEW_LENGTH = 96; export const FORWARDED_PREVIEW_LENGTH = 84;
// Link for "Contact Us". // Link for "Contact Us".
export const LINK_CONTACT_US = 'email:support@tinode.co'; export const LINK_CONTACT_US = 'email:support@tinode.co';
+7
View File
@@ -1353,5 +1353,12 @@
"description": "Contact search prompt when forwarding a message.", "description": "Contact search prompt when forwarding a message.",
"missing": false, "missing": false,
"obsolete": false "obsolete": false
},
"label_new_password": {
"translation": "Neues Kennwort",
"defaultMessage": "New password",
"description": "Label for entering a new password",
"missing": false,
"obsolete": false
} }
} }
+7 -56
View File
@@ -34,13 +34,6 @@
"missing": false, "missing": false,
"obsolete": false "obsolete": false
}, },
"action_more": {
"translation": "More",
"defaultMessage": "More",
"description": "Action for showing more content",
"missing": false,
"obsolete": true
},
"action_report_chat": { "action_report_chat": {
"translation": "Report Conversation", "translation": "Report Conversation",
"defaultMessage": "Report Conversation", "defaultMessage": "Report Conversation",
@@ -461,13 +454,6 @@
"missing": false, "missing": false,
"obsolete": false "obsolete": false
}, },
"label_default_access": {
"translation": "Default access mode:",
"defaultMessage": "Default access mode:",
"description": "Section title",
"missing": false,
"obsolete": true
},
"label_default_access_mode": { "label_default_access_mode": {
"translation": "Default access mode:", "translation": "Default access mode:",
"defaultMessage": "Default access mode:", "defaultMessage": "Default access mode:",
@@ -1033,13 +1019,6 @@
"missing": false, "missing": false,
"obsolete": false "obsolete": false
}, },
"sidepanel_title_acc_general": {
"translation": "General",
"defaultMessage": "General",
"description": "Sidepanel title for TopicCommonView.",
"missing": false,
"obsolete": true
},
"sidepanel_title_acc_notifications": { "sidepanel_title_acc_notifications": {
"translation": "Notifications", "translation": "Notifications",
"defaultMessage": "Notifications", "defaultMessage": "Notifications",
@@ -1047,13 +1026,6 @@
"missing": false, "missing": false,
"obsolete": false "obsolete": false
}, },
"sidepanel_title_acc_security": {
"translation": "Security",
"defaultMessage": "Security",
"description": "Sidepanel title for AccSecurityView.",
"missing": false,
"obsolete": true
},
"sidepanel_title_acc_support": { "sidepanel_title_acc_support": {
"translation": "Support", "translation": "Support",
"defaultMessage": "Support", "defaultMessage": "Support",
@@ -1089,13 +1061,6 @@
"missing": false, "missing": false,
"obsolete": false "obsolete": false
}, },
"sidepanel_title_crop": {
"translation": "Drag to Adjust",
"defaultMessage": "Drag to Adjust",
"description": "Sidepanel title for AvatarCropView.",
"missing": false,
"obsolete": true
},
"sidepanel_title_login": { "sidepanel_title_login": {
"translation": "Sign In", "translation": "Sign In",
"defaultMessage": "Sign In", "defaultMessage": "Sign In",
@@ -1187,13 +1152,6 @@
"missing": false, "missing": false,
"obsolete": false "obsolete": false
}, },
"title_info": {
"translation": "Info",
"defaultMessage": "Info",
"description": "Title for InfoView",
"missing": false,
"obsolete": true
},
"title_manage_tags": { "title_manage_tags": {
"translation": "Manage", "translation": "Manage",
"defaultMessage": "Manage", "defaultMessage": "Manage",
@@ -1299,20 +1257,6 @@
"missing": false, "missing": false,
"obsolete": false "obsolete": false
}, },
"panel_title_acc_general": {
"translation": "General",
"defaultMessage": "General",
"description": "Title for TopicCommonView.",
"missing": false,
"obsolete": true
},
"panel_title_acc_security": {
"translation": "Security",
"defaultMessage": "Security",
"description": "Title for TopicSecirityView and AccSecurityView.",
"missing": false,
"obsolete": true
},
"panel_title_crop": { "panel_title_crop": {
"translation": "Drag to Adjust", "translation": "Drag to Adjust",
"defaultMessage": "Drag to Adjust", "defaultMessage": "Drag to Adjust",
@@ -1409,5 +1353,12 @@
"description": "Contact search prompt when forwarding a message.", "description": "Contact search prompt when forwarding a message.",
"missing": false, "missing": false,
"obsolete": false "obsolete": false
},
"label_new_password": {
"translation": "New password",
"defaultMessage": "New password",
"description": "Label for entering a new password",
"missing": false,
"obsolete": false
} }
} }
+7
View File
@@ -1353,5 +1353,12 @@
"description": "Contact search prompt when forwarding a message.", "description": "Contact search prompt when forwarding a message.",
"missing": false, "missing": false,
"obsolete": false "obsolete": false
},
"label_new_password": {
"translation": "Nueva contraseña",
"defaultMessage": "New password",
"description": "Label for entering a new password",
"missing": false,
"obsolete": false
} }
} }
+4
View File
@@ -302,6 +302,10 @@
"defaultMessage": "Muted:", "defaultMessage": "Muted:",
"description": "Label for Muting/unmuting the topic" "description": "Label for Muting/unmuting the topic"
}, },
"label_new_password": {
"defaultMessage": "New password",
"description": "Label for entering a new password"
},
"label_other_user": { "label_other_user": {
"defaultMessage": "Other", "defaultMessage": "Other",
"description": "Label for the other user when the user is unnamed" "description": "Label for the other user when the user is unnamed"
+7
View File
@@ -1353,5 +1353,12 @@
"description": "Contact search prompt when forwarding a message.", "description": "Contact search prompt when forwarding a message.",
"missing": false, "missing": false,
"obsolete": false "obsolete": false
},
"label_new_password": {
"translation": "새 비밀번호",
"defaultMessage": "New password",
"description": "Label for entering a new password",
"missing": false,
"obsolete": false
} }
} }
+7
View File
@@ -1353,5 +1353,12 @@
"description": "Contact search prompt when forwarding a message.", "description": "Contact search prompt when forwarding a message.",
"missing": false, "missing": false,
"obsolete": false "obsolete": false
},
"label_new_password": {
"translation": "Parolă Nouă",
"defaultMessage": "New password",
"description": "Label for entering a new password",
"missing": false,
"obsolete": false
} }
} }
+7
View File
@@ -1353,5 +1353,12 @@
"description": "Contact search prompt when forwarding a message.", "description": "Contact search prompt when forwarding a message.",
"missing": false, "missing": false,
"obsolete": false "obsolete": false
},
"label_new_password": {
"translation": "Новый пароль",
"defaultMessage": "New password",
"description": "Label for entering a new password",
"missing": false,
"obsolete": false
} }
} }
+7
View File
@@ -1353,5 +1353,12 @@
"description": "Contact search prompt when forwarding a message.", "description": "Contact search prompt when forwarding a message.",
"missing": false, "missing": false,
"obsolete": false "obsolete": false
},
"label_new_password": {
"translation": "新密碼",
"defaultMessage": "New password",
"description": "Label for entering a new password",
"missing": false,
"obsolete": false
} }
} }
+7
View File
@@ -1353,5 +1353,12 @@
"description": "Contact search prompt when forwarding a message.", "description": "Contact search prompt when forwarding a message.",
"missing": false, "missing": false,
"obsolete": false "obsolete": false
},
"label_new_password": {
"translation": "新密码",
"defaultMessage": "New password",
"description": "Label for entering a new password",
"missing": false,
"obsolete": false
} }
} }
+3 -3
View File
@@ -1,6 +1,4 @@
// File and image helper functions. // File and image helper functions.
import { MAX_INBAND_ATTACHMENT_SIZE, MAX_IMAGE_DIM } from '../config.js';
import { bytesToHumanSize } from './strformat.js'
// Supported image MIME types and corresponding file extensions. // Supported image MIME types and corresponding file extensions.
export const SUPPORTED_IMAGE_FORMATS = ['image/jpeg', 'image/gif', 'image/png', 'image/svg', 'image/svg+xml']; export const SUPPORTED_IMAGE_FORMATS = ['image/jpeg', 'image/gif', 'image/png', 'image/svg', 'image/svg+xml'];
@@ -9,11 +7,13 @@ export const MIME_EXTENSIONS = ['jpg', 'gif', 'png',
// Get an URL from a theCard photo: either create a data: URL or return reference URL. // Get an URL from a theCard photo: either create a data: URL or return reference URL.
export function makeImageUrl(photo) { export function makeImageUrl(photo) {
if (photo && typeof photo == 'object') { if (photo && typeof photo == 'object') {
if (photo.ref) {
return photo.ref
}
if (photo.data && photo.type) { if (photo.data && photo.type) {
const mime = photo.type.startsWith('image/') ? photo.type : ('image/' + photo.type); const mime = photo.type.startsWith('image/') ? photo.type : ('image/' + photo.type);
return 'data:' + mime + ';base64,' + photo.data; return 'data:' + mime + ';base64,' + photo.data;
} }
return photo.ref;
} }
return null; return null;
} }
+61 -33
View File
@@ -124,25 +124,31 @@ function quotedImage(data) {
// onImagePreview: this.handleImagePreview // onImagePreview: this.handleImagePreview
// onFormButtonClick: this.handleFormButtonClick // onFormButtonClick: this.handleFormButtonClick
// onQuoteClick: this.handleQuoteClick (optional) // onQuoteClick: this.handleQuoteClick (optional)
export function fullFormatter(style, data, values, key) { export function fullFormatter(style, data, values, key, stack) {
if (stack.includes('QQ')) {
return quoteFormatter.call(this, style, data, values, key);
}
if (!style) { if (!style) {
// Unformatted. // Unformatted.
return values; return values;
} }
if (style == 'HD') {
// Hidden.
return null;
}
let el = Drafty.tagName(style); let el = Drafty.tagName(style);
const attr = Drafty.attrValue(style, data) || {}; const attr = Drafty.attrValue(style, data) || {};
attr.key = key; attr.key = key;
switch (style) { switch (style) {
case 'BR':
values = null;
break;
case 'HL': case 'HL':
// Highlighted text. Assign class name. // Highlighted text. Assign class name.
attr.className = 'highlight'; attr.className = 'highlight';
break; break;
case 'HD':
el = null;
values = null;
break;
case 'IM': case 'IM':
// Additional processing for images // Additional processing for images
el = handleImageData.call(this, el, data, attr); el = handleImageData.call(this, el, data, attr);
@@ -192,7 +198,7 @@ export function fullFormatter(style, data, values, key) {
return values; return values;
} }
return React.createElement(el, attr, values); return React.createElement(el, attr, values);
}; }
// Converts Drafty object into a one-line preview. 'this' is set by the caller. // Converts Drafty object into a one-line preview. 'this' is set by the caller.
// 'this' must contain: // 'this' must contain:
@@ -204,11 +210,6 @@ export function previewFormatter(style, data, values, key) {
return values; return values;
} }
if (style == 'HD') {
// Hidden.
return null;
}
let el = Drafty.tagName(style); let el = Drafty.tagName(style);
const attr = { key: key }; const attr = { key: key };
switch (style) { switch (style) {
@@ -256,6 +257,11 @@ export function previewFormatter(style, data, values, key) {
el = React.Fragment; el = React.Fragment;
values = [<i key="ex" className="material-icons">attachment</i>, this.formatMessage(messages.drafty_attachment)]; values = [<i key="ex" className="material-icons">attachment</i>, this.formatMessage(messages.drafty_attachment)];
break; break;
case 'QQ':
case 'HD':
el = null;
values = null;
break;
default: default:
if (el == '_UNKN') { if (el == '_UNKN') {
el = React.Fragment; el = React.Fragment;
@@ -292,12 +298,15 @@ function inlineImageAttr(attr, data) {
// messages: formatjs messages defined with defineMessages. // messages: formatjs messages defined with defineMessages.
// authorizeURL: this.props.tinode.authorizeURL // authorizeURL: this.props.tinode.authorizeURL
// onQuoteClick: this.handleQuoteClick (optional) // onQuoteClick: this.handleQuoteClick (optional)
export function quoteFormatter(style, data, values, key) { function quoteFormatter(style, data, values, key) {
if (['BR', 'EX', 'IM', 'MN', 'QQ'].includes(style)) { if (['BR', 'EX', 'IM', 'MN'].includes(style)) {
let el = Drafty.tagName(style); let el = Drafty.tagName(style);
let attr = Drafty.attrValue(style, data) || {}; let attr = Drafty.attrValue(style, data) || {};
attr.key = key; attr.key = key;
switch(style) { switch(style) {
case 'BR':
values = null;
break;
case 'IM': case 'IM':
attr = inlineImageAttr.call(this, attr, data); attr = inlineImageAttr.call(this, attr, data);
values = [React.createElement('img', attr, null), ' ', attr.alt]; values = [React.createElement('img', attr, null), ' ', attr.alt];
@@ -312,10 +321,6 @@ export function quoteFormatter(style, data, values, key) {
attr.className += ' ' + idToColorClass(data.val, false, true); attr.className += ' ' + idToColorClass(data.val, false, true);
} }
break; break;
case 'QQ':
attr.className = 'reply-quote';
attr.onClick = this.onQuoteClick;
break;
case 'EX': case 'EX':
let fname; let fname;
if (data) { if (data) {
@@ -344,15 +349,21 @@ function quoteImage(data) {
// Get the blob from the image data. // Get the blob from the image data.
if (data.val) { if (data.val) {
const blob = base64ToBlob(data.val, data.mime); const blob = base64ToBlob(data.val, data.mime);
promise = blob ? Promise.resolve(blob) : Promise.reject(new Error("Invalid image")); if (!blob) {
throw new Error("Invalid image");
}
promise = Promise.resolve(blob);
} else if (data.ref) {
promise = fetch(this.authorizeURL(sanitizeImageUrl(data.ref)))
.then(evt => {
if (evt.ok) {
return evt.blob();
} else {
throw new Error(`Image fetch unsuccessful: ${evt.status} ${evt.statusText}`);
}
});
} else { } else {
promise = fetch(this.authorizeURL(sanitizeImageUrl(data.ref))).then(evt => { throw new Error("Missing image data");
if (evt.ok) {
return evt.blob();
} else {
throw new Error(`Image fetch unsuccessful: ${evt.status} ${evt.statusText}`);
}
});
} }
// Scale the blob. // Scale the blob.
@@ -384,12 +395,29 @@ function quoteImage(data) {
} }
// Create a preview of a reply. // Create a preview of a reply.
export function replyFormatter(style, data, values, key) { export function replyFormatter(style, data, values, key, stack) {
if (style != 'IM') { if (style == 'IM') {
return quoteFormatter.call(this, style, data, values, key); const attr = inlineImageAttr.call(this, {key: key}, data);
let loadedPromise;
try {
loadedPromise = cancelablePromise(quoteImage.call(this, data));
} catch (error) {
loadedPromise = cancelablePromise(error);
}
attr.whenDone = loadedPromise;
values = [React.createElement(LazyImage, attr, null), ' ', attr.alt];
return React.createElement(React.Fragment, {key: key}, values);
} else if (style == 'QQ') {
if (stack.includes('QQ')) {
// Quote inside quote when forwarding a message.
return React.createElement('span', {key: key},
[<i key="qq" className="material-icons">format_quote</i>, ' ']);
}
const attr = Drafty.attrValue('QQ', data) || {};
attr.key = key;
attr.className = 'reply-quote'
return React.createElement(Drafty.tagName('QQ'), attr, values);
} }
const attr = inlineImageAttr.call(this, {key: key}, data); return quoteFormatter.call(this, style, data, values, key);
attr.whenDone = cancelablePromise(quoteImage.call(this, data));
values = [React.createElement(LazyImage, attr, null), ' ', attr.alt];
return React.createElement(React.Fragment, {key: key}, values);
} }
+11 -7
View File
@@ -183,19 +183,23 @@ export function deliveryMarker(received) {
} }
// Wraps a promise to make it cancelable. // Wraps a promise to make it cancelable.
// The parameter can be either a promise or an error. If it's an error, the wrapped promise is
// created in a rejected state.
export function cancelablePromise(promise) { export function cancelablePromise(promise) {
let hasCanceled = false; let hasCanceled = false;
const wrappedPromise = new Promise((resolve, reject) => { const wrappedPromise = promise instanceof Error ?
promise.then( Promise.reject(promise) :
result => hasCanceled ? reject({isCanceled: true}) : resolve(result), new Promise((resolve, reject) => {
error => hasCanceled ? reject({isCanceled: true}) : reject(error) promise.then(
); result => hasCanceled ? reject({isCanceled: true}) : resolve(result),
}); error => hasCanceled ? reject({isCanceled: true}) : reject(error)
);
});
return { return {
promise: wrappedPromise, promise: wrappedPromise,
cancel( ) { cancel() {
hasCanceled = true; hasCanceled = true;
}, },
}; };
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,3 +1,3 @@
// This is a generated file. Don't edit. // This is a generated file. Don't edit.
export const PACKAGE_VERSION = "0.18.0-beta1"; export const PACKAGE_VERSION = "0.18.2-beta1";
+103 -57
View File
@@ -14,19 +14,20 @@ import Invitation from '../widgets/invitation.jsx';
import LetterTile from '../widgets/letter-tile.jsx'; import LetterTile from '../widgets/letter-tile.jsx';
import LoadSpinner from '../widgets/load-spinner.jsx'; import LoadSpinner from '../widgets/load-spinner.jsx';
import LogoView from './logo-view.jsx'; import LogoView from './logo-view.jsx';
import MetaMessage from '../widgets/meta-message.jsx';
import SendMessage from '../widgets/send-message.jsx'; import SendMessage from '../widgets/send-message.jsx';
import { DEFAULT_P2P_ACCESS_MODE, IMAGE_PREVIEW_DIM, IMAGE_THUMBNAIL_DIM, KEYPRESS_DELAY, import { DEFAULT_P2P_ACCESS_MODE, IMAGE_PREVIEW_DIM, KEYPRESS_DELAY,
MESSAGES_PAGE, MAX_EXTERN_ATTACHMENT_SIZE, MAX_IMAGE_DIM, MAX_INBAND_ATTACHMENT_SIZE, MESSAGES_PAGE, MAX_EXTERN_ATTACHMENT_SIZE, MAX_IMAGE_DIM, MAX_INBAND_ATTACHMENT_SIZE,
READ_DELAY, QUOTED_REPLY_LENGTH } from '../config.js'; READ_DELAY, QUOTED_REPLY_LENGTH } from '../config.js';
import { SUPPORTED_IMAGE_FORMATS, blobToBase64, base64ToBlob, filePasted, fileToBase64, import { blobToBase64, fileToBase64, imageScaled, makeImageUrl } from '../lib/blob-helpers.js';
imageScaled, makeImageUrl } from '../lib/blob-helpers.js';
import HashNavigation from '../lib/navigation.js'; import HashNavigation from '../lib/navigation.js';
import { bytesToHumanSize, shortDateFormat, letterTileColorId } from '../lib/strformat.js'; import { bytesToHumanSize, shortDateFormat } from '../lib/strformat.js';
import { sanitizeImageUrl } from '../lib/utils.js';
// Run timer with this frequency (ms) for checking notification queue. // Run timer with this frequency (ms) for checking notification queue.
const NOTIFICATION_EXEC_INTERVAL = 300; const NOTIFICATION_EXEC_INTERVAL = 300;
// Scroll distance before [go to latest message] button is shown.
const SHOW_GO_TO_LAST_DIST = 100;
const messages = defineMessages({ const messages = defineMessages({
online_now: { online_now: {
@@ -110,6 +111,7 @@ class MessagesView extends React.Component {
this.handleCancelUpload = this.handleCancelUpload.bind(this); this.handleCancelUpload = this.handleCancelUpload.bind(this);
this.postReadNotification = this.postReadNotification.bind(this); this.postReadNotification = this.postReadNotification.bind(this);
this.clearNotificationQueue = this.clearNotificationQueue.bind(this); this.clearNotificationQueue = this.clearNotificationQueue.bind(this);
this.goToLatestMessage = this.goToLatestMessage.bind(this);
this.handlePickReply = this.handlePickReply.bind(this); this.handlePickReply = this.handlePickReply.bind(this);
this.handleCancelReply = this.handleCancelReply.bind(this); this.handleCancelReply = this.handleCancelReply.bind(this);
@@ -149,13 +151,13 @@ class MessagesView extends React.Component {
// 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. // or vertical shrinking.
componentDidUpdate(prevProps, prevState) { componentDidUpdate(prevProps, prevState) {
if (this.messagesScroller) { if (this.messagesScroller &&
if (prevState.topic != this.state.topic || prevState.messageCount != this.state.messageCount) { (prevState.topic != this.state.topic || prevState.messageCount != this.state.messageCount)) {
// New message // New message.
this.messagesScroller.scrollTop = this.messagesScroller.scrollHeight - this.state.scrollPosition; if (this.state.scrollPosition < SHOW_GO_TO_LAST_DIST) {
} else if (prevProps.viewportHeight > this.props.viewportHeight) { this.messagesScroller.scrollTop = this.messagesScroller.scrollHeight -
// Componet changed height. this.state.scrollPosition -
this.messagesScroller.scrollTop += prevProps.viewportHeight - this.props.viewportHeight; this.messagesScroller.offsetHeight;
} }
} }
@@ -210,7 +212,8 @@ class MessagesView extends React.Component {
fetchingMessages: false, fetchingMessages: false,
peerMessagingDisabled: false, peerMessagingDisabled: false,
channel: false, channel: false,
reply: null reply: null,
showGoToLastButton: false
}; };
} else if (nextProps.topic != prevState.topic) { } else if (nextProps.topic != prevState.topic) {
const topic = nextProps.tinode.getTopic(nextProps.topic); const topic = nextProps.tinode.getTopic(nextProps.topic);
@@ -221,7 +224,6 @@ class MessagesView extends React.Component {
const preview = nextProps.forwardMessage.preview; const preview = nextProps.forwardMessage.preview;
reply = { reply = {
content: preview, content: preview,
forwarded: nextProps.forwardMessage.head.forwarded,
seq: null seq: null
}; };
} }
@@ -234,7 +236,8 @@ class MessagesView extends React.Component {
typingIndicator: false, typingIndicator: false,
scrollPosition: 0, scrollPosition: 0,
fetchingMessages: false, fetchingMessages: false,
reply: reply reply: reply,
showGoToLastButton: false
}; };
if (topic) { if (topic) {
@@ -405,6 +408,7 @@ class MessagesView extends React.Component {
} }
} }
// Don't use React.createRef as the ref.current is not available in componentDidMount in this component.
handleScrollReference(node) { handleScrollReference(node) {
if (node) { if (node) {
node.addEventListener('scroll', this.handleScrollEvent); node.addEventListener('scroll', this.handleScrollEvent);
@@ -413,9 +417,14 @@ class MessagesView extends React.Component {
} }
} }
// Get older messages // Get older messages and show/hide [go to latest message] button.
handleScrollEvent(event) { handleScrollEvent(event) {
this.setState({scrollPosition: event.target.scrollHeight - event.target.scrollTop}); const pos = event.target.scrollHeight - event.target.scrollTop - event.target.offsetHeight;
this.setState({
scrollPosition: pos,
showGoToLastButton: pos > SHOW_GO_TO_LAST_DIST,
});
if (this.state.fetchingMessages) { if (this.state.fetchingMessages) {
return; return;
} }
@@ -432,6 +441,13 @@ class MessagesView extends React.Component {
} }
} }
goToLatestMessage() {
this.setState({scrollPosition: 0});
if (this.messagesScroller) {
this.messagesScroller.scrollTop = this.messagesScroller.scrollHeight;
}
}
handleDescChange(desc) { handleDescChange(desc) {
if (desc.public) { if (desc.public) {
this.setState({ this.setState({
@@ -557,7 +573,11 @@ class MessagesView extends React.Component {
// Scroll to the bottom if the message is added to the end of the message list. // Scroll to the bottom if the message is added to the end of the message list.
// TODO: This should be replaced by showing a "scroll to bottom" button. // TODO: This should be replaced by showing a "scroll to bottom" button.
if (topic.isNewMessage(msg.seq)) { if (topic.isNewMessage(msg.seq)) {
this.setState({scrollPosition: 0}); if (this.state.scrollPosition > SHOW_GO_TO_LAST_DIST) {
this.setState({showGoToLastButton: true});
} else {
this.setState({scrollPosition: 0});
}
} }
// Aknowledge messages except own messages. They are // Aknowledge messages except own messages. They are
@@ -693,7 +713,7 @@ class MessagesView extends React.Component {
if (typeof msg == 'string') { if (typeof msg == 'string') {
msg = Drafty.parse(msg); msg = Drafty.parse(msg);
} }
msg = Drafty.append(this.state.reply.content, msg); msg = Drafty.append(Drafty.sanitizeEntities(this.state.reply.content), msg);
this.handleCancelReply(); this.handleCancelReply();
} }
this.props.sendMessage(msg, uploadCompletionPromise, uploader, head); this.props.sendMessage(msg, uploadCompletionPromise, uploader, head);
@@ -847,18 +867,21 @@ class MessagesView extends React.Component {
uploader.cancel(); uploader.cancel();
} }
handlePickReply(seq, content, forwarded, senderId, senderName) { // seq: seq ID of the source message
// context: message content.
// forwarded: true if the source message is also a forwarded message.
// senderId: UID of the sender of the source message.
// senderName: full name of the sender of the original message.
handlePickReply(seq, content, senderId, senderName) {
this.setState({reply: null}); this.setState({reply: null});
if (!seq || !content) { if (!seq || !content) {
return; return;
} }
content = forwarded ? content = typeof content == 'string' ? Drafty.init(content) : content;
Drafty.forwardedContent(content) :
typeof content == 'string' ? Drafty.init(content) : content;
if (Drafty.isValid(content)) { if (Drafty.isValid(content)) {
content = Drafty.preview(content, QUOTED_REPLY_LENGTH, undefined, !forwarded); content = Drafty.replyContent(content, QUOTED_REPLY_LENGTH);
} else { } else {
// /!\ invalid content. // /!\ invalid content.
content = Drafty.append(Drafty.init('\u26A0 '), content = Drafty.append(Drafty.init('\u26A0 '),
@@ -949,9 +972,11 @@ class MessagesView extends React.Component {
icon_badges.push({icon: 'dangerous', color: 'badge-inv'}); icon_badges.push({icon: 'dangerous', color: 'badge-inv'});
} }
} }
let messageNodes = [];
const messageNodes = [];
let previousFrom = null; let previousFrom = null;
let chatBoxClass = null; let chatBoxClass = null;
const dateFmt = new Intl.DateTimeFormat(this.props.intl.locale);
topic.messages((msg, prev, next, i) => { topic.messages((msg, prev, next, i) => {
let nextFrom = next ? (next.from || 'chan') : null; let nextFrom = next ? (next.from || 'chan') : null;
@@ -986,37 +1011,54 @@ class MessagesView extends React.Component {
replyToSeq = null; replyToSeq = null;
} }
messageNodes.push( // This message was sent on a different date than the previous.
<ChatMessage if (!prev || new Date(prev.ts).toDateString() != new Date(msg.ts).toDateString()) {
tinode={this.props.tinode} messageNodes.push(
content={msg.content} <MetaMessage
forwarded={msg.head ? msg.head.forwarded : null} date={dateFmt.format(msg.ts)}
deleted={msg.hi} locale={this.props.intl.locale}
mimeType={msg.head ? msg.head.mime : null} key={'date-' + msg.seq} />
timestamp={msg.ts} );
response={isReply} }
seq={msg.seq}
isGroup={groupTopic} if (msg.hi) {
isChan={this.state.channel} // Deleted message.
userFrom={userFrom} messageNodes.push(
userName={userName} <MetaMessage
userAvatar={userAvatar} deleted={true}
sequence={sequence} key={msg.seq} />
received={deliveryStatus} );
uploader={msg._uploader} } else {
viewportWidth={this.props.viewportWidth} // Used by `formatter`. messageNodes.push(
showContextMenu={this.handleShowMessageContextMenu} <ChatMessage
onImagePreview={this.handleImagePostview} tinode={this.props.tinode}
onFormResponse={this.handleFormResponse} content={msg.content}
onError={this.props.onError} mimeType={msg.head ? msg.head.mime : null}
onCancelUpload={this.handleCancelUpload} timestamp={msg.ts}
pickReply={this.handlePickReply} response={isReply}
replyToSeq={replyToSeq} seq={msg.seq}
onQuoteClick={this.handleQuoteClick} isGroup={groupTopic}
ref={ref} isChan={this.state.channel}
userIsWriter={this.state.isWriter} userFrom={userFrom}
key={msg.seq} /> userName={userName}
); userAvatar={userAvatar}
sequence={sequence}
received={deliveryStatus}
uploader={msg._uploader}
viewportWidth={this.props.viewportWidth} // Used by `formatter`.
showContextMenu={this.handleShowMessageContextMenu}
onImagePreview={this.handleImagePostview}
onFormResponse={this.handleFormResponse}
onError={this.props.onError}
onCancelUpload={this.handleCancelUpload}
pickReply={this.handlePickReply}
replyToSeq={replyToSeq}
onQuoteClick={this.handleQuoteClick}
ref={ref}
userIsWriter={this.state.isWriter}
key={msg.seq} />
);
}
}); });
let lastSeen = null; let lastSeen = null;
@@ -1082,6 +1124,10 @@ class MessagesView extends React.Component {
: null} : null}
<LoadSpinner show={this.state.fetchingMessages} /> <LoadSpinner show={this.state.fetchingMessages} />
<div id="messages-container"> <div id="messages-container">
<button className={'action-button' + (this.state.showGoToLastButton ? '' : ' hidden')}
onClick={this.goToLatestMessage}>
<i className="material-icons">arrow_downward</i>
</button>
<div id="messages-panel" ref={this.handleScrollReference}> <div id="messages-panel" ref={this.handleScrollReference}>
<ul id="scroller" className={chatBoxClass}> <ul id="scroller" className={chatBoxClass}>
{messageNodes} {messageNodes}
@@ -1112,7 +1158,7 @@ class MessagesView extends React.Component {
<SendMessage <SendMessage
tinode={this.props.tinode} tinode={this.props.tinode}
noInput={!!this.props.forwardMessage} noInput={!!this.props.forwardMessage}
disabled={!this.state.isWriter} disabled={!this.state.isWriter || topic.deleted}
onKeyPress={this.sendKeyPress} onKeyPress={this.sendKeyPress}
onSendMessage={this.sendMessage} onSendMessage={this.sendMessage}
onAttachFile={this.props.forwardMessage ? null : this.handleAttachFile} onAttachFile={this.props.forwardMessage ? null : this.handleAttachFile}
+4 -5
View File
@@ -35,7 +35,7 @@ class NewTopicView extends React.Component {
super(props); super(props);
this.state = { this.state = {
tabSelected: 'p2p', tabSelected: 'find',
searchQuery: null searchQuery: null
}; };
@@ -62,7 +62,7 @@ class NewTopicView extends React.Component {
} }
handleContactSelected(uid) { handleContactSelected(uid) {
if (this.state.tabSelected == 'p2p') { if (this.state.tabSelected == 'find') {
HashNavigation.navigateTo(HashNavigation.removeUrlParam(window.location.hash, 'tab')); HashNavigation.navigateTo(HashNavigation.removeUrlParam(window.location.hash, 'tab'));
this.props.onCreateTopic(uid); this.props.onCreateTopic(uid);
} }
@@ -87,8 +87,8 @@ class NewTopicView extends React.Component {
return ( return (
<div className="flex-column"> <div className="flex-column">
<ul className="tabbar"> <ul className="tabbar">
<li className={this.state.tabSelected === "p2p" ? "active" : null}> <li className={this.state.tabSelected === "find" ? "active" : null}>
<a href="#" data-id="p2p" onClick={this.handleTabClick}> <a href="#" data-id="find" onClick={this.handleTabClick}>
<FormattedMessage id="tabtitle_find_user" defaultMessage="find" <FormattedMessage id="tabtitle_find_user" defaultMessage="find"
description="Tab title Find" /> description="Tab title Find" />
</a> </a>
@@ -116,7 +116,6 @@ class NewTopicView extends React.Component {
onError={this.props.onError} /> : onError={this.props.onError} /> :
<div className="flex-column"> <div className="flex-column">
<SearchContacts <SearchContacts
type="p2p"
placeholder={search_placeholder} placeholder={search_placeholder}
onSearchContacts={this.handleSearchContacts} /> onSearchContacts={this.handleSearchContacts} />
<ContactList <ContactList
+7 -1
View File
@@ -53,15 +53,21 @@ export default class PasswordResetView extends React.PureComponent {
return ( return (
<form id="password-reset-form" onSubmit={this.handleSubmit}> <form id="password-reset-form" onSubmit={this.handleSubmit}>
{(this.state.token && this.state.scheme) ? {(this.state.token && this.state.scheme) ?
<><label htmlFor="new-password">
<FormattedMessage id="label_new_password"
defaultMessage="New password"
description="Label for entering a new password" />
</label>
<FormattedMessage id="new_password_placeholder" defaultMessage="Enter new password" <FormattedMessage id="new_password_placeholder" defaultMessage="Enter new password"
description="Placeholder for entering new password">{ description="Placeholder for entering new password">{
(placeholder) => <VisiblePassword (placeholder) => <VisiblePassword
id="new-password"
placeholder={placeholder} placeholder={placeholder}
autoComplete="new-password" autoComplete="new-password"
value={this.state.password} value={this.state.password}
required={true} autoFocus={true} required={true} autoFocus={true}
onChange={this.handlePasswordChange} /> onChange={this.handlePasswordChange} />
}</FormattedMessage> }</FormattedMessage></>
: this.state.sent ? : this.state.sent ?
<> <>
<br/> <br/>
+20 -10
View File
@@ -16,7 +16,7 @@ import MessagesView from './messages-view.jsx';
import SidepanelView from './sidepanel-view.jsx'; import SidepanelView from './sidepanel-view.jsx';
import { API_KEY, APP_NAME, DEFAULT_P2P_ACCESS_MODE, FORWARDED_PREVIEW_LENGTH, LOGGING_ENABLED, import { API_KEY, APP_NAME, DEFAULT_P2P_ACCESS_MODE, FORWARDED_PREVIEW_LENGTH, LOGGING_ENABLED,
MEDIA_BREAKPOINT, QUOTED_REPLY_LENGTH, RECEIVED_DELAY } from '../config.js'; MEDIA_BREAKPOINT, RECEIVED_DELAY } from '../config.js';
import { PACKAGE_VERSION } from '../version.js'; import { PACKAGE_VERSION } from '../version.js';
import { base64ReEncode, makeImageUrl } from '../lib/blob-helpers.js'; import { base64ReEncode, makeImageUrl } from '../lib/blob-helpers.js';
import { detectServerAddress, isLocalHost, isSecureConnection } from '../lib/host-name.js'; import { detectServerAddress, isLocalHost, isSecureConnection } from '../lib/host-name.js';
@@ -288,9 +288,12 @@ class TinodeWeb extends React.Component {
}); });
} }
// Save possible topic name and navigate to blank state. // Maybe navigate to home screen.
this.setState({requestedTopic: parsedNav.path[1]}); if (!['cred', 'reset', 'register'].includes(parsedNav.path[0])) {
HashNavigation.navigateTo(''); // Save possible topic name.
this.setState({requestedTopic: parsedNav.path[1]});
HashNavigation.navigateTo('');
}
this.readTimer = null; this.readTimer = null;
this.readTimerCallback = null; this.readTimerCallback = null;
@@ -1199,6 +1202,8 @@ class TinodeWeb extends React.Component {
delete parsed.params.code; delete parsed.params.code;
delete parsed.params.method; delete parsed.params.method;
delete parsed.params.tab; delete parsed.params.tab;
delete parsed.params.scheme;
delete parsed.params.token;
} }
HashNavigation.navigateTo(HashNavigation.composeUrlHash(parsed.path, parsed.params)); HashNavigation.navigateTo(HashNavigation.composeUrlHash(parsed.path, parsed.params));
this.setState({errorText: '', errorLevel: null}); this.setState({errorText: '', errorLevel: null});
@@ -1230,8 +1235,10 @@ class TinodeWeb extends React.Component {
params.desc = {defacs: {auth: DEFAULT_P2P_ACCESS_MODE}}; params.desc = {defacs: {auth: DEFAULT_P2P_ACCESS_MODE}};
} else { } else {
topicName = topicName || this.tinode.newGroupTopicName(isChannel); topicName = topicName || this.tinode.newGroupTopicName(isChannel);
params.desc = {public: newTopicParams.public, private: {comment: newTopicParams.private}}; if (newTopicParams) {
params.tags = newTopicParams.tags; params.desc = {public: newTopicParams.public, private: {comment: newTopicParams.private}};
params.tags = newTopicParams.tags;
}
} }
params._topicName = topicName; params._topicName = topicName;
this.setState({newTopicParams: params}, () => {this.handleTopicSelected(topicName)}); this.setState({newTopicParams: params}, () => {this.handleTopicSelected(topicName)});
@@ -1443,22 +1450,22 @@ class TinodeWeb extends React.Component {
}); });
} }
//
handleShowForwardDialog(params) { handleShowForwardDialog(params) {
if (this.state.sidePanelSelected == 'newtpk') { if (this.state.sidePanelSelected == 'newtpk') {
// If the Find panel is active, close it. // If the Find panel is active, close it.
this.handleSidepanelCancel(); this.handleSidepanelCancel();
} }
const header = '➦ ' + params.userName; const header = '➦ ' + params.userName;
const content = params.forwarded ? const content = typeof params.content == 'string' ?
Tinode.Drafty.forwardedContent(params.content) : Tinode.Drafty.init(params.content) : Tinode.Drafty.forwardedContent(params.content);
typeof params.content == 'string' ?
Tinode.Drafty.init(params.content) : params.content;
const preview = Tinode.Drafty.preview(content, FORWARDED_PREVIEW_LENGTH, const preview = Tinode.Drafty.preview(content, FORWARDED_PREVIEW_LENGTH,
undefined, params.forwarded != null); undefined, params.forwarded != null);
const msg = Tinode.Drafty.append( const msg = Tinode.Drafty.append(
Tinode.Drafty.appendLineBreak( Tinode.Drafty.appendLineBreak(
Tinode.Drafty.mention(header, params.userFrom)), Tinode.Drafty.mention(header, params.userFrom)),
content); content);
const msgPreview = Tinode.Drafty.quote(header, params.userFrom, preview); const msgPreview = Tinode.Drafty.quote(header, params.userFrom, preview);
const head = { const head = {
forwarded: params.topicName + ':' + params.seq forwarded: params.topicName + ':' + params.seq
@@ -1611,6 +1618,9 @@ class TinodeWeb extends React.Component {
.then(() => { .then(() => {
return this.tinode.updateAccountBasic(null, null, newPassword, {token: token}); return this.tinode.updateAccountBasic(null, null, newPassword, {token: token});
}) })
.then(() => {
HashNavigation.navigateTo('');
})
.catch((err) => { .catch((err) => {
// Socket error // Socket error
this.handleError(err.message, 'err'); this.handleError(err.message, 'err');
+1 -1
View File
@@ -34,7 +34,7 @@ export default class AvatarUpload extends React.Component {
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.
const randId = 'file-input-avatar-' + (Math.random() + '').substr(2); const randId = 'file-input-avatar-' + ('' + Math.random()).substring(0, 4);
const className = 'avatar-upload' + (this.props.readOnly ? ' read-only' : ''); const className = 'avatar-upload' + (this.props.readOnly ? ' read-only' : '');
return ( return (
<div className={className}> <div className={className}>
+9 -18
View File
@@ -1,4 +1,5 @@
// Single message, sent or received. // Single chat message bubble, sent or received.
import React from 'react'; import React from 'react';
import { FormattedMessage, injectIntl } from 'react-intl'; import { FormattedMessage, injectIntl } from 'react-intl';
import { Drafty } from 'tinode-sdk'; import { Drafty } from 'tinode-sdk';
@@ -7,8 +8,8 @@ import Attachment from './attachment.jsx';
import LetterTile from './letter-tile.jsx'; import LetterTile from './letter-tile.jsx';
import ReceivedMarker from './received-marker.jsx' import ReceivedMarker from './received-marker.jsx'
import { fullFormatter, quoteFormatter } from '../lib/formatters.js'; import { fullFormatter } from '../lib/formatters.js';
import { sanitizeImageUrl, sanitizeUrl } from '../lib/utils.js'; import { sanitizeUrl } from '../lib/utils.js';
class BaseChatMessage extends React.PureComponent { class BaseChatMessage extends React.PureComponent {
constructor(props) { constructor(props) {
@@ -29,7 +30,6 @@ class BaseChatMessage extends React.PureComponent {
this.handleQuoteClick = this.handleQuoteClick.bind(this); this.handleQuoteClick = this.handleQuoteClick.bind(this);
this.formatterContext = { this.formatterContext = {
getFormatter: (tp) => { return tp == 'QQ' ? quoteFormatter : null; },
formatMessage: props.intl.formatMessage.bind(props.intl), formatMessage: props.intl.formatMessage.bind(props.intl),
viewportWidth: props.viewportWidth, viewportWidth: props.viewportWidth,
authorizeURL: props.tinode.authorizeURL.bind(props.tinode), authorizeURL: props.tinode.authorizeURL.bind(props.tinode),
@@ -87,7 +87,6 @@ class BaseChatMessage extends React.PureComponent {
content: this.props.content, content: this.props.content,
userFrom: this.props.userFrom, userFrom: this.props.userFrom,
userName: this.props.userName, userName: this.props.userName,
forwarded: this.props.forwarded,
y: e.pageY, y: e.pageY,
x: e.pageX, x: e.pageX,
pickReply: this.props.pickReply pickReply: this.props.pickReply
@@ -112,11 +111,10 @@ class BaseChatMessage extends React.PureComponent {
} }
render() { render() {
const sideClass = this.props.deleted ? 'center' : const sideClass = this.props.sequence + ' ' + (this.props.response ? 'left' : 'right');
(this.props.sequence + ' ' + (this.props.response ? 'left' : 'right'));
const bubbleClass = (this.props.sequence == 'single' || this.props.sequence == 'last') ? 'bubble tip' : 'bubble'; const bubbleClass = (this.props.sequence == 'single' || this.props.sequence == 'last') ? 'bubble tip' : 'bubble';
const avatar = this.props.deleted ? null : (this.props.userAvatar || true); const avatar = this.props.userAvatar || true;
const fullDisplay = (this.props.isGroup && this.props.response && !this.props.deleted && const fullDisplay = (this.props.isGroup && this.props.response &&
(this.props.sequence == 'single' || this.props.sequence == 'last')); (this.props.sequence == 'single' || this.props.sequence == 'last'));
let content = this.props.content; let content = this.props.content;
@@ -142,12 +140,6 @@ class BaseChatMessage extends React.PureComponent {
}, this); }, this);
const tree = Drafty.format(content, fullFormatter, this.formatterContext); const tree = Drafty.format(content, fullFormatter, this.formatterContext);
content = React.createElement(React.Fragment, null, tree); content = React.createElement(React.Fragment, null, tree);
} else if (this.props.deleted) {
// Message represents a range of deleted messages.
content = <><i className="material-icons gray">block</i> <i className="gray">
<FormattedMessage id="deleted_content"
defaultMessage="content deleted" description="Shown when messages are deleted" />
</i></>
} else if (typeof content != 'string') { } else if (typeof content != 'string') {
content = <><i className="material-icons gray">warning_amber</i> <i className="gray"> content = <><i className="material-icons gray">warning_amber</i> <i className="gray">
<FormattedMessage id="invalid_content" <FormattedMessage id="invalid_content"
@@ -180,13 +172,12 @@ class BaseChatMessage extends React.PureComponent {
received={this.props.received} /> received={this.props.received} />
: null} : null}
</div> </div>
{this.props.deleted || !this.props.showContextMenu ? {this.props.showContextMenu ?
null :
<span className="menuTrigger"> <span className="menuTrigger">
<a href="#" onClick={this.handleContextClick}> <a href="#" onClick={this.handleContextClick}>
<i className="material-icons">expand_more</i> <i className="material-icons">expand_more</i>
</a> </a>
</span> </span> : null
} }
</div> </div>
{fullDisplay ? {fullDisplay ?
+7 -3
View File
@@ -84,11 +84,14 @@ class ContactList extends React.Component {
if (msg) { if (msg) {
forwarded = msg.head ? msg.head.forwarded : null; forwarded = msg.head ? msg.head.forwarded : null;
deliveryStatus = msg._status || c.msgStatus(msg, true); deliveryStatus = msg._status || c.msgStatus(msg, true);
preview = typeof msg.content == 'string' ? if (msg.content) {
msg.content.substr(0, MESSAGE_PREVIEW_LENGTH) : preview = typeof msg.content == 'string' ?
Drafty.preview(msg.content, MESSAGE_PREVIEW_LENGTH, undefined, forwarded != null); msg.content.substr(0, MESSAGE_PREVIEW_LENGTH) :
Drafty.preview(msg.content, MESSAGE_PREVIEW_LENGTH);
}
} }
} }
contactNodes.push( contactNodes.push(
<Contact <Contact
tinode={this.props.tinode} tinode={this.props.tinode}
@@ -112,6 +115,7 @@ class ContactList extends React.Component {
isVerified={c.trusted && c.trusted.verified} isVerified={c.trusted && c.trusted.verified}
isStaff={c.trusted && c.trusted.staff} isStaff={c.trusted && c.trusted.staff}
isDangerous={c.trusted && c.trusted.danger} isDangerous={c.trusted && c.trusted.danger}
deleted={c.deleted}
item={key} item={key}
index={contactNodes.length} index={contactNodes.length}
key={key} /> key={key} />
+18 -14
View File
@@ -4,7 +4,6 @@ import { FormattedMessage, injectIntl } from 'react-intl';
import ContactBadges from './contact-badges.jsx'; import ContactBadges from './contact-badges.jsx';
import LetterTile from './letter-tile.jsx'; import LetterTile from './letter-tile.jsx';
import ReceivedMarker from './received-marker.jsx'
import UnreadBadge from './unread-badge.jsx'; import UnreadBadge from './unread-badge.jsx';
import { Drafty } from 'tinode-sdk'; import { Drafty } from 'tinode-sdk';
@@ -69,17 +68,20 @@ class Contact extends React.Component {
} }
} }
const subtitle = this.props.preview ? // The this.props.preview contains alreay shortened Drafty or string.
(typeof this.props.preview == 'string' ? this.props.preview : let preview;
Drafty.isValid(this.props.preview) ? if (typeof this.props.preview == 'string') {
React.createElement(React.Fragment, null, Drafty.format(this.props.preview, previewFormatter, preview = this.props.preview;
{formatMessage: this.props.intl.formatMessage})) : } else if (Drafty.isValid(this.props.preview)) {
preview = React.createElement(React.Fragment, null, Drafty.format(this.props.preview, previewFormatter,
{formatMessage: this.props.intl.formatMessage}));
} else if (this.props.preview) {
preview =
<><i className="material-icons gray">warning_amber</i> <i className="gray"> <><i className="material-icons gray">warning_amber</i> <i className="gray">
<FormattedMessage id="invalid_content" <FormattedMessage id="invalid_content"
defaultMessage="invalid content" description="Shown when the message is unreadable" /></i> defaultMessage="invalid content" description="Shown when the message is unreadable" /></i>
</> </>;
) : }
this.props.comment;
const icon = deliveryMarker(this.props.received); const icon = deliveryMarker(this.props.received);
const marker = icon ? <i className={'material-icons small space-right' + const marker = icon ? <i className={'material-icons small space-right' +
@@ -92,11 +94,13 @@ class Contact extends React.Component {
tinode={this.props.tinode} tinode={this.props.tinode}
avatar={avatar} avatar={avatar}
title={this.props.title} title={this.props.title}
topic={this.props.item} /> topic={this.props.item}
{this.props.showOnline ? <span className={online} /> : disabled={this.props.deleted} />
(this.props.showCheckmark && this.props.selected ? {this.props.deleted ? <i className="deleted material-icons">cancel</i> :
this.props.showOnline ? <span className={online} /> :
(this.props.showCheckmark && this.props.selected) ?
<i className="checkmark material-icons">check_circle</i> <i className="checkmark material-icons">check_circle</i>
: null)} : null}
</div> </div>
<div className="text-box"> <div className="text-box">
<div><span className="contact-title">{title}</span> <div><span className="contact-title">{title}</span>
@@ -105,7 +109,7 @@ class Contact extends React.Component {
</div> </div>
{this.props.showMode ? {this.props.showMode ?
<span><ContactBadges badges={badges} /></span> : <span><ContactBadges badges={badges} /></span> :
this.props.small ? null : <div className="contact-comment">{marker}<span>{subtitle || '\u00A0'}</span></div>} this.props.small ? null : <div className="contact-comment">{marker}<span>{preview || this.props.comment || '\u00A0'}</span></div>}
</div> </div>
{this.props.showContextMenu ? {this.props.showContextMenu ?
<span className="menuTrigger"> <span className="menuTrigger">
+1 -1
View File
@@ -434,7 +434,7 @@ class ContextMenu extends React.Component {
} }
replyToMessage(params, errorHandler) { replyToMessage(params, errorHandler) {
params.pickReply(params.seq, params.content, params.forwarded, params.userFrom, params.userName, errorHandler); params.pickReply(params.seq, params.content, params.userFrom, params.userName, errorHandler);
} }
render() { render() {
+1 -1
View File
@@ -55,7 +55,7 @@ export default class ImagePreview extends React.PureComponent {
<div id="image-preview"> <div id="image-preview">
<div id="image-preview-caption-panel"> <div id="image-preview-caption-panel">
{!this.props.onSendMessage ? {!this.props.onSendMessage ?
<a href={this.props.content.url} download={this.props.content.name}> <a href={this.props.content.url} download={true}>
<i className="material-icons">file_download</i> <FormattedMessage <i className="material-icons">file_download</i> <FormattedMessage
id="download_action" defaultMessage="download" description="Call to action [download]" /> id="download_action" defaultMessage="download" description="Call to action [download]" />
</a> </a>
+1
View File
@@ -31,6 +31,7 @@ export default class LazyImage extends React.PureComponent {
if (prevProps.whenDone != this.props.whenDone) { if (prevProps.whenDone != this.props.whenDone) {
this.setState({src: 'img/placeholder.png', style: {...this.state.style, padding: '4px'}}); this.setState({src: 'img/placeholder.png', style: {...this.state.style, padding: '4px'}});
this.props.whenDone this.props.whenDone
.promise
.then(data => this.setState({src: data.src, style: {...this.state.style, padding: 0}})) .then(data => this.setState({src: data.src, style: {...this.state.style, padding: 0}}))
.catch(() => this.setState({src: 'img/broken_image.png'})); .catch(() => this.setState({src: 'img/broken_image.png'}));
} }
+6 -6
View File
@@ -9,21 +9,21 @@ export default class LetterTile extends React.PureComponent {
render() { render() {
let avatar; let avatar;
if (this.props.avatar === true) { if (this.props.avatar === true) {
const isGroup = (Tinode.topicType(this.props.topic) == 'grp'); const isGroup = Tinode.isGroupTopicName(this.props.topic);
const iconColor = idToColorClass(this.props.topic, isGroup); const iconColor = idToColorClass(this.props.topic, isGroup);
if (this.props.topic && this.props.title && this.props.title.trim()) { if (this.props.topic && this.props.title && this.props.title.trim()) {
const letter = this.props.title.trim().charAt(0); const letter = this.props.title.trim().charAt(0);
const className = 'lettertile ' + iconColor; const className = 'lettertile ' + iconColor + (this.props.disabled ? ' disabled' : '');
avatar = (<div className={className}><div>{letter}</div></div>) avatar = (<div className={className}><div>{letter}</div></div>)
} else { } else {
const className = 'material-icons ' + iconColor; const className = 'material-icons ' + iconColor + (this.props.disabled ? ' disabled' : '');
avatar = isGroup ? avatar = isGroup ? <i className={className}>group</i> : <i className={className}>person</i>;
<i className={className}>group</i> : <i className={className}>person</i>;
} }
} else if (this.props.avatar) { } else if (this.props.avatar) {
const url = this.props.tinode.authorizeURL(sanitizeImageUrl(this.props.avatar)); const url = this.props.tinode.authorizeURL(sanitizeImageUrl(this.props.avatar));
// If avatar image is invalid, show a placeholder. // If avatar image is invalid, show a placeholder.
avatar = <img className="avatar" alt="avatar" src={url} const className = 'avatar' + (this.props.disabled ? ' disabled' : '');
avatar = <img className={className} alt="avatar" src={url}
onError={(e)=>{e.target.onerror = null; e.target.src="../img/broken_image.png"}} />; onError={(e)=>{e.target.onerror = null; e.target.src="../img/broken_image.png"}} />;
} else { } else {
avatar = null; avatar = null;
+41
View File
@@ -0,0 +1,41 @@
// Message bubble with non-payload content, such as an indicator of deleted content or a date stamp.
import React from 'react';
import { FormattedMessage } from 'react-intl';
export default class MetaMessage extends React.PureComponent {
constructor(props) {
super(props);
}
render() {
let content = null;
let bubbleClass = 'bubble';
if (this.props.deleted) {
// Message represents a range of deleted messages.
content = <><i className="material-icons gray">block</i> <i className="gray">
<FormattedMessage id="deleted_content"
defaultMessage="content deleted" description="Shown when messages are deleted" />
</i></>
bubbleClass += ' deleted';
} else if (this.props.date) {
// A date badge.
content = <>{this.props.date}</>
bubbleClass += ' date';
}
if (!content) {
return <>{null}</>
}
return (
<li className="meta">
<div className={bubbleClass}>
<div className="message-content">
{content}
</div>
</div>
</li>
);
}
};
+1 -2
View File
@@ -4,7 +4,6 @@ import { defineMessages, injectIntl } from 'react-intl';
import Tinode from 'tinode-sdk'; import Tinode from 'tinode-sdk';
import { shortDateFormat } from '../lib/strformat.js';
import { deliveryMarker } from '../lib/utils.js'; import { deliveryMarker } from '../lib/utils.js';
const messages = defineMessages({ const messages = defineMessages({
@@ -29,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.message_sending_failed); timestamp = formatMessage(messages.message_sending_failed);
} else { } else {
timestamp = shortDateFormat(this.props.timestamp, this.props.intl.locale); timestamp = this.props.timestamp.toLocaleTimeString(this.props.intl.locale, { timeStyle: 'short' });
} }
const icon = deliveryMarker(this.props.received); const icon = deliveryMarker(this.props.received);
-1
View File
@@ -1,5 +1,4 @@
import React from 'react'; import React from 'react';
import { FormattedMessage } from 'react-intl';
import Tinode from 'tinode-sdk'; import Tinode from 'tinode-sdk';
+2 -1
View File
@@ -5,7 +5,7 @@ import { Drafty } from 'tinode-sdk';
import { KEYPRESS_DELAY } from '../config.js'; import { KEYPRESS_DELAY } from '../config.js';
import { filePasted } from '../lib/blob-helpers.js'; import { filePasted } from '../lib/blob-helpers.js';
import { quoteFormatter, replyFormatter } from '../lib/formatters.js'; import { replyFormatter } from '../lib/formatters.js';
const messages = defineMessages({ const messages = defineMessages({
messaging_disabled: { messaging_disabled: {
@@ -40,6 +40,7 @@ class SendMessage extends React.PureComponent {
super(props); super(props);
this.state = { this.state = {
quote: null,
message: '', message: '',
// Make initial keypress time as if it happened 5001 milliseconds in the past. // Make initial keypress time as if it happened 5001 milliseconds in the past.
keypressTimestamp: new Date().getTime() - KEYPRESS_DELAY - 1 keypressTimestamp: new Date().getTime() - KEYPRESS_DELAY - 1
+3 -3
View File
@@ -10,8 +10,8 @@ import AvatarUpload from './avatar-upload.jsx';
import InPlaceEdit from './in-place-edit.jsx'; import InPlaceEdit from './in-place-edit.jsx';
import TagManager from './tag-manager.jsx'; import TagManager from './tag-manager.jsx';
import { AVATAR_SIZE, MAX_AVATAR_BYTES, MAX_EXTERN_ATTACHMENT_SIZE, import { AVATAR_SIZE, MAX_AVATAR_BYTES, MAX_EXTERN_ATTACHMENT_SIZE, MAX_TITLE_LENGTH,
MAX_INBAND_ATTACHMENT_SIZE, MAX_TITLE_LENGTH, MAX_TOPIC_DESCRIPTION_LENGTH } from '../config.js'; MAX_TOPIC_DESCRIPTION_LENGTH } from '../config.js';
import { imageScaled, blobToBase64, makeImageUrl } from '../lib/blob-helpers.js'; import { imageScaled, blobToBase64, makeImageUrl } from '../lib/blob-helpers.js';
import { arrayEqual, theCard } from '../lib/utils.js'; import { arrayEqual, theCard } from '../lib/utils.js';
@@ -109,7 +109,7 @@ export default class TopicDescEdit extends React.Component {
// for uploading it to the server out of band. // for uploading it to the server out of band.
uploadAvatar(mime, blob, width, height) { uploadAvatar(mime, blob, width, height) {
const readyToUpload = (image) => { const readyToUpload = (image) => {
let {mime, blob, width, height} = image; let {mime, blob} = image;
if (blob.size > MAX_AVATAR_BYTES) { if (blob.size > MAX_AVATAR_BYTES) {
// 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.
const uploader = this.props.tinode.getLargeFileHelper(); const uploader = this.props.tinode.getLargeFileHelper();
+353 -206
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+4660 -6470
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,3 +1,3 @@
// This is a generated file. Don't edit. // This is a generated file. Don't edit.
const PACKAGE_VERSION = "0.18.0-beta1"; const PACKAGE_VERSION = "0.18.2-beta1";
Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB