Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,4 @@ scripts/lint-to-json.js
eslint-rule-summary.csv

.hooks
.coderabbit.yml
68 changes: 34 additions & 34 deletions components/header/AppSidebarUserInfo.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { useSeizeConnectContext } from "../auth/SeizeConnectContext";
import { useAuth } from "../auth/Auth";
import { resolveIpfsUrlSync } from "@/components/ipfs/IPFSContext";
import { useIdentity } from "@/hooks/useIdentity";
import Image from "next/image";
import { useAuth } from "../auth/Auth";
import { useSeizeConnectContext } from "../auth/SeizeConnectContext";
import UserLevel from "../user/utils/level/UserLevel";
import AppSidebarUserStats from "./AppSidebarUserStats";

Expand All @@ -13,7 +15,11 @@ export default function AppSidebarUserInfo() {
});

const handleOrWallet = (() => {
if (activeProfileProxy) return activeProfileProxy.created_by.handle;
if (activeProfileProxy)
return (
activeProfileProxy.created_by.handle ??
activeProfileProxy.created_by.primary_address
);
if (profile?.handle) return profile.handle;
if (address) return address;
return "";
Expand All @@ -31,47 +37,41 @@ export default function AppSidebarUserInfo() {
return profile?.pfp ?? null;
})();

const level = activeProfileProxy
? activeProfileProxy.created_by.level
: profile?.level ?? 0;
const tdh = activeProfileProxy
? activeProfileProxy.created_by.tdh
: profile?.tdh ?? 0;
const tdh_rate = activeProfileProxy
? activeProfileProxy.created_by.tdh_rate
: profile?.tdh_rate ?? 0;
const rep = activeProfileProxy
? activeProfileProxy.created_by.rep
: profile?.rep ?? 0;
const xtdh = activeProfileProxy
? activeProfileProxy.created_by.xtdh
: profile?.xtdh ?? 0;
const xtdh_rate = activeProfileProxy
? activeProfileProxy.created_by.xtdh_rate
: profile?.xtdh_rate ?? 0;
const cic = activeProfileProxy
? activeProfileProxy.created_by.cic
: profile?.cic ?? 0;
const resolvedPfp = pfp && resolveIpfsUrlSync(pfp);

const source = activeProfileProxy?.created_by ?? profile;

const level = source?.level ?? 0;
const tdh = source?.tdh ?? 0;
const tdh_rate = source?.tdh_rate ?? 0;
const rep = source?.rep ?? 0;
const xtdh = source?.xtdh ?? 0;
const xtdh_rate = source?.xtdh_rate ?? 0;
const cic = source?.cic ?? 0;

return (
<div className="tailwind-scope tw-flex tw-flex-col tw-gap-3 tw-py-2">
{pfp ? (
<img
src={pfp}
alt="pfp"
className="tw-w-12 tw-h-12 tw-rounded-full tw-ring-2 tw-ring-iron-700 tw-bg-iron-900 tw-object-contain"
/>
{resolvedPfp ? (
<div className="tw-relative tw-h-12 tw-w-12">
<Image
src={resolvedPfp}
alt={`${handleOrWallet}'s profile picture`}
fill
sizes="48px"
className="tw-rounded-full tw-bg-iron-900 tw-object-contain tw-ring-2 tw-ring-iron-700"
/>
</div>
) : (
<div className="tw-w-12 tw-h-12 tw-rounded-full tw-ring-2 tw-ring-iron-700 tw-bg-iron-900" />
<div className="tw-h-12 tw-w-12 tw-rounded-full tw-bg-iron-900 tw-ring-2 tw-ring-iron-700" />
)}
<div className="tw-space-y-1 tw-flex tw-flex-col tw-items-start">
<span className="tw-text-iron-50 tw-text-base sm:tw-text-lg tw-font-semibold tw-truncate">
<div className="tw-flex tw-flex-col tw-items-start tw-space-y-1">
<span className="tw-truncate tw-text-base tw-font-semibold tw-text-iron-50 sm:tw-text-lg">
{label}
</span>
<UserLevel level={level} size="xs" />
</div>
<AppSidebarUserStats
handle={handleOrWallet ?? ""}
handle={handleOrWallet}
tdh={tdh}
tdh_rate={tdh_rate}
xtdh={xtdh}
Expand Down
51 changes: 27 additions & 24 deletions components/memes/drops/MemesLeaderboardDrop.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,31 @@
"use client";

import React, { useState } from "react";
import { createPortal } from "react-dom";
import type { ExtendedDrop } from "@/helpers/waves/drop.helpers";
import { DropLocation } from "@/components/waves/drops/Drop";
import { useDropInteractionRules } from "@/hooks/drops/useDropInteractionRules";
import MemesLeaderboardDropCard from "./MemesLeaderboardDropCard";
import MemesLeaderboardDropHeader from "./MemesLeaderboardDropHeader";
import MemesLeaderboardDropDescription from "./MemesLeaderboardDropDescription";
import MemesLeaderboardDropVoteSummary from "./MemesLeaderboardDropVoteSummary";
import MemesLeaderboardDropArtistInfo from "./MemesLeaderboardDropArtistInfo";
import MemeDropTraits from "./MemeDropTraits";
import DropListItemContentMedia from "@/components/drops/view/item/content/media/DropListItemContentMedia";
import WaveDropActionsOptions from "@/components/waves/drops/WaveDropActionsOptions";
import WaveDropActionsOpen from "@/components/waves/drops/WaveDropActionsOpen";
import { VotingModal, MobileVotingModal } from "@/components/voting";
import VotingModalButton from "@/components/voting/VotingModalButton";
import useIsMobileScreen from "@/hooks/isMobileScreen";
import useDeviceInfo from "@/hooks/useDeviceInfo";
import useLongPressInteraction from "@/hooks/useLongPressInteraction";
import CommonDropdownItemsMobileWrapper from "@/components/utils/select/dropdown/CommonDropdownItemsMobileWrapper";
import { MobileVotingModal, VotingModal } from "@/components/voting";
import VotingModalButton from "@/components/voting/VotingModalButton";
import { DropLocation } from "@/components/waves/drops/Drop";
import WaveDropActionsOpen from "@/components/waves/drops/WaveDropActionsOpen";
import WaveDropActionsOptions from "@/components/waves/drops/WaveDropActionsOptions";
import WaveDropMobileMenuDelete from "@/components/waves/drops/WaveDropMobileMenuDelete";
import WaveDropMobileMenuOpen from "@/components/waves/drops/WaveDropMobileMenuOpen";
import Link from "next/link";
import { formatNumberWithCommas } from "@/helpers/Helpers";
import { ImageScale } from "@/helpers/image.helpers";
import type { ExtendedDrop } from "@/helpers/waves/drop.helpers";
import { useDropInteractionRules } from "@/hooks/drops/useDropInteractionRules";
import useIsMobileScreen from "@/hooks/isMobileScreen";
import useDeviceInfo from "@/hooks/useDeviceInfo";
import useLongPressInteraction from "@/hooks/useLongPressInteraction";
import { useMediaQuery } from "@/hooks/useMediaQuery";
import Link from "next/link";
import React, { useState } from "react";
import { createPortal } from "react-dom";
import MemeDropTraits from "./MemeDropTraits";
import MemesLeaderboardDropArtistInfo from "./MemesLeaderboardDropArtistInfo";
import MemesLeaderboardDropCard from "./MemesLeaderboardDropCard";
import MemesLeaderboardDropDescription from "./MemesLeaderboardDropDescription";
import MemesLeaderboardDropHeader from "./MemesLeaderboardDropHeader";
import MemesLeaderboardDropVoteSummary from "./MemesLeaderboardDropVoteSummary";

interface MemesLeaderboardDropProps {
readonly drop: ExtendedDrop;
Expand Down Expand Up @@ -59,17 +59,17 @@ export const MemesLeaderboardDrop: React.FC<MemesLeaderboardDropProps> = ({

// Extract metadata
const title =
drop.metadata?.find((m) => m.data_key === "title")?.data_value ??
drop.metadata.find((m) => m.data_key === "title")?.data_value ??
"Artwork Title";
const description =
drop.metadata?.find((m) => m.data_key === "description")?.data_value ??
drop.metadata.find((m) => m.data_key === "description")?.data_value ??
"This is an artwork submission for The Memes collection.";

// Get artwork media URL if available
const artworkMedia = drop.parts.at(0)?.media.at(0);

// Get top voters for votes display
const firstThreeVoters = drop.top_raters?.slice(0, 3) ?? [];
const firstThreeVoters = drop.top_raters.slice(0, 3);

return (
<div
Expand Down Expand Up @@ -143,8 +143,11 @@ export const MemesLeaderboardDrop: React.FC<MemesLeaderboardDropProps> = ({
<div className="tw-flex tw-items-center -tw-space-x-2">
{firstThreeVoters.map((voter) => (
<Link
key={voter.profile.handle}
href={`/${voter.profile.handle}`}
key={
voter.profile.handle ??
voter.profile.primary_address
}
href={`/${voter.profile.handle ?? voter.profile.primary_address}`}
onClick={(e) => e.stopPropagation()}
>
{voter.profile.pfp ? (
Expand Down
96 changes: 53 additions & 43 deletions components/navigation/ViewContext.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
"use client";

import type {
ReactNode} from "react";
import type { ReactNode } from "react";
import React, {
createContext,
useContext,
useState,
useMemo,
useCallback,
useEffect,
useRef,
} from "react";
import type { ViewKey, NavItem } from "./navTypes";
import { commonApiFetch } from "@/services/api/common-api";
Expand All @@ -22,8 +22,7 @@ import {
getWaveRoute,
getWavesBaseRoute,
} from "@/helpers/navigation.helpers";
import type {
HomeTab} from "@/components/home/useHomeTabs";
import type { HomeTab } from "@/components/home/useHomeTabs";
import {
HOME_TAB_EVENT,
getStoredHomeTab,
Expand All @@ -46,27 +45,32 @@ export const ViewProvider: React.FC<{ readonly children: ReactNode }> = ({
const router = useRouter();
const searchParams = useSearchParams();
const { isApp } = useDeviceInfo();
const [activeView, setActiveView] = useState<ViewKey | null>(null);
const [lastVisitedWave, setLastVisitedWave] = useState<string | null>(null);
const [lastVisitedDm, setLastVisitedDm] = useState<string | null>(null);
const [homeActiveTab, setHomeActiveTab] = useState<HomeTab>(() =>
getStoredHomeTab()
);
const waveParam = searchParams?.get("wave");
const viewParam = searchParams?.get("view");
const [lastFetchedWaveId, setLastFetchedWaveId] = useState<string | null>(null);
const waveParam = searchParams.get("wave");
const viewParam = searchParams.get("view");
const lastFetchedWaveIdRef = useRef<string | null>(null);

// Derived state: activeView is computed from URL params, not stored in state
const activeView = useMemo<ViewKey | null>(() => {
if (waveParam) {
return null;
}
return viewParam ? (viewParam as ViewKey) : null;
}, [waveParam, viewParam]);

useEffect(() => {
const { window: browserWindow } = globalThis as typeof globalThis & {
window?: Window | undefined;
};
if (browserWindow === undefined) {
return;
}

const handleTabEvent = (event: Event) => {
const detail = (event as CustomEvent<{ tab?: HomeTab | undefined }>).detail;
const tab = detail?.tab;
const detail = (event as CustomEvent<{ tab?: HomeTab | undefined }>)
.detail;
const tab = detail.tab;
if (tab !== "feed" && tab !== "latest") return;
setHomeActiveTab(tab);
};
Expand All @@ -89,61 +93,54 @@ export const ViewProvider: React.FC<{ readonly children: ReactNode }> = ({
endpoint: `/waves/${targetWaveId}`,
})
.then((res) => {
if (!res) {
return;
}
if (res.chat.scope.group?.is_direct_message) {
setLastVisitedDm(res.id);
} else {
setLastVisitedWave(res.id);
}
})
.catch((error) => {
.catch((error: unknown) => {
console.warn("Failed to fetch wave metadata", error);
})
.finally(() => {
setLastFetchedWaveId(targetWaveId);
lastFetchedWaveIdRef.current = targetWaveId;
});
}, []);

// Effect for fetching wave details - only handles the external API call
useEffect(() => {
if (waveParam) {
setActiveView(null);
if (waveParam !== lastFetchedWaveId) {
fetchWaveDetails(waveParam);
}
} else {
if (lastFetchedWaveId !== null) {
setLastFetchedWaveId(null);
}
if (viewParam) {
setActiveView(viewParam as ViewKey);
} else {
setActiveView(null);
}
if (waveParam && waveParam !== lastFetchedWaveIdRef.current) {
fetchWaveDetails(waveParam);
} else if (!waveParam) {
lastFetchedWaveIdRef.current = null;
}
}, [waveParam, viewParam, fetchWaveDetails, lastFetchedWaveId]);
}, [waveParam, fetchWaveDetails]);

const handleNavClick = useCallback(
async (item: NavItem) => {
(item: NavItem) => {
if (item.kind === "route") {
if (item.name === "Stream") {
setActiveView(null);
// activeView will become null automatically when URL changes (no wave/view params)
setLastVisitedWave(null);
setLastVisitedDm(null);
setHomeActiveTab("feed");
setStoredHomeTab("feed");
router.push(getHomeFeedRoute());
} else if (item.name === "Home") {
setActiveView(null);
// activeView will become null automatically when URL changes
setHomeActiveTab("latest");
setStoredHomeTab("latest");
router.push(getHomeLatestRoute());
} else {
router.push(item.href);
}
} else if (item.kind === "view" && item.viewKey === "waves") {
if (lastVisitedWave) {
} else if (item.viewKey === "waves") {
const currentWaveId = searchParams.get("wave");

if (currentWaveId) {
setLastVisitedWave(null);
router.push(getWavesBaseRoute(isApp));
} else if (lastVisitedWave) {
router.push(
getWaveRoute({
waveId: lastVisitedWave,
Expand All @@ -154,8 +151,14 @@ export const ViewProvider: React.FC<{ readonly children: ReactNode }> = ({
} else {
router.push(getWavesBaseRoute(isApp));
}
} else if (item.kind === "view" && item.viewKey === "messages") {
if (lastVisitedDm) {
} else {
// item.viewKey === "messages" (only remaining case)
const currentWaveId = searchParams.get("wave");

if (currentWaveId) {
setLastVisitedDm(null);
router.push(getMessagesBaseRoute(isApp));
} else if (lastVisitedDm) {
router.push(
getWaveRoute({
waveId: lastVisitedDm,
Expand All @@ -168,15 +171,16 @@ export const ViewProvider: React.FC<{ readonly children: ReactNode }> = ({
}
}
},
[router, lastVisitedWave, lastVisitedDm, isApp]
[router, lastVisitedWave, lastVisitedDm, isApp, searchParams]
);

const hardBack = useCallback(
(v: ViewKey) => {
if (v === "messages") {
setLastVisitedDm(null);
router.push(getMessagesBaseRoute(isApp));
} else if (v === "waves") {
} else {
// v === "waves" (only remaining case)
setLastVisitedWave(null);
router.push(getWavesBaseRoute(isApp));
}
Expand All @@ -193,7 +197,13 @@ export const ViewProvider: React.FC<{ readonly children: ReactNode }> = ({
}, []);

const providerValue = useMemo(
() => ({ activeView, handleNavClick, hardBack, homeActiveTab, clearLastVisited }),
() => ({
activeView,
handleNavClick,
hardBack,
homeActiveTab,
clearLastVisited,
}),
[activeView, handleNavClick, hardBack, homeActiveTab, clearLastVisited]
);

Expand Down
Loading