merged with next

This commit is contained in:
or-else
2022-05-26 10:16:09 -07:00
30 changed files with 844 additions and 4008 deletions
+1 -1
View File
@@ -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
View File
@@ -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;
} }
/* /*
+1 -1
View File
File diff suppressed because one or more lines are too long
-1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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": "",
-4
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;
} }
+7 -2
View File
@@ -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
View File
File diff suppressed because one or more lines are too long
+12 -15
View File
@@ -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) {
+8 -16
View File
@@ -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();
+31 -36
View File
@@ -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>
</> </>
+2 -5
View File
@@ -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 -4
View File
@@ -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">
+1 -1
View File
@@ -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} />
+8 -11
View File
@@ -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
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+176 -120
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long