Twitterクローンを作ります #32 投稿一覧(2)

前回プロフィール画面に投稿を表示するようにしましたが、返信やいいねがうまく動かないのでこれを直していきます。

# リファクタリング

現在の実装は HomeViewにreply-modalが定義してあって、PostPanelから飛んでくるclickReplyイベントをHomeViewで受け取ってモーダルを表示する、みたいな処理になっています。

投稿が表示されるのがホーム画面だけならこれでも問題無いですが、いろいろな場所で表示しようとするとそれぞれに同じようなコードを書いていかなければなくなるのでこの方式自体をまず修正してしまいましょう。

PostPanel自体がモーダルを持っておけば `<post-panel :post="post"></post-panel>` とするだけで返信モーダルも開けるので良さそうです。

しかし、返信先を表示するのにReplyModalがPostPanelに依存しているので、単純にPostPanelにReplyModalを追加してしまうと循環参照でうまくいきません。

現在の PostPanel を SimplePostPanel などに名前を変えて、ReplyModal は SimplePostPanel を利用して表示する。
PostPanelはSimplePostPanel + ReplyModal、とすればうまくいきそうです。
やってみましょう。

```vue:client/src/components/PostPanel.vue
<template>
  <div>
    <simple-post-panel
      :post="post"
      :disableLink="disableLink"
      :main="main"
      @clickReply="onReplyClick"
    ></simple-post-panel>
    <div ref="replyModal" uk-modal>
      <reply-modal @closeModal="closeModal"></reply-modal>
    </div>
  </div>
</template>

<script>
import UIkit from "uikit";
import { ref } from "vue";
import { useStore } from "vuex";
import ReplyModal from "@/components/ReplyModal.vue";
import SimplePostPanel from "@/components/SimplePostPanel.vue";

export default {
  components: {
    ReplyModal,
    SimplePostPanel,
  },
  props: {
    post: Object,
    disableLink: Boolean,
    main: {
      type: Boolean,
      default: false,
    },
  },
  setup(props) {
    const store = useStore();
    const replyModal = ref(null);
    const onReplyClick = () => {
      store.dispatch("openReplyModal", { post: props.post }).then(() => {
        UIkit.modal(replyModal.value).show();
      });
    };
    const closeModal = () => {
      UIkit.modal(replyModal.value).hide();
    };
    return {
      replyModal,
      onReplyClick,
      closeModal,
    };
  },
};
</script>

```

差分がかなり大きいですが、PostPanelを使うたびにReplyModal用のdivを用意したりイベントハンドルしなくてよくなりました。

# プロフィール画面での返信

自分のプロフィール画面で返信した場合に、"ツイートと返信"を更新してあげましょう。
ReplyModalから返信成功イベントを投げて、プロフィール画面で受け取ってtweetsAndRepliesを更新するという方針で修正しましょう。

```vue:client/src/components/ReplyModal.vue
...
    const onReplyClick = () => {
      store
        .dispatch("createPost", {
          content: content.value,
          reply_to: post.value._id,
        })
        .then(() => {
          content.value = "";
          context.emit("closeModal");
          context.emit("replySuccess");
        })
        .catch((error) => {
          UIkit.notification(error.response.data.error.message, {
            status: "danger",
          });
        });
    };
...

```

```vue:client/src/components/PostPanel.vue
<template>
  <div>
    <simple-post-panel
      :post="post"
      :disableLink="disableLink"
      :main="main"
      @clickReply="onReplyClick"
    ></simple-post-panel>
    <div ref="replyModal" uk-modal>
      <reply-modal
        @replySuccess="onReplySuccess"
        @closeModal="closeModal"
      ></reply-modal>
    </div>
  </div>
</template>

<script>
import UIkit from "uikit";
import { ref } from "vue";
import { useStore } from "vuex";

import ReplyModal from "@/components/ReplyModal.vue";
import SimplePostPanel from "@/components/SimplePostPanel.vue";

export default {
  components: {
    ReplyModal,
    SimplePostPanel,
  },
  props: {
    post: Object,
    disableLink: Boolean,
    main: {
      type: Boolean,
      default: false,
    },
  },
  setup(props, context) {
...
    const onReplySuccess = () => {
      context.emit("replySuccess");
    };
...
    return {
      replyModal,
      onReplyClick,
      onReplySuccess,
      closeModal,
    };
  },
};
</script>
```

```vue:client/src/views/ProfileView.vue
<template>
  <div class="uk-width-1-1 uk-position-relative">
...
        <post-panel
          v-for="post in posts"
          :key="post._id"
          :post="post"
          @replySuccess="onReplySuccess"
        ></post-panel>
...
  </div>
</template>

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

import AvatarPic from "@/components/AvatarPic.vue";
import PostPanel from "@/components/PostPanel.vue";

export default {
  components: {
    AvatarPic,
    PostPanel,
  },
  setup() {
...

    const onReplySuccess = () => {
      if (isMe.value && tab.value === "tweetsAndReplies") {
        store.dispatch("loadProfilePosts", {
          username: user.value.username,
          tab: "tweetsAndReplies",
        });
      }
    };

    return {
      isMe,
      isFollowing,
      user,
      tab,
      posts,
      onFollowClick,
      onReplySuccess,
    };
  },
};
</script>

...
</style>
```

孫からのイベントの受け渡しの仕方がちょっとイケてないですがとりあえずいいでしょう。

# いいねとリツイートの反映

これも返信と同様に like の api の成功イベントを発火してそれぞれで必要な処理を指定する形がきれいな気もしますが、とりあえず現状のupdatePostで対応してしまいましょう。

```javascript:client/src/store/index.js
...
    updatePost(state, args) {
      const targets = [
        state.posts,
        state.profile.tweets,
        state.profile.tweetsAndReplies,
        state.profile.likes,
      ];

      if (state.post) {
        targets.push([state.post]);
      }

      targets.forEach((target) => {
        if (!target) {
          return;
        }
        const orig = target.find((p) => p._id === args.post._id);
        if (orig) {
          Object.assign(orig, args.post);
        }
        target
          .filter(
            (p) => p.retweeted_post && p.retweeted_post._id === args.post._id
          )
          .forEach((p) => Object.assign(p.retweeted_post, args.post));
        target
          .filter((p) => p.reply_to && p.reply_to._id === args.post._id)
          .forEach((p) => Object.assign(p.reply_to, args.post));
      });

      if (state.post) {
        state.post.replies
          .filter((p) => p._id === args.post._id)
          .forEach((p) => Object.assign(p, args.post));
      }
    },
...
```