Twitterクローンを作ります #30 ツイートの削除

# API

単純に消すだけだとまずいので、以下も行わなければなりません。

- Likeから該当ツイートに関するレコードを削除
- Retweetから該当ツイートに関するレコードを削除
- reply_to があった場合返信先の reply_count を更新

削除できるのは自分の投稿だけとします。

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

@app.route("/api/posts/<post_id>", methods=["DELETE"])
@login_required()
def delete_post(post_id):
    post = Post._collection.find_one({"_id": ObjectId(post_id)})
    if not post:
        return error("投稿が存在しません", 404)
    user_id = session["user"]["_id"]
    if str(post["posted_by"]) is not user_id:
        return error("投稿を削除できません")
    Like._collection.delete_many({"post": ObjectId(post_id)})
    Retweet._collection.delete_many({"post": ObjectId(post_id)})

    Post._collection.find_one_and_delete({"_id": ObjectId(post_id)})

    if "reply_to" in post and post["reply_to"]:
        reply_to = post["reply_to"]
        reply_count = Post._collection.count_documents({"reply_to": reply_to})
        Post._collection.find_one_and_update(
            {"_id": reply_to}, {"$set": {"reply_count": reply_count}}
        )
    
    return "", 204

...
```

# 画面

```javascript: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: [],
    },
    replyTo: undefined,
    post: undefined,
  },
  getters: {},
  mutations: {
...
    deletePost(state, args) {
      const index = state.posts.findIndex((p) => p._id === args.postId);
      if (index >= 0) {
        state.posts.splice(index, 1);
      }
    },
...
  },
  actions: {
...
    async deletePost({ commit }, args) {
      return axios
        .delete(`/api/posts/${args.postId}`)
        .then(() => {
          commit("deletePost", { postId: args.postId });
        })
        .catch(defaultErrorHandler);
    },
...
  },
  modules: {},
});
```

```vue:client/src/components/PostPanel.vue
<template>
...
          <!-- 削除ボタン -->
          <span
            v-if="isOwnPost && !disableLink"
            uk-icon="icon: close; ratio: 0.8"
            @click.stop="onDeleteClick"
          ></span>
...
  </div>
</template>

<script>
import UIkit from "uikit";
import { computed } from "vue";
import { useRouter } from "vue-router";
import { useStore } from "vuex";

import AvatarPic from "@/components/AvatarPic.vue";
import DisplayName from "@/components/DisplayName.vue";
import UsernameLink from "@/components/UsernameLink.vue";

export default {
  components: {
    AvatarPic,
    DisplayName,
    UsernameLink,
  },
  props: {
    post: Object,
    disableLink: Boolean,
    main: {
      type: Boolean,
      default: false,
    },
  },
  setup(props, context) {
...

    const isOwnPost = computed(() => {
      return (
        displayPost.value.posted_by &&
        displayPost.value.posted_by._id === store.state.userLoggedIn._id
      );
    });

    const onDeleteClick = () => {
      UIkit.modal.confirm("ツイートを削除します。").then(
        () => {
          store.dispatch("deletePost", { postId: displayPost.value._id });
        },
        () => {
          // noop
        }
      );
    };

    return {
      isRetweet,
      isReply,
      isOwnPost,
      displayPost,
      onLikeClick,
      onRetweetClick,
      onReplyClick,
      onBodyClick,
      onDeleteClick,
    };
  },
};
</script>

...
```

削除されたときの挙動で考慮できていない点がいくつかありそうですが、ひとまず投稿を削除することができました。