Skip to content

Commit

Permalink
add thread view
Browse files Browse the repository at this point in the history
  • Loading branch information
hellno committed Aug 24, 2023
1 parent 00ebf22 commit 6b064f6
Show file tree
Hide file tree
Showing 6 changed files with 167 additions and 45 deletions.
8 changes: 4 additions & 4 deletions src/common/components/CastRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import { ImgurImage } from "@/common/components/PostEmbeddedContent";

interface CastRowProps {
cast: CastType;
isSelected: boolean;
showChannel: boolean;
onSelect: () => void;
channels: ChannelType[];
onSelect?: () => void;
isSelected?: boolean;
}

export const CastRow = ({ cast, isSelected, showChannel, onSelect, channels }: CastRowProps) => {
Expand Down Expand Up @@ -65,10 +65,10 @@ export const CastRow = ({ cast, isSelected, showChannel, onSelect, channels }: C

return (<div className="flex grow">
<div
onClick={() => onSelect()}
onClick={() => onSelect && onSelect()}
className={classNames(
isSelected ? "bg-gray-700 border-l border-gray-200" : "",
"grow rounded-r-sm py-1.5 px-3 cursor-pointer"
"grow rounded-r-sm py-2 cursor-pointer"
)}>
<div className="flex justify-between gap-x-4">
<div className="flex flex-row py-0.5 text-xs leading-5 text-gray-300">
Expand Down
75 changes: 75 additions & 0 deletions src/common/components/CastThreadView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import React, { useEffect, useState } from "react";
import { CastType } from "@/common/constants/farcaster"
import { getNeynarCastThreadEndpoint } from "../helpers/neynar";
import { Loading } from "./Loading";
import { CastRow } from "./CastRow";
import { useAccountStore } from "@/stores/useAccountStore";

// todo: cast in thread has slightly different structure
// pfps don't seem to render
// can auto expand images
// add up down selector hotkeys in list with j and k

export const CastThreadView = ({ cast, onBack, fid }: { cast: CastType, onBack: () => void, fid?: string }) => {
const [isLoading, setIsLoading] = useState(true);
const [casts, setCasts] = useState<CastType[]>([]);

const {
channels,
selectedChannelIdx
} = useAccountStore();

const renderGoBackButton = () => (
<button
type="button"
className="w-20 inline-flex items-center px-2 py-1 border border-transparent shadow-sm text-sm font-medium rounded-sm text-gray-100 bg-gray-700 hover:bg-gray-600 focus:outline-none"
onClick={() => onBack()}
>
go back
</button>
);

useEffect(() => {
const loadData = async () => {
const neynarEndpoint = getNeynarCastThreadEndpoint({ castHash: cast.hash, fid });
await fetch(neynarEndpoint)
.then((response) => response.json())
.then((resp) => {
console.log(resp.result.casts)
setCasts(resp.result.casts)
})
.catch((error) => {
console.log({ error })
})
.finally(() => {
setIsLoading(false)
})
}

loadData();
}, [])

const renderThread = () => (
<ul role="list" className="divide-y divide-gray-700">
{casts.map((cast: CastType, idx: number) => (
<li key={cast.hash}
className="relative flex items-center space-x-4 py-2 max-w-full md:max-w-2xl xl:max-w-4xl">
<CastRow
cast={cast}
channels={channels}
showChannel={selectedChannelIdx === null}
/* isSelected={selectedCastIdx === idx} */
/* onSelect={() => selectedCastIdx === idx ? onSelectCast(idx) : setSelectedCastIdx(idx)} */
/>
</li>
))}
</ul>
);

return <div className="flex flex-col text-gray-100 text-lg">
{isLoading ? <Loading /> : renderThread()}
<div className="py-2">
{renderGoBackButton()}
</div>
</div>
}
1 change: 0 additions & 1 deletion src/common/components/HotkeyTooltipWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import React from "react";
import * as Tooltip from '@radix-ui/react-tooltip';
import { KeyboardIcon } from '@radix-ui/react-icons';
import { Kbd } from "@radix-ui/themes";


type HotkeyTooltipWrapperProps = {
Expand Down
1 change: 1 addition & 0 deletions src/common/components/Loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const Loading = () => <span className="my-4 font-semibold text-gray-200">Loading...</span>
42 changes: 42 additions & 0 deletions src/common/helpers/neynar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { VITE_NEYNAR_API_KEY } from "@/common/constants/farcaster";

const DEFAULT_FEED_PAGE_SIZE = 15;

type FeedEndpointProps = {
fid?: string;
parentUrl?: string;
cursor?: string;
limit?: number;
};

type CastThreadEndpointProps = {
castHash: string;
fid?: string;
}


export const getNeynarFeedEndpoint = ({ fid, parentUrl, cursor, limit }: FeedEndpointProps): string => {
let neynarEndpoint = `https://api.neynar.com/v2/farcaster/feed/?api_key=${VITE_NEYNAR_API_KEY}&limit=${limit || DEFAULT_FEED_PAGE_SIZE}`;

if (parentUrl) {
neynarEndpoint += `&feed_type=filter&filter_type=parent_url&parent_url=${parentUrl}`;
} else if (fid) {
neynarEndpoint += `&fid=${fid}`;
}

if (cursor) {
neynarEndpoint += `&cursor=${cursor}`;
}

return neynarEndpoint;
}

export const getNeynarCastThreadEndpoint = ({ castHash, fid }: CastThreadEndpointProps): string => {
let neynarEndpoint = `https://api.neynar.com/v1/farcaster/all-casts-in-thread/?api_key=${VITE_NEYNAR_API_KEY}&threadHash=${castHash}`;

if (fid) {
neynarEndpoint += `&viewerFid=${fid}`;
}

return neynarEndpoint;
}
85 changes: 45 additions & 40 deletions src/pages/Feed/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useEffect, useRef, useState } from "react";
import { AccountObjectType, useAccountStore } from "@/stores/useAccountStore";
import { CastType, VITE_NEYNAR_API_KEY } from "@/common/constants/farcaster";
import { CastType } from "@/common/constants/farcaster";
import { useHotkeys } from "react-hotkeys-hook";
import uniqBy from 'lodash.uniqby';
import get from 'lodash.get';
Expand All @@ -9,6 +9,9 @@ import { Key } from 'ts-key-enum';
import { openWindow } from "@/common/helpers/navigation";
import { useInView } from 'react-intersection-observer';
import isEmpty from "lodash.isempty";
import { CastThreadView } from "@/common/components/CastThreadView";
import { getNeynarFeedEndpoint } from "@/common/helpers/neynar";
import { Loading } from "@/common/components/Loading";

type FeedType = {
[key: string]: CastType[]
Expand All @@ -20,6 +23,8 @@ export default function Feed() {
const [isLoadingFeed, setIsLoadingFeed] = useState(false);
const [nextFeedOffset, setNextFeedOffset] = useState("");
const [selectedCastIdx, setSelectedCastIdx] = useState(0);
const [showCastThreadView, setShowCastThreadView] = useState(false);

const {
accounts,
channels,
Expand Down Expand Up @@ -58,10 +63,11 @@ export default function Feed() {
}

const onExpandCast = (idx: number) => {
const cast = feed[idx];
// const cast = feed[idx];
setShowCastThreadView(true);

const url = `https://warpcast.com/${cast.author.username}/${cast.hash.slice(0, 8)}`;
openWindow(url);
// const url = `https://warpcast.com/${cast.author.username}/${cast.hash.slice(0, 8)}`;
// openWindow(url);
}

useHotkeys(['j', Key.ArrowDown], () => {
Expand All @@ -85,7 +91,7 @@ export default function Feed() {
}, [selectedCastIdx], {
})

useHotkeys('o', () => {
useHotkeys(['o', Key.Enter], () => {
onExpandCast(selectedCastIdx);
}, [selectedCastIdx], {
})
Expand All @@ -95,25 +101,18 @@ export default function Feed() {
}, [selectedCastIdx], {
})

useHotkeys('esc', () => {
setShowCastThreadView(false);
}, [selectedCastIdx], {
})

const getFeed = async ({ fid, parentUrl, cursor }: { fid: string, parentUrl?: string, cursor?: string }) => {
if (isLoadingFeed) {
return;
}
setIsLoadingFeed(true);

const limit = 15;
let neynarEndpoint = `https://api.neynar.com/v2/farcaster/feed/?api_key=${VITE_NEYNAR_API_KEY}&limit=${limit}`;

if (parentUrl) {
neynarEndpoint += `&feed_type=filter&filter_type=parent_url&parent_url=${parentUrl}`;
} else if (fid) {
neynarEndpoint += `&fid=${fid}`;
}

if (cursor) {
neynarEndpoint += `&cursor=${cursor}`;
}
const neynarEndpoint = getNeynarFeedEndpoint({ fid, parentUrl, cursor });
await fetch(neynarEndpoint)
.then((response) => response.json())
.then((data) => {
Expand All @@ -134,6 +133,8 @@ export default function Feed() {
useEffect(() => {
if (account) {
setSelectedCastIdx(0);
setShowCastThreadView(false);

const fid = account.platformAccountId;
getFeed({ parentUrl: selectedChannelParentUrl, fid });
}
Expand All @@ -155,30 +156,34 @@ export default function Feed() {
}
}, [selectedCastIdx]);


const renderFeed = () => (
<ul role="list" className="divide-y divide-gray-700">
{feed.map((cast: CastType, idx: number) => (
<li key={cast.hash} ref={(selectedCastIdx === idx - 3) ? scollToRef : null}
className="relative flex items-center space-x-4 py-2 max-w-full md:max-w-2xl xl:max-w-4xl">
<CastRow
cast={cast}
channels={channels}
showChannel={selectedChannelIdx === null}
isSelected={selectedCastIdx === idx}
onSelect={() => selectedCastIdx === idx ? onSelectCast(idx) : setSelectedCastIdx(idx)}
/>
</li>
))}
<li ref={ref} className="" />
</ul>
);

return (
<div
className="mr-6"
/* ref={listRef} */
>
<ul role="list" className="divide-y divide-gray-700">
{feed.map((cast: CastType, idx: number) => {
return (
<li key={cast.hash} ref={(selectedCastIdx === idx - 3) ? scollToRef : null
}
className="relative flex items-center space-x-4 py-2 max-w-full md:max-w-2xl xl:max-w-4xl" >
<CastRow
cast={cast}
channels={channels}
showChannel={selectedChannelIdx === null}
isSelected={selectedCastIdx === idx}
onSelect={() => selectedCastIdx === idx ? onSelectCast(idx) : setSelectedCastIdx(idx)}
/>
</li>
);
})}
<li ref={ref} className="" />
</ul>
{isLoadingFeed && (<span className="my-4 font-semibold text-gray-200">Loading...</span>)}
<div className="mr-4">
{showCastThreadView ?
<CastThreadView
cast={feed[selectedCastIdx]}
fid={account.platformAccountId}
onBack={() => setShowCastThreadView(false)}
/> : renderFeed()}
{isLoadingFeed && <Loading />}
</div >
)
}

0 comments on commit 6b064f6

Please sign in to comment.