fix unread banner position when first unread is visible

This commit is contained in:
audric 2021-07-22 12:30:01 +10:00
parent c2b5ac68d6
commit c8f0150aaf
14 changed files with 88 additions and 92 deletions

View File

@ -341,11 +341,15 @@
window.setMediaPermissions(!value);
};
Whisper.Notifications.on('click', (id, messageId) => {
Whisper.Notifications.on('click', async (id, messageId) => {
window.showWindow();
if (id) {
const firstUnreadIdOnOpen = await window.Signal.Data.getFirstUnreadMessageIdInConversation(
id
);
window.inboxStore.dispatch(
window.actionsCreators.openConversationExternal({ id, messageId })
window.actionsCreators.openConversationExternal({ id, messageId, firstUnreadIdOnOpen })
);
} else {
appView.openInbox({

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, { useCallback } from 'react';
import classNames from 'classnames';
import { isEmpty } from 'lodash';
import { contextMenu } from 'react-contexify';
@ -26,6 +26,7 @@ import { SessionIcon, SessionIconSize, SessionIconType } from './session/icon';
import { useDispatch, useSelector } from 'react-redux';
import { SectionType } from '../state/ducks/section';
import { getFocusedSection } from '../state/selectors/section';
import { getFirstUnreadMessageIdInConversation } from '../data/data';
// tslint:disable-next-line: no-empty-interface
export interface ConversationListItemProps extends ReduxConversationType {}
@ -240,13 +241,16 @@ const ConversationListItem = (props: Props) => {
const dispatch = useDispatch();
const openConvo = useCallback(async () => {
const firstUnreadIdOnOpen = await getFirstUnreadMessageIdInConversation(conversationId);
dispatch(openConversationExternal({ id: conversationId, firstUnreadIdOnOpen }));
}, [conversationId]);
return (
<div key={key}>
<div
role="button"
onClick={() => {
dispatch(openConversationExternal({ id: conversationId }));
}}
onClick={openConvo}
onContextMenu={(e: any) => {
contextMenu.show({
id: triggerId,

View File

@ -94,7 +94,13 @@ export const MessageSearchResult = (props: Props) => {
<div
role="button"
onClick={() => {
dispatch(openConversationExternal({ id: conversationId, messageId }));
dispatch(
openConversationExternal({
id: conversationId,
messageId,
firstUnreadIdOnOpen: undefined,
})
);
}}
className={classNames(
'module-message-search-result',

View File

@ -11,6 +11,7 @@ import { updateUserDetailsModal } from '../state/ducks/modalDialog';
import { openConversationExternal } from '../state/ducks/conversations';
// tslint:disable-next-line: no-submodule-imports
import useKey from 'react-use/lib/useKey';
import { getFirstUnreadMessageIdInConversation } from '../data/data';
type Props = {
conversationId: string;
authorAvatarPath?: string;
@ -33,8 +34,11 @@ export const UserDetailsDialog = (props: Props) => {
convo.id,
ConversationTypeEnum.PRIVATE
);
const firstUnreadIdOnOpen = await getFirstUnreadMessageIdInConversation(conversation.id);
window.inboxStore?.dispatch(openConversationExternal({ id: conversation.id }));
window.inboxStore?.dispatch(
openConversationExternal({ id: conversation.id, firstUnreadIdOnOpen })
);
closeDialog();
}

View File

@ -39,7 +39,10 @@ import { ClickToTrustSender } from './message/ClickToTrustSender';
import { getMessageById } from '../../data/data';
import { connect } from 'react-redux';
import { StateType } from '../../state/reducer';
import { getSelectedMessageIds } from '../../state/selectors/conversations';
import {
getQuotedMessageToAnimate,
getSelectedMessageIds,
} from '../../state/selectors/conversations';
import {
messageExpired,
showLightBox,
@ -64,7 +67,10 @@ interface State {
const EXPIRATION_CHECK_MINIMUM = 2000;
const EXPIRED_DELAY = 600;
type Props = MessageRegularProps & { selectedMessages: Array<string> };
type Props = MessageRegularProps & {
selectedMessages: Array<string>;
quotedMessageToAnimate: string | undefined;
};
const onClickAttachment = async (onClickProps: {
attachment: AttachmentTypeWithPath;
@ -570,14 +576,7 @@ class MessageInner extends React.PureComponent<Props, State> {
// tslint:disable-next-line: cyclomatic-complexity
public render() {
const {
direction,
id,
multiSelectMode,
conversationType,
isUnread,
selectedMessages,
} = this.props;
const { direction, id, conversationType, isUnread, selectedMessages } = this.props;
const { expired, expiring } = this.state;
if (expired) {
@ -601,7 +600,7 @@ class MessageInner extends React.PureComponent<Props, State> {
divClasses.push('public-chat-message-wrapper');
}
if (this.props.isQuotedMessageToAnimate) {
if (this.props.quotedMessageToAnimate === this.props.id) {
divClasses.push('flash-green-once');
}
@ -851,6 +850,7 @@ class MessageInner extends React.PureComponent<Props, State> {
const mapStateToProps = (state: StateType) => {
return {
selectedMessages: getSelectedMessageIds(state),
quotedMessageToAnimate: getQuotedMessageToAnimate(state),
};
};

View File

@ -15,7 +15,7 @@ export const ReadableMessage = (props: ReadableMessageProps) => {
useFocus(onChange);
return (
<InView {...props} as="div" threshold={1} delay={200} triggerOnce={false}>
<InView {...props} as="div" threshold={0.5} delay={20} triggerOnce={false}>
{props.children}
</InView>
);

View File

@ -26,6 +26,7 @@ import autoBind from 'auto-bind';
import { onsNameRegex } from '../../session/snode_api/SNodeAPI';
import { SNodeAPI } from '../../session/snode_api';
import { clearSearch, search, updateSearchTerm } from '../../state/ducks/search';
import { getFirstUnreadMessageIdInConversation } from '../../data/data';
export interface Props {
searchTerm: string;
@ -319,7 +320,11 @@ export class LeftPaneMessageSection extends React.Component<Props, State> {
pubkeyorOns,
ConversationTypeEnum.PRIVATE
);
window.inboxStore?.dispatch(openConversationExternal({ id: pubkeyorOns }));
const firstUnreadIdOnOpen = await getFirstUnreadMessageIdInConversation(pubkeyorOns);
window.inboxStore?.dispatch(
openConversationExternal({ id: pubkeyorOns, firstUnreadIdOnOpen })
);
this.handleToggleOverlay(undefined);
} else {
// this might be an ONS, validate the regex first
@ -339,7 +344,12 @@ export class LeftPaneMessageSection extends React.Component<Props, State> {
resolvedSessionID,
ConversationTypeEnum.PRIVATE
);
window.inboxStore?.dispatch(openConversationExternal({ id: resolvedSessionID }));
const firstUnreadIdOnOpen = await getFirstUnreadMessageIdInConversation(resolvedSessionID);
window.inboxStore?.dispatch(
openConversationExternal({ id: resolvedSessionID, firstUnreadIdOnOpen })
);
this.handleToggleOverlay(undefined);
} catch (e) {
window?.log?.warn('failed to resolve ons name', pubkeyorOns, e);

View File

@ -30,7 +30,7 @@ import {
PropsForDataExtractionNotification,
QuoteClickOptions,
} from '../../../models/messageType';
import { getFirstUnreadMessageIdInConversation, getMessagesBySentAt } from '../../../data/data';
import { getMessagesBySentAt } from '../../../data/data';
import autoBind from 'auto-bind';
import { ConversationTypeEnum } from '../../../models/conversation';
import { DataExtractionNotification } from '../../conversation/DataExtractionNotification';
@ -44,9 +44,9 @@ import {
getSelectedConversationKey,
getShowScrollButton,
isMessageSelectionMode,
getFirstUnreadMessageIndex,
areMoreMessagesBeingFetched,
isFirstUnreadMessageIdAbove,
getFirstUnreadMessageId,
} from '../../../state/selectors/conversations';
import { isElectronWindowFocused } from '../../../session/utils/WindowUtils';
import useInterval from 'react-use/lib/useInterval';
@ -65,8 +65,9 @@ type Props = SessionMessageListProps & {
areMoreMessagesBeingFetched: boolean;
};
const UnreadIndicator = (props: { messageId: string; show: boolean }) => {
if (!props.show) {
const UnreadIndicator = (props: { messageId: string }) => {
const isFirstUnreadOnOpen = useSelector(getFirstUnreadMessageId);
if (!isFirstUnreadOnOpen || isFirstUnreadOnOpen !== props.messageId) {
return null;
}
return <SessionLastSeenIndicator key={`unread-indicator-${props.messageId}`} />;
@ -75,12 +76,11 @@ const UnreadIndicator = (props: { messageId: string; show: boolean }) => {
const GroupUpdateItem = (props: {
messageId: string;
groupNotificationProps: PropsForGroupUpdate;
showUnreadIndicator: boolean;
}) => {
return (
<React.Fragment key={props.messageId}>
<GroupNotification key={props.messageId} {...props.groupNotificationProps} />
<UnreadIndicator messageId={props.messageId} show={props.showUnreadIndicator} />
<UnreadIndicator messageId={props.messageId} />
</React.Fragment>
);
};
@ -88,13 +88,12 @@ const GroupUpdateItem = (props: {
const GroupInvitationItem = (props: {
messageId: string;
propsForGroupInvitation: PropsForGroupInvitation;
showUnreadIndicator: boolean;
}) => {
return (
<React.Fragment key={props.messageId}>
<GroupInvitation key={props.messageId} {...props.propsForGroupInvitation} />
<UnreadIndicator messageId={props.messageId} show={props.showUnreadIndicator} />
<UnreadIndicator messageId={props.messageId} />
</React.Fragment>
);
};
@ -102,7 +101,6 @@ const GroupInvitationItem = (props: {
const DataExtractionNotificationItem = (props: {
messageId: string;
propsForDataExtractionNotification: PropsForDataExtractionNotification;
showUnreadIndicator: boolean;
}) => {
return (
<React.Fragment key={props.messageId}>
@ -111,7 +109,7 @@ const DataExtractionNotificationItem = (props: {
{...props.propsForDataExtractionNotification}
/>
<UnreadIndicator messageId={props.messageId} show={props.showUnreadIndicator} />
<UnreadIndicator messageId={props.messageId} />
</React.Fragment>
);
};
@ -119,13 +117,12 @@ const DataExtractionNotificationItem = (props: {
const TimerNotificationItem = (props: {
messageId: string;
timerProps: PropsForExpirationTimer;
showUnreadIndicator: boolean;
}) => {
return (
<React.Fragment key={props.messageId}>
<TimerNotification key={props.messageId} {...props.timerProps} />
<UnreadIndicator messageId={props.messageId} show={props.showUnreadIndicator} />
<UnreadIndicator messageId={props.messageId} />
</React.Fragment>
);
};
@ -134,12 +131,10 @@ const GenericMessageItem = (props: {
messageId: string;
messageProps: SortedMessageModelProps;
playableMessageIndex?: number;
showUnreadIndicator: boolean;
scrollToQuoteMessage: (options: QuoteClickOptions) => Promise<void>;
playNextMessage?: (value: number) => void;
}) => {
const multiSelectMode = useSelector(isMessageSelectionMode);
const quotedMessageToAnimate = useSelector(getQuotedMessageToAnimate);
const nextMessageToPlay = useSelector(getNextMessageToPlayIndex);
const messageId = props.messageId;
@ -152,7 +147,6 @@ const GenericMessageItem = (props: {
...props.messageProps.propsForMessage,
firstMessageOfSeries: props.messageProps.firstMessageOfSeries,
multiSelectMode,
isQuotedMessageToAnimate: messageId === quotedMessageToAnimate,
nextMessageToPlay,
playNextMessage: props.playNextMessage,
onQuoteClick,
@ -166,7 +160,7 @@ const GenericMessageItem = (props: {
multiSelectMode={multiSelectMode}
key={messageId}
/>
<UnreadIndicator messageId={props.messageId} show={props.showUnreadIndicator} />
<UnreadIndicator messageId={props.messageId} />
</React.Fragment>
);
};
@ -176,7 +170,6 @@ const MessageList = (props: {
playNextMessage?: (value: number) => void;
}) => {
const messagesProps = useSelector(getSortedMessagesOfSelectedConversation);
const firstUnreadMessageIndex = useSelector(getFirstUnreadMessageIndex);
const isAbove = useSelector(isFirstUnreadMessageIdAbove);
console.warn('isAbove', isAbove);
@ -191,19 +184,12 @@ const MessageList = (props: {
const groupNotificationProps = messageProps.propsForGroupNotification;
// IF we found the first unread message
// AND we are not scrolled all the way to the bottom
// THEN, show the unread banner for the current message
const showUnreadIndicator =
Boolean(firstUnreadMessageIndex) && firstUnreadMessageIndex === index;
if (groupNotificationProps) {
return (
<GroupUpdateItem
key={messageProps.propsForMessage.id}
groupNotificationProps={groupNotificationProps}
messageId={messageProps.propsForMessage.id}
showUnreadIndicator={showUnreadIndicator}
/>
);
}
@ -214,7 +200,6 @@ const MessageList = (props: {
key={messageProps.propsForMessage.id}
propsForGroupInvitation={propsForGroupInvitation}
messageId={messageProps.propsForMessage.id}
showUnreadIndicator={showUnreadIndicator}
/>
);
}
@ -225,7 +210,6 @@ const MessageList = (props: {
key={messageProps.propsForMessage.id}
propsForDataExtractionNotification={propsForDataExtractionNotification}
messageId={messageProps.propsForMessage.id}
showUnreadIndicator={showUnreadIndicator}
/>
);
}
@ -236,7 +220,6 @@ const MessageList = (props: {
key={messageProps.propsForMessage.id}
timerProps={timerProps}
messageId={messageProps.propsForMessage.id}
showUnreadIndicator={showUnreadIndicator}
/>
);
}
@ -255,7 +238,6 @@ const MessageList = (props: {
playableMessageIndex={playableMessageIndex}
messageId={messageProps.propsForMessage.id}
messageProps={messageProps}
showUnreadIndicator={showUnreadIndicator}
scrollToQuoteMessage={props.scrollToQuoteMessage}
playNextMessage={props.playNextMessage}
/>
@ -266,7 +248,6 @@ const MessageList = (props: {
};
class SessionMessagesListInner extends React.Component<Props> {
private scrollOffsetBottomPx: number = Number.MAX_VALUE;
private ignoreScrollEvents: boolean;
private timeoutResetQuotedScroll: NodeJS.Timeout | null = null;
@ -301,7 +282,7 @@ class SessionMessagesListInner extends React.Component<Props> {
) {
// displayed conversation changed. We have a bit of cleaning to do here
this.ignoreScrollEvents = true;
this.setupTimeoutResetQuotedHighlightedMessage(true);
this.setupTimeoutResetQuotedHighlightedMessage(this.props.animateQuotedMessageId);
this.initialMessageLoadingPosition();
} else {
// if we got new message for this convo, and we are scrolled to bottom
@ -355,7 +336,7 @@ class SessionMessagesListInner extends React.Component<Props> {
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// ~~~~~~~~~~~~~ MESSAGE HANDLING ~~~~~~~~~~~~~
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
private updateReadMessages() {
private updateReadMessages(forceIsOnBottom = false) {
const { messagesProps, conversationKey } = this.props;
if (!messagesProps || messagesProps.length === 0 || !conversationKey) {
@ -372,7 +353,7 @@ class SessionMessagesListInner extends React.Component<Props> {
return;
}
if (this.getScrollOffsetBottomPx() === 0 && isElectronWindowFocused()) {
if ((forceIsOnBottom || this.getScrollOffsetBottomPx() === 0) && isElectronWindowFocused()) {
void conversation.markRead(messagesProps[0].propsForMessage.receivedAt || 0);
}
}
@ -450,10 +431,10 @@ class SessionMessagesListInner extends React.Component<Props> {
window.inboxStore?.dispatch(showScrollToBottomButton(showScrollButton));
// trigger markRead if we hit the bottom
const isScrolledToBottom = bottomOfBottomMessage >= containerBottom - 5;
const isScrolledToBottom = bottomOfBottomMessage <= containerBottom - 5;
if (isScrolledToBottom) {
// Mark messages read
this.updateReadMessages();
this.updateReadMessages(true);
}
}
@ -522,19 +503,15 @@ class SessionMessagesListInner extends React.Component<Props> {
* So we need to reset the state of of the highlighted message so when the users clicks again,
* the highlight is shown once again
*/
private setupTimeoutResetQuotedHighlightedMessage(clearOnly = false) {
private setupTimeoutResetQuotedHighlightedMessage(messageId: string | undefined) {
if (this.timeoutResetQuotedScroll) {
clearTimeout(this.timeoutResetQuotedScroll);
}
// only clear the timeout, do not schedule once again
if (clearOnly) {
return;
}
if (this.props.animateQuotedMessageId !== undefined) {
if (messageId !== undefined) {
this.timeoutResetQuotedScroll = global.setTimeout(() => {
window.inboxStore?.dispatch(quotedMessageToAnimate(undefined));
}, 3000);
}, 2000); // should match .flash-green-once
}
}
@ -548,7 +525,7 @@ class SessionMessagesListInner extends React.Component<Props> {
// we consider that a `smooth` set to true, means it's a quoted message, so highlight this message on the UI
if (smooth) {
window.inboxStore?.dispatch(quotedMessageToAnimate(messageId));
this.setupTimeoutResetQuotedHighlightedMessage();
this.setupTimeoutResetQuotedHighlightedMessage(messageId);
}
const messageContainer = this.props.messageContainerRef.current;

View File

@ -1087,8 +1087,17 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
public async markRead(readAt: number) {
this.markReadNoCommit(readAt);
await this.commit();
const convo = this.getConversation();
if (convo) {
const beforeUnread = convo.get('unreadCount');
const unreadCount = await convo.getUnreadCount();
if (beforeUnread !== unreadCount) {
convo.set({ unreadCount });
await convo.commit();
}
}
}
public markReadNoCommit(readAt: number) {

View File

@ -244,7 +244,6 @@ export interface MessageRegularProps {
multiSelectMode: boolean;
firstMessageOfSeries: boolean;
isUnread: boolean;
isQuotedMessageToAnimate?: boolean;
isTrustedForAttachmentDownload: boolean;
onQuoteClick?: (options: QuoteClickOptions) => Promise<void>;

View File

@ -31,10 +31,7 @@ import { forceSyncConfigurationNowIfNeeded } from '../session/utils/syncUtils';
import { getMessageController } from '../session/messages';
import { ClosedGroupEncryptionPairReplyMessage } from '../session/messages/outgoing/controlMessage/group/ClosedGroupEncryptionPairReplyMessage';
import { queueAllCachedFromSource } from './receiver';
import {
actions as conversationActions,
openConversationExternal,
} from '../state/ducks/conversations';
import { openConversationExternal } from '../state/ducks/conversations';
import { getSwarmPollingInstance } from '../session/snode_api';
import { MessageModel } from '../models/message';
@ -955,7 +952,9 @@ export async function createClosedGroup(groupName: string, members: Array<string
await forceSyncConfigurationNowIfNeeded();
window.inboxStore?.dispatch(openConversationExternal({ id: groupPublicKey }));
window.inboxStore?.dispatch(
openConversationExternal({ id: groupPublicKey, firstUnreadIdOnOpen: undefined })
);
}
/**

View File

@ -292,7 +292,6 @@ export type SortedMessageModelProps = MessageModelProps & {
type FetchedMessageResults = {
conversationKey: string;
messagesProps: Array<MessageModelProps>;
firstUnreadMessageId: string | undefined;
};
export const fetchMessagesForConversation = createAsyncThunk(
@ -307,8 +306,6 @@ export const fetchMessagesForConversation = createAsyncThunk(
const beforeTimestamp = Date.now();
console.time('fetchMessagesForConversation');
const messagesProps = await getMessages(conversationKey, count);
const firstUnreadMessageId = await getFirstUnreadMessageIdInConversation(conversationKey);
const afterTimestamp = Date.now();
console.timeEnd('fetchMessagesForConversation');
@ -318,7 +315,6 @@ export const fetchMessagesForConversation = createAsyncThunk(
return {
conversationKey,
messagesProps,
firstUnreadMessageId,
};
}
);
@ -594,12 +590,14 @@ const conversationsSlice = createSlice({
state: ConversationsStateType,
action: PayloadAction<{
id: string;
firstUnreadIdOnOpen: string | undefined;
messageId?: string;
}>
) {
if (state.selectedConversation === action.payload.id) {
return state;
}
return {
conversationLookup: state.conversationLookup,
selectedConversation: action.payload.id,
@ -615,7 +613,7 @@ const conversationsSlice = createSlice({
showScrollButton: false,
animateQuotedMessageId: undefined,
mentionMembers: [],
firstUnreadMessageId: undefined,
firstUnreadMessageId: action.payload.firstUnreadIdOnOpen,
};
},
showLightBox(
@ -662,14 +660,13 @@ const conversationsSlice = createSlice({
fetchMessagesForConversation.fulfilled,
(state: ConversationsStateType, action: PayloadAction<FetchedMessageResults>) => {
// this is called once the messages are loaded from the db for the currently selected conversation
const { messagesProps, conversationKey, firstUnreadMessageId } = action.payload;
const { messagesProps, conversationKey } = action.payload;
// double check that this update is for the shown convo
if (conversationKey === state.selectedConversation) {
return {
...state,
messages: messagesProps,
areMoreMessagesBeingFetched: false,
firstUnreadMessageId,
};
}
return state;

View File

@ -5,11 +5,7 @@ import { cleanSearchTerm } from '../../util/cleanSearchTerm';
import { searchConversations, searchMessages } from '../../../ts/data/data';
import { makeLookup } from '../../util/makeLookup';
import {
openConversationExternal,
PropsForSearchResults,
ReduxConversationType,
} from './conversations';
import { PropsForSearchResults, ReduxConversationType } from './conversations';
import { PubKey } from '../../session/types';
import { MessageModel } from '../../models/message';
import { MessageModelType } from '../../models/messageType';

View File

@ -402,14 +402,6 @@ function sortMessages(
return messagesSorted;
}
export const getFirstUnreadMessageIndex = createSelector(
getSortedMessagesOfSelectedConversation,
(messageModelsProps: Array<MessageModelProps>): number | undefined => {
const firstUnreadIndex = getFirstMessageUnreadIndex(messageModelsProps);
return firstUnreadIndex;
}
);
function getFirstMessageUnreadIndex(messages: Array<MessageModelProps>) {
if (!messages || messages.length === 0) {
return -1;
@ -442,7 +434,6 @@ function getFirstMessageUnreadIndex(messages: Array<MessageModelProps>) {
export const getFirstUnreadMessageId = createSelector(
getConversations,
(state: ConversationsStateType): string | undefined => {
console.warn('getFirstUnreadMessageId', state.firstUnreadMessageId);
return state.firstUnreadMessageId;
}
);