Compare commits
44
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0598a42164 | ||
|
|
9a811708ba | ||
|
|
bb63cb204b | ||
|
|
fb45c442a8 | ||
|
|
e5545f5f14 | ||
|
|
24d75dfbbc | ||
|
|
84a78ce6b5 | ||
|
|
c8756c3a01 | ||
|
|
e3cb8a7291 | ||
|
|
3c11cbbdf1 | ||
|
|
9657a1e61e | ||
|
|
728b3a3a1b | ||
|
|
41bf6f3941 | ||
|
|
a02be831f2 | ||
|
|
5cfcd5789f | ||
|
|
28c34bcf7d | ||
|
|
e87a36421c | ||
|
|
479752036d | ||
|
|
c2d95725e4 | ||
|
|
9acca449e3 | ||
|
|
bd2def7882 | ||
|
|
b201f019ef | ||
|
|
1d59d29519 | ||
|
|
9582a9f2cd | ||
|
|
363104755e | ||
|
|
ab7160170d | ||
|
|
afcb8d8616 | ||
|
|
69cf5817f0 | ||
|
|
572fc66a69 | ||
|
|
6a877620e6 | ||
|
|
c7a60b9608 | ||
|
|
3feca32d13 | ||
|
|
081eb8723d | ||
|
|
a1034654a9 | ||
|
|
faa3f60b55 | ||
|
|
31345eb298 | ||
|
|
a9332495cf | ||
|
|
e4c5da3178 | ||
|
|
f3c6581fa5 | ||
|
|
d6cf419266 | ||
|
|
939804eae0 | ||
|
|
2fc8826403 | ||
|
|
298bca4042 | ||
|
|
f5d3b3af0b |
+2
-1
@@ -1,3 +1,4 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
src/i18n/src/
|
||||
src/i18n/srci/
|
||||
webapp.code-workspace.code-workspace
|
||||
|
||||
@@ -16,13 +16,14 @@ For demo access and other instructions [see here](https://github.com/tinode/chat
|
||||
|
||||
## 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
|
||||
|
||||
* 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 bugs and feature requests [open an issue](https://github.com/tinode/webchat/issues/new).
|
||||
* Use https://tinode.co/contact for commercial inquiries.
|
||||
|
||||
## Internationalization
|
||||
|
||||
@@ -43,6 +44,7 @@ More translations are welcome. See [instructions](https://github.com/tinode/chat
|
||||
|
||||
## Not done yet
|
||||
|
||||
* Voice or video messages, video or audio calling.
|
||||
* End-to-End encryption.
|
||||
* Emoji support is weak.
|
||||
* 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.
|
||||
* 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
|
||||
|
||||
### 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
|
||||
|
||||
<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>
|
||||
|
||||
+129
-43
@@ -46,36 +46,6 @@ a.gray:focus {
|
||||
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 {
|
||||
font-size: 1.1rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
@@ -147,6 +117,7 @@ button.round.small {
|
||||
line-height: 2rem;
|
||||
box-shadow: 0 2px 0.35rem 0 rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
button:focus, button:hover, button:active {
|
||||
color: #fff;
|
||||
}
|
||||
@@ -252,6 +223,7 @@ textarea {
|
||||
|
||||
textarea.inplace-edit {
|
||||
height: unset;
|
||||
font-size: unset;
|
||||
}
|
||||
|
||||
/* END of input and buttons */
|
||||
@@ -265,6 +237,7 @@ textarea.inplace-edit {
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
-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);
|
||||
}
|
||||
|
||||
@@ -432,6 +405,7 @@ div.trusted-badge {
|
||||
font-size: 120%;
|
||||
}
|
||||
|
||||
/* Other universal style components */
|
||||
.small {
|
||||
font-size: 90%;
|
||||
}
|
||||
@@ -461,6 +435,76 @@ div.trusted-badge {
|
||||
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 */
|
||||
.group-focus {
|
||||
outline: none;
|
||||
@@ -963,6 +1007,10 @@ i.search {
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
.avatar.disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.contact-box .avatar-box > .material-icons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -1043,7 +1091,6 @@ img.channel {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
font-size: 1.5rem;
|
||||
line-height: 1.5rem;
|
||||
color: #0097A7;
|
||||
position: absolute;
|
||||
right: 0.05rem;
|
||||
@@ -1052,6 +1099,18 @@ img.channel {
|
||||
overflow: visible;
|
||||
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 */
|
||||
|
||||
#add-topic {
|
||||
@@ -1434,7 +1493,6 @@ img.channel {
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#image-preview {
|
||||
@@ -1515,12 +1573,6 @@ img.channel {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.chat-box .center {
|
||||
justify-content: center;
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.bubble {
|
||||
position: relative;
|
||||
display: flex;
|
||||
@@ -1530,6 +1582,7 @@ img.channel {
|
||||
padding: 0.75rem 0.5rem 0.75rem 0.5rem;
|
||||
margin: 0;
|
||||
text-align: left;
|
||||
color: #212121;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 1px 0.5px rgba(0, 0, 0, 0.12);
|
||||
transition: ease-out background-color 0.5s;
|
||||
@@ -1582,11 +1635,28 @@ img.channel {
|
||||
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;
|
||||
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 .bubble.tip::before {
|
||||
content: '';
|
||||
@@ -1649,7 +1719,6 @@ img.channel {
|
||||
|
||||
.bubble div.message-content {
|
||||
position: relative;
|
||||
color: #212121;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
max-width: 36rem;
|
||||
@@ -1959,6 +2028,10 @@ ul.tabbar li.active > a {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.lettertile.disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Colors of letter tiles */
|
||||
.lt-bg-0 {
|
||||
background-color: #ef9a9a;
|
||||
@@ -2254,7 +2327,7 @@ ul.tabbar li.active > a {
|
||||
|
||||
.chip > a {
|
||||
display: inline-block;
|
||||
color: #ccc;
|
||||
color: #eee;
|
||||
background-color: #aaa;
|
||||
border-radius: 50%;
|
||||
margin: 0.3rem;
|
||||
@@ -2268,7 +2341,7 @@ ul.tabbar li.active > a {
|
||||
.chip > a:focus,
|
||||
.chip > a:hover {
|
||||
text-decoration: none;
|
||||
color: #ddd;
|
||||
color: #fff;
|
||||
background-color: #bbb;
|
||||
}
|
||||
|
||||
@@ -2471,7 +2544,11 @@ ul.tabbar li.active > a {
|
||||
}
|
||||
|
||||
.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 {
|
||||
@@ -2487,6 +2564,10 @@ ul.tabbar li.active > a {
|
||||
margin-bottom: 0.1rem;
|
||||
}
|
||||
|
||||
.reply-quote img.inline-image::before {
|
||||
content: '';
|
||||
}
|
||||
|
||||
#reply-quote-preview {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -2519,6 +2600,11 @@ ul.tabbar li.active > a {
|
||||
margin-bottom: 0.35rem;
|
||||
}
|
||||
|
||||
.forwarding-quote {
|
||||
font-size: 200%;
|
||||
line-height: 200%;
|
||||
}
|
||||
|
||||
.mention {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+2
-2
@@ -34,8 +34,8 @@
|
||||
<!-- React-Intl for translations -->
|
||||
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/react-intl@5/react-intl.iife.js"></script>
|
||||
<!-- 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@9/firebase-messaging.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@8/firebase-messaging.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
+2
-2
@@ -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-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/firebase@9.4.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-app.js"></script>
|
||||
<script crossorigin="anonymous" src="https://cdn.jsdelivr.net/npm/firebase@8.10.0/firebase-messaging.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<noscript><div id="noscript-message"><div id="noscript-title">Javascript support is required.</div>
|
||||
|
||||
Generated
+2882
-3234
File diff suppressed because it is too large
Load Diff
+4
-4
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "tinode-webapp",
|
||||
"description": "Tinode messenger for the web",
|
||||
"version": "0.18.0-beta1",
|
||||
"version": "0.18.2-beta1",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/tinode/example-react-js.git"
|
||||
@@ -66,11 +66,11 @@
|
||||
"author": "Tinode Authors <info@tinode.co>",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"firebase": "^9.4.0",
|
||||
"firebase": "^8.10.0",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-intl": "^5.20.2",
|
||||
"tinode-sdk": "^0.18.0-beta1"
|
||||
"tinode-sdk": "^0.18.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.14.5",
|
||||
@@ -80,7 +80,7 @@
|
||||
"babel-loader": "^8.2.2",
|
||||
"babel-plugin-formatjs": "^10.2.19",
|
||||
"browserslist": "^4.16.5",
|
||||
"copy-webpack-plugin": "^9.0.0",
|
||||
"copy-webpack-plugin": "^10.0.0",
|
||||
"cssnano": "^5.0.6",
|
||||
"glob": "^7.1.7",
|
||||
"mkdirp": "^1.0.4",
|
||||
|
||||
@@ -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
@@ -96,7 +96,7 @@ export const MESSAGE_PREVIEW_LENGTH = 80;
|
||||
// Length of a quote in quoted reply (for outgoing messages).
|
||||
export const QUOTED_REPLY_LENGTH = 30;
|
||||
// Length of a preview of a forwarded message.
|
||||
export const FORWARDED_PREVIEW_LENGTH = 96;
|
||||
export const FORWARDED_PREVIEW_LENGTH = 84;
|
||||
|
||||
// Link for "Contact Us".
|
||||
export const LINK_CONTACT_US = 'email:support@tinode.co';
|
||||
|
||||
@@ -1353,5 +1353,12 @@
|
||||
"description": "Contact search prompt when forwarding a message.",
|
||||
"missing": 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
@@ -34,13 +34,6 @@
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"action_more": {
|
||||
"translation": "More",
|
||||
"defaultMessage": "More",
|
||||
"description": "Action for showing more content",
|
||||
"missing": false,
|
||||
"obsolete": true
|
||||
},
|
||||
"action_report_chat": {
|
||||
"translation": "Report Conversation",
|
||||
"defaultMessage": "Report Conversation",
|
||||
@@ -461,13 +454,6 @@
|
||||
"missing": 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": {
|
||||
"translation": "Default access mode:",
|
||||
"defaultMessage": "Default access mode:",
|
||||
@@ -1033,13 +1019,6 @@
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"sidepanel_title_acc_general": {
|
||||
"translation": "General",
|
||||
"defaultMessage": "General",
|
||||
"description": "Sidepanel title for TopicCommonView.",
|
||||
"missing": false,
|
||||
"obsolete": true
|
||||
},
|
||||
"sidepanel_title_acc_notifications": {
|
||||
"translation": "Notifications",
|
||||
"defaultMessage": "Notifications",
|
||||
@@ -1047,13 +1026,6 @@
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"sidepanel_title_acc_security": {
|
||||
"translation": "Security",
|
||||
"defaultMessage": "Security",
|
||||
"description": "Sidepanel title for AccSecurityView.",
|
||||
"missing": false,
|
||||
"obsolete": true
|
||||
},
|
||||
"sidepanel_title_acc_support": {
|
||||
"translation": "Support",
|
||||
"defaultMessage": "Support",
|
||||
@@ -1089,13 +1061,6 @@
|
||||
"missing": 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": {
|
||||
"translation": "Sign In",
|
||||
"defaultMessage": "Sign In",
|
||||
@@ -1187,13 +1152,6 @@
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"title_info": {
|
||||
"translation": "Info",
|
||||
"defaultMessage": "Info",
|
||||
"description": "Title for InfoView",
|
||||
"missing": false,
|
||||
"obsolete": true
|
||||
},
|
||||
"title_manage_tags": {
|
||||
"translation": "Manage",
|
||||
"defaultMessage": "Manage",
|
||||
@@ -1299,20 +1257,6 @@
|
||||
"missing": 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": {
|
||||
"translation": "Drag to Adjust",
|
||||
"defaultMessage": "Drag to Adjust",
|
||||
@@ -1409,5 +1353,12 @@
|
||||
"description": "Contact search prompt when forwarding a message.",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"label_new_password": {
|
||||
"translation": "New password",
|
||||
"defaultMessage": "New password",
|
||||
"description": "Label for entering a new password",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1353,5 +1353,12 @@
|
||||
"description": "Contact search prompt when forwarding a message.",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"label_new_password": {
|
||||
"translation": "Nueva contraseña",
|
||||
"defaultMessage": "New password",
|
||||
"description": "Label for entering a new password",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -302,6 +302,10 @@
|
||||
"defaultMessage": "Muted:",
|
||||
"description": "Label for Muting/unmuting the topic"
|
||||
},
|
||||
"label_new_password": {
|
||||
"defaultMessage": "New password",
|
||||
"description": "Label for entering a new password"
|
||||
},
|
||||
"label_other_user": {
|
||||
"defaultMessage": "Other",
|
||||
"description": "Label for the other user when the user is unnamed"
|
||||
|
||||
@@ -1353,5 +1353,12 @@
|
||||
"description": "Contact search prompt when forwarding a message.",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"label_new_password": {
|
||||
"translation": "새 비밀번호",
|
||||
"defaultMessage": "New password",
|
||||
"description": "Label for entering a new password",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1353,5 +1353,12 @@
|
||||
"description": "Contact search prompt when forwarding a message.",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"label_new_password": {
|
||||
"translation": "Parolă Nouă",
|
||||
"defaultMessage": "New password",
|
||||
"description": "Label for entering a new password",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1353,5 +1353,12 @@
|
||||
"description": "Contact search prompt when forwarding a message.",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"label_new_password": {
|
||||
"translation": "Новый пароль",
|
||||
"defaultMessage": "New password",
|
||||
"description": "Label for entering a new password",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1353,5 +1353,12 @@
|
||||
"description": "Contact search prompt when forwarding a message.",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"label_new_password": {
|
||||
"translation": "新密碼",
|
||||
"defaultMessage": "New password",
|
||||
"description": "Label for entering a new password",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1353,5 +1353,12 @@
|
||||
"description": "Contact search prompt when forwarding a message.",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
"label_new_password": {
|
||||
"translation": "新密码",
|
||||
"defaultMessage": "New password",
|
||||
"description": "Label for entering a new password",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
// 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.
|
||||
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.
|
||||
export function makeImageUrl(photo) {
|
||||
if (photo && typeof photo == 'object') {
|
||||
if (photo.ref) {
|
||||
return photo.ref
|
||||
}
|
||||
if (photo.data && photo.type) {
|
||||
const mime = photo.type.startsWith('image/') ? photo.type : ('image/' + photo.type);
|
||||
return 'data:' + mime + ';base64,' + photo.data;
|
||||
}
|
||||
return photo.ref;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
+54
-26
@@ -124,25 +124,31 @@ function quotedImage(data) {
|
||||
// onImagePreview: this.handleImagePreview
|
||||
// onFormButtonClick: this.handleFormButtonClick
|
||||
// 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) {
|
||||
// Unformatted.
|
||||
return values;
|
||||
}
|
||||
|
||||
if (style == 'HD') {
|
||||
// Hidden.
|
||||
return null;
|
||||
}
|
||||
|
||||
let el = Drafty.tagName(style);
|
||||
const attr = Drafty.attrValue(style, data) || {};
|
||||
attr.key = key;
|
||||
switch (style) {
|
||||
case 'BR':
|
||||
values = null;
|
||||
break;
|
||||
case 'HL':
|
||||
// Highlighted text. Assign class name.
|
||||
attr.className = 'highlight';
|
||||
break;
|
||||
case 'HD':
|
||||
el = null;
|
||||
values = null;
|
||||
break;
|
||||
case 'IM':
|
||||
// Additional processing for images
|
||||
el = handleImageData.call(this, el, data, attr);
|
||||
@@ -192,7 +198,7 @@ export function fullFormatter(style, data, values, key) {
|
||||
return values;
|
||||
}
|
||||
return React.createElement(el, attr, values);
|
||||
};
|
||||
}
|
||||
|
||||
// Converts Drafty object into a one-line preview. 'this' is set by the caller.
|
||||
// 'this' must contain:
|
||||
@@ -204,11 +210,6 @@ export function previewFormatter(style, data, values, key) {
|
||||
return values;
|
||||
}
|
||||
|
||||
if (style == 'HD') {
|
||||
// Hidden.
|
||||
return null;
|
||||
}
|
||||
|
||||
let el = Drafty.tagName(style);
|
||||
const attr = { key: key };
|
||||
switch (style) {
|
||||
@@ -256,6 +257,11 @@ export function previewFormatter(style, data, values, key) {
|
||||
el = React.Fragment;
|
||||
values = [<i key="ex" className="material-icons">attachment</i>, this.formatMessage(messages.drafty_attachment)];
|
||||
break;
|
||||
case 'QQ':
|
||||
case 'HD':
|
||||
el = null;
|
||||
values = null;
|
||||
break;
|
||||
default:
|
||||
if (el == '_UNKN') {
|
||||
el = React.Fragment;
|
||||
@@ -292,12 +298,15 @@ function inlineImageAttr(attr, data) {
|
||||
// messages: formatjs messages defined with defineMessages.
|
||||
// authorizeURL: this.props.tinode.authorizeURL
|
||||
// onQuoteClick: this.handleQuoteClick (optional)
|
||||
export function quoteFormatter(style, data, values, key) {
|
||||
if (['BR', 'EX', 'IM', 'MN', 'QQ'].includes(style)) {
|
||||
function quoteFormatter(style, data, values, key) {
|
||||
if (['BR', 'EX', 'IM', 'MN'].includes(style)) {
|
||||
let el = Drafty.tagName(style);
|
||||
let attr = Drafty.attrValue(style, data) || {};
|
||||
attr.key = key;
|
||||
switch(style) {
|
||||
case 'BR':
|
||||
values = null;
|
||||
break;
|
||||
case 'IM':
|
||||
attr = inlineImageAttr.call(this, attr, data);
|
||||
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);
|
||||
}
|
||||
break;
|
||||
case 'QQ':
|
||||
attr.className = 'reply-quote';
|
||||
attr.onClick = this.onQuoteClick;
|
||||
break;
|
||||
case 'EX':
|
||||
let fname;
|
||||
if (data) {
|
||||
@@ -344,15 +349,21 @@ function quoteImage(data) {
|
||||
// Get the blob from the image data.
|
||||
if (data.val) {
|
||||
const blob = base64ToBlob(data.val, data.mime);
|
||||
promise = blob ? Promise.resolve(blob) : Promise.reject(new Error("Invalid image"));
|
||||
} else {
|
||||
promise = fetch(this.authorizeURL(sanitizeImageUrl(data.ref))).then(evt => {
|
||||
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 {
|
||||
throw new Error("Missing image data");
|
||||
}
|
||||
|
||||
// Scale the blob.
|
||||
@@ -384,12 +395,29 @@ function quoteImage(data) {
|
||||
}
|
||||
|
||||
// Create a preview of a reply.
|
||||
export function replyFormatter(style, data, values, key) {
|
||||
if (style != 'IM') {
|
||||
return quoteFormatter.call(this, style, data, values, key);
|
||||
}
|
||||
export function replyFormatter(style, data, values, key, stack) {
|
||||
if (style == 'IM') {
|
||||
const attr = inlineImageAttr.call(this, {key: key}, data);
|
||||
attr.whenDone = cancelablePromise(quoteImage.call(this, 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);
|
||||
}
|
||||
return quoteFormatter.call(this, style, data, values, key);
|
||||
}
|
||||
|
||||
+6
-2
@@ -183,10 +183,14 @@ export function deliveryMarker(received) {
|
||||
}
|
||||
|
||||
// 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) {
|
||||
let hasCanceled = false;
|
||||
|
||||
const wrappedPromise = new Promise((resolve, reject) => {
|
||||
const wrappedPromise = promise instanceof Error ?
|
||||
Promise.reject(promise) :
|
||||
new Promise((resolve, reject) => {
|
||||
promise.then(
|
||||
result => hasCanceled ? reject({isCanceled: true}) : resolve(result),
|
||||
error => hasCanceled ? reject({isCanceled: true}) : reject(error)
|
||||
@@ -195,7 +199,7 @@ export function cancelablePromise(promise) {
|
||||
|
||||
return {
|
||||
promise: wrappedPromise,
|
||||
cancel( ) {
|
||||
cancel() {
|
||||
hasCanceled = true;
|
||||
},
|
||||
};
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,3 +1,3 @@
|
||||
// This is a generated file. Don't edit.
|
||||
|
||||
export const PACKAGE_VERSION = "0.18.0-beta1";
|
||||
export const PACKAGE_VERSION = "0.18.2-beta1";
|
||||
|
||||
+73
-27
@@ -14,19 +14,20 @@ import Invitation from '../widgets/invitation.jsx';
|
||||
import LetterTile from '../widgets/letter-tile.jsx';
|
||||
import LoadSpinner from '../widgets/load-spinner.jsx';
|
||||
import LogoView from './logo-view.jsx';
|
||||
import MetaMessage from '../widgets/meta-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,
|
||||
READ_DELAY, QUOTED_REPLY_LENGTH } from '../config.js';
|
||||
import { SUPPORTED_IMAGE_FORMATS, blobToBase64, base64ToBlob, filePasted, fileToBase64,
|
||||
imageScaled, makeImageUrl } from '../lib/blob-helpers.js';
|
||||
import { blobToBase64, fileToBase64, imageScaled, makeImageUrl } from '../lib/blob-helpers.js';
|
||||
import HashNavigation from '../lib/navigation.js';
|
||||
import { bytesToHumanSize, shortDateFormat, letterTileColorId } from '../lib/strformat.js';
|
||||
import { sanitizeImageUrl } from '../lib/utils.js';
|
||||
import { bytesToHumanSize, shortDateFormat } from '../lib/strformat.js';
|
||||
|
||||
// Run timer with this frequency (ms) for checking notification queue.
|
||||
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({
|
||||
online_now: {
|
||||
@@ -110,6 +111,7 @@ class MessagesView extends React.Component {
|
||||
this.handleCancelUpload = this.handleCancelUpload.bind(this);
|
||||
this.postReadNotification = this.postReadNotification.bind(this);
|
||||
this.clearNotificationQueue = this.clearNotificationQueue.bind(this);
|
||||
this.goToLatestMessage = this.goToLatestMessage.bind(this);
|
||||
|
||||
this.handlePickReply = this.handlePickReply.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
|
||||
// or vertical shrinking.
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (this.messagesScroller) {
|
||||
if (prevState.topic != this.state.topic || prevState.messageCount != this.state.messageCount) {
|
||||
// New message
|
||||
this.messagesScroller.scrollTop = this.messagesScroller.scrollHeight - this.state.scrollPosition;
|
||||
} else if (prevProps.viewportHeight > this.props.viewportHeight) {
|
||||
// Componet changed height.
|
||||
this.messagesScroller.scrollTop += prevProps.viewportHeight - this.props.viewportHeight;
|
||||
if (this.messagesScroller &&
|
||||
(prevState.topic != this.state.topic || prevState.messageCount != this.state.messageCount)) {
|
||||
// New message.
|
||||
if (this.state.scrollPosition < SHOW_GO_TO_LAST_DIST) {
|
||||
this.messagesScroller.scrollTop = this.messagesScroller.scrollHeight -
|
||||
this.state.scrollPosition -
|
||||
this.messagesScroller.offsetHeight;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,7 +212,8 @@ class MessagesView extends React.Component {
|
||||
fetchingMessages: false,
|
||||
peerMessagingDisabled: false,
|
||||
channel: false,
|
||||
reply: null
|
||||
reply: null,
|
||||
showGoToLastButton: false
|
||||
};
|
||||
} else if (nextProps.topic != prevState.topic) {
|
||||
const topic = nextProps.tinode.getTopic(nextProps.topic);
|
||||
@@ -221,7 +224,6 @@ class MessagesView extends React.Component {
|
||||
const preview = nextProps.forwardMessage.preview;
|
||||
reply = {
|
||||
content: preview,
|
||||
forwarded: nextProps.forwardMessage.head.forwarded,
|
||||
seq: null
|
||||
};
|
||||
}
|
||||
@@ -234,7 +236,8 @@ class MessagesView extends React.Component {
|
||||
typingIndicator: false,
|
||||
scrollPosition: 0,
|
||||
fetchingMessages: false,
|
||||
reply: reply
|
||||
reply: reply,
|
||||
showGoToLastButton: false
|
||||
};
|
||||
|
||||
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) {
|
||||
if (node) {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
if (desc.public) {
|
||||
this.setState({
|
||||
@@ -557,8 +573,12 @@ class MessagesView extends React.Component {
|
||||
// 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.
|
||||
if (topic.isNewMessage(msg.seq)) {
|
||||
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
|
||||
// automatically assumed to be read and recived.
|
||||
@@ -693,7 +713,7 @@ class MessagesView extends React.Component {
|
||||
if (typeof msg == 'string') {
|
||||
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.props.sendMessage(msg, uploadCompletionPromise, uploader, head);
|
||||
@@ -847,18 +867,21 @@ class MessagesView extends React.Component {
|
||||
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});
|
||||
|
||||
if (!seq || !content) {
|
||||
return;
|
||||
}
|
||||
|
||||
content = forwarded ?
|
||||
Drafty.forwardedContent(content) :
|
||||
typeof content == 'string' ? Drafty.init(content) : content;
|
||||
content = typeof content == 'string' ? Drafty.init(content) : content;
|
||||
if (Drafty.isValid(content)) {
|
||||
content = Drafty.preview(content, QUOTED_REPLY_LENGTH, undefined, !forwarded);
|
||||
content = Drafty.replyContent(content, QUOTED_REPLY_LENGTH);
|
||||
} else {
|
||||
// /!\ invalid content.
|
||||
content = Drafty.append(Drafty.init('\u26A0 '),
|
||||
@@ -949,9 +972,11 @@ class MessagesView extends React.Component {
|
||||
icon_badges.push({icon: 'dangerous', color: 'badge-inv'});
|
||||
}
|
||||
}
|
||||
let messageNodes = [];
|
||||
|
||||
const messageNodes = [];
|
||||
let previousFrom = null;
|
||||
let chatBoxClass = null;
|
||||
const dateFmt = new Intl.DateTimeFormat(this.props.intl.locale);
|
||||
topic.messages((msg, prev, next, i) => {
|
||||
let nextFrom = next ? (next.from || 'chan') : null;
|
||||
|
||||
@@ -986,12 +1011,28 @@ class MessagesView extends React.Component {
|
||||
replyToSeq = null;
|
||||
}
|
||||
|
||||
// This message was sent on a different date than the previous.
|
||||
if (!prev || new Date(prev.ts).toDateString() != new Date(msg.ts).toDateString()) {
|
||||
messageNodes.push(
|
||||
<MetaMessage
|
||||
date={dateFmt.format(msg.ts)}
|
||||
locale={this.props.intl.locale}
|
||||
key={'date-' + msg.seq} />
|
||||
);
|
||||
}
|
||||
|
||||
if (msg.hi) {
|
||||
// Deleted message.
|
||||
messageNodes.push(
|
||||
<MetaMessage
|
||||
deleted={true}
|
||||
key={msg.seq} />
|
||||
);
|
||||
} else {
|
||||
messageNodes.push(
|
||||
<ChatMessage
|
||||
tinode={this.props.tinode}
|
||||
content={msg.content}
|
||||
forwarded={msg.head ? msg.head.forwarded : null}
|
||||
deleted={msg.hi}
|
||||
mimeType={msg.head ? msg.head.mime : null}
|
||||
timestamp={msg.ts}
|
||||
response={isReply}
|
||||
@@ -1017,6 +1058,7 @@ class MessagesView extends React.Component {
|
||||
userIsWriter={this.state.isWriter}
|
||||
key={msg.seq} />
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
let lastSeen = null;
|
||||
@@ -1082,6 +1124,10 @@ class MessagesView extends React.Component {
|
||||
: null}
|
||||
<LoadSpinner show={this.state.fetchingMessages} />
|
||||
<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}>
|
||||
<ul id="scroller" className={chatBoxClass}>
|
||||
{messageNodes}
|
||||
@@ -1112,7 +1158,7 @@ class MessagesView extends React.Component {
|
||||
<SendMessage
|
||||
tinode={this.props.tinode}
|
||||
noInput={!!this.props.forwardMessage}
|
||||
disabled={!this.state.isWriter}
|
||||
disabled={!this.state.isWriter || topic.deleted}
|
||||
onKeyPress={this.sendKeyPress}
|
||||
onSendMessage={this.sendMessage}
|
||||
onAttachFile={this.props.forwardMessage ? null : this.handleAttachFile}
|
||||
|
||||
@@ -35,7 +35,7 @@ class NewTopicView extends React.Component {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
tabSelected: 'p2p',
|
||||
tabSelected: 'find',
|
||||
searchQuery: null
|
||||
};
|
||||
|
||||
@@ -62,7 +62,7 @@ class NewTopicView extends React.Component {
|
||||
}
|
||||
|
||||
handleContactSelected(uid) {
|
||||
if (this.state.tabSelected == 'p2p') {
|
||||
if (this.state.tabSelected == 'find') {
|
||||
HashNavigation.navigateTo(HashNavigation.removeUrlParam(window.location.hash, 'tab'));
|
||||
this.props.onCreateTopic(uid);
|
||||
}
|
||||
@@ -87,8 +87,8 @@ class NewTopicView extends React.Component {
|
||||
return (
|
||||
<div className="flex-column">
|
||||
<ul className="tabbar">
|
||||
<li className={this.state.tabSelected === "p2p" ? "active" : null}>
|
||||
<a href="#" data-id="p2p" onClick={this.handleTabClick}>
|
||||
<li className={this.state.tabSelected === "find" ? "active" : null}>
|
||||
<a href="#" data-id="find" onClick={this.handleTabClick}>
|
||||
<FormattedMessage id="tabtitle_find_user" defaultMessage="find"
|
||||
description="Tab title Find" />
|
||||
</a>
|
||||
@@ -116,7 +116,6 @@ class NewTopicView extends React.Component {
|
||||
onError={this.props.onError} /> :
|
||||
<div className="flex-column">
|
||||
<SearchContacts
|
||||
type="p2p"
|
||||
placeholder={search_placeholder}
|
||||
onSearchContacts={this.handleSearchContacts} />
|
||||
<ContactList
|
||||
|
||||
@@ -53,15 +53,21 @@ export default class PasswordResetView extends React.PureComponent {
|
||||
return (
|
||||
<form id="password-reset-form" onSubmit={this.handleSubmit}>
|
||||
{(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"
|
||||
description="Placeholder for entering new password">{
|
||||
(placeholder) => <VisiblePassword
|
||||
id="new-password"
|
||||
placeholder={placeholder}
|
||||
autoComplete="new-password"
|
||||
value={this.state.password}
|
||||
required={true} autoFocus={true}
|
||||
onChange={this.handlePasswordChange} />
|
||||
}</FormattedMessage>
|
||||
}</FormattedMessage></>
|
||||
: this.state.sent ?
|
||||
<>
|
||||
<br/>
|
||||
|
||||
@@ -16,7 +16,7 @@ import MessagesView from './messages-view.jsx';
|
||||
import SidepanelView from './sidepanel-view.jsx';
|
||||
|
||||
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 { base64ReEncode, makeImageUrl } from '../lib/blob-helpers.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.
|
||||
if (!['cred', 'reset', 'register'].includes(parsedNav.path[0])) {
|
||||
// Save possible topic name.
|
||||
this.setState({requestedTopic: parsedNav.path[1]});
|
||||
HashNavigation.navigateTo('');
|
||||
}
|
||||
|
||||
this.readTimer = null;
|
||||
this.readTimerCallback = null;
|
||||
@@ -1199,6 +1202,8 @@ class TinodeWeb extends React.Component {
|
||||
delete parsed.params.code;
|
||||
delete parsed.params.method;
|
||||
delete parsed.params.tab;
|
||||
delete parsed.params.scheme;
|
||||
delete parsed.params.token;
|
||||
}
|
||||
HashNavigation.navigateTo(HashNavigation.composeUrlHash(parsed.path, parsed.params));
|
||||
this.setState({errorText: '', errorLevel: null});
|
||||
@@ -1230,9 +1235,11 @@ class TinodeWeb extends React.Component {
|
||||
params.desc = {defacs: {auth: DEFAULT_P2P_ACCESS_MODE}};
|
||||
} else {
|
||||
topicName = topicName || this.tinode.newGroupTopicName(isChannel);
|
||||
if (newTopicParams) {
|
||||
params.desc = {public: newTopicParams.public, private: {comment: newTopicParams.private}};
|
||||
params.tags = newTopicParams.tags;
|
||||
}
|
||||
}
|
||||
params._topicName = topicName;
|
||||
this.setState({newTopicParams: params}, () => {this.handleTopicSelected(topicName)});
|
||||
}
|
||||
@@ -1443,22 +1450,22 @@ class TinodeWeb extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
handleShowForwardDialog(params) {
|
||||
if (this.state.sidePanelSelected == 'newtpk') {
|
||||
// If the Find panel is active, close it.
|
||||
this.handleSidepanelCancel();
|
||||
}
|
||||
const header = '➦ ' + params.userName;
|
||||
const content = params.forwarded ?
|
||||
Tinode.Drafty.forwardedContent(params.content) :
|
||||
typeof params.content == 'string' ?
|
||||
Tinode.Drafty.init(params.content) : params.content;
|
||||
const content = typeof params.content == 'string' ?
|
||||
Tinode.Drafty.init(params.content) : Tinode.Drafty.forwardedContent(params.content);
|
||||
const preview = Tinode.Drafty.preview(content, FORWARDED_PREVIEW_LENGTH,
|
||||
undefined, params.forwarded != null);
|
||||
const msg = Tinode.Drafty.append(
|
||||
Tinode.Drafty.appendLineBreak(
|
||||
Tinode.Drafty.mention(header, params.userFrom)),
|
||||
content);
|
||||
|
||||
const msgPreview = Tinode.Drafty.quote(header, params.userFrom, preview);
|
||||
const head = {
|
||||
forwarded: params.topicName + ':' + params.seq
|
||||
@@ -1611,6 +1618,9 @@ class TinodeWeb extends React.Component {
|
||||
.then(() => {
|
||||
return this.tinode.updateAccountBasic(null, null, newPassword, {token: token});
|
||||
})
|
||||
.then(() => {
|
||||
HashNavigation.navigateTo('');
|
||||
})
|
||||
.catch((err) => {
|
||||
// Socket error
|
||||
this.handleError(err.message, 'err');
|
||||
|
||||
@@ -34,7 +34,7 @@ export default class AvatarUpload extends React.Component {
|
||||
render() {
|
||||
// Randomize id value in case more than one AvatarUpload is shown
|
||||
// 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' : '');
|
||||
return (
|
||||
<div className={className}>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// Single message, sent or received.
|
||||
// Single chat message bubble, sent or received.
|
||||
|
||||
import React from 'react';
|
||||
import { FormattedMessage, injectIntl } from 'react-intl';
|
||||
import { Drafty } from 'tinode-sdk';
|
||||
@@ -7,8 +8,8 @@ import Attachment from './attachment.jsx';
|
||||
import LetterTile from './letter-tile.jsx';
|
||||
import ReceivedMarker from './received-marker.jsx'
|
||||
|
||||
import { fullFormatter, quoteFormatter } from '../lib/formatters.js';
|
||||
import { sanitizeImageUrl, sanitizeUrl } from '../lib/utils.js';
|
||||
import { fullFormatter } from '../lib/formatters.js';
|
||||
import { sanitizeUrl } from '../lib/utils.js';
|
||||
|
||||
class BaseChatMessage extends React.PureComponent {
|
||||
constructor(props) {
|
||||
@@ -29,7 +30,6 @@ class BaseChatMessage extends React.PureComponent {
|
||||
this.handleQuoteClick = this.handleQuoteClick.bind(this);
|
||||
|
||||
this.formatterContext = {
|
||||
getFormatter: (tp) => { return tp == 'QQ' ? quoteFormatter : null; },
|
||||
formatMessage: props.intl.formatMessage.bind(props.intl),
|
||||
viewportWidth: props.viewportWidth,
|
||||
authorizeURL: props.tinode.authorizeURL.bind(props.tinode),
|
||||
@@ -87,7 +87,6 @@ class BaseChatMessage extends React.PureComponent {
|
||||
content: this.props.content,
|
||||
userFrom: this.props.userFrom,
|
||||
userName: this.props.userName,
|
||||
forwarded: this.props.forwarded,
|
||||
y: e.pageY,
|
||||
x: e.pageX,
|
||||
pickReply: this.props.pickReply
|
||||
@@ -112,11 +111,10 @@ class BaseChatMessage extends React.PureComponent {
|
||||
}
|
||||
|
||||
render() {
|
||||
const sideClass = this.props.deleted ? 'center' :
|
||||
(this.props.sequence + ' ' + (this.props.response ? 'left' : 'right'));
|
||||
const sideClass = this.props.sequence + ' ' + (this.props.response ? 'left' : 'right');
|
||||
const bubbleClass = (this.props.sequence == 'single' || this.props.sequence == 'last') ? 'bubble tip' : 'bubble';
|
||||
const avatar = this.props.deleted ? null : (this.props.userAvatar || true);
|
||||
const fullDisplay = (this.props.isGroup && this.props.response && !this.props.deleted &&
|
||||
const avatar = this.props.userAvatar || true;
|
||||
const fullDisplay = (this.props.isGroup && this.props.response &&
|
||||
(this.props.sequence == 'single' || this.props.sequence == 'last'));
|
||||
|
||||
let content = this.props.content;
|
||||
@@ -142,12 +140,6 @@ class BaseChatMessage extends React.PureComponent {
|
||||
}, this);
|
||||
const tree = Drafty.format(content, fullFormatter, this.formatterContext);
|
||||
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') {
|
||||
content = <><i className="material-icons gray">warning_amber</i> <i className="gray">
|
||||
<FormattedMessage id="invalid_content"
|
||||
@@ -180,13 +172,12 @@ class BaseChatMessage extends React.PureComponent {
|
||||
received={this.props.received} />
|
||||
: null}
|
||||
</div>
|
||||
{this.props.deleted || !this.props.showContextMenu ?
|
||||
null :
|
||||
{this.props.showContextMenu ?
|
||||
<span className="menuTrigger">
|
||||
<a href="#" onClick={this.handleContextClick}>
|
||||
<i className="material-icons">expand_more</i>
|
||||
</a>
|
||||
</span>
|
||||
</span> : null
|
||||
}
|
||||
</div>
|
||||
{fullDisplay ?
|
||||
|
||||
@@ -84,11 +84,14 @@ class ContactList extends React.Component {
|
||||
if (msg) {
|
||||
forwarded = msg.head ? msg.head.forwarded : null;
|
||||
deliveryStatus = msg._status || c.msgStatus(msg, true);
|
||||
if (msg.content) {
|
||||
preview = typeof msg.content == 'string' ?
|
||||
msg.content.substr(0, MESSAGE_PREVIEW_LENGTH) :
|
||||
Drafty.preview(msg.content, MESSAGE_PREVIEW_LENGTH, undefined, forwarded != null);
|
||||
Drafty.preview(msg.content, MESSAGE_PREVIEW_LENGTH);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
contactNodes.push(
|
||||
<Contact
|
||||
tinode={this.props.tinode}
|
||||
@@ -112,6 +115,7 @@ class ContactList extends React.Component {
|
||||
isVerified={c.trusted && c.trusted.verified}
|
||||
isStaff={c.trusted && c.trusted.staff}
|
||||
isDangerous={c.trusted && c.trusted.danger}
|
||||
deleted={c.deleted}
|
||||
item={key}
|
||||
index={contactNodes.length}
|
||||
key={key} />
|
||||
|
||||
+18
-14
@@ -4,7 +4,6 @@ import { FormattedMessage, injectIntl } from 'react-intl';
|
||||
|
||||
import ContactBadges from './contact-badges.jsx';
|
||||
import LetterTile from './letter-tile.jsx';
|
||||
import ReceivedMarker from './received-marker.jsx'
|
||||
import UnreadBadge from './unread-badge.jsx';
|
||||
|
||||
import { Drafty } from 'tinode-sdk';
|
||||
@@ -69,17 +68,20 @@ class Contact extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
const subtitle = this.props.preview ?
|
||||
(typeof this.props.preview == 'string' ? this.props.preview :
|
||||
Drafty.isValid(this.props.preview) ?
|
||||
React.createElement(React.Fragment, null, Drafty.format(this.props.preview, previewFormatter,
|
||||
{formatMessage: this.props.intl.formatMessage})) :
|
||||
// The this.props.preview contains alreay shortened Drafty or string.
|
||||
let preview;
|
||||
if (typeof this.props.preview == 'string') {
|
||||
preview = this.props.preview;
|
||||
} 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">
|
||||
<FormattedMessage id="invalid_content"
|
||||
defaultMessage="invalid content" description="Shown when the message is unreadable" /></i>
|
||||
</>
|
||||
) :
|
||||
this.props.comment;
|
||||
</>;
|
||||
}
|
||||
|
||||
const icon = deliveryMarker(this.props.received);
|
||||
const marker = icon ? <i className={'material-icons small space-right' +
|
||||
@@ -92,11 +94,13 @@ class Contact extends React.Component {
|
||||
tinode={this.props.tinode}
|
||||
avatar={avatar}
|
||||
title={this.props.title}
|
||||
topic={this.props.item} />
|
||||
{this.props.showOnline ? <span className={online} /> :
|
||||
(this.props.showCheckmark && this.props.selected ?
|
||||
topic={this.props.item}
|
||||
disabled={this.props.deleted} />
|
||||
{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>
|
||||
: null)}
|
||||
: null}
|
||||
</div>
|
||||
<div className="text-box">
|
||||
<div><span className="contact-title">{title}</span>
|
||||
@@ -105,7 +109,7 @@ class Contact extends React.Component {
|
||||
</div>
|
||||
{this.props.showMode ?
|
||||
<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>
|
||||
{this.props.showContextMenu ?
|
||||
<span className="menuTrigger">
|
||||
|
||||
@@ -434,7 +434,7 @@ class ContextMenu extends React.Component {
|
||||
}
|
||||
|
||||
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() {
|
||||
|
||||
@@ -55,7 +55,7 @@ export default class ImagePreview extends React.PureComponent {
|
||||
<div id="image-preview">
|
||||
<div id="image-preview-caption-panel">
|
||||
{!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
|
||||
id="download_action" defaultMessage="download" description="Call to action [download]" />
|
||||
</a>
|
||||
|
||||
@@ -31,6 +31,7 @@ export default class LazyImage extends React.PureComponent {
|
||||
if (prevProps.whenDone != this.props.whenDone) {
|
||||
this.setState({src: 'img/placeholder.png', style: {...this.state.style, padding: '4px'}});
|
||||
this.props.whenDone
|
||||
.promise
|
||||
.then(data => this.setState({src: data.src, style: {...this.state.style, padding: 0}}))
|
||||
.catch(() => this.setState({src: 'img/broken_image.png'}));
|
||||
}
|
||||
|
||||
@@ -9,21 +9,21 @@ export default class LetterTile extends React.PureComponent {
|
||||
render() {
|
||||
let avatar;
|
||||
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);
|
||||
if (this.props.topic && this.props.title && this.props.title.trim()) {
|
||||
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>)
|
||||
} else {
|
||||
const className = 'material-icons ' + iconColor;
|
||||
avatar = isGroup ?
|
||||
<i className={className}>group</i> : <i className={className}>person</i>;
|
||||
const className = 'material-icons ' + iconColor + (this.props.disabled ? ' disabled' : '');
|
||||
avatar = isGroup ? <i className={className}>group</i> : <i className={className}>person</i>;
|
||||
}
|
||||
} else if (this.props.avatar) {
|
||||
const url = this.props.tinode.authorizeURL(sanitizeImageUrl(this.props.avatar));
|
||||
// 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"}} />;
|
||||
} else {
|
||||
avatar = null;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -4,7 +4,6 @@ import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
import Tinode from 'tinode-sdk';
|
||||
|
||||
import { shortDateFormat } from '../lib/strformat.js';
|
||||
import { deliveryMarker } from '../lib/utils.js';
|
||||
|
||||
const messages = defineMessages({
|
||||
@@ -29,7 +28,7 @@ class ReceivedMarker extends React.PureComponent {
|
||||
} else if (this.props.received == Tinode.MESSAGE_STATUS_FAILED) {
|
||||
timestamp = formatMessage(messages.message_sending_failed);
|
||||
} 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);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import Tinode from 'tinode-sdk';
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Drafty } from 'tinode-sdk';
|
||||
|
||||
import { KEYPRESS_DELAY } from '../config.js';
|
||||
import { filePasted } from '../lib/blob-helpers.js';
|
||||
import { quoteFormatter, replyFormatter } from '../lib/formatters.js';
|
||||
import { replyFormatter } from '../lib/formatters.js';
|
||||
|
||||
const messages = defineMessages({
|
||||
messaging_disabled: {
|
||||
@@ -40,6 +40,7 @@ class SendMessage extends React.PureComponent {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
quote: null,
|
||||
message: '',
|
||||
// Make initial keypress time as if it happened 5001 milliseconds in the past.
|
||||
keypressTimestamp: new Date().getTime() - KEYPRESS_DELAY - 1
|
||||
|
||||
@@ -10,8 +10,8 @@ import AvatarUpload from './avatar-upload.jsx';
|
||||
import InPlaceEdit from './in-place-edit.jsx';
|
||||
import TagManager from './tag-manager.jsx';
|
||||
|
||||
import { AVATAR_SIZE, MAX_AVATAR_BYTES, MAX_EXTERN_ATTACHMENT_SIZE,
|
||||
MAX_INBAND_ATTACHMENT_SIZE, MAX_TITLE_LENGTH, MAX_TOPIC_DESCRIPTION_LENGTH } from '../config.js';
|
||||
import { AVATAR_SIZE, MAX_AVATAR_BYTES, MAX_EXTERN_ATTACHMENT_SIZE, MAX_TITLE_LENGTH,
|
||||
MAX_TOPIC_DESCRIPTION_LENGTH } from '../config.js';
|
||||
import { imageScaled, blobToBase64, makeImageUrl } from '../lib/blob-helpers.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.
|
||||
uploadAvatar(mime, blob, width, height) {
|
||||
const readyToUpload = (image) => {
|
||||
let {mime, blob, width, height} = image;
|
||||
let {mime, blob} = image;
|
||||
if (blob.size > MAX_AVATAR_BYTES) {
|
||||
// Too large to send inband - uploading out of band and sending as a link.
|
||||
const uploader = this.props.tinode.getLargeFileHelper();
|
||||
|
||||
+296
-149
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
+3345
-5155
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -1,3 +1,3 @@
|
||||
// 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 |
Reference in New Issue
Block a user