

# IVS 聊天用戶端傳訊 SDK
<a name="chat-sdk"></a>

Amazon 互動影片服務 (IVS) 聊天用戶端傳訊開發套件適用於使用 Amazon IVS 建置應用程式的開發人員。此開發套件的設計目的是利用 Amazon IVS 架構和 Amazon IVS 聊天功能，並推出改良後的新版功能。作為原生開發套件，其設計目的是將對您的應用程式和使用者存取應用程式的裝置的效能影響降至最低。

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

### 桌面瀏覽器
<a name="chat-desktop-browsers"></a>


| 瀏覽器 | 支援的版本 | 
| --- | --- | 
| Chrome | 兩個主要版本 (目前版本和最新的先前版本) | 
| Edge | 兩個主要版本 (目前版本和最新的先前版本) | 
| 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))。

將此聊天室的識別碼提供給 AWS Support。使用它，他們就可以取得資訊來協助您對問題進行疑難排解。

**注意：**關於可用版本以及已修正的問題，請參閱 [Amazon IVS 聊天功能版本備註](release-notes.md)。如果適當，請在聯絡支援部門之前，先更新您的開發套件版本，並查看是否可以解決您的問題。

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

Amazon IVS 聊天用戶端傳訊 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。

此外，在 2023 年底或發行 3.x 之前，將支援 1.x 版，以較晚者為準。

## Amazon IVS 聊天功能開發套件
<a name="chat-sdk-chat-apis"></a>

伺服器端 (不由開發套件管理) 有兩個 API，每個 API 都有自己負責的部分：
+ **資料平面** — 此 [IVS Chat 傳訊 API](https://docs.aws.amazon.com//ivs/latest/chatmsgapireference/welcome.html) 是一種 WebSockets API，主要由以字符型身分驗證方案驅動的前端應用程式 (iOS、Android、macOS 等) 使用。透過先前產生的聊天字符，即可使用此 API 與現有的聊天室連線。

  *Amazon IVS 聊天用戶端傳訊 SDK 僅與資料平面有關。開發套件會假設您已經透過後端產生聊天字符。擷取這些字符的工作應由前端應用程式管理，而非由開發套件管理。*
+ **控制平面** — 此 [IVS Chat 控制平面 API](https://docs.aws.amazon.com//ivs/latest/ChatAPIReference/Welcome.html) 為您自己的*後端應用程式*提供了一個介面，使用此介面可管理和建立聊天室以及加入聊天室的使用者。可將這個平面視為應用程式聊天體驗 (由*您自己的後端*管理) 的管理面板。有些控制平面操作負責建立*聊天權杖*，資料平面需對這些權杖進行身分驗證，通過驗證者才能進入聊天室。

  **重要事項：***IVS Chat 用戶端傳訊開發套件不會呼叫任何控制平面操作。您必須設定後端才能為您自己建立聊天字符。您的前端應用程式必須與後端通訊才能擷取此聊天字符。*

# 《IVS 聊天用戶端傳訊 SDK：Android 版指南》
<a name="chat-sdk-android"></a>

Amazon Interactive Video (IVS) Chat 用戶端傳訊 Android 版開發套件的介面可讓您輕鬆使用 Android 來整合我們平台上的 [IVS 聊天傳訊 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 聊天功能用戶端傳訊 Android 版開發套件中最重要方法的資訊，請參閱參考文件，網址為 [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 level 21) 或更高版本。

# 開始使用 IVS 聊天功能用戶端傳訊 Android SDK
<a name="chat-android-getting-started"></a>

在開始使用之前，請先詳閱 [Amazon IVS 聊天功能入門](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) 通訊的端點才能進行此整合。使用[官方 AWS 程式庫](https://aws.amazon.com/developer/tools/)從您的伺服器存取 Amazon IVS API。透過公開套件就可以使用這些程式庫，而且有多種程式語言可供選用；例如 node.js 和 Java。

接下來，建立一個可與 [Amazon IVS 聊天功能 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() ` 生命週期方法中這樣做，以確保它在應用程式從背景中恢復時保持連線。

```
room.connect()
```

開發套件會試圖連線至聊天室，而該聊天室是以從伺服器收到的聊天權杖加以編碼。如果失敗，它會試圖重新連線，直至達到在聊天室執行個體中指定的次數為止。

## 在聊天室中執行動作
<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 連線就會停止運作，因此建議您在從背景狀態來回轉換時手動連線/中斷連線。若要這樣做，則 Android `Activity` 或 `Fragment` 上 `onResume()` 生命週期方法中的 `room.connect()` 呼叫必須與 `onPause()` 生命週期方法中的 `room.disconnect()` 呼叫相符。

# IVS 聊天用戶端傳訊 SDK：Android 版教學課程第 1 部分：聊天室
<a name="chat-sdk-android-tutorial-chat-rooms"></a>

這是由兩部分組成的教學課程的第一部分。透過使用 [Kotlin](https://kotlinlang.org/) 程式設計語言建置功能完整的 Android 應用程式，您將學習使用 Amazon IVS 聊天功能傳訊 SDK 的基礎知識。我們稱呼該應用程式為 *Chatterbox*。

在開始該模組之前，請花幾分鐘時間熟悉先決條件、聊天權杖背後的重要概念以及建立聊天室所需的後端伺服器。

這些教學課程專為經驗豐富的 Android 開發人員而建立，他們不熟悉 IVS 聊天功能傳訊 SDK。您將需要熟悉 Kotlin 程式設計語言並在 Android 平台上建立 UI。

本教學課程的第一部分分為幾個部分：

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 文件，請先閱讀 [Amazon 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/) (聊天用戶端傳訊：Android 版 SDK 參考) (位於 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 聊天功能入門](getting-started-chat.md)。
+ 使用現有 IAM 政策中定義的 `CreateChatToken` 和 `CreateRoom` 功能建立 AWS IAM 使用者。(請參閱 [Amazon IVS 聊天功能入門](getting-started-chat.md))。
+ 確保將此使用者的私密/存取金鑰儲存在 AWS 憑證檔案中。如需指示，請參閱《[AWS 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 聊天功能入門](getting-started-chat.md)。(如果您未保存該 ARN，稍後可以使用主控台或 Chat API 來查詢。)

## 設定本機身分驗證/授權伺服器
<a name="chat-android-rooms-auth-server"></a>

後端伺服器負責建立聊天室並產生 IVS 聊天功能 Android SDK 需要的聊天權杖，以便對聊天室的用戶端執行身分驗證和授權。

請參閱 *Amazon IVS 聊天功能入門*中的[建立聊天字符](getting-started-chat-auth.md)。如流程圖所示，您的伺服器端程式碼會負責建立聊天權杖。這意味著應用程式必須透過從伺服器端應用程式請求聊天字符，來提供自己產生聊天字符的方法。

我們使用 [Ktor](https://ktor.io/) 架構建立即時本機伺服器，以管理使用本機 AWS 環境建立聊天權杖的作業。

此時，希望您已正確設定 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
```

*注意：您可以直接將這裡的程式碼複製/貼上到參考的檔案中。*

接下來，我們會新增所有必要的相依性和外掛程式，以使 auth 伺服器正常工作：

**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")
}
```

現在，我們需要為 auth 伺服器設定記錄功能。(如需詳細資訊，請參閱[設定記錄器](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)。)

為了方便地從專案根目錄中執行 auth 伺服器 (在上一節中建立)，我們將其作為新模組包含在 `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 專案中，所以您可以使用下列命令從專案的根目錄中執行 auth 伺服器：

**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` 來執行個體化聊天室連線。

**注意**：將在[下一節](#chat-android-rooms-token-provider)中實作下面程式碼片段中的 `fetchChatToken` 函數。

**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)中，並將其初始化為聊天室的預設 DISCONNECTED 狀態 (請參閱[《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)。) 我們首先向 [App Manifest](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>
```

接下來，我們需要新增一個新的相依性，並[新增 Gson 轉換器](https://github.com/square/retrofit/tree/trunk/retrofit-converters/gson)以用於解析 HTTP 回應。*在下面的程式碼中，將 `<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>

本教學課程的第二部分 (也是最後一部分) 分為幾個部分：

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 文件，請先閱讀 [Amazon 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/) (聊天用戶端傳訊：Android 版 SDK 參考) (位於 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 Coroutines 版教學課程第 1 部分：聊天室
<a name="chat-sdk-kotlin-tutorial-chat-rooms"></a>

這是由兩部分組成的教學課程的第一部分。透過使用 [Kotlin](https://kotlinlang.org/) 程式設計語言和 [coroutines](https://kotlinlang.org/docs/coroutines-overview.html) 建置功能完整的 Android 應用程式，您將學習使用 Amazon IVS 聊天功能傳訊 SDK 的基礎知識。我們稱呼該應用程式為 *Chatterbox*。

在開始該模組之前，請花幾分鐘時間熟悉先決條件、聊天權杖背後的重要概念以及建立聊天室所需的後端伺服器。

這些教學課程專為經驗豐富的 Android 開發人員而建立，他們不熟悉 IVS 聊天功能傳訊 SDK。您將需要熟悉 Kotlin 程式設計語言並在 Android 平台上建立 UI。

本教學課程的第一部分分為幾個部分：

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 文件，請先閱讀 [Amazon 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/) (聊天用戶端傳訊：Android 版 SDK 參考) (位於 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 聊天功能入門](getting-started-chat.md)。
+ 使用現有 IAM 政策中定義的 `CreateChatToken` 和 `CreateRoom` 功能建立 AWS IAM 使用者。(請參閱 [Amazon IVS 聊天功能入門](getting-started-chat.md))。
+ 確保將此使用者的私密/存取金鑰儲存在 AWS 憑證檔案中。如需指示，請參閱《[AWS 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 聊天功能入門](getting-started-chat.md)。(如果您未保存該 ARN，稍後可以使用主控台或 Chat API 來查詢。)

## 設定本機身分驗證/授權伺服器
<a name="chat-kotlin-rooms-auth-server"></a>

後端伺服器負責建立聊天室並產生 IVS 聊天功能 Android SDK 需要的聊天權杖，以便對聊天室的用戶端執行身分驗證和授權。

請參閱 *Amazon IVS 聊天功能入門*中的[建立聊天字符](getting-started-chat-auth.md)。如流程圖所示，您的伺服器端程式碼會負責建立聊天權杖。這意味著應用程式必須透過從伺服器端應用程式請求聊天字符，來提供自己產生聊天字符的方法。

我們使用 [Ktor](https://ktor.io/) 架構建立即時本機伺服器，以管理使用本機 AWS 環境建立聊天權杖的作業。

此時，希望您已正確設定 AWS 憑證。如需逐步說明，請參閱[設定適用於開發的 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
```

*注意：您可以直接將這裡的程式碼複製/貼上到參考的檔案中。*

接下來，我們會新增所有必要的相依性和外掛程式，以使 auth 伺服器正常工作：

**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")
}
```

現在，我們需要為 auth 伺服器設定記錄功能。(如需詳細資訊，請參閱[設定記錄器](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)。)

為了方便地從專案根目錄中執行我們的 auth 伺服器 (在上一節中建立)，我們將其作為新模組包含在 `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 專案中，所以您可以使用下列命令從專案的根目錄中執行 auth 伺服器：

**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` 來執行個體化聊天室連線。

**注意：**將在[下一節](#chat-kotlin-rooms-token-provider)中實作下面程式碼片段中的 `fetchChatToken` 函數。

**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` 聊天應用程式的重要部分。在開始與聊天室互動之前，我們必須訂閱聊天室連線狀態事件，以獲取更新。

在適用於 coroutine 的聊天功能 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)中，並將其初始化為聊天室的預設 DISCONNECTED 狀態 (請參閱《[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)。) 我們首先向 [App Manifest](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>
```

接下來，我們需要新增一個新的相依性，並[新增 Gson 轉換器](https://github.com/square/retrofit/tree/trunk/retrofit-converters/gson)以用於解析 HTTP 回應。*在下面的程式碼中，將 *`<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 Coroutines 教學課程的第 2 部分：[訊息和事件](chat-sdk-kotlin-tutorial-messages-events.md)。

# IVS 聊天用戶端傳訊 SDK：Kotlin Coroutines 教學課程第 2 部分：訊息和事件
<a name="chat-sdk-kotlin-tutorial-messages-events"></a>

本教學課程的第二部分 (也是最後一部分) 分為幾個部分：

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 文件，請先閱讀 [Amazon 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/) (聊天用戶端傳訊：Android 版 SDK 參考) (位於 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 Service (IVS) Chat 用戶端傳訊 iOS 版開發套件提供的介面，可讓您使用 Apple 的 [Swift 程式設計語言](https://developer.apple.com/swift/)整合平台上的 [IVS Chat 傳訊 API](https://docs.aws.amazon.com//ivs/latest/chatmsgapireference/welcome.html)。

**最新版 IVS 聊天用戶端傳訊 iOS SDK：**1.0.1 ([版本備註](https://docs.aws.amazon.com//ivs/latest/ChatUserGuide/release-notes.html#aug08-25))

**參考文件和教學課程：**如需有關 Amazon IVS 聊天功能用戶端傳訊 iOS 版開發套件中最重要方法的資訊，請參閱參考文件，網址為 [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 聊天用戶端傳訊 iOS SDK
<a name="chat-ios-getting-started"></a>

我們建議您透過 [Swift Package Manager](#chat-ios-install-sdk-swiftpm) 來整合開發套件。您也可以使用[手動整合架構](#chat-ios-install-sdk-manual)。

整合開發套件後，您可以在相關的 Swift 檔案最上方新增下列程式碼以匯入開發套件：

```
import AmazonIVSChatMessaging
```

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

若要在 Swift Package Manager 專案中使用 `AmazonIVSChatMessaging` 程式庫，請將其新增至套件和相關目標的相依性：

1. 從 [https://ivschat.live-video.net/1.0.1/AmazonIVSChatMessaging.xcframework.zip](https://ivschat.live-video.net/1.0.1/AmazonIVSChatMessaging.xcframework.zip).下載最新版本的 `.xcframework`。

1. 在您的終端機中執行：

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

1. 取得上一步的輸出，並將其貼到專案 `Package.swift` 檔案中 `.binaryTarget` 的 Checksum 屬性內，如下所示：

   ```
   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` 包含用於裝置和模擬器的 SDK。

1. 內嵌擷取的 `AmazonIVSChatMessaging.xcframework`，方法是將其拖曳至應用程式目標的 **General** (一般) 索引標籤的 **Frameworks, Libraries, and Embedded Content** (架構、程式庫和內嵌內容) 區段：  
![\[您的應用程式目標的一般索引標籤的架構、程式庫和內嵌內容區段。\]](http://docs.aws.amazon.com/zh_tw/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) 版的範例應用程式。

若要與聊天室連線，您的應用程式必須設法擷取後端提供的聊天權杖。您的應用程式可能會向後端發出網路請求以擷取聊天權杖。

若要將此擷取的聊天權杖傳送給開發套件，開發套件的 `ChatRoom` 模型會要求您提供符合在初始化時所提供 `ChatTokenProvider` 通訊協定的 `async` 函數或物件執行個體。這些方法中的任何一種傳回的值都必須是開發套件 `ChatToken` 模型的執行個體。

**注意：**請使用從後端擷取的資料來填入 `ChatToken` 模型的執行個體。初始化 `ChatToken` 執行個體所需的欄位與 [CreateChatToken](https://docs.aws.amazon.com//ivs/latest/ChatAPIReference/API_CreateChatToken.html) 回應中的欄位相同。如需有關初始化 `ChatToken` 模型執行個體的詳細資訊，請參閱[建立 ChatToken 執行個體](#chat-ios-create-chattoken)。請謹記，*您的後端*必須負責向應用程式提供 `CreateChatToken` 回應中的資料。您用來與後端通訊以產生聊天權杖的方式取決於您的應用程式及其基礎架構。

選擇向開發套件提供 `ChatToken` 的策略之後，請在使用您的權杖提供者，以及您的後端用來建立所嘗試連線之聊天室的 *AWS 區域*，成功初始化 `.connect()` 執行個體後呼叫 `ChatRoom`。請注意，`.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()
```

## 建立 ChatToken 執行個體
<a name="chat-ios-create-chattoken"></a>

使用開發套件中提供的初始設定式可輕鬆建立 `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` 回應承載會使用以[網際網路時間戳記的 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` 支援。

為了方便起見，開發套件在 `JSONDecoder.DateDecodingStrategy` 中提供了公有擴充功能，其額外的 `.preciseISO8601` 策略可讓您成功使用 `JSONDecoder` 來解碼 `ChatToken` 的執行個體：

```
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 在背景執行緒上連線至 ChatRoom。如果您想要在 UI 中建構可停用傳送訊息按鈕等功能，開發套件對此提供兩種使用 `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:)`，並傳入開發套件中提供的其中一個 `ChatRequest` 物件的執行個體。支援的請求位於 `Request.swift` 中。

當後端應用程式呼叫 `CreateChatToken` 時，連線的使用者必須具備授予他們的特定功能，才能在聊天室中執行某些動作。開發套件在設計上無法辨別連線使用者的功能。因此，雖然您可以試著在連線的 `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 版開發套件提供的介面，可讓您使用 Apple 的 [Swift 程式設計語言](https://developer.apple.com/swift/)整合平台上的 [IVS Chat 傳訊 API](https://docs.aws.amazon.com//ivs/latest/chatmsgapireference/welcome.html)。

 如需 Chat iOS 版開發套件教學課程，請參閱 [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 版開發套件可讓您在使用 Web 瀏覽器的平台上整合我們的 [https://docs.aws.amazon.com/ivs/latest/chatmsgapireference/welcome.html](https://docs.aws.amazon.com/ivs/latest/chatmsgapireference/welcome.html)。

**IVS Chat 用戶端傳訊 JavaScript 開發套件的最新版本：**1.0.2 ([版本備註](https://docs.aws.amazon.com//ivs/latest/ChatUserGuide/release-notes.html#nov09-22))

**參考文件：**如需 Amazon IVS 聊天用戶端傳訊 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/)

**範例程式碼：**請參閱 GitHub 上的範例儲存庫，以取得使用 JavaScript 版開發套件的 Web 專用示範，網址為：[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 聊天功能入門](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 版開發套件具有使用 `crypto.getRandomValues` 方法的 `uuid` 相依性。由於 React Native 中不支援此方法，因此您需要安裝額外的 polyfill `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 聊天功能 API](https://docs.aws.amazon.com//ivs/latest/ChatAPIReference/Welcome.html) 通訊的端點才能執行這項整合。使用[官方 AWS 程式庫](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();
```

 開發套件會嘗試與聊天室建立連線，而該聊天室是以從伺服器收到的聊天字符編碼建立而成。

 呼叫 `connect()` 後，聊天室會轉換為 `connecting` 狀態並發出 `connecting` 事件。成功連線至聊天室後，聊天室會轉換為 `connected` 狀態並發出 `connect` 事件。

 擷取字符或連線至 WebSocket 時一旦發生問題，可能會導致連線失敗。這種情況下，聊天室會嘗試自動重新連線，最多會嘗試至 `maxReconnectAttempts` 建構函數參數所指示的次數。嘗試重新連線期間，聊天室會處於 `connecting` 狀態，不會發出其他事件。用盡所有重新連線的嘗試次數後，聊天室會轉換為 `disconnected` 狀態並發出 `disconnect` 事件 (附有相關的中斷連線原因)。在 `disconnected` 狀態下，聊天室就不會再嘗試連線；您必須再次呼叫 `connect()` 才能觸發連線程序。

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

Amazon IVS 聊天功能傳訊開發套件提供各種使用者動作，可供傳送訊息、刪除訊息和中斷其他使用者的連線。這些動作都可以在 `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>

這是由兩部分組成的教學課程的第一部分。您將透過使用 JavaScript/TypeScript 建置功能完整的應用程式，來學習使用 Amazon IVS 聊天用戶端傳訊 JavaScript SDK 的基礎知識。我們稱呼該應用程式為 *Chatterbox*。

目標對象是初次使用 Amazon IVS 聊天功能傳訊開發套件的經驗豐富的開發人員。您應該很熟悉 JavaScript/TypeScript 程式設計語言和 React 程式庫。

為了簡潔起見，我們將 Amazon IVS 聊天用戶端傳訊 JavaScript SDK 稱為 Chat JS SDK。

**注意**：在某些情況下，JavaScript 和 TypeScript 的程式碼範例是相同的，因此我們會將兩者的範例合併。

本教學課程的第一部分分為幾個部分：

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 文件，請先閱讀 [Amazon 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/) (聊天用戶端傳訊：JavaScript 版 SDK 參考) (位於 GitHub 上)。

## 必要條件
<a name="chat-js-rooms-prerequisites"></a>
+ 熟悉 JavaScript/TypeScript 和 React 程式庫。如果您不熟悉 React，可在此[井字遊戲教學課程](https://react.dev/learn/tutorial-tic-tac-toe)中學習基礎知識。
+ 閱讀並理解 [Amazon IVS 聊天功能入門](getting-started-chat.md)。
+ 使用現有 IAM 政策中定義的 CreateChatToken 和 CreateRoom 功能建立 AWS IAM 使用者。(請參閱 [Amazon IVS 聊天功能入門](getting-started-chat.md))。
+ 確保將此使用者的私密/存取金鑰儲存在 AWS 憑證檔案中。如需指示，請參閱《[AWS 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 聊天功能入門](getting-started-chat.md)。(如果您未保存該 ARN，稍後可以使用主控台或 Chat API 來查詢。)
+ 使用 NPM 或 Yarn 套件管理工具安裝 Node.js 14\$1 環境。

## 設定本機身分驗證/授權伺服器
<a name="chat-js-rooms-auth-server"></a>

後端應用程式負責建立聊天室並產生 Chat JS SDK 需要的聊天字符，以便對聊天室的用戶端執行身分驗證和授權。您必須使用自己的後端，因為您無法將 AWS 金鑰安全地存放在行動應用程式中；成熟的攻擊者可擷取這些金鑰並取得 AWS 帳戶的存取權。

請參閱 *Amazon IVS 聊天功能入門*中的[建立聊天字符](getting-started-chat-auth.md)。如流程圖所示，您的伺服器端應用程式會負責建立聊天字符。這意味著應用程式必須透過從伺服器端應用程式請求聊天字符，來提供自己產生聊天字符的方法。

在本節中，您將學習在後端建立字符提供者的基礎知識。我們使用快速架構建立即時本機伺服器，以管理使用本機 AWS 環境建立聊天字符的作業。

使用 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` (託管聊天室的 AWS 區域) 和 `tokenProvider` (用於後續步驟中建立的後端身分驗證/授權流程)。

**重要事項**：您必須使用與在 [Amazon IVS 聊天功能入門](getting-started-chat-create-room.md)中建立聊天室的區域相同的 AWS 區域。該 API 是 AWS 區域服務。如需支援區域和 Amazon IVS 聊天功能 HTTPS 服務端點的清單，請參閱 [Amazon IVS 聊天功能區域](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 請求。聊天字符包含開發套件成功建立聊天室連線所需的資訊。Chat API 使用這些字符作為驗證使用者身分、聊天室內功能和工作階段持續時間的安全方式。

在專案導覽器中，建立名為 `fetchChatToken` 的新 TypeScript/JavaScript 檔案。建立 `backend` 應用程式的擷取請求，並從回應中傳回 `ChatToken` 物件。新增建立聊天字符所需的請求主體屬性。使用針對 [Amazon Resource Name (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>

本教學課程的第二部分 (也是最後一部分) 分為幾個部分：

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 文件，請先閱讀 [Amazon 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/) (聊天用戶端傳訊：JavaScript 版 SDK 參考) (位於 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` 類別允許在聊天室中傳送訊息。在此處，您會使用自己在[建立訊息輸入](chat-sdk-js-tutorial-chat-rooms.md#chat-js-rooms-message-input) (在本教學課程的第 1 部分) 中建立的元件來修改 `App`，以便傳送訊息請求。

若要開始，請使用 `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` 的新布林狀態。使用此狀態，根據 `isDeleting` 的目前狀態將 `Button` 的內容更新為不同內容。在 `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` 事件，並透過刪除具有與 `message` 參數相符 ID 的訊息來更新 `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>

這是由兩部分組成的教學課程的第一部分。您將透過使用 React Native 建置功能完整的應用程式，來學習使用 Amazon IVS 聊天用戶端傳訊 JavaScript SDK 的基礎知識。我們稱呼該應用程式為 *Chatterbox*。

目標對象是初次使用 Amazon IVS 聊天功能傳訊開發套件的經驗豐富的開發人員。您應該很熟悉 TypeScript 或 JavaScript 程式設計語言和 React Native 程式庫。

為了簡潔起見，我們將 Amazon IVS 聊天用戶端傳訊 JavaScript SDK 稱為 Chat JS SDK。

**注意**：在某些情況下，JavaScript 和 TypeScript 的程式碼範例是相同的，因此我們會將兩者的範例合併。

本教學課程的第一部分分為幾個部分：

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，請在 [React Native 簡介](https://reactnative.dev/docs/tutorial)中了解基礎知識。
+ 閱讀並理解 [Amazon IVS 聊天功能入門](getting-started-chat.md)。
+ 使用現有 IAM 政策中定義的 CreateChatToken 和 CreateRoom 功能建立 AWS IAM 使用者。(請參閱 [Amazon IVS 聊天功能入門](getting-started-chat.md))。
+ 確保將此使用者的私密/存取金鑰儲存在 AWS 憑證檔案中。如需指示，請參閱《[AWS 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 聊天功能入門](getting-started-chat.md)。(如果您未保存該 ARN，稍後可以使用主控台或 Chat API 來查詢。)
+ 使用 NPM 或 Yarn 套件管理工具安裝 Node.js 14\$1 環境。

## 設定本機身分驗證/授權伺服器
<a name="chat-react-rooms-auth-server"></a>

後端應用程式負責建立聊天室並產生 Chat JS SDK 需要的聊天字符，以便對聊天室的用戶端執行身分驗證和授權。您必須使用自己的後端，因為您無法將 AWS 金鑰安全地存放在行動應用程式中；成熟的攻擊者可擷取這些金鑰並取得 AWS 帳戶的存取權。

請參閱 *Amazon IVS 聊天功能入門*中的[建立聊天字符](getting-started-chat-auth.md)。如流程圖所示，您的伺服器端應用程式會負責建立聊天字符。這意味著應用程式必須透過從伺服器端應用程式請求聊天字符，來提供自己產生聊天字符的方法。

在本節中，您將學習在後端建立字符提供者的基礎知識。我們使用快速架構建立即時本機伺服器，以管理使用本機 AWS 環境建立聊天字符的作業。

使用 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` (託管聊天室的 AWS 區域) 和 `tokenProvider` (用於後續步驟中建立的後端身分驗證/授權流程)。

**重要事項**：您必須使用與在 [Amazon IVS 聊天功能入門](getting-started-chat-create-room.md)中建立聊天室的區域相同的 AWS 區域。該 API 是 AWS 區域服務。如需支援區域和 Amazon IVS 聊天功能 HTTPS 服務端點的清單，請參閱 [Amazon IVS 聊天功能區域](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 請求。聊天字符包含開發套件成功建立聊天室連線所需的資訊。Chat API 使用這些字符作為驗證使用者身分、聊天室內功能和工作階段持續時間的安全方式。

在專案導覽器中，建立名為 `fetchChatToken` 的新 TypeScript/JavaScript 檔案。建立 `backend` 應用程式的擷取請求，並從回應中傳回 `ChatToken` 物件。新增建立聊天字符所需的請求主體屬性。使用針對 [Amazon Resource Name (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>

本教學課程的第二部分 (也是最後一部分) 分為幾個部分：

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` 類別允許在聊天室中傳送訊息。在此處，您會使用自己在[建立訊息輸入](chat-sdk-react-tutorial-chat-rooms.md#chat-react-rooms-message-input) (在本教學課程的第 1 部分) 中建立的元件來修改 `App`，以便傳送訊息請求。

若要開始，請使用 `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` 事件，並透過刪除具有與 `message` 參數相符 ID 的訊息來更新 `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 and 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` 分離出來，以免聊天訊息接聽程式在更新訊息狀態時發生多次重新渲染。請記得將 `ChatMessageContext` 套用至您會使用 `ChatMessageProvider` 的元件。

### 實作
<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>

如果您在應用程式中使用多個並行聊天室，建議您為每個聊天建立各自的提供者，並將其用於聊天提供者。在此範例中，我們建立了 Help Bot 和 Customer Help 聊天。我們為兩者都建立了提供者。

------
#### [ 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 />
    </>
  );
};
```