Show mentioned badges & enable scrolling to mentions in conversations
This commit is contained in:
parent
caaeda8abe
commit
d012779e87
21 changed files with 694 additions and 184 deletions
|
@ -369,6 +369,7 @@ export function ConversationList({
|
|||
'typingContactId',
|
||||
'unblurredAvatarPath',
|
||||
'unreadCount',
|
||||
'unreadMentionsCount',
|
||||
'uuid',
|
||||
]);
|
||||
const { badges, title, unreadCount, lastMessage } = itemProps;
|
||||
|
|
|
@ -7,15 +7,17 @@ import { action } from '@storybook/addon-actions';
|
|||
|
||||
import { setupI18n } from '../../util/setupI18n';
|
||||
import enMessages from '../../../_locales/en/messages.json';
|
||||
import type { Props } from './ScrollDownButton';
|
||||
import { ScrollDownButton } from './ScrollDownButton';
|
||||
import type { ScrollDownButtonPropsType } from './ScrollDownButton';
|
||||
import { ScrollDownButton, ScrollDownButtonVariant } from './ScrollDownButton';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||
const createProps = (
|
||||
overrideProps: Partial<ScrollDownButtonPropsType> = {}
|
||||
): ScrollDownButtonPropsType => ({
|
||||
variant: ScrollDownButtonVariant.UNREAD_MESSAGES,
|
||||
i18n,
|
||||
scrollDown: action('scrollDown'),
|
||||
conversationId: 'fake-conversation-id',
|
||||
onClick: action('scrollDown'),
|
||||
...overrideProps,
|
||||
});
|
||||
|
||||
|
@ -23,7 +25,7 @@ export default {
|
|||
title: 'Components/Conversation/ScrollDownButton',
|
||||
component: ScrollDownButton,
|
||||
argTypes: {
|
||||
unreadCount: {
|
||||
count: {
|
||||
control: { type: 'radio' },
|
||||
options: {
|
||||
None: undefined,
|
||||
|
@ -36,10 +38,22 @@ export default {
|
|||
} as Meta;
|
||||
|
||||
// eslint-disable-next-line react/function-component-definition
|
||||
const Template: Story<Props> = args => <ScrollDownButton {...args} />;
|
||||
const Template: Story<ScrollDownButtonPropsType> = args => (
|
||||
<ScrollDownButton {...args} />
|
||||
);
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = createProps({});
|
||||
Default.story = {
|
||||
name: 'Default',
|
||||
export const UnreadMessages = Template.bind({});
|
||||
UnreadMessages.args = createProps({
|
||||
variant: ScrollDownButtonVariant.UNREAD_MESSAGES,
|
||||
});
|
||||
UnreadMessages.story = {
|
||||
name: 'UnreadMessages',
|
||||
};
|
||||
|
||||
export const UnreadMentions = Template.bind({});
|
||||
UnreadMentions.args = createProps({
|
||||
variant: ScrollDownButtonVariant.UNREAD_MENTIONS,
|
||||
});
|
||||
UnreadMentions.story = {
|
||||
name: 'UnreadMentions',
|
||||
};
|
||||
|
|
|
@ -1,53 +1,69 @@
|
|||
// Copyright 2019 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
|
||||
import type { LocalizerType } from '../../types/Util';
|
||||
import { getClassNamesFor } from '../../util/getClassNamesFor';
|
||||
|
||||
export type Props = {
|
||||
unreadCount?: number;
|
||||
conversationId: string;
|
||||
|
||||
scrollDown: (conversationId: string) => void;
|
||||
export enum ScrollDownButtonVariant {
|
||||
UNREAD_MESSAGES = 'unread-messages',
|
||||
UNREAD_MENTIONS = 'unread-mentions',
|
||||
}
|
||||
|
||||
export type ScrollDownButtonPropsType = {
|
||||
variant: ScrollDownButtonVariant;
|
||||
count?: number;
|
||||
onClick: VoidFunction;
|
||||
i18n: LocalizerType;
|
||||
};
|
||||
|
||||
export function ScrollDownButton({
|
||||
conversationId,
|
||||
unreadCount,
|
||||
variant,
|
||||
count,
|
||||
onClick,
|
||||
i18n,
|
||||
scrollDown,
|
||||
}: Props): JSX.Element {
|
||||
const altText = unreadCount
|
||||
? i18n('icu:messagesBelow')
|
||||
: i18n('icu:scrollDown');
|
||||
}: ScrollDownButtonPropsType): JSX.Element {
|
||||
const getClassName = getClassNamesFor('ScrollDownButton');
|
||||
|
||||
let badgeText: string | undefined;
|
||||
if (unreadCount) {
|
||||
if (unreadCount < 100) {
|
||||
badgeText = unreadCount.toString();
|
||||
if (count) {
|
||||
if (count < 100) {
|
||||
badgeText = count.toString();
|
||||
} else {
|
||||
badgeText = '99+';
|
||||
}
|
||||
}
|
||||
|
||||
let altText: string;
|
||||
switch (variant) {
|
||||
case ScrollDownButtonVariant.UNREAD_MESSAGES:
|
||||
altText = count ? i18n('icu:messagesBelow') : i18n('icu:scrollDown');
|
||||
break;
|
||||
case ScrollDownButtonVariant.UNREAD_MENTIONS:
|
||||
altText = i18n('icu:mentionsBelow');
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unexpected variant: ${variant}`);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="ScrollDownButton">
|
||||
<button
|
||||
type="button"
|
||||
className="ScrollDownButton__button"
|
||||
onClick={() => {
|
||||
scrollDown(conversationId);
|
||||
}}
|
||||
title={altText}
|
||||
>
|
||||
{badgeText ? (
|
||||
<div className="ScrollDownButton__button__badge">{badgeText}</div>
|
||||
) : null}
|
||||
<div className="ScrollDownButton__button__icon" />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className={classNames(getClassName(''), getClassName(`__${variant}`))}
|
||||
onClick={onClick}
|
||||
title={altText}
|
||||
>
|
||||
{badgeText ? (
|
||||
<div className={getClassName('__badge')}>{badgeText}</div>
|
||||
) : null}
|
||||
<div
|
||||
className={classNames(
|
||||
getClassName('__icon'),
|
||||
getClassName(`__icon--${variant}`)
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -277,6 +277,7 @@ const actions = () => ({
|
|||
markMessageRead: action('markMessageRead'),
|
||||
toggleSelectMessage: action('toggleSelectMessage'),
|
||||
targetMessage: action('targetMessage'),
|
||||
scrollToOldestUnreadMention: action('scrollToOldestUnreadMention'),
|
||||
clearTargetedMessage: action('clearTargetedMessage'),
|
||||
updateSharedGroups: action('updateSharedGroups'),
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ import React from 'react';
|
|||
import Measure from 'react-measure';
|
||||
|
||||
import type { ReadonlyDeep } from 'type-fest';
|
||||
import { ScrollDownButton } from './ScrollDownButton';
|
||||
import { ScrollDownButton, ScrollDownButtonVariant } from './ScrollDownButton';
|
||||
|
||||
import type { LocalizerType, ThemeType } from '../../types/Util';
|
||||
import type { ConversationType } from '../../state/ducks/conversations';
|
||||
|
@ -100,6 +100,7 @@ type PropsHousekeepingType = {
|
|||
isIncomingMessageRequest: boolean;
|
||||
isSomeoneTyping: boolean;
|
||||
unreadCount?: number;
|
||||
unreadMentionsCount?: number;
|
||||
|
||||
targetedMessageId?: string;
|
||||
invitedContactsForNewlyCreatedGroup: Array<ConversationType>;
|
||||
|
@ -168,6 +169,7 @@ export type PropsActionsType = {
|
|||
safeConversationId: string;
|
||||
}>
|
||||
) => void;
|
||||
scrollToOldestUnreadMention: (conversationId: string) => unknown;
|
||||
};
|
||||
|
||||
export type PropsType = PropsDataType &
|
||||
|
@ -776,10 +778,12 @@ export class Timeline extends React.Component<
|
|||
renderTypingBubble,
|
||||
reviewGroupMemberNameCollision,
|
||||
reviewMessageRequestNameCollision,
|
||||
scrollToOldestUnreadMention,
|
||||
shouldShowMiniPlayer,
|
||||
theme,
|
||||
totalUnseen,
|
||||
unreadCount,
|
||||
unreadMentionsCount,
|
||||
} = this.props;
|
||||
const {
|
||||
hasRecentlyScrolled,
|
||||
|
@ -815,7 +819,7 @@ export class Timeline extends React.Component<
|
|||
areAnyMessagesUnread &&
|
||||
areAnyMessagesBelowCurrentPosition
|
||||
);
|
||||
const shouldShowScrollDownButton = Boolean(
|
||||
const shouldShowScrollDownButtons = Boolean(
|
||||
areThereAnyMessages &&
|
||||
(areUnreadBelowCurrentPosition || areSomeMessagesBelowCurrentPosition)
|
||||
);
|
||||
|
@ -1127,14 +1131,24 @@ export class Timeline extends React.Component<
|
|||
/>
|
||||
</div>
|
||||
</main>
|
||||
{shouldShowScrollDownButtons ? (
|
||||
<div className="module-timeline__scrolldown-buttons">
|
||||
{unreadMentionsCount ? (
|
||||
<ScrollDownButton
|
||||
variant={ScrollDownButtonVariant.UNREAD_MENTIONS}
|
||||
count={unreadMentionsCount}
|
||||
onClick={() => scrollToOldestUnreadMention(id)}
|
||||
i18n={i18n}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{shouldShowScrollDownButton ? (
|
||||
<ScrollDownButton
|
||||
conversationId={id}
|
||||
unreadCount={areUnreadBelowCurrentPosition ? unreadCount : 0}
|
||||
scrollDown={this.onClickScrollDownButton}
|
||||
i18n={i18n}
|
||||
/>
|
||||
<ScrollDownButton
|
||||
variant={ScrollDownButtonVariant.UNREAD_MESSAGES}
|
||||
count={areUnreadBelowCurrentPosition ? unreadCount : 0}
|
||||
onClick={this.onClickScrollDownButton}
|
||||
i18n={i18n}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -52,6 +52,7 @@ type PropsType = {
|
|||
onClick?: () => void;
|
||||
shouldShowSpinner?: boolean;
|
||||
unreadCount?: number;
|
||||
unreadMentionsCount?: number;
|
||||
avatarSize?: AvatarSize;
|
||||
testId?: string;
|
||||
} & Pick<
|
||||
|
@ -107,6 +108,7 @@ export const BaseConversationListItem: FunctionComponent<PropsType> =
|
|||
title,
|
||||
unblurredAvatarPath,
|
||||
unreadCount,
|
||||
unreadMentionsCount,
|
||||
uuid,
|
||||
} = props;
|
||||
|
||||
|
@ -166,6 +168,25 @@ export const BaseConversationListItem: FunctionComponent<PropsType> =
|
|||
);
|
||||
}
|
||||
|
||||
const unreadIndicators = (() => {
|
||||
if (!isUnread) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className={`${CONTENT_CLASS_NAME}__unread-indicators`}>
|
||||
{unreadMentionsCount ? (
|
||||
<UnreadIndicator variant={UnreadIndicatorVariant.UNREAD_MENTIONS} />
|
||||
) : null}
|
||||
{unreadCount ? (
|
||||
<UnreadIndicator
|
||||
variant={UnreadIndicatorVariant.UNREAD_MESSAGES}
|
||||
count={unreadCount}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})();
|
||||
|
||||
const contents = (
|
||||
<>
|
||||
<div className={AVATAR_CONTAINER_CLASS_NAME}>
|
||||
|
@ -189,7 +210,7 @@ export const BaseConversationListItem: FunctionComponent<PropsType> =
|
|||
? { badge: props.badge, theme: props.theme }
|
||||
: { badge: undefined })}
|
||||
/>
|
||||
<UnreadIndicator count={unreadCount} isUnread={isUnread} />
|
||||
{unreadIndicators}
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
|
@ -216,7 +237,7 @@ export const BaseConversationListItem: FunctionComponent<PropsType> =
|
|||
</div>
|
||||
)}
|
||||
{messageStatusIcon}
|
||||
<UnreadIndicator count={unreadCount} isUnread={isUnread} />
|
||||
{unreadIndicators}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
@ -315,17 +336,46 @@ function Timestamp({
|
|||
);
|
||||
}
|
||||
|
||||
function UnreadIndicator({
|
||||
count = 0,
|
||||
isUnread,
|
||||
}: Readonly<{ count?: number; isUnread: boolean }>) {
|
||||
if (!isUnread) {
|
||||
return null;
|
||||
enum UnreadIndicatorVariant {
|
||||
UNREAD_MESSAGES = 'unread-messages',
|
||||
UNREAD_MENTIONS = 'unread-mentions',
|
||||
}
|
||||
|
||||
type UnreadIndicatorPropsType =
|
||||
| {
|
||||
variant: UnreadIndicatorVariant.UNREAD_MESSAGES;
|
||||
count: number;
|
||||
}
|
||||
| { variant: UnreadIndicatorVariant.UNREAD_MENTIONS };
|
||||
|
||||
function UnreadIndicator(props: UnreadIndicatorPropsType) {
|
||||
let content: React.ReactNode;
|
||||
|
||||
switch (props.variant) {
|
||||
case UnreadIndicatorVariant.UNREAD_MESSAGES:
|
||||
content = props.count > 0 && props.count;
|
||||
break;
|
||||
case UnreadIndicatorVariant.UNREAD_MENTIONS:
|
||||
content = (
|
||||
<div
|
||||
className={classNames(
|
||||
`${BASE_CLASS_NAME}__unread-indicator--${props.variant}__icon`
|
||||
)}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw new Error('Unexpected variant');
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classNames(`${BASE_CLASS_NAME}__unread-indicator`)}>
|
||||
{Boolean(count) && count}
|
||||
<div
|
||||
className={classNames(
|
||||
`${BASE_CLASS_NAME}__unread-indicator`,
|
||||
`${BASE_CLASS_NAME}__unread-indicator--${props.variant}`
|
||||
)}
|
||||
>
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -63,6 +63,7 @@ export type PropsData = Pick<
|
|||
| 'typingContactId'
|
||||
| 'unblurredAvatarPath'
|
||||
| 'unreadCount'
|
||||
| 'unreadMentionsCount'
|
||||
| 'uuid'
|
||||
> & {
|
||||
badge?: BadgeType;
|
||||
|
@ -106,6 +107,7 @@ export const ConversationListItem: FunctionComponent<Props> = React.memo(
|
|||
typingContactId,
|
||||
unblurredAvatarPath,
|
||||
unreadCount,
|
||||
unreadMentionsCount,
|
||||
uuid,
|
||||
}) {
|
||||
const isMuted = Boolean(muteExpiresAt && Date.now() < muteExpiresAt);
|
||||
|
@ -217,6 +219,7 @@ export const ConversationListItem: FunctionComponent<Props> = React.memo(
|
|||
theme={theme}
|
||||
title={title}
|
||||
unreadCount={unreadCount}
|
||||
unreadMentionsCount={unreadMentionsCount}
|
||||
unblurredAvatarPath={unblurredAvatarPath}
|
||||
uuid={uuid}
|
||||
/>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue