

# IVS 聊天功能客户端消息收发 SDK：JavaScript 教程第 2 部分：消息和事件
<a name="chat-sdk-js-tutorial-messages-events"></a>

本教程的第 2 部分（也是最后一部分）分为几个章节：

1. [订阅聊天消息事件](#chat-js-messages-events-subscribe)

1. [显示收到的消息](#chat-js-messages-events-show)

   1.  [创建消息组件](#chat-js-messages-create-component)

   1. [识别当前用户发送的消息](#chat-js-messages-recognize)

   1. [创建消息列表组件](#chat-js-messages-create-list-component)

   1. [呈现聊天消息列表](#chat-js-messages-render-list)

1. [在聊天室中执行操作](#chat-js-messages-events-room-actions)

   1. [发送消息](#chat-js-room-actions-sending-message)

   1. [删除消息](#chat-js-room-actions-deleting-message)

1. [后续步骤](#chat-js-messages-events-next-steps)

**注意**：在某些情况下，JavaScript 和 TypeScript 的代码示例是相同的，因此我们将它们合并在一起。

要学习完整的 SDK 文档，请从[亚马逊 IVS 聊天功能客户端消息收发 SDK](chat-sdk.md)（在《Amazon IVS 聊天功能用户指南**》中）和 [Chat Client Messaging: SDK for JavaScript Reference](https://aws.github.io/amazon-ivs-chat-messaging-sdk-js/latest/)（在 GitHub 上）开始。

## 先决条件
<a name="chat-js-messages-events-prerequisite"></a>

确保您已完成本教程的第 1 部分[聊天室](chat-sdk-js-tutorial-chat-rooms.md)。

## 订阅聊天消息事件
<a name="chat-js-messages-events-subscribe"></a>

当聊天室中发生事件时，`ChatRoom` 实例使用事件进行通信。要开始实现聊天体验，您需要向用户显示其他用户在他们连接的聊天室中发送消息的时间。

在这里，您订阅了聊天消息事件。稍后，我们将向您展示如何更新您创建的消息列表，该列表将随着每条消息/事件而更新。

在您的 `App` 中，请在 `useEffect` 钩子内订阅所有消息事件：

```
// App.tsx / App.jsx

useEffect(() => {
  // ...
  const unsubscribeOnMessageReceived = room.addListener('message', (message) => {});

  return () => {
    // ...
    unsubscribeOnMessageReceived();
  };
}, []);
```

## 显示收到的消息
<a name="chat-js-messages-events-show"></a>

接收消息是聊天体验的核心部分。通过使用 Chat JS SDK，您可以对代码进行设置，以轻松接收来自连接到聊天室的其他用户的事件。

稍后，我们将向您展示如何利用您在此处创建的组件在聊天室中执行操作。

在您的 `App` 中，使用名为 `messages` 的 `ChatMessage` 数组类型定义名为 `messages` 的状态：

------
#### [ TypeScript ]

```
// App.tsx

// ...

import { ChatRoom, ChatMessage, ConnectionState } from 'amazon-ivs-chat-messaging';

export default function App() {
  const [messages, setMessages] = useState<ChatMessage[]>([]);

  //...
}
```

------
#### [ JavaScript ]

```
// App.jsx

// ...

export default function App() {
  const [messages, setMessages] = useState([]);

  //...
}
```

------

接下来，在 `message` 侦听器函数中，将 `message` 附加到 `messages` 数组中：

```
// App.jsx / App.tsx

// ...

const unsubscribeOnMessageReceived = room.addListener('message', (message) => {
  setMessages((msgs) => [...msgs, message]);
});

// ...
```

下面我们逐步完成显示收到的消息的任务：

1.  [创建消息组件](#chat-js-messages-create-component)

1. [识别当前用户发送的消息](#chat-js-messages-recognize)

1. [创建消息列表组件](#chat-js-messages-create-list-component)

1. [呈现聊天消息列表](#chat-js-messages-render-list)

### 创建消息组件
<a name="chat-js-messages-create-component"></a>

`Message` 组件负责呈现您的聊天室收到的消息内容。在本节中，您将创建一个消息组件，用于在 `App` 中呈现单个聊天消息。

在 `src` 目录中创建一个新文件并将其命名为 `Message`。传入该组件的 `ChatMessage` 类型，然后从 `ChatMessage` 属性中传递 `content` 字符串，以显示从聊天室消息侦听器收到的消息文本。在项目导航器中，转到 `Message`。

------
#### [ TypeScript ]

```
// Message.tsx

import * as React from 'react';
import { ChatMessage } from 'amazon-ivs-chat-messaging';

type Props = {
  message: ChatMessage;
}

export const Message = ({ message }: Props) => {
  return (
    <div style={{ backgroundColor: 'silver', padding: 6, borderRadius: 10, margin: 10 }}>
      <p>{message.content}</p>
    </div>
  );
};
```

------
#### [ JavaScript ]

```
// Message.jsx

import * as React from 'react';

export const Message = ({ message }) => {
  return (
    <div style={{ backgroundColor: 'silver', padding: 6, borderRadius: 10, margin: 10 }}>
      <p>{message.content}</p>
    </div>
  );
};
```

------

提示：使用该组件存储您要在消息行中呈现的不同属性；例如，头像 URL、用户名和消息发送时的时间戳。

### 识别当前用户发送的消息
<a name="chat-js-messages-recognize"></a>

为了识别当前用户发送的消息，我们修改了代码并创建了 React 上下文来存储当前用户的 `userId`。

在 `src` 目录中创建一个新文件并将其命名为 `UserContext`：

------
#### [ TypeScript ]

```
// UserContext.tsx

import React, { ReactNode, useState, useContext, createContext } from 'react';

type UserContextType = {
  userId: string;
  setUserId: (userId: string) => void;
};

const UserContext = createContext<UserContextType | undefined>(undefined);

export const useUserContext = () => {
  const context = useContext(UserContext);

  if (context === undefined) {
    throw new Error('useUserContext must be within UserProvider');
  }

  return context;
};

type UserProviderType = {
  children: ReactNode;
}

export const UserProvider = ({ children }: UserProviderType) => {
  const [userId, setUserId] = useState('Mike');

  return <UserContext.Provider value={{ userId, setUserId }}>{children}</UserContext.Provider>;
};
```

------
#### [ JavaScript ]

```
// UserContext.jsx

import React, { useState, useContext, createContext } from 'react';

const UserContext = createContext(undefined);

export const useUserContext = () => {
  const context = useContext(UserContext);

  if (context === undefined) {
    throw new Error('useUserContext must be within UserProvider');
  }

  return context;
};

export const UserProvider = ({ children }) => {
  const [userId, setUserId] = useState('Mike');

  return <UserContext.Provider value={{ userId, setUserId }}>{children}</UserContext.Provider>;
};
```

------

注意：这里我们使用了 `useState` 钩子来存储 `userId` 值。今后，您可以使用 `setUserId` 来更改用户上下文或用于登录目的。

接下来，使用之前创建的上下文替换传递给 `tokenProvider` 的第一个参数中的 `userId`：

```
// App.jsx / App.tsx

// ...

import { useUserContext } from './UserContext';

// ...


export default function App() {
  const [messages, setMessages] = useState<ChatMessage[]>([]);
  const { userId } = useUserContext();
  const [room] = useState(
    () =>
      new ChatRoom({
        regionOrUrl: process.env.REGION,
        tokenProvider: () => tokenProvider(userId, ['SEND_MESSAGE']),
      }),
  );

  // ...
}
```

在您的 `Message` 组件中，使用之前创建的 `UserContext`，声明 `isMine` 变量，将发件人的 `userId` 与上下文中的 `userId` 进行匹配，然后为当前用户应用不同样式的消息。

------
#### [ TypeScript ]

```
// Message.tsx

import * as React from 'react';
import { ChatMessage } from 'amazon-ivs-chat-messaging';
import { useUserContext } from './UserContext';

type Props = {
  message: ChatMessage;
}

export const Message = ({ message }: Props) => {
  const { userId } = useUserContext();

  const isMine = message.sender.userId === userId;

  return (
    <div style={{ backgroundColor: isMine ? 'lightblue' : 'silver', padding: 6, borderRadius: 10, margin: 10 }}>
      <p>{message.content}</p>
    </div>
  );
};
```

------
#### [ JavaScript ]

```
// Message.jsx

import * as React from 'react';
import { useUserContext } from './UserContext';

export const Message = ({ message }) => {
  const { userId } = useUserContext();

  const isMine = message.sender.userId === userId;

  return (
    <div style={{ backgroundColor: isMine ? 'lightblue' : 'silver', padding: 6, borderRadius: 10, margin: 10 }}>
      <p>{message.content}</p>
    </div>
  );
};
```

------

### 创建消息列表组件
<a name="chat-js-messages-create-list-component"></a>

`MessageList` 组件负责随时间显示聊天室的对话。`MessageList` 文件是存放所有消息的容器。`Message` 是 `MessageList` 中的一行。

在 `src` 目录中创建一个新文件并将其命名为 `MessageList`。使用类型为 `ChatMessage` 数组的 `messages` 定义 `Props`。在正文内，映射我们的 `messages` 属性并将 `Props` 传递给您的 `Message` 组件。

------
#### [ TypeScript ]

```
// MessageList.tsx

import React from 'react';
import { ChatMessage } from 'amazon-ivs-chat-messaging';
import { Message } from './Message';

interface Props {
  messages: ChatMessage[];
}

export const MessageList = ({ messages }: Props) => {
  return (
    <div>
      {messages.map((message) => (
        <Message key={message.id} message={message}/>
      ))}
    </div>
  );
};
```

------
#### [ JavaScript ]

```
// MessageList.jsx

import React from 'react';
import { Message } from './Message';

export const MessageList = ({ messages }) => {
  return (
    <div>
      {messages.map((message) => (
        <Message key={message.id} message={message} />
      ))}
    </div>
  );
};
```

------

### 呈现聊天消息列表
<a name="chat-js-messages-render-list"></a>

现在，将新的 `MessageList` 导入您的主要 `App` 组件中：

```
// App.jsx / App.tsx

import { MessageList } from './MessageList';
// ...

return (
  <div style={{ display: 'flex', flexDirection: 'column', padding: 10 }}>
    <h4>Connection State: {connectionState}</h4>
    <MessageList messages={messages} />
    <div style={{ flexDirection: 'row', display: 'flex', width: '100%', backgroundColor: 'red' }}>
      <MessageInput value={messageToSend} onValueChange={setMessageToSend} />
      <SendButton disabled={isSendDisabled} onPress={onMessageSend} />
    </div>
  </div>
);

// ...
```

所有部分都已准备就绪，您的 `App` 可以开始呈现聊天室收到的消息。继续阅读下文，了解如何在聊天室中利用您创建的组件执行操作。

## 在聊天室中执行操作
<a name="chat-js-messages-events-room-actions"></a>

在聊天室中发送消息和执行主持人操作是您与聊天室互动的一些主要方式。在这里，您将学习如何使用各种 `ChatRequest` 对象在 Chatterbox 中执行常见操作，例如发送消息、删除消息和断开其他用户的连接。

聊天室中的所有操作都遵循一个通用模式：对于您在聊天室中执行的每个操作，都有一个相应的请求对象。对于每个请求，您在请求确认时都会收到相应的响应对象。

只要在您创建聊天令牌时用户获得了正确的权限，他们就可以使用请求对象成功执行相应的操作，以查看您可以在聊天室中执行哪些请求。

下面，我们介绍如何[发送消息](#chat-js-room-actions-sending-message)和[删除消息](#chat-js-room-actions-deleting-message)。

### 发送消息
<a name="chat-js-room-actions-sending-message"></a>

`SendMessageRequest` 类允许在聊天室中发送消息。在这里，您可以修改您的 `App`，以使用您在[创建消息输入](chat-sdk-js-tutorial-chat-rooms.md#chat-js-rooms-message-input)（在本教程的第 1 部分）中创建的组件发送消息请求。

首先，使用 `useState` 钩子定义一个名为 `isSending` 的新布尔属性。使用这个新属性通过 `isSendDisabled` 常量切换 `button` HTML 元素的禁用状态。在 `SendButton` 的事件处理程序中，清除 `messageToSend` 的值并将 `isSending` 设置为 true。

*由于您将通过此按钮进行 API 调用，因此添加 `isSending` 布尔值有助于防止同时发生多个 API 调用，方法是在请求完成之前禁用 `SendButton` 上的用户交互。*

```
// App.jsx / App.tsx

// ...

const [isSending, setIsSending] = useState(false);

// ...

const onMessageSend = () => {
  setIsSending(true);
  setMessageToSend('');
};

// ...

const isSendDisabled = connectionState !== 'connected' || isSending;

// ...
```

通过创建一个新的 `SendMessageRequest` 实例并将消息内容传递给构造函数来准备请求。设置 `isSending` 和 `messageToSend` 状态后，调用 `sendMessage` 方法，将该请求发送到聊天室。最后，在收到确认或拒绝该请求的响应后清除 `isSending` 标志。

------
#### [ TypeScript ]

```
// App.tsx

// ...
import { ChatMessage, ChatRoom, ConnectionState, SendMessageRequest } from 'amazon-ivs-chat-messaging'
// ...

const onMessageSend = async () => {
  const request = new SendMessageRequest(messageToSend);
  setIsSending(true);
  setMessageToSend('');

  try {
    const response = await room.sendMessage(request);
  } catch (e) {
    console.log(e);
    // handle the chat error here...
  } finally {
    setIsSending(false);
  }
};

// ...
```

------
#### [ JavaScript ]

```
// App.jsx

// ...
import { ChatRoom, SendMessageRequest } from 'amazon-ivs-chat-messaging'
// ...

const onMessageSend = async () => {
  const request = new SendMessageRequest(messageToSend);
  setIsSending(true);
  setMessageToSend('');

  try {
    const response = await room.sendMessage(request);
  } catch (e) {
    console.log(e);
    // handle the chat error here...
  } finally {
    setIsSending(false);
  }
};

// ...
```

------

试运行 Chatterbox：尝试发送一条消息，方法是用 `MessageInput` 编写一条消息，然后点击 `SendButton`。您应该会看到您发送的消息在之前创建的 `MessageList` 中呈现。

### 删除消息
<a name="chat-js-room-actions-deleting-message"></a>

要从聊天室删除消息，您需要具有适当的权限。权限是在聊天令牌（向聊天室进行身份验证时需要使用该令牌）的初始化期间授予的。就本节而言，[设置本地身份验证/授权服务器](chat-sdk-js-tutorial-chat-rooms.md#chat-js-rooms-auth-server)（在本教程的第 1 部分）中的 `ServerApp` 允许您指定主持人权限。这是在您的应用程序中使用您在[构建令牌提供程序](chat-sdk-js-tutorial-chat-rooms.md#chat-js-rooms-token-provider)（也在第 1 部分）中创建的 `tokenProvider` 对象完成的。

在这里，您可以通过添加删除消息的函数来修改您的 `Message`。

首先，打开 `App.tsx` 并添加 `DELETE_MESSAGE` 权限。（`capabilities` 是 `tokenProvider` 函数的第二个参数。）

注意：`ServerApp` 通过这种方式通知 IVS Chat API，与生成的聊天令牌相关联的用户可以删除聊天室中的消息。在实际情况中，您可能需要更复杂的后端逻辑来管理服务器应用程序基础架构中的用户权限。

------
#### [ TypeScript ]

```
// App.tsx

// ...

const [room] = useState( () =>
    new ChatRoom({
      regionOrUrl: process.env.REGION as string,
      tokenProvider: () => tokenProvider(userId, ['SEND_MESSAGE', 'DELETE_MESSAGE']),
    }),
);

// ...
```

------
#### [ JavaScript ]

```
// App.jsx

// ...

const [room] = useState( () =>
  new ChatRoom({
    regionOrUrl: process.env.REGION,
    tokenProvider: () => tokenProvider(userId, ['SEND_MESSAGE', 'DELETE_MESSAGE']),
  }),
);

// ...
```

------

在接下来的步骤中，您将更新 `Message` 以显示删除按钮。

打开 `Message` 并使用初始值为 `false` 的 `useState` 钩子定义一个名为 `isDeleting` 的新布尔状态。使用此状态，将 `Button` 的内容更新为因 `isDeleting` 的当前状态而异。当 `isDeleting` 为 true 时禁用您的按钮；这样可以防止您尝试同时发出两个删除消息请求。

------
#### [ TypeScript ]

```
// Message.tsx

import React, { useState } from 'react';
import { ChatMessage } from 'amazon-ivs-chat-messaging';
import { useUserContext } from './UserContext';

type Props = {
  message: ChatMessage;
}

export const Message = ({ message }: Props) => {
  const { userId } = useUserContext();
  const [isDeleting, setIsDeleting] = useState(false);

  const isMine = message.sender.userId === userId;

  return (
    <div style={{ backgroundColor: isMine ? 'lightblue' : 'silver', padding: 6, borderRadius: 10, margin: 10 }}>
      <p>{message.content}</p>
      <button disabled={isDeleting}>Delete</button>
    </div>
  );
};
```

------
#### [ JavaScript ]

```
// Message.jsx

import React from 'react';
import { useUserContext } from './UserContext';

export const Message = ({ message }) => {
  const { userId } = useUserContext();
  const [isDeleting, setIsDeleting] = useState(false);

  return (
    <div style={{ backgroundColor: isMine ? 'lightblue' : 'silver', padding: 6, borderRadius: 10, margin: 10 }}>
      <p>{message.content}</p>
      <button disabled={isDeleting}>Delete</button>
    </div>
  );
};
```

------

定义一个名为 `onDelete` 的新函数，该函数接受字符串作为其参数之一并返回 `Promise`。在 `Button` 操作闭包的正文中，使用 `setIsDeleting` 在调用 `onDelete` 之前和之后切换 `isDeleting` 布尔值。对于字符串参数，传入您的组件消息 ID。

------
#### [ TypeScript ]

```
// Message.tsx

import React, { useState } from 'react';
import { ChatMessage } from 'amazon-ivs-chat-messaging';
import { useUserContext } from './UserContext';

export type Props = {
  message: ChatMessage;
  onDelete(id: string): Promise<void>;
};

export const Message = ({ message onDelete }: Props) => {
  const { userId } = useUserContext();
  const [isDeleting, setIsDeleting] = useState(false);
  const isMine = message.sender.userId === userId;
  const handleDelete = async () => {
    setIsDeleting(true);
    try {
      await onDelete(message.id);
    } catch (e) {
      console.log(e);
      // handle chat error here...
    } finally {
      setIsDeleting(false);
    }
  };

  return (
    <div style={{ backgroundColor: isMine ? 'lightblue' : 'silver', padding: 6, borderRadius: 10, margin: 10 }}>
      <p>{content}</p>
      <button onClick={handleDelete} disabled={isDeleting}>
        Delete
      </button>
    </div>
  );
};
```

------
#### [ JavaScript ]

```
// Message.jsx

import React, { useState } from 'react';
import { useUserContext } from './UserContext';

export const Message = ({ message, onDelete }) => {
  const { userId } = useUserContext();
  const [isDeleting, setIsDeleting] = useState(false);
  const isMine = message.sender.userId === userId;
  const handleDelete = async () => {
    setIsDeleting(true);
    try {
      await onDelete(message.id);
    } catch (e) {
      console.log(e);
      // handle the exceptions here...
    } finally {
      setIsDeleting(false);
    }
  };

  return (
    <div style={{ backgroundColor: 'silver', padding: 6, borderRadius: 10, margin: 10 }}>
      <p>{message.content}</p>
      <button onClick={handleDelete} disabled={isDeleting}>
        Delete
      </button>
    </div>
  );
};
```

------

接下来，更新 `MessageList` 以反映对 `Message` 组件的最新更改。

打开 `MessageList` 并定义一个名为 `onDelete` 的新函数，该函数接受字符串作为参数并返回 `Promise`。更新您的 `Message` 并将其传递给 `Message` 的属性。新闭包中的字符串参数将是您要删除的消息的 ID，该消息是从您的 `Message` 中传递的。

------
#### [ TypeScript ]

```
// MessageList.tsx

import * as React from 'react';
import { ChatMessage } from 'amazon-ivs-chat-messaging';
import { Message } from './Message';

interface Props {
  messages: ChatMessage[];
  onDelete(id: string): Promise<void>;
}

export const MessageList = ({ messages, onDelete }: Props) => {
  return (
    <>
      {messages.map((message) => (
        <Message key={message.id} onDelete={onDelete} content={message.content} id={message.id} />
      ))}
    </>
  );
};
```

------
#### [ JavaScript ]

```
// MessageList.jsx

import * as React from 'react';
import { Message } from './Message';

export const MessageList = ({ messages, onDelete }) => {
  return (
    <>
      {messages.map((message) => (
        <Message key={message.id} onDelete={onDelete} content={message.content} id={message.id} />
      ))}
    </>
  );
};
```

------

接下来，更新 `App` 以反映对 `MessageList` 的最新更改。

在 `App` 中，定义一个名为 `onDeleteMessage` 的函数，并将其传递给 `MessageList onDelete` 属性：

------
#### [ TypeScript ]

```
// App.tsx

// ...

const onDeleteMessage = async (id: string) => {};

return (
  <div style={{ display: 'flex', flexDirection: 'column', padding: 10 }}>
    <h4>Connection State: {connectionState}</h4>
    <MessageList onDelete={onDeleteMessage} messages={messages} />
    <div style={{ flexDirection: 'row', display: 'flex', width: '100%' }}>
      <MessageInput value={messageToSend} onMessageChange={setMessageToSend} />
      <SendButton disabled={isSendDisabled} onSendPress={onMessageSend} />
    </div>
  </div>
);

// ...
```

------
#### [ JavaScript ]

```
// App.jsx

// ...

const onDeleteMessage = async (id) => {};

return (
  <div style={{ display: 'flex', flexDirection: 'column', padding: 10 }}>
    <h4>Connection State: {connectionState}</h4>
    <MessageList onDelete={onDeleteMessage} messages={messages} />
    <div style={{ flexDirection: 'row', display: 'flex', width: '100%' }}>
      <MessageInput value={messageToSend} onMessageChange={setMessageToSend} />
      <SendButton disabled={isSendDisabled} onSendPress={onMessageSend} />
    </div>
  </div>
);

// ...
```

------

通过创建一个新的 `DeleteMessageRequest` 实例并将相关的消息 ID 传递给构造函数参数来准备请求，然后调用接受上述准备好的请求的 `deleteMessage`：

------
#### [ TypeScript ]

```
// App.tsx

// ...

const onDeleteMessage = async (id: string) => {
  const request = new DeleteMessageRequest(id);
  await room.deleteMessage(request);
};

// ...
```

------
#### [ JavaScript ]

```
// App.jsx

// ...

const onDeleteMessage = async (id) => {
  const request = new DeleteMessageRequest(id);
  await room.deleteMessage(request);
};

// ...
```

------

接下来，更新 `messages` 状态以反映新的消息列表，该列表忽略了您刚刚删除的消息。

在 `useEffect` 钩子中，侦听 `messageDelete` 事件，并通过删除 ID 与 `message` 参数相匹配的消息来更新 `messages` 状态数组。

注意：当消息被当前用户或聊天室中的任何其他用户删除时，可能会引发 `messageDelete` 事件。如果在事件处理程序中（而不是 `deleteMessage` 请求旁边）处理该事件，您可以统一处理删除消息。

```
// App.jsx / App.tsx

// ...

const unsubscribeOnMessageDeleted = room.addListener('messageDelete', (deleteMessageEvent) => {
  setMessages((prev) => prev.filter((message) => message.id !== deleteMessageEvent.id));
});

return () => {
  // ...

  unsubscribeOnMessageDeleted();
};

// ...
```

现在，您可以从聊天应用程序的聊天室中删除用户。

## 后续步骤
<a name="chat-js-messages-events-next-steps"></a>

作为实验，尝试在聊天室中执行其他操作，例如断开其他用户的连接。