merged with next
This commit is contained in:
@@ -48,7 +48,7 @@ More translations are welcome. See [instructions](https://github.com/tinode/chat
|
|||||||
* End-to-End encryption.
|
* End-to-End encryption.
|
||||||
* Emoji support is weak.
|
* Emoji support is weak.
|
||||||
* Mentions, hashtags.
|
* Mentions, hashtags.
|
||||||
* Previews not generated for videos, audio, links or docs.
|
* Previews not generated for videos, links or docs.
|
||||||
|
|
||||||
## Other
|
## Other
|
||||||
|
|
||||||
|
|||||||
+5
-6
@@ -1121,7 +1121,6 @@ img.channel {
|
|||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
padding-left: 0.5px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* END of avatar box */
|
/* END of avatar box */
|
||||||
@@ -2324,7 +2323,7 @@ ul.tabbar li.active > a {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
font-size: 90%;
|
font-size: 90%;
|
||||||
max-width: 12rem;
|
max-width: 12rem;
|
||||||
overflow-x: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2671,11 +2670,11 @@ ul.tabbar li.active > a {
|
|||||||
|
|
||||||
.audio-player .material-icons.large {
|
.audio-player .material-icons.large {
|
||||||
font-size: 2.4rem;
|
font-size: 2.4rem;
|
||||||
color: #667575;
|
color: #667575CC;
|
||||||
}
|
}
|
||||||
|
|
||||||
.audio-player .material-icons.disabled {
|
.audio-player .material-icons.disabled {
|
||||||
color: #999;
|
color: #999C;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2692,13 +2691,13 @@ ul.tabbar li.active > a {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
color: #757575;
|
color: #757575CC;
|
||||||
font-size: small;
|
font-size: small;
|
||||||
}
|
}
|
||||||
|
|
||||||
.audio-player .timer {
|
.audio-player .timer {
|
||||||
font-size: smaller;
|
font-size: smaller;
|
||||||
color: #666;
|
color: #666C;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -71,7 +71,6 @@
|
|||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-intl": "^5.20.2",
|
"react-intl": "^5.20.2",
|
||||||
"react-intl-formatted-duration": "^4.0.0",
|
|
||||||
"tinode-sdk": "^0.20.0-alpha1",
|
"tinode-sdk": "^0.20.0-alpha1",
|
||||||
"webrtc-adapter": "^8.1.1"
|
"webrtc-adapter": "^8.1.1"
|
||||||
},
|
},
|
||||||
|
|||||||
+8
-3
@@ -16,7 +16,7 @@ export const DEFAULT_HOST = KNOWN_HOSTS.hosted;
|
|||||||
export const LOGGING_ENABLED = true;
|
export const LOGGING_ENABLED = true;
|
||||||
|
|
||||||
// Minimum time between two keypress notifications, milliseconds.
|
// Minimum time between two keypress notifications, milliseconds.
|
||||||
export const KEYPRESS_DELAY = 3*1000;
|
export const KEYPRESS_DELAY = 3_000;
|
||||||
// Delay before sending a {note} for reciving a message, milliseconds.
|
// Delay before sending a {note} for reciving a message, milliseconds.
|
||||||
export const RECEIVED_DELAY = 500;
|
export const RECEIVED_DELAY = 500;
|
||||||
// Delay before sending a read notification, milliseconds.
|
// Delay before sending a read notification, milliseconds.
|
||||||
@@ -64,7 +64,7 @@ export const MESSAGES_PAGE = 24;
|
|||||||
// for instance 1024 bytes.
|
// for instance 1024 bytes.
|
||||||
// Increase this limit to a greater value in production, if desired. Also increase
|
// Increase this limit to a greater value in production, if desired. Also increase
|
||||||
// max_message_size in the server config.
|
// max_message_size in the server config.
|
||||||
export const MAX_INBAND_ATTACHMENT_SIZE = 262144;
|
export const MAX_INBAND_ATTACHMENT_SIZE = 262_144;
|
||||||
|
|
||||||
// Default absolute maximum attachment size to be used with the server = 8MB.
|
// Default absolute maximum attachment size to be used with the server = 8MB.
|
||||||
// Used when the server-provided value is unavailable. Increase to something like 100MB in production.
|
// Used when the server-provided value is unavailable. Increase to something like 100MB in production.
|
||||||
@@ -98,8 +98,13 @@ export const QUOTED_REPLY_LENGTH = 30;
|
|||||||
// Length of a preview of a forwarded message.
|
// Length of a preview of a forwarded message.
|
||||||
export const FORWARDED_PREVIEW_LENGTH = 84;
|
export const FORWARDED_PREVIEW_LENGTH = 84;
|
||||||
|
|
||||||
|
// Minimum duration of an audio recording in milliseconds (2 sec).
|
||||||
|
export const MIN_DURATION = 2_000;
|
||||||
|
// Maximum duration of an audio recording in milliseconds (10 min).
|
||||||
|
export const MAX_DURATION = 600_000;
|
||||||
|
|
||||||
// Link for "Contact Us".
|
// Link for "Contact Us".
|
||||||
export const LINK_CONTACT_US = 'email:support@tinode.co';
|
export const LINK_CONTACT_US = 'mailto:support@tinode.co';
|
||||||
|
|
||||||
// Link to Privacy Policy.
|
// Link to Privacy Policy.
|
||||||
export const LINK_PRIVACY_POLICY = 'https://tinode.co/privacy.html';
|
export const LINK_PRIVACY_POLICY = 'https://tinode.co/privacy.html';
|
||||||
|
|||||||
+1
-1
@@ -1373,7 +1373,7 @@
|
|||||||
"defaultMessage": "unavailable",
|
"defaultMessage": "unavailable",
|
||||||
"description": "Message shown when media preview is not available",
|
"description": "Message shown when media preview is not available",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": true
|
||||||
},
|
},
|
||||||
"calls_incoming": {
|
"calls_incoming": {
|
||||||
"translation": "",
|
"translation": "",
|
||||||
|
|||||||
+1
-1
@@ -1373,7 +1373,7 @@
|
|||||||
"defaultMessage": "unavailable",
|
"defaultMessage": "unavailable",
|
||||||
"description": "Message shown when media preview is not available",
|
"description": "Message shown when media preview is not available",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": true
|
||||||
},
|
},
|
||||||
"calls_incoming": {
|
"calls_incoming": {
|
||||||
"translation": "Incoming Call",
|
"translation": "Incoming Call",
|
||||||
|
|||||||
+1
-1
@@ -1373,7 +1373,7 @@
|
|||||||
"defaultMessage": "unavailable",
|
"defaultMessage": "unavailable",
|
||||||
"description": "Message shown when media preview is not available",
|
"description": "Message shown when media preview is not available",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": true
|
||||||
},
|
},
|
||||||
"calls_incoming": {
|
"calls_incoming": {
|
||||||
"translation": "",
|
"translation": "",
|
||||||
|
|||||||
@@ -628,10 +628,6 @@
|
|||||||
"defaultMessage": "phone",
|
"defaultMessage": "phone",
|
||||||
"description": "Dative case of 'phone', i.e. 'phone' in 'by phone'"
|
"description": "Dative case of 'phone', i.e. 'phone' in 'by phone'"
|
||||||
},
|
},
|
||||||
"preview_unavailable": {
|
|
||||||
"defaultMessage": "unavailable",
|
|
||||||
"description": "Message shown when media preview is not available"
|
|
||||||
},
|
|
||||||
"private_editing_placeholder": {
|
"private_editing_placeholder": {
|
||||||
"defaultMessage": "Visible to you only",
|
"defaultMessage": "Visible to you only",
|
||||||
"description": "Placeholder for editing 'private'"
|
"description": "Placeholder for editing 'private'"
|
||||||
|
|||||||
+1
-1
@@ -1373,7 +1373,7 @@
|
|||||||
"defaultMessage": "unavailable",
|
"defaultMessage": "unavailable",
|
||||||
"description": "Message shown when media preview is not available",
|
"description": "Message shown when media preview is not available",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": true
|
||||||
},
|
},
|
||||||
"calls_incoming": {
|
"calls_incoming": {
|
||||||
"translation": "",
|
"translation": "",
|
||||||
|
|||||||
+1
-1
@@ -1373,7 +1373,7 @@
|
|||||||
"defaultMessage": "unavailable",
|
"defaultMessage": "unavailable",
|
||||||
"description": "Message shown when media preview is not available",
|
"description": "Message shown when media preview is not available",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": true
|
||||||
},
|
},
|
||||||
"calls_incoming": {
|
"calls_incoming": {
|
||||||
"translation": "",
|
"translation": "",
|
||||||
|
|||||||
+1
-1
@@ -1373,7 +1373,7 @@
|
|||||||
"defaultMessage": "unavailable",
|
"defaultMessage": "unavailable",
|
||||||
"description": "Message shown when media preview is not available",
|
"description": "Message shown when media preview is not available",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": true
|
||||||
},
|
},
|
||||||
"calls_incoming": {
|
"calls_incoming": {
|
||||||
"translation": "",
|
"translation": "",
|
||||||
|
|||||||
+1
-1
@@ -1373,7 +1373,7 @@
|
|||||||
"defaultMessage": "unavailable",
|
"defaultMessage": "unavailable",
|
||||||
"description": "Message shown when media preview is not available",
|
"description": "Message shown when media preview is not available",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": true
|
||||||
},
|
},
|
||||||
"calls_incoming": {
|
"calls_incoming": {
|
||||||
"translation": "",
|
"translation": "",
|
||||||
|
|||||||
+1
-1
@@ -1373,7 +1373,7 @@
|
|||||||
"defaultMessage": "unavailable",
|
"defaultMessage": "unavailable",
|
||||||
"description": "Message shown when media preview is not available",
|
"description": "Message shown when media preview is not available",
|
||||||
"missing": false,
|
"missing": false,
|
||||||
"obsolete": false
|
"obsolete": true
|
||||||
},
|
},
|
||||||
"calls_incoming": {
|
"calls_incoming": {
|
||||||
"translation": "",
|
"translation": "",
|
||||||
|
|||||||
@@ -199,7 +199,7 @@ export function blobToBase64(blob) {
|
|||||||
// File pasted from the clipboard. It's either an inline image or a file attachment.
|
// File pasted from the clipboard. It's either an inline image or a file attachment.
|
||||||
export function filePasted(event, onImageSuccess, onAttachmentSuccess, onError) {
|
export function filePasted(event, onImageSuccess, onAttachmentSuccess, onError) {
|
||||||
const items = (event.clipboardData || event.originalEvent.clipboardData || {}).items;
|
const items = (event.clipboardData || event.originalEvent.clipboardData || {}).items;
|
||||||
if (!Array.isArray(items)) {
|
if (!items || !items.length) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,16 +16,21 @@ export function shortDateFormat(then, locale) {
|
|||||||
return then.toLocaleDateString(locale, {year: 'numeric', month: 'short', day: 'numeric'});
|
return then.toLocaleDateString(locale, {year: 'numeric', month: 'short', day: 'numeric'});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert seconds to minutes:seconds, i.e. 156 sec -> 2:36.
|
// Convert seconds to [hours:]minutes:seconds, i.e. 156 sec -> 2:36, 3756 sec -> 1:02:36.
|
||||||
// If <code>fixedMins</code> is true, then minutes are represented by at least two digits.
|
// If <code>fixedMins</code> is true, then minutes are represented by at least two digits.
|
||||||
export function secondsToTime(seconds, fixedMin) {
|
export function secondsToTime(seconds, fixedMin) {
|
||||||
let min = Math.floor(seconds / 60) | 0;
|
let min = Math.floor(seconds / 60) | 0;
|
||||||
if (fixedMin) {
|
let hours = Math.floor(min / 60) | 0;
|
||||||
|
|
||||||
|
if (fixedMin || hours > 0) {
|
||||||
min = min < 10 ? `0${min}` : min;
|
min = min < 10 ? `0${min}` : min;
|
||||||
}
|
}
|
||||||
let sec = (seconds % 60) | 0;
|
let sec = (seconds % 60) | 0;
|
||||||
sec = sec < 10 ? `0${sec}` : sec;
|
sec = sec < 10 ? `0${sec}` : sec;
|
||||||
|
if (hours == 0) {
|
||||||
return `${min}:${sec}`;
|
return `${min}:${sec}`;
|
||||||
|
}
|
||||||
|
return `${hours}:${min}:${sec}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert a number of bytes to human-readable format.
|
// Convert a number of bytes to human-readable format.
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
+12
-15
@@ -241,16 +241,6 @@ class MessagesView extends React.Component {
|
|||||||
} else if (nextProps.topic != prevState.topic) {
|
} else if (nextProps.topic != prevState.topic) {
|
||||||
const topic = nextProps.tinode.getTopic(nextProps.topic);
|
const topic = nextProps.tinode.getTopic(nextProps.topic);
|
||||||
|
|
||||||
let reply = null;
|
|
||||||
if (nextProps.forwardMessage) {
|
|
||||||
// We are forwarding a message. Show preview.
|
|
||||||
const preview = nextProps.forwardMessage.preview;
|
|
||||||
reply = {
|
|
||||||
content: preview,
|
|
||||||
seq: null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
nextState = {
|
nextState = {
|
||||||
topic: nextProps.topic,
|
topic: nextProps.topic,
|
||||||
docPreview: null,
|
docPreview: null,
|
||||||
@@ -260,11 +250,18 @@ class MessagesView extends React.Component {
|
|||||||
typingIndicator: false,
|
typingIndicator: false,
|
||||||
scrollPosition: 0,
|
scrollPosition: 0,
|
||||||
fetchingMessages: false,
|
fetchingMessages: false,
|
||||||
reply: reply,
|
|
||||||
showGoToLastButton: false,
|
showGoToLastButton: false,
|
||||||
deleted: topic._deleted
|
deleted: topic._deleted
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (nextProps.forwardMessage) {
|
||||||
|
// We are forwarding a message. Show preview.
|
||||||
|
nextState.reply = {
|
||||||
|
content: nextProps.forwardMessage.preview,
|
||||||
|
seq: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (topic) {
|
if (topic) {
|
||||||
// Topic exists.
|
// Topic exists.
|
||||||
const subs = [];
|
const subs = [];
|
||||||
@@ -461,7 +458,8 @@ class MessagesView extends React.Component {
|
|||||||
const pos = event.target.scrollHeight - event.target.scrollTop - event.target.offsetHeight;
|
const pos = event.target.scrollHeight - event.target.scrollTop - event.target.offsetHeight;
|
||||||
this.setState({
|
this.setState({
|
||||||
scrollPosition: pos,
|
scrollPosition: pos,
|
||||||
showGoToLastButton: pos > SHOW_GO_TO_LAST_DIST,
|
// Show [go to latest message] if far enough from bottom and scrolling down.
|
||||||
|
showGoToLastButton: (pos > SHOW_GO_TO_LAST_DIST) && (pos < this.state.scrollPosition),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.state.fetchingMessages) {
|
if (this.state.fetchingMessages) {
|
||||||
@@ -609,8 +607,8 @@ class MessagesView extends React.Component {
|
|||||||
clearTimeout(this.keyPressTimer)
|
clearTimeout(this.keyPressTimer)
|
||||||
this.setState({messageCount: topic.messageCount(), typingIndicator: false});
|
this.setState({messageCount: topic.messageCount(), typingIndicator: false});
|
||||||
|
|
||||||
// Scroll to the bottom if the message is added to the end of the message list.
|
// Scroll to the bottom if the message is added to the end of the message
|
||||||
// TODO: This should be replaced by showing a "scroll to bottom" button.
|
// list if already at the bottom, otherwise show [go to latest] button.
|
||||||
if (topic.isNewMessage(msg.seq)) {
|
if (topic.isNewMessage(msg.seq)) {
|
||||||
if (this.state.scrollPosition > SHOW_GO_TO_LAST_DIST) {
|
if (this.state.scrollPosition > SHOW_GO_TO_LAST_DIST) {
|
||||||
this.setState({showGoToLastButton: true});
|
this.setState({showGoToLastButton: true});
|
||||||
@@ -961,7 +959,6 @@ class MessagesView extends React.Component {
|
|||||||
|
|
||||||
// seq: seq ID of the source message
|
// seq: seq ID of the source message
|
||||||
// context: message content.
|
// context: message content.
|
||||||
// forwarded: true if the source message is also a forwarded message.
|
|
||||||
// senderId: UID of the sender of the source message.
|
// senderId: UID of the sender of the source message.
|
||||||
// senderName: full name of the sender of the original message.
|
// senderName: full name of the sender of the original message.
|
||||||
handlePickReply(seq, content, senderId, senderName) {
|
handlePickReply(seq, content, senderId, senderName) {
|
||||||
|
|||||||
@@ -347,21 +347,7 @@ class TinodeWeb extends React.Component {
|
|||||||
|
|
||||||
// Tinode received a push notification from the server.
|
// Tinode received a push notification from the server.
|
||||||
handlePushMessage(payload) {
|
handlePushMessage(payload) {
|
||||||
const data = payload.data || {};
|
this.tinode.oobNotification(payload.data || {});
|
||||||
switch (data.what) {
|
|
||||||
case 'msg':
|
|
||||||
if (Tinode.isChannelTopicName(data.topic)) {
|
|
||||||
// The last argument is a fake user Id: otherwise the update is seen as one from the current user.
|
|
||||||
this.tinode.oobNotification('msg', data.topic, data.seq, 'fake-uid');
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'read':
|
|
||||||
// Message read on another device, clear it here.
|
|
||||||
this.tinode.oobNotification('read', data.topic, data.seq);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
console.warn("Unknown push type ignored", data.what, data);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
initFCMessaging() {
|
initFCMessaging() {
|
||||||
@@ -622,7 +608,7 @@ class TinodeWeb extends React.Component {
|
|||||||
this.handleError(
|
this.handleError(
|
||||||
formatMessage(messages.reconnect_countdown, {seconds: timeLeft}),
|
formatMessage(messages.reconnect_countdown, {seconds: timeLeft}),
|
||||||
'warn',
|
'warn',
|
||||||
() => {
|
_ => {
|
||||||
clearInterval(this.reconnectCountdown);
|
clearInterval(this.reconnectCountdown);
|
||||||
this.tinode.reconnect();
|
this.tinode.reconnect();
|
||||||
},
|
},
|
||||||
@@ -1532,6 +1518,12 @@ class TinodeWeb extends React.Component {
|
|||||||
defaultTopicContextMenu(topicName) {
|
defaultTopicContextMenu(topicName) {
|
||||||
const topic = this.tinode.getTopic(topicName);
|
const topic = this.tinode.getTopic(topicName);
|
||||||
|
|
||||||
|
if (topic._deleted) {
|
||||||
|
return [
|
||||||
|
'topic_delete'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
let muted = false, blocked = false, self_blocked = false, subscribed = false, deleter = false, archived = false;
|
let muted = false, blocked = false, self_blocked = false, subscribed = false, deleter = false, archived = false;
|
||||||
if (topic) {
|
if (topic) {
|
||||||
subscribed = topic.isSubscribed();
|
subscribed = topic.isSubscribed();
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
// Audio recorder widget.
|
// Audio recorder widget.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
|
||||||
|
|
||||||
import { secondsToTime } from '../lib/strformat';
|
import { secondsToTime } from '../lib/strformat';
|
||||||
import { base64ToIntArray } from '../lib/blob-helpers';
|
import { base64ToIntArray } from '../lib/blob-helpers';
|
||||||
@@ -13,9 +12,9 @@ const LINE_WIDTH = 3 * CANVAS_UPSCALING;
|
|||||||
// Spacing between two visualization bars.
|
// Spacing between two visualization bars.
|
||||||
const SPACING = 2 * CANVAS_UPSCALING;
|
const SPACING = 2 * CANVAS_UPSCALING;
|
||||||
// Color of histogram bars.
|
// Color of histogram bars.
|
||||||
const BAR_COLOR = '#bbb';
|
const BAR_COLOR = '#888A';
|
||||||
const BAR_COLOR_DARK = '#888';
|
const BAR_COLOR_DARK = '#666C';
|
||||||
const THUMB_COLOR = '#666';
|
const THUMB_COLOR = '#444E';
|
||||||
// Minimum number of amplitude bars to draw.
|
// Minimum number of amplitude bars to draw.
|
||||||
const MIN_PREVIEW_LENGTH = 16;
|
const MIN_PREVIEW_LENGTH = 16;
|
||||||
|
|
||||||
@@ -24,7 +23,7 @@ export default class AudioPlayer extends React.PureComponent {
|
|||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
let preview = base64ToIntArray(this.props.preview);
|
let preview = base64ToIntArray(this.props.preview);
|
||||||
if (Array.isArray(preview) && preview.length < MIN_PREVIEW_LENGTH) {
|
if (!Array.isArray(preview) || preview.length < MIN_PREVIEW_LENGTH) {
|
||||||
preview = null;
|
preview = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,10 +56,8 @@ export default class AudioPlayer extends React.PureComponent {
|
|||||||
this.initAudio();
|
this.initAudio();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.preview) {
|
|
||||||
this.initCanvas();
|
this.initCanvas();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
if (this.audioPlayer) {
|
if (this.audioPlayer) {
|
||||||
@@ -79,7 +76,7 @@ export default class AudioPlayer extends React.PureComponent {
|
|||||||
|
|
||||||
if (this.props.preview != prevProps.preview) {
|
if (this.props.preview != prevProps.preview) {
|
||||||
let preview = base64ToIntArray(this.props.preview);
|
let preview = base64ToIntArray(this.props.preview);
|
||||||
if (Array.isArray(preview) && preview.length < MIN_PREVIEW_LENGTH) {
|
if (!Array.isArray(preview) || preview.length < MIN_PREVIEW_LENGTH) {
|
||||||
preview = null;
|
preview = null;
|
||||||
}
|
}
|
||||||
this.setState({preview: preview}, this.initCanvas);
|
this.setState({preview: preview}, this.initCanvas);
|
||||||
@@ -99,7 +96,7 @@ export default class AudioPlayer extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
initCanvas() {
|
initCanvas() {
|
||||||
// Force canvas aspect ration to match one of the element + upscale canvas to reduce blurring.
|
// Force canvas aspect ratio to match one of the element + upscale canvas to reduce blurring.
|
||||||
this.canvasRef.current.width = this.canvasRef.current.offsetWidth * CANVAS_UPSCALING;
|
this.canvasRef.current.width = this.canvasRef.current.offsetWidth * CANVAS_UPSCALING;
|
||||||
this.canvasRef.current.height = this.canvasRef.current.offsetHeight * CANVAS_UPSCALING;
|
this.canvasRef.current.height = this.canvasRef.current.offsetHeight * CANVAS_UPSCALING;
|
||||||
|
|
||||||
@@ -122,7 +119,7 @@ export default class AudioPlayer extends React.PureComponent {
|
|||||||
this.canvasContext.lineWidth = LINE_WIDTH;
|
this.canvasContext.lineWidth = LINE_WIDTH;
|
||||||
|
|
||||||
const drawFrame = () => {
|
const drawFrame = () => {
|
||||||
if (!this.canvasRef.current) {
|
if (!this.canvasRef.current || !this.audioPlayer) {
|
||||||
// The component is unmounted.
|
// The component is unmounted.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -134,7 +131,7 @@ export default class AudioPlayer extends React.PureComponent {
|
|||||||
window.requestAnimationFrame(drawFrame);
|
window.requestAnimationFrame(drawFrame);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Current pplayback position.
|
// Current playback position.
|
||||||
const thumbAt = this.props.duration ?
|
const thumbAt = this.props.duration ?
|
||||||
Math.max(0, Math.min(this.audioPlayer.currentTime * 1000 / this.props.duration, 1)) * (width - LINE_WIDTH * 2) : -1;
|
Math.max(0, Math.min(this.audioPlayer.currentTime * 1000 / this.props.duration, 1)) * (width - LINE_WIDTH * 2) : -1;
|
||||||
|
|
||||||
@@ -173,27 +170,36 @@ export default class AudioPlayer extends React.PureComponent {
|
|||||||
|
|
||||||
// Quick and dirty downsampling of the original preview bars into a smaller (or equal) number of bars we can display here.
|
// Quick and dirty downsampling of the original preview bars into a smaller (or equal) number of bars we can display here.
|
||||||
resampleBars(original) {
|
resampleBars(original) {
|
||||||
const barCount = Math.min(original.length, ((this.canvasRef.current.width - SPACING) / (LINE_WIDTH + SPACING)) | 0);
|
const dstCount = ((this.canvasRef.current.width - SPACING) / (LINE_WIDTH + SPACING)) | 0;
|
||||||
const factor = original.length / barCount;
|
// Remove extra padding on the right due to fractional bar which is not drawn.
|
||||||
|
this.effectiveWidth = dstCount * (LINE_WIDTH + SPACING) + SPACING;
|
||||||
|
|
||||||
|
if (!Array.isArray(original) || original.length == 0) {
|
||||||
|
return Array.apply(null, Array(dstCount)).map(_ => 0.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
const factor = original.length / dstCount;
|
||||||
let amps = [];
|
let amps = [];
|
||||||
let max = -1;
|
let maxAmp = -1;
|
||||||
for (let i=0;i<barCount;i++) {
|
for (let i=0; i<dstCount; i++) {
|
||||||
let lo = (i * factor) | 0; // low bound;
|
let lo = (i * factor) | 0; // low bound;
|
||||||
let hi = ((i + 1) * factor) | 0; // high bound;
|
let hi = ((i + 1) * factor) | 0; // high bound;
|
||||||
|
if (hi == lo) {
|
||||||
|
amps[i] = original[lo];
|
||||||
|
} else {
|
||||||
let amp = 0.0;
|
let amp = 0.0;
|
||||||
for (let j=lo; j<hi; j++) {
|
for (let j=lo; j<hi; j++) {
|
||||||
amp += original[j]
|
amp += original[j]
|
||||||
}
|
}
|
||||||
amps[i] = Math.max(0, amp / (hi - lo));
|
amps[i] = Math.max(0, amp / (hi - lo));
|
||||||
max = Math.max(amps[i], max);
|
}
|
||||||
|
maxAmp = Math.max(amps[i], maxAmp);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (max > 0) {
|
if (maxAmp > 0) {
|
||||||
// Remove extra padding on the right due to fractional bar which is not drawn.
|
return amps.map(a => a / maxAmp);
|
||||||
this.effectiveWidth = barCount * (LINE_WIDTH + SPACING) + SPACING;
|
|
||||||
return amps.map(a => a / max);
|
|
||||||
}
|
}
|
||||||
return null;
|
return Array.apply(null, Array(dstCount)).map(_ => 0.01);
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePlay(e) {
|
handlePlay(e) {
|
||||||
@@ -211,7 +217,7 @@ export default class AudioPlayer extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleError(err) {
|
handleError(err) {
|
||||||
console.log(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleSeek(e) {
|
handleSeek(e) {
|
||||||
@@ -221,7 +227,7 @@ export default class AudioPlayer extends React.PureComponent {
|
|||||||
const offset = (e.clientX - rect.left) / this.effectiveWidth * CANVAS_UPSCALING;
|
const offset = (e.clientX - rect.left) / this.effectiveWidth * CANVAS_UPSCALING;
|
||||||
this.audioPlayer.currentTime = this.props.duration * offset / 1000;
|
this.audioPlayer.currentTime = this.props.duration * offset / 1000;
|
||||||
this.setState({currentTime: secondsToTime(this.audioPlayer.currentTime, this.state.longMin)});
|
this.setState({currentTime: secondsToTime(this.audioPlayer.currentTime, this.state.longMin)});
|
||||||
if (!this.state.playing && this.state.preview) {
|
if (!this.state.playing) {
|
||||||
this.visualize();
|
this.visualize();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -237,25 +243,14 @@ export default class AudioPlayer extends React.PureComponent {
|
|||||||
</a>);
|
</a>);
|
||||||
return (<div className="audio-player">{this.props.short ?
|
return (<div className="audio-player">{this.props.short ?
|
||||||
<>
|
<>
|
||||||
<div>
|
<canvas className="playback" ref={this.canvasRef} onClick={this.handleSeek} />
|
||||||
{this.state.preview ?
|
|
||||||
<canvas className="playback" ref={this.canvasRef} onClick={this.handleSeek} /> :
|
|
||||||
<div className="playback"> - </div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
{play}
|
{play}
|
||||||
</>
|
</>
|
||||||
:
|
:
|
||||||
<>
|
<>
|
||||||
{play}
|
{play}
|
||||||
<div>
|
<div>
|
||||||
{this.state.preview ?
|
<canvas className="playback" ref={this.canvasRef} onClick={this.handleSeek} />
|
||||||
<canvas className="playback" ref={this.canvasRef} onClick={this.handleSeek} /> :
|
|
||||||
<div className="playback">
|
|
||||||
<FormattedMessage
|
|
||||||
id="preview_unavailable" defaultMessage="unavailable" description="Message shown when media preview is not available" />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
<div className="timer">{this.state.currentTime}/{this.state.duration}</div>
|
<div className="timer">{this.state.currentTime}/{this.state.duration}</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import fixWebmDuration from 'fix-webm-duration';
|
|||||||
|
|
||||||
import { intArrayToBase64 } from '../lib/blob-helpers.js'
|
import { intArrayToBase64 } from '../lib/blob-helpers.js'
|
||||||
import { secondsToTime } from '../lib/strformat';
|
import { secondsToTime } from '../lib/strformat';
|
||||||
|
import { MAX_DURATION, MIN_DURATION } from '../config.js';
|
||||||
|
|
||||||
// FFT resolution.
|
// FFT resolution.
|
||||||
const BUFFER_SIZE = 256;
|
const BUFFER_SIZE = 256;
|
||||||
@@ -20,13 +21,9 @@ const SPACING = 2 * CANVAS_UPSCALING;
|
|||||||
// Duration represented by one visualization bar.
|
// Duration represented by one visualization bar.
|
||||||
const MILLIS_PER_BAR = 100;
|
const MILLIS_PER_BAR = 100;
|
||||||
// Color of histogram bars
|
// Color of histogram bars
|
||||||
const BAR_COLOR = '#bbb';
|
const BAR_COLOR = '#BBBD';
|
||||||
// Vertical scaling for visualization bars.
|
// Vertical scaling for visualization bars.
|
||||||
const BAR_SCALE = 64.0;
|
const BAR_SCALE = 64.0;
|
||||||
// Minimum duration of a recording in milliseconds.
|
|
||||||
const MIN_DURATION = 200;
|
|
||||||
// Maximum duration of a recording in milliseconds (10 min).
|
|
||||||
const MAX_DURATION = 600000;
|
|
||||||
// Number of bars in preview.
|
// Number of bars in preview.
|
||||||
const VISUALIZATION_BARS = 96;
|
const VISUALIZATION_BARS = 96;
|
||||||
// Maximum number of samples per bar.
|
// Maximum number of samples per bar.
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FormattedMessage, injectIntl } from 'react-intl';
|
import { FormattedMessage, injectIntl } from 'react-intl';
|
||||||
import FormattedDuration from 'react-intl-formatted-duration';
|
|
||||||
import { Drafty, Tinode } from 'tinode-sdk';
|
import { Drafty, Tinode } from 'tinode-sdk';
|
||||||
|
|
||||||
import Attachment from './attachment.jsx';
|
import Attachment from './attachment.jsx';
|
||||||
@@ -11,6 +10,7 @@ import ReceivedMarker from './received-marker.jsx'
|
|||||||
|
|
||||||
import { fullFormatter } from '../lib/formatters.js';
|
import { fullFormatter } from '../lib/formatters.js';
|
||||||
import { isVideoCall, sanitizeUrl } from '../lib/utils.js';
|
import { isVideoCall, sanitizeUrl } from '../lib/utils.js';
|
||||||
|
import { secondsToTime } from '../lib/strformat.js';
|
||||||
|
|
||||||
class BaseChatMessage extends React.PureComponent {
|
class BaseChatMessage extends React.PureComponent {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -157,9 +157,7 @@ class BaseChatMessage extends React.PureComponent {
|
|||||||
defaultMessage="Outgoing Call" description="Outgoing call label" />;
|
defaultMessage="Outgoing Call" description="Outgoing call label" />;
|
||||||
const isCallDropped = this.props.content == 'disconnected';
|
const isCallDropped = this.props.content == 'disconnected';
|
||||||
content = <><i className="material-icons" style={{color: isCallDropped ? 'red' : 'green'}}>{direction}</i>{text}
|
content = <><i className="material-icons" style={{color: isCallDropped ? 'red' : 'green'}}>{direction}</i>{text}
|
||||||
{ !isCallDropped && this.props.duration ?
|
{ !isCallDropped && this.props.duration ? [' (', secondsToTime(this.props.duration / 1000), ')'] : null}
|
||||||
[' (', <FormattedDuration seconds={this.props.duration / 1000} format="{hours} {minutes} {seconds}" />, ')'] :
|
|
||||||
null}
|
|
||||||
</>
|
</>
|
||||||
} else if (typeof content != 'string') {
|
} else if (typeof content != 'string') {
|
||||||
content = <><i className="material-icons gray">warning_amber</i> <i className="gray">
|
content = <><i className="material-icons gray">warning_amber</i> <i className="gray">
|
||||||
|
|||||||
@@ -116,12 +116,12 @@ class ContactList extends React.Component {
|
|||||||
selected={selected}
|
selected={selected}
|
||||||
showOnline={this.props.showOnline && !isChannel}
|
showOnline={this.props.showOnline && !isChannel}
|
||||||
isChannel={isChannel}
|
isChannel={isChannel}
|
||||||
onSelected={this.props.onTopicSelected}
|
|
||||||
showContextMenu={this.props.showContextMenu}
|
showContextMenu={this.props.showContextMenu}
|
||||||
isVerified={c.trusted && c.trusted.verified}
|
isVerified={c.trusted && c.trusted.verified}
|
||||||
isStaff={c.trusted && c.trusted.staff}
|
isStaff={c.trusted && c.trusted.staff}
|
||||||
isDangerous={c.trusted && c.trusted.danger}
|
isDangerous={c.trusted && c.trusted.danger}
|
||||||
deleted={c._deleted}
|
deleted={c._deleted}
|
||||||
|
onSelected={this.props.onTopicSelected}
|
||||||
item={key}
|
item={key}
|
||||||
index={contactNodes.length}
|
index={contactNodes.length}
|
||||||
key={key} />
|
key={key} />
|
||||||
|
|||||||
@@ -84,7 +84,8 @@ class SendMessage extends React.PureComponent {
|
|||||||
|
|
||||||
if (prevProps.topicName != this.props.topicName) {
|
if (prevProps.topicName != this.props.topicName) {
|
||||||
this.setState({message: '', audioRec: false, quote: null});
|
this.setState({message: '', audioRec: false, quote: null});
|
||||||
} else if (prevProps.reply != this.props.reply) {
|
}
|
||||||
|
if (prevProps.reply != this.props.reply) {
|
||||||
this.setState({quote: this.formatReply()});
|
this.setState({quote: this.formatReply()});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -103,12 +104,8 @@ class SendMessage extends React.PureComponent {
|
|||||||
}
|
}
|
||||||
// FIXME: handle large files too.
|
// FIXME: handle large files too.
|
||||||
if (filePasted(e,
|
if (filePasted(e,
|
||||||
(bits, mime, width, height, fname) => {
|
file => { this.props.onAttachImage(file); },
|
||||||
this.props.onAttachImage(mime, bits, width, height, fname);
|
file => { this.props.onAttachFile(file); },
|
||||||
},
|
|
||||||
(mime, bits, fname) => {
|
|
||||||
this.props.onAttachFile(mime, bits, fname);
|
|
||||||
},
|
|
||||||
this.props.onError)) {
|
this.props.onError)) {
|
||||||
|
|
||||||
// If a file was pasted, don't paste base64 data into input field.
|
// If a file was pasted, don't paste base64 data into input field.
|
||||||
@@ -196,17 +193,17 @@ class SendMessage extends React.PureComponent {
|
|||||||
formatMessage(messages[this.props.messagePrompt]) :
|
formatMessage(messages[this.props.messagePrompt]) :
|
||||||
formatMessage(messages.type_new_message));
|
formatMessage(messages.type_new_message));
|
||||||
|
|
||||||
const quote =
|
const quote = this.state.quote ?
|
||||||
(<div id="reply-quote-preview">
|
(<div id="reply-quote-preview">
|
||||||
<div className="cancel">
|
<div className="cancel">
|
||||||
<a href="#" onClick={(e) => {e.preventDefault(); this.props.onCancelReply();}}><i className="material-icons gray">close</i></a>
|
<a href="#" onClick={(e) => {e.preventDefault(); this.props.onCancelReply();}}><i className="material-icons gray">close</i></a>
|
||||||
</div>
|
</div>
|
||||||
{this.state.quote}
|
{this.state.quote}
|
||||||
</div>);
|
</div>) : null;
|
||||||
const audioEnabled = this.state.audioAvailable && this.props.onAttachAudio;
|
const audioEnabled = this.state.audioAvailable && this.props.onAttachAudio;
|
||||||
return (
|
return (
|
||||||
<div id="send-message-wrapper">
|
<div id="send-message-wrapper">
|
||||||
{this.state.quote && !this.props.noInput ? quote : null}
|
{!this.props.noInput ? quote : null}
|
||||||
<div id="send-message-panel">
|
<div id="send-message-panel">
|
||||||
{!this.props.disabled ?
|
{!this.props.disabled ?
|
||||||
<>
|
<>
|
||||||
@@ -222,7 +219,7 @@ class SendMessage extends React.PureComponent {
|
|||||||
:
|
:
|
||||||
null}
|
null}
|
||||||
{this.props.noInput ?
|
{this.props.noInput ?
|
||||||
(this.state.quote ? quote : <div className="hr thin" />) :
|
(quote || <div className="hr thin" />) :
|
||||||
(this.state.audioRec ?
|
(this.state.audioRec ?
|
||||||
<AudioRecorder
|
<AudioRecorder
|
||||||
onDeleted={_ => this.setState({audioRec: false})}
|
onDeleted={_ => this.setState({audioRec: false})}
|
||||||
|
|||||||
+467
-3667
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
+176
-120
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