利用者管理アプリ開発:チャット機能の実装
はじめに
今回は、面談記録の詳細ページに実装されているチャット機能について解説していきます!
前回の記事はこちら↓
コントローラーの実装:メッセージ表示
メッセージをデータベースから取得してフロントエンドの渡す仕組みを実装していきます。
コントローラーとリクエストの作成
以下のコマンドを用いてメッセージのコントローラーを生成します。
php artisan make:controller MessageController
MeetingLogControllerのShowメソッドの修正
面談記録のコントローラーであるMeetingLogController
のshow
メソッドを以下のように書き加えます。
public function show(MeetingLog $meetingLog)
{
+ $messages = Message::where('meeting_logs_id', $meetingLog->id)
+ ->latest()
+ ->paginate(10);
return inertia('MeetingLog/Show', [
'meetingLog' => new MeetingLogResource($meetingLog),
+ 'messages' => MessageResource::collection($messages),
]);
}
アクセスした詳細ページを同じmeeting_logs_id
を持つメッセージを、最新のものから10件ずつページ分割して取得します。
そして、messages
としてフロントエンド側にデータを渡しています。
コントローラーの実装:メッセージの保存
メッセージの保存機能を実装していきます。
MeetingLog
コントローラーのstore
メソッドを以下のように書き換えます。
public function store(StoreMessageRequest $request)
{
$data = $request->validated();
$data['sender_id'] = Auth::user()->id;
$message = Message::create($data);
SocketMessage::dispatch($message);
return new MessageResource($message);
}
まずvalidated()
で、送られてきたデータをバリデーションします。
次に、現在ログインしているユーザーのIDを送信者IDとしてデータの中に加えます。
さらに、create()
メソッドを用いて、データベースに登録します。
その後、dispatch()
を用いて、メッセージが送信されたことをイベント側に通知する。
最後に、メッセージの内容をフロントエンド側に返して、メッセージが作成されたことを伝えます。
コントローラーの実装:メッセージの削除
メッセージの削除機能を実装していきます。
MeetingLog
コントローラーのdestroy
メソッドを以下のように書き換えます。
public function destroy(Message $message)
{
if ($message->sender_id !== Auth::user()->id) {
return response()->json(['message' => 'Forbidden'], 403);
}
$message->delete();
return response('', 204);
}
まず、メッセージ送信者のIDがログインしているユーザーのIDと一致するか確認します。
一致しない場合、つまり送信者がログインしているアカウントではない場合、エラーコード403をフロントエンド側に返します。
一致している場合は、delete()
メソッドで削除処理を行い、コード204をフロントエンド側に返します。
執筆段階では、削除機能の実装完了していませんが、今後実装予定です。
リソースの実装
続いて、リソースを実装していきます。
リソースの作成
以下のコマンドを用いて、リソースファイルを生成していきます。
php artisan make:resource MessageResource
メッセージリソースの実装
メッセージのリソースは以下のように実装します。
class MessageResource extends JsonResource
{
public static $wrap = false;
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'message' => $this->message,
'sender' => new UserResource($this->sender),
'meeting_logs_id' => $this->meeting_logs_id,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
}
基本的には、$this
を用いてMessage
モデルの各カラムの値を参照しています。
sender
は送信者のIDをもとに該当する送信者の情報をUserResource
に沿ったデータの形にして参照しています。
イベントの実装
チャットにメッセージを送信したときに、ページ更新を挟むことなく、チャットを開いているユーザー全員にメッセージを反映するために、イベントを実装していきます。
イベント
イベントの実装のためにlaravel broadcast
とlaravel reverb
を利用していきます。
▼公式ドキュメント:ブロードキャスト
▼公式ドキュメント:リバーブ
イベントファイルの生成
以下のコマンドを用いて、イベントファイルを生成します。
php artisan make:event SocketMessage
イベントのReverbサーバーへの送信設定
メッセージが送信されたとき、すなわちstore
メソッド内でSocketMeesage
をdispatch
したときに、他ユーザーにもリアルタイムにメッセージが送信したいです。
そのために、ShouldBroadcast
かShouldBroadcastNow
インターフェースを継承する必要があります。
class SocketMessage implements ShouldBroadcastNow
{
}
今回は、直ちにメッセージを反映したいので、ShouldBroadcastNow
を継承します。
メッセージが送信されたときに実行されるイベントの実装
ShouldBroadcastNow
インターフェースを継承した場合、broadcastOn
メソッドを定義する必要があります。
broadcastOn
メソッドでは、発生したイベントがどのチャンネルに送信するかを記述していきます。
public function broadcastOn(): array
{
$m = $this->message;
$channels = [];
if ($m->meeting_logs_id) {
$channels[] = new PrivateChannel('message.meetinglog.' . $m->meeting_logs_id);
}
return $channels;
}
今回は、各面談記録に対して、チャット機能がついているので、メッセージデータ内のmeeting_logs_id
を使って、チャンネルを面談記録ごとに分けています。
また、認証されたユーザーにのみチャンネルを許可したいので、PrivateChannel
クラスを使っていきます。
なお、誰でも書き込めるようなオープンチャットにしたい場合は、Channel
クラスを使います。
チャンネルの許可
PrivateChannel
を用いた場合、routes/channels.php
で実際に特定のチャンネルをリッスンできることを認証する必要があります。
Broadcast::channel('message.meetinglog.{meetinglogId}', function(User $user) {
return $user ? $user : null;
});
今回は、ログインできるユーザーであれば誰でもチャットに参加できるようにしたいので、ユーザーデータが存在するかどうかの確認のみ行っています。
送信データの形を指定するときの実装
配信されるデータをより細かく制御したいときは、broadcastWith
メソッド用いて、設定していきます。
public function broadcastWith(): array
{
return [
'message' => new MessageResource($this->message),
];
}
今回は、MessageResource
でメッセージデータの形を作っているので、それを利用していきます。
リクエストの実装
まず、以下のコマンドを用いてリクエストファイルを生成します。
php artisan make:request StoreMessageRequest
MessageController
のstore
メソッドでバリデーションを行っているので、生成したStoreMessageRequest.php
にて、バリデーションルールを設定していきます。
class StoreMessageRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
"message" => "required|string",
"meeting_logs_id" => "required",
];
}
}
メッセージの内容は文字列型かつ必須である必要があり、メッセージと面談記録を紐付けるmeeting_logs_id
は必須なのでrequired
にしています。
ルーティング
最後に、ルーティングを設定して、バックエンド周りはほぼ実装完了です。
Route::middleware(['auth', 'verified'])->group(function () {
Route::post('/message',[MessageController::class, 'store'])
->name('message.store');
Route::delete('message/{message}',[MessageController::class, 'destroy'])
->name('message.destroy');
Route::get('/message/older/{message}',[MessageController::class, 'loadOlder'])
->name('message.loadOlder');
});
チャット機能の表示部分の実装
続いて、フロントエンド部分の実装に入っていきます。
Show.jsxの編集
チャットと入力欄が表示できるように以下のように実装します。
以下、部分ごとに解説していきます。
なおMessgeItem
とMessageInput
コンポーネントは後ほど解説します。
ここでは、useRef
を用いて、メッセージ表示部分のDOM要素を取得しています。
const messagesCtrRef = useRef(null);
そしてuseEffect
を用いて、ページがレンダリングされるたびに、scrollTop
の値をscrollHeight
の値に書き換えて、スクロールバーが一番下に来るようにしています。
useEffect(() => {
setTimeout(() => {
if (messagesCtrRef.current) {
messagesCtrRef.current.scrollTop = messagesCtrRef.current.scrollHeight;
}
}, 10);
//// 省略 ////
}, [meetingLog]);
さらに、メッセージが更新されるたびに、メッセージデータを更新して最新のメッセージを反映したいので、useEffect
を追加します。
export default function Show({ auth, meetingLog, messages }) {
const [localMessages, setLocalMessages] = useState([]);
////省略////
useEffect(() => {
setLocalMessages(messages ? messages.data.reverse() : []);
}, [messages]);
}
useEffect
を用いることで、messages
の値が更新されるたびに、再レンダリングをしてlocalMessages
を更新するようにしています。
再レンダリングの際、取得されるメッセージデータは最新のデータから降順で並んでします。
しかし、メッセージは、新しいものを一番下に表示して、上に遡るほどに古いものにしたいので、messages.data.reverse()
することで、昇順にしています。
続いて、return
部分の解説をしていきます。
<div>
<label className="font-bold text-lg">チャット</label>
<div className="mt-1 whitespace-pre-wrap">
<>
<div
ref={messagesCtrRef}
className="flex-1 overflow-y-auto p-5 max-h-[400px]"
>
{/* {messages} */}
{localMessages.length === 0 && (
<div className="flex justify-center items-center h-full">
<div className="text-lg text-gray-500">
メッセージがありません
</div>
</div>
)}
{localMessages.length > 0 && (
<div className="flex-1 flex flex-col">
<div ref={loadMoreIntersect}></div>
{localMessages.map((message) => (
<MessageItem
key={message.id}
message={message}
/>
))}
</div>
)}
</div>
<MessageInput meetingLogId={meetingLog.id} />
</>
</div>
</div>
DOM要素を記録しているref
、すなわちmessageCtrRef
とloadMoreIntersect
は次回の記事で解説予定です。
チャット欄の構成は、ラベル・メッセージ表示部分・チャット入力部分に分かれています。
<div>
<label className="font-bold text-lg">チャット</label>
<div className="mt-1 whitespace-pre-wrap">
<>
<div
ref={messagesCtrRef}
className="flex-1 overflow-y-auto p-5 max-h-[400px]"
>
{/* {messages} */}
</div>
<MessageInput meetingLogId={meetingLog.id} />
</>
</div>
</div>
チャットの表示部分は、表示するメッセージがある場合は表示をし、無い場合は「メッセージがありません」と表示されるようになっています。
メッセージの表示は、MessageItem
コンポーネントを作成して用いています。
{localMessages.length === 0 && (
<div className="flex justify-center items-center h-full">
<div className="text-lg text-gray-500">
メッセージがありません
</div>
</div>
)}
{localMessages.length > 0 && (
<div className="flex-1 flex flex-col">
<div ref={loadMoreIntersect}></div>
{localMessages.map((message) => (
<MessageItem
key={message.id}
message={message}
/>
))}
</div>
)}
UserAvatarコンポーネントの実装
ユーザーのアイコンを表示するためのコンポーネントを作ります。
user.avatar
に値が存在する場合は、画像URLを参照して、アイコン画像を表示します。
ない場合は、アカウント名の頭文字を用いてアイコン画像の代わりにしています。
cssの書き方は公式ドキュメントを参考にしました。
MessageItemコンポーネントの実装
各メッセージを表示するためのコンポーネントを作ります。
こちらも、公式ドキュメントを参考にしています。
そのメッセージの送信者が現在ページを開いているユーザーである場合は、吹き出しを右側(chat-start
)に、そうでない場合は、吹き出しを左側(chat-end
)に出すようにしています。
MessageInputコンポーネントの実装
メッセージ入力のコンポーネントを作ります。
NewMessageInput
コンポーネントには、メッセージの内容であるnewMessage
と送信処理をするonSendClick
、入力したテキストを反映するsetNewMessage
を渡しています。
送信ボタンを押すと、onSendClick
関数を呼び出して、送信処理を行います。
messageSending
の値がtrue
の場合、ローディングを表すアイコンを表示するようにしています。
const [newMessage, setNewMessage] = useState("");
const [inputErrorMessage, setInputErrorMessage] = useState("");
const [messageSending, setMessageSending] = useState(false);
// meetingLogIdがオブジェクト型だったので配列型に直している
const id = Object.values(meetingLogId)
const onSendClick = () => {
if (messageSending) {
return;
}
if (newMessage.trim() === "") {
setInputErrorMessage("メッセージを入力してください");
setTimeout(() => {
setInputErrorMessage("");
}, 3000);
return;
}
const formData = new FormData();
formData.append("message", newMessage);
formData.append("meeting_logs_id", id[0]);
setMessageSending(true);
axios.post(route("message.store"), formData, {
onUploadProgress: (progressEvent) => {
const progress = Math.round(
(progressEvent.loaded / progressEvent.total) * 100
);
console.log("progress", progress);
}
}).then((response) => {
setNewMessage("");
setMessageSending(false);
}).catch((error) => {
setMessageSending(false);
});
}
useState
を用いて、以下の3つの変数を定義していきます。
変数名 | 説明 |
---|---|
newMessage | メッセージ内容を書くのする変数 |
inputErrorMessage | 入力時のエラー内容を格納する変数 |
messageSending | メッセージが送信中かどうかを表す変数 |
次に、送信ボタンが押されたときの処理であるonSendClick
を実装していきます。
まず、メッセージが送信中である場合は、処理を終了します。
次に、newMessage.trim()
で、入力した文字列の両端の空白文字を削除した値が何もなければ、エラーメッセージを3秒間表示します。
また、formData
インターフェースを用いて、メッセージを保存するためのフォームデータを作っていきます。
以下のサイトによるとFormData
とは、
FormData インターフェイスは、フォームフィールドおよびそれらの値から表現されるキーと値のペアのセットを簡単に構築する手段を提供します。
append()
メソッドを用いて、キーとデータのペアを作っていきます。
そして、axios
ライブラリを用いて、バックエンドにPOSTリクエスト送信していきます。
バックエンド側から正常なレスポンスが返されたら、入力したテキストを初期化して、messageSending
をfalse
にします。
NewMessageInputコンポーネントの実装
メッセージ入力のテキスト入力欄のコンポーネントです。
まず、表示部分はデフォルトで1行の<textarea>
を使っています。
onKeyDown
でなにかキーが押されたときにonInputKeyDown
関数を呼び出しています。
onInputKeyDown
は、エンターキーとシフトキー以外のキーが押された場合、ev.preventDefault
で他のイベントストップさせた上で、onSend
、すなわちMessageInput.jsx
のonSendClick
を実行して、メッセージを送信しています。
onChange
でonChangeEvent
関数を呼び出して、入力値が変わった場合、adjustHeight
を呼び出しています。
adjustHeight
は、textarea
タグの高さの設定をauto
にした上で、要素の高さscrollHeight
に1足した値を返しています。
これにより、Shift+Enterを押した場合と、入力欄をはみ出した場合、改行がなされ、テキスト入力欄が1行分拡張されます。
動作確認
一通り実装できたと思うので、http://localhost:8000/meetinglog
にアクセスして、適当な面談記録を開いてみます。
すると以下のような出力になりました。
どうやら、TailwindCSS
内のDaisyUI
が機能していないみたいです。
DaisyUIの有効化
公式ドキュメントによると、TailwindCSS
の設定ファイルであるtailwind.config.js
を開いて、DaisyUI
をインポートする必要があるみたいです。
▼公式ドキュメント
インポートするには、以下のように記述する必要があります。
/** @type {import('tailwindcss').Config} */
export default {
- plugins: [forms],
+ plugins: [forms, require('daisyui')],
};
再度、http://localhost:8000/meetinglog
にアクセスして、適当な面談記録を開いてみます。
ちゃんとメッセージが出力されています。
リアルタイムチャットの実装
リアルタイムにチャットがチャットを送信したユーザーとページ開いているユーザーに反映されるように、機能を実装していきます。
HandleInertiaRequests.phpの編集
自分の作成した面談記録のチャットチャンネルは、常にリッスンしておきたいです。
なので、どのページにいても、作成した面談記録の情報を取得できる状態にしておきたいです。
この状態を実現するために、Inertiajs
のデータ共有機能を使っていきます。
実装するには、app/Http/Middleware/HandleInertiaRequests.php
を以下のように書き換えます。
public function share(Request $request): array
{
return [
...parent::share($request),
'auth' => [
'user' => $request->user(),
],
'conversations' => Auth::id() ? MeetingLog::getMeetingLogsForUser() : [],
];
}
後で説明しますがgetMeetingLogsForUser
メソッドは、ログインしているユーザーが作成した面談記録の一覧を配列として取得するメソッドです。
こうすることで、どのページからでもconversations
にアクセスして、自身の作成した面談記録のデータを取得することができます。
MeetingLog.phpの編集
上述した該当面談記録を取得するgetMeetingLogsForUser
の実装は、MeetingLog
モデルに記述していきます。
public static function getMeetingLogsForUser()
{
$userId = Auth::user()->id;
$query = MeetingLog::where("user_id", "=", $userId);
return $query->get()->toArray();
}
ログインしているユーザーIDを取得して、id
を基にユーザーが作成した面談記録を取得し、配列型で返しています。
AuthenticatedLayout.jsxにイベントリスナーを追加
上記で作成したデータの面談記録IDをフロントエンドから取得して、それを基にリッスンするチャンネルを判別していきます。
HandleInertiaRequests.php
で設定したデータにアクセスするには、usePage
フック使います。
さらに、Echo
ライブラリのprivate
メソッドを利用して、上記で設定したプライベートチャンネルにアクセスします。
export default function AuthenticatedLayout({ header, children }) {
const page = usePage();
const user = page.props.auth.user;
const conversations = page.props.conversations;
useEffect(() => {
conversations.forEach((conversation) => {
let channel = [];
if (user.id === conversation.user_id) {
channel = `message.meetinglog.${conversation.id}`
}
Echo.private(channel)
.error((error) => {
console.error(error);
})
.listen("SocketMessage", (e) => {
console.log("SocketMessage", e);
const message = e.message;
});
});
return () => {
conversations.forEach((conversation) => {
let channel = `message.meetinglog.${conversation.id}`;
Echo.leave(channel);
});
}
},[conversations]);
}
useEffect
を用いて、conversations
が更新されるごとに再レンダリングされるようにします。
useEffect(() => {
},[conversations]);
次に、取得した配列データであるconversations
のIDとログインしているユーザーのIDが同じ場合、接続するchannel
を設定して、private
メソッドでチャンネルに接続します。
conversations.forEach((conversation) => {
let channel = [];
if (user.id === conversation.user_id) {
channel = `message.meetinglog.${conversation.id}`
}
});
Echo.private
メソッドでチャンネルに接続します。
Echo.private(channel)
.error((error) => {
console.error(error);
})
.listen("SocketMessage", (e) => {
const message = e.message;
});
さらに、listen
メソッドを用いて、SocketMessage
イベントをリッスンします。
リッスンしているチャンネルにメッセージが送信されたら、message
にメッセージ内容が取得されます。
チャンネルを離れるときは、leave
メソッドを用いて、チャンネルを離れます。
return () => {
conversations.forEach((conversation) => {
let channel = `message.meetinglog.${conversation.id}`;
Echo.leave(channel);
});
なお、Echo
ライブラリは、resources/js/echo.js
でインポートされ、インスタンス化されています。
echo.jsのソースコード
import Echo from 'laravel-echo';
import Pusher from 'pusher-js';
window.Pusher = Pusher;
window.Echo = new Echo({
broadcaster: 'reverb',
key: import.meta.env.VITE_REVERB_APP_KEY,
wsHost: import.meta.env.VITE_REVERB_HOST,
wsPort: import.meta.env.VITE_REVERB_PORT ?? 80,
wssPort: import.meta.env.VITE_REVERB_PORT ?? 443,
forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https',
enabledTransports: ['ws', 'wss'],
});
Show.jsxにイベントリスナーを追加
今回のアプリでは、自身の作成した面談記録と現在開いているページの面談記録のチャンネルをリッスンできるようにしたいと考えています。
AuthenticatedLayout.jsx
では、作成した面談記録のチャンネルにアクセスできるようにしたので、各面談記録の詳細画面では、その面談記録のチャンネルにアクセスできるようにしていきたいです。
なので、AuthenticatedLayout.jsx
と同様の実装をShow.jsx
にも施していきます。
export default function Show({ auth, meetingLog, messages }) {
////省略////
const page = usePage();
const conversations = page.props.conversations;
useEffect(() => {
let channel = `message.meetinglog.${meetingLog.id}`;
conversations.forEach((conversation) => {
if (channel === `message.meetinglog.${conversation.id}`) {
channel = [];
return;
}
});
if (channel.length != 0) {
Echo.private(channel)
.error((error) => {
console.error(error);
})
.listen("SocketMessage", (e) => {
console.log("SocketMessage", e);
const message = e.message;
if (message.sender_id === auth.id) {
return;
}
});
return () => {
let channel = `message.meetinglog.${meetingLog.id}`;
Echo.leave(channel);
}
}
}, [meetingLog]);
////省略////
}
AuthenticatedLayout.jsx
の実装とほぼ同じです。
ただし、自分が作成した面談記録のページにアクセスした場合、2重にチャンネルへアクセスするのを防ぐために、conversations
のIDと比較して、同じIDが見つかった場合は、channel
の値を空にします。
そして、channel
に値が入っている場合のみ、チャンネルに接続するようにしています。
EventBusの実装
ここまでで、リアルタイム送受信を実装することができました。
しかし、このままではメッセージの内容をページへリアルタイムに反映することができません。
EventBus.jsx
を実装することでそれを実現していきます。
EventBus.jsxの実装
メッセージをページへリアルタイムに反映するために、on
/emit
関数を実装します。
そして、それらの関数をAuthenticatedLayout.jsx
とShow.jsx
の異なるレイヤーのコンポーネントに反映する必要があります。
これを実現するために、Context
を用いていきます。
Context
とは、通常Propsをバケツリレーして異なるレイヤーのコンポーネントへ渡さなければならないような情報を、任意のコンポーネントがダイレクトに情報を受け取れる仕組みです。
それでは、EventBus.jsx
を実装していきます。
import React from "react";
export const EventBusContext = React.createContext();
export const EventBusProvider = ({ children }) => {
const [events, setEvents] = React.useState({});
const emit = (name, data) => {
if (events[name]) {
for (let cb of events[name]) {
cb(data);
}
}
}
const on = (name, cb) => {
if(!events[name]) {
events[name] = [];
}
events[name].push(cb);
return () => {
events[name] = events[name].filter((callback) => callback !== cb);
}
}
return (
<EventBusContext.Provider value={{emit, on}}>
{ children }
</EventBusContext.Provider>
);
}
export const useEventBus = () => {
return React.useContext(EventBusContext);
}
まず、Context
の利用を宣言するために、createContext
メソッドを用います。
後程、アプリ全体にEventBus
を反映するために、app.jsx
をラップするEventBusProvider
を作ります。
このEventBusProvider
の戻り値に、EventBusContext.Provider
がchildren
をラップしたものを設定します。
このとき、emit
/on
関数をprops
として渡します。
こうすることで、ラップされたchildren
すなわちapp.jsx
以降の子コンポーネントのどこからでもemit
/on
関数を呼び出すことができるようになりました。
emit
/on
関数はそれぞれ以下のような役割があります。
-
on
-
event[name].push(cb)
でevents
配列にname
をキー/cb
を値として保存する。 -
cb
以外のコールバックをevent[name]
に代入して返す。
-
-
emit
-
event[name]
に値が存在している場合、すべてのコールバック関数をdata
を引数として実行する。
-
app.jsxの編集
アプリ全体にEventBus
コンポーネントを適用するために、App
コンポーネントをEventBusProvider
でラップします。
createInertiaApp({
title: (title) => `${title} - ${appName}`,
resolve: (name) => resolvePageComponent(`./Pages/${name}.jsx`, import.meta.glob('./Pages/**/*.jsx')),
setup({ el, App, props }) {
const root = createRoot(el);
root.render(
+ <EventBusProvider>
<App {...props} />
+ </EventBusProvider>
);
},
progress: {
color: '#4B5563',
},
});
Show.jsxの編集
Show.jsx
にon
/emit
とコールバック関数を実装していきます。
ソースコード
export default function Show({ auth, meetingLog, messages }) {
////省略////
+ const { on, emit } = useEventBus();
+ const messageCreated = (message) => {
+ if (meetingLog && meetingLog.id == message.meeting_logs_id) {
+ setLocalMessages((prevMessages) => [...prevMessages, message]);
+ }
+ }
useEffect(() => {
let channel = `message.meetinglog.${meetingLog.id}`;
conversations.forEach((conversation) => {
if (channel === `message.meetinglog.${conversation.id}`) {
channel = [];
return;
}
});
if (channel.length != 0) {
Echo.private(channel)
.error((error) => {
console.error(error);
})
.listen("SocketMessage", (e) => {
console.log("SocketMessage", e);
+ const message = e.message;
+ emit("message.created", message);
+ });
});
return () => {
let channel = `message.meetinglog.${meetingLog.id}`;
Echo.leave(channel);
}
}
}, [meetingLog]);
useEffect(() => {
setTimeout(() => {
if (messagesCtrRef.current) {
messagesCtrRef.current.scrollTop = messagesCtrRef.current.scrollHeight;
}
}, 10);
+ const offCreated = on('message.created', messageCreated);
+ return () => {
+ offCreated();
+ }
}, [meetingLog]);
}
まず、const { on, emit } = useEventBus();
で、on
/emit
を宣言します。
次に、コールバック関数messageCreated
を実装します。
ここでは、setLocalMessages((prevMessages) => [...prevMessages, message]);
で、チャットの内容であるlocalMessages
の値を、入力したメッセージを追加して更新します。
さらに、listen
メソッド内に、emit
関数を追加することで、他ユーザーからメッセージを受信した場合や、自身がメッセージを送信した場合に、コールバック関数を呼び出して、localMessages
の更新を行っています。
そして、メッセージが追加された際のスクロール処理の部分に、on
関数を追加することで、ページにアクセスした際に、emit
で呼び出されるコールバックをname
キーとともに設定する。
on
関数の戻り値として受け取ったoffCreated
を、useEffect
のreturn
部分で実行することで、コンポーネントのアンマウント時に、マウント時に作成した自身のコールバックを破棄しています。
AuthenticatedLayout.jsxの編集
Show.jsx
では、開いた面談記録のみemit
でコールバックを作成しているので、自身の作成した面談記録のコールバックはAuthenticatedLayout.jsx
で実装します。
ソースコード
export default function AuthenticatedLayout({ header, children }) {
////省略////
+ const { emit } = useEventBus();
useEffect(() => {
conversations.forEach((conversation) => {
let channel = [];
if (user.id === conversation.user_id) {
channel = `message.meetinglog.${conversation.id}`
}
Echo.private(channel)
.error((error) => {
console.error(error);
})
.listen("SocketMessage", (e) => {
console.log("SocketMessage", e);
+ const message = e.message;
+ emit("message.created", message);
});
});
});
return () => {
conversations.forEach((conversation) => {
let channel = `message.meetinglog.${conversation.id}`;
Echo.leave(channel);
});
}
},[conversations]);
}
Show.jsx
と同様の手順でemit
関数を追加しています。
おわりに
今回は、チャット機能の実装を解説しました。
自分の理解を深めるために、かなり長々と解説してしまいました。
まだ理解の及んでない部分のあるかと思うので、随時更新していきます!
ではでは!
Discussion