使用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 %>
现在就是这样,您可以通过将以下列表发送到后续组件来提高代码效率,从而无需访问数据库即可设置按钮,从而改进代码。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。