diff --git a/stylesheets/_session_conversation.scss b/stylesheets/_session_conversation.scss index f2ab032ee..ade04fc42 100644 --- a/stylesheets/_session_conversation.scss +++ b/stylesheets/_session_conversation.scss @@ -224,6 +224,7 @@ $composition-container-height: 60px; background-color: $session-shade-4; border: 1px solid $session-shade-6-alt; border-radius: 8px; + padding-bottom: $session-margin-sm; .emoji-mart-category-label { @@ -259,7 +260,6 @@ $composition-container-height: 60px; border: 0.7px solid $session-shade-6-alt; clip-path: polygon(100% 100%, 7.2px 100%, 100% 7.2px); } - } } diff --git a/ts/components/conversation/Message.tsx b/ts/components/conversation/Message.tsx index c1199c2d1..02d156605 100644 --- a/ts/components/conversation/Message.tsx +++ b/ts/components/conversation/Message.tsx @@ -994,7 +994,7 @@ export class Message extends React.PureComponent { const dimensions = getGridDimensions(attachments); if (dimensions) { return dimensions.width; - } + } } if (previews && previews.length) { diff --git a/ts/components/session/conversation/SessionCompositionBox.tsx b/ts/components/session/conversation/SessionCompositionBox.tsx index 5ddc56159..96dc61c3e 100644 --- a/ts/components/session/conversation/SessionCompositionBox.tsx +++ b/ts/components/session/conversation/SessionCompositionBox.tsx @@ -208,7 +208,8 @@ export class SessionCompositionBox extends React.Component { private onChooseAttachment() { - this.fileInput.current?.click(); + const fileInput = this.fileInput.current; + if(fileInput) fileInput.click(); } private onChoseAttachment() { @@ -250,12 +251,13 @@ export class SessionCompositionBox extends React.Component { private onSendMessage(){ - // FIXME VINCE: Get emoiji, attachments, etc - const messagePlaintext = this.textarea.current?.value; - const {attachments} = this.state; const messageInput = this.textarea.current; - if (!messageInput) return; + + const messagePlaintext = messageInput.value; + const {attachments} = this.state; + + // FIXME VINCE: Get emoiji, attachments, etc console.log(`[vince][msg] Message:`, messagePlaintext); console.log(`[vince][msg] fileAttachments:`, attachments); diff --git a/ts/components/session/conversation/SessionConversation.tsx b/ts/components/session/conversation/SessionConversation.tsx index afd2d876f..b57394179 100644 --- a/ts/components/session/conversation/SessionConversation.tsx +++ b/ts/components/session/conversation/SessionConversation.tsx @@ -28,6 +28,7 @@ interface State { export class SessionConversation extends React.Component { private messagesEndRef: React.RefObject; + private messageContainerRef: React.RefObject; constructor(props: any) { super(props); @@ -67,20 +68,21 @@ export class SessionConversation extends React.Component { 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 {
)} -
+
{this.renderMessages()}
@@ -213,6 +219,7 @@ export class SessionConversation extends React.Component { 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 { ); } - 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 { // 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 { 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 { 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 { // { 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 { onShowAllMedia: async () => { conversation.updateHeader(); }, + onUpdateGroupName: () => { + conversation.onUpdateGroupName(); + }, onShowGroupMembers: async () => { await conversation.showMembers(); conversation.updateHeader(); @@ -633,10 +728,12 @@ export class SessionConversation extends React.Component { } 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 { 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 { } + + public getTimestamp() { + return Math.floor(Date.now() / 1000); + } } diff --git a/yarn.lock b/yarn.lock index 97b36538d..57e1f018c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -198,6 +198,13 @@ dependencies: electron-is-dev "*" +"@types/emoji-mart@^2.11.3": + version "2.11.3" + resolved "https://registry.yarnpkg.com/@types/emoji-mart/-/emoji-mart-2.11.3.tgz#9949f6a8a231aea47aac1b2d4212597b41140b07" + integrity sha512-pRlU6+CFIB+9+FwjGGCVtDQq78u7N0iUijrO0Qh1j9RJ6T23DSNNfe0X6kf81N4ubVhF9jVckCI1M3kHpkwjqA== + dependencies: + "@types/react" "*" + "@types/events@*": version "3.0.0" resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7"