47W00Ff%!tmu<(2J2v6`l5u8(-S8fh{Fn=jt
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
literal 0
HcmV?d00001
diff --git a/docs/build/local-first.md b/docs/build/local-first.md
new file mode 100644
index 000000000..fcf19db02
--- /dev/null
+++ b/docs/build/local-first.md
@@ -0,0 +1,25 @@
+---
+sidebar_label: Local-first cache
+sidebar_position: 1.5
+description: "Get started building apps with XMTP."
+---
+
+# Use local-first architecture
+
+If you're building a production-grade app, be sure to use a local-first architecture to help you build a performant app. Using this local-first architecture, the client prioritizes using the local cache on the device where it’s running.
+
+For example, use the XMTP SDK to initially retrieve existing message data from the XMTP network and place it in the local cache. Asynchronously load new and updated message data as needed. Build your app to get message data from the local cache.
+
+Here’s an overview of how your app frontend, local cache, client SDK, and the XMTP network work together in this local-first approach:
+
+import localfirst from '/docs/build/img/local-first-arch.jpeg';
+
+
+
+- When building a web app with the [React SDK](https://github.com/xmtp/xmtp-web/tree/main/packages/react-sdk), the local-first architecture is automatically provided by the SDK.
+
+- When building a web app with the [xmtp-js SDK](https://github.com/xmtp/xmtp-js), you can use the browser `localStorage` as the local cache to store encrypted data, decrypting data each time before display.
+
+- When building native iOS and Android mobile apps, you can use the device's encrypted container as the local cache to store decrypted data.
+
+For more performance best practices, see [Optimize performance of your app](/docs/launch/performance)
diff --git a/docs/build/messages.md b/docs/build/messages.md
index 44a1fc851..3bc9ce40e 100644
--- a/docs/build/messages.md
+++ b/docs/build/messages.md
@@ -59,10 +59,93 @@ conversation.send(text = "Hello world")
```tsx
import { useSendMessage } from "@xmtp/react-sdk";
+import type { Conversation } from "@xmtp/react-sdk";
+import { useCallback, useState } from "react";
-const sendMessage = useSendMessage(conversation);
+export const SendMessage: React.FC<{ conversation: CachedConversation }> = ({
+ conversation,
+}) => {
+ const [peerAddress, setPeerAddress] = useState("");
+ const [message, setMessage] = useState("");
+ const [isSending, setIsSending] = useState(false);
+ const sendMessage = useSendMessage();
+
+ const handleAddressChange = useCallback(
+ (e: ChangeEvent) => {
+ setPeerAddress(e.target.value);
+ },
+ [],
+ );
+
+ const handleMessageChange = useCallback(
+ (e: ChangeEvent) => {
+ setMessage(e.target.value);
+ },
+ [],
+ );
+
+ const handleSendMessage = useCallback(
+ async (e: React.FormEvent) => {
+ e.preventDefault();
+ if (peerAddress && isValidAddress(peerAddress) && message) {
+ setIsLoading(true);
+ await sendMessage(conversation, message);
+ setIsLoading(false);
+ }
+ },
+ [message, peerAddress, sendMessage],
+ );
+
+ return (
+
+ );
+};
+```
+
+### Optimistic sending with React
+
+When a user sends a message with XMTP, they might experience a slight delay between sending the message and seeing their sent message display in their app UI.
+
+This is because when a user sends a message, they typically have to wait for the XMTP network to finish processing the message before the app can display it in the UI.
+
+The local-first architecture of the React SDK automatically includes optimistic sending to immediately display the sent message in the sender’s UI while processing the message in the background. This provides the user with immediate feedback and enables them to continue messaging without having to wait for their previous message to finish processing.
+
+Messages that are in the sending state will have a `true` value for their `isSending` property.
+
+### Handle messages that fail to send with React
+
+If a message fails to complete the sending process, you must provide an error state that alerts the user and enables them to either resend the message or cancel sending the message.
+
+While in this unsent state, the message remains in its original location in the user’s conversation flow, with any newer sent and received messages displaying after it.
-await sendMessage(message);
+If the user chooses to resend the message, the message moves into the most recently sent message position in the conversation. Once it successfully sends, it remains in that position.
+
+If the user chooses to cancel sending the message, the message is removed from the conversation flow.
+
+Messages that fail to send will have the `hasSendError` property set to `true`.
+
+#### Resend a failed message
+
+Use the `resendMessage` function from the `useResendMessage` hook to resend a failed message.
+
+```tsx
+const { resendMessage } = useResendMessage();
+
+// resend the message
+resendMessage(failedMessage);
```
@@ -129,15 +212,25 @@ for (conversation in client.conversations.list()) {
```tsx
import { useMessages } from "@xmtp/react-sdk";
+import type { CachedConversation } from "@xmtp/react-sdk";
-const [conversation, setConversation] = useState(null);
-const { messages } = useMessages(conversation);
+export const Messages: React.FC<{
+ conversation: CachedConversation;
+}> = ({ conversation }) => {
+ const { error, messages, isLoading } = useMessages(conversation);
-useEffect(() => {
- if (messages) {
- console.log("Loaded message history:", messages.length);
+ if (error) {
+ return "An error occurred while loading messages";
}
-}, [messages]);
+
+ if (isLoading) {
+ return "Loading messages...";
+ }
+
+ return (
+ ...
+ );
+};
```
diff --git a/docs/build/streams.md b/docs/build/streams.md
index 677237d27..20f320796 100644
--- a/docs/build/streams.md
+++ b/docs/build/streams.md
@@ -73,25 +73,33 @@ client.conversations.stream().collect {
```tsx
-//The `useStreamConversations` hook listens for new conversations in real-time and calls the passed callback when a new conversation is created. It also exposes an error state.
-
import { useCallback, useState } from "react";
import { useStreamConversations } from "@xmtp/react-sdk";
+import type { Conversation } from "@xmtp/react-sdk";
+
+export const NewConversations: React.FC = () => {
+ // track streamed conversations
+ const [streamedConversations, setStreamedConversations] = useState<
+ Conversation[]
+ >([]);
+
+ // callback to handle incoming conversations
+ const onConversation = useCallback(
+ (conversation: Conversation) => {
+ setStreamedConversations((prev) => [...prev, conversation]);
+ },
+ [],
+ );
+ const { error } = useStreamConversations(onConversation);
-// track streamed conversations
-const [streamedConversations, setStreamedConversations] = useState<
- Conversation[]
->([]);
-
-// callback to handle incoming conversations
-const onConversation = useCallback((conversation: Conversation) => {
- setStreamedConversations((prev) => [...prev, conversation]);
-}, []);
-const { error } = useStreamConversations(onConversation);
+ if (error) {
+ return "An error occurred while streaming conversations";
+ }
-if (error) {
- return "An error occurred while streaming conversations";
-}
+ return (
+ ...
+ );
+};
```
@@ -181,15 +189,40 @@ conversation.streamMessages().collect {
```tsx
+// The useStreamMessages hook streams new conversation messages on mount
+// and exposes an error state.
import { useStreamMessages } from "@xmtp/react-sdk";
+import type { CachedConversation, DecodedMessage } from "@xmtp/react-sdk";
+import { useCallback, useEffect, useState } from "react";
+
+export const StreamMessages: React.FC<{
+ conversation: CachedConversation;
+}> = ({
+ conversation,
+}) => {
+ // track streamed messages
+ const [streamedMessages, setStreamedMessages] = useState(
+ [],
+ );
-const onMessage = useCallback((message) => {
- setHistory((prevMessages) => {
- const msgsnew = [...prevMessages, message];
- return msgsnew;
- });
-}, []);
-useStreamMessages(conversation, onMessage);
+ // callback to handle incoming messages
+ const onMessage = useCallback(
+ (message: DecodedMessage) => {
+ setStreamedMessages((prev) => [...prev, message]);
+ },
+ [streamedMessages],
+ );
+
+ useStreamMessages(conversation, onMessage);
+
+ useEffect(() => {
+ setStreamedMessages([]);
+ }, [conversation]);
+
+ return (
+ ...
+ );
+};
```
@@ -236,8 +269,13 @@ for await (const message of await xmtp.conversations.streamAllMessages()) {
```tsx
+// The useStreamAllMessages hook streams new messages from all conversations
+// on mount and exposes an error state.
import { useStreamAllMessages } from "@xmtp/react-sdk";
+import type { DecodedMessage } from "@xmtp/react-sdk";
+import { useCallback, useState } from "react";
+export const StreamAllMessages: React.FC = () => {
// track streamed messages
const [streamedMessages, setStreamedMessages] = useState(
[],
diff --git a/docs/content-types/attachment.mdx b/docs/content-types/attachment.mdx
index 8d0b8f3f7..3c3a31cbf 100644
--- a/docs/content-types/attachment.mdx
+++ b/docs/content-types/attachment.mdx
@@ -18,6 +18,12 @@ You are welcome to provide feedback on this implementation by commenting on the
:::
+:::info Attachments with React
+
+To learn how to use the attachment content type with the React SDK, see [Handle content types with the React SDK](/docs/content-types/react-content-types).
+
+:::
+
### Install the package
diff --git a/docs/content-types/react-content-types.md b/docs/content-types/react-content-types.md
new file mode 100644
index 000000000..05c8e94e9
--- /dev/null
+++ b/docs/content-types/react-content-types.md
@@ -0,0 +1,213 @@
+---
+sidebar_label: For React
+sidebar_position: 1.5
+---
+
+# Handle content types with the React SDK
+
+As the ecosystem of apps built with XMTP grows exponentially, so will the number of available content types.
+
+All apps built with an XMTP client SDK automatically support the plain text standard content type. However, there are other content types that you can choose to support.
+
+These include standards-track content types and custom content types.
+
+To learn more about content types, see [Content types with XMTP](https://xmtp.org/docs/concepts/content-types).
+
+## Standards-track content types
+
+Standards-track content types are proposals being actively reviewed for adoption as standard content types through the XMTP Improvement Proposal (XIP) process. Once a content type has been adopted as a standard, it is included in the XMTP client SDKs. Apps can use an SDK to automatically handle the new standard content types.
+
+Here are some examples of standards-track content types that you can review, test, and adopt in your app today:
+
+### Remote attachments
+
+This content type supports sending file attachments that are stored off-network. Use it to enable your app to send and receive message attachments.
+
+- [XIP proposal discussion](https://github.com/xmtp/XIPs/blob/main/XIPs/xip-17-remote-attachment-content-type-proposal.md)
+
+- [Source code](https://github.com/xmtp/xmtp-js-content-types/tree/main/packages/content-type-remote-attachment)
+
+### Replies
+
+This content type supports direct replies to messages.
+
+- [XIP idea discussion](https://github.com/orgs/xmtp/discussions/35)
+
+- [Source code](https://github.com/xmtp/xmtp-js-content-types/tree/main/packages/content-type-reply)
+
+### Reactions
+
+This content type supports reactions to messages.
+
+- [XIP idea discussion](https://github.com/orgs/xmtp/discussions/36)
+
+- [Source code](https://github.com/xmtp/xmtp-js-content-types/tree/main/packages/content-type-reaction)
+
+### Read receipts
+
+This content type supports read receipts to messages
+
+- [XIP idea discussion](https://github.com/orgs/xmtp/discussions/43)
+
+- [Source code](https://github.com/xmtp/xmtp-js-content-types/tree/main/packages/content-type-read-receipt)
+
+## Integrate standards-track content types with the React SDK
+
+The React SDK supports all current standards-track content types, but only text messages are enabled out of the box. Adding support for other standards-track content types requires a bit of configuration.
+
+```tsx
+import {
+ XMTPProvider,
+ attachmentContentTypeConfig,
+ reactionContentTypeConfig,
+ readReceiptContentTypeConfig,
+ replyContentTypeConfig,
+} from "@xmtp/react-sdk";
+
+const contentTypeConfigs = [
+ attachmentContentTypeConfig,
+ reactionContentTypeConfig,
+ readReceiptContentTypeConfig,
+ replyContentTypeConfig,
+];
+
+createRoot(document.getElementById("root") as HTMLElement).render(
+
+
+
+
+ ,
+);
+```
+
+## Custom content types
+
+On the other hand, custom content types are those that an app chooses to use in its implementation, but which are not standards and are not being actively reviewed as standards-track content types.
+
+When your app encounters a custom content type, you can:
+
+- Choose to adopt the custom content type if you think it is a good fit for your app.
+- Update your app to gracefully handle the unsupported content type.
+- Ignore it
+
+### Integrate custom content types with the React SDK
+
+> **Note**
+> This part of the React SDK is still under active development and may change in the future.
+
+To support a custom content type, you must create a content type configuration to integrate with the [local-first architecture](https://github.com/xmtp/xmtp-web/blob/main/packages/react-sdk/README.md#local-first-architecture). There are five possible options in a content type configuration, and two are required. They are outlined below.
+
+**Namespace (required)**
+
+This must be specified and unique to all other content type namespaces.
+
+**Message processors (required)**
+
+Processing a message as it arrives from the XMTP network is a key aspect of the local-first architecture. It determines if and how a message is cached locally.
+
+**Codecs (required)**
+
+All custom content types require at least one codec.
+
+**Schema (optional)**
+
+When working with some content types, you may want to cache data in a separate table. This option allows you to specify a new Dexie table schema.
+
+**Validators (optional)**
+
+Content validators help to ensure that custom content type messages are in the correct format and can be processed properly.
+
+**Example configuration**
+
+```tsx
+import type { ContentCodec } from "@xmtp/xmtp-js";
+
+const MyContentType = new ContentTypeId({
+ authorityId: "mydomain.com",
+ typeId: "myContentType",
+ versionMajor: 1,
+ versionMinor: 0,
+});
+
+class MyContentTypeCodec implements ContentCodec { ... };
+
+export const myCustomContentTypeConfiguration = {
+ namespace: "MyContentType",
+ codecs: [new MyContentTypeCodec()],
+ processors: {
+ [MyContentType.toString()]: [processMyContentType],
+ },
+ validators: {
+ [MyContentType.toString()]: validateMyContentType,
+ },
+};
+```
+
+### Send custom content type messages
+
+The `useSendMessage` hook supports custom content types. Pass in the content type as the third parameter as shown in the example below.
+
+```tsx
+import { useCallback, useState } from "react";
+import { useSendMessage } from "@xmtp/react-sdk";
+import type { Conversation } from "@xmtp/react-sdk";
+import type { Reaction } from "@xmtp/content-type-reaction";
+import { ContentTypeReaction } from "@xmtp/content-type-reaction";
+
+export const SendCustomContentTypeMessage: React.FC<{
+ conversation: CachedConversation;
+}> = ({ conversation }) => {
+ const [isSending, setIsSending] = useState(false);
+ const sendMessage = useSendMessage();
+
+ const handleSendMessage = useCallback(
+ async (e: React.FormEvent) => {
+ e.preventDefault();
+ setIsLoading(true);
+ // send custom content type
+ await sendMessage(
+ conversation,
+ {
+ action: "added",
+ content: "👍",
+ reference: "",
+ schema: "unicode",
+ } as Reaction,
+ ContentTypeReaction,
+ );
+ setIsLoading(false);
+ },
+ [message, peerAddress, sendMessage],
+ );
+
+ return (
+
+ );
+};
+```
+
+## Handle unsupported content types
+
+Messages that contain unsupported content types are stored in the local cache, but are not processed. The `content` property of an unsupported message will be `undefined`. Each time a client is initialized, any messages that were previously unprocessed because their content type was unsupported at the time, will attempt to be reprocessed.
+
+If you wish to display an unsupported content type, there’s a `contentFallback` property that may include a useful string. However, it is recommended that you manually process unsupported content types.
+
+**Example**
+
+```ts
+import { ContentTypeId } from "@xmtp/xmtp-js";
+import { ContentTypeAttachment } from "@xmtp/content-type-remote-attachment";
+
+const MessageContent = ({ message }) => {
+ if (
+ message.content === undefined &&
+ ContentTypeId.fromString(message.contentType).sameAs(ContentTypeAttachment)
+ ) {
+ return "This message contains an attachment, which is not supported by this client.";
+ }
+};
+```
diff --git a/docs/content-types/reaction.mdx b/docs/content-types/reaction.mdx
index 1a5b20d81..d9e6d8997 100644
--- a/docs/content-types/reaction.mdx
+++ b/docs/content-types/reaction.mdx
@@ -18,6 +18,12 @@ You are welcome to provide feedback on this implementation by commenting on the
:::
+:::info Reactions with React
+
+To learn how to use the reaction content type with the React SDK, see [Handle content types with the React SDK](/docs/content-types/react-content-types).
+
+:::
+
### Install the package
diff --git a/docs/content-types/read-receipt.mdx b/docs/content-types/read-receipt.mdx
index 32735228e..1bd786881 100644
--- a/docs/content-types/read-receipt.mdx
+++ b/docs/content-types/read-receipt.mdx
@@ -18,6 +18,12 @@ You are welcome to provide feedback on this implementation by commenting on the
:::
+:::info Read receipts with React
+
+To learn how to use the read receipt content type with the React SDK, see [Handle content types with the React SDK](/docs/content-types/react-content-types).
+
+:::
+
### Install the package
diff --git a/docs/content-types/remote-attachment.mdx b/docs/content-types/remote-attachment.mdx
index 96d675f9e..1599bc731 100644
--- a/docs/content-types/remote-attachment.mdx
+++ b/docs/content-types/remote-attachment.mdx
@@ -18,6 +18,12 @@ You are welcome to provide feedback on this implementation by commenting on the
:::
+:::info Remote attachments with React
+
+To learn how to use the remote attachment content type with the React SDK, see [Handle content types with the React SDK](/docs/content-types/react-content-types).
+
+:::
+
### Install the package
diff --git a/docs/content-types/reply.mdx b/docs/content-types/reply.mdx
index 017e40b49..3a4a8abf1 100644
--- a/docs/content-types/reply.mdx
+++ b/docs/content-types/reply.mdx
@@ -18,6 +18,12 @@ You are welcome to provide feedback on this implementation by commenting on the
:::
+:::info Replies with React
+
+To learn how to use the reply content type with the React SDK, see [Handle content types with the React SDK](/docs/content-types/react-content-types).
+
+:::
+
### Install the package
Use the following commands to install the package:
diff --git a/docs/launch/performance.md b/docs/launch/performance.md
index cd3f227ac..fe07ed1a4 100644
--- a/docs/launch/performance.md
+++ b/docs/launch/performance.md
@@ -14,19 +14,7 @@ Follow these guidelines to optimize your app’s performance. To learn about tes
## Use a local cache
-If you're building a production-grade app, be sure to use an architecture that includes a local cache backed by an XMTP SDK.
-
-
-
-Use the XMTP SDK to initially retrieve existing message data from the XMTP network and place it in the local cache. Asynchronously load new and updated message data as needed.
-
-Build your app to get message data from the local cache.
-
-For example, use the XMTP SDK to get conversation lists from the XMTP network. Store the conversation lists in the local cache. Build your app to get conversation lists from the local cache.
-
-When building native iOS and Android mobile apps, you can use the device's encrypted container as the local cache to store decrypted data.
-
-When building web apps, you can use the browser `localStorage` as the local cache to store **encrypted** data, decrypting data each time before display.
+To learn more, see [Use local-first architecture](/docs/build/local-first).
## Cache the conversation list
@@ -39,7 +27,6 @@ To learn more, see [Cache the conversation list](/docs/build/conversations#cache
Serialize securely stored `DecodedMessage` histories, avoiding the need to download and decrypt the same message on every session.
- Use the JavaScript client SDK (`xmtp-js`) to [serialize securely stored decoded message histories](https://github.com/xmtp/xmtp-js/releases/tag/v8.0.0)
-- With the React client SDK (`react-sdk`), use message caching with the `useCachedMessages` hook
## Page through messages
@@ -91,6 +78,23 @@ conversation.send(
)
```
+
+
+
+Message content can be optionally compressed using the `compression` option. The value of the option is the name of the compression algorithm to use. Currently supported are `gzip` and `deflate`. Compression is applied to the bytes produced by the content codec.
+
+Content will be decompressed transparently on the receiving end. Note that `Client` enforces maximum content size. The default limit can be overridden through the `ClientOptions`. Consequently, a message that would expand beyond that limit on the receiving end will fail to decode.
+
+```tsx
+import { Compression, ContentTypeText } from '@xmtp/react-sdk'
+
+const sendMessage = useSendMessage();
+
+await sendMessage(conversation, '#'.repeat(1000), ContentTypeText, {
+ compression: Compression.COMPRESSION_DEFLATE,
+})
+```
+
From bfba0efbae76fedea583902a630ab8732c8bed22 Mon Sep 17 00:00:00 2001
From: Jennifer Hasegawa <5481259+jhaaaa@users.noreply.github.com>
Date: Mon, 21 Aug 2023 16:09:31 -0700
Subject: [PATCH 2/8] updates to local db doc
---
docs/launch/local-db.md | 68 ++++++++++++++++++++++++++---------------
1 file changed, 43 insertions(+), 25 deletions(-)
diff --git a/docs/launch/local-db.md b/docs/launch/local-db.md
index a89d56998..b806263dd 100644
--- a/docs/launch/local-db.md
+++ b/docs/launch/local-db.md
@@ -1,37 +1,49 @@
---
sidebar_label: Local DB
sidebar_position: 1
-description: Managing Local Data with Dexie in a React Application
+description: Manage local data with Dexie in a web app built with the xmtp-js SDK
---
-# Managing Local Data with Dexie in a React Application
+# Manage local data with Dexie in a web app built with xmtp-js
-The performance of a web application can be significantly improved by leveraging local data storage. Particularly in the context of loading messages, using a local database can result in a 10x performance increase compared to solely relying on a network-based data source.
+
-This guide provides a walkthrough on managing local data storage in a React application using the Dexie.js library. Dexie.js is a minimalistic wrapper for IndexedDB, which is a low-level API for client-side storage of significant amounts of structured data.
+
+
+
+
+The performance of a web app can be significantly improved by leveraging local data storage. Particularly in the context of loading messages, using a local database can result in a 10x performance increase compared to solely relying on a network-based data source.
+
+This guide provides a walkthrough on managing local data storage in a web application built with the [xmtp-js SDK](https://github.com/xmtp/xmtp-js) and using the Dexie.js library. Dexie.js is a minimalistic wrapper for IndexedDB, which is a low-level API for client-side storage of significant amounts of structured data.
import ReactPlayer from 'react-player'
-#### Experimental Playground 🎲
+#### Experimental playground 🎲
+
+
For hands-on experience, check out the React Playground.
-[GitHub repo](https://github.com/xmtp/xmtp-react-playground) | [Live Version](https://xmtp.github.io/xmtp-react-playground/#/new).
+[GitHub repo](https://github.com/xmtp/xmtp-react-playground) | [Live version](https://xmtp.github.io/xmtp-react-playground/#/new)
-## Step 1: Installing Libraries
+## Step 1: Install libraries
To start, install the necessary libraries:
+
+
```bash
npm install dexie dexie-react-hooks
```
-## Step 2: Defining the Database Schema
+## Step 2: Define the database schema
Create a **`DB.ts`** file and define your database schema. Here's an example of a potential database schema:
+
+
:::success
This file defines the local database schema for our app. Any time we show any
@@ -150,11 +162,11 @@ export default db;
In this schema, we define interfaces for different types of data we want to store: conversations, messages, message attachments, and message reactions. We then create a class for the database that extends Dexie, and within that class, we define the tables and their structure.
-## Step 3: Database Operations
+## Step 3: Perform database operations
After defining the schema, you can perform various database operations such as adding, updating, and retrieving data.
-### Adding Messages
+### Add messages
When a new message is sent, it's first saved to the local database before being sent to the network.
@@ -177,7 +189,7 @@ const message: Message = {
message.id = await db.messages.add(message);
```
-### Updating Messages
+### Update messages
After a message is sent to the network and you receive the decoded message back, update the original message in the database with the ID of the message on the network and set **`isSending`** to false.
@@ -190,7 +202,7 @@ await db.messages.update(message.id!, {
});
```
-### Checking for Existing Messages
+### Check for existing messages
Before saving a received message, check if it already exists in the database. If the message doesn't exist, save it; otherwise, skip the saving process.
@@ -202,7 +214,7 @@ const existing = await db.messages
.first();
```
-### Finding a Conversation
+### Find a conversation
When you need to find a specific conversation in the **`conversations`** table, search by the `topic` field.
@@ -214,7 +226,7 @@ return await db.conversations
.first();
```
-### Updating a Conversation
+### Update a conversation
When a new message is received, update the `updatedAt` timestamp of the related conversation.
@@ -228,7 +240,7 @@ if (conversation && conversation.updatedAt < updatedAt) {
}
```
-### Adding Conversations
+### Add conversations
When a new conversation is started, it's first saved to the local database.
@@ -248,7 +260,7 @@ const conversation: Conversation = {
conversation.id = await db.conversations.add(conversation);
```
-### Checking for Existing Conversations
+### Check for existing conversations
Before saving a new conversation, check if it already exists in the database. If the conversation doesn't exist, save it; otherwise, return the existing one.
@@ -260,11 +272,13 @@ const existing = await db.conversations
.first();
```
-## Step 4: Load Initial Data
+## Step 4: Load initial data
To load initial data when the application starts, use the **`useConversations`** function. This function fetches conversations from an XMTP client, saves these conversations to the local database (if they're not already stored), and returns an array of all conversations.
-### Use Effect Hook to Fetch and Save Conversations
+### Use useEffect hook to fetch and save conversations
+
+
Start by using React's **`useEffect`** hook to run an asynchronous operation when the component mounts. This operation should fetch the list of conversations from the client, and for each conversation, save it to the local database and fetch and save the latest message for the conversation:
@@ -288,7 +302,7 @@ useEffect(() => {
}, []);
```
-### Define Functions to Save Conversations and Messages
+### Define functions to save conversations and messages
Next, define two functions: **`saveConversation`** and **`saveMessage`**. These functions should take an XMTP conversation or message as an argument, check if it already exists in the local database, and if it doesn't, save it to the database:
@@ -329,7 +343,9 @@ async function saveMessage(
}
```
-### Use Effect Hook to Stream Conversations
+### Use useEffect hook to stream conversations
+
+
Use another **`useEffect`** hook to listen for new conversations in real-time. As new conversations come in, save them to the local database:
@@ -344,7 +360,7 @@ useEffect(() => {
}, []);
```
-### Fetch Conversations from Local DB
+### Fetch conversations from local DB
Finally, return the conversations from the local database:
@@ -356,11 +372,13 @@ return (
);
```
+
+
This hook, **`useLiveQuery`**, automatically re-renders the component whenever the data in the local database changes. It is used to fetch all conversations from the database, sort them in reverse order by their updated time, and return the result. If the query returns nothing, an empty array is returned by default.
By loading initial data, you ensure that your application loads instantly on each refresh without crashes and slow loading spinners.
-## Step 5: Preventing Race Conditions
+## Step 5: Prevent race conditions
Race conditions can occur when multiple operations that read from and write to the same database area are executed in close sequence, leading to inconsistent data. To prevent race conditions, use a mutex (short for "mutual exclusion object"), which ensures that only one operation can happen at a time.
@@ -383,7 +401,7 @@ const conversationMutex = new Mutex();
Now, we can use these mutexes to ensure that only one operation happens at a time. Here are some examples of how to use a mutex:
-### When Updating a Conversation
+### When updating a conversation
Wrap the database operation within the **`runExclusive`** method of the mutex instance. This guarantees that no other operation can happen until the current operation is finished:
@@ -397,7 +415,7 @@ if (conversation && conversation.updatedAt < updatedAt) {
}
```
-### When Saving a Conversation
+### When saving a conversation
The same applies when saving a new conversation. The operation is wrapped in the **`runExclusive`** method:
@@ -428,4 +446,4 @@ Using a mutex in this way helps prevent race conditions and maintain the integri
Managing local data storage in a React application can be complex. However, with Dexie.js and the right strategies for handling database operations, it can be much more manageable. Always remember to handle potential errors and race conditions to ensure the integrity of your data. Now that you've learned these steps, consider trying them out in your own projects. Happy coding!
-- For more information on using Dexie.js, check out the **[official documentation](https://dexie.org/docs/Tutorial/Getting-started)**.
+For more information on using Dexie.js, check out the **[official documentation](https://dexie.org/docs/Tutorial/Getting-started)**.
From b9ac1267589c3655fa8a39f552074fd649194b5d Mon Sep 17 00:00:00 2001
From: Jennifer Hasegawa <5481259+jhaaaa@users.noreply.github.com>
Date: Tue, 22 Aug 2023 06:20:16 -0700
Subject: [PATCH 3/8] inc ry's feedback <3
---
docs/build/conversations.md | 1 +
docs/build/local-first.md | 311 ++++++++++++++++++++++++-
docs/launch/local-db.md | 449 ------------------------------------
3 files changed, 311 insertions(+), 450 deletions(-)
delete mode 100644 docs/launch/local-db.md
diff --git a/docs/build/conversations.md b/docs/build/conversations.md
index 2d6e68a15..90d105d12 100644
--- a/docs/build/conversations.md
+++ b/docs/build/conversations.md
@@ -281,6 +281,7 @@ for (conversation in allConversations) {
```tsx
export const ListConversations: React.FC = () => {
+ const { initialize } = useClient();
const { conversations, error, isLoading } = useConversations();
if (error) {
diff --git a/docs/build/local-first.md b/docs/build/local-first.md
index fcf19db02..eca501e7c 100644
--- a/docs/build/local-first.md
+++ b/docs/build/local-first.md
@@ -1,7 +1,7 @@
---
sidebar_label: Local-first cache
sidebar_position: 1.5
-description: "Get started building apps with XMTP."
+description: "If you're building a production-grade app, be sure to use a local-first architecture to help you build a performant app."
---
# Use local-first architecture
@@ -23,3 +23,312 @@ import localfirst from '/docs/build/img/local-first-arch.jpeg';
- When building native iOS and Android mobile apps, you can use the device's encrypted container as the local cache to store decrypted data.
For more performance best practices, see [Optimize performance of your app](/docs/launch/performance)
+
+## Manage local data with Dexie in a web app built with `xmtp-js`
+
+The performance of a web app can be significantly improved by leveraging local data storage. Particularly in the context of loading messages, using a local cache can result in a 10x performance increase compared to solely relying on a network-based data source.
+
+This guide provides a walkthrough on managing local data storage in a web app built with the [xmtp-js SDK](https://github.com/xmtp/xmtp-js) and using the Dexie.js library. Dexie.js is a minimalistic wrapper for IndexedDB, which is a low-level API for client-side storage of significant amounts of structured data.
+
+import ReactPlayer from 'react-player'
+
+#### Experimental playground 🎲
+
+For a hands-on experience, check out the React Playground, built with the xmtp-js SDK:
+
+[GitHub repo](https://github.com/xmtp/xmtp-react-playground) | [Live version](https://xmtp.github.io/xmtp-react-playground/#/new)
+
+
+
+### Step 1: Install libraries
+
+To start, install the necessary libraries:
+
+```bash
+npm install dexie dexie-react-hooks
+```
+
+### Step 2: Define the database schema
+
+Create a `DB.ts` file and define your database schema. Here's an example of a potential database schema:
+
+:::tip
+
+This file defines the local database schema for our app. Any time we show any
+data in the UI, it should come from the database.
+
+:::
+
+```tsx
+import Dexie from "dexie";
+
+// Define a conversation interface
+export interface Conversation {
+ id?: number;
+ topic: string;
+ title: string | undefined;
+ createdAt: Date;
+ updatedAt: Date;
+ isGroup: boolean;
+ peerAddress: string;
+ groupMembers?: string[] | undefined;
+}
+
+// Define a message interface
+export interface Message {
+ id?: number;
+ inReplyToID: string;
+ conversationTopic: string;
+ xmtpID: string;
+ senderAddress: string;
+ sentByMe: boolean;
+ sentAt: Date;
+ contentType: {
+ authorityId: string;
+ typeId: string;
+ versionMajor: number;
+ versionMinor: number;
+ };
+ content: any;
+ metadata?: { [key: string]: [value: string] };
+ isSending: boolean;
+}
+
+// Define a message attachment interface
+export interface MessageAttachment {
+ id?: number;
+ messageID: number;
+ filename: string;
+ mimeType: string;
+ data: Uint8Array;
+}
+
+// Define a message reaction interface
+export interface MessageReaction {
+ id?: number;
+ reactor: string;
+ messageXMTPID: string;
+ name: string;
+}
+
+// Create a class for the database
+class DB extends Dexie {
+ // Define tables for the database
+ conversations!: Dexie.Table;
+ messages!: Dexie.Table;
+ attachments!: Dexie.Table;
+ reactions!: Dexie.Table;
+
+ constructor() {
+ super("DB");
+ this.version(2).stores({
+ // Define the structure and indexes for each table
+ conversations: `
+ ++id,
+ topic,
+ title,
+ createdAt,
+ updatedAt,
+ isGroup,
+ groupMembers,
+ peerAddress
+ `,
+ messages: `
+ ++id,
+ [conversationTopic+inReplyToID],
+ inReplyToID,
+ conversationTopic,
+ xmtpID,
+ senderAddress,
+ sentByMe,
+ sentAt,
+ contentType,
+ content
+ `,
+ attachments: `
+ ++id,
+ messageID,
+ filename,
+ mimeType,
+ data
+ `,
+ reactions: `
+ ++id,
+ [messageXMTPID+reactor+name],
+ messageXMTPID,
+ reactor,
+ name
+ `,
+ });
+ }
+}
+
+// Initialize the database and export it
+const db = new DB();
+export default db;
+```
+
+In this schema, we define interfaces for different types of data we want to store: conversations, messages, message attachments, and message reactions. We then create a class for the database that extends Dexie, and within that class, we define the tables and their structure.
+
+### Step 3: Perform database operations
+
+After defining the schema, you can perform various database operations such as adding, updating, and retrieving data.
+
+#### Add messages
+
+When a new message is sent, it's first saved to the local database before being sent to the network.
+
+```jsx
+// Create a new message
+const message: Message = {
+ //Properties
+ conversationTopic: stripTopicName(conversation.topic),
+ inReplyToID: "",
+ xmtpID: "PENDING-" + new Date().toString(),
+ senderAddress: client.address,
+ sentByMe: true,
+ sentAt: new Date(),
+ contentType: { ...contentType },
+ content: content,
+ isSending: true,
+};
+
+// Save the message to the database and get its ID
+message.id = await db.messages.add(message);
+```
+
+#### Update messages
+
+After a message is sent to the network and you receive the decoded message back, update the original message in the database with the ID of the message on the network and set `isSending` to false.
+
+```jsx
+// Update the message in the database
+await db.messages.update(message.id!, {
+ xmtpID: decodedMessage.id,
+ sentAt: decodedMessage.sent,
+ isSending: false,
+});
+```
+
+#### Check for existing messages
+
+Before saving a received message, check if it already exists in the database. If the message doesn't exist, save it; otherwise, skip the saving process.
+
+```jsx
+// Check if the message already exists in the database
+const existing = await db.messages
+ .where("xmtpID")
+ .equals(decodedMessage.id)
+ .first();
+```
+
+#### Find a conversation
+
+When you need to find a specific conversation in the `conversations` table, search by the `topic` field.
+
+```jsx
+// Find a conversation by topic
+return await db.conversations
+ .where("topic")
+ .equals(stripTopicName(topic))
+ .first();
+```
+
+#### Update a conversation
+
+When a new message is received, update the `updatedAt` timestamp of the related conversation.
+
+```jsx
+// Check if the conversation needs to be updated
+if (conversation && conversation.updatedAt < updatedAt) {
+ // If it does, update the updatedAt timestamp
+ await conversationMutex.runExclusive(async () => {
+ await db.conversations.update(conversation, { updatedAt });
+ });
+}
+```
+
+#### Add conversations
+
+When a new conversation is started, it's first saved to the local database.
+
+```jsx
+// Create a new conversation
+const conversation: Conversation = {
+ /* conversation properties */
+ topic: stripTopicName(xmtpConversation.topic),
+ title: undefined,
+ createdAt: xmtpConversation.createdAt,
+ updatedAt: xmtpConversation.createdAt,
+ isGroup: xmtpConversation.isGroup,
+ peerAddress: xmtpConversation.peerAddress,
+};
+
+// Save the conversation to the database and get its ID
+conversation.id = await db.conversations.add(conversation);
+```
+
+#### Check for existing conversations
+
+Before saving a new conversation, check if it already exists in the database. If the conversation doesn't exist, save it; otherwise, return the existing one.
+
+```jsx
+// Check if the conversation already exists in the database
+const existing = await db.conversations
+ .where("topic")
+ .equals(stripTopicName(xmtpConversation.topic))
+ .first();
+```
+
+### Step 4: Load initial data
+
+
+
+To load initial data when the application starts, use the `useConversations` function. This function fetches conversations from an XMTP client, saves these conversations to the local database (if they're not already stored), and returns an array of all conversations.
+
+#### Define functions to save conversations and messages
+
+Next, define two functions: `saveConversation` and `saveMessage`. These functions should take an XMTP conversation or message as an argument, check if it already exists in the local database, and if it doesn't, save it to the database:
+
+```jsx
+async function saveConversation(xmtpConversation: ConversationType) {
+ const existing = await db.conversations
+ .where("topic")
+ .equals(stripTopicName(xmtpConversation.topic))
+ .first();
+ if (!existing) {
+ const conversation: Conversation = {
+ /* conversation properties */
+ };
+ conversation.id = await db.conversations.add(conversation);
+ return conversation;
+ }
+ return existing;
+}
+
+async function saveMessage(
+ client: XMTP.Client,
+ conversation: Conversation,
+ xmtpMessage: XMTP.Message,
+) {
+ const decodedMessage = await client.messages.decode(xmtpMessage);
+ const existing = await db.messages
+ .where("xmtpID")
+ .equals(decodedMessage.id)
+ .first();
+ if (!existing) {
+ const message: Message = {
+ /* message properties */
+ };
+ message.id = await db.messages.add(message);
+ return message;
+ }
+ return existing;
+}
+```
+
+### Conclusion
+
+Managing local data storage in a web app can be complex. However, with Dexie.js and the right strategies for handling database operations, it can be much more manageable. Always remember to handle potential errors and race conditions to ensure the integrity of your data. Now that you've learned these steps, consider trying them out in your own projects. Happy coding!
+
+For more information about using Dexie.js, check out the **[official documentation](https://dexie.org/docs/Tutorial/Getting-started)**.
diff --git a/docs/launch/local-db.md b/docs/launch/local-db.md
deleted file mode 100644
index b806263dd..000000000
--- a/docs/launch/local-db.md
+++ /dev/null
@@ -1,449 +0,0 @@
----
-sidebar_label: Local DB
-sidebar_position: 1
-description: Manage local data with Dexie in a web app built with the xmtp-js SDK
----
-
-# Manage local data with Dexie in a web app built with xmtp-js
-
-
-
-
-
-
-
-The performance of a web app can be significantly improved by leveraging local data storage. Particularly in the context of loading messages, using a local database can result in a 10x performance increase compared to solely relying on a network-based data source.
-
-This guide provides a walkthrough on managing local data storage in a web application built with the [xmtp-js SDK](https://github.com/xmtp/xmtp-js) and using the Dexie.js library. Dexie.js is a minimalistic wrapper for IndexedDB, which is a low-level API for client-side storage of significant amounts of structured data.
-
-import ReactPlayer from 'react-player'
-
-#### Experimental playground 🎲
-
-
-
-For hands-on experience, check out the React Playground.
-
-[GitHub repo](https://github.com/xmtp/xmtp-react-playground) | [Live version](https://xmtp.github.io/xmtp-react-playground/#/new)
-
-
-
-## Step 1: Install libraries
-
-To start, install the necessary libraries:
-
-
-
-```bash
-npm install dexie dexie-react-hooks
-```
-
-## Step 2: Define the database schema
-
-Create a **`DB.ts`** file and define your database schema. Here's an example of a potential database schema:
-
-
-
-:::success
-
-This file defines the local database schema for our app. Any time we show any
-data in the UI, it should come from the database.
-
-:::
-
-```tsx
-import Dexie from "dexie";
-
-// Define a conversation interface
-export interface Conversation {
- id?: number;
- topic: string;
- title: string | undefined;
- createdAt: Date;
- updatedAt: Date;
- isGroup: boolean;
- peerAddress: string;
- groupMembers?: string[] | undefined;
-}
-
-// Define a message interface
-export interface Message {
- id?: number;
- inReplyToID: string;
- conversationTopic: string;
- xmtpID: string;
- senderAddress: string;
- sentByMe: boolean;
- sentAt: Date;
- contentType: {
- authorityId: string;
- typeId: string;
- versionMajor: number;
- versionMinor: number;
- };
- content: any;
- metadata?: { [key: string]: [value: string] };
- isSending: boolean;
-}
-
-// Define a message attachment interface
-export interface MessageAttachment {
- id?: number;
- messageID: number;
- filename: string;
- mimeType: string;
- data: Uint8Array;
-}
-
-// Define a message reaction interface
-export interface MessageReaction {
- id?: number;
- reactor: string;
- messageXMTPID: string;
- name: string;
-}
-
-// Create a class for the database
-class DB extends Dexie {
- // Define tables for the database
- conversations!: Dexie.Table;
- messages!: Dexie.Table;
- attachments!: Dexie.Table;
- reactions!: Dexie.Table;
-
- constructor() {
- super("DB");
- this.version(2).stores({
- // Define the structure and indexes for each table
- conversations: `
- ++id,
- topic,
- title,
- createdAt,
- updatedAt,
- isGroup,
- groupMembers,
- peerAddress
- `,
- messages: `
- ++id,
- [conversationTopic+inReplyToID],
- inReplyToID,
- conversationTopic,
- xmtpID,
- senderAddress,
- sentByMe,
- sentAt,
- contentType,
- content
- `,
- attachments: `
- ++id,
- messageID,
- filename,
- mimeType,
- data
- `,
- reactions: `
- ++id,
- [messageXMTPID+reactor+name],
- messageXMTPID,
- reactor,
- name
- `,
- });
- }
-}
-
-// Initialize the database and export it
-const db = new DB();
-export default db;
-```
-
-In this schema, we define interfaces for different types of data we want to store: conversations, messages, message attachments, and message reactions. We then create a class for the database that extends Dexie, and within that class, we define the tables and their structure.
-
-## Step 3: Perform database operations
-
-After defining the schema, you can perform various database operations such as adding, updating, and retrieving data.
-
-### Add messages
-
-When a new message is sent, it's first saved to the local database before being sent to the network.
-
-```jsx
-// Create a new message
-const message: Message = {
- //Properties
- conversationTopic: stripTopicName(conversation.topic),
- inReplyToID: "",
- xmtpID: "PENDING-" + new Date().toString(),
- senderAddress: client.address,
- sentByMe: true,
- sentAt: new Date(),
- contentType: { ...contentType },
- content: content,
- isSending: true,
-};
-
-// Save the message to the database and get its ID
-message.id = await db.messages.add(message);
-```
-
-### Update messages
-
-After a message is sent to the network and you receive the decoded message back, update the original message in the database with the ID of the message on the network and set **`isSending`** to false.
-
-```jsx
-// Update the message in the database
-await db.messages.update(message.id!, {
- xmtpID: decodedMessage.id,
- sentAt: decodedMessage.sent,
- isSending: false,
-});
-```
-
-### Check for existing messages
-
-Before saving a received message, check if it already exists in the database. If the message doesn't exist, save it; otherwise, skip the saving process.
-
-```jsx
-// Check if the message already exists in the database
-const existing = await db.messages
- .where("xmtpID")
- .equals(decodedMessage.id)
- .first();
-```
-
-### Find a conversation
-
-When you need to find a specific conversation in the **`conversations`** table, search by the `topic` field.
-
-```jsx
-// Find a conversation by topic
-return await db.conversations
- .where("topic")
- .equals(stripTopicName(topic))
- .first();
-```
-
-### Update a conversation
-
-When a new message is received, update the `updatedAt` timestamp of the related conversation.
-
-```jsx
-// Check if the conversation needs to be updated
-if (conversation && conversation.updatedAt < updatedAt) {
- // If it does, update the updatedAt timestamp
- await conversationMutex.runExclusive(async () => {
- await db.conversations.update(conversation, { updatedAt });
- });
-}
-```
-
-### Add conversations
-
-When a new conversation is started, it's first saved to the local database.
-
-```jsx
-// Create a new conversation
-const conversation: Conversation = {
- /* conversation properties */
- topic: stripTopicName(xmtpConversation.topic),
- title: undefined,
- createdAt: xmtpConversation.createdAt,
- updatedAt: xmtpConversation.createdAt,
- isGroup: xmtpConversation.isGroup,
- peerAddress: xmtpConversation.peerAddress,
-};
-
-// Save the conversation to the database and get its ID
-conversation.id = await db.conversations.add(conversation);
-```
-
-### Check for existing conversations
-
-Before saving a new conversation, check if it already exists in the database. If the conversation doesn't exist, save it; otherwise, return the existing one.
-
-```jsx
-// Check if the conversation already exists in the database
-const existing = await db.conversations
- .where("topic")
- .equals(stripTopicName(xmtpConversation.topic))
- .first();
-```
-
-## Step 4: Load initial data
-
-To load initial data when the application starts, use the **`useConversations`** function. This function fetches conversations from an XMTP client, saves these conversations to the local database (if they're not already stored), and returns an array of all conversations.
-
-### Use useEffect hook to fetch and save conversations
-
-
-
-Start by using React's **`useEffect`** hook to run an asynchronous operation when the component mounts. This operation should fetch the list of conversations from the client, and for each conversation, save it to the local database and fetch and save the latest message for the conversation:
-
-```jsx
-useEffect(() => {
- (async () => {
- if (!client) return;
- for (const xmtpConversation of await client.conversations.list()) {
- const conversation = await saveConversation(xmtpConversation);
- const latestMessage = (
- await xmtpConversation.messages({
- direction: XMTP.SortDirection.SORT_DIRECTION_DESCENDING,
- limit: 1,
- })
- )[0];
- if (latestMessage) {
- await saveMessage(client, conversation, latestMessage);
- }
- }
- })();
-}, []);
-```
-
-### Define functions to save conversations and messages
-
-Next, define two functions: **`saveConversation`** and **`saveMessage`**. These functions should take an XMTP conversation or message as an argument, check if it already exists in the local database, and if it doesn't, save it to the database:
-
-```jsx
-async function saveConversation(xmtpConversation: ConversationType) {
- const existing = await db.conversations
- .where("topic")
- .equals(stripTopicName(xmtpConversation.topic))
- .first();
- if (!existing) {
- const conversation: Conversation = {
- /* conversation properties */
- };
- conversation.id = await db.conversations.add(conversation);
- return conversation;
- }
- return existing;
-}
-
-async function saveMessage(
- client: XMTP.Client,
- conversation: Conversation,
- xmtpMessage: XMTP.Message,
-) {
- const decodedMessage = await client.messages.decode(xmtpMessage);
- const existing = await db.messages
- .where("xmtpID")
- .equals(decodedMessage.id)
- .first();
- if (!existing) {
- const message: Message = {
- /* message properties */
- };
- message.id = await db.messages.add(message);
- return message;
- }
- return existing;
-}
-```
-
-### Use useEffect hook to stream conversations
-
-
-
-Use another **`useEffect`** hook to listen for new conversations in real-time. As new conversations come in, save them to the local database:
-
-```jsx
-useEffect(() => {
- (async () => {
- if (!client) return;
- for await (const conversation of await client.conversations.stream()) {
- await saveConversation(conversation);
- }
- })();
-}, []);
-```
-
-### Fetch conversations from local DB
-
-Finally, return the conversations from the local database:
-
-```jsx
-return (
- useLiveQuery(async () => {
- return await db.conversations.reverse().sortBy("updatedAt");
- }) || []
-);
-```
-
-
-
-This hook, **`useLiveQuery`**, automatically re-renders the component whenever the data in the local database changes. It is used to fetch all conversations from the database, sort them in reverse order by their updated time, and return the result. If the query returns nothing, an empty array is returned by default.
-
-By loading initial data, you ensure that your application loads instantly on each refresh without crashes and slow loading spinners.
-
-## Step 5: Prevent race conditions
-
-Race conditions can occur when multiple operations that read from and write to the same database area are executed in close sequence, leading to inconsistent data. To prevent race conditions, use a mutex (short for "mutual exclusion object"), which ensures that only one operation can happen at a time.
-
-First, install the necessary library:
-
-```bash
-npm install async-mutex
-```
-
-Then, import and initialize a mutex in your script:
-
-```jsx
-// Import the Mutex class from the async-mutex library
-import { Mutex } from "async-mutex";
-
-// Initialize a mutex for messages and conversations
-const messageMutex = new Mutex();
-const conversationMutex = new Mutex();
-```
-
-Now, we can use these mutexes to ensure that only one operation happens at a time. Here are some examples of how to use a mutex:
-
-### When updating a conversation
-
-Wrap the database operation within the **`runExclusive`** method of the mutex instance. This guarantees that no other operation can happen until the current operation is finished:
-
-```jsx
-// Check if the conversation needs to be updated
-if (conversation && conversation.updatedAt < updatedAt) {
- // If it does, update the updatedAt timestamp
- await conversationMutex.runExclusive(async () => {
- await db.conversations.update(conversation, { updatedAt });
- });
-}
-```
-
-### When saving a conversation
-
-The same applies when saving a new conversation. The operation is wrapped in the **`runExclusive`** method:
-
-```jsx
-// Save the conversation to the database
-return await conversationMutex.runExclusive(async () => {
- // Check if the conversation already exists in the database
- const existing = await db.conversations
- .where("topic")
- .equals(stripTopicName(xmtpConversation.topic))
- .first();
-
- // If it doesn't exist, create a new conversation and save it to the database
- if (!existing) {
- const conversation: Conversation = {
- /* conversation properties */
- };
- conversation.id = await db.conversations.add(conversation);
- }
-
- return existing || conversation;
-});
-```
-
-Using a mutex in this way helps prevent race conditions and maintain the integrity of your data.
-
-## Conclusion
-
-Managing local data storage in a React application can be complex. However, with Dexie.js and the right strategies for handling database operations, it can be much more manageable. Always remember to handle potential errors and race conditions to ensure the integrity of your data. Now that you've learned these steps, consider trying them out in your own projects. Happy coding!
-
-For more information on using Dexie.js, check out the **[official documentation](https://dexie.org/docs/Tutorial/Getting-started)**.
From b4a12edbfefcebc3736a9bbc86f61f3c37b2c1e1 Mon Sep 17 00:00:00 2001
From: Jennifer Hasegawa <5481259+jhaaaa@users.noreply.github.com>
Date: Tue, 22 Aug 2023 06:40:56 -0700
Subject: [PATCH 4/8] update changelog
---
docs/build/local-first.md | 9 +++++----
docs/changelog.md | 9 +++++++++
2 files changed, 14 insertions(+), 4 deletions(-)
diff --git a/docs/build/local-first.md b/docs/build/local-first.md
index eca501e7c..0eeff8fd2 100644
--- a/docs/build/local-first.md
+++ b/docs/build/local-first.md
@@ -18,17 +18,17 @@ import localfirst from '/docs/build/img/local-first-arch.jpeg';
- When building a web app with the [React SDK](https://github.com/xmtp/xmtp-web/tree/main/packages/react-sdk), the local-first architecture is automatically provided by the SDK.
-- When building a web app with the [xmtp-js SDK](https://github.com/xmtp/xmtp-js), you can use the browser `localStorage` as the local cache to store encrypted data, decrypting data each time before display.
+- When building a web app with the [xmtp-js SDK](https://github.com/xmtp/xmtp-js), you can use the browser `localStorage` as the local cache to store encrypted data, decrypting data each time before display. You might also consider [using Dexie to manage your web app's local data](#manage-local-data-with-dexie-in-a-web-app-built-with-xmtp-js).
- When building native iOS and Android mobile apps, you can use the device's encrypted container as the local cache to store decrypted data.
For more performance best practices, see [Optimize performance of your app](/docs/launch/performance)
-## Manage local data with Dexie in a web app built with `xmtp-js`
+## Manage local data with Dexie in a web app built with xmtp-js
The performance of a web app can be significantly improved by leveraging local data storage. Particularly in the context of loading messages, using a local cache can result in a 10x performance increase compared to solely relying on a network-based data source.
-This guide provides a walkthrough on managing local data storage in a web app built with the [xmtp-js SDK](https://github.com/xmtp/xmtp-js) and using the Dexie.js library. Dexie.js is a minimalistic wrapper for IndexedDB, which is a low-level API for client-side storage of significant amounts of structured data.
+This guide provides a walkthrough on managing local data storage using the Dexie.js library in a web app built with the [xmtp-js SDK](https://github.com/xmtp/xmtp-js). Dexie.js is a minimalistic wrapper for IndexedDB, which is a low-level API for client-side storage of significant amounts of structured data.
import ReactPlayer from 'react-player'
@@ -39,6 +39,7 @@ For a hands-on experience, check out the React Playground, built with the xmtp-j
[GitHub repo](https://github.com/xmtp/xmtp-react-playground) | [Live version](https://xmtp.github.io/xmtp-react-playground/#/new)
+
### Step 1: Install libraries
@@ -331,4 +332,4 @@ async function saveMessage(
Managing local data storage in a web app can be complex. However, with Dexie.js and the right strategies for handling database operations, it can be much more manageable. Always remember to handle potential errors and race conditions to ensure the integrity of your data. Now that you've learned these steps, consider trying them out in your own projects. Happy coding!
-For more information about using Dexie.js, check out the **[official documentation](https://dexie.org/docs/Tutorial/Getting-started)**.
+To learn more aboutDexie.js, see [Getting Started with Dexie.js](https://dexie.org/docs/Tutorial/Getting-started).
diff --git a/docs/changelog.md b/docs/changelog.md
index 0e54f7491..d2a253638 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -10,6 +10,15 @@ This changelog provides information about release milestones for XMTP SDKs, deve
---
+#### Aug 2023
+
+## [React client SDK](https://github.com/xmtp/xmtp-web/tree/main/packages/react-sdk) v1.0.0 is Production Ready
+
+This release delivers local-first architecture, optimistic sending, and support for standards-track content types via configuration. **[Read the release notes](https://github.com/xmtp/xmtp-web/releases/tag/%40xmtp%2Freact-sdk%401.0.0
+)** (Aug 22)
+
+---
+
#### July 2023
## [React Native client SDK](https://github.com/xmtp/xmtp-react-native) is in Beta (xmtp-react-native)
From 427bbbdee4a7c735adc8e05d59b97246c6cc24ec Mon Sep 17 00:00:00 2001
From: Jennifer Hasegawa <5481259+jhaaaa@users.noreply.github.com>
Date: Tue, 22 Aug 2023 06:57:35 -0700
Subject: [PATCH 5/8] fix local first diagram
---
docs/build/img/local-first-arch.jpeg | Bin 55043 -> 54782 bytes
1 file changed, 0 insertions(+), 0 deletions(-)
diff --git a/docs/build/img/local-first-arch.jpeg b/docs/build/img/local-first-arch.jpeg
index a3ea3327bff61c16b1d2babff5fb0a97a89bf25b..c9e411ad1c5cedc22d5b9a5046c9ee5f5bd77166 100644
GIT binary patch
literal 54782
zcmeFZcT`)+vNwvy8Alm|iS`%~Og2G8gJ(?67D$LBFd0b%lT9#;1DMPJ7C|5wGDt|6
zAYn-e9LUiGAu=XsFxe#gG3T5)bLM>apYOi=-dpQ?r`8g?H(k5B)w{Z@`&U)^KJfiB
z%T+xsT`iUq$0UA2oQ384FpE0N$sc~F-~XOGrc-B5{Z8l3oH>2w{5e+E^XJZ=XT5mk
zBI||A7tWu*1h{nh%GIlYtE?BVv0b~$c3i&tyO0yVKY8-hx#I_~UO0c@_`m;~^!*(R
z;M@-_rw&e@0I>W3IB^ni;(I*{-*H!2emHsDU6w!S?3r_?PM<&d!|&B@S6NP+Jns3~
zvzN}ZvYt72_5{m`A5NY+eda9S+_m#;*Ew%X_wfG=Gk=XvVduCZub>Eq+W93_k$^I0
zUIEW4D!XMt4}AQehbI>F2`K59*n9iFDr^wc&MB|&<&sl3b$F5dUe_@qH}3=E%W-R5
zzYp6n{n3-->v7jkoj$G-2OM|v#1E%W{cz&+KNwE@062M#O#`4?
zeNj*RKE!hICh*mOfiI
z@2_2;9$n!4g0>Pa4$8F2?_J+cNN8CLRWd$zwqX#kMI6mp-&36vnPB-!<5xouSE=0>
zu9>;_QfuRou(g7tov&`6{~g)2{jIEee#oM#_16JkwZU}rwYWUlD9Zj7_@s=9Vg|#_6!OD6Cu>zfU*f$x1hVblM+;^cK61P
z)If0fL?TT8>Ba&K4&`>mc{X9#Vhe73sV4O_?gL+u-`1kPQzjJ4QbsOA@^J1Qg=2J&bp}ar5uFkmpspn
z2mp_)_;q`M$U}u&%3M>nbZ=29V?D{3+{Nf9`O^NUP&N)10qUR1dnL}@osS$J9R7jG
zZE0yfD!19b<>;l=$LR7&%^_%LLZR~-KY{io@+)~{mz-a?T*M{=EbP&btYM*qZRLU8
z+>GRAmW}~~Rh~~2A
ztcbD|DXG_o#d^q{l}fncL8iu2HC%LL8M79jXG!GY4{{up?i&z*cmpxT#U(h=fcE}x
zzx=7aY;k$z=r*YhDIWW!d#leUes;}BGU#L2V+?@~y)&q+m_6E&rirZU%OS5TwGUj}w!Y3a{4
z|5-Y0;q`8uFL1DF$|ZGpBjYufG>!I8lvzprG;%sYHsE%N34~;)vZ6ZH-T<9km?u77
zUnz{bQIbd@9wK+8YzkAiBP+xfWG}svHUFo~zZK5ZxWzfY0Lb#XG>maBN_`!_#%$oT
zn(EPUe_gThL7H8v_zwYL#bJ2hr=5CsD>O#>sQu
z34L1Q&Ch91dKeL_CJi{9)*F~&w&CuuHTX=wm=BS{$m09@^K5%c-?j{f!jWL^`ed{dhFgSF>g5?j(k)gu~vM)--yp!WPx8B%Subq5K=LW?V*|fv8WWqB_~|r5`~(qPR7+
zco(fs^nxuVA5r$P#4<4^NJ)3gGVH<%z-W{^8F;r*|(<~GeB*uKTK)$l9>XDHGdrc?db
z`hOSqk6s8Jz}ux-jez?tg++=-{MqQ{jz@p8Mz3Jk9yNVuQPAFwjOkr^IbLWc7O?hI
zVO{*_IzROCPk(SK!tiv#=UwoSw*pK0o<&JG-{)sLggF}VSQ-z&X1n`_7Kt5J0)S{Z
z+EEO%1}(C#r?R$JHe961Gl0O-xW0||0(x6ed%PeDPF-QTuE;@A$u{AleV9#e36ANm
z7C+e+uFJobb|_5K>)2YXVsg_z>1rDO3>E_hThwm(cG%Zv5SYQ#RVX_?H#f&hm?xPk
z`wFNa`<+F!Lbfm4aYS1Xls>*~WUaHH2fTdsp-K3WlXCmqDhE73&APaEde3>vXE
zrlIbBA)W9b|ZL1eaR-NvNC8dXm^Jsifw3WZv=mTO1u7{Ypqk_}a43Q@hbjzgz83
z3EpfX<#4U@P3D7r3K*TKl%OXo3=prSPM?ng_PPgBlmzx=!r9fXEft}(>|~!L=mw-y
zviG2gS@{P-lS&dcg!RK$-Vs6ZvQd*?yGGJhMiu-07Kn5{J@2XvZ=r4*#Q6OcbO?W6
zU=r;+ivW@RK&if@>%htO9OPZOTy5&B?bnZM&C5Auw8Fqlo@XeunVENFox2%Yl{-Cnvj>DFi?h9y2cgkSepZ
zD2i~qYsD9kf>;oAEG^La+7O&$wjN2ts^CUfcQ9ML^>b&Q$~0`Gy4L*G#o88EC*fr>
zD(Vi_20@JTvalTv3@~q$7*7qRFs6`-#BApbn=odlEhSp?1PxT=Dt)9j94)^@Eky>($!9#Zxh$F@MwkXYanHA
zI`WmUQ8!nr{kmFwlPnq6Jc6wstK2+$L*Jdc@nrFB$}qCXNMwJNxaCEmtKhqdxm5ou
z=krzC`Q<|H-&rauGYS6okaB?Un3bS^G)CB1LwVug@|WNH1F5O*3d!ymo!?lyHB`oa
zDbC3ccyk_?!7a*9e}v`{1+jxcW(z$phf>NEA(q0)s)C+?Zlf6-M(;k
zW#xAV$K2;U{DSMIg*$hrCzKWJ4pbea-uSF*PUHhgHVjCw%irSOVNAVxF%vC^cIf+xMKb*uSHJ
zrkx$mNbz~~gvrch^e6hI>c
z_*{2ce@^cYY06pyLc-Okx~v&G0Hh$^0#X+#x+-dN2j
zg&eH#V|?W6iOhOuUe!pl7p~Rdw%Q|y;`|K0sWSzds6{{DKHpCG&bv7k>J%?kSCkk-
zr!ts6P~^Dpq|$Yws*~J=P^IbQJ%=RDpbG7Z3k981nquj@9ga!E131j?mNz<2G3xbV
zP`eEF7th^$5;s)`>#jq@h-qgwWfuaH{R9eG0nCFipG8yK?5@XG6oerV@*cey$b`&!UpWFPdfQJQND(K6+XYVc_nzaKQD
z0#Me$d8J5a#g7_nAGEsW!03RW%zlB_>--DfSt3dHXO?|qW68h!fWY}+%3)4Q9A5Con`#!D8ozb_J?%a
z@a0`Si{fkCtvryshTO^(95n6n;u5@daUFjN5lxVYK;@%Je~f{~>rz82u19U{XH)w#&}ygO
zlilvKJ0kZZtX%U7-H0eCH&KfpSsYD8eMvWH-?ZUZk`sQRF7^2Gy&diC&1YY}@lUJe
zZP-EyzcSW&4lBca;!xEA)m%gjii+93J>Q=mYnPI#F(=VluOha*5Jf<9P?s?uUU&DA
zxEI{}2Z{@wv3e~(YEkfCQ;SOc3?DXvByOc7TXszp(^O2%+*YdLYd^kOnIu+Tg7=u4
ziO>qV?*zJC@yA{OvY9jXee*JLQDs`p-G$W@zn$SL>Rqyr{!r+vBh|Z17ljT^hi%ZA
zl=+w1kiYEj{>B!uY`b~_wKulAgVil8GZAU|+kT3BoSf3(+1bu@Q|$dGaq~6Sau>4h
zwN+gUR>{qh>g2DMyWq0GBW}{o8BlNSVEu;XQnq=>j>S-IOS;9+-n3azznK5+cdy%)
z5_=VVLODPI$nLrYAe6Vy&_oLd1oPHIw|2UUrU{!ji&G5kg2#p_-&x?I=`MTOKGcsPHU;PEDxlVw7lAsQHi)kQ5r(<5(vJ^qGjsl@p?*rP$nh0=2emDI2&I&
zY*<DJFhBar=FRp*i5F8tH)2q+TBj&_ELF}TcdEPeV
z=J|R)K#a{R^i?(3+hO^Idd6rRnvS)cG*jC*vk*HKr`EXPt!>|wNPdj4YYE<_KR0(C
zhrMJSYAIbI0mLp>9
zanbrzsjE#&zDA%fN6$DMm0#6@)=xCcMMCB~W!X}JCQ7;=^omz9x;r2ct0UIjqA<$q
z*#vM&OSThHF{cgw>)k9h3b+fVZ}*;KfAG;Wus~vIfjv$kScAEq>^d44t}#h*qVQzO
zhitbE8Pu0sq9ppzK3yjfiwr!_Oc)P^`9bBN>M6v*k0fmO>r{~c$~#UT&aPDR8>}{!
zXRq~8)(s6^jNexHFVU}XM$W0ObO%&(PJ8N!F2-w67YrI*x@Dh4)hplQ`T&S1Xy_Y|
zRhId1l;THmB6N9UL8gx2vW4jMav7*X&q-=PloYLar2&k
z=J|G`B1^ATg}owoPT--aXB#gV&)~#&<8pliVsm8`tvi8tF8J@Z;YKbv*QW)av%fI*
zV_lc&C5{`_WTmB{0VueqJr+7S1y{bI30(t$c}t2|eWHx7-CcNNdrpv8#Ao$|!*k~J
zcNR&!q_@l+L1KE33@9>
zjhQYC*}cBq-YP6nLT$TuB(sp~d(?Fl`g|L;B;ShgG*Ec-zLd4}5ShF#9E!+8C
z4_zk4C;Ttxxri0%l^DS=5xHAtC2~a~NBrY|t^Z^B{L3|BIb$W@Zk>o?!jcdsb80I(
z6b1zS@=ul@#K-~k7q<>aZ}sy_857w;)6EIv^sVvw%B_b{5T$?eolqV6VKbHE9<~9L
zUVZ_2_v8GnOK=5ku7||=ctxtIF7$vF9i2h!9xB8Xmbe3$KaTK%>b;&-f7uVR0(jb
za#;3%*cyE^*
zD>-H(DyXrf+v&=b?{YBl;zaEX2O`cqbX!m63cMy8Pf{)yyV<~~fYvHbW9RX`xvjgn
zkn_0#adHG|uSktO|IMT<=Z=8kgE4$eT|D_H=%k_KTdgAf#5XXy%E>U@qXGyhXu3#m
z-Ro$rarmyI55H`NafY4l*-mUcFj7;9nA(KVh~Vd5#@l(QXHe}3rP1?LzuQl_#igI#jntK
zZ~ErfS5i)V7$1{t&}y*W$`4;B>Y1moee!ck6^7T76});?&1c+P2S`zg{J1=L?&@l;
z6iAA5*o2h1nlhDE^~b9}aj7lrslQK1cf@_uhX{{I4L`lV?0kRcJLl2x?K4Lmb}bCi
zhe2-hXQuv5$4)k539w63ZktU3rEyg-hy|f9Hu*aAA;>HV8PVC7EqLef
zRfb1fK|=BB9VKiW+wK-o3EECo+4RkS*wz{4->O9rD@Ee)^n2VKEyX$GlnKk2h#&JH
z@EbZ}$R}G2W!UOvG+1R76}->0I*jY$%0qcO+>?0H<%
zT>>8s0{MFQn11#NW#fTy3}50OBwFGt(OCzP6|Y=XOV)V}-IuOgtR-4y%`P=Gwgx(9
zc)^*sxEiF{=q-#(o{E?rF6Qe&o6m#0Z>74n>+%5Hqk6s5L&-u3_jfMrcDr1eBpzsN
zA-B{;{q#Sb>}1S-V?H6eU8>f)-mtZ!jBh7L>%&bYwy5h2QD)n&ylc#;RB(>5QKpNL
zUECap8P0#d$yiI4_C#$OLG+grv?-m4o*_29L2
z3AO)BshDFKDhf$#fU@PH2(&BTP%P-Lp```WSuxZJ!4R<9_}(kuF-gkq;t
z6=kh*=j70>OJKW9?}IoM2?oMKALgEVn300IFCqf-XKx@I
zC^=Tab{O#+6%rX|heeUNJIkp&@Y<|nv9rElO(18PH$OkkuLL(AficaPQHX_IjNbkx
zIq;oDsILxU*L8G%xbAbkKfHnct?-L)dok|lds$S5dKfM*RRi7{u^w}6p8j-fVYt!n
z?u^Uz2RmXzz}qs{-Um}1mY()o+xZiGu2EB-3H#guB;p;kDGRjH8i&kCtgwH6UcT4GztPoV@KK!>-VC}La#
zf>l*9^Ij>ldO%1WATGteRV2c!`bs3AY8U8MZ#%OVl^u(p3&Mk#F>#UjaR_uRSvn4?
zF&m%yg6#(j!XE5tyF&L*OATXdf^<<|#&TYSA{wX(S@lcYPy#FS<$(_Rda0XBFN{=W
zzJAFcbQNk{z7?7O3~!|!=6Bs>VN}4>w0XDM_8I&XYA@pDe!CLB#b8Ks3K}pFvFT3?
zJu>v*sD>DEvd^1?HPvi0pR9;3+rx8ctdn|hIa
zqVEQ$EZ7K<(__Z%XOCfoztNRR9EiOWqM^Zn2EPln&*B;)K6lpXgR`4YLztT4AnH$P!?Z
zXf(mMnQpZa0R9kz%TsJx=-E;Q@QTg`mS-nsW%MzG?siYT4?g%aHCymx^(nvu>A^8
zDy(f9BuRFP;3^_e^4kwGcg;t8#h}Un$8@{UnEnA?j2~u5(JwO;Y%%WYbWc=`A@iMO
zj(d$!Z}dtj0QO}p`3&KI(@MH(pO!ju{m$|rY$`#kZXzXhrfv>
zR_T1JAH&JgwL?a^`aRyxd*^N$jKYNqEGy0pcxWy7o}po=kGZMJv5jePZx
z?DlyNtt8s_^%}I;Wt<<$kJnP-Ws(p!$F9Ur#UXl7l|@BEP9wJ|MLoUuTq4)Cw1;G^9D-HZAshDMV2zYhajnGZWSo*!4Zf#B
zYe*i!z~}dk@k_*1W*abSzUE1}$`m=c8K5JxMKJB)@IN@ib}@foWV
zOSosWG*j9r7IEQy<=SK0?4>X*eiAjgsD&hCcr$GI##(OYtp@l`)I4K80