├── .gitignore ├── README.md ├── package.json ├── src ├── api.ts ├── codegen.yaml ├── components │ ├── Feed.tsx │ ├── Icons │ │ ├── CollectIcon.tsx │ │ ├── CommentIcon.tsx │ │ ├── FilledHeartIcon.tsx │ │ ├── MirrorIcon.tsx │ │ ├── SearchIcon.tsx │ │ └── UnfilledHeartIcon.tsx │ ├── LensProvider.tsx │ ├── Profile.tsx │ ├── ProfileHeader.tsx │ ├── ProfileListItem.tsx │ ├── Profiles.tsx │ ├── Publication.tsx │ ├── Search.tsx │ └── index.tsx ├── context.ts ├── graphql │ ├── common.graphql │ ├── doesFollow.graphql │ ├── exploreProfiles.graphql │ ├── explorePublications.graphql │ ├── generated.ts │ ├── getFollowing.graphql │ ├── getProfile.graphql │ ├── getPublication.graphql │ ├── getPublications.graphql │ ├── profile-feed.graphql │ ├── searchProfiles.graphql │ └── searchPublications.graphql ├── index.ts ├── types.ts └── utils.ts ├── tsconfig.json └── watcher.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | yarn.lock 4 | dist 5 | yarn-error.log 6 | .DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Native Lens UI Kit 🌿 (alpha) 2 | 3 | ![React Native Lens](https://arweave.net/3COKWStwD_qjTpZNRGUxKoiyjtyZFqXMyYF-Tekk7zo) 4 | 5 | A React Native UI kit for [Lens Protocol](https://lens.xyz/). Get started building with as little as 2 lines of code. Mix and match components to supercharge your mobile development workflow. 6 | 7 | Example app codebase located [here](https://github.com/dabit3/dabit3-react-native-lens-example) 8 | 9 | > You can also view a video tutorial of how to use the library [here](https://www.youtube.com/watch?v=qs6OE9kef6I) 10 | 11 | ## Getting started 🚀 12 | 13 | ### Prerequisites 14 | 15 | First, install and configure [React Native SVG](https://github.com/software-mansion/react-native-svg) and [React Native Video](https://github.com/react-native-video/react-native-video) into your Expo or bare React Native project. 16 | 17 | ### Installation 18 | 19 | ```sh 20 | npm install @lens-protocol/react-native-lens-ui-kit 21 | ``` 22 | 23 | # Components 🍃 24 | 25 | - [Feed](#feed) 26 | - [Profiles](#profiles) 27 | - [Profile](#profile) 28 | - [Profile Header](#profile-header) 29 | - [Publication](#publication) 30 | - [ProfileListItem](#profilelistitem) 31 | - [Search](#search) 32 | - [LensProvider](#lensprovider) 33 | 34 | ## Feed 35 | 36 | A feed of posts from Lens. 37 | 38 | ```tsx 39 | import { Feed } from '@lens-protocol/react-native-lens-ui-kit' 40 | 41 | 42 | ``` 43 | 44 | ### Options 45 | 46 | ``` 47 | profileId?: string 48 | publicationsQuery?: PublicationsQuery 49 | ListHeaderComponent?: React.FC 50 | ListFooterComponent?: React.FC 51 | signedInUser?: ProfileMetadata 52 | feed?: ExtendedPublication[] 53 | onCollectPress?: (publication: ExtendedPublication) => void 54 | onCommentPress?: (publication: ExtendedPublication) => void 55 | onMirrorPress?: (publication: ExtendedPublication) => void 56 | onLikePress?: (publication: ExtendedPublication) => void 57 | onProfileImagePress?: (publication: ExtendedPublication) => void 58 | hideLikes?: boolean 59 | hideComments?: boolean 60 | hideMirrors?: boolean 61 | hideCollects?: boolean 62 | iconColor?: string 63 | infiniteScroll?: boolean 64 | onEndReachedThreshold?: number 65 | styles?: FeedStyles 66 | publicationStyles?: PublicationStyles 67 | ``` 68 | 69 | ### Styles 70 | 71 | ``` 72 | styles = StyleSheet.create({ 73 | container: { 74 | flex: 1 75 | }, 76 | loadingIndicatorStyle : { 77 | marginVertical: 20 78 | }, 79 | noCommentsMessage: { 80 | margin: 20, 81 | fontSize: 14, 82 | fontWeight: '500' 83 | } 84 | }) 85 | ``` 86 | 87 | ### Query options for `Feed` 88 | 89 | __explorePublications (default)__ 90 | [explorePublications](./src/graphql/explorePublications.graphql) 91 | 92 | __getPublications__ 93 | [getPublications](./src/graphql/getPublications.graphql) 94 | 95 | __getComments__ 96 | [getPublications](./src/graphql/getPublications.graphql) 97 | 98 | ## Profiles 99 | 100 | A list of profiles 101 | 102 | ```tsx 103 | import { Profiles } from '@lens-protocol/react-native-lens-ui-kit' 104 | 105 | 106 | ``` 107 | 108 | ### Options 109 | 110 | ``` 111 | onFollowPress?: (profile: ExtendedProfile, profiles: ExtendedProfile[]) => void 112 | onProfilePress?: (profile: ExtendedProfile) => void 113 | profileData?: ExtendedProfile[] 114 | onEndReachedThreshold?: number 115 | infiniteScroll?: boolean 116 | profilesQuery?: ProfilesQuery 117 | signedInUserAddress?: string 118 | ``` 119 | 120 | ### Query options for `Profiles` 121 | 122 | __exploreProfiles (default)__ 123 | [exploreProfiles](./src/graphql/exploreProfiles.graphql) 124 | 125 | __getFollowing__ 126 | [getFollowing](./src/graphql/getFollowing.graphql) 127 | 128 | 129 | ## Profile 130 | 131 | Renders an individual profile 132 | 133 | ```tsx 134 | import { Profile } from '@lens-protocol/react-native-lens-ui-kit' 135 | 136 | 141 | ``` 142 | 143 | ### Options 144 | 145 | ``` 146 | profile: ExtendedProfile 147 | ListHeaderComponent?: React.FC 148 | ListFooterComponent?: React.FC 149 | feed?: Publication[] 150 | signedInUser?: ProfileMetadata 151 | hideLikes?: boolean 152 | hideComments?: boolean 153 | hideMirrors?: boolean 154 | hideCollects?: boolean 155 | iconColor?: string 156 | infiniteScroll?: boolean 157 | onEndReachedThreshold?: number 158 | headerStyles?: ProfileHeaderStyles 159 | feedStyles?: FeedStyles 160 | publicationStyles?: PublicationStyles 161 | publicationsQuery?: publicationsQuery 162 | onFollowingPress?: (profile: ExtendedProfile) => void 163 | onFollowersPress?: (profile: ExtendedProfile) => void 164 | onProfileImagePress?: (publication: ExtendedPublication) => void 165 | onCollectPress?: (publication: ExtendedPublication) => void 166 | onCommentPress?: (publication: ExtendedPublication) => void 167 | onMirrorPress?: (publication: ExtendedPublication) => void 168 | onLikePress?: (publication: ExtendedPublication) => void 169 | ``` 170 | 171 | ### Styles 172 | publicationStyles = [PublicationStyles](https://github.com/lens-protocol/react-native-lens-ui-kit/blob/main/src/types.ts#L137) 173 | headerStyles = [ProfileHeaderStyles](https://github.com/lens-protocol/react-native-lens-ui-kit/blob/main/src/types.ts#L157) 174 | feedStyles = [FeedStyles](https://github.com/lens-protocol/react-native-lens-ui-kit/blob/main/src/types.ts#L188) 175 | 176 | ## Profile Header 177 | 178 | Renders a profile header component. 179 | 180 | ```tsx 181 | import { ProfileHeader } from '@lens-protocol/react-native-lens-ui-kit' 182 | 183 | 188 | ``` 189 | 190 | ### Options 191 | 192 | ``` 193 | profileId?: number 194 | profile?: ExtendedProfile 195 | onFollowingPress?: (profile: ExtendedProfile) => void 196 | onFollowersPress?: (profile: ExtendedProfile) => void 197 | styles?: ProfileHeaderStyles 198 | ``` 199 | 200 | ### Styles 201 | [ProfileHeaderStyles](https://github.com/lens-protocol/react-native-lens-ui-kit/blob/main/src/types.ts#L157) 202 | 203 | ## Publication 204 | 205 | Renders an individual publication. 206 | 207 | ```tsx 208 | import { Publication } from '@lens-protocol/react-native-lens-ui-kit' 209 | 210 | 213 | ``` 214 | 215 | ### Options 216 | 217 | ``` 218 | publication: ExtendedPublication 219 | signedInUser?: ProfileMetadata 220 | hideLikes?: boolean 221 | hideComments?: boolean 222 | hideMirrors?: boolean 223 | hideCollects?: boolean 224 | iconColor?: string 225 | onCollectPress?: (publication: ExtendedPublication) => void 226 | onCommentPress?:(publication: ExtendedPublication) => void 227 | onMirrorPress?: (publication: ExtendedPublication) => void 228 | onLikePress?: (publication: ExtendedPublication) => void 229 | onProfileImagePress?: (publication: ExtendedPublication) => void 230 | styles?: PublicationStyles 231 | ``` 232 | 233 | 234 | ### Styles 235 | [PublicationStyles](https://github.com/lens-protocol/react-native-lens-ui-kit/blob/main/src/types.ts#L137) 236 | 237 | ## ProfileListItem 238 | 239 | Renders a list item for a profile overview. 240 | 241 | ```tsx 242 | import { ProfileListItem } from '@lens-protocol/react-native-lens-ui-kit' 243 | 244 | 247 | ``` 248 | 249 | ### Options 250 | 251 | ``` 252 | profile: ExtendedProfile 253 | onProfilePress?: (profile: ExtendedProfile) => void 254 | onFollowPress?: (profile: ExtendedProfile) => void 255 | isFollowing?: boolean 256 | styles?: ProfileListItemStyles 257 | ``` 258 | 259 | ### Styles 260 | [ProfileListItemStyles](https://github.com/lens-protocol/react-native-lens-ui-kit/blob/main/src/types.ts#L173) 261 | 262 | ## Search 263 | 264 | A component for searching profiles and publications. 265 | 266 | ### Options 267 | 268 | ``` 269 | searchType?: SearchType 270 | styles?: SearchStyles 271 | placeholder?: string 272 | autoCapitalize?: AutoCapitalizeOptions 273 | selectionColor?: string 274 | ListFooterComponent?: React.FC 275 | iconColor?: string 276 | profilesQuery?: ProfilesQuery 277 | publicationsQuery?: publicationsQuery 278 | infiniteScroll?: boolean 279 | onEndReachedThreshold?: number 280 | publicationStyles?: PublicationStyles 281 | signedInUser?: ProfileMetadata 282 | hideLikes?: any 283 | hideComments?: boolean 284 | hideMirrors?: boolean 285 | hideCollects?: boolean 286 | onCollectPress?: (publication: ExtendedPublication) => void 287 | onCommentPress?: (publication: ExtendedPublication) => void 288 | onMirrorPress?: (publication: ExtendedPublication) => void 289 | onLikePress?: (publication: ExtendedPublication) => void 290 | onProfileImagePress?: (publication: ExtendedPublication) => void 291 | onFollowPress?: (profile: ExtendedProfile, profiles: ExtendedProfile[]) => void 292 | onProfilePress?: (profile: ExtendedProfile) => void 293 | ``` 294 | 295 | ### Usage 296 | 297 | ```tsx 298 | import { Search } from '@lens-protocol/react-native-lens-ui-kit' 299 | 300 | 301 | 302 | // default is profile search, for publication search: 303 | import { Search, SearchType } from '@lens-protocol/react-native-lens-ui-kit' 304 | 305 | 308 | ``` 309 | 310 | ## LensProvider 311 | 312 | Allows you to pass global configurations to React Native Lens UI Kit. 313 | 314 | ### Options 315 | 316 | ``` 317 | environment? = 'testnet' | 'mainnet' (default) | 'sandbox' 318 | theme? = 'light' (default) | 'dark 319 | ``` 320 | 321 | ### Usage 322 | 323 | ```tsx 324 | import { 325 | LensProvider, 326 | Theme, 327 | Environment 328 | } from '@lens-protocol/react-native-lens-ui-kit' 329 | 330 | 334 | 335 | 336 | ``` 337 | 338 | # Roadmap 339 | 340 | Currently this project is in Alpha. 341 | 342 | ### Beta Roadmap 343 | 344 | - Custom styling / layout (temporary implementation in place, want to make it more granular) 345 | - More query options (easy contribution, help wanted) 346 | - Authentication 347 | - Custom components 348 | - Support video 349 | - Gallery view for Feed 350 | 351 | ### V1 Roadmap 352 | 353 | - Improved theme-ing 354 | - Better TypeScript support 355 | - Support audio 356 | 357 | ### Contribute 358 | 359 | To run and develop with the project locally, do the following: 360 | 361 | 1. Clone the repo: 362 | 363 | ```sh 364 | git clone git@github.com:lens-protocol/react-native-lens-ui-kit.git 365 | ``` 366 | 367 | 2. Install the dependencies 368 | 369 | ```sh 370 | npm install 371 | 372 | # or use yarn, pnpm, etc.. 373 | ``` 374 | 375 | 3. Open `watcher.js` and configure the directory of your React Native project (`destDir`). 376 | 377 | 4. Run the develop scripts: 378 | 379 | ```sh 380 | npm run dev 381 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@lens-protocol/react-native-lens-ui-kit", 3 | "version": "0.2.9", 4 | "description": "React Native UI Kit for Lens Protocol", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "generate": "graphql-codegen --config ./src/codegen.yaml", 9 | "dev": "concurrently npm:watch:*", 10 | "watch:ts": "npx tsc --watch", 11 | "watch:copy": "node watcher.js" 12 | }, 13 | "keywords": [ 14 | "Lens Protocol", 15 | "web3 social", 16 | "React Native" 17 | ], 18 | "author": "Nader Dabit", 19 | "dependencies": { 20 | "@graphql-codegen/fragment-matcher": "^3.3.1", 21 | "@types/react-native-video": "^5.0.15", 22 | "date-fns": "^2.29.3", 23 | "graphql": "^16.6.0", 24 | "react-native-media-console": "^2.0.7", 25 | "react-native-svg": "13.4.0", 26 | "react-native-video": "^5.2.1", 27 | "ts-node": "^10.9.1", 28 | "typescript": "^4.9.3", 29 | "urql": "^3.0.3" 30 | }, 31 | "license": "ISC", 32 | "devDependencies": { 33 | "@graphql-codegen/cli": "^2.15.0", 34 | "@graphql-codegen/typed-document-node": "^2.3.8", 35 | "@graphql-codegen/typescript": "^2.8.3", 36 | "@graphql-codegen/typescript-operations": "^2.5.8", 37 | "@types/date-fns": "^2.6.0", 38 | "@types/react": "^18.0.26", 39 | "@types/react-native": "^0.70.7", 40 | "concurrently": "^7.6.0", 41 | "react-native": "0.70.6" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/api.ts: -------------------------------------------------------------------------------- 1 | import { createClient as createUrqlClient } from 'urql' 2 | import { Environment } from './types' 3 | 4 | const environments = { 5 | testnet: 'https://api-mumbai.lens.dev', 6 | sandbox: 'https://api-sandbox-mumbai.lens.dev', 7 | mainnet: 'https://api.lens.dev' 8 | } 9 | 10 | /* creates the API client */ 11 | export function createClient(env:Environment) { 12 | const APIURL = environments[env] || environments.mainnet 13 | return createUrqlClient({ 14 | url: APIURL 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /src/codegen.yaml: -------------------------------------------------------------------------------- 1 | 2 | 3 | schema: 'https://api.lens.dev' 4 | documents: './src/graphql/*.graphql' 5 | generates: 6 | ./src/graphql/generated.ts: 7 | plugins: 8 | - typescript 9 | - typescript-operations 10 | - typed-document-node 11 | - fragment-matcher 12 | config: 13 | fetcher: fetch 14 | dedupeFragments: true -------------------------------------------------------------------------------- /src/components/Feed.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useContext } from 'react' 2 | import { 3 | View, 4 | FlatList, 5 | Text, 6 | StyleSheet, 7 | ActivityIndicator 8 | } from 'react-native' 9 | import { createClient } from '../api' 10 | import { 11 | ProfileMetadata, 12 | PublicationsQuery, 13 | ExtendedPublication, 14 | PublicationStyles, 15 | FeedStyles, 16 | PublicationFetchResults, 17 | LensContextType 18 | } from '../types' 19 | import { 20 | configureMirrorAndIpfsUrl, 21 | filterMimeTypes 22 | } from '../utils' 23 | import { Publication as PublicationComponent } from './' 24 | import { 25 | ExplorePublicationsDocument, 26 | PublicationsDocument, 27 | ProfileFeedDocument, 28 | ProfileDocument, 29 | PaginatedResultInfo, 30 | PublicationTypes, 31 | PublicationSortCriteria, 32 | PaginatedFeedResult, 33 | FeedItemRoot 34 | } from '../graphql/generated' 35 | import { LensContext } from '../context' 36 | 37 | export function Feed({ 38 | profileId, 39 | publicationsQuery = { 40 | name: "explorePublications", 41 | publicationTypes: [PublicationTypes.Post, PublicationTypes.Comment, PublicationTypes.Mirror], 42 | publicationSortCriteria: PublicationSortCriteria.Latest, 43 | limit: 20 44 | }, 45 | ListHeaderComponent, 46 | ListFooterComponent, 47 | feed, 48 | signedInUser, 49 | hideLikes = false, 50 | hideComments = false, 51 | hideMirrors = false, 52 | hideCollects = false, 53 | iconColor, 54 | infiniteScroll = true, 55 | onEndReachedThreshold = .025, 56 | onCollectPress = publication => console.log({ publication }), 57 | onCommentPress = publication => console.log({ publication }), 58 | onMirrorPress = publication => console.log({ publication }), 59 | onLikePress = publication => console.log({ publication }), 60 | onProfileImagePress = publication => console.log({ publication }), 61 | publicationStyles, 62 | styles = baseStyles, 63 | }: { 64 | profileId?: string, 65 | publicationsQuery?: PublicationsQuery, 66 | ListHeaderComponent?: React.FC, 67 | ListFooterComponent?: React.FC, 68 | signedInUser?: ProfileMetadata 69 | feed?: ExtendedPublication[], 70 | onCollectPress?: (publication: ExtendedPublication) => void, 71 | onCommentPress?: (publication: ExtendedPublication) => void, 72 | onMirrorPress?: (publication: ExtendedPublication) => void, 73 | onLikePress?: (publication: ExtendedPublication) => void, 74 | onProfileImagePress?: (publication: ExtendedPublication) => void, 75 | hideLikes?: boolean, 76 | hideComments?: boolean, 77 | hideMirrors?: boolean, 78 | hideCollects?: boolean, 79 | iconColor?: string, 80 | infiniteScroll?: boolean, 81 | onEndReachedThreshold?: number, 82 | styles?: FeedStyles, 83 | publicationStyles?: PublicationStyles 84 | }) { 85 | const [publications, setPublications] = useState([]) 86 | const [paginationInfo, setPaginationInfo] = useState() 87 | const [loading, setLoading] = useState(false) 88 | const [canPaginate, setCanPaginate] = useState(true) 89 | 90 | const { environment, IPFSGateway } = useContext(LensContext) 91 | const client = createClient(environment) 92 | 93 | useEffect(() => { 94 | fetchPublications() 95 | }, []) 96 | 97 | async function fetchResponse(cursor?: string) { 98 | if (profileId) { 99 | try { 100 | let { data } = await client.query(ProfileFeedDocument, { 101 | request: { 102 | cursor, 103 | profileId, 104 | limit: 50 105 | } 106 | }).toPromise() 107 | if (data) { 108 | const { feed } = data 109 | let { 110 | pageInfo, 111 | items 112 | } = feed as PaginatedFeedResult 113 | const rootItems = items.map(item => item.root) 114 | return { 115 | pageInfo, items: rootItems 116 | } 117 | } 118 | } catch (err) { 119 | console.log('error fetching feed... ', err) 120 | } 121 | } 122 | if (publicationsQuery.name === 'explorePublications') { 123 | try { 124 | let { data } = await client.query(ExplorePublicationsDocument, { 125 | request: { 126 | cursor, 127 | publicationTypes: publicationsQuery.publicationTypes, 128 | sortCriteria: publicationsQuery.publicationSortCriteria || PublicationSortCriteria.Latest, 129 | limit: publicationsQuery.limit 130 | } 131 | }).toPromise() 132 | if (data) { 133 | const { explorePublications } = data 134 | let { 135 | pageInfo, 136 | items 137 | } = explorePublications 138 | return { 139 | pageInfo, items 140 | } as PublicationFetchResults 141 | } 142 | } catch (err) { 143 | console.log('Error fetching explorePublications: ', err) 144 | } 145 | } 146 | if (publicationsQuery.name === 'getPublications') { 147 | let data 148 | let id 149 | if (publicationsQuery.handle) { 150 | let handle = publicationsQuery.handle 151 | if (!handle.includes('.lens')) { 152 | handle = handle + '.lens' 153 | } 154 | const { data: profileData } = await client.query(ProfileDocument, { 155 | request: { handle } 156 | }).toPromise() 157 | id = profileData?.profile?.id 158 | } else { 159 | id = publicationsQuery.profileId 160 | } 161 | let { data: publicationsData } = await client.query(PublicationsDocument, { 162 | request: { 163 | profileId: id, 164 | cursor, 165 | publicationTypes: publicationsQuery.publicationTypes 166 | } 167 | }).toPromise() 168 | data = publicationsData 169 | if (data) { 170 | const { publications: { pageInfo, items }} = data 171 | return { 172 | pageInfo, items 173 | } as PublicationFetchResults 174 | } 175 | } 176 | if (publicationsQuery.name === 'getComments') { 177 | try { 178 | let { data } = await client.query(PublicationsDocument, { 179 | request: { 180 | commentsOf: publicationsQuery.publicationId, 181 | cursor 182 | } 183 | }).toPromise() 184 | if (data) { 185 | const { publications: { pageInfo, items }} = data 186 | return { 187 | pageInfo, items 188 | } as PublicationFetchResults 189 | } 190 | } catch (err) { 191 | console.log('error fetching comments...', err) 192 | } 193 | } 194 | throw Error('No query defined...') 195 | } 196 | 197 | async function fetchNextItems() { 198 | try { 199 | if (canPaginate && paginationInfo) { 200 | const { next } = paginationInfo 201 | if (!next) { 202 | setCanPaginate(false) 203 | } else { 204 | fetchPublications(next) 205 | } 206 | } 207 | } catch (err) { 208 | console.log('Error fetching next items:', err) 209 | } 210 | } 211 | 212 | async function fetchPublications(cursor?: string) { 213 | try { 214 | if ( 215 | !feed || 216 | feed && cursor 217 | ) { 218 | setLoading(true) 219 | let { 220 | items, 221 | pageInfo 222 | } = await fetchResponse(cursor) 223 | setPaginationInfo(pageInfo) 224 | items = filterMimeTypes(items) 225 | items = configureMirrorAndIpfsUrl(items, IPFSGateway) 226 | if (cursor) { 227 | let newData = [...publications, ...items] 228 | if (publicationsQuery.publicationSortCriteria === "LATEST") { 229 | newData = [...new Map(newData.map(m => [m.id, m])).values()] 230 | } 231 | setPublications(newData) 232 | } else { 233 | setPublications(items) 234 | } 235 | setLoading(false) 236 | } else { 237 | setPublications(feed) 238 | } 239 | } catch (err) { 240 | console.log('error fetching publications...', err) 241 | } 242 | } 243 | 244 | function onEndReached() { 245 | if (infiniteScroll) { 246 | fetchNextItems() 247 | } 248 | } 249 | 250 | function renderItem({ 251 | item 252 | } : { 253 | item: ExtendedPublication, 254 | index: number 255 | }) { 256 | return ( 257 | 273 | ) 274 | } 275 | 276 | let initialNumToRender = 25 277 | if (publicationsQuery) { 278 | initialNumToRender = publicationsQuery.limit || 25 279 | } 280 | 281 | return ( 282 | 283 | { 284 | !loading && 285 | publications.length === Number(0) && 286 | publicationsQuery.name === 'getComments' && ( 287 | No comments... 288 | ) 289 | } 290 | String(index)} 296 | onEndReachedThreshold={onEndReachedThreshold} 297 | initialNumToRender={initialNumToRender} 298 | ListFooterComponent={ 299 | ListFooterComponent ? 300 | ListFooterComponent : 301 | loading ? ( 302 | 305 | ) : null 306 | } 307 | /> 308 | 309 | ) 310 | } 311 | 312 | let baseStyles = StyleSheet.create({ 313 | container: { 314 | flex: 1 315 | }, 316 | loadingIndicatorStyle : { 317 | marginVertical: 20 318 | }, 319 | noCommentsMessage: { 320 | margin: 20, 321 | fontSize: 14, 322 | fontWeight: '500' 323 | } 324 | }) -------------------------------------------------------------------------------- /src/components/Icons/CollectIcon.tsx: -------------------------------------------------------------------------------- 1 | import Svg, { Path, G } from 'react-native-svg' 2 | import { TouchableHighlight } from 'react-native' 3 | 4 | export function CollectIcon({ 5 | iconSize = "16", 6 | color = "black", 7 | onPress = () => {}, 8 | width = 16 9 | 10 | }) { 11 | return ( 12 | 17 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | } -------------------------------------------------------------------------------- /src/components/Icons/CommentIcon.tsx: -------------------------------------------------------------------------------- 1 | import Svg, { Path, G } from 'react-native-svg' 2 | import { TouchableHighlight } from 'react-native' 3 | 4 | export function CommentIcon({ 5 | iconSize = "16", 6 | color = "black", 7 | onPress = () => {}, 8 | width = 16 9 | }) { 10 | return ( 11 | 16 | 19 | 20 | 21 | 22 | ) 23 | } -------------------------------------------------------------------------------- /src/components/Icons/FilledHeartIcon.tsx: -------------------------------------------------------------------------------- 1 | import Svg, { Path } from 'react-native-svg' 2 | import { TouchableHighlight } from 'react-native' 3 | 4 | export function FilledHeartIcon({ 5 | iconSize = "16", 6 | color = "black", 7 | onPress = () => {}, 8 | width = 16 9 | }) { 10 | return ( 11 | 16 | 19 | 20 | 21 | 22 | ) 23 | } -------------------------------------------------------------------------------- /src/components/Icons/MirrorIcon.tsx: -------------------------------------------------------------------------------- 1 | import Svg, { Path, G } from 'react-native-svg' 2 | import { TouchableHighlight } from 'react-native' 3 | 4 | export function MirrorIcon({ 5 | iconSize = "16", 6 | color = "black", 7 | onPress = () => {}, 8 | width = 16 9 | }) { 10 | return ( 11 | 16 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ) 26 | } -------------------------------------------------------------------------------- /src/components/Icons/SearchIcon.tsx: -------------------------------------------------------------------------------- 1 | import Svg, { Path, G } from 'react-native-svg' 2 | import { TouchableHighlight } from 'react-native' 3 | 4 | export function SearchIcon({ 5 | iconSize = "16", 6 | color = "#b0b0b0", 7 | onPress = () => {}, 8 | width = 16 9 | 10 | }) { 11 | return ( 12 | 17 | 20 | 21 | 22 | 23 | 24 | 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /src/components/Icons/UnfilledHeartIcon.tsx: -------------------------------------------------------------------------------- 1 | import Svg, { Path } from 'react-native-svg' 2 | import { TouchableHighlight } from 'react-native' 3 | 4 | export function UnfilledHeartIcon({ 5 | iconSize = "16", 6 | color = "black", 7 | onPress = () => {}, 8 | width = 16 9 | }) { 10 | return ( 11 | 16 | 19 | 20 | 21 | 22 | ) 23 | } -------------------------------------------------------------------------------- /src/components/LensProvider.tsx: -------------------------------------------------------------------------------- 1 | import { LensContext } from '../context' 2 | import { Theme, Environment } from '../types' 3 | 4 | export function LensProvider({ 5 | children, 6 | theme = Theme.light, 7 | environment = Environment.mainnet, 8 | IPFSGateway = 'https://cloudflare-ipfs.com/ipfs' 9 | }: { 10 | children: React.ReactNode, 11 | theme?: Theme, 12 | environment?: Environment, 13 | IPFSGateway?: string 14 | }) { 15 | return ( 16 | 22 | {children} 23 | 24 | ) 25 | } -------------------------------------------------------------------------------- /src/components/Profile.tsx: -------------------------------------------------------------------------------- 1 | import { Feed, ProfileHeader } from './' 2 | import { PublicationTypes, Publication } from '../graphql/generated' 3 | import { 4 | ProfileHeaderStyles, 5 | FeedStyles, 6 | PublicationStyles, 7 | PublicationsQuery, 8 | ExtendedProfile, 9 | ExtendedPublication, 10 | ProfileMetadata 11 | } from '../types' 12 | 13 | export function Profile({ 14 | profile, 15 | profileId, 16 | handle, 17 | ListHeaderComponent, 18 | ListFooterComponent, 19 | feed, 20 | signedInUser, 21 | hideLikes = false, 22 | hideComments = false, 23 | hideMirrors = false, 24 | hideCollects = false, 25 | iconColor, 26 | infiniteScroll = true, 27 | onEndReachedThreshold = .65, 28 | headerStyles, 29 | feedStyles, 30 | publicationStyles, 31 | publicationsQuery = { 32 | name: "getPublications", 33 | handle, 34 | profileId: profile?.id || profileId, 35 | publicationTypes: [PublicationTypes.Post, PublicationTypes.Mirror] 36 | }, 37 | onFollowingPress = profile => console.log({ profile }), 38 | onFollowersPress = profile => console.log({ profile }), 39 | onProfileImagePress = publication => console.log({ publication }), 40 | onCollectPress = publication => console.log({ publication }), 41 | onCommentPress = publication => console.log({ publication }), 42 | onMirrorPress = publication => console.log({ publication }), 43 | onLikePress = publication => console.log({ publication }), 44 | } : { 45 | profile?: ExtendedProfile, 46 | profileId?: string, 47 | handle?: string, 48 | ListHeaderComponent?: React.FC, 49 | ListFooterComponent?: React.FC, 50 | feed?: Publication[], 51 | signedInUser?: ProfileMetadata, 52 | hideLikes?: boolean, 53 | hideComments?: boolean, 54 | hideMirrors?: boolean, 55 | hideCollects?: boolean, 56 | iconColor?: string, 57 | infiniteScroll?: boolean, 58 | onEndReachedThreshold?: number, 59 | headerStyles?: ProfileHeaderStyles, 60 | feedStyles?: FeedStyles, 61 | publicationStyles?: PublicationStyles, 62 | publicationsQuery?: PublicationsQuery, 63 | onFollowingPress?: (profile: ExtendedProfile) => void, 64 | onFollowersPress?: (profile: ExtendedProfile) => void, 65 | onProfileImagePress?: (publication: ExtendedPublication) => void, 66 | onCollectPress?: (publication: ExtendedPublication) => void, 67 | onCommentPress?: (publication: ExtendedPublication) => void, 68 | onMirrorPress?: (publication: ExtendedPublication) => void, 69 | onLikePress?: (publication: ExtendedPublication) => void, 70 | }) { 71 | const HeaderComponent = ListHeaderComponent ? 72 | ListHeaderComponent : ( 73 | () => 81 | ) 82 | return ( 83 | 104 | ) 105 | } 106 | -------------------------------------------------------------------------------- /src/components/ProfileHeader.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useContext } from "react" 2 | import { 3 | View, 4 | TouchableHighlight, 5 | StyleSheet, 6 | Image, 7 | Text 8 | } from "react-native" 9 | import { createClient } from "../api" 10 | import { ProfileDocument } from "../graphql/generated" 11 | import { 12 | ExtendedProfile, 13 | ProfileHeaderStyles, 14 | LensContextType, 15 | ThemeColors, 16 | Theme, 17 | } from "../types" 18 | import { LensContext } from "../context" 19 | 20 | export function ProfileHeader({ 21 | profileId, 22 | profile: user, 23 | handle, 24 | isFollowing, 25 | onFollowPress = (profile) => console.log({ profile }), 26 | onFollowingPress = (profile) => console.log({ profile }), 27 | onFollowersPress = (profile) => console.log({ profile }), 28 | styles = baseStyles, 29 | }: { 30 | profileId?: string; 31 | profile?: ExtendedProfile; 32 | handle?: string, 33 | isFollowing?: boolean, 34 | onFollowPress?: (profile: ExtendedProfile) => void; 35 | onFollowingPress?: (profile: ExtendedProfile) => void; 36 | onFollowersPress?: (profile: ExtendedProfile) => void; 37 | styles?: ProfileHeaderStyles; 38 | }) { 39 | const [fetchedProfile, setFetchedProfile] = useState(null) 40 | const { environment, theme, IPFSGateway } = useContext(LensContext) 41 | const client = createClient(environment) 42 | if (theme === "dark") { 43 | styles = darkThemeStyles 44 | } 45 | useEffect(() => { 46 | if (!profile) { 47 | fetchProfile() 48 | } 49 | }) 50 | 51 | async function fetchProfile() { 52 | try { 53 | if (profileId) { 54 | const { data } = await client 55 | .query(ProfileDocument, { 56 | request: { 57 | profileId, 58 | }, 59 | }) 60 | .toPromise() 61 | if (data) { 62 | const { profile: userProfile } = data 63 | setFetchedProfile(userProfile) 64 | } 65 | } 66 | if (handle) { 67 | if (!handle.includes('.lens')) { 68 | handle = handle + '.lens' 69 | } 70 | const { data } = await client 71 | .query(ProfileDocument, { 72 | request: { 73 | handle, 74 | } 75 | }) 76 | .toPromise() 77 | if (data) { 78 | const { profile: userProfile } = data 79 | setFetchedProfile(userProfile) 80 | } 81 | } 82 | } catch (err) { 83 | console.log("error fetching profile: ", err) 84 | } 85 | } 86 | if (!user && !fetchedProfile) return null 87 | const profile = user || fetchedProfile 88 | let { picture, coverPicture } = profile 89 | if (picture?.uri) { 90 | picture.original = { 91 | url: picture.uri, 92 | } 93 | } else if (picture?.original) { 94 | if (picture.original.url.startsWith("ipfs://")) { 95 | let result = picture.original.url.substring( 96 | 7, 97 | picture.original.url.length 98 | ) 99 | profile.picture.original.url = `${IPFSGateway}/${result}` 100 | } 101 | if (picture.original.url.startsWith("ar://")) { 102 | let result = picture.original.url.substring( 103 | 5, 104 | picture.original.url.length 105 | ) 106 | profile.picture.original.url = `${IPFSGateway}/${result}` 107 | } 108 | } else { 109 | profile.missingAvatar = true 110 | } 111 | if (coverPicture?.original.url) { 112 | if (coverPicture.original.url.startsWith("ipfs://")) { 113 | let hash = coverPicture.original.url.substring( 114 | 7, 115 | coverPicture.original.url.length 116 | ) 117 | profile.coverPicture.original.url = `${IPFSGateway}/${hash}` 118 | } 119 | if (coverPicture.original.url.startsWith("ar://")) { 120 | let result = coverPicture.original.url.substring( 121 | 5, 122 | coverPicture.original.url.length 123 | ) 124 | profile.coverPicture.original.url = `https://arweave.net/${result}` 125 | } 126 | } else { 127 | profile.missingCover = true 128 | } 129 | 130 | return ( 131 | 132 | {profile.missingCover ? ( 133 | 134 | ) : ( 135 | 139 | )} 140 | {profile.missingAvatar ? ( 141 | 142 | ) : ( 143 | 147 | )} 148 | { 149 | renderFollowButton(isFollowing, theme) 150 | } 151 | 152 | {profile.name} 153 | @{profile.handle} 154 | {profile.bio} 155 | 156 | onFollowingPress(profile)} 158 | underlayColor="transparent" 159 | > 160 | 161 | 162 | {profile.stats.totalFollowing} 163 | 164 | Following 165 | 166 | 167 | onFollowersPress(profile)} 169 | underlayColor="transparent" 170 | > 171 | 172 | 173 | {profile.stats.totalFollowers} 174 | 175 | Followers 176 | 177 | 178 | 179 | 180 | 181 | ) 182 | 183 | function renderFollowButton(isFollowing?: boolean, theme?: Theme) { 184 | if (!isFollowing) { 185 | return ( 186 | onFollowPress(profile)} 189 | style={styles.followButton} 190 | > 191 | 194 | Follow 195 | 196 | 197 | ) 198 | } else { 199 | return ( 200 | onFollowPress(profile)} 203 | style={styles.followingButton} 204 | > 205 | 208 | Following 209 | 210 | 211 | ) 212 | } 213 | } 214 | } 215 | 216 | const baseStyles = StyleSheet.create({ 217 | container: { 218 | borderBottomWidth: 1, 219 | borderBottomColor: 'rgba(0, 0, 0, .045)', 220 | paddingBottom: 3 221 | }, 222 | blankHeader: { 223 | height: 120, 224 | backgroundColor: "black", 225 | }, 226 | headerImage: { 227 | width: "100%", 228 | height: 120, 229 | }, 230 | avatar: { 231 | width: 100, 232 | height: 100, 233 | borderRadius: 50, 234 | marginTop: -50, 235 | marginLeft: 25, 236 | }, 237 | followButton: { 238 | backgroundColor: ThemeColors.black, 239 | width: 110, 240 | position: 'absolute', 241 | top: 128, 242 | right: 20, 243 | alignItems: 'center', 244 | justifyContent: 'center', 245 | paddingVertical: 8, 246 | borderRadius: 30 247 | }, 248 | followButtonText: { 249 | color: ThemeColors.white, 250 | fontWeight: '600' 251 | }, 252 | followingButton: { 253 | borderWidth: 1, 254 | borderColor: ThemeColors.black, 255 | width: 110, 256 | position: 'absolute', 257 | top: 128, 258 | right: 20, 259 | alignItems: 'center', 260 | justifyContent: 'center', 261 | paddingVertical: 8, 262 | borderRadius: 30 263 | }, 264 | followingButtonText: { 265 | color: ThemeColors.black 266 | }, 267 | userDetails: { 268 | paddingHorizontal: 25, 269 | paddingVertical: 10, 270 | }, 271 | name: { 272 | fontWeight: "600", 273 | fontSize: 20, 274 | }, 275 | handle: { 276 | fontSize: 14, 277 | }, 278 | bio: { 279 | marginTop: 10, 280 | color: "rgba(0, 0, 0, .5)", 281 | }, 282 | profileStats: { 283 | flexDirection: "row", 284 | marginTop: 15, 285 | }, 286 | statsData: { 287 | fontWeight: "600", 288 | fontSize: 16, 289 | }, 290 | statsHeader: { 291 | marginLeft: 3, 292 | opacity: 0.7, 293 | }, 294 | profileFollowingData: { 295 | flexDirection: "row", 296 | alignItems: "center", 297 | }, 298 | profileFollowerData: { 299 | marginLeft: 15, 300 | flexDirection: "row", 301 | alignItems: "center", 302 | }, 303 | }) 304 | 305 | const darkThemeStyles = StyleSheet.create({ 306 | container: { 307 | backgroundColor: ThemeColors.black, 308 | paddingBottom: 10, 309 | borderBottomWidth: 1, 310 | borderBottomColor: 'rgba(255, 255, 255, .05)', 311 | }, 312 | blankHeader: { 313 | height: 120, 314 | backgroundColor: "black", 315 | }, 316 | headerImage: { 317 | width: "100%", 318 | height: 120, 319 | }, 320 | avatar: { 321 | width: 100, 322 | height: 100, 323 | borderRadius: 50, 324 | marginTop: -55, 325 | marginLeft: 25, 326 | }, 327 | followButton: { 328 | backgroundColor: 'rgba(255, 255, 255, 1)', 329 | width: 110, 330 | position: 'absolute', 331 | top: 128, 332 | right: 20, 333 | alignItems: 'center', 334 | justifyContent: 'center', 335 | paddingVertical: 8, 336 | borderRadius: 30 337 | }, 338 | followButtonText: { 339 | color: ThemeColors.black, 340 | fontWeight: '600' 341 | }, 342 | followingButton: { 343 | borderWidth: 1, 344 | borderColor: 'rgba(255, 255, 255, .2)', 345 | width: 110, 346 | position: 'absolute', 347 | top: 128, 348 | right: 20, 349 | alignItems: 'center', 350 | justifyContent: 'center', 351 | paddingVertical: 8, 352 | borderRadius: 30 353 | }, 354 | followingButtonText: { 355 | color: ThemeColors.white 356 | }, 357 | userDetails: { 358 | paddingHorizontal: 25, 359 | paddingVertical: 10, 360 | }, 361 | name: { 362 | fontWeight: "600", 363 | fontSize: 20, 364 | color: ThemeColors.white, 365 | }, 366 | handle: { 367 | fontSize: 14, 368 | color: ThemeColors.lightGray, 369 | }, 370 | bio: { 371 | marginTop: 10, 372 | color: "white", 373 | }, 374 | profileStats: { 375 | flexDirection: "row", 376 | marginTop: 15, 377 | }, 378 | statsData: { 379 | fontWeight: "600", 380 | fontSize: 16, 381 | color: ThemeColors.lightGray, 382 | }, 383 | statsHeader: { 384 | marginLeft: 3, 385 | color: ThemeColors.white, 386 | }, 387 | profileFollowingData: { 388 | flexDirection: "row", 389 | alignItems: "center", 390 | }, 391 | profileFollowerData: { 392 | marginLeft: 15, 393 | flexDirection: "row", 394 | alignItems: "center", 395 | }, 396 | }) 397 | -------------------------------------------------------------------------------- /src/components/ProfileListItem.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | TouchableHighlight, 3 | View, 4 | Image, 5 | Text, 6 | StyleSheet, 7 | } from "react-native"; 8 | import { useContext } from "react"; 9 | import { 10 | ProfileListItemStyles, 11 | ExtendedProfile, 12 | ThemeColors, 13 | LensContextType, 14 | } from "../types"; 15 | import { LensContext } from "../context"; 16 | 17 | export function ProfileListItem({ 18 | profile, 19 | onProfilePress = (profile) => console.log({ profile }), 20 | onFollowPress = (profile) => console.log({ profile }), 21 | isFollowing, 22 | styles = baseStyles, 23 | }: { 24 | profile: ExtendedProfile; 25 | onProfilePress?: (profile: ExtendedProfile) => void; 26 | onFollowPress?: (profile: ExtendedProfile) => void; 27 | isFollowing?: boolean; 28 | styles?: ProfileListItemStyles; 29 | }) { 30 | const { theme } = useContext(LensContext); 31 | if (theme === "dark") { 32 | styles = darkThemeStyles; 33 | } 34 | function renderFollowButton(isFollowing: boolean = false) { 35 | if (isFollowing) { 36 | return ( 37 | 38 | Following 39 | 40 | ); 41 | } else { 42 | return ( 43 | 44 | Follow 45 | 46 | ); 47 | } 48 | } 49 | 50 | return ( 51 | onProfilePress(profile)} 54 | underlayColor="transparent" 55 | > 56 | 57 | 58 | 67 | 68 | 69 | 70 | {profile.name || profile.handle} 71 | 72 | @{profile.handle} 73 | 74 | {profile.bio && profile.bio.substring(0, 150)} 75 | 76 | 77 | 78 | onFollowPress(profile)} 81 | activeOpacity={0.6} 82 | > 83 | {renderFollowButton(isFollowing)} 84 | 85 | 86 | 87 | 88 | ); 89 | } 90 | 91 | const baseStyles = StyleSheet.create({ 92 | container: { 93 | flexDirection: "row", 94 | paddingLeft: 15, 95 | paddingVertical: 10, 96 | borderBottomWidth: 1, 97 | borderBottomColor: "rgba(0, 0, 0, .06)", 98 | }, 99 | avatarContainer: { 100 | padding: 5, 101 | }, 102 | avatar: { 103 | width: 44, 104 | height: 44, 105 | borderRadius: 22, 106 | }, 107 | profileName: { 108 | fontWeight: "600", 109 | fontSize: 16, 110 | maxWidth: 200, 111 | }, 112 | profileHandle: { 113 | marginTop: 3, 114 | maxWidth: 200, 115 | }, 116 | profileBio: { 117 | maxWidth: 200, 118 | marginTop: 15, 119 | color: "rgba(0, 0, 0, .5)", 120 | }, 121 | infoContainer: { 122 | justifyContent: "center", 123 | paddingLeft: 10, 124 | maxWidth: 200, 125 | }, 126 | followButtonContainer: { 127 | flex: 1, 128 | alignItems: "flex-end", 129 | paddingRight: 20, 130 | }, 131 | followButton: { 132 | borderWidth: 1, 133 | borderRadius: 34, 134 | paddingHorizontal: 17, 135 | paddingVertical: 7, 136 | marginTop: 3, 137 | backgroundColor: "black", 138 | }, 139 | followingButton: { 140 | borderWidth: 1, 141 | borderRadius: 34, 142 | paddingHorizontal: 17, 143 | paddingVertical: 7, 144 | marginTop: 3, 145 | }, 146 | followButtonText: { 147 | fontSize: 12, 148 | fontWeight: "700", 149 | color: "white", 150 | }, 151 | followingButtonText: { 152 | fontSize: 12, 153 | fontWeight: "700", 154 | color: "black", 155 | }, 156 | }); 157 | 158 | const darkThemeStyles = StyleSheet.create({ 159 | container: { 160 | flexDirection: "row", 161 | paddingLeft: 15, 162 | paddingVertical: 12, 163 | backgroundColor: ThemeColors.black, 164 | borderBottomColor: ThemeColors.clearWhite, 165 | borderBottomWidth: 1, 166 | }, 167 | avatarContainer: { 168 | padding: 5, 169 | }, 170 | avatar: { 171 | width: 44, 172 | height: 44, 173 | borderRadius: 22, 174 | }, 175 | profileName: { 176 | fontWeight: "600", 177 | fontSize: 16, 178 | maxWidth: 200, 179 | color: ThemeColors.white, 180 | }, 181 | profileHandle: { 182 | marginTop: 3, 183 | maxWidth: 200, 184 | color: ThemeColors.lightGray, 185 | }, 186 | profileBio: { 187 | maxWidth: 200, 188 | marginTop: 15, 189 | color: ThemeColors.white, 190 | }, 191 | infoContainer: { 192 | justifyContent: "center", 193 | paddingLeft: 10, 194 | maxWidth: 200, 195 | }, 196 | followButtonContainer: { 197 | flex: 1, 198 | alignItems: "flex-end", 199 | paddingRight: 20, 200 | }, 201 | followButton: { 202 | borderWidth: 1, 203 | borderRadius: 34, 204 | paddingHorizontal: 17, 205 | paddingVertical: 7, 206 | marginTop: 3, 207 | backgroundColor: ThemeColors.white, 208 | color: ThemeColors.black, 209 | }, 210 | followingButton: { 211 | borderWidth: 1, 212 | borderRadius: 34, 213 | paddingHorizontal: 17, 214 | paddingVertical: 7, 215 | marginTop: 3, 216 | borderColor: ThemeColors.white, 217 | }, 218 | followButtonText: { 219 | fontSize: 12, 220 | fontWeight: "700", 221 | color: ThemeColors.black, 222 | }, 223 | followingButtonText: { 224 | fontSize: 12, 225 | fontWeight: "700", 226 | color: ThemeColors.white, 227 | }, 228 | }); 229 | -------------------------------------------------------------------------------- /src/components/Profiles.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useContext } from 'react' 2 | import { 3 | FlatList, ActivityIndicator, StyleSheet 4 | } from 'react-native' 5 | import { createClient } from '../api' 6 | import { 7 | ProfilesQuery, 8 | ExtendedProfile, 9 | LensContextType 10 | } from '../types' 11 | import { 12 | Profile, 13 | ExploreProfilesDocument, 14 | FollowingDocument, 15 | ProfileSortCriteria, 16 | PaginatedResultInfo, 17 | DoesFollowDocument 18 | } from '../graphql/generated' 19 | import { 20 | ProfileListItem 21 | } from './' 22 | import { LensContext } from '../context' 23 | import { formatProfilePictures } from '../utils' 24 | 25 | export function Profiles({ 26 | onFollowPress = profile => console.log({ profile }), 27 | onProfilePress = profile => console.log({ profile }), 28 | profileData, 29 | onEndReachedThreshold = .7, 30 | infiniteScroll = true, 31 | signedInUserAddress, 32 | profilesQuery = { 33 | name: 'exploreProfiles', 34 | profileSortCriteria: ProfileSortCriteria.MostFollowers, 35 | limit: 25 36 | } 37 | } : { 38 | onFollowPress?: (profile: ExtendedProfile, profiles: ExtendedProfile[]) => void, 39 | onProfilePress?: (profile: ExtendedProfile) => void, 40 | profileData?: ExtendedProfile[], 41 | onEndReachedThreshold?: number, 42 | infiniteScroll?: boolean, 43 | profilesQuery?: ProfilesQuery, 44 | signedInUserAddress?: string 45 | }) { 46 | const [profiles, setProfiles] = useState([]) 47 | const [loading, setLoading] = useState(true) 48 | const [canPaginate, setCanPaginate] = useState(true) 49 | const [paginationInfo, setPaginationInfo] = useState() 50 | const { environment, IPFSGateway } = useContext(LensContext) 51 | const client = createClient(environment) 52 | 53 | useEffect(() => { 54 | fetchProfiles() 55 | }, []) 56 | 57 | async function fetchResponse(cursor = null) { 58 | if (profilesQuery.name === 'exploreProfiles') { 59 | try { 60 | let { data } = await client.query(ExploreProfilesDocument, { 61 | request: { 62 | sortCriteria: profilesQuery.profileSortCriteria || ProfileSortCriteria.MostFollowers, 63 | cursor, 64 | limit: profilesQuery.limit 65 | } 66 | }).toPromise() 67 | if (data && data.exploreProfiles) { 68 | let { exploreProfiles: { pageInfo, items } } = data 69 | if (signedInUserAddress) { 70 | const requestData = items.map(i => ({ 71 | followerAddress: signedInUserAddress, 72 | profileId: i.id 73 | })) 74 | const response = await client.query(DoesFollowDocument, { 75 | request: { 76 | followInfos: requestData 77 | } 78 | }).toPromise() 79 | items = items.map((item, index) => { 80 | item.isFollowing = response?.data?.doesFollow[index].follows || false 81 | return item 82 | }) 83 | } 84 | return { 85 | pageInfo, items, 86 | } 87 | } 88 | } catch (err) { 89 | console.log('Error fetching profiles: ', err) 90 | setLoading(false) 91 | } 92 | } 93 | if (profilesQuery.name === 'getFollowing') { 94 | let { data } = await client.query(FollowingDocument, { 95 | request: { 96 | address: profilesQuery.ethereumAddress, 97 | cursor, 98 | limit: profilesQuery.limit || 25 99 | } 100 | }).toPromise() 101 | if (data) { 102 | let { following } = data 103 | let { pageInfo, items } : { 104 | pageInfo: PaginatedResultInfo, 105 | items: any 106 | } = following 107 | if (signedInUserAddress) { 108 | const requestData = items.map((i: any) => ({ 109 | followerAddress: signedInUserAddress, 110 | profileId: i.profile.id 111 | })) 112 | const response = await client.query(DoesFollowDocument, { 113 | request: { 114 | followInfos: requestData 115 | } 116 | }).toPromise() 117 | items = items.map((item: any, index: number) => { 118 | item.profile.isFollowing = response?.data?.doesFollow[index].follows 119 | return item.profile 120 | }) 121 | } else { 122 | items = items.map((item: any) => { 123 | return item.profile 124 | }) 125 | } 126 | return { 127 | pageInfo, items 128 | } 129 | } 130 | } 131 | } 132 | 133 | async function fetchProfiles(cursor=null) { 134 | if (profileData) { 135 | setProfiles(profileData) 136 | return 137 | } 138 | try { 139 | if ( 140 | !profileData || 141 | profileData && profiles.length 142 | ) { 143 | setLoading(true) 144 | let { 145 | items, pageInfo 146 | } = await fetchResponse(cursor) as { 147 | items: ExtendedProfile[], pageInfo: PaginatedResultInfo 148 | } 149 | setPaginationInfo(pageInfo) 150 | items = formatProfilePictures(items, IPFSGateway) 151 | setLoading(false) 152 | setProfiles([...profiles, ...items]) 153 | } else { 154 | setProfiles(profileData) 155 | } 156 | } catch (err) { 157 | console.log("Error fetching profiles... ", err) 158 | setLoading(false) 159 | } 160 | } 161 | 162 | function onEndReached() { 163 | if (infiniteScroll) { 164 | fetchNextItems() 165 | } 166 | } 167 | 168 | async function fetchNextItems() { 169 | try { 170 | if (canPaginate && paginationInfo) { 171 | const { next } = paginationInfo 172 | if (!next) { 173 | setCanPaginate(false) 174 | } else { 175 | fetchProfiles(next) 176 | } 177 | } 178 | } catch (err) { 179 | console.log('Error fetching next items: ', err) 180 | } 181 | } 182 | 183 | function renderItem({ item, index } : { 184 | item: Profile, 185 | index: number 186 | }) { 187 | return ( 188 | onFollowPress(profile, profiles)} 193 | isFollowing={item.isFollowing} 194 | /> 195 | ) 196 | } 197 | 198 | return ( 199 | 209 | ) : null 210 | } 211 | /> 212 | ) 213 | } 214 | 215 | const styles = StyleSheet.create({ 216 | loadingIndicatorStyle: { 217 | marginVertical: 20 218 | } 219 | }) -------------------------------------------------------------------------------- /src/components/Publication.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | View, 3 | StyleSheet, 4 | Text, 5 | Dimensions, 6 | Image, 7 | TouchableHighlight, 8 | Modal 9 | } from 'react-native' 10 | import Video from 'react-native-media-console' 11 | import { useContext, useState } from 'react' 12 | import { formatDistanceStrict } from 'date-fns' 13 | import { 14 | PublicationStyles, 15 | ExtendedPublication, 16 | LensContextType, 17 | ThemeColors, 18 | ProfileMetadata 19 | } from '../types' 20 | import { returnIpfsPathOrUrl } from '../utils' 21 | import { LensContext } from '../context' 22 | import { CommentIcon, MirrorIcon, CollectIcon, UnfilledHeartIcon, FilledHeartIcon } from './' 23 | 24 | const width = Dimensions.get('window').width 25 | 26 | export function Publication({ 27 | publication, 28 | signedInUser, 29 | hideLikes = false, 30 | hideComments = false, 31 | hideMirrors = false, 32 | hideCollects = false, 33 | iconColor, 34 | onCollectPress = publication => console.log({ publication }), 35 | onCommentPress = publication => console.log({ publication }), 36 | onMirrorPress= publication => console.log({ publication }), 37 | onLikePress = publication => console.log({ publication }), 38 | onProfileImagePress = publication => console.log({ publication }), 39 | styles = baseStyles 40 | }: { 41 | publication: ExtendedPublication, 42 | signedInUser?: ProfileMetadata, 43 | hideLikes?: boolean, 44 | hideComments?: boolean, 45 | hideMirrors?: boolean, 46 | hideCollects?: boolean, 47 | iconColor?: string, 48 | onCollectPress?: (publication: ExtendedPublication) => void, 49 | onCommentPress?:(publication: ExtendedPublication) => void, 50 | onMirrorPress?: (publication: ExtendedPublication) => void, 51 | onLikePress?: (publication: ExtendedPublication) => void, 52 | onProfileImagePress?: (publication: ExtendedPublication) => void, 53 | styles?: PublicationStyles 54 | }) { 55 | const [isFullScreen, setIsFullScreen] = useState(false) 56 | const [isPaused, setIsPaused] = useState(true) 57 | const { theme, IPFSGateway } = useContext(LensContext) 58 | if (theme) { 59 | if (theme === 'dark') { 60 | styles = darkThemeStyles 61 | iconColor = ThemeColors.lightGray 62 | } 63 | } 64 | 65 | const ComponentType = isFullScreen ? Modal : View 66 | 67 | return ( 68 | 72 | 75 | 76 | onProfileImagePress(publication)} 79 | > 80 | { 81 | publication?.profile?.picture?.__typename === 'NftImage' ? ( 82 | 88 | ) : 89 | publication?.profile?.picture?.__typename !== 'MediaSet' || 90 | publication.profile.missingAvatar ? ( 91 | 94 | ) : ( 95 | 101 | ) 102 | } 103 | 104 | 105 | 106 | { 107 | publication.__typename === 'Mirror' && publication.mirrorOf && ( 108 | 109 | 112 | Mirrored by {publication?.originalProfile?.name} 113 | 114 | ) 115 | } 116 | 117 | {publication.profile.name} 118 | @{publication.profile.handle} 119 | • {reduceDate(publication.createdAt)} 120 | 121 | { 122 | publication.metadata.content && ( 123 | {publication.metadata.content} 124 | ) 125 | } 126 | { 127 | Number(publication.metadata.media.length) > 0 && ( 128 | publication.metadata.media[0].original.mimeType.includes('image') ? 129 | : ( 136 | 137 | 143 | 173 | 174 | ) 175 | ) 176 | } 177 | { 178 | publication.stats && ( 179 | 180 | { 181 | !hideComments && ( 182 | 183 | onCommentPress(publication)} 185 | color={iconColor} 186 | /> 187 | {publication.stats.totalAmountOfComments} 188 | 189 | ) 190 | } 191 | { 192 | !hideMirrors && ( 193 | 194 | onMirrorPress(publication)} 196 | color={iconColor} 197 | /> 198 | {publication.stats.totalAmountOfMirrors} 199 | 200 | ) 201 | } 202 | { 203 | !hideCollects && ( 204 | 205 | onCollectPress(publication)} 207 | color={iconColor} 208 | /> 209 | {publication.stats.totalAmountOfCollects} 210 | 211 | ) 212 | } 213 | { 214 | !signedInUser && !hideLikes && ( 215 | 216 | onLikePress(publication)} 218 | color={iconColor} 219 | /> 220 | {publication.stats.totalUpvotes} 221 | 222 | ) 223 | } 224 | { 225 | signedInUser && !hideLikes && ( 226 | 227 | onLikePress(publication)} 229 | color={iconColor} 230 | /> 231 | {publication.stats.totalUpvotes} 232 | 233 | ) 234 | } 235 | 236 | ) 237 | } 238 | 239 | 240 | 241 | ) 242 | } 243 | 244 | function reduceDate(date: any) { 245 | const formattedDate = formatDistanceStrict(new Date(date), new Date()) 246 | const dateArr = formattedDate.split(' ') 247 | const dateInfo = dateArr[1].charAt(0) 248 | return `${dateArr[0]}${dateInfo}` 249 | } 250 | 251 | const baseStyles = StyleSheet.create({ 252 | videoContainer: { 253 | width: '100%', 254 | height: 220, 255 | marginTop: 10 256 | }, 257 | video: { 258 | position: 'absolute', 259 | top: 0, 260 | left: 0, 261 | bottom: 0, 262 | right: 0, 263 | }, 264 | publicationWrapper: { 265 | borderBottomWidth: 1, 266 | borderBottomColor: 'rgba(0, 0, 0, .05)', 267 | padding: 20 268 | }, 269 | publicationContainer: { 270 | flexDirection: 'row', 271 | }, 272 | userProfileContainer: { 273 | }, 274 | missingAvatarPlaceholder: { 275 | width: 40, 276 | height: 40, 277 | borderRadius: 20, 278 | backgroundColor: 'rgba(0, 0, 0, .4)' 279 | }, 280 | smallAvatar: { 281 | width: 40, 282 | height: 40, 283 | borderRadius: 20, 284 | }, 285 | postContentContainer: { 286 | flexShrink: 1, 287 | paddingLeft: 15 288 | }, 289 | postText: { 290 | flexShrink: 1, 291 | marginTop: 7, 292 | marginBottom: 5 293 | }, 294 | metadataImage: { 295 | marginTop: 10, 296 | flex: 1, 297 | width: width - 100, 298 | height: width - 100, 299 | }, 300 | statsContainer: { 301 | marginTop: 20, 302 | flexDirection: 'row', 303 | }, 304 | statsDetailContainer: { 305 | flexDirection: 'row', 306 | marginRight: 20, 307 | alignItems: 'center' 308 | }, 309 | statsDetailText: { 310 | marginLeft: 10, 311 | fontSize: 12 312 | }, 313 | postOwnerDetailsContainer: { 314 | flexDirection: 'row', 315 | alignItems: 'center' 316 | }, 317 | postOwnerName: { 318 | fontWeight: '600' 319 | }, 320 | postOwnerHandle: { 321 | marginLeft: 4, 322 | color: 'rgba(0, 0, 0, .5)' 323 | }, 324 | timestamp: { 325 | marginLeft: 4, 326 | color: 'rgba(0, 0, 0, .5)', 327 | fontSize: 12, 328 | fontWeight: '600' 329 | }, 330 | activityIndicatorContainer: { 331 | height: 60, 332 | justifyContent: 'center', 333 | marginBottom: 10, 334 | }, 335 | mirrorContainer: { 336 | flexDirection: 'row' 337 | }, 338 | mirrorText: { 339 | fontWeight: '600', 340 | color: 'rgba(0, 0, 0, .6)', 341 | fontSize: 12, 342 | marginBottom: 7, 343 | marginLeft: 5 344 | } 345 | }) 346 | 347 | const darkThemeStyles = StyleSheet.create({ 348 | videoContainer: { 349 | width: '100%', 350 | height: 220, 351 | marginTop: 10 352 | }, 353 | video: { 354 | position: 'absolute', 355 | top: 0, 356 | left: 0, 357 | bottom: 0, 358 | right: 0, 359 | }, 360 | publicationWrapper: { 361 | borderBottomWidth: 1, 362 | padding: 20, 363 | backgroundColor: ThemeColors.black, 364 | borderBottomColor: ThemeColors.clearWhite 365 | }, 366 | publicationContainer: { 367 | flexDirection: 'row', 368 | }, 369 | userProfileContainer: { 370 | }, 371 | missingAvatarPlaceholder: { 372 | width: 40, 373 | height: 40, 374 | borderRadius: 20, 375 | backgroundColor: 'rgba(0, 0, 0, .4)' 376 | }, 377 | smallAvatar: { 378 | width: 40, 379 | height: 40, 380 | borderRadius: 20, 381 | }, 382 | postContentContainer: { 383 | flexShrink: 1, 384 | paddingLeft: 15 385 | }, 386 | postText: { 387 | flexShrink: 1, 388 | marginTop: 7, 389 | marginBottom: 5, 390 | color: ThemeColors.white 391 | }, 392 | metadataImage: { 393 | marginTop: 10, 394 | flex: 1, 395 | width: width - 100, 396 | height: width - 100, 397 | }, 398 | statsContainer: { 399 | marginTop: 20, 400 | flexDirection: 'row', 401 | paddingLeft: 20, 402 | }, 403 | statsDetailContainer: { 404 | flexDirection: 'row', 405 | marginRight: 20, 406 | alignItems: 'center' 407 | }, 408 | statsDetailText: { 409 | marginLeft: 10, 410 | fontSize: 12, 411 | color: ThemeColors.white 412 | }, 413 | postOwnerDetailsContainer: { 414 | flexDirection: 'row', 415 | alignItems: 'center' 416 | }, 417 | postOwnerName: { 418 | fontWeight: '600', 419 | color: ThemeColors.white 420 | }, 421 | postOwnerHandle: { 422 | marginLeft: 4, 423 | color: ThemeColors.lightGray 424 | }, 425 | timestamp: { 426 | marginLeft: 4, 427 | fontSize: 12, 428 | fontWeight: '600', 429 | color: ThemeColors.lightGray 430 | }, 431 | activityIndicatorContainer: { 432 | height: 60, 433 | justifyContent: 'center', 434 | marginBottom: 10, 435 | }, 436 | mirrorContainer: { 437 | flexDirection: 'row' 438 | }, 439 | mirrorText: { 440 | fontWeight: '600', 441 | fontSize: 12, 442 | marginBottom: 7, 443 | marginLeft: 5, 444 | color: ThemeColors.lightGray 445 | } 446 | }) -------------------------------------------------------------------------------- /src/components/Search.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | View, 3 | StyleSheet, 4 | TextInput, 5 | FlatList, 6 | ActivityIndicator 7 | } from 'react-native' 8 | import { 9 | useState, 10 | useCallback, 11 | useContext, 12 | useEffect 13 | } from 'react' 14 | import { 15 | SearchType, 16 | AutoCapitalizeOptions, 17 | ProfilesQuery, 18 | PublicationsQuery, 19 | LensContextType, 20 | ExtendedProfile, 21 | PublicationStyles, 22 | ExtendedPublication, 23 | ProfileMetadata, 24 | ThemeColors, 25 | SearchStyles 26 | } from '../types' 27 | import { createClient } from '../api' 28 | import { 29 | ProfileListItem, 30 | SearchIcon, 31 | Publication as PublicationComponent 32 | } from './' 33 | import { 34 | debounce, 35 | formatProfilePictures, 36 | filterMimeTypes, 37 | configureMirrorAndIpfsUrl 38 | } from '../utils' 39 | import { 40 | SearchProfilesDocument, 41 | PaginatedResultInfo, 42 | ExploreProfilesDocument, 43 | ProfileSortCriteria, 44 | PublicationSortCriteria, 45 | ExplorePublicationsDocument, 46 | PublicationTypes, 47 | SearchPublicationsDocument, 48 | SearchRequestTypes, 49 | ProfileSearchResult, 50 | ExplorePublicationResult, 51 | ExploreProfileResult, 52 | PublicationSearchResult 53 | } from '../graphql/generated' 54 | import { LensContext } from '../context' 55 | 56 | export function Search({ 57 | searchType = SearchType.profile, 58 | styles = baseStyles, 59 | placeholder = 'Search', 60 | autoCapitalize = AutoCapitalizeOptions.none, 61 | selectionColor = '#b0b0b0', 62 | infiniteScroll = true, 63 | ListFooterComponent, 64 | publicationStyles, 65 | signedInUser, 66 | hideLikes = false, 67 | hideComments = false, 68 | hideMirrors = false, 69 | hideCollects = false, 70 | iconColor, 71 | profilesQuery = { 72 | profileSortCriteria: ProfileSortCriteria.MostFollowers, 73 | limit: 25 74 | }, 75 | publicationsQuery = { 76 | publicationTypes: [PublicationTypes.Post, PublicationTypes.Comment, PublicationTypes.Mirror], 77 | publicationSortCriteria: PublicationSortCriteria.Latest, 78 | limit: 25 79 | }, 80 | onEndReachedThreshold = .65, 81 | onFollowPress = profile => console.log({ profile }), 82 | onProfilePress = profile => console.log({ profile }), 83 | onCollectPress = publication => console.log({ publication }), 84 | onCommentPress = publication => console.log({ publication }), 85 | onMirrorPress = publication => console.log({ publication }), 86 | onLikePress = publication => console.log({ publication }), 87 | onProfileImagePress = publication => console.log({ publication }), 88 | } : { 89 | searchType?: SearchType, 90 | styles?: SearchStyles, 91 | placeholder?: string, 92 | autoCapitalize?: AutoCapitalizeOptions, 93 | selectionColor?: string, 94 | ListFooterComponent?: React.FC, 95 | iconColor?: string, 96 | profilesQuery?: ProfilesQuery, 97 | publicationsQuery?: PublicationsQuery, 98 | infiniteScroll?: boolean, 99 | onEndReachedThreshold?: number, 100 | publicationStyles?: PublicationStyles, 101 | signedInUser?: ProfileMetadata, 102 | hideLikes?: any, 103 | hideComments?: boolean, 104 | hideMirrors?: boolean, 105 | hideCollects?: boolean, 106 | onCollectPress?: (publication: ExtendedPublication) => void, 107 | onCommentPress?: (publication: ExtendedPublication) => void, 108 | onMirrorPress?: (publication: ExtendedPublication) => void, 109 | onLikePress?: (publication: ExtendedPublication) => void, 110 | onProfileImagePress?: (publication: ExtendedPublication) => void, 111 | onFollowPress?: (profile: ExtendedProfile, profiles: ExtendedProfile[]) => void, 112 | onProfilePress?: (profile: ExtendedProfile) => void 113 | }) { 114 | const [searchString, updateSearchString] = useState('') 115 | const [paginationInfo, setPaginationInfo] = useState() 116 | const [profiles, setProfiles] = useState([]) 117 | const [publications, setPublications] = useState([]) 118 | const [loading, setLoading] = useState(false) 119 | const [canPaginate, setCanPaginate] = useState(true) 120 | 121 | const { environment, theme, IPFSGateway } = useContext(LensContext) 122 | const client = createClient(environment) 123 | 124 | if (theme === 'dark') { 125 | styles = darkThemeStyles 126 | } 127 | 128 | useEffect(() => { 129 | if (searchType === SearchType.profile) { 130 | fetchProfiles() 131 | } else { 132 | fetchPublications() 133 | } 134 | }, [searchType]) 135 | 136 | async function fetchProfiles(cursor?: string) { 137 | setLoading(true) 138 | try { 139 | const { data } = await client.query(ExploreProfilesDocument, { 140 | request: { 141 | sortCriteria: profilesQuery.profileSortCriteria || ProfileSortCriteria.MostFollowers, 142 | limit: profilesQuery.limit, 143 | cursor 144 | } 145 | }).toPromise() 146 | if (data && data.exploreProfiles.__typename === 'ExploreProfileResult') { 147 | let { 148 | items, pageInfo 149 | } = data.exploreProfiles as ExploreProfileResult 150 | setPaginationInfo(pageInfo) 151 | items = formatProfilePictures(items, IPFSGateway) 152 | if (cursor) { 153 | let newData = [...profiles, ...items] 154 | setProfiles(newData) 155 | } else { 156 | setProfiles(items) 157 | } 158 | setLoading(false) 159 | } 160 | } catch (err) { 161 | console.log('error fetching profiles... ', err) 162 | } 163 | } 164 | 165 | async function fetchPublications(cursor?: string) { 166 | setLoading(true) 167 | try { 168 | const { 169 | data 170 | } = await client.query(ExplorePublicationsDocument, { 171 | request: { 172 | publicationTypes: publicationsQuery.publicationTypes, 173 | limit: publicationsQuery.limit, 174 | sortCriteria: publicationsQuery.publicationSortCriteria || PublicationSortCriteria.Latest, 175 | cursor, 176 | } 177 | }).toPromise() 178 | if (data && data.explorePublications.__typename === 'ExplorePublicationResult') { 179 | let { 180 | items, pageInfo 181 | } = data.explorePublications as ExplorePublicationResult 182 | setPaginationInfo(pageInfo) 183 | items = filterMimeTypes(items) 184 | items = configureMirrorAndIpfsUrl(items, IPFSGateway) 185 | if (cursor) { 186 | let newData = [...publications, ...items] 187 | if (publicationsQuery.publicationSortCriteria === "LATEST") { 188 | newData = [...new Map(newData.map(m => [m.id, m])).values()] 189 | } 190 | setPublications(newData) 191 | } else { 192 | setPublications(items) 193 | } 194 | setLoading(false) 195 | } 196 | } catch (err) { 197 | console.log('error fetching publications... ', err) 198 | setLoading(false) 199 | } 200 | } 201 | 202 | async function searchPublications(value: string, cursor?: string) { 203 | setLoading(true) 204 | try { 205 | const { 206 | data 207 | } = await client.query(SearchPublicationsDocument, { 208 | request: { 209 | type: SearchRequestTypes.Publication, 210 | query: value, 211 | limit: publicationsQuery.limit, 212 | cursor, 213 | } 214 | }).toPromise() 215 | if (data && data.search.__typename === 'PublicationSearchResult') { 216 | let { 217 | items, pageInfo 218 | } = data.search as PublicationSearchResult 219 | setPaginationInfo(pageInfo) 220 | items = filterMimeTypes(items) 221 | items = configureMirrorAndIpfsUrl(items, IPFSGateway) 222 | if (cursor) { 223 | let newData = [...publications, ...items] 224 | if (publicationsQuery.publicationSortCriteria === "LATEST") { 225 | newData = [...new Map(newData.map(m => [m.id, m])).values()] 226 | } 227 | setPublications(newData) 228 | } else { 229 | setPublications(items) 230 | } 231 | setLoading(false) 232 | } 233 | } catch (err) { 234 | console.log('error searching publications... ', err) 235 | } 236 | } 237 | 238 | function onEndReached() { 239 | if (infiniteScroll) { 240 | fetchNextItems() 241 | } 242 | } 243 | 244 | async function fetchNextItems() { 245 | try { 246 | if (canPaginate && paginationInfo) { 247 | const { next } = paginationInfo 248 | if (!next) { 249 | setCanPaginate(false) 250 | } else { 251 | if (searchType === SearchType.profile) { 252 | if (searchString) { 253 | searchProfiles(searchString, next) 254 | } else { 255 | fetchProfiles(next) 256 | } 257 | } 258 | if (searchType === SearchType.publication) { 259 | if (searchString) { 260 | searchPublications(searchString, next) 261 | } else { 262 | fetchPublications(next) 263 | } 264 | } 265 | } 266 | } 267 | } catch (err) { 268 | console.log('Error fetching next items... ', err) 269 | } 270 | } 271 | 272 | async function searchProfiles(value: string, cursor?: string) { 273 | try { 274 | setLoading(true) 275 | const { data } = await client.query(SearchProfilesDocument, { 276 | request: { 277 | query: value, 278 | type: SearchRequestTypes.Profile, 279 | cursor 280 | } 281 | }).toPromise() 282 | if (data && data.search.__typename === 'ProfileSearchResult') { 283 | let { 284 | items, pageInfo 285 | } = data.search as ProfileSearchResult 286 | setPaginationInfo(pageInfo) 287 | items = formatProfilePictures(items, IPFSGateway) 288 | if (cursor) { 289 | let newData = [...profiles, ...items] 290 | setProfiles(newData) 291 | } else { 292 | setProfiles(items) 293 | } 294 | setLoading(false) 295 | } 296 | } catch (err) { 297 | setLoading(false) 298 | console.log('error searching...', err) 299 | } 300 | } 301 | 302 | function renderProfile({ item, index} : { 303 | item: ExtendedProfile, 304 | index: number 305 | }) { 306 | return ( 307 | onFollowPress(profile, profiles)} 312 | isFollowing={item.isFollowing} 313 | /> 314 | ) 315 | } 316 | 317 | function renderPublication({ 318 | item, index 319 | } : { 320 | item: ExtendedPublication, 321 | index: number 322 | }) { 323 | return ( 324 | 340 | ) 341 | } 342 | 343 | const onChangeText = (value:string) => { 344 | updateSearchString(value) 345 | if (searchType === SearchType.profile) { 346 | searchProfiles(value) 347 | } else { 348 | searchPublications(value) 349 | } 350 | } 351 | 352 | const callback = useCallback(debounce(onChangeText, 400), []); 353 | 354 | return ( 355 | 356 | 357 | 358 | 359 | 368 | 369 | 370 | { 371 | searchType === SearchType.profile ? ( 372 | String(index)} 378 | onEndReachedThreshold={onEndReachedThreshold} 379 | ListFooterComponent={ 380 | ListFooterComponent ? 381 | ListFooterComponent : 382 | loading ? ( 383 | 386 | ) : null 387 | } 388 | /> 389 | ) : ( 390 | String(index)} 396 | onEndReachedThreshold={onEndReachedThreshold} 397 | ListFooterComponent={ 398 | ListFooterComponent ? 399 | ListFooterComponent : 400 | loading ? ( 401 | 404 | ) : null 405 | } 406 | /> 407 | ) 408 | } 409 | 410 | 411 | ) 412 | } 413 | 414 | const baseStyles = StyleSheet.create({ 415 | containerStyle: { 416 | }, 417 | inputContainerStyle: { 418 | paddingTop: 10, 419 | alignItems: 'center', 420 | backgroundColor: 'white', 421 | paddingBottom: 10 422 | }, 423 | inputWrapperStyle: { 424 | flexDirection: 'row', 425 | alignItems: 'center', 426 | width: '95%', 427 | paddingLeft: 15, 428 | backgroundColor: 'rgba(0, 0, 0, .065)', 429 | borderRadius: 30, 430 | height: 40, 431 | paddingRight: 10 432 | }, 433 | inputStyle: { 434 | marginLeft: 8, 435 | flex: 1, 436 | }, 437 | loadingIndicatorStyle : { 438 | marginVertical: 20 439 | } 440 | }) 441 | 442 | const darkThemeStyles = StyleSheet.create({ 443 | containerStyle: { 444 | backgroundColor: ThemeColors.black, 445 | }, 446 | inputContainerStyle: { 447 | alignItems: 'center', 448 | backgroundColor: ThemeColors.black, 449 | marginBottom: 8 450 | }, 451 | inputWrapperStyle: { 452 | flexDirection: 'row', 453 | alignItems: 'center', 454 | width: '95%', 455 | paddingLeft: 15, 456 | backgroundColor: ThemeColors.darkGray, 457 | borderRadius: 30, 458 | height: 40, 459 | paddingRight: 10 460 | }, 461 | inputStyle: { 462 | marginLeft: 8, 463 | flex: 1, 464 | color: ThemeColors.lightGray 465 | }, 466 | loadingIndicatorStyle : { 467 | marginVertical: 20 468 | } 469 | }) -------------------------------------------------------------------------------- /src/components/index.tsx: -------------------------------------------------------------------------------- 1 | /* icons */ 2 | export { FilledHeartIcon } from './Icons/FilledHeartIcon' 3 | export { UnfilledHeartIcon } from './Icons/UnfilledHeartIcon' 4 | export { MirrorIcon } from './Icons/MirrorIcon' 5 | export { CollectIcon } from './Icons/CollectIcon' 6 | export { CommentIcon } from './Icons/CommentIcon' 7 | export { SearchIcon } from './Icons/SearchIcon' 8 | 9 | /* components */ 10 | export { Profile } from './Profile' 11 | export { Profiles } from './Profiles' 12 | export { ProfileListItem } from './ProfileListItem' 13 | export { Feed } from './Feed' 14 | export { ProfileHeader } from './ProfileHeader' 15 | export { Publication } from './Publication' 16 | export { Search } from './Search' 17 | 18 | /* provider */ 19 | export { LensProvider } from './LensProvider' -------------------------------------------------------------------------------- /src/context.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react' 2 | import { 3 | LensContextType, 4 | Environment, 5 | Theme 6 | } from './types' 7 | 8 | export const LensContext = createContext({ 9 | environment: Environment.mainnet, 10 | IPFSGateway: 'https://cloudflare-ipfs.com/ipfs', 11 | theme: Theme.light 12 | }) 13 | -------------------------------------------------------------------------------- /src/graphql/common.graphql: -------------------------------------------------------------------------------- 1 | fragment MediaFields on Media { 2 | url 3 | width 4 | height 5 | mimeType 6 | } 7 | 8 | fragment ProfileFields on Profile { 9 | id 10 | name 11 | bio 12 | attributes { 13 | displayType 14 | traitType 15 | key 16 | value 17 | } 18 | isFollowedByMe 19 | isFollowing(who: null) 20 | followNftAddress 21 | metadata 22 | isDefault 23 | handle 24 | picture { 25 | ... on NftImage { 26 | contractAddress 27 | tokenId 28 | uri 29 | verified 30 | } 31 | ... on MediaSet { 32 | original { 33 | ...MediaFields 34 | } 35 | small { 36 | ...MediaFields 37 | } 38 | medium { 39 | ...MediaFields 40 | } 41 | } 42 | } 43 | coverPicture { 44 | ... on NftImage { 45 | contractAddress 46 | tokenId 47 | uri 48 | verified 49 | } 50 | ... on MediaSet { 51 | original { 52 | ...MediaFields 53 | } 54 | small { 55 | ...MediaFields 56 | } 57 | medium { 58 | ...MediaFields 59 | } 60 | } 61 | } 62 | ownedBy 63 | dispatcher { 64 | address 65 | canUseRelay 66 | } 67 | stats { 68 | totalFollowers 69 | totalFollowing 70 | totalPosts 71 | totalComments 72 | totalMirrors 73 | totalPublications 74 | totalCollects 75 | } 76 | followModule { 77 | ...FollowModuleFields 78 | } 79 | onChainIdentity { 80 | ens { 81 | name 82 | } 83 | proofOfHumanity 84 | sybilDotOrg { 85 | verified 86 | source { 87 | twitter { 88 | handle 89 | } 90 | } 91 | } 92 | worldcoin { 93 | isHuman 94 | } 95 | } 96 | } 97 | 98 | fragment PublicationStatsFields on PublicationStats { 99 | totalAmountOfMirrors 100 | totalAmountOfCollects 101 | totalAmountOfComments 102 | totalUpvotes 103 | } 104 | 105 | fragment MetadataOutputFields on MetadataOutput { 106 | name 107 | description 108 | content 109 | media { 110 | original { 111 | ...MediaFields 112 | } 113 | small { 114 | ...MediaFields 115 | } 116 | medium { 117 | ...MediaFields 118 | } 119 | } 120 | attributes { 121 | displayType 122 | traitType 123 | value 124 | } 125 | encryptionParams { 126 | providerSpecificParams { 127 | encryptionKey 128 | } 129 | accessCondition { 130 | ...AccessConditionFields 131 | } 132 | encryptedFields { 133 | animation_url 134 | content 135 | external_url 136 | image 137 | media { 138 | ...EncryptedMediaSetFields 139 | } 140 | } 141 | } 142 | } 143 | 144 | fragment Erc20Fields on Erc20 { 145 | name 146 | symbol 147 | decimals 148 | address 149 | } 150 | 151 | fragment PostFields on Post { 152 | id 153 | profile { 154 | ...ProfileFields 155 | } 156 | stats { 157 | ...PublicationStatsFields 158 | } 159 | metadata { 160 | ...MetadataOutputFields 161 | } 162 | createdAt 163 | collectModule { 164 | ...CollectModuleFields 165 | } 166 | referenceModule { 167 | ...ReferenceModuleFields 168 | } 169 | appId 170 | hidden 171 | reaction(request: null) 172 | mirrors(by: null) 173 | hasCollectedByMe 174 | isGated 175 | } 176 | 177 | fragment MirrorBaseFields on Mirror { 178 | id 179 | profile { 180 | ...ProfileFields 181 | } 182 | stats { 183 | ...PublicationStatsFields 184 | } 185 | metadata { 186 | ...MetadataOutputFields 187 | } 188 | createdAt 189 | collectModule { 190 | ...CollectModuleFields 191 | } 192 | referenceModule { 193 | ...ReferenceModuleFields 194 | } 195 | appId 196 | hidden 197 | reaction(request: null) 198 | hasCollectedByMe 199 | isGated 200 | } 201 | 202 | fragment MirrorFields on Mirror { 203 | ...MirrorBaseFields 204 | mirrorOf { 205 | ... on Post { 206 | ...PostFields 207 | } 208 | ... on Comment { 209 | ...CommentFields 210 | } 211 | } 212 | } 213 | 214 | fragment CommentBaseFields on Comment { 215 | id 216 | profile { 217 | ...ProfileFields 218 | } 219 | stats { 220 | ...PublicationStatsFields 221 | } 222 | metadata { 223 | ...MetadataOutputFields 224 | } 225 | createdAt 226 | collectModule { 227 | ...CollectModuleFields 228 | } 229 | referenceModule { 230 | ...ReferenceModuleFields 231 | } 232 | appId 233 | hidden 234 | reaction(request: null) 235 | mirrors(by: null) 236 | hasCollectedByMe 237 | isGated 238 | isDataAvailability 239 | onChainContentURI 240 | } 241 | 242 | fragment CommentFields on Comment { 243 | ...CommentBaseFields 244 | mainPost { 245 | ... on Post { 246 | ...PostFields 247 | } 248 | ... on Mirror { 249 | ...MirrorBaseFields 250 | mirrorOf { 251 | ... on Post { 252 | ...PostFields 253 | } 254 | ... on Comment { 255 | ...CommentMirrorOfFields 256 | } 257 | } 258 | } 259 | } 260 | } 261 | 262 | fragment CommentMirrorOfFields on Comment { 263 | ...CommentBaseFields 264 | mainPost { 265 | ... on Post { 266 | ...PostFields 267 | } 268 | ... on Mirror { 269 | ...MirrorBaseFields 270 | } 271 | } 272 | } 273 | 274 | fragment TxReceiptFields on TransactionReceipt { 275 | to 276 | from 277 | contractAddress 278 | transactionIndex 279 | root 280 | gasUsed 281 | logsBloom 282 | blockHash 283 | transactionHash 284 | blockNumber 285 | confirmations 286 | cumulativeGasUsed 287 | effectiveGasPrice 288 | byzantium 289 | type 290 | status 291 | logs { 292 | blockNumber 293 | blockHash 294 | transactionIndex 295 | removed 296 | address 297 | data 298 | topics 299 | transactionHash 300 | logIndex 301 | } 302 | } 303 | 304 | fragment WalletFields on Wallet { 305 | address 306 | defaultProfile { 307 | ...ProfileFields 308 | } 309 | } 310 | 311 | fragment CommonPaginatedResultInfoFields on PaginatedResultInfo { 312 | prev 313 | next 314 | totalCount 315 | } 316 | 317 | fragment FollowModuleFields on FollowModule { 318 | ... on FeeFollowModuleSettings { 319 | type 320 | amount { 321 | asset { 322 | name 323 | symbol 324 | decimals 325 | address 326 | } 327 | value 328 | } 329 | recipient 330 | } 331 | ... on ProfileFollowModuleSettings { 332 | type 333 | contractAddress 334 | } 335 | ... on RevertFollowModuleSettings { 336 | type 337 | contractAddress 338 | } 339 | ... on UnknownFollowModuleSettings { 340 | type 341 | contractAddress 342 | followModuleReturnData 343 | } 344 | } 345 | 346 | fragment CollectModuleFields on CollectModule { 347 | __typename 348 | ... on FreeCollectModuleSettings { 349 | type 350 | followerOnly 351 | contractAddress 352 | } 353 | ... on FeeCollectModuleSettings { 354 | type 355 | amount { 356 | asset { 357 | ...Erc20Fields 358 | } 359 | value 360 | } 361 | recipient 362 | referralFee 363 | } 364 | ... on LimitedFeeCollectModuleSettings { 365 | type 366 | collectLimit 367 | amount { 368 | asset { 369 | ...Erc20Fields 370 | } 371 | value 372 | } 373 | recipient 374 | referralFee 375 | } 376 | ... on LimitedTimedFeeCollectModuleSettings { 377 | type 378 | collectLimit 379 | amount { 380 | asset { 381 | ...Erc20Fields 382 | } 383 | value 384 | } 385 | recipient 386 | referralFee 387 | endTimestamp 388 | } 389 | ... on RevertCollectModuleSettings { 390 | type 391 | } 392 | ... on TimedFeeCollectModuleSettings { 393 | type 394 | amount { 395 | asset { 396 | ...Erc20Fields 397 | } 398 | value 399 | } 400 | recipient 401 | referralFee 402 | endTimestamp 403 | } 404 | ... on UnknownCollectModuleSettings { 405 | type 406 | contractAddress 407 | collectModuleReturnData 408 | } 409 | } 410 | 411 | fragment ReferenceModuleFields on ReferenceModule { 412 | ... on FollowOnlyReferenceModuleSettings { 413 | type 414 | contractAddress 415 | } 416 | ... on UnknownReferenceModuleSettings { 417 | type 418 | contractAddress 419 | referenceModuleReturnData 420 | } 421 | ... on DegreesOfSeparationReferenceModuleSettings { 422 | type 423 | contractAddress 424 | commentsRestricted 425 | mirrorsRestricted 426 | degreesOfSeparation 427 | } 428 | } 429 | 430 | fragment Erc20OwnershipFields on Erc20OwnershipOutput { 431 | contractAddress 432 | amount 433 | chainID 434 | condition 435 | decimals 436 | } 437 | 438 | fragment EoaOwnershipFields on EoaOwnershipOutput { 439 | address 440 | } 441 | 442 | fragment NftOwnershipFields on NftOwnershipOutput { 443 | contractAddress 444 | chainID 445 | contractType 446 | tokenIds 447 | } 448 | 449 | fragment ProfileOwnershipFields on ProfileOwnershipOutput { 450 | profileId 451 | } 452 | 453 | fragment FollowConditionFields on FollowConditionOutput { 454 | profileId 455 | } 456 | 457 | fragment CollectConditionFields on CollectConditionOutput { 458 | publicationId 459 | thisPublication 460 | } 461 | 462 | fragment AndConditionFields on AndConditionOutput { 463 | criteria { 464 | ...AccessConditionFields 465 | } 466 | } 467 | 468 | fragment OrConditionFields on OrConditionOutput { 469 | criteria { 470 | ...AccessConditionFields 471 | } 472 | } 473 | fragment AndConditionFieldsNoRecursive on AndConditionOutput { 474 | criteria { 475 | ...SimpleConditionFields 476 | } 477 | } 478 | 479 | fragment OrConditionFieldsNoRecursive on OrConditionOutput { 480 | criteria { 481 | ...SimpleConditionFields 482 | } 483 | } 484 | 485 | fragment SimpleConditionFields on AccessConditionOutput { 486 | nft { 487 | ...NftOwnershipFields 488 | } 489 | eoa { 490 | ...EoaOwnershipFields 491 | } 492 | token { 493 | ...Erc20OwnershipFields 494 | } 495 | profile { 496 | ...ProfileOwnershipFields 497 | } 498 | follow { 499 | ...FollowConditionFields 500 | } 501 | collect { 502 | ...CollectConditionFields 503 | } 504 | } 505 | 506 | fragment BooleanConditionFieldsRecursive on AccessConditionOutput { 507 | and { 508 | criteria { 509 | ...SimpleConditionFields 510 | and { 511 | criteria { 512 | ...SimpleConditionFields 513 | } 514 | } 515 | or { 516 | criteria { 517 | ...SimpleConditionFields 518 | } 519 | } 520 | } 521 | } 522 | or { 523 | criteria { 524 | ...SimpleConditionFields 525 | and { 526 | criteria { 527 | ...SimpleConditionFields 528 | } 529 | } 530 | or { 531 | criteria { 532 | ...SimpleConditionFields 533 | } 534 | } 535 | } 536 | } 537 | } 538 | 539 | fragment AccessConditionFields on AccessConditionOutput { 540 | ...SimpleConditionFields 541 | ...BooleanConditionFieldsRecursive 542 | } 543 | 544 | fragment EncryptedMediaFields on EncryptedMedia { 545 | url 546 | width 547 | height 548 | mimeType 549 | } 550 | 551 | fragment EncryptedMediaSetFields on EncryptedMediaSet { 552 | original { 553 | ...EncryptedMediaFields 554 | } 555 | small { 556 | ...EncryptedMediaFields 557 | } 558 | medium { 559 | ...EncryptedMediaFields 560 | } 561 | } -------------------------------------------------------------------------------- /src/graphql/doesFollow.graphql: -------------------------------------------------------------------------------- 1 | query doesFollow($request: DoesFollowRequest!) { 2 | doesFollow(request: $request) { 3 | followerAddress 4 | profileId 5 | follows 6 | } 7 | } -------------------------------------------------------------------------------- /src/graphql/exploreProfiles.graphql: -------------------------------------------------------------------------------- 1 | query exploreProfiles($request: ExploreProfilesRequest!) { 2 | exploreProfiles(request: $request) { 3 | items { 4 | ...ProfileFields 5 | } 6 | pageInfo { 7 | ...CommonPaginatedResultInfoFields 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /src/graphql/explorePublications.graphql: -------------------------------------------------------------------------------- 1 | query ExplorePublications($request: ExplorePublicationRequest!) { 2 | explorePublications(request: $request) { 3 | items { 4 | __typename 5 | ... on Post { 6 | ...PostFields 7 | } 8 | ... on Comment { 9 | ...CommentFields 10 | } 11 | ... on Mirror { 12 | ...MirrorFields 13 | } 14 | } 15 | pageInfo { 16 | ...CommonPaginatedResultInfoFields 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /src/graphql/getFollowing.graphql: -------------------------------------------------------------------------------- 1 | query following($request: FollowingRequest!) { 2 | following(request: $request) { 3 | items { 4 | profile { 5 | ...ProfileFields 6 | } 7 | totalAmountOfTimesFollowing 8 | } 9 | pageInfo { 10 | ...CommonPaginatedResultInfoFields 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /src/graphql/getProfile.graphql: -------------------------------------------------------------------------------- 1 | query profile($request: SingleProfileQueryRequest!) { 2 | profile(request: $request) { 3 | ...ProfileFields 4 | } 5 | } -------------------------------------------------------------------------------- /src/graphql/getPublication.graphql: -------------------------------------------------------------------------------- 1 | query publication($request: PublicationQueryRequest!) { 2 | publication(request: $request) { 3 | __typename 4 | ... on Post { 5 | ...PostFields 6 | } 7 | ... on Comment { 8 | ...CommentFields 9 | } 10 | ... on Mirror { 11 | ...MirrorFields 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /src/graphql/getPublications.graphql: -------------------------------------------------------------------------------- 1 | query publications($request: PublicationsQueryRequest!) { 2 | publications(request: $request) { 3 | items { 4 | __typename 5 | ... on Post { 6 | ...PostFields 7 | } 8 | ... on Comment { 9 | ...CommentFields 10 | } 11 | ... on Mirror { 12 | ...MirrorFields 13 | } 14 | } 15 | pageInfo { 16 | ...CommonPaginatedResultInfoFields 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /src/graphql/profile-feed.graphql: -------------------------------------------------------------------------------- 1 | query ProfileFeed($request: FeedRequest!) { 2 | feed(request: $request) { 3 | items { 4 | root { 5 | __typename 6 | ... on Post { 7 | ...PostFields 8 | } 9 | ... on Comment { 10 | ...CommentFields 11 | } 12 | } 13 | electedMirror { 14 | mirrorId 15 | profile { 16 | id 17 | handle 18 | } 19 | timestamp 20 | } 21 | mirrors { 22 | profile { 23 | id 24 | handle 25 | } 26 | timestamp 27 | } 28 | collects { 29 | profile { 30 | id 31 | handle 32 | } 33 | timestamp 34 | } 35 | reactions { 36 | profile { 37 | id 38 | handle 39 | } 40 | reaction 41 | timestamp 42 | } 43 | comments { 44 | ...CommentFields 45 | } 46 | } 47 | pageInfo { 48 | prev 49 | next 50 | totalCount 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /src/graphql/searchProfiles.graphql: -------------------------------------------------------------------------------- 1 | query SearchProfiles($request: SearchQueryRequest!) { 2 | search(request: $request) { 3 | ... on ProfileSearchResult { 4 | __typename 5 | items { 6 | ... on Profile { 7 | ...ProfileFields 8 | } 9 | } 10 | pageInfo { 11 | ...CommonPaginatedResultInfoFields 12 | } 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /src/graphql/searchPublications.graphql: -------------------------------------------------------------------------------- 1 | query SearchPublications($request: SearchQueryRequest!) { 2 | search(request: $request) { 3 | ... on PublicationSearchResult { 4 | __typename 5 | items { 6 | __typename 7 | ... on Comment { 8 | ...CommentFields 9 | } 10 | ... on Post { 11 | ...PostFields 12 | } 13 | } 14 | pageInfo { 15 | ...CommonPaginatedResultInfoFields 16 | } 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types' 2 | export * from './components' -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { 2 | PublicationTypes, 3 | PublicationSortCriteria, 4 | ProfileSortCriteria, 5 | Profile, 6 | Post, 7 | Comment, 8 | Mirror, 9 | PaginatedResultInfo, 10 | PublicationMetadataFilters 11 | } from './graphql/generated' 12 | 13 | export type PublicationsQuery = { 14 | name?: string; 15 | publicationSortCriteria?: PublicationSortCriteria, 16 | publicationTypes?: PublicationTypes[], 17 | limit?: number; 18 | profileId?: number; 19 | handle?: string; 20 | publicationId?: number; 21 | cursor?: string; 22 | metadata?: PublicationMetadataFilters; 23 | } 24 | 25 | export type ProfilesQuery = { 26 | name?: string; 27 | profileSortCriteria?: ProfileSortCriteria; 28 | limit?: number; 29 | ethereumAddress?: string; 30 | cursor?: string; 31 | } 32 | 33 | export enum Theme { 34 | light = 'light', 35 | dark = 'dark' 36 | } 37 | 38 | export enum Environment { 39 | testnet = 'testnet', 40 | mainnet = 'mainnet', 41 | sandbox = 'sandbox', 42 | } 43 | 44 | export interface LensContextType { 45 | environment: Environment; 46 | theme: Theme; 47 | IPFSGateway: string; 48 | } 49 | 50 | export enum ThemeColors { 51 | black = '#131313', 52 | white = '#ffffff', 53 | lightGray = 'rgba(255, 255, 255, .6)', 54 | clearWhite = 'rgba(255, 255, 255, .15)', 55 | darkGray = '#202020' 56 | } 57 | 58 | export enum SearchType { 59 | profile = 'profile', 60 | publication = 'publication' 61 | } 62 | 63 | export enum AutoCapitalizeOptions { 64 | characters = 'characters', 65 | words = 'words', 66 | sentences = 'sentences', 67 | none = 'none' 68 | } 69 | 70 | /* Lens specific */ 71 | export enum MetadataDisplayType { 72 | number = 'number', 73 | string = 'string', 74 | date = 'date', 75 | } 76 | 77 | export interface SignatureContext { 78 | signature?: string; 79 | } 80 | 81 | export interface GenericMetadata { 82 | /** 83 | * The metadata version. 84 | */ 85 | version: string; 86 | 87 | /** 88 | * The metadata id can be anything but if your uploading to ipfs 89 | * you will want it to be random.. using uuid could be an option! 90 | */ 91 | metadata_id: string; 92 | /** 93 | * Signed metadata to validate the owner 94 | */ 95 | signatureContext?: SignatureContext; 96 | /** 97 | * This is the appId the content belongs to 98 | */ 99 | appId?: string; 100 | } 101 | 102 | export interface AttributeData { 103 | displayType?: MetadataDisplayType; 104 | traitType?: string; 105 | value: string; 106 | key: string; 107 | } 108 | 109 | export interface ProfileMetadata extends GenericMetadata { 110 | name?: string; 111 | bio?: string; 112 | cover_picture?: string; 113 | attributes: AttributeData[]; 114 | } 115 | 116 | export interface ExtendedProfile extends Profile { 117 | missingAvatar?: boolean; 118 | missingCover?: boolean; 119 | } 120 | 121 | export interface ExtendedPost extends Post { 122 | profile: ExtendedProfile; 123 | profileSet?: boolean; 124 | originalProfile?: ExtendedProfile; 125 | } 126 | 127 | export interface ExtendedComment extends Comment { 128 | profile: ExtendedProfile; 129 | profileSet?: boolean; 130 | originalProfile?: ExtendedProfile; 131 | } 132 | 133 | export interface ExtendedMirror extends Mirror { 134 | profile: ExtendedProfile; 135 | profileSet?: boolean; 136 | originalProfile?: ExtendedProfile; 137 | } 138 | 139 | export type ExtendedPublication = ExtendedComment | ExtendedMirror | ExtendedPost 140 | 141 | export interface PublicationFetchResults { 142 | pageInfo: PaginatedResultInfo; 143 | items: ExtendedPublication[]; 144 | } 145 | 146 | /* Styles */ 147 | export type PublicationStyles = { 148 | publicationWrapper: {}, 149 | publicationContainer: {}, 150 | userProfileContainer: {}, 151 | missingAvatarPlaceholder: {}, 152 | smallAvatar: {}, 153 | postContentContainer: {}, 154 | postText: {}, 155 | metadataImage: {}, 156 | statsContainer: {}, 157 | statsDetailContainer: {}, 158 | statsDetailText: {}, 159 | postOwnerDetailsContainer: {}, 160 | postOwnerName: {}, 161 | postOwnerHandle: {}, 162 | timestamp: {}, 163 | activityIndicatorContainer: {}, 164 | mirrorContainer: {}, 165 | mirrorText: {}, 166 | video: {}, 167 | videoContainer: {} 168 | } 169 | 170 | export type ProfileHeaderStyles = { 171 | container: {}, 172 | blankHeader: {}, 173 | headerImage: {}, 174 | avatar: {}, 175 | followButton: {}, 176 | followingButton: {}, 177 | followingButtonText: {}, 178 | followButtonText: {}, 179 | userDetails: {}, 180 | name: {}, 181 | handle: {}, 182 | bio: {}, 183 | profileStats: {}, 184 | statsData: {}, 185 | statsHeader: {}, 186 | profileFollowingData: {}, 187 | profileFollowerData: {} 188 | } 189 | 190 | export type ProfileListItemStyles = { 191 | container: {}, 192 | avatarContainer: {}, 193 | avatar: {}, 194 | profileName: {}, 195 | profileHandle: {}, 196 | profileBio: {}, 197 | infoContainer: {}, 198 | followButtonContainer: {}, 199 | followButton: {}, 200 | followingButton: {}, 201 | followButtonText: {}, 202 | followingButtonText: {}, 203 | } 204 | 205 | export type FeedStyles = { 206 | container: {}, 207 | loadingIndicatorStyle: {}, 208 | noCommentsMessage: {} 209 | } 210 | 211 | export type SearchStyles = { 212 | containerStyle: {}, 213 | inputContainerStyle: {}, 214 | inputWrapperStyle: {}, 215 | inputStyle: {}, 216 | loadingIndicatorStyle : {} 217 | } 218 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ExtendedProfile 3 | } from './types' 4 | 5 | export function configureIpfsUrl(uri: string, IPFSGateway: string) { 6 | if (uri.startsWith('ar://')) { 7 | let result = uri.substring(5, uri.length) 8 | let modifiedUrl = `https://arweave.net/${result}` 9 | return modifiedUrl 10 | } else if (uri.startsWith('ipfs://')) { 11 | let result = uri.substring(7, uri.length) 12 | let modifiedUrl = `${IPFSGateway}/${result}` 13 | return modifiedUrl 14 | } else if (uri.startsWith('https://')) { 15 | return uri 16 | } else { 17 | return null 18 | } 19 | } 20 | 21 | export function returnIpfsPathOrUrl(uri: string, IPFSGateway: string) { 22 | if (uri.startsWith('ar://')) { 23 | let result = uri.substring(5, uri.length) 24 | let modifiedUrl = `https://arweave.net/${result}` 25 | return modifiedUrl 26 | } else if (uri.startsWith('ipfs://')) { 27 | let result = uri.substring(7, uri.length) 28 | let modifiedUrl = `${IPFSGateway}/${result}` 29 | return modifiedUrl 30 | } else { 31 | return uri 32 | } 33 | } 34 | 35 | export const debounce = (fn: Function, ms = 300) => { 36 | let timeoutId: ReturnType 37 | return function (this: any, ...args: any[]) { 38 | clearTimeout(timeoutId) 39 | timeoutId = setTimeout(() => fn.apply(this, args), ms) 40 | } 41 | } 42 | 43 | export function formatProfilePictures(profiles: ExtendedProfile[], IPFSGateway:string) { 44 | return profiles.map(profile => { 45 | let { picture, coverPicture } = profile 46 | if (picture && picture.__typename === 'MediaSet') { 47 | if (picture.original && picture.original.url) { 48 | picture.original.url = returnIpfsPathOrUrl(picture.original.url, IPFSGateway) 49 | } else { 50 | profile.missingAvatar = true 51 | } 52 | } 53 | if (coverPicture && coverPicture.__typename === 'MediaSet') { 54 | if (coverPicture.original.url) { 55 | coverPicture.original.url = returnIpfsPathOrUrl(coverPicture.original.url, IPFSGateway) 56 | } else { 57 | profile.missingCover = true 58 | } 59 | } 60 | return profile 61 | }) 62 | } 63 | 64 | export function filterMimeTypes(items: any[]) { 65 | items = items.filter(item => { 66 | // if (item.__typename === 'Mirror') return true 67 | const { metadata: { media } } = item 68 | if (media.length) { 69 | if (media[0].original) { 70 | if (media[0].original.mimeType === 'image/jpeg') return true 71 | if (media[0].original.mimeType === 'image/gif') return true 72 | if (media[0].original.mimeType === 'image/png') return true 73 | if (media[0].original.mimeType.includes('video')) return true 74 | return false 75 | } 76 | } else { 77 | return true 78 | } 79 | }) 80 | return items 81 | } 82 | 83 | export function configureMirrorAndIpfsUrl(items: any[], IPFSGateway: string) { 84 | return items.map(item => { 85 | if (item.profileSet) return item 86 | let { profile } = item 87 | if (item.__typename === 'Mirror') { 88 | if (item.mirrorOf) { 89 | item.originalProfile = profile 90 | item.stats = item.mirrorOf.stats 91 | profile = item.mirrorOf.profile 92 | } 93 | } 94 | if (profile?.picture?.uri) { 95 | profile.picture.original = { 96 | url: profile.picture.uri 97 | } 98 | } else if (profile?.picture?.__typename === 'MediaSet' && profile.picture.original && profile.picture.original.url) { 99 | const url = configureIpfsUrl(profile.picture.original.url, IPFSGateway) 100 | if (url) { 101 | profile.picture.original.url = url 102 | } else { 103 | profile.missingAvatar = true 104 | } 105 | } else { 106 | profile.missingAvatar = true 107 | } 108 | 109 | item.profile = profile 110 | item.profileSet = true 111 | return item 112 | }) 113 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ESNext", 4 | "removeComments": true, 5 | "preserveConstEnums": true, 6 | "sourceMap": true, 7 | "outDir": "./dist", 8 | "moduleResolution": "Node", 9 | "noUnusedLocals": false, 10 | "noUnusedParameters": false, 11 | "declaration": true, 12 | "jsx": "react-jsx", 13 | "downlevelIteration": true, 14 | "allowSyntheticDefaultImports": true, 15 | "skipLibCheck": true, 16 | "strict": true, 17 | // "noErrorTruncation": true 18 | }, 19 | "include": ["src/**/*"] 20 | } -------------------------------------------------------------------------------- /watcher.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | 3 | const srcDir = `./dist`; 4 | const destDir = `../RNLensExpoTesting/node_modules/@lens-protocol/react-native-lens-ui-kit/dist`; 5 | 6 | fs.watch("./dist/", {recursive: true}, () => { 7 | console.log('copying...') 8 | fs.cp(srcDir, destDir, { overwrite: true, recursive: true }, function() { 9 | console.log('copied') 10 | }) 11 | }) 12 | --------------------------------------------------------------------------------