チャットApp(チャットルームを検索する)

今回はユーザが作成したチャットルームを検索する機能を追加していきます。それで最初に少し仕様変更を。
前回以下のようにdocumentの部分はusername:idで書き込むようにしていたました。

スクリーンショット 2020-08-20 13.10.31

それを次のように変更しました。

スクリーンショット 2020-08-21 13.52.56

理由としてはfirestoreのクエリでusernameから検索が可能でdocumentまで取得可能なので、一意の値であるuidで書き込めば重複なく書き込めます。もちろんこれはユーザのIDを不特定多数に公開する形になるので実際のサービスだと悪手だと思います。本来ならdocument用に一意のハッシュ値などを生成しユーザIDと紐付けてdocumentに設定する方が良いと思います。しかしこのアプリはちゃちゃっとできるチャットなのでそこらへんのセキュリティは無視しましょう。最終的には認証も匿名認証にしてログアウトした時点でID, チャットルームも消すような設定にしようかと思います。

firestoreに書き込む関数の作成

前置きが長くなりましたが、まずは仕様の変更に伴いfirestoreに書き込む関数を変更しましょう。

functions/database.ts

/**
* firestoreにチャットルームを保存する。
* 保存の成功・失敗ごとにメッセージを返す。
* @param chatroom :chatroomオブジェクトを指定
* @param uid :documentを指定
*/
export const setChatroomToFirestore = async(chatroom:ChatroomType, uid:string):Promise<string> => {
   const msg:string = await FBdb.collection("chatrooms").doc(uid).set({
       owner:    chatroom.owner,
       roomname: chatroom.roomname,
       member:   chatroom.member,
       chats:    chatroom.chats,
   })
   .then(() => {
       return "set chatroom successfully!";
   })
   .catch(() => {
       return "set chatroom failed.";
   });
   return msg;
}
/**
* firestoreから指定されたchatroomオブジェクトを取ってくる。
* 成功したらオブジェクトを返す。
* 保存の成功・失敗ごとにメッセージを返す。
* @param uid :uidを指定
*/
export const getChatroomFromFirestore = async(uid:string):Promise<[string, any]> => {
   let chatroom = {};
   const msg:string = await FBdb.collection("chatrooms").doc(uid).get()
   .then(snapshot => {
       chatroom = {
           owner:    snapshot.data().owner,
           roomname: snapshot.data().roomname,
           member:   snapshot.data().member,
           chats:    snapshot.data().chats,
       }
       return "get chatroom successfully!";
   })
   .catch(error => {
       return "get chatroom failed.";
   });
   return [msg, chatroom];
}/**
* firestoreにチャットルームを保存する。
* 保存の成功・失敗ごとにメッセージを返す。
* @param chatroom :chatroomオブジェクトを指定
* @param uid :documentを指定
*/
export const setChatroomToFirestore = async(chatroom:ChatroomType, uid:string):Promise<string> => {
   const msg:string = await FBdb.collection("chatrooms").doc(uid).set({
       owner:    chatroom.owner,
       roomname: chatroom.roomname,
       member:   chatroom.member,
       chats:    chatroom.chats,
   })
   .then(() => {
       return "set chatroom successfully!";
   })
   .catch(() => {
       return "set chatroom failed.";
   });
   return msg;
}
/**
* firestoreから指定されたchatroomオブジェクトを取ってくる。
* 成功したらオブジェクトを返す。
* 保存の成功・失敗ごとにメッセージを返す。
* @param uid :uidを指定
*/
export const getChatroomFromFirestore = async(uid:string):Promise<[string, any]> => {
   let chatroom = {};
   const msg:string = await FBdb.collection("chatrooms").doc(uid).get()
   .then(snapshot => {
       chatroom = {
           owner:    snapshot.data().owner,
           roomname: snapshot.data().roomname,
           member:   snapshot.data().member,
           chats:    snapshot.data().chats,
       }
       return "get chatroom successfully!";
   })
   .catch(error => {
       return "get chatroom failed.";
   });
   return [msg, chatroom];
}

変更点は仕様変更に伴ったもので、主にdocをuidで指定するようにしました。なので[username].tsxも少し書き換えが必要ですが引数に関する部分なので割愛します。他にもだいぶ変わったような気がしますが変数名などをfirebaseのドキュメントに合わせた等でロジック自体は全然変わっていません。
次に、本題である指定したユーザネームのチャットルームを取得する関数を同じdatabase.tsに追加します。

/**
* firestoreからownerが指定されたusernameのdocument(chatroom)を全て取得する。
* 取得したチャットルームを配列にして返す。
* @param username 
*/
export const getChatroomListWithUsername = async(username:string):Promise<any[]> => {
   let roomList = [];
   await FBdb.collection("chatrooms").where("owner", "==", username).get()
   .then(snapshot => {
       if (snapshot.empty) {
           console.log('No matching documents.');
           return;
       }
   
       snapshot.forEach(doc => {
           // console.log(doc.id, '=>', doc.data());
           const room = {
               id: doc.id,
               owner: doc.data().owner,
           }
           roomList.push(room);
       });
   })
   .catch(error => {
       console.log(error);
   });
   return roomList;
}

説明はコード内のコメントの通りです。ちなみにご存知の方も多いと思いますが関数の上にこのようにコメントを書いておくと、vscodeでは読み込んだ先で関数にカーソルを当てるとこの説明が表示されます。何気に便利なのですが他のエディタでも同様の機能はあるのでしょうか?

話を戻して今作った関数を使って機能を実装していきましょう。ここでは[username].tsxではなく、そこで読み込んでいるSearchBox.tsxに実装します。

components/compo/searchbox.tsx

import { FC, useState } from "react";
import { useRouter } from "next/router";
import BasicParagraph from "../atom/basicP";
import BasicTextField from "../atom/textbox";
import BasicButton from "../atom/button";
import { makeStyles, createStyles } from "@material-ui/core";
import { getChatroomListWithUsername } from "../../functions/database";
const useStyles = makeStyles(() => createStyles({
   container: {
       width: "100%",
   },
   textfield: {
       position: "relative",
       display: "inline-block",
       boxSizing: "border-box",
       width: "70%",
   },
   btnBox: {
       position: "relative",
       display: "inline-block",
       width: "30%",
       textAlign: "center",
       top: "18px",
   },
   ulStyle: {
       listStyle: "none",
       padding: "0",
   }
}));
const SearchBox:FC = () => {
   const classes = useStyles();
   const router  = useRouter();
   const [searchWord, setSearchWord] = useState("");
   const [rooms, setRooms] = useState([]);
   const handleChangeWord = (event:React.ChangeEvent<{ value: unknown }>) => {
       setSearchWord(event.target.value as string);
   }
   
   // firestoreから該当ユーザネームのチャットルームを取得しrooms変数に格納
   const searchChatroom = async() => {
       const roomList = await getChatroomListWithUsername(searchWord);
       setRooms(roomList);
   }
   
   // roomsに格納された値から各ルームへのリンクをボタンとして表示
   const searchResult = () => {
       if (rooms.length === 0) {
           return (
               <ul className={ classes.ulStyle }>
                   <li>ルームなし</li>
               </ul>
           );
       }
       return (
           <ul className={ classes.ulStyle }>
               { rooms.map((room, index) => {
                   return (
                       <li key={ index }>
                           <BasicButton
                               fullWidth={ true }
                               onclick={() => router.push("/chatroom/[roomid]", `/chatroom/${room.id}`) }
                           >
                               { room.owner + ": " + room.id }
                           </BasicButton>
                       </li>
                   );
               })}
           </ul>
       );
   }
   
   return (
       <div className={ classes.container }>
           <div className={classes.textfield }>
               <BasicTextField
                   label="チャットルームを検索"
                   value={ searchWord }
                   onchange={ handleChangeWord }
                   fullWidth={ true }
               />
           </div>
           <div className={ classes.btnBox }>
               <div>
                   <BasicButton
                       onclick={ searchChatroom }
                   >
                       検索
                   </BasicButton>
               </div>
           </div>
           <div>
               <br />
               <BasicParagraph>検索結果</BasicParagraph>
               { searchResult() }
           </div>
       </div>
   );
}
export default SearchBox;

ユーザネームに該当するチャットルームをstateに格納する関数とstateから各ルームへのリンクボタンを生成する関数を追加しています。これで実行すると

スクリーンショット 2020-08-21 11.22.56

スクリーンショット 2020-08-21 11.23.20

スクリーンショット 2020-08-21 11.23.34

このように検索ワードに該当するルームがボタンとして表示されクリックするとそのチャットルームに移動します。チャットルームはpages/chatroom/[roomid].tsxで作成していますが、中はまだ作っていないのでメッセージが表示されるだけです。

さて次回からはいよいよチャット機能を作るためチャットルームを作成していきます。ここでroomid=documentなので一意のものです。つまり誰かを招待したい時はブラウザに表示されているURLを教えるだけで誰でもアクセスすることが出来ます。
見ていただければ解りますがroomidは文字の適当な羅列なので教えた人以外が入ってくることはまずありません。それに上でも話しましたが、最終的には匿名認証のみにし、ログアウトと同時に消去する予定なのでワンタイムパスワードと考えるとセキュリティ上の驚異は微々たるものだと思います。
しかし、教えてもらった人が匿名ログインを済ませてないと問題があります。なのでチャットルームにURLから直接ログインした人はログイン画面にリダイレクトする設定が必要です。しかしそれを確かめるには、ログインしていない端末が必要になるので、匿名認証のIDや関連データが残ってしまう今の状態だと確認が大変です。そこで次回は匿名認証のみのログインだけに変更し、ログアウト機能とログアウトしたら関連データを消去する機能を実装しようと思います。

この記事が気に入ったらサポートをしてみませんか?