diff --git a/__tests__/components/header/AppSidebar.test.tsx b/__tests__/components/header/AppSidebar.test.tsx index c76597a4ab..697f4ef56b 100644 --- a/__tests__/components/header/AppSidebar.test.tsx +++ b/__tests__/components/header/AppSidebar.test.tsx @@ -43,6 +43,7 @@ jest.mock("@/components/app-wallets/AppWalletsContext"); expect(menuProps.menu).toEqual( expect.arrayContaining([ expect.objectContaining({ label: "Profile", path: "/profile" }), + expect.objectContaining({ label: "Discovery", path: "/discover" }), ]) ); expect(networkChildren).toEqual( diff --git a/__tests__/components/navigation/BottomNavigation.test.tsx b/__tests__/components/navigation/BottomNavigation.test.tsx index b83d80d7f1..dd4b71a167 100644 --- a/__tests__/components/navigation/BottomNavigation.test.tsx +++ b/__tests__/components/navigation/BottomNavigation.test.tsx @@ -44,7 +44,7 @@ describe("BottomNavigation", () => { const passedItems = navItemCalls.map((call) => call[0].item); expect(passedItems.map((item: { name: string }) => item.name)).toEqual([ - "Profile", + "Discovery", "Waves", "Messages", "Home", diff --git a/app/discover/page.tsx b/app/discover/page.tsx new file mode 100644 index 0000000000..fc4c6bc366 --- /dev/null +++ b/app/discover/page.tsx @@ -0,0 +1,25 @@ +import { ExploreWavesSection } from "@/components/home/explore-waves/ExploreWavesSection"; +import { getAppMetadata } from "@/components/providers/metadata"; +import type { Metadata } from "next"; + +export default function DiscoverPage() { + return ( +
+ +
+ ); +} + +export function generateMetadata(): Metadata { + return getAppMetadata({ + title: "Discovery", + description: "Active discussions you are not yet following", + }); +} diff --git a/components/common/icons/DiscoverIcon.tsx b/components/common/icons/DiscoverIcon.tsx new file mode 100644 index 0000000000..babb83b3bb --- /dev/null +++ b/components/common/icons/DiscoverIcon.tsx @@ -0,0 +1,18 @@ +const DiscoverIcon = ({ + className, +}: { + readonly className?: string | undefined; +}) => ( + + + +); + +export default DiscoverIcon; diff --git a/components/common/icons/ProfileIcon.tsx b/components/common/icons/ProfileIcon.tsx deleted file mode 100644 index 61a7cc512f..0000000000 --- a/components/common/icons/ProfileIcon.tsx +++ /dev/null @@ -1,22 +0,0 @@ -const ProfileIcon = ({ - className, -}: { - readonly className?: string | undefined; -}) => ( - - - -); - -export default ProfileIcon; diff --git a/components/header/AppSidebar.tsx b/components/header/AppSidebar.tsx index 963c94ea9f..f1037faaee 100644 --- a/components/header/AppSidebar.tsx +++ b/components/header/AppSidebar.tsx @@ -5,15 +5,22 @@ import { Transition, TransitionChild, } from "@headlessui/react"; -import { DocumentTextIcon, WrenchIcon } from "@heroicons/react/24/outline"; +import { + DocumentTextIcon, + UserIcon, + WrenchIcon, +} from "@heroicons/react/24/outline"; import { Fragment, useCallback, useEffect, useMemo } from "react"; import { useAppWallets } from "../app-wallets/AppWalletsContext"; +import DiscoverIcon from "../common/icons/DiscoverIcon"; import UsersIcon from "../common/icons/UsersIcon"; import AppSidebarHeader from "./AppSidebarHeader"; import AppSidebarMenuItems from "./AppSidebarMenuItems"; import AppUserConnect from "./AppUserConnect"; const MENU = [ + { label: "Profile", path: "/profile", icon: UserIcon }, + { label: "Discovery", path: "/discover", icon: DiscoverIcon }, { label: "Network", icon: UsersIcon, diff --git a/components/header/header-search/HeaderSearchModal.tsx b/components/header/header-search/HeaderSearchModal.tsx index b607289c9d..ae3b25f11d 100644 --- a/components/header/header-search/HeaderSearchModal.tsx +++ b/components/header/header-search/HeaderSearchModal.tsx @@ -3,6 +3,7 @@ import { useAppWallets } from "@/components/app-wallets/AppWalletsContext"; import BellIcon from "@/components/common/icons/BellIcon"; import ChatBubbleIcon from "@/components/common/icons/ChatBubbleIcon"; +import DiscoverIcon from "@/components/common/icons/DiscoverIcon"; import HomeIcon from "@/components/common/icons/HomeIcon"; import WavesIcon from "@/components/common/icons/WavesIcon"; import { useCookieConsent } from "@/components/cookies/CookieConsentContext"; @@ -98,6 +99,12 @@ const PRIMARY_NAVIGATION_PAGES: SidebarPageEntry[] = [ section: "Main", icon: ChatBubbleIcon, }, + { + name: "Discovery", + href: "/discover", + section: "Main", + icon: DiscoverIcon, + }, { name: "Notifications", href: "/notifications", diff --git a/components/home/explore-waves/ExploreWavesSection.tsx b/components/home/explore-waves/ExploreWavesSection.tsx index 8b112b600b..6b29d9275a 100644 --- a/components/home/explore-waves/ExploreWavesSection.tsx +++ b/components/home/explore-waves/ExploreWavesSection.tsx @@ -1,5 +1,6 @@ "use client"; +import { useAuth } from "@/components/auth/Auth"; import type { ApiWave } from "@/generated/models/ApiWave"; import { commonApiFetch } from "@/services/api/common-api"; import { ArrowRightIcon } from "@heroicons/react/24/outline"; @@ -8,25 +9,55 @@ import Link from "next/link"; import { ExploreWaveCard } from "./ExploreWaveCard"; import { ExploreWaveCardSkeleton } from "./ExploreWaveCardSkeleton"; -const WAVES_LIMIT = 6; +const DEFAULT_WAVES_LIMIT = 6; + +interface ExploreWavesSectionProps { + readonly title?: string | undefined; + readonly subtitle?: string | null | undefined; + readonly limit?: number | undefined; + readonly endpoint?: string | undefined; + readonly viewAllHref?: string | null | undefined; + readonly excludeFollowed?: boolean | undefined; +} + +export function ExploreWavesSection({ + title = "Tired of bot replies? Join the most interesting chats in crypto", + subtitle = "Most active waves", + limit = DEFAULT_WAVES_LIMIT, + endpoint = "waves-overview/hot", + viewAllHref = "/waves", + excludeFollowed = false, +}: ExploreWavesSectionProps) { + const { connectedProfile } = useAuth(); + const userScope = + connectedProfile?.id ?? + connectedProfile?.normalised_handle ?? + connectedProfile?.handle ?? + null; -export function ExploreWavesSection() { const { data: waves, isLoading, isError, - } = useQuery({ - queryKey: ["explore-waves-homepage", WAVES_LIMIT], + } = useQuery({ + queryKey: [ + "explore-waves", + endpoint, + limit, + excludeFollowed, + excludeFollowed ? userScope : null, + ], queryFn: async () => { const data = await commonApiFetch({ - endpoint: "waves-overview/hot", + endpoint, + params: excludeFollowed ? { exclude_followed: "true" } : undefined, }); - return data.slice(0, WAVES_LIMIT); + return data.slice(0, limit); }, - staleTime: 5 * 60 * 1000, // 5 minutes + staleTime: excludeFollowed ? 0 : 5 * 60 * 1000, + ...(excludeFollowed ? { gcTime: 0 } : {}), }); - // Hide section on error or if no waves if (isError) { return null; } @@ -41,17 +72,19 @@ export function ExploreWavesSection() {
- Tired of bot replies? Join the most interesting chats in crypto + {title} -

- Most active waves -

+ {subtitle && ( +

+ {subtitle} +

+ )}
{isLoading - ? Array.from({ length: WAVES_LIMIT }).map((_, index) => ( + ? Array.from({ length: limit }).map((_, index) => (
@@ -63,18 +96,20 @@ export function ExploreWavesSection() { ))}
-
- - View all - - -
+ {viewAllHref && ( +
+ + View all + + +
+ )} ); diff --git a/components/layout/sidebar/WebSidebarNav.tsx b/components/layout/sidebar/WebSidebarNav.tsx index 279a508cd6..97944b2fb9 100644 --- a/components/layout/sidebar/WebSidebarNav.tsx +++ b/components/layout/sidebar/WebSidebarNav.tsx @@ -3,6 +3,7 @@ import { useAppWallets } from "@/components/app-wallets/AppWalletsContext"; import { useAuth } from "@/components/auth/Auth"; import ChatBubbleIcon from "@/components/common/icons/ChatBubbleIcon"; +import DiscoverIcon from "@/components/common/icons/DiscoverIcon"; import HomeIcon from "@/components/common/icons/HomeIcon"; import WavesIcon from "@/components/common/icons/WavesIcon"; import { useCookieConsent } from "@/components/cookies/CookieConsentContext"; @@ -240,6 +241,16 @@ const WebSidebarNav = React.forwardRef< /> +
  • + +
  • + {networkSection && (
  • - + If true then result excludes waves the authenticated user already + follows + required: false + schema: + type: boolean responses: "200": description: successful operation @@ -4939,6 +4948,37 @@ paths: application/json: schema: $ref: "#/components/schemas/ApiWaveDropsFeed" + /waves/{id}/pinned-drop: + post: + tags: + - Waves + summary: Change pinned drop in a wave + operationId: setWavePinnedDrop + parameters: + - name: id + in: path + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/ApiSetPinnedDropRequest" + responses: + "200": + description: successful operation + content: + application/json: + schema: + $ref: "#/components/schemas/ApiWave" + "400": + description: Invalid request + "403": + description: Only wave creators or admins can change pinned drop + "404": + description: Wave or drop not found /waves/{id}/pins: post: tags: @@ -6422,6 +6462,58 @@ components: format: int64 contributors: $ref: "#/components/schemas/ApiCicContributorsPage" + ApiCollectedStats: + type: object + required: + - boost + - nextgen_balance + - gradients_balance + - memes_balance + - unique_memes + - seasons + properties: + boost: + type: number + format: double + nextgen_balance: + type: integer + format: int64 + gradients_balance: + type: integer + format: int64 + memes_balance: + type: integer + format: int64 + unique_memes: + type: integer + format: int64 + seasons: + type: array + items: + $ref: "#/components/schemas/ApiCollectedStatsSeason" + ApiCollectedStatsSeason: + type: object + required: + - season + - total_cards_in_season + - sets_held + - partial_set_unique_cards_held + - total_cards_held + properties: + season: + type: string + total_cards_in_season: + type: integer + format: int64 + sets_held: + type: integer + format: int64 + partial_set_unique_cards_held: + type: integer + format: int64 + total_cards_held: + type: integer + format: int64 ApiCommunityMemberMinimal: type: object required: @@ -9580,58 +9672,6 @@ components: platform: type: string description: Optional platform (ios, android, web) - ApiCollectedStats: - type: object - required: - - boost - - nextgen_balance - - gradients_balance - - memes_balance - - unique_memes - - seasons - properties: - boost: - type: number - format: double - nextgen_balance: - type: integer - format: int64 - gradients_balance: - type: integer - format: int64 - memes_balance: - type: integer - format: int64 - unique_memes: - type: integer - format: int64 - seasons: - type: array - items: - $ref: "#/components/schemas/ApiCollectedStatsSeason" - ApiCollectedStatsSeason: - type: object - required: - - season - - total_cards_in_season - - sets_held - - partial_set_unique_cards_held - - total_cards_held - properties: - season: - type: string - total_cards_in_season: - type: integer - format: int64 - sets_held: - type: integer - format: int64 - partial_set_unique_cards_held: - type: integer - format: int64 - total_cards_held: - type: integer - format: int64 ApiRepCategoriesPage: type: object required: @@ -9770,6 +9810,13 @@ components: curation_wave_id: type: string nullable: true + ApiSetPinnedDropRequest: + type: object + required: + - drop_id + properties: + drop_id: + type: string ApiStartMultipartMediaUploadResponse: required: - upload_id