firebaseでiOSとブラウザのチャット

[firebase realtime database](https://firebase.google.com/docs/database/?hl=ja) 練習用に。
ブラウザとiOSでチャットの実装。
QRコードを介して招待したりされたり。

#成果物

<div style="display:block;">
<img src="https://j.gifs.com/ElDgK0.gif" height="400" style="display:inline-block;margin:5px;" />
<img src="https://j.gifs.com/mwBz29.gif" height="400" style="display:inline-block;margin:5px;" />
<img src="https://j.gifs.com/58jvlK.gif" height="400" style="display:inline-block;margin:5px;" />
</div>

デモ
https://anywaychat.netlify.com/

ブラウザ
https://github.com/nakadoribooks/anywaychat-web/releases/tag/v1.0.1

iOS
https://github.com/nakadoribooks/anywaychat-ios/releases/tag/v1.0.0

# 実装

## database設計

よくわかってないが、浅く作るということで。

<img width="364" alt="スクリーンショット 2017-08-31 16.23.55.png" src="https://qiita-image-store.s3.amazonaws.com/0/159266/64e1519e-276e-6677-a1ca-1eb9ecbafba9.png" style="border:1px solid #cccccc; padding:10px;">


## ブラウザ

### 準備
- [vue.js](https://vuejs.org/)

ライブラリ読み込み

```html:index.html
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.5.0/css/bulma.min.css">

<script src="https://unpkg.com/vue"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.24.0/babel.js"></script> 
<script src="https://cdnjs.cloudflare.com/ajax/libs/animejs/2.0.2/anime.js"></script>
<script src="https://www.gstatic.com/firebasejs/4.2.0/firebase.js"></script>
<script src="https://www.gstatic.com/firebasejs/3.8.0/firebase-app.js"></script>
<script src="https://www.gstatic.com/firebasejs/3.8.0/firebase-database.js"></script>

```

### サーバとの時間差を取ってくる

```js:script.js
// 初期化
created: function () {
    // firebaseの設定
    this.setupFirebase().then(()=>{
        // chatの設定
        this.setupChat()
        // メッセージを読み込む
        this.loadMessage()
    })
},

setupFirebase: function(){
    // 略
    return new Promise((resolve, reject)=>{
        firebase.database().ref("/.info/serverTimeOffset").on('value', (offset) => {
            var offsetVal = offset.val() || 0;
            this.timestampOffset = offsetVal
            resolve()
        });
    })
},
```

### チャット(ルーム?)のセットアップ

ハッシュからchatIdを探す、あったらそれ使ってなかったらつくる

```js:script.js
setupChat: function(){
    let ref = firebase.database().ref('chats')
    let hash = location.hash

    // chatId があったとき
    if(hash != null && hash.length > 0){
        let chatId = hash.slice( 1 ) ;
        this.chatRef = ref.child(chatId)
        console.log("read chat", chatId)
    }
    // なかったとき
    else{
        let createdAt = this.timestamp()

        // 新しいチャットを作って
        this.chatRef = ref.push()

        // 保存する
        this.chatRef.set({
            createdAt: createdAt
            , createdAtReverse: -createdAt
        })

        location.href = location.origin + location.pathname + "#" + this.chatRef.key
    }

    this.qrUrl = "http://chart.apis.google.com/chart?chs=150x150&cht=qr&chl=anywaychat://" + this.chatRef.key
},
```

### 既存メッセージの取得

```js:script.js
// メッセージを読み込む
, loadMessage:function(){
    
    // 最初に全部取ってくる、
    let messageListRef = this.chatRef.child("messageList")

    // 1. messageIdのリスト(messageList)を取得
    messageListRef.once('value').then((snapshot) => {

        // 2. message の実態を持ってくる
        var promiseList = []
        let messageRef = this.messageRef()

        snapshot.forEach(function(childSnapshot) {
            var messageKey = childSnapshot.key
            promiseList.push(messageRef.child(messageKey).once("value"))
        })
        
        return Promise.all(promiseList)
    }).then((results) => {
        // 3. 整形
        
        // 略

        // 5. 表示に反映
        this.messageList = messageList

        // 6. 下までスクロール
        this.scrollToBottom()

        // 7. 追加の監視
        this.observeMessage()
    });
},

```

### メッセージ追加の監視

```js:script.js
// メッセージの監視
observeMessage: function(){
    let messageListRef = this.chatRef.child("messageList")
    messageListRef.on("child_added", (snapshot) => {
        let newMessageKey = snapshot.key

        // すでにあるやつは弾く
        for(var i=0,max=this.messageList.length;i<max;i++) {
            let message = this.messageList[i]
            if(message.key == newMessageKey){
                return
            }
        }
        
        // 実態を取りに行く
       this.messageRef().child(newMessageKey).once("value").then((snapshot)=>{
            // 整形して表示に反映
            let data = snapshot.val()
            data.key = snapshot.key
            this.messageList.push(data)
            this.scrollToBottom()
        })
    })
},
```

## メッセージの送信

```js:script.js
// メッセージを送る
send: function(event){

    // 新しいメッセージを作って
    let message = this.messageRef().push()
    let createdAt = firebase.database.ServerValue.TIMESTAMP

    // 保存する
    let chat = this.chatRef.key
    message.set({
        chat: chat
        , message:this.message
        , userName: this.userName
        , userId: this.userId
        , createdAt: createdAt
        , platform: PlatformType.browser.val
    })

    this.chatRef.child("messageList").child(message.key).set(1)

    // 入力エリアリセット
    this.message = ""
}
```

## iOS

### 準備

#### Firebaseライブラリの登録

```:Podfile
target 'anywaychat' do
  use_frameworks!
  pod 'Firebase/Core'
  pod 'Firebase/Database'
end
```

#### URLスキーム(anywaychat://)のハンドリング

招待(QRコード)からの起動ように。

```swift:AppDelegate.swift
func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {
        
    if let host = url.host{
        Global.currentChatId = host
    }

    return true
}
```

### サーバとの時間差を取ってくる


```swift:ViewController.swift
private func setup(){
    // 略
    let offsetRef = Database.database().reference(withPath: ".info/serverTimeOffset")
    offsetRef.observe(.value, with: { snapshot in
        if let offset = snapshot.value as? TimeInterval {
            Global.timestampOffset = offset
        }
        
        self.loadMessageList()
    })
}

```

### チャット(ルーム?)のセットアップ

あったらそれ使ってなかったらつくる

```swift:ViewController.swift
private func setupRef(){
    let ref = Database.database().reference()
    if let chatId = Global.currentChatId{
        self.chatRef = ref.child("chats").child(chatId)
    }else{
        self.chatRef = ref.child("chats").childByAutoId()
        Global.currentChatId = self.chatRef.key
    }
}
```

### 既存メッセージの取得

複数の実態の取得の完了はどうやってとるのだろうか。
loaded ++ がなんともダサい.

```swift:ViewController.swift
private func loadMessageList(){
        
    // 最初の読み込み
    // messageIdList の取得
    self.chatRef.child("messageList").observeSingleEvent(of: .value, with: { (snapshot) in
        
        var results:[Message] = []
        
        var loaded:UInt = 0
        let total = snapshot.childrenCount
        
        if total == 0{
            self.onLoadMessageList()
            return
        }
        
        // id を元にそれぞれの実態を取得
        for child in snapshot.children{
            guard let childSnapshot = child as? DataSnapshot else{
                continue;
            }
            
            let messageKey = childSnapshot.key
            self.messageRef().child(messageKey).observeSingleEvent(of: .value, with: { (snapshot) in
                let message = Message(snapshot: snapshot)
                results.append(message)
                loaded = loaded + 1
                if loaded == total{
                    results.sort(by: { (m1, m2) -> Bool in
                        return m1.createdAt.compare(m2.createdAt) == .orderedAscending
                    })
                    
                    self.messageList = results
                    self.onLoadMessageList()
                }
            })
        }
        
    })
}
```

### メッセージ追加の監視

```swift:ViewController.swift
private func onLoadMessageList(){
    // 監視
    self.chatRef.child("messageList").observe(.childAdded, with: { (snapshot) in
        let newMessageKey = snapshot.key
        
        // すでにあるやつは弾く
        for m in self.messageList{
            if m.key == newMessageKey{
                return
            }
        }
        
        // 実態を取ってくる
        self.messageRef().child(newMessageKey).observeSingleEvent(of: .value, with: { (snapshot) in
            // 表示に反映
            let message = Message(snapshot: snapshot)
            self.messageList.append(message)
            let indexPath = IndexPath(row: self.messageList.count - 1, section: 0)
            self.tableView.insertRows(at: [indexPath], with: .automatic)
        })
    })
}
```

### メッセージの送信

```swift:ViewController.swift
func onCommitMessage(message: String) {
        
    let createdAt = ServerValue.timestamp()
    
    let messageDic:[String:Any] = [
        "userId": Global.userId,
        "createdAt": createdAt,
        "chat": self.chatRef.key,
        "message": message,
        "userName": userName,
        "platform": PlatformType.ios.val()
    ]
    
    let ref = Database.database().reference()
    let message = ref.child("messages").childByAutoId()
    
    message.setValue(messageDic)
    
    chatRef.child("messageList").child(message.key).setValue(1)
}
```

# まとめ

ほぼ同じノリでかける。

# 次

Android