diff --git a/js/read_syncs.js b/js/read_syncs.js index 70aebf1c4..c7337ec07 100644 --- a/js/read_syncs.js +++ b/js/read_syncs.js @@ -59,7 +59,7 @@ // If message is unread, we mark it read. Otherwise, we update the expiration // timer to the time specified by the read sync if it's earlier than // the previous read time. - if (message.isUnread()) { + if (message.isUnread() && window.isFocused()) { await message.markRead(readAt); // onReadMessage may result in messages older than this one being diff --git a/preload.js b/preload.js index 87b3d051b..b9f6206c9 100644 --- a/preload.js +++ b/preload.js @@ -56,7 +56,7 @@ window.lokiFeatureFlags = { useFileOnionRequests: true, useFileOnionRequestsV2: true, // more compact encoding of files in response padOutgoingAttachments: true, - enablePinConversations: false, + enablePinConversations: true, }; if (typeof process.env.NODE_ENV === 'string' && process.env.NODE_ENV.includes('test-integration')) { diff --git a/ts/components/ConversationListItem.tsx b/ts/components/ConversationListItem.tsx index b17f00bbd..c23db163a 100644 --- a/ts/components/ConversationListItem.tsx +++ b/ts/components/ConversationListItem.tsx @@ -63,6 +63,8 @@ const ConversationListItem = (props: Props) => { isMe, isPinned, isTyping, + isPublic, + left, type, lastMessage, memberAvatars, @@ -126,6 +128,8 @@ const ConversationListItem = (props: Props) => { triggerId={triggerId} type={type} isMe={isMe} + isPublic={isPublic} + left={left} notificationForConvo={notificationForConvo} currentNotificationSetting={currentNotificationSetting} /> diff --git a/ts/components/conversation/Message.tsx b/ts/components/conversation/Message.tsx index 407574d58..dda2867bc 100644 --- a/ts/components/conversation/Message.tsx +++ b/ts/components/conversation/Message.tsx @@ -42,6 +42,7 @@ import { MessageInteraction } from '../../interactions'; import autoBind from 'auto-bind'; import { AudioPlayerWithEncryptedFile } from './H5AudioPlayer'; import { ClickToTrustSender } from './message/ClickToTrustSender'; +import { ReadableMessage } from './ReadableMessage'; // Same as MIN_WIDTH in ImageGrid.tsx const MINIMUM_LINK_PREVIEW_IMAGE_WIDTH = 200; @@ -728,8 +729,8 @@ class MessageInner extends React.PureComponent { divClasses.push('flash-green-once'); } - const onVisible = (inView: boolean) => { - if (inView && shouldMarkReadWhenVisible) { + const onVisible = (inView: boolean | Object) => { + if (inView === true && shouldMarkReadWhenVisible && window.isFocused()) { // mark the message as read. // this will trigger the expire timer. void markRead(Date.now()); @@ -737,14 +738,10 @@ class MessageInner extends React.PureComponent { }; return ( - {this.renderAvatar()} @@ -818,7 +815,7 @@ class MessageInner extends React.PureComponent { {this.renderError(!isIncoming)} {this.renderContextMenu()} - + ); } diff --git a/ts/components/conversation/ReadableMessage.tsx b/ts/components/conversation/ReadableMessage.tsx new file mode 100644 index 000000000..d56fa2c4e --- /dev/null +++ b/ts/components/conversation/ReadableMessage.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { useFocus } from '../../hooks/useFocus'; +import { InView, useInView } from 'react-intersection-observer'; + +type ReadableMessageProps = { + children: React.ReactNode; + id: string; + className: string; + onChange: (inView: boolean) => void; + onContextMenu: (e: any) => void; +}; + +export const ReadableMessage = (props: ReadableMessageProps) => { + const { onChange } = props; + useFocus(onChange); + + return ( + + {props.children} + + ); +}; diff --git a/ts/components/session/conversation/SessionMessagesList.tsx b/ts/components/session/conversation/SessionMessagesList.tsx index e3af18792..922a40d31 100644 --- a/ts/components/session/conversation/SessionMessagesList.tsx +++ b/ts/components/session/conversation/SessionMessagesList.tsx @@ -378,7 +378,7 @@ export class SessionMessagesList extends React.Component { return; } - if (this.getScrollOffsetBottomPx() === 0) { + if (this.getScrollOffsetBottomPx() === 0 && window.isFocused()) { void conversation.markRead(messages[0].attributes.received_at); } } diff --git a/ts/components/session/registration/RegistrationTabs.tsx b/ts/components/session/registration/RegistrationTabs.tsx index d3128df43..9c94ec543 100644 --- a/ts/components/session/registration/RegistrationTabs.tsx +++ b/ts/components/session/registration/RegistrationTabs.tsx @@ -17,7 +17,7 @@ import { import { fromHex } from '../../../session/utils/String'; import { TaskTimedOutError } from '../../../session/utils/Promise'; import { mn_decode } from '../../../session/crypto/mnemonic'; -import { SwarmPolling } from '../../../session/snode_api/swarmPolling'; +import { getSwarmPollingInstance } from '../../../session/snode_api/swarmPolling'; export const MAX_USERNAME_LENGTH = 20; // tslint:disable: use-simple-attributes @@ -191,11 +191,9 @@ export async function signInWithLinking(signInDetails: { try { await resetRegistration(); await window.setPassword(password); - const pubkeyStr = await signInByLinkingDevice(userRecoveryPhrase, 'english'); - + await signInByLinkingDevice(userRecoveryPhrase, 'english'); let displayNameFromNetwork = ''; - SwarmPolling.getInstance().addPubkey(pubkeyStr); - SwarmPolling.getInstance().start(); + await getSwarmPollingInstance().start(); await PromiseUtils.waitForTask(done => { window.Whisper.events.on('configurationMessageReceived', (displayName: string) => { diff --git a/ts/hooks/useFocus.ts b/ts/hooks/useFocus.ts new file mode 100644 index 000000000..8f62de92e --- /dev/null +++ b/ts/hooks/useFocus.ts @@ -0,0 +1,10 @@ +import { useEffect } from 'react'; + +export const useFocus = (action: (param: any) => void) => { + useEffect(() => { + window.addEventListener('focus', action); + return () => { + window.removeEventListener('focus', action); + }; + }); +}; diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index 5110bad0f..7b879d0a4 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -173,6 +173,7 @@ export class ConversationModel extends Backbone.Model { private typingRefreshTimer?: NodeJS.Timeout | null; private typingPauseTimer?: NodeJS.Timeout | null; private typingTimer?: NodeJS.Timeout | null; + private lastReadTimestamp: number; private pending: any; @@ -188,11 +189,19 @@ export class ConversationModel extends Backbone.Model { this.updateLastMessage = _.throttle(this.bouncyUpdateLastMessage.bind(this), 1000); this.throttledNotify = _.debounce(this.notify, 500, { maxWait: 1000 }); //start right away the function is called, and wait 1sec before calling it again - this.markRead = _.debounce(this.markReadBouncy, 1000, { leading: true }); + const markReadDebounced = _.debounce(this.markReadBouncy, 1000, { leading: true }); + this.markRead = (newestUnreadDate: number) => { + const lastReadTimestamp = this.lastReadTimestamp; + if (newestUnreadDate > lastReadTimestamp) { + this.lastReadTimestamp = newestUnreadDate; + } + void markReadDebounced(newestUnreadDate); + }; // Listening for out-of-band data updates this.typingRefreshTimer = null; this.typingPauseTimer = null; + this.lastReadTimestamp = 0; window.inboxStore?.dispatch(conversationActions.conversationChanged(this.id, this.getProps())); } @@ -914,6 +923,11 @@ export class ConversationModel extends Backbone.Model { } public async markReadBouncy(newestUnreadDate: number, providedOptions: any = {}) { + const lastReadTimestamp = this.lastReadTimestamp; + if (newestUnreadDate < lastReadTimestamp) { + return; + } + const options = providedOptions || {}; _.defaults(options, { sendReadReceipts: true }); @@ -959,7 +973,7 @@ export class ConversationModel extends Backbone.Model { const cachedUnreadCountOnConvo = this.get('unreadCount'); if (cachedUnreadCountOnConvo !== read.length) { // reset the unreadCount on the convo to the real one coming from markRead messages on the db - this.set({ unreadCount: 0 }); + this.set({ unreadCount: realUnreadCount }); await this.commit(); } else { // window?.log?.info('markRead(): nothing newly read.'); diff --git a/ts/models/message.ts b/ts/models/message.ts index 339058372..ec58ecf97 100644 --- a/ts/models/message.ts +++ b/ts/models/message.ts @@ -1047,6 +1047,8 @@ export class MessageModel extends Backbone.Model { public async markRead(readAt: number) { this.markReadNoCommit(readAt); + this.getConversation()?.markRead(this.attributes.received_at); + await this.commit(); } diff --git a/ts/window.d.ts b/ts/window.d.ts index 5f43a4e83..5a7b1e7d5 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -40,6 +40,7 @@ declare global { getFriendsFromContacts: any; getSettingValue: any; i18n: LocalizerType; + isFocused: () => boolean; libloki: Libloki; libsignal: LibsignalProtocol; log: any;