Skip to content

Commit

Permalink
feat(notifications): implement room unread highlight tracking and wir…
Browse files Browse the repository at this point in the history
…e up to notifications (#2485)

* feat(notifications): use room unread count to process notifications

* refactor(matrix-client/notification-saga): remove outdated notification processing

* feat(notifications): implement room unread highlight tracking and wire up to notifications
  • Loading branch information
domw30 authored Dec 4, 2024
1 parent 3280d51 commit 5c722b8
Show file tree
Hide file tree
Showing 23 changed files with 321 additions and 129 deletions.
24 changes: 21 additions & 3 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,14 @@ import { denormalizeConversations } from './store/channels-list';
import { DefaultRoomLabels } from './store/channels';

export const App = () => {
const { isAuthenticated, mainClassName, videoBackgroundSrc, wrapperClassName, hasUnreadNotifications } = useAppMain();
const {
isAuthenticated,
mainClassName,
videoBackgroundSrc,
wrapperClassName,
hasUnreadNotifications,
hasUnreadHighlights,
} = useAppMain();

return (
// See: ZOS-115
Expand All @@ -25,7 +32,7 @@ export const App = () => {
{videoBackgroundSrc && <VideoBackground src={videoBackgroundSrc} />}
<div className={wrapperClassName}>
<DialogManager />
<AppBar hasUnreadNotifications={hasUnreadNotifications} />
<AppBar hasUnreadNotifications={hasUnreadNotifications} hasUnreadHighlights={hasUnreadHighlights} />
<AppRouter />
</div>
</>
Expand All @@ -44,7 +51,17 @@ const useAppMain = () => {
const conversations = denormalizeConversations(state);
return conversations.some(
(channel) =>
channel.unreadCount > 0 &&
channel.unreadCount?.total > 0 &&
!channel.labels?.includes(DefaultRoomLabels.ARCHIVED) &&
!channel.labels?.includes(DefaultRoomLabels.MUTE)
);
});

const hasUnreadHighlights = useSelector((state: RootState) => {
const conversations = denormalizeConversations(state);
return conversations.some(
(channel) =>
channel.unreadCount?.highlight > 0 &&
!channel.labels?.includes(DefaultRoomLabels.ARCHIVED) &&
!channel.labels?.includes(DefaultRoomLabels.MUTE)
);
Expand All @@ -64,6 +81,7 @@ const useAppMain = () => {
videoBackgroundSrc,
wrapperClassName,
hasUnreadNotifications,
hasUnreadHighlights,
};
};

Expand Down
16 changes: 14 additions & 2 deletions src/components/app-bar/container.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
import { useRouteMatch } from 'react-router-dom';
import { AppBar as AppBarComponent } from './';

export const AppBar = ({ hasUnreadNotifications }: { hasUnreadNotifications: boolean }) => {
export const AppBar = ({
hasUnreadNotifications,
hasUnreadHighlights,
}: {
hasUnreadNotifications: boolean;
hasUnreadHighlights: boolean;
}) => {
const match = useRouteMatch('/:app');

return <AppBarComponent activeApp={match?.params?.app ?? ''} hasUnreadNotifications={hasUnreadNotifications} />;
return (
<AppBarComponent
activeApp={match?.params?.app ?? ''}
hasUnreadNotifications={hasUnreadNotifications}
hasUnreadHighlights={hasUnreadHighlights}
/>
);
};
2 changes: 1 addition & 1 deletion src/components/app-bar/container.vitest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ vi.mock('./', () => ({
const renderComponent = (route: string | undefined = '/') => {
render(
<MemoryRouter initialEntries={[route]}>
<AppBar hasUnreadNotifications={false} />
<AppBar hasUnreadNotifications={false} hasUnreadHighlights={false} />
</MemoryRouter>
);
};
Expand Down
8 changes: 5 additions & 3 deletions src/components/app-bar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const cn = bemClassName('app-bar');
export interface Properties {
activeApp: string | undefined;
hasUnreadNotifications: boolean;
hasUnreadHighlights: boolean;
}

interface State {
Expand All @@ -29,12 +30,13 @@ export class AppBar extends React.Component<Properties, State> {
closeModal = () => this.setState({ isModalOpen: false });

renderNotificationIcon = () => {
const { hasUnreadNotifications } = this.props;
const { hasUnreadNotifications, hasUnreadHighlights } = this.props;

return (
<div {...cn('notification-icon-wrapper')}>
<IconBell1 size={24} />
{hasUnreadNotifications && <div {...cn('notification-dot')} />}
<IconBell1 size={24} {...cn('notification-icon', hasUnreadHighlights && 'highlight')} />
{hasUnreadNotifications && !hasUnreadHighlights && <div {...cn('notification-dot')} />}
{hasUnreadHighlights && <div {...cn('highlight-dot')} />}
</div>
);
};
Expand Down
1 change: 1 addition & 0 deletions src/components/app-bar/index.vitest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ vi.mock('./world-panel-item', () => ({
const DEFAULT_PROPS: Properties = {
activeApp: undefined,
hasUnreadNotifications: false,
hasUnreadHighlights: false,
};

const renderComponent = (props: Partial<Properties>) => {
Expand Down
16 changes: 16 additions & 0 deletions src/components/app-bar/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@
position: relative;
}

&__notification-icon {
&--highlight {
color: rgba(255 132 49);
}
}

&__notification-dot {
position: absolute;
top: -2px;
Expand All @@ -29,4 +35,14 @@
border-radius: 50%;
background-color: theme.$color-secondary-11;
}

&__highlight-dot {
position: absolute;
top: -2px;
right: -2px;
width: 8px;
height: 8px;
border-radius: 50%;
background-color: rgba(255 132 49);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,28 @@
letter-spacing: 0.32px;
}

&__unread-highlight {
@include glass-state-default-color;
@include glass-glow-highlight-small;
@include glass-materials-raised;

display: flex;
justify-content: center;
align-items: center;
flex-grow: 0;
flex-shrink: 0;
color: rgba(255 132 49);
border-radius: 50%;
width: 16px;
height: 16px;
text-align: center;
font-size: 8px;
font-weight: 700;
line-height: 11px;
margin-left: 3px;
letter-spacing: 0.32px;
}

&__content {
display: flex;
align-items: center;
Expand Down
30 changes: 25 additions & 5 deletions src/components/messenger/list/conversation-item/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,27 +92,39 @@ describe(ConversationItem, () => {

it('does not show unread count if there are no unread messages', function () {
const wrapper = subject({
conversation: { id: 'id', unreadCount: 0, otherMembers: [] } as any,
conversation: { id: 'id', unreadCount: { total: 0, highlight: 0 }, otherMembers: [] } as any,
});

expect(wrapper).not.toHaveElement(c('unread-count'));
});

it('shows unread message count', function () {
const wrapper = subject({ conversation: { id: 'id', unreadCount: 7, otherMembers: [] } as any });
const wrapper = subject({
conversation: { id: 'id', unreadCount: { total: 7, highlight: 0 }, otherMembers: [] } as any,
});

expect(wrapper.find(c('unread-count'))).toHaveText('7');
});

it('renders the message preview', function () {
const wrapper = subject({ conversation: { messagePreview: 'I said something here', otherMembers: [] } as any });
const wrapper = subject({
conversation: {
messagePreview: 'I said something here',
otherMembers: [],
unreadCount: { total: 0, highlight: 0 },
} as any,
});

expect(wrapper.find(ContentHighlighter)).toHaveProp('message', 'I said something here');
});

it('renders the otherMembersTyping', function () {
const wrapper = subject({
conversation: { otherMembersTyping: ['Johnny'], otherMembers: [] } as any,
conversation: {
otherMembersTyping: ['Johnny'],
otherMembers: [],
unreadCount: { total: 0, highlight: 0 },
} as any,
});

expect(wrapper.find(ContentHighlighter)).toHaveProp('message', 'Johnny is typing...');
Expand All @@ -124,14 +136,21 @@ describe(ConversationItem, () => {
messagePreview: 'I said something here',
otherMembersTyping: ['Dale', 'Dom'],
otherMembers: [],
unreadCount: { total: 0, highlight: 0 },
} as any,
});

expect(wrapper.find(ContentHighlighter)).toHaveProp('message', 'Dale and Dom are typing...');
});

it('renders the previewDisplayDate', function () {
const wrapper = subject({ conversation: { previewDisplayDate: 'Aug 1, 2021', otherMembers: [] } as any });
const wrapper = subject({
conversation: {
previewDisplayDate: 'Aug 1, 2021',
otherMembers: [],
unreadCount: { total: 0, highlight: 0 },
} as any,
});

expect(wrapper.find(c('timestamp'))).toHaveText('Aug 1, 2021');
});
Expand All @@ -145,5 +164,6 @@ function convoWith(...otherMembers): any {
return {
id: 'convo-id',
otherMembers,
unreadCount: { total: 0, highlight: 0 },
};
}
8 changes: 6 additions & 2 deletions src/components/messenger/list/conversation-item/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,8 @@ export class ConversationItem extends React.Component<Properties, State> {
render() {
const { conversation, activeConversationId } = this.props;
const { previewDisplayDate, otherMembersTyping } = conversation;
const hasUnreadMessages = conversation.unreadCount !== 0;
const hasUnreadMessages = conversation.unreadCount.total !== 0;
const hasUnreadHighlights = conversation.unreadCount.highlight !== 0;
const isUnread = hasUnreadMessages ? 'true' : 'false';
const isActive = conversation.id === activeConversationId ? 'true' : 'false';
const isTyping = (otherMembersTyping || []).length > 0 ? 'true' : 'false';
Expand Down Expand Up @@ -161,7 +162,10 @@ export class ConversationItem extends React.Component<Properties, State> {
<div {...cn('message')} is-unread={isUnread} is-typing={isTyping}>
<ContentHighlighter message={this.getMessagePreview()} variant='negative' tabIndex={-1} />
</div>
{hasUnreadMessages && <div {...cn('unread-count')}>{conversation.unreadCount}</div>}
{hasUnreadMessages && !hasUnreadHighlights && (
<div {...cn('unread-count')}>{conversation.unreadCount.total}</div>
)}
{hasUnreadHighlights && <div {...cn('unread-highlight')}>{conversation.unreadCount.highlight}</div>}
</div>
</div>
)}
Expand Down
Loading

0 comments on commit 5c722b8

Please sign in to comment.