Twitterクローンを作ります #43 チャット機能(5)

今回はメッセージの投稿機能を作っていきます。

# API

```python:server/models/message.py
from datetime import datetime
from typing import List

from bson.objectid import ObjectId

from database import db


class Message:
    _collection = db["messages"]

    def __init__(
        self,
        content: str,
        chat: str,
        sender: str,
        created_at: datetime = None,
        updated_at: datetime = None,
    ):
        self.content = content
        self.chat = chat
        self.sender = sender
        self.read_by = []
        if not created_at:
            created_at = datetime.now()
        self.created_at = created_at
        if not updated_at:
            updated_at = datetime.now()
        self.updated_at = updated_at

    def create(self):
        data = vars(self)
        data["chat"] = ObjectId(data["chat"])
        data["sender"] = ObjectId(data["sender"])
        return self._collection.insert_one(data)

    @classmethod
    def validate(cls, body, logged_in_user_id):
        if not body.get("sender"):
            return "投稿者は必須です。"
        if body["sender"] != logged_in_user_id:
            return "この操作は許可されていません。"
        if not body.get("content", "").trim():
            return "本文が指定されていません。"
        return

```

```python:server/apis/chats.py
...


@app.route("/api/chats/<chat_id>/messages", methods=["POST"])
@login_required()
def create_message(chat_id):
    body = request.json

    user_id = session["user"]["_id"]
    error_message = Message.validate(body, user_id)
    if error_message:
        return error(error_message)
    
    chat = Chat._collection.find_one({"_id": ObjectId(chat_id)})
    if not chat or ObjectId(user_id) not in chat.get("users", []):
        return error("チャットが存在しないか、参加していないチャットです")
    
    message = Message(content=body["content"], chat=chat_id, sender=user_id)
    message.create()
    message = vars(message)
    message["sender"] = session["user"]
    return jsonify(message)

```


# 画面

画面から投稿できるようにしましょう。
チャット画面下部のテキストボックス内でエンターキーを押しても投稿できるようにしたいので、以前と同様に変換の確定では投稿しないような工夫をしておきます。

```vue:client/src/views/ChatView.vue
<template>
  <div class="uk-margin-right uk-flex uk-flex-column uk-height-1-1">
    <div>
      <h3 class="uk-margin-top">チャット</h3>
    </div>
    <div class="uk-text-bold">チャットのタイトル</div>
    <hr class="top-hr" />
    <div class="uk-flex-1 uk-flex uk-flex-column content">
      <div class="message mine uk-flex">
        <div class="container uk-flex uk-flex-column">
          <span class="body uk-text-small">
            hi hi hi hi hi hi hi hi hi hi hi hi hi hi hi hi
          </span>
        </div>
      </div>
      <div class="message theirs uk-flex">
        <div class="image"></div>
        <div class="container uk-flex uk-flex-column">
          <span class="sender uk-text-muted uk-text-small">hoge</span>
          <span class="body uk-text-small">
            hi hi hi hi hi hi hi hi hi hi hi hi hi hi hi hi
          </span>
        </div>
      </div>
      <div class="message theirs uk-flex">
        <div class="image"></div>
        <div class="container uk-flex uk-flex-column">
          <span class="body uk-text-small">
            hi hi hi hi hi hi hi hi hi hi hi hi hi hi hi hi
          </span>
        </div>
      </div>
      <div class="message theirs uk-flex">
        <div class="image">
          <img src="https://ui-avatars.com/api/?name=?" />
        </div>
        <div class="container uk-flex uk-flex-column">
          <span class="body uk-text-small">
            hi hi hi hi hi hi hi hi hi hi hi hi hi hi hi hi
          </span>
        </div>
      </div>

      <div class="message mine uk-flex">
        <div class="container uk-flex uk-flex-column">
          <span class="body uk-text-small">
            hi hi hi hi hi hi hi hi hi hi hi hi hi hi hi hi
          </span>
        </div>
      </div>
      <div class="message theirs uk-flex">
        <div class="image"></div>
        <div class="container uk-flex uk-flex-column">
          <span class="sender uk-text-muted uk-text-small">hoge</span>
          <span class="body uk-text-small">
            hi hi hi hi hi hi hi hi hi hi hi hi hi hi hi hi
          </span>
        </div>
      </div>
      <div class="message theirs uk-flex">
        <div class="image"></div>
        <div class="container uk-flex uk-flex-column">
          <span class="body uk-text-small">
            hi hi hi hi hi hi hi hi hi hi hi hi hi hi hi hi
          </span>
        </div>
      </div>
      <div class="message theirs uk-flex">
        <div class="image">
          <img src="https://ui-avatars.com/api/?name=?" />
        </div>
        <div class="container uk-flex uk-flex-column">
          <span class="body uk-text-small">
            hi hi hi hi hi hi hi hi hi hi hi hi hi hi hi hi
          </span>
        </div>
      </div>

      <div class="message mine uk-flex">
        <div class="container uk-flex uk-flex-column">
          <span class="body uk-text-small">
            hi hi hi hi hi hi hi hi hi hi hi hi hi hi hi hi
          </span>
        </div>
      </div>
      <div class="message theirs uk-flex">
        <div class="image"></div>
        <div class="container uk-flex uk-flex-column">
          <span class="sender uk-text-muted uk-text-small">hoge</span>
          <span class="body uk-text-small">
            hi hi hi hi hi hi hi hi hi hi hi hi hi hi hi hi
          </span>
        </div>
      </div>
      <div class="message theirs uk-flex">
        <div class="image"></div>
        <div class="container uk-flex uk-flex-column">
          <span class="body uk-text-small">
            hi hi hi hi hi hi hi hi hi hi hi hi hi hi hi hi
          </span>
        </div>
      </div>
      <div class="message theirs uk-flex">
        <div class="image">
          <img src="https://ui-avatars.com/api/?name=?" />
        </div>
        <div class="container uk-flex uk-flex-column">
          <span class="body uk-text-small">
            hi hi hi hi hi hi hi hi hi hi hi hi hi hi hi hi
          </span>
        </div>
      </div>

      <div class="message mine uk-flex">
        <div class="container uk-flex uk-flex-column">
          <span class="body uk-text-small">
            hi hi hi hi hi hi hi hi hi hi hi hi hi hi hi hi
          </span>
        </div>
      </div>
      <div class="message theirs uk-flex">
        <div class="image"></div>
        <div class="container uk-flex uk-flex-column">
          <span class="sender uk-text-muted uk-text-small">hoge</span>
          <span class="body uk-text-small">
            hi hi hi hi hi hi hi hi hi hi hi hi hi hi hi hi
          </span>
        </div>
      </div>
      <div class="message theirs uk-flex">
        <div class="image"></div>
        <div class="container uk-flex uk-flex-column">
          <span class="body uk-text-small">
            hi hi hi hi hi hi hi hi hi hi hi hi hi hi hi hi
          </span>
        </div>
      </div>
      <div class="message theirs uk-flex">
        <div class="image">
          <img src="https://ui-avatars.com/api/?name=?" />
        </div>
        <div class="container uk-flex uk-flex-column">
          <span class="body uk-text-small">
            hi hi hi hi hi hi hi hi hi hi hi hi hi hi hi hi
          </span>
        </div>
      </div>
      <div class="message theirs uk-flex">
        <div class="image"></div>
        <div class="container uk-flex uk-flex-column">
          <span class="sender uk-text-muted uk-text-small">hoge</span>
          <span class="body uk-text-small">
            hi hi hi hi hi hi hi hi hi hi hi hi hi hi hi hi
          </span>
        </div>
      </div>
      <div class="message theirs uk-flex">
        <div class="image"></div>
        <div class="container uk-flex uk-flex-column">
          <span class="body uk-text-small">
            hi hi hi hi hi hi hi hi hi hi hi hi hi hi hi hi
          </span>
        </div>
      </div>
      <div class="message theirs uk-flex">
        <div class="image">
          <img src="https://ui-avatars.com/api/?name=?" />
        </div>
        <div class="container uk-flex uk-flex-column">
          <span class="body uk-text-small">
            hi hi hi hi hi hi hi hi hi hi hi hi hi hi hi hi
          </span>
        </div>
      </div>
    </div>
    <hr class="bottom-hr" />
    <div class="uk-flex uk-flex-row uk-flex-middle uk-margin-bottom">
      <input
        type="text"
        class="uk-input uk-flex-1 uk-margin-right"
        v-model="text"
        @keypress.prevent.enter.exact="enableSubmit"
        @keyup.prevent.enter.exact="submit"
      />
      <span
        uk-icon="icon: comment; ratio: 1.5"
        @click="onSendClick"
        :class="{ clickable: isValid }"
        :disabled="!isValid"
      ></span>
    </div>
  </div>
</template>

<script>
import { computed, ref } from "vue";
import { useRoute } from "vue-router";
import { useStore } from "vuex";

export default {
  setup() {
    const store = useStore();
    const route = useRoute();

    const text = ref("");

    const canSubmit = ref(false);

    const enableSubmit = () => {
      canSubmit.value = true;
    };

    const _post = () => {
      store
        .dispatch("sendMessage", {
          content: text.value,
          sender: store.state.userLoggedIn._id,
          chat: route.params["chatId"],
        })
        .then(() => (text.value = ""));
    };

    const submit = () => {
      if (!canSubmit.value || !text.value.trim()) {
        return;
      }
      _post();
      canSubmit.value = false;
    };

    const onSendClick = () => {
      if (!text.value.trim()) {
        return;
      }
      _post();
      canSubmit.value = false;
    };

    const isValid = computed(() => {
      return Boolean(text.value);
    });

    return {
      text,
      enableSubmit,
      submit,
      onSendClick,
      isValid,
    };
  },
};
</script>

<style scoped>
.content {
  overflow-y: auto;
  margin: 0;
  padding: 5px;
}

.top-hr {
  margin-bottom: 0;
}

.bottom-hr {
  margin-top: 0;
}

.message {
  align-items: flex-end;
  padding-bottom: 4px;
}

.message.mine {
  flex-direction: row-reverse;
}

.container {
  max-width: 55%;
}

.body {
  background-color: #f1f0f0;
  padding: 6px 12px;
  border-radius: 18px;
}

.mine .body {
  background-color: #1fa2f1;
  color: white;
}

.image {
  height: 24px;
  width: 24px;
  margin-right: 7px;
}

.image img {
  height: 100%;
  border-radius: 50%;
  vertical-align: bottom;
}

.clickable {
  cursor: pointer;
}
</style>
```

これでメッセージを投稿できるようになりました。

次回はチャットページを開いたときにチャット情報とメッセージを読み込んで表示するようにしていきます。