diff --git a/js/models/messages.js b/js/models/messages.js index 2c1ea257e..d03eebe84 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -596,6 +596,7 @@ quote: this.getPropsForQuote(options), authorAvatarPath, isExpired: this.hasExpired, + isUnread: this.isUnread(), expirationLength, expirationTimestamp, isPublic: !!this.get('isPublic'), @@ -618,6 +619,7 @@ onRetrySend: () => this.retrySend(), onShowDetail: () => this.trigger('show-message-detail', this), onClickLinkPreview: url => this.trigger('navigate-to', url), + markRead: readAt => this.markRead(readAt), onShowUserDetails: pubkey => window.Whisper.events.trigger('onShowUserDetails', { diff --git a/package.json b/package.json index 7592f992f..840fdf340 100644 --- a/package.json +++ b/package.json @@ -111,6 +111,7 @@ "react-emoji": "^0.5.0", "react-emoji-render": "^1.2.4", "react-h5-audio-player": "^3.2.0", + "react-intersection-observer": "^8.30.3", "react-mentions": "^4.0.2", "react-portal": "^4.2.0", "react-qr-svg": "^2.2.1", diff --git a/ts/components/conversation/Message.tsx b/ts/components/conversation/Message.tsx index 6d16cfa5c..142402685 100644 --- a/ts/components/conversation/Message.tsx +++ b/ts/components/conversation/Message.tsx @@ -37,6 +37,7 @@ import { SessionIcon, SessionIconSize, SessionIconType } from '../session/icon'; import _ from 'lodash'; import { animation, contextMenu, Item, Menu } from 'react-contexify'; import uuid from 'uuid'; +import { InView } from 'react-intersection-observer'; // Same as MIN_WIDTH in ImageGrid.tsx const MINIMUM_LINK_PREVIEW_IMAGE_WIDTH = 200; @@ -97,6 +98,7 @@ export interface Props { // whether or not to show check boxes multiSelectMode: boolean; firstMessageOfSeries: boolean; + isUnread: boolean; onClickAttachment?: (attachment: AttachmentType) => void; onClickLinkPreview?: (url: string) => void; @@ -110,6 +112,7 @@ export interface Props { onBanUser?: () => void; onShowDetail: () => void; onShowUserDetails: (userPubKey: string) => void; + markRead: (readAt: number) => Promise; } interface State { @@ -987,7 +990,8 @@ export class Message extends React.PureComponent { conversationType, isPublic, text, - firstMessageOfSeries, + isUnread, + markRead, } = this.props; const { expired, expiring } = this.state; @@ -1008,6 +1012,7 @@ export class Message extends React.PureComponent { const isIncoming = direction === 'incoming'; const shouldHightlight = mentionMe && isIncoming && isPublic; + const shouldMarkReadWhenVisible = isIncoming && isUnread; const divClasses = ['session-message-wrapper']; if (shouldHightlight) { @@ -1021,10 +1026,23 @@ export class Message extends React.PureComponent { divClasses.push('public-chat-message-wrapper'); } + const onVisible = (inView: boolean) => { + if (inView && shouldMarkReadWhenVisible) { + // mark the message as read. + // this will trigger the expire timer. + void markRead(Date.now()); + } + }; + return ( -
{this.renderAvatar()} @@ -1099,7 +1117,7 @@ export class Message extends React.PureComponent { {this.renderError(!isIncoming)} {this.renderContextMenu()}
- + ); } diff --git a/yarn.lock b/yarn.lock index 5d1614d00..57301b223 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8778,6 +8778,11 @@ react-icons@^2.2.7: dependencies: react-icon-base "2.1.0" +react-intersection-observer@^8.30.3: + version "8.30.3" + resolved "https://registry.yarnpkg.com/react-intersection-observer/-/react-intersection-observer-8.30.3.tgz#99850f0aacc5b474dddb04976e58d7ee9315ba19" + integrity sha512-hKYTJUrU99hAf7h1lNY3pjYXt+09BaPNC6fcLw1B8OLJJUDXTWrwzu4hRuztougeRgPYpxmNaTn1FS4F3EQnhA== + react-is@^16.12.0, react-is@^16.7.0, react-is@^16.8.1: version "16.13.0" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.0.tgz#0f37c3613c34fe6b37cd7f763a0d6293ab15c527"