PHPで"誰々が入力しています"と表示できるチャットを作った

# はじめに

大学の卒業研究で、チャットについて調査をしました。
チャットでは、誰かが入力しているときに「入力しています」と表示されるものがあります。しかしこの機能は必ずあるわけではなく、例えばSlackにはありますが無効化することもできます。またLINEでは誰かが入力中でもわからないようになっています。このような「入力中」という表示があると入力を一旦待つ人がいたり、また話しやすさに影響があるのではないかと考えました。

この記事内では、コードの一部を抜粋して掲載していますが、全体をGitHubで公開する予定です。これを元に必要に応じて修正してお使いください。


# 注意

卒業研究のために少人数かつ短期間の利用を想定して制作しました。そのため、簡易的な仕組みで動いておりセキュリティ対策は特に行っていません。例えば、アドレスがわかれば誰でもチャットの内容にアクセスできるようになっています。また、データベース名やパスワード等をPHPファイルに直書きしていますが、これらが流出しないとも限らないので検討が必要と思います(GitHubで公開したコードでは`****`に書き換えています)。実際に公開して利用する場合には、入力・送信内容をチェックするなどセキュリティに配慮するように改良する必要があります。
現状ではデータベースにアクセスするPHPファイル(7個ある)それぞれにパスワード等を書かなければならないので(少し面倒)、パスワード等を書く一つのファイルを用意し、これを読み込むようにする改良できるかもしれません。
なお現時点では、メッセージを削除する機能がありません。


# このチャットのキーポイント

サーバーではPHPとPostgreSQLを使用します。また、チャットはブラウザで閲覧・投稿をするようになっています。
比較的最近の一般的なブラウザであればOSを問わず、PC、スマートフォン、タブレット端末から利用できます。
ただしテレビやゲーム機のブラウザや、アプリ内ブラウザではうまく動かないかもしれません。

誰かが入力しているときに、入力していることを他の参加者がわかるようになっています。
"入力中"の表示方法については3種類あります。


# 使い方

本アプリケーションの使い方を簡単に説明します。
まず、GitHubのリポジトリからzipファイルをダウンロードするか、クローンします。GitHubでは、ログインしなくてもブラウザからzipファイルをダウンロードできます。私はGitHub経由でHerokuにデプロイしました。勝手に更新されたくないので、Manual deployにしました。PHPファイルのソースコード中にデータベースのパスワード等を書く必要があるのでプライベートリポジトリを使いましょう。コードがPCに保存されている場合、PCからHerokuへ直接デプロイすることもできます。
話題やメンバーごとにトークルームを作成することができます。「入力中」の表示方法ごとに必要なJavaScriptファイルが異なります。HTMLファイルのサンプルを3つ用意したので、これを元に8行目あたりにある「dbname_message」「dbname_typing」、14行目あたりにある「titleタグの内容」、19行目あたりにある「h2タグの内容」をそれぞれ変更します。
最後に、データベースにトークルーム用のテーブルを作成します。管理用のHTMLファイルを用意したので、これを使うと便利だと思います。メッセージが送信できるか、「入力中」の表示が出るか、等を確認してください。これで準備完了です。
![ブラウザでGitHubからZIPファイル形式でダウンロードする](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/517986/3b5987cf-a3fa-1879-a019-dcb254f1e792.png)


Herokuのダッシュボード画面右上にある、点が9個あるボタン(弁当メニューというそうです)をクリックし、Dataを開けるとデータベースの管理画面に行けます。(https://data.heroku.com/に行く)ここで、Settingsの「Database Credentials」のところにある「View Credentials...」のボタンをクリックするとデータベースのパスワード等が表示されます。
PHPファイルの`$host_url`にHerokuの管理画面にある`Host`の内容、`$db_name`に`Database`の内容、`$php_user`に`User`の内容、`$php_pass`に`Password`の内容をそれぞれコピペします。

チャットの利用者は、まずチャットのURLをWebブラウザで開きます。次にポップアップが表示されるので、ユーザー名とIDを入力します。そして、画面上部にはチャットのトークルーム名と名前・IDが表示されていることを確認します。画面の中央部にはこれまでに送信されたメッセージが表示されます。画面下部には入力欄と送信ボタンがあります。また設定によっては誰かが入力中なら「入力中」と表示される場合があります。


# "入力中"の表示方法

このチャットでは誰かが入力しているときに、他の誰かが入力しているということがわかる場合とわからない場合があります。また、他の誰かが入力しているということがわかる場合でも、誰が入力しているかわかる場合と誰が入力しているのかはわからない場合があります。

| Type | 誰かが入力していたらわかる | 誰が入力しているかわかる | 1人が入力している場合 | 2人以上入力している場合 | 誰も入力していない場合 |
|:-:|:-:|:-:|:-:|:-:|:-:|
| A | ⭕️ Yes | ⭕️ Yes | 〇〇が入力しています | 〇〇、〇〇 が入力しています | 誰も入力していません |
| B | ⭕️ Yes | ❌  No | 入力している人がいます | 入力している人がいます | 誰も入力していません |
| C | ❌  No | ❌  No | (表示なし) | (表示なし) | (表示なし) |

（〇〇の部分は入力者名）

スクリーンショット


# トークルーム管理

トークルームごとにHTMLファイルを一つ用意します。ベースとなるHTMLファイルからの変更するところは、JavaScriptのファイルを"入力中"の表示方法に応じて選択することと、ルームIDをHTMLファイルのheadに直書きしていることです。なお、当初はJavaScriptファイル、PHPファイルともにトークルームごとに用意していたのですが、管理が面倒すぎるので共通で使えるように修正しました。

現状では、トークルームごとにHTMLを作ってデプロイしなおさなければならないので、PHPでHTMLファイルを自動生成したり、HTMLもPHPで自動生成するように改良できると思います。


# メッセージの送信と表示

入力されたメッセージをXMLHttpRequest(XHR)を利用して送信します。mozillaのサンプルコードでは、`xhr.send();`の引数にPHPのURLの引数を渡していましたが、その方法ではうまく動きませんでした。そのため引数を含めたURLを生成し、`xhr.send();`の引数は空にしました。
XHRでは第3引数を`true`にし、非同期で処理を行っています。
なお、ユーザーが入力するときには改行も含めて入力できますが、現時点では改行は削除されます。
「入力中」の情報を送信する部分もXHRを利用して、ユーザー名とユーザーID、ルーム名を送信しています。


# データベース管理

サーバーでのデータベース管理はすべてPHPファイルに書かれたPDOによって処理されます。Postgresqlを利用しましたが、PDOの部分を少し変更するだけでMySQL等でも同様の処理ができると思います。
Heroku標準の「Heroku Postgres」を使いましたが、Herokuを別の目的で使っている場合やデプロイしなおす場合にHeroku内のappが別であってもデータベースは共通で使われるようです。テーブル名が同じ場合同じデータが出てくるので注意が必要です。


# PHPで取得したJSONのUNICODEを読めるようにする

データベースからPHPで生成したJSONファイルを見ると、`"\u3042\u3044\u3046"`のような形式になっています。Unicodeでエンコードされているようです。これを読めるように変換するには、VSCodeに「Encode Decode」という拡張機能をインストールし、Unicodeの部分を選択状態にしてから`Unicode to String`を実行するとできました。以下のサイトがとても参考になりました。

https://yukushige.com/blog/?p=511


# ログイン処理

ここの見出しには`ログイン処理`なんて書いてありますが、実際にはよくあるIDやパスワードを入力するようなログイン処理はいたしません。今回は、登録不要ですぐに参加できるよう、ポップアップに任意の名前とIDを入力する形式にしています。名前は日本語の漢字やカナも使えます。IDは3〜4桁程度の数字としました。

正しく入力されたか、以下のようなよくある構文を使って確認しています。

```javascript
if (!hoge) { ... }
```

しかし、これでは`null、undefined、数字の0、空文字('')、false`の場合でも正しく入力できていないことになります。例えば名前が0(数字のゼロ)だった場合でも正しく入力されていないことになります。
IDは0より大きい数字ではない場合に正しく入力されていないことになります。
名前やIDは事前にルールを決めておくと良いと思います。

また、正しく入力されるまで繰り返しポップアップを表示されるので、キャンセルするにはタブを閉じるしかありません。タブを閉じずに再読み込みした場合にも入力が要求されます。キャンセル機能や名前を変更する機能もあったら良いかと思います。


# メッセージを画面上に表示する

初めは新しいメッセージだけを追加しようとしたのですがうまいやり方が見つからず、画面上のメッセージを更新するには全部消してから入れ直すようにしました。複数の子要素をまとめて消すにはループ処理を使うのが良さそうです。私が試した限りでは画面のちらつきはありませんでしたがメッセージ数が増えた場合には画面上のメッセージを更新するたびに重くなりそうです。また少し効率が悪いように思うので、新しいメッセージだけを追加するような良い方法を見つけたい。

メッセージは、JSON形式で取得されます。


# 入力を始めたら通知する

「onfocus」イベントと「onblur」イベントによって、メッセージを入力するテキストボックスが入力状態になったことと入力をやめたことを検知することができます。また、テキストボックスをaddEventListenerでinputされることを検知し、もし一文字でも入力または削除されたら入力中の処理をおこないます。
入力をはじめたら現在のユーザーIDとユーザー名、また現在のチャットがどれかという情報を、先程のメッセージの送受信と同様にサーバーへ送ります。入力をやめた場合には現在のユーザーIDをサーバーに送信し、そのユーザーの"入力中"情報をすべて削除します。POSTメソッドで送信していますが、本当はDELETEメソッドを使った方がよかったかもしれません。
今回の研究ではこのような仕様でも問題ありませんでしたが、"入力中"情報を削除してしまうとどの程度"入力中"の状態になったか後でわからないので、改善の余地がありそうです。

```javascript
input.onblur = inputBlur;
input.onfocus = inputFocus;

inputBlur(){
  //入力をやめた場合の処理
}

inputFocus(){
  // 入力を始めたときの処理
}
```


# Safari対応

Safari(Mac、iPhone、iPad)ではJavaScriptで日時を比較する処理が動きませんでした。コンソールで確認すると(MacのSafariでは環境設定の詳細タブにある「メニューバーに"開発"メニューを表示」を有効にすると「JavaScriptコンソールを表示」ができるようになります)ハイフンの付いた日時では`Invalid Date`なるようです。ちなみにiPhoneやiPadではChromeやEdgeもSafariがベースになっているようで、どのブラウザでも動きませんでした。ハイフンをスラッシュに置換し、タイムコードの処理方法を修正することで、Safariを含め問題なく動くようになりました。


# 誰かが「入力中」だったら画面上でわかるようにする

日時の変数に対し`getTime()`で1970/1/1午前0時から現在までのミリ秒を取得できます。ユーザーの端末上での時計がズレているかもしれないことを考慮し、「入力中」の情報がサーバーに送られてきたらサーバー上の日時を記録し、またPHPが返すJSONにはサーバー上での現在の日時を併せて書き込んでいます。それぞれのユーザーごとの最も後に「入力中」だった時間と現在の時間のそれぞれのミリ秒を比較することによってほんのちょっと前に入力中だった人がわかるというわけです。今回は最近の9900ミリ秒(9.9秒)以内に入力中だった人を今「入力中」と判定しています。入力欄にカーソルをあわせたまま入力を中断したり再読み込みしたりする人もいると思うので、「入力中」との情報が送られてきても入力をやめたという情報がない場合はある程度時間がたったら入力をしていないと判定しています。もちろん文字を入力している時間や途中で考えている時間もあるので、ある程度は余裕をとった時間にしました。とはいえ何か根拠があって決めた時間ではないので、この時間については十分に検討する必要があるかもしれません。
9900ミリ秒以内に入力中で、なおかつ自分ではない人のユーザー名とIDを配列に格納しています。また、同じ人が何人も同時に表示されないように、IDを入れる配列を確認し、まだ追加されていない場合のみ追加するようにしています。誰かが入力中のときに「入力している人がいます」と表示する場合(typeB)では、一人でも入力中とわかったらこれ以上入力中の人を見つける必要はないので、`break`文でforループを終了します。また、入力中の人がわからないようになっているため、入力中のユーザーを格納する配列はありません(名前を表示する必要がない)


# Bootstrapのカスタム

Bootstrap4では、SCSSでのカスタマイズができるようになっています。まずはダウンロードページで「ソースをダウンロード」ボタンをクリックしてbootstrap-4.3.1.zipをダウンロードしました。次にダウンロードしたzipファイルを展開し、展開された先にあるscssフォルダに以下のようなscssファイルを保存します。実際に変更したのは実質色だけですが、カスタムされたbootstrapのファイルを使えばいつもテーマカラーを反映された状態にできるので便利です。
ここでテーマカラーは最初に書くことと、セミコロンを書き忘れないことが重要です。なお、今回は最後の`@import ./bootstrap;`の部分を相対パスで書きました。拡張子は不要なようです。最後にpreprosというソフトでコンパイルを行いました。右側のバーにある`OUTPUT FILE`の下に`→ custom.css`とありますが、ここをクリックすると保存場所とファイル名を変更できます。
![Prepros.app](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/517986/2706b59f-a798-ac57-41ce-12009fa6aace.png)


```custom.scss
$theme-colors: (
  "primary": #00519a,
  "success": #005100,
  "info": #50c5e9,
  "warning": #f69779,
  "danger": #d03721,
  "light": #fef4f4
);

@import "./bootstrap";
```

https://getbootstrap.jp/docs/4.3/getting-started/download/


# 入力中の表示方法について

Slackでは「入力しています(英語では)」
Twitter(DM/グループ)「アイコンと点」
LINE「なし」


# 参考

プログラム実装にあたって少しでも参考にしたサイトのアドレスを書いておきます(順不同)

http://www.tohoho-web.com/bootstrap/index.html
https://getbootstrap.jp/docs/4.3/getting-started/download/
https://prepros.io/
https://qiita.com/tonkotsuboy_com/items/1855734522bfe7ef7dad
https://qiita.com/pikanji/items/111cfb353bd6ec1eabb5
https://qiita.com/pearmaster8293/items/b5b0df28147eb049f1ea
http://artteknika.hatenablog.com/entry/2016/10/02/163609
https://qiita.com/labocho/items/5fbaa0491b67221419b4
https://zxcvbnmnbvcxz.com/javascript-gettime/index.html
https://www.sejuku.net/blog/80755
https://www.php.net/manual/ja/function.is-null.php
https://www.php.net/manual/ja/function.isset.php
https://www.php.net/manual/ja/book.pdo.php
https://www.php.net/manual/ja/language.variables.basics.php
https://www.php.net/manual/ja/language.operators.string.php
https://kiwipan.hatenablog.com/entry/2018/03/27/105253
https://gray-code.com/php/conectted-to-postgresql/
https://gray-code.com/php/create-table-to-postgresql/
https://gray-code.com/php/insert-data-to-postgresql/
https://gray-code.com/php/update-data-by-using-postgresql/
https://gray-code.com/php/insert-data-by-using-pdo/
https://gray-code.com/php/getting-data-by-using-pdo/
https://gray-code.com/php/update-data-by-using-pdo/
https://gray-code.com/php/delete-data-by-using-pdo/
https://qiita.com/H40831/items/69f06dc98a6c446c817c
https://developer.mozilla.org/ja/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest
https://developer.mozilla.org/ja/docs/Web/API/XMLHttpRequest/send
https://developer.mozilla.org/ja/docs/Web/API/HTMLElement/input_event
https://developer.mozilla.org/ja/docs/Web/API/GlobalEventHandlers/onblur
https://developer.mozilla.org/ja/docs/Web/API/GlobalEventHandlers/onfocus
https://developer.mozilla.org/ja/docs/Web/API/GlobalEventHandlers/onload
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Number/toString
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/parseInt
https://developer.mozilla.org/ja/docs/Web/API/XMLHttpRequest/Synchronous_and_Asynchronous_Requests
https://developer.mozilla.org/ja/docs/Web/API/XMLHttpRequest/send
https://qiita.com/phi/items/723aa59851b0716a87e3
https://www.php.net/manual/ja/function.array.php
https://techacademy.jp/magazine/5537
https://qiita.com/kiwaki/items/5995a38e6577dee12767
https://developer.mozilla.org/ja/docs/Learn/JavaScript/Objects/JSON
http://cly7796.net/wp/javascript/create-elements-with-javascript/
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/push
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/includes
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/join
https://www.php.net/manual/ja/function.exit.php
https://www.php.net/manual/ja/function.json-encode.php
https://qiita.com/fantm21/items/891192da1a095e94c9e1
https://yukushige.com/blog/?p=511