关于phoenix:使用-Phoenix-LiveView-构建-Instagram-6

33次阅读

共计 24115 个字符,预计需要花费 61 分钟才能阅读完成。

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

<!–more–>

在第 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

正文完
 0