-
Notifications
You must be signed in to change notification settings - Fork 36
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature/farcaster feed #284
Changes from 44 commits
06f032c
b26dfe7
cf5b9c7
04a3b13
3851cc4
156b999
d67f147
f4e1f97
9b3bfe1
a43a85c
172fdc1
bfa3121
fda8c51
a95f257
7240f92
1dc0f09
a41f0f7
6d131f6
8e4b88a
a95c883
cbdb385
cbc0e9c
b788827
3b1ef08
7866336
cf4ac5c
714973d
bb6c33d
5749f86
677b810
5008985
ff627cb
b756edd
55f2e2d
60677be
962edbf
9fbd61d
42e5566
a6fdca1
e55278e
ecf7d2c
b7c8a67
6ac654b
22384cb
8d3d5a2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
export const BUILDER_COLLECTION = '0xdf9b7d26c8fc806b1ae6273684556761ff02d422' | ||
export const PURPLE_COLLECTION = '0xa45662638e9f3bbb7a6fecb4b17853b7ba0f3a60' | ||
|
||
export const CAST_ENABLED = [BUILDER_COLLECTION, PURPLE_COLLECTION] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import { getSSLHubRpcClient } from '@farcaster/hub-nodejs' | ||
|
||
export const farcasterClient = () => { | ||
const client = getSSLHubRpcClient( | ||
process.env.FARCASTER_HUB || 'testnet1.farcaster.xyz:2283' | ||
) | ||
|
||
return client | ||
} |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For this we can check There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good idea! Done. Refactored it all so we use native throws and try catches in the handlers. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
import { hexStringToBytes, isCastAddMessage } from '@farcaster/hub-nodejs' | ||
|
||
import { farcasterClient } from '../client' | ||
import { getFarcasterProfile, getfName } from './farcasterProfile.ts' | ||
|
||
const createChannelString = (collectionAddress: string, chainId: string) => { | ||
return `chain://eip155:${chainId}/erc721:${collectionAddress}` | ||
} | ||
|
||
const handleMentions = async (fIDs: number[], mentionsPositions: number[]) => { | ||
if (!fIDs || !mentionsPositions) return [] | ||
|
||
const res = await Promise.all( | ||
fIDs.map(async (fid) => { | ||
const res = await getfName(fid) | ||
return res?.fName || null | ||
}) | ||
) | ||
|
||
return res | ||
} | ||
|
||
export const getDAOfeed = async (feedId: string) => { | ||
const client = farcasterClient() | ||
|
||
const [collectionAddress, chainId, nextToken] = feedId.split(':') | ||
|
||
const nextBufferArray = | ||
nextToken?.slice(0, 2) === '0x' | ||
? hexStringToBytes(nextToken)._unsafeUnwrap() | ||
: undefined | ||
|
||
const res = await client.getCastsByParent({ | ||
parentUrl: createChannelString(collectionAddress, chainId), | ||
reverse: true, | ||
pageSize: 5, | ||
pageToken: nextBufferArray, | ||
}) | ||
|
||
client.close() | ||
|
||
if (res.isErr()) { | ||
throw new Error(res.error.message) | ||
} | ||
|
||
// Coerce Messages into Casts, should not actually filter out any messages | ||
const casts = res.value.messages.filter(isCastAddMessage) | ||
|
||
try { | ||
const castsWithProfiles = await Promise.all( | ||
casts.map(async (cast) => { | ||
const [profile, mentionsfNames] = await Promise.all([ | ||
getFarcasterProfile(cast.data.fid), | ||
handleMentions( | ||
cast.data.castAddBody.mentions, | ||
cast.data.castAddBody.mentionsPositions | ||
), | ||
]) | ||
return { | ||
...cast, | ||
profile, | ||
mentionsfNames, | ||
} | ||
}) | ||
) | ||
return { | ||
data: castsWithProfiles.filter((msg) => !msg.data.castAddBody.parentCastId), | ||
nextPageToken: res.value.nextPageToken, | ||
} | ||
} catch (error) { | ||
console.error('error', error) | ||
throw new Error(error as any) | ||
} | ||
} |
jordanlesich marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import { UserDataType } from '@farcaster/hub-nodejs' | ||
|
||
import { farcasterClient } from '../client' | ||
|
||
const logFetchError = (dataType: string, fid: number, errorMsg: string) => { | ||
console.error( | ||
`${dataType} Not Found | ||
Error: ${errorMsg} | ||
fid: ${fid} | ||
` | ||
) | ||
} | ||
|
||
export const getfName = async (fid: number) => { | ||
const client = farcasterClient() | ||
|
||
const fName = await client.getUserData({ fid, userDataType: UserDataType.FNAME }) | ||
jordanlesich marked this conversation as resolved.
Show resolved
Hide resolved
|
||
client.close() | ||
if (fName.isErr()) logFetchError('Farcaster Name', fid, fName.error.message) | ||
return { | ||
fName: fName.isOk() ? fName.value.data?.userDataBody?.value : undefined, | ||
} | ||
} | ||
|
||
export const getFarcasterProfile = async (fid: number) => { | ||
const client = farcasterClient() | ||
|
||
const [pfpRes, nameRes, fName] = await Promise.all([ | ||
client.getUserData({ fid, userDataType: UserDataType.PFP }), | ||
client.getUserData({ fid, userDataType: UserDataType.DISPLAY }), | ||
client.getUserData({ fid, userDataType: UserDataType.FNAME }), | ||
]) | ||
|
||
client.close() | ||
|
||
// Decided to not throw errors here as that would block either the | ||
// entire feed or the entire profile from loading. Instead, I'll | ||
// try to log the error with as many details as possible | ||
|
||
if (nameRes.isErr()) logFetchError('Display Name', fid, nameRes.error.message) | ||
if (pfpRes.isErr()) logFetchError('Profile Picture', fid, pfpRes.error.message) | ||
if (fName.isErr()) logFetchError('Farcaster Name', fid, fName.error.message) | ||
|
||
// Neverthrow typing didn't allow for optional chaining checks. Need to use isOk() instead. | ||
return { | ||
displayName: nameRes.isOk() ? nameRes.value.data?.userDataBody?.value : undefined, | ||
pfp: pfpRes.isOk() ? pfpRes.value.data?.userDataBody?.value : undefined, | ||
fName: fName.isOk() ? fName.value.data?.userDataBody?.value : undefined, | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import { Box } from '@zoralabs/zord' | ||
|
||
import { cardSkeleton } from './Feed.css' | ||
|
||
export const CardSkeleton = () => ( | ||
<Box className={cardSkeleton} borderRadius="normal" backgroundColor="background2" /> | ||
) |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: can we remove className here instead of commenting out |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
import { Box, Flex, Text } from '@zoralabs/zord' | ||
import dayjs from 'dayjs' | ||
import relativeTime from 'dayjs/plugin/relativeTime' | ||
import React, { useMemo } from 'react' | ||
|
||
import { CasterProfile } from './Feed' | ||
import { cardLink, cardWrapper, castText, pfpStyles, pfpWrapper } from './Feed.css' | ||
|
||
export const CastCard = ({ | ||
text, | ||
timestamp, | ||
hexHash, | ||
mentions, | ||
mentionsPositions, | ||
profile, | ||
mentionsfNames, | ||
}: { | ||
text: string | ||
fid: number | ||
timestamp: number | ||
hexHash: string | ||
mentions: number[] | ||
mentionsPositions: number[] | ||
profile: CasterProfile | ||
mentionsfNames: string[] | ||
}) => { | ||
const time = useMemo(() => { | ||
dayjs.extend(relativeTime) | ||
const date = dayjs.unix(timestamp / 1000).fromNow() | ||
return date | ||
}, [timestamp]) | ||
|
||
const textWithMentions = useMemo(() => { | ||
if (!mentionsfNames || !text) return | ||
if (!mentions.length) return text | ||
|
||
const encoder = new TextEncoder() | ||
const bytes = encoder.encode(text) | ||
|
||
const decoder = new TextDecoder() | ||
let newText = '' | ||
let indexBytes = 0 | ||
|
||
for (let i = 0; i < mentions.length; i++) { | ||
newText += decoder.decode(bytes.slice(indexBytes, mentionsPositions[i])) | ||
|
||
const fName = mentionsfNames[i] | ||
|
||
newText += '@' + fName | ||
|
||
indexBytes = mentionsPositions[i] | ||
} | ||
newText += decoder.decode(bytes.slice(indexBytes, bytes.length)) | ||
|
||
return newText | ||
}, [text, mentionsfNames, mentions, mentionsPositions]) | ||
|
||
return ( | ||
<Box | ||
className={cardWrapper} | ||
py={{ '@initial': 'x4', '@768': 'x6' }} | ||
px={{ '@initial': 'x2', '@768': 'x6' }} | ||
borderRadius={'phat'} | ||
borderStyle={'solid'} | ||
borderWidth={'normal'} | ||
borderColor={'border'} | ||
mb={'x6'} | ||
> | ||
<Flex align={'center'} mb={'x4'}> | ||
<a | ||
className={cardLink} | ||
href={`https://warpcast.com/~/conversations/${hexHash}`} | ||
target="_blank" | ||
/> | ||
<Box mr="x2" borderRadius="round"> | ||
<div className={pfpWrapper}> | ||
<img | ||
alt="profile picture" | ||
src={profile?.pfp || '/nouns-avatar-circle.png'} | ||
className={pfpStyles} | ||
/> | ||
</div> | ||
</Box> | ||
<Flex direction={{ '@initial': 'column', '@768': 'row' }}> | ||
<Text mr={'x1'} fontWeight={'display'}> | ||
{profile?.displayName || '@' + profile?.fName || 'Name Not Found'} | ||
</Text> | ||
<Flex> | ||
<Text color="text3" mr={'x1'}> | ||
@{profile?.fName || 'NotFound'} | ||
</Text> | ||
<Text color="text3" mr={'x1'}> | ||
- | ||
</Text> | ||
<Text color="text3">{time}</Text> | ||
</Flex> | ||
</Flex> | ||
</Flex> | ||
<Text className={castText}>{textWithMentions}</Text> | ||
</Box> | ||
) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import { Flex, Text } from '@zoralabs/zord' | ||
import React from 'react' | ||
|
||
export const DisplayPanel = ({ | ||
title, | ||
description, | ||
}: { | ||
title: string | ||
description: string | ||
}) => { | ||
return ( | ||
<Flex | ||
borderRadius={'phat'} | ||
borderStyle={'solid'} | ||
height={'x64'} | ||
width={'100%'} | ||
borderWidth={'normal'} | ||
borderColor={'border'} | ||
direction={'column'} | ||
justify={'center'} | ||
align={'center'} | ||
> | ||
<Text fontSize={28} fontWeight={'display'} mb="x4" color={'text3'}> | ||
{title} | ||
</Text> | ||
<Text color={'text3'}>{description}</Text> | ||
</Flex> | ||
) | ||
} |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is there a way we can do these styles without There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So the fix for this is pretty weird. I just deleted the styles altogether. The global attribute selector that was targeting it before is no longer targeting it for some reason. The selector seemed machine generated so perhaps styles that are getting applied to the link component based on props that are not explicitly setting styles? Either way, no more !important in the code. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import { keyframes, style } from '@vanilla-extract/css' | ||
import { atoms } from '@zoralabs/zord' | ||
|
||
export const feed = style([ | ||
atoms({ | ||
m: 'auto', | ||
}), | ||
{ | ||
maxWidth: 912, | ||
}, | ||
]) | ||
|
||
export const castCardStyle = style({}) | ||
|
||
const pulse = keyframes({ | ||
'0%': { opacity: '1' }, | ||
'100%': { opacity: '1' }, | ||
'50%': { opacity: '.5' }, | ||
}) | ||
|
||
export const cardSkeleton = style({ | ||
animation: `${pulse} 2s cubic-bezier(0.4, 0, 0.6, 1) infinite`, | ||
height: '10rem', | ||
marginBottom: '1.6rem', | ||
}) | ||
|
||
export const cardWrapper = style({ | ||
width: '100%', | ||
transition: 'all 0.15s ease-in-out', | ||
position: 'relative', | ||
selectors: { | ||
'&:hover': { | ||
backgroundColor: '#fafafa', | ||
cursor: 'pointer', | ||
}, | ||
}, | ||
}) | ||
export const loadMoreButton = style({ | ||
width: '100%', | ||
}) | ||
|
||
export const pfpStyles = style({ | ||
position: 'absolute', | ||
top: '50%', | ||
left: '50%', | ||
height: '100%', | ||
width: '100%', | ||
transform: 'translate(-50%, -50%)', | ||
objectFit: 'cover', | ||
borderRadius: '50%', | ||
}) | ||
export const pfpWrapper = style({ | ||
width: '32px', | ||
height: '32px', | ||
position: 'relative', | ||
overflow: 'hidden', | ||
}) | ||
export const castText = style({ | ||
wordBreak: 'break-word', | ||
whiteSpace: 'break-spaces', | ||
}) | ||
export const cardLink = style({ | ||
textDecoration: 'none', | ||
color: 'inherit', | ||
position: 'absolute', | ||
top: 0, | ||
left: 0, | ||
bottom: 0, | ||
right: 0, | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of adding neverthrow can we just unwrap the neverthrow objects from
hub-nodejs
. that way we don't need to add as a depThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I dug into the farcaster repo to see if it was exported for reuse. It isn't.
But you mentioned below to just use classic throws and remove neverthrow altogether, so I did that instead.