Skip to content
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

Merged
merged 45 commits into from
Jul 5, 2023
Merged
Show file tree
Hide file tree
Changes from 44 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
06f032c
farcaster client setup
jordanlesich Jun 22, 2023
b26dfe7
add neverthrow to validate farcaster monadic errors, rewrite getDAOfeed
jordanlesich Jun 22, 2023
cf5b9c7
create new cached endpoint for DAO feed
jordanlesich Jun 22, 2023
04a3b13
create feed module structure
jordanlesich Jun 22, 2023
3851cc4
opening feed tab triggers API fetch for farcaster feed data
jordanlesich Jun 22, 2023
156b999
renders body text to tab
jordanlesich Jun 23, 2023
d67f147
feed layout and cast cards
jordanlesich Jun 23, 2023
f4e1f97
Merge branch 'main' of https://github.com/ourzora/nouns-builder into …
jordanlesich Jun 23, 2023
9b3bfe1
fetch for user display and pfp
jordanlesich Jun 23, 2023
a43a85c
create API route and caching for profile fetches
jordanlesich Jun 23, 2023
172fdc1
get dates using native library, we may want to use another library
jordanlesich Jun 23, 2023
bfa3121
loading state for entire feed, cast card skeleton
jordanlesich Jun 25, 2023
fda8c51
loading state skeleton for card profile fetches
jordanlesich Jun 25, 2023
a95f257
no casts found state, farcaster links, fnames
jordanlesich Jun 26, 2023
7240f92
clean some styles, add description text
jordanlesich Jun 27, 2023
1dc0f09
refactor Feed subcomponents to separate files
jordanlesich Jun 27, 2023
a41f0f7
turn dynamic feed fetching back on
jordanlesich Jun 27, 2023
6d131f6
wrestle with pnpm
jordanlesich Jun 27, 2023
8e4b88a
merge conflicts
jordanlesich Jun 28, 2023
a95c883
use swr infinite pagination and nextTokens to fetch more data
jordanlesich Jun 29, 2023
cbdb385
switch card styles to match mock
jordanlesich Jun 29, 2023
cbc0e9c
correct sizing issue
jordanlesich Jun 29, 2023
b788827
bring page size down to 5
jordanlesich Jun 29, 2023
3b1ef08
use colon seperator
jordanlesich Jun 30, 2023
7866336
remove relativeTime symlink from package.json
jordanlesich Jun 30, 2023
cf4ac5c
remove cloudinary img allow
jordanlesich Jun 30, 2023
714973d
remove test CAIP-19 enpoints
jordanlesich Jun 30, 2023
bb6c33d
refactor to native error throw pattern with try/catch in api handlers…
jordanlesich Jun 30, 2023
5749f86
remove link styles altogether
jordanlesich Jun 30, 2023
677b810
remove maxWidth
jordanlesich Jul 3, 2023
5008985
fix formatting, new lines
jordanlesich Jul 3, 2023
ff627cb
fetch for mentions
jordanlesich Jul 3, 2023
b756edd
handle text decoding and mention placement
jordanlesich Jul 3, 2023
55f2e2d
change card link styles to prevent element nesting errors
jordanlesich Jul 3, 2023
60677be
style inline links
jordanlesich Jul 3, 2023
962edbf
restructure for top-level fetch, add fault tolerance and error handli…
jordanlesich Jul 4, 2023
9fbd61d
Merge branch 'main' of https://github.com/jordanlesich/nouns-builder …
jordanlesich Jul 4, 2023
42e5566
fix next env
jordanlesich Jul 4, 2023
a6fdca1
fix styling
jordanlesich Jul 4, 2023
e55278e
fix Error UI
jordanlesich Jul 4, 2023
ecf7d2c
remove no posts found UI
jordanlesich Jul 4, 2023
b7c8a67
remove inner profile and mention links
jordanlesich Jul 4, 2023
6ac654b
feed only veiwable for cast enabled DAOs
jordanlesich Jul 4, 2023
22384cb
add no casts found UI
jordanlesich Jul 4, 2023
8d3d5a2
remove extra API call, remove dumb fetch in serverside props
jordanlesich Jul 5, 2023
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 apps/web/package.json
Copy link
Collaborator

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 dep

Copy link
Collaborator Author

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.

Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"dependencies": {
"@ethersproject/abstract-provider": "^5.7.0",
"@ethersproject/providers": "^5.7.0",
"@farcaster/hub-nodejs": "^0.8.0",
"@fontsource/inter": "4.5.10",
"@fontsource/londrina-solid": "^4.5.9",
"@rainbow-me/rainbowkit": "^0.12.15",
Expand Down
8 changes: 8 additions & 0 deletions apps/web/src/constants/cacheTimes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,12 @@ export const CACHE_TIMES = {
maxAge: ONE_DAY,
swr: ONE_WEEK,
},
DAO_FEED: {
maxAge: ONE_MINUTE,
swr: ONE_HOUR,
},
CASTR_PROFILE: {
maxAge: ONE_DAY,
swr: ONE_WEEK,
},
}
4 changes: 4 additions & 0 deletions apps/web/src/constants/farcasterEnabled.ts
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]
1 change: 1 addition & 0 deletions apps/web/src/constants/swrKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const SWR_KEYS = {
TOKEN: 'token',
AUCTION: 'auction',
DAO_INFO: 'dao-info',
DAO_FEED: 'dao-feed',
TOKEN_IMAGE: 'token-image',
DYNAMIC: {
MY_DAOS(str: string) {
Expand Down
9 changes: 9 additions & 0 deletions apps/web/src/data/farcaster/client.ts
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
}
74 changes: 74 additions & 0 deletions apps/web/src/data/farcaster/queries/daoDiscussion.ts
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this we can check isErr and throw the error + return the result as a standard object instead of ok to remove dependence on neverthrow

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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)
}
}
50 changes: 50 additions & 0 deletions apps/web/src/data/farcaster/queries/farcasterProfile.ts.ts
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,
}
}
7 changes: 7 additions & 0 deletions apps/web/src/modules/dao/components/Feed/CardSkeleton.tsx
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" />
)
102 changes: 102 additions & 0 deletions apps/web/src/modules/dao/components/Feed/CastCard.tsx
Copy link
Collaborator

Choose a reason for hiding this comment

The 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>
)
}
29 changes: 29 additions & 0 deletions apps/web/src/modules/dao/components/Feed/DisplayPanel.tsx
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>
)
}
70 changes: 70 additions & 0 deletions apps/web/src/modules/dao/components/Feed/Feed.css.ts
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a way we can do these styles without !important @nadia ie overwrite the zord link styles?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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,
})
Loading
Loading