mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
desktop: float unread indicator + fix sidebar scroll jump (#777)
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
import { useRouter } from "@tanstack/react-router";
|
||||
import * as React from "react";
|
||||
|
||||
/**
|
||||
* Prevents TanStack Router's scroll restoration from moving the sidebar.
|
||||
*
|
||||
* The router registers a document-level capture listener for "scroll" that
|
||||
* records every scrollable element's position. On navigation it restores those
|
||||
* positions synchronously inside an "onRendered" event. We snapshot the
|
||||
* sidebar's scrollTop in "onBeforeLoad" (before any restoration happens) and
|
||||
* re-apply it in "onRendered" (after the router's restoration subscriber has
|
||||
* already run, since our subscription is registered later).
|
||||
*/
|
||||
export function useSidebarScrollLock(
|
||||
scrollRef: React.RefObject<HTMLDivElement | null>,
|
||||
) {
|
||||
const savedScrollTop = React.useRef(0);
|
||||
const router = useRouter();
|
||||
|
||||
React.useEffect(() => {
|
||||
const unsubBefore = router.subscribe("onBeforeLoad", () => {
|
||||
const el = scrollRef.current;
|
||||
if (el) {
|
||||
savedScrollTop.current = el.scrollTop;
|
||||
}
|
||||
});
|
||||
|
||||
const unsubRendered = router.subscribe("onRendered", () => {
|
||||
const el = scrollRef.current;
|
||||
if (el && el.scrollTop !== savedScrollTop.current) {
|
||||
el.scrollTop = savedScrollTop.current;
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubBefore();
|
||||
unsubRendered();
|
||||
};
|
||||
}, [router, scrollRef]);
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import { PresenceDot } from "@/features/presence/ui/PresenceBadge";
|
||||
import { ProfileAvatar } from "@/features/profile/ui/ProfileAvatar";
|
||||
import { ProfilePopover } from "@/features/profile/ui/ProfilePopover";
|
||||
import { useDmSidebarMetadata } from "@/features/sidebar/useDmSidebarMetadata";
|
||||
import { useSidebarScrollLock } from "@/features/sidebar/lib/useSidebarScrollLock";
|
||||
import { useUnreadOverflow } from "@/features/sidebar/lib/useUnreadOverflow";
|
||||
import { MoreUnreadButton } from "@/features/sidebar/ui/MoreUnreadButton";
|
||||
import {
|
||||
@@ -418,6 +419,7 @@ export function AppSidebar({
|
||||
const isNewDmOpen = isNewDmOpenProp ?? isNewDmOpenInternal;
|
||||
const setIsNewDmOpen = onNewDmOpenChange ?? setIsNewDmOpenInternal;
|
||||
const scrollRef = React.useRef<HTMLDivElement>(null);
|
||||
useSidebarScrollLock(scrollRef);
|
||||
const [profilePopoverOpen, setProfilePopoverOpen] = React.useState(false);
|
||||
const [createDialogKind, setCreateDialogKind] =
|
||||
React.useState<CreateChannelKind | null>(null);
|
||||
@@ -625,12 +627,13 @@ export function AppSidebar({
|
||||
</SidebarMenu>
|
||||
</SidebarHeader>
|
||||
|
||||
<div className="flex min-h-0 flex-1 flex-col">
|
||||
<div className="relative flex min-h-0 flex-1 flex-col">
|
||||
{unreadAboveCount > 0 ? (
|
||||
<MoreUnreadButton
|
||||
count={unreadAboveCount}
|
||||
icon={<ArrowUp />}
|
||||
onClick={scrollToNextAbove}
|
||||
position="top"
|
||||
testId="sidebar-more-unread-above"
|
||||
/>
|
||||
) : null}
|
||||
@@ -740,6 +743,7 @@ export function AppSidebar({
|
||||
count={unreadBelowCount}
|
||||
icon={<ArrowDown />}
|
||||
onClick={scrollToNextBelow}
|
||||
position="bottom"
|
||||
testId="sidebar-more-unread-below"
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -3,28 +3,32 @@ import type * as React from "react";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
|
||||
const MORE_UNREAD_BUTTON_CLASS =
|
||||
"h-7 min-h-7 gap-1.5 rounded-full border-border/50 bg-background/85 px-2.5 text-[11px] font-medium text-muted-foreground shadow-xs backdrop-blur-sm hover:bg-muted/70 hover:text-foreground [&_svg]:size-3.5";
|
||||
"h-7 min-h-7 gap-1.5 rounded-full border-0 bg-primary px-2.5 text-[11px] font-medium text-primary-foreground shadow-md hover:bg-primary/90 [&_svg]:size-3.5";
|
||||
|
||||
export function MoreUnreadButton({
|
||||
count,
|
||||
icon,
|
||||
onClick,
|
||||
position,
|
||||
testId,
|
||||
}: {
|
||||
count: number;
|
||||
icon: React.ReactNode;
|
||||
onClick: () => void;
|
||||
position: "top" | "bottom";
|
||||
testId: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex justify-center py-1">
|
||||
<div
|
||||
className={`pointer-events-none absolute inset-x-0 z-10 flex justify-center py-1 ${position === "top" ? "top-0" : "bottom-0"}`}
|
||||
>
|
||||
<Button
|
||||
className={MORE_UNREAD_BUTTON_CLASS}
|
||||
className={`pointer-events-auto ${MORE_UNREAD_BUTTON_CLASS}`}
|
||||
data-testid={testId}
|
||||
onClick={onClick}
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="outline"
|
||||
variant="ghost"
|
||||
>
|
||||
{icon}
|
||||
{count} more unread
|
||||
|
||||
Reference in New Issue
Block a user