Skip to content

Commit

Permalink
Merge pull request #3632 from tloncorp/po/tlon-2110-add-mark-as-read-…
Browse files Browse the repository at this point in the history
…to-home-button

web: add mark all read button to home button
  • Loading branch information
patosullivan authored Jun 18, 2024
2 parents 0eb7bff + fbc57ac commit a69256d
Show file tree
Hide file tree
Showing 6 changed files with 139 additions and 22 deletions.
2 changes: 2 additions & 0 deletions apps/tlon-web/src/components/ActionMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -166,4 +166,6 @@ const ActionMenu = React.memo(
}
);

ActionMenu.displayName = 'ActionMenu';

export default ActionMenu;
27 changes: 25 additions & 2 deletions apps/tlon-web/src/components/NavTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ import {
AnchorHTMLAttributes,
ButtonHTMLAttributes,
PropsWithChildren,
useEffect,
useState,
} from 'react';
import { NavLink, NavLinkProps } from 'react-router-dom';

import useLongPress from '@/logic/useLongPress';

const DOUBLE_CLICK_WINDOW = 300;

type NavTabProps = PropsWithChildren<
Expand Down Expand Up @@ -66,17 +69,27 @@ type DoubleClickableNavTabProps = PropsWithChildren<
linkClass?: string;
onSingleClick: () => void;
onDoubleClick: () => void;
onLongPress?: () => void;
}
>;

export function DoubleClickableNavTab({
onSingleClick,
onDoubleClick,
onLongPress,
...props
}: DoubleClickableNavTabProps) {
const [clickTimeout, setClickTimeout] = useState<number | null>(null);
const { action, handlers } = useLongPress();

useEffect(() => {
if (action === 'longpress') {
onLongPress?.();
}
}, [action, onLongPress]);

const onClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
handlers.onClick();
e.preventDefault();

if (clickTimeout !== null) {
Expand All @@ -93,7 +106,7 @@ export function DoubleClickableNavTab({
};

return (
<NavTab onClick={onClick} {...props}>
<NavTab {...handlers} onClick={onClick} {...props}>
{props.children}
</NavTab>
);
Expand All @@ -104,17 +117,27 @@ type DoubleClickableNavButtonProps = PropsWithChildren<
className?: string;
onSingleClick: () => void;
onDoubleClick: () => void;
onLongPress?: () => void;
}
>;

export function DoubleClickableNavButton({
onSingleClick,
onDoubleClick,
onLongPress,
...props
}: DoubleClickableNavButtonProps) {
const [clickTimeout, setClickTimeout] = useState<number | null>(null);
const { action, handlers } = useLongPress();

useEffect(() => {
if (action === 'longpress') {
onLongPress?.();
}
}, [action, onLongPress]);

const onClick = (e: React.MouseEvent<HTMLButtonElement>) => {
handlers.onClick();
e.preventDefault();

if (clickTimeout !== null) {
Expand All @@ -131,7 +154,7 @@ export function DoubleClickableNavButton({
};

return (
<button onClick={onClick} {...props}>
<button {...handlers} onClick={onClick} {...props}>
{props.children}
</button>
);
Expand Down
79 changes: 59 additions & 20 deletions apps/tlon-web/src/components/Sidebar/AppNav.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import cn from 'classnames';
import { useContext, useEffect, useState } from 'react';
import { useCallback, useContext, useEffect, useState } from 'react';
import { Outlet, useLocation, useNavigate } from 'react-router';
import { Link } from 'react-router-dom';

Expand All @@ -16,8 +16,10 @@ import { useMessagesFilter } from '@/state/settings';
import {
useCombinedChatUnreads,
useCombinedGroupUnreads,
useMarkAllGroupsRead,
} from '@/state/unreads';

import ActionMenu from '../ActionMenu';
import Avatar from '../Avatar';
import useLeap from '../Leap/useLeap';
import NavTab, {
Expand All @@ -31,9 +33,11 @@ import MessagesIcon from '../icons/MessagesIcon';
import useActiveTab, { ActiveTab } from './util';

function GroupsTab(props: { isInactive: boolean; isDarkMode: boolean }) {
const [optionsOpen, setOptionsOpen] = useState(false);
const navigate = useNavigate();
const { groupsLocation } = useLocalState.getState();
const groupsUnread = useCombinedGroupUnreads();
const markAllGroupsRead = useMarkAllGroupsRead();
const isMobile = useIsMobile();

const onSingleClick = () => {
Expand All @@ -44,11 +48,16 @@ function GroupsTab(props: { isInactive: boolean; isDarkMode: boolean }) {
}
};

const onLongPress = useCallback(() => {
setOptionsOpen(true);
}, [setOptionsOpen]);

if (isMobile) {
return (
<DoubleClickableNavTab
onSingleClick={onSingleClick}
onDoubleClick={() => navigate('/')}
onLongPress={onLongPress}
linkClass="h-full !pb-0 flex flex-col items-start justify-start"
aria-label="Groups"
data-testid="groups-tab"
Expand All @@ -73,36 +82,66 @@ function GroupsTab(props: { isInactive: boolean; isDarkMode: boolean }) {
)}
/>
</div>
<ActionMenu
open={optionsOpen}
onOpenChange={setOptionsOpen}
actions={[
{
key: 'mark-all-read',
content: 'Mark all read',
onClick: () => markAllGroupsRead(),
},
]}
/>
</DoubleClickableNavTab>
);
}

return (
<DoubleClickableNavButton
className={cn(
'relative m-auto flex h-10 w-10 items-center justify-center rounded-lg hover:bg-gray-50',
!props.isInactive && '!bg-gray-100'
)}
className="m-auto flex rounded-lg"
onSingleClick={onSingleClick}
onDoubleClick={() => navigate('/')}
onLongPress={onLongPress}
aria-label="Groups"
data-testid="groups-tab"
>
<HomeIconMobileNav
isInactive={props.isInactive}
isDarkMode={props.isDarkMode}
className="h-6 w-6"
/>
<div
className={cn(
'h-1 w-1 rounded-full top-1 right-1 absolute',
groupsUnread.unread
? groupsUnread.notify
? 'bg-blue'
: 'bg-gray-400'
: ''
)}
/>
<ActionMenu
open={optionsOpen}
onOpenChange={setOptionsOpen}
disabled={!optionsOpen}
actions={[
{
key: 'mark-all-read',
content: 'Mark all read',
onClick: () => markAllGroupsRead(),
},
]}
asChild
>
<div
className={cn(
'relative flex h-10 w-10 items-center justify-center rounded-lg hover:bg-gray-50',
!props.isInactive && '!bg-gray-100'
)}
>
<HomeIconMobileNav
isInactive={props.isInactive}
isDarkMode={props.isDarkMode}
className="h-6 w-6"
/>
<div
className={cn(
'h-1 w-1 rounded-full top-1 right-1 absolute',
groupsUnread.unread
? groupsUnread.notify
? 'bg-blue'
: 'bg-gray-400'
: ''
)}
/>
</div>
</ActionMenu>
</DoubleClickableNavButton>
);
}
Expand Down
2 changes: 2 additions & 0 deletions apps/tlon-web/src/components/Sidebar/GroupsSidebarItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,6 @@ const GroupsSidebarItem = React.memo(
}
);

GroupsSidebarItem.displayName = 'GroupsSidebarItem';

export default GroupsSidebarItem;
2 changes: 2 additions & 0 deletions apps/tlon-web/src/groups/GroupActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -387,4 +387,6 @@ const GroupActions = React.memo(
}
);

GroupActions.displayName = 'GroupActions';

export default GroupActions;
49 changes: 49 additions & 0 deletions apps/tlon-web/src/state/unreads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import create from 'zustand';

import { createDevLogger } from '@/logic/utils';

import { useMarkReadMutation } from './activity';
import { SidebarFilter } from './settings';

export type ReadStatus = 'read' | 'seen' | 'unread';
Expand Down Expand Up @@ -398,6 +399,54 @@ export function useCombinedChatUnreads(messagesFilter: SidebarFilter) {
);
}

export function useAllGroupUnreads() {
const sources = useUnreadsStore(useCallback((s) => s.sources, []));
return Object.entries(sources).filter(
([key, source]) =>
key.startsWith('group') &&
source.combined.count > 0 &&
source.combined.status === 'unread'
);
}

export function useMarkAllGroupsRead() {
const allGroupUnreads = useAllGroupUnreads();
const { read } = useUnreadsStore();
const { mutate } = useMarkReadMutation();

const markAllRead = useCallback(() => {
allGroupUnreads.forEach(([sourceId, groupUnread]) => {
if (groupUnread.status === 'unread') {
read(sourceId);
mutate({ source: { group: sourceId } });
}

const groupId = sourceId.split('/').slice(1).join('/');

const unreadChildrenIds = Object.entries(groupUnread.children ?? {})
.filter(([_, childUnread]) => childUnread.count > 0)
.map(([childId]) => childId);

unreadChildrenIds.forEach((childId) => {
read(childId);
if (childId.startsWith('channel')) {
const channelId = childId.split('/').slice(1).join('/');
mutate({
source: {
channel: {
group: groupId,
nest: channelId,
},
},
});
}
});
});
}, [allGroupUnreads, read, mutate]);

return markAllRead;
}

export function useCombinedGroupUnreads() {
const sources = useUnreadsStore(useCallback((s) => s.sources, []));
return Object.entries(sources).reduce((acc, [key, source]) => {
Expand Down

0 comments on commit a69256d

Please sign in to comment.