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

# ホーム画面

ホーム画面に表示されている投稿はフォローに関係なく全件表示してしまっているので、自分がフォローしているユーザーと自分自身の投稿のみを表示するように修正します。

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

@app.route("/api/posts", methods=["GET"])
@login_required()
def list_posts():
    ids = [ObjectId(idx) for idx in session["user"]["following"]]
    ids.append(ObjectId(session["user"]["_id"]))

    results = []
    for post in Post._collection.find(
        {"posted_by": {"$in": ids}}, sort=[("created_at", pymongo.DESCENDING)]
    ):
        results.append(_populate(post))
    ...

...
```

# プロフィール画面

プロフィール画面の3つのタブは、本家の仕様にならうと以下のようになるようです。

- ツイート
→ posted_by が該当ユーザー かつ reply_to が空
- ツイートと返信
→ posted_by が該当ユーザー
- いいね
→ 該当ユーザーが いいね した投稿


APIは `GET /api/users/<username>/posts?type=XXXX` のようにして、typeで tweets, tweets_and_replies, likes の挙動を切り替える感じにしましょうか。

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

@app.route("/api/posts", methods=["GET"])
@login_required()
def list_posts():
    ids = [ObjectId(idx) for idx in session["user"]["following"]]
    ids.append(ObjectId(session["user"]["_id"]))

    results = []
    for post in Post._collection.find(
        {"posted_by": {"$in": ids}}, sort=[("created_at", pymongo.DESCENDING)]
    ):
        results.append(_populate(post))

    if results:
        _set_liking_and_retweeting(results)
    
    return jsonify(results)


@app.route("/api/users/<username>/posts", methods=["GET"])
@login_required()
def list_user_posts(username):
    user = User._collection.find_one({"username": username})
    if not user:
        return error("ユーザーが存在しません。")

    post_type = request.args.get("type", "tweets")

    results = []
    if post_type == "tweets":
        condition = {"posted_by": user["_id"], "reply_to": None}
        for post in Post._collection.find(condition, sort=[("created_at", pymongo.DESCENDING)]):
            results.append(_populate(post))
    elif post_type == "tweets_and_replies":
        condition = {"posted_by": user["_id"]}
        for post in Post._collection.find(condition, sort=[("created_at", pymongo.DESCENDING)]):
            results.append(_populate(post))
    elif post_type == "likes":
        for like in Like._collection.find({"liked_by": user["_id"]}, sort=[("liked_at", pymongo.DESCENDING)]):
            post = Post._collection.find_one({"_id": like["post"]})
            results.append(_populate(post))
    else:
        app.logger.error(f"不明な種別が指定されました: {post_type}")
        return error("Internal Server Error", 500)

    if results:
        _set_liking_and_retweeting(results)
    
    return jsonify(results)


def _set_liking_and_retweeting(results):
    """
    resultsの各要素に、ログインユーザーが いいね / リツイート をしたかどうかを
    表す liking / retweeting フィールドを設定する。
    
    results は Post._collection.find の戻り値の各要素を _populate した配列
    が渡されることを想定している。
    """
    user_id = ObjectId(session["user"]["_id"])
    term = {"$gte": results[-1]["created_at"], "$lte": results[0]["created_at"]}

    liked_posts = {
            like["post"]
            for like in Like._collection.find({"liked_by": user_id, "posted_at": term})
        }
    retweeted_posts = {
            retweet["post"]
            for retweet in Retweet._collection.find(
                {"retweeted_by": user_id, "posted_at": term}
            )
        }
    for result in results:
        result["liking"] = result["_id"] in liked_posts
        result["retweeting"] = result["_id"] in retweeted_posts

    for result in results:
        if (
            "retweeted_post" in result
                and result["retweeted_post"]
                and "_id" in result["retweeted_post"]
        ):
            retweeted_post_id = ObjectId(result["retweeted_post"]["_id"])
            result["retweeted_post"]["liking"] = (
                    Like._collection.find_one(
                        {"liked_by": user_id, "post": retweeted_post_id}
                    )
                    is not None
                )
            result["retweeted_post"]["retweeting"] = (
                    Retweet._collection.find_one(
                        {"retweeted_by": user_id, "post": retweeted_post_id}
                    )
                    is not None
                )

...
```

ログインユーザーが いいね または リツイート した投稿かどうかの判定は使い回せるので関数に切り出しました。
いいね に関してはユーザーがいいねした日時が新しい順に表示できるようにしています。

それでは画面に表示していきましょう。

まず、各タブにURLを割り当てます。

```javascript:client/src/router/index.js
...

  {
    path: "/profile/:username/tweets_and_replies",
    name: "profileTweetsAndReplies",
    meta: { loginRequired: true, tab: "tweetsAndReplies" },
    component: () => import("../views/ProfileView.vue"),
  },
  {
    path: "/profile/:username/likes",
    name: "profileLikes",
    meta: { loginRequired: true, tab: "likes" },
    component: () => import("../views/ProfileView.vue"),
  },
  {
    path: "/profile/:username?",
    name: "profile",
    meta: { loginRequired: true, tab: "tweets" },
    component: () => import("../views/ProfileView.vue"),
  },

...
```

以前実装した フォロー中 / フォロワー のタブ切り替えと同じ感じですね。

ストアは以下のようにしました。

```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: [],
      tweets: [],
      tweetsAndReplies: [],
      likes: [],
    },
    replyTo: undefined,
    post: undefined,
  },
  getters: {},
  mutations: {
...
    setProfilePosts(state, args) {
      state.profile[args.tab] = args.posts;
    },
...
  },
  actions: {
...
    async loadProfilePosts({ commit }, args) {
      let type;
      switch (args.tab) {
        case "tweets":
        case "likes":
          type = args.tab;
          break;
        default:
          type = "tweets_and_replies";
          break;
      }
      return axios
        .get(`/api/users/${args.username}/posts?type=${type}`)
        .then((resp) => {
          commit("setProfilePosts", { posts: resp.data, tab: args.tab });
        })
        .catch(defaultErrorHandler);
    },
...
  },
  modules: {},
});

```


ProfileView.vue は下記のようになりました。

```vue:client/src/views/ProfileView.vue
<template>
...
      <div class="uk-margin-top uk-margin-right">
        <ul class="uk-child-width-expand" ref="tabs" uk-tab>
          <li :class="{ 'uk-active': tab === 'tweets' }">
            <router-link :to="'/profile/' + user.username">
              ツイート
            </router-link>
          </li>
          <li :class="{ 'uk-active': tab === 'tweetsAndReplies' }">
            <router-link
              :to="'/profile/' + user.username + '/tweets_and_replies'"
            >
              ツイートと返信
            </router-link>
          </li>
          <li :class="{ 'uk-active': tab === 'likes' }">
            <router-link :to="'/profile/' + user.username + '/likes'">
              いいね
            </router-link>
          </li>
        </ul>
      </div>
      <div>
        <post-panel
          v-for="post in posts"
          :key="post._id"
          :post="post"
        ></post-panel>
      </div>
    </div>
    <div class="uk-position-absolute error-message" v-else>
      <h3>ユーザーが存在しません</h3>
    </div>
  </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 route = useRoute();
    const store = useStore();

    let tab = computed(() => route.meta.tab);
    let posts = computed(() => store.state.profile[tab.value]);
    const isMe = ref(false);
    const isFollowing = computed(
      () =>
        store.state.userLoggedIn.following &&
        store.state.userLoggedIn.following.includes(
          store.state.profile.user._id
        )
    );
    const user = computed(() => store.state.profile.user);

    watch(
      () => [route.params.username, route.meta.tab],
      () => {
        const username = route.params.username
          ? route.params.username
          : store.state.userLoggedIn.username;
        store
          .dispatch("loadProfileUser", { username })
          .then(() => {
            isMe.value = username === store.state.userLoggedIn.username;
            store.dispatch("loadProfilePosts", { username, tab: tab.value });
          })
          .catch(() => {
            // noop
          });
      },
      {
        immediate: true,
      }
    );

...

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

...
```

![スクリーンショット 2022-03-25 18.10.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2536525/ebe0e3c7-a1ca-6fff-e790-4f0f95d2c647.png)

とりあえずそれぞれのツイート一覧は表示できるようになりました。
返信等がうまく動かないので次回はそこを修正していきましょう。