

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

實驗時，請嘗試在聊天室中實作其他動作，例如中斷其他使用者的連線。