├── README.md
├── backend
├── .env.example
├── .gitignore
├── .yarn
│ ├── install-state.gz
│ └── releases
│ │ └── yarn-3.2.0.cjs
├── .yarnrc.yml
├── Procfile
├── README.md
├── bundle.js
├── cartoon.mp4
├── index.js
├── package.json
├── rollup.config.js
└── uploads
│ └── cartoon9.mp4
└── frontend
├── .firebaserc
├── .gitignore
├── README.md
├── firebase.json
├── index.html
├── package-lock.json
├── package.json
├── public
├── favicon.ico
├── logo.png
├── manifest.json
└── robots.txt
├── src
├── App.jsx
├── abi
│ └── LensHub.json
├── assets
│ ├── Bookmark.jsx
│ ├── Camera.jsx
│ ├── CaretDown.jsx
│ ├── Check.jsx
│ ├── Comment.jsx
│ ├── Community.jsx
│ ├── Compass.jsx
│ ├── Error.jsx
│ ├── Explore.jsx
│ ├── Eye.jsx
│ ├── EyeSlash.jsx
│ ├── Filmstrip.jsx
│ ├── FullScreen.jsx
│ ├── Globe.jsx
│ ├── Headphones.jsx
│ ├── Heart.jsx
│ ├── Home.jsx
│ ├── Logout.jsx
│ ├── Pause.jsx
│ ├── Play.jsx
│ ├── Profile.jsx
│ ├── Retweet.jsx
│ ├── Spinner.jsx
│ ├── Subscriptions.jsx
│ ├── Thumbnail.jsx
│ ├── VolumeSpeaker.jsx
│ ├── Webcam.jsx
│ ├── X.jsx
│ ├── avatar.png
│ ├── bg.png
│ ├── iris.svg
│ ├── logo-open.png
│ ├── logo.svg
│ ├── opensea.svg
│ ├── rainbow.png
│ └── settings.svg
├── components
│ ├── Apollo.jsx
│ ├── Button.jsx
│ ├── Card.jsx
│ ├── Checkbox.jsx
│ ├── Collect.jsx
│ ├── Comment.jsx
│ ├── Compose.jsx
│ ├── Feed.jsx
│ ├── Follow.jsx
│ ├── Icon.jsx
│ ├── Image.jsx
│ ├── Like.jsx
│ ├── Livepeer.jsx
│ ├── Livestream.jsx
│ ├── Login.jsx
│ ├── Mirror.jsx
│ ├── Modal.jsx
│ ├── Nav.jsx
│ ├── Post.jsx
│ ├── Profile.jsx
│ ├── Toast.jsx
│ ├── Unfollow.jsx
│ ├── Video.jsx
│ ├── VisibilitySelector.jsx
│ ├── Wallet.jsx
│ └── WalletButton.jsx
├── index.jsx
├── pages
│ ├── LandingPage.jsx
│ ├── NewProfile.jsx
│ ├── NotFound.jsx
│ ├── Outlet.jsx
│ ├── Post.jsx
│ └── User.jsx
├── react-app-env.d.ts
├── theme
│ ├── GlobalStyle.jsx
│ └── ThemeProvider.jsx
└── utils
│ ├── constants.jsx
│ ├── gradients.jsx
│ ├── index.jsx
│ ├── infuraClient.jsx
│ ├── litIntegration.jsx
│ ├── pollUntilIndexed.jsx
│ ├── queries.jsx
│ └── wallet.jsx
├── tsconfig.json
└── vite.config.js
/README.md:
--------------------------------------------------------------------------------
1 | # iris
2 |
3 | lens protocol social media implementation
4 |
5 | ## Getting Started
6 |
7 | You will need Metamask installed on Google Chrome, connected to Polygon Mumbai network
8 |
9 | `npm install` to install all dependencies
10 |
11 | ## Frontend
12 |
13 | `cd frontend`
14 |
15 | `npm install` install deps
16 |
17 | `npm start` run react app at http://localhost:3000/
18 |
19 | ### Gasless
20 |
21 | On localhost you must run app on port 4783 to enable gasless tx with Lens API
22 |
23 | `/frontend/.env` add `PORT=4783`
24 |
25 | ### Changing Chain
26 |
27 | Default chain on localhost is `mumbai`. If you want to change it change `/frontend/.env` to `VITE_CHAIN="polygon"`
28 |
29 |
30 | Remember all `.env` changes require an `npm start` restart.
31 |
32 | ## Deploying
33 |
34 | Testnet
35 | - change `.env` to `VITE_CHAIN="mumbai"`
36 | - `npm run build`
37 | - `firebase deploy --only hosting:testnet`
38 |
39 | Prod
40 | - change `.env` to `VITE_CHAIN="polygon"` or remove `VITE_CHAIN`
41 | - `npm run build`
42 | - `firebase deploy --only hosting:prod`
--------------------------------------------------------------------------------
/backend/.env.example:
--------------------------------------------------------------------------------
1 | LIVEPEER_API_KEY = ""
--------------------------------------------------------------------------------
/backend/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
--------------------------------------------------------------------------------
/backend/.yarn/install-state.gz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/irisxyz/iris/b852582acf9135e8c557afcd72e464bbef5cdbee/backend/.yarn/install-state.gz
--------------------------------------------------------------------------------
/backend/.yarnrc.yml:
--------------------------------------------------------------------------------
1 | nodeLinker: node-modules
2 |
3 | yarnPath: .yarn/releases/yarn-3.2.0.cjs
4 |
--------------------------------------------------------------------------------
/backend/Procfile:
--------------------------------------------------------------------------------
1 | web: node index.js
2 |
--------------------------------------------------------------------------------
/backend/README.md:
--------------------------------------------------------------------------------
1 | 1. Just yarn install
2 | 2. And then run `node ./index.js`
3 | 3. Create heroku account, install CLI
4 | 4. `heroku create -a irisxyz-abc`
5 | 5. `heroku git:remote -a irisxyz-abc`
6 | 6. Heroku deploy: `git subtree push --prefix backend heroku main`
--------------------------------------------------------------------------------
/backend/bundle.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | //import dotenv from 'dotenv';
4 | const express = require("express");
5 | const multer = require("multer");
6 | const cors = require("cors");
7 | const fs = require("fs");
8 | const { VideoNFT } = require("@livepeer/video-nft/dist/index.cjs.js");
9 | require("dotenv").config();
10 | const PORT = 3001;
11 |
12 | new VideoNFT({
13 | auth: { apiKey: process.env.LIVEPEER_API_KEY },
14 | endpoint: "https://livepeer.com",
15 | });
16 |
17 | const storage = multer.diskStorage({
18 | destination: (req, file, cb) => {
19 | cb(null, "uploads");
20 | },
21 | filename: (req, file, cb) => {
22 | const { originalname } = file;
23 | cb(null, originalname);
24 | },
25 | });
26 |
27 | const app = express();
28 | app.use(cors());
29 |
30 | const upload = multer({ storage });
31 |
32 | function printProgress(progress) {
33 | console.log(` - progress: ${100 * progress}%`);
34 | }
35 |
36 | async function maybeTranscode(sdk, asset) {
37 | const { possible, desiredProfile } = sdk.checkNftNormalize(asset);
38 | if (!possible || !desiredProfile) {
39 | if (!possible) {
40 | console.error(
41 | `Warning: Asset is larger than OpenSea file limit and can't be transcoded down since it's too large. ` +
42 | `It will still be stored in IPFS and referenced in the NFT metadata, so a proper application is still able to play it back. ` +
43 | `For more information check http://bit.ly/opensea-file-limit`
44 | );
45 | }
46 | return asset;
47 | }
48 |
49 | console.log(
50 | `File is too big for OpenSea 100MB limit (learn more at http://bit.ly/opensea-file-limit).`
51 | );
52 |
53 | console.log(
54 | `Transcoding asset to ${desiredProfile.name} at ${Math.round(
55 | desiredProfile.bitrate / 1024
56 | )} kbps bitrate`
57 | );
58 | return await sdk.nftNormalize(asset, printProgress);
59 | }
60 |
61 | app.post("/upload", upload.array("fileName"), async (req, res) => {
62 | console.log("Testing");
63 | console.log(req.files);
64 | const sdk = new VideoNFT({
65 | auth: { apiKey: process.env.LIVEPEER_API_KEY },
66 | endpoint: "https://livepeer.com",
67 | });
68 |
69 | let file = null;
70 | let asset;
71 | try {
72 | file = fs.createReadStream(req.files[0].path);
73 | console.log(file);
74 | console.log("Uploading file...");
75 | asset = await sdk.createAsset(req.files[0].path, file, printProgress);
76 | } finally {
77 | file?.close();
78 | }
79 |
80 | asset = await maybeTranscode(sdk, asset);
81 |
82 | console.log("Starting export...");
83 | let ipfs = await sdk.exportToIPFS(
84 | asset.id ?? "",
85 | JSON.parse(
86 | JSON.stringify({
87 | name: req.files[0].filename,
88 | description: `Livepeer video from asset ${JSON.stringify(
89 | req.files[0].filename
90 | )}`,
91 | image: `ipfs://bafkreidmlgpjoxgvefhid2xjyqjnpmjjmq47yyrcm6ifvoovclty7sm4wm`,
92 | properties: {},
93 | })
94 | ),
95 | printProgress
96 | );
97 | console.log(`Export successful! Result: \n${JSON.stringify(ipfs, null, 2)}`);
98 |
99 | console.log(
100 | `Mint your NFT at:\n` +
101 | `https://livepeer.com/mint-nft?tokenUri=${ipfs?.nftMetadataUrl}`
102 | );
103 | return res.send({ status: "OK", data: ipfs?.nftMetadataUrl });
104 | });
105 |
106 | app.listen(PORT, () => {
107 | console.log(`gm! localhost:${PORT}`);
108 | });
109 |
--------------------------------------------------------------------------------
/backend/cartoon.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/irisxyz/iris/b852582acf9135e8c557afcd72e464bbef5cdbee/backend/cartoon.mp4
--------------------------------------------------------------------------------
/backend/index.js:
--------------------------------------------------------------------------------
1 | //import dotenv from 'dotenv';
2 | const express = require("express");
3 | const multer = require("multer");
4 | const cors = require("cors");
5 | const fs = require("fs");
6 | const bodyParser = require('body-parser')
7 | const { VideoNFT } = require("@livepeer/video-nft/dist/index.cjs.js");
8 | require("dotenv").config();
9 | const PORT = 3001;
10 |
11 | const jsonParser = bodyParser.json()
12 | const axios = require("axios");
13 | const request = require("request");
14 |
15 |
16 | const sdk = new VideoNFT({
17 | auth: { apiKey: process.env.LIVEPEER_API_KEY },
18 | endpoint: "https://livepeer.com",
19 | });
20 |
21 | const storage = multer.diskStorage({
22 | destination: (req, file, cb) => {
23 | cb(null, "uploads");
24 | },
25 | filename: (req, file, cb) => {
26 | const { originalname } = file;
27 | cb(null, originalname);
28 | },
29 | });
30 |
31 | const app = express();
32 | app.use(cors());
33 |
34 | const upload = multer({ storage });
35 |
36 | function printProgress(progress) {
37 | console.log(` - progress: ${100 * progress}%`);
38 | }
39 |
40 | async function maybeTranscode(sdk, asset) {
41 | const { possible, desiredProfile } = sdk.checkNftNormalize(asset);
42 | if (!possible || !desiredProfile) {
43 | if (!possible) {
44 | console.error(
45 | `Warning: Asset is larger than OpenSea file limit and can't be transcoded down since it's too large. ` +
46 | `It will still be stored in IPFS and referenced in the NFT metadata, so a proper application is still able to play it back. ` +
47 | `For more information check http://bit.ly/opensea-file-limit`
48 | );
49 | }
50 | return asset;
51 | }
52 |
53 | console.log(
54 | `File is too big for OpenSea 100MB limit (learn more at http://bit.ly/opensea-file-limit).`
55 | );
56 |
57 | console.log(
58 | `Transcoding asset to ${desiredProfile.name} at ${Math.round(
59 | desiredProfile.bitrate / 1024
60 | )} kbps bitrate`
61 | );
62 | return await sdk.nftNormalize(asset, printProgress);
63 | }
64 |
65 | app.post('/new-stream', jsonParser, (req, res) => {
66 |
67 | console.log(req.body.wallet)
68 | console.log(req.body.handle)
69 |
70 |
71 | var options = {
72 | 'method': 'POST',
73 | 'url': 'https://livepeer.com/api/stream',
74 | 'headers': {
75 | 'content-type': 'application/json',
76 | 'authorization': `Bearer ${process.env.LIVEPEER_API_KEY}`
77 | },
78 | body: JSON.stringify({
79 | "name": `${req.body.wallet},${req.body.handle}`,
80 | "profiles": [
81 | {
82 | "name": "720p",
83 | "bitrate": 2000000,
84 | "fps": 30,
85 | "width": 1280,
86 | "height": 720
87 | },
88 | {
89 | "name": "480p",
90 | "bitrate": 1000000,
91 | "fps": 30,
92 | "width": 854,
93 | "height": 480
94 | },
95 | {
96 | "name": "360p",
97 | "bitrate": 500000,
98 | "fps": 30,
99 | "width": 640,
100 | "height": 360
101 | }
102 | ]
103 | })
104 |
105 | };
106 |
107 | request(options, function (error, response, body) {
108 | res.send(body)
109 | });
110 |
111 | });
112 |
113 | app.post("/upload", upload.array("fileName"), async (req, res) => {
114 | console.log("Testing");
115 | console.log(req.files);
116 | const sdk = new VideoNFT({
117 | auth: { apiKey: process.env.LIVEPEER_API_KEY },
118 | endpoint: "https://livepeer.com",
119 | });
120 |
121 | let file = null;
122 | let asset;
123 | try {
124 | file = fs.createReadStream(req.files[0].path);
125 | console.log(file);
126 | console.log("Uploading file...");
127 | asset = await sdk.createAsset(req.files[0].path, file, printProgress);
128 | } finally {
129 | file?.close();
130 | }
131 |
132 | asset = await maybeTranscode(sdk, asset);
133 |
134 | console.log("Starting export...");
135 | let ipfs = await sdk.exportToIPFS(
136 | asset.id ?? "",
137 | JSON.parse(
138 | JSON.stringify({
139 | name: req.files[0].filename,
140 | description: `Livepeer video from asset ${JSON.stringify(
141 | req.files[0].filename
142 | )}`,
143 | image: `ipfs://bafkreidmlgpjoxgvefhid2xjyqjnpmjjmq47yyrcm6ifvoovclty7sm4wm`,
144 | properties: {},
145 | })
146 | ),
147 | printProgress
148 | );
149 | console.log(`Export successful! Result: \n${JSON.stringify(ipfs, null, 2)}`);
150 |
151 | console.log(
152 | `Mint your NFT at:\n` +
153 | `https://livepeer.com/mint-nft?tokenUri=${ipfs?.nftMetadataUrl}`
154 | );
155 | return res.send({ status: "OK", data: ipfs?.nftMetadataUrl, ...ipfs });
156 | });
157 |
158 | app.listen(process.env.PORT || 3001, () => {
159 | console.log(`gm! localhost:${PORT}`);
160 | });
161 |
--------------------------------------------------------------------------------
/backend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "video-nft-server",
3 | "version": "1.0.0",
4 | "engines": {
5 | "node": "16.x"
6 | },
7 | "license": "MIT",
8 | "main": "index.js",
9 | "scripts": {
10 | "start": "node index.js"
11 | },
12 | "dependencies": {
13 | "@livepeer/video-nft": "^0.2.0",
14 | "axios": "^0.26.1",
15 | "body-parser": "^1.19.2",
16 | "cors": "^2.8.5",
17 | "dotenv": "^16.0.0",
18 | "express": "^4.17.3",
19 | "multer": "^1.4.4",
20 | "request": "^2.88.2"
21 | },
22 | "devDependencies": {
23 | "@types/express": "^4.17.13",
24 | "@types/node": "^17.0.23",
25 | "prettier": "2.6.1",
26 | "rollup": "^2.70.1",
27 | "ts-node": "^10.7.0",
28 | "typescript": "^4.6.3"
29 | },
30 | "packageManager": "yarn@3.2.0"
31 | }
32 |
--------------------------------------------------------------------------------
/backend/rollup.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | input: "index.js",
3 | output: {
4 | file: "bundle.js",
5 | format: "cjs",
6 | },
7 | };
8 |
--------------------------------------------------------------------------------
/backend/uploads/cartoon9.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/irisxyz/iris/b852582acf9135e8c557afcd72e464bbef5cdbee/backend/uploads/cartoon9.mp4
--------------------------------------------------------------------------------
/frontend/.firebaserc:
--------------------------------------------------------------------------------
1 | {
2 | "projects": {
3 | "default": "irisxyz"
4 | },
5 | "targets": {
6 | "irisxyz": {
7 | "hosting": {
8 | "prod": [
9 | "irisxyz"
10 | ],
11 | "testnet": [
12 | "iris-testnet"
13 | ]
14 | }
15 | }
16 | }
17 | }
--------------------------------------------------------------------------------
/frontend/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
25 | .env
26 |
27 | /.firebase
--------------------------------------------------------------------------------
/frontend/README.md:
--------------------------------------------------------------------------------
1 | # Getting Started with Create React App
2 |
3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
4 |
5 | ## Available Scripts
6 |
7 | In the project directory, you can run:
8 |
9 | ### `npm start`
10 |
11 | Runs the app in the development mode.\
12 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
13 |
14 | The page will reload when you make changes.\
15 | You may also see any lint errors in the console.
16 |
17 | ### `npm test`
18 |
19 | Launches the test runner in the interactive watch mode.\
20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
21 |
22 | ### `npm run build`
23 |
24 | Builds the app for production to the `build` folder.\
25 | It correctly bundles React in production mode and optimizes the build for the best performance.
26 |
27 | The build is minified and the filenames include the hashes.\
28 | Your app is ready to be deployed!
29 |
30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
31 |
32 | ### `npm run eject`
33 |
34 | **Note: this is a one-way operation. Once you `eject`, you can't go back!**
35 |
36 | If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
37 |
38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
39 |
40 | You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
41 |
42 | ## Learn More
43 |
44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
45 |
46 | To learn React, check out the [React documentation](https://reactjs.org/).
47 |
48 | ### Code Splitting
49 |
50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
51 |
52 | ### Analyzing the Bundle Size
53 |
54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
55 |
56 | ### Making a Progressive Web App
57 |
58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
59 |
60 | ### Advanced Configuration
61 |
62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
63 |
64 | ### Deployment
65 |
66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
67 |
68 | ### `npm run build` fails to minify
69 |
70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
71 |
--------------------------------------------------------------------------------
/frontend/firebase.json:
--------------------------------------------------------------------------------
1 | {
2 | "hosting": [
3 | {
4 | "target": "prod",
5 | "public": "build",
6 | "ignore": [
7 | "firebase.json",
8 | "**/.*",
9 | "**/node_modules/**"
10 | ],
11 | "rewrites": [
12 | {
13 | "source": "**",
14 | "destination": "/index.html"
15 | }
16 | ]
17 | },
18 | {
19 | "target": "testnet",
20 | "public": "build",
21 | "ignore": [
22 | "firebase.json",
23 | "**/.*",
24 | "**/node_modules/**"
25 | ],
26 | "rewrites": [
27 | {
28 | "source": "**",
29 | "destination": "/index.html"
30 | }
31 | ]
32 | }
33 | ]
34 | }
35 |
--------------------------------------------------------------------------------
/frontend/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 | iris
19 |
22 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@apollo/client": "^3.5.10",
7 | "@esbuild-plugins/node-globals-polyfill": "^0.1.1",
8 | "@livepeer/react": "^1.5.2",
9 | "@rainbow-me/rainbowkit": "^0.8.0",
10 | "@vitejs/plugin-react": "^2.2.0",
11 | "@vitejs/plugin-react-refresh": "^1.3.6",
12 | "cross-fetch": "^3.1.5",
13 | "dotenv": "^16.0.3",
14 | "ethers": "^5.7.2",
15 | "graphql": "^16.3.0",
16 | "hls.js": "^1.1.5",
17 | "ipfs-http-client": "^56.0.1",
18 | "moment": "^2.29.3",
19 | "omit-deep": "^0.3.0",
20 | "react": "^17.0.2",
21 | "react-dom": "^17.0.2",
22 | "react-router-dom": "^6.2.2",
23 | "react-string-replace": "^1.1.0",
24 | "styled-components": "^5.3.3",
25 | "uuid": "^8.3.2",
26 | "vite": "^3.2.4",
27 | "vite-plugin-svgr": "^2.2.2",
28 | "wagmi": "^0.8.5"
29 | },
30 | "scripts": {
31 | "start": "vite",
32 | "build": "vite build",
33 | "serve": "vite preview"
34 | },
35 | "eslintConfig": {
36 | "extends": [
37 | "react-app",
38 | "react-app/jest"
39 | ]
40 | },
41 | "browserslist": {
42 | "production": [
43 | ">0.2%",
44 | "not dead",
45 | "not op_mini all"
46 | ],
47 | "development": [
48 | "last 1 chrome version",
49 | "last 1 firefox version",
50 | "last 1 safari version"
51 | ]
52 | },
53 | "devDependencies": {
54 | "react-error-overlay": "6.0.9"
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/frontend/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/irisxyz/iris/b852582acf9135e8c557afcd72e464bbef5cdbee/frontend/public/favicon.ico
--------------------------------------------------------------------------------
/frontend/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/irisxyz/iris/b852582acf9135e8c557afcd72e464bbef5cdbee/frontend/public/logo.png
--------------------------------------------------------------------------------
/frontend/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo.png",
12 | "type": "image/png",
13 | "sizes": "128x128"
14 | }
15 | ],
16 | "start_url": ".",
17 | "display": "standalone",
18 | "theme_color": "#000000",
19 | "background_color": "#ffffff"
20 | }
21 |
--------------------------------------------------------------------------------
/frontend/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/frontend/src/App.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { Routes, Route } from "react-router-dom";
3 | import styled from "styled-components";
4 |
5 | import ApolloProvider from "./components/Apollo";
6 | import LivepeerProvider from "./components/Livepeer";
7 | import GlobalStyle from "./theme/GlobalStyle";
8 | import ThemeProvider from "./theme/ThemeProvider";
9 | import NotFound from "./pages/NotFound";
10 | import Outlet from "./pages/Outlet";
11 | import User from "./pages/User";
12 | import Post from "./pages/Post";
13 | import NewProfile from "./pages/NewProfile";
14 | import Profile from "./components/Profile";
15 | import Nav from "./components/Nav";
16 | import Wallet from "./components/Wallet";
17 | import Compose from "./components/Compose";
18 | import Login from "./components/Login";
19 | import Feed from "./components/Feed";
20 | import Card from "./components/Card";
21 | // import Livelinks from "./components/Livelinks";
22 | import logo from "./assets/iris.svg";
23 | // import LandingPage from './pages/LandingPage'
24 | import { CHAIN } from "./utils/constants";
25 | import { WalletContextProvider } from "./utils/wallet";
26 | import '@rainbow-me/rainbowkit/styles.css';
27 |
28 | import {
29 | getDefaultWallets,
30 | RainbowKitProvider,
31 | darkTheme
32 | } from '@rainbow-me/rainbowkit';
33 | import {
34 | chain,
35 | configureChains,
36 | createClient,
37 | WagmiConfig,
38 | } from 'wagmi';
39 | import { alchemyProvider } from 'wagmi/providers/alchemy';
40 | import { publicProvider } from 'wagmi/providers/public';
41 |
42 |
43 | const Container = styled.div`
44 | max-width: 1000px;
45 | padding: 0 1em 1em 1em;
46 | min-height: 90vh;
47 | box-sizing: border-box;
48 | margin: auto;
49 | @media (max-width: 768px) {
50 | padding: 0 0.5em 0.5em 0.5em;
51 | margin-bottom: 3em;
52 | }
53 | `;
54 |
55 | const LogoContainer = styled.div`
56 | display: flex;
57 | padding: 0.6em;
58 | gap: 8px;
59 | `;
60 |
61 | const Navbar = styled.nav`
62 | box-sizing: border-box;
63 | height: 50px;
64 | display: flex;
65 | justify-content: space-between;
66 | align-items: center;
67 | margin: 0.7em 0;
68 | `;
69 |
70 | const Columns = styled.div`
71 | display: flex;
72 | gap: 2em;
73 | `;
74 |
75 | const Sidebar = styled.div`
76 | width: 300px;
77 | height: 100%
78 | float: left;
79 | @media (max-width: 768px) {
80 | display: none;
81 | }
82 | `;
83 |
84 | const Content = styled.main`
85 | width: 100%;
86 | @media (min-width: 768px) {
87 | width: 700px;
88 | }
89 | `;
90 |
91 | const MobileNav = styled(Nav)`
92 | @media (min-width: 768px) {
93 | display: none;
94 | }
95 | `
96 |
97 | const Announcement = styled(Card)`
98 | margin-top: 1em;
99 | margin-bottom: 0.5em;
100 | background: #FFCBBB;
101 | border: #FF9776 1px solid;
102 | h4 {
103 | margin: 0;
104 | padding-bottom: .25em;
105 | color: #F66030;
106 | font-weight: 500;
107 |
108 | }
109 | `
110 |
111 | function App() {
112 | const [profile, setProfile] = useState({});
113 |
114 | // useEffect(() => {
115 | // const initLit = async () => {
116 | // const client = new LitJsSdk.LitNodeClient({
117 | // alertWhenUnauthorized: false,
118 | // debug: false,
119 | // });
120 | // await client.connect();
121 | // window.litNodeClient = client;
122 | // };
123 | // initLit();
124 | // }, []);
125 |
126 | const { chains, provider } = configureChains(
127 | import.meta.env.VITE_CHAIN === 'mumbai' ? [chain.polygonMumbai] : [chain.polygon],
128 | [
129 | alchemyProvider({ apiKey: import.meta.env.ALCHEMY_ID }),
130 | publicProvider()
131 | ]
132 | );
133 |
134 | const { connectors } = getDefaultWallets({
135 | appName: 'Iris',
136 | chains
137 | });
138 |
139 | const wagmiClient = createClient({
140 | autoConnect: true,
141 | connectors,
142 | provider
143 | })
144 |
145 | return (
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 | iris
159 |
160 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 | hello fren 🍑
173 | iris is still in early development so you may run into bugs as your explore the app. Handle with care 💌
174 |
175 | © iris
176 |
177 |
178 |
179 |
183 | {profile && profile.__typename && }
184 |
185 |
186 | }
187 | />
188 | { CHAIN === 'mumbai' && } />}
189 | } />
190 | }>
191 | } />
192 |
193 | }>
194 | } />
195 |
196 | } />
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 | );
208 | }
209 |
210 | export default App;
211 |
--------------------------------------------------------------------------------
/frontend/src/assets/Bookmark.jsx:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | const Path = styled.path`
4 | stroke: #747c90;
5 | stroke-width: 3px;
6 | transition: all 100ms ease-in-out;
7 | &:hover {
8 | stroke: #F28A56;
9 | cursor: pointer;
10 | }
11 | ${(p) => p.filled && `stroke: #F28A56;`}
12 | `;
13 |
14 | const Icon = ({ filled, ...props }) => {
15 | return (
16 |
32 | );
33 | };
34 |
35 | export default Icon;
36 |
--------------------------------------------------------------------------------
/frontend/src/assets/Camera.jsx:
--------------------------------------------------------------------------------
1 | const Icon = () => {
2 | return (
3 |
7 | );
8 | };
9 |
10 | export default Icon;
11 |
--------------------------------------------------------------------------------
/frontend/src/assets/CaretDown.jsx:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | const Path = styled.path`
4 | stroke: #747c90;
5 | fill: #747c90;
6 | stroke-width: 3px;
7 | transition: all 100ms ease-in-out;
8 | &:hover {
9 | stroke: #F28A56;
10 | fill: #F28A56;
11 | }
12 | `;
13 |
14 | const Icon = ({ filled, ...props }) => {
15 | return (
16 |
28 | );
29 | };
30 |
31 | export default Icon;
32 |
--------------------------------------------------------------------------------
/frontend/src/assets/Check.jsx:
--------------------------------------------------------------------------------
1 | const Icon = () => {
2 | return (
3 |
8 | );
9 | };
10 |
11 | export default Icon;
12 |
--------------------------------------------------------------------------------
/frontend/src/assets/Comment.jsx:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | const Path = styled.path`
4 | stroke: #747c90;
5 | stroke-width: 3px;
6 | transition: all 100ms ease-in-out;
7 | &:hover {
8 | stroke: #F28A56;
9 | cursor: pointer;
10 | }
11 | ${(p) => p.filled && `stroke: #F28A56;`}
12 | `;
13 |
14 | const Icon = ({ filled, ...props }) => {
15 | return (
16 |
32 | );
33 | };
34 |
35 | export default Icon;
36 |
--------------------------------------------------------------------------------
/frontend/src/assets/Community.jsx:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | const SVG = styled.svg`
4 | path {
5 | stroke: #747c90;
6 | ${(p) => p.filled && `stroke: #F28A56;`}
7 | stroke-width: 2.5px;
8 | transition: all 100ms ease-in-out;
9 | }
10 | transition: all 100ms ease-in-out;
11 | &:hover {
12 | cursor: pointer;
13 | path {
14 | stroke: #F28A56;
15 | }
16 | }
17 | ${(p) => p.filled && `stroke: #F28A56;`}
18 | `;
19 |
20 | const Icon = ({ ...props }) => {
21 | return (
22 |
48 | );
49 | };
50 |
51 | export default Icon;
52 |
53 |
--------------------------------------------------------------------------------
/frontend/src/assets/Compass.jsx:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | const SVG = styled.svg`
4 | path {
5 | stroke: #F28A56;
6 | stroke-width: 3px;
7 | transition: all 100ms ease-in-out;
8 | }
9 | transition: all 100ms ease-in-out;
10 | &:hover {
11 | cursor: pointer;
12 | path {
13 | stroke: #F28A56;
14 | }
15 | }
16 | ${(p) => p.filled && `stroke: #F28A56;`}
17 | `;
18 |
19 | const Icon = ({ ...props }) => {
20 | return (
21 |
37 | );
38 | };
39 |
40 | export default Icon;
41 |
--------------------------------------------------------------------------------
/frontend/src/assets/Error.jsx:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | const FillPath = styled.path`
4 | fill: ${p => p.theme.error};
5 | `
6 |
7 | const Icon = ({ ...props }) => {
8 | return (
9 |
12 | );
13 | };
14 |
15 | export default Icon;
16 |
--------------------------------------------------------------------------------
/frontend/src/assets/Explore.jsx:
--------------------------------------------------------------------------------
1 | const Icon = () => {
2 | return (
3 |
9 | );
10 | };
11 |
12 | export default Icon;
13 |
--------------------------------------------------------------------------------
/frontend/src/assets/Eye.jsx:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | const SVG = styled.svg`
4 | path {
5 | stroke: #747c90;
6 | stroke-width: 3px;
7 | transition: all 100ms ease-in-out;
8 | }
9 | transition: all 100ms ease-in-out;
10 | &:hover {
11 | cursor: pointer;
12 | path {
13 | stroke: #F28A56;
14 | }
15 | }
16 | ${(p) => p.filled && `stroke: #F28A56;`}
17 | `;
18 |
19 | const Icon = ({ ...props }) => {
20 | return (
21 |
25 | );
26 | };
27 |
28 | export default Icon;
29 |
30 |
--------------------------------------------------------------------------------
/frontend/src/assets/EyeSlash.jsx:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | const SVG = styled.svg`
4 | path {
5 | stroke: #747c90;
6 | stroke-width: 2.5px;
7 | transition: all 100ms ease-in-out;
8 | }
9 | transition: all 100ms ease-in-out;
10 | &:hover {
11 | cursor: pointer;
12 | path {
13 | stroke: #F28A56;
14 | }
15 | }
16 | ${(p) => p.filled && `stroke: #F28A56;`}
17 | `;
18 |
19 | const Icon = ({ ...props }) => {
20 | return (
21 |
28 | );
29 | };
30 |
31 | export default Icon;
32 |
--------------------------------------------------------------------------------
/frontend/src/assets/Filmstrip.jsx:
--------------------------------------------------------------------------------
1 | const Icon = () => {
2 | return (
3 |
13 | );
14 | };
15 |
16 | export default Icon;
17 |
--------------------------------------------------------------------------------
/frontend/src/assets/FullScreen.jsx:
--------------------------------------------------------------------------------
1 | const Icon = () => {
2 | return (
3 |
6 | );
7 | };
8 |
9 | export default Icon;
10 |
--------------------------------------------------------------------------------
/frontend/src/assets/Globe.jsx:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | const SVG = styled.svg`
4 | path {
5 | stroke: #747c90;
6 | stroke-width: 2px;
7 | transition: all 100ms ease-in-out;
8 | ${(p) => p.filled && `stroke: #F28A56;`}
9 | }
10 | transition: all 100ms ease-in-out;
11 | &:hover {
12 | cursor: pointer;
13 | path {
14 | stroke: #F28A56;
15 | }
16 | }
17 | ${(p) => p.filled && `stroke: #F28A56;`}
18 | `;
19 |
20 | const Icon = ({ ...props }) => {
21 | return (
22 |
28 | );
29 | };
30 |
31 | export default Icon;
32 |
--------------------------------------------------------------------------------
/frontend/src/assets/Headphones.jsx:
--------------------------------------------------------------------------------
1 | const Icon = () => {
2 | return (
3 |
6 | );
7 | };
8 |
9 | export default Icon;
10 |
--------------------------------------------------------------------------------
/frontend/src/assets/Heart.jsx:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | const Path = styled.path`
4 | stroke: #747c90;
5 | stroke-width: 3px;
6 | transition: all 100ms ease-in-out;
7 | &:hover {
8 | stroke: #F28A56;
9 | cursor: pointer;
10 | }
11 | ${(p) => p.filled && `stroke: #F28A56;`}
12 | `;
13 |
14 | const Icon = ({ filled, ...props }) => {
15 | return (
16 |
33 | );
34 | };
35 |
36 | export default Icon;
37 |
--------------------------------------------------------------------------------
/frontend/src/assets/Home.jsx:
--------------------------------------------------------------------------------
1 | const Icon = () => {
2 | return (
3 |
9 | );
10 | };
11 |
12 | export default Icon;
13 |
--------------------------------------------------------------------------------
/frontend/src/assets/Logout.jsx:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | const SVG = styled.svg`
4 | path {
5 | stroke: #747c90;
6 | stroke-width: 3px;
7 | transition: all 100ms ease-in-out;
8 | }
9 | transition: all 100ms ease-in-out;
10 | &:hover {
11 | cursor: pointer;
12 | path {
13 | stroke: #F28A56;
14 | }
15 | }
16 | ${(p) => p.filled && `stroke: #F28A56;`}
17 | `;
18 |
19 | const Icon = ({ ...props }) => {
20 | return (
21 |
26 | );
27 | };
28 |
29 | export default Icon;
30 |
--------------------------------------------------------------------------------
/frontend/src/assets/Pause.jsx:
--------------------------------------------------------------------------------
1 | const Icon = () => {
2 | return (
3 |
6 | );
7 | };
8 |
9 | export default Icon;
10 |
--------------------------------------------------------------------------------
/frontend/src/assets/Play.jsx:
--------------------------------------------------------------------------------
1 | const Icon = () => {
2 | return (
3 |
6 | );
7 | };
8 |
9 | export default Icon;
10 |
--------------------------------------------------------------------------------
/frontend/src/assets/Profile.jsx:
--------------------------------------------------------------------------------
1 | const Icon = () => {
2 | return (
3 |
9 | );
10 | };
11 |
12 | export default Icon;
13 |
--------------------------------------------------------------------------------
/frontend/src/assets/Retweet.jsx:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | const SVG = styled.svg`
4 | path {
5 | stroke: #747c90;
6 | stroke-width: 3px;
7 | transition: all 100ms ease-in-out;
8 | }
9 | transition: all 100ms ease-in-out;
10 | &:hover {
11 | cursor: pointer;
12 | path {
13 | stroke: #F28A56;
14 | }
15 | }
16 | ${(p) => p.filled && `stroke: #F28A56;`}
17 | `;
18 |
19 | const Icon = ({ ...props }) => {
20 | return (
21 |
51 | );
52 | };
53 |
54 | export default Icon;
55 |
--------------------------------------------------------------------------------
/frontend/src/assets/Spinner.jsx:
--------------------------------------------------------------------------------
1 | const Icon = () => {
2 | return (
3 |
117 | );
118 | };
119 |
120 | export default Icon;
121 |
--------------------------------------------------------------------------------
/frontend/src/assets/Subscriptions.jsx:
--------------------------------------------------------------------------------
1 |
2 | const Icon = () => {
3 | return (
4 |
7 |
8 | )
9 | }
10 |
11 | export default Icon
--------------------------------------------------------------------------------
/frontend/src/assets/Thumbnail.jsx:
--------------------------------------------------------------------------------
1 | const Icon = () => {
2 | return (
3 |
8 | );
9 | };
10 |
11 | export default Icon;
12 |
--------------------------------------------------------------------------------
/frontend/src/assets/VolumeSpeaker.jsx:
--------------------------------------------------------------------------------
1 | const Icon = () => {
2 | return (
3 |
6 | );
7 | };
8 |
9 | export default Icon;
10 |
--------------------------------------------------------------------------------
/frontend/src/assets/Webcam.jsx:
--------------------------------------------------------------------------------
1 | const Icon = () => {
2 | return (
3 |
9 | );
10 | };
11 |
12 | export default Icon;
13 |
--------------------------------------------------------------------------------
/frontend/src/assets/X.jsx:
--------------------------------------------------------------------------------
1 | const Icon = ({...props }) => {
2 | return (
3 |
7 | );
8 | };
9 |
10 | export default Icon;
11 |
--------------------------------------------------------------------------------
/frontend/src/assets/avatar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/irisxyz/iris/b852582acf9135e8c557afcd72e464bbef5cdbee/frontend/src/assets/avatar.png
--------------------------------------------------------------------------------
/frontend/src/assets/bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/irisxyz/iris/b852582acf9135e8c557afcd72e464bbef5cdbee/frontend/src/assets/bg.png
--------------------------------------------------------------------------------
/frontend/src/assets/iris.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/frontend/src/assets/logo-open.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/irisxyz/iris/b852582acf9135e8c557afcd72e464bbef5cdbee/frontend/src/assets/logo-open.png
--------------------------------------------------------------------------------
/frontend/src/assets/logo.svg:
--------------------------------------------------------------------------------
1 |
20 |
--------------------------------------------------------------------------------
/frontend/src/assets/opensea.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/frontend/src/assets/rainbow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/irisxyz/iris/b852582acf9135e8c557afcd72e464bbef5cdbee/frontend/src/assets/rainbow.png
--------------------------------------------------------------------------------
/frontend/src/assets/settings.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/frontend/src/components/Apollo.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {
3 | ApolloClient,
4 | ApolloLink,
5 | HttpLink,
6 | InMemoryCache,
7 | ApolloProvider,
8 | } from '@apollo/client'
9 | import { onError } from '@apollo/client/link/error'
10 | import { CHAIN } from '../utils/constants'
11 |
12 | const httpLink = new HttpLink({
13 | uri: CHAIN === 'polygon' ? 'https://api.lens.dev/' : 'https://api-mumbai.lens.dev/',
14 | fetch,
15 | });
16 |
17 | const authLink = new ApolloLink((operation, forward) => {
18 | // const token = window.authToken;
19 | const token = window.sessionStorage.getItem('lensToken')
20 | // console.log('jwt token:', token);
21 |
22 | // Use the setContext method to set the HTTP headers.
23 | operation.setContext({
24 | headers: {
25 | 'x-access-token': token ? `Bearer ${token}` : '',
26 | },
27 | });
28 |
29 | // Call the next link in the middleware chain.
30 | return forward(operation);
31 | });
32 |
33 | // Reset state for unauthenticated users TODO: make this less sus, ie setProfile({}) instead of location.reload()
34 | const errorLink = onError(({ operation, graphQLErrors, forward }) => {
35 | if (graphQLErrors && graphQLErrors[0].extensions.code === 'UNAUTHENTICATED') {
36 | window.sessionStorage.removeItem('lensToken')
37 | window.sessionStorage.removeItem('signature')
38 | // window.location.reload()
39 | console.log('User token expired or was not authenticated')
40 | }
41 | return
42 | });
43 |
44 | export const client = new ApolloClient({
45 | link: ApolloLink.from([authLink, errorLink, httpLink]), // authLink.concat(errorLink).concat(httpLink),
46 | cache: new InMemoryCache(),
47 | });
48 |
49 | function Apollo({ children }) {
50 | return { children }
51 | }
52 |
53 | export default Apollo
--------------------------------------------------------------------------------
/frontend/src/components/Button.jsx:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | const Button = styled.button`
4 | border: none;
5 | border-radius: 6px;
6 | padding: 0.6em 2em;
7 | font-family: ${(p) => p.theme.font};
8 | font-weight: 500;
9 | color: ${(p) => p.theme.textLight};
10 | background: ${(p) => p.theme.primary};
11 | letter-spacing: 0.02em;
12 | transition: all 100ms;
13 | :hover {
14 | background: ${(p) => p.theme.primaryHover};
15 | cursor: pointer;
16 | }
17 | :focus {
18 | box-shadow: 0px 2px 2px -1px rgba(0, 0, 0, 0.12), 0px 0px 0px 3px #D25D38;
19 | outline: none;
20 | }
21 | :disabled {
22 | opacity: 35%;
23 | cursor: inherit;
24 | }
25 | :disabled:hover {
26 | background: ${(p) => p.theme.primary};
27 | }
28 | `;
29 |
30 |
31 | export const RoundedButton = styled(Button)`
32 | border-radius: 5em;
33 | padding: 0.6em 2em;
34 | color: black;
35 | background: ${(p) => p.theme.textLight};
36 | letter-spacing: 0.02em;
37 | transition: all 100ms;
38 | :hover {
39 | background: #ffe8e8;
40 | }
41 | :focus {
42 | box-shadow: 0px 2px 2px -1px rgba(0, 0, 0, 0.72), 0px 0px 0px 3px #D25D38;
43 | outline: none;
44 | }
45 | `;
46 |
47 | export const OutlineButton = styled(Button)`
48 | width: 9em;
49 | color: ${(p) => p.theme.primary};
50 | background: ${(p) => p.theme.textLight};
51 | outline: 2px solid ${(p) => p.theme.primary};
52 | outline-offset: -2px;
53 | :hover span {
54 | display: none;
55 | }
56 | :hover:before {
57 | color: ${(p) => p.theme.textLight};
58 | content: "Unfollow";
59 | }
60 | `;
61 |
62 | export default Button;
63 |
--------------------------------------------------------------------------------
/frontend/src/components/Card.jsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 |
3 | const Card = styled.div`
4 | background: #fff;
5 | box-shadow: 0px 2px 5px rgba(200, 176, 178, 0.6);
6 | border-radius: 8px;
7 | box-sizing: border-box;
8 | padding: ${p => p.padding || '1em'};
9 | width: 100%;
10 |
11 | @media (max-width: 768px) {
12 | padding: 0.75em;
13 | overflow-wrap: break-word;
14 | word-wrap: break-word;
15 | word-break: break-word;
16 | hyphens: auto;
17 | }
18 |
19 | `
20 |
21 | export default Card
--------------------------------------------------------------------------------
/frontend/src/components/Checkbox.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 | import styled from 'styled-components'
3 |
4 | const Wrapper = styled.div`
5 | width: fit-content;
6 | display: flex;
7 | flex-flow: wrap row;
8 | align-items: center;
9 | justify-content: flex-start;
10 | gap: 0.6rem;
11 | cursor: pointer;
12 | `
13 |
14 | const Input = styled.input`;
15 | width: 1.625rem;
16 | height: 1.625rem;
17 | margin: 0;
18 | padding: 0;
19 | opacity: 0;
20 | position: absolute;
21 | transform: translateX(-100%);
22 | pointer-events: none;
23 | visibility: hidden;
24 | `
25 |
26 | const Button = styled.button`
27 | width: 1.625rem;
28 | height: 1.625rem;
29 | margin: 0;
30 | padding: 0;
31 | display: flex;
32 | cursor: pointer;
33 | align-items: center;
34 | justify-content: center;
35 | outline-style: none;
36 | border: none;
37 | border-radius: .25rem;
38 | background-color: hsl(0, 0%, 100%);
39 | box-shadow: hsl(0, 0%, 0%, 14%) 0 2px 6px;
40 |
41 | &:focus-within,
42 | &:focus-visible {
43 | outline: .125rem solid hsl(228, 24%, 22%);
44 | outline-offset: .125rem
45 | }
46 | `
47 |
48 | const Span = styled.span`
49 | width: 100%;
50 | height: 100%;
51 | opacity: 0;
52 | display: flex;
53 | align-items: center;
54 | justify-content: center;
55 | line-height: 1;
56 | transition-property: opacity;
57 | transition-duration: .16s;
58 | transition-timing-function: ease;
59 |
60 | ${p => p.checked && 'opacity: 1;'}
61 | `
62 |
63 | const Label = styled.label`
64 | cursor: pointer;
65 | `
66 |
67 | const Checkbox = ({ children }) => {
68 | const [checked, setChecked] = useState(false)
69 | console.log(checked)
70 | return setChecked(!checked)}>
71 |
72 |
75 |
76 |
77 | }
78 |
79 | export default Checkbox
--------------------------------------------------------------------------------
/frontend/src/components/Collect.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 | import { useMutation } from '@apollo/client'
3 | import { utils } from 'ethers'
4 | import { useAccount, useSigner } from 'wagmi'
5 | import { CREATE_COLLECT_TYPED_DATA, BROADCAST } from '../utils/queries'
6 | import omitDeep from 'omit-deep'
7 | import Bookmark from '../assets/Bookmark'
8 | import Community from '../assets/Community'
9 | import pollUntilIndexed from '../utils/pollUntilIndexed'
10 | import { RoundedButton } from './Button'
11 | import { useWallet } from '../utils/wallet'
12 |
13 | function Collect({ profileId, publicationId, collected, stats, isCommunity, isCta, setToastMsg }) {
14 | const { lensHub } = useWallet()
15 | const { data: signer } = useSigner()
16 | const { address } = useAccount()
17 | const [createCollectTyped, createCollectTypedData] = useMutation(CREATE_COLLECT_TYPED_DATA)
18 | const [broadcast, broadcastData] = useMutation(BROADCAST)
19 | const [savedTypedData, setSavedTypedData] = useState({})
20 | const [apiError, setApiError] = useState('')
21 |
22 | const handleClick = async (e) => {
23 | e.stopPropagation()
24 | const collectReq = {
25 | publicationId: publicationId,
26 | };
27 | try {
28 | await createCollectTyped({
29 | variables: {
30 | request: collectReq,
31 | },
32 | });
33 | }
34 | catch (err) {
35 | alert(err)
36 | setApiError(apiError)
37 | }
38 | };
39 |
40 | useEffect(() => {
41 | if (!createCollectTypedData.data) return;
42 |
43 | const handleCreate = async () => {
44 |
45 | const typedData = createCollectTypedData.data.createCollectTypedData.typedData;
46 |
47 | const { domain, types, value } = typedData;
48 |
49 | const signature = await signer._signTypedData(
50 | omitDeep(domain, "__typename"),
51 | omitDeep(types, "__typename"),
52 | omitDeep(value, "__typename")
53 | );
54 |
55 | setToastMsg({ type: 'loading', msg: 'Transaction indexing...' })
56 |
57 | setSavedTypedData({
58 | ...typedData,
59 | signature
60 | })
61 |
62 | broadcast({
63 | variables: {
64 | request: {
65 | id: createCollectTypedData.data.createCollectTypedData.id,
66 | signature
67 | }
68 | }
69 | })
70 | }
71 | handleCreate();
72 |
73 | }, [createCollectTypedData.data]);
74 |
75 |
76 | useEffect(() => {
77 | if (!broadcastData.data) return;
78 | const processBroadcast = async () => {
79 |
80 | if (broadcastData.data.broadcast.__typename === 'RelayError') {
81 | console.log('asking user to pay for gas because error', broadcastData.data.broadcast.reason)
82 |
83 | const { v, r, s } = utils.splitSignature(savedTypedData.signature);
84 |
85 | const tx = await lensHub.collectWithSig({
86 | collector: address,
87 | profileId: savedTypedData.value.profileId,
88 | pubId: savedTypedData.value.pubId,
89 | data: savedTypedData.value.data,
90 | sig: {
91 | v,
92 | r,
93 | s,
94 | deadline: savedTypedData.value.deadline,
95 | },
96 | },
97 | { gasLimit: 1000000 }
98 | );
99 |
100 | console.log('collect: tx hash', tx.hash);
101 | await pollUntilIndexed(tx.hash)
102 | console.log('collect: success')
103 | setToastMsg({ type: 'success', msg: 'Transaction indexed' })
104 |
105 | return;
106 | }
107 |
108 | const txHash = broadcastData.data.broadcast.txHash
109 | console.log('collect: tx hash', txHash);
110 | if (!txHash) return;
111 | await pollUntilIndexed(txHash)
112 | console.log('collect: success')
113 | setToastMsg({ type: 'success', msg: 'Transaction indexed' })
114 | }
115 | processBroadcast()
116 |
117 | }, [broadcastData.data])
118 |
119 | if(isCta) return {collected ? 'Joined' : 'Join Community'}
120 |
121 | return (
122 |
123 | {isCommunity
124 | ?
125 | :
126 | }
127 |
{ stats.totalAmountOfCollects }
128 |
129 | );
130 | }
131 |
132 | export default Collect;
133 |
--------------------------------------------------------------------------------
/frontend/src/components/Comment.jsx:
--------------------------------------------------------------------------------
1 | import CommentIcon from '../assets/Comment'
2 |
3 | function Comment({ publicationId, stats }) {
4 | return (
5 |
6 |
7 |
{ stats.totalAmountOfComments }
8 |
9 | );
10 | }
11 |
12 | export default Comment;
13 |
--------------------------------------------------------------------------------
/frontend/src/components/Compose.jsx:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useState } from 'react'
2 | import styled from 'styled-components'
3 | import { useMutation } from '@apollo/client'
4 | import { utils } from 'ethers'
5 | import omitDeep from 'omit-deep'
6 |
7 | import Button from './Button'
8 | import Card from './Card'
9 | import Image from './Image'
10 | import Video from './Video'
11 | import Filmstrip from '../assets/Filmstrip'
12 | import ImageIcon from '../assets/Thumbnail'
13 | import { CREATE_POST_TYPED_DATA, CREATE_COMMENT_TYPED_DATA, BROADCAST } from '../utils/queries'
14 | import pollUntilIndexed from '../utils/pollUntilIndexed'
15 | import { handleCompose } from '../utils/litIntegration'
16 | import { CHAIN } from '../utils/constants'
17 | import VisibilitySelector from './VisibilitySelector'
18 | import Toast from './Toast'
19 | import { useWallet } from '../utils/wallet'
20 | import { useSigner } from 'wagmi'
21 |
22 | const StyledCard = styled(Card)`
23 | width: 100%;
24 | display: inline-block;
25 | margin-bottom: 1em;
26 | `
27 |
28 | const TextArea = styled.textarea`
29 | border: none;
30 | border-radius: 6px;
31 | font-family: ${p => p.theme.font};
32 | overflow: auto;
33 | outline: none;
34 | padding: 0.3em;
35 | margin-bottom: 0.4em;
36 |
37 | -webkit-box-shadow: none;
38 | -moz-box-shadow: none;
39 | box-shadow: none;
40 |
41 | resize: none; /*remove the resize handle on the bottom right*/
42 | box-sizing: border-box;
43 | resize: none;
44 | font-size: 1em;
45 | height: ${p => p.height || 3}em;
46 | width: 100%;
47 | padding-bottom: 1em;
48 | color: #000;
49 | transition: all 100ms ease-in-out;
50 |
51 | &:focus {
52 | background: ${p => p.theme.darken2};
53 | }
54 | `
55 |
56 | const InputWrapper = styled.div`
57 | margin-bottom: 0.4em;
58 | `;
59 |
60 | const FileInput = styled.input`
61 | opacity: 0;
62 | width: 0.1px;
63 | height: 0.1px;
64 | position: absolute;
65 | `
66 |
67 | const CustomLabel = styled.label`
68 | border: none;
69 | border-radius: 6px;
70 | padding: 0.9em 0.3em 0.1em;
71 | display: inline-block;
72 | font-family: ${p => p.theme.font};
73 | font-weight: 500;
74 | font-size: 0.8em;
75 | color: ${p => p.theme.textLight};
76 | letter-spacing: 0.02em;
77 | transition: all 100ms;
78 | :focus {
79 | box-shadow: 0px 2px 2px -1px rgba(0, 0, 0, 0.12), 0px 0px 0px 3px #D25D38;
80 | outline: none;
81 | }
82 | svg: hover {
83 | cursor: pointer;
84 | }
85 | svg: hover path {
86 | stroke: ${p => p.theme.primaryHover};
87 | }
88 | `
89 |
90 | const PostButtonWrapper = styled.div`
91 | align-items: right;
92 | text-align: right;
93 | `
94 |
95 | const Actions = styled.div`
96 | display: flex;
97 | align-items: center;
98 | `
99 |
100 | const videoFileTypes = ['.mp4','.mov','.webm','.3gpp','.3gpp2','.flv','.mpeg']
101 |
102 | const imageFileTypes = ['.jpg','.jpeg','.png','.gif']
103 |
104 | const Compose = ({
105 | profileId,
106 | profileName,
107 | cta,
108 | placeholder,
109 | replyTo,
110 | isPost,
111 | isCommunity,
112 | isComment,
113 | }) => {
114 | const { data: signer } = useSigner()
115 | const { lensHub } = useWallet()
116 | const [description, setDescription] = useState('')
117 | const [selectedVisibility, setSelectedVisibility] = useState('public')
118 | const [mutatePostTypedData, typedPostData] = useMutation(CREATE_POST_TYPED_DATA)
119 | const [mutateCommentTypedData, typedCommentData] = useMutation(CREATE_COMMENT_TYPED_DATA)
120 | const [broadcast, broadcastData] = useMutation(BROADCAST)
121 | const [savedTypedData, setSavedTypedData] = useState({})
122 | const [showModal, setShowModal] = useState(false)
123 | const [toastMsg, setToastMsg] = useState({})
124 |
125 | // Uploading Video
126 | const [videoUploading, setVideoUploading] = useState(false);
127 | const [selectedFile, setSelectedFile] = useState("");
128 | const [video, setVideo] = useState("")
129 | const [videoNftMetadata, setVideoNftMetadata] = useState({})
130 |
131 | const videoUpload = async () => {
132 | setVideoUploading(true)
133 | const formData = new FormData();
134 | console.log(selectedFile)
135 | formData.append(
136 | "fileName",
137 | selectedFile,
138 | selectedFile.name
139 | );
140 |
141 | const response = await fetch('https://irisxyz.herokuapp.com/upload', { method: "POST", body: formData, mode: "cors" });
142 | const data = await response.json();
143 |
144 | console.log(data);
145 |
146 | // console.log("The nftmetadataURL ", data["nftMetadataGatewayUrl"])
147 |
148 | // Get metadata from livepeer
149 | const responseVidNftMetadata = await fetch(data["nftMetadataGatewayUrl"], { method: "GET" });
150 | const vidNftData = await responseVidNftMetadata.json();
151 |
152 | setVideoNftMetadata(vidNftData)
153 | console.log("VideoNFTMetaData :", vidNftData)
154 |
155 | setVideoUploading(false)
156 |
157 |
158 | // console.log(data);
159 | // const ipfs = await fetch(`https://ipfs.io/${data.data.replace(":", "")}`);
160 | // const nftMetadata = await ipfs.json()
161 | // console.log(nftMetadata);
162 | // setVideo(`https://ipfs.io/${nftMetadata.properties.video.replace(":", "")}`)
163 |
164 | }
165 |
166 | const handleSubmit = async () => {
167 | await handleCompose({description, lensHub, profileId, profileName, selectedVisibility, replyTo, mutateCommentTypedData, mutatePostTypedData})
168 | }
169 |
170 | useEffect(() => {
171 | const processPost = async (data) => {
172 | const { domain, types, value } = data.typedData
173 |
174 | const signature = await signer._signTypedData(
175 | omitDeep(domain, '__typename'),
176 | omitDeep(types, '__typename'),
177 | omitDeep(value, '__typename'),
178 | )
179 |
180 | setToastMsg({type: 'loading', msg: 'Transaction indexing...'})
181 |
182 | setSavedTypedData({
183 | ...data.typedData,
184 | signature,
185 | })
186 |
187 | broadcast({
188 | variables: {
189 | request: {
190 | id: data.id,
191 | signature,
192 | }
193 | }
194 | })
195 |
196 | }
197 | if (typedPostData.data) processPost(typedPostData.data.createPostTypedData);
198 | else if (typedCommentData.data) processPost(typedCommentData.data.createCommentTypedData);
199 |
200 | }, [typedPostData.data, typedCommentData.data])
201 |
202 | useEffect(() => {
203 | if (!broadcastData.data) return;
204 | const processBroadcast = async () => {
205 |
206 | if (broadcastData.data.broadcast.__typename === 'RelayError') {
207 | console.log('asking user to pay for gas because error', broadcastData.data.broadcast.reason)
208 |
209 | const { v, r, s } = utils.splitSignature(savedTypedData.signature);
210 |
211 | const tx = await lensHub.postWithSig({
212 | profileId: savedTypedData.value.profileId,
213 | contentURI: savedTypedData.value.contentURI,
214 | collectModule: savedTypedData.value.collectModule,
215 | collectModuleInitData: savedTypedData.value.collectModuleInitData,
216 | referenceModule: savedTypedData.value.referenceModule,
217 | referenceModuleInitData: savedTypedData.value.referenceModuleInitData,
218 | sig: {
219 | v,
220 | r,
221 | s,
222 | deadline: savedTypedData.value.deadline,
223 | },
224 | });
225 |
226 | console.log('create post: tx hash', tx.hash);
227 | await pollUntilIndexed(tx.hash)
228 | setShowModal(false)
229 | setDescription('')
230 | setToastMsg({type: 'success', msg: 'Transaction indexed'})
231 | return;
232 | }
233 |
234 | const txHash = broadcastData.data.broadcast.txHash
235 | console.log('create post: tx hash', txHash);
236 | if (!txHash) return;
237 | await pollUntilIndexed(txHash)
238 | setShowModal(false)
239 | setDescription('')
240 | setToastMsg({type: 'success', msg: 'Transaction indexed'})
241 | }
242 | processBroadcast()
243 |
244 | }, [broadcastData.data])
245 |
246 | const isValidFileType = (validFileTypes, file) => {
247 | if (!file.type) {
248 | return false;
249 | }
250 | const fileType = "." + file.type.split("/").pop();
251 | return validFileTypes.includes(fileType);
252 | }
253 |
254 | return (
255 | <>
256 | {toastMsg.msg}
257 |
258 |
300 | >
301 | )
302 | }
303 |
304 | export default Compose
--------------------------------------------------------------------------------
/frontend/src/components/Feed.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 | import { useLazyQuery } from '@apollo/client'
3 | import { GET_TIMELINE, EXPLORE_PUBLICATIONS } from '../utils/queries'
4 | import Post from '../components/Post'
5 | import Card from '../components/Card'
6 |
7 |
8 | function Feed({ profile = {}, isExplore }) {
9 | const [publications, setPublications] = useState([]);
10 |
11 | const [getTimeline, timelineData] = useLazyQuery(GET_TIMELINE);
12 | const [explorePublications, explorePublicationsData] = useLazyQuery(EXPLORE_PUBLICATIONS);
13 |
14 | useEffect(() => {
15 | if (!profile.id || isExplore) {
16 | if (publications.length > 0) return;
17 | explorePublications({
18 | variables: {
19 | request: {
20 | sortCriteria: 'TOP_COLLECTED',
21 | limit: 10,
22 | },
23 | reactionRequest: profile.id ? { profileId: profile.id } : null,
24 | },
25 | })
26 | return
27 | };
28 |
29 | if (isExplore) return;
30 | getTimeline({
31 | variables: {
32 | request: { profileId: profile.id },
33 | reactionRequest: { profileId: profile.id },
34 | },
35 | })
36 | }, [getTimeline, profile])
37 |
38 | useEffect(() => {
39 | if (!timelineData.data) return;
40 |
41 | if (timelineData.data.timeline.items.length < 1) {
42 | return;
43 | }
44 |
45 | // console.log('timeline loaded')
46 |
47 | const pubIds = {}
48 | const pubs = []
49 |
50 | timelineData.data.timeline.items.forEach((post) => {
51 | if (pubIds[post.id]) return;
52 | else {
53 | pubIds[post.id] = true
54 | pubs.push(post)
55 | }
56 | })
57 |
58 | setPublications(pubs);
59 |
60 | }, [timelineData.data]);
61 |
62 | useEffect(() => {
63 | if (profile.id && !isExplore) return;
64 | if (!explorePublicationsData.data) return;
65 |
66 | if (publications.length > 0) return;
67 |
68 | if (explorePublicationsData.data.explorePublications.items.length < 1) {
69 | return;
70 | }
71 |
72 | setPublications(explorePublicationsData.data.explorePublications.items);
73 | }, [explorePublicationsData.data]);
74 |
75 | return <>
76 | {!profile.id && Popular Posts
}
77 |
78 | {publications.map((post) => {
79 | return ;
80 | })}
81 |
82 | >
83 | }
84 |
85 | export default Feed;
86 |
--------------------------------------------------------------------------------
/frontend/src/components/Follow.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 | import { useMutation } from '@apollo/client'
3 | import { utils } from 'ethers'
4 | import { useAccount, useSigner } from 'wagmi'
5 | import { CREATE_FOLLOW_TYPED_DATA, BROADCAST } from '../utils/queries'
6 | import omitDeep from 'omit-deep'
7 | import Button from './Button'
8 | import Toast from './Toast'
9 | import pollUntilIndexed from '../utils/pollUntilIndexed'
10 | import { useWallet } from '../utils/wallet'
11 |
12 | // profile being the user being viewed, profileId the id of the user using the app
13 | function Follow({ profile = {}, profileId }) {
14 | const { data: signer } = useSigner()
15 | const { address } = useAccount()
16 | const { lensHub} = useWallet()
17 | const [toastMsg, setToastMsg] = useState('')
18 | const [createFollowTyped, createFollowTypedData] = useMutation(CREATE_FOLLOW_TYPED_DATA, {
19 | onError(error){
20 | setToastMsg({ type: 'error', msg: error.message })
21 | }
22 | });
23 | const [broadcast, broadcastData] = useMutation(BROADCAST)
24 | const [savedTypedData, setSavedTypedData] = useState({})
25 |
26 | const followRequest = {
27 | profile: profile.id,
28 | followModule: null,
29 | }
30 |
31 | if (profile?.followModule?.type === 'ProfileFollowModule') {
32 | followRequest.followModule = {
33 | profileFollowModule: {
34 | profileId: profileId
35 | }
36 | }
37 | }
38 |
39 | const handleClick = async () => {
40 | // if (profile.followModule !== null) {
41 | // const followSubscriptionRequest = [
42 | // {
43 | // profile: profile.id,
44 | // followModule: {
45 | // feeFollowModule: {
46 | // amount: {
47 | // currency: "0x9c3c9283d3e44854697cd22d3faa240cfb032889",
48 | // value: profile.followModule.amount.value,
49 | // },
50 | // },
51 | // },
52 | // },
53 | // ];
54 | // createFollowTyped({
55 | // variables: {
56 | // request: {
57 | // follow: followSubscriptionRequest,
58 | // },
59 | // },
60 | // });
61 | // } else {
62 | createFollowTyped({
63 | variables: {
64 | request: {
65 | follow: [followRequest],
66 | },
67 | },
68 | });
69 |
70 | // }
71 | };
72 |
73 | useEffect(() => {
74 | if (!createFollowTypedData.data) return;
75 |
76 | const handleCreate = async () => {
77 | const typedData = createFollowTypedData.data.createFollowTypedData.typedData;
78 | const { domain, types, value } = typedData;
79 |
80 | const signature = await signer._signTypedData(
81 | omitDeep(domain, "__typename"),
82 | omitDeep(types, "__typename"),
83 | omitDeep(value, "__typename")
84 | );
85 | setToastMsg({ type: 'loading', msg: 'Transaction indexing...' })
86 |
87 | setSavedTypedData({
88 | ...typedData,
89 | signature
90 | })
91 |
92 | broadcast({
93 | variables: {
94 | request: {
95 | id: createFollowTypedData.data.createFollowTypedData.id,
96 | signature
97 | }
98 | }
99 | })
100 |
101 | };
102 |
103 | handleCreate();
104 | }, [createFollowTypedData.data])
105 |
106 |
107 | useEffect(() => {
108 | if (!broadcastData.data) return;
109 | const processBroadcast = async () => {
110 |
111 | if (broadcastData.data.broadcast.__typename === 'RelayError') {
112 | console.log('asking user to pay for gas because error', broadcastData.data.broadcast.reason)
113 |
114 | const { v, r, s } = utils.splitSignature(savedTypedData.signature);
115 |
116 | const tx = await lensHub.followWithSig(
117 | {
118 | follower: address,
119 | profileIds: savedTypedData.value.profileIds,
120 | datas: savedTypedData.value.datas,
121 | sig: {
122 | v,
123 | r,
124 | s,
125 | deadline: savedTypedData.value.deadline,
126 | },
127 | },
128 | { gasLimit: 1000000 }
129 | );
130 |
131 | console.log('follow: tx hash', tx.hash);
132 | await pollUntilIndexed(tx.hash)
133 | console.log('follow: success')
134 | setToastMsg({ type: 'success', msg: 'Transaction indexed' })
135 | return;
136 | }
137 |
138 | const txHash = broadcastData.data.broadcast.txHash
139 | console.log('follow: tx hash', txHash);
140 | if (!txHash) return;
141 | await pollUntilIndexed(txHash)
142 | console.log('follow: success')
143 | setToastMsg({ type: 'success', msg: 'Transaction indexed' })
144 | }
145 | processBroadcast()
146 |
147 | }, [broadcastData.data])
148 |
149 | return (
150 |
151 | {toastMsg.msg}
152 |
153 |
154 | );
155 | }
156 |
157 | export default Follow;
158 |
--------------------------------------------------------------------------------
/frontend/src/components/Icon.jsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/irisxyz/iris/b852582acf9135e8c557afcd72e464bbef5cdbee/frontend/src/components/Icon.jsx
--------------------------------------------------------------------------------
/frontend/src/components/Image.jsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 | import X from '../assets/X';
3 |
4 | const Container = styled.div`
5 | display: flex;
6 | justify-content: center;
7 | `;
8 |
9 | const CloseButton = styled.button`
10 | color: white;
11 | padding-top: 3px;
12 | background-color: rgba(0, 0, 0, 0.5);
13 | border: none;
14 | border-radius: 4px;
15 | position: absolute;
16 | top: 5px;
17 | right: 5px;
18 | :hover {
19 | cursor: pointer;
20 | background-color:rgba(100, 100, 100, 0.5);
21 | }
22 | `
23 |
24 | const Img = styled.img`
25 | max-height: 18em;
26 | border-radius: 6px;
27 | filter: drop-shadow(0px 4px 4px rgba(0, 0, 0, 0.25));
28 | `
29 |
30 | const ImageWrapper = styled.div`
31 | position: relative;
32 | display: inline-block;
33 | `;
34 |
35 | const Image = ({src, hasCloseButton, closeButtonFn}) => {
36 | return (
37 |
38 |
39 |
40 | {hasCloseButton && }
41 |
42 |
43 | )
44 | }
45 |
46 | export default Image;
--------------------------------------------------------------------------------
/frontend/src/components/Like.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 | import { useMutation } from '@apollo/client'
3 | import { ADD_REACTION_MUTATION, REMOVE_REACTION_MUTATION } from '../utils/queries'
4 | import Heart from '../assets/Heart'
5 |
6 | function Like({ profileId, publicationId, liked, stats, setToastMsg }) {
7 | const [addReaction] = useMutation(ADD_REACTION_MUTATION, {
8 | onError(error) {
9 | setLiked(false)
10 | setCount(count)
11 | console.warn(error)
12 | }
13 | })
14 | const [removeReaction] = useMutation(REMOVE_REACTION_MUTATION, {
15 | onError(error) {
16 | setLiked(true)
17 | setCount(count)
18 | console.warn(error)
19 | }
20 | })
21 | const [stateLiked, setLiked] = useState(liked)
22 | const [count, setCount] = useState(stats.totalUpvotes)
23 |
24 | useEffect(() => {
25 | setCount(stats.totalUpvotes)
26 | }, [stats.totalUpvotes])
27 |
28 | const [apiError, setApiError] = useState('')
29 |
30 | const handleClick = async (e) => {
31 | setLiked(!stateLiked)
32 |
33 | e.stopPropagation()
34 |
35 | try {
36 | if (stateLiked) {
37 | setCount(count-1)
38 | await removeReaction({
39 | variables: {
40 | request: {
41 | profileId: profileId,
42 | reaction: 'UPVOTE',
43 | publicationId: publicationId,
44 | },
45 | },
46 | });
47 | } else {
48 | setCount(count+1)
49 | await addReaction({
50 | variables: {
51 | request: {
52 | profileId: profileId,
53 | reaction: 'UPVOTE',
54 | publicationId: publicationId,
55 | },
56 | },
57 | });
58 | }
59 | }
60 | catch (err) {
61 | alert(err)
62 | setApiError(apiError)
63 | }
64 | };
65 |
66 | return (
67 |
71 | );
72 | }
73 |
74 | export default Like;
75 |
--------------------------------------------------------------------------------
/frontend/src/components/Livepeer.jsx:
--------------------------------------------------------------------------------
1 | import {
2 | LivepeerConfig,
3 | createReactClient,
4 | studioProvider,
5 | } from '@livepeer/react';
6 |
7 | export const client = createReactClient({
8 | provider: studioProvider({
9 | apiKey: process.env.REACT_APP_LIVEPEER_API_KEY
10 | }),
11 | });
12 |
13 | function Livepeer({ children }) {
14 | return { children }
15 | }
16 |
17 | export default Livepeer
--------------------------------------------------------------------------------
/frontend/src/components/Livestream.jsx:
--------------------------------------------------------------------------------
1 | import { useLayoutEffect, useRef } from 'react';
2 | // import Plyr from 'plyr';
3 | import Hls from 'hls.js';
4 | // import 'plyr/dist/plyr.css';
5 |
6 | const PlyrComponent = ({playbackId}) => {
7 | const video = useRef();
8 | const playerInstance = useRef();
9 |
10 | // useLayoutEffect(() => {
11 | // const source = `https://cdn.livepeer.com/hls/${playbackId}/index.m3u8`;
12 | // playerInstance.current = new Plyr(video.current);
13 | // const hls = new Hls();
14 | // hls.loadSource(source);
15 | // hls.attachMedia(video.current);
16 | // window.hls = hls;
17 | // playerInstance.current.speed = 1
18 | // video.current.addEventListener('ended',myHandler,false);
19 | // function myHandler(e) {
20 | // // What you want to do after the event
21 | // console.log('done')
22 | // }
23 | // return () => {
24 | // playerInstance.current.destroy();
25 | // };
26 | // }, []);
27 |
28 | return ;
29 | };
30 |
31 | export default PlyrComponent;
--------------------------------------------------------------------------------
/frontend/src/components/Login.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react'
2 | import { useLazyQuery, useMutation } from '@apollo/client'
3 | import { GET_CHALLENGE, AUTHENTICATION } from '../utils/queries'
4 | import Button from './Button'
5 | import { useWallet } from '../utils/wallet'
6 | import { useAccount, useSigner } from 'wagmi'
7 |
8 | function Login({ ...props }) {
9 | const { authToken, setAuthToken } = useWallet()
10 | const { data: signer } = useSigner()
11 | const { address } = useAccount()
12 | const [getChallenge, challengeData] = useLazyQuery(GET_CHALLENGE)
13 | const [mutateAuth, authData] = useMutation(AUTHENTICATION)
14 |
15 | const handleClick = async () => {
16 |
17 | if (authToken) {
18 | console.log('login: already logged in');
19 | return;
20 | }
21 |
22 | console.log('login: address', address);
23 |
24 | getChallenge({
25 | variables: {
26 | request: {
27 | address: address,
28 | },
29 | },
30 | })
31 | }
32 |
33 | useEffect(() => {
34 | if (!challengeData.data) return
35 |
36 | const handleSign = async () => {
37 | const signature = await signer.signMessage(challengeData.data.challenge.text);
38 | window.sessionStorage.setItem('signature', JSON.stringify({
39 | sig: signature,
40 | derivedVia: 'ethers.signer.signMessage',
41 | signedMessage: challengeData.data.challenge.text,
42 | address: address,
43 | }))
44 | mutateAuth({
45 | variables: {
46 | request: {
47 | address: address,
48 | signature,
49 | },
50 | },
51 | });
52 | }
53 |
54 | handleSign()
55 | }, [challengeData.data])
56 |
57 | useEffect(() => {
58 | if (!authData.data) return
59 |
60 | // window.authToken = authData.data.authenticate.accessToken
61 | window.sessionStorage.setItem('lensToken', authData.data.authenticate.accessToken)
62 |
63 | setAuthToken(true)
64 |
65 | }, [authData.data])
66 |
67 | useEffect(() => {
68 | if (window.sessionStorage.getItem('lensToken')) {
69 | setAuthToken(true)
70 | }
71 | }, [])
72 |
73 | if(!address || authToken) return '';
74 | return ;
75 | }
76 |
77 | export default Login
--------------------------------------------------------------------------------
/frontend/src/components/Mirror.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react'
2 | import { useMutation } from '@apollo/client'
3 | import { utils } from 'ethers'
4 | import { CREATE_MIRROR_TYPED_DATA, BROADCAST } from '../utils/queries'
5 | import omitDeep from 'omit-deep'
6 | import { useAccount, useSigner } from 'wagmi'
7 | import Retweet from '../assets/Retweet'
8 | import Button from './Button'
9 | import pollUntilIndexed from '../utils/pollUntilIndexed'
10 | import { useWallet } from '../utils/wallet'
11 |
12 | function Mirror({ profileId, publicationId, stats, setToastMsg }) {
13 | const { lensHub } = useWallet()
14 | const { data: signer } = useSigner()
15 | const [createMirrorTyped, createMirrorTypedData] = useMutation(CREATE_MIRROR_TYPED_DATA)
16 | const [broadcast, broadcastData] = useMutation(BROADCAST)
17 | const [savedTypedData, setSavedTypedData] = useState({})
18 |
19 | const handleClick = async (e) => {
20 | e.stopPropagation()
21 | const mirrorRequest = {
22 | profileId: profileId,
23 | publicationId: publicationId,
24 | referenceModule: {
25 | followerOnlyReferenceModule: true,
26 | },
27 | };
28 |
29 | createMirrorTyped({
30 | variables: {
31 | request: mirrorRequest,
32 | },
33 | });
34 | };
35 |
36 | useEffect(() => {
37 | if (!createMirrorTypedData.data) return;
38 |
39 | const handleCreate = async () => {
40 |
41 | const typedData = createMirrorTypedData.data.createMirrorTypedData.typedData
42 |
43 | const { domain, types, value } = typedData
44 |
45 | const signature = await signer._signTypedData(
46 | omitDeep(domain, "__typename"),
47 | omitDeep(types, "__typename"),
48 | omitDeep(value, "__typename")
49 | )
50 |
51 | setToastMsg({ type: 'loading', msg: 'Transaction indexing...' })
52 |
53 | setSavedTypedData({
54 | ...typedData,
55 | signature
56 | })
57 |
58 | broadcast({
59 | variables: {
60 | request: {
61 | id: createMirrorTypedData.data.createMirrorTypedData.id,
62 | signature
63 | }
64 | }
65 | })
66 |
67 | };
68 |
69 | handleCreate();
70 | }, [createMirrorTypedData.data])
71 |
72 |
73 | useEffect(() => {
74 | if (!broadcastData.data) return;
75 | const processBroadcast = async () => {
76 |
77 | if (broadcastData.data.broadcast.__typename === 'RelayError') {
78 | console.log('asking user to pay for gas because error', broadcastData.data.broadcast.reason)
79 |
80 | const { v, r, s } = utils.splitSignature(savedTypedData.signature);
81 |
82 | const tx = await lensHub.mirrorWithSig({
83 | profileId: savedTypedData.value.profileId,
84 | profileIdPointed: savedTypedData.value.profileIdPointed,
85 | pubIdPointed: savedTypedData.value.pubIdPointed,
86 | referenceModuleData: savedTypedData.value.referenceModuleData,
87 | referenceModule: savedTypedData.value.referenceModule,
88 | referenceModuleInitData: savedTypedData.value.referenceModuleInitData,
89 | sig: {
90 | v,
91 | r,
92 | s,
93 | deadline: savedTypedData.value.deadline,
94 | },
95 | });
96 |
97 | console.log('mirror: tx hash', tx.hash);
98 | await pollUntilIndexed(tx.hash)
99 | console.log('mirror: success')
100 | setToastMsg({ type: 'success', msg: 'Transaction indexed' })
101 |
102 | return;
103 | }
104 |
105 | const txHash = broadcastData.data.broadcast.txHash
106 | console.log('mirror: tx hash', txHash);
107 | if (!txHash) return;
108 | await pollUntilIndexed(txHash)
109 | console.log('mirror: success')
110 | setToastMsg({ type: 'success', msg: 'Transaction indexed' })
111 | }
112 | processBroadcast()
113 |
114 | }, [broadcastData.data])
115 |
116 | return (
117 |
118 |
119 |
{ stats.totalAmountOfMirrors }
120 |
121 | );
122 | }
123 |
124 | export default Mirror;
125 |
--------------------------------------------------------------------------------
/frontend/src/components/Modal.jsx:
--------------------------------------------------------------------------------
1 | import { createRef } from 'react'
2 | import styled from "styled-components"
3 | import Card from "./Card"
4 |
5 | const ModalContainer = styled.div`
6 | position: fixed;
7 | top: 0;
8 | left: 0;
9 | height: 100vh;
10 | width: 100vw;
11 | z-index: 1000;
12 | background-color: rgba(130, 71, 220, 0.1);
13 | backdrop-filter: blur(4px);
14 | `
15 |
16 | const StyledCard = styled(Card)`
17 | z-index: 1005;
18 | max-width: ${p => p.width || 'fit-content'};
19 | max-height: 75vh;
20 | margin: auto;
21 | margin-top: 10vh;
22 | padding: ${p => p.padding || '2em'};
23 | `
24 |
25 | const Modal = ({ children, onExit, ...props }) => {
26 | const ref = createRef()
27 | return (
28 | {
29 | if (e.target === ref.current) {
30 | onExit()
31 | }
32 | }
33 | }
34 | >
35 |
36 | { children }
37 |
38 |
39 | )
40 | }
41 |
42 | export default Modal
--------------------------------------------------------------------------------
/frontend/src/components/Nav.jsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components'
2 | import { Link } from 'react-router-dom'
3 | import Card from './Card'
4 | import Home from '../assets/Home'
5 | import Profile from '../assets/Profile'
6 | import Subscriptions from '../assets/Subscriptions'
7 | import Compass from '../assets/Compass'
8 | import Share from '../assets/Logout'
9 | import Heart from '../assets/Heart'
10 | import { useWallet } from '../utils/wallet'
11 |
12 | const StyledLink = styled(Link)`
13 | text-decoration: none;
14 | display: flex;
15 | align-items: center;
16 | gap: 12px;
17 | p {
18 | padding-top: 1px;
19 | font-weight: 600;
20 | display: inline;
21 | color: black;
22 | transition: all 100ms ease-in-out;
23 | @media (max-width: 768px) {
24 | display: none;
25 | }
26 | }
27 |
28 | padding: .5em;
29 | transition: all 150ms ease-in-out;
30 | border-radius: 8px;
31 | border: 2px solid transparent;
32 | &:hover {
33 | border: ${p=>p.theme.border};
34 | cursor: pointer;
35 | p {
36 | color: ${p=>p.theme.primary};
37 | }
38 | }
39 | `
40 |
41 | const StyledCard = styled(Card)`
42 | margin-top: 1em;
43 | @media (max-width: 768px) {
44 | display: flex;
45 | justify-content: space-between;
46 | position: fixed;
47 | bottom: 0;
48 | z-index: 100;
49 | border-radius: 0;
50 | padding: 0.3em 1em;
51 | }
52 | `
53 |
54 | function Nav({ handle, setProfile, ...props }) {
55 | const { authToken } = useWallet()
56 | const handleClick = () => {
57 | window.sessionStorage.removeItem('lensToken')
58 | window.sessionStorage.removeItem('signature')
59 | setProfile({})
60 | }
61 |
62 | return (
63 |
64 |
65 |
66 | Home
67 |
68 | {handle &&
69 |
70 | Profile
71 | }
72 | {/*
73 |
74 | Subscriptions
75 |
76 |
77 |
78 | Collection
79 | */}
80 |
81 |
82 | Explore
83 |
84 | {authToken &&
85 |
86 | Logout
87 | }
88 |
89 | );
90 | }
91 |
92 | export default Nav
--------------------------------------------------------------------------------
/frontend/src/components/Post.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 | import styled from 'styled-components'
3 | import { Link, useNavigate } from 'react-router-dom'
4 | import moment from 'moment'
5 | import { useSigner } from 'wagmi'
6 | import reactStringReplace from 'react-string-replace'
7 | import { Player } from '@livepeer/react'
8 | import Card from '../components/Card'
9 | import { UserIcon } from '../components/Wallet'
10 | import Comment from './Comment'
11 | import Mirror from './Mirror'
12 | import Like from './Like'
13 | import Collect from './Collect'
14 | import Modal from './Modal'
15 | import { Avatar } from './Profile'
16 | import Toast from './Toast'
17 | import Retweet from '../assets/Retweet'
18 | import { CHAIN } from '../utils/constants'
19 | import { random } from '../utils'
20 | import { client } from '../utils/infuraClient'
21 |
22 | const NameLink = styled(Link)`
23 | display: flex;
24 | align-items: center;
25 | gap: 5px;
26 | text-decoration: none;
27 | color: black;
28 | &:hover {
29 | color: black;
30 | }
31 | `;
32 |
33 | const Underlined = styled.span`
34 | color: black;
35 | ${(p) => p.theme.hrefUnderline}
36 | &:hover {
37 | color: ${(p) => p.theme.primaryHover}
38 | }
39 | `
40 |
41 | const Icon = styled(UserIcon)`
42 | display: inline-block;
43 | width: 3em;
44 | height: 3em;
45 | `;
46 |
47 | const Container = styled.div`
48 | position: relative;
49 | display: flex;
50 | gap: 10px;
51 | `;
52 |
53 | const Actions = styled.div`
54 | margin-top: 1em;
55 | display: flex;
56 | gap: 10px;
57 | align-items: center;
58 | justify-content: space-between;
59 | max-width: 400px;
60 | `;
61 |
62 | const Header = styled.div`
63 | display: flex;
64 | justify-content: space-between;
65 | width: 100%;
66 | `
67 |
68 | const Content = styled.div`
69 | margin-top: -4px;
70 | width: 100%;
71 | `;
72 |
73 | const Premium = styled.span`
74 | background: ${p=>p.theme.darken2};
75 | display: inline-block;
76 | border-radius: 60px;
77 | padding: 0.2em 0.8em;
78 | font-size: 0.8em;
79 | margin-bottom: 2px;
80 | color: ${p=>p.theme.greyed};
81 | `;
82 |
83 | const PostArea = styled.div`
84 | padding: 1em;
85 | transition: background 100ms;
86 | &:hover {
87 | background: ${p => p.theme.darken};
88 | cursor: pointer;
89 | }
90 | border-bottom: #D9D9D9 1px solid;
91 | `
92 |
93 | const MediaContainer = styled.div`
94 | margin-top: 0.75em;
95 | display: flex;
96 | overflow-x: auto;
97 | `
98 |
99 | const StyledImage = styled.div`
100 | cursor: pointer;
101 | width: 100%;
102 | height: 16em;
103 | border-radius: 0.5em;
104 | background: url(${p => p.src});
105 | background-size: cover;
106 |
107 | &:nth-child(n+2) {
108 | margin-left: 0.6em;
109 | }
110 | `
111 |
112 | const ImageDisplay = styled.img`
113 | border-radius: 0.5em;
114 | max-width: 100%;
115 | max-height: 75vh;
116 | `
117 |
118 | const CommunityDisplay = styled.div`
119 | padding: 1.5em;
120 | text-align: center;
121 | color: white;
122 | width: 100%;
123 | height: 10em;
124 | border-radius: 1em;
125 | background: rgb(168,73,231);
126 | background: linear-gradient(144deg, rgba(168,73,231,1) 0%, rgba(255,108,108,1) 50%, rgba(255,176,64,1) 100%);
127 | `
128 |
129 | const StyledA = styled.a`
130 | ${(p) => p.theme.hrefUnderline}
131 | `
132 |
133 | const A = ({ children, ...props }) => {
134 | return e.stopPropagation()}>{children}
135 | }
136 |
137 | const MirrorContainer = styled.div`
138 | display: flex;
139 | align-items: center;
140 | margin-bottom: 0.5em;
141 | gap: 4px;
142 | `
143 |
144 | const Supertext = styled.span`
145 | font-size: 0.8em;
146 | padding-bottom: 1px;
147 | `
148 |
149 | const Mirrored = ({ children }) => {
150 | return
151 |
152 | {children}
153 |
154 | }
155 |
156 | const exclusiveLabel = (postType) => {
157 | switch(postType) {
158 | case 'Post':
159 | return 'Follower Exclusive';
160 | case 'Comment':
161 | return 'Collector Exclusive';
162 | case 'CommunityPost':
163 | return 'Community Exclusive';
164 | default:
165 | return 'Exclusive';
166 | }
167 | }
168 |
169 | const exclusiveDescription = (postType) => {
170 | switch(postType) {
171 | case 'Post':
172 | return 'Post for followers only';
173 | case 'Comment':
174 | return 'Comment for post collectors only';
175 | case 'CommunityPost':
176 | return 'Message for community members only';
177 | default:
178 | return 'Exclusive';
179 | }
180 | }
181 |
182 | const PostBody = ({ children }) => {
183 | // Match URLs
184 | let replacedText = reactStringReplace(children, /(https?:\/\/\S+)/g, (match, i) => {
185 | if (match.length > 50) return {match.substring(0,30)}...{match.substring(match.length-24,match.length-1)}
186 | return {match}
187 | });
188 |
189 | // Match newlines
190 | replacedText = reactStringReplace(replacedText, /(\n)/g, (match, i) => (
191 |
192 | ));
193 |
194 | // Match @xyz.lens-mentions
195 | const taggedRegex = CHAIN === 'polygon' ? /@(\w+\.lens)/g : /@(\w+\.test)/g
196 | replacedText = reactStringReplace(replacedText, taggedRegex, (match, i) => (
197 | @{match}
198 | ));
199 |
200 | // Match @xyz-mentions
201 | replacedText = reactStringReplace(replacedText, /@(\w+)/g, (match, i) => (
202 | @{match}
203 | ));
204 |
205 | // Match hashtags
206 | replacedText = reactStringReplace(replacedText, /#(\w+)/g, (match, i) => (
207 | #{match}
208 | ));
209 |
210 | return <>{ replacedText }>
211 | }
212 |
213 | function Post({ profileId, isCommunityPost, ...props }) {
214 | const { data: signer } = useSigner()
215 | const [decryptedMsg, setDecryptedMsg] = useState("")
216 | const [showModal, setShowModal] = useState(false)
217 | const [selectedImage, setSelectedImage] = useState('')
218 | const [post, setPost] = useState(props.post)
219 | const [mirror, setMirror] = useState(null)
220 | const [toastMsg, setToastMsg] = useState({})
221 |
222 | const navigate = useNavigate()
223 |
224 | moment.updateLocale('en', {
225 | relativeTime: {
226 | future: 'in %s',
227 | past: '%s ago',
228 | s: '1s',
229 | ss: '%ss',
230 | m: '1m',
231 | mm: '%dm',
232 | h: '1h',
233 | hh: '%dh',
234 | d: '1d',
235 | dd: '%dd',
236 | M: '1M',
237 | MM: '%dM',
238 | y: '1Y',
239 | yy: '%dY'
240 | }
241 | });
242 |
243 | useEffect(() => {
244 | if(props.post.__typename === 'Mirror') {
245 | setPost(props.post.mirrorOf)
246 | setMirror(props.post)
247 | } else {
248 | setPost(props.post)
249 | }
250 | }, [props.post])
251 |
252 | useEffect(() => {
253 |
254 | if (!signer) return;
255 |
256 | const decode = async () => {
257 | await new Promise(r => setTimeout(r, 100));
258 |
259 | if (post.appId === "iris exclusive") {
260 | const encryptedPostRaw = post.metadata?.attributes?.filter((attr) => attr.traitType === 'Encoded Post Data')[0].value
261 | const encryptedPost = JSON.parse(encryptedPostRaw);
262 |
263 | const isthisblob = client.cat(encryptedPost.blobPath);
264 | let newEcnrypt;
265 | (async () => {
266 | const authSig = JSON.parse(window.sessionStorage.getItem('signature'))
267 |
268 | for await (const chunk of isthisblob) {
269 | newEcnrypt = new Blob([chunk], {
270 | type: "encryptedString.type", // or whatever your Content-Type is
271 | });
272 | }
273 | const key = await window.litNodeClient.getEncryptionKey({
274 | accessControlConditions: encryptedPost.accessControlConditions,
275 | // Note, below we convert the encryptedSymmetricKey from a UInt8Array to a hex string. This is because we obtained the encryptedSymmetricKey from "saveEncryptionKey" which returns a UInt8Array. But the getEncryptionKey method expects a hex string.
276 | toDecrypt: encryptedPost.key,
277 | chain: CHAIN,
278 | authSig,
279 | });
280 |
281 | // const decryptedString = await LitJsSdk.decryptString(newEcnrypt, key);
282 |
283 | // setDecryptedMsg(decryptedString);
284 | })();
285 | }
286 |
287 | }
288 |
289 | decode()
290 | }, [signer]);
291 |
292 | const handleImageClick = (media) => {
293 | setShowModal(true)
294 | setSelectedImage(media)
295 | }
296 |
297 | let postType = post.__typename;
298 | if (isCommunityPost) {
299 | postType = 'CommunityPost'
300 | }
301 |
302 | post?.metadata?.attributes.forEach(attribute => {
303 | if(attribute.value === 'community') {
304 | postType = 'Community';
305 | }
306 | })
307 |
308 | const profileHandle = post.profile?.handle
309 | const profileName = post.profile?.name || post.profile?.handle
310 |
311 | return <>
312 | {showModal && setShowModal(false)}>
313 |
314 | }
315 | {toastMsg.msg}
316 | navigate(`/post/${post.id}`)}>
317 | {mirror && mirrored by {mirror.profile?.name || mirror.profile?.handle}}
318 |
319 | e.stopPropagation()}>
320 |
321 |
322 |
323 |
333 |
334 | {post.appId === "iris exclusive" ? <>{decryptedMsg ? decryptedMsg :
{exclusiveDescription(postType)}
}> :
{post.metadata.content}}
335 |
336 | {post?.metadata?.media?.length && post?.metadata?.media[0]?.original?.mimeType === 'video/mp4' ? : }
337 | {post.metadata.media.length && (post.metadata.media[0]?.original.mimeType === 'image/jpeg' || post.metadata.media[0]?.original.mimeType === 'image/png') ?
338 | e.stopPropagation()}>
339 | {
340 | post.metadata.media.map((media) => {
341 | if(media.original.mimeType.includes('image')) {
342 | return handleImageClick(media.original?.url)}
347 | />
348 | }
349 | return Video
350 | })
351 | }
352 | : ''}
353 |
354 | {postType === 'Community' &&
355 |
356 |
357 |
358 |
359 | {post.metadata?.name}
360 |
361 |
362 | }
363 |
364 |
365 |
366 |
367 |
368 |
369 | {/* */}
370 |
371 |
372 |
373 |
374 | >
375 | }
376 |
377 | export default Post;
378 |
--------------------------------------------------------------------------------
/frontend/src/components/Profile.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 | import styled from 'styled-components'
3 | import { Link } from 'react-router-dom'
4 | import Card from './Card'
5 | import avatar from '../assets/avatar.png'
6 | import Button from './Button'
7 | import Modal from './Modal'
8 |
9 | export const Avatar = styled.div`
10 | height: 75px;
11 | width: 75px;
12 | border-radius: 100px;
13 | &:hover {
14 | cursor: pointer;
15 | }
16 | display: flex;
17 | justify-content: center;
18 | align-items: center;
19 | background: url(${p => p.src || avatar});
20 | background-size: cover;
21 | margin: auto;
22 | `
23 |
24 | const Handle = styled.h2`
25 | text-align: center;
26 | color: black;
27 | transition: all 100ms;
28 | &:hover {
29 | color: ${p => p.theme.primary};
30 | }
31 | `
32 |
33 | const StyledLink = styled(Link)`
34 | text-decoration: none;
35 | `
36 |
37 | const Stats = styled.div`
38 | display: flex;
39 | justify-content: space-evenly;
40 | `
41 |
42 | const Header = styled.h2`
43 | margin: 0;
44 | color: ${p => p.theme.primary};
45 | `
46 |
47 | const Centered = styled.div`
48 | text-align: center;
49 | `
50 |
51 | function Profile({ profile = {}, children }) {
52 |
53 | // Streaming
54 | const [liveStreamModal, setLiveStreamModal] = useState(false)
55 | const [streamInfo, setStreamInfo] = useState({});
56 |
57 |
58 | const goLiveStream = async () => {
59 | const response = await fetch("https://irisxyz.herokuapp.com/new-stream",
60 | {
61 | method: "POST",
62 | 'headers': {
63 | 'Content-Type': 'application/json'
64 | },
65 | // body: JSON.stringify({ wallet: address, handle: profile.handle }), mode: "cors"
66 | });
67 | const data = await response.json();
68 |
69 | console.log("goLive was pressed")
70 | setStreamInfo(data)
71 |
72 | setLiveStreamModal(true)
73 | }
74 | useEffect(() => {
75 |
76 | const isUserLivestreaming = async () => {
77 |
78 | try {
79 | // const response = await fetch("https://livepeer.com/api/stream?streamsonly=1&filters=[{id: isActive, value: true}]",
80 | // {
81 | // headers: {
82 | // // TODO: Remove API KEY in the future
83 | // "authorization": "Bearer fe3ed427-ab88-415e-b691-8fba9e7e6fb0"
84 | // }
85 | // },
86 | // );
87 | // const responseData = await response.json();
88 |
89 | // responseData.map((streamInfo) => {
90 | // if (streamInfo.isActive & streamInfo.name === `${address},${profile.handle}`) {
91 |
92 | // console.log("PROFILE Woooo")
93 | // setStreamInfo(streamInfo)
94 |
95 | // }
96 | // })
97 |
98 | } catch (err) {
99 | console.log(err)
100 | }
101 |
102 |
103 |
104 | }
105 | isUserLivestreaming()
106 |
107 | }, [profile])
108 |
109 | const showLiveStreamInfo = async () => {
110 | setLiveStreamModal(true)
111 | }
112 |
113 | if (!profile.id) return (
114 | <>{children}>
115 | )
116 |
117 | return (
118 |
119 | {liveStreamModal && setLiveStreamModal(false)}>
120 |
121 |
122 |
123 | Stream ID {streamInfo.id}
124 |
125 |
126 | Stream key {streamInfo.streamKey}
127 |
128 |
129 | RTMP ingest URL rtmp://rtmp.livepeer.com/live
130 |
131 |
132 | SRT ingest URL srt://rtmp.livepeer.com:2935?streamid={streamInfo.streamKey}
133 |
134 |
135 | Playback URL https://cdn.livepeer.com/hls/{streamInfo.playbackId}/index.m3u8
136 |
137 |
138 | Stream From Browser https://justcast.it/to/{streamInfo.streamKey}
139 |
140 |
141 | }
142 |
143 |
144 | @{profile.handle}
145 |
146 |
147 | {profile.stats?.totalFollowers} followers
148 | {profile.stats?.totalFollowing} following
149 |
150 | {/*
151 | {streamInfo.playbackId ?
152 | :
155 |
158 | }
159 | */}
160 |
161 | );
162 | }
163 |
164 | export default Profile
--------------------------------------------------------------------------------
/frontend/src/components/Toast.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 | import styled from 'styled-components'
3 | import Check from '../assets/Check'
4 | import Spinner from '../assets/Spinner'
5 | import Error from '../assets/Error'
6 |
7 | const ToastDiv = styled.div`
8 | display: flex;
9 | align-items: center;
10 | gap: 4px;
11 | background-color: #fff;
12 | position: fixed;
13 | z-index: 10000;
14 | bottom: ${p => (p.shown ? '40px' : '-100px')};
15 | opacity: ${p => (p.shown ? '100%' : '0%')};
16 | transition: all 0.75s ease;
17 | margin-left: auto;
18 | margin-right: auto;
19 | padding: 0.25em 1em 0.25em 0.5em;
20 | right: 3em;
21 | border-radius: 6px;
22 | word-break: break-word;
23 | ${p => p.type === 'success' && `border-left: #4DD06A 4px solid;`}
24 | ${p => p.type === 'loading' && `border-left: ${p.theme.primary} 4px solid;`}
25 | ${p => p.type === 'error' && `border-left: ${p.theme.error} 4px solid;`}
26 | box-shadow: 0px 2px 9px rgba(236, 176, 178, 0.6);
27 | `
28 | const ToastText = styled.p`
29 | text-align: center;
30 | color: ${p => p.theme.toastText};
31 | `
32 |
33 | export default function Toast({ children, type = 'error' }) {
34 | const [showToast, setShowToast] = useState(false)
35 |
36 | useEffect(() => {
37 | if (children) {
38 | setShowToast(true)
39 | } else {
40 | setShowToast(false)
41 | }
42 |
43 | if (type !== 'loading') {
44 | const toastTimeout = setTimeout(() => {
45 | setShowToast(false)
46 | }, 4000)
47 | return () => {
48 | clearTimeout(toastTimeout)
49 | }
50 | }
51 | }, [children])
52 |
53 | return (
54 |
55 | {type === 'success' && }
56 | {type === 'loading' && }
57 | {type === 'error' && }
58 | {children}
59 |
60 | )
61 | }
--------------------------------------------------------------------------------
/frontend/src/components/Unfollow.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 | import { useMutation } from "@apollo/client";
3 | import { utils, ethers } from "ethers";
4 | import { useSigner } from 'wagmi'
5 | // import { LENS_FOLLOW_NFT_ABI } from "../config.ts";
6 | import { CREATE_UNFOLLOW_TYPED_DATA } from "../utils/queries";
7 | import omitDeep from "omit-deep";
8 | import { OutlineButton } from '../components/Button';
9 |
10 | function Follow({ profileId }) {
11 | const { data: signer } = useSigner()
12 | const [createUnfollowTyped, createUnfollowTypedData] = useMutation(CREATE_UNFOLLOW_TYPED_DATA);
13 |
14 | const unfollowRequest = {
15 | profile: profileId,
16 | };
17 |
18 | const handleClick = async () => {
19 | createUnfollowTyped({
20 | variables: {
21 | request: unfollowRequest,
22 | },
23 | });
24 | };
25 |
26 | useEffect(() => {
27 | if (!createUnfollowTypedData.data) return;
28 |
29 | const handleCreate = async () => {
30 | console.log(createUnfollowTypedData.data);
31 |
32 | const typedData = createUnfollowTypedData.data.createUnfollowTypedData.typedData;
33 | const { domain, types, value } = typedData;
34 |
35 | const signature = await signer._signTypedData(
36 | omitDeep(domain, "__typename"),
37 | omitDeep(types, "__typename"),
38 | omitDeep(value, "__typename")
39 | );
40 |
41 | const { v, r, s } = utils.splitSignature(signature);
42 |
43 | // load up the follower nft contract
44 | // this is defs broken now
45 | const followNftContract = new ethers.Contract(
46 | typedData.domain.verifyingContract,
47 | // LENS_FOLLOW_NFT_ABI,
48 | signer
49 | );
50 |
51 | const sig = {
52 | v,
53 | r,
54 | s,
55 | deadline: typedData.value.deadline,
56 | };
57 |
58 | const tx = await followNftContract.burnWithSig(typedData.value.tokenId, sig);
59 | console.log("Unfollowed:", tx.hash);
60 | };
61 |
62 | handleCreate();
63 | }, [createUnfollowTypedData.data]);
64 |
65 | return (
66 |
67 |
68 | Following
69 |
70 |
71 | );
72 | }
73 |
74 | export default Follow;
75 |
--------------------------------------------------------------------------------
/frontend/src/components/Video.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components'
3 | import X from '../assets/X';
4 | // import 'plyr-react/plyr.css'
5 | // import Plyr from 'plyr-react'
6 |
7 | const CloseButton = styled.button`
8 | color: white;
9 | padding-top: 3px;
10 | background-color: rgba(0, 0, 0, 0.5);
11 | border: none;
12 | border-radius: 4px;
13 | position: absolute;
14 | top: 5px;
15 | right: 5px;
16 | :hover {
17 | cursor: pointer;
18 | background-color:rgba(100, 100, 100, 0.5);
19 | }
20 | `
21 |
22 | const Container = styled.div`
23 | background-color: rgba(100, 100, 100, 0);
24 | display: flex;
25 | justify-content: center;
26 | flex-direction: column;
27 | width: 100%;
28 | position: relative;
29 |
30 | --plyr-color-main: ${p => p.theme.primary};
31 |
32 | .plyr {
33 | background-color: rgba(0, 0, 0, 0);
34 | border-radius: 6px;
35 | filter: drop-shadow(0px 4px 4px rgba(0, 0, 0, 0.25));
36 | }
37 | `;
38 |
39 | const areEqual = (prevProps, nextProps) => {
40 | return (prevProps.name === nextProps.name && prevProps.lastModified === nextProps.lastModified && prevProps.lastModifiedDate === nextProps.lastModifiedDate && prevProps.size === nextProps.size && prevProps.type === nextProps.type && prevProps.webkitRelativePath === nextProps.webkitRelativePath)
41 | }
42 |
43 | // Using memo so video does not re-render after writing in text box.
44 |
45 | const Video = React.memo(({src, hasCloseButton, closeButtonFn}) => {
46 | const url = URL.createObjectURL(src);
47 |
48 | return (
49 |
50 | {/* */}
69 | {hasCloseButton && }
70 |
71 | );
72 |
73 | }, areEqual)
74 |
75 | export default Video;
--------------------------------------------------------------------------------
/frontend/src/components/VisibilitySelector.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 | import styled from 'styled-components'
3 | import Modal from './Modal'
4 | import CaretDown from '../assets/CaretDown'
5 | import Globe from '../assets/Globe'
6 | import Community from '../assets/Community'
7 |
8 | const StyledModal = styled(Modal)`
9 | max-width: 500px;
10 | `
11 |
12 | const Container = styled.button`
13 | border: none;
14 | font-family: ${p => p.theme.font};
15 | margin-left: auto;
16 | background: ${p => p.theme.darken2};
17 | padding: 0.4em 1em 0.4em 0.75em;
18 | border-radius: 6px;
19 | font-size: 0.8em;
20 | display: flex;
21 | align-items: center;
22 | gap: 0.25em;
23 | transition: all 100ms ease;
24 | &:hover {
25 | cursor: pointer;
26 | background: ${p => p.theme.darken};
27 | }
28 | `
29 |
30 | const StyledButton = styled.button`
31 | background: none;
32 | font-family: ${p => p.theme.font};
33 | font-size: 1em;
34 | text-align: left;
35 | b {
36 | font-weight: 600;
37 | display: inline;
38 | color: black;
39 | transition: all 100ms ease-in-out;
40 | }
41 |
42 | margin-top: 4px;
43 | padding: .5em 1em .5em .75em;
44 | transition: all 150ms ease-in-out;
45 | border-radius: 12px;
46 | border: 2px solid transparent;
47 | &:hover {
48 | border: ${p=>p.theme.border};
49 | cursor: pointer;
50 | b {
51 | color: ${p=>p.theme.primary};
52 | }
53 | }
54 | ${p => p.selected && `
55 | border: ${p.theme.border};
56 | cursor: pointer;
57 | b {
58 | color: ${p.theme.primary};
59 | }
60 | `}
61 | `
62 |
63 | const Span = styled.span`
64 | display: flex;
65 | align-items: center;
66 | gap: 0.5em;
67 | `
68 |
69 | const VisibilitySelector = ({ showFollower, showCommunity, showCollector, selectedVisibility, setSelectedVisibility }) => {
70 | const [showModal, setShowModal] = useState(false)
71 |
72 | return <>
73 | {showModal && setShowModal(false)}>
74 | Post Visibility
75 | setSelectedVisibility('public')}>
78 |
79 |
80 | Public
81 |
82 | Everyone on the internet can view this post.
83 |
84 | {showFollower && setSelectedVisibility('follower')}>
87 |
88 |
89 | Follower Only
90 |
91 | Only your followers can view this post.
92 | }
93 | {showCommunity && setSelectedVisibility('community')}>
96 |
97 |
98 | Community Only
99 |
100 | Only members of this community can view this post.
101 | }
102 | {showCollector && setSelectedVisibility('collector')}>
105 |
106 |
107 | Collector Only
108 |
109 | Only collectors of the post can view your comment.
110 | }
111 | }
112 | setShowModal(true)}>
113 | {selectedVisibility === 'public' && <>
114 |
115 | Public
116 | >}
117 | {selectedVisibility === 'follower' && <>
118 |
119 | Follower
120 | >}
121 | {selectedVisibility === 'community' && <>
122 |
123 | Community
124 | >}
125 | {selectedVisibility === 'collector' && <>
126 |
127 | Collector
128 | >}
129 |
130 |
131 | >
132 | }
133 |
134 | export default VisibilitySelector
--------------------------------------------------------------------------------
/frontend/src/components/Wallet.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 | import styled from 'styled-components'
3 | import { ethers } from 'ethers'
4 | import { Link } from 'react-router-dom'
5 | import { useLazyQuery } from '@apollo/client'
6 | import { ConnectButton } from '@rainbow-me/rainbowkit'
7 | import { useAccount, useSigner } from 'wagmi'
8 | import { GET_PROFILES } from '../utils/queries'
9 | import { CHAIN } from '../utils/constants'
10 | import avatar from '../assets/avatar.png'
11 | import Login from './Login'
12 | import LensHub from '../abi/LensHub.json'
13 | import { useWallet } from '../utils/wallet'
14 |
15 | const WalletContainer = styled.div`
16 | display: flex;
17 | gap: 5px;
18 | position: relative;
19 | z-index: 3;
20 | `
21 |
22 | export const Address = styled.code`
23 | box-shadow: 0px 2px 5px rgba(190, 176, 178, 0.6);
24 | border-radius: 100px;
25 | height: 34px;
26 | display: flex;
27 | align-items: center;
28 | padding: 0 .6em;
29 | background: white;
30 | `
31 |
32 | export const UserIcon = styled.div`
33 | height: 30px;
34 | width: 30px;
35 | border-radius: 100px;
36 | &:hover {
37 | cursor: pointer;
38 | }
39 | display: flex;
40 | justify-content: center;
41 | align-items: center;
42 | background: url(${p => p.href || avatar});
43 | background-size: cover;
44 | transition: all 100ms ease-in-out;
45 | border: 2px solid #fff;
46 | ${p=>p.link && `&:hover {
47 | border: ${p.theme.border};
48 | cursor: pointer;
49 | }`}
50 | ${p => p.selected && `
51 | border: ${p.theme.border};
52 | `}
53 | `
54 |
55 | const AccountPicker = styled.div`
56 | position: absolute;
57 | top: 38px;
58 | right: -10px;
59 | padding: 0 10px;
60 | width: 260px;
61 | border: ${p=>p.theme.border};
62 | border-radius: 16px;
63 | background: #fff;
64 | z-index: -300;
65 | transition: all 300ms cubic-bezier(0.455, 0.030, 0.515, 0.955);
66 | ${p => !p.show && `
67 | opacity: 0;
68 | display: none;
69 | `}
70 | `
71 |
72 | const StyledProfile = styled.div`
73 | margin: 0.75em 0;
74 | text-align: right;
75 | display: flex;
76 | align-items: center;
77 | justify-content: right;
78 | gap: 7px;
79 | box-sizing: border-box;
80 | border: 2px solid transparent;
81 | ${p => p.selected && `
82 | background: ${p.theme.primary};
83 | color: #fff;
84 | `}
85 | padding: 0.3em;
86 | border-radius: 16px;
87 | transition: all 100ms ease-in-out;
88 | &:hover {
89 | border: ${p=>p.theme.border};
90 | cursor: pointer;
91 | }
92 | `
93 |
94 | const StyledA = styled.a`
95 | text-decoration: none;
96 | color: black;
97 | transition: all 50ms ease-in-out;
98 | `
99 |
100 | const StyledLink = styled(Link)`
101 | text-decoration: none;
102 | color: black;
103 | transition: all 50ms ease-in-out;
104 | `
105 |
106 | const StyledLogin = styled(Login)`
107 | width: 100%;
108 | background: white;
109 | color: black;
110 | :hover {
111 | background: white;
112 | color: ${p=>p.theme.primary};
113 | }
114 | `
115 |
116 | const Profile = ({ profile, currProfile, handleClick }) => {
117 | return handleClick(profile)} selected={currProfile.id === profile.id}>
118 | @{profile.handle}
119 |
120 |
121 | }
122 |
123 |
124 | function Wallet({ currProfile, setProfile }) {
125 | const { setLensHub, authToken } = useWallet()
126 | const [getProfiles, profiles] = useLazyQuery(GET_PROFILES)
127 | const [openPicker, setPicker] = useState(false)
128 | const { address } = useAccount()
129 | const { data: signer } = useSigner()
130 |
131 | const handleSelect = (profile) => {
132 | setProfile(profile)
133 | setPicker(false)
134 | }
135 |
136 | const handleNew = () => {
137 | console.log('new profile')
138 | setPicker(false)
139 | }
140 |
141 | useEffect(() => {
142 | if (!authToken) return;
143 | if (!address) return;
144 | getProfiles({
145 | variables: {
146 | request: {
147 | // profileIds?: string[];
148 | ownedBy: [address]
149 | // handles?: string[];
150 | // whoMirroredPublicationId?: string;
151 | },
152 | },
153 | })
154 |
155 | }, [address, authToken, getProfiles])
156 |
157 | useEffect(() => {
158 | if (!address) return;
159 | const contractAddr = CHAIN === 'polygon' ? '0xDb46d1Dc155634FbC732f92E853b10B288AD5a1d' : '0x60Ae865ee4C725cd04353b5AAb364553f56ceF82';
160 | const contract = new ethers.Contract(contractAddr, LensHub, signer)
161 | setLensHub(contract)
162 | }, [address, signer, setLensHub])
163 |
164 | useEffect(() => {
165 | if (!profiles.data) return
166 |
167 | setProfile(profiles.data.profiles.items[0])
168 |
169 | }, [profiles.data, setProfile])
170 |
171 | return (
172 |
173 | { address
174 | ? <>
175 |
176 | { authToken
177 | ? <>
178 | {
179 | profiles.data?.profiles.items.map((profile) => )
180 | }
181 | { CHAIN === 'polygon'
182 | ?
183 | handleNew()}>
184 | + Create Profile
185 |
186 |
187 |
188 | :
189 | handleNew()}>
190 | + Create Profile
191 |
192 |
193 |
194 | }
195 | >
196 | :
197 | }
198 |
199 | {address.substring(0, 6)}...{address.substring(38, address.length)}
200 | setPicker(!openPicker)} link={true} selected={openPicker} href={profiles.data?.profiles.items[0]?.picture?.original?.url} />
201 | >
202 | :
203 | }
204 |
205 | );
206 | }
207 |
208 | export default Wallet
--------------------------------------------------------------------------------
/frontend/src/components/WalletButton.jsx:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 | import Button from "./Button";
3 |
4 | const WalletButton = styled(Button)`
5 | width: 14em;
6 | `
7 |
8 | export default WalletButton;
--------------------------------------------------------------------------------
/frontend/src/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { BrowserRouter } from 'react-router-dom'
4 | import App from './App';
5 | ReactDOM.render(
6 |
7 |
8 |
9 |
10 | ,
11 | document.getElementById('root')
12 | );
13 |
14 | // If you want to start measuring performance in your app, pass a function
15 | // to log results (for example: reportWebVitals(console.log))
16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
17 | // reportWebVitals();
18 |
--------------------------------------------------------------------------------
/frontend/src/pages/LandingPage.jsx:
--------------------------------------------------------------------------------
1 | import styled, { withTheme } from 'styled-components'
2 | import bg from '../assets/bg.png'
3 | import logo from '../assets/logo-open.png'
4 | import Wallet from '../components/Wallet'
5 |
6 | const LandingStyle = styled.div`
7 | width: 100vw;
8 | height: 100vh;
9 | display: flex;
10 | background: url(${bg});
11 | background-size: cover;
12 | `
13 |
14 | const Logo = styled.div`
15 | width: 184px;
16 | height: 184px;
17 | display: flex;
18 | padding: 10px;
19 | `
20 |
21 | const Title = styled.div`
22 | font-weight: 600;
23 | font-size: 8em;
24 | color: #220D6D;
25 | display: flex;
26 | float: left;
27 | padding: 10px;
28 | `
29 | const Subtitle = styled.div`
30 | font-weight: 300;
31 | font-size: 2em;
32 | color: #220D6D;
33 | display: flex;
34 | padding: 5px;
35 | `
36 | const Body = styled.div`
37 | margin: auto;
38 | display: flex;
39 | align-items: center;
40 | justify-content: center;
41 | flex-direction: column;
42 | `
43 |
44 | const Center = styled.div`
45 | align-items: center;
46 | display: flex;
47 | justify-content: center;
48 | padding: 2em;
49 | flex-direction: column;
50 | `
51 | const Row = styled.div`
52 | flex-direction: row;
53 | `
54 |
55 | function Landing({ wallet, setWallet, authToken, currProfile, setProfile, setLensHub }) {
56 | return (
57 | <>
58 |
59 |
60 |
61 | iris
62 |
63 |
64 |
65 | decentralized sharing at your fingertips
66 |
67 |
68 | Connect Wallet
69 |
70 |
71 |
72 | >
73 | );
74 | }
75 |
76 |
77 | export default Landing
--------------------------------------------------------------------------------
/frontend/src/pages/NewProfile.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, createRef } from "react";
2 | import styled from "styled-components";
3 | import { Link } from "react-router-dom";
4 | import { useQuery, useMutation } from "@apollo/client";
5 | import { CREATE_PROFILE, MODULE_APPROVAL_DATA } from "../utils/queries";
6 | import Toast from "../components/Toast";
7 | import Card from "../components/Card";
8 | import Button from "../components/Button";
9 | import avatar from "../assets/avatar.png";
10 | import rainbow from "../assets/rainbow.png";
11 |
12 | const Icon = styled.div`
13 | height: 100px;
14 | width: 100px;
15 | border: #fff 4px solid;
16 | border-radius: 100px;
17 | &:hover {
18 | cursor: pointer;
19 | }
20 | display: flex;
21 | justify-content: center;
22 | align-items: center;
23 | background: url(${avatar});
24 | background-size: cover;
25 | `;
26 |
27 | const Handle = styled.input`
28 | border: none;
29 | font-size: 2em;
30 | outline: none;
31 | font-family: ${(p) => p.theme.font};
32 | font-weight: 600;
33 | `;
34 |
35 | const Bio = styled.textarea`
36 | margin: auto;
37 | border: none;
38 | font-size: 1em;
39 | outline: none;
40 | width: 270px;
41 | font-family: ${(p) => p.theme.font};
42 | resize: none; /*remove the resize handle on the bottom right*/
43 | border: #e2e4e8 1px solid;
44 | border-radius: 6px;
45 | margin-top: 1em;
46 | `;
47 |
48 | const Cost = styled.input`
49 | border: none;
50 | font-size: 1em;
51 | outline: none;
52 | font-family: ${(p) => p.theme.font};
53 | font-weight: 600;
54 | width: 270px;
55 | `;
56 |
57 | const StyledCard = styled(Card)`
58 | padding: 0;
59 | `;
60 |
61 | const CardContent = styled.div`
62 | margin-top: -6em;
63 | padding: 2em;
64 | `;
65 |
66 | const Cover = styled.div`
67 | width: 100%;
68 | height: 200px;
69 | background: url(${rainbow});
70 | background-size: cover;
71 | border-radius: 16px 15px 0 0;
72 | `;
73 |
74 | function NewProfile({ profile = {} }) {
75 | const [toastMsg, setToastMsg] = useState({})
76 | const [createProfile, createProfileData] = useMutation(CREATE_PROFILE);
77 | const [submittedHandle, setSubmittedHandle] = useState('')
78 | const handleRef = createRef();
79 | // const costRef = createRef();
80 | // const bioRef = createRef()
81 |
82 | const handleCreate = async () => {
83 | const handle = handleRef.current.value.replace("@", "");
84 | if (!handle) {
85 | console.log("no handle");
86 | return;
87 | }
88 | // const cost = costRef.current.value;
89 | // if (!cost) {
90 | // console.log("no cost");
91 | // return;
92 | // }
93 |
94 | // Block submitting the same handle twice
95 | if(submittedHandle === handle) return;
96 |
97 | const profileRequest = {
98 | handle: handle,
99 | };
100 |
101 | // const bio = bioRef.current.value
102 | setToastMsg({ type: 'loading', msg: 'Creating profile...' })
103 | createProfile({
104 | variables: {
105 | request: profileRequest,
106 | },
107 | });
108 | setSubmittedHandle(handle)
109 | };
110 |
111 | useEffect(() => {
112 | if (!createProfileData.data) return;
113 | console.log(createProfileData.data);
114 | if (createProfileData.data.createProfile.reason === 'HANDLE_TAKEN') {
115 | setToastMsg({ type: 'error', msg: 'Handle Taken' })
116 | } else {
117 | setToastMsg({ type: 'success', msg: 'Profile created!' })
118 | }
119 | }, [createProfileData.data]);
120 |
121 | // const moduleApprovalRequest = {
122 | // currency: "0x9c3c9283d3e44854697cd22d3faa240cfb032889",
123 | // value: "1",
124 | // followModule: "FeeFollowModule",
125 | // };
126 |
127 | // const approveModule = useQuery(MODULE_APPROVAL_DATA, {
128 | // variables: {
129 | // request: moduleApprovalRequest,
130 | // },
131 | // });
132 |
133 | // useEffect(() => {
134 | // if (!approveModule.data) return;
135 |
136 | // const handleCreate = async () => {
137 | // console.log(approveModule.data);
138 |
139 | // const generateModuleCurrencyApprovalData = approveModule.data.generateModuleCurrencyApprovalData;
140 |
141 | // const tx = await wallet.signer.sendTransaction({
142 | // to: generateModuleCurrencyApprovalData.to,
143 | // from: generateModuleCurrencyApprovalData.from,
144 | // data: generateModuleCurrencyApprovalData.data,
145 | // });
146 | // console.log(tx.hash);
147 | // };
148 |
149 | // handleCreate();
150 | // }, [approveModule.data]);
151 |
152 | const handleHandle = (e) => {
153 | if (e.target.value[0] !== "@") {
154 | e.target.value = "@" + e.target.value;
155 | }
156 | if (e.target.value.length === 1) {
157 | e.target.value = "";
158 | }
159 | };
160 |
161 | return <>
162 | {toastMsg.msg}
163 |
164 |
165 |
166 |
167 |
168 | {/* */}
169 | {/* */}
170 |
171 |
172 |
173 |
174 |
175 | >
176 | }
177 |
178 | export default NewProfile;
--------------------------------------------------------------------------------
/frontend/src/pages/NotFound.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Link } from "react-router-dom";
3 |
4 | function NotFound() {
5 | return (
6 | <>
7 |
8 | 404. There's nothing here!
9 | Go Home
10 |
11 | >
12 | );
13 | }
14 |
15 | export default NotFound
--------------------------------------------------------------------------------
/frontend/src/pages/Outlet.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Outlet } from "react-router-dom";
3 |
4 | function OutletPage() {
5 | return (
6 | <>
7 |
8 | >
9 | );
10 | }
11 |
12 | export default OutletPage
--------------------------------------------------------------------------------
/frontend/src/pages/Post.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react'
2 | import styled from 'styled-components'
3 | import { useParams } from 'react-router-dom'
4 | import { useLazyQuery } from '@apollo/client'
5 | import { useAccount } from 'wagmi'
6 | import { GET_PUBLICATION, GET_PUBLICATIONS } from '../utils/queries'
7 | import PostComponent from '../components/Post'
8 | import Compose from '../components/Compose'
9 | import Card from '../components/Card'
10 | import { useWallet } from '../utils/wallet'
11 |
12 | const StyledCard = styled(Card)`
13 | padding: 0;
14 | margin-bottom: 1em;
15 | `
16 |
17 | function Post({ profileId, profileName }) {
18 | const { address } = useAccount()
19 | let params = useParams();
20 | const [publication, setPublication] = useState({})
21 | const [notFound, setNotFound] = useState(false)
22 | const [comments, setComments] = useState([]);
23 | const [isCommunity, setIsCommunity] = useState(false)
24 |
25 | const [getPublication, publicationData] = useLazyQuery(GET_PUBLICATION)
26 | const [getPublications, publicationsData] = useLazyQuery(GET_PUBLICATIONS);
27 |
28 | useEffect(() => {
29 | getPublication({
30 | variables: {
31 | request: { publicationId: params.postId },
32 | reactionRequest: profileId ? { profileId } : null,
33 | },
34 | });
35 | }, [profileId])
36 |
37 | useEffect(() => {
38 | if (!publicationData.data) return;
39 | if (!publicationData.data.publication) {
40 | setNotFound(true)
41 | return
42 | };
43 |
44 | setPublication({...publication, ...publicationData.data.publication})
45 | publicationData.data.publication.metadata?.attributes.forEach(attribute => {
46 | if(attribute.value === 'community') {
47 | setIsCommunity(true)
48 | }
49 | })
50 | }, [publicationData.data])
51 |
52 | useEffect(() => {
53 | getPublications({
54 | variables: {
55 | request: {
56 | commentsOf: params.postId
57 | },
58 | },
59 | });
60 | }, [getPublications, params.postId])
61 |
62 |
63 | useEffect(() => {
64 | if (!publicationsData.data) return;
65 |
66 | setComments(publicationsData.data.publications.items);
67 |
68 | }, [address, publicationsData.data]);
69 |
70 | return (
71 | <>
72 | {notFound && No Post Found
}
73 |
74 | {publication.metadata && }
75 |
76 |
85 | {comments.length > 0 && Comments
}
86 | {comments.map((post) => {
87 | return ;
88 | })}
89 | >
90 | );
91 | }
92 |
93 | export default Post
94 |
--------------------------------------------------------------------------------
/frontend/src/pages/User.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | import { useParams } from "react-router-dom";
3 | import styled from "styled-components";
4 | import { useLazyQuery, useQuery } from "@apollo/client";
5 | import { GET_PROFILES, GET_PUBLICATIONS } from "../utils/queries";
6 | import { hexToDec } from "../utils";
7 | import { CHAIN } from '../utils/constants'
8 | import Follow from "../components/Follow";
9 | import Unfollow from "../components/Unfollow";
10 | import Post from "../components/Post";
11 | import Card from "../components/Card";
12 | import Livestream from "../components/Livestream";
13 | import Button from '../components/Button'
14 | import avatar from "../assets/avatar.png";
15 | import rainbow from "../assets/rainbow.png";
16 | import opensea from "../assets/opensea.svg";
17 | import { useWallet } from "../utils/wallet";
18 |
19 | const Icon = styled.div`
20 | height: 100px;
21 | width: 100px;
22 | border: #fff 4px solid;
23 | border-radius: 100px;
24 | display: flex;
25 | justify-content: center;
26 | align-items: center;
27 | background: url(${p => p.href || avatar});
28 | background-size: cover;
29 | margin-bottom: -0.8em;
30 | `;
31 |
32 | const LiveIcon = styled.div`
33 | height: 50px;
34 | width: 50px;
35 | border: ${p => p.theme.primary} 4px solid;
36 | border-radius: 100px;
37 | &:hover {
38 | cursor: pointer;
39 | }
40 | background: url(${p => p.href || avatar});
41 | background-size: cover;
42 | margin-bottom: -0.6em;
43 | `;
44 |
45 | const StyledCard = styled(Card)`
46 | padding: 0;
47 | margin-bottom: 2em;
48 | `;
49 |
50 | const CardContent = styled.div`
51 | margin-top: -6em;
52 | padding: 2em;
53 | `;
54 |
55 | const LiveCardContent = styled.div`
56 | padding: 2em;
57 | `;
58 |
59 | const Live = styled.span`
60 | position: absolute;
61 | margin-top: 60px;
62 | margin-left: 13px;
63 | background: red;
64 | color: white;
65 | font-weight: 500;
66 | font-size: 12px;
67 | border-radius: 6px;
68 | padding: 0.1em 0.4em;
69 | `;
70 |
71 |
72 | const Cover = styled.div`
73 | width: 100%;
74 | height: 200px;
75 | background: url(${p => p.src || rainbow});
76 | background-size: cover;
77 | border-radius: 8px 8px 0 0;
78 | `;
79 |
80 | const Stats = styled.div`
81 | @media (min-width: 768px) {
82 | width: 400px;
83 | display: flex;
84 | justify-content: space-between;
85 | }
86 | `;
87 |
88 | const Columns = styled.div`
89 | display: flex;
90 | justify-content: space-between;
91 | `;
92 |
93 | const Name = styled.h1`
94 | margin-bottom: 0;
95 | `;
96 |
97 | const Handle = styled.h3`
98 | font-weight: normal;
99 | margin: 0 0 1em 0;
100 | `;
101 |
102 | const Address = styled.code`
103 | box-shadow: 0px 2px 5px rgba(200, 176, 178, 0.6);
104 | border-radius: 100px;
105 | padding: 0.6em;
106 | background: white;
107 | margin: 4em 0;
108 | `;
109 |
110 | const UserInfo = styled.div`
111 | margin-bottom: 2em;
112 | `;
113 |
114 | const ProfileOptions = styled.div`
115 | display: flex;
116 | flex-direction: row;
117 | gap: 1em;
118 | `;
119 |
120 | function User({ profileId }) {
121 | const { wallet } = useWallet()
122 | let params = useParams();
123 | const [notFound, setNotFound] = useState(false);
124 | const [publications, setPublications] = useState([]);
125 | const [profile, setProfile] = useState("");
126 | const [following, setFollowing] = useState(false);
127 |
128 | const [streamInfo, setStreamInfo] = useState({});
129 |
130 | const { data } = useQuery(GET_PROFILES, {
131 | variables: {
132 | request: {
133 | handles: [params.handle],
134 | limit: 1,
135 | },
136 | },
137 | });
138 |
139 | const [getPublications, publicationsData] = useLazyQuery(GET_PUBLICATIONS);
140 |
141 | useEffect(() => {
142 | if (!data) return;
143 |
144 | if (data.profiles.items.length < 1) {
145 | setNotFound(true);
146 | return;
147 | }
148 |
149 | const ownedBy = data.profiles.items[0].ownedBy;
150 | const id = data.profiles.items[0].id;
151 | const decId = hexToDec(id.replace("0x", ""));
152 |
153 | setProfile({
154 | ...data.profiles.items[0],
155 | address: `${ownedBy.substring(0, 6)}...${ownedBy.substring(37, ownedBy.length - 1)}`,
156 | decId,
157 | });
158 |
159 | getPublications({
160 | variables: {
161 | request: {
162 | profileId: data.profiles.items[0].id,
163 | publicationTypes: ["POST", "COMMENT", "MIRROR"],
164 | },
165 | reactionRequest: profileId ? { profileId } : null,
166 | },
167 | });
168 |
169 | // const isUserLivestreaming = async () => {
170 |
171 | // const response = await fetch("https://livepeer.com/api/stream?streamsonly=1&filters=[{id: isActive, value: true}]",
172 | // {
173 | // headers: {
174 | // // TODO: Remove API KEY in the future
175 | // "authorization": "Bearer fe3ed427-ab88-415e-b691-8fba9e7e6fb0"
176 | // }
177 | // },
178 | // );
179 | // const responseData = await response.json();
180 |
181 | // responseData.map((streamInfo) => {
182 | // if (streamInfo.isActive & streamInfo.name === `${ownedBy},${handle}`) {
183 | // setStreamInfo(streamInfo)
184 | // }
185 | // })
186 |
187 | // }
188 |
189 | // isUserLivestreaming()
190 |
191 |
192 | }, [data, profileId, getPublications]);
193 |
194 | useEffect(() => {
195 | if (!publicationsData.data) return;
196 |
197 | setPublications(publicationsData.data.publications.items);
198 |
199 | }, [publicationsData.data]);
200 |
201 | if (notFound) {
202 | return (
203 | <>
204 | No user with handle {params.handle}!
205 | >
206 | );
207 | }
208 |
209 | return (
210 | <>
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 | {profile.name || params.handle}
219 | @{params.handle}
220 | {profile?.address}
221 | {/*
226 |
227 | */}
228 |
229 |
230 | {profile.stats?.totalFollowers} followers
231 | {profile.stats?.totalFollowing} following
232 | {profile.stats?.totalPublications} posts
233 | {profile.stats?.totalCollects} collects
234 |
235 |
236 |
237 |
238 | {profile.isFollowedByMe ? (
239 |
240 | ) : (
241 |
242 | )}
243 |
244 |
249 |
250 |
251 |
252 |
253 |
254 | {publications.map((post) => {
255 | return ;
256 | })}
257 |
258 | >
259 | );
260 | }
261 |
262 | export default User;
263 |
--------------------------------------------------------------------------------
/frontend/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/frontend/src/theme/GlobalStyle.jsx:
--------------------------------------------------------------------------------
1 | import { createGlobalStyle } from 'styled-components'
2 |
3 | export default createGlobalStyle`
4 | @font-face {
5 | font-family: 'General Sans';
6 | src: url('https://cdn.fontshare.com/wf/MFQT7HFGCR2L5ULQTW6YXYZXXHMPKLJ3/YWQ244D6TACUX5JBKATPOW5I5MGJ3G73/7YY3ZAAE3TRV2LANYOLXNHTPHLXVWTKH.woff2') format('woff2'),
7 | url('https://cdn.fontshare.com/wf/MFQT7HFGCR2L5ULQTW6YXYZXXHMPKLJ3/YWQ244D6TACUX5JBKATPOW5I5MGJ3G73/7YY3ZAAE3TRV2LANYOLXNHTPHLXVWTKH.woff') format('woff'),
8 | url('https://cdn.fontshare.com/wf/MFQT7HFGCR2L5ULQTW6YXYZXXHMPKLJ3/YWQ244D6TACUX5JBKATPOW5I5MGJ3G73/7YY3ZAAE3TRV2LANYOLXNHTPHLXVWTKH.ttf') format('truetype');
9 | font-weight: 400;
10 | font-display: swap;
11 | font-style: normal;
12 | }
13 |
14 | @font-face {
15 | font-family: 'General Sans';
16 | src: url('https://cdn.fontshare.com/wf/3RZHWSNONLLWJK3RLPEKUZOMM56GO4LJ/BPDRY7AHVI3MCDXXVXTQQ76H3UXA63S3/SB2OEB6IKZPRR6JT4GFJ2TFT6HBB6AZN.woff2') format('woff2'),
17 | url('https://cdn.fontshare.com/wf/3RZHWSNONLLWJK3RLPEKUZOMM56GO4LJ/BPDRY7AHVI3MCDXXVXTQQ76H3UXA63S3/SB2OEB6IKZPRR6JT4GFJ2TFT6HBB6AZN.woff') format('woff'),
18 | url('https://cdn.fontshare.com/wf/3RZHWSNONLLWJK3RLPEKUZOMM56GO4LJ/BPDRY7AHVI3MCDXXVXTQQ76H3UXA63S3/SB2OEB6IKZPRR6JT4GFJ2TFT6HBB6AZN.ttf') format('truetype');
19 | font-weight: 500;
20 | font-display: swap;
21 | font-style: normal;
22 | }
23 |
24 | @font-face {
25 | font-family: 'General Sans';
26 | src: url('https://cdn.fontshare.com/wf/K46YRH762FH3QJ25IQM3VAXAKCHEXXW4/ISLWQPUZHZF33LRIOTBMFOJL57GBGQ4B/3ZLMEXZEQPLTEPMHTQDAUXP5ZZXCZAEN.woff2') format('woff2'),
27 | url('https://cdn.fontshare.com/wf/K46YRH762FH3QJ25IQM3VAXAKCHEXXW4/ISLWQPUZHZF33LRIOTBMFOJL57GBGQ4B/3ZLMEXZEQPLTEPMHTQDAUXP5ZZXCZAEN.woff') format('woff'),
28 | url('https://cdn.fontshare.com/wf/K46YRH762FH3QJ25IQM3VAXAKCHEXXW4/ISLWQPUZHZF33LRIOTBMFOJL57GBGQ4B/3ZLMEXZEQPLTEPMHTQDAUXP5ZZXCZAEN.ttf') format('truetype');
29 | font-weight: 600;
30 | font-display: swap;
31 | font-style: normal;
32 | }
33 |
34 | @font-face {
35 | font-family: 'General Sans';
36 | src: url('https://cdn.fontshare.com/wf/KWXO5X3YW4X7OLUMPO4X24HQJGJU7E2Q/VOWUQZS3YLP66ZHPTXAFSH6YACY4WJHT/NIQ54PVBBIWVK3PFSOIOUJSXIJ5WTNDP.woff2') format('woff2'),
37 | url('https://cdn.fontshare.com/wf/KWXO5X3YW4X7OLUMPO4X24HQJGJU7E2Q/VOWUQZS3YLP66ZHPTXAFSH6YACY4WJHT/NIQ54PVBBIWVK3PFSOIOUJSXIJ5WTNDP.woff') format('woff'),
38 | url('https://cdn.fontshare.com/wf/KWXO5X3YW4X7OLUMPO4X24HQJGJU7E2Q/VOWUQZS3YLP66ZHPTXAFSH6YACY4WJHT/NIQ54PVBBIWVK3PFSOIOUJSXIJ5WTNDP.ttf') format('truetype');
39 | font-weight: 700;
40 | font-display: swap;
41 | font-style: normal;
42 | }
43 | @font-face {
44 | font-family: 'Fira Mono';
45 | font-style: normal;
46 | font-weight: 400;
47 | font-display: swap;
48 | src: url(https://fonts.gstatic.com/s/firamono/v12/N0bX2SlFPv1weGeLZDtgJv7S.woff2) format('woff2');
49 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
50 | }
51 | body {
52 | margin: 0;
53 | font-family: ${p => p.theme.font};
54 | background: ${p => p.theme.background};
55 | color: ${p => p.theme.text};
56 | letter-spacing: 0.02em;
57 | }
58 | h1, h2, h3, h4, b {
59 | font-weight: 600;
60 | margin: .4em 0;
61 | }
62 | p {
63 | margin: 0.3em 0;
64 | }
65 | code {
66 | font-family: 'Fira Mono', monospace;
67 | font-size: 0.9em;
68 | }
69 | a {
70 | text-decoration: none;
71 | color: ${(p) => p.theme.primary};
72 | transition: all 50ms ease-in-out;
73 | &:hover {
74 | color: ${(p) => p.theme.primaryHover};
75 | }
76 | }
77 | `
--------------------------------------------------------------------------------
/frontend/src/theme/ThemeProvider.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { ThemeProvider } from 'styled-components'
3 |
4 | const theme = {
5 | primary: '#FF835B',
6 | primaryHover: '#FF9C7D',
7 | border: '#F28A56 2px solid',
8 | background: '#EFEFEF',
9 | text: '#232323',
10 | textLight: '#fff',
11 | greyed: '#747c90',
12 | error: '#FF3236',
13 | darken: '#fffaf8',
14 | darken2: '#FFF3EE',
15 | font: `'General Sans', sans-serif`,
16 | hrefUnderline: `
17 | display: inline-block;
18 | &:after {
19 | content: '';
20 | display: block;
21 | margin: auto;
22 | height: 2px;
23 | width: 0px;
24 | background: transparent;
25 | transition: width 150ms ease, background-color 150ms ease;
26 | }
27 | &:hover:after {
28 | width: 100%;
29 | background: #FF9C7D;
30 | }
31 | `,
32 | }
33 |
34 |
35 | export default ({ children }) => {children}
--------------------------------------------------------------------------------
/frontend/src/utils/constants.jsx:
--------------------------------------------------------------------------------
1 | export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
2 | export const CHAIN = import.meta.env.VITE_CHAIN ? import.meta.env.VITE_CHAIN : import.meta.env.NODE_ENV === 'production' ? 'polygon' : 'mumbai'
--------------------------------------------------------------------------------
/frontend/src/utils/index.jsx:
--------------------------------------------------------------------------------
1 | export function hexToDec(s) {
2 | var i, j, digits = [0], carry;
3 | for (i = 0; i < s.length; i += 1) {
4 | carry = parseInt(s.charAt(i), 16);
5 | for (j = 0; j < digits.length; j += 1) {
6 | digits[j] = digits[j] * 16 + carry;
7 | carry = digits[j] / 10 | 0;
8 | digits[j] %= 10;
9 | }
10 | while (carry > 0) {
11 | digits.push(carry % 10);
12 | carry = carry / 10 | 0;
13 | }
14 | }
15 | return digits.reverse().join('');
16 | }
17 |
18 | export const blobToBase64 = (blob) => {
19 | return new Promise((resolve) => {
20 | const reader = new FileReader();
21 | reader.readAsDataURL(blob);
22 | reader.onloadend = function () {
23 | resolve(reader.result);
24 | };
25 | });
26 | };
27 |
28 | export const random = () => {
29 | return (Math.random() + 1).toString(36).substring(7);
30 | }
31 |
32 | export const toHex = (num) => {
33 | const val = Number(num);
34 | return "0x" + val.toString(16);
35 | };
36 |
--------------------------------------------------------------------------------
/frontend/src/utils/infuraClient.jsx:
--------------------------------------------------------------------------------
1 | import { create } from 'ipfs-http-client'
2 |
3 | const auth = 'Basic ' + Buffer.from(import.meta.env.VITE_INFURA_PROJECT_ID + ':' + import.meta.env.VITE_INFURA_API_KEY).toString('base64');
4 |
5 | export const client = create({
6 | host: 'ipfs.infura.io',
7 | port: 5001,
8 | protocol: 'https',
9 | headers: {
10 | authorization: auth,
11 | },
12 | });
--------------------------------------------------------------------------------
/frontend/src/utils/litIntegration.jsx:
--------------------------------------------------------------------------------
1 | import { v4 as uuidv4 } from 'uuid'
2 | import { CHAIN } from '../utils/constants'
3 | import { client } from './infuraClient'
4 |
5 | const getAccessControlConditions = async (params) => {
6 | const {description, lensHub, profileId, profileName, selectedVisibility, replyTo} = params
7 |
8 | switch(selectedVisibility) {
9 | case 'follower':
10 | const followNFTAddr = await lensHub.getFollowNFT(profileId);
11 | return [
12 | {
13 | contractAddress: followNFTAddr,
14 | standardContractType: 'ERC721',
15 | chain: CHAIN,
16 | method: 'balanceOf',
17 | parameters: [
18 | ':userAddress',
19 | ],
20 | returnValueTest: {
21 | comparator: '>',
22 | value: '0'
23 | }
24 | }
25 | ]
26 | case 'collector':
27 | case 'community':
28 | const pubProfileId = replyTo.split('-')[0]
29 | const pubId = replyTo.split('-')[1]
30 | const collectNFTAddr = await lensHub.getCollectNFT(pubProfileId, pubId);
31 | if (collectNFTAddr === '0x0000000000000000000000000000000000000000') {
32 | console.warn('getCollectNFT returned 0x0 address')
33 | return
34 | }
35 | return [
36 | {
37 | contractAddress: collectNFTAddr,
38 | standardContractType: 'ERC721',
39 | chain: CHAIN,
40 | method: 'balanceOf',
41 | parameters: [
42 | ':userAddress',
43 | ],
44 | returnValueTest: {
45 | comparator: '>',
46 | value: '0'
47 | }
48 | }
49 | ]
50 | default:
51 | console.warn(`invalid selectedVisibilty ${selectedVisibility}`)
52 | return []
53 | }
54 |
55 | }
56 |
57 | // const getEncodedMetadata = async (params) => {
58 | // const {description, profileId, profileName, selectedVisibility, replyTo} = params
59 |
60 | // const { encryptedString, symmetricKey } = await LitJsSdk.encryptString(
61 | // description
62 | // );
63 |
64 | // const authSig = JSON.parse(window.sessionStorage.getItem('signature'))
65 |
66 | // const accessControlConditions = await getAccessControlConditions(params)
67 |
68 | // const encryptedSymmetricKey = await window.litNodeClient.saveEncryptionKey({
69 | // accessControlConditions,
70 | // symmetricKey,
71 | // authSig,
72 | // chain: CHAIN,
73 | // });
74 |
75 | // const ipfsResult = await client.add(encryptedString)
76 |
77 | // const encryptedPost = {
78 | // key: LitJsSdk.uint8arrayToString(encryptedSymmetricKey, "base16"),
79 | // blobPath: ipfsResult.path,
80 | // accessControlConditions
81 | // }
82 |
83 | // const attributes = [];
84 | // const metadata = {
85 | // name: `encoded post by ${profileName}`,
86 | // description: `This post is encoded. View it on https://irisapp.xyz`,
87 | // content: `This post is encoded. View it on https://irisapp.xyz`,
88 | // external_url: `https://irisapp.xyz`,
89 | // image: null,
90 | // imageMimeType: null,
91 | // version: "1.0.0",
92 | // appId: 'iris exclusive',
93 | // attributes,
94 | // media: [],
95 | // metadata_id: uuidv4(),
96 | // }
97 |
98 | // attributes.push({
99 | // traitType: 'Encoded Post Data',
100 | // value: `${JSON.stringify(encryptedPost)}`,
101 | // })
102 |
103 | // return metadata
104 | // }
105 |
106 | export const handleCompose = async (params) => {
107 | const {description, profileId, profileName, selectedVisibility, replyTo, mutateCommentTypedData, mutatePostTypedData} = params
108 | if (!description) return;
109 |
110 | let ipfsResult;
111 | let metadata;
112 |
113 | // if(selectedVisibility !== 'public') {
114 | // metadata = await getEncodedMetadata(params)
115 | // } else {
116 | metadata = {
117 | name: `post by ${profileName}`,
118 | description,
119 | content: description,
120 | external_url: null,
121 | image: null,
122 | imageMimeType: null,
123 | version: "1.0.0",
124 | appId: 'iris',
125 | attributes: [],
126 | media: [],
127 | metadata_id: uuidv4(),
128 | }
129 | // }
130 |
131 | // For Only Text Post
132 | ipfsResult = await client.add(JSON.stringify(metadata))
133 |
134 | if(replyTo) {
135 | const createCommentRequest = {
136 | profileId,
137 | publicationId: replyTo,
138 | contentURI: 'ipfs://' + ipfsResult.path,
139 | collectModule: {
140 | freeCollectModule: {
141 | followerOnly: false
142 | },
143 | },
144 | referenceModule: {
145 | followerOnlyReferenceModule: false,
146 | },
147 | };
148 |
149 | mutateCommentTypedData({
150 | variables: {
151 | request: createCommentRequest ,
152 | }
153 | })
154 | } else {
155 | const createPostRequest = {
156 | profileId,
157 | contentURI: 'ipfs://' + ipfsResult.path,
158 | collectModule: {
159 | freeCollectModule: {
160 | followerOnly: false
161 | },
162 | },
163 | referenceModule: {
164 | followerOnlyReferenceModule: false,
165 | },
166 | };
167 |
168 | mutatePostTypedData({
169 | variables: {
170 | request: createPostRequest,
171 | }
172 | })
173 | }
174 |
175 | }
176 |
--------------------------------------------------------------------------------
/frontend/src/utils/pollUntilIndexed.jsx:
--------------------------------------------------------------------------------
1 | import { HAS_TX_BEEN_INDEXED } from './queries'
2 | import { client } from '../components/Apollo'
3 |
4 | const hasTxBeenIndexed = (txHash) => {
5 | return client.query({
6 | query: HAS_TX_BEEN_INDEXED,
7 | variables: {
8 | request: {
9 | txHash,
10 | },
11 | },
12 | fetchPolicy: 'network-only',
13 | });
14 | };
15 |
16 | async function pollUntilIndexed (txHash) {
17 | const sleep = (milliseconds) => {
18 | return new Promise((resolve) => setTimeout(resolve, milliseconds));
19 | };
20 |
21 | while (true) {
22 | const result = await hasTxBeenIndexed(txHash);
23 | // console.log('pool until indexed: result', result.data);
24 |
25 | const response = result.data.hasTxHashBeenIndexed;
26 | if (response.__typename === 'TransactionIndexedResult') {
27 | // console.log('pool until indexed: indexed', response.indexed);
28 | // console.log('pool until metadataStatus: metadataStatus', response.metadataStatus);
29 |
30 | if (response.metadataStatus) {
31 | if (response.metadataStatus.status === 'SUCCESS') {
32 | return response;
33 | }
34 |
35 | if (response.metadataStatus.status === 'METADATA_VALIDATION_FAILED') {
36 | throw new Error(response.metadataStatus.reason);
37 | }
38 | } else {
39 | if (response.indexed) {
40 | return response;
41 | }
42 | }
43 |
44 | // console.log('pool until indexed: sleep for 1500 milliseconds then try again');
45 | // sleep for a second before trying again
46 | await sleep(1500);
47 | } else {
48 | // it got reverted and failed!
49 | throw new Error(response.reason);
50 | }
51 | }
52 | };
53 |
54 |
55 | export default pollUntilIndexed
56 |
--------------------------------------------------------------------------------
/frontend/src/utils/wallet.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | const WalletContext = React.createContext();
4 |
5 | const WalletContextProvider = ({ children }) => {
6 | const [authToken, setAuthToken] = useState(false);
7 | const [lensHub, setLensHub] = useState();
8 |
9 | return (
10 |
11 | {children}
12 |
13 | );
14 | };
15 |
16 | const useWallet = () => {
17 | const context = React.useContext(WalletContext)
18 | if (context === undefined) {
19 | throw new Error('useWallet must be used within a WalletContextProvider')
20 | }
21 | return context
22 | }
23 |
24 | export { WalletContextProvider, useWallet }
25 |
--------------------------------------------------------------------------------
/frontend/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "types": ["vite/client", "vite-plugin-svgr/client"],
10 | "allowJs": false,
11 | "skipLibCheck": false,
12 | "esModuleInterop": false,
13 | "allowSyntheticDefaultImports": true,
14 | "strict": true,
15 | "forceConsistentCasingInFileNames": true,
16 | "noFallthroughCasesInSwitch": true,
17 | "module": "esnext",
18 | "moduleResolution": "node",
19 | "resolveJsonModule": true,
20 | "isolatedModules": true,
21 | "noEmit": true,
22 | "jsx": "react-jsx"
23 | },
24 | "include": [
25 | "src"
26 | ]
27 | }
28 |
--------------------------------------------------------------------------------
/frontend/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import reactRefresh from '@vitejs/plugin-react'
3 | import svgrPlugin from 'vite-plugin-svgr'
4 | import { NodeGlobalsPolyfillPlugin } from '@esbuild-plugins/node-globals-polyfill'
5 |
6 | // https://vitejs.dev/config/
7 | export default defineConfig({
8 | // This changes the out put dir from dist to build
9 | // comment this out if that isn't relevant for your project
10 | build: {
11 | outDir: 'build',
12 | },
13 | plugins: [
14 | reactRefresh(),
15 | svgrPlugin({
16 | svgrOptions: {
17 | icon: true,
18 | // ...svgr options (https://react-svgr.com/docs/options/)
19 | },
20 | }),
21 | ],
22 | optimizeDeps: {
23 | esbuildOptions: {
24 | // Node.js global to browser globalThis
25 | define: {
26 | global: 'globalThis'
27 | },
28 | // Enable esbuild polyfill plugins
29 | plugins: [
30 | NodeGlobalsPolyfillPlugin({
31 | buffer: true
32 | })
33 | ]
34 | }
35 | }
36 | })
--------------------------------------------------------------------------------