共计 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