Sending progress

pull/1102/head
Vincent 5 years ago
parent 5087e86f7d
commit 8e240d5218

@ -68,6 +68,7 @@ $composition-container-height: 60px;
&__content { &__content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 100%;
outline: none; outline: none;
} }
@ -76,7 +77,7 @@ $composition-container-height: 60px;
height: 100%; height: 100%;
right: 0vw; right: 0vw;
transition: transform $session-transition-duration ease-in-out; transition: transform 1.5 * $session-transition-duration ease-in-out;
transform: translateX(100%); transform: translateX(100%);
will-change: transform; will-change: transform;
@ -285,16 +286,23 @@ $composition-container-height: 60px;
.session-progress { .session-progress {
position: relative; position: relative;
background-color: rgba(30, 30, 30, 0.5); z-index: 100;
&__progress { &__progress {
transition: opacity 0.15s; transition: opacity 0.25s;
will-change: transform;
width: 100%;
position: absolute; position: absolute;
left: 0px; left: 0px;
font-size: 0px; font-size: 0px;
height: 3px; height: 3px;
background-color: $session-color-green; background-color: $session-color-green;
&.fade {
opacity: 0;
}
} }
} }

@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import classNames from 'classnames';
interface Props { interface Props {
// Value ranges from 0 to 100 // Value ranges from 0 to 100
@ -6,12 +7,12 @@ interface Props {
// Optional. Load with initial value and have // Optional. Load with initial value and have
// it shoot to new value immediately // it shoot to new value immediately
prevValue?: number; prevValue?: number;
sendStatus: -1 | 0 | 1 | 2;
visible: boolean; visible: boolean;
fadeOnComplete: boolean; fadeOnComplete: boolean;
} }
interface State { interface State {
value: number;
visible: boolean; visible: boolean;
startFade: boolean; startFade: boolean;
} }
@ -24,42 +25,39 @@ export class SessionProgress extends React.PureComponent<Props, State> {
constructor(props: any) { constructor(props: any) {
super(props); super(props);
const { visible, value, prevValue } = this.props; const { visible } = this.props;
this.state = { this.state = {
visible, visible,
startFade: false, startFade: false,
value: prevValue || value,
}; };
} }
public componentWillMount() {
setTimeout(() => {
this.setState({
value: this.props.value,
});
}, 20);
}
public render() { public render() {
const { startFade, value } = this.state; const { startFade } = this.state;
const { prevValue } = this.props; const { value, prevValue, sendStatus } = this.props;
// Duration will be the decimal (in seconds) of // Duration will be the decimal (in seconds) of
// the percentage differnce, else 0.25s; // the percentage differnce, else 0.25s;
// Minimum shift duration of 0.25s; // Minimum shift duration of 0.25s;
const shiftDuration = this.getShiftDuration(this.props.value, prevValue); const shiftDuration = this.getShiftDuration(this.props.value, prevValue).toFixed(2);
// 1. Width depends on progress. // 1. Width depends on progress.
// 2. Opacity is the inverse of fade. // 2. Opacity is the inverse of fade.
// 3. Transition duration scales with the // 3. Transition duration scales with the
// distance it needs to travel // distance it needs to travel
// FIXME VINCE - globalise all JS color references
const sessionBrandColor = '#00f782';
const sessionDangerAlt = '#ff4538';
const successColor = sessionBrandColor;
const failureColor = sessionDangerAlt;
const backgroundColor = sendStatus === 2 ? failureColor : successColor;
const style = { const style = {
width: `${this.state.value}%`, 'background-color': backgroundColor,
opacity: `${Number(!startFade)}`, transform: `translateX(-${100 - value}%)`,
transition: `width ${shiftDuration.toFixed( transition: `transform ${shiftDuration}s cubic-bezier(0.25, 0.46, 0.45, 0.94)`,
2
)}s cubic-bezier(0.25, 0.46, 0.45, 0.94)`,
}; };
if (value >= 100) { if (value >= 100) {
@ -68,7 +66,10 @@ export class SessionProgress extends React.PureComponent<Props, State> {
return ( return (
<div className="session-progress"> <div className="session-progress">
<div className="session-progress__progress" style={style}> <div
className={classNames('session-progress__progress', startFade && 'fade')}
style={style}
>
&nbsp &nbsp
</div> </div>
</div> </div>

@ -15,7 +15,11 @@ import { SignalService } from '../../../../ts/protobuf';
interface Props { interface Props {
placeholder?: string; placeholder?: string;
sendMessage: any; sendMessage: any;
onMessageSending: any;
onMessageSuccess: any;
onMessageFailure: any;
onLoadVoiceNoteView: any; onLoadVoiceNoteView: any;
onExitVoiceNoteView: any; onExitVoiceNoteView: any;
@ -270,23 +274,30 @@ export class SessionCompositionBox extends React.Component<Props, State> {
// Send message // Send message
const messageSuccess = this.props.sendMessage( this.props.onMessageSending();
this.props.sendMessage(
messagePlaintext, messagePlaintext,
attachments, attachments,
undefined, undefined,
undefined, undefined,
null, null,
{}, {},
); ).then(() => {
// Message sending sucess
this.props.onMessageSuccess();
if (messageSuccess) {
// Empty attachments // Empty attachments
// Empty composition box // Empty composition box
this.setState({ this.setState({
message: '', message: '',
attachments: [], attachments: [],
}); });
} }).catch(() => {
// Message sending failed
this.props.onMessageFailure();
});
} }
private async sendVoiceMessage(audioBlob: Blob) { private async sendVoiceMessage(audioBlob: Blob) {
@ -333,7 +344,7 @@ export class SessionCompositionBox extends React.Component<Props, State> {
} }
window.pushToast({ window.pushToast({
id: window.generateID(), id: 'audioPermissionNeeded',
title: window.i18n('audioPermissionNeededTitle'), title: window.i18n('audioPermissionNeededTitle'),
description: window.i18n('audioPermissionNeededDescription'), description: window.i18n('audioPermissionNeededDescription'),
type: 'info', type: 'info',

@ -16,8 +16,14 @@ import { SessionGroupSettings } from './SessionGroupSettings';
interface State { interface State {
conversationKey: string; conversationKey: string;
sendingProgess: number; sendingProgress: number;
prevSendingProgess: number; prevSendingProgress: number;
// Sending failed: -1
// Not send yet: 0
// Sending message: 1
// Sending success: 2
sendingProgressStatus: -1 | 0 | 1 | 2;
unreadCount: number; unreadCount: number;
messages: Array<any>; messages: Array<any>;
selectedMessages: Array<string>; selectedMessages: Array<string>;
@ -36,15 +42,16 @@ export class SessionConversation extends React.Component<any, State> {
constructor(props: any) { constructor(props: any) {
super(props); super(props);
console.log(`[conv] Props:`, props);
const conversationKey = this.props.conversations.selectedConversation; const conversationKey = this.props.conversations.selectedConversation;
const conversation = this.props.conversations.conversationLookup[conversationKey]; const conversation = this.props.conversations.conversationLookup[conversationKey];
const unreadCount = conversation.unreadCount; const unreadCount = conversation.unreadCount;
console.log(`[conv] Conversation:`, conversation);
this.state = { this.state = {
sendingProgess: 0, sendingProgress: 0,
prevSendingProgess: 0, prevSendingProgress: 0,
sendingProgressStatus: 0,
conversationKey, conversationKey,
unreadCount, unreadCount,
messages: [], messages: [],
@ -54,7 +61,7 @@ export class SessionConversation extends React.Component<any, State> {
displayScrollToBottomButton: false, displayScrollToBottomButton: false,
messageFetchTimestamp: 0, messageFetchTimestamp: 0,
showRecordingView: false, showRecordingView: false,
showOptionsPane: true, showOptionsPane: false,
}; };
this.handleScroll = this.handleScroll.bind(this); this.handleScroll = this.handleScroll.bind(this);
@ -65,21 +72,34 @@ export class SessionConversation extends React.Component<any, State> {
this.renderTimerNotification = this.renderTimerNotification.bind(this); this.renderTimerNotification = this.renderTimerNotification.bind(this);
this.renderFriendRequest = this.renderFriendRequest.bind(this); this.renderFriendRequest = this.renderFriendRequest.bind(this);
// Group options panels // Group settings panel
this.toggleOptionsPane = this.toggleOptionsPane.bind(this); this.toggleGroupSettingsPane = this.toggleGroupSettingsPane.bind(this);
this.getGroupSettingsProps = this.getGroupSettingsProps.bind(this);
// Recording View render and unrender // Recording view
this.onLoadVoiceNoteView = this.onLoadVoiceNoteView.bind(this); this.onLoadVoiceNoteView = this.onLoadVoiceNoteView.bind(this);
this.onExitVoiceNoteView = this.onExitVoiceNoteView.bind(this); this.onExitVoiceNoteView = this.onExitVoiceNoteView.bind(this);
this.onKeyDown = this.onKeyDown.bind(this); // Messages
this.selectMessage = this.selectMessage.bind(this); this.selectMessage = this.selectMessage.bind(this);
this.resetSelection = this.resetSelection.bind(this); this.resetSelection = this.resetSelection.bind(this);
this.updateSendingProgres = this.updateSendingProgres.bind(this);
this.onMessageSending = this.onMessageSending.bind(this);
this.onMessageSuccess = this.onMessageSuccess.bind(this);
this.onMessageFailure = this.onMessageFailure.bind(this);
this.messagesEndRef = React.createRef(); this.messagesEndRef = React.createRef();
this.messageContainerRef = React.createRef(); this.messageContainerRef = React.createRef();
// Keyboard navigation
this.onKeyDown = this.onKeyDown.bind(this);
} }
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// ~~~~~~~~~~~~~~~~ LIFECYCLES ~~~~~~~~~~~~~~~~
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
public componentDidMount() { public componentDidMount() {
this.getMessages().then(() => { this.getMessages().then(() => {
// Pause thread to wait for rendering to complete // Pause thread to wait for rendering to complete
@ -92,10 +112,6 @@ export class SessionConversation extends React.Component<any, State> {
}); });
}, 100); }, 100);
}); });
//FIXME VINCE
// Only now should you renderGroupOptionsPane
} }
public componentDidUpdate(){ public componentDidUpdate(){
@ -117,9 +133,10 @@ export class SessionConversation extends React.Component<any, State> {
} }
} }
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// ~~~~~~~~~~~~~~ RENDER METHODS ~~~~~~~~~~~~~~
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
public render() { public render() {
console.log(`[vince][info] Props`, this.props);
const { messages, conversationKey, doneInitialScroll, showRecordingView, showOptionsPane } = this.state; const { messages, conversationKey, doneInitialScroll, showRecordingView, showOptionsPane } = this.state;
const loading = !doneInitialScroll || messages.length === 0; const loading = !doneInitialScroll || messages.length === 0;
const selectionMode = !!this.state.selectedMessages.length; const selectionMode = !!this.state.selectedMessages.length;
@ -130,6 +147,9 @@ export class SessionConversation extends React.Component<any, State> {
const sendMessageFn = conversationModel.sendMessage.bind(conversationModel); const sendMessageFn = conversationModel.sendMessage.bind(conversationModel);
const shouldRenderGroupSettings = !conversationModel.isPrivate() && !conversationModel.isRss()
const groupSettingsProps = this.getGroupSettingsProps();
return ( return (
<> <>
<div <div
@ -143,8 +163,9 @@ export class SessionConversation extends React.Component<any, State> {
<SessionProgress <SessionProgress
visible={true} visible={true}
value={this.state.sendingProgess} value={this.state.sendingProgress}
prevValue={this.state.prevSendingProgess} prevValue={this.state.prevSendingProgress}
sendStatus={this.state.sendingProgressStatus}
/> />
<div className="messages-wrapper"> <div className="messages-wrapper">
@ -170,6 +191,9 @@ export class SessionConversation extends React.Component<any, State> {
{ !isRss && ( { !isRss && (
<SessionCompositionBox <SessionCompositionBox
sendMessage={sendMessageFn} sendMessage={sendMessageFn}
onMessageSending={this.onMessageSending}
onMessageSuccess={this.onMessageSuccess}
onMessageFailure={this.onMessageFailure}
onLoadVoiceNoteView={this.onLoadVoiceNoteView} onLoadVoiceNoteView={this.onLoadVoiceNoteView}
onExitVoiceNoteView={this.onExitVoiceNoteView} onExitVoiceNoteView={this.onExitVoiceNoteView}
/> />
@ -177,36 +201,11 @@ export class SessionConversation extends React.Component<any, State> {
</div> </div>
{shouldRenderGroupSettings && (
<div className={classNames('conversation-item__options-pane', showOptionsPane && 'show')}> <div className={classNames('conversation-item__options-pane', showOptionsPane && 'show')}>
{/* Don't render this to the DOM unless it needs to be rendered */} <SessionGroupSettings {...groupSettingsProps}/>
{/* { showOptionsPane && ( */}
<SessionGroupSettings
id={conversationKey}
name={"asdfasd"}
memberCount={345}
description={"Super cool open group"}
avatarPath={conversation.avatarPath}
timerOptions={
window.Whisper.ExpirationTimerOptions.map((item: any) => ({
name: item.getName(),
value: item.get('seconds'),
}))
}
isPublic={conversation.isPublic}
isAdmin={conversation.isAdmin}
amMod={conversation.amMod}
onGoBack={this.toggleOptionsPane}
onInviteFriends={() => null}
onLeaveGroup={() => null}
onUpdateGroupName={() => null}
onUpdateGroupMembers={() => null}
onShowLightBox={(options: any) => null}
onSetDisappearingMessages={(seconds: number) => null}
/>
{/* )} */}
</div> </div>
)}
</> </>
); );
} }
@ -301,89 +300,39 @@ export class SessionConversation extends React.Component<any, State> {
const selected = !! messageProps?.id const selected = !! messageProps?.id
&& this.state.selectedMessages.includes(messageProps.id); && this.state.selectedMessages.includes(messageProps.id);
messageProps.i18n = window.i18n;
messageProps.selected = selected;
messageProps.firstMessageOfSeries = firstMessageOfSeries;
messageProps.onSelectMessage = (messageId: string) => this.selectMessage(messageId);
messageProps.quote = quoteProps || undefined;
return ( return (
<Message <Message {...messageProps} />
i18n = {window.i18n}
text = {messageProps?.text}
direction = {messageProps?.direction}
selected = {selected}
timestamp = {messageProps?.timestamp}
attachments = {messageProps?.attachments}
authorAvatarPath = {messageProps?.authorAvatarPath}
authorColor = {messageProps?.authorColor}
authorName = {messageProps?.authorName}
authorPhoneNumber = {messageProps?.authorPhoneNumber}
firstMessageOfSeries = {firstMessageOfSeries}
authorProfileName = {messageProps?.authorProfileName}
contact = {messageProps?.contact}
conversationType = {messageProps?.conversationType}
convoId = {messageProps?.convoId}
expirationLength = {messageProps?.expirationLength}
expirationTimestamp = {messageProps?.expirationTimestamp}
id = {messageProps?.id}
isDeletable = {messageProps?.isDeletable}
isExpired = {messageProps?.isExpired}
isModerator = {messageProps?.isModerator}
isPublic = {messageProps?.isPublic}
isRss = {messageProps?.isRss}
multiSelectMode = {messageProps?.multiSelectMode}
onBanUser = {messageProps?.onBanUser}
onClickAttachment = {messageProps?.onClickAttachment}
onClickLinkPreview = {messageProps?.onClickLinkPreview}
onCopyPubKey = {messageProps?.onCopyPubKey}
onCopyText = {messageProps?.onCopyText}
onDelete = {messageProps?.onDelete}
onDownload = {messageProps?.onDownload}
onReply = {messageProps?.onReply}
onRetrySend = {messageProps?.onRetrySend}
onSelectMessage = {messageId => this.selectMessage(messageId)}
onSelectMessageUnchecked = {messageProps?.onSelectMessageUnchecked}
onShowDetail = {messageProps?.onShowDetail}
onShowUserDetails = {messageProps?.onShowUserDetails}
previews = {messageProps?.previews}
quote = {quoteProps || undefined}
senderIsModerator = {messageProps?.senderIsModerator}
status = {messageProps?.status}
textPending = {messageProps?.textPending}
/>
); );
} }
public renderTimerNotification(timerProps: any) { public renderTimerNotification(timerProps: any) {
timerProps.i18n = window.i18n;
return ( return (
<TimerNotification <TimerNotification {...timerProps} />
type={timerProps.type}
phoneNumber={timerProps.phoneNumber}
profileName={timerProps.profileName}
name={timerProps.name}
disabled={timerProps.disabled}
timespan={timerProps.timespan}
i18n={window.i18n}
/>
); );
} }
public renderFriendRequest(friendRequestProps: any){ public renderFriendRequest(friendRequestProps: any){
friendRequestProps.i18n = window.i18n;
return ( return (
<FriendRequest <FriendRequest {...friendRequestProps} />
text={friendRequestProps.text}
direction={friendRequestProps.direction}
status={friendRequestProps.status}
friendStatus={friendRequestProps.friendStatus}
i18n={window.i18n}
isBlocked={friendRequestProps.isBlocked}
timestamp={friendRequestProps.timestamp}
onAccept={friendRequestProps.onAccept}
onDecline={friendRequestProps.onDecline}
onDeleteConversation={friendRequestProps.onDeleteConversation}
onRetrySend={friendRequestProps.onRetrySend}
onBlockUser={friendRequestProps.onBlockUser}
onUnblockUser={friendRequestProps.onUnblockUser}
/>
); );
} }
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// ~~~~~~~~~~~~~~ GETTER METHODS ~~~~~~~~~~~~~~
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
public async getMessages(numMessages?: number, fetchInterval = window.CONSTANTS.MESSAGE_FETCH_INTERVAL, loopback = false){ public async getMessages(numMessages?: number, fetchInterval = window.CONSTANTS.MESSAGE_FETCH_INTERVAL, loopback = false){
const { conversationKey, messageFetchTimestamp } = this.state; const { conversationKey, messageFetchTimestamp } = this.state;
const timestamp = getTimestamp(); const timestamp = getTimestamp();
@ -441,161 +390,10 @@ export class SessionConversation extends React.Component<any, State> {
return { newTopMessage, previousTopMessage }; return { newTopMessage, previousTopMessage };
} }
public updateReadMessages() {
const { isScrolledToBottom, messages, conversationKey } = this.state;
let unread;
if (!messages || messages.length === 0) {
return;
}
console.log(`[unread] isScrollToBottom:`, isScrolledToBottom);
if (isScrolledToBottom) {
unread = messages[messages.length - 1];
} else {
console.log(`[unread] Calling findNewestVisibleUnread`)
unread = this.findNewestVisibleUnread();
}
//console.log(`[unread] Messages:`, messages);
console.log(`[unread] Updating read messages: `, unread);
if (unread) {
const model = window.ConversationController.get(conversationKey);
model.markRead(unread.attributes.received_at);
}
}
public findNewestVisibleUnread() {
const messageContainer = this.messageContainerRef.current;
if (!messageContainer) return null;
const { messages, unreadCount } = this.state;
const { length } = messages;
const viewportBottom = (messageContainer?.clientHeight + messageContainer?.scrollTop) || 0;
console.log(`[findNew] messages`, messages);
// Start with the most recent message, search backwards in time
let foundUnread = 0;
for (let i = length - 1; i >= 0; i -= 1) {
// Search the latest 30, then stop if we believe we've covered all known
// unread messages. The unread should be relatively recent.
// Why? local notifications can be unread but won't be reflected the
// conversation's unread count.
if (i > 30 && foundUnread >= unreadCount) {
console.log(`[findNew] foundUnread > unreadCount`);
return null;
}
const message = messages[i];
if (!message.attributes.unread) {
// eslint-disable-next-line no-continue
console.log(`[findNew] no message.attributes`);
continue;
}
foundUnread += 1;
const el = document.getElementById(`${message.id}`);
if (!el) {
// eslint-disable-next-line no-continue
console.log(`[findNew] no message.id`);
continue;
}
const top = el.offsetTop;
// If the bottom fits on screen, we'll call it visible. Even if the
// message is really tall.
const height = el.offsetHeight;
const bottom = top + height;
// We're fully below the viewport, continue searching up.
if (top > viewportBottom) {
// eslint-disable-next-line no-continue
console.log(`[findNew] top > viewportBottom`);
continue;
}
if (bottom <= viewportBottom) {
console.log(`[findNew] bottom <= viewportBottom`);
console.log(`[findNew] Message set`);
return message;
}
// Continue searching up.
}
return null;
}
public toggleOptionsPane() {
const { showOptionsPane } = this.state;
this.setState({ showOptionsPane: !showOptionsPane });
}
public async handleScroll() {
const messageContainer = this.messageContainerRef.current;
if (!messageContainer) return;
const isScrolledToBottom = messageContainer.scrollHeight - messageContainer.clientHeight <= messageContainer.scrollTop + 1;
// Mark messages read
console.log(`[unread] Updating messages from handleScroll`);
this.updateReadMessages();
// Pin scroll to bottom on new message, unless user has scrolled up
if (this.state.isScrolledToBottom !== isScrolledToBottom){
this.setState({ isScrolledToBottom });
}
// Fetch more messages when nearing the top of the message list
const shouldFetchMoreMessages = messageContainer.scrollTop <= window.CONSTANTS.MESSAGE_CONTAINER_BUFFER_OFFSET_PX;
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;
previousTopMessage && this.scrollToMessage(previousTopMessage);
}
}
public scrollToUnread() {
const { messages, unreadCount } = this.state;
const message = messages[(messages.length - 1) - unreadCount];
if(message) this.scrollToMessage(message.id);
}
public scrollToMessage(messageId: string) {
const topUnreadMessage = document.getElementById(messageId);
topUnreadMessage?.scrollIntoView();
}
public scrollToBottom() {
// FIXME VINCE: Smooth scrolling that isn't slow@!
// this.messagesEndRef.current?.scrollIntoView(
// { behavior: firstLoad ? 'auto' : 'smooth' }
// );
const messageContainer = this.messageContainerRef.current;
if (!messageContainer) return;
messageContainer.scrollTop = messageContainer.scrollHeight - messageContainer.clientHeight;
}
public getHeaderProps() { public getHeaderProps() {
const {conversationKey} = this.state; const {conversationKey} = this.state;
const conversation = window.getConversationByKey(conversationKey); const conversation = window.getConversationByKey(conversationKey);
console.log(`[header] Conversation`, conversation);
const expireTimer = conversation.get('expireTimer'); const expireTimer = conversation.get('expireTimer');
const expirationSettingName = expireTimer const expirationSettingName = expireTimer
? window.Whisper.ExpirationTimerOptions.getName(expireTimer || 0) ? window.Whisper.ExpirationTimerOptions.getName(expireTimer || 0)
@ -707,30 +505,17 @@ export class SessionConversation extends React.Component<any, State> {
userPubKey: pubkey, userPubKey: pubkey,
}); });
} else if (!conversation.isRss()) { } else if (!conversation.isRss()) {
this.toggleOptionsPane(); this.toggleGroupSettingsPane();
} }
}, },
}; };
}; };
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)
: [...this.state.selectedMessages, messageId];
this.setState({ selectedMessages },
() => console.log(`[vince] SelectedMessages: `, this.state.selectedMessages)
);
}
public resetSelection(){
this.setState({selectedMessages: []});
}
public getGroupSettingsProps() { public getGroupSettingsProps() {
const {conversationKey} = this.state; const { conversationKey } = this.state;
const conversation = window.getConversationByKey[conversationKey]; const conversation = window.getConversationByKey(conversationKey);
console.log(`[settings] Conversation:`, conversation);
const ourPK = window.textsecure.storage.user.getNumber(); const ourPK = window.textsecure.storage.user.getNumber();
const members = conversation.get('members') || []; const members = conversation.get('members') || [];
@ -738,6 +523,7 @@ export class SessionConversation extends React.Component<any, State> {
return { return {
id: conversation.id, id: conversation.id,
name: conversation.getName(), name: conversation.getName(),
memberCount: members.length,
phoneNumber: conversation.getNumber(), phoneNumber: conversation.getNumber(),
profileName: conversation.getProfileName(), profileName: conversation.getProfileName(),
color: conversation.getColor(), color: conversation.getColor(),
@ -746,7 +532,6 @@ export class SessionConversation extends React.Component<any, State> {
isPublic: conversation.isPublic(), isPublic: conversation.isPublic(),
isAdmin: conversation.get('groupAdmins').includes(ourPK), isAdmin: conversation.get('groupAdmins').includes(ourPK),
isRss: conversation.isRss(), isRss: conversation.isRss(),
memberCount: members.length,
timerOptions: window.Whisper.ExpirationTimerOptions.map((item: any) => ({ timerOptions: window.Whisper.ExpirationTimerOptions.map((item: any) => ({
name: item.getName(), name: item.getName(),
@ -757,7 +542,7 @@ export class SessionConversation extends React.Component<any, State> {
conversation.setDisappearingMessages(seconds), conversation.setDisappearingMessages(seconds),
onGoBack: () => { onGoBack: () => {
conversation.hideConversationRight(); this.toggleGroupSettingsPane();
}, },
onUpdateGroupName: () => { onUpdateGroupName: () => {
@ -780,6 +565,196 @@ export class SessionConversation extends React.Component<any, State> {
}; };
}; };
public toggleGroupSettingsPane() {
const { showOptionsPane } = this.state;
this.setState({ showOptionsPane: !showOptionsPane });
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// ~~~~~~~~~~~~~ MESSAGE HANDLING ~~~~~~~~~~~~~
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
public updateSendingProgres(value: number, status: -1 | 0 | 1 | 2) {
// If you're sending a new message, reset previous value to zero
const prevSendingProgress = status === 1 ? 0 : this.state.sendingProgress;
this.setState({
sendingProgress: value,
prevSendingProgress,
sendingProgressStatus: status,
});
}
public onMessageSending() {
// Set sending state to random between 10% and 50% to show message sending
const minInitVal = 10;
const maxInitVal = 50;
const initialValue = minInitVal + (maxInitVal - minInitVal) * Math.random();
console.log(`[sending] Message Sending`);
this.updateSendingProgres(initialValue, 1);
}
public onMessageSuccess(){
console.log(`[sending] Message Sent`);
this.updateSendingProgres(100, 2);
}
public onMessageFailure(){
console.log(`[sending] Message Failure`);
this.updateSendingProgres(100, -1);
}
public updateReadMessages() {
const { isScrolledToBottom, messages, conversationKey } = this.state;
let unread;
if (!messages || messages.length === 0) {
return;
}
if (isScrolledToBottom) {
unread = messages[messages.length - 1];
} else {
unread = this.findNewestVisibleUnread();
}
if (unread) {
const model = window.ConversationController.get(conversationKey);
model.markRead(unread.attributes.received_at);
}
}
public findNewestVisibleUnread() {
const messageContainer = this.messageContainerRef.current;
if (!messageContainer) return null;
const { messages, unreadCount } = this.state;
const { length } = messages;
const viewportBottom = (messageContainer?.clientHeight + messageContainer?.scrollTop) || 0;
// Start with the most recent message, search backwards in time
let foundUnread = 0;
for (let i = length - 1; i >= 0; i -= 1) {
// Search the latest 30, then stop if we believe we've covered all known
// unread messages. The unread should be relatively recent.
// Why? local notifications can be unread but won't be reflected the
// conversation's unread count.
if (i > 30 && foundUnread >= unreadCount) {
return null;
}
const message = messages[i];
if (!message.attributes.unread) {
// eslint-disable-next-line no-continue
continue;
}
foundUnread += 1;
const el = document.getElementById(`${message.id}`);
if (!el) {
// eslint-disable-next-line no-continue
continue;
}
const top = el.offsetTop;
// If the bottom fits on screen, we'll call it visible. Even if the
// message is really tall.
const height = el.offsetHeight;
const bottom = top + height;
// We're fully below the viewport, continue searching up.
if (top > viewportBottom) {
// eslint-disable-next-line no-continue
continue;
}
if (bottom <= viewportBottom) {
return message;
}
// Continue searching up.
}
return null;
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// ~~~~~~~~~~~~ SCROLLING METHODS ~~~~~~~~~~~~~
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
public async handleScroll() {
const messageContainer = this.messageContainerRef.current;
if (!messageContainer) return;
const isScrolledToBottom = messageContainer.scrollHeight - messageContainer.clientHeight <= messageContainer.scrollTop + 1;
// Mark messages read
this.updateReadMessages();
// Pin scroll to bottom on new message, unless user has scrolled up
if (this.state.isScrolledToBottom !== isScrolledToBottom){
this.setState({ isScrolledToBottom });
}
// Fetch more messages when nearing the top of the message list
const shouldFetchMoreMessages = messageContainer.scrollTop <= window.CONSTANTS.MESSAGE_CONTAINER_BUFFER_OFFSET_PX;
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;
previousTopMessage && this.scrollToMessage(previousTopMessage);
}
}
public scrollToUnread() {
const { messages, unreadCount } = this.state;
const message = messages[(messages.length - 1) - unreadCount];
if(message) this.scrollToMessage(message.id);
}
public scrollToMessage(messageId: string) {
const topUnreadMessage = document.getElementById(messageId);
topUnreadMessage?.scrollIntoView();
}
public scrollToBottom() {
// FIXME VINCE: Smooth scrolling that isn't slow@!
// this.messagesEndRef.current?.scrollIntoView(
// { behavior: firstLoad ? 'auto' : 'smooth' }
// );
const messageContainer = this.messageContainerRef.current;
if (!messageContainer) return;
messageContainer.scrollTop = messageContainer.scrollHeight - messageContainer.clientHeight;
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// ~~~~~~~~~~~~ MESSAGE SELECTION ~~~~~~~~~~~~~
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
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)
: [...this.state.selectedMessages, messageId];
this.setState({ selectedMessages });
}
public resetSelection(){
this.setState({selectedMessages: []});
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// ~~~~~~~~~~~~ MICROPHONE METHODS ~~~~~~~~~~~~
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
private onLoadVoiceNoteView() { private onLoadVoiceNoteView() {
this.setState({ this.setState({
showRecordingView: true, showRecordingView: true,
@ -791,10 +766,11 @@ export class SessionConversation extends React.Component<any, State> {
this.setState({ this.setState({
showRecordingView: false, showRecordingView: false,
}); });
console.log(`[vince] Stopped recording entirely`);
} }
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// ~~~~~~~~~~~ KEYBOARD NAVIGATION ~~~~~~~~~~~~
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
private onKeyDown(event: any) { private onKeyDown(event: any) {
const messageContainer = this.messageContainerRef.current; const messageContainer = this.messageContainerRef.current;
if (!messageContainer) return; if (!messageContainer) return;

@ -15,12 +15,12 @@ interface Props {
id: string; id: string;
name: string; name: string;
memberCount: number; memberCount: number;
description: string; description?: string;
avatarPath: string; avatarPath: string;
timerOptions: Array<TimerOption>; timerOptions: Array<TimerOption>;
isPublic: boolean; isPublic: boolean;
isAdmin: boolean; isAdmin?: boolean;
amMod: boolean; amMod?: boolean;
onGoBack: () => void; onGoBack: () => void;
onInviteFriends: () => void; onInviteFriends: () => void;

Loading…
Cancel
Save