使用 Phoenix LiveView 构建 Instagram (6)

发布时间 2023-09-01 10:06:18作者: MarkHoo

使用PETAL(Phoenix、Elixir、TailwindCSS、AlpineJS、LiveView)技术栈构建一个简化版的Instagram Web应用程序

在第 5 部分中,我们添加了 show-post 页面,在这部分中,我们将在主页上进行工作。您可以赶上Instagram 克隆 GitHub Repo。

首先,我们在 posts 上下文中添加一个函数来获取 feed,然后添加另一个函数来获取 feed 的总数 open lib/instagram_clone/posts.ex

  @doc """
  Returns the list of paginated posts of a given user id
  And posts of following list of given user id
  With user and likes preloaded
  With 2 most recent comments preloaded with user and likes
  User, page, and per_page are given with the socket assigns
  ## Examples

      iex> get_accounts_feed(following_list, assigns)
      [%{photo_url: "", url_id: ""}, ...]

  """
  def get_accounts_feed(following_list, assigns) do
    user = assigns.current_user
    page = assigns.page
    per_page = assigns.per_page
    query =
      from c in Comment,
      select: %{id: c.id, row_number: over(row_number(), :posts_partition)},
      windows: [posts_partition: [partition_by: :post_id, order_by: [desc: :id]]]
    comments_query =
      from c in Comment,
      join: r in subquery(query),
      on: c.id == r.id and r.row_number <= 2

    Post
    |> where([p], p.user_id in ^following_list)
    |> or_where([p], p.user_id == ^user.id)
    |> limit(^per_page)
    |> offset(^((page - 1) * per_page))
    |> order_by(desc: :id)
    |> preload([:user, :likes, comments: ^{comments_query, [:user, :likes]}])
    |> Repo.all()
  end

  def get_accounts_feed_total(following_list, assigns) do
    user = assigns.current_user

    Post
    |> where([p], p.user_id in ^following_list)
    |> or_where([p], p.user_id == ^user.id)
    |> select([p], count(p.id))
    |> Repo.one()
  end

我们需要以下列表,在里面lib/instagram_clone/accounts.ex添加以下函数:

  @doc """
  Returns the list of following user ids

  ## Examples

      iex> get_following_list(user)
      [3, 2, 1]
  """
  def get_following_list(user) do
    Follows
    |> select([f], f.followed_id)
    |> where(follower_id: ^user.id)
    |> Repo.all()
  end

在里面lib/instagram_clone_web/live/page_live.ex让我们分配提要:


  alias InstagramClone.Uploaders.Avatar
  alias InstagramClone.Accounts
  alias InstagramCloneWeb.UserLive.FollowComponent
  alias InstagramClone.Posts
  alias InstagramCloneWeb.Live.LikeComponent

  @impl true
  def mount(_params, session, socket) do
    socket = assign_defaults(session, socket)

    {:ok,
      socket
      |> assign(page: 1, per_page: 15),
      temporary_assigns: [user_feed: []]}
  end

  @impl true
  def handle_params(_params, _uri, socket) do
    {:noreply,
      socket
      |> assign(live_action: apply_action(socket.assigns.current_user))
      |> assign_posts()}
  end

  defp apply_action(current_user) do
    if !current_user, do: :root_path
  end

  defp assign_posts(socket) do
    if socket.assigns.current_user do
      current_user = socket.assigns.current_user
      following_list = Accounts.get_following_list(current_user)
      accounts_feed_total = Posts.get_accounts_feed_total(following_list, socket.assigns)
  
      socket
      |> assign(following_list: following_list)
      |> assign(accounts_feed_total: accounts_feed_total)
      |> assign_user_feed()
    else
      socket
    end
  end

  defp assign_user_feed(socket, following_list) do
    user_feed = Posts.get_accounts_feed(socket.assigns.following_list, socket.assigns)
    socket |> assign(user_feed: user_feed)
  end

页和每页被分配给我们的挂载函数中的套接字。我们正在检查用户是否登录以获取以下列表并将其传递给分配提要函数以返回分配了提要的套接字,我们在句柄参数函数中执行此操作。

现在让我们为帖子提要创建一个组件,在我们的 live 文件夹中添加以下文件:

lib/instagram_clone_web/live/page_post_feed_component.ex lib/instagram_clone_web/live/page_post_feed_component.html.leex

里面lib/instagram_clone_web/live/page_live.html.leex

<%= if @current_user do %>
  <section class="flex">
    <div id="user-feed" class="w-3/5" phx-update="append">
      <%= for post <- @user_feed do %>
        <%= live_component @socket,
          InstagramCloneWeb.Live.PagePostFeedComponent,
          post: post,
          id: post.id,
          current_user: @current_user %>
      <% end %>
    </div>

  </section>
  
  <div
    id="profile-posts-footer"
    class="flex justify-center"
    phx-hook="ProfilePostsScroll">
  </div>
<% else %>
  <%= live_component @socket,
    InstagramCloneWeb.PageLiveComponent,
    id: 1 %>
<% end %>

里面lib/instagram_clone_web/live/page_post_feed_component.ex

defmodule InstagramCloneWeb.Live.PagePostFeedComponent do
  use InstagramCloneWeb, :live_component

  alias InstagramClone.Uploaders.Avatar
  alias InstagramClone.Comments
  alias InstagramClone.Comments.Comment

  @impl true
  def mount(socket) do
    {:ok,
      socket
      |> assign(changeset: Comments.change_comment(%Comment{})),
      temporary_assigns: [comments: []]}
  end

  @impl true
  def handle_event("save", %{"comment" => comment_param}, socket) do
    %{"body" => body} = comment_param
    current_user = socket.assigns.current_user
    post = socket.assigns.post

    if body == "" do
      {:noreply, socket}
    else
      comment = Comments.create_comment(current_user, post, comment_param)
      {:noreply,
        socket
        |> update(:comments, fn comments -> [comment | comments] end)
        |> assign(changeset: Comments.change_comment(%Comment{}))}
    end
  end
end

我们正在设置表单变更集和临时注释,我们将使用它们来附加新注释。保存句柄功能与我们在展示页面上使用的功能相同。

里面lib/instagram_clone_web/live/page_post_feed_component.html.leex

<div class="mb-16 shadow" id="post-<%= @post.id %>">
  <div class="flex p-4 items-center">
    <!-- Post header section -->
    <%= live_redirect to: Routes.user_profile_path(@socket, :index, @post.user.username) do %>
      <%= img_tag Avatar.get_thumb(@post.user.avatar_url), class: "w-8 h-8 rounded-full object-cover object-center" %>
    <% end %>
    <div class="ml-3">
      <%= live_redirect @post.user.username,
        to: Routes.user_profile_path(@socket, :index, @post.user.username),
        class: "truncate font-bold text-sm text-gray-500 hover:underline" %>
    </div>
    <!-- End post header section -->
  </div>
  <!-- Post Image section -->
  <%= img_tag @post.photo_url,
          class: "w-full object-contain h-full shadow-sm" %>
  <!-- End Post Image section -->

  <div class="w-full">
    <!-- Action icons section -->
    <div class="flex pl-4 pr-2 pt-2">
      <%= live_component @socket,
          InstagramCloneWeb.Live.LikeComponent,
          id: @post.id,
          liked: @post,
          w_h: "w-8 h-8",
          current_user: @current_user %>

      <%= live_redirect to: Routes.live_path(@socket, InstagramCloneWeb.PostLive.Show, @post.url_id) do %>
        <div class="ml-4 w-8 h-8">
          <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
          </svg>
        </div>
      <% end %>
      <div class="ml-4 w-8 h-8 cursor-pointer">
        <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
          <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1" d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z" />
        </svg>
      </div>
      <div class="w-8 h-8 ml-auto cursor-pointer">
        <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
          <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1" d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z" />
        </svg>
      </div>
    </div>
    <!-- End Action icons section -->

    <!-- Description section -->
    <button class="px-5 text-xs text-gray-500 font-bold focus:outline-none"><%= @post.total_likes %> likes</button>
    <!-- End Description Section -->
  </div>

  <%= if @post.description do %>
    <!-- Description section -->
    <div class="flex mt-2">
      <div class="px-4 w-11/12">
        <%= live_redirect @post.user.username,
        to: Routes.user_profile_path(@socket, :index, @post.user.username),
        class: "font-bold text-sm text-gray-500 hover:underline" %>
        <span class="text-sm text-gray-700">
          <p class="inline"><%= @post.description %></p></span>
        </span>
      </div>

    </div>
  <!-- End Description Section -->
  <% end %>

  <%= if @post.total_comments > 2 do %>
    <%= live_redirect to: Routes.live_path(@socket, InstagramCloneWeb.PostLive.Show, @post.url_id) do %>
      <h6 class="px-5 text-sm text-gray-400">
        View all <%= @post.total_comments %> comments
      </h6>
    <% end %>
  <% end %>


  <section id="comments" phx-update="append">
    <%= for comment <- @post.comments do %>
      <div class="flex" id="comment-<%= comment.id %>">
        <div class="px-4 w-11/12">
          <%= live_redirect comment.user.username,
                to: Routes.user_profile_path(@socket, :index, comment.user.username),
                class: "truncate font-bold text-sm text-gray-500 hover:underline" %>
          <span class="text-sm text-gray-700">
            <p class="inline"><%= comment.body %></p>
          </span>
        </div>

        <%= live_component @socket,
            InstagramCloneWeb.Live.LikeComponent,
            id: comment.id,
            liked: comment,
            w_h: "w-5 h-5",
            current_user: @current_user %>
      </div>
    <% end %>
    <%= for comment <- @comments do %>
      <div class="flex" id="comment-<%= comment.id %>">
        <div class="px-4 w-11/12">
          <%= live_redirect comment.user.username,
                to: Routes.user_profile_path(@socket, :index, comment.user.username),
                class: "truncate font-bold text-sm text-gray-500 hover:underline" %>
          <span class="text-sm text-gray-700">
            <p class="inline"><%= comment.body %></p>
          </span>
        </div>

        <%= live_component @socket,
            InstagramCloneWeb.Live.LikeComponent,
            id: comment.id,
            liked: comment,
            w_h: "w-5 h-5",
            current_user: @current_user %>
      </div>
    <% end %>
  </section>


  <h6 class="px-5 py-2 text-xs text-gray-400"><%= Timex.from_now(@post.inserted_at) %></h6>

  <!-- Comment input section -->
  <%= f = form_for @changeset, "#",
	id: @id,
    phx_submit: "save",
    phx_target: @myself,
    class: "p-2 flex items-center mt-3 border-t-2 border-gray-100",
    x_data: "{
      disableSubmit: true,
      inputText: null,
      displayCommentBtn: (refs) => {
        refs.cbtn.classList.remove('opacity-30')
        refs.cbtn.classList.remove('cursor-not-allowed')
      },
      disableCommentBtn: (refs) => {
        refs.cbtn.classList.add('opacity-30')
        refs.cbtn.classList.add('cursor-not-allowed')
      }
    }" %>
    <div class="w-full">
      <%= textarea f, :body,
        class: "w-full border-0 focus:ring-transparent resize-none",
        rows: 1,
        placeholder: "Add a comment...",
        aria_label: "Add a comment...",
        autocorrect: "off",
        autocomplete: "off",
        x_model: "inputText",
        "@input": "[
          (inputText.length != 0) ? [disableSubmit = false, displayCommentBtn($refs)] : [disableSubmit = true, disableCommentBtn($refs)]
        ]" %>
    </div>
    <div>
      <%= submit "Post",
        phx_disable_with: "Posting...",
        class: "text-light-blue-500 opacity-30 cursor-not-allowed font-bold pb-2 text-sm focus:outline-none",
        x_ref: "cbtn",
        "@click": "inputText = null",
        "x_bind:disabled": "disableSubmit" %>
    </div>
  </form>
</div>

我们使用与显示页面上使用的相同的表单来添加新评论,并且循环遍历帖子评论和临时评论,以便在添加新评论时能够更新评论。

当我们喜欢帖子或评论时,我们需要处理从 Like 组件发送的消息,我们还必须处理用钩子触发的事件以加载更多帖子,更新lib/instagram_clone_web/live/page_live.ex为以下内容:

defmodule InstagramCloneWeb.PageLive do
  use InstagramCloneWeb, :live_view

  alias InstagramClone.Uploaders.Avatar
  alias InstagramClone.Accounts
  alias InstagramCloneWeb.UserLive.FollowComponent
  alias InstagramClone.Posts
  alias InstagramCloneWeb.Live.LikeComponent


  @impl true
  def mount(_params, session, socket) do
    socket = assign_defaults(session, socket)

    {:ok,
      socket
      |> assign(page: 1, per_page: 15),
      temporary_assigns: [user_feed: []]}
  end

  @impl true
  def handle_params(_params, _uri, socket) do
    {:noreply,
      socket
      |> assign(live_action: apply_action(socket.assigns.current_user))
      |> assign_posts()}
  end

  @impl true
  def handle_event("load-more-profile-posts", _, socket) do
    {:noreply, socket |> load_posts}
  end

  defp load_posts(socket) do
    total_posts = socket.assigns.accounts_feed_total
    page = socket.assigns.page
    per_page = socket.assigns.per_page
    total_pages = ceil(total_posts / per_page)

    if page == total_pages do
      socket
    else
      socket
      |> update(:page, &(&1 + 1))
      |> assign_user_feed()
    end
  end

  @impl true
  def handle_info({LikeComponent, :update_comment_likes, _}, socket) do
    {:noreply, socket}
  end

  @impl true
  def handle_info({LikeComponent, :update_post_likes, post}, socket) do
    post_feed = Posts.get_post_feed!(post.id)
    {:noreply,
      socket
      |> update(:user_feed, fn user_feed -> [post_feed | user_feed] end)}
  end

  defp apply_action(current_user) do
    if !current_user, do: :root_path
  end

  defp assign_posts(socket) do
    if socket.assigns.current_user do
      current_user = socket.assigns.current_user
      following_list = Accounts.get_following_list(current_user)
      accounts_feed_total = Posts.get_accounts_feed_total(following_list, socket.assigns)

      socket
      |> assign(following_list: following_list)
      |> assign(accounts_feed_total: accounts_feed_total)
      |> assign_user_feed()
    else
      socket
    end
  end

  defp assign_user_feed(socket) do
    user_feed = Posts.get_accounts_feed(socket.assigns.following_list, socket.assigns)
    socket |> assign(user_feed: user_feed)
  end
end

让我们对 Like 组件进行一些更改,因为我们在帖子和评论之间共享它,将文件移动到 post_live 文件夹外部的 live 文件夹并将模块重命名为以下内​​容:

lib/instagram_clone_web/live/like_component.ex

defmodule  InstagramCloneWeb.Live.LikeComponent  do

lib/instagram_clone_web/live/post_live/show.html.leex在第 70 行内部重命名该组件:


...
        InstagramCloneWeb.Live.LikeComponent,

...

第 24 行内部lib/instagram_clone_web/live/post_live/comment_component.html.leex也重命名了该组件:


...

        InstagramCloneWeb.PostLive.LikeComponent,
        
...

在内部lib/instagram_clone_web/live/like_component.ex,让我们更新send_msg()以将 like 作为变量发送,而不仅仅是 id:


...

  defp send_msg(liked) do
    msg = get_struct_msg_atom(liked)
    send(self(), {__MODULE__, msg, liked})
  end

...

同样在 内部lib/instagram_clone_web/live/like_component.ex,让我们删除该liked?()函数,然后检查用户 ID 是否在第 61 行的用户 ID 列表中:


...

    if assigns.current_user.id in assigns.liked.likes do # LINE 61



  # DELETE THIS FUNCTION WE WON"T NEED ANYMORE
  # Enum.any?(likes, fn l ->
  #   l.user_id == user_id
  # end)
...

在第 30 行,我们更新以检查数据库:


...

    if Likes.liked?(current_user.id, liked.id) do
...

我们新更新的文件应如下所示:

defmodule InstagramCloneWeb.Live.LikeComponent do
  use InstagramCloneWeb, :live_component

  alias InstagramClone.Likes

  @impl true
  def update(assigns, socket) do
    get_btn_status(socket, assigns)
  end

  @impl true
  def render(assigns) do
    ~L"""
    <button
      phx-target="<%= @myself %>"
      phx-click="toggle-status"
      class="<%= @w_h %> focus:outline-none">

      <%= @icon %>

    </button>
    """
  end

  @impl true
  def handle_event("toggle-status", _params, socket) do
    current_user = socket.assigns.current_user
    liked = socket.assigns.liked

    if Likes.liked?(current_user.id, liked.id) do
      unlike(socket, current_user.id, liked)
    else
      like(socket, current_user, liked)
    end
  end

  defp like(socket, current_user, liked) do
    Likes.create_like(current_user, liked)
    send_msg(liked)

    {:noreply,
      socket
      |> assign(icon: unlike_icon(socket.assigns))}
  end

  defp unlike(socket, current_user_id, liked) do
    Likes.unlike(current_user_id, liked)
    send_msg(liked)

    {:noreply,
      socket
      |> assign(icon: like_icon(socket.assigns))}
  end

  defp send_msg(liked) do
    msg = get_struct_msg_atom(liked)
    send(self(), {__MODULE__, msg, liked})
  end

  defp get_btn_status(socket, assigns) do
    if assigns.current_user.id in assigns.liked.likes do
      get_socket_assigns(socket, assigns, unlike_icon(assigns))
    else
      get_socket_assigns(socket, assigns, like_icon(assigns))
    end
  end

  defp get_socket_assigns(socket, assigns, icon) do
    {:ok,
      socket
      |> assign(assigns)
      |> assign(icon: icon)}
  end

  defp get_struct_name(struct) do
    struct.__struct__
    |> Module.split()
    |> List.last()
    |> String.downcase()
  end

  defp get_struct_msg_atom(struct) do
    name = get_struct_name(struct)
    update_struct_likes = "update_#{name}_likes"
    String.to_atom(update_struct_likes)
  end

  defp like_icon(assigns) do
    ~L"""
    <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
      <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
    </svg>
    """
  end

  defp unlike_icon(assigns) do
    ~L"""
    <svg class="text-red-600" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
      <path fill-rule="evenodd" d="M3.172 5.172a4 4 0 015.656 0L10 6.343l1.172-1.171a4 4 0 115.656 5.656L10 17.657l-6.828-6.829a4 4 0 010-5.656z" clip-rule="evenodd" />
    </svg>
    """
  end

end

现在,当我们预加载点赞时,我们只需发送 id 列表、打开,lib/instagram_clone/posts.ex并且在我们收到帖子的每个功能上,我们必须更新预加载点赞的方式:

defmodule InstagramClone.Posts do
  @moduledoc """
  The Posts context.
  """

  import Ecto.Query, warn: false
  alias InstagramClone.Repo

  alias InstagramClone.Posts.Post
  alias InstagramClone.Accounts.User
  alias InstagramClone.Comments.Comment
  alias InstagramClone.Likes.Like

  @doc """
  Returns the list of posts.

  ## Examples

      iex> list_posts()
      [%Post{}, ...]

  """
  def list_posts do
    Repo.all(Post)
  end

  @doc """
  Returns the list of paginated posts of a given user id.

  ## Examples

      iex> list_user_posts(page: 1, per_page: 10, user_id: 1)
      [%{photo_url: "", url_id: ""}, ...]

  """
  def list_profile_posts(page: page, per_page: per_page, user_id: user_id) do
    Post
    |> select([p], map(p, [:url_id, :photo_url]))
    |> where(user_id: ^user_id)
    |> limit(^per_page)
    |> offset(^((page - 1) * per_page))
    |> order_by(desc: :id)
    |> Repo.all
  end

  @doc """
  Returns the list of paginated posts of a given user id
  And posts of following list of given user id
  With user and likes preloaded
  With 2 most recent comments preloaded with user and likes
  User, page, and per_page are given with the socket assigns

  ## Examples

      iex> get_accounts_feed(following_list, assigns)
      [%{photo_url: "", url_id: ""}, ...]

  """
  def get_accounts_feed(following_list, assigns) do
    user = assigns.current_user
    page = assigns.page
    per_page = assigns.per_page
    query =
      from c in Comment,
      select: %{id: c.id, row_number: over(row_number(), :posts_partition)},
      windows: [posts_partition: [partition_by: :post_id, order_by: [desc: :id]]]
    comments_query =
      from c in Comment,
      join: r in subquery(query),
      on: c.id == r.id and r.row_number <= 2
    likes_query = Like |> select([l], l.user_id)

    Post
    |> where([p], p.user_id in ^following_list)
    |> or_where([p], p.user_id == ^user.id)
    |> limit(^per_page)
    |> offset(^((page - 1) * per_page))
    |> order_by(desc: :id)
    |> preload([:user, likes: ^likes_query, comments: ^{comments_query, [:user, likes: likes_query]}])
    |> Repo.all()
  end

  @doc """
  Gets a single post.

  Raises `Ecto.NoResultsError` if the Post does not exist.

  ## Examples

      iex> get_post!(123)
      %Post{}

      iex> get_post!(456)
      ** (Ecto.NoResultsError)

  """
  def get_post!(id) do
    likes_query = Like |> select([l], l.user_id)

    Repo.get!(Post, id)
    |> Repo.preload([:user, likes: likes_query])
  end

  def get_post_feed!(id) do
    query =
      from c in Comment,
      select: %{id: c.id, row_number: over(row_number(), :posts_partition)},
      windows: [posts_partition: [partition_by: :post_id, order_by: [desc: :id]]]
    comments_query =
      from c in Comment,
      join: r in subquery(query),
      on: c.id == r.id and r.row_number <= 2
    likes_query = Like |> select([l], l.user_id)

    Post
    |> preload([:user, likes: ^likes_query, comments: ^{comments_query, [:user, likes: likes_query]}])
    |> Repo.get!(id)
  end

  def get_post_by_url!(id) do
    likes_query = Like |> select([l], l.user_id)

    Repo.get_by!(Post, url_id: id)
    |> Repo.preload([:user, likes: likes_query])
  end

  @doc """
  Creates a post.

  ## Examples

      iex> create_post(%{field: value})
      {:ok, %Post{}}

      iex> create_post(%{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  def create_post(%Post{} = post, attrs \\ %{}, user) do
    post = Ecto.build_assoc(user, :posts, put_url_id(post))
    changeset = Post.changeset(post, attrs)
    update_posts_count = from(u in User, where: u.id == ^user.id)

    Ecto.Multi.new()
    |> Ecto.Multi.update_all(:update_posts_count, update_posts_count, inc: [posts_count: 1])
    |> Ecto.Multi.insert(:post, changeset)
    |> Repo.transaction()
  end

  # Generates a base64-encoding 8 bytes
  defp put_url_id(post) do
    url_id = Base.encode64(:crypto.strong_rand_bytes(8), padding: false)

    %Post{post | url_id: url_id}
  end

  @doc """
  Updates a post.

  ## Examples

      iex> update_post(post, %{field: new_value})
      {:ok, %Post{}}

      iex> update_post(post, %{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  def update_post(%Post{} = post, attrs) do
    post
    |> Post.changeset(attrs)
    |> Repo.update()
  end

  @doc """
  Deletes a post.

  ## Examples

      iex> delete_post(post)
      {:ok, %Post{}}

      iex> delete_post(post)
      {:error, %Ecto.Changeset{}}

  """
  def delete_post(%Post{} = post) do
    Repo.delete(post)
  end

  @doc """
  Returns an `%Ecto.Changeset{}` for tracking post changes.

  ## Examples

      iex> change_post(post)
      %Ecto.Changeset{data: %Post{}}

  """
  def change_post(%Post{} = post, attrs \\ %{}) do
    Post.changeset(post, attrs)
  end
end

我们还必须对注释执行相同的操作,打开lib/instagram_clone/comments.ex文件并将其更新为以下内容:

defmodule InstagramClone.Comments do
  @moduledoc """
  The Comments context.
  """

  import Ecto.Query, warn: false
  alias InstagramClone.Repo
  alias InstagramClone.Likes.Like
  alias InstagramClone.Comments.Comment

  @doc """
  Returns the list of comments.

  ## Examples

      iex> list_comments()
      [%Comment{}, ...]

  """
  def list_comments do
    Repo.all(Comment)
  end

  def list_post_comments(assigns, public: public) do
    user = assigns.current_user
    post_id = assigns.post.id
    per_page = assigns.per_page
    page = assigns.page
    likes_query = Like |> select([l], l.user_id)

    Comment
    |> where(post_id: ^post_id)
    |> get_post_comments_sorting(public, user)
    |> limit(^per_page)
    |> offset(^((page - 1) * per_page))
    |> preload([:user, likes: ^likes_query])
    |> Repo.all
  end

  defp get_post_comments_sorting(module, public, user) do
    if public do
      order_by(module, asc: :id)
    else
      order_by(module, fragment("(CASE WHEN user_id = ? then 1 else 2 end)", ^user.id))
    end
  end

  @doc """
  Gets a single comment.

  Raises `Ecto.NoResultsError` if the Comment does not exist.

  ## Examples

      iex> get_comment!(123)
      %Comment{}

      iex> get_comment!(456)
      ** (Ecto.NoResultsError)

  """
  def get_comment!(id) do
    likes_query = Like |> select([l], l.user_id)

    Repo.get!(Comment, id)
    |> Repo.preload([:user, likes: likes_query])
  end

  @doc """
  Creates a comment.

  ## Examples

      iex> create_comment(%{field: value})
      {:ok, %Comment{}}

      iex> create_comment(%{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  def create_comment(user, post, attrs \\ %{}) do
    update_total_comments = post.__struct__ |> where(id: ^post.id)
    comment_attrs = %Comment{} |> Comment.changeset(attrs)
    comment =
      comment_attrs
      |> Ecto.Changeset.put_assoc(:user, user)
      |> Ecto.Changeset.put_assoc(:post, post)

    Ecto.Multi.new()
    |> Ecto.Multi.update_all(:update_total_comments, update_total_comments, inc: [total_comments: 1])
    |> Ecto.Multi.insert(:comment, comment)
    |> Repo.transaction()
    |> case do
      {:ok, %{comment: comment}} ->
        likes_query = Like |> select([l], l.user_id)
        comment |> Repo.preload(likes: likes_query)
    end
  end

  @doc """
  Updates a comment.

  ## Examples

      iex> update_comment(comment, %{field: new_value})
      {:ok, %Comment{}}

      iex> update_comment(comment, %{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  def update_comment(%Comment{} = comment, attrs) do
    comment
    |> Comment.changeset(attrs)
    |> Repo.update()
  end

  @doc """
  Deletes a comment.

  ## Examples

      iex> delete_comment(comment)
      {:ok, %Comment{}}

      iex> delete_comment(comment)
      {:error, %Ecto.Changeset{}}

  """
  def delete_comment(%Comment{} = comment) do
    Repo.delete(comment)
  end

  @doc """
  Returns an `%Ecto.Changeset{}` for tracking comment changes.

  ## Examples

      iex> change_comment(comment)
      %Ecto.Changeset{data: %Comment{}}

  """
  def change_comment(%Comment{} = comment, attrs \\ %{}) do
    Comment.changeset(comment, attrs)
  end
end

内部lib/instagram_clone_web/live/post_live/show.ex更新第 6 行:


...

  alias InstagramCloneWeb.Live.LikeComponent

...

里面lib/instagram_clone_web/live/post_live/show.html.leex更新第70行和行:


...

              InstagramCloneWeb.Live.LikeComponent,

...

内部lib/instagram_clone_web/live/post_live/comment_component.html.leex更新第 24 行:


...

        InstagramCloneWeb.Live.LikeComponent,

...

更新lib/instagram_clone/likes.ex如下:

defmodule InstagramClone.Likes do
  import Ecto.Query, warn: false
  alias InstagramClone.Repo
  alias InstagramClone.Likes.Like

  def create_like(user, liked) do
    user = Ecto.build_assoc(user, :likes)
    like = Ecto.build_assoc(liked, :likes, user)
    update_total_likes = liked.__struct__ |> where(id: ^liked.id)

    Ecto.Multi.new()
    |> Ecto.Multi.insert(:like, like)
    |> Ecto.Multi.update_all(:update_total_likes, update_total_likes, inc: [total_likes: 1])
    |> Repo.transaction()
  end

  def unlike(user_id, liked) do
    like = liked?(user_id, liked.id)
    update_total_likes = liked.__struct__ |> where(id: ^liked.id)

    Ecto.Multi.new()
    |> Ecto.Multi.delete(:like, like)
    |> Ecto.Multi.update_all(:update_total_likes, update_total_likes, inc: [total_likes: -1])
    |> Repo.transaction()
  end


  # Returns nil if not found
  def liked?(user_id, liked_id) do
    Repo.get_by(Like, [user_id: user_id, liked_id: liked_id])
  end
end

让我们添加一个包含 5 个随机用户建议的侧边栏,在里面lib/instagram_clone/accounts.ex添加以下函数:

  def random_5(user) do
    following_list = get_following_list(user)

    User
    |> where([u], u.id not in ^following_list)
    |> where([u], u.id != ^user.id)
    |> order_by(desc: fragment("Random()"))
    |> limit(5)
    |> Repo.all()
  end

在里面lib/instagram_clone_web/live/page_live.ex添加一个handle_info()并将私有assign_posts()函数更新为以下内容:


...
  @impl true
  def handle_info({FollowComponent, :update_totals, _}, socket) do
    {:noreply, socket}
  end
  
  defp assign_posts(socket) do
    if socket.assigns.current_user do
      current_user = socket.assigns.current_user
      random_5_users = Accounts.random_5(current_user)

      socket
      |> assign(users: random_5_users)
      |> assign_user_feed()
    else
      socket
    end
  end

现在要显示带有随机用户的侧边栏,请将 Inside 更新lib/instagram_clone_web/live/page_live.html.leex为以下内容:

<%= if @current_user do %>
  <section class="flex">
    <div id="user-feed" class="w-3/5" phx-update="append">
      <%= for post <- @user_feed do %>
        <%= live_component @socket,
          InstagramCloneWeb.Live.PagePostFeedComponent,
          post: post,
          id: post.id,
          current_user: @current_user %>
      <% end %>
    </div>


    <div>
      <sidebar class="fixed w-1/4">
        <section class=" ml-auto pl-8">
          <div class="flex items-center">
            <!-- Post header section -->
            <%= live_redirect to: Routes.user_profile_path(@socket, :index, @current_user.username) do %>
              <%= img_tag Avatar.get_thumb(@current_user.avatar_url), class: "w-14 h-14 rounded-full object-cover object-center" %>
            <% end %>
            <div class="ml-3">
              <%= live_redirect @current_user.username,
                to: Routes.user_profile_path(@socket, :index, @current_user.username),
                class: "truncate font-bold text-sm text-gray-500 hover:underline" %>
              <h2 class="text-sm text-gray-500"><%= @current_user.full_name %></h2>
            </div>
            <!-- End post header section -->
          </div>
          <h1 class="text-gray-500 mt-5">Suggestions For You</h1>
          <%= for user <- @users do %>
            <div class="flex items-center p-3">
              <!-- Post header section -->
              <%= live_redirect to: Routes.user_profile_path(@socket, :index, user.username) do %>
                <%= img_tag Avatar.get_thumb(user.avatar_url), class: "w-10 h-10 rounded-full object-cover object-center" %>
              <% end %>
              <div class="ml-3">
                <%= live_redirect user.username,
                  to: Routes.user_profile_path(@socket, :index, user.username),
                  class: "truncate font-bold text-sm text-gray-500 hover:underline" %>
                <h2 class="text-xs text-gray-500">Suggested for you</h2>
              </div>
              <span class="ml-auto">
                <%= live_component @socket,
                  InstagramCloneWeb.UserLive.FollowComponent,
                  id: user.id,
                  user: user,
                  current_user: @current_user %>
              </span>
              <!-- End post header section -->
            </div>
          <% end %>
        </section>
      </sidebar>
    </div>
  </section>

  <div
    id="profile-posts-footer"
    class="flex justify-center"
    phx-hook="ProfilePostsScroll">
  </div>
<% else %>
  <%= live_component @socket,
    InstagramCloneWeb.PageLiveComponent,
    id: 1 %>
<% end %>

现在就是这样,您可以通过将以下列表发送到后续组件来提高代码效率,从而无需访问数据库即可设置按钮,从而改进代码。

转自:Elixirprogrammer