Skip to content

Commit

Permalink
Merge pull request #2996 from KeeJef/unread-message-scroll-button-cha…
Browse files Browse the repository at this point in the history
…nges

Add unread message count indicator per conversation
  • Loading branch information
Bilb committed Mar 27, 2024
2 parents 0ad6298 + 444e60c commit f6b1eac
Show file tree
Hide file tree
Showing 9 changed files with 76 additions and 38 deletions.
6 changes: 4 additions & 2 deletions ts/components/SessionScrollButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { useSelector } from 'react-redux';
import styled from 'styled-components';
import { getShowScrollButton } from '../state/selectors/conversations';

import { useSelectedUnreadCount } from '../state/selectors/selectedConversation';
import { SessionIconButton } from './icon';
import { Noop } from '../types/Util';

const SessionScrollButtonDiv = styled.div`
position: fixed;
Expand All @@ -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 unreadCount = useSelectedUnreadCount();

return (
<SessionScrollButtonDiv>
Expand All @@ -29,6 +30,7 @@ export const SessionScrollButton = (props: { onClickScrollBottom: Noop }) => {
isHidden={!show}
onClick={props.onClickScrollBottom}
dataTestId="scroll-to-bottom-button"
unreadCount={unreadCount}
/>
</SessionScrollButtonDiv>
);
Expand Down
1 change: 1 addition & 0 deletions ts/components/icon/SessionIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export type SessionIconProps = {
noScale?: boolean;
backgroundColor?: string;
dataTestId?: string;
unreadCount?: number;
};

const getIconDimensionFromIconSize = (iconSize: SessionIconSize | number) => {
Expand Down
7 changes: 4 additions & 3 deletions ts/components/icon/SessionIconButton.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import React, { KeyboardEvent } from 'react';
import classNames from 'classnames';
import _ from 'lodash';
import React, { KeyboardEvent } from 'react';
import styled from 'styled-components';

import { SessionIcon, SessionIconProps } from '.';
import { SessionNotificationCount } from './SessionNotificationCount';
import { SessionNotificationCount, SessionUnreadCount } from './SessionNotificationCount';

interface SProps extends SessionIconProps {
onClick?: (e?: React.MouseEvent<HTMLDivElement>) => void;
Expand Down Expand Up @@ -61,6 +60,7 @@ const SessionIconButtonInner = React.forwardRef<HTMLDivElement, SProps>((props,
dataTestIdIcon,
style,
tabIndex,
unreadCount,
} = props;
const clickHandler = (e: React.MouseEvent<HTMLDivElement>) => {
if (props.onClick) {
Expand Down Expand Up @@ -103,6 +103,7 @@ const SessionIconButtonInner = React.forwardRef<HTMLDivElement, SProps>((props,
dataTestId={dataTestIdIcon}
/>
{Boolean(notificationCount) && <SessionNotificationCount count={notificationCount} />}
{Boolean(unreadCount) && <SessionUnreadCount count={unreadCount} />}
</StyledSessionIconButton>
);
});
Expand Down
72 changes: 48 additions & 24 deletions ts/components/icon/SessionNotificationCount.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import React from 'react';
import styled from 'styled-components';
import { Constants } from '../../session';

type Props = {
overflowingAt: number;
centeredOnTop: boolean;
count?: number;
};

const StyledCountContainer = styled.div<{ shouldRender: boolean }>`
const StyledCountContainer = styled.div<{ centeredOnTop: boolean }>`
position: absolute;
font-size: 18px;
line-height: 1.2;
top: 27px;
left: 28px;
padding: 1px 4px;
opacity: 1;
top: ${props => (props.centeredOnTop ? '-10px' : '27px')};
left: ${props => (props.centeredOnTop ? '50%' : '28px')};
transform: ${props => (props.centeredOnTop ? 'translateX(-50%)' : 'none')};
padding: ${props => (props.centeredOnTop ? '3px 3px' : '1px 4px')};
display: flex;
align-items: center;
justify-content: center;
Expand All @@ -21,34 +23,56 @@ const StyledCountContainer = styled.div<{ shouldRender: boolean }>`
font-weight: 700;
background: var(--unread-messages-alert-background-color);
transition: var(--default-duration);
opacity: ${props => (props.shouldRender ? 1 : 0)};
text-align: center;
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;
font-size: 0.6rem;
font-size: ${props => (props.centeredOnTop ? 'var(--font-size-xs)' : '0.6rem')};
`;

export const SessionNotificationCount = (props: Props) => {
const { count } = props;
const overflow = Boolean(count && count > 99);
const shouldRender = Boolean(count && count > 0);
const OverflowingAt = (props: { overflowingAt: number }) => {
return (
<>
{props.overflowingAt}
<span>+</span>
</>
);
};

if (overflow) {
return (
<StyledCountContainer shouldRender={shouldRender}>
<StyledCount>
{99}
<span>+</span>
</StyledCount>
</StyledCountContainer>
);
const NotificationOrUnreadCount = ({ centeredOnTop, overflowingAt, count }: Props) => {
if (!count) {
return null;
}
const overflowing = count > overflowingAt;

return (
<StyledCountContainer shouldRender={shouldRender}>
<StyledCount>{count}</StyledCount>
<StyledCountContainer centeredOnTop={centeredOnTop}>
<StyledCount centeredOnTop={centeredOnTop}>
{overflowing ? <OverflowingAt overflowingAt={overflowingAt} /> : count}
</StyledCount>
</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}
/>
);
};
9 changes: 8 additions & 1 deletion ts/components/leftpane/conversation-list-item/HeaderItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
useMentionedUs,
useUnreadCount,
} from '../../../hooks/useParamSelector';
import { Constants } from '../../../session';
import {
openConversationToSpecificMessage,
openConversationWithMessages,
Expand Down Expand Up @@ -160,8 +161,14 @@ const UnreadCount = ({ convoId }: { convoId: string }) => {
const unreadMsgCount = useUnreadCount(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 ? (
<p className="module-conversation-list-item__unread-count">{unreadMsgCount || ' '}</p>
<p className="module-conversation-list-item__unread-count">{unreadWithOverflow}</p>
) : null;
};

Expand Down
6 changes: 2 additions & 4 deletions ts/hooks/useParamSelector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
hasValidOutgoingRequestValues,
} from '../models/conversation';
import { isUsAnySogsFromCache } from '../session/apis/open_group_api/sogsv3/knownBlindedkeys';
import { CONVERSATION } from '../session/constants';
import { TimerOptions, TimerOptionsArray } from '../session/disappearing_messages/timerOptions';
import { PubKey } from '../session/types';
import { UserUtils } from '../session/utils';
Expand Down Expand Up @@ -241,12 +240,11 @@ export function useMessageReactsPropsById(messageId?: string) {

/**
* 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 {
const convoProps = useConversationPropsById(conversationId);
const convoUnreadCount = convoProps?.unreadCount || 0;
return Math.min(CONVERSATION.MAX_UNREAD_COUNT, convoUnreadCount);
return convoProps?.unreadCount || 0;
}

export function useHasUnread(conversationId?: string): boolean {
Expand Down
5 changes: 3 additions & 2 deletions ts/session/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,9 @@ export const CONVERSATION = {
// Maximum voice message duraton of 5 minutes
// which equates to 1.97 MB
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.
Expand Down
2 changes: 0 additions & 2 deletions ts/state/selectors/conversations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,6 @@ const _getGlobalUnreadCount = (sortedConversations: Array<ReduxConversationType>
}

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

return globalUnreadCount;
};

Expand Down
6 changes: 6 additions & 0 deletions ts/state/selectors/selectedConversation.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { isString } from 'lodash';
import { useSelector } from 'react-redux';
import { useUnreadCount } from '../../hooks/useParamSelector';
import { ConversationTypeEnum, isOpenOrClosedGroup } from '../../models/conversationAttributes';
import {
DisappearingMessageConversationModeType,
Expand Down Expand Up @@ -302,6 +303,11 @@ export function useSelectedIsActive() {
return useSelector(getIsSelectedActive);
}

export function useSelectedUnreadCount() {
const selectedConversation = useSelectedConversationKey();
return useUnreadCount(selectedConversation);
}

export function useSelectedIsNoteToSelf() {
return useSelector(getIsSelectedNoteToSelf);
}
Expand Down

0 comments on commit f6b1eac

Please sign in to comment.