├── .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 | 
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 |
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 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------