Let's Build An Instagram Clone With The PETAL(Phoenix, Elixir, TailwindCSS, AlpineJS, LiveView) Stack [PART 6]
In part 5 we added the show-post page, in this part, we will work on the homepage. You can catch up with the Instagram Clone GitHub Repo.
Let's start by adding a function to our posts context to get the feed and another one to get the total number of the 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
We need the list of following, inside lib/instagram_clone/accounts.ex
add the following function:
@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
Inside lib/instagram_clone_web/live/page_live.ex
let's assign the feed:
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
Page and per page were assigned to the socket in our mount function. We are checking if a user is logged in to get the following list and pass it to the assign feed function to return the socket with the feed assigned, we do that in our handle params function.
Now let's create a component for posts feed, inside our live folder add the following files:
lib/instagram_clone_web/live/page_post_feed_component.ex
lib/instagram_clone_web/live/page_post_feed_component.html.leex
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>
</section>
<div
id="profile-posts-footer"
class="flex justify-center"
phx-hook="ProfilePostsScroll">
</div>
<% else %>
<%= live_component @socket,
InstagramCloneWeb.PageLiveComponent,
id: 1 %>
<% end %>
Inside 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
We are setting the form changeset and temporary comments that we will use to append new comments. The save handle function is the same one that we used on our show page.
Inside 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>
We are using the same form to add new comments that we used on our show page, and we are looping through the post comments and the temporary comments to be able to update the comments when a new one is added.
We need to handle the messages sent from the like component when we like a post or a comment, also we have to handle the event triggered with a hook to load more posts, update lib/instagram_clone_web/live/page_live.ex
to the following:
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
Let's make some changes to our like component, because we are sharing it between posts and comments, move the file to the live folder outside the post_live folder and rename the module to the following:
lib/instagram_clone_web/live/like_component.ex
defmodule InstagramCloneWeb.Live.LikeComponent do
Inside lib/instagram_clone_web/live/post_live/show.html.leex
on line 70 rename the component:
...
InstagramCloneWeb.Live.LikeComponent,
...
Inside lib/instagram_clone_web/live/post_live/comment_component.html.leex
on line 24 also rename the component:
...
InstagramCloneWeb.PostLive.LikeComponent,
...
Inside lib/instagram_clone_web/live/like_component.ex
, let's update send_msg()
to send the liked as variable instead of just the id:
...
defp send_msg(liked) do
msg = get_struct_msg_atom(liked)
send(self(), {__MODULE__, msg, liked})
end
...
Also inside lib/instagram_clone_web/live/like_component.ex
, let's delete the liked?()
function and instead let's check if the user id is inside of a list of user ids on line 61:
...
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)
...
And on line 30 let's update to check the database:
...
if Likes.liked?(current_user.id, liked.id) do
...
Our new updated file should look like the following:
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
Now when we preload the likes we have to only sent the list of ids, open lib/instagram_clone/posts.ex
and on every function that we are getting posts, we have to update how we preload the likes:
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
We also have to do the same for comments, open lib/instagram_clone/comments.ex
and update the file to the following:
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
Inside lib/instagram_clone_web/live/post_live/show.ex
update line 6:
...
alias InstagramCloneWeb.Live.LikeComponent
...
Inside lib/instagram_clone_web/live/post_live/show.html.leex
update line 70 and line:
...
InstagramCloneWeb.Live.LikeComponent,
...
Inside lib/instagram_clone_web/live/post_live/comment_component.html.leex
update line 24:
...
InstagramCloneWeb.Live.LikeComponent,
...
Update lib/instagram_clone/likes.ex
to the following:
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
Let's add a sidebar with 5 random users suggestion, inside lib/instagram_clone/accounts.ex
add the following function:
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
Inside lib/instagram_clone_web/live/page_live.ex
add a handle_info()
and update the private assign_posts()
function to the following:
...
@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
Now to display the sidebar with the random users update Inside lib/instagram_clone_web/live/page_live.html.leex
to the following:
<%= 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 %>
That's it for now, you could improve the code by making it more efficient by sending the following list to the follow component, to set the button without having to go to the database.
I really appreciate your time, thank you so much for reading.