FirebaseでNEETがCHATを作った

# はじめに

Firebase の勉強のためにチャットアプリを作ってみました 🤤
Google 認証 ・ Firestore ・ Vuetify ・ vuexfire を使っています 🙄

### DEMO

[https://neetschat.firebaseapp.com/](https://neetschat.firebaseapp.com/)

![Screen Shot 2019-06-19 at 16.32.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29699/5c5647e1-7a30-97be-f10b-032e357e64d0.png)


コードは[こちら](https://github.com/youbeer/vue-firebase-chat)にあります

### 対象読者

Vue と Firebase をさらっと勉強して何か作ってみたい方

# 以下コードを抜粋したものです

### Plugins

Firebase の設定
(firebase.initializeApp はご自身のプロジェクトの値を設定してください)

```javascript:src/plugins/firebase.js
import firebase from "firebase/app";
import "firebase/firestore";
import "firebase/auth";

firebase.initializeApp({
  apiKey: "xxxxxx",
  authDomain: "xxxxxx.firebaseapp.com",
  databaseURL: "xxxxxx.firebaseio.com",
  projectId: "xxxxxx",
  storageBucket: "xxxxxx.appspot.com",
  messagingSenderId: "xxxxxx"
});

export default firebase;
```

ログインしているかの確認用

```javascript:src/plugins/auth.js
import firebase from "@/plugins/firebase";

function auth() {
  return new Promise(resolve => {
    firebase.auth().onAuthStateChanged(user => {
      resolve(user || false);
    });
  });
}
export default auth;
```

Vuetify の設定
@mdi/font パッケージを追加してアイコンフォントを mdi に変更しました
[Installing Icons — Vuetify\.js](https://vuetifyjs.com/ja/framework/icons)

```javascript:src/plugins/vuetify.js
import "@mdi/font/css/materialdesignicons.css";
import Vue from "vue";
import Vuetify from "vuetify/lib";
import "vuetify/src/stylus/app.styl";

Vue.use(Vuetify, {
  iconfont: "mdi"
});
```

### Store

vuexfire を使うと firestore と state を bind してくれて捗ります。

- [vuefire](https://github.com/vuejs/vuefire/tree/master/packages/vuexfire)
- [Introduction \| Vuefire](https://vuefire.vuejs.org/vuexfire/#why)

getters で message と user 情報を map している箇所がありますが、
firestore には参照型があるそうなので、そっちを使ったほうがいいのかも

- [Firestore の参照型によるリレーションを試す \- Crieit](https://crieit.net/posts/Firestore)

```javascript:src/store.js
import Vue from "vue";
import Vuex from "vuex";
import firebase from "@/plugins/firebase";
import { vuexfireMutations, firestoreAction } from "vuexfire";

const firestore = firebase.firestore();
const provider = new firebase.auth.GoogleAuthProvider();

Vue.use(Vuex);

export default new Vuex.Store({
  strict: true,
  state: {
    user: null,
    users: [],
    messages: [],
    isLoaded: false
  },
  mutations: {
    ...vuexfireMutations,
    setCredential(state, user) {
      state.user = user;
    },
    setIsLoaded(state, isLoaded) {
      state.isLoaded = isLoaded;
    }
  },
  getters: {
    messages: state => {
      return state.messages.map(message => {
        message.user = state.users.find(user => user.uid === message.uid);
        return message;
      });
    },
    user: state => state.user,
    users: state => state.users,
    isLoaded: state => state.isLoaded,
    isLoggedin: state => !!state.user
  },
  actions: {
    initUsers: firestoreAction(({ bindFirestoreRef }) => {
      const usersCollection = firestore.collection("users");
      bindFirestoreRef("users", usersCollection);
    }),
    initMessages: firestoreAction(({ bindFirestoreRef }) => {
      const messagesCollection = firestore
        .collection("messages")
        .orderBy("createdAt", "desc");
      bindFirestoreRef("messages", messagesCollection);
    }),
    signIn() {
      firebase.auth().signInWithRedirect(provider);
    },
    async signOut({ commit }) {
      await firebase.auth().signOut();
      commit("setCredential", null);
    },
    async setCredential({ commit }, user) {
      if (!user) return;
      const usersCollection = firestore.collection("users");
      await usersCollection.doc(user.email).set({
        name: user.displayName,
        uid: user.uid,
        email: user.email,
        icon: user.photoURL
      });

      commit("setCredential", {
        name: user.displayName,
        uid: user.uid,
        icon: user.photoURL
      });
    },
    addMessage(context, { content, uid }) {
      const serverTimestamp = firebase.firestore.FieldValue.serverTimestamp();
      firestore
        .collection("messages")
        .add({
          content,
          uid,
          createdAt: serverTimestamp,
          updatedAt: serverTimestamp
        })
        .then(docRef => {
          console.log(`${docRef.id} successfully added!`);
        })
        .catch(error => {
          console.log(error);
        });
    },
    removeMessage(context, id) {
      firestore
        .collection("messages")
        .doc(id)
        .delete()
        .then(function() {
          console.log("message successfully deleted!");
        })
        .catch(error => {
          console.log(error);
        });
    },
    loadComplete({ commit }) {
      commit("setIsLoaded", true);
    }
  }
});
```

### App.vue

認証チェック ・ メッセージ と ユーザー情報の初期化 をしています。

```vue:src/App.vue
<template>
  <v-app>
    <v-navigation-drawer v-model="drawer" fixed app>
      <v-list dense>
        <v-list-tile to="/">
          <v-list-tile-action>
            <v-icon>home</v-icon>
          </v-list-tile-action>
          <v-list-tile-content>
            <v-list-tile-title>Home</v-list-tile-title>
          </v-list-tile-content>
        </v-list-tile>
        <v-list-tile to="/code">
          <v-list-tile-action>
            <v-icon>code</v-icon>
          </v-list-tile-action>
          <v-list-tile-content>
            <v-list-tile-title>Code</v-list-tile-title>
          </v-list-tile-content>
        </v-list-tile>
      </v-list>
    </v-navigation-drawer>
    <v-toolbar color="indigo" dark fixed app prominent>
      <v-toolbar-side-icon @click.stop="drawer = !drawer"></v-toolbar-side-icon>
      <v-toolbar-title>
        <span>NEET</span>
        <span class="font-weight-light">Chat</span>
      </v-toolbar-title>
      <v-spacer></v-spacer>
      <v-toolbar-items v-if="isLoaded">
        <div v-if="isLoggedin" class="text-xs-center">
          <v-menu v-model="menu" :close-on-content-click="false" offset-y>
            <template v-slot:activator="{ on }">
              <v-btn flat fab v-on="on">
                <v-avatar>
                  <img :src="user.icon" />
                </v-avatar>
              </v-btn>
            </template>

            <v-card>
              <v-list>
                <v-list-tile avatar>
                  <v-list-tile-avatar>
                    <img :src="user.icon" :alt="user.name" />
                  </v-list-tile-avatar>

                  <v-list-tile-content>
                    <v-list-tile-title>{{ user.name }}</v-list-tile-title>
                    <v-list-tile-sub-title>
                      {{ user.email }}
                    </v-list-tile-sub-title>
                  </v-list-tile-content>
                </v-list-tile>
              </v-list>
              <v-divider></v-divider>
              <v-list>
                <v-list-tile @click="signOut">
                  <v-list-tile-title>Signout</v-list-tile-title>
                </v-list-tile>
              </v-list>
            </v-card>
          </v-menu>
        </div>
        <v-btn v-if="!isLoggedin" flat @click="signIn">Signin Google</v-btn>
      </v-toolbar-items>
    </v-toolbar>
    <v-content>
      <Loading v-if="!isLoaded" />
      <router-view v-else />
    </v-content>
    <v-footer color="indigo" app>
      <span class="white--text">&copy; 2019</span>
    </v-footer>
  </v-app>
</template>

<script>
import { mapActions, mapGetters } from "vuex";
import auth from "@/plugins/auth";
import Loading from "@/components/Loading";

export default {
  name: "App",
  components: {
    Loading
  },
  data: () => ({
    drawer: null,
    menu: false
  }),
  computed: {
    ...mapGetters(["user", "users", "messages", "isLoaded", "isLoggedin"])
  },
  async mounted() {
    let user;
    if (!this.user) user = await auth();
    await Promise.all([
      this.user
        ? Promise.resolve()
        : this.$store.dispatch("setCredential", user || null),
      this.users.length ? Promise.resolve() : this.$store.dispatch("initUsers"),
      this.messages.length
        ? Promise.resolve()
        : this.$store.dispatch("initMessages")
    ]);
    this.$store.dispatch("loadComplete");
  },
  methods: {
    ...mapActions(["signOut", "signIn"])
  }
};
</script>
```

### Component

#### メッセージ入力

ログインユーザのみメッセージ入力できるように `v-if` で表示切替しています。
絵文字入力には、シンプルで使いやすそうだった v-emoji-picker を使っています。
[joaoeudes7/V\-Emoji\-Picker](https://github.com/joaoeudes7/V-Emoji-Picker)

関係ないですが絵文字って海外でも emoji で通じるんですね！

```vue:src/components/MessageInput.vue
<template>
  <v-flex v-if="isLoggedin" class="my-3" xs12 sm10>
    <v-form ref="messageForm" v-model="valid" @submit.prevent="addMessage">
      <v-text-field
        ref="messageText"
        v-model="newMessage"
        :rules="required"
        label="message"
        prepend-inner-icon="mdi-emoticon"
        append-outer-icon="mdi-send"
        required
        clear-icon="mdi-close-circle"
        clearable
        @click:clear="clearMessage"
        @click:prepend-inner="toggleEmojiPicker"
        @click:append-outer="addMessage"
        @blur="$refs.messageForm.resetValidation()"
      ></v-text-field>
      <v-dialog v-model="dialog" :hide-overlay="true" width="325px">
        <VEmojiPicker
          :pack="pack"
          :show-search="false"
          :emojis-by-row="8"
          label-search="search"
          @select="selectEmoji"
        />
      </v-dialog>
    </v-form>
  </v-flex>
  <v-flex v-else class="my-3" xs12 sm10>
    <p class="font-weight-medium headline red--text">
      Sign in. with your Google Account.
    </p>
  </v-flex>
</template>

<script>
import { mapGetters } from "vuex";
import VEmojiPicker from "v-emoji-picker";
import packData from "v-emoji-picker/data/emojis.json";

export default {
  name: "MessageInput",

  components: {
    VEmojiPicker
  },
  data: () => ({
    newMessage: "",
    valid: true,
    pack: packData,
    required: [v => !!(v + "").trim() || "message is required"],
    dialog: false
  }),
  computed: {
    ...mapGetters(["isLoggedin", "user"])
  },
  watch: {
    dialog() {
      if (!this.dialog) {
        this.inputFocus();
      }
    }
  },
  mounted() {
    this.inputFocus();
  },
  methods: {
    async addMessage() {
      if (!this.$refs.messageForm.validate()) {
        return;
      }

      await this.$store.dispatch("addMessage", {
        content: this.newMessage,
        uid: this.user.uid
      });
      this.clearMessage();
    },
    selectEmoji(dataEmoji) {
      if (!this.dialog) {
        return;
      }

      if (!this.newMessage) {
        this.newMessage = "";
      }

      this.newMessage += dataEmoji.emoji;
      this.dialog = false;
      this.inputFocus();
    },
    toggleEmojiPicker() {
      this.dialog = !this.dialog;
    },
    inputFocus() {
      this.$refs.messageText.focus();
    },
    clearMessage() {
      this.$refs.messageForm.resetValidation();
      this.newMessage = "";
    }
  }
};
</script>
```

#### メッセージリストの表示

自分が投稿したメッセージのみ削除できるようにしています。

```vue:src/components/MessageList.vue
<template>
  <v-flex class="my-3" xs12 sm10>
    <v-list three-line>
      <transition-group name="slide-fade">
        <template v-for="(message, index) in messages">
          <v-divider v-if="index != 0" :key="index"></v-divider>
          <v-list-tile :key="message.id" avatar>
            <v-list-tile-avatar>
              <img :src="message.user.icon" />
            </v-list-tile-avatar>
            <v-list-tile-content>
              <v-list-tile-sub-title class="text--primary subheading">{{
                message.content
              }}</v-list-tile-sub-title>
              <v-list-tile-sub-title>{{
                fromNow(message.createdAt)
              }}</v-list-tile-sub-title>
            </v-list-tile-content>
            <v-list-tile-action v-if="user && user.uid == message.uid">
              <v-btn icon ripple @click="removeMessage(message.id)">
                <v-icon color="amber lighten-1">delete</v-icon>
              </v-btn>
            </v-list-tile-action>
          </v-list-tile>
        </template>
      </transition-group>
    </v-list>
  </v-flex>
</template>

<script>
import { mapGetters } from "vuex";
import moment from "moment-timezone";

export default {
  name: "MessageList",
  computed: {
    ...mapGetters(["messages", "user"])
  },
  methods: {
    removeMessage(id) {
      this.$store.dispatch("removeMessage", id);
    },
    fromNow(timestamp) {
      if (!timestamp) {
        return;
      }

      return moment(timestamp.toDate().toISOString()).fromNow();
    }
  }
};
</script>

<style scoped>
.slide-fade-enter-active,
.slide-fade-leave-active {
  transition: all 0.5s ease;
}
.slide-fade-enter,
.slide-fade-leave-to {
  transform: translateX(10px);
  opacity: 0;
}
</style>
```

# さいごに

最後まで読んでいただきありがとうございました 😆

Firebase を触る前は「サーバレス何それって」感じだったんですが Firebase かなりイイ感じですね
なんと言っても無料でスタートできるって、スゴイですよね Google さんまじ太っ腹 😁