+75
-158
@@ -317,36 +317,21 @@ li .menuTrigger {
|
||||
position: absolute;
|
||||
top: 0.25rem;
|
||||
right: 0.25rem;
|
||||
width: 1.5rem;
|
||||
height: 1.2rem;
|
||||
line-height: 1.2rem;
|
||||
border-radius: 0.6rem;
|
||||
width: 1.4rem;
|
||||
height: 1.4rem;
|
||||
line-height: 1.4rem;
|
||||
border-radius: 0.7rem;
|
||||
text-align: center;
|
||||
opacity: 0;
|
||||
background-color: inherit;
|
||||
}
|
||||
li:hover .menuTrigger {
|
||||
opacity: 1.0;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
li:hover .menuTrigger a {
|
||||
color: #757575;
|
||||
}
|
||||
|
||||
ul.contact-box .menuTrigger {
|
||||
background-image: radial-gradient(#eceff1 40%, transparent 75%);
|
||||
}
|
||||
|
||||
li.left:hover .menuTrigger {
|
||||
background-image: radial-gradient(#c5e1a5 40%, transparent 75%);
|
||||
}
|
||||
li.right:hover .menuTrigger {
|
||||
background-image: radial-gradient(#fafafa 40%, transparent 75%);
|
||||
}
|
||||
|
||||
li.selected .menuTrigger {
|
||||
background-image: radial-gradient(#cfd8dc 40%, transparent 75%);
|
||||
}
|
||||
|
||||
/* END popup/dropdown menu */
|
||||
|
||||
/* Begin Alert: modal window with message and buttons. */
|
||||
@@ -815,7 +800,7 @@ i.search {
|
||||
}
|
||||
|
||||
.info-box > span {
|
||||
line-height: 1.5rem;
|
||||
line-height: 1.35rem;
|
||||
}
|
||||
|
||||
.info-box .cancel {
|
||||
@@ -1533,7 +1518,6 @@ img.channel {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
align-items: flex-end;
|
||||
transition: background-color 1s ease-out;
|
||||
}
|
||||
|
||||
.chat-box .right {
|
||||
@@ -1546,7 +1530,7 @@ img.channel {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.chat-box .bubble {
|
||||
.bubble {
|
||||
position: relative;
|
||||
display: flex;
|
||||
max-width: 36rem;
|
||||
@@ -1557,6 +1541,8 @@ img.channel {
|
||||
text-align: left;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 1px 0.5px rgba(0, 0, 0, 0.12);
|
||||
transition: ease-out background-color 0.5s;
|
||||
transition-delay: 0.3s;
|
||||
}
|
||||
|
||||
.chat-box li.single {
|
||||
@@ -1579,55 +1565,79 @@ img.channel {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.chat-box .left .bubble {
|
||||
.left .bubble {
|
||||
background-color: #c5e1a5;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.chat-box .right .bubble {
|
||||
.left.flash .bubble {
|
||||
background-color: #94A97C;
|
||||
}
|
||||
|
||||
.left .bubble.tip {
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.right .bubble {
|
||||
background-color: #fafafa;
|
||||
margin-left: 1.75rem;
|
||||
}
|
||||
|
||||
.chat-box .center .bubble {
|
||||
.right.flash .bubble {
|
||||
background-color: #ccc;
|
||||
}
|
||||
|
||||
.right .bubble.tip {
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.center .bubble {
|
||||
background-color: #e3f2fd;
|
||||
padding: 0.75rem 0.75rem 0.5rem 0.75rem;
|
||||
}
|
||||
|
||||
/* Left triangle placed bottom left flush. */
|
||||
.chat-box .left .tip::before {
|
||||
.left .bubble.tip::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
right: auto;
|
||||
left: -.8rem;
|
||||
top: auto;
|
||||
width: 0.85rem;
|
||||
height: 0.85rem;
|
||||
left: -0.8rem;
|
||||
bottom: 0;
|
||||
border: 0.62rem solid #c5e1a5;
|
||||
color: #c5e1a5;
|
||||
border-top-color: transparent;
|
||||
border-left-color: transparent;
|
||||
box-shadow: 0 1.6px 0.5px -0.8px rgba(0, 0, 0, 0.12);
|
||||
background-color: #c5e1a5;
|
||||
clip-path: polygon(100% 0, 0 110%, 100% 110%);
|
||||
-webkit-clip-path: polygon(100% 0, 0 110%, 100% 110%);
|
||||
box-shadow: 0 1px 0.5px rgb(0 0 0 / 12%);
|
||||
border-bottom-left-radius: 0.3rem;
|
||||
-moz-transition: ease-out background-color 0.5s;
|
||||
transition: ease-out background-color 0.5s;
|
||||
transition-delay: 0.3s;
|
||||
}
|
||||
|
||||
.left.flash .bubble.tip::before {
|
||||
background-color: #94A97C;
|
||||
}
|
||||
|
||||
/* Right triangle placed bottom right flush. */
|
||||
.chat-box .right .tip::before {
|
||||
.right .bubble.tip::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 0;
|
||||
height: 0;
|
||||
left: auto;
|
||||
right: -.8rem;
|
||||
top: auto;
|
||||
width: 0.85rem;
|
||||
height: 0.85rem;
|
||||
left: 99.8%;
|
||||
bottom: 0;
|
||||
border: 0.62rem solid #fafafa;
|
||||
color: #fafafa;
|
||||
border-top-color: transparent;
|
||||
border-right-color: transparent;
|
||||
box-shadow: 0 1.6px 0.5px -0.8px rgba(0, 0, 0, 0.12);
|
||||
background-color: #fafafa;
|
||||
clip-path: polygon(0 0, 100% 110%, 0 110%);
|
||||
-webkit-clip-path: polygon(0 0, 100% 110%, 0 110%);
|
||||
box-shadow: 0 1px 0.5px rgb(0 0 0 / 12%);
|
||||
border-bottom-right-radius: 0.3rem;
|
||||
-moz-transition: ease-out background-color 0.5s;
|
||||
transition: ease-out background-color 0.5s;
|
||||
transition-delay: 0.3s;
|
||||
}
|
||||
|
||||
.right.flash .bubble.tip::before {
|
||||
background-color: #ccc;
|
||||
}
|
||||
|
||||
.chat-box .avatar-box {
|
||||
@@ -1642,11 +1652,11 @@ img.channel {
|
||||
}
|
||||
|
||||
/* Message text + timestamp and delivery marker */
|
||||
.chat-box .bubble div.content-meta {
|
||||
.bubble div.content-meta {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.chat-box .bubble div.message-content {
|
||||
.bubble div.message-content {
|
||||
position: relative;
|
||||
color: #212121;
|
||||
margin: 0;
|
||||
@@ -1658,7 +1668,7 @@ img.channel {
|
||||
|
||||
/* The timestamp is a floating element. Need to ensure div.message-content is expanded
|
||||
* to accomodate it. It's usually called clearfix. */
|
||||
.chat-box .bubble div.message-content:after {
|
||||
.bubble div.message-content:after {
|
||||
content: ".";
|
||||
display: block;
|
||||
height: 0;
|
||||
@@ -1679,6 +1689,7 @@ img.channel {
|
||||
top: 0.35rem;
|
||||
float: right;
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* The name of the person who sent the message
|
||||
@@ -2156,106 +2167,6 @@ ul.tabbar li.active > a {
|
||||
color: #D84315!important;
|
||||
}
|
||||
|
||||
/* Colors of user mentions */
|
||||
.mn-light-color0 {
|
||||
color: #ef9a9a;
|
||||
}
|
||||
.mn-light-color1 {
|
||||
color: #90caf9;
|
||||
}
|
||||
.mn-light-color2 {
|
||||
color: #b0bec5;
|
||||
}
|
||||
.mn-light-color3 {
|
||||
color: #b39ddb;
|
||||
}
|
||||
.mn-light-color4 {
|
||||
color: #ffab91;
|
||||
}
|
||||
.mn-light-color5 {
|
||||
color: #a5d6a7;
|
||||
}
|
||||
.mn-light-color6 {
|
||||
color: #eeeeee;
|
||||
}
|
||||
.mn-light-color7 {
|
||||
color: #e6ee9c;
|
||||
}
|
||||
.mn-light-color8 {
|
||||
color: #c5e1a5;
|
||||
}
|
||||
.mn-light-color9 {
|
||||
color: #f0e57d;
|
||||
}
|
||||
.mn-light-color10 {
|
||||
color: #f48fb1;
|
||||
}
|
||||
.mn-light-color11 {
|
||||
color: #9fa8da;
|
||||
}
|
||||
.mn-light-color12 {
|
||||
color: #ffe082;
|
||||
}
|
||||
.mn-light-color13 {
|
||||
color: #bcaaa4;
|
||||
}
|
||||
.mn-light-color14 {
|
||||
color: #80deea;
|
||||
}
|
||||
.mn-light-color15 {
|
||||
color: #ce93d8;
|
||||
}
|
||||
|
||||
/* Darker version of user mention colors */
|
||||
.mn-dark-color0 {
|
||||
color: #C62828;
|
||||
}
|
||||
.mn-dark-color1 {
|
||||
color: #AD1457;
|
||||
}
|
||||
.mn-dark-color2 {
|
||||
color: #6A1B9A;
|
||||
}
|
||||
.mn-dark-color3 {
|
||||
color: #4527A0;
|
||||
}
|
||||
.mn-dark-color4 {
|
||||
color: #283593;
|
||||
}
|
||||
.mn-dark-color5 {
|
||||
color: #1565C0;
|
||||
}
|
||||
.mn-dark-color6 {
|
||||
color: #0277BD;
|
||||
}
|
||||
.mn-dark-color7 {
|
||||
color: #00838F;
|
||||
}
|
||||
.mn-dark-color8 {
|
||||
color: #00695C;
|
||||
}
|
||||
.mn-dark-color9 {
|
||||
color: #2E7D32;
|
||||
}
|
||||
.mn-dark-color10 {
|
||||
color: #558B2F;
|
||||
}
|
||||
.mn-dark-color11 {
|
||||
color: #9E9D24;
|
||||
}
|
||||
.mn-dark-color12 {
|
||||
color: #F9A825;
|
||||
}
|
||||
.mn-dark-color13 {
|
||||
color: #FF8F00;
|
||||
}
|
||||
.mn-dark-color14 {
|
||||
color: #EF6C00;
|
||||
}
|
||||
.mn-dark-color15 {
|
||||
color: #D84315;
|
||||
}
|
||||
|
||||
/* Material icons resized from the default 24pt */
|
||||
.material-icons {
|
||||
font-size: 1.4rem;
|
||||
@@ -2565,6 +2476,7 @@ ul.tabbar li.active > a {
|
||||
font-weight: lighter;
|
||||
border-radius: 0.25rem;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.chat-box .bubble .reply-quote {
|
||||
@@ -2572,15 +2484,16 @@ ul.tabbar li.active > a {
|
||||
}
|
||||
|
||||
.chat-box .left .reply-quote {
|
||||
background-color: #daebc6;
|
||||
background-color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.chat-box .right .reply-quote {
|
||||
background-color: #eee;
|
||||
background-color: rgba(128, 128, 128, 0.12);
|
||||
}
|
||||
|
||||
.reply-quote-header {
|
||||
color: #039be5;
|
||||
.reply-quote .inline-image {
|
||||
vertical-align: middle;
|
||||
margin-bottom: 0.1rem;
|
||||
}
|
||||
|
||||
#reply-quote-preview {
|
||||
@@ -2599,3 +2512,7 @@ ul.tabbar li.active > a {
|
||||
min-width: 2.4rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mention {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -15,7 +15,7 @@
|
||||
"i18n:missing": "node scripts/i18n.js missing",
|
||||
"i18n:obsolete": "node scripts/i18n.js obsolete",
|
||||
"vers": "node scripts/gen-version.js",
|
||||
"extract": "formatjs extract src/**/*.jsx --out-file src/i18n/ex/base-en.json --id-interpolation-pattern '[sha512:contenthash:base64:6]'",
|
||||
"extract": "formatjs extract \"src/**/*.{js,jsx}\" --out-file src/i18n/ex/base-en.json --id-interpolation-pattern '[sha512:contenthash:base64:6]'",
|
||||
"compile": "formatjs compile"
|
||||
},
|
||||
"browserslist": "> 0.5%, not IE 11, not op_mini all, not and_uc >0",
|
||||
|
||||
+1
-1
@@ -79,7 +79,7 @@ export const MAX_IMAGE_DIM = 1024;
|
||||
export const IMAGE_PREVIEW_DIM = 64;
|
||||
|
||||
// Linear dimensions of image thumbnail: shrink image under this size for thumbnails in reply quote previews.
|
||||
export const IMAGE_THUMBNAIL_DIM = 25;
|
||||
export const IMAGE_THUMBNAIL_DIM = 36;
|
||||
|
||||
// Maximum number of online users to be shown in a topic title bar. Others will be
|
||||
// hidden under "+X more"
|
||||
|
||||
+1
-1
@@ -429,7 +429,7 @@
|
||||
"invalid_content": {
|
||||
"translation": "ungültiger Inhalt",
|
||||
"defaultMessage": "invalid content",
|
||||
"description": "Shown when message is unreadable",
|
||||
"description": "Shown when the message is unreadable",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
|
||||
+1
-1
@@ -436,7 +436,7 @@
|
||||
"invalid_content": {
|
||||
"translation": "invalid content",
|
||||
"defaultMessage": "invalid content",
|
||||
"description": "Shown when message is unreadable",
|
||||
"description": "Shown when the message is unreadable",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
|
||||
+1
-1
@@ -429,7 +429,7 @@
|
||||
"invalid_content": {
|
||||
"translation": "contenido inválido",
|
||||
"defaultMessage": "invalid content",
|
||||
"description": "Shown when message is unreadable",
|
||||
"description": "Shown when the message is unreadable",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
|
||||
@@ -253,7 +253,7 @@
|
||||
},
|
||||
"invalid_content": {
|
||||
"defaultMessage": "invalid content",
|
||||
"description": "Shown when message is unreadable"
|
||||
"description": "Shown when the message is unreadable"
|
||||
},
|
||||
"invalid_security_token": {
|
||||
"defaultMessage": "Invalid security token",
|
||||
|
||||
+1
-1
@@ -429,7 +429,7 @@
|
||||
"invalid_content": {
|
||||
"translation": "잘못된내용",
|
||||
"defaultMessage": "invalid content",
|
||||
"description": "Shown when message is unreadable",
|
||||
"description": "Shown when the message is unreadable",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
|
||||
+1
-1
@@ -429,7 +429,7 @@
|
||||
"invalid_content": {
|
||||
"translation": "conținut invalid",
|
||||
"defaultMessage": "invalid content",
|
||||
"description": "Shown when message is unreadable",
|
||||
"description": "Shown when the message is unreadable",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
|
||||
+1
-1
@@ -429,7 +429,7 @@
|
||||
"invalid_content": {
|
||||
"translation": "сообщение не читается",
|
||||
"defaultMessage": "invalid content",
|
||||
"description": "Shown when message is unreadable",
|
||||
"description": "Shown when the message is unreadable",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
|
||||
+1
-1
@@ -429,7 +429,7 @@
|
||||
"invalid_content": {
|
||||
"translation": "內容無效",
|
||||
"defaultMessage": "invalid content",
|
||||
"description": "Shown when message is unreadable",
|
||||
"description": "Shown when the message is unreadable",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
|
||||
+1
-1
@@ -429,7 +429,7 @@
|
||||
"invalid_content": {
|
||||
"translation": "无效内容",
|
||||
"defaultMessage": "invalid content",
|
||||
"description": "Shown when message is unreadable",
|
||||
"description": "Shown when the message is unreadable",
|
||||
"missing": false,
|
||||
"obsolete": false
|
||||
},
|
||||
|
||||
+5
-4
@@ -23,10 +23,11 @@ const language = (params && params.hl) ||
|
||||
const baseLanguage = language.toLowerCase().split(/[-_]/)[0];
|
||||
|
||||
// Try the full locale first, then the locale without the region code, fallback to 'en'.
|
||||
const messages =
|
||||
allMessages[language] ||
|
||||
allMessages[baseLanguage] ||
|
||||
allMessages.en;
|
||||
const htmlLang = allMessages[language] ? langauge : allMessages[baseLanguage] ? baseLanguage : 'en';
|
||||
const messages = allMessages[htmlLang];
|
||||
|
||||
// Set lang attribute of the HTML element: <html lang="XX">
|
||||
document.getElementsByTagName('html')[0].setAttribute('lang', htmlLang);
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
|
||||
@@ -0,0 +1,283 @@
|
||||
import React from 'react';
|
||||
import { defineMessages } from 'react-intl';
|
||||
|
||||
import { Drafty } from 'tinode-sdk';
|
||||
|
||||
import UploadingImage from '../widgets/uploading-image.jsx'
|
||||
|
||||
import { IMAGE_THUMBNAIL_DIM, BROKEN_IMAGE_SIZE, REM_SIZE } from '../config.js';
|
||||
import { fitImageSize } from './blob-helpers.js';
|
||||
import { idToColorClass } from './strformat.js';
|
||||
import { sanitizeImageUrl } from './utils.js';
|
||||
|
||||
const messages = defineMessages({
|
||||
drafty_form: {
|
||||
id: 'drafty_form',
|
||||
defaultMessage: 'Form: ',
|
||||
description: 'Comment for form in drafty preview'
|
||||
},
|
||||
drafty_attachment: {
|
||||
id: 'drafty_attachment',
|
||||
defaultMessage: 'Attachment',
|
||||
description: 'Comment for attachment in drafty preview'
|
||||
},
|
||||
drafty_image: {
|
||||
id: 'drafty_image',
|
||||
defaultMessage: 'Picture',
|
||||
description: 'Comment for embedded images in drafty preview'
|
||||
},
|
||||
});
|
||||
|
||||
function handleImageData(el, data, attr) {
|
||||
if (!data) {
|
||||
attr.src = 'img/broken_image.png';
|
||||
attr.style = {
|
||||
width: IMAGE_THUMBNAIL_DIM + 'px',
|
||||
height: IMAGE_THUMBNAIL_DIM + 'px',
|
||||
// Looks like a Chrome bug: broken image does not respect 'width' and 'height'.
|
||||
minWidth: IMAGE_THUMBNAIL_DIM + 'px',
|
||||
minHeight: IMAGE_THUMBNAIL_DIM + 'px'
|
||||
};
|
||||
return el;
|
||||
}
|
||||
|
||||
attr.className = 'inline-image';
|
||||
const dim = fitImageSize(data.width, data.height,
|
||||
this.viewportWidth > 0 ? Math.min(this.viewportWidth - REM_SIZE * 6.5, REM_SIZE * 34.5) :
|
||||
REM_SIZE * 34.5, REM_SIZE * 24, false) ||
|
||||
{dstWidth: BROKEN_IMAGE_SIZE, dstHeight: BROKEN_IMAGE_SIZE};
|
||||
attr.style = {
|
||||
width: dim.dstWidth + 'px',
|
||||
height: dim.dstHeight + 'px',
|
||||
// Looks like a Chrome bug: broken image does not respect 'width' and 'height'.
|
||||
minWidth: dim.dstWidth + 'px',
|
||||
minHeight: dim.dstHeight + 'px'
|
||||
};
|
||||
if (!Drafty.isProcessing(data)) {
|
||||
attr.src = this.authorizeURL(sanitizeImageUrl(attr.src));
|
||||
attr.alt = data.name;
|
||||
if (attr.src) {
|
||||
if (Math.max(data.width || 0, data.height || 0) > IMAGE_THUMBNAIL_DIM) {
|
||||
// Allow previews for large enough images.
|
||||
attr.onClick = this.onImagePreview;
|
||||
attr.className += ' image-clickable';
|
||||
}
|
||||
attr.loading = 'lazy';
|
||||
} else {
|
||||
attr.src = 'img/broken_image.png';
|
||||
}
|
||||
} else {
|
||||
// Use custom element instead of <img>.
|
||||
el = UploadingImage;
|
||||
}
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
function shortenFileName(filename) {
|
||||
return filename.length > 16 ? filename.slice(0, 7) + '…' + filename.slice(-7) : filename;
|
||||
}
|
||||
|
||||
// The main Drafty formatter: converts Drafty elements into React classes. 'this' is set by the caller.
|
||||
// 'this' must contain:
|
||||
// viewportWidth: this.props.viewportWidth;
|
||||
// authorizeURL: this.props.tinode.authorizeURL
|
||||
// onImagePreview: this.handleImagePreview
|
||||
// onFormButtonClick: this.handleFormButtonClick
|
||||
// onQuoteClick: this.handleQuoteClick (optional)
|
||||
export function fullFormatter(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 'HL':
|
||||
// Highlighted text. Assign class name.
|
||||
attr.className = 'highlight';
|
||||
break;
|
||||
case 'IM':
|
||||
// Additional processing for images
|
||||
el = handleImageData.call(this, el, data, attr);
|
||||
// Image element cannot have content.
|
||||
values = null;
|
||||
break;
|
||||
case 'BN':
|
||||
// Button
|
||||
attr.onClick = this.onFormButtonClick;
|
||||
let inner = React.Children.map(values, (child) => {
|
||||
return typeof child == 'string' ? child : undefined;
|
||||
});
|
||||
if (!inner || inner.length == 0) {
|
||||
inner = [attr.name]
|
||||
}
|
||||
// Get text which will be sent back when the button is clicked.
|
||||
attr['data-title'] = inner.join('');
|
||||
break;
|
||||
case 'MN':
|
||||
// Mention
|
||||
if (data && data.hasOwnProperty('colorId')) {
|
||||
attr.className = 'mn-dark-color' + data.colorId;
|
||||
}
|
||||
break;
|
||||
case 'FM':
|
||||
// Form
|
||||
attr.className = 'bot-form';
|
||||
break;
|
||||
case 'RW':
|
||||
// Form element formatting is dependent on element content.
|
||||
break;
|
||||
case 'QQ':
|
||||
// Quote/citation.
|
||||
attr.className = 'reply-quote'
|
||||
attr.onClick = this.onQuoteClick;
|
||||
break;
|
||||
default:
|
||||
if (el == '_UNKN') {
|
||||
// Unknown element.
|
||||
el = React.Fragment;
|
||||
values = [<i className="material-icons gray">extension</i>, ' '].concat(values || []);
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (!el) {
|
||||
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:
|
||||
// formatMessage: this.props.intl.formatMessage
|
||||
// messages: formatjs messages defined with defineMessages.
|
||||
export function previewFormatter(style, data, values, key) {
|
||||
if (!style) {
|
||||
// Unformatted.
|
||||
return values;
|
||||
}
|
||||
|
||||
if (style == 'HD') {
|
||||
// Hidden.
|
||||
return null;
|
||||
}
|
||||
|
||||
let el = Drafty.tagName(style);
|
||||
const attr = { key: key };
|
||||
switch (style) {
|
||||
case 'BR':
|
||||
// Replace new line with a space.
|
||||
el = React.Fragment;
|
||||
values = [' '];
|
||||
break;
|
||||
case 'HL':
|
||||
// Make highlight less prominent in preview.
|
||||
attr.className = 'highlight preview';
|
||||
break;
|
||||
case 'LN':
|
||||
case 'MN':
|
||||
// Disable links in previews.
|
||||
el = 'span';
|
||||
break;
|
||||
case 'IM':
|
||||
// Replace image with '[icon] Image'.
|
||||
el = React.Fragment;
|
||||
values = [<i key="im" className="material-icons">photo</i>, this.formatMessage(messages.drafty_image)];
|
||||
break;
|
||||
case 'BN':
|
||||
el = 'span';
|
||||
attr.className = 'flat-button faux';
|
||||
break;
|
||||
case 'FM':
|
||||
el = React.Fragment;
|
||||
values = [<i key="fm" className="material-icons">dashboard</i>,
|
||||
this.formatMessage(messages.drafty_form)].concat(' ', values || []);
|
||||
break;
|
||||
case 'RW':
|
||||
el = React.Fragment;
|
||||
break;
|
||||
case 'EX':
|
||||
if (data) {
|
||||
if (data.mime == 'application/json') {
|
||||
// Ignore JSON attachments: they are form response payloads.
|
||||
return null;
|
||||
}
|
||||
// Clear payload.
|
||||
delete data.val;
|
||||
delete data.ref;
|
||||
}
|
||||
el = React.Fragment;
|
||||
values = [<i key="ex" className="material-icons">attachment</i>, this.formatMessage(messages.drafty_attachment)];
|
||||
break;
|
||||
default:
|
||||
if (el == '_UNKN') {
|
||||
el = React.Fragment;
|
||||
values = [<i key="unkn" className="material-icons">extension</i>, ' '].concat(values || []);
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (!el) {
|
||||
return values;
|
||||
}
|
||||
return React.createElement(el, attr, values);
|
||||
};
|
||||
|
||||
// Converts Drafty object into a quoted reply. 'this' is set by the caller.
|
||||
// 'this' must contain:
|
||||
// formatMessage: this.props.intl.formatMessage
|
||||
// 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)) {
|
||||
let el = Drafty.tagName(style);
|
||||
let attr = Drafty.attrValue(style, data) || {};
|
||||
attr.key = key;
|
||||
switch(style) {
|
||||
case 'IM':
|
||||
const img = handleImageData.call(this, el, data, attr);
|
||||
values = [React.createElement(img, attr, null), ' ',
|
||||
shortenFileName((data && data.name) || this.formatMessage(messages.drafty_image))];
|
||||
el = React.Fragment;
|
||||
// Fragment attributes.
|
||||
attr = {key: key};
|
||||
break;
|
||||
case 'MN':
|
||||
el = 'span';
|
||||
attr.className = 'mention'
|
||||
if (data) {
|
||||
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) {
|
||||
if (data.mime == 'application/json') {
|
||||
// Ignore JSON attachments: they are form response payloads.
|
||||
return null;
|
||||
}
|
||||
fname = data.name;
|
||||
// Clear payload.
|
||||
delete data.val;
|
||||
delete data.ref;
|
||||
}
|
||||
el = React.Fragment;
|
||||
values = [<i key="ex" className="material-icons">attachment</i>,
|
||||
shortenFileName(fname || this.formatMessage(messages.drafty_attachment))];
|
||||
break;
|
||||
}
|
||||
return React.createElement(el, attr, values);
|
||||
}
|
||||
return previewFormatter.call(this, style, data, values, key);
|
||||
}
|
||||
@@ -58,7 +58,7 @@ export function letterTileColorId(userId) {
|
||||
// params:
|
||||
// - {string} id: user or topic ID
|
||||
// - {boolean} light: light or dark version of the color.
|
||||
// - {boolean} fg: foreground or background color.
|
||||
// - {boolean} fg: foreground (text) or background color.
|
||||
export function idToColorClass(id, light, fg) {
|
||||
return (light ? 'lt-' : 'dk-') + (fg ? 'fg-' : 'bg-') + letterTileColorId(id);
|
||||
}
|
||||
|
||||
+132
-308
@@ -15,13 +15,12 @@ import LetterTile from '../widgets/letter-tile.jsx';
|
||||
import LoadSpinner from '../widgets/load-spinner.jsx';
|
||||
import LogoView from './logo-view.jsx';
|
||||
import SendMessage from '../widgets/send-message.jsx';
|
||||
import UploadingImage from '../widgets/uploading-image.jsx'
|
||||
|
||||
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,
|
||||
IMAGE_THUMBNAIL_DIM, BROKEN_IMAGE_SIZE, REM_SIZE } from '../config.js';
|
||||
IMAGE_THUMBNAIL_DIM } from '../config.js';
|
||||
import { SUPPORTED_IMAGE_FORMATS, blobToBase64, base64ToBlob, filePasted, fileToBase64,
|
||||
imageScaled, makeImageUrl, fitImageSize } from '../lib/blob-helpers.js';
|
||||
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';
|
||||
@@ -55,16 +54,10 @@ const messages = defineMessages({
|
||||
defaultMessage: 'The file size {size} exceeds the {limit} limit.',
|
||||
description: 'Error message when attachment is too large'
|
||||
},
|
||||
|
||||
drafty_form: {
|
||||
id: 'drafty_form',
|
||||
defaultMessage: 'Form: ',
|
||||
description: 'Comment for form in drafty preview'
|
||||
},
|
||||
drafty_attachment: {
|
||||
id: 'drafty_attachment',
|
||||
defaultMessage: 'Attachment',
|
||||
description: 'Comment for attachment in drafty preview'
|
||||
invalid_content: {
|
||||
id: 'invalid_content',
|
||||
defaultMessage: 'invalid content',
|
||||
description: 'Shown when the message is unreadable'
|
||||
},
|
||||
});
|
||||
|
||||
@@ -93,6 +86,7 @@ class MessagesView extends React.Component {
|
||||
|
||||
this.leave = this.leave.bind(this);
|
||||
this.sendMessage = this.sendMessage.bind(this);
|
||||
this.retrySend = this.retrySend.bind(this);
|
||||
this.sendImageAttachment = this.sendImageAttachment.bind(this);
|
||||
this.sendFileAttachment = this.sendFileAttachment.bind(this);
|
||||
this.sendKeyPress = this.sendKeyPress.bind(this);
|
||||
@@ -138,7 +132,6 @@ class MessagesView extends React.Component {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// this.propsChange(this.props, this.state);
|
||||
if (this.messagesScroller) {
|
||||
this.messagesScroller.addEventListener('scroll', this.handleScrollEvent);
|
||||
}
|
||||
@@ -365,12 +358,12 @@ class MessagesView extends React.Component {
|
||||
// If there are unsent messages, try sending them now.
|
||||
topic.queuedMessages((pub) => {
|
||||
if (!pub._sending && topic.isSubscribed()) {
|
||||
this.sendMessage(pub);
|
||||
this.retrySend(pub);
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log("Failed subscription to", this.state.topic);
|
||||
console.error("Failed subscription to", this.state.topic);
|
||||
this.props.onError(err.message, 'err');
|
||||
const blankState = MessagesView.getDerivedStateFromProps({}, {});
|
||||
blankState.title = this.props.intl.formatMessage(messages.not_found);
|
||||
@@ -591,7 +584,7 @@ class MessagesView extends React.Component {
|
||||
this.forceUpdate();
|
||||
break;
|
||||
default:
|
||||
console.log("Other change in topic: ", info.what);
|
||||
console.info("Other change in topic: ", info.what);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -627,7 +620,7 @@ class MessagesView extends React.Component {
|
||||
url.search = params;
|
||||
window.open(url, '_blank');
|
||||
} else {
|
||||
console.log("Unknown action in form", action);
|
||||
console.info("Unknown action in form", action);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -678,12 +671,17 @@ class MessagesView extends React.Component {
|
||||
if (typeof msg == 'string') {
|
||||
msg = Drafty.parse(msg);
|
||||
}
|
||||
msg = Drafty.attachQuote(msg, this.state.reply.content);
|
||||
msg = Drafty.append(this.state.reply.content, msg);
|
||||
this.setState({reply: null});
|
||||
}
|
||||
this.props.sendMessage(msg, uploadCompletionPromise, uploader, head);
|
||||
}
|
||||
|
||||
// Retry sending a message.
|
||||
retrySend(pub) {
|
||||
this.props.sendMessage(pub.content, undefined, undefined, pub.head);
|
||||
}
|
||||
|
||||
// Send attachment as Drafty message:
|
||||
// - if file is too large, upload it and send a s link.
|
||||
// - if file is small enough, just send it in-band.
|
||||
@@ -835,122 +833,122 @@ class MessagesView extends React.Component {
|
||||
uploader.cancel();
|
||||
}
|
||||
|
||||
handlePickReply(m) {
|
||||
handlePickReply(seq, content) {
|
||||
this.setState({reply: null});
|
||||
if (m) {
|
||||
let cont = m.content
|
||||
if (cont) {
|
||||
if (typeof cont == 'string') {
|
||||
cont = Drafty.init(cont);
|
||||
}
|
||||
cont = Drafty.preview(cont, 30, quotePreviewFmt);
|
||||
|
||||
// Get the author.
|
||||
let header;
|
||||
const seq = m.seq;
|
||||
if (!seq || !content) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof content == 'string') {
|
||||
content = Drafty.init(content);
|
||||
}
|
||||
if (Drafty.isValid(content)) {
|
||||
content = Drafty.preview(content, 30);
|
||||
} else {
|
||||
// /!\ invalid content.
|
||||
content = Drafty.append(Drafty.init('\u26A0 '),
|
||||
Drafty.wrapInto(this.props.intl.formatMessage(messages.invalid_content), 'EM'));
|
||||
}
|
||||
|
||||
// Get the author.
|
||||
const topic = this.props.tinode.getTopic(this.state.topic);
|
||||
const msg = topic.findMessage(seq);
|
||||
let senderName, senderId;
|
||||
if (msg) {
|
||||
senderId = msg.from || 'chan';
|
||||
if (senderId != this.props.myUserId) {
|
||||
const topic = this.props.tinode.getTopic(this.state.topic);
|
||||
const m0 = topic.findMessage(seq);
|
||||
let thisFrom;
|
||||
if (m0) {
|
||||
thisFrom = m0.from || 'chan';
|
||||
if (thisFrom != this.props.myUserId) {
|
||||
const topic = this.props.tinode.getTopic(this.state.topic);
|
||||
const user = topic.userDesc(thisFrom);
|
||||
if (user && user.public) {
|
||||
header = user.public.fn;
|
||||
}
|
||||
} else {
|
||||
header = this.props.myUserName;
|
||||
}
|
||||
const user = topic.userDesc(senderId);
|
||||
if (user && user.public) {
|
||||
senderName = user.public.fn;
|
||||
}
|
||||
|
||||
header = Drafty.init(header || '');
|
||||
// Make small image previews.
|
||||
const ents = [];
|
||||
if (cont.ent) {
|
||||
Drafty.entities(cont, (data, idx, tp) => {
|
||||
if (tp == 'IM') {
|
||||
ents.push({
|
||||
tp: tp,
|
||||
data: data
|
||||
});
|
||||
}
|
||||
|
||||
}, this);
|
||||
}
|
||||
|
||||
// Turn all images into thumbnails.
|
||||
const promises = [];
|
||||
ents.map((ex) => {
|
||||
let p = new Promise((resolve, reject) => {
|
||||
const handleFailure = () => {
|
||||
ex.data.val = '';
|
||||
ex.data.name = '';
|
||||
ex.data.width = IMAGE_THUMBNAIL_DIM;
|
||||
ex.data.height = IMAGE_THUMBNAIL_DIM;
|
||||
};
|
||||
const scale = (origBlob) => {
|
||||
imageScaled(origBlob, IMAGE_THUMBNAIL_DIM, IMAGE_THUMBNAIL_DIM, -1, false,
|
||||
// Success
|
||||
(mime, blob, width, height, fname) => {
|
||||
ex.data.mime = mime;
|
||||
ex.data.size = blob.size;
|
||||
ex.data.width = width;
|
||||
ex.data.height = height;
|
||||
ex.data.name = fname;
|
||||
ex.data.ref = undefined;
|
||||
|
||||
blobToBase64(blob, (blobMime, tinyBits64) => {
|
||||
ex.data.val = tinyBits64;
|
||||
resolve(true);
|
||||
});
|
||||
},
|
||||
// Failure
|
||||
(err) => {
|
||||
handleFailure();
|
||||
reject(`Could not scale image: ${err}`);
|
||||
});
|
||||
}
|
||||
if (ex.data.val) {
|
||||
const b = base64ToBlob(ex.data.val, ex.data.mime);
|
||||
if (b) {
|
||||
scale(b);
|
||||
} else {
|
||||
handleFailure();
|
||||
}
|
||||
} else {
|
||||
const from = this.props.tinode.authorizeURL(sanitizeImageUrl(ex.data.ref));
|
||||
fetch(from)
|
||||
.then(e => {
|
||||
if (e.ok) {
|
||||
return e.blob();
|
||||
} else {
|
||||
handleFailure();
|
||||
reject(`Image fetch unsuccessful: ${e.status} - ${e.statusText}`);
|
||||
}
|
||||
})
|
||||
.then((b) => scale(b))
|
||||
.catch((err) => reject(`Error fetching image data: ${err}`));
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
promises.push(p);
|
||||
});
|
||||
|
||||
Promise.all(promises)
|
||||
.catch((err) => {
|
||||
this.props.onError(err, 'err');
|
||||
})
|
||||
.finally(() => {
|
||||
// All done. Create a reply quote.
|
||||
const msg = Drafty.createQuote(header, cont, letterTileColorId(thisFrom));
|
||||
this.setState({reply: {content: msg, seq: m.seq}});
|
||||
});
|
||||
|
||||
return;
|
||||
} else {
|
||||
senderName = this.props.myUserName;
|
||||
}
|
||||
}
|
||||
|
||||
// Make small image previews.
|
||||
const images = [];
|
||||
Drafty.entities(content, (data, idx, tp) => {
|
||||
if (tp == 'IM') {
|
||||
images.push({
|
||||
tp: tp,
|
||||
data: data
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Turn all images into thumbnails.
|
||||
const promises = images.map((ex) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const handleFailure = () => {
|
||||
delete ex.data.val;
|
||||
delete ex.data.name;
|
||||
ex.data.width = IMAGE_THUMBNAIL_DIM;
|
||||
ex.data.height = IMAGE_THUMBNAIL_DIM;
|
||||
ex.data.maxWidth = IMAGE_THUMBNAIL_DIM;
|
||||
ex.data.maxHeight = IMAGE_THUMBNAIL_DIM;
|
||||
};
|
||||
const scale = (origBlob) => {
|
||||
imageScaled(origBlob, IMAGE_THUMBNAIL_DIM, IMAGE_THUMBNAIL_DIM, -1, false,
|
||||
// Success
|
||||
(mime, blob, width, height, unused) => {
|
||||
ex.data.mime = mime;
|
||||
ex.data.size = blob.size;
|
||||
ex.data.width = width;
|
||||
ex.data.height = height;
|
||||
delete ex.data.ref;
|
||||
// Keeping the original file name, if provided: ex.data.name;
|
||||
|
||||
blobToBase64(blob, (blobMime, tinyBits64) => {
|
||||
ex.data.val = tinyBits64;
|
||||
resolve(true);
|
||||
});
|
||||
},
|
||||
// Failure
|
||||
(err) => {
|
||||
handleFailure();
|
||||
reject(`Could not scale image: ${err}`);
|
||||
});
|
||||
}
|
||||
if (ex.data.val) {
|
||||
const b = base64ToBlob(ex.data.val, ex.data.mime);
|
||||
if (b) {
|
||||
scale(b);
|
||||
} else {
|
||||
handleFailure();
|
||||
}
|
||||
} else {
|
||||
fetch(this.props.tinode.authorizeURL(sanitizeImageUrl(ex.data.ref)))
|
||||
.then(e => {
|
||||
if (e.ok) {
|
||||
return e.blob();
|
||||
} else {
|
||||
handleFailure();
|
||||
reject(`Image fetch unsuccessful: ${e.status} ${e.statusText}`);
|
||||
}
|
||||
})
|
||||
.then((b) => scale(b))
|
||||
.catch((err) => reject(`Error fetching image data: ${err}`));
|
||||
return;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Promise.all(promises)
|
||||
.catch((err) => {
|
||||
this.props.onError(err, 'err');
|
||||
})
|
||||
.finally(() => {
|
||||
// All done. Create a reply quote.
|
||||
this.setState({
|
||||
reply: {
|
||||
content: Drafty.quote(senderName, senderId, content),
|
||||
seq: seq
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
handleCancelReply() {
|
||||
@@ -959,16 +957,12 @@ class MessagesView extends React.Component {
|
||||
|
||||
handleQuoteClick(replyToSeq) {
|
||||
const ref = this.getOrCreateMessageRef(replyToSeq);
|
||||
if (!ref) {
|
||||
return;
|
||||
}
|
||||
const element = ref.current;
|
||||
if (element) {
|
||||
element.scrollIntoView({block: "center", behavior: "smooth"});
|
||||
element.style.backgroundColor = 'rgb(0, 0, 0, 0.4)';
|
||||
setTimeout(() => { element.style.backgroundColor = ''; } , 1000);
|
||||
if (ref && ref.current) {
|
||||
ref.current.scrollIntoView({block: "center", behavior: "smooth"});
|
||||
ref.current.classList.add('flash');
|
||||
setTimeout(() => { ref.current.classList.remove('flash') } , 1000);
|
||||
} else {
|
||||
console.error("Unresolved message ref: seqId", replyToSeq);
|
||||
console.error("Unresolved message ref", replyToSeq);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -993,7 +987,6 @@ class MessagesView extends React.Component {
|
||||
content={this.state.imagePreview}
|
||||
tinode={this.props.tinode}
|
||||
replyTo={this.state.reply}
|
||||
formatter={draftyFormatter}
|
||||
onCancelReply={this.handleCancelReply}
|
||||
onClose={this.handleClosePreview}
|
||||
onSendMessage={this.sendImageAttachment} />
|
||||
@@ -1012,7 +1005,6 @@ class MessagesView extends React.Component {
|
||||
content={this.state.docPreview}
|
||||
tinode={this.props.tinode}
|
||||
replyTo={this.state.reply}
|
||||
formatter={draftyFormatter}
|
||||
onCancelReply={this.handleCancelReply}
|
||||
onClose={this.handleClosePreview}
|
||||
onSendMessage={this.sendFileAttachment} />
|
||||
@@ -1099,7 +1091,6 @@ class MessagesView extends React.Component {
|
||||
pickReply={this.handlePickReply}
|
||||
replyToSeq={replyToSeq}
|
||||
onQuoteClick={this.handleQuoteClick}
|
||||
formatter={draftyFormatter}
|
||||
ref={ref}
|
||||
userIsWriter={this.state.isWriter}
|
||||
key={msg.seq} />
|
||||
@@ -1206,7 +1197,6 @@ class MessagesView extends React.Component {
|
||||
onError={this.props.onError}
|
||||
replyTo={this.state.reply}
|
||||
onQuoteClick={this.handleQuoteClick}
|
||||
formatter={draftyFormatter}
|
||||
onCancelReply={this.handleCancelReply} />}
|
||||
</>
|
||||
);
|
||||
@@ -1218,170 +1208,4 @@ class MessagesView extends React.Component {
|
||||
}
|
||||
};
|
||||
|
||||
// Transforms styles (and corresponding entities) for formatting reply quotes.
|
||||
function quotePreviewFmt(fmt, ent) {
|
||||
let tp = fmt.tp;
|
||||
if (!tp) {
|
||||
if (!ent || !ent.tp) {
|
||||
return [null, null];
|
||||
}
|
||||
tp = ent.tp;
|
||||
}
|
||||
const new_fmt = {at: fmt.at, len: fmt.len, tp: fmt.tp};
|
||||
switch (tp) {
|
||||
case 'BR':
|
||||
// Replace new line with a space.
|
||||
return [null, null];
|
||||
case 'HL':
|
||||
return [new_fmt, ent];
|
||||
case 'LN':
|
||||
// Disable links in previews.
|
||||
return [null, null];
|
||||
case 'IM':
|
||||
// Keep images as is.
|
||||
return [new_fmt, ent];
|
||||
case 'BN':
|
||||
new_fmt.tp = null;
|
||||
return [new_fmt, { tp: 'IC', data: { orig: 'BN', name: 'button'}}];
|
||||
case 'FM':
|
||||
new_fmt.tp = null;
|
||||
return [new_fmt, {tp: 'IC', data: { orig: 'FM', name: 'form'}}];
|
||||
case 'RW':
|
||||
return [null, null];
|
||||
case 'EX':
|
||||
// Make it an icon.
|
||||
new_fmt.tp = null;
|
||||
if (new_fmt.at == -1) {
|
||||
// Render it normally.
|
||||
new_fmt.at = 0;
|
||||
}
|
||||
return [new_fmt, {tp: 'IC', data: { orig: 'EX', name: 'attachment'}}];
|
||||
case 'QQ':
|
||||
// Quote/citation.
|
||||
return [null,null];
|
||||
default:
|
||||
return [new_fmt, ent];
|
||||
}
|
||||
}
|
||||
|
||||
// Converts Drafty elements into React classes.
|
||||
// 'this' is set by the caller.
|
||||
function draftyFormatter(style, data, values, key) {
|
||||
if (style == 'EX') {
|
||||
// attachments are handled elsewhere.
|
||||
return null;
|
||||
}
|
||||
|
||||
let el = Drafty.tagName(style);
|
||||
if (el) {
|
||||
const { formatMessage } = this.props.intl;
|
||||
let attr = Drafty.attrValue(style, data) || {};
|
||||
attr.key = key;
|
||||
switch (style) {
|
||||
case 'HL':
|
||||
// Highlighted text. Assign class name.
|
||||
attr.className = 'highlight';
|
||||
break;
|
||||
case 'IM':
|
||||
// Additional processing for images
|
||||
if (data) {
|
||||
attr.className = 'inline-image';
|
||||
const dim = fitImageSize(data.width, data.height,
|
||||
this.props.hasOwnProperty('viewportWidth') ? Math.min(this.props.viewportWidth - REM_SIZE * 6.5, REM_SIZE * 34.5) : REM_SIZE * 34.5,
|
||||
REM_SIZE * 24, false) ||
|
||||
{dstWidth: BROKEN_IMAGE_SIZE, dstHeight: BROKEN_IMAGE_SIZE};
|
||||
attr.style = {
|
||||
width: dim.dstWidth + 'px',
|
||||
height: dim.dstHeight + 'px',
|
||||
// Looks like a Chrome bug: broken image does not respect 'width' and 'height'.
|
||||
minWidth: dim.dstWidth + 'px',
|
||||
minHeight: dim.dstHeight + 'px'
|
||||
};
|
||||
if (!Drafty.isProcessing(data)) {
|
||||
attr.src = this.props.tinode.authorizeURL(sanitizeImageUrl(attr.src));
|
||||
attr.alt = data.name;
|
||||
if (attr.src) {
|
||||
if (Math.max(data.width || 0, data.height || 0) > IMAGE_THUMBNAIL_DIM) {
|
||||
// Allow previews for large enough images.
|
||||
attr.onClick = this.handleImagePreview;
|
||||
attr.className += ' image-clickable';
|
||||
}
|
||||
attr.loading = 'lazy';
|
||||
} else {
|
||||
attr.src = 'img/broken_image.png';
|
||||
}
|
||||
} else {
|
||||
// Use custom element instead of <img>.
|
||||
el = UploadingImage;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'BN':
|
||||
// Button
|
||||
attr.onClick = this.handleFormButtonClick;
|
||||
let inner = React.Children.map(values, (child) => {
|
||||
return typeof child == 'string' ? child : undefined;
|
||||
});
|
||||
if (!inner || inner.length == 0) {
|
||||
inner = [attr.name]
|
||||
}
|
||||
// Get text which will be sent back when the button is clicked.
|
||||
attr['data-title'] = inner.join('');
|
||||
break;
|
||||
case 'MN':
|
||||
// Mention
|
||||
if (data && data.hasOwnProperty('colorId')) {
|
||||
attr.className = 'mn-dark-color' + data.colorId;
|
||||
}
|
||||
break;
|
||||
case 'FM':
|
||||
// Form
|
||||
attr.className = 'bot-form';
|
||||
break;
|
||||
case 'RW':
|
||||
// Form element formatting is dependent on element content.
|
||||
break;
|
||||
case 'QQ':
|
||||
// Quote/citation.
|
||||
attr.className = 'reply-quote'
|
||||
attr.onClick = this.handleQuoteClick;
|
||||
break;
|
||||
case 'IC':
|
||||
// Icon.
|
||||
if (data.name == 'button') {
|
||||
attr.className = 'flat-button faux';
|
||||
} else {
|
||||
const icons = {
|
||||
form: {
|
||||
name: 'dashboard', title: 'drafty_form'
|
||||
},
|
||||
attachment: {
|
||||
name: 'attachment', title: 'drafty_attachment'
|
||||
}
|
||||
};
|
||||
const icon = icons[data.name];
|
||||
el = React.Fragment;
|
||||
if (icon) {
|
||||
const key = data.orig.toLowerCase();
|
||||
values = [<i key={key} className="material-icons">{icon.name}</i>,
|
||||
formatMessage(messages[icon.title])].concat(' ', values || []);
|
||||
} else {
|
||||
values = [];
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (el == '_UNKN') {
|
||||
// Unknown element.
|
||||
el = React.Fragment;
|
||||
values = [<i className="material-icons gray">extension</i>, ' '].concat(values || []);
|
||||
}
|
||||
break;
|
||||
}
|
||||
return React.createElement(el, attr, values);
|
||||
} else {
|
||||
return values;
|
||||
}
|
||||
};
|
||||
|
||||
export default injectIntl(MessagesView);
|
||||
|
||||
@@ -7,6 +7,7 @@ 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';
|
||||
|
||||
class BaseChatMessage extends React.PureComponent {
|
||||
@@ -25,8 +26,17 @@ class BaseChatMessage extends React.PureComponent {
|
||||
this.handleFormButtonClick = this.handleFormButtonClick.bind(this);
|
||||
this.handleContextClick = this.handleContextClick.bind(this);
|
||||
this.handleCancelUpload = this.handleCancelUpload.bind(this);
|
||||
|
||||
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),
|
||||
onImagePreview: this.handleImagePreview,
|
||||
onFormButtonClick: this.handleFormButtonClick,
|
||||
onQuoteClick: this.handleQuoteClick
|
||||
};
|
||||
}
|
||||
|
||||
handleImagePreview(e) {
|
||||
@@ -99,7 +109,7 @@ class BaseChatMessage extends React.PureComponent {
|
||||
let content = this.props.content;
|
||||
const attachments = [];
|
||||
if (this.props.mimeType == Drafty.getContentType() && Drafty.isValid(content)) {
|
||||
Drafty.attachments(content, function(att, i) {
|
||||
Drafty.attachments(content, (att, i) => {
|
||||
if (att.mime == 'application/json') {
|
||||
// Don't show json objects as attachments.
|
||||
// They are not meant for users.
|
||||
@@ -117,7 +127,7 @@ class BaseChatMessage extends React.PureComponent {
|
||||
onError={this.props.onError}
|
||||
key={i} />);
|
||||
}, this);
|
||||
const tree = Drafty.format(content, this.props.formatter, 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.
|
||||
@@ -126,9 +136,9 @@ class BaseChatMessage extends React.PureComponent {
|
||||
defaultMessage="content deleted" description="Shown when messages are deleted" />
|
||||
</i></>
|
||||
} else if (typeof content != 'string') {
|
||||
content = <><i className="material-icons gray">error_outline</i> <i className="gray">
|
||||
content = <><i className="material-icons gray">warning_amber</i> <i className="gray">
|
||||
<FormattedMessage id="invalid_content"
|
||||
defaultMessage="invalid content" description="Shown when message is unreadable" /></i></>
|
||||
defaultMessage="invalid content" description="Shown when the message is unreadable" /></i></>
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
+6
-82
@@ -1,6 +1,6 @@
|
||||
// A single topic or user.
|
||||
import React from 'react';
|
||||
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
|
||||
import { FormattedMessage, injectIntl } from 'react-intl';
|
||||
|
||||
import ContactBadges from './contact-badges.jsx';
|
||||
import LetterTile from './letter-tile.jsx';
|
||||
@@ -9,26 +9,9 @@ import UnreadBadge from './unread-badge.jsx';
|
||||
|
||||
import { Drafty } from 'tinode-sdk';
|
||||
|
||||
import { previewFormatter } from '../lib/formatters.js';
|
||||
import { deliveryMarker } from '../lib/utils.js';
|
||||
|
||||
const messages = defineMessages({
|
||||
drafty_image: {
|
||||
id: 'drafty_image',
|
||||
defaultMessage: 'Picture',
|
||||
description: 'Comment for embedded images in drafty preview'
|
||||
},
|
||||
drafty_form: {
|
||||
id: 'drafty_form',
|
||||
defaultMessage: 'Form: ',
|
||||
description: 'Comment for form in drafty preview'
|
||||
},
|
||||
drafty_attachment: {
|
||||
id: 'drafty_attachment',
|
||||
defaultMessage: 'Attachment',
|
||||
description: 'Comment for attachment in drafty preview'
|
||||
},
|
||||
});
|
||||
|
||||
class Contact extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@@ -89,10 +72,11 @@ 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, draftyFormatter, this)) :
|
||||
<><i className="material-icons gray">error_outline</i> <i className="gray">
|
||||
React.createElement(React.Fragment, null, Drafty.format(this.props.preview, previewFormatter,
|
||||
{formatMessage: this.props.intl.formatMessage})) :
|
||||
<><i className="material-icons gray">warning_amber</i> <i className="gray">
|
||||
<FormattedMessage id="invalid_content"
|
||||
defaultMessage="invalid content" description="Shown when message is unreadable" /></i>
|
||||
defaultMessage="invalid content" description="Shown when the message is unreadable" /></i>
|
||||
</>
|
||||
) :
|
||||
this.props.comment;
|
||||
@@ -134,64 +118,4 @@ class Contact extends React.Component {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Converts Drafty object into a one-line preview.
|
||||
function draftyFormatter(style, data, values, key) {
|
||||
let el = Drafty.tagName(style);
|
||||
const attr = { key: key };
|
||||
if (el) {
|
||||
const { formatMessage } = this.props.intl;
|
||||
switch (style) {
|
||||
case 'BR':
|
||||
// Replace new line with a space.
|
||||
el = React.Fragment;
|
||||
values = [' '];
|
||||
break;
|
||||
case 'HL':
|
||||
// Make highlight less prominent in preview.
|
||||
attr.className = 'highlight preview';
|
||||
break;
|
||||
case 'LN':
|
||||
case 'MN':
|
||||
// Disable links in previews.
|
||||
el = 'span';
|
||||
break;
|
||||
case 'IM':
|
||||
// Replace image with '[icon] Image'.
|
||||
el = React.Fragment;
|
||||
values = [<i key="im" className="material-icons">photo</i>, formatMessage(messages.drafty_image)];
|
||||
break;
|
||||
case 'BN':
|
||||
el = 'span';
|
||||
attr.className = 'flat-button faux';
|
||||
break;
|
||||
case 'FM':
|
||||
el = React.Fragment;
|
||||
values = [<i key="fm" className="material-icons">dashboard</i>,
|
||||
formatMessage(messages.drafty_form)].concat(' ', values || []);
|
||||
break;
|
||||
case 'RW':
|
||||
el = React.Fragment;
|
||||
break;
|
||||
case 'EX':
|
||||
if (data && data.mime == 'application/json') {
|
||||
// Ignore JSON attachments: they are form response payloads.
|
||||
return null;
|
||||
}
|
||||
el = React.Fragment;
|
||||
values = [<i key="ex" className="material-icons">attachment</i>, formatMessage(messages.drafty_attachment)];
|
||||
break;
|
||||
default:
|
||||
if (el == '_UNKN') {
|
||||
el = React.Fragment;
|
||||
values = [<i key="unkn" className="material-icons">extension</i>, ' '].concat(values || []);
|
||||
}
|
||||
break;
|
||||
}
|
||||
return React.createElement(el, attr, values);
|
||||
} else {
|
||||
return values;
|
||||
}
|
||||
};
|
||||
|
||||
export default injectIntl(Contact);
|
||||
|
||||
@@ -424,7 +424,7 @@ class ContextMenu extends React.Component {
|
||||
}
|
||||
|
||||
replyToMessage(params, errorHandler) {
|
||||
params.pickReply({ seq: params.seq, content: params.content });
|
||||
params.pickReply(params.seq, params.content, errorHandler);
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
@@ -47,7 +47,6 @@ export default class DocPreview extends React.PureComponent {
|
||||
noInput={true}
|
||||
tinode={this.props.tinode}
|
||||
replyTo={this.props.replyTo}
|
||||
formatter={this.props.formatter}
|
||||
onCancelReply={this.props.onCancelReply}
|
||||
onSendMessage={this.handleSendDoc}
|
||||
onError={this.props.onError} />
|
||||
|
||||
@@ -75,7 +75,6 @@ export default class ImagePreview extends React.PureComponent {
|
||||
acceptBlank={true}
|
||||
tinode={this.props.tinode}
|
||||
replyTo={this.props.replyTo}
|
||||
formatter={this.props.formatter}
|
||||
onCancelReply={this.props.onCancelReply}
|
||||
onSendMessage={this.handleSendImage}
|
||||
onError={this.props.onError} />
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Drafty } from 'tinode-sdk';
|
||||
|
||||
import { KEYPRESS_DELAY } from '../config.js';
|
||||
import { filePasted } from '../lib/blob-helpers.js';
|
||||
import { quoteFormatter } from '../lib/formatters.js';
|
||||
|
||||
const messages = defineMessages({
|
||||
messaging_disabled: {
|
||||
@@ -153,17 +154,19 @@ class SendMessage extends React.PureComponent {
|
||||
}
|
||||
|
||||
render() {
|
||||
const {formatMessage} = this.props.intl;
|
||||
const { formatMessage } = this.props.intl;
|
||||
const prompt = this.props.disabled ?
|
||||
formatMessage(messages.messaging_disabled) :
|
||||
(this.props.messagePrompt ?
|
||||
formatMessage(messages[this.props.messagePrompt]) :
|
||||
formatMessage(messages.type_new_message));
|
||||
|
||||
let quote = null;
|
||||
if (this.props.replyTo) {
|
||||
quote = Drafty.format(this.props.replyTo.content, this.props.formatter, this);
|
||||
}
|
||||
const quote = this.props.replyTo ?
|
||||
Drafty.format(this.props.replyTo.content, quoteFormatter, {
|
||||
formatMessage: formatMessage.bind(this.props.intl),
|
||||
authorizeURL: this.props.tinode.authorizeURL.bind(this.props.tinode)
|
||||
}) :
|
||||
null;
|
||||
|
||||
return (
|
||||
<div id="send-message-wrapper">
|
||||
|
||||
+503
-501
File diff suppressed because it is too large
Load Diff
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
+633
-634
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user