チャットの情報とメッセージをAPIで取得し、チャット画面に表示していきます。
API
まずはチャットの1件取得APIです。
server/models/chat.py
@classmethod
def get_chat(cls, chat_id):
pipelines = [
{
"$match": {
"_id": ObjectId(chat_id),
}
},
{
"$lookup": {
"from": "users",
"localField": "users",
"foreignField": "_id",
"as": "users",
}
},
]
result = list(cls._collection.aggregate(pipelines))
if len(result):
return result[0]
else:
return None
server/apis/chats.py
...
@app.route("/api/chats/<chat_id>", methods=["GET"])
@login_required()
def get_chat(chat_id):
chat = Chat.get_chat(chat_id)
if not chat:
return error("チャットが存在しないか、参加していないチャットです")
user_id = session["user"]["_id"]
if not [u for u in chat.get("users", []) if u["_id"] == ObjectId(user_id)]:
return error("チャットが存在しないか、参加していないチャットです")
return jsonify(chat)
画面
メッセージはややこしいのでコンポーネント化します。
client/src/components/ChatMessage.vue
<template>
<div class="message uk-flex" :class="{ mine: isMine, theirs: !isMine }">
<div class="image">
<img v-if="!isMine && isLast" :src="imageUrl" />
</div>
<div class="container uk-flex uk-flex-column">
<span
class="sender uk-text-muted uk-text-small"
v-if="!isMine && isFirst"
>
{{ message.sender.display_name }}
</span>
<span class="body uk-text-small">
{{ message.content }}
</span>
</div>
</div>
</template>
<script>
export default {
props: {
message: Object,
isMine: Boolean,
isFirst: Boolean,
isLast: Boolean,
},
setup(props) {
const imageUrl = ((user) => {
if (!user || !user.display_name) {
return "https://ui-avatars.com/api/?name=?";
}
if (!user.profile_pic) {
return `https://ui-avatars.com/api/?name=${user.display_name}`;
}
return user.profile_pic;
})(props.message.sender);
return {
imageUrl,
};
},
};
</script>
<style scoped>
.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;
}
</style>
コンポーネントに切り出した部分を削除しつつ、表示時に必要なデータをロードします。
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">
<chat-message
v-for="(message, idx) in messages"
:key="message._id"
:message="message"
:isMine="isMine(message)"
:isFirst="isFirst(message, idx)"
:isLast="isLast(message, idx)"
></chat-message>
</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, watch } from "vue";
import { useRoute } from "vue-router";
import { useStore } from "vuex";
import ChatMessage from "@/components/ChatMessage.vue";
export default {
components: {
ChatMessage,
},
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);
});
watch(
() => route.params,
(newParams) => {
store.dispatch("loadChat", { chat: newParams["chatId"] });
store.dispatch("loadMessages", { chat: newParams["chatId"] });
},
{
immediate: true,
}
);
const messages = computed(() => store.state.chat.messages);
const isMine = (message) => {
return message.sender._id === store.state.userLoggedIn._id;
};
const isFirst = (message, idx) => {
if (idx === 0) {
return true;
}
const prev = messages.value[idx - 1];
return prev.sender._id != message.sender._id;
};
const isLast = (message, idx) => {
if (idx === messages.value.length - 1) {
return true;
}
const next = messages.value[idx + 1];
return next.sender._id != message.sender._id;
};
return {
messages,
text,
enableSubmit,
submit,
onSendClick,
isValid,
isMine,
isFirst,
isLast,
};
},
};
</script>
<style scoped>
.content {
overflow-y: auto;
margin: 0;
padding: 5px;
}
.top-hr {
margin-bottom: 0;
}
.bottom-hr {
margin-top: 0;
}
.clickable {
cursor: pointer;
}
</style>
ストアも対応させましょう。
client/src/store/index.js
import axios from "axios";
import UIkit from "uikit";
import { createStore } from "vuex";
import router from "@/router";
function defaultErrorHandler(error) {
if (error.response.status === 401) {
router.push("/login");
} else {
UIkit.notification(error.response.data.error.message, {
status: "danger",
});
}
}
export default createStore({
state: {
userLoggedIn: undefined,
posts: [],
profile: {
user: {
display_name: "",
username: "",
},
users: [],
tweets: [],
tweetsAndReplies: [],
likes: [],
},
search: {
posts: [],
users: [],
},
chat: {
users: [],
chats: [],
messages: [],
},
notifications: [],
unreadNotificationCount: 0,
replyTo: undefined,
post: undefined,
},
getters: {},
mutations: {
...
setMessages(state, args) {
state.chat.messages = args.messages;
},
},
actions: {
...
async loadChat(_, args) {
return axios
.get(`/api/chats/${args.chat}`)
.then((resp) => {
// TODO
console.log(resp.data);
})
.catch(defaultErrorHandler);
},
async loadMessages({ commit }, args) {
return axios
.get(`/api/chats/${args.chat}/messages`)
.then((resp) => {
console.log(resp.data);
commit("setMessages", { messages: resp.data });
})
.catch(defaultErrorHandler);
},
},
modules: {},
});
投稿したタイミングでは表示は変わりませんが、その後ページをリロードすると上記のようにメッセージが表示されるようになりました。
今回はここまでにしましょう。