Skip to content

Commit

Permalink
Merge pull request #50 from xmtp/rygine/caching
Browse files Browse the repository at this point in the history
Add local first architecture to React SDK
  • Loading branch information
rygine authored Aug 17, 2023
2 parents 6a99730 + 6040078 commit cb7d916
Show file tree
Hide file tree
Showing 109 changed files with 8,172 additions and 1,570 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,14 @@ jobs:
run: |
corepack enable
corepack prepare yarn@3.4.1 --activate
- name: Start docker container
run: ./dev/up
- name: Install dependencies
run: yarn
- name: Run tests
run: yarn test
- name: Stop docker container
run: ./dev/down

typecheck:
runs-on: ubuntu-latest
Expand Down
25 changes: 25 additions & 0 deletions dev/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
services:
waku-node:
image: xmtp/node-go:latest
platform: linux/amd64
environment:
- GOWAKU-NODEKEY=8a30dcb604b0b53627a5adc054dbf434b446628d4bd1eccc681d223f0550ce67
command:
- --ws
- --store
- --message-db-connection-string=postgres://postgres:xmtp@db:5432/postgres?sslmode=disable
- --message-db-reader-connection-string=postgres://postgres:xmtp@db:5432/postgres?sslmode=disable
- --lightpush
- --filter
- --ws-port=9001
- --wait-for-db=30s
- --api.authn.enable
ports:
- 9001:9001
- 5555:5555
depends_on:
- db
db:
image: postgres:13
environment:
POSTGRES_PASSWORD: xmtp
4 changes: 4 additions & 0 deletions dev/down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/bin/bash
set -e

docker-compose -p xmtp-js -f dev/docker-compose.yml down
9 changes: 9 additions & 0 deletions dev/up
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/bin/bash
set -e

if ! which docker &>/dev/null; then
echo "Docker required to run dev/up. Install it and run this again."
exit 1
fi

docker-compose -p xmtp-js -f dev/docker-compose.yml up -d
6 changes: 4 additions & 2 deletions examples/react-quickstart/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@
"format:base": "prettier --ignore-path ../../.gitignore",
"format:check": "yarn format:base -c .",
"format": "yarn format:base -w .",
"quickstart": "yarn dev",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@heroicons/react": "^2.0.18",
"@rainbow-me/rainbowkit": "^0.12.16",
"@xmtp/content-type-remote-attachment": "^1.0.7",
"@xmtp/react-components": "workspace:*",
"@xmtp/react-sdk": "workspace:*",
"ethers": "5.7.2",
Expand All @@ -27,14 +29,14 @@
"devDependencies": {
"@types/react": "^18.2.14",
"@types/react-dom": "^18.2.6",
"@vitejs/plugin-react": "^4.0.2",
"@vitejs/plugin-react": "^4.0.4",
"@xmtp/tsconfig": "workspace:*",
"autoprefixer": "^10.4.14",
"eslint": "^8.44.0",
"eslint-config-xmtp-web": "workspace:*",
"postcss": "^8.4.25",
"postcss-preset-env": "^8.5.1",
"typescript": "^5.1.6",
"vite": "^4.4.1"
"vite": "^4.4.9"
}
}
2 changes: 1 addition & 1 deletion examples/react-quickstart/src/components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const App = () => {

// disconnect XMTP client when the wallet changes
useEffect(() => {
disconnect();
void disconnect();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [signer]);

Expand Down
26 changes: 26 additions & 0 deletions examples/react-quickstart/src/components/ConversationCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { useLastMessage, type CachedConversation } from "@xmtp/react-sdk";
import { ConversationPreview } from "@xmtp/react-components";

type ConversationCardProps = {
conversation: CachedConversation;
isSelected: boolean;
onConversationClick?: (conversation: CachedConversation) => void;
};

export const ConversationCard: React.FC<ConversationCardProps> = ({
conversation,
onConversationClick,
isSelected,
}) => {
const lastMessage = useLastMessage(conversation.topic);

return (
<ConversationPreview
key={conversation.topic}
conversation={conversation}
isSelected={isSelected}
onClick={onConversationClick}
lastMessage={lastMessage}
/>
);
};
39 changes: 17 additions & 22 deletions examples/react-quickstart/src/components/Conversations.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { useConversations, useStreamConversations } from "@xmtp/react-sdk";
import type { Conversation } from "@xmtp/react-sdk";
import type { CachedConversation } from "@xmtp/react-sdk";
import { ChatBubbleLeftIcon } from "@heroicons/react/24/outline";
import { useCallback, useState } from "react";
import { ConversationPreviewList } from "@xmtp/react-components";
import { ConversationList } from "@xmtp/react-components";
import { Notification } from "./Notification";
import { ConversationCard } from "./ConversationCard";

type ConversationsProps = {
selectedConversation?: Conversation;
onConversationClick?: (conversation: Conversation) => void;
selectedConversation?: CachedConversation;
onConversationClick?: (conversation: CachedConversation) => void;
};

const NoConversations: React.FC = () => (
Expand All @@ -21,28 +21,23 @@ export const Conversations: React.FC<ConversationsProps> = ({
onConversationClick,
selectedConversation,
}) => {
const [streamedConversations, setStreamedConversations] = useState<
Conversation[]
>([]);
const { conversations, isLoading } = useConversations();
const onConversation = useCallback(
(conversation: Conversation) => {
// prevent duplicates
if (!conversations.some((convo) => convo.topic === conversation.topic)) {
setStreamedConversations((prev) => [...prev, conversation]);
}
},
[conversations],
);
useStreamConversations(onConversation);
useStreamConversations();

const previews = conversations.map((conversation) => (
<ConversationCard
key={conversation.topic}
conversation={conversation}
isSelected={conversation.topic === selectedConversation?.topic}
onConversationClick={onConversationClick}
/>
));

return (
<ConversationPreviewList
<ConversationList
isLoading={isLoading}
conversations={[...conversations, ...streamedConversations]}
onConversationClick={onConversationClick}
conversations={previews}
renderEmpty={<NoConversations />}
selectedConversation={selectedConversation}
/>
);
};
22 changes: 11 additions & 11 deletions examples/react-quickstart/src/components/Inbox.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import "./Inbox.css";
import { useCallback, useState } from "react";
import type { Conversation } from "@xmtp/react-sdk";
import { type CachedConversation } from "@xmtp/react-sdk";
import {
ArrowRightOnRectangleIcon,
PlusCircleIcon,
Expand All @@ -13,13 +13,13 @@ import { NoSelectedConversationNotification } from "./NoSelectedConversationNoti

export const Inbox: React.FC = () => {
const { disconnect } = useWallet();
const [conversation, setConversation] = useState<Conversation | undefined>(
undefined,
);
const [selectedConversation, setSelectedConversation] = useState<
CachedConversation | undefined
>(undefined);
const [isNewMessage, setIsNewMessage] = useState(false);

const handleConversationClick = useCallback((convo: Conversation) => {
setConversation(convo);
const handleConversationClick = useCallback((convo: CachedConversation) => {
setSelectedConversation(convo);
setIsNewMessage(false);
}, []);

Expand All @@ -28,8 +28,8 @@ export const Inbox: React.FC = () => {
}, []);

const handleStartNewConversationSuccess = useCallback(
(convo?: Conversation) => {
setConversation(convo);
(convo?: CachedConversation) => {
setSelectedConversation(convo);
setIsNewMessage(false);
},
[],
Expand Down Expand Up @@ -64,14 +64,14 @@ export const Inbox: React.FC = () => {
<div className="InboxConversations__list">
<Conversations
onConversationClick={handleConversationClick}
selectedConversation={conversation}
selectedConversation={selectedConversation}
/>
</div>
<div className="InboxConversations__messages">
{isNewMessage ? (
<NewMessage onSuccess={handleStartNewConversationSuccess} />
) : conversation ? (
<Messages conversation={conversation} />
) : selectedConversation ? (
<Messages conversation={selectedConversation} />
) : (
<NoSelectedConversationNotification
onStartNewConversation={handleStartNewConversation}
Expand Down
36 changes: 13 additions & 23 deletions examples/react-quickstart/src/components/Messages.tsx
Original file line number Diff line number Diff line change
@@ -1,56 +1,45 @@
import {
useClient,
useMessages,
useSendMessage,
useStreamMessages,
} from "@xmtp/react-sdk";
import type { Conversation, DecodedMessage } from "@xmtp/react-sdk";
import type { CachedConversation } from "@xmtp/react-sdk";
import { useCallback, useEffect, useRef, useState } from "react";
import "./Messages.css";
import {
AddressInput,
ConversationMessages,
Messages as MessagesList,
MessageInput,
} from "@xmtp/react-components";

type ConversationMessagesProps = {
conversation: Conversation;
conversation: CachedConversation;
};

export const Messages: React.FC<ConversationMessagesProps> = ({
conversation,
}) => {
const [isSending, setIsSending] = useState(false);
const [streamedMessages, setStreamedMessages] = useState<DecodedMessage[]>(
[],
);
const messageInputRef = useRef<HTMLTextAreaElement>(null);
const { messages, isLoading } = useMessages(conversation);
const onMessage = useCallback(
(message: DecodedMessage) => {
// prevent duplicates
if (!streamedMessages.some((msg) => msg.id === message.id)) {
setStreamedMessages((prev) => [...prev, message]);
}
},
[streamedMessages],
);
useStreamMessages(conversation, onMessage);
const { sendMessage } = useSendMessage(conversation);
const { client } = useClient();
useStreamMessages(conversation);
const { sendMessage } = useSendMessage();

const handleSendMessage = useCallback(
async (message: string) => {
setIsSending(true);
await sendMessage(message);
await sendMessage(conversation, message);
setIsSending(false);
// ensure focus of input by waiting for a browser tick
setTimeout(() => messageInputRef.current?.focus(), 0);
},
[sendMessage],
[conversation, sendMessage],
);

useEffect(() => {
messageInputRef.current?.focus();
setStreamedMessages([]);
}, [conversation]);

return (
Expand All @@ -59,10 +48,11 @@ export const Messages: React.FC<ConversationMessagesProps> = ({
value={conversation.peerAddress}
avatarUrlProps={{ address: conversation.peerAddress }}
/>
<ConversationMessages
<MessagesList
conversation={conversation}
isLoading={isLoading}
messages={[...messages, ...streamedMessages]}
clientAddress={conversation?.clientAddress ?? ""}
messages={messages.filter((message) => message.content !== undefined)}
clientAddress={client?.address}
/>
<div className="MessageInputWrapper">
<MessageInput
Expand Down
18 changes: 8 additions & 10 deletions examples/react-quickstart/src/components/NewMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,13 @@ import {
useCanMessage,
useStartConversation,
} from "@xmtp/react-sdk";
import {
AddressInput,
Messages as ConversationMessages,
MessageInput,
} from "@xmtp/react-components";
import type { Conversation } from "@xmtp/react-sdk";
import { AddressInput, MessageInput } from "@xmtp/react-components";
import type { CachedConversation } from "@xmtp/react-sdk";
import { useCallback, useEffect, useRef, useState } from "react";
import "./NewMessage.css";

type NewMessageProps = {
onSuccess?: (conversation?: Conversation) => void;
onSuccess?: (conversation?: CachedConversation) => void;
};

export const NewMessage: React.FC<NewMessageProps> = ({ onSuccess }) => {
Expand All @@ -33,9 +29,11 @@ export const NewMessage: React.FC<NewMessageProps> = ({ onSuccess }) => {
async (message: string) => {
if (peerAddress && isOnNetwork) {
setIsLoading(true);
const conversation = await startConversation(peerAddress, message);
const result = await startConversation(peerAddress, message);
setIsLoading(false);
onSuccess?.(conversation);
if (result) {
onSuccess?.(result.cachedConversation);
}
}
},
[isOnNetwork, onSuccess, peerAddress, startConversation],
Expand Down Expand Up @@ -84,7 +82,7 @@ export const NewMessage: React.FC<NewMessageProps> = ({ onSuccess }) => {
address: isOnNetwork ? peerAddress : "",
}}
/>
<ConversationMessages />
<div />
<div className="NewMessageInputWrapper">
<MessageInput
isDisabled={isLoading || !isValidAddress(peerAddress) || isError}
Expand Down
4 changes: 3 additions & 1 deletion examples/react-quickstart/src/components/XMTPConnect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ const XMTPConnectButton: React.FC<XMTPConnectButtonProps> = ({ label }) => {
const { initialize } = useClient();

const handleConnect = useCallback(() => {
void initialize({ signer });
void initialize({
signer,
});
}, [initialize, signer]);

return (
Expand Down
5 changes: 5 additions & 0 deletions examples/react-quickstart/src/globals.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
interface ImportMeta {
env: {
VITE_PROJECT_ID: string;
};
}
Loading

0 comments on commit cb7d916

Please sign in to comment.