Skip to content

Commit

Permalink
feat: useOptimisticCreatePost hook (#878)
Browse files Browse the repository at this point in the history
* Fix typo

* Generalized DeferredTaskState<TData, TError> ready to support coming changes

* Fix examples/react-native app on Android

* Iteration

* useOptimisticCreatePost hook

* Improves IUploader interface

* Address code review comments and improves docs

* Adds initialize method to IUploader interface
  • Loading branch information
cesarenaldi authored Mar 19, 2024
1 parent 8fbfdc9 commit 177879d
Show file tree
Hide file tree
Showing 42 changed files with 1,642 additions and 267 deletions.
10 changes: 10 additions & 0 deletions .changeset/early-gorillas-enjoy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@lens-protocol/react-native": minor
"@lens-protocol/react-web": minor
"@lens-protocol/react": minor
"@lens-protocol/gated-content": patch
"@lens-protocol/shared-kernel": patch
"@lens-protocol/api-bindings": patch
---

**feat:** adds experimental `useOptimisticCreatePost` hook
3 changes: 2 additions & 1 deletion examples/react-native/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "native-lens",
"name": "example-native",
"version": "0.0.1",
"private": true,
"scripts": {
Expand Down Expand Up @@ -40,6 +40,7 @@
"@rnx-kit/metro-config": "^1.3.14",
"@rnx-kit/metro-resolver-symlinks": "^0.1.35",
"@types/react": "^18.2.6",
"@types/react-native": "^0.73.0",
"eslint": "^8.54.0",
"prettier": "2.8.8",
"typescript": "5.0.4"
Expand Down
12 changes: 7 additions & 5 deletions examples/react-native/src/MyProfile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,13 @@ function ProfileAvatar({ profile }: ProfileAvatarProps) {
<AvatarFallbackText fontFamily="$heading">
{profile.handle?.localName ?? profile.ownedBy.address}
</AvatarFallbackText>
<AvatarImage
source={{
uri,
}}
/>
{uri && (
<AvatarImage
source={{
uri,
}}
/>
)}
</Avatar>
);
}
Expand Down
2 changes: 2 additions & 0 deletions examples/web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ import {
UseLazyPublications,
UseNotInterestedToggle,
UseOpenAction,
UseOptimisticCreatePost,
UsePublication,
UsePublications,
UseReactionToggle,
Expand Down Expand Up @@ -106,6 +107,7 @@ export function App() {
<Route path="/publications">
<Route index element={<PublicationsPage />} />
<Route path="useCreatePost" element={<UseCreatePost />} />
<Route path="useOptimisticCreatePost" element={<UseOptimisticCreatePost />} />
<Route path="useCreateComment" element={<UseCreateComment />} />
<Route path="useCreateMirror" element={<UseCreateMirror />} />
<Route path="useCreateQuote" element={<UseCreateQuote />} />
Expand Down
8 changes: 5 additions & 3 deletions examples/web/src/components/cards.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ function MetadataSwitch({ metadata }: { metadata: PublicationMetadata }) {
switch (metadata.__typename) {
case 'ArticleMetadataV3':
case 'TextOnlyMetadataV3':
case 'ImageMetadataV3':
return <p>{metadata.content}</p>;

case 'ImageMetadataV3':
return <img src={metadata.asset.image.raw.uri} alt={metadata.asset.altTag ?? undefined} />;

default:
return <p>{metadata.__typename} not supported in this example</p>;
}
Expand Down Expand Up @@ -45,7 +47,7 @@ function PublicationFacts({ publication }: { publication: PrimaryPublication })
</span>
</p>

{publication.operations.hasCollected && <p>You already collected this publication</p>}
{publication.operations.hasCollected.value && <p>You already collected this publication</p>}
</>
);
}
Expand Down Expand Up @@ -78,7 +80,7 @@ type PublicationCardProps = {

export function PublicationCard({ publication, children }: PublicationCardProps) {
return (
<article>
<article style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }}>
<div
style={{
display: 'flex',
Expand Down
50 changes: 35 additions & 15 deletions examples/web/src/hooks/useIrysUploader.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Web3Provider } from '@ethersproject/providers';
import { WebIrys } from '@irys/sdk';
import { Uploader } from '@lens-protocol/react-web';
import { useMemo } from 'react';
import { Account, Chain, Client, Transport } from 'viem';
import { useConnectorClient } from 'wagmi';

Expand Down Expand Up @@ -30,31 +32,49 @@ async function getWebIrys(client: Client<Transport, Chain, Account>) {
return webIrys;
}

export function useIrysUploader() {
export function useIrysUploadHandler() {
const { data: client } = useConnectorClient();

return {
uploadMetadata: async (data: unknown) => {
const confirm = window.confirm(
`In this example we will now upload metadata file via the Bundlr Network.
return async (data: unknown) => {
const confirm = window.confirm(
`In this example we will now upload metadata file via the Irys.
Please make sure your wallet is connected to the Polygon Mumbai testnet.
You can get some Mumbai MATIC from the Mumbai Faucet: https://mumbaifaucet.com/`,
);
);

if (!confirm) {
throw new Error('User cancelled');
}

const irys = await getWebIrys(client ?? never('viem Client not found'));

const serialized = JSON.stringify(data);
const tx = await irys.upload(serialized, {
tags: [{ name: 'Content-Type', value: 'application/json' }],
});

return `https://arweave.net/${tx.id}`;
};
}

export function useIrysUploader() {
const { data: client } = useConnectorClient();

return useMemo(() => {
return new Uploader(async (file: File) => {
const irys = await getWebIrys(client ?? never('viem Client not found'));

const confirm = window.confirm(`Uploading '${file.name}' via the Irys.`);

if (!confirm) {
throw new Error('User cancelled');
}

const irys = await getWebIrys(client ?? never('viem Client not found'));

const serialized = JSON.stringify(data);
const tx = await irys.upload(serialized, {
tags: [{ name: 'Content-Type', value: 'application/json' }],
});
const receipt = await irys.uploadFile(file);

return `https://arweave.net/${tx.id}`;
},
};
return `https://arweave.net/${receipt.id}`;
});
}, [client]);
}
4 changes: 2 additions & 2 deletions examples/web/src/misc/UseApproveModule.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { Logs } from '../components/Logs';
import { PublicationCard } from '../components/cards';
import { ErrorMessage } from '../components/error/ErrorMessage';
import { Loading } from '../components/loading/Loading';
import { useIrysUploader } from '../hooks/useIrysUploader';
import { useIrysUploadHandler } from '../hooks/useIrysUploader';
import { useLogs } from '../hooks/useLogs';
import { never } from '../utils';

Expand Down Expand Up @@ -86,7 +86,7 @@ function TestScenario({ publicationId }: { publicationId: PublicationId }) {
}

export function UseApproveModule() {
const { uploadMetadata } = useIrysUploader();
const uploadMetadata = useIrysUploadHandler();
const [id, setId] = useState<PublicationId | undefined>();
const { address } = useAccount();
const { data: currencies, loading, error } = useCurrencies();
Expand Down
4 changes: 2 additions & 2 deletions examples/web/src/profiles/UseSetProfileMetadata.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ import { Profile, useSetProfileMetadata } from '@lens-protocol/react-web';
import { toast } from 'react-hot-toast';

import { RequireProfileSession } from '../components/auth';
import { useIrysUploader } from '../hooks/useIrysUploader';
import { useIrysUploadHandler } from '../hooks/useIrysUploader';
import { ProfileCard } from './components/ProfileCard';

type UpdateProfileFormProps = {
activeProfile: Profile;
};

function UpdateProfileForm({ activeProfile }: UpdateProfileFormProps) {
const { uploadMetadata } = useIrysUploader();
const uploadMetadata = useIrysUploadHandler();
const { execute: update, error, loading } = useSetProfileMetadata();

async function onSubmit(event: React.FormEvent<HTMLFormElement>) {
Expand Down
5 changes: 5 additions & 0 deletions examples/web/src/publications/PublicationsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ const publicationHooks = [
description: `Create a post.`,
path: '/publications/useCreatePost',
},
{
label: 'useOptimisticCreatePost',
description: `Experimental: create a post with optimistic logic.`,
path: '/publications/useOptimisticCreatePost',
},
{
label: 'useCreateComment',
description: `Leave a comment on another publication.`,
Expand Down
4 changes: 2 additions & 2 deletions examples/web/src/publications/UseCreateComment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@ import { RequireProfileSession } from '../components/auth';
import { CommentCard, PublicationCard } from '../components/cards';
import { ErrorMessage } from '../components/error/ErrorMessage';
import { Loading } from '../components/loading/Loading';
import { useIrysUploader } from '../hooks/useIrysUploader';
import { useIrysUploadHandler } from '../hooks/useIrysUploader';
import { never } from '../utils';

type CommentComposerProps = {
commentOn: AnyPublication;
};

function CommentComposer({ commentOn }: CommentComposerProps) {
const { uploadMetadata } = useIrysUploader();
const uploadMetadata = useIrysUploadHandler();
const { execute, loading, error } = useCreateComment();
const { data: comments, prev: refresh } = usePublications({
where: { commentOn: { id: commentOn.id } },
Expand Down
4 changes: 2 additions & 2 deletions examples/web/src/publications/UseCreatePost.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import { useCreatePost } from '@lens-protocol/react-web';
import { toast } from 'react-hot-toast';

import { RequireProfileSession } from '../components/auth';
import { useIrysUploader } from '../hooks/useIrysUploader';
import { useIrysUploadHandler } from '../hooks/useIrysUploader';

function PostComposer() {
const { uploadMetadata } = useIrysUploader();
const uploadMetadata = useIrysUploadHandler();
const { execute, loading, error } = useCreatePost();

const submit = async (event: React.FormEvent<HTMLFormElement>) => {
Expand Down
4 changes: 2 additions & 2 deletions examples/web/src/publications/UseCreateQuote.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import { toast } from 'react-hot-toast';

import { RequireProfileSession } from '../components/auth';
import { PublicationCard } from '../components/cards';
import { useIrysUploader } from '../hooks/useIrysUploader';
import { useIrysUploadHandler } from '../hooks/useIrysUploader';

const target = publicationId('0x56-0x02');

function QuoteComposer() {
const { uploadMetadata } = useIrysUploader();
const uploadMetadata = useIrysUploadHandler();
const { execute: load, data } = useLazyPublication();
const { execute, loading, error } = useCreateQuote();

Expand Down
4 changes: 2 additions & 2 deletions examples/web/src/publications/UseOpenAction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { RequireProfileSession, RequireWalletSession } from '../components/auth'
import { PublicationCard } from '../components/cards';
import { ErrorMessage } from '../components/error/ErrorMessage';
import { Loading } from '../components/loading/Loading';
import { useIrysUploader } from '../hooks/useIrysUploader';
import { useIrysUploadHandler } from '../hooks/useIrysUploader';
import { useLogs } from '../hooks/useLogs';
import { invariant } from '../utils';

Expand Down Expand Up @@ -94,7 +94,7 @@ function TestScenario({ id }: { id: PublicationId }) {
}

export function UseOpenAction() {
const { uploadMetadata } = useIrysUploader();
const uploadMetadata = useIrysUploadHandler();
const { logs, clear, log } = useLogs();
const [id, setId] = useState<PublicationId | undefined>();

Expand Down
98 changes: 98 additions & 0 deletions examples/web/src/publications/UseOptimisticCreatePost.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { MediaImageMimeType, image } from '@lens-protocol/metadata';
import { fileToUri, useOptimisticCreatePost } from '@lens-protocol/react-web';
import { toast } from 'react-hot-toast';

import { RequireProfileSession } from '../components/auth';
import { PublicationCard } from '../components/cards';
import { useIrysUploader } from '../hooks/useIrysUploader';

function PostComposer() {
const uploader = useIrysUploader();
const { data, execute, loading, error } = useOptimisticCreatePost(uploader);

const submit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
const form = event.currentTarget;
const formData = new FormData(form);

const file = formData.get('image') as File;

// create post metadata
const metadata = image({
image: {
item: fileToUri(file),
type: MediaImageMimeType.JPEG,
altTag: formData.get('description') as string,
},
});

// publish post
const result = await execute({
metadata,
});

// check for failure scenarios
if (result.isFailure()) {
toast.error(result.error.message);
return;
}

toast.success(`Post broadcasted, waiting for completion...`);

// wait for full completion
const completion = await result.value.waitForCompletion();

// check for late failures
if (completion.isFailure()) {
toast.error(completion.error.message);
return;
}

// post was created
const post = completion.value;
toast.success(`Post ID: ${post.id}`);
};

return (
<form onSubmit={submit}>
<fieldset>
<label>
Attach an image
<input type="file" name="image" disabled={loading} accept="image/jpeg" />
</label>

<textarea
name="description"
minLength={1}
required
rows={3}
placeholder="Give it a description."
style={{ resize: 'none' }}
disabled={loading}
></textarea>

<button type="submit" disabled={loading}>
Post
</button>

{!loading && error && <pre>{error.message}</pre>}
</fieldset>

{data && <PublicationCard publication={data} />}
</form>
);
}

export function UseOptimisticCreatePost() {
return (
<div>
<h1>
<code>useOptimisticCreatePost</code>
</h1>

<RequireProfileSession message="Log in to create a post.">
<PostComposer />
</RequireProfileSession>
</div>
);
}
1 change: 1 addition & 0 deletions examples/web/src/publications/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export * from './UseHidePublication';
export * from './UseLazyPublications';
export * from './UseNotInterestedToggle';
export * from './UseOpenAction';
export * from './UseOptimisticCreatePost';
export * from './UsePublication';
export * from './UsePublications';
export * from './UseReactionToggle';
Expand Down
1 change: 1 addition & 0 deletions packages/api-bindings/src/lens/graphql/fragments.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -880,6 +880,7 @@ fragment PublicationMetadataMediaImage on PublicationMetadataMediaImage {
...EncryptableImageSet
}
license
altTag
attributes {
type
key
Expand Down
Loading

0 comments on commit 177879d

Please sign in to comment.