

# IVS chat 客户端消息收发 SDK
<a name="chat-sdk"></a>

Amazon Interactive Video Services（IVS）Chat 客户端消息收发 SDK 适用于使用 Amazon IVS 构建应用程序的开发人员。此 SDK 旨在利用 Amazon IVS 架构，并将与 Amazon IVS Chat 一起查看更新。作为本机 SDK，它旨在最大限度地减少对应用程序以及用户有权访问应用程序所在设备的性能影响。

## 平台要求
<a name="chat-sdk-platform-requirements"></a>

### 桌面浏览器
<a name="chat-desktop-browsers"></a>


| 浏览器 | 受支持的版本 | 
| --- | --- | 
| Chrome | 两个主要版本（当前版本和最新版本） | 
| 边缘 | 两个主要版本（当前版本和最新版本） | 
| Firefox | 两个主要版本（当前版本和最新版本） | 
| Opera | 两个主要版本（当前版本和最新版本） | 
| Safari | 两个主要版本（当前版本和最新版本） | 

### 移动浏览器
<a name="chat-mobile-browsers"></a>


| 浏览器 | 受支持的版本 | 
| --- | --- | 
| Android 版 Chrome | 两个主要版本（当前版本和最新版本） | 
| 适用于 Android 的 Firefox | 两个主要版本（当前版本和最新版本） | 
| 适用于 Android 的 Opera | 两个主要版本（当前版本和最新版本） | 
| 适用于 Android 的 WebView | 两个主要版本（当前版本和最新版本） | 
| Samsung Internet | 两个主要版本（当前版本和最新版本） | 
| 适用于 iOS 的 Safari 浏览器 | 两个主要版本（当前版本和最新版本） | 

### 本机平台
<a name="chat-native-platforms"></a>


| 平台 | 受支持的版本 | 
| --- | --- | 
| Android | 5.0 和更高版本 | 
| iOS |  13.0 和更高版本  | 

## 支持
<a name="chat-sdk-support"></a>

如果聊天室出现错误或其他问题，请通过 IVS Chat API 确定唯一的聊天室标识符（请参阅 [ListRooms](https://docs.aws.amazon.com//ivs/latest/ChatAPIReference/API_ListRooms.html)）。

与 Amazon Support 共享此聊天室标识符。利用它可获得有助于解决问题的信息。

**注意：**请参阅 [Amazon IVS 聊天功能发布说明](release-notes.md)了解可用版本和已修复问题。如果合适，请在联系支持部门之前更新您的 SDK 版本，看看这是否解决了您的问题。

### 版本控制
<a name="chat-sdk-support-versioning"></a>

Amazon IVS Chat 客户端消息收发 SDK 使用[语义版本控制](https://semver.org/)。

在此讨论中，假设：
+ 最新版本是 4.1.3。
+ 先前主要版本的最新版本为 3.2.4。
+ 版本 1.x 最新版本是 1.5.6。

最新版本的次要版本已添加向后兼容的新功能。在本例中，版本 4.2.0 已添加新功能。

最新版本的补丁版本已添加向后兼容、次要错误修复。在这里，版本 4.1.4 已添加次要错误修复。

向后兼容、主要错误修复处理方式不同；将在以下几个版本中添加：
+ 最新版本补丁版本。在本例中是版本 4.1.4。
+ 先前次要版本的补丁版本。在本例中是版本 3.2.5。
+ 最新版本 1.x 版本的补丁版本。在本例中是版本 1.5.7。

主要错误修复由 Amazon IVS 产品团队定义。典型示例包括关键安全更新和客户所需的其他选定修复。

**注意：**在上面的例子中，发布的版本递增但不会跳过任何数字（例如，从 4.1.3 到 4.1.4）。实际上，一个或多个补丁编号可能保留在内部而不发布，因此发布版本可以从 4.1.3 增加到 4.1.6。

此外，版本 1.x 将一直受支持，直到 2023 年底或 3.x 发布时，以较迟者为准。

## Amazon IVS Chat API
<a name="chat-sdk-chat-apis"></a>

在服务器端（并非由 SDK 托管），存在两个 API，每个 API 都有自己的职责：
+ **数据面板** - [IVS Chat 消息收发 API](https://docs.aws.amazon.com//ivs/latest/chatmsgapireference/welcome.html) 是一个 WebSocket API，旨在供由基于令牌的身份验证方案提供支持的前端应用程序（iOS、Android、macOS 等）使用。通过之前生成的聊天令牌，您可以使用此 API 连接到现有的聊天室。

  *Amazon IVS Chat 客户端消息收发 SDK 仅与数据面板有关。SDK 假设您已经通过后端生成聊天令牌。假设这些令牌的检索由前端应用程序而不是 SDK 进行托管。*
+ **控制面板** - [IVS Chat 控制面板 API](https://docs.aws.amazon.com//ivs/latest/ChatAPIReference/Welcome.html) 为您自己的*后端应用程序*提供了一个界面，用于管理和创建聊天室以及加入聊天室的用户。您可以将此界面视为应用程序聊天功能（由*您自己的后端*进行托管）的管理面板。有些控制面板操作负责创建数据面板对聊天室进行身份验证所需的*聊天令牌*。

  **重要提示：***IVS 聊天功能客户端消息收发 SDK 不调用任何控制面板操作。必须设置后端才能创建聊天令牌。前端应用程序必须与后端通信才能检索此聊天令牌。*

# IVS 聊天功能客户端消息收发 SDK：Android 指南
<a name="chat-sdk-android"></a>

Amazon Interactive Video（IVS）Chat 客户端消息收发 Android SDK 提供界面，可让您在使用 Android 的平台上轻松整合我们的 [IVS Chat 消息收发 API](https://docs.aws.amazon.com//ivs/latest/chatmsgapireference/welcome.html)。

`com.amazonaws:ivs-chat-messaging` 软件包实现了本文档中所描述的接口。

**IVS 聊天功能客户端消息收发 Android SDK 的最新版本：**1.1.0（[发布说明](https://docs.aws.amazon.com//ivs/latest/ChatUserGuide/release-notes.html#jan31-23)）

**参考文档：**有关 Amazon IVS Chat 客户端消息收发 Android SDK 中最重要方法的信息，请参阅参考文档，网址为 [https://aws.github.io/amazon-ivs-chat-messaging-sdk-android/1.1.0/](https://aws.github.io/amazon-ivs-chat-messaging-sdk-android/1.1.0/)。

**示例代码：**请参阅 GitHub 上的 Android 示例存储库：[https://github.com/aws-samples/amazon-ivs-chat-for-android-demo](https://github.com/aws-samples/amazon-ivs-chat-for-android-demo)

**平台要求：**开发需要 Android 5.0（API 级别 21）或更高版本。

# IVS 聊天功能客户端消息收发 Android SDK 入门
<a name="chat-android-getting-started"></a>

在开始之前，应该熟悉 [Amazon IVS Chat 入门](getting-started-chat.md)。

## 添加程序包
<a name="chat-android-add-package"></a>

将 `com.amazonaws:ivs-chat-messaging` 添加到 `build.gradle` 依赖项：

```
dependencies {
   implementation 'com.amazonaws:ivs-chat-messaging'
}
```

## 添加 Proguard 规则
<a name="chat-android-proguard-rules"></a>

将以下条目添加到 R8/Proguard 规则文件 (`proguard-rules.pro`)：

```
-keep public class com.amazonaws.ivs.chat.messaging.** { *; }
-keep public interface com.amazonaws.ivs.chat.messaging.** { *; }
```

## 设置您的后端
<a name="chat-android-setup-backend"></a>

此集成需要服务器上的端点与 [Amazon IVS API](https://docs.aws.amazon.com//ivs/latest/LowLatencyAPIReference/Welcome.html) 通信。使用[官方亚马逊云科技库](https://aws.amazon.com/developer/tools/)从服务器访问 Amazon IVS API。这些库可以从 node.js 和 Java 等公共程序包以多种语言进行访问。

接下来，创建一个用于与 [Amazon IVS Chat API](https://docs.aws.amazon.com//ivs/latest/ChatAPIReference/Welcome.html) 通信的服务器端点，然后创建令牌。

## 设置服务器连接
<a name="chat-android-setup-server"></a>

创建一个将 `ChatTokenCallback` 作为参数的方法，然后从后端获取聊天令牌。将该令牌传递给回调的 `onSuccess` 方法。如果发生错误，将异常传递给回调的 `onError` 方法。在下一步中实例化主 `ChatRoom` 实体时需要执行此操作。

您可以在下面找到使用 `Retrofit` 调用实现上述操作的示例代码。

```
// ...

private fun fetchChatToken(callback: ChatTokenCallback) {
    apiService.createChatToken(userId, roomId).enqueue(object : Callback<ChatToken> {
        override fun onResponse(call: Call<ExampleResponse>, response: Response<ExampleResponse>) {
            val body = response.body()
            val token = ChatToken(
                body.token,
                body.sessionExpirationTime,
                body.tokenExpirationTime
            )
            callback.onSuccess(token)
        }

        override fun onFailure(call: Call<ChatToken>, throwable: Throwable) {
            callback.onError(throwable)
        }
    })
}
// ...
```

# 使用 IVS 聊天功能客户端消息收发 Android SDK
<a name="chat-android-using-sdk"></a>

本文档将引导您完成使用 Amazon IVS 聊天功能客户端消息收发 Android SDK 涉及的步骤。

## 初始化聊天室实例
<a name="chat-android-initialize-room"></a>

创建 `ChatRoom` 类的实例。此操作需要传递 `regionOrUrl`（通常是托管聊天室的 AWS 区域）和 `tokenProvider`（上一步中创建的令牌获取方法）。

```
val room = ChatRoom(
    regionOrUrl = "us-west-2",
    tokenProvider = ::fetchChatToken
)
```

接下来，创建一个侦听器对象，该对象将实现聊天相关事件的处理程序，然后将其分配给 `room.listener` 属性：

```
private val roomListener = object : ChatRoomListener {
    override fun onConnecting(room: ChatRoom) {
      // Called when room is establishing the initial connection or reestablishing connection after socket failure/token expiration/etc
    }

    override fun onConnected(room: ChatRoom) {
        // Called when connection has been established
    }

    override fun onDisconnected(room: ChatRoom, reason: DisconnectReason) {
        // Called when a room has been disconnected
    }

    override fun onMessageReceived(room: ChatRoom, message: ChatMessage) {
        // Called when chat message has been received
    }

    override fun onEventReceived(room: ChatRoom, event: ChatEvent) {
        // Called when chat event has been received
    }

    override fun onDeleteMessage(room: ChatRoom, event: DeleteMessageEvent) {
       // Called when DELETE_MESSAGE event has been received
    }
}

val room = ChatRoom(
    region = "us-west-2",
    tokenProvider = ::fetchChatToken
)

room.listener = roomListener // <- add this line

// ...
```

基本初始化的最后一步是通过建立 WebSocket 连接来连接到特定聊天室。为此，请在聊天室实例中调用 `connect()` 方法。我们建议在 `onResume() `lifecycle 方法中执行此操作，以确保在应用程序从后台恢复时保持连接状态。

```
room.connect()
```

SDK 将尝试与聊天室建立连接，该聊天室是使用从服务器收到的聊天令牌进行编码的。如果失败，它将尝试重新连接聊天室实例中指定的次数。

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

`ChatRoom` 类包含发送和删除消息以及与其他用户断开连接的操作。这些操作接受可选的回调参数，该参数允许您获取请求确认或拒绝通知。

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

对于此请求，您必须使用聊天令牌对 `SEND_MESSAGE` 功能进行编码。

触发发送消息请求：

```
val request = SendMessageRequest("Test Echo")
room.sendMessage(request)
```

要获得请求的确认/拒绝，请提供回调作为第二个参数：

```
room.sendMessage(request, object : SendMessageCallback {
   override fun onConfirmed(request: SendMessageRequest, response: ChatMessage) {
      // Message was successfully sent to the chat room.
   }
   override fun onRejected(request: SendMessageRequest, error: ChatError) {
      // Send-message request was rejected. Inspect the `error` parameter for details.
   }
})
```

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

对于此请求，您必须在聊天令牌中编码 DELETE\$1MESSAGE 功能。

触发删除消息请求：

```
val request = DeleteMessageRequest(messageId, "Some delete reason")
room.deleteMessage(request)
```

要获得请求的确认/拒绝，请提供回调作为第二个参数：

```
room.deleteMessage(request, object : DeleteMessageCallback {
   override fun onConfirmed(request: DeleteMessageRequest, response: DeleteMessageEvent) {
      // Message was successfully deleted from the chat room.
   }
   override fun onRejected(request: DeleteMessageRequest, error: ChatError) {
      // Delete-message request was rejected. Inspect the `error` parameter for details.
   }
})
```

### 断开与其他用户的连接
<a name="chat-android-room-actions-disconnect-user"></a>

对于此请求，您必须使用聊天令牌对 `DISCONNECT_USER` 功能进行编码。

出于审核目的与其他用户断开连接：

```
val request = DisconnectUserRequest(userId, "Reason for disconnecting user")
room.disconnectUser(request)
```

要获得请求的确认/拒绝，请提供回调作为第二个参数：

```
room.disconnectUser(request, object : DisconnectUserCallback {
   override fun onConfirmed(request: SendMessageRequest, response: ChatMessage) {
      // User was disconnected from the chat room.
   }
   override fun onRejected(request: SendMessageRequest, error: ChatError) {
      // Disconnect-user request was rejected. Inspect the `error` parameter for details.
   }
})
```

## 断开与聊天室的连接
<a name="chat-android-disconnect-room"></a>

要关闭与聊天室的连接，请对聊天室实例调用 `disconnect()` 方法：

```
room.disconnect()
```

由于当应用程序处于后台状态时，WebSocket 连接将在短时间后停止工作，因此我们建议您在从后台状态转换为其他状态/转换为后台状态时手动连接/断开连接。为此，请将 `onResume()` 生命周期方法中有关 Android `Activity` 或 `Fragment` 的 `room.connect()` 调用与 `onPause()` 生命周期方法中的 `room.disconnect()` 调用相匹配。

# IVS 聊天功能客户端消息收发 SDK：Android 教程第 1 部分：聊天室
<a name="chat-sdk-android-tutorial-chat-rooms"></a>

这是一个由两部分组成的教程的第 1 部分。通过使用 [Kotlin](https://kotlinlang.org/) 编程语言构建功能齐全的 Android 应用程序，您将了解使用 Amazon IVS 聊天功能消息收发 SDK 的基本知识。我们把这个应用程序称为 *Chatterbox*。

在开始本模块之前，请花几分钟时间熟悉先决条件、聊天令牌背后的关键概念以及创建聊天室所需的后端服务器。

这些教程是为刚接触 IVS 聊天功能消息收发 SDK 但经验丰富的 Android 开发人员创建的。您需要熟悉 Kotlin 编程语言并在 Android 平台上创建 UI。

本教程的第 1 部分分为几个章节：

1. [设置本地身份验证/授权服务器](#chat-android-rooms-auth-server)

1. [创建 Chatterbox 项目](#chat-android-rooms-chatterbox)

1. [连接到聊天室和观察连接更新](#chat-android-rooms-connect-state)

1. [构建令牌提供程序](#chat-android-rooms-token-provider)

1. [后续步骤](#chat-android-rooms-next-steps)

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

## 先决条件
<a name="chat-android-rooms-prerequisites"></a>
+ 熟悉 Kotlin 并在 Android 平台上创建应用程序。如果您对创建 Android 应用程序不熟悉，请学习面向 Android 开发者的[构建您的第一个应用程序](https://developer.android.com/codelabs/basic-android-kotlin-compose-first-app#0)指南中的基础知识。
+ 仔细阅读并理解 [Amazon IVS Chat 入门](getting-started-chat.md)。
+ 使用现有的 IAM policy 中定义的 `CreateChatToken` 和 `CreateRoom` 功能创建 AWS IAM 用户。（请参见 [Amazon IVS Chat 入门](getting-started-chat.md)。）
+ 确保该用户的私有密钥/访问密钥存储在 Amazon 凭证文件中。有关说明，请参阅 [Amazon CLI 用户指南](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-welcome.html)（尤其是[配置和凭证文件设置](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html)部分）。
+ 创建聊天室并保存其 ARN。请参阅[Amazon IVS Chat 入门](getting-started-chat.md)。（如果不保存 ARN，您可以稍后使用控制台或 Chat API 查找 ARN。）

## 设置本地身份验证/授权服务器
<a name="chat-android-rooms-auth-server"></a>

您的后端服务器负责创建聊天室和生成 IVS 聊天功能 Android SDK 所需的聊天令牌，以便在您的客户端连接聊天室时进行身份验证和授权。

请参阅*《Amazon IVS Chat 入门》*中的[创建聊天令牌](getting-started-chat-auth.md)。如其中的流程图所示，您的服务器端代码负责创建聊天令牌。这意味着您的应用程序必须通过向服务器端应用程序请求聊天令牌，来提供自己生成聊天令牌的方法。

我们使用 [Ktor](https://ktor.io/) 框架创建一个实时本地服务器，该服务器使用您的本地 Amazon 环境管理聊天令牌的创建。

此时，我们希望您已正确设置 AWS 凭证。有关分步说明，请参阅[设置用于开发的 AWS 凭证和区域](https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/setup-credentials.html)。

创建一个新目录并命名为 `chatterbox`，在其中再创建一个，然后命名为 `auth-server`。

我们的服务器文件夹将具有如下结构：

```
- auth-server
  - src
    - main
      - kotlin
        - com
          - chatterbox
            - authserver
              - Application.kt
       - resources
         - application.conf
         - logback.xml
   - build.gradle.kts
```

*注意：您可以直接将此处的代码复制/粘贴到引用文件中。*

接下来，我们添加所有必要的依赖项和插件，以便身份验证服务器正常工作：

**Kotlin 脚本：**

```
// ./auth-server/build.gradle.kts

plugins {
   application
   kotlin("jvm")
   kotlin("plugin.serialization").version("1.7.10")
}

application {
   mainClass.set("io.ktor.server.netty.EngineMain")
}

dependencies {
   implementation("software.amazon.awssdk:ivschat:2.18.1")
   implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.20")

   implementation("io.ktor:ktor-server-core:2.1.3")
   implementation("io.ktor:ktor-server-netty:2.1.3")
   implementation("io.ktor:ktor-server-content-negotiation:2.1.3")
   implementation("io.ktor:ktor-serialization-kotlinx-json:2.1.3")

   implementation("ch.qos.logback:logback-classic:1.4.4")
}
```

现在我们需要为身份验证服务器设置日志记录功能。（有关更多信息，请参阅[配置记录器](https://ktor.io/docs/logging.html#configure-logger)。）

**XML：**

```
// ./auth-server/src/main/resources/logback.xml

<configuration>
   <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
      <encoder>
         <pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
      </encoder>
   </appender>
   <root level="trace">
      <appender-ref ref="STDOUT"/>
   </root>
   <logger name="org.eclipse.jetty" level="INFO"/>
   <logger name="io.netty" level="INFO"/>
</configuration>
```

[Ktor](https://ktor.io/docs/welcome.html) 服务器需要自动从 `resources` 目录的 `application.*` 文件中加载的配置设置，所以我们也添加了这些设置。（有关更多信息，请参阅[文件中的配置](https://ktor.io/docs/configurations.html#configuration-file)。）

**HOCON**：

```
// ./auth-server/src/main/resources/application.conf

ktor {
   deployment {
      port = 3000
   }
   application {
      modules = [ com.chatterbox.authserver.ApplicationKt.main ]
   }
}
```

最后，让我们实施我们的服务器：

**Kotlin**：

```
// ./auth-server/src/main/kotlin/com/chatterbox/authserver/Application.kt

package com.chatterbox.authserver

import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import software.amazon.awssdk.services.ivschat.IvschatClient
import software.amazon.awssdk.services.ivschat.model.CreateChatTokenRequest

@Serializable
data class ChatTokenParams(var userId: String, var roomIdentifier: String)

@Serializable
data class ChatToken(
   val token: String,
   val sessionExpirationTime: String,
   val tokenExpirationTime: String,
)

fun Application.main() {
   install(ContentNegotiation) {
      json(Json)
   }

   routing {
      post("/create_chat_token") {
         val callParameters = call.receive<ChatTokenParams>()
         val request = CreateChatTokenRequest.builder().roomIdentifier(callParameters.roomIdentifier)
            .userId(callParameters.userId).build()
         val token = IvschatClient.create()
            .createChatToken(request)

         call.respond(
            ChatToken(
                token.token(),
                token.sessionExpirationTime().toString(),
                token.tokenExpirationTime().toString()
            )
         )
      }
   }
}
```

## 创建 Chatterbox 项目
<a name="chat-android-rooms-chatterbox"></a>

要创建 Android 项目，请安装并打开 [Android Studio](https://developer.android.com/studio)。

按照 Android 官方在[创建项目指南](https://developer.android.com/studio/projects/create-project)中列出的步骤进行操作。
+ 在[选择您的项目类型](https://developer.android.com/studio/projects/create-project)中，为我们的 Chatterbox 应用程序选择**空活动**项目模板。
+ 在[配置您的项目](https://developer.android.com/studio/projects/create-project#configure)中，为配置字段选择以下值：
  + **名称**：My App
  + **程序包名称**：com.chatterbox.myapp
  + **保存位置**：指向在上一步中创建的 `chatterbox` 目录
  + **语言**：Kotlin
  + **最低 API 级别**：API 21：Android 5.0（Lollipop）

正确指定所有配置参数后，我们在 `chatterbox` 文件夹内的文件结构应如下所示：

```
- app
  - build.gradle
  ...
- gradle
- .gitignore
- build.gradle
- gradle.properties
- gradlew
- gradlew.bat
- local.properties
- settings.gradle
- auth-server
  - src
    - main
      - kotlin
        - com
          - chatterbox
            - authserver
              - Application.kt
       - resources
         - application.conf
         - logback.xml
   - build.gradle.kts
```

现在我们有一个正在运行的 Android 项目，可以将 [com.amazonaws:ivs-chat-messaging](https://mvnrepository.com/artifact/com.amazonaws/ivs-chat-messaging) 添加到 `build.gradle` 依赖项中。（有关 [Gradle](https://gradle.org/) 构建工具包的更多信息，请参阅[配置您的构建](https://developer.android.com/build)。）

**注意**：在每个代码段的顶部，都有一个文件路径，您应该在其中对项目进行更改。路径相对于项目的根目录。

*在下面的代码中，将 `<version>` 替换为聊天功能 Android SDK 的当前版本号（例如 1.0.0）*。

**Kotlin**：

```
// ./app/build.gradle

plugins {
// ...
}

android {
// ...
}

dependencies {
   implementation("com.amazonaws:ivs-chat-messaging:<version>")
// ...
}
```

添加新依赖项后，在 Android Studio 中运行**将项目与 Gradle 文件同步**，将项目与新依赖项同步。（有关更多信息，请参阅[添加构建依赖项](https://developer.android.com/build/dependencies)。）

为了方便从项目根目录运行我们的身份验证服务器（在上一部分中创建），我们将其作为新模块包含在 `settings.gradle` 中。（有关更多信息，请参阅[使用 Gradle 构建软件组件](https://docs.gradle.org/current/userguide/multi_project_builds.html)。）

**Kotlin 脚本：**

```
// ./settings.gradle

// ...

rootProject.name = "Chatterbox"
include ':app'
include ':auth-server'
```

从现在起，由于 `auth-server` 已包含在 Android 项目中，您可以从项目的根目录使用以下命令运行身份验证服务器：

**Shell：**

```
./gradlew :auth-server:run
```

## 连接到聊天室和观察连接更新
<a name="chat-android-rooms-connect-state"></a>

为了打开聊天室连接，我们使用了 [onCreate() 活动生命周期回调](https://developer.android.com/guide/components/activities/activity-lifecycle)，该回调在首次创建活动时触发。[ChatRoom 构造函数](https://aws.github.io/amazon-ivs-chat-messaging-sdk-android/1.0.0/-amazon%20-i-v-s%20-chat%20-messaging%20-s-d-k%20for%20-android/com.amazonaws.ivs.chat.messaging/-chat-room/index.html)要求我们提供 `region` 和 `tokenProvider` 以实例化聊天室连接。

**注意：**以下代码段中的 `fetchChatToken` 功能将在[下一部分](#chat-android-rooms-token-provider)中实现。

**Kotlin：**

```
// ./app/src/main/java/com/chatterbox/myapp/MainActivity.kt

package com.chatterbox.myapp

// ...
import androidx.appcompat.app.AppCompatActivity
// ...

// AWS region of the room that was created in Getting Started with Amazon IVS Chat
const val REGION = "us-west-2"

class MainActivity : AppCompatActivity() {
    private var room: ChatRoom? = null
    // ...

   override fun onCreate(savedInstanceState: Bundle?) {
      super.onCreate(savedInstanceState)
      setContentView(R.layout.activity_main)

      // Create room instance
      room = ChatRoom(REGION, ::fetchChatToken)
   }

// ...
}
```

显示聊天室连接的变化并对其做出反应是制作 `chatterbox` 等聊天应用程序的重要组成部分。在我们开始与聊天室互动之前，必须订阅聊天室连接状态事件以获取更新。

[ChatRoom](https://aws.github.io/amazon-ivs-chat-messaging-sdk-android/1.0.0/-amazon%20-i-v-s%20-chat%20-messaging%20-s-d-k%20for%20-android/com.amazonaws.ivs.chat.messaging/-chat-room/index.html) 希望我们附加 [ChatroomListener 接口](https://aws.github.io/amazon-ivs-chat-messaging-sdk-android/1.0.0/-amazon%20-i-v-s%20-chat%20-messaging%20-s-d-k%20for%20-android/com.amazonaws.ivs.chat.messaging/-chat-room/listener.html)实现来引发生命周期事件。目前，侦听器函数在调用时只会记录确认消息：

**Kotlin**：

```
// ./app/src/main/java/com/chatterbox/myapp/MainActivity.kt

// ...
package com.chatterbox.myapp
// ...
const val TAG = "IVSChat-App"

class MainActivity : AppCompatActivity() {
// ...

    private val roomListener = object : ChatRoomListener {
        override fun onConnecting(room: ChatRoom) {
            Log.d(TAG, "onConnecting")
        }

        override fun onConnected(room: ChatRoom) {
            Log.d(TAG, "onConnected")
        }

        override fun onDisconnected(room: ChatRoom, reason: DisconnectReason) {
            Log.d(TAG, "onDisconnected $reason")
        }

        override fun onMessageReceived(room: ChatRoom, message: ChatMessage) {
            Log.d(TAG, "onMessageReceived $message")
        }

        override fun onMessageDeleted(room: ChatRoom, event: DeleteMessageEvent) {
            Log.d(TAG, "onMessageDeleted $event")
        }

        override fun onEventReceived(room: ChatRoom, event: ChatEvent) {
            Log.d(TAG, "onEventReceived $event")
        }

        override fun onUserDisconnected(room: ChatRoom, event:    DisconnectUserEvent) {
            Log.d(TAG, "onUserDisconnected $event")
        }
    }
}
```

现在我们已经实现 `ChatRoomListener`，接下来将其附加到聊天室实例上：

**Kotlin**：

```
// ./app/src/main/java/com/chatterbox/myapp/MainActivity.kt

package com.chatterbox.myapp
// ...

override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   binding = ActivityMainBinding.inflate(layoutInflater)
   setContentView(binding.root)

   // Create room instance
   room = ChatRoom(REGION, ::fetchChatToken).apply {
      listener = roomListener
   }
}

private val roomListener = object : ChatRoomListener {
// ...
}
```

此后，我们需要提供读取聊天室连接状态的功能。我们会将其保留在 `MainActivity.kt` [属性](https://kotlinlang.org/docs/properties.html)中并将其初始化为聊天室连接默认断开状态（参阅 [IVS 聊天功能 Android SDK 参考](https://aws.github.io/amazon-ivs-chat-messaging-sdk-android/latest/)中的 `ChatRoom state`）。为了保持最新的本地状态，我们需要实现一个状态更新器函数，我们称之为 `updateConnectionState`：

**Kotlin：**

```
// ./app/src/main/java/com/chatterbox/myapp/MainActivity.kt

package com.chatterbox.myapp
// ...

enum class ConnectionState {
   CONNECTED,
   DISCONNECTED,
   LOADING
}

class MainActivity : AppCompatActivity() {
   private var connectionState = ConnectionState.DISCONNECTED
// ...

   private fun updateConnectionState(state: ConnectionState) {
      connectionState = state

      when (state) {
         ConnectionState.CONNECTED -> {
            Log.d(TAG, "room connected")
         }
         ConnectionState.DISCONNECTED -> {
            Log.d(TAG, "room disconnected")
         }
         ConnectionState.LOADING -> {
            Log.d(TAG, "room loading")
         }
      }
   }
}
```

接下来，我们将状态更新器函数与 [ChatRoom.listener](https://aws.github.io/amazon-ivs-chat-messaging-sdk-android/1.0.0/-amazon%20-i-v-s%20-chat%20-messaging%20-s-d-k%20for%20-android/com.amazonaws.ivs.chat.messaging/-chat-room/listener.html) 属性集成：

**Kotlin：**

```
// ./app/src/main/java/com/chatterbox/myapp/MainActivity.kt

package com.chatterbox.myapp
// ...

class MainActivity : AppCompatActivity() {
// ...

   private val roomListener = object : ChatRoomListener {
      override fun onConnecting(room: ChatRoom) {
         Log.d(TAG, "onConnecting")
         runOnUiThread {
            updateConnectionState(ConnectionState.LOADING)
         }
      }

      override fun onConnected(room: ChatRoom) {
         Log.d(TAG, "onConnected")
         runOnUiThread {
            updateConnectionState(ConnectionState.CONNECTED)
         }
      }

      override fun onDisconnected(room: ChatRoom, reason: DisconnectReason) {
         Log.d(TAG, "[${Thread.currentThread().name}] onDisconnected")
         runOnUiThread {
            updateConnectionState(ConnectionState.DISCONNECTED)
         }
      }
   }
}
```

现在我们能够保存、侦听和响应 [ChatRoom](https://aws.github.io/amazon-ivs-chat-messaging-sdk-android/1.0.0/-amazon%20-i-v-s%20-chat%20-messaging%20-s-d-k%20for%20-android/com.amazonaws.ivs.chat.messaging/-chat-room/index.html) 状态更新，接下来可以初始化连接了：

**Kotlin：**

```
// ./app/src/main/java/com/chatterbox/myapp/MainActivity.kt

package com.chatterbox.myapp
// ...

enum class ConnectionState {
   CONNECTED,
   DISCONNECTED,
   LOADING
}

class MainActivity : AppCompatActivity() {
   private var connectionState = ConnectionState.DISCONNECTED
// ...

   private fun connect() {
      try {
         room?.connect()
      } catch (ex: Exception) {
         Log.e(TAG, "Error while calling connect()", ex)
      }
   }

   private val roomListener = object : ChatRoomListener {
      // ...
      override fun onConnecting(room: ChatRoom) {
         Log.d(TAG, "onConnecting")
         runOnUiThread {
            updateConnectionState(ConnectionState.LOADING)
         }
      }

      override fun onConnected(room: ChatRoom) {
         Log.d(TAG, "onConnected")
         runOnUiThread {
            updateConnectionState(ConnectionState.CONNECTED)
         }
      }
      // ...
   }
}
```

## 构建令牌提供程序
<a name="chat-android-rooms-token-provider"></a>

现在可以创建一个函数，负责在我们的应用程序中创建和管理聊天令牌。在此示例中，我们使用 [Android 版 Retrofit HTTP 客户端](https://square.github.io/retrofit/)。

在发送任何网络流量之前，我们必须设置适用于 Android 的网络安全配置。（有关更多信息，请参阅[网络安全配置](https://developer.android.com/privacy-and-security/security-config)。） 我们首先向[应用程序清单](https://developer.android.com/guide/topics/manifest/manifest-intro)文件添加网络权限。请注意，添加的 `user-permission` 标签和 `networkSecurityConfig` 属性将指向我们的新网络安全配置。*在下面的代码中，将 `<version>` 替换为聊天功能 Android SDK 的当前版本号（例如 1.0.0）*。

**XML**：

```
// ./app/src/main/AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.chatterbox.myapp">
    <uses-permission android:name="android.permission.INTERNET" />
    <application
        android:allowBackup="true"
        android:fullBackupContent="@xml/backup_rules"
        android:label="@string/app_name"
        android:networkSecurityConfig="@xml/network_security_config"
// ...

// ./app/build.gradle


dependencies {
   implementation("com.amazonaws:ivs-chat-messaging:<version>")
// ...

   implementation("com.squareup.retrofit2:retrofit:2.9.0")
}
```

声明 `10.0.2.2` 和 `localhost` 为可信域，以开始与我们的后端交换消息：

**XML**：

```
// ./app/src/main/res/xml/network_security_config.xml

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <domain-config cleartextTrafficPermitted="true">
        <domain includeSubdomains="true">10.0.2.2</domain>
        <domain includeSubdomains="true">localhost</domain>
    </domain-config>
</network-security-config>
```

接下来，我们需要添加一个新的依赖项以及用于解析 HTTP 响应的 [Gson 转换器](https://github.com/square/retrofit/tree/trunk/retrofit-converters/gson)。*在下面的代码中，将 `<version>` 替换为聊天功能 Android SDK 的当前版本号（例如 1.0.0）*。

**Kotlin 脚本**：

```
// ./app/build.gradle

dependencies {
   implementation("com.amazonaws:ivs-chat-messaging:<version>")
// ...

   implementation("com.squareup.retrofit2:retrofit:2.9.0")
}
```

要检索聊天令牌，我们需要从 `chatterbox` 应用程序发出 POST HTTP 请求。我们在 Retrofit 接口中定义请求，以便实现。（请参阅 [Retrofit 文档](https://square.github.io/retrofit/)，也请熟悉 [CreateChatToken](https://docs.aws.amazon.com//ivs/latest/ChatAPIReference/API_CreateChatToken.html#API_CreateChatToken_RequestBody) 操作规范。）

**Kotlin：**

```
// ./app/src/main/java/com/chatterbox/myapp/network/ApiService.kt

package com.chatterbox.myapp.network
// ...


import androidx.annotation.Keep
import com.amazonaws.ivs.chat.messaging.ChatToken
import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.POST

data class CreateTokenParams(var userId: String, var roomIdentifier: String)

interface ApiService {
   @POST("create_chat_token")
   fun createChatToken(@Body params: CreateTokenParams): Call<ChatToken>
}
```

现在，网络设置完成后，可以添加一个函数，负责创建和管理我们的聊天令牌。我们将其添加到 `MainActivity.kt`，它是在项目[生成](#chat-android-rooms-chatterbox)时自动创建的：

**Kotlin：**

```
// ./app/src/main/java/com/chatterbox/myapp/MainActivity.kt


package com.chatterbox.myapp

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import com.amazonaws.ivs.chat.messaging.*
import com.chatterbox.myapp.network.CreateTokenParams
import com.chatterbox.myapp.network.RetrofitFactory
import retrofit2.Call
import java.io.IOException
import retrofit2.Callback
import retrofit2.Response

// custom tag for logging purposes
const val TAG = "IVSChat-App"

// any ID to be associated with auth token
const val USER_ID = "test user id"
// ID of the room the app wants to access. Must be an ARN. See Amazon Resource Names(ARNs)
const val ROOM_ID = "arn:aws:..."
// AWS region of the room that was created in Getting Started with Amazon IVS Chat
const val REGION = "us-west-2"

class MainActivity : AppCompatActivity() {
   private val service = RetrofitFactory.makeRetrofitService()
   private lateinit var userId: String

   override fun onCreate(savedInstanceState: Bundle?) {
      super.onCreate(savedInstanceState)
      setContentView(R.layout.activity_main)
   }

   private fun fetchChatToken(callback: ChatTokenCallback) {
      val params = CreateTokenParams(userId, ROOM_ID)
      service.createChatToken(params).enqueue(object : Callback<ChatToken> {
         override fun onResponse(call: Call<ChatToken>, response: Response<ChatToken>) {
            val token = response.body()
            if (token == null) {
               Log.e(TAG, "Received empty token response")
               callback.onFailure(IOException("Empty token response"))
               return
            }

            Log.d(TAG, "Received token response $token")
            callback.onSuccess(token)
         }

         override fun onFailure(call: Call<ChatToken>, throwable: Throwable) {
            Log.e(TAG, "Failed to fetch token", throwable)
            callback.onFailure(throwable)
         }
      })
   }
}
```

## 后续步骤
<a name="chat-android-rooms-next-steps"></a>

现在，您已经建立聊天室连接，请继续阅读本 Android 教程的第 2 部分：[消息和事件](chat-sdk-android-tutorial-messages-events.md)。

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

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

1. [为发送消息创建一个 UI](#chat-android-messages-events-create_ui)

   1. [UI 主布局](#create-ui-main-layout)

   1. [UI 将文本单元格抽象化以便一致地显示文本](#create-ui-text-cell)

   1. [UI 左侧聊天消息](#create-ui-left-chat-message)

   1. [UI 右侧聊天消息](#create-ui-right-chat-message)

   1. [UI 其他颜色值](#create-ui-color-values)

1. [应用视图绑定](#chat-android-messages-events-view-binding)

1. [管理聊天消息请求](#chat-android-messages-events-requests)

1. [最终步骤](#chat-android-messages-events-final-steps)

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

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

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

## 为发送消息创建一个 UI
<a name="chat-android-messages-events-create_ui"></a>

现在我们已成功初始化聊天室连接，接下来可以发送第一条消息了。要使用此功能，需要一个 UI。我们将添加：
+ `connect/disconnect` 按钮
+ 使用 `send` 按钮输入消息
+ 动态消息列表。为了构建此内容，我们使用了 Android Jetpack [RecyclerView](https://developer.android.com/develop/ui/views/layout/recyclerview)。

### UI 主布局
<a name="create-ui-main-layout"></a>

请参阅 Android 开发者文档中的 Android Jetpack [布局](https://developer.android.com/develop/ui/views/layout/declaring-layout)。

**XML：**

```
// ./app/src/main/res/layout/activity_main.xml


<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
                                                     xmlns:app="http://schemas.android.com/apk/res-auto"
                                                     xmlns:tools="http://schemas.android.com/tools"
                                                     android:layout_width="match_parent"
                                                     android:layout_height="match_parent">

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
                  xmlns:app="http://schemas.android.com/apk/res-auto"
                  android:id="@+id/connect_view"
                  android:layout_width="match_parent"
                  android:layout_height="match_parent"
                  android:gravity="center"
                  android:orientation="vertical">

        <androidx.cardview.widget.CardView
                android:id="@+id/connect_button"
                android:layout_width="match_parent"
                android:layout_height="48dp"
                android:layout_gravity=""
                android:layout_marginStart="16dp"
                android:layout_marginTop="4dp"
                android:layout_marginEnd="16dp"
                android:clickable="true"
                android:elevation="16dp"
                android:focusable="true"
                android:foreground="?android:attr/selectableItemBackground"
                app:cardBackgroundColor="@color/purple_500"
                app:cardCornerRadius="10dp">

            <TextView
                    android:id="@+id/connect_text"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_alignParentEnd="true"
                    android:layout_gravity="center"
                    android:layout_weight="1"
                    android:paddingHorizontal="12dp"
                    android:text="Connect"
                    android:textColor="@color/white"
                    android:textSize="16sp"/>

            <ProgressBar
                    android:id="@+id/activity_indicator"
                    android:layout_width="20dp"
                    android:layout_height="20dp"
                    android:layout_gravity="center"
                    android:layout_marginHorizontal="20dp"
                    android:indeterminateOnly="true"
                    android:indeterminateTint="@color/white"
                    android:indeterminateTintMode="src_atop"
                    android:keepScreenOn="true"
                    android:visibility="gone"/>
        </androidx.cardview.widget.CardView>

    </LinearLayout>

    <androidx.constraintlayout.widget.ConstraintLayout
            android:id="@+id/chat_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:clipToPadding="false"
            android:visibility="visible"
            tools:context=".MainActivity">

        <RelativeLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="vertical"
                app:layout_constraintBottom_toTopOf="@+id/layout_message_input"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent">

            <androidx.recyclerview.widget.RecyclerView
                    android:id="@+id/recycler_view"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:clipToPadding="false"
                    android:paddingTop="70dp"
                    android:paddingBottom="20dp"/>
        </RelativeLayout>

        <RelativeLayout
                android:id="@+id/layout_message_input"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@android:color/white"
                android:clipToPadding="false"
                android:drawableTop="@android:color/black"
                android:elevation="18dp"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintStart_toStartOf="parent">

            <EditText
                    android:id="@+id/message_edit_text"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_centerVertical="true"
                    android:layout_marginStart="16dp"
                    android:layout_toStartOf="@+id/send_button"
                    android:background="@android:color/transparent"
                    android:hint="Enter Message"
                    android:inputType="text"
                    android:maxLines="6"
                    tools:ignore="Autofill"/>

            <Button
                    android:id="@+id/send_button"
                    android:layout_width="84dp"
                    android:layout_height="48dp"
                    android:layout_alignParentEnd="true"
                    android:background="@color/black"
                    android:foreground="?android:attr/selectableItemBackground"
                    android:text="Send"
                    android:textColor="@color/white"
                    android:textSize="12dp"/>
        </RelativeLayout>
    </androidx.constraintlayout.widget.ConstraintLayout>


</androidx.coordinatorlayout.widget.CoordinatorLayout>
```

### UI 将文本单元格抽象化以便一致地显示文本
<a name="create-ui-text-cell"></a>

**XML：**

```
// ./app/src/main/res/layout/common_cell.xml
   
<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:id="@+id/layout_container"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:background="@color/light_gray"
              android:minWidth="100dp"
              android:orientation="vertical">

    <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="horizontal">

        <TextView
                android:id="@+id/card_message_me_text_view"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:layout_marginBottom="8dp"
                android:maxWidth="260dp"
                android:paddingLeft="12dp"
                android:paddingTop="8dp"
                android:paddingRight="12dp"
                android:text="This is a Message"
                android:textColor="#ffffff"
                android:textSize="16sp"/>

        <TextView
                android:id="@+id/failed_mark"
                android:layout_width="40dp"
                android:layout_height="match_parent"
                android:paddingRight="5dp"
                android:src="@drawable/ic_launcher_background"
                android:text="!"
                android:textAlignment="viewEnd"
                android:textColor="@color/white"
                android:textSize="25dp"
                android:visibility="gone"/>
    </LinearLayout>

</LinearLayout>
```

### UI 左侧聊天消息
<a name="create-ui-left-chat-message"></a>

**XML**：

```
// ./app/src/main/res/layout/card_view_left.xml
 
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              xmlns:app="http://schemas.android.com/apk/res-auto"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:layout_marginStart="8dp"
              android:layout_marginBottom="12dp"
              android:orientation="vertical">

    <TextView
            android:id="@+id/username_edit_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="UserName"/>

    <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

        <androidx.cardview.widget.CardView
                android:id="@+id/card_message_other"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="left"
                android:layout_marginBottom="4dp"
                android:foreground="?android:attr/selectableItemBackground"
                app:cardBackgroundColor="@color/light_gray_2"
                app:cardCornerRadius="10dp"
                app:cardElevation="0dp"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintStart_toStartOf="parent">

            <include layout="@layout/common_cell"/>
        </androidx.cardview.widget.CardView>

        <TextView
                android:id="@+id/dateText"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginLeft="4dp"
                android:layout_marginBottom="4dp"
                android:text="10:00"
                app:layout_constraintBottom_toBottomOf="@+id/card_message_other"
                app:layout_constraintLeft_toRightOf="@+id/card_message_other"/>
    </androidx.constraintlayout.widget.ConstraintLayout>


</LinearLayout>
```

### UI 右侧聊天消息
<a name="create-ui-right-chat-message"></a>

**XML**：

```
// ./app/src/main/res/layout/card_view_right.xml
 
<?xml version="1.0" encoding="utf-8"?>

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"                                                   xmlns:app="http://schemas.android.com/apk/res-auto"                                                   android:layout_width="match_parent"                                                   android:layout_height="wrap_content" 
android:layout_marginEnd="8dp">

    <androidx.cardview.widget.CardView
            android:id="@+id/card_message_me"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="right"
            android:layout_marginBottom="10dp"
            android:foreground="?android:attr/selectableItemBackground"
            app:cardBackgroundColor="@color/purple_500"
            app:cardCornerRadius="10dp"
            app:cardElevation="0dp"
            app:cardPreventCornerOverlap="false"
            app:cardUseCompatPadding="true"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent">

        <include layout="@layout/common_cell"/>

    </androidx.cardview.widget.CardView>

    <TextView
            android:id="@+id/dateText"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginRight="12dp"
            android:layout_marginBottom="4dp"
            android:text="10:00"
            app:layout_constraintBottom_toBottomOf="@+id/card_message_me"
            app:layout_constraintRight_toLeftOf="@+id/card_message_me"/>

</androidx.constraintlayout.widget.ConstraintLayout>
```

### UI 其他颜色值
<a name="create-ui-color-values"></a>

**XML：**

```
// ./app/src/main/res/values/colors.xml
 
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!--    ...-->
    <color name="dark_gray">#4F4F4F</color>
    <color name="blue">#186ED3</color>
    <color name="dark_red">#b30000</color>
    <color name="light_gray">#B7B7B7</color>
    <color name="light_gray_2">#eef1f6</color>
</resources>
```

## 应用视图绑定
<a name="chat-android-messages-events-view-binding"></a>

我们利用 Android [视图绑定](https://developer.android.com/topic/libraries/view-binding)功能来引用 XML 布局的绑定类。要启用该功能，请在 `./app/build.gradle` 中将 `viewBinding` 构建选项设置为 `true`：

**Kotlin 脚本：**

```
 // ./app/build.gradle

android {
//    ...

    buildFeatures {
        viewBinding = true
    }
//    ...
}
```

现在可以将 UI 与我们的 Kotlin 代码连接起来：

**Kotlin：**

```
// ./app/src/main/java/com/chatterbox/myapp/MainActivity.kt
package com.chatterbox.myapp
// ...
const val TAG = "Chatterbox-MyApp"


class MainActivity : AppCompatActivity() {
//    ...

    private fun sendMessage(request: SendMessageRequest) {
        try {
            room?.sendMessage(
                request,
                object : SendMessageCallback {
                    override fun onRejected(request: SendMessageRequest, error: ChatError) {
                        runOnUiThread {
                            entries.addFailedRequest(request)
                            scrollToBottom()
                            Log.e(TAG, "Message rejected: ${error.errorMessage}")
                        }
                    }
                }
            )

            entries.addPendingRequest(request)

            binding.messageEditText.text.clear()
            scrollToBottom()
        } catch (error: Exception) {
            Log.e(TAG, error.message ?: "Unknown error occurred")
        }
    }

    private fun scrollToBottom() {
        binding.recyclerView.smoothScrollToPosition(entries.size - 1)
    }

    private fun sendButtonClick(view: View) {
        val content = binding.messageEditText.text.toString()
        if (content.trim().isEmpty()) {
            return
        }

        val request = SendMessageRequest(content)
        sendMessage(request)
    }
}
```

我们还添加了删除消息和断开用户聊天连接的方法，可以使用聊天消息上下文菜单调用这些方法：

**Kotlin：**

```
// ./app/src/main/java/com/chatterbox/myapp/MainActivity.kt

package com.chatterbox.myapp
//    ...

class MainActivity : AppCompatActivity() {
//    ...

    private fun deleteMessage(request: DeleteMessageRequest) {
        room?.deleteMessage(
            request,
            object : DeleteMessageCallback {
                override fun onRejected(request: DeleteMessageRequest, error: ChatError) {
                    runOnUiThread {
                        Log.d(TAG, "Delete message rejected: ${error.errorMessage}")
                    }
                }
            }
        )
    }

    private fun disconnectUser(request: DisconnectUserRequest) {
        room?.disconnectUser(
            request,
            object : DisconnectUserCallback {
                override fun onRejected(request: DisconnectUserRequest, error: ChatError) {
                    runOnUiThread {
                        Log.d(TAG, "Disconnect user rejected: ${error.errorMessage}")
                    }
                }
            }
        )
    }
}
```

## 管理聊天消息请求
<a name="chat-android-messages-events-requests"></a>

我们需要一种用于管理不同状态的聊天消息请求的方法：
+ 待处理 — 消息已发送到聊天室，但尚未被确认或拒绝。
+ 已确认 — 消息已被聊天室发送给所有用户（包括我们）。
+ 已拒绝 — 消息已被聊天室拒绝，并提示带有错误对象。

我们会把未解决的聊天请求和聊天消息保留在[列表](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/mutable-list-of.html)中。该列表属于一个单独的类，我们称之为 `ChatEntries.kt`：

**Kotlin：**

```
// ./app/src/main/java/com/chatterbox/myapp/ChatEntries.kt

package com.chatterbox.myapp

import com.amazonaws.ivs.chat.messaging.entities.ChatMessage
import com.amazonaws.ivs.chat.messaging.requests.SendMessageRequest

sealed class ChatEntry() {
    class Message(val message: ChatMessage) : ChatEntry()
    class PendingRequest(val request: SendMessageRequest) : ChatEntry()
    class FailedRequest(val request: SendMessageRequest) : ChatEntry()
}

class ChatEntries {
    /* This list is kept in sorted order. ChatMessages are sorted by date, while pending and failed requests are kept in their original insertion point. */
    val entries = mutableListOf<ChatEntry>()
    var adapter: ChatListAdapter? = null

    val size get() = entries.size

    /**
     * Insert pending request at the end.
     */
    fun addPendingRequest(request: SendMessageRequest) {
        val insertIndex = entries.size
        entries.add(insertIndex, ChatEntry.PendingRequest(request))
        adapter?.notifyItemInserted(insertIndex)
    }

    /**
     * Insert received message at proper place based on sendTime. This can cause removal of pending requests.
     */
    fun addReceivedMessage(message: ChatMessage) {
        /* Skip if we have already handled that message. */
        val existingIndex = entries.indexOfLast { it is ChatEntry.Message && it.message.id == message.id }
        if (existingIndex != -1) {
            return
        }

        val removeIndex = entries.indexOfLast {
            it is ChatEntry.PendingRequest && it.request.requestId == message.requestId
        }
        if (removeIndex != -1) {
            entries.removeAt(removeIndex)
        }

        val insertIndexRaw = entries.indexOfFirst { it is ChatEntry.Message && it.message.sendTime > message.sendTime }
        val insertIndex = if (insertIndexRaw == -1) entries.size else insertIndexRaw
        entries.add(insertIndex, ChatEntry.Message(message))

        if (removeIndex == -1) {
            adapter?.notifyItemInserted(insertIndex)
        } else if (removeIndex == insertIndex) {
            adapter?.notifyItemChanged(insertIndex)
        } else {
            adapter?.notifyItemRemoved(removeIndex)
            adapter?.notifyItemInserted(insertIndex)
        }
    }

    fun addFailedRequest(request: SendMessageRequest) {
        val removeIndex = entries.indexOfLast {
            it is ChatEntry.PendingRequest && it.request.requestId == request.requestId
        }
        if (removeIndex != -1) {
            entries.removeAt(removeIndex)
            entries.add(removeIndex, ChatEntry.FailedRequest(request))
            adapter?.notifyItemChanged(removeIndex)
        } else {
            val insertIndex = entries.size
            entries.add(insertIndex, ChatEntry.FailedRequest(request))
            adapter?.notifyItemInserted(insertIndex)
        }
    }

    fun removeMessage(messageId: String) {
        val removeIndex = entries.indexOfFirst { it is ChatEntry.Message && it.message.id == messageId }
        entries.removeAt(removeIndex)
        adapter?.notifyItemRemoved(removeIndex)
    }

    fun removeFailedRequest(requestId: String) {
        val removeIndex = entries.indexOfFirst { it is ChatEntry.FailedRequest && it.request.requestId == requestId }
        entries.removeAt(removeIndex)
        adapter?.notifyItemRemoved(removeIndex)
    }

    fun removeAll() {
        entries.clear()
    }
}
```

为了将列表与 UI 连接起来，我们使用[适配器](https://developer.android.com/reference/android/widget/Adapter)。有关更多信息，请参阅[使用 AdapterView 绑定到数据](https://developer.android.com/develop/ui/views/layout/binding)和[生成的绑定类](https://developer.android.com/topic/libraries/data-binding/generated-binding)。

**Kotlin：**

```
// ./app/src/main/java/com/chatterbox/myapp/ChatListAdapter.kt

package com.chatterbox.myapp

import android.content.Context
import android.graphics.Color
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.core.view.isGone
import androidx.recyclerview.widget.RecyclerView
import com.amazonaws.ivs.chat.messaging.requests.DisconnectUserRequest
import java.text.DateFormat


class ChatListAdapter(
    private val entries: ChatEntries,
    private val onDisconnectUser: (request: DisconnectUserRequest) -> Unit,
) :
    RecyclerView.Adapter<ChatListAdapter.ViewHolder>() {
    var context: Context? = null
    var userId: String? = null

    class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        val container: LinearLayout = view.findViewById(R.id.layout_container)
        val textView: TextView = view.findViewById(R.id.card_message_me_text_view)
        val failedMark: TextView = view.findViewById(R.id.failed_mark)
        val userNameText: TextView? = view.findViewById(R.id.username_edit_text)
        val dateText: TextView? = view.findViewById(R.id.dateText)
    }

    override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ViewHolder {
        if (viewType == 0) {
            val rightView = LayoutInflater.from(viewGroup.context).inflate(R.layout.card_view_right, viewGroup, false)
            return ViewHolder(rightView)
        }
        val leftView = LayoutInflater.from(viewGroup.context).inflate(R.layout.card_view_left, viewGroup, false)
        return ViewHolder(leftView)
    }

    override fun getItemViewType(position: Int): Int {
        // Int 0 indicates to my message while Int 1 to other message
        val chatMessage = entries.entries[position]
        return if (chatMessage is ChatEntry.Message && chatMessage.message.sender.userId != userId) 1 else 0
    }

    override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {
        return when (val entry = entries.entries[position]) {
            is ChatEntry.Message -> {
                viewHolder.textView.text = entry.message.content

                val bgColor = if (entry.message.sender.userId == userId) {
                    R.color.purple_500
                } else {
                    R.color.light_gray_2
                }
                viewHolder.container.setBackgroundColor(ContextCompat.getColor(context!!, bgColor))

                if (entry.message.sender.userId != userId) {
                    viewHolder.textView.setTextColor(Color.parseColor("#000000"))
                }

                viewHolder.failedMark.isGone = true

                viewHolder.itemView.setOnCreateContextMenuListener { menu, _, _ ->
                    menu.add("Kick out").setOnMenuItemClickListener {
                        val request = DisconnectUserRequest(entry.message.sender.userId, "Some reason")
                        onDisconnectUser(request)
                        true
                    }
                }

                viewHolder.userNameText?.text = entry.message.sender.userId
                viewHolder.dateText?.text =
                    DateFormat.getTimeInstance(DateFormat.SHORT).format(entry.message.sendTime)
            }

            is ChatEntry.PendingRequest -> {
                viewHolder.container.setBackgroundColor(ContextCompat.getColor(context!!, R.color.light_gray))
                viewHolder.textView.text = entry.request.content
                viewHolder.failedMark.isGone = true
                viewHolder.itemView.setOnCreateContextMenuListener(null)
                viewHolder.dateText?.text = "Sending"
            }

            is ChatEntry.FailedRequest -> {
                viewHolder.textView.text = entry.request.content
                viewHolder.container.setBackgroundColor(ContextCompat.getColor(context!!, R.color.dark_red))
                viewHolder.failedMark.isGone = false
                viewHolder.dateText?.text = "Failed"
            }
        }
    }

    override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
        super.onAttachedToRecyclerView(recyclerView)
        context = recyclerView.context
    }

    override fun getItemCount() = entries.entries.size
}
```

## 最终步骤
<a name="chat-android-messages-events-final-steps"></a>

现在可以挂载我们的新适配器，将 `ChatEntries` 类绑定到 `MainActivity`：

**Kotlin**：

```
// ./app/src/main/java/com/chatterbox/myapp/MainActivity.kt

package com.chatterbox.myapp
// ...

import com.chatterbox.myapp.databinding.ActivityMainBinding
import com.chatterbox.myapp.ChatListAdapter
import com.chatterbox.myapp.ChatEntries

class MainActivity : AppCompatActivity() {
    // ...
    private var entries = ChatEntries()
    private lateinit var adapter: ChatListAdapter
    private lateinit var binding: ActivityMainBinding

    /* see https://developer.android.com/topic/libraries/data-binding/generated-binding#create */
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        /* Create room instance. */
        room = ChatRoom(REGION, ::fetchChatToken).apply {
            listener = roomListener
        }

        binding.sendButton.setOnClickListener(::sendButtonClick)
        binding.connectButton.setOnClickListener { connect() }

        setUpChatView()

        updateConnectionState(ConnectionState.DISCONNECTED)
    }

    private fun setUpChatView() {
        /* Setup Android Jetpack RecyclerView - see https://developer.android.com/develop/ui/views/layout/recyclerview.*/
        adapter = ChatListAdapter(entries, ::disconnectUser)
        entries.adapter = adapter

        val recyclerViewLayoutManager = LinearLayoutManager(this@MainActivity, LinearLayoutManager.VERTICAL, false)
        binding.recyclerView.layoutManager = recyclerViewLayoutManager
        binding.recyclerView.adapter = adapter

        binding.sendButton.setOnClickListener(::sendButtonClick)
        binding.messageEditText.setOnEditorActionListener { _, _, event ->
            val isEnterDown = (event.action == KeyEvent.ACTION_DOWN) && (event.keyCode == KeyEvent.KEYCODE_ENTER)
            if (!isEnterDown) {
                return@setOnEditorActionListener false
            }

            sendButtonClick(binding.sendButton)
            return@setOnEditorActionListener true
        }
    }
}
```

由于我们已经有一个用于负责跟踪我们聊天请求的类 (`ChatEntries`)，我们已经准备好实现用于在 `roomListener` 中操作 `entries` 的代码。我们将根据自己正在响应的事件来更新 `entries` 和 `connectionState`：

**Kotlin：**

```
// ./app/src/main/java/com/chatterbox/myapp/MainActivity.kt

package com.chatterbox.myapp
// ...

class MainActivity : AppCompatActivity() {
    //...
    

    private fun sendMessage(request: SendMessageRequest) {
    //...

    }

    private fun scrollToBottom() {
        binding.recyclerView.smoothScrollToPosition(entries.size - 1)
    }

    private val roomListener = object : ChatRoomListener {
        override fun onConnecting(room: ChatRoom) {
            Log.d(TAG, "[${Thread.currentThread().name}] onConnecting")
            runOnUiThread {
                updateConnectionState(ConnectionState.LOADING)
            }
        }

        override fun onConnected(room: ChatRoom) {
            Log.d(TAG, "[${Thread.currentThread().name}] onConnected")
            runOnUiThread {
                updateConnectionState(ConnectionState.CONNECTED)
            }
        }

        override fun onDisconnected(room: ChatRoom, reason: DisconnectReason) {
            Log.d(TAG, "[${Thread.currentThread().name}] onDisconnected")
            runOnUiThread {
                updateConnectionState(ConnectionState.DISCONNECTED)
                entries.removeAll()
            }
        }

        override fun onMessageReceived(room: ChatRoom, message: ChatMessage) {
            Log.d(TAG, "[${Thread.currentThread().name}] onMessageReceived $message")
            runOnUiThread {
                entries.addReceivedMessage(message)
                scrollToBottom()
            }
        }

        override fun onEventReceived(room: ChatRoom, event: ChatEvent) {
            Log.d(TAG, "[${Thread.currentThread().name}] onEventReceived $event")
        }

        override fun onMessageDeleted(room: ChatRoom, event: DeleteMessageEvent) {
            Log.d(TAG, "[${Thread.currentThread().name}] onMessageDeleted $event")
        }

        override fun onUserDisconnected(room: ChatRoom, event: DisconnectUserEvent) {
            Log.d(TAG, "[${Thread.currentThread().name}] onUserDisconnected $event")
        }
    }
}
```

现在应该可以运行您的应用程序了！（请参阅[构建和运行您的应用程序](https://developer.android.com/studio/run#basic-build-run)。） 使用应用程序时，请记住让您的后端服务器保持运行。您可以通过使用命令 `./gradlew :auth-server:run`，从我们项目根目录的终端启动，或是直接从 Android Studio 执行 `auth-server:run` Gradle 任务来启动。

# IVS 聊天功能客户端消息收发 SDK：Kotlin 协同教程第 1 部分：聊天室
<a name="chat-sdk-kotlin-tutorial-chat-rooms"></a>

这是一个由两部分组成的教程的第 1 部分。通过使用 [Kotlin](https://kotlinlang.org/) 编程语言和[协同例程](https://kotlinlang.org/docs/coroutines-overview.html)构建功能齐全的 Android 应用程序，您将了解使用 Amazon IVS 聊天功能消息收发 SDK 的基本知识。我们把这个应用程序称为 *Chatterbox*。

在开始本模块之前，请花几分钟时间熟悉先决条件、聊天令牌背后的关键概念以及创建聊天室所需的后端服务器。

这些教程是为刚接触 IVS 聊天功能消息收发 SDK 但经验丰富的 Android 开发人员创建的。您需要熟悉 Kotlin 编程语言并在 Android 平台上创建 UI。

本教程的第 1 部分分为几个章节：

1. [设置本地身份验证/授权服务器](#chat-kotlin-rooms-auth-server)

1. [创建 Chatterbox 项目](#chat-kotlin-rooms-chatterbox)

1. [连接到聊天室和观察连接更新](#chat-kotlin-rooms-connect)

1. [构建令牌提供程序](#chat-kotlin-rooms-token-provider)

1. [后续步骤](#chat-kotlin-rooms-next-steps)

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

## 先决条件
<a name="chat-kotlin-rooms-prerequisites"></a>
+ 熟悉 Kotlin 并在 Android 平台上创建应用程序。如果您对创建 Android 应用程序不熟悉，请学习面向 Android 开发者的[构建您的第一个应用程序](https://developer.android.com/codelabs/basic-android-kotlin-compose-first-app#0)指南中的基础知识。
+ 阅读并理解 [Amazon IVS Chat 入门](getting-started-chat.md)。
+ 使用现有的 IAM policy 中定义的 `CreateChatToken` 和 `CreateRoom` 功能创建 AWS IAM 用户。（请参见 [Amazon IVS Chat 入门](getting-started-chat.md)。）
+ 确保该用户的私有密钥/访问密钥存储在 Amazon 凭证文件中。有关说明，请参阅 [Amazon CLI 用户指南](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-welcome.html)（尤其是[配置和凭证文件设置](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html)部分）。
+ 创建聊天室并保存其 ARN。请参阅[Amazon IVS Chat 入门](getting-started-chat.md)。（如果不保存 ARN，您可以稍后使用控制台或 Chat API 查找 ARN。）

## 设置本地身份验证/授权服务器
<a name="chat-kotlin-rooms-auth-server"></a>

您的后端服务器负责创建聊天室和生成 IVS 聊天功能 Android SDK 所需的聊天令牌，以便在您的客户端连接聊天室时进行身份验证和授权。

请参阅*《Amazon IVS Chat 入门》*中的[创建聊天令牌](getting-started-chat-auth.md)。如其中的流程图所示，您的服务器端代码负责创建聊天令牌。这意味着您的应用程序必须通过向服务器端应用程序请求聊天令牌，来提供自己生成聊天令牌的方法。

我们使用 [Ktor](https://ktor.io/) 框架创建一个实时本地服务器，该服务器使用您的本地 Amazon 环境管理聊天令牌的创建。

此时，我们希望您已正确设置 AWS 凭证。有关分步说明，请参阅 [Set up AWS temporary credentials and AWS Region for development](https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/setup-credentials.html)。

创建一个新目录并命名为 `chatterbox`，在其中再创建一个，然后命名为 `auth-server`*。*

我们的服务器文件夹将具有如下结构：

```
- auth-server
  - src
    - main
      - kotlin
        - com
          - chatterbox
            - authserver
              - Application.kt
       - resources
         - application.conf
         - logback.xml
   - build.gradle.kts
```

*注意：您可以直接将此处的代码复制/粘贴到引用文件中。*

接下来，我们添加所有必要的依赖项和插件，以便身份验证服务器正常工作：

**Kotlin 脚本：**

```
// ./auth-server/build.gradle.kts

plugins {
   application
   kotlin("jvm")
   kotlin("plugin.serialization").version("1.7.10")
}

application {   
   mainClass.set("io.ktor.server.netty.EngineMain")
}

dependencies {
   implementation("software.amazon.awssdk:ivschat:2.18.1")
   implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.20")

   implementation("io.ktor:ktor-server-core:2.1.3")
   implementation("io.ktor:ktor-server-netty:2.1.3")
   implementation("io.ktor:ktor-server-content-negotiation:2.1.3")
   implementation("io.ktor:ktor-serialization-kotlinx-json:2.1.3")

   implementation("ch.qos.logback:logback-classic:1.4.4")
}
```

现在我们需要为身份验证服务器设置日志记录功能。（有关更多信息，请参阅[配置记录器](https://ktor.io/docs/logging.html#configure-logger)。）

**XML：**

```
// ./auth-server/src/main/resources/logback.xml

<configuration>
   <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
      <encoder>
         <pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
      </encoder>
   </appender>
   <root level="trace">
      <appender-ref ref="STDOUT"/>
   </root>
   <logger name="org.eclipse.jetty" level="INFO"/>
   <logger name="io.netty" level="INFO"/>
</configuration>
```

[Ktor](https://ktor.io/docs/welcome.html) 服务器需要自动从 `resources` 目录的 `application.*` 文件中加载的配置设置，所以我们也添加了这些设置。（有关更多信息，请参阅[文件中的配置](https://ktor.io/docs/configurations.html#configuration-file)。）

**HOCON**：

```
// ./auth-server/src/main/resources/application.conf

ktor {
   deployment {
      port = 3000
   }
   application {
      modules = [ com.chatterbox.authserver.ApplicationKt.main ]
   }
}
```

最后，让我们实施我们的服务器：

**Kotlin：**

```
// ./auth-server/src/main/kotlin/com/chatterbox/authserver/Application.kt

package com.chatterbox.authserver

import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import software.amazon.awssdk.services.ivschat.IvschatClient
import software.amazon.awssdk.services.ivschat.model.CreateChatTokenRequest

@Serializable
data class ChatTokenParams(var userId: String, var roomIdentifier: String)

@Serializable
data class ChatToken(
   val token: String,
   val sessionExpirationTime: String,
   val tokenExpirationTime: String,
)

fun Application.main() {
   install(ContentNegotiation) {
      json(Json)
   }

   routing {
      post("/create_chat_token") {
         val callParameters = call.receive<ChatTokenParams>()
         val request = CreateChatTokenRequest.builder().roomIdentifier(callParameters.roomIdentifier)
            .userId(callParameters.userId).build()
         val token = IvschatClient.create()
            .createChatToken(request)

         call.respond(
            ChatToken(
                token.token(),
                token.sessionExpirationTime().toString(),
                token.tokenExpirationTime().toString()
            )
         )
      }
   }
}
```

## 创建 Chatterbox 项目
<a name="chat-kotlin-rooms-chatterbox"></a>

要创建 Android 项目，请安装并打开 [Android Studio](https://developer.android.com/studio)。

按照 Android 官方在[创建项目指南](https://developer.android.com/studio/projects/create-project)中列出的步骤进行操作。
+ 在[选择您的项目](https://developer.android.com/studio/projects/create-project)中，为我们的 Chatterbox 应用程序选择**空活动**项目模板。
+ 在[配置您的项目](https://developer.android.com/studio/projects/create-project#configure)中，为配置字段选择以下值：
  + **名称**：My App
  + **程序包名称**：com.chatterbox.myapp
  + **保存位置**：指向在上一步中创建的 `chatterbox` 目录
  + **语言**：Kotlin
  + **最低 API 级别**：API 21：Android 5.0（Lollipop）

正确指定所有配置参数后，我们在 `chatterbox` 文件夹内的文件结构应如下所示：

```
- app
  - build.gradle
  ...
- gradle
- .gitignore
- build.gradle
- gradle.properties
- gradlew
- gradlew.bat
- local.properties
- settings.gradle
- auth-server
  - src
    - main
      - kotlin
        - com
          - chatterbox
            - authserver
              - Application.kt
       - resources
         - application.conf
         - logback.xml
   - build.gradle.kts
```

现在我们有一个正在运行的 Android 项目，可以将 [com.amazonaws:ivs-chat-messaging](https://mvnrepository.com/artifact/com.amazonaws/ivs-chat-messaging) 和 [org.jetbrains.kotlinx:kotlinx-coroutines-core](https://github.com/Kotlin/kotlinx.coroutines) 添加到 `build.gradle` 依赖项中。（有关 [Gradle](https://gradle.org/) 构建工具包的更多信息，请参阅[配置您的构建](https://developer.android.com/build)。） 

**注意**：在每个代码段的顶部，都有一个文件路径，您应该在其中对项目进行更改。路径相对于项目的根目录。

**Kotlin：**

```
// ./app/build.gradle

plugins {
// ...
}

android {
// ...
}

dependencies {
    implementation 'com.amazonaws:ivs-chat-messaging:1.1.0'
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4'

// ...
}
```

添加新依赖项后，在 Android Studio 中运行**将项目与 Gradle 文件同步**，将项目与新依赖项同步。（有关更多信息，请参阅[添加构建依赖项](https://developer.android.com/build/dependencies)。）

为了方便从项目根目录运行我们的身份验证服务器（在上一部分中创建），我们将其作为新模块包含在 `settings.gradle` 中。（有关更多信息，请参阅[使用 Gradle 构建软件组件](https://docs.gradle.org/current/userguide/multi_project_builds.html)。）

**Kotlin 脚本：**

```
// ./settings.gradle

// ...

rootProject.name = "My App"
include ':app'
include ':auth-server'
```

从现在起，由于 `auth-server` 已包含在 Android 项目中，您可以从项目的根目录使用以下命令运行身份验证服务器：

**Shell：**

```
./gradlew :auth-server:run         
```

## 连接到聊天室和观察连接更新
<a name="chat-kotlin-rooms-connect"></a>

为了打开聊天室连接，我们使用了 [onCreate() 活动生命周期回调](https://developer.android.com/guide/components/activities/activity-lifecycle)，该回调在首次创建活动时触发。[ChatRoom 构造函数](https://aws.github.io/amazon-ivs-chat-messaging-sdk-android/1.0.0/-amazon%20-i-v-s%20-chat%20-messaging%20-s-d-k%20for%20-android/com.amazonaws.ivs.chat.messaging/-chat-room/index.html)要求我们提供 `region` 和 `tokenProvider` 以实例化聊天室连接。

**注意：**以下代码段中的 `fetchChatToken` 功能将在[下一部分](#chat-kotlin-rooms-token-provider)中实现。

**Kotlin：**

```
// ./app/src/main/java/com/chatterbox/myapp/MainActivity.kt

package com.chatterbox.myapp
// ...

// AWS region of the room that was created in Getting Started with Amazon IVS Chat
const val REGION = "us-west-2"

class MainActivity : AppCompatActivity() {
    private var room: ChatRoom? = null
    // ...

   override fun onCreate(savedInstanceState: Bundle?) {
      super.onCreate(savedInstanceState)
      setContentView(R.layout.activity_main)

      // Create room instance
      room = ChatRoom(REGION, ::fetchChatToken)
   }

// ...
}
```

显示聊天室连接的变化并对其做出反应是制作 `chatterbox` 等聊天应用程序的重要组成部分。在我们开始与聊天室互动之前，必须订阅聊天室连接状态事件以获取更新。

在协同例程的聊天功能 SDK 中，[https://aws.github.io/amazon-ivs-chat-messaging-sdk-android/1.0.0/-amazon%20-i-v-s%20-chat%20-messaging%20-s-d-k%20for%20-android/com.amazonaws.ivs.chat.messaging/-chat-room/index.html](https://aws.github.io/amazon-ivs-chat-messaging-sdk-android/1.0.0/-amazon%20-i-v-s%20-chat%20-messaging%20-s-d-k%20for%20-android/com.amazonaws.ivs.chat.messaging/-chat-room/index.html) 希望我们在[流](https://kotlinlang.org/docs/flow.html)中处理聊天室生命周期事件。目前，函数在调用时只会记录确认消息：

**Kotlin：**

```
// ./app/src/main/java/com/chatterbox/myapp/MainActivity.kt

package com.chatterbox.myapp
// ...

const val TAG = "Chatterbox-MyApp"

class MainActivity : AppCompatActivity() {
// ...

    override fun onCreate(savedInstanceState: Bundle?) {
        // ...

        // Create room instance
        room = ChatRoom(REGION, ::fetchChatToken).apply {
            lifecycleScope.launch {
                stateChanges().collect { state ->
                    Log.d(TAG, "state change to $state")
                }
            }

            lifecycleScope.launch {
                receivedMessages().collect { message ->
                    Log.d(TAG, "messageReceived $message")
                }
            }

            lifecycleScope.launch {
                receivedEvents().collect { event ->
                    Log.d(TAG, "eventReceived $event")
                }
            }

            lifecycleScope.launch {
                deletedMessages().collect { event ->
                    Log.d(TAG, "messageDeleted $event")
                }
            }

            lifecycleScope.launch {
                disconnectedUsers().collect { event ->
                    Log.d(TAG, "userDisconnected $event")
                }
            }
        }
    }
}
```

此后，我们需要提供读取聊天室连接状态的功能。我们会将其保留在 `MainActivity.kt` [属性](https://kotlinlang.org/docs/properties.html)中并将其初始化为聊天室连接默认断开状态（参阅 [IVS 聊天功能 Android SDK 参考](https://aws.github.io/amazon-ivs-chat-messaging-sdk-android/latest/)中的 `ChatRoom` `state`）。为了保持最新的本地状态，我们需要实现一个状态更新器函数，我们称之为 `updateConnectionState`：

**Kotlin：**

```
// ./app/src/main/java/com/chatterbox/myapp/MainActivity.kt

package com.chatterbox.myapp
// ...

class MainActivity : AppCompatActivity() {
   private var connectionState = ChatRoom.State.DISCONNECTED

// ...

   private fun updateConnectionState(state: ChatRoom.State) {
      connectionState = state

     when (state) {
          ChatRoom.State.CONNECTED -> {
              Log.d(TAG, "room connected")
          }
          ChatRoom.State.DISCONNECTED -> {
              Log.d(TAG, "room disconnected")
          }
          ChatRoom.State.CONNECTING -> {
              Log.d(TAG, "room connecting")
          }
      }
}
```

接下来，我们将状态更新器函数与 [ChatRoom.listener](https://aws.github.io/amazon-ivs-chat-messaging-sdk-android/1.0.0/-amazon%20-i-v-s%20-chat%20-messaging%20-s-d-k%20for%20-android/com.amazonaws.ivs.chat.messaging/-chat-room/listener.html) 属性集成：

**Kotlin：**

```
// ./app/src/main/java/com/chatterbox/myapp/MainActivity.kt

package com.chatterbox.myapp
// ...

class MainActivity : AppCompatActivity() {
// ...

    override fun onCreate(savedInstanceState: Bundle?) {
        // ...

        // Create room instance
        room = ChatRoom(REGION, ::fetchChatToken).apply {
            lifecycleScope.launch {
                stateChanges().collect { state ->
                    Log.d(TAG, "state change to $state")
                    updateConnectionState(state)

                }
            }

      // ...

      }
   }
}
```

现在我们能够保存、侦听和响应 [ChatRoom](https://aws.github.io/amazon-ivs-chat-messaging-sdk-android/1.0.0/-amazon%20-i-v-s%20-chat%20-messaging%20-s-d-k%20for%20-android/com.amazonaws.ivs.chat.messaging/-chat-room/index.html) 状态更新，接下来可以初始化连接了：

**Kotlin：**

```
// ./app/src/main/java/com/chatterbox/myapp/MainActivity.kt

package com.chatterbox.myapp
// ...

class MainActivity : AppCompatActivity() {
// ...

   private fun connect() {
      try {
         room?.connect()
      } catch (ex: Exception) {
         Log.e(TAG, "Error while calling connect()", ex)
      }
   }

   // ...
}
```

## 构建令牌提供程序
<a name="chat-kotlin-rooms-token-provider"></a>

现在可以创建一个函数，负责在我们的应用程序中创建和管理聊天令牌。在此示例中，我们使用 [Android 版 Retrofit HTTP 客户端](https://square.github.io/retrofit/)。

在发送任何网络流量之前，我们必须设置适用于 Android 的网络安全配置。（有关更多信息，请参阅[网络安全配置](https://developer.android.com/privacy-and-security/security-config)。） 我们首先向[应用程序清单](https://developer.android.com/guide/topics/manifest/manifest-intro)文件添加网络权限。请注意，添加的 `user-permission` 标签和 `networkSecurityConfig` 属性将指向我们的新网络安全配置。*在下面的代码中，将 *`<version>`* 替换为聊天功能 Android SDK 的当前版本号（例如 1.1.0）。*

**XML：**

```
// ./app/src/main/AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.chatterbox.myapp">
    <uses-permission android:name="android.permission.INTERNET" />
    <application
        android:allowBackup="true"
        android:fullBackupContent="@xml/backup_rules"
        android:label="@string/app_name"
        android:networkSecurityConfig="@xml/network_security_config"
// ...

// ./app/build.gradle


dependencies {
   implementation("com.amazonaws:ivs-chat-messaging:<version>")
// ...

   implementation("com.squareup.retrofit2:retrofit:2.9.0")
   implementation("com.squareup.retrofit2:converter-gson:2.9.0")
}
```

声明您的本地 IP 地址（例如 `10.0.2.2` 和 `localhost`）为可信域，开始与我们的后端交换消息：

**XML：**

```
// ./app/src/main/res/xml/network_security_config.xml

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <domain-config cleartextTrafficPermitted="true">
        <domain includeSubdomains="true">10.0.2.2</domain>
        <domain includeSubdomains="true">localhost</domain>
    </domain-config>
</network-security-config>
```

接下来，我们需要添加一个新的依赖项以及用于解析 HTTP 响应的 [Gson 转换器](https://github.com/square/retrofit/tree/trunk/retrofit-converters/gson)。*在下面的代码中，将 *`<version>`* 替换为聊天功能 Android SDK 的当前版本号（例如 1.1.0）。*

**Kotlin 脚本：**

```
// ./app/build.gradle

dependencies {
   implementation("com.amazonaws:ivs-chat-messaging:<version>")
// ...

   implementation("com.squareup.retrofit2:retrofit:2.9.0")
   implementation("com.squareup.retrofit2:converter-gson:2.9.0")
}
```

要检索聊天令牌，我们需要从 `chatterbox` 应用程序发出 POST HTTP 请求。我们在 Retrofit 接口中定义请求，以便实现。（请参阅 [Retrofit 文档](https://square.github.io/retrofit/)，也请熟悉 [CreateChatToken](https://docs.aws.amazon.com/ivs/latest/ChatAPIReference/API_CreateChatToken.html#API_CreateChatToken_RequestBody) 操作规范。）

**Kotlin：**

```
// ./app/src/main/java/com/chatterbox/myapp/network/ApiService.kt

package com.chatterbox.myapp.network

import com.amazonaws.ivs.chat.messaging.ChatToken
import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.POST

data class CreateTokenParams(var userId: String, var roomIdentifier: String)

interface ApiService {
   @POST("create_chat_token")
   fun createChatToken(@Body params: CreateTokenParams): Call<ChatToken>
}


// ./app/src/main/java/com/chatterbox/myapp/network/RetrofitFactory.kt

package com.chatterbox.myapp.network

import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

object RetrofitFactory {
   private const val BASE_URL = "http://10.0.2.2:3000"

   fun makeRetrofitService(): ApiService {
       return Retrofit.Builder()
           .baseUrl(BASE_URL)
           .addConverterFactory(GsonConverterFactory.create())
           .build().create(ApiService::class.java)
   }
}
```

现在，网络设置完成后，可以添加一个函数，负责创建和管理我们的聊天令牌。我们将其添加到 `MainActivity.kt`，它是在项目[生成](#chat-kotlin-rooms-chatterbox)时自动创建的：

**Kotlin：**

```
// ./app/src/main/java/com/chatterbox/myapp/MainActivity.kt

package com.chatterbox.myapp

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import com.amazonaws.ivs.chat.messaging.*
import com.amazonaws.ivs.chat.messaging.coroutines.*
import com.chatterbox.myapp.network.CreateTokenParams
import com.chatterbox.myapp.network.RetrofitFactory
import retrofit2.Call
import java.io.IOException
import retrofit2.Callback
import retrofit2.Response

// custom tag for logging purposes
const val TAG = "Chatterbox-MyApp"

// any ID to be associated with auth token
const val USER_ID = "test user id"
// ID of the room the app wants to access. Must be an ARN. See Amazon Resource Names(ARNs)
const val ROOM_ID = "arn:aws:..."
// AWS region of the room that was created in Getting Started with Amazon IVS Chat
const val REGION = "us-west-2"

class MainActivity : AppCompatActivity() {

   private val service = RetrofitFactory.makeRetrofitService()
   private var userId: String = USER_ID

// ...

   private fun fetchChatToken(callback: ChatTokenCallback) {
      val params = CreateTokenParams(userId, ROOM_ID)
      service.createChatToken(params).enqueue(object : Callback<ChatToken> {
         override fun onResponse(call: Call<ChatToken>, response: Response<ChatToken>) {
            val token = response.body()
            if (token == null) {
               Log.e(TAG, "Received empty token response")
               callback.onFailure(IOException("Empty token response"))
               return
            }

            Log.d(TAG, "Received token response $token")
            callback.onSuccess(token)
         }

         override fun onFailure(call: Call<ChatToken>, throwable: Throwable) {
            Log.e(TAG, "Failed to fetch token", throwable)
            callback.onFailure(throwable)
         }
      })
   }
}
```

## 后续步骤
<a name="chat-kotlin-rooms-next-steps"></a>

现在，您已经建立聊天室连接，请继续阅读本 Kotlin 协同教程的第 2 部分：[消息和事件](chat-sdk-kotlin-tutorial-messages-events.md)。

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

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

1. [为发送消息创建一个 UI](#chat-kotlin-messages-events-ui)

   1. [UI 主布局](#chat-kotlin-messages-events-ui-main)

   1. [UI 将文本单元格抽象化以便一致地显示文本](#chat-kotlin-messages-events-consistent-text)

   1. [UI 左侧聊天消息](#chat-kotlin-messages-events-ui-left)

   1. [UI 右侧消息](#chat-kotlin-messages-events-ui-right)

   1. [UI 其他颜色值](#chat-kotlin-messages-events-additional-color)

1. [应用视图绑定](#chat-kotlin-messages-events-apply-view-binding)

1. [管理聊天消息请求](#chat-kotlin-messages-events-chat-message)

1. [最终步骤](#chat-kotlin-messages-events-final-steps)

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

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

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

## 为发送消息创建一个 UI
<a name="chat-kotlin-messages-events-ui"></a>

现在我们已成功初始化聊天室连接，接下来可以发送第一条消息了。要使用此功能，需要一个 UI。我们将添加：
+ `connect`/`disconnect` 按钮
+ 使用 `send` 按钮输入消息
+ 动态消息列表。为了构建此内容，我们使用了 Android Jetpack [RecyclerView](https://developer.android.com/develop/ui/views/layout/recyclerview)。

### UI 主布局
<a name="chat-kotlin-messages-events-ui-main"></a>

请参阅 Android 开发者文档中的 Android Jetpack [布局](https://developer.android.com/develop/ui/views/layout/declaring-layout)。

**XML：**

```
// ./app/src/main/res/layout/activity_main.xml


<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
                                                     xmlns:app="http://schemas.android.com/apk/res-auto"
                                                     xmlns:tools="http://schemas.android.com/tools"
                                                     android:layout_width="match_parent"
                                                     android:layout_height="match_parent">

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
                  xmlns:app="http://schemas.android.com/apk/res-auto"
                  android:id="@+id/connect_view"
                  android:layout_width="match_parent"
                  android:layout_height="match_parent"
                  android:gravity="center"
                  android:orientation="vertical">

        <androidx.cardview.widget.CardView
                android:id="@+id/connect_button"
                android:layout_width="match_parent"
                android:layout_height="48dp"
                android:layout_gravity=""
                android:layout_marginStart="16dp"
                android:layout_marginTop="4dp"
                android:layout_marginEnd="16dp"
                android:clickable="true"
                android:elevation="16dp"
                android:focusable="true"
                android:foreground="?android:attr/selectableItemBackground"
                app:cardBackgroundColor="@color/purple_500"
                app:cardCornerRadius="10dp">

            <TextView
                    android:id="@+id/connect_text"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:layout_alignParentEnd="true"
                    android:layout_gravity="center"
                    android:layout_weight="1"
                    android:paddingHorizontal="12dp"
                    android:text="Connect"
                    android:textColor="@color/white"
                    android:textSize="16sp"/>

            <ProgressBar
                    android:id="@+id/activity_indicator"
                    android:layout_width="20dp"
                    android:layout_height="20dp"
                    android:layout_gravity="center"
                    android:layout_marginHorizontal="20dp"
                    android:indeterminateOnly="true"
                    android:indeterminateTint="@color/white"
                    android:indeterminateTintMode="src_atop"
                    android:keepScreenOn="true"
                    android:visibility="gone"/>
        </androidx.cardview.widget.CardView>

    </LinearLayout>

    <androidx.constraintlayout.widget.ConstraintLayout
            android:id="@+id/chat_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:clipToPadding="false"
            android:visibility="visible"
            tools:context=".MainActivity">

        <RelativeLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="vertical"
                app:layout_constraintBottom_toTopOf="@+id/layout_message_input"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent">

            <androidx.recyclerview.widget.RecyclerView
                    android:id="@+id/recycler_view"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:clipToPadding="false"
                    android:paddingTop="70dp"
                    android:paddingBottom="20dp"/>
        </RelativeLayout>

        <RelativeLayout
                android:id="@+id/layout_message_input"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@android:color/white"
                android:clipToPadding="false"
                android:drawableTop="@android:color/black"
                android:elevation="18dp"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintStart_toStartOf="parent">

            <EditText
                    android:id="@+id/message_edit_text"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_centerVertical="true"
                    android:layout_marginStart="16dp"
                    android:layout_toStartOf="@+id/send_button"
                    android:background="@android:color/transparent"
                    android:hint="Enter Message"
                    android:inputType="text"
                    android:maxLines="6"
                    tools:ignore="Autofill"/>

            <Button
                    android:id="@+id/send_button"
                    android:layout_width="84dp"
                    android:layout_height="48dp"
                    android:layout_alignParentEnd="true"
                    android:background="@color/black"
                    android:foreground="?android:attr/selectableItemBackground"
                    android:text="Send"
                    android:textColor="@color/white"
                    android:textSize="12dp"/>
        </RelativeLayout>
    </androidx.constraintlayout.widget.ConstraintLayout>


</androidx.coordinatorlayout.widget.CoordinatorLayout>
```

### UI 将文本单元格抽象化以便一致地显示文本
<a name="chat-kotlin-messages-events-consistent-text"></a>

**XML：**

```
// ./app/src/main/res/layout/common_cell.xml
   
<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:id="@+id/layout_container"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:background="@color/light_gray"
              android:minWidth="100dp"
              android:orientation="vertical">

    <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="horizontal">

        <TextView
                android:id="@+id/card_message_me_text_view"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:layout_marginBottom="8dp"
                android:maxWidth="260dp"
                android:paddingLeft="12dp"
                android:paddingTop="8dp"
                android:paddingRight="12dp"
                android:text="This is a Message"
                android:textColor="#ffffff"
                android:textSize="16sp"/>

        <TextView
                android:id="@+id/failed_mark"
                android:layout_width="40dp"
                android:layout_height="match_parent"
                android:paddingRight="5dp"
                android:src="@drawable/ic_launcher_background"
                android:text="!"
                android:textAlignment="viewEnd"
                android:textColor="@color/white"
                android:textSize="25dp"
                android:visibility="gone"/>
    </LinearLayout>

</LinearLayout>
```

### UI 左侧聊天消息
<a name="chat-kotlin-messages-events-ui-left"></a>

**XML：**

```
// ./app/src/main/res/layout/card_view_left.xml
 
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              xmlns:app="http://schemas.android.com/apk/res-auto"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:layout_marginStart="8dp"
              android:layout_marginBottom="12dp"
              android:orientation="vertical">

    <TextView
            android:id="@+id/username_edit_text"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="UserName"/>

    <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

        <androidx.cardview.widget.CardView
                android:id="@+id/card_message_other"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="left"
                android:layout_marginBottom="4dp"
                android:foreground="?android:attr/selectableItemBackground"
                app:cardBackgroundColor="@color/light_gray_2"
                app:cardCornerRadius="10dp"
                app:cardElevation="0dp"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintStart_toStartOf="parent">

            <include layout="@layout/common_cell"/>
        </androidx.cardview.widget.CardView>

        <TextView
                android:id="@+id/dateText"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginLeft="4dp"
                android:layout_marginBottom="4dp"
                android:text="10:00"
                app:layout_constraintBottom_toBottomOf="@+id/card_message_other"
                app:layout_constraintLeft_toRightOf="@+id/card_message_other"/>
    </androidx.constraintlayout.widget.ConstraintLayout>


</LinearLayout>
```

### UI 右侧消息
<a name="chat-kotlin-messages-events-ui-right"></a>

**XML：**

```
// ./app/src/main/res/layout/card_view_right.xml
 
<?xml version="1.0" encoding="utf-8"?>

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"                                                   xmlns:app="http://schemas.android.com/apk/res-auto"                                                   android:layout_width="match_parent"                                                   android:layout_height="wrap_content" 
android:layout_marginEnd="8dp">

    <androidx.cardview.widget.CardView
            android:id="@+id/card_message_me"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="right"
            android:layout_marginBottom="10dp"
            android:foreground="?android:attr/selectableItemBackground"
            app:cardBackgroundColor="@color/purple_500"
            app:cardCornerRadius="10dp"
            app:cardElevation="0dp"
            app:cardPreventCornerOverlap="false"
            app:cardUseCompatPadding="true"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent">

        <include layout="@layout/common_cell"/>

    </androidx.cardview.widget.CardView>

    <TextView
            android:id="@+id/dateText"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginRight="12dp"
            android:layout_marginBottom="4dp"
            android:text="10:00"
            app:layout_constraintBottom_toBottomOf="@+id/card_message_me"
            app:layout_constraintRight_toLeftOf="@+id/card_message_me"/>

</androidx.constraintlayout.widget.ConstraintLayout>
```

### UI 其他颜色值
<a name="chat-kotlin-messages-events-additional-color"></a>

**XML：**

```
// ./app/src/main/res/values/colors.xml
 
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!--    ...-->
    <color name="dark_gray">#4F4F4F</color>
    <color name="blue">#186ED3</color>
    <color name="dark_red">#b30000</color>
    <color name="light_gray">#B7B7B7</color>
    <color name="light_gray_2">#eef1f6</color>
</resources>
```

## 应用视图绑定
<a name="chat-kotlin-messages-events-apply-view-binding"></a>

我们利用 Android [视图绑定](https://developer.android.com/topic/libraries/view-binding)功能来引用 XML 布局的绑定类。要启用该功能，请在 `./app/build.gradle` 中将 `viewBinding` 构建选项设置为 `true`：

**Kotlin 脚本：**

```
 // ./app/build.gradle

android {
//    ...

    buildFeatures {
        viewBinding = true
    }
//    ...
}
```

现在可以将 UI 与我们的 Kotlin 代码连接起来：

**Kotlin：**

```
// ./app/src/main/java/com/chatterbox/myapp/MainActivity.kt

package com.chatterbox.myapp
// ...

class MainActivity : AppCompatActivity() {
    // ...
    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        // Create room instance
        room = ChatRoom(REGION, ::fetchChatToken).apply {
            // ...
        }

        binding.sendButton.setOnClickListener(::sendButtonClick)
        binding.connectButton.setOnClickListener {connect()}

        setUpChatView()

        updateConnectionState(ChatRoom.State.DISCONNECTED)
    }

    private fun sendMessage(request: SendMessageRequest) {
        lifecycleScope.launch {
           try {
               binding.messageEditText.text.clear()
               room?.awaitSendMessage(request)
           } catch (exception: ChatException) {
               Log.e(TAG, "Message rejected: ${exception.message}")
           } catch (exception: Exception) {
               Log.e(TAG, exception.message ?: "Unknown error occurred")
           }
        }
    }

    private fun sendButtonClick(view: View) {
        val content = binding.messageEditText.text.toString()
        if (content.trim().isEmpty()) {
            return
        }

        val request = SendMessageRequest(content)
        sendMessage(request)
    }
// ...

}
```

我们还添加了删除消息和断开用户聊天连接的方法，可以使用聊天消息上下文菜单调用这些方法：

**Kotlin：**

```
// ./app/src/main/java/com/chatterbox/myapp/MainActivity.kt

package com.chatterbox.myapp
//    ...

class MainActivity : AppCompatActivity() {
//    ...

    private fun deleteMessage(request: DeleteMessageRequest) {
        lifecycleScope.launch {
           try {
               room?.awaitDeleteMessage(request)
           } catch (exception: ChatException) {
               Log.e(TAG, "Delete message rejected: ${exception.message}")
           } catch (exception: Exception) {
               Log.e(TAG, exception.message ?: "Unknown error occurred")
           }
        }
    }

    private fun disconnectUser(request: DisconnectUserRequest) {
        lifecycleScope.launch {
           try {
               room?.awaitDisconnectUser(request)
           } catch (exception: ChatException) {
               Log.e(TAG, "Disconnect user rejected: ${exception.message}")
           } catch (exception: Exception) {
               Log.e(TAG, exception.message ?: "Unknown error occurred")
           }
        }
    }
}
```

## 管理聊天消息请求
<a name="chat-kotlin-messages-events-chat-message"></a>

我们需要一种用于管理不同状态的聊天消息请求的方法：
+ 待处理 — 消息已发送到聊天室，但尚未被确认或拒绝。
+ 已确认 — 消息已被聊天室发送给所有用户（包括我们）。
+ 已拒绝 — 消息已被聊天室拒绝，并提示带有错误对象。

我们会把未解决的聊天请求和聊天消息保留在[列表](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/mutable-list-of.html)中。该列表属于一个单独的类，我们称之为 `ChatEntries.kt`：

**Kotlin：**

```
// ./app/src/main/java/com/chatterbox/myapp/ChatEntries.kt

package com.chatterbox.myapp

import com.amazonaws.ivs.chat.messaging.entities.ChatMessage
import com.amazonaws.ivs.chat.messaging.requests.SendMessageRequest

sealed class ChatEntry() {
    class Message(val message: ChatMessage) : ChatEntry()
    class PendingRequest(val request: SendMessageRequest) : ChatEntry()
    class FailedRequest(val request: SendMessageRequest) : ChatEntry()
}

class ChatEntries {
    /* This list is kept in sorted order. ChatMessages are sorted by date, while pending and failed requests are kept in their original insertion point. */
    val entries = mutableListOf<ChatEntry>()
    var adapter: ChatListAdapter? = null

    val size get() = entries.size

    /**
     * Insert pending request at the end.
     */
    fun addPendingRequest(request: SendMessageRequest) {
        val insertIndex = entries.size
        entries.add(insertIndex, ChatEntry.PendingRequest(request))
        adapter?.notifyItemInserted(insertIndex)
    }

    /**
     * Insert received message at proper place based on sendTime. This can cause removal of pending requests.
     */
    fun addReceivedMessage(message: ChatMessage) {
        /* Skip if we have already handled that message. */
        val existingIndex = entries.indexOfLast { it is ChatEntry.Message && it.message.id == message.id }
        if (existingIndex != -1) {
            return
        }

        val removeIndex = entries.indexOfLast {
            it is ChatEntry.PendingRequest && it.request.requestId == message.requestId
        }
        if (removeIndex != -1) {
            entries.removeAt(removeIndex)
        }

        val insertIndexRaw = entries.indexOfFirst { it is ChatEntry.Message && it.message.sendTime > message.sendTime }
        val insertIndex = if (insertIndexRaw == -1) entries.size else insertIndexRaw
        entries.add(insertIndex, ChatEntry.Message(message))

        if (removeIndex == -1) {
            adapter?.notifyItemInserted(insertIndex)
        } else if (removeIndex == insertIndex) {
            adapter?.notifyItemChanged(insertIndex)
        } else {
            adapter?.notifyItemRemoved(removeIndex)
            adapter?.notifyItemInserted(insertIndex)
        }
    }

    fun addFailedRequest(request: SendMessageRequest) {
        val removeIndex = entries.indexOfLast {
            it is ChatEntry.PendingRequest && it.request.requestId == request.requestId
        }
        if (removeIndex != -1) {
            entries.removeAt(removeIndex)
            entries.add(removeIndex, ChatEntry.FailedRequest(request))
            adapter?.notifyItemChanged(removeIndex)
        } else {
            val insertIndex = entries.size
            entries.add(insertIndex, ChatEntry.FailedRequest(request))
            adapter?.notifyItemInserted(insertIndex)
        }
    }

    fun removeMessage(messageId: String) {
        val removeIndex = entries.indexOfFirst { it is ChatEntry.Message && it.message.id == messageId }
        entries.removeAt(removeIndex)
        adapter?.notifyItemRemoved(removeIndex)
    }

    fun removeFailedRequest(requestId: String) {
        val removeIndex = entries.indexOfFirst { it is ChatEntry.FailedRequest && it.request.requestId == requestId }
        entries.removeAt(removeIndex)
        adapter?.notifyItemRemoved(removeIndex)
    }

    fun removeAll() {
        entries.clear()
    }
}
```

为了将列表与 UI 连接起来，我们使用[适配器](https://developer.android.com/reference/android/widget/Adapter)。有关更多信息，请参阅[使用 AdapterView 绑定到数据](https://developer.android.com/develop/ui/views/layout/binding)和[生成的绑定类](https://developer.android.com/topic/libraries/data-binding/generated-binding)。

**Kotlin：**

```
// ./app/src/main/java/com/chatterbox/myapp/ChatListAdapter.kt

package com.chatterbox.myapp

import android.content.Context
import android.graphics.Color
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.core.view.isGone
import androidx.recyclerview.widget.RecyclerView
import com.amazonaws.ivs.chat.messaging.requests.DisconnectUserRequest
import java.text.DateFormat


class ChatListAdapter(
    private val entries: ChatEntries,
    private val onDisconnectUser: (request: DisconnectUserRequest) -> Unit,
) :
    RecyclerView.Adapter<ChatListAdapter.ViewHolder>() {
    var context: Context? = null
    var userId: String? = null

    class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        val container: LinearLayout = view.findViewById(R.id.layout_container)
        val textView: TextView = view.findViewById(R.id.card_message_me_text_view)
        val failedMark: TextView = view.findViewById(R.id.failed_mark)
        val userNameText: TextView? = view.findViewById(R.id.username_edit_text)
        val dateText: TextView? = view.findViewById(R.id.dateText)
    }

    override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ViewHolder {
        if (viewType == 0) {
            val rightView = LayoutInflater.from(viewGroup.context).inflate(R.layout.card_view_right, viewGroup, false)
            return ViewHolder(rightView)
        }
        val leftView = LayoutInflater.from(viewGroup.context).inflate(R.layout.card_view_left, viewGroup, false)
        return ViewHolder(leftView)
    }

    override fun getItemViewType(position: Int): Int {
        // Int 0 indicates to my message while Int 1 to other message
        val chatMessage = entries.entries[position]
        return if (chatMessage is ChatEntry.Message && chatMessage.message.sender.userId != userId) 1 else 0
    }

    override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {
        return when (val entry = entries.entries[position]) {
            is ChatEntry.Message -> {
                viewHolder.textView.text = entry.message.content

                val bgColor = if (entry.message.sender.userId == userId) {
                    R.color.purple_500
                } else {
                    R.color.light_gray_2
                }
                viewHolder.container.setBackgroundColor(ContextCompat.getColor(context!!, bgColor))

                if (entry.message.sender.userId != userId) {
                    viewHolder.textView.setTextColor(Color.parseColor("#000000"))
                }

                viewHolder.failedMark.isGone = true

                viewHolder.itemView.setOnCreateContextMenuListener { menu, _, _ ->
                    menu.add("Kick out").setOnMenuItemClickListener {
                        val request = DisconnectUserRequest(entry.message.sender.userId, "Some reason")
                        onDisconnectUser(request)
                        true
                    }
                }

                viewHolder.userNameText?.text = entry.message.sender.userId
                viewHolder.dateText?.text =
                    DateFormat.getTimeInstance(DateFormat.SHORT).format(entry.message.sendTime)
            }

            is ChatEntry.PendingRequest -> {
                viewHolder.container.setBackgroundColor(ContextCompat.getColor(context!!, R.color.light_gray))
                viewHolder.textView.text = entry.request.content
                viewHolder.failedMark.isGone = true
                viewHolder.itemView.setOnCreateContextMenuListener(null)
                viewHolder.dateText?.text = "Sending"
            }

            is ChatEntry.FailedRequest -> {
                viewHolder.textView.text = entry.request.content
                viewHolder.container.setBackgroundColor(ContextCompat.getColor(context!!, R.color.dark_red))
                viewHolder.failedMark.isGone = false
                viewHolder.dateText?.text = "Failed"
            }
        }
    }

    override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
        super.onAttachedToRecyclerView(recyclerView)
        context = recyclerView.context
    }

    override fun getItemCount() = entries.entries.size
}
```

## 最终步骤
<a name="chat-kotlin-messages-events-final-steps"></a>

现在可以挂载我们的新适配器，将 `ChatEntries` 类绑定到 `MainActivity`：

**Kotlin：**

```
// ./app/src/main/java/com/chatterbox/myapp/MainActivity.kt

package com.chatterbox.myapp
// ...

import com.chatterbox.myapp.databinding.ActivityMainBinding
import com.chatterbox.myapp.ChatListAdapter
import com.chatterbox.myapp.ChatEntries

class MainActivity : AppCompatActivity() {
    // ...
    private var entries = ChatEntries()
    private lateinit var adapter: ChatListAdapter

    // ...

    private fun setUpChatView() {
        adapter = ChatListAdapter(entries, ::disconnectUser)
        entries.adapter = adapter

        val recyclerViewLayoutManager = LinearLayoutManager(this@MainActivity, LinearLayoutManager.VERTICAL, false)
        binding.recyclerView.layoutManager = recyclerViewLayoutManager
        binding.recyclerView.adapter = adapter

        binding.sendButton.setOnClickListener(::sendButtonClick)
        binding.messageEditText.setOnEditorActionListener { _, _, event ->
            val isEnterDown = (event.action == KeyEvent.ACTION_DOWN) && (event.keyCode == KeyEvent.KEYCODE_ENTER)
            if (!isEnterDown) {
                return@setOnEditorActionListener false
            }

            sendButtonClick(binding.sendButton)
            return@setOnEditorActionListener true
        }
    }
}
```

由于我们已经有一个用于负责跟踪我们聊天请求的类（`ChatEntries`），我们已经准备好实现用于在 roomListener 中操作 `entries` 的代码。我们将根据自己正在响应的事件来更新 `entries` 和 `connectionState`：

**Kotlin：**

```
// ./app/src/main/java/com/chatterbox/myapp/MainActivity.kt

package com.chatterbox.myapp
// ...

class MainActivity : AppCompatActivity() {
// ...


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        // Create room instance
        room = ChatRoom(REGION, ::fetchChatToken).apply {
            lifecycleScope.launch {
                stateChanges().collect { state ->
                    Log.d(TAG, "state change to $state")
                    updateConnectionState(state)
                    if (state == ChatRoom.State.DISCONNECTED) {
                       entries.removeAll()
                    }
                }
            }

            lifecycleScope.launch {
                receivedMessages().collect { message ->
                    Log.d(TAG, "messageReceived $message")
                    entries.addReceivedMessage(message)
                }
            }

            lifecycleScope.launch {
                receivedEvents().collect { event ->
                    Log.d(TAG, "eventReceived $event")
                }
            }

            lifecycleScope.launch {
                deletedMessages().collect { event ->
                    Log.d(TAG, "messageDeleted $event")
                    entries.removeMessage(event.messageId)
                }
            }

            lifecycleScope.launch {
                disconnectedUsers().collect { event ->
                    Log.d(TAG, "userDisconnected $event")
                }
            }
        }

        binding.sendButton.setOnClickListener(::sendButtonClick)
        binding.connectButton.setOnClickListener {connect()}

        setUpChatView()

        updateConnectionState(ChatRoom.State.DISCONNECTED)
    }

// ...

}
```

现在应该可以运行您的应用程序了！（请参阅[构建和运行您的应用程序](https://developer.android.com/studio/run#basic-build-run)。） 使用应用程序时，请记住让您的后端服务器保持运行。您可以通过使用命令 `./gradlew :auth-server:run`，从我们项目根目录的终端启动，或是直接从 Android Studio 执行 `auth-server:run` Gradle 任务来启动。

# IVS 聊天功能客户端消息收发 SDK：iOS 指南
<a name="chat-sdk-ios"></a>

Amazon Interactive Video（IVS）Chat 客户端消息收发 iOS SDK 提供界面，可让您在使用 Apple 的 [Swift 编程语言](https://developer.apple.com/swift/)的平台上整合我们的 [IVS Chat 消息收发 API](https://docs.aws.amazon.com//ivs/latest/chatmsgapireference/welcome.html)。

**IVS Chat 客户端消息收发 iOS SDK 的最新版本：**1.0.1（[发布说明](https://docs.aws.amazon.com//ivs/latest/ChatUserGuide/release-notes.html#aug08-25)）

**参考文档和教程：**有关 Amazon IVS 聊天功能客户端消息收发 iOS SDK 中最重要方法的信息，请参阅参考文档，网址为 [https://aws.github.io/amazon-ivs-chat-messaging-sdk-ios/1.0.1/](https://aws.github.io/amazon-ivs-chat-messaging-sdk-ios/1.0.1/)。此存储库还包含各种文章和教程。

**示例代码：**请参阅 GitHub 上的 iOS 示例存储库：[https://github.com/aws-samples/amazon-ivs-chat-for-ios-demo](https://github.com/aws-samples/amazon-ivs-chat-for-ios-demo)。

**平台要求：**开发需要 iOS 13.0 或更高版本。

# IVS Chat 客户端消息收发 iOS SDK 入门
<a name="chat-ios-getting-started"></a>

我们建议您通过 [Swift Package Manager](#chat-ios-install-sdk-swiftpm) 集成 SDK。您也可以[手动集成框架](#chat-ios-install-sdk-manual)。

集成 SDK 后，您可以通过在相关 Swift 文件顶部添加以下代码来导入 SDK：

```
import AmazonIVSChatMessaging
```

## Swift Package Manager
<a name="chat-ios-install-sdk-swiftpm"></a>

要在 Swift Package Manager 项目中使用 `AmazonIVSChatMessaging` 库，请将其添加到程序包的依赖项和相关目标的依赖项中：

1. 最新 `.xcframework` 的下载链接：[https://ivschat.live-video.net/1.0.1/AmazonIVSChatMessaging.xcframework.zip](https://ivschat.live-video.net/1.0.1/AmazonIVSChatMessaging.xcframework.zip)。

1. 在终端中，运行：

   ```
   shasum -a 256 path/to/downloaded/AmazonIVSChatMessaging.xcframework.zip
   ```

1. 获取上一步的输出并将其粘贴到 `.binaryTarget` 的校验和属性中，如下面项目的 `Package.swift` 文件中所示：

   ```
   let package = Package(
      // name, platforms, products, etc.
      dependencies: [
         // other dependencies
      ],
      targets: [
         .target(
            name: "<target-name>",
            dependencies: [
               // If you want to only bring in the SDK
               .binaryTarget(
                  name: "AmazonIVSChatMessaging",
                  url: "https://ivschat.live-video.net/1.0.1/AmazonIVSChatMessaging.xcframework.zip",
                  checksum: "<SHA-extracted-using-steps-detailed-above>"
               ),
               // your other dependencies
            ],
         ),
         // other targets
      ]
   )
   ```

## 手动安装
<a name="chat-ios-install-sdk-manual"></a>

1. 最新版本下载链接：[https://ivschat.live-video.net/1.0.1/AmazonIVSChatMessaging.xcframework.zip](https://ivschat.live-video.net/1.0.1/AmazonIVSChatMessaging.xcframework.zip)。

1. 提取归档的内容。`AmazonIVSChatMessaging.xcframework` 包含适用于设备和模拟器的开发工具包。

1. 通过以下方法嵌入提取的 `AmazonIVSChatMessaging.xcframework`：将其拖动到应用程序目标 **General**（常规）选项卡上的 **Frameworks, Libraries, and Embedded Content**（框架、库和嵌入式内容）部分：  
![\[应用程序目标 General（常规）选项卡上的 Frameworks, Libraries, and Embedded Content（框架、库和嵌入式内容）部分：\]](http://docs.aws.amazon.com/zh_cn/ivs/latest/ChatUserGuide/images/Chat_SDK_iOS_Manual_Installation.png)

# 使用 IVS 聊天功能客户端消息收发 iOS SDK
<a name="chat-ios-using-sdk"></a>

本文档将引导您完成使用 Amazon IVS 聊天功能客户端消息收发 iOS SDK 涉及的步骤。

## 连接到聊天室
<a name="chat-ios-connect-room"></a>

在开始之前，应该熟悉 [Amazon IVS 聊天功能入门](getting-started-chat.md)。另请参阅适用于 [Web](https://github.com/aws-samples/amazon-ivs-chat-web-demo)、[Android](https://github.com/aws-samples/amazon-ivs-chat-for-android-demo) 和 [iOS](https://github.com/aws-samples/amazon-ivs-chat-for-ios-demo) 的示例应用程序。

要连接到聊天室，您的应用程序需要以某种方式来检索后端提供的聊天令牌。应用程序可能会使用对后端的网络请求来检索聊天令牌。

要将获取的此聊天令牌与 SDK 进行通信，SDK 的 `ChatRoom` 模型要求您在初始化时提供 `async` 函数或符合所提供的 `ChatTokenProvider` 协议的对象实例。这两种方法返回的值都必须是 SDK `ChatToken` 模型实例。

**注意：**您使用从后端检索的数据填充 `ChatToken` 模型的实例。初始化 `ChatToken` 实例所需的字段与 [CreateChatToken](https://docs.aws.amazon.com//ivs/latest/ChatAPIReference/API_CreateChatToken.html) 响应中的字段相同。有关初始化 `ChatToken` 模型实例的更多信息，请参阅[创建 ChatToken 实例](#chat-ios-create-chattoken)。请记住，*您的后端*负责将 `CreateChatToken` 响应中的数据提供给您的应用程序。您决定如何与后端通信以生成聊天令牌取决于您的应用程序及其基础设施。

选择向 SDK 提供 `ChatToken` 的策略后，请在使用令牌提供程序和 *亚马逊云科技区域*（后端用于创建您要尝试连接到的聊天室）成功初始化 `ChatRoom` 实例后调用 `.connect()`。请注意，`.connect()` 是抛出异步函数：

```
import AmazonIVSChatMessaging

let room = ChatRoom(
   awsRegion: <region-your-backend-created-the-chat-room-in>,
   tokenProvider: <your-chosen-token-provider-strategy>
)
try await room.connect()
```

### 符合 ChatTokenProvider 协议
<a name="chat-ios-chattokenprovider-protocol"></a>

对于 `ChatRoom` 初始化程序中的 `tokenProvider` 参数，您可以提供 `ChatTokenProvider` 的实例。以下是符合 `ChatTokenProvider` 的对象的示例：

```
import AmazonIVSChatMessaging

// This object should exist somewhere in your app
class ChatService: ChatTokenProvider {
   func getChatToken() async throws -> ChatToken {
      let request = YourApp.getTokenURLRequest
      let data = try await URLSession.shared.data(for: request).0
      ...
      return ChatToken(
         token: String(data: data, using: .utf8)!,
         tokenExpirationTime: ..., // this is optional
         sessionExpirationTime: ... // this is optional
      )
   }
}
```

然后，您可以采用符合条件的此对象的实例并将其传递给 `ChatRoom` 的初始化程序：

```
// This should be the same AWS Region that you used to create
// your Chat Room in the Control Plane
let awsRegion = "us-west-2"
let service = ChatService()
let room = ChatRoom(
   awsRegion: awsRegion,
   tokenProvider: service
)
try await room.connect()
```

### 在 Swift 中提供异步函数
<a name="chat-ios-retrievechattoken-async-function"></a>

假设您已拥有用于管理应用程序的网络请求的管理器。它可能如下所示：

```
import AmazonIVSChatMessaging

class EndpointManager {
   func getAccounts() async -> AppUser {...}
   func signIn(user: AppUser) async {...}
   ...
}
```

您只需在管理器中添加另一个函数即可从后端检索 `ChatToken`：

```
import AmazonIVSChatMessaging

class EndpointManager {
   ...
   func retrieveChatToken() async -> ChatToken {...}
}
```

然后，在初始化 `ChatRoom` 时在 Swift 中使用对该函数的引用：

```
import AmazonIVSChatMessaging

let endpointManager: EndpointManager
let room = ChatRoom(
   awsRegion: endpointManager.awsRegion,
   tokenProvider: endpointManager.retrieveChatToken
)
try await room.connect()
```

## 创建 ChatTToken 实例
<a name="chat-ios-create-chattoken"></a>

您可以使用 SDK 中提供的初始化程序轻松创建 `ChatToken` 实例。请参阅 `Token.swift` 中的文档以了解有关 `ChatToken` 的各个属性的更多信息。

```
import AmazonIVSChatMessaging

let chatToken = ChatToken(
   token: <token-string-retrieved-from-your-backend>,
   tokenExpirationTime: nil, // this is optional
   sessionExpirationTime: nil // this is optional
)
```

### 使用可解码功能
<a name="chat-ios-create-chattoken-decodable"></a>

如果在与 IVS Chat API 交互时，后端决定简单地将 [CreateChatToken](https://docs.aws.amazon.com/ivs/latest/ChatAPIReference/API_CreateChatToken.html) 响应转发到前端应用程序，那么您就可以利用 `ChatToken` 对 Swift 的 `Decodable` 协议的符合性。然而，存在一个隐患。

`CreateChatToken` 响应有效负载使用字符串表示使用 [Internet 时间戳 ISO 8601 标准](https://en.wikipedia.org/wiki/ISO_8601)设置格式的日期。通常在 Swift 中，[您会提供](https://www.hackingwithswift.com/example-code/language/how-to-use-iso-8601-dates-with-jsondecoder-and-codable) `JSONDecoder.DateDecodingStrategy.iso8601` 作为 `JSONDecoder` `.dateDecodingStrategy` 属性的值。但是，`CreateChatToken` 在其字符串中使用精确到小数的秒，而 `JSONDecoder.DateDecodingStrategy.iso8601` 不支持此精度。

为方便起见，SDK 会对 `JSONDecoder.DateDecodingStrategy` 提供公共扩展以及允许您在解码 `ChatToken` 实例时成功使用 `JSONDecoder` 的附加 `.preciseISO8601` 策略：

```
import AmazonIVSChatMessaging

// The CreateChatToken data forwarded by your backend
let responseData: Data

let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .preciseISO8601
let token = try decoder.decode(ChatToken.self, from: responseData)
```

## 断开与聊天室的连接
<a name="chat-ios-disconnect-room"></a>

要手动断开已成功连接到的 `ChatRoom` 实例，请调用 `room.disconnect()`。默认情况下，聊天室在解除分配时会自动调用此函数。

```
import AmazonIVSChatMessaging

let room = ChatRoom(...)
try await room.connect()

// Disconnect
room.disconnect()
```

## 接收聊天消息/事件
<a name="chat-ios-receive-message"></a>

要在聊天室中发送和接收消息，您需要在成功初始化 `ChatRoom` 实例并调用 `room.connect()` 后提供符合 `ChatRoomDelegate` 协议的对象。以下是一个使用 `UIViewController` 的典型示例：

```
import AmazonIVSChatMessaging
import Foundation
import UIKit

class ViewController: UIViewController {
   let room: ChatRoom = ChatRoom(
      awsRegion: "us-west-2",
      tokenProvider: EndpointManager.shared
   )

   override func viewDidLoad() {
      super.viewDidLoad()
      Task { try await setUpChatRoom() }
   }

   private func setUpChatRoom() async throws {
      // Set the delegate to start getting notifications for room events
      room.delegate = self
      try await room.connect()
   }
}

extension ViewController: ChatRoomDelegate {
   func room(_ room: ChatRoom, didReceive message: ChatMessage) { ... }
   func room(_ room: ChatRoom, didReceive event: ChatEvent) { ... }
   func room(_ room: ChatRoom, didDelete message: DeletedMessageEvent) { ... }
}
```

## 连接发生变化时收到通知
<a name="chat-ios-room-connection-state"></a>

正如所料，在聊天室完全连接之前，您无法执行诸如在聊天室中发送消息之类的操作。SDK 的架构会尝试建议您通过异步 API 在后台线程上连接到聊天室。如果您想在 UI 中构建禁用发送消息按钮之类的功能，SDK 将使用 `Combine` 或 `ChatRoomDelegate` 提供两种策略，以便您在聊天室的连接状态发生变化时收到通知。这些内容如下所述。

**重要提示：**聊天室的连接状态也可能由于网络连接断开等原因而发生变化。构建应用程序时应将这一点考虑在内。

### 使用 Combine
<a name="room-connection-state-combine"></a>

每个 `ChatRoom` 实例都会以 `state` 属性形式附带自己的 `Combine` 发布程序：

```
import AmazonIVSChatMessaging
import Combine

var cancellables: Set<AnyCancellable> = []

let room = ChatRoom(...)
room.state.sink { state in
   switch state {
   case .connecting:
      let image = UIImage(named: "antenna.radiowaves.left.and.right")
      sendMessageButton.setImage(image, for: .normal)
      sendMessageButton.isEnabled = false
   case .connected:
      let image = UIImage(named: "paperplane.fill")
      sendMessageButton.setImage(image, for: .normal)
      sendMessageButton.isEnabled = true
   case .disconnected:
      let image = UIImage(named: "antenna.radiowaves.left.and.right.slash")
      sendMessageButton.setImage(image, for: .normal)
      sendMessageButton.isEnabled = false
   }
}.assign(to: &cancellables)

// Connect to `ChatRoom` on a background thread
Task(priority: .background) {
   try await room.connect()
}
```

### 使用 ChatRoomDelegate
<a name="room-connection-state-chatroomdelegate"></a>

或者，在符合 `ChatRoomDelegate` 的对象中使用可选函数 `roomDidConnect(_:)`、`roomIsConnecting(_:)` 和 `roomDidDisconnect(_:)`。下面是使用 `UIViewController` 的示例：

```
import AmazonIVSChatMessaging
import Foundation
import UIKit

class ViewController: UIViewController {
   let room: ChatRoom = ChatRoom(
      awsRegion: "us-west-2",
      tokenProvider: EndpointManager.shared
   )

   override func viewDidLoad() {
      super.viewDidLoad()
      Task { try await setUpChatRoom() }
   }

   private func setUpChatRoom() async throws {
      // Set the delegate to start getting notifications for room events
      room.delegate = self
      try await room.connect()
   }
}

extension ViewController: ChatRoomDelegate {
   func roomDidConnect(_ room: ChatRoom) {
      print("room is connected!")
   }
   func roomIsConnecting(_ room: ChatRoom) {
      print("room is currently connecting or fetching a token")
   }
   func roomDidDisconnect(_ room: ChatRoom) {
      print("room disconnected!")
   }
}
```

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

不同的用户能够在聊天室中执行的操作各不相同；例如，发送消息、删除消息或与用户断开连接。要执行其中一项操作，请对已连接的 `ChatRoom` 调用 `perform(request:)`，从而传递 SDK 中所提供的 `ChatRequest` 对象之一的实例。支持的请求位于 `Request.swift` 中。

当您的后端应用程序调用 `CreateChatToken` 时，在聊天室中执行的某些操作要求已连接的用户具有特定能力。在设计上，SDK 无法识别所连接用户具备的能力。因此，虽然您可以尝试在连接的 `ChatRoom` 实例中执行监管人操作，但控制面板 API 最终决定该操作是否会成功。

通过 `room.perform(request:)` 的所有操作都将等至聊天室收到的预期模型实例（其类型与请求对象本身相关联）与收到的模型和请求对象的 `requestId` 相匹配为止。如果请求存在问题，`ChatRoom` 将始终以 `ChatError` 的形式抛出错误。`ChatError` 的定义位于 `Error.swift` 中。

### 发送消息
<a name="room-action-send-message"></a>

要发送聊天消息，请使用 `SendMessageRequest` 的实例：

```
import AmazonIVSChatMessaging

let room = ChatRoom(...)
try await room.connect()
try await room.perform(
   request: SendMessageRequest(
      content: "Release the Kraken!"
   )
)
```

如上所述，`ChatRoom` 收到相应的 `ChatMessage` 后就会返回 `room.perform(request:)`。如果请求存在问题（如超出聊天室的消息字符限制），则改为抛出 `ChatError` 实例。然后，您可以在 UI 中显示这些有用的信息：

```
import AmazonIVSChatMessaging

do {
   let message = try await room.perform(
      request: SendMessageRequest(
         content: "Release the Kraken!"
      )
   )
   print(message.id)
} catch let error as ChatError {
   switch error.errorCode {
   case .invalidParameter:
      print("Exceeded the character limit!")
   case .tooManyRequests:
      print("Exceeded message request limit!")
   default:
      break
   }

   print(error.errorMessage)
}
```

### 将元数据附加到消息
<a name="room-action-message-metadata"></a>

[发送消息](#room-action-send-message)时，可以附加将与之关联的元数据。`SendMessageRequest` 具有 `attributes` 属性，可以使用该属性初始化请求。当其他人在聊天室中收到该消息时，您在此处附加的数据会附加到该消息中。

以下是将表情数据附加到正在发送的消息的示例：

```
import AmazonIVSChatMessaging

let room = ChatRoom(...)
try await room.connect()
try await room.perform(
   request: SendMessageRequest(
      content: "Release the Kraken!",
      attributes: [
         "messageReplyId" : "<other-message-id>",
         "attached-emotes" : "krakenCry,krakenPoggers,krakenCheer"
      ]
   )
)
```

在 `SendMessageRequest` 中使用 `attributes` 对于在聊天产品中构建复杂的功能非常有用。例如，可以使用 `SendMessageRequest` 中的 `[String : String]` 属性字典构建线程功能。

`attributes` 有效负载非常灵活且功能强大。使用它可以获取以其他方式所无法获取的消息的信息。例如，使用属性比解析消息字符串以获取诸如表情之类的信息要容易得多。

### 删除消息
<a name="room-action-delete-message"></a>

删除聊天消息与发送聊天消息一样。对 `ChatRoom` 使用 `room.perform(request:)` 函数以通过创建 `DeleteMessageRequest` 实例来实现此目的。

要轻松访问以前收到的聊天消息实例，请将 `message.id` 的值传递给 `DeleteMessageRequest` 的初始化程序。

（可选）向 `DeleteMessageRequest` 提供一个原因字符串，以便在 UI 中显示该字符串。

```
import AmazonIVSChatMessaging

let room = ChatRoom(...)
try await room.connect()
try await room.perform(
   request: DeleteMessageRequest(
      id: "<other-message-id-to-delete>",
      reason: "Abusive chat is not allowed!"
   )
)
```

由于这是监管人执行的操作，因此您的用户实际上可能无法删除其他用户的消息。当用户在没有相应权限的情况下尝试删除消息时，您可以使用 Swift 的可抛出函数机制在 UI 中显示错误消息。

后端为用户调用 `CreateChatToken` 时，它需要将 `"DELETE_MESSAGE"` 传递到 `capabilities` 字段中，从而为已连接的聊天用户激活该功能。

以下示例说明了如何捕获在没有相应权限的情况下尝试删除消息时抛出的功能错误：

```
import AmazonIVSChatMessaging

do {
   // `deleteEvent` is the same type as the object that gets sent to
   // `ChatRoomDelegate`'s `room(_:didDeleteMessage:)` function
   let deleteEvent = try await room.perform(
      request: DeleteMessageRequest(
         id: "<other-message-id-to-delete>",
         reason: "Abusive chat is not allowed!"
      )
   )
   dataSource.messages[deleteEvent.messageID] = nil
   tableView.reloadData()
} catch let error as ChatError {
   switch error.errorCode {
   case .forbidden:
      print("You cannot delete another user's messages. You need to be a mod to do that!")
   default:
      break
   }

   print(error.errorMessage)
}
```

### 断开与其他用户的连接
<a name="room-action-disconnect-user"></a>

使用 `room.perform(request:)` 断开其他用户与聊天室的连接。具体来说，是使用 `DisconnectUserRequest` 的实例。`ChatRoom` 收到的所有 `ChatMessage` 均具有 `sender` 属性，其中包含您使用 `DisconnectUserRequest` 实例正确初始化所需的用户 ID。（可选）提供断开连接请求的原因字符串。

```
import AmazonIVSChatMessaging

let room = ChatRoom(...)
try await room.connect()

let message: ChatMessage = dataSource.messages["<message-id>"]
let sender: ChatUser = message.sender
let userID: String = sender.userId
let reason: String = "You've been disconnected due to abusive behavior"

try await room.perform(
   request: DisconnectUserRequest(
      id: userID,
      reason: reason
   )
)
```

由于这是监管人所执行操作的另一个示例，因此您可以尝试断开其他用户的连接，但除非您具有 `DISCONNECT_USER` 权限，否则无法断开连接。当后端应用程序调用 `CreateChatToken` 并将 `"DISCONNECT_USER"` 字符串注入 `capabilities` 字段时，您将获得该权限。

如果您的用户无权断开其他用户的连接，则 `room.perform(request:)` 会像抛出其他请求一样抛出 `ChatError` 实例。您可以检查错误的 `errorCode` 属性，以确定请求是否因缺少监管人权限而失败：

```
import AmazonIVSChatMessaging

do {
   let message: ChatMessage = dataSource.messages["<message-id>"]
   let sender: ChatUser = message.sender
   let userID: String = sender.userId
   let reason: String = "You've been disconnected due to abusive behavior"

   try await room.perform(
      request: DisconnectUserRequest(
         id: userID,
         reason: reason
      )
   )
} catch let error as ChatError {
   switch error.errorCode {
   case .forbidden:
      print("You cannot disconnect another user. You need to be a mod to do that!")
   default:
      break
   }

   print(error.errorMessage)
}
```

# IVS 聊天功能客户端消息收发 SDK：iOS 教程
<a name="chat-sdk-ios-tutorial"></a>

 Amazon Interactive Video (IVS) Chat 客户端消息收发 iOS SDK 提供界面，可让您在使用 Apple [Swift 编程语言](https://developer.apple.com/swift/)的平台上整合我们的 [IVS Chat 消息收发 API](https://docs.aws.amazon.com//ivs/latest/chatmsgapireference/welcome.html)。

 有关 Chat iOS SDK 教程，请参阅 [https://aws.github.io/amazon-ivs-chat-messaging-sdk-ios/latest/tutorials/table-of-contents/](https://aws.github.io/amazon-ivs-chat-messaging-sdk-ios/latest/tutorials/table-of-contents/)。

# IVS 聊天功能客户端消息收发 SDK：JavaScript 指南
<a name="chat-sdk-js"></a>



Amazon Interactive Video（IVS）Chat 客户端消息收发 JavaScript SDK 可让您在使用 Web 浏览器的平台上轻松整合我们的 [https://docs.aws.amazon.com/ivs/latest/chatmsgapireference/welcome.html](https://docs.aws.amazon.com/ivs/latest/chatmsgapireference/welcome.html)。

**IVS Chat 客户端消息收发 JavaScript SDK 的最新版本：**1.0.2（[发布说明](https://docs.aws.amazon.com//ivs/latest/ChatUserGuide/release-notes.html#nov09-22)）

**参考文档：**有关 Amazon IVS Chat 客户端消息收发 JavaScript SDK 中最重要方法的信息，请参阅参考文档，网址为 [https://aws.github.io/amazon-ivs-chat-messaging-sdk-js/1.0.2/](https://aws.github.io/amazon-ivs-chat-messaging-sdk-js/1.0.2/)。

**示例代码：**有关使用 JavaScript SDK 的 Web 特定演示的信息，请参阅 GitHub 上的示例存储库：[https://github.com/aws-samples/amazon-ivs-chat-web-demo](https://github.com/aws-samples/amazon-ivs-chat-web-demo)

# IVS 聊天功能客户端消息收发 JavaScript SDK 入门
<a name="chat-js-getting-started"></a>

在开始之前，应该熟悉 [Amazon IVS Chat 入门](getting-started-chat.md)。

## 添加程序包
<a name="chat-js-add-package"></a>

请使用以下任一命令：

```
$ npm install --save amazon-ivs-chat-messaging
```

或者：

```
$ yarn add amazon-ivs-chat-messaging
```

## React Native 支持
<a name="chat-js-react-native-support"></a>

IVS Chat 客户端消息收发 JavaScript SDK 具有使用 `crypto.getRandomValues` 方法的 `uuid` 依赖项。由于 React Native 中不支持此方法，因此您需要安装额外的填充代码 `react-native-get-random-value` 并在 `index.js` 文件顶部导入：

```
import 'react-native-get-random-values';
import {AppRegistry} from 'react-native';
import App from './src/App';
import {name as appName} from './app.json';

AppRegistry.registerComponent(appName, () => App);
```

## 设置您的后端
<a name="chat-js-setup-backend"></a>

此集成需要服务器上的端点与 [Amazon IVS Chat API](https://docs.aws.amazon.com//ivs/latest/ChatAPIReference/Welcome.html) 通信。使用[官方亚马逊云科技库](https://aws.amazon.com/developer/tools/)从服务器访问 Amazon IVS API。这些库可以从 [node.js](https://www.npmjs.com/package/aws-sdk)、[java](https://github.com/aws/aws-sdk-java) 和 [go](https://github.com/aws/aws-sdk-go) 等公共程序包以多种语言进行访问。

创建与 Amazon IVS 聊天功能 API [CreateChatToken](https://docs.aws.amazon.com//ivs/latest/ChatAPIReference/API_CreateChatToken.html) 操作通信的服务器端点，为聊天用户创建聊天令牌。

# 使用 IVS 聊天功能客户端消息收发 JavaScript SDK
<a name="chat-js-using-sdk"></a>

本文档将引导您完成使用 Amazon IVS 聊天功能客户端消息收发 JavaScript SDK 涉及的步骤。

## 初始化聊天室实例
<a name="chat-js-initialize-room"></a>

创建 `ChatRoom` 类的实例。这需要传递 `regionOrUrl`（托管聊天室的 AWS 区域）和 `tokenProvider`（将在下一步中创建的令牌获取方式）：

```
const room = new ChatRoom({
  regionOrUrl: 'us-west-2',
  tokenProvider: tokenProvider,
});
```

## 令牌提供程序函数
<a name="chat-js-token-provider-function"></a>

创建从后端获取聊天令牌的异步令牌提供程序函数：

```
type ChatTokenProvider = () => Promise<ChatToken>;
```

该函数不应接受任何参数，并返回包含聊天令牌对象的 [https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)：

```
type ChatToken = {
  token: string;
  sessionExpirationTime?: Date;
  tokenExpirationTime?: Date;
}
```

[初始化 ChatRoom 对象](#chat-js-initialize-room)需要此函数。使用从后端收到的值填写下方 `<token>` 和 `<date-time>` 字段：

```
// You will need to fetch a fresh token each time this method is called by
// the IVS Chat Messaging SDK, since each token is only accepted once.
function tokenProvider(): Promise<ChatToken> {
  // Call your backend to fetch chat token from IVS Chat endpoint: 
  // e.g. const token = await appBackend.getChatToken()
  return {
    token: "<token>",
    sessionExpirationTime: new Date("<date-time>"),
    tokenExpirationTime: new Date("<date-time>")
  }
}
```

 请记得将 `tokenProvider` 传递给 ChatRoom 构造函数。连接中断或会话过期时，ChatRoom 会刷新令牌。请勿在任意地方使用 `tokenProvider` 存储令牌；ChatRoom 会为您处理。

## 接收事件
<a name="chat-js-receive-event"></a>

下一步，订阅聊天室事件以接收生命周期事件以及聊天室中传送的消息和事件：

```
/**
* Called when room is establishing the initial connection or reestablishing
* connection after socket failure/token expiration/etc
*/
const unsubscribeOnConnecting = room.addListener('connecting', () => { });

/** Called when connection has been established. */
const unsubscribeOnConnected = room.addListener('connect', () => { });

/** Called when a room has been disconnected. */
const unsubscribeOnDisconnected = room.addListener('disconnect', () => { });

/** Called when a chat message has been received. */
const unsubscribeOnMessageReceived = room.addListener('message', (message) => {
 /* Example message:
  * {
  *   id: "5OPsDdX18qcJ",
  *   sender: { userId: "user1" },
  *   content: "hello world",
  *   sendTime: new Date("2022-10-11T12:46:41.723Z"),
  *   requestId: "d1b511d8-d5ed-4346-b43f-49197c6e61de"
  * }
  */
});

/** Called when a chat event has been received. */
const unsubscribeOnEventReceived = room.addListener('event', (event) => {
 /* Example event:
  * {
  *   id: "5OPsDdX18qcJ",
  *   eventName: "customEvent,
  *   sendTime: new Date("2022-10-11T12:46:41.723Z"),
  *   requestId: "d1b511d8-d5ed-4346-b43f-49197c6e61de",
  *   attributes: { "Custom Attribute": "Custom Attribute Value" }
  * }
  */
});

/** Called when `aws:DELETE_MESSAGE` system event has been received. */
const unsubscribeOnMessageDelete = room.addListener('messageDelete', (deleteMessageEvent) => {
 /* Example delete message event:
  * {
  *   id: "AYk6xKitV4On",
  *   messageId: "R1BLTDN84zEO",
  *   reason: "Spam",
  *   sendTime: new Date("2022-10-11T12:56:41.113Z"),
  *   requestId: "b379050a-2324-497b-9604-575cb5a9c5cd",
  *   attributes: { MessageID: "R1BLTDN84zEO", Reason: "Spam" }
  * }
  */
});

/** Called when `aws:DISCONNECT_USER` system event has been received. */
const unsubscribeOnUserDisconnect = room.addListener('userDisconnect', (disconnectUserEvent) => {
 /* Example event payload:
  * {
  *   id: "AYk6xKitV4On",
  *   userId": "R1BLTDN84zEO",
  *   reason": "Spam",
  *   sendTime": new Date("2022-10-11T12:56:41.113Z"),
  *   requestId": "b379050a-2324-497b-9604-575cb5a9c5cd",
  *   attributes": { UserId: "R1BLTDN84zEO", Reason: "Spam" }
  * }
  */
});
```

## 连接到聊天室
<a name="chat-js-connect-to-chat-room"></a>

基本初始化的最后一步是通过建立 WebSocket 连接来连接到聊天室。为此，只需在聊天室实例中调用 `connect()` 方法：

```
room.connect();
```

 SDK 将尝试与聊天室建立连接，该聊天室使用从服务器收到的聊天令牌进行编码。

 调用 `connect()` 后，房间将转换到 `connecting` 状态并发出 `connecting` 事件。房间连接成功后，将转换到 `connected` 状态并发出 `connect` 事件。

 获取令牌或连接到 WebSocket 时出现问题可能会导致连接失败。在这种情况下，房间会尝试自动重新连接，尝试连接的次数不超过 `maxReconnectAttempts` 构造函数参数所指示的次数。在尝试重新连接期间，房间处于 `connecting` 状态，并不会发出其他事件。重新连接尝试次数耗尽后，房间会转换到 `disconnected` 状态并发出 `disconnect` 事件（带有相关的断开连接原因）。在 `disconnected` 状态下，房间不再尝试连接；您必须再次调用 `connect()` 才能触发连接进程。

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

Amazon IVS Chat Messaging SDK 为用户提供发送消息、删除消息和与其他用户断开连接的操作。可在 `ChatRoom` 实例上执行这些操作。这些操作将返回 `Promise` 对象，该对象允许您接收请求确认或拒绝。

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

对于此请求，您必须在聊天令牌中对 `SEND_MESSAGE` 功能进行编码。

触发发送消息请求：

```
const request = new SendMessageRequest('Test Echo');
room.sendMessage(request);
```

若要获得请求的确认或拒绝，`await` 返回的 Promise 或使用 `then()` 方法：

```
try {
  const message = await room.sendMessage(request);
  // Message was successfully sent to chat room
} catch (error) {
  // Message request was rejected. Inspect the `error` parameter for details.
}
```

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

对于此请求，您必须在聊天令牌中对 `DELETE_MESSAGE` 功能进行编码。

若出于审核目的删除消息，请调用 `deleteMessage()` 方法：

```
const request = new DeleteMessageRequest(messageId, 'Reason for deletion');
room.deleteMessage(request);
```

若要获得请求的确认或拒绝，`await` 返回的 Promise 或使用 `then()` 方法：

```
try {
  const deleteMessageEvent = await room.deleteMessage(request);
  // Message was successfully deleted from chat room
} catch (error) {
  // Delete message request was rejected. Inspect the `error` parameter for details.
}
```

### 断开与其他用户的连接
<a name="chat-js-room-actions-disconnect-user"></a>

对于此请求，您必须在聊天令牌中对 `DISCONNECT_USER` 功能进行编码。

若出于审核目的与其他用户断开连接，请调用 `disconnectUser()` 方法：

```
const request = new DisconnectUserRequest(userId, 'Reason for disconnecting user');
room.disconnectUser(request);
```

若要获得请求的确认或拒绝，`await` 返回的 Promise 或使用 `then()` 方法：

```
try {
  const disconnectUserEvent = await room.disconnectUser(request);
  // User was successfully disconnected from the chat room
} catch (error) {
  // Disconnect user request was rejected. Inspect the `error` parameter for details.
}
```

## 断开与聊天室的连接
<a name="chat-js-disconnect-room"></a>

若要关闭与聊天室的连接，请在 `room` 实例上调用 `disconnect()` 方法：

```
room.disconnect();
```

调用此方法会导致房间按顺序关闭底层 WebSocket。房间实例转换到 `disconnected` 状态并发出断开连接事件，`disconnect` 原因设置为 `"clientDisconnect"`。

# IVS 聊天功能客户端消息收发 SDK：JavaScript 教程第 1 部分：聊天室
<a name="chat-sdk-js-tutorial-chat-rooms"></a>

这是一个由两部分组成的教程的第 1 部分。通过使用 JavaScript/TypeScript 构建功能齐全的应用程序，您将了解使用 Amazon IVS Chat 客户端消息收发 JavaScript SDK 的基本知识。我们把这个应用程序称为 *Chatterbox*。

目标受众是刚接触 Amazon IVS Chat 消息收发 SDK 的经验丰富的开发人员。您应该熟悉 JavaScript/TypeScript 编程语言和 React 库。

为简洁起见，我们将 Amazon IVS Chat 客户端消息收发 JavaScript SDK 称为 Chat JS SDK。

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

本教程的第 1 部分分为几个章节：

1. [设置本地身份验证/授权服务器](#chat-js-rooms-auth-server)

1. [创建 Chatterbox 项目](#chat-js-rooms-chatterbox)

1. [连接到聊天室](#chat-js-rooms-connect)

1. [构建令牌提供程序](#chat-js-rooms-token-provider)

1. [观察连接更新](#chat-js-rooms-connection-state)

1. [创建发送按钮组件](#chat-js-rooms-send-button)

1. [创建消息输入](#chat-js-rooms-message-input)

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

要学习完整的 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-rooms-prerequisites"></a>
+ 熟悉 JavaScript/TypeScript 和 React 库。如果不熟悉 React Native，请学习本[井字游戏教程](https://react.dev/learn/tutorial-tic-tac-toe)中的基础知识。
+ 阅读并理解 [Amazon IVS Chat 入门](getting-started-chat.md)。
+ 使用现有的 IAM policy 中定义的 CreateChatToken 和 CreateRoom 功能创建 AWS IAM 用户。（请参见 [Amazon IVS Chat 入门](getting-started-chat.md)。）
+ 确保该用户的私有密钥/访问密钥存储在 Amazon 凭证文件中。有关说明，请参阅 [Amazon CLI 用户指南](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-welcome.html)（尤其是[配置和凭证文件设置](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html)部分）。
+ 创建聊天室并保存其 ARN。请参阅[Amazon IVS Chat 入门](getting-started-chat.md)。（如果不保存 ARN，您可以稍后使用控制台或 Chat API 查找 ARN。）
+ 使用 NPM 或 Yarn 程序包管理器安装 Node.js 14\$1 环境。

## 设置本地身份验证/授权服务器
<a name="chat-js-rooms-auth-server"></a>

您的后端应用程序负责创建聊天室和生成 Chat JS SDK 所需的聊天令牌，以便在您的客户端连接聊天室时进行身份验证和授权。您必须使用自己的后端，因为您无法在移动应用程序中安全地存储 Amazon 密钥；老练的攻击者可以提取这些密钥并获得对您的 Amazon 账户的访问权限。

请参阅*《Amazon IVS Chat 入门》*中的[创建聊天令牌](getting-started-chat-auth.md)。如其中的流程图所示，您的服务器端应用程序负责创建聊天令牌。这意味着您的应用程序必须通过向服务器端应用程序请求聊天令牌，来提供自己生成聊天令牌的方法。

在本节中，您将学习在后端创建令牌提供程序的基础知识。我们使用 Express 框架创建一个实时本地服务器，该服务器使用您的本地 Amazon 环境管理聊天令牌的创建。

使用 NPM 创建一个空的 `npm` 项目。创建一个用于存放应用程序的目录，并将其设为您的工作目录：

```
$ mkdir backend & cd backend
```

使用 `npm init` 为您的应用程序创建 `package.json` 文件：

```
$ npm init
```

此命令会提示您某些信息，包括您的应用程序的名称和版本。现在，只需按下 **RETURN** 即可接受其中大多数的默认值，但以下情况除外：

```
entry point: (index.js)
```

按下 **RETURN** 接受建议的默认文件名 `index.js`，或者输入任何您想要的主文件名。

现在安装所需的依赖项：

```
$ npm install express aws-sdk cors dotenv
```

`aws-sdk` 需要配置环境变量，这些变量会自动从根目录中名为 `.env` 的文件加载。要对其进行配置，请创建一个名为 `.env` 的新文件并填写缺少的配置信息：

```
# .env

# The region to send service requests to.
AWS_REGION=us-west-2

# Access keys use an access key ID and secret access key
# that you use to sign programmatic requests to AWS.

# AWS access key ID.
AWS_ACCESS_KEY_ID=...

# AWS secret access key.
AWS_SECRET_ACCESS_KEY=...
```

现在，我们在根目录中创建一个入口点文件，其名称与您在上面的 `npm init` 命令中输入的名称相同。在本例中，我们使用 `index.js` 并导入所有必需的程序包：

```
// index.js
import express from 'express';
import AWS from 'aws-sdk';
import 'dotenv/config';
import cors from 'cors';
```

现在创建一个新的 `express` 实例：

```
const app = express();
const port = 3000;

app.use(express.json());
app.use(cors({ origin: ['http://127.0.0.1:5173'] }));
```

之后，您可以为令牌提供程序创建您的第一个端点 POST 方法。从请求正文中获取所需的参数（`roomId`、`userId`、`capabilities` 和 `sessionDurationInMinutes`）：

```
app.post('/create_chat_token', (req, res) => {
  const { roomIdentifier, userId, capabilities, sessionDurationInMinutes } = req.body || {};
});
```

添加必填字段的验证：

```
app.post('/create_chat_token', (req, res) => {
  const { roomIdentifier, userId, capabilities, sessionDurationInMinutes } = req.body || {};

  if (!roomIdentifier || !userId) {
    res.status(400).json({ error: 'Missing parameters: `roomIdentifier`, `userId`' });
    return;
  }
});
```

准备好 POST 方法后，我们将 `createChatToken` 与 `aws-sdk` 进行集成，以实现身份验证/授权的核心功能：

```
app.post('/create_chat_token', (req, res) => {
  const { roomIdentifier, userId, capabilities, sessionDurationInMinutes } = req.body || {};

  if (!roomIdentifier || !userId || !capabilities) {
    res.status(400).json({ error: 'Missing parameters: `roomIdentifier`, `userId`, `capabilities`' });
    return;
  }

  ivsChat.createChatToken({ roomIdentifier, userId, capabilities, sessionDurationInMinutes }, (error, data) => {
    if (error) {
      console.log(error);
      res.status(500).send(error.code);
    } else if (data.token) {
      const { token, sessionExpirationTime, tokenExpirationTime } = data;
      console.log(`Retrieved Chat Token: ${JSON.stringify(data, null, 2)}`);

      res.json({ token, sessionExpirationTime, tokenExpirationTime });
    }
  });
});
```

在文件末尾，为您的 `express` 应用程序添加一个端口侦听器：

```
app.listen(port, () => {
  console.log(`Backend listening on port ${port}`);
});
```

现在，您可以从项目的根目录使用以下命令运行服务器：

```
$ node index.js
```

**提示**：此服务器在 https://localhost:3000 接受 URL 请求。

## 创建 Chatterbox 项目
<a name="chat-js-rooms-chatterbox"></a>

首先创建名为 `chatterbox` 的 React 项目。运行以下命令：

```
npx create-react-app chatterbox
```

您可以通过[节点程序包管理器](https://www.npmjs.com/)或 [Yarn 程序包管理器](https://yarnpkg.com/)集成 Chat 客户端消息收发 JS SDK：
+ Npm：`npm install amazon-ivs-chat-messaging`
+ Yarn：`yarn add amazon-ivs-chat-messaging`

## 连接到聊天室
<a name="chat-js-rooms-connect"></a>

在这里，您可以创建 `ChatRoom` 并使用异步方法对其进行连接。`ChatRoom` 类负责管理您的用户与 Chat JS SDK 的连接。要成功连接到聊天室，您必须在 React 应用程序中提供一个 `ChatToken` 实例。

导航到在默认 `chatterbox` 项目中创建的 `App` 文件，然后删除两个 `<div>` 标签之间的所有内容。不需要任何预填充的代码。此时，我们的 `App` 还很空。

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

import * as React from 'react';

export default function App() {
  return <div>Hello!</div>;
}
```

创建一个新的 `ChatRoom` 实例并使用 `useState` 钩子将其传递给状态。该实例需要传递 `regionOrUrl`（托管聊天室的亚马逊云科技区域）和 `tokenProvider`（用于后续步骤中创建的后端身份验证/授权流程）。

**重要提示**：您必须使用与您在 [Amazon IVS Chat 入门](getting-started-chat-create-room.md)中创建聊天室的区域相同的亚马逊云科技区域。该 API 是一项亚马逊云科技区域服务。有关支持的区域和 Amazon IVS Chat HTTPS 服务终端节点的列表，请参阅 [Amazon IVS Chat 区域](https://docs.aws.amazon.com/general/latest/gr/ivs.html#ivs_region)页面。

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

import React, { useState } from 'react';
import { ChatRoom } from 'amazon-ivs-chat-messaging';

export default function App() {
  const [room] = useState(() =>
    new ChatRoom({
      regionOrUrl: process.env.REGION as string,
      tokenProvider: () => {},
    }),
  );

  return <div>Hello!</div>;
}
```

## 构建令牌提供程序
<a name="chat-js-rooms-token-provider"></a>

下一步，我们需要构建 `ChatRoom` 构造函数所需的无参数 `tokenProvider` 函数。首先，我们将创建一个 `fetchChatToken` 函数，该函数将向您在 [设置本地身份验证/授权服务器](#chat-js-rooms-auth-server) 中设置的后端应用程序发出 POST 请求。聊天令牌包含该 SDK 成功建立聊天室连接所需的信息。Chat API 使用这些令牌作为验证用户身份、聊天室中的功能和会话持续时间的安全方式。

在项目导航器中，创建一个名为 `fetchChatToken` 的新 TypeScript/JavaScript 文件。构建对 `backend` 应用程序的提取请求，并从响应中返回 `ChatToken` 对象。添加创建聊天令牌所需的请求正文属性。使用为 [Amazon 资源名称 (ARN)](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference-arns.html) 定义的规则。这些属性记录在 [CreateChatToken](https://docs.aws.amazon.com//ivs/latest/ChatAPIReference/API_CreateChatToken.html#API_CreateChatToken_RequestBody) 操作中。

**注意**：您在此处使用的 URL 与运行后端应用程序时本地服务器创建的 URL 相同。

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

```
// fetchChatToken.ts

import { ChatToken } from 'amazon-ivs-chat-messaging';

type UserCapability = 'DELETE_MESSAGE' | 'DISCONNECT_USER' | 'SEND_MESSAGE';

export async function fetchChatToken(
  userId: string,
  capabilities: UserCapability[] = [],
  attributes?: Record<string, string>,
  sessionDurationInMinutes?: number,
): Promise<ChatToken> {
  const response = await fetch(`${process.env.BACKEND_BASE_URL}/create_chat_token`, {
    method: 'POST',
    headers: {
      Accept: 'application/json',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      userId,
      roomIdentifier: process.env.ROOM_ID,
      capabilities,
      sessionDurationInMinutes,
      attributes
    }),
  });

  const token = await response.json();

  return {
    ...token,
    sessionExpirationTime: new Date(token.sessionExpirationTime),
    tokenExpirationTime: new Date(token.tokenExpirationTime),
  };
}
```

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

```
// fetchChatToken.js

export async function fetchChatToken(
  userId,
  capabilities = [],
  attributes,
  sessionDurationInMinutes) {
  const response = await fetch(`${process.env.BACKEND_BASE_URL}/create_chat_token`, {
    method: 'POST',
    headers: {
      Accept: 'application/json',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      userId,
      roomIdentifier: process.env.ROOM_ID,
      capabilities,
      sessionDurationInMinutes,
      attributes
    }),
  });

  const token = await response.json();

  return {
    ...token,
    sessionExpirationTime: new Date(token.sessionExpirationTime),
    tokenExpirationTime: new Date(token.tokenExpirationTime),
  };
}
```

------

## 观察连接更新
<a name="chat-js-rooms-connection-state"></a>

对聊天室连接状态的变化做出反应是制作聊天应用程序的重要组成部分。让我们从订阅相关事件开始：

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

import React, { useState, useEffect } from 'react';
import { ChatRoom } from 'amazon-ivs-chat-messaging';
import { fetchChatToken } from './fetchChatToken';

export default function App() {
  const [room] = useState(
    () =>
      new ChatRoom({
        regionOrUrl: process.env.REGION as string,
        tokenProvider: () => fetchChatToken('Mike', ['SEND_MESSAGE']),
      }),
  );

  useEffect(() => {
    const unsubscribeOnConnecting = room.addListener('connecting', () => {});
    const unsubscribeOnConnected = room.addListener('connect', () => {});
    const unsubscribeOnDisconnected = room.addListener('disconnect', () => {});

    return () => {
      // Clean up subscriptions.
      unsubscribeOnConnecting();
      unsubscribeOnConnected();
      unsubscribeOnDisconnected();
    };
  }, [room]);

  return <div>Hello!</div>;
}
```

接下来，我们需要提供读取连接状态的功能。我们使用 `useState` 钩子在 `App` 中创建一些本地状态，并在每个侦听器内设置连接状态。

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

```
// App.tsx

import React, { useState, useEffect } from 'react';
import { ChatRoom, ConnectionState } from 'amazon-ivs-chat-messaging';
import { fetchChatToken } from './fetchChatToken';

export default function App() {  
  const [room] = useState(
    () =>
      new ChatRoom({
        regionOrUrl: process.env.REGION as string,
        tokenProvider: () => fetchChatToken('Mike', ['SEND_MESSAGE']),
      }),
  );
  const [connectionState, setConnectionState] = useState<ConnectionState>('disconnected');

  useEffect(() => {
    const unsubscribeOnConnecting = room.addListener('connecting', () => {
      setConnectionState('connecting');
    });

    const unsubscribeOnConnected = room.addListener('connect', () => {
      setConnectionState('connected');
    });

    const unsubscribeOnDisconnected = room.addListener('disconnect', () => {
      setConnectionState('disconnected');
    });

    return () => {
      unsubscribeOnConnecting();
      unsubscribeOnConnected();
      unsubscribeOnDisconnected();
    };
  }, [room]);

  return <div>Hello!</div>;
}
```

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

```
// App.jsx

import React, { useState, useEffect } from 'react';
import { ChatRoom } from 'amazon-ivs-chat-messaging';
import { fetchChatToken } from './fetchChatToken';

export default function App() {
  const [room] = useState(
    () =>
      new ChatRoom({
        regionOrUrl: process.env.REGION,
        tokenProvider: () => fetchChatToken('Mike', ['SEND_MESSAGE']),
      }),
  );
  const [connectionState, setConnectionState] = useState('disconnected');

  useEffect(() => {
    const unsubscribeOnConnecting = room.addListener('connecting', () => {
      setConnectionState('connecting');
    });

    const unsubscribeOnConnected = room.addListener('connect', () => {
      setConnectionState('connected');
    });

    const unsubscribeOnDisconnected = room.addListener('disconnect', () => {
      setConnectionState('disconnected');
    });

    return () => {
      unsubscribeOnConnecting();
      unsubscribeOnConnected();
      unsubscribeOnDisconnected();
    };
  }, [room]);

  return <div>Hello!</div>;
}
```

------

订阅连接状态后，显示连接状态并使用 `useEffect` 钩子内的 `room.connect` 方法连接到聊天室：

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

// ...

useEffect(() => {
  const unsubscribeOnConnecting = room.addListener('connecting', () => {
    setConnectionState('connecting');
  });

  const unsubscribeOnConnected = room.addListener('connect', () => {
    setConnectionState('connected');
  });

  const unsubscribeOnDisconnected = room.addListener('disconnect', () => {
    setConnectionState('disconnected');
  });

  room.connect();

  return () => {
    unsubscribeOnConnecting();
    unsubscribeOnConnected();
    unsubscribeOnDisconnected();
  };
}, [room]);

// ...

return (
  <div>
    <h4>Connection State: {connectionState}</h4>
  </div>
);

// ...
```

您已成功实现聊天室连接。

## 创建发送按钮组件
<a name="chat-js-rooms-send-button"></a>

在本节中，您将创建一个发送按钮，该按钮针对每种连接状态都有不同的设计。该发送按钮有助于在聊天室中发送消息。它还可以直观地指示是否/何时可以发送消息；例如，在连接断开或聊天会话过期的情况下。

首先，在 Chatterbox 项目的 `src` 目录中创建一个新文件并将其命名为 `SendButton`。然后，创建一个组件，该组件将显示聊天应用程序的按钮。导出您的 `SendButton` 并将其导入 `App`。在空的 `<div></div>` 中，添加 `<SendButton />`。

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

```
// SendButton.tsx

import React from 'react';

interface Props {
  onPress?: () => void;
  disabled?: boolean;
}

export const SendButton = ({ onPress, disabled }: Props) => {
  return (
    <button disabled={disabled} onClick={onPress}>
      Send
    </button>
  );
};

// App.tsx

import { SendButton } from './SendButton';

// ...

return (
  <div>
    <div>Connection State: {connectionState}</div>
    <SendButton />
  </div>
);
```

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

```
// SendButton.jsx

import React from 'react';

export const SendButton = ({ onPress, disabled }) => {
  return (
    <button disabled={disabled} onClick={onPress}>
      Send
    </button>
  );
};

// App.jsx

import { SendButton } from './SendButton';

// ...

return (
  <div>
    <div>Connection State: {connectionState}</div>
    <SendButton />
  </div>
);
```

------

接下来，在 `App` 中定义一个名为 `onMessageSend` 的函数，并将其传递给 `SendButton onPress` 属性。定义另一个名为 `isSendDisabled` 的变量（该变量可防止在未连接聊天室时发送消息），并将其传递给 `SendButton disabled` 属性。

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

// ...

const onMessageSend = () => {};

const isSendDisabled = connectionState !== 'connected';

return (
  <div>
    <div>Connection State: {connectionState}</div>
    <SendButton disabled={isSendDisabled} onPress={onMessageSend} />
  </div>
);

// ...
```

## 创建消息输入
<a name="chat-js-rooms-message-input"></a>

Chatterbox 消息栏是您将与之交互以向聊天室发送消息的组件。通常，它包含用于编写消息的文本输入和用于发送消息的按钮。

要创建 `MessageInput` 组件，首先在 `src` 目录中创建一个新文件并将其命名为 `MessageInput`。然后，创建一个受控输入组件，该组件将显示聊天应用程序的输入。导出您的 `MessageInput` 并将其导入 `App`（在 `<SendButton />` 上方）。

使用 `useState` 钩子创建一个名为 `messageToSend` 的新状态，其默认值为空字符串。在应用程序正文中，将 `messageToSend` 传递给 `MessageInput` 的 `value`，并将 `setMessageToSend` 传递给 `onMessageChange` 属性：

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

```
// MessageInput.tsx

import * as React from 'react';

interface Props {
  value?: string;
  onValueChange?: (value: string) => void;
}

export const MessageInput = ({ value, onValueChange }: Props) => {
  return (
    <input type="text" value={value} onChange={(e) => onValueChange?.(e.target.value)} placeholder="Send a message" />
  );
};


// App.tsx

// ...  

import { MessageInput } from './MessageInput';

// ...

export default function App() {
  const [messageToSend, setMessageToSend] = useState('');

// ...

return (
  <div>
    <h4>Connection State: {connectionState}</h4>
    <MessageInput value={messageToSend} onMessageChange={setMessageToSend} />
    <SendButton disabled={isSendDisabled} onPress={onMessageSend} />
  </div>
);
```

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

```
// MessageInput.jsx

import * as React from 'react';

export const MessageInput = ({ value, onValueChange }) => {
  return (
    <input type="text" value={value} onChange={(e) => onValueChange?.(e.target.value)} placeholder="Send a message" />
  );
};

// App.jsx

// ...  

import { MessageInput } from './MessageInput';

// ...

export default function App() {
  const [messageToSend, setMessageToSend] = useState('');

// ...


return (
  <div>
    <h4>Connection State: {connectionState}</h4>
    <MessageInput value={messageToSend} onMessageChange={setMessageToSend} />
    <SendButton disabled={isSendDisabled} onPress={onMessageSend} />
  </div>
);
```

------

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

现在，您已经为 Chatterbox 构建了一个消息栏，请继续阅读本 JavaScript 教程的第 2 部分[消息和事件](chat-sdk-js-tutorial-messages-events.md)。

# 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>

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

# IVS 聊天功能客户端消息收发 SDK：React Native 教程第 1 部分：聊天室
<a name="chat-sdk-react-tutorial-chat-rooms"></a>

这是一个由两部分组成的教程的第 1 部分。您将通过使用 React Native 构建功能齐全的应用程序，了解使用 Amazon IVS 聊天功能客户端消息收发 JavaScript SDK 的基本知识。我们把这个应用程序称为 *Chatterbox*。

目标受众是刚接触 Amazon IVS Chat 消息收发 SDK 的经验丰富的开发人员。您需要熟悉 TypeScript 或 JavaScript 编程语言以及 React Native 库。

为简洁起见，我们将 Amazon IVS Chat 客户端消息收发 JavaScript SDK 称为 Chat JS SDK。

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

本教程的第 1 部分分为几个章节：

1. [设置本地身份验证/授权服务器](#chat-react-rooms-auth-server)

1. [创建 Chatterbox 项目](#chat-react-rooms-chatterbox)

1. [连接到聊天室](#chat-react-rooms-connect)

1. [构建令牌提供程序](#chat-react-rooms-token-provider)

1. [观察连接更新](#chat-react-rooms-connection-state)

1. [创建发送按钮组件](#chat-react-rooms-send-button)

1. [创建消息输入](#chat-react-rooms-message-input)

1. [后续步骤](#chat-react-rooms-next-steps)

## 先决条件
<a name="chat-react-rooms-prerequisites"></a>
+ 熟悉 TypeScript 或 JavaScript 以及 React Native 库。如果您不熟悉 React Native，请访问 [Intro to React Native](https://reactnative.dev/docs/tutorial)（React Native 简介）学习基础知识。
+ 阅读并理解 [Amazon IVS Chat 入门](getting-started-chat.md)。
+ 使用现有的 IAM policy 中定义的 CreateChatToken 和 CreateRoom 功能创建 AWS IAM 用户。（请参见 [Amazon IVS Chat 入门](getting-started-chat.md)。）
+ 确保该用户的私有密钥/访问密钥存储在 Amazon 凭证文件中。有关说明，请参阅 [Amazon CLI 用户指南](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-welcome.html)（尤其是[配置和凭证文件设置](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html)部分）。
+ 创建聊天室并保存其 ARN。请参阅[Amazon IVS Chat 入门](getting-started-chat.md)。（如果不保存 ARN，您可以稍后使用控制台或 Chat API 查找 ARN。）
+ 使用 NPM 或 Yarn 程序包管理器安装 Node.js 14\$1 环境。

## 设置本地身份验证/授权服务器
<a name="chat-react-rooms-auth-server"></a>

您的后端应用程序负责创建聊天室和生成 Chat JS SDK 所需的聊天令牌，以便在您的客户端连接聊天室时进行身份验证和授权。您必须使用自己的后端，因为您无法在移动应用程序中安全地存储 Amazon 密钥；老练的攻击者可以提取这些密钥并获得对您的 Amazon 账户的访问权限。

请参阅*《Amazon IVS Chat 入门》*中的[创建聊天令牌](getting-started-chat-auth.md)。如其中的流程图所示，您的服务器端应用程序负责创建聊天令牌。这意味着您的应用程序必须通过向服务器端应用程序请求聊天令牌，来提供自己生成聊天令牌的方法。

在本节中，您将学习在后端创建令牌提供程序的基础知识。我们使用 Express 框架创建一个实时本地服务器，该服务器使用您的本地 Amazon 环境管理聊天令牌的创建。

使用 NPM 创建一个空的 `npm` 项目。创建一个用于存放应用程序的目录，并将其设为您的工作目录：

```
$ mkdir backend & cd backend
```

使用 `npm init` 为您的应用程序创建 `package.json` 文件：

```
$ npm init
```

此命令会提示您某些信息，包括您的应用程序的名称和版本。现在，只需按下 **RETURN** 即可接受其中大多数的默认值，但以下情况除外：

```
entry point: (index.js)
```

按下 **RETURN** 接受建议的默认文件名 `index.js`，或者输入任何您想要的主文件名。

现在安装所需的依赖项：

```
$ npm install express aws-sdk cors dotenv
```

`aws-sdk` 需要配置环境变量，这些变量会自动从根目录中名为 `.env` 的文件加载。要对其进行配置，请创建一个名为 `.env` 的新文件并填写缺少的配置信息：

```
# .env

# The region to send service requests to.
AWS_REGION=us-west-2

# Access keys use an access key ID and secret access key
# that you use to sign programmatic requests to AWS.

# AWS access key ID.
AWS_ACCESS_KEY_ID=...

# AWS secret access key.
AWS_SECRET_ACCESS_KEY=...
```

现在，我们在根目录中创建一个入口点文件，其名称与您在上面的 `npm init` 命令中输入的名称相同。在本例中，我们使用 `index.js` 并导入所有必需的程序包：

```
// index.js
import express from 'express';
import AWS from 'aws-sdk';
import 'dotenv/config';
import cors from 'cors';
```

现在创建一个新的 `express` 实例：

```
const app = express();
const port = 3000;

app.use(express.json());
app.use(cors({ origin: ['http://127.0.0.1:5173'] }));
```

之后，您可以为令牌提供程序创建您的第一个端点 POST 方法。从请求正文中获取所需的参数（`roomId`、`userId`、`capabilities` 和 `sessionDurationInMinutes`）：

```
app.post('/create_chat_token', (req, res) => {
  const { roomIdentifier, userId, capabilities, sessionDurationInMinutes } = req.body || {};
});
```

添加必填字段的验证：

```
app.post('/create_chat_token', (req, res) => {
  const { roomIdentifier, userId, capabilities, sessionDurationInMinutes } = req.body || {};

  if (!roomIdentifier || !userId) {
    res.status(400).json({ error: 'Missing parameters: `roomIdentifier`, `userId`' });
    return;
  }
});
```

准备好 POST 方法后，我们将 `createChatToken` 与 `aws-sdk` 进行集成，以实现身份验证/授权的核心功能：

```
app.post('/create_chat_token', (req, res) => {
  const { roomIdentifier, userId, capabilities, sessionDurationInMinutes } = req.body || {};

  if (!roomIdentifier || !userId || !capabilities) {
    res.status(400).json({ error: 'Missing parameters: `roomIdentifier`, `userId`, `capabilities`' });
    return;
  }

  ivsChat.createChatToken({ roomIdentifier, userId, capabilities, sessionDurationInMinutes }, (error, data) => {
    if (error) {
      console.log(error);
      res.status(500).send(error.code);
    } else if (data.token) {
      const { token, sessionExpirationTime, tokenExpirationTime } = data;
      console.log(`Retrieved Chat Token: ${JSON.stringify(data, null, 2)}`);

      res.json({ token, sessionExpirationTime, tokenExpirationTime });
    }
  });
});
```

在文件末尾，为您的 `express` 应用程序添加一个端口侦听器：

```
app.listen(port, () => {
  console.log(`Backend listening on port ${port}`);
});
```

现在，您可以从项目的根目录使用以下命令运行服务器：

```
$ node index.js
```

**提示**：此服务器在 https://localhost:3000 接受 URL 请求。

## 创建 Chatterbox 项目
<a name="chat-react-rooms-chatterbox"></a>

首先创建名为 `chatterbox` 的 React Native 项目。运行以下命令：

```
npx create-expo-app
```

或者使用 TypeScript 模板创建一个 expo 项目。

```
npx create-expo-app -t expo-template-blank-typescript
```

您可以通过[节点程序包管理器](https://www.npmjs.com/)或 [Yarn 程序包管理器](https://yarnpkg.com/)集成 Chat 客户端消息收发 JS SDK：
+ Npm：`npm install amazon-ivs-chat-messaging`
+ Yarn：`yarn add amazon-ivs-chat-messaging`

## 连接到聊天室
<a name="chat-react-rooms-connect"></a>

在这里，您可以创建 `ChatRoom` 并使用异步方法对其进行连接。`ChatRoom` 类负责管理您的用户与 Chat JS SDK 的连接。要成功连接到聊天室，您必须在 React 应用程序中提供一个 `ChatToken` 实例。

导航到在默认 `chatterbox` 项目中创建的 `App` 文件，并删除功能组件返回的所有内容。不需要任何预填充的代码。此时，我们的 `App` 还很空。

**TypeScript/JavaScript**：

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

import * as React from 'react';
import { Text } from 'react-native';

export default function App() {
  return <Text>Hello!</Text>;
}
```

创建一个新的 `ChatRoom` 实例并使用 `useState` 钩子将其传递给状态。该实例需要传递 `regionOrUrl`（托管聊天室的亚马逊云科技区域）和 `tokenProvider`（用于后续步骤中创建的后端身份验证/授权流程）。

**重要提示**：您必须使用与您在 [Amazon IVS Chat 入门](getting-started-chat-create-room.md)中创建聊天室的区域相同的亚马逊云科技区域。该 API 是一项亚马逊云科技区域服务。有关支持的区域和 Amazon IVS Chat HTTPS 服务终端节点的列表，请参阅 [Amazon IVS Chat 区域](https://docs.aws.amazon.com/general/latest/gr/ivs.html#ivs_region)页面。

**TypeScript/JavaScript**：

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

import React, { useState } from 'react';
import { Text } from 'react-native';
import { ChatRoom } from 'amazon-ivs-chat-messaging';

export default function App() {
  const [room] = useState(() =>
    new ChatRoom({
      regionOrUrl: process.env.REGION,
      tokenProvider: () => {},
    }),
  );

  return <Text>Hello!</Text>;
}
```

## 构建令牌提供程序
<a name="chat-react-rooms-token-provider"></a>

下一步，我们需要构建 `ChatRoom` 构造函数所需的无参数 `tokenProvider` 函数。首先，我们将创建一个 `fetchChatToken` 函数，该函数将向您在 [设置本地身份验证/授权服务器](#chat-react-rooms-auth-server) 中设置的后端应用程序发出 POST 请求。聊天令牌包含该 SDK 成功建立聊天室连接所需的信息。Chat API 使用这些令牌作为验证用户身份、聊天室中的功能和会话持续时间的安全方式。

在项目导航器中，创建一个名为 `fetchChatToken` 的新 TypeScript/JavaScript 文件。构建对 `backend` 应用程序的提取请求，并从响应中返回 `ChatToken` 对象。添加创建聊天令牌所需的请求正文属性。使用为 [Amazon 资源名称 (ARN)](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference-arns.html) 定义的规则。这些属性记录在 [CreateChatToken](https://docs.aws.amazon.com//ivs/latest/ChatAPIReference/API_CreateChatToken.html#API_CreateChatToken_RequestBody) 操作中。

**注意**：您在此处使用的 URL 与运行后端应用程序时本地服务器创建的 URL 相同。

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

```
// fetchChatToken.ts

import { ChatToken } from 'amazon-ivs-chat-messaging';

type UserCapability = 'DELETE_MESSAGE' | 'DISCONNECT_USER' | 'SEND_MESSAGE';

export async function fetchChatToken(
  userId: string,
  capabilities: UserCapability[] = [],
  attributes?: Record<string, string>,
  sessionDurationInMinutes?: number,
): Promise<ChatToken> {
  const response = await fetch(`${process.env.BACKEND_BASE_URL}/create_chat_token`, {
    method: 'POST',
    headers: {
      Accept: 'application/json',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      userId,
      roomIdentifier: process.env.ROOM_ID,
      capabilities,
      sessionDurationInMinutes,
      attributes
    }),
  });

  const token = await response.json();

  return {
    ...token,
    sessionExpirationTime: new Date(token.sessionExpirationTime),
    tokenExpirationTime: new Date(token.tokenExpirationTime),
  };
}
```

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

```
// fetchChatToken.js

export async function fetchChatToken(
  userId,
  capabilities = [],
  attributes,
  sessionDurationInMinutes) {
  const response = await fetch(`${process.env.BACKEND_BASE_URL}/create_chat_token`, {
    method: 'POST',
    headers: {
      Accept: 'application/json',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      userId,
      roomIdentifier: process.env.ROOM_ID,
      capabilities,
      sessionDurationInMinutes,
      attributes
    }),
  });

  const token = await response.json();

  return {
    ...token,
    sessionExpirationTime: new Date(token.sessionExpirationTime),
    tokenExpirationTime: new Date(token.tokenExpirationTime),
  };
}
```

------

## 观察连接更新
<a name="chat-react-rooms-connection-state"></a>

对聊天室连接状态的变化做出反应是制作聊天应用程序的重要组成部分。让我们从订阅相关事件开始：

**TypeScript/JavaScript**：

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

import React, { useState, useEffect } from 'react';
import { Text } from 'react-native';
import { ChatRoom } from 'amazon-ivs-chat-messaging';
import { fetchChatToken } from './fetchChatToken';

export default function App() {
  const [room] = useState(
    () =>
      new ChatRoom({
        regionOrUrl: process.env.REGION,
        tokenProvider: () => fetchChatToken('Mike', ['SEND_MESSAGE']),
      }),
  );

  useEffect(() => {
    const unsubscribeOnConnecting = room.addListener('connecting', () => {});
    const unsubscribeOnConnected = room.addListener('connect', () => {});
    const unsubscribeOnDisconnected = room.addListener('disconnect', () => {});

    return () => {
      // Clean up subscriptions.
      unsubscribeOnConnecting();
      unsubscribeOnConnected();
      unsubscribeOnDisconnected();
    };
  }, [room]);

  return <Text>Hello!</Text>;
}
```

接下来，我们需要提供读取连接状态的功能。我们使用 `useState` 钩子在 `App` 中创建一些本地状态，并在每个侦听器内设置连接状态。

**TypeScript/JavaScript**：

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

import React, { useState, useEffect } from 'react';
import { Text } from 'react-native';
import { ChatRoom, ConnectionState } from 'amazon-ivs-chat-messaging';
import { fetchChatToken } from './fetchChatToken';

export default function App() {  
  const [room] = useState(
    () =>
      new ChatRoom({
        regionOrUrl: process.env.REGION,
        tokenProvider: () => fetchChatToken('Mike', ['SEND_MESSAGE']),
      }),
  );
  const [connectionState, setConnectionState] = useState<ConnectionState>('disconnected');

  useEffect(() => {
    const unsubscribeOnConnecting = room.addListener('connecting', () => {
      setConnectionState('connecting');
    });

    const unsubscribeOnConnected = room.addListener('connect', () => {
      setConnectionState('connected');
    });

    const unsubscribeOnDisconnected = room.addListener('disconnect', () => {
      setConnectionState('disconnected');
    });

    return () => {
      unsubscribeOnConnecting();
      unsubscribeOnConnected();
      unsubscribeOnDisconnected();
    };
  }, [room]);

  return <Text>Hello!</Text>;
}
```

订阅连接状态后，显示连接状态并使用 `useEffect` 钩子内的 `room.connect` 方法连接到聊天室：

**TypeScript/JavaScript**：

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

// ...

useEffect(() => {
  const unsubscribeOnConnecting = room.addListener('connecting', () => {
    setConnectionState('connecting');
  });

  const unsubscribeOnConnected = room.addListener('connect', () => {
    setConnectionState('connected');
  });

  const unsubscribeOnDisconnected = room.addListener('disconnect', () => {
    setConnectionState('disconnected');
  });

  room.connect();

  return () => {
    unsubscribeOnConnecting();
    unsubscribeOnConnected();
    unsubscribeOnDisconnected();
  };
}, [room]);

// ...

return (
  <SafeAreaView style={styles.root}>
    <Text>Connection State: {connectionState}</Text>
  </SafeAreaView>
);

const styles = StyleSheet.create({
  root: {
    flex: 1,
  }
});

// ...
```

您已成功实现聊天室连接。

## 创建发送按钮组件
<a name="chat-react-rooms-send-button"></a>

在本节中，您将创建一个发送按钮，该按钮针对每种连接状态都有不同的设计。该发送按钮有助于在聊天室中发送消息。它还可以直观地指示是否/何时可以发送消息；例如，在连接断开或聊天会话过期的情况下。

首先，在 Chatterbox 项目的 `src` 目录中创建一个新文件并将其命名为 `SendButton`。然后，创建一个组件，该组件将显示聊天应用程序的按钮。导出您的 `SendButton` 并将其导入 `App`。在空的 `<View></View>` 中，添加 `<SendButton />`。

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

```
// SendButton.tsx

import React from 'react';
import { TouchableOpacity, Text, ActivityIndicator, StyleSheet } from 'react-native';

interface Props {
  onPress?: () => void;
  disabled: boolean;
  loading: boolean;
}

export const SendButton = ({ onPress, disabled, loading }: Props) => {
  return (
    <TouchableOpacity style={styles.root} disabled={disabled} onPress={onPress}>
      {loading ? <Text>Send</Text> : <ActivityIndicator />}
    </TouchableOpacity>
  );
};

const styles = StyleSheet.create({
  root: {
    width: 50,
    height: 50,
    borderRadius: 30,
    marginLeft: 10,
    justifyContent: 'center',
    alignContent: 'center',
  }
});

// App.tsx

import { SendButton } from './SendButton';

// ...

return (
  <SafeAreaView style={styles.root}>
    <Text>Connection State: {connectionState}</Text>
    <SendButton />
  </SafeAreaView>
);
```

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

```
// SendButton.jsx

import React from 'react';
import { TouchableOpacity, Text, ActivityIndicator, StyleSheet } from 'react-native';

export const SendButton = ({ onPress, disabled, loading }) => {
  return (
    <TouchableOpacity style={styles.root} disabled={disabled} onPress={onPress}>
      {loading ? <Text>Send</Text> : <ActivityIndicator />}
    </TouchableOpacity>
  );
};

const styles = StyleSheet.create({
  root: {
    width: 50,
    height: 50,
    borderRadius: 30,
    marginLeft: 10,
    justifyContent: 'center',
    alignContent: 'center',
  }
});

// App.jsx

import { SendButton } from './SendButton';

// ...

return (
  <SafeAreaView style={styles.root}>
    <Text>Connection State: {connectionState}</Text>
    <SendButton />
  </SafeAreaView>
);
```

------

接下来，在 `App` 中定义一个名为 `onMessageSend` 的函数，并将其传递给 `SendButton onPress` 属性。定义另一个名为 `isSendDisabled` 的变量（该变量可防止在未连接聊天室时发送消息），并将其传递给 `SendButton disabled` 属性。

**TypeScript/JavaScript**：

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

// ...

const onMessageSend = () => {};

const isSendDisabled = connectionState !== 'connected';

return (
  <SafeAreaView style={styles.root}>
    <Text>Connection State: {connectionState}</Text>
    <SendButton disabled={isSendDisabled} onPress={onMessageSend} />
  </SafeAreaView>
);

// ...
```

## 创建消息输入
<a name="chat-react-rooms-message-input"></a>

Chatterbox 消息栏是您将与之交互以向聊天室发送消息的组件。通常，它包含用于编写消息的文本输入和用于发送消息的按钮。

要创建 `MessageInput` 组件，首先在 `src` 目录中创建一个新文件并将其命名为 `MessageInput`。然后创建一个输入组件，这将会显示聊天应用程序的输入。导出您的 `MessageInput` 并将其导入 `App`（在 `<SendButton />` 上方）。

使用 `useState` 钩子创建一个名为 `messageToSend` 的新状态，其默认值为空字符串。在应用程序正文中，将 `messageToSend` 传递给 `MessageInput` 的 `value`，并将 `setMessageToSend` 传递给 `onMessageChange` 属性：

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

```
// MessageInput.tsx

import * as React from 'react';

interface Props {
  value?: string;
  onValueChange?: (value: string) => void;
}

export const MessageInput = ({ value, onValueChange }: Props) => {
  return (
    <TextInput style={styles.input} value={value} onChangeText={onValueChange} placeholder="Send a message" />
  );
};

const styles = StyleSheet.create({
  input: {
    fontSize: 20,
    backgroundColor: 'rgb(239,239,240)',
    paddingHorizontal: 18,
    paddingVertical: 15,
    borderRadius: 50,
    flex: 1,
  }
})

// App.tsx

// ...

import { MessageInput } from './MessageInput';

// ...

export default function App() {
  const [messageToSend, setMessageToSend] = useState('');

// ...

return (
  <SafeAreaView style={styles.root}>
    <Text>Connection State: {connectionState}</Text>
    <View style={styles.messageBar}>
      <MessageInput value={messageToSend} onMessageChange={setMessageToSend} />
      <SendButton disabled={isSendDisabled} onPress={onMessageSend} />
    </View>
  </SafeAreaView>
);

const styles = StyleSheet.create({
  root: {
    flex: 1,
  },
  messageBar: {
    borderTopWidth: StyleSheet.hairlineWidth,
    borderTopColor: 'rgb(160,160,160)',
    flexDirection: 'row',
    padding: 16,
    alignItems: 'center',
    backgroundColor: 'white',
  }
});
```

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

```
// MessageInput.jsx

import * as React from 'react';

export const MessageInput = ({ value, onValueChange }) => {
  return (
    <TextInput style={styles.input} value={value} onChangeText={onValueChange} placeholder="Send a message" />
  );
};

const styles = StyleSheet.create({
  input: {
    fontSize: 20,
    backgroundColor: 'rgb(239,239,240)',
    paddingHorizontal: 18,
    paddingVertical: 15,
    borderRadius: 50,
    flex: 1,
  }
})

// App.jsx

// ...

import { MessageInput } from './MessageInput';

// ...

export default function App() {
  const [messageToSend, setMessageToSend] = useState('');

// ...

return (
  <SafeAreaView style={styles.root}>
    <Text>Connection State: {connectionState}</Text>
    <View style={styles.messageBar}>
      <MessageInput value={messageToSend} onMessageChange={setMessageToSend} />
      <SendButton disabled={isSendDisabled} onPress={onMessageSend} />
    </View>
  </SafeAreaView>
);

const styles = StyleSheet.create({
  root: {
    flex: 1,
  },
  messageBar: {
    borderTopWidth: StyleSheet.hairlineWidth,
    borderTopColor: 'rgb(160,160,160)',
    flexDirection: 'row',
    padding: 16,
    alignItems: 'center',
    backgroundColor: 'white',
  }
});
```

------

## 后续步骤
<a name="chat-react-rooms-next-steps"></a>

现在，您已经为 Chatterbox 构建了一个消息栏，请继续阅读本 React Native 教程第 2 部分：[消息和事件](chat-sdk-react-tutorial-messages-events.md)。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

**TypeScript/JavaScript**：

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

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

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

## 显示收到的消息
<a name="chat-react-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

// ...

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

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

  //...
}
```

------

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

**TypeScript/JavaScript**：

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

// ...

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

// ...
```

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

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

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

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

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

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

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

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

```
// Message.tsx

import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { ChatMessage } from 'amazon-ivs-chat-messaging';

type Props = {
  message: ChatMessage;
}

export const Message = ({ message }: Props) => {
  return (
    <View style={styles.root}>
      <Text>{message.sender.userId}</Text>
      <Text style={styles.textContent}>{message.content}</Text>
    </View>
  );
};

const styles = StyleSheet.create({
  root: {
    backgroundColor: 'silver',
    padding: 6,
    borderRadius: 10,
    marginHorizontal: 12,
    marginVertical: 5,
    marginRight: 50,
  },
  textContent: {
    fontSize: 17,
    fontWeight: '500',
    flexShrink: 1,
  },
});
```

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

```
// Message.jsx

import React from 'react';
import { View, Text, StyleSheet } from 'react-native';

export const Message = ({ message }) => {
  return (
    <View style={styles.root}>
      <Text>{message.sender.userId}</Text>
      <Text style={styles.textContent}>{message.content}</Text>
    </View>
  );
};

const styles = StyleSheet.create({
  root: {
    backgroundColor: 'silver',
    padding: 6,
    borderRadius: 10,
    marginHorizontal: 12,
    marginVertical: 5,
    marginRight: 50,
  },
  textContent: {
    fontSize: 17,
    fontWeight: '500',
    flexShrink: 1,
  },
});
```

------

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

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

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

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

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

```
// UserContext.tsx

import React from 'react';

const UserContext = React.createContext<string | undefined>(undefined);

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

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

  return context;
};

export const UserProvider = UserContext.Provider;
```

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

```
// UserContext.jsx

import React from 'react';

const UserContext = React.createContext(undefined);

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

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

  return context;
};

export const UserProvider = UserContext.Provider;
```

------

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

然后使用之前创建的上下文替换传递给 `tokenProvider` 的第一个参数中的 `userId`。务必要按下文的规定，将 `SEND_MESSAGE` 功能添加到您的令牌提供者；发送消息将需要此功能：

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

```
// 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']),
      }),
  );

  // ...
}
```

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

```
// App.jsx

// ...

import { useUserContext } from './UserContext';

// ...


export default function App() {
  const [messages, setMessages] = useState([]);
  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 React from 'react';
import { View, Text, StyleSheet } from 'react-native';
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 (
    <View style={[styles.root, isMine && styles.mine]}>
      {!isMine && <Text>{message.sender.userId}</Text>}
      <Text style={styles.textContent}>{message.content}</Text>
    </View>
  );
};

const styles = StyleSheet.create({
  root: {
    backgroundColor: 'silver',
    padding: 6,
    borderRadius: 10,
    marginHorizontal: 12,
    marginVertical: 5,
    marginRight: 50,
  },
  textContent: {
    fontSize: 17,
    fontWeight: '500',
    flexShrink: 1,
  },
  mine: {
    flexDirection: 'row-reverse',
    backgroundColor: 'lightblue',
  },
});
```

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

```
// Message.jsx

import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { ChatMessage } from 'amazon-ivs-chat-messaging';
import { useUserContext } from './UserContext';

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

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

  return (
    <View style={[styles.root, isMine && styles.mine]}>
      {!isMine && <Text>{message.sender.userId}</Text>}
      <Text style={styles.textContent}>{message.content}</Text>
    </View>
  );
};

const styles = StyleSheet.create({
  root: {
    backgroundColor: 'silver',
    padding: 6,
    borderRadius: 10,
    marginHorizontal: 12,
    marginVertical: 5,
    marginRight: 50,
  },
  textContent: {
    fontSize: 17,
    fontWeight: '500',
    flexShrink: 1,
  },
  mine: {
    flexDirection: 'row-reverse',
    backgroundColor: 'lightblue',
  },
});
```

------

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

现在使用 `FlatList` 和 `Message` 组件列出消息：

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

```
// App.tsx

// ...

const renderItem = useCallback<ListRenderItem<ChatMessage>>(({ item }) => {
  return (
    <Message key={item.id} message={item} />
  );
}, []);

return (
  <SafeAreaView style={styles.root}>
    <Text>Connection State: {connectionState}</Text>
    <FlatList inverted data={messages} renderItem={renderItem} />
    <View style={styles.messageBar}>
      <MessageInput value={messageToSend} onMessageChange={setMessageToSend} />
      <SendButton disabled={isSendDisabled} onPress={onMessageSend} />
    </View>
  </SafeAreaView>
);

// ...
```

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

```
// App.jsx

// ...

const renderItem = useCallback(({ item }) => {
  return (
    <Message key={item.id} message={item} />
  );
}, []);

return (
  <SafeAreaView style={styles.root}>
    <Text>Connection State: {connectionState}</Text>
    <FlatList inverted data={messages} renderItem={renderItem} />
    <View style={styles.messageBar}>
      <MessageInput value={messageToSend} onMessageChange={setMessageToSend} />
      <SendButton disabled={isSendDisabled} onPress={onMessageSend} />
    </View>
  </SafeAreaView>
);

// ...
```

------

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

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

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

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

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

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

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

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

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

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

注意：只有当您向令牌提供者添加了该 `SEND_MESSAGE` 功能时，发送消息才能正常工作，详见上文 [识别当前用户发送的消息](#chat-react-messages-recognize)。

**TypeScript/JavaScript**：

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

// ...

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

// ...

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

// ...

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

// ...
```

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

**TypeScript/JavaScript**：

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

// ...
import { 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);
  }
};

// ...
```

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

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

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

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

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

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

**TypeScript/JavaScript**：

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

// ...

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

// ...
```

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

定义一个名为 `onDelete` 的新函数，该函数接受字符串作为其参数之一并返回 `Promise`。对于字符串参数，传入您的组件消息 ID。

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

```
// Message.tsx

import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
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 isMine = message.sender.userId === userId;
  const handleDelete = () => onDelete(message.id);

  return (
    <View style={[styles.root, isMine && styles.mine]}>
      {!isMine && <Text>{message.sender.userId}</Text>}
      <View style={styles.content}>
        <Text style={styles.textContent}>{message.content}</Text>
        <TouchableOpacity onPress={handleDelete}>
          <Text>Delete<Text/>
        </TouchableOpacity>
      </View>
    </View>
  );
};

const styles = StyleSheet.create({
  root: {
    backgroundColor: 'silver',
    padding: 6,
    borderRadius: 10,
    marginHorizontal: 12,
    marginVertical: 5,
    marginRight: 50,
  },
  content: {
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'space-between',
  },
  textContent: {
    fontSize: 17,
    fontWeight: '500',
    flexShrink: 1,
  },
  mine: {
    flexDirection: 'row-reverse',
    backgroundColor: 'lightblue',
  },
});
```

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

```
// Message.jsx

import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { ChatMessage } from 'amazon-ivs-chat-messaging';
import { useUserContext } from './UserContext';

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

  const isMine = message.sender.userId === userId;
  const handleDelete = () => onDelete(message.id);

  return (
    <View style={[styles.root, isMine && styles.mine]}>
      {!isMine && <Text>{message.sender.userId}</Text>}
      <View style={styles.content}>
        <Text style={styles.textContent}>{message.content}</Text>
        <TouchableOpacity onPress={handleDelete}>
          <Text>Delete<Text/>
        </TouchableOpacity>
      </View>
    </View>
  );
};

const styles = StyleSheet.create({
  root: {
    backgroundColor: 'silver',
    padding: 6,
    borderRadius: 10,
    marginHorizontal: 12,
    marginVertical: 5,
    marginRight: 50,
  },
  content: {
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'space-between',
  },
  textContent: {
    fontSize: 17,
    fontWeight: '500',
    flexShrink: 1,
  },
  mine: {
    flexDirection: 'row-reverse',
    backgroundColor: 'lightblue',
  },
});
```

------

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

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

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

```
// App.tsx

// ...

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

const renderItem = useCallback<ListRenderItem<ChatMessage>>(({ item }) => {
  return (
    <Message key={item.id} message={item} onDelete={handleDeleteMessage} />
  );
}, [handleDeleteMessage]);

// ...
```

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

```
// App.jsx

// ...

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

const renderItem = useCallback(({ item }) => {
  return (
    <Message key={item.id} message={item} onDelete={handleDeleteMessage} />
  );
}, [handleDeleteMessage]);

// ...
```

------

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

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

```
// App.tsx

// ...

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

// ...
```

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

```
// App.jsx

// ...

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

// ...
```

------

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

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

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

**TypeScript/JavaScript**：

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

// ...

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

return () => {
  // ...

  unsubscribeOnMessageDeleted();
};

// ...
```

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

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

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

# IVS 聊天功能客户端消息收发 SDK：React 和 React Native 最佳实践
<a name="chat-sdk-react-best-practices"></a>

此文档说明了使用适用于 React 和 React Native 的 Amazon IVS 聊天功能消息收发 SDK 的最重要实践。利用这些信息，您应该能够在 React 应用程序中构建典型的聊天功能，此外还包括相关的背景知识，以帮助您更深入了解 IVS 聊天功能消息收发 SDK 的更高级部分。

## 创建聊天室初始化程序挂钩
<a name="chatroom-initializer-hook"></a>

`ChatRoom` 类包含核心的聊天方法和侦听器，用于管理连接状态和侦听各种事件，例如已收到消息和已删除消息。我们将在本教程中演示如何在挂钩中正确地存储聊天实例。

### 实施
<a name="chatroom-initializer-hook-implementation"></a>

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

```
// useChatRoom.ts

import React from 'react';
import { ChatRoom, ChatRoomConfig } from 'amazon-ivs-chat-messaging';

export const useChatRoom = (config: ChatRoomConfig) => {
  const [room] = React.useState(() => new ChatRoom(config));

  return { room };
};
```

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

```
import React from 'react';
import { ChatRoom } from 'amazon-ivs-chat-messaging';

export const useChatRoom = (config) => {
  const [room] = React.useState(() => new ChatRoom(config));

  return { room };
};
```

------

注意：我们不会使用来自 `setState` 挂钩的 `dispatch` 方法，因为您无法即时更新配置参数。SDK 会一次性创建实例，并且无法更新令牌提供者。

**重要提示**：一次使用 `ChatRoom` 初始化程序挂钩来初始化一个新的聊天室实例。

### 示例
<a name="chatroom-initializer-hook-example"></a>

**TypeScript/JavaScript**：

```
// ...

const MyChatScreen = () => {
  const userId = 'Mike';
  const { room } = useChatRoom({
    regionOrUrl: SOCKET_URL,
    tokenProvider: () => tokenProvider(ROOM_ID, ['SEND_MESSAGE']),
  });

  const handleConnect = () => {
    room.connect();
  };

  // ...
};

// ...
```

### 侦听连接状态
<a name="chatroom-initializer-hook-connection-state"></a>

您还可以在聊天室挂钩中订阅连接状态更新。

#### 实施
<a name="connection-state-implementation"></a>

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

```
// useChatRoom.ts

import React from 'react';
import { ChatRoom, ChatRoomConfig, ConnectionState } from 'amazon-ivs-chat-messaging';

export const useChatRoom = (config: ChatRoomConfig) => {
  const [room] = useState(() => new ChatRoom(config));

  const [state, setState] = React.useState<ConnectionState>('disconnected');

  React.useEffect(() => {
    const unsubscribeOnConnecting = room.addListener('connecting', () => {
      setState('connecting');
    });

    const unsubscribeOnConnected = room.addListener('connect', () => {
      setState('connected');
    });

    const unsubscribeOnDisconnected = room.addListener('disconnect', () => {
      setState('disconnected');
    });

    return () => {
      unsubscribeOnConnecting();
      unsubscribeOnConnected();
      unsubscribeOnDisconnected();
    };
  }, []);

  return { room, state };
};
```

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

```
// useChatRoom.js

import React from 'react';
import { ChatRoom } from 'amazon-ivs-chat-messaging';

export const useChatRoom = (config) => {
  const [room] = useState(() => new ChatRoom(config));

  const [state, setState] = React.useState('disconnected');

  React.useEffect(() => {
    const unsubscribeOnConnecting = room.addListener('connecting', () => {
      setState('connecting');
    });

    const unsubscribeOnConnected = room.addListener('connect', () => {
      setState('connected');
    });

    const unsubscribeOnDisconnected = room.addListener('disconnect', () => {
      setState('disconnected');
    });

    return () => {
      unsubscribeOnConnecting();
      unsubscribeOnConnected();
      unsubscribeOnDisconnected();
    };
  }, []);

  return { room, state };
};
```

------

## 聊天室实例提供者
<a name="chatroom-instance-provider"></a>

要在其他组件中使用挂钩（以避免道具钻取），您可以使用 React `context` 创建聊天室提供者。

### 实施
<a name="chatroom-instance-provider-implementation"></a>

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

```
// ChatRoomContext.tsx

import React from 'react';
import { ChatRoom } from 'amazon-ivs-chat-messaging';

const ChatRoomContext = React.createContext<ChatRoom | undefined>(undefined);

export const useChatRoomContext = () => {
  const context = React.useContext(ChatRoomContext);

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

  return context;
};

export const ChatRoomProvider = ChatRoomContext.Provider;
```

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

```
// ChatRoomContext.jsx

import React from 'react';
import { ChatRoom } from 'amazon-ivs-chat-messaging';

const ChatRoomContext = React.createContext(undefined);

export const useChatRoomContext = () => {
  const context = React.useContext(ChatRoomContext);

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

  return context;
};

export const ChatRoomProvider = ChatRoomContext.Provider;
```

------

### 示例
<a name="chatroom-instance-provider-example"></a>

创建 `ChatRoomProvider` 后，您可以通过 `useChatRoomContext` 来使用您的实例。

**重要提示**：请仅在您需要访问聊天屏幕和中间的其他组件之间的 `context` 时，才将提供者置于根级别，以避免在侦听连接时进行不必要的重新渲染。否则，应将提供者放置在尽可能靠近聊天屏幕的位置。

**TypeScript/JavaScript**：

```
// AppContainer

const AppContainer = () => {
  const { room } = useChatRoom({
    regionOrUrl: SOCKET_URL,
    tokenProvider: () => tokenProvider(ROOM_ID, ['SEND_MESSAGE']),
  });

  return (
    <ChatRoomProvider value={room}>
      <MyChatScreen />
    </ChatRoomProvider>
  );
};

// MyChatScreen

const MyChatScreen = () => {
  const room = useChatRoomContext();

  const handleConnect = () => {
    room.connect();
  };
  // ...
};

// ...
```

## 创建消息侦听器
<a name="message-listener"></a>

要及时了解所有传入的消息，您需要订阅 `message` 和 `deleteMessage` 事件。以下是一些为您的组件提供聊天消息的代码。

**重要提示**：为确保理想的性能，我们将 `ChatMessageContext` 与 `ChatRoomProvider` 分开，因为当聊天消息侦听器更新其消息状态时，我们可能会进行多次重新渲染。请记住要在您要使用 `ChatMessageProvider` 的组件中应用 `ChatMessageContext`。

### 实施
<a name="message-listener-implementation"></a>

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

```
// ChatMessagesContext.tsx

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

const ChatMessagesContext = React.createContext<ChatMessage[] | undefined>(undefined);

export const useChatMessagesContext = () => {
  const context = React.useContext(ChatMessagesContext);

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

  return context;
};

export const ChatMessagesProvider = ({ children }: { children: React.ReactNode }) => {
  const room = useChatRoomContext();

  const [messages, setMessages] = React.useState<ChatMessage[]>([]);

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

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

    return () => {
      unsubscribeOnMessageDeleted();
      unsubscribeOnMessageReceived();
    };
  }, [room]);

  return <ChatMessagesContext.Provider value={messages}>{children}</ChatMessagesContext.Provider>;
};
```

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

```
// ChatMessagesContext.jsx

import React from 'react';
import { useChatRoomContext } from './ChatRoomContext';

const ChatMessagesContext = React.createContext(undefined);

export const useChatMessagesContext = () => {
  const context = React.useContext(ChatMessagesContext);

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

  return context;
};

export const ChatMessagesProvider = ({ children }) => {
  const room = useChatRoomContext();

  const [messages, setMessages] = React.useState([]);

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

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

    return () => {
      unsubscribeOnMessageDeleted();
      unsubscribeOnMessageReceived();
    };
  }, [room]);

  return <ChatMessagesContext.Provider value={messages}>{children}</ChatMessagesContext.Provider>;
};
```

------

### React 示例
<a name="message-listener-example-react"></a>

**重要提示**：请记住要用 `ChatMessagesProvider` 打包您的消息容器。`Message` 行是一个显示消息内容的示例组件。

**TypeScript/JavaScript**：

```
// your message list component...

import React from 'react';
import { useChatMessagesContext } from './ChatMessagesContext';

const MessageListContainer = () => {
  const messages = useChatMessagesContext();

  return (
    <React.Fragment>
      {messages.map((message) => (
        <MessageRow message={message} />
      ))}
    </React.Fragment>
  );
};
```

### React Native 示例
<a name="message-listener-example-react-native"></a>

`ChatMessage` 会默认包含 `id`，后者将在 `FlatList` 中自动用作每行的 React 键；因此，您无需传递 `keyExtractor`。

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

```
// MessageListContainer.tsx

import React from 'react';
import { ListRenderItemInfo, FlatList } from 'react-native';
import { ChatMessage } from 'amazon-ivs-chat-messaging';
import { useChatMessagesContext } from './ChatMessagesContext';

const MessageListContainer = () => {
  const messages = useChatMessagesContext();

  const renderItem = useCallback(({ item }: ListRenderItemInfo<ChatMessage>) => <MessageRow />, []);

  return <FlatList data={messages} renderItem={renderItem} />;
};
```

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

```
// MessageListContainer.jsx

import React from 'react';
import { FlatList } from 'react-native';
import { useChatMessagesContext } from './ChatMessagesContext';

const MessageListContainer = () => {
  const messages = useChatMessagesContext();

  const renderItem = useCallback(({ item }) => <MessageRow />, []);

  return <FlatList data={messages} renderItem={renderItem} />;
};
```

------

## 一个应用程序中的多个聊天室实例
<a name="multiple-chatroom-instances"></a>

如果您在应用程序中使用多个并发聊天室，我们建议为每个聊天创建一个提供者，并在聊天提供者中使用该提供者。在此例中，我们将创建一个帮助机器人和客户帮助聊天。我们将为两者分别创建了一个提供者。

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

```
// SupportChatProvider.tsx

import React from 'react';
import { SUPPORT_ROOM_ID, SOCKET_URL } from '../../config';
import { tokenProvider } from '../tokenProvider';
import { ChatRoomProvider } from './ChatRoomContext';
import { useChatRoom } from './useChatRoom';

export const SupportChatProvider = ({ children }: { children: React.ReactNode }) => {
  const { room } = useChatRoom({
    regionOrUrl: SOCKET_URL,
    tokenProvider: () => tokenProvider(SUPPORT_ROOM_ID, ['SEND_MESSAGE']),
  });

  return <ChatRoomProvider value={room}>{children}</ChatRoomProvider>;
};

// SalesChatProvider.tsx

import React from 'react';
import { SALES_ROOM_ID, SOCKET_URL } from '../../config';
import { tokenProvider } from '../tokenProvider';
import { ChatRoomProvider } from './ChatRoomContext';
import { useChatRoom } from './useChatRoom';

export const SalesChatProvider = ({ children }: { children: React.ReactNode }) => {
  const { room } = useChatRoom({
    regionOrUrl: SOCKET_URL,
    tokenProvider: () => tokenProvider(SALES_ROOM_ID, ['SEND_MESSAGE']),
  });

  return <ChatRoomProvider value={room}>{children}</ChatRoomProvider>;
};
```

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

```
// SupportChatProvider.jsx

import React from 'react';
import { SUPPORT_ROOM_ID, SOCKET_URL } from '../../config';
import { tokenProvider } from '../tokenProvider';
import { ChatRoomProvider } from './ChatRoomContext';
import { useChatRoom } from './useChatRoom';

export const SupportChatProvider = ({ children }) => {
  const { room } = useChatRoom({
    regionOrUrl: SOCKET_URL,
    tokenProvider: () => tokenProvider(SUPPORT_ROOM_ID, ['SEND_MESSAGE']),
  });

  return <ChatRoomProvider value={room}>{children}</ChatRoomProvider>;
};

// SalesChatProvider.jsx

import React from 'react';
import { SALES_ROOM_ID, SOCKET_URL } from '../../config';
import { tokenProvider } from '../tokenProvider';
import { ChatRoomProvider } from './ChatRoomContext';
import { useChatRoom } from './useChatRoom';

export const SalesChatProvider = ({ children }) => {
  const { room } = useChatRoom({
    regionOrUrl: SOCKET_URL,
    tokenProvider: () => tokenProvider(SALES_ROOM_ID, ['SEND_MESSAGE']),
  });

  return <ChatRoomProvider value={room}>{children}</ChatRoomProvider>;
};
```

------

### React 示例
<a name="multiple-chatroom-instances-example-react"></a>

您现在可以使用具有相同 `ChatRoomProvider` 的不同聊天提供者。稍后您可以在每个屏幕/视图中重复使用相同的 `useChatRoomContext`。

**TypeScript/JavaScript**：

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

const App = () => {
  return (
    <Routes>
      <Route
        element={
          <SupportChatProvider>
            <SupportChatScreen />
          </SupportChatProvider>
        }
      />
      <Route
        element={
          <SalesChatProvider>
            <SalesChatScreen />
          </SalesChatProvider>
        }
      />
    </Routes>
  );
};
```

### React Native 示例
<a name="multiple-chatroom-instances-example-react-native"></a>

**TypeScript/JavaScript**：

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

const App = () => {
  return (
    <Stack.Navigator>
      <Stack.Screen name="SupportChat">
        <SupportChatProvider>
          <SupportChatScreen />
        </SupportChatProvider>
      </Stack.Screen>
      <Stack.Screen name="SalesChat">
        <SalesChatProvider>
          <SalesChatScreen />
        </SalesChatProvider>
      </Stack.Screen>
    </Stack.Navigator>
  );
};
```

**TypeScript/JavaScript**：

```
// SupportChatScreen.tsx / SupportChatScreen.jsx

// ...

const SupportChatScreen = () => {
  const room = useChatRoomContext();

  const handleConnect = () => {
    room.connect();
  };

  return (
    <>
      <Button title="Connect" onPress={handleConnect} />
      <MessageListContainer />
    </>
  );
};

// SalesChatScreen.tsx / SalesChatScreen.jsx

// ...

const SalesChatScreen = () => {
  const room = useChatRoomContext();

  const handleConnect = () => {
    room.connect();
  };

  return (
    <>
      <Button title="Connect" onPress={handleConnect} />
      <MessageListContainer />
    </>
  );
};
```