revive-ts-refactor

pull/1102/head
Vincent 5 years ago
parent 83528f4b8d
commit 6078be1657

@ -790,8 +790,7 @@
},
"audioPermissionNeededTitle": {
"message": "Microphone access required",
"description":
"Shown if the user attempts to send an audio message without audio permssions turned on"
"description": "Shown if the user attempts to send an audio message without audio permssions turned on"
},
"audioPermissionNeededDescription": {
"message": "To send audio messages, allow Session to access your microphone.",

@ -2066,7 +2066,6 @@
);
let unreadMessages = await this.getUnread();
const oldUnread = unreadMessages.filter(
message => message.get('received_at') <= newestUnreadDate

@ -106,21 +106,21 @@ window.CONSTANTS = new (function() {
// ///////////////////////// //
// User Interface //
// //////////////////////// //
this.MAX_MESSAGE_BODY_LENGTH = 2000,
this.MAX_MESSAGE_BODY_LENGTH = 2000;
// Limited due to the proof-of-work requirement
this.DEFAULT_MEDIA_FETCH_COUNT = 50,
this.DEFAULT_DOCUMENTS_FETCH_COUNT = 150,
this.DEFAULT_MESSAGE_FETCH_COUNT = 30,
this.MAX_MESSAGE_FETCH_COUNT = 500,
this.DEFAULT_MEDIA_FETCH_COUNT = 50;
this.DEFAULT_DOCUMENTS_FETCH_COUNT = 150;
this.DEFAULT_MESSAGE_FETCH_COUNT = 30;
this.MAX_MESSAGE_FETCH_COUNT = 500;
// Pixels (scroll) from the top of the top of message container
// at which more messages should be loaded
this.MESSAGE_CONTAINER_BUFFER_OFFSET_PX = 30,
this.MESSAGE_FETCH_INTERVAL = 1,
this.MESSAGE_CONTAINER_BUFFER_OFFSET_PX = 30;
this.MESSAGE_FETCH_INTERVAL = 1;
// Maximum voice message duraiton of 5 minutes
// which equates to 1.97 MB
this.MAX_VOICE_MESSAGE_DURATION = 300,
this.MAX_VOICE_MESSAGE_DURATION = 300;
// Max attachment size: 10 MB
this.MAX_ATTACHMENT_FILESIZE = 10000000,
this.MAX_ATTACHMENT_FILESIZE = 10000000;
})();
window.versionInfo = {

@ -1390,7 +1390,7 @@
line-height: 20px;
letter-spacing: 0.3px;
& > div{
& > div {
align-self: center;
justify-self: center;
display: flex;
@ -1401,7 +1401,6 @@
.module-contact-name__profile-name {
text-align: center;
}
}
.module-notification--with-click-handler {

@ -248,7 +248,6 @@ $composition-container-height: 60px;
border-radius: 8px;
padding-bottom: $session-margin-sm;
.emoji-mart-category-label {
top: -2px;
@ -262,13 +261,12 @@ $composition-container-height: 60px;
.emoji-mart-bar:last-child {
border: none;
.emoji-mart-preview {
.emoji-mart-preview {
display: none;
}
}
&:after{
&:after {
content: '';
position: absolute;
top: calc(100% - 40px);
@ -291,7 +289,7 @@ $composition-container-height: 60px;
&__progress {
will-change: transform;
width: 100%;
position: absolute;
left: 0px;
@ -403,5 +401,4 @@ $composition-container-height: 60px;
animation: pulseLight 4s infinite;
}
}
}

@ -38,7 +38,6 @@ interface Props {
isMe: boolean;
isClosable?: boolean;
isGroup: boolean;
isArchived?: boolean;
isPublic: boolean;
isRss: boolean;
amMod: boolean;
@ -73,9 +72,7 @@ interface Props {
onCloseOverlay: () => void;
onDeleteSelectedMessages: () => void;
onArchive?: () => void;
onMoveToInbox: () => void;
onShowSafetyNumber: () => void;
onShowAllMedia: () => void;
onShowGroupMembers: () => void;
@ -92,7 +89,6 @@ interface Props {
onLeaveGroup: () => void;
onAddModerators: () => void;
onRemoveModerators: () => void;
onInviteContacts: () => void;
onAvatarClick?: (userPubKey: string) => void;
onUpdateGroupName: () => void;

@ -1015,7 +1015,7 @@ export class Message extends React.PureComponent<Props, State> {
const dimensions = getGridDimensions(attachments);
if (dimensions) {
return dimensions.width;
}
}
}
if (previews && previews.length) {
@ -1133,10 +1133,7 @@ export class Message extends React.PureComponent<Props, State> {
const enableContextMenu = !isRss && !multiSelectMode && !isKickedFromGroup;
return (
<div
id={id}
className={classNames(divClasses)}
>
<div id={id} className={classNames(divClasses)}>
<ContextMenuTrigger id={rightClickTriggerId}>
{this.renderAvatar()}
<div
@ -1159,10 +1156,9 @@ export class Message extends React.PureComponent<Props, State> {
return;
}
if (id){
if (id) {
this.props.onSelectMessage(id);
}
}}
>
{this.renderError(isIncoming)}

@ -80,9 +80,7 @@ export class TimerNotification extends React.Component<Props> {
/>
</div>
<div>
{this.renderContents()}
</div>
<div>{this.renderContents()}</div>
</div>
</div>
);

@ -1,5 +1,5 @@
import React from 'react';
import classNames from 'classnames';
import classNames from 'classnames';
interface Props {
// Value ranges from 0 to 100
@ -39,7 +39,7 @@ export class SessionProgress extends React.PureComponent<Props, State> {
public componentWillReceiveProps() {
// Reset show for each reset
this.setState({show: true});
this.setState({ show: true });
}
public render() {
@ -53,7 +53,7 @@ export class SessionProgress extends React.PureComponent<Props, State> {
// 1. Width depends on progress.
// 2. Transition duration scales with the
// distance it needs to travel
// FIXME VINCE - globalise all JS color references
const sessionBrandColor = '#00f782';
const sessionDangerAlt = '#ff4538';
@ -61,55 +61,49 @@ export class SessionProgress extends React.PureComponent<Props, State> {
const failureColor = sessionDangerAlt;
const backgroundColor = sendStatus === -1 ? failureColor : successColor;
const shiftDurationMs = this.getShiftDuration(this.props.value, prevValue) * 1000;
const shiftDurationMs =
this.getShiftDuration(this.props.value, prevValue) * 1000;
const showDurationMs = 500;
const showOffsetMs = shiftDurationMs + 500;
const willComplete = value >= 100;
if (willComplete && !show){
setTimeout(
this.onComplete,
shiftDurationMs,
);
if (willComplete && !show) {
setTimeout(this.onComplete, shiftDurationMs);
}
const style = {
'background-color': backgroundColor,
'transform': `translateX(-${100 - value}%)`,
'transition-property': 'transform',
'background-color': backgroundColor,
transform: `translateX(-${100 - value}%)`,
'transition-property': 'transform',
// 'transition-property': 'transform, opacity',
'transition-duration': `${shiftDurationMs}ms`,
'transition-duration': `${shiftDurationMs}ms`,
// 'transition-duration': `${shiftDurationMs}ms, ${showDurationMs}ms`,
'transition-delay': `0ms`,
'transition-delay': `0ms`,
// 'transition-delay': `0ms, ${showOffsetMs}ms`,
'transition-timing-funtion':'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
'transition-timing-funtion': 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
//'transition-timing-funtion':'cubic-bezier(0.25, 0.46, 0.45, 0.94), linear',
}
};
return (
<div className="session-progress">
{ show && (
<div
className="session-progress__progress"
style={style}
>
{show && (
<div className="session-progress__progress" style={style}>
&nbsp
</div>
)}
</div>
);
}
public onComplete(){
public onComplete() {
if (!this.state.show) {
return;
}
console.log(`[sending] ONCOMPLETE`);
this.setState({show: false}, () => {
this.setState({ show: false }, () => {
setTimeout(this.props.resetProgress, 2000);
});
}
private getShiftDuration(value: number, prevValue?: number) {

@ -12,7 +12,6 @@ import { SessionRecording } from './SessionRecording';
import { SignalService } from '../../../../ts/protobuf';
interface Props {
placeholder?: string;
@ -76,16 +75,15 @@ export class SessionCompositionBox extends React.Component<Props, State> {
this.onKeyDown = this.onKeyDown.bind(this);
this.onChange = this.onChange.bind(this);
}
public componentWillReceiveProps(){
public componentWillReceiveProps() {
console.log(`[vince][info] Here are my composition props: `, this.props);
}
public async componentWillMount(){
public async componentWillMount() {
const mediaSetting = await window.getSettingValue('media-permissions');
this.setState({mediaSetting});
this.setState({ mediaSetting });
}
public render() {
@ -93,7 +91,7 @@ export class SessionCompositionBox extends React.Component<Props, State> {
return (
<div className="composition-container">
{ showRecordingView ? (
{showRecordingView ? (
<>{this.renderRecordingView()}</>
) : (
<>{this.renderCompositionView()}</>
@ -108,7 +106,7 @@ export class SessionCompositionBox extends React.Component<Props, State> {
}
this.toggleEmojiPanel();
};
}
private showEmojiPanel() {
document.addEventListener('mousedown', this.handleClick, false);
@ -141,7 +139,7 @@ export class SessionCompositionBox extends React.Component<Props, State> {
onLoadVoiceNoteView={this.onLoadVoiceNoteView}
onExitVoiceNoteView={this.onExitVoiceNoteView}
/>
);
);
}
private renderCompositionView() {
@ -163,7 +161,7 @@ export class SessionCompositionBox extends React.Component<Props, State> {
type="file"
onChange={this.onChoseAttachment}
/>
<SessionIconButton
iconType={SessionIconType.Microphone}
iconSize={SessionIconSize.Huge}
@ -203,17 +201,18 @@ export class SessionCompositionBox extends React.Component<Props, State> {
onKeyDown={this.onKeyDown}
role="button"
>
<SessionEmojiPanel onEmojiClicked={this.onEmojiClick} show={showEmojiPanel}/>
<SessionEmojiPanel
onEmojiClicked={this.onEmojiClick}
show={showEmojiPanel}
/>
</div>
</>
);
}
private onChooseAttachment() {
const fileInput = this.fileInput.current;
if(fileInput) fileInput.click();
if (fileInput) fileInput.click();
}
private onChoseAttachment() {
@ -223,7 +222,6 @@ export class SessionCompositionBox extends React.Component<Props, State> {
const attachments: Array<Attachment> = [];
Array.from(attachmentsFileList).forEach(async (file: File) => {
const fileBlob = new Blob([file]);
const fileBuffer = await new Response(fileBlob).arrayBuffer();
@ -242,10 +240,10 @@ export class SessionCompositionBox extends React.Component<Props, State> {
}
});
this.setState({attachments});
this.setState({ attachments });
}
private onKeyDown(event: any) {
private onKeyDown(event: any) {
if (event.key === 'Enter' && !event.shiftKey) {
// If shift, newline. Else send message.
event.preventDefault();
@ -255,7 +253,6 @@ export class SessionCompositionBox extends React.Component<Props, State> {
}
}
private onSendMessage() {
const messageInput = this.textarea.current;
if (!messageInput) {
@ -269,42 +266,41 @@ export class SessionCompositionBox extends React.Component<Props, State> {
return;
}
// handle Attachments
const {attachments} = this.state;
const { attachments } = this.state;
console.log(`[vince][msg] Message:`, messagePlaintext);
console.log(`[vince][msg] fileAttachments:`, attachments);
// Handle emojis
// Send message
this.props.onMessageSending();
this.props.sendMessage(
messagePlaintext,
attachments,
undefined,
undefined,
null,
{}
).then(() => {
// Message sending sucess
this.props.onMessageSuccess();
// Empty attachments
// Empty composition box
this.setState({
message: '',
attachments: [],
this.props
.sendMessage(
messagePlaintext,
attachments,
undefined,
undefined,
null,
{}
)
.then(() => {
// Message sending sucess
this.props.onMessageSuccess();
// Empty attachments
// Empty composition box
this.setState({
message: '',
attachments: [],
});
})
.catch(() => {
// Message sending failed
this.props.onMessageFailure();
});
}).catch(() => {
// Message sending failed
this.props.onMessageFailure();
});
}
private async sendVoiceMessage(audioBlob: Blob) {
@ -326,7 +322,7 @@ export class SessionCompositionBox extends React.Component<Props, State> {
undefined,
undefined,
null,
{},
{}
);
if (messageSuccess) {
@ -335,13 +331,12 @@ export class SessionCompositionBox extends React.Component<Props, State> {
console.log(`[compositionbox] Sending voice message:`, audioBlob);
this.onExitVoiceNoteView();
}
private onLoadVoiceNoteView() {
// Do stuff for component, then run callback to SessionConversation
const {mediaSetting} = this.state;
const { mediaSetting } = this.state;
if (mediaSetting) {
this.setState({
@ -359,7 +354,6 @@ export class SessionCompositionBox extends React.Component<Props, State> {
description: window.i18n('audioPermissionNeededDescription'),
type: 'info',
});
}
private onExitVoiceNoteView() {
@ -375,10 +369,10 @@ export class SessionCompositionBox extends React.Component<Props, State> {
}
private onChange(event: any) {
this.setState({message: event.target.value});
this.setState({ message: event.target.value });
}
private onEmojiClick({native}: any) {
private onEmojiClick({ native }: any) {
const messageBox = this.textarea.current;
if (!messageBox) {
return;
@ -405,5 +399,4 @@ export class SessionCompositionBox extends React.Component<Props, State> {
}, 20);
});
}
}

@ -5,7 +5,7 @@ import classNames from 'classnames';
import { ConversationHeader } from '../../conversation/ConversationHeader';
import { SessionCompositionBox } from './SessionCompositionBox';
import { SessionProgress } from '../SessionProgress'
import { SessionProgress } from '../SessionProgress';
import { Message } from '../../conversation/Message';
import { FriendRequest } from '../../conversation/FriendRequest';
@ -17,7 +17,6 @@ import { SessionScrollButton } from '../SessionScrollButton';
import { SessionGroupSettings } from './SessionGroupSettings';
import { ResetSessionNotification } from '../../conversation/ResetSessionNotification';
interface State {
conversationKey: string;
sendingProgress: number;
@ -49,7 +48,9 @@ export class SessionConversation extends React.Component<any, State> {
super(props);
const conversationKey = this.props.conversations.selectedConversation;
const conversation = this.props.conversations.conversationLookup[conversationKey];
const conversation = this.props.conversations.conversationLookup[
conversationKey
];
const unreadCount = conversation.unreadCount;
console.log(`[conv] Conversation:`, conversation);
@ -100,7 +101,6 @@ export class SessionConversation extends React.Component<any, State> {
// Keyboard navigation
this.onKeyDown = this.onKeyDown.bind(this);
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -108,24 +108,26 @@ export class SessionConversation extends React.Component<any, State> {
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
public componentDidMount() {
this.getMessages().then(() => {
// Pause thread to wait for rendering to complete
setTimeout(() => {
this.scrollToUnread();
}, 0);
setTimeout(() => {
this.setState({
doneInitialScroll: true,
});
}, 100);
}).catch();
this.getMessages()
.then(() => {
// Pause thread to wait for rendering to complete
setTimeout(() => {
this.scrollToUnread();
}, 0);
setTimeout(() => {
this.setState({
doneInitialScroll: true,
});
}, 100);
})
.catch();
this.updateReadMessages();
}
public componentDidUpdate(){
public componentDidUpdate() {
// Keep scrolled to bottom unless user scrolls up
if (this.state.isScrolledToBottom){
if (this.state.isScrolledToBottom) {
this.scrollToBottom();
}
@ -146,30 +148,43 @@ export class SessionConversation extends React.Component<any, State> {
// ~~~~~~~~~~~~~~ RENDER METHODS ~~~~~~~~~~~~~~
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
public render() {
const { messages, conversationKey, doneInitialScroll, showRecordingView, showOptionsPane, showScrollButton } = this.state;
const {
messages,
conversationKey,
doneInitialScroll,
showRecordingView,
showOptionsPane,
showScrollButton,
} = this.state;
const loading = !doneInitialScroll || messages.length === 0;
const selectionMode = !!this.state.selectedMessages.length;
const conversation = this.props.conversations.conversationLookup[conversationKey];
const conversationModel = window.ConversationController.get(conversationKey);
const conversation = this.props.conversations.conversationLookup[
conversationKey
];
const conversationModel = window.ConversationController.get(
conversationKey
);
const isRss = conversation.isRss;
const sendMessageFn = conversationModel.sendMessage.bind(conversationModel);
const shouldRenderGroupSettings = !conversationModel.isPrivate() && !conversationModel.isRss()
const shouldRenderGroupSettings =
!conversationModel.isPrivate() && !conversationModel.isRss();
const groupSettingsProps = this.getGroupSettingsProps();
return (
<>
<div
className={classNames('conversation-item__content', selectionMode && 'selection-mode')}
className={classNames(
'conversation-item__content',
selectionMode && 'selection-mode'
)}
tabIndex={0}
onKeyDown={this.onKeyDown}
role="navigation"
>
<div className="conversation-header">
{this.renderHeader()}
</div>
<div className="conversation-header">{this.renderHeader()}</div>
<SessionProgress
visible={true}
@ -180,9 +195,7 @@ export class SessionConversation extends React.Component<any, State> {
/>
<div className="messages-wrapper">
{ loading && (
<div className="messages-container__loading"/>
)}
{loading && <div className="messages-container__loading" />}
<div
className="messages-container"
@ -193,13 +206,16 @@ export class SessionConversation extends React.Component<any, State> {
<div ref={this.messagesEndRef} />
</div>
<SessionScrollButton show={showScrollButton} onClick={this.scrollToBottom}/>
{ showRecordingView && (
<div className="messages-wrapper--blocking-overlay"/>
<SessionScrollButton
show={showScrollButton}
onClick={this.scrollToBottom}
/>
{showRecordingView && (
<div className="messages-wrapper--blocking-overlay" />
)}
</div>
{ !isRss && (
{!isRss && (
<SessionCompositionBox
sendMessage={sendMessageFn}
onMessageSending={this.onMessageSending}
@ -209,12 +225,16 @@ export class SessionConversation extends React.Component<any, State> {
onExitVoiceNoteView={this.onExitVoiceNoteView}
/>
)}
</div>
{shouldRenderGroupSettings && (
<div className={classNames('conversation-item__options-pane', showOptionsPane && 'show')}>
<SessionGroupSettings {...groupSettingsProps}/>
<div
className={classNames(
'conversation-item__options-pane',
showOptionsPane && 'show'
)}
>
<SessionGroupSettings {...groupSettingsProps} />
</div>
)}
</>
@ -227,14 +247,23 @@ export class SessionConversation extends React.Component<any, State> {
// FIXME VINCE: IF MESSAGE IS THE TOP OF UNREAD, THEN INSERT AN UNREAD BANNER
return (
<>{
messages.map((message: any) => {
<>
{messages.map((message: any) => {
const messageProps = message.propsForMessage;
const quoteProps = message.propsForQuote;
const timerProps = message.propsForTimerNotification && {i18n: window.i18n, ...message.propsForTimerNotification};
const friendRequestProps = message.propsForFriendRequest && {i18n: window.i18n, ...message.propsForFriendRequest};
const resetSessionProps = message.propsForResetSessionNotification && {i18n: window.i18n, ...message.propsForResetSessionNotification};
const timerProps = message.propsForTimerNotification && {
i18n: window.i18n,
...message.propsForTimerNotification,
};
const friendRequestProps = message.propsForFriendRequest && {
i18n: window.i18n,
...message.propsForFriendRequest,
};
const resetSessionProps = message.propsForResetSessionNotification && {
i18n: window.i18n,
...message.propsForResetSessionNotification,
};
const attachmentProps = message.propsForAttachment;
const groupNotificationProps = message.propsForGroupNotification;
@ -242,19 +271,34 @@ export class SessionConversation extends React.Component<any, State> {
let item;
// firstMessageOfSeries tells us to render the avatar only for the first message
// in a series of messages from the same user
item = messageProps ? this.renderMessage(messageProps, message.firstMessageOfSeries) : item;
item = quoteProps ? this.renderMessage(timerProps, message.firstMessageOfSeries, quoteProps) : item;
item = timerProps ? <TimerNotification {...timerProps} /> : item;
item = friendRequestProps ? <FriendRequest {...friendRequestProps} /> : item;
item = resetSessionProps ? <ResetSessionNotification {...resetSessionProps} /> : item;
item = messageProps
? this.renderMessage(messageProps, message.firstMessageOfSeries)
: item;
item = quoteProps
? this.renderMessage(
timerProps,
message.firstMessageOfSeries,
quoteProps
)
: item;
item = timerProps ? <TimerNotification {...timerProps} /> : item;
item = friendRequestProps ? (
<FriendRequest {...friendRequestProps} />
) : (
item
);
item = resetSessionProps ? (
<ResetSessionNotification {...resetSessionProps} />
) : (
item
);
// item = attachmentProps ? this.renderMessage(timerProps) : item;
return item;
})
}</>
})}
</>
);
}
public renderHeader() {
@ -265,24 +309,27 @@ export class SessionConversation extends React.Component<any, State> {
return (
<ConversationHeader
id={headerProps.id}
name={headerProps.name}
phoneNumber={headerProps.phoneNumber}
profileName={headerProps.profileName}
avatarPath={headerProps.avatarPath}
isVerified={headerProps.isVerified}
isMe={headerProps.isMe}
isFriend={headerProps.isFriend}
i18n={window.i18n}
isClosable={headerProps.isClosable}
isGroup={headerProps.isGroup}
isPublic={headerProps.isPublic}
isRss={headerProps.isRss}
amMod={headerProps.amMod}
members={headerProps.members}
subscriberCount={headerProps.subscriberCount}
expirationSettingName={headerProps.expirationSettingName}
showBackButton={headerProps.showBackButton}
timerOptions={headerProps.timerOptions}
isBlocked={headerProps.isBlocked}
hasNickname={headerProps.hasNickname}
isFriendRequestPending={headerProps.isFriendRequestPending}
isBlocked={headerProps.isBlocked}
isOnline={headerProps.isOnline}
selectedMessages={headerProps.selectedMessages}
onUpdateGroupName={headerProps.onUpdateGroupName}
isKickedFromGroup={headerProps.isKickedFromGroup}
onSetDisappearingMessages={headerProps.onSetDisappearingMessages}
onDeleteMessages={headerProps.onDeleteMessages}
onDeleteContact={headerProps.onDeleteContact}
@ -302,16 +349,21 @@ export class SessionConversation extends React.Component<any, State> {
onLeaveGroup={headerProps.onLeaveGroup}
onAddModerators={headerProps.onAddModerators}
onRemoveModerators={headerProps.onRemoveModerators}
onInviteFriends={headerProps.onInviteFriends}
onAvatarClick={headerProps.onAvatarClick}
onUpdateGroupName={headerProps.onUpdateGroupName}
i18n={window.i18n}
/>
);
}
public renderMessage(messageProps: any, firstMessageOfSeries: boolean, quoteProps?: any) {
const selected = !! messageProps?.id
&& this.state.selectedMessages.includes(messageProps.id);
public renderMessage(
messageProps: any,
firstMessageOfSeries: boolean,
quoteProps?: any
) {
const selected =
!!messageProps?.id &&
this.state.selectedMessages.includes(messageProps.id);
messageProps.i18n = window.i18n;
messageProps.selected = selected;
@ -322,17 +374,17 @@ export class SessionConversation extends React.Component<any, State> {
messageProps.quote = quoteProps || undefined;
return (
<Message {...messageProps} />
);
return <Message {...messageProps} />;
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// ~~~~~~~~~~~~~~ GETTER METHODS ~~~~~~~~~~~~~~
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
public async getMessages(numMessages?: number, fetchInterval = window.CONSTANTS.MESSAGE_FETCH_INTERVAL) {
public async getMessages(
numMessages?: number,
fetchInterval = window.CONSTANTS.MESSAGE_FETCH_INTERVAL
) {
const { conversationKey, messageFetchTimestamp } = this.state;
const timestamp = getTimestamp();
@ -343,28 +395,32 @@ export class SessionConversation extends React.Component<any, State> {
return { newTopMessage: undefined, previousTopMessage: undefined };
}
let msgCount = numMessages || Number(window.CONSTANTS.DEFAULT_MESSAGE_FETCH_COUNT) + this.state.unreadCount;
msgCount = msgCount > window.CONSTANTS.MAX_MESSAGE_FETCH_COUNT
? window.CONSTANTS.MAX_MESSAGE_FETCH_COUNT
: msgCount;
let msgCount =
numMessages ||
Number(window.CONSTANTS.DEFAULT_MESSAGE_FETCH_COUNT) +
this.state.unreadCount;
msgCount =
msgCount > window.CONSTANTS.MAX_MESSAGE_FETCH_COUNT
? window.CONSTANTS.MAX_MESSAGE_FETCH_COUNT
: msgCount;
const messageSet = await window.Signal.Data.getMessagesByConversation(
conversationKey,
{ limit: msgCount, MessageCollection: window.Whisper.MessageCollection },
{ limit: msgCount, MessageCollection: window.Whisper.MessageCollection }
);
// Set first member of series here.
const messageModels = messageSet.models;
const messages = [];
let previousSender;
for (let i = 0; i < messageModels.length; i++){
for (let i = 0; i < messageModels.length; i++) {
// Handle firstMessageOfSeries for conditional avatar rendering
let firstMessageOfSeries = true;
if (i > 0 && previousSender === messageModels[i].authorPhoneNumber){
if (i > 0 && previousSender === messageModels[i].authorPhoneNumber) {
firstMessageOfSeries = false;
}
messages.push({...messageModels[i], firstMessageOfSeries});
messages.push({ ...messageModels[i], firstMessageOfSeries });
previousSender = messageModels[i].authorPhoneNumber;
}
@ -384,7 +440,7 @@ export class SessionConversation extends React.Component<any, State> {
}
public getHeaderProps() {
const {conversationKey} = this.state;
const { conversationKey } = this.state;
const conversation = window.ConversationController.get(conversationKey);
const expireTimer = conversation.get('expireTimer');
@ -402,8 +458,6 @@ export class SessionConversation extends React.Component<any, State> {
color: conversation.getColor(),
avatarPath: conversation.getAvatarPath(),
isVerified: conversation.isVerified(),
isFriendRequestPending: conversation.isPendingFriendRequest(),
isFriend: conversation.isFriend(),
isMe: conversation.isMe(),
isClosable: conversation.isClosable(),
isBlocked: conversation.isBlocked(),
@ -417,8 +471,11 @@ export class SessionConversation extends React.Component<any, State> {
members,
subscriberCount: conversation.get('subscriberCount'),
selectedMessages: this.state.selectedMessages,
isKickedFromGroup: conversation.get('isKickedFromGroup'),
expirationSettingName,
showBackButton: Boolean(conversation.panels && conversation.panels.length),
showBackButton: Boolean(
conversation.panels && conversation.panels.length
),
timerOptions: window.Whisper.ExpirationTimerOptions.map((item: any) => ({
name: item.getName(),
value: item.get('seconds'),
@ -426,7 +483,7 @@ export class SessionConversation extends React.Component<any, State> {
hasNickname: !!conversation.getNickname(),
onSetDisappearingMessages: (seconds: any) =>
conversation.updateExpirationTimer(seconds),
conversation.updateExpirationTimer(seconds),
onDeleteMessages: () => conversation.destroyMessages(),
onDeleteSelectedMessages: () => conversation.deleteSelectedMessages(),
onCloseOverlay: () => conversation.resetMessageSelection(),
@ -477,10 +534,6 @@ export class SessionConversation extends React.Component<any, State> {
window.Whisper.events.trigger('leaveGroup', conversation);
},
onInviteFriends: () => {
window.Whisper.events.trigger('inviteFriends', conversation);
},
onAddModerators: () => {
window.Whisper.events.trigger('addModerators', conversation);
},
@ -549,14 +602,11 @@ export class SessionConversation extends React.Component<any, State> {
window.Whisper.events.trigger('leaveGroup', conversation);
},
onInviteFriends: () => {
window.Whisper.events.trigger('inviteFriends', conversation);
},
onShowLightBox: (lightBoxOptions = {}) => {
conversation.showChannelLightbox(lightBoxOptions);
},
};
};
}
public toggleGroupSettingsPane() {
const { showOptionsPane } = this.state;
@ -593,12 +643,12 @@ export class SessionConversation extends React.Component<any, State> {
console.log(`[sending] Message Sending`);
}
public onMessageSuccess(){
public onMessageSuccess() {
console.log(`[sending] Message Sent`);
this.updateSendingProgress(100, 2);
}
public onMessageFailure(){
public onMessageFailure() {
console.log(`[sending] Message Failure`);
this.updateSendingProgress(100, -1);
}
@ -609,7 +659,7 @@ export class SessionConversation extends React.Component<any, State> {
// If you're not friends, don't mark anything as read. Otherwise
// this will automatically accept friend request.
const conversation = window.ConversationController.get(conversationKey);
if (!conversation.isFriend()){
if (conversation.isBlocked()) {
return;
}
@ -640,7 +690,8 @@ export class SessionConversation extends React.Component<any, State> {
const { messages, unreadCount } = this.state;
const { length } = messages;
const viewportBottom = (messageContainer?.clientHeight + messageContainer?.scrollTop) || 0;
const viewportBottom =
messageContainer?.clientHeight + messageContainer?.scrollTop || 0;
// Start with the most recent message, search backwards in time
let foundUnread = 0;
@ -697,27 +748,32 @@ export class SessionConversation extends React.Component<any, State> {
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
public async handleScroll() {
const messageContainer = this.messageContainerRef.current;
if (!messageContainer){
if (!messageContainer) {
return;
}
const scrollTop = messageContainer.scrollTop;
const scrollHeight = messageContainer.scrollHeight;
const clientHeight = messageContainer.clientHeight;
const scrollButtonViewShowLimit = 0.75;
const scrollButtonViewHideLimit = 0.40;
const scrollButtonViewHideLimit = 0.4;
const scrollOffsetPx = scrollHeight - scrollTop - clientHeight;
const scrollOffsetPc = scrollOffsetPx / clientHeight;
// Scroll button appears if you're more than 75% scrolled up
if (scrollOffsetPc > scrollButtonViewShowLimit && !this.state.showScrollButton){
this.setState({showScrollButton: true});
if (
scrollOffsetPc > scrollButtonViewShowLimit &&
!this.state.showScrollButton
) {
this.setState({ showScrollButton: true });
}
// Scroll button disappears if you're more less than 40% scrolled up
if (scrollOffsetPc < scrollButtonViewHideLimit && this.state.showScrollButton){
this.setState({showScrollButton: false});
if (
scrollOffsetPc < scrollButtonViewHideLimit &&
this.state.showScrollButton
) {
this.setState({ showScrollButton: false });
}
console.log(`[scroll] scrollOffsetPx: `, scrollOffsetPx);
@ -733,26 +789,31 @@ export class SessionConversation extends React.Component<any, State> {
this.updateReadMessages();
// Pin scroll to bottom on new message, unless user has scrolled up
if (this.state.isScrolledToBottom !== isScrolledToBottom){
if (this.state.isScrolledToBottom !== isScrolledToBottom) {
this.setState({ isScrolledToBottom });
}
// Fetch more messages when nearing the top of the message list
const shouldFetchMoreMessages = scrollTop <= window.CONSTANTS.MESSAGE_CONTAINER_BUFFER_OFFSET_PX;
const shouldFetchMoreMessages =
scrollTop <= window.CONSTANTS.MESSAGE_CONTAINER_BUFFER_OFFSET_PX;
if (shouldFetchMoreMessages){
const numMessages = this.state.messages.length + window.CONSTANTS.DEFAULT_MESSAGE_FETCH_COUNT;
if (shouldFetchMoreMessages) {
const numMessages =
this.state.messages.length +
window.CONSTANTS.DEFAULT_MESSAGE_FETCH_COUNT;
// Prevent grabbing messags with scroll more frequently than once per 5s.
const messageFetchInterval = 2;
const previousTopMessage = (await this.getMessages(numMessages, messageFetchInterval, true))?.previousTopMessage;
const previousTopMessage = (
await this.getMessages(numMessages, messageFetchInterval, true)
)?.previousTopMessage;
previousTopMessage && this.scrollToMessage(previousTopMessage);
}
}
public scrollToUnread() {
const { messages, unreadCount } = this.state;
const message = messages[(messages.length - 1) - unreadCount];
const message = messages[messages.length - 1 - unreadCount];
if (message) {
this.scrollToMessage(message.id);
@ -774,7 +835,8 @@ export class SessionConversation extends React.Component<any, State> {
if (!messageContainer) {
return;
}
messageContainer.scrollTop = messageContainer.scrollHeight - messageContainer.clientHeight;
messageContainer.scrollTop =
messageContainer.scrollHeight - messageContainer.clientHeight;
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -782,15 +844,15 @@ export class SessionConversation extends React.Component<any, State> {
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
public selectMessage(messageId: string) {
const selectedMessages = this.state.selectedMessages.includes(messageId)
// Add to array if not selected. Else remove.
? this.state.selectedMessages.filter(id => id !== messageId)
? // Add to array if not selected. Else remove.
this.state.selectedMessages.filter(id => id !== messageId)
: [...this.state.selectedMessages, messageId];
this.setState({ selectedMessages });
}
public resetSelection(){
this.setState({selectedMessages: []});
public resetSelection() {
this.setState({ selectedMessages: [] });
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -800,7 +862,7 @@ export class SessionConversation extends React.Component<any, State> {
this.setState({
showRecordingView: true,
selectedMessages: [],
})
});
}
private onExitVoiceNoteView() {
@ -823,7 +885,7 @@ export class SessionConversation extends React.Component<any, State> {
const pageHeight = messageContainer.clientHeight;
const arrowScrollPx = 50;
const pageScrollPx = 0.80 * pageHeight;
const pageScrollPx = 0.8 * pageHeight;
console.log(`[vince][key] event: `, event);
@ -833,7 +895,7 @@ export class SessionConversation extends React.Component<any, State> {
//
}
switch(event.key){
switch (event.key) {
case 'Escape':
if (selectionMode) {
this.resetSelection();
@ -855,6 +917,5 @@ export class SessionConversation extends React.Component<any, State> {
break;
default:
}
}
}

@ -1,82 +1,82 @@
export interface MessageFetchType {
messages: Array<any>,
messageFetchTimestamp: number,
newTopMessage: any,
previousTopMessage: any,
messages: Array<any>;
messageFetchTimestamp: number;
newTopMessage: any;
previousTopMessage: any;
}
export async function getMessages(
conversationKey: string,
currentMessages: Array<any>,
messageFetchTimestamp: number,
unreadCount: number,
onGotMessages?: any,
numMessages?: number,
fetchInterval = window.CONSTANTS.MESSAGE_FETCH_INTERVAL,
loopback = false,
){
const timestamp = getTimestamp();
conversationKey: string,
currentMessages: Array<any>,
messageFetchTimestamp: number,
unreadCount: number,
onGotMessages?: any,
numMessages?: number,
fetchInterval = window.CONSTANTS.MESSAGE_FETCH_INTERVAL,
loopback = false
) {
const timestamp = getTimestamp();
// If we have pulled messages in the last interval, don't bother rescanning
// This avoids getting messages on every re-render.
const timeBuffer = timestamp - messageFetchTimestamp;
if (timeBuffer < fetchInterval) {
// Loopback gets messages after time has elapsed,
// rather than completely cancelling the fetch.
// if (loopback) {
// setTimeout(() => {
// this.getMessages(numMessages, fetchInterval, false);
// }, timeBuffer * 1000);
// }
// If we have pulled messages in the last interval, don't bother rescanning
// This avoids getting messages on every re-render.
const timeBuffer = timestamp - messageFetchTimestamp;
if (timeBuffer < fetchInterval) {
// Loopback gets messages after time has elapsed,
// rather than completely cancelling the fetch.
// if (loopback) {
// setTimeout(() => {
// this.getMessages(numMessages, fetchInterval, false);
// }, timeBuffer * 1000);
// }
return { newTopMessage: undefined, previousTopMessage: undefined };
}
return { newTopMessage: undefined, previousTopMessage: undefined };
}
let msgCount = numMessages || window.CONSTANTS.DEFAULT_MESSAGE_FETCH_COUNT + unreadCount;
msgCount = msgCount > window.CONSTANTS.MAX_MESSAGE_FETCH_COUNT
let msgCount =
numMessages || window.CONSTANTS.DEFAULT_MESSAGE_FETCH_COUNT + unreadCount;
msgCount =
msgCount > window.CONSTANTS.MAX_MESSAGE_FETCH_COUNT
? window.CONSTANTS.MAX_MESSAGE_FETCH_COUNT
: msgCount;
const messageSet = await window.Signal.Data.getMessagesByConversation(
conversationKey,
{ limit: msgCount, MessageCollection: window.Whisper.MessageCollection },
);
// Set first member of series here.
const messageModels = messageSet.models;
const messages = [];
let previousSender;
for (let i = 0; i < messageModels.length; i++){
// Handle firstMessageOfSeries for conditional avatar rendering
let firstMessageOfSeries = true;
if (i > 0 && previousSender === messageModels[i].authorPhoneNumber){
firstMessageOfSeries = false;
}
const messageSet = await window.Signal.Data.getMessagesByConversation(
conversationKey,
{ limit: msgCount, MessageCollection: window.Whisper.MessageCollection }
);
messages.push({...messageModels[i], firstMessageOfSeries});
previousSender = messageModels[i].authorPhoneNumber;
// Set first member of series here.
const messageModels = messageSet.models;
const messages = [];
let previousSender;
for (let i = 0; i < messageModels.length; i++) {
// Handle firstMessageOfSeries for conditional avatar rendering
let firstMessageOfSeries = true;
if (i > 0 && previousSender === messageModels[i].authorPhoneNumber) {
firstMessageOfSeries = false;
}
const previousTopMessage = currentMessages[0]?.id;
const newTopMessage = messages[0]?.id;
messages.push({ ...messageModels[i], firstMessageOfSeries });
previousSender = messageModels[i].authorPhoneNumber;
}
const previousTopMessage = currentMessages[0]?.id;
const newTopMessage = messages[0]?.id;
messageFetchTimestamp = getTimestamp();
messageFetchTimestamp = getTimestamp();
// Callback to onGotMessages
if (onGotMessages) onGotMessages(
messages,
messageFetchTimestamp,
previousTopMessage,
newTopMessage,
// Callback to onGotMessages
if (onGotMessages)
onGotMessages(
messages,
messageFetchTimestamp,
previousTopMessage,
newTopMessage
);
return { newTopMessage, previousTopMessage };
}
return { newTopMessage, previousTopMessage };
}
export function getTimestamp(asInt = false){
const timestamp = Date.now() / 1000;
return asInt ? Math.floor(timestamp) : timestamp;
}
export function getTimestamp(asInt = false) {
const timestamp = Date.now() / 1000;
return asInt ? Math.floor(timestamp) : timestamp;
}

@ -4,8 +4,12 @@ import moment from 'moment';
import { getTimestamp } from './SessionConversationManager';
import { SessionIconButton, SessionIconSize, SessionIconType } from '../icon';
import { SessionButton, SessionButtonType, SessionButtonColor } from '../SessionButton';
import { SessionIconButton, SessionIconSize, SessionIconType } from '../icon';
import {
SessionButton,
SessionButtonType,
SessionButtonColor,
} from '../SessionButton';
interface Props {
sendVoiceMessage: any;
@ -25,12 +29,12 @@ interface State {
// Steam information and data
mediaBlob?: any;
audioElement?: HTMLAudioElement;
streamParams?: {
streamParams?: {
stream: any;
media: any;
input: any;
input: any;
processor: any;
}
};
canvasParams: {
width: number;
@ -42,11 +46,11 @@ interface State {
barColorPlay: string;
maxBarHeight: number;
minBarHeight: number;
}
};
startTimestamp: number;
nowTimestamp: number;
updateTimerInterval: NodeJS.Timeout;
}
@ -61,7 +65,7 @@ export class SessionRecording extends React.Component<Props, State> {
// Mouse interaction
this.handleHoverActions = this.handleHoverActions.bind(this);
this.handleUnhoverActions = this.handleUnhoverActions.bind(this);
// Component actions
this.playAudio = this.playAudio.bind(this);
this.pauseAudio = this.pauseAudio.bind(this);
@ -87,7 +91,7 @@ export class SessionRecording extends React.Component<Props, State> {
const now = getTimestamp();
const updateTimerInterval = setInterval(this.timerUpdate, 500);
this.state = {
recordDuration: 0,
isRecording: true,
@ -98,7 +102,7 @@ export class SessionRecording extends React.Component<Props, State> {
mediaBlob: undefined,
audioElement: undefined,
streamParams: undefined,
startTimestamp: now,
nowTimestamp: now,
updateTimerInterval,
@ -116,10 +120,9 @@ export class SessionRecording extends React.Component<Props, State> {
minBarHeight: 3,
},
};
}
public async componentWillMount(){
public async componentWillMount() {
// This turns on the microphone on the system. Later we need to turn it off.
this.initiateRecordingStream();
}
@ -129,15 +132,15 @@ export class SessionRecording extends React.Component<Props, State> {
this.updateCanvasDimensions();
}
public componentWillUnmount(){
public componentWillUnmount() {
clearInterval(this.state.updateTimerInterval);
window.removeEventListener('resize', this.updateCanvasDimensions);
}
public componentDidUpdate() {
const { audioElement, isPlaying } = this.state;
if (audioElement){
if (audioElement) {
if (isPlaying) {
audioElement.play();
} else {
@ -160,16 +163,18 @@ export class SessionRecording extends React.Component<Props, State> {
const actionStopRecording = actionHover && isRecording;
const actionPlayAudio = !isRecording && !isPlaying;
const actionPauseAudio = !isRecording && !isPaused && isPlaying;
const actionDefault = !actionStopRecording && !actionPlayAudio && !actionPauseAudio;
const actionDefault =
!actionStopRecording && !actionPlayAudio && !actionPauseAudio;
const displayTimeMs = isRecording
? (nowTimestamp - startTimestamp) * 1000
: audioElement && audioElement?.currentTime * 1000 || 0;
: (audioElement && audioElement?.currentTime * 1000) || 0;
const displayTimeString = moment.utc(displayTimeMs).format('m:ss');
const actionPauseFn = isPlaying ? this.pauseAudio : this.stopRecordingStream;
const actionPauseFn = isPlaying
? this.pauseAudio
: this.stopRecordingStream;
return (
<div
@ -178,9 +183,9 @@ export class SessionRecording extends React.Component<Props, State> {
onKeyDown={this.onKeyDown}
>
<div
className="session-recording--actions"
onMouseEnter={this.handleHoverActions}
onMouseLeave={this.handleUnhoverActions}
className="session-recording--actions"
onMouseEnter={this.handleHoverActions}
onMouseLeave={this.handleUnhoverActions}
>
{actionStopRecording && (
<SessionIconButton
@ -207,7 +212,7 @@ export class SessionRecording extends React.Component<Props, State> {
onClick={this.playAudio}
/>
)}
{actionDefault && (
<SessionIconButton
iconType={SessionIconType.Microphone}
@ -217,22 +222,26 @@ export class SessionRecording extends React.Component<Props, State> {
</div>
<div
className="session-recording--visualisation"
ref={this.visualisationRef}
className="session-recording--visualisation"
ref={this.visualisationRef}
>
{!isRecording && <canvas ref={this.playbackCanvas}></canvas>}
{isRecording && <canvas ref={this.visualisationCanvas}></canvas>}
</div>
<div className={classNames('session-recording--timer', !isRecording && 'playback-timer')}>
{ displayTimeString }
{ isRecording && (
<div
className={classNames(
'session-recording--timer',
!isRecording && 'playback-timer'
)}
>
{displayTimeString}
{isRecording && (
<div className="session-recording--timer-light"></div>
)}
</div>
{ !isRecording && (
{!isRecording && (
<div className="send-message-button">
<SessionIconButton
iconType={SessionIconType.Send}
@ -245,7 +254,7 @@ export class SessionRecording extends React.Component<Props, State> {
)}
<div className="session-recording--status">
{ isRecording ? (
{isRecording ? (
<SessionButton
text={window.i18n('recording')}
buttonType={SessionButtonType.Brand}
@ -259,47 +268,45 @@ export class SessionRecording extends React.Component<Props, State> {
onClick={this.onDeleteVoiceMessage}
/>
)}
</div>
</div>
);
}
private handleHoverActions() {
if ((this.state.isRecording) && !this.state.actionHover) {
this.setState({
actionHover: true,
});
if (this.state.isRecording && !this.state.actionHover) {
this.setState({
actionHover: true,
});
}
}
private timerUpdate(){
private timerUpdate() {
const { nowTimestamp, startTimestamp } = this.state;
const elapsedTime = (nowTimestamp - startTimestamp);
const elapsedTime = nowTimestamp - startTimestamp;
// Prevent voice messages exceeding max length.
if (elapsedTime >= window.CONSTANTS.MAX_VOICE_MESSAGE_DURATION){
if (elapsedTime >= window.CONSTANTS.MAX_VOICE_MESSAGE_DURATION) {
this.stopRecordingStream();
}
this.setState({
nowTimestamp: getTimestamp()
nowTimestamp: getTimestamp(),
});
}
private handleUnhoverActions() {
if (this.state.isRecording && this.state.actionHover) {
this.setState({
actionHover: false,
});
this.setState({
actionHover: false,
});
}
}
private async stopRecording() {
this.setState({
isRecording: false,
isPaused: true,
isRecording: false,
isPaused: true,
});
}
@ -307,11 +314,11 @@ export class SessionRecording extends React.Component<Props, State> {
// Generate audio element if it doesn't exist
const generateAudioElement = () => {
const { mediaBlob, recordDuration } = this.state;
if (!mediaBlob){
if (!mediaBlob) {
return undefined;
}
const audioURL = window.URL.createObjectURL(mediaBlob.data);
const audioElement = new Audio(audioURL);
@ -323,50 +330,47 @@ export class SessionRecording extends React.Component<Props, State> {
if (duration && audioElement.currentTime < duration) {
audioElement.play();
}
};
return audioElement;
}
};
const audioElement = this.state.audioElement || generateAudioElement();
if (!audioElement) return;
// Draw sweeping timeline
const drawSweepingTimeline = () => {
const { isPaused } = this.state;
const {
width,
height,
barColorPlay,
} = this.state.canvasParams;
const { width, height, barColorPlay } = this.state.canvasParams;
const canvas = this.playbackCanvas.current;
if ( !canvas || isPaused ) return;
if (!canvas || isPaused) return;
// Once audioElement is fully buffered, we get the true duration
let audioDuration = this.state.recordDuration
if (audioElement.duration !== Infinity) audioDuration = audioElement.duration;
let audioDuration = this.state.recordDuration;
if (audioElement.duration !== Infinity)
audioDuration = audioElement.duration;
const progress = width * (audioElement.currentTime / audioDuration);
const canvasContext = canvas.getContext(`2d`);
if (!canvasContext) return;
canvasContext.beginPath();
canvasContext.fillStyle = barColorPlay
canvasContext.fillStyle = barColorPlay;
canvasContext.globalCompositeOperation = 'source-atop';
canvasContext.fillRect(0, 0, progress, height);
// Pause audio when it reaches the end of the blob
if (audioElement.duration && audioElement.currentTime === audioElement.duration){
if (
audioElement.duration &&
audioElement.currentTime === audioElement.duration
) {
this.pauseAudio();
return;
}
requestAnimationFrame(drawSweepingTimeline);
}
};
this.setState({
audioElement,
@ -375,17 +379,18 @@ export class SessionRecording extends React.Component<Props, State> {
isPlaying: true,
});
// If end of audio reached, reset the position of the sweeping timeline
if (audioElement.duration && audioElement.currentTime === audioElement.duration){
if (
audioElement.duration &&
audioElement.currentTime === audioElement.duration
) {
this.initPlaybackView();
}
audioElement.play();
requestAnimationFrame(drawSweepingTimeline);
}
private pauseAudio() {
this.state.audioElement?.pause();
@ -409,7 +414,9 @@ export class SessionRecording extends React.Component<Props, State> {
// Is the audio file > attachment filesize limit
if (audioBlob.size > window.CONSTANTS.MAX_ATTACHMENT_FILESIZE) {
console.log(`[send] Voice message too large: ${audioBlob.size / 1000000} MB`);
console.log(
`[send] Voice message too large: ${audioBlob.size / 1000000} MB`
);
return;
}
@ -419,23 +426,27 @@ export class SessionRecording extends React.Component<Props, State> {
}
private async initiateRecordingStream() {
navigator.getUserMedia({audio:true}, this.onRecordingStream, this.onStreamError);
navigator.getUserMedia(
{ audio: true },
this.onRecordingStream,
this.onStreamError
);
}
private stopRecordingStream() {
const { streamParams} = this.state;
const { streamParams } = this.state;
// Exit if parameters aren't yet set
if (!streamParams){
if (!streamParams) {
return;
}
// Stop the stream
if (streamParams.media.state !== 'inactive') streamParams.media.stop();
streamParams.input.disconnect();
streamParams.processor.disconnect();
streamParams.stream.getTracks().forEach((track: any) => track.stop);
// Stop recording
this.stopRecording();
}
@ -448,9 +459,9 @@ export class SessionRecording extends React.Component<Props, State> {
}
// Start recording the stream
const media = new window.MediaRecorder(stream, {mimeType: 'audio/webm'});
const media = new window.MediaRecorder(stream, { mimeType: 'audio/webm' });
media.ondataavailable = (mediaBlob: any) => {
this.setState({mediaBlob}, () => {
this.setState({ mediaBlob }, () => {
// Generate PCM waveform for playback
this.initPlaybackView();
});
@ -460,7 +471,7 @@ export class SessionRecording extends React.Component<Props, State> {
// Audio Context
const audioContext = new window.AudioContext();
const input = audioContext.createMediaStreamSource(stream);
const bufferSize = 1024;
const analyser = audioContext.createAnalyser();
analyser.smoothingTimeConstant = 0.3;
@ -469,8 +480,8 @@ export class SessionRecording extends React.Component<Props, State> {
const processor = audioContext.createScriptProcessor(bufferSize, 1, 1);
processor.onaudioprocess = () => {
const streamParams = {stream, media, input, processor};
this.setState({streamParams});
const streamParams = { stream, media, input, processor };
this.setState({ streamParams });
const {
width,
@ -479,7 +490,7 @@ export class SessionRecording extends React.Component<Props, State> {
barPadding,
barColorInit,
maxBarHeight,
minBarHeight
minBarHeight,
} = this.state.canvasParams;
// Array of volumes by frequency (not in Hz, arbitrary unit)
@ -487,54 +498,52 @@ export class SessionRecording extends React.Component<Props, State> {
analyser.getByteFrequencyData(freqTypedArray);
const freqArray = Array.from(freqTypedArray);
// CANVAS CONTEXT
const drawRecordingCanvas = () => {
const canvas = this.visualisationCanvas.current;
const numBars = width / (barPadding + barWidth);
let volumeArray = freqArray.map(n => {
const maxVal = Math.max(...freqArray);
const initialHeight = maxBarHeight * (n / maxVal);
const freqBarHeight = initialHeight > minBarHeight
? initialHeight
: minBarHeight;
const freqBarHeight =
initialHeight > minBarHeight ? initialHeight : minBarHeight;
return freqBarHeight;
});
// Create initial fake bars to improve appearance.
// Gradually increasing wave rather than a wall at the beginning
const frontLoadLen = Math.ceil(volumeArray.length / 10);
const frontLoad = volumeArray.slice(0, frontLoadLen - 1).reverse().map(n => n * 0.80);
const frontLoad = volumeArray
.slice(0, frontLoadLen - 1)
.reverse()
.map(n => n * 0.8);
volumeArray = [...frontLoad, ...volumeArray];
// Chop off values which exceed the bounds of the container
volumeArray = volumeArray.slice(0, numBars);
canvas && (canvas.height = height);
canvas && (canvas.width = width);
const canvasContext = canvas && (canvas.getContext(`2d`));
const canvasContext = canvas && canvas.getContext(`2d`);
for (var i = 0; i < volumeArray.length; i++) {
const barHeight = Math.ceil(volumeArray[i]);
const offset_x = Math.ceil(i * (barWidth + barPadding));
const offset_y = Math.ceil((height / 2 ) - (barHeight / 2 ));
const offset_y = Math.ceil(height / 2 - barHeight / 2);
// FIXME VINCE - Globalise JS references to colors
canvasContext && (canvasContext.fillStyle = barColorInit);
canvasContext && this.drawRoundedRect(
canvasContext,
offset_x,
offset_y,
barHeight,
);
canvasContext &&
this.drawRoundedRect(canvasContext, offset_x, offset_y, barHeight);
}
}
};
this.state.isRecording && requestAnimationFrame(drawRecordingCanvas);
}
};
// Init listeners for visualisation
input.connect(analyser);
@ -552,18 +561,18 @@ export class SessionRecording extends React.Component<Props, State> {
// Eg. [73, 6, 1, 9, 5, 11, 2, 19, 35] of groupSize 3, becomes
// = [(73 + 6 + 1) / 3 + (9 + 5 + 11) / 3 + (2 + 19 + 35) / 3]
// = [27, 8, 19]
// It's used to get a fixed number of freqBars or volumeBars out of
// It's used to get a fixed number of freqBars or volumeBars out of
// a huge sample array.
const groupSize = Math.floor(array.length / numGroups);
let compacted = new Float32Array(numGroups);
let sum = 0;
for (let i = 0; i < array.length; i++){
for (let i = 0; i < array.length; i++) {
sum += array[i];
if ((i + 1) % groupSize === 0){
const compactedIndex = ((i + 1) / groupSize)
if ((i + 1) % groupSize === 0) {
const compactedIndex = (i + 1) / groupSize;
const average = sum / groupSize;
compacted[compactedIndex] = average;
sum = 0;
@ -581,7 +590,7 @@ export class SessionRecording extends React.Component<Props, State> {
barPadding,
barColorInit,
maxBarHeight,
minBarHeight
minBarHeight,
} = this.state.canvasParams;
const numBars = width / (barPadding + barWidth);
@ -589,30 +598,30 @@ export class SessionRecording extends React.Component<Props, State> {
// Scan through audio file getting average volume per bar
// to display amplitude over time as a static image
const blob = this.state.mediaBlob.data;
const arrayBuffer = await new Response(blob).arrayBuffer();
const audioContext = new window.AudioContext();
audioContext.decodeAudioData(arrayBuffer, (buffer: AudioBuffer) => {
this.setState({
recordDuration: buffer.duration
recordDuration: buffer.duration,
});
// Get audio amplitude with PCM Data in Float32
// Grab single channel only to save compuation
const channelData = buffer.getChannelData(0);
const pcmData = this.compactPCM(channelData, numBars);
const pcmDataArray = Array.from(pcmData);
const pcmDataArrayNormalised = pcmDataArray.map(v => Math.abs(v));
// Prepare values for drawing to canvas
const maxAmplitude = Math.max(...pcmDataArrayNormalised);
const barSizeArray = pcmDataArrayNormalised.map(amplitude => {
let barSize = maxBarHeight * (amplitude / maxAmplitude);
// Prevent values that are too small
if (barSize < minBarHeight){
if (barSize < minBarHeight) {
barSize = minBarHeight;
}
@ -621,60 +630,58 @@ export class SessionRecording extends React.Component<Props, State> {
// CANVAS CONTEXT
const drawPlaybackCanvas = () => {
const canvas = this.playbackCanvas.current;
if (!canvas) return;
canvas.height = height;
canvas.width = width;
const canvasContext = canvas.getContext(`2d`);
if (!canvasContext) return;
for (let i = 0; i < barSizeArray.length; i++){
for (let i = 0; i < barSizeArray.length; i++) {
const barHeight = Math.ceil(barSizeArray[i]);
const offset_x = Math.ceil(i * (barWidth + barPadding));
const offset_y = Math.ceil((height / 2 ) - (barHeight / 2 ));
const offset_y = Math.ceil(height / 2 - barHeight / 2);
// FIXME VINCE - Globalise JS references to colors
canvasContext.fillStyle = barColorInit;
this.drawRoundedRect(
canvasContext,
offset_x,
offset_y,
barHeight,
);
this.drawRoundedRect(canvasContext, offset_x, offset_y, barHeight);
}
}
};
drawPlaybackCanvas();
});
}
private drawRoundedRect (ctx: CanvasRenderingContext2D, x: number, y: number, h: number) {
private drawRoundedRect(
ctx: CanvasRenderingContext2D,
x: number,
y: number,
h: number
) {
let r = this.state.canvasParams.barRadius;
const w = this.state.canvasParams.barWidth;
if (w < 2 * r) r = w / 2;
if (h < 2 * r) r = h / 2;
ctx.beginPath();
ctx.moveTo(x+r, y);
ctx.arcTo(x+w, y, x+w, y+h, r);
ctx.arcTo(x+w, y+h, x, y+h, r);
ctx.arcTo(x, y+h, x, y, r);
ctx.arcTo(x, y, x+w, y, r);
ctx.moveTo(x + r, y);
ctx.arcTo(x + w, y, x + w, y + h, r);
ctx.arcTo(x + w, y + h, x, y + h, r);
ctx.arcTo(x, y + h, x, y, r);
ctx.arcTo(x, y, x + w, y, r);
ctx.closePath();
ctx.fill();
}
private updateCanvasDimensions(){
const canvas = this.visualisationCanvas.current || this.playbackCanvas.current;
private updateCanvasDimensions() {
const canvas =
this.visualisationCanvas.current || this.playbackCanvas.current;
const width = canvas?.clientWidth || 0;
this.setState({
canvasParams: {...this.state.canvasParams, width}
canvasParams: { ...this.state.canvasParams, width },
});
}
@ -684,5 +691,4 @@ export class SessionRecording extends React.Component<Props, State> {
this.onDeleteVoiceMessage();
}
}
}

@ -225,4 +225,3 @@ export const icons = {
viewBox: '0 0 486.463 486.463',
},
};

@ -3,21 +3,18 @@ import { mapDispatchToProps } from '../actions';
import { SessionConversation } from '../../components/session/conversation/SessionConversation';
import { StateType } from '../reducer';
const mapStateToProps = (state: StateType) => {
// Get messages here!!!!!
// FIXME VINCE: Get messages for all conversations, not just this one
// Store as object of objects with key refs
// console.log(`[update] State from dispatch:`, state);
// const message: Array<any> = [];
// if(state.conversations) {
// const conversationKey = state.conversations.selectedConversation;
// // FIXME VINCE: msgCount should not be a magic number
// const msgCount = 30;
@ -39,7 +36,6 @@ const mapStateToProps = (state: StateType) => {
// previousSender = messageModels[i].authorPhoneNumber;
// }
// }
return {
conversations: state.conversations,

@ -351,14 +351,13 @@
resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-5.0.0.tgz#a3014921991066193f6c8e47290d4d598dfd19e6"
integrity sha512-ZS0vBV7Jn5Z/Q4T3VXauEKMDCV8nWOtJJg90OsDylkYJiQwcWtKuLzohWzrthBkerUF7DLMmJcwOPEP0i/AOXw==
<<<<<<< HEAD
"@types/moment@^2.13.0":
version "2.13.0"
resolved "https://registry.yarnpkg.com/@types/moment/-/moment-2.13.0.tgz#604ebd189bc3bc34a1548689404e61a2a4aac896"
integrity sha1-YE69GJvDvDShVIaJQE5hoqSqyJY=
dependencies:
moment "*"
=======
"@types/node-fetch@^2.5.7":
version "2.5.7"
resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.5.7.tgz#20a2afffa882ab04d44ca786449a276f9f6bbf3c"
@ -366,7 +365,6 @@
dependencies:
"@types/node" "*"
form-data "^3.0.0"
>>>>>>> d5cfcf9edc2e788540f470b6c168de21ab93bfd7
"@types/node@*":
version "13.9.1"
@ -2860,20 +2858,7 @@ diff@3.3.1:
resolved "https://registry.yarnpkg.com/diff/-/diff-3.3.1.tgz#aa8567a6eed03c531fc89d3f711cd0e5259dec75"
integrity sha512-MKPHZDMB0o6yHyDryUOScqZibp914ksXwAMYMTHj6KO8UeKsRYNJD3oNCKjTqZon+V488P7N/HzXF8t7ZR95ww==
<<<<<<< HEAD
diff@^3.1.0:
=======
diff@^3.2.0:
>>>>>>> d5cfcf9edc2e788540f470b6c168de21ab93bfd7
version "3.5.0"
resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12"
integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==
<<<<<<< HEAD
diff@^4.0.1:
=======
diff@^4.0.2:
>>>>>>> d5cfcf9edc2e788540f470b6c168de21ab93bfd7
diff@^4.0.1, diff@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
@ -10258,85 +10243,60 @@ ts-loader@4.1.0:
micromatch "^3.1.4"
semver "^5.0.1"
<<<<<<< HEAD
tslib@^1.10.0, tslib@^1.8.1:
=======
ts-mock-imports@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/ts-mock-imports/-/ts-mock-imports-1.3.0.tgz#ed9b743349f3c27346afe5b7454ffd2bcaa2302d"
integrity sha512-cCrVcRYsp84eDvPict0ZZD/D7ppQ0/JSx4ve6aEU8DjlsaWRJWV6ADMovp2sCuh6pZcduLFoIYhKTDU2LARo7Q==
tslib@^1.8.0, tslib@^1.8.1:
>>>>>>> d5cfcf9edc2e788540f470b6c168de21ab93bfd7
version "1.11.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.1.tgz#eb15d128827fbee2841549e171f45ed338ac7e35"
integrity sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==
tslint-microsoft-contrib@^6.2.0:
version "6.2.0"
resolved "https://registry.yarnpkg.com/tslint-microsoft-contrib/-/tslint-microsoft-contrib-6.2.0.tgz#8aa0f40584d066d05e6a5e7988da5163b85f2ad4"
integrity sha512-6tfi/2tHqV/3CL77pULBcK+foty11Rr0idRDxKnteTaKm6gWF9qmaCNU17HVssOuwlYNyOmd9Jsmjd+1t3a3qw==
tslint-microsoft-contrib@6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/tslint-microsoft-contrib/-/tslint-microsoft-contrib-6.0.0.tgz#7bff73c9ad7a0b7eb5cdb04906de58f42a2bf7a2"
integrity sha512-R//efwn+34IUjTJeYgNDAJdzG0jyLWIehygPt/PHuZAieTolFVS56FgeFW7DOLap9ghXzMiFPTmDgm54qaL7QA==
dependencies:
tsutils "^2.27.2 <2.29.0"
tslint-react@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/tslint-react/-/tslint-react-4.2.0.tgz#41b16e0438365f8d3ed4120501f02cabff9fd1e4"
integrity sha512-lO22+FKr9ZZGueGiuALzvZE/8ANoDoCHGCknX1Ge3ALrfcLQHQ1VGdyb1scZXQFdEQEfwBTIU40r5BUlJpn0JA==
tslint-react@3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/tslint-react/-/tslint-react-3.6.0.tgz#7f462c95c4a0afaae82507f06517ff02942196a1"
integrity sha512-AIv1QcsSnj7e9pFir6cJ6vIncTqxfqeFF3Lzh8SuuBljueYzEAtByuB6zMaD27BL0xhMEqsZ9s5eHuCONydjBw==
dependencies:
tsutils "^3.9.1"
tsutils "^2.13.1"
<<<<<<< HEAD
tslint@^6.1.1:
version "6.1.1"
resolved "https://registry.yarnpkg.com/tslint/-/tslint-6.1.1.tgz#ac03fbd17f85bfefaae348b353b25a88efe10cde"
integrity sha512-kd6AQ/IgPRpLn6g5TozqzPdGNZ0q0jtXW4//hRcj10qLYBaa3mTUU2y2MCG+RXZm8Zx+KZi0eA+YCrMyNlF4UA==
=======
tslint@5.19.0:
version "5.19.0"
resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.19.0.tgz#a2cbd4a7699386da823f6b499b8394d6c47bb968"
integrity sha512-1LwwtBxfRJZnUvoS9c0uj8XQtAnyhWr9KlNvDIdB+oXyT+VpsOAaEhEgKi1HrZ8rq0ki/AAnbGSv4KM6/AfVZw==
>>>>>>> d5cfcf9edc2e788540f470b6c168de21ab93bfd7
dependencies:
"@babel/code-frame" "^7.0.0"
builtin-modules "^1.1.1"
chalk "^2.3.0"
commander "^2.12.1"
diff "^4.0.1"
diff "^3.2.0"
glob "^7.1.1"
js-yaml "^3.13.1"
minimatch "^3.0.4"
mkdirp "^0.5.3"
mkdirp "^0.5.1"
resolve "^1.3.2"
semver "^5.3.0"
<<<<<<< HEAD
tslib "^1.10.0"
tsutils "^2.29.0"
"tsutils@^2.27.2 <2.29.0":
version "2.28.0"
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.28.0.tgz#6bd71e160828f9d019b6f4e844742228f85169a1"
integrity sha512-bh5nAtW0tuhvOJnx1GLRn5ScraRLICGyJV5wJhtRWOLsxW70Kk5tZtpK3O/hW6LDnqKS9mlUMPZj9fEMJ0gxqA==
dependencies:
tslib "^1.8.1"
tsutils@^2.29.0:
=======
tslib "^1.8.0"
tsutils "^2.29.0"
tsutils@^2.13.1, tsutils@^2.29.0:
>>>>>>> d5cfcf9edc2e788540f470b6c168de21ab93bfd7
version "2.29.0"
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.29.0.tgz#32b488501467acbedd4b85498673a0812aca0b99"
integrity sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==
dependencies:
tslib "^1.8.1"
tsutils@^3.9.1:
version "3.17.1"
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759"
integrity sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g==
"tsutils@^2.27.2 <2.29.0":
version "2.28.0"
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.28.0.tgz#6bd71e160828f9d019b6f4e844742228f85169a1"
integrity sha512-bh5nAtW0tuhvOJnx1GLRn5ScraRLICGyJV5wJhtRWOLsxW70Kk5tZtpK3O/hW6LDnqKS9mlUMPZj9fEMJ0gxqA==
dependencies:
tslib "^1.8.1"
@ -10399,17 +10359,10 @@ typedarray@^0.0.6:
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
<<<<<<< HEAD
typescript@^3.8.3:
version "3.8.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.8.3.tgz#409eb8544ea0335711205869ec458ab109ee1061"
integrity sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==
=======
typescript@3.7.4:
version "3.7.4"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.4.tgz#1743a5ec5fef6a1fa9f3e4708e33c81c73876c19"
integrity sha512-A25xv5XCtarLwXpcDNZzCGvW2D1S3/bACratYBx2sax8PefsFhlYmkQicKHvpYflFS8if4zne5zT5kpJ7pzuvw==
>>>>>>> d5cfcf9edc2e788540f470b6c168de21ab93bfd7
uc.micro@^1.0.1:
version "1.0.6"

Loading…
Cancel
Save