|
|
|
@ -28,6 +28,7 @@ interface State {
|
|
|
|
|
|
|
|
|
|
export class SessionConversation extends React.Component<any, State> {
|
|
|
|
|
private messagesEndRef: React.RefObject<HTMLDivElement>;
|
|
|
|
|
private messageContainerRef: React.RefObject<HTMLDivElement>;
|
|
|
|
|
|
|
|
|
|
constructor(props: any) {
|
|
|
|
|
super(props);
|
|
|
|
@ -67,20 +68,21 @@ export class SessionConversation extends React.Component<any, State> {
|
|
|
|
|
this.resetSelection = this.resetSelection.bind(this);
|
|
|
|
|
|
|
|
|
|
this.messagesEndRef = React.createRef();
|
|
|
|
|
this.messageContainerRef = React.createRef();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async componentWillMount() {
|
|
|
|
|
await this.getMessages();
|
|
|
|
|
|
|
|
|
|
// Pause thread to wait for rendering to complete
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
this.scrollToUnread();
|
|
|
|
|
}, 0);
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
this.setState({
|
|
|
|
|
doneInitialScroll: true,
|
|
|
|
|
});
|
|
|
|
|
}, 100);
|
|
|
|
|
public componentDidMount() {
|
|
|
|
|
this.getMessages().then(() => {
|
|
|
|
|
// Pause thread to wait for rendering to complete
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
this.scrollToUnread();
|
|
|
|
|
}, 0);
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
this.setState({
|
|
|
|
|
doneInitialScroll: true,
|
|
|
|
|
});
|
|
|
|
|
}, 100);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public componentDidUpdate(){
|
|
|
|
@ -134,7 +136,11 @@ export class SessionConversation extends React.Component<any, State> {
|
|
|
|
|
<div className="messages-container__loading"></div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<div className="messages-container" onScroll={this.handleScroll}>
|
|
|
|
|
<div
|
|
|
|
|
className="messages-container"
|
|
|
|
|
onScroll={this.handleScroll}
|
|
|
|
|
ref={this.messageContainerRef}
|
|
|
|
|
>
|
|
|
|
|
{this.renderMessages()}
|
|
|
|
|
<div ref={this.messagesEndRef} />
|
|
|
|
|
</div>
|
|
|
|
@ -213,6 +219,7 @@ export class SessionConversation extends React.Component<any, State> {
|
|
|
|
|
isFriendRequestPending={headerProps.isFriendRequestPending}
|
|
|
|
|
isOnline={headerProps.isOnline}
|
|
|
|
|
selectedMessages={headerProps.selectedMessages}
|
|
|
|
|
onUpdateGroupName={headerProps.onUpdateGroupName}
|
|
|
|
|
onSetDisappearingMessages={headerProps.onSetDisappearingMessages}
|
|
|
|
|
onDeleteMessages={headerProps.onDeleteMessages}
|
|
|
|
|
onDeleteContact={headerProps.onDeleteContact}
|
|
|
|
@ -238,7 +245,6 @@ export class SessionConversation extends React.Component<any, State> {
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public renderMessage(messageProps: any, firstMessageOfSeries: boolean, quoteProps?: any) {
|
|
|
|
|
const selected = !! messageProps?.id
|
|
|
|
|
&& this.state.selectedMessages.includes(messageProps.id);
|
|
|
|
@ -357,7 +363,7 @@ export class SessionConversation extends React.Component<any, State> {
|
|
|
|
|
|
|
|
|
|
// Set first member of series here.
|
|
|
|
|
const messageModels = messageSet.models;
|
|
|
|
|
let messages = [];
|
|
|
|
|
const messages = [];
|
|
|
|
|
let previousSender;
|
|
|
|
|
for (let i = 0; i < messageModels.length; i++){
|
|
|
|
|
// Handle firstMessageOfSeries for conditional avatar rendering
|
|
|
|
@ -373,24 +379,109 @@ export class SessionConversation extends React.Component<any, State> {
|
|
|
|
|
const previousTopMessage = this.state.messages[0]?.id;
|
|
|
|
|
const newTopMessage = messages[0]?.id;
|
|
|
|
|
|
|
|
|
|
await this.setState({ messages, messageFetchTimestamp: timestamp });
|
|
|
|
|
this.setState({ messages, messageFetchTimestamp: timestamp }, () => {
|
|
|
|
|
if (this.state.isScrolledToBottom) {
|
|
|
|
|
this.updateReadMessages();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return { newTopMessage, previousTopMessage };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public getTimestamp() {
|
|
|
|
|
return Math.floor(Date.now() / 1000);
|
|
|
|
|
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.bind(model)(unread.attributes.received_at);
|
|
|
|
|
|
|
|
|
|
console.log(`[read] Unread:`, unread);
|
|
|
|
|
console.log(`[read] Model:`, model);
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public async handleScroll() {
|
|
|
|
|
const { messages } = this.state;
|
|
|
|
|
const messageContainer = document.getElementsByClassName('messages-container')[0];
|
|
|
|
|
const messageContainer = this.messageContainerRef.current;
|
|
|
|
|
if (!messageContainer) return;
|
|
|
|
|
|
|
|
|
|
const isScrolledToBottom = messageContainer.scrollHeight - messageContainer.clientHeight <= messageContainer.scrollTop + 1;
|
|
|
|
|
|
|
|
|
|
// FIXME VINCE: Update unread count
|
|
|
|
|
// In models/conversations
|
|
|
|
|
// Update unread count by geting all divs of .session-message
|
|
|
|
|
// which are currently in view.
|
|
|
|
|
this.updateReadMessages();
|
|
|
|
|
|
|
|
|
|
// Pin scroll to bottom on new message, unless user has scrolled up
|
|
|
|
|
if (this.state.isScrolledToBottom !== isScrolledToBottom){
|
|
|
|
@ -412,9 +503,9 @@ export class SessionConversation extends React.Component<any, State> {
|
|
|
|
|
|
|
|
|
|
public scrollToUnread() {
|
|
|
|
|
const { messages, unreadCount } = this.state;
|
|
|
|
|
|
|
|
|
|
const message = messages[(messages.length - 1) - unreadCount];
|
|
|
|
|
message && this.scrollToMessage(message.id);
|
|
|
|
|
|
|
|
|
|
if(message) this.scrollToMessage(message.id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public scrollToMessage(messageId: string) {
|
|
|
|
@ -428,7 +519,8 @@ export class SessionConversation extends React.Component<any, State> {
|
|
|
|
|
// { behavior: firstLoad ? 'auto' : 'smooth' }
|
|
|
|
|
// );
|
|
|
|
|
|
|
|
|
|
const messageContainer = document.getElementsByClassName('messages-container')[0];
|
|
|
|
|
const messageContainer = this.messageContainerRef.current;
|
|
|
|
|
if (!messageContainer) return;
|
|
|
|
|
messageContainer.scrollTop = messageContainer.scrollHeight - messageContainer.clientHeight;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -491,6 +583,9 @@ export class SessionConversation extends React.Component<any, State> {
|
|
|
|
|
onShowAllMedia: async () => {
|
|
|
|
|
conversation.updateHeader();
|
|
|
|
|
},
|
|
|
|
|
onUpdateGroupName: () => {
|
|
|
|
|
conversation.onUpdateGroupName();
|
|
|
|
|
},
|
|
|
|
|
onShowGroupMembers: async () => {
|
|
|
|
|
await conversation.showMembers();
|
|
|
|
|
conversation.updateHeader();
|
|
|
|
@ -633,10 +728,12 @@ export class SessionConversation extends React.Component<any, State> {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private onKeyDown(event: any) {
|
|
|
|
|
const messageContainer = this.messageContainerRef.current;
|
|
|
|
|
if (!messageContainer) return;
|
|
|
|
|
|
|
|
|
|
const selectionMode = !!this.state.selectedMessages.length;
|
|
|
|
|
const recordingMode = this.state.showRecordingView;
|
|
|
|
|
|
|
|
|
|
const messageContainer = document.getElementsByClassName('messages-container')[0];
|
|
|
|
|
const pageHeight = messageContainer.clientHeight;
|
|
|
|
|
const arrowScrollPx = 50;
|
|
|
|
|
const pageScrollPx = 0.80 * pageHeight;
|
|
|
|
@ -646,7 +743,7 @@ export class SessionConversation extends React.Component<any, State> {
|
|
|
|
|
console.log(`[vince][key] key: `, event.key);
|
|
|
|
|
console.log(`[vince][key] key: `, event.keyCode);
|
|
|
|
|
if (event.key === 'Escape') {
|
|
|
|
|
|
|
|
|
|
//
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch(event.key){
|
|
|
|
@ -674,6 +771,10 @@ export class SessionConversation extends React.Component<any, State> {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public getTimestamp() {
|
|
|
|
|
return Math.floor(Date.now() / 1000);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|