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 { ProfileAvatar } from "@/features/profile/ui/ProfileAvatar";
|
||||||
import { ProfilePopover } from "@/features/profile/ui/ProfilePopover";
|
import { ProfilePopover } from "@/features/profile/ui/ProfilePopover";
|
||||||
import { useDmSidebarMetadata } from "@/features/sidebar/useDmSidebarMetadata";
|
import { useDmSidebarMetadata } from "@/features/sidebar/useDmSidebarMetadata";
|
||||||
|
import { useSidebarScrollLock } from "@/features/sidebar/lib/useSidebarScrollLock";
|
||||||
import { useUnreadOverflow } from "@/features/sidebar/lib/useUnreadOverflow";
|
import { useUnreadOverflow } from "@/features/sidebar/lib/useUnreadOverflow";
|
||||||
import { MoreUnreadButton } from "@/features/sidebar/ui/MoreUnreadButton";
|
import { MoreUnreadButton } from "@/features/sidebar/ui/MoreUnreadButton";
|
||||||
import {
|
import {
|
||||||
@@ -418,6 +419,7 @@ export function AppSidebar({
|
|||||||
const isNewDmOpen = isNewDmOpenProp ?? isNewDmOpenInternal;
|
const isNewDmOpen = isNewDmOpenProp ?? isNewDmOpenInternal;
|
||||||
const setIsNewDmOpen = onNewDmOpenChange ?? setIsNewDmOpenInternal;
|
const setIsNewDmOpen = onNewDmOpenChange ?? setIsNewDmOpenInternal;
|
||||||
const scrollRef = React.useRef<HTMLDivElement>(null);
|
const scrollRef = React.useRef<HTMLDivElement>(null);
|
||||||
|
useSidebarScrollLock(scrollRef);
|
||||||
const [profilePopoverOpen, setProfilePopoverOpen] = React.useState(false);
|
const [profilePopoverOpen, setProfilePopoverOpen] = React.useState(false);
|
||||||
const [createDialogKind, setCreateDialogKind] =
|
const [createDialogKind, setCreateDialogKind] =
|
||||||
React.useState<CreateChannelKind | null>(null);
|
React.useState<CreateChannelKind | null>(null);
|
||||||
@@ -625,12 +627,13 @@ export function AppSidebar({
|
|||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
</SidebarHeader>
|
</SidebarHeader>
|
||||||
|
|
||||||
<div className="flex min-h-0 flex-1 flex-col">
|
<div className="relative flex min-h-0 flex-1 flex-col">
|
||||||
{unreadAboveCount > 0 ? (
|
{unreadAboveCount > 0 ? (
|
||||||
<MoreUnreadButton
|
<MoreUnreadButton
|
||||||
count={unreadAboveCount}
|
count={unreadAboveCount}
|
||||||
icon={<ArrowUp />}
|
icon={<ArrowUp />}
|
||||||
onClick={scrollToNextAbove}
|
onClick={scrollToNextAbove}
|
||||||
|
position="top"
|
||||||
testId="sidebar-more-unread-above"
|
testId="sidebar-more-unread-above"
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -740,6 +743,7 @@ export function AppSidebar({
|
|||||||
count={unreadBelowCount}
|
count={unreadBelowCount}
|
||||||
icon={<ArrowDown />}
|
icon={<ArrowDown />}
|
||||||
onClick={scrollToNextBelow}
|
onClick={scrollToNextBelow}
|
||||||
|
position="bottom"
|
||||||
testId="sidebar-more-unread-below"
|
testId="sidebar-more-unread-below"
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -3,28 +3,32 @@ import type * as React from "react";
|
|||||||
import { Button } from "@/shared/ui/button";
|
import { Button } from "@/shared/ui/button";
|
||||||
|
|
||||||
const MORE_UNREAD_BUTTON_CLASS =
|
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({
|
export function MoreUnreadButton({
|
||||||
count,
|
count,
|
||||||
icon,
|
icon,
|
||||||
onClick,
|
onClick,
|
||||||
|
position,
|
||||||
testId,
|
testId,
|
||||||
}: {
|
}: {
|
||||||
count: number;
|
count: number;
|
||||||
icon: React.ReactNode;
|
icon: React.ReactNode;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
|
position: "top" | "bottom";
|
||||||
testId: string;
|
testId: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
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
|
<Button
|
||||||
className={MORE_UNREAD_BUTTON_CLASS}
|
className={`pointer-events-auto ${MORE_UNREAD_BUTTON_CLASS}`}
|
||||||
data-testid={testId}
|
data-testid={testId}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
size="sm"
|
size="sm"
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="ghost"
|
||||||
>
|
>
|
||||||
{icon}
|
{icon}
|
||||||
{count} more unread
|
{count} more unread
|
||||||
|
|||||||
Reference in New Issue
Block a user