

# 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)。