Merge pull request #2996 from KeeJef/unread-message-scroll-button-changes

Add unread message count indicator per conversation
pull/3063/head
Audric Ackermann 1 year ago committed by GitHub
commit f6b1eac5ad
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -3,8 +3,8 @@ import { useSelector } from 'react-redux';
import styled from 'styled-components'; import styled from 'styled-components';
import { getShowScrollButton } from '../state/selectors/conversations'; import { getShowScrollButton } from '../state/selectors/conversations';
import { useSelectedUnreadCount } from '../state/selectors/selectedConversation';
import { SessionIconButton } from './icon'; import { SessionIconButton } from './icon';
import { Noop } from '../types/Util';
const SessionScrollButtonDiv = styled.div` const SessionScrollButtonDiv = styled.div`
position: fixed; position: fixed;
@ -18,8 +18,9 @@ const SessionScrollButtonDiv = styled.div`
} }
`; `;
export const SessionScrollButton = (props: { onClickScrollBottom: Noop }) => { export const SessionScrollButton = (props: { onClickScrollBottom: () => void }) => {
const show = useSelector(getShowScrollButton); const show = useSelector(getShowScrollButton);
const unreadCount = useSelectedUnreadCount();
return ( return (
<SessionScrollButtonDiv> <SessionScrollButtonDiv>
@ -29,6 +30,7 @@ export const SessionScrollButton = (props: { onClickScrollBottom: Noop }) => {
isHidden={!show} isHidden={!show}
onClick={props.onClickScrollBottom} onClick={props.onClickScrollBottom}
dataTestId="scroll-to-bottom-button" dataTestId="scroll-to-bottom-button"
unreadCount={unreadCount}
/> />
</SessionScrollButtonDiv> </SessionScrollButtonDiv>
); );

@ -16,6 +16,7 @@ export type SessionIconProps = {
noScale?: boolean; noScale?: boolean;
backgroundColor?: string; backgroundColor?: string;
dataTestId?: string; dataTestId?: string;
unreadCount?: number;
}; };
const getIconDimensionFromIconSize = (iconSize: SessionIconSize | number) => { const getIconDimensionFromIconSize = (iconSize: SessionIconSize | number) => {

@ -1,10 +1,9 @@
import React, { KeyboardEvent } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import _ from 'lodash'; import _ from 'lodash';
import React, { KeyboardEvent } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { SessionIcon, SessionIconProps } from '.'; import { SessionIcon, SessionIconProps } from '.';
import { SessionNotificationCount } from './SessionNotificationCount'; import { SessionNotificationCount, SessionUnreadCount } from './SessionNotificationCount';
interface SProps extends SessionIconProps { interface SProps extends SessionIconProps {
onClick?: (e?: React.MouseEvent<HTMLDivElement>) => void; onClick?: (e?: React.MouseEvent<HTMLDivElement>) => void;
@ -61,6 +60,7 @@ const SessionIconButtonInner = React.forwardRef<HTMLDivElement, SProps>((props,
dataTestIdIcon, dataTestIdIcon,
style, style,
tabIndex, tabIndex,
unreadCount,
} = props; } = props;
const clickHandler = (e: React.MouseEvent<HTMLDivElement>) => { const clickHandler = (e: React.MouseEvent<HTMLDivElement>) => {
if (props.onClick) { if (props.onClick) {
@ -103,6 +103,7 @@ const SessionIconButtonInner = React.forwardRef<HTMLDivElement, SProps>((props,
dataTestId={dataTestIdIcon} dataTestId={dataTestIdIcon}
/> />
{Boolean(notificationCount) && <SessionNotificationCount count={notificationCount} />} {Boolean(notificationCount) && <SessionNotificationCount count={notificationCount} />}
{Boolean(unreadCount) && <SessionUnreadCount count={unreadCount} />}
</StyledSessionIconButton> </StyledSessionIconButton>
); );
}); });

@ -1,18 +1,20 @@
import React from 'react'; import React from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { Constants } from '../../session';
type Props = { type Props = {
overflowingAt: number;
centeredOnTop: boolean;
count?: number; count?: number;
}; };
const StyledCountContainer = styled.div<{ centeredOnTop: boolean }>`
const StyledCountContainer = styled.div<{ shouldRender: boolean }>`
position: absolute; position: absolute;
font-size: 18px; font-size: 18px;
line-height: 1.2; line-height: 1.2;
top: 27px; top: ${props => (props.centeredOnTop ? '-10px' : '27px')};
left: 28px; left: ${props => (props.centeredOnTop ? '50%' : '28px')};
padding: 1px 4px; transform: ${props => (props.centeredOnTop ? 'translateX(-50%)' : 'none')};
opacity: 1; padding: ${props => (props.centeredOnTop ? '3px 3px' : '1px 4px')};
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@ -21,34 +23,56 @@ const StyledCountContainer = styled.div<{ shouldRender: boolean }>`
font-weight: 700; font-weight: 700;
background: var(--unread-messages-alert-background-color); background: var(--unread-messages-alert-background-color);
transition: var(--default-duration); transition: var(--default-duration);
opacity: ${props => (props.shouldRender ? 1 : 0)};
text-align: center; text-align: center;
color: var(--unread-messages-alert-text-color); color: var(--unread-messages-alert-text-color);
white-space: ${props => (props.centeredOnTop ? 'nowrap' : 'normal')};
`; `;
const StyledCount = styled.div` const StyledCount = styled.div<{ centeredOnTop: boolean }>`
position: relative; position: relative;
font-size: 0.6rem; font-size: ${props => (props.centeredOnTop ? 'var(--font-size-xs)' : '0.6rem')};
`; `;
export const SessionNotificationCount = (props: Props) => { const OverflowingAt = (props: { overflowingAt: number }) => {
const { count } = props; return (
const overflow = Boolean(count && count > 99); <>
const shouldRender = Boolean(count && count > 0); {props.overflowingAt}
<span>+</span>
</>
);
};
if (overflow) { const NotificationOrUnreadCount = ({ centeredOnTop, overflowingAt, count }: Props) => {
return ( if (!count) {
<StyledCountContainer shouldRender={shouldRender}> return null;
<StyledCount>
{99}
<span>+</span>
</StyledCount>
</StyledCountContainer>
);
} }
const overflowing = count > overflowingAt;
return ( return (
<StyledCountContainer shouldRender={shouldRender}> <StyledCountContainer centeredOnTop={centeredOnTop}>
<StyledCount>{count}</StyledCount> <StyledCount centeredOnTop={centeredOnTop}>
{overflowing ? <OverflowingAt overflowingAt={overflowingAt} /> : count}
</StyledCount>
</StyledCountContainer> </StyledCountContainer>
); );
}; };
export const SessionNotificationCount = (props: Pick<Props, 'count'>) => {
return (
<NotificationOrUnreadCount
centeredOnTop={false}
overflowingAt={Constants.CONVERSATION.MAX_GLOBAL_UNREAD_COUNT}
count={props.count}
/>
);
};
export const SessionUnreadCount = (props: Pick<Props, 'count'>) => {
return (
<NotificationOrUnreadCount
centeredOnTop={true}
overflowingAt={Constants.CONVERSATION.MAX_CONVO_UNREAD_COUNT}
count={props.count}
/>
);
};

@ -13,6 +13,7 @@ import {
useMentionedUs, useMentionedUs,
useUnreadCount, useUnreadCount,
} from '../../../hooks/useParamSelector'; } from '../../../hooks/useParamSelector';
import { Constants } from '../../../session';
import { import {
openConversationToSpecificMessage, openConversationToSpecificMessage,
openConversationWithMessages, openConversationWithMessages,
@ -160,8 +161,14 @@ const UnreadCount = ({ convoId }: { convoId: string }) => {
const unreadMsgCount = useUnreadCount(convoId); const unreadMsgCount = useUnreadCount(convoId);
const forcedUnread = useIsForcedUnreadWithoutUnreadMsg(convoId); const forcedUnread = useIsForcedUnreadWithoutUnreadMsg(convoId);
const unreadWithOverflow =
unreadMsgCount > Constants.CONVERSATION.MAX_CONVO_UNREAD_COUNT
? `${Constants.CONVERSATION.MAX_CONVO_UNREAD_COUNT}+`
: unreadMsgCount || ' ';
// TODO would be good to merge the style of this with SessionNotificationCount or SessionUnreadCount at some point.
return unreadMsgCount > 0 || forcedUnread ? ( return unreadMsgCount > 0 || forcedUnread ? (
<p className="module-conversation-list-item__unread-count">{unreadMsgCount || ' '}</p> <p className="module-conversation-list-item__unread-count">{unreadWithOverflow}</p>
) : null; ) : null;
}; };

@ -7,7 +7,6 @@ import {
hasValidOutgoingRequestValues, hasValidOutgoingRequestValues,
} from '../models/conversation'; } from '../models/conversation';
import { isUsAnySogsFromCache } from '../session/apis/open_group_api/sogsv3/knownBlindedkeys'; import { isUsAnySogsFromCache } from '../session/apis/open_group_api/sogsv3/knownBlindedkeys';
import { CONVERSATION } from '../session/constants';
import { TimerOptions, TimerOptionsArray } from '../session/disappearing_messages/timerOptions'; import { TimerOptions, TimerOptionsArray } from '../session/disappearing_messages/timerOptions';
import { PubKey } from '../session/types'; import { PubKey } from '../session/types';
import { UserUtils } from '../session/utils'; import { UserUtils } from '../session/utils';
@ -241,12 +240,11 @@ export function useMessageReactsPropsById(messageId?: string) {
/** /**
* Returns the unread count of that conversation, or 0 if none are found. * Returns the unread count of that conversation, or 0 if none are found.
* Note: returned value is capped at a max of CONVERSATION.MAX_UNREAD_COUNT * Note: returned value is capped at a max of CONVERSATION.MAX_CONVO_UNREAD_COUNT
*/ */
export function useUnreadCount(conversationId?: string): number { export function useUnreadCount(conversationId?: string): number {
const convoProps = useConversationPropsById(conversationId); const convoProps = useConversationPropsById(conversationId);
const convoUnreadCount = convoProps?.unreadCount || 0; return convoProps?.unreadCount || 0;
return Math.min(CONVERSATION.MAX_UNREAD_COUNT, convoUnreadCount);
} }
export function useHasUnread(conversationId?: string): boolean { export function useHasUnread(conversationId?: string): boolean {

@ -51,8 +51,9 @@ export const CONVERSATION = {
// Maximum voice message duraton of 5 minutes // Maximum voice message duraton of 5 minutes
// which equates to 1.97 MB // which equates to 1.97 MB
MAX_VOICE_MESSAGE_DURATION: 300, MAX_VOICE_MESSAGE_DURATION: 300,
MAX_UNREAD_COUNT: 999, MAX_CONVO_UNREAD_COUNT: 999,
}; MAX_GLOBAL_UNREAD_COUNT: 99, // the global one does not look good with 4 digits (999+) so we have a smaller one for it
} as const;
/** /**
* The file server and onion request max upload size is 10MB precisely. * The file server and onion request max upload size is 10MB precisely.

@ -336,7 +336,6 @@ const _getGlobalUnreadCount = (sortedConversations: Array<ReduxConversationType>
} }
if ( if (
globalUnreadCount < 100 &&
isNumber(conversation.unreadCount) && isNumber(conversation.unreadCount) &&
isFinite(conversation.unreadCount) && isFinite(conversation.unreadCount) &&
conversation.unreadCount > 0 && conversation.unreadCount > 0 &&
@ -345,7 +344,6 @@ const _getGlobalUnreadCount = (sortedConversations: Array<ReduxConversationType>
globalUnreadCount += conversation.unreadCount; globalUnreadCount += conversation.unreadCount;
} }
} }
return globalUnreadCount; return globalUnreadCount;
}; };

@ -1,5 +1,6 @@
import { isString } from 'lodash'; import { isString } from 'lodash';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { useUnreadCount } from '../../hooks/useParamSelector';
import { ConversationTypeEnum, isOpenOrClosedGroup } from '../../models/conversationAttributes'; import { ConversationTypeEnum, isOpenOrClosedGroup } from '../../models/conversationAttributes';
import { import {
DisappearingMessageConversationModeType, DisappearingMessageConversationModeType,
@ -302,6 +303,11 @@ export function useSelectedIsActive() {
return useSelector(getIsSelectedActive); return useSelector(getIsSelectedActive);
} }
export function useSelectedUnreadCount() {
const selectedConversation = useSelectedConversationKey();
return useUnreadCount(selectedConversation);
}
export function useSelectedIsNoteToSelf() { export function useSelectedIsNoteToSelf() {
return useSelector(getIsSelectedNoteToSelf); return useSelector(getIsSelectedNoteToSelf);
} }

Loading…
Cancel
Save