From 5d6cad0d419d4331096eab25df485984fc571fc9 Mon Sep 17 00:00:00 2001 From: ragnep Date: Thu, 30 Apr 2026 11:54:15 +0300 Subject: [PATCH 1/5] wip Signed-off-by: ragnep --- .../web/WebBrainLeftSidebarWaves.tsx | 5 +- .../web/WebDirectMessagesList.tsx | 19 +- .../brain/left-sidebar/web/WebLeftSidebar.tsx | 3 + .../left-sidebar/web/WebUnifiedWavesList.tsx | 11 +- .../web/WebUnifiedWavesListWaves.tsx | 593 +++++++++++------- .../CommunityCurations.tsx | 10 +- .../CommunityCurationsMasonry.tsx | 38 +- components/shared/WavesMessagesWrapper.tsx | 6 +- components/waves/drops/WaveDropQuote.tsx | 197 +++--- .../drops/WaveDropQuoteDisplayContext.tsx | 40 ++ 10 files changed, 576 insertions(+), 346 deletions(-) create mode 100644 components/waves/drops/WaveDropQuoteDisplayContext.tsx diff --git a/components/brain/left-sidebar/web/WebBrainLeftSidebarWaves.tsx b/components/brain/left-sidebar/web/WebBrainLeftSidebarWaves.tsx index d740365045..013ea70ad7 100644 --- a/components/brain/left-sidebar/web/WebBrainLeftSidebarWaves.tsx +++ b/components/brain/left-sidebar/web/WebBrainLeftSidebarWaves.tsx @@ -5,14 +5,14 @@ import { useMyStream } from "../../../../contexts/wave/MyStreamContext"; interface WebBrainLeftSidebarWavesProps { readonly scrollContainerRef: React.RefObject; readonly isCollapsed?: boolean | undefined; + readonly showProfileFeedShortcut?: boolean | undefined; } const WebBrainLeftSidebarWaves: React.FC = ({ scrollContainerRef, isCollapsed = false, + showProfileFeedShortcut = true, }) => { - - const { waves, registerWave } = useMyStream(); const onNextPage = () => { @@ -31,6 +31,7 @@ const WebBrainLeftSidebarWaves: React.FC = ({ onHover={registerWave} scrollContainerRef={scrollContainerRef} isCollapsed={isCollapsed} + showProfileFeedShortcut={showProfileFeedShortcut} /> ); }; diff --git a/components/brain/left-sidebar/web/WebDirectMessagesList.tsx b/components/brain/left-sidebar/web/WebDirectMessagesList.tsx index bfad294ad3..e3aaeeb4c5 100644 --- a/components/brain/left-sidebar/web/WebDirectMessagesList.tsx +++ b/components/brain/left-sidebar/web/WebDirectMessagesList.tsx @@ -17,7 +17,6 @@ import PrimaryButton from "../../../utils/button/PrimaryButton"; import CreateDirectMessageModal from "../../../waves/create-dm/CreateDirectMessageModal"; import UnifiedWavesListEmpty from "../waves/UnifiedWavesListEmpty"; import { UnifiedWavesListLoader } from "../waves/UnifiedWavesListLoader"; -import type { WebUnifiedWavesListWavesHandle } from "./WebUnifiedWavesListWaves"; import WebUnifiedWavesListWaves from "./WebUnifiedWavesListWaves"; interface WebDirectMessagesListProps { @@ -37,7 +36,7 @@ const WebDirectMessagesList: React.FC = ({ const shouldRenderCreateDirectMessage = !isApp; - const listRef = useRef(null); + const sentinelRef = useRef(null); const { directMessages, registerWave } = useMyStream(); useInfiniteScroll( @@ -45,7 +44,7 @@ const WebDirectMessagesList: React.FC = ({ directMessages.isFetchingNextPage, directMessages.fetchNextPage, scrollContainerRef, - listRef.current?.sentinelRef || { current: null }, + sentinelRef, "100px" ); @@ -70,7 +69,6 @@ const WebDirectMessagesList: React.FC = ({ } else { listContent = ( = ({ hidePin={true} basePath="/messages" isCollapsed={isCollapsed} + sentinelRef={sentinelRef} /> ); } @@ -204,13 +203,11 @@ const WebDirectMessagesList: React.FC = ({ /> )} - {connectedProfile && ( - - )} + ); }; diff --git a/components/brain/left-sidebar/web/WebLeftSidebar.tsx b/components/brain/left-sidebar/web/WebLeftSidebar.tsx index cf897863f4..62699ded3f 100644 --- a/components/brain/left-sidebar/web/WebLeftSidebar.tsx +++ b/components/brain/left-sidebar/web/WebLeftSidebar.tsx @@ -22,6 +22,7 @@ import { ChevronDoubleRightIcon } from "@heroicons/react/24/outline"; */ interface WebLeftSidebarProps { readonly isCollapsed?: boolean | undefined; + readonly showProfileFeedShortcut?: boolean | undefined; } const WebLeftSidebarQuickVoteOwner: React.FC<{ @@ -45,6 +46,7 @@ const WebLeftSidebarQuickVoteOwner: React.FC<{ const WebLeftSidebar: React.FC = ({ isCollapsed = false, + showProfileFeedShortcut = true, }) => { const scrollContainerRef = useRef(null); const pathname = usePathname(); @@ -93,6 +95,7 @@ const WebLeftSidebar: React.FC = ({ )} {isMessagesView && ( diff --git a/components/brain/left-sidebar/web/WebUnifiedWavesList.tsx b/components/brain/left-sidebar/web/WebUnifiedWavesList.tsx index 5c863550e3..bdd3cf3034 100644 --- a/components/brain/left-sidebar/web/WebUnifiedWavesList.tsx +++ b/components/brain/left-sidebar/web/WebUnifiedWavesList.tsx @@ -4,7 +4,6 @@ import React, { useRef } from "react"; import { useInfiniteScroll } from "../../../../hooks/useInfiniteScroll"; import UnifiedWavesListEmpty from "../waves/UnifiedWavesListEmpty"; import { UnifiedWavesListLoader } from "../waves/UnifiedWavesListLoader"; -import type { WebUnifiedWavesListWavesHandle } from "./WebUnifiedWavesListWaves"; import WebUnifiedWavesListWaves from "./WebUnifiedWavesListWaves"; import type { MinimalWave } from "@/contexts/wave/hooks/useEnhancedWavesListCore"; @@ -17,6 +16,7 @@ interface WebUnifiedWavesListProps { readonly onHover: (waveId: string) => void; readonly scrollContainerRef: React.RefObject; readonly isCollapsed?: boolean | undefined; + readonly showProfileFeedShortcut?: boolean | undefined; } const WebUnifiedWavesList: React.FC = (props) => { @@ -29,9 +29,9 @@ const WebUnifiedWavesList: React.FC = (props) => { onHover, scrollContainerRef, isCollapsed = false, + showProfileFeedShortcut = true, } = props; - // Refs to the scroll container and sentinel - const listRef = useRef(null); + const sentinelRef = useRef(null); // Use the custom hook for infinite scroll useInfiniteScroll( @@ -39,7 +39,7 @@ const WebUnifiedWavesList: React.FC = (props) => { isFetchingNextPage, fetchNextPage, scrollContainerRef, - listRef.current?.sentinelRef || { current: null }, + sentinelRef, "100px" ); @@ -48,11 +48,12 @@ const WebUnifiedWavesList: React.FC = (props) => {
{/* Unified Waves List */} {/* Loading indicator and intersection trigger */} diff --git a/components/brain/left-sidebar/web/WebUnifiedWavesListWaves.tsx b/components/brain/left-sidebar/web/WebUnifiedWavesListWaves.tsx index f934b33138..c32a6f0c12 100644 --- a/components/brain/left-sidebar/web/WebUnifiedWavesListWaves.tsx +++ b/components/brain/left-sidebar/web/WebUnifiedWavesListWaves.tsx @@ -1,11 +1,13 @@ "use client"; import PrimaryButton from "@/components/utils/button/PrimaryButton"; +import { useMyStream } from "@/contexts/wave/MyStreamContext"; import useCreateModalState from "@/hooks/useCreateModalState"; import useIsTouchDevice from "@/hooks/useIsTouchDevice"; import { faPlus } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import React, { forwardRef, useImperativeHandle, useMemo, useRef } from "react"; +import Link from "next/link"; +import React, { useMemo, useRef } from "react"; import { Tooltip as ReactTooltip } from "react-tooltip"; import type { VirtualItem } from "../../../../hooks/useVirtualizedWaves"; import { useVirtualizedWaves } from "../../../../hooks/useVirtualizedWaves"; @@ -34,9 +36,38 @@ const EMPTY_WAVES_PLACEHOLDER_HEIGHT = "48px" as const; const WAVE_ROW_HEIGHT_DEFAULT = 62 as const; const WAVE_ROW_HEIGHT_COLLAPSED = 52 as const; +const PROFILE_FEED_TOOLTIP_ID = "profile-feed-shortcut-tooltip"; +const PROFILE_FEED_LABEL = "Profile Waves Feed"; +const TOOLTIP_STYLE = { + padding: "6px 10px", + background: "#37373E", + color: "white", + fontSize: "12px", + fontWeight: 500, + borderRadius: "6px", + boxShadow: "0 4px 12px rgba(0, 0, 0, 0.3)", + zIndex: 10000, +} as const satisfies React.CSSProperties; -export interface WebUnifiedWavesListWavesHandle { - sentinelRef: React.RefObject; +function MasonryGridIcon() { + return ( + + ); } interface WebUnifiedWavesListWavesProps { @@ -48,80 +79,205 @@ interface WebUnifiedWavesListWavesProps { readonly scrollContainerRef?: React.RefObject | undefined; readonly basePath?: string | undefined; readonly isCollapsed?: boolean | undefined; + readonly showProfileFeedShortcut?: boolean | undefined; + readonly sentinelRef: React.RefObject; } -const WebUnifiedWavesListWaves = forwardRef< - WebUnifiedWavesListWavesHandle, - WebUnifiedWavesListWavesProps ->( - ( - { - waves, - onHover, - hideHeaders = false, - hideToggle = false, - hidePin = false, - scrollContainerRef, - basePath = "/waves", - isCollapsed = false, - }, - ref - ) => { - const listContainerRef = useRef(null); - const sentinelRef = useRef(null); - const { connectedProfile } = useAuth(); - const { openWave, isApp } = useCreateModalState(); - const isTouchDevice = useIsTouchDevice(); - const seizeSettings = useSeizeSettingsOptional(); +function ProfileFeedAvatar({ isActive }: { readonly isActive: boolean }) { + return ( +
+
+
+ +
+
+ ); +} + +function isModifiedClick(event: React.MouseEvent) { + return ( + event.metaKey || + event.ctrlKey || + event.shiftKey || + event.altKey || + event.button === 1 || + event.button === 2 + ); +} - useImperativeHandle(ref, () => ({ - sentinelRef, - })); +function WebProfileFeedShortcut({ + basePath, + isCollapsed, +}: { + readonly basePath: string; + readonly isCollapsed: boolean; +}) { + const { activeWave } = useMyStream(); + const isActive = activeWave.id === null; - const showCreateWaveButton = !isApp && !!connectedProfile; + const handleClick = (event: React.MouseEvent) => { + if (event.defaultPrevented || isModifiedClick(event)) { + return; + } - const { announcementWaves, pinnedWaves, regularWaves } = useMemo(() => { - const announcements: MinimalWave[] = []; - const pinned: MinimalWave[] = []; - const regular: MinimalWave[] = []; + event.preventDefault(); + activeWave.set(null, { isDirectMessage: false }); + }; - for (const wave of waves) { - if (seizeSettings?.isAnnouncementsWave(wave.id)) { - announcements.push(wave); - } else if (wave.isPinned) { - pinned.push(wave); - } else { - regular.push(wave); - } + if (isCollapsed) { + return ( +
+ + + +
+ ); + } + + return ( +
+ + +
+
{PROFILE_FEED_LABEL}
+
+ +
+ ); +} + +const WebUnifiedWavesListWaves: React.FC = ({ + waves, + onHover, + hideHeaders = false, + hideToggle = false, + hidePin = false, + scrollContainerRef, + basePath = "/waves", + isCollapsed = false, + showProfileFeedShortcut = true, + sentinelRef, +}) => { + const listContainerRef = useRef(null); + const { connectedProfile } = useAuth(); + const { openWave, isApp } = useCreateModalState(); + const isTouchDevice = useIsTouchDevice(); + const seizeSettings = useSeizeSettingsOptional(); + + const showCreateWaveButton = !isApp && !!connectedProfile; + const shouldShowProfileFeedShortcut = !hideHeaders && showProfileFeedShortcut; + + const { announcementWaves, pinnedWaves, regularWaves } = useMemo(() => { + const announcements: MinimalWave[] = []; + const pinned: MinimalWave[] = []; + const regular: MinimalWave[] = []; + + for (const wave of waves) { + if (seizeSettings?.isAnnouncementsWave(wave.id)) { + announcements.push(wave); + } else if (wave.isPinned) { + pinned.push(wave); + } else { + regular.push(wave); } + } - return { - announcementWaves: announcements, - pinnedWaves: pinned, - regularWaves: regular, - }; - }, [waves, seizeSettings]); + return { + announcementWaves: announcements, + pinnedWaves: pinned, + regularWaves: regular, + }; + }, [waves, seizeSettings]); - const rowHeight = isCollapsed - ? WAVE_ROW_HEIGHT_COLLAPSED - : WAVE_ROW_HEIGHT_DEFAULT; + const rowHeight = isCollapsed + ? WAVE_ROW_HEIGHT_COLLAPSED + : WAVE_ROW_HEIGHT_DEFAULT; - const virtual = useVirtualizedWaves( - regularWaves, - "web-unified-waves-regular", - scrollContainerRef ?? listContainerRef, - listContainerRef, - rowHeight, - 5 - ); + const virtual = useVirtualizedWaves( + regularWaves, + "web-unified-waves-regular", + scrollContainerRef ?? listContainerRef, + listContainerRef, + rowHeight, + 5 + ); - return ( - <> -
- {!hideHeaders && - (isCollapsed ? ( - showCreateWaveButton && ( -
+ return ( + <> +
+ {!hideHeaders && + (isCollapsed ? ( + showCreateWaveButton && ( +
+
+ + + +
+
+ ) + ) : ( +
-
- ) - ) : ( - - - - -
- ) : undefined - } - /> - ))} - {!hideHeaders && !hideToggle && !isCollapsed && ( -
- -
- )} + ) : undefined + } + /> + ))} + {!hideHeaders && !hideToggle && !isCollapsed && ( +
+ +
+ )} + {shouldShowProfileFeedShortcut && ( + + )} -
- {announcementWaves.length > 0 && ( -
- {announcementWaves - .filter((wave): wave is MinimalWave => { - if (!isValidWave(wave)) { - console.warn("Invalid announcement wave object", wave); - return false; - } - return true; - }) - .map((wave) => ( -
- -
- ))} -
- )} - {announcementWaves.length > 0 && - !hideHeaders && - (pinnedWaves.length > 0 || regularWaves.length > 0) && ( -
- )} - {!hideHeaders && pinnedWaves.length > 0 && ( -
- {pinnedWaves - .filter((wave): wave is MinimalWave => { - if (!isValidWave(wave)) { - console.warn("Invalid pinned wave object", wave); - return false; - } - return true; - }) - .map((wave) => ( -
- -
- ))} -
- )} - {!hideHeaders && - pinnedWaves.length > 0 && - regularWaves.length > 0 && ( -
- )} - {regularWaves.length > 0 ? ( -
- {virtual.virtualItems.map((v: VirtualItem) => { - if (v.index === regularWaves.length) { - return ( -
- ); +
+ {announcementWaves.length > 0 && ( +
+ {announcementWaves + .filter((wave): wave is MinimalWave => { + if (!isValidWave(wave)) { + console.warn("Invalid announcement wave object", wave); + return false; } - const wave = regularWaves[v.index]; + return true; + }) + .map((wave) => ( +
+ +
+ ))} +
+ )} + {announcementWaves.length > 0 && + !hideHeaders && + (pinnedWaves.length > 0 || regularWaves.length > 0) && ( +
+ )} + {!hideHeaders && pinnedWaves.length > 0 && ( +
+ {pinnedWaves + .filter((wave): wave is MinimalWave => { if (!isValidWave(wave)) { - console.warn("Invalid wave object at index", v.index, wave); - return null; + console.warn("Invalid pinned wave object", wave); + return false; } + return true; + }) + .map((wave) => ( +
+ +
+ ))} +
+ )} + {!hideHeaders && + pinnedWaves.length > 0 && + regularWaves.length > 0 && ( +
+ )} + {regularWaves.length > 0 ? ( +
+ {virtual.virtualItems.map((v: VirtualItem) => { + if (v.index === regularWaves.length) { return (
- -
+ /> ); - })} -
- ) : ( -
- )} -
+ } + const wave = regularWaves[v.index]; + if (!isValidWave(wave)) { + console.warn("Invalid wave object at index", v.index, wave); + return null; + } + return ( +
+ +
+ ); + })} +
+ ) : ( +
+ )}
+
- {!isTouchDevice && ( + {!isTouchDevice && ( + <> - )} - - ); - } -); + {shouldShowProfileFeedShortcut && ( + + )} + + )} + + ); +}; WebUnifiedWavesListWaves.displayName = "WebUnifiedWavesListWaves"; export default WebUnifiedWavesListWaves; diff --git a/components/community-curations/CommunityCurations.tsx b/components/community-curations/CommunityCurations.tsx index f82f7d5a4a..f2f2b4b9d0 100644 --- a/components/community-curations/CommunityCurations.tsx +++ b/components/community-curations/CommunityCurations.tsx @@ -163,11 +163,11 @@ export default function CommunityCurations() {
-

- Community Curations +

+ Latest From Profile Waves

- Community-curated drops from across 6529 Waves. + Drops 6529 users are featuring from their own profile waves.

@@ -184,8 +184,8 @@ export default function CommunityCurations() { {shouldShowEmptyState && ( )} diff --git a/components/community-curations/CommunityCurationsMasonry.tsx b/components/community-curations/CommunityCurationsMasonry.tsx index 3bd629b2c2..b3e23edfc4 100644 --- a/components/community-curations/CommunityCurationsMasonry.tsx +++ b/components/community-curations/CommunityCurationsMasonry.tsx @@ -6,6 +6,7 @@ import CircleLoader, { import { TweetPreviewModeProvider } from "@/components/tweets/TweetPreviewModeContext"; import CurationDropFooter from "@/components/waves/drops/CurationDropFooter"; import Drop, { DropLocation } from "@/components/waves/drops/Drop"; +import { WaveDropQuoteDisplayProvider } from "@/components/waves/drops/WaveDropQuoteDisplayContext"; import type { ExtendedDrop } from "@/helpers/waves/drop.helpers"; import { useNavigateToDropWave } from "@/hooks/useNavigateToDropWave"; import { useIntersectionObserver } from "@/hooks/scroll/useIntersectionObserver"; @@ -249,24 +250,25 @@ function CommunityCurationsMasonryItem({ return (
- } - identityMode="default" - timestampLayout="stacked" - showInteractions={false} - /> + + } + timestampLayout="stacked" + showInteractions={false} + /> +