add interactions

This commit is contained in:
byte[] 2019-11-16 21:20:33 -05:00
parent 6513fb39b8
commit d60c6ccb06
14 changed files with 409 additions and 273 deletions

View file

@ -5,9 +5,9 @@
import { fetchJson } from './utils/requests';
const endpoints = {
fave: `${window.booru.apiEndpoint}interactions/fave`,
vote: `${window.booru.apiEndpoint}interactions/vote`,
hide: `${window.booru.apiEndpoint}interactions/hide`,
vote(imageId) { return `/images/${imageId}/vote` },
fave(imageId) { return `/images/${imageId}/fave` },
hide(imageId) { return `/images/${imageId}/hide` },
};
const spoilerDownvoteMsg =
@ -22,10 +22,8 @@ function onImage(id, selector, cb) {
function setScore(imageId, data) {
onImage(imageId, '.score',
el => el.textContent = data.score);
onImage(imageId, '.votes',
el => el.textContent = data.votes);
onImage(imageId, '.favorites',
el => el.textContent = data.favourites);
el => el.textContent = data.faves);
onImage(imageId, '.upvotes',
el => el.textContent = data.upvotes);
onImage(imageId, '.downvotes',
@ -73,10 +71,8 @@ function resetHidden(imageId) {
el => el.classList.remove('active'));
}
function interact(type, imageId, value) {
return fetchJson('PUT', endpoints[type], {
class: 'Image', id: imageId, value
})
function interact(type, imageId, method, data = {}) {
return fetchJson(method, endpoints[type](imageId), data)
.then(res => res.json())
.then(res => setScore(imageId, res));
}
@ -129,37 +125,37 @@ const targets = {
/* Active-state targets first */
'.interaction--upvote.active'(imageId) {
interact('vote', imageId, 'false')
interact('vote', imageId, 'DELETE')
.then(() => resetVoted(imageId));
},
'.interaction--downvote.active'(imageId) {
interact('vote', imageId, 'false')
interact('vote', imageId, 'DELETE')
.then(() => resetVoted(imageId));
},
'.interaction--fave.active'(imageId) {
interact('fave', imageId, 'false')
interact('fave', imageId, 'DELETE')
.then(() => resetFaved(imageId));
},
'.interaction--hide.active'(imageId) {
interact('hide', imageId, 'false')
interact('hide', imageId, 'DELETE')
.then(() => resetHidden(imageId));
},
/* Inactive targets */
'.interaction--upvote:not(.active)'(imageId) {
interact('vote', imageId, 'up')
interact('vote', imageId, 'POST', { up: true })
.then(() => { resetVoted(imageId); showUpvoted(imageId); });
},
'.interaction--downvote:not(.active)'(imageId) {
interact('vote', imageId, 'down')
interact('vote', imageId, 'POST', { up: false })
.then(() => { resetVoted(imageId); showDownvoted(imageId); });
},
'.interaction--fave:not(.active)'(imageId) {
interact('fave', imageId, 'true')
interact('fave', imageId, 'POST')
.then(() => { resetVoted(imageId); showFaved(imageId); showUpvoted(imageId); });
},
'.interaction--hide:not(.active)'(imageId) {
interact('hide', imageId, 'true')
interact('hide', imageId, 'POST')
.then(() => { showHidden(imageId); });
},

View file

@ -4,101 +4,51 @@ defmodule Philomena.ImageFaves do
"""
import Ecto.Query, warn: false
alias Philomena.Repo
alias Ecto.Multi
alias Philomena.Images.Image
alias Philomena.ImageFaves.ImageFave
@doc """
Returns the list of image_faves.
## Examples
iex> list_image_faves()
[%ImageFave{}, ...]
Creates a image_hide.
"""
def list_image_faves do
Repo.all(ImageFave)
end
def create_fave_transaction(image, user) do
fave =
%ImageFave{image_id: image.id, user_id: user.id}
|> ImageFave.changeset(%{})
@doc """
Gets a single image_fave.
image_query =
Image
|> where(id: ^image.id)
Raises `Ecto.NoResultsError` if the Image fave does not exist.
## Examples
iex> get_image_fave!(123)
%ImageFave{}
iex> get_image_fave!(456)
** (Ecto.NoResultsError)
"""
def get_image_fave!(id), do: Repo.get!(ImageFave, id)
@doc """
Creates a image_fave.
## Examples
iex> create_image_fave(%{field: value})
{:ok, %ImageFave{}}
iex> create_image_fave(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_image_fave(attrs \\ %{}) do
%ImageFave{}
|> ImageFave.changeset(attrs)
|> Repo.insert()
end
@doc """
Updates a image_fave.
## Examples
iex> update_image_fave(image_fave, %{field: new_value})
{:ok, %ImageFave{}}
iex> update_image_fave(image_fave, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def update_image_fave(%ImageFave{} = image_fave, attrs) do
image_fave
|> ImageFave.changeset(attrs)
|> Repo.update()
Multi.new
|> Multi.insert(:fave, fave)
|> Multi.update_all(:inc_faves_count, image_query, inc: [faves_count: 1])
end
@doc """
Deletes a ImageFave.
## Examples
iex> delete_image_fave(image_fave)
{:ok, %ImageFave{}}
iex> delete_image_fave(image_fave)
{:error, %Ecto.Changeset{}}
"""
def delete_image_fave(%ImageFave{} = image_fave) do
Repo.delete(image_fave)
end
def delete_fave_transaction(image, user) do
fave_query =
ImageFave
|> where(image_id: ^image.id)
|> where(user_id: ^user.id)
@doc """
Returns an `%Ecto.Changeset{}` for tracking image_fave changes.
image_query =
Image
|> where(id: ^image.id)
## Examples
Multi.new
|> Multi.delete_all(:unfave, fave_query)
|> Multi.run(:dec_faves_count, fn repo, %{unfave: {faves, nil}} ->
{count, nil} =
image_query
|> repo.update_all(inc: [faves_count: -faves])
iex> change_image_fave(image_fave)
%Ecto.Changeset{source: %ImageFave{}}
"""
def change_image_fave(%ImageFave{} = image_fave) do
ImageFave.changeset(image_fave, %{})
{:ok, count}
end)
end
end

View file

@ -4,101 +4,51 @@ defmodule Philomena.ImageHides do
"""
import Ecto.Query, warn: false
alias Philomena.Repo
alias Ecto.Multi
alias Philomena.Images.Image
alias Philomena.ImageHides.ImageHide
@doc """
Returns the list of image_hides.
## Examples
iex> list_image_hides()
[%ImageHide{}, ...]
"""
def list_image_hides do
Repo.all(ImageHide)
end
@doc """
Gets a single image_hide.
Raises `Ecto.NoResultsError` if the Image hide does not exist.
## Examples
iex> get_image_hide!(123)
%ImageHide{}
iex> get_image_hide!(456)
** (Ecto.NoResultsError)
"""
def get_image_hide!(id), do: Repo.get!(ImageHide, id)
@doc """
Creates a image_hide.
## Examples
iex> create_image_hide(%{field: value})
{:ok, %ImageHide{}}
iex> create_image_hide(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_image_hide(attrs \\ %{}) do
%ImageHide{}
|> ImageHide.changeset(attrs)
|> Repo.insert()
end
def create_hide_transaction(image, user) do
hide =
%ImageHide{image_id: image.id, user_id: user.id}
|> ImageHide.changeset(%{})
@doc """
Updates a image_hide.
image_query =
Image
|> where(id: ^image.id)
## Examples
iex> update_image_hide(image_hide, %{field: new_value})
{:ok, %ImageHide{}}
iex> update_image_hide(image_hide, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def update_image_hide(%ImageHide{} = image_hide, attrs) do
image_hide
|> ImageHide.changeset(attrs)
|> Repo.update()
Multi.new
|> Multi.insert(:hide, hide)
|> Multi.update_all(:inc_hides_count, image_query, inc: [hides_count: 1])
end
@doc """
Deletes a ImageHide.
## Examples
iex> delete_image_hide(image_hide)
{:ok, %ImageHide{}}
iex> delete_image_hide(image_hide)
{:error, %Ecto.Changeset{}}
"""
def delete_image_hide(%ImageHide{} = image_hide) do
Repo.delete(image_hide)
end
def delete_hide_transaction(image, user) do
hide_query =
ImageHide
|> where(image_id: ^image.id)
|> where(user_id: ^user.id)
@doc """
Returns an `%Ecto.Changeset{}` for tracking image_hide changes.
image_query =
Image
|> where(id: ^image.id)
## Examples
Multi.new
|> Multi.delete_all(:unhide, hide_query)
|> Multi.run(:dec_hides_count, fn repo, %{unhide: {hides, nil}} ->
{count, nil} =
image_query
|> repo.update_all(inc: [hides_count: -hides])
iex> change_image_hide(image_hide)
%Ecto.Changeset{source: %ImageHide{}}
"""
def change_image_hide(%ImageHide{} = image_hide) do
ImageHide.changeset(image_hide, %{})
{:ok, count}
end)
end
end

View file

@ -4,101 +4,62 @@ defmodule Philomena.ImageVotes do
"""
import Ecto.Query, warn: false
alias Philomena.Repo
alias Ecto.Multi
alias Philomena.Images.Image
alias Philomena.ImageVotes.ImageVote
@doc """
Returns the list of image_votes.
## Examples
iex> list_image_votes()
[%ImageVote{}, ...]
"""
def list_image_votes do
Repo.all(ImageVote)
end
@doc """
Gets a single image_vote.
Raises `Ecto.NoResultsError` if the Image vote does not exist.
## Examples
iex> get_image_vote!(123)
%ImageVote{}
iex> get_image_vote!(456)
** (Ecto.NoResultsError)
"""
def get_image_vote!(id), do: Repo.get!(ImageVote, id)
@doc """
Creates a image_vote.
## Examples
iex> create_image_vote(%{field: value})
{:ok, %ImageVote{}}
iex> create_image_vote(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_image_vote(attrs \\ %{}) do
%ImageVote{}
|> ImageVote.changeset(attrs)
|> Repo.insert()
end
def create_vote_transaction(image, user, up) do
vote =
%ImageVote{image_id: image.id, user_id: user.id, up: up}
|> ImageVote.changeset(%{})
@doc """
Updates a image_vote.
image_query =
Image
|> where(id: ^image.id)
## Examples
upvotes = if up, do: 1, else: 0
downvotes = if up, do: 0, else: 1
iex> update_image_vote(image_vote, %{field: new_value})
{:ok, %ImageVote{}}
iex> update_image_vote(image_vote, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def update_image_vote(%ImageVote{} = image_vote, attrs) do
image_vote
|> ImageVote.changeset(attrs)
|> Repo.update()
Multi.new
|> Multi.insert(:vote, vote)
|> Multi.update_all(:inc_vote_count, image_query, inc: [upvotes_count: upvotes, downvotes_count: downvotes, score: upvotes - downvotes])
end
@doc """
Deletes a ImageVote.
## Examples
iex> delete_image_vote(image_vote)
{:ok, %ImageVote{}}
iex> delete_image_vote(image_vote)
{:error, %Ecto.Changeset{}}
"""
def delete_image_vote(%ImageVote{} = image_vote) do
Repo.delete(image_vote)
end
def delete_vote_transaction(image, user) do
upvote_query =
ImageVote
|> where(image_id: ^image.id)
|> where(user_id: ^user.id)
|> where(up: true)
@doc """
Returns an `%Ecto.Changeset{}` for tracking image_vote changes.
downvote_query =
ImageVote
|> where(image_id: ^image.id)
|> where(user_id: ^user.id)
|> where(up: false)
## Examples
image_query =
Image
|> where(id: ^image.id)
iex> change_image_vote(image_vote)
%Ecto.Changeset{source: %ImageVote{}}
Multi.new
|> Multi.delete_all(:unupvote, upvote_query)
|> Multi.delete_all(:undownvote, downvote_query)
|> Multi.run(:dec_votes_count, fn repo, %{unupvote: {upvotes, nil}, undownvote: {downvotes, nil}} ->
{count, nil} =
image_query
|> repo.update_all(inc: [upvotes_count: -upvotes, downvotes_count: -downvotes, score: downvotes - upvotes])
"""
def change_image_vote(%ImageVote{} = image_vote) do
ImageVote.changeset(image_vote, %{})
{:ok, count}
end)
end
end

View file

@ -104,6 +104,22 @@ defmodule Philomena.Images do
Image.changeset(image, %{})
end
def reindex_image(%Image{} = image) do
spawn fn ->
Image
|> preload(^indexing_preloads())
|> where(id: ^image.id)
|> Repo.one()
|> Image.index_document()
end
image
end
def indexing_preloads do
[:user, :favers, :downvoters, :upvoters, :hiders, :deleter, :gallery_interactions, tags: [:aliases, :aliased_tag]]
end
alias Philomena.Images.Subscription
@doc """

View file

@ -83,4 +83,13 @@ defmodule Philomena.Images.Image do
|> cast(attrs, [])
|> validate_required([])
end
def interaction_data(image) do
%{
score: image.score,
faves: image.faves_count,
upvotes: image.upvotes_count,
downvotes: image.downvotes_count
}
end
end

View file

@ -0,0 +1,60 @@
defmodule Philomena.Interactions do
import Ecto.Query
alias Philomena.ImageHides.ImageHide
alias Philomena.ImageFaves.ImageFave
alias Philomena.ImageVotes.ImageVote
alias Philomena.Repo
def user_interactions(_images, nil),
do: []
def user_interactions(images, user) do
ids =
images
|> Enum.flat_map(fn
nil -> []
%{id: id} -> [id]
enum -> Enum.map(enum, & &1.id)
end)
|> Enum.uniq()
hide_interactions =
ImageHide
|> select([h], %{image_id: h.image_id, user_id: h.user_id, interaction_type: ^"hidden", value: ^""})
|> where([h], h.image_id in ^ids)
|> where(user_id: ^user.id)
fave_interactions =
ImageFave
|> select([f], %{image_id: f.image_id, user_id: f.user_id, interaction_type: ^"faved", value: ^""})
|> where([f], f.image_id in ^ids)
|> where(user_id: ^user.id)
upvote_interactions =
ImageVote
|> select([v], %{image_id: v.image_id, user_id: v.user_id, interaction_type: ^"voted", value: ^"up"})
|> where([v], v.image_id in ^ids)
|> where(user_id: ^user.id, up: true)
downvote_interactions =
ImageVote
|> select([v], %{image_id: v.image_id, user_id: v.user_id, interaction_type: ^"voted", value: ^"down"})
|> where([v], v.image_id in ^ids)
|> where(user_id: ^user.id, up: false)
[
hide_interactions,
fave_interactions,
upvote_interactions,
downvote_interactions
]
|> union_all_queries()
|> Repo.all()
end
defp union_all_queries([query]),
do: query
defp union_all_queries([query | rest]),
do: query |> union_all(^union_all_queries(rest))
end

View file

@ -1,7 +1,9 @@
defmodule PhilomenaWeb.ActivityController do
use PhilomenaWeb, :controller
alias Philomena.{Images, Images.Image, Images.Feature, Comments.Comment, Channels.Channel, Topics.Topic, Forums.Forum}
alias Philomena.{Images.Image, ImageFeatures.ImageFeature, Comments.Comment, Channels.Channel, Topics.Topic, Forums.Forum}
alias Philomena.Interactions
alias Philomena.Images
alias Philomena.Repo
import Ecto.Query
@ -15,8 +17,11 @@ defmodule PhilomenaWeb.ActivityController do
%{
query: %{
bool: %{
must_not: filter,
must: image_query
must: image_query,
must_not: [
filter,
%{term: %{hidden_from_users: true}}
],
}
},
sort: %{created_at: :desc}
@ -30,8 +35,11 @@ defmodule PhilomenaWeb.ActivityController do
%{
query: %{
bool: %{
must_not: filter,
must: %{range: %{first_seen_at: %{gt: "now-3d"}}}
must: %{range: %{first_seen_at: %{gt: "now-3d"}}},
must_not: [
filter,
%{term: %{hidden_from_users: true}}
]
}
},
sort: [%{score: :desc}, %{first_seen_at: :desc}]
@ -67,8 +75,11 @@ defmodule PhilomenaWeb.ActivityController do
%{
query: %{
bool: %{
must_not: filter,
must: watched_query
must: watched_query,
must_not: [
filter,
%{term: %{hidden_from_users: true}}
]
}
},
sort: %{created_at: :desc}
@ -80,7 +91,7 @@ defmodule PhilomenaWeb.ActivityController do
featured_image =
Image
|> join(:inner, [i], f in Feature, on: [image_id: i.id])
|> join(:inner, [i], f in ImageFeature, on: [image_id: i.id])
|> order_by([i, f], desc: f.created_at)
|> limit(1)
|> preload([:tags])
@ -105,6 +116,12 @@ defmodule PhilomenaWeb.ActivityController do
|> limit(6)
|> Repo.all()
interactions =
Interactions.user_interactions(
[images, top_scoring, watched, featured_image],
user
)
render(
conn,
"index.html",
@ -114,7 +131,8 @@ defmodule PhilomenaWeb.ActivityController do
watched: watched,
featured_image: featured_image,
streams: streams,
topics: topics
topics: topics,
interactions: interactions
)
end
end

View file

@ -0,0 +1,60 @@
defmodule PhilomenaWeb.Image.FaveController do
use PhilomenaWeb, :controller
alias Philomena.{Images, Images.Image}
alias Philomena.{ImageFaves, ImageVotes}
alias Philomena.Repo
alias Ecto.Multi
plug PhilomenaWeb.Plugs.FilterBannedUsers
plug :load_and_authorize_resource, model: Image, id_name: "image_id", persisted: true
def create(conn, _params) do
user = conn.assigns.current_user
image = conn.assigns.image
Multi.append(
ImageFaves.delete_fave_transaction(image, user),
ImageFaves.create_fave_transaction(image, user)
)
|> Multi.append(ImageVotes.delete_vote_transaction(image, user))
|> Multi.append(ImageVotes.create_vote_transaction(image, user, true))
|> Repo.transaction()
|> case do
{:ok, _result} ->
image =
Images.get_image!(image.id)
|> Images.reindex_image()
conn
|> json(Image.interaction_data(image))
_error ->
conn
|> Plug.Conn.put_status(409)
|> json(%{})
end
end
def delete(conn, _params) do
user = conn.assigns.current_user
image = conn.assigns.image
ImageFaves.delete_fave_transaction(image, user)
|> Repo.transaction()
|> case do
{:ok, _result} ->
image =
Images.get_image!(image.id)
|> Images.reindex_image()
conn
|> json(Image.interaction_data(image))
_error ->
conn
|> Plug.Conn.put_status(409)
|> json(%{})
end
end
end

View file

@ -0,0 +1,58 @@
defmodule PhilomenaWeb.Image.HideController do
use PhilomenaWeb, :controller
alias Philomena.{Images, Images.Image}
alias Philomena.ImageHides
alias Philomena.Repo
alias Ecto.Multi
plug PhilomenaWeb.Plugs.FilterBannedUsers
plug :load_and_authorize_resource, model: Image, id_name: "image_id", persisted: true
def create(conn, _params) do
user = conn.assigns.current_user
image = conn.assigns.image
Multi.append(
ImageHides.delete_hide_transaction(image, user),
ImageHides.create_hide_transaction(image, user)
)
|> Repo.transaction()
|> case do
{:ok, _result} ->
image =
Images.get_image!(image.id)
|> Images.reindex_image()
conn
|> json(Image.interaction_data(image))
_error ->
conn
|> Plug.Conn.put_status(409)
|> json(%{})
end
end
def delete(conn, _params) do
user = conn.assigns.current_user
image = conn.assigns.image
ImageHides.delete_hide_transaction(image, user)
|> Repo.transaction()
|> case do
{:ok, _result} ->
image =
Images.get_image!(image.id)
|> Images.reindex_image()
conn
|> json(Image.interaction_data(image))
_error ->
conn
|> Plug.Conn.put_status(409)
|> json(%{})
end
end
end

View file

@ -1,18 +1,58 @@
defmodule PhilomenaWeb.Image.VoteController do
use PhilomenaWeb, :controller
alias Philomena.Images.Image
# alias Philomena.Repo
# alias Ecto.Multi
alias Philomena.{Images, Images.Image}
alias Philomena.ImageVotes
alias Philomena.Repo
alias Ecto.Multi
plug PhilomenaWeb.Plugs.FilterBannedUsers
plug :load_and_authorize_resource, model: Image, id_name: "image_id", persisted: true
def create(conn, _params) do
def create(conn, params) do
user = conn.assigns.current_user
image = conn.assigns.image
Multi.append(
ImageVotes.delete_vote_transaction(image, user),
ImageVotes.create_vote_transaction(image, user, params["up"] == true)
)
|> Repo.transaction()
|> case do
{:ok, _result} ->
image =
Images.get_image!(image.id)
|> Images.reindex_image()
conn
|> json(Image.interaction_data(image))
_error ->
conn
|> Plug.Conn.put_status(409)
|> json(%{})
end
end
def delete(conn, _params) do
user = conn.assigns.current_user
image = conn.assigns.image
ImageVotes.delete_vote_transaction(image, user)
|> Repo.transaction()
|> case do
{:ok, _result} ->
image =
Images.get_image!(image.id)
|> Images.reindex_image()
conn
|> json(Image.interaction_data(image))
_error ->
conn
|> Plug.Conn.put_status(409)
|> json(%{})
end
end
end

View file

@ -2,6 +2,7 @@ defmodule PhilomenaWeb.ImageController do
use PhilomenaWeb, :controller
alias Philomena.{Images.Image, Comments.Comment, Textile.Renderer}
alias Philomena.Interactions
alias Philomena.Repo
import Ecto.Query
@ -20,13 +21,18 @@ defmodule PhilomenaWeb.ImageController do
Image |> preload([:tags, :user])
)
render(conn, "index.html", images: images)
interactions =
Interactions.user_interactions(images, conn.assigns.current_user)
render(conn, "index.html", images: images, interactions: interactions)
end
def show(conn, %{"id" => _id}) do
image = conn.assigns.image
comments =
Comment
|> where(image_id: ^conn.assigns.image.id)
|> where(image_id: ^image.id)
|> preload([:image, user: [awards: :badge]])
|> order_by(desc: :created_at)
|> limit(25)
@ -40,9 +46,12 @@ defmodule PhilomenaWeb.ImageController do
%{comments | entries: Enum.zip(comments.entries, rendered)}
description =
%{body: conn.assigns.image.description}
%{body: image.description}
|> Renderer.render_one()
render(conn, "show.html", image: conn.assigns.image, comments: comments, description: description)
interactions =
Interactions.user_interactions([image], conn.assigns.current_user)
render(conn, "show.html", image: image, comments: comments, description: description, interactions: interactions)
end
end

View file

@ -2,13 +2,14 @@ defmodule PhilomenaWeb.SearchController do
use PhilomenaWeb, :controller
alias Philomena.Images.{Image, Query}
alias Philomena.Interactions
alias Pow.Plug
import Ecto.Query
def index(conn, params) do
filter = conn.assigns[:compiled_filter]
user = conn |> Plug.current_user()
filter = conn.assigns.compiled_filter
user = conn.assigns.current_user
with {:ok, query} <- Query.compile(user, params["q"]) do
images =
@ -21,8 +22,11 @@ defmodule PhilomenaWeb.SearchController do
Image |> preload(:tags)
)
interactions =
Interactions.user_interactions(images, user)
conn
|> render("index.html", images: images, search_query: params["q"])
|> render("index.html", images: images, search_query: params["q"], interactions: interactions)
else
{:error, msg} ->
conn

View file

@ -54,6 +54,11 @@ defmodule PhilomenaWeb.Router do
resources "/notifications", NotificationController, only: [:index, :delete]
resources "/conversations", ConversationController, only: [:index, :show]
resources "/images", ImageController, only: [] do
resources "/vote", Image.VoteController, only: [:create, :delete], singleton: true
resources "/fave", Image.FaveController, only: [:create, :delete], singleton: true
resources "/hide", Image.HideController, only: [:create, :delete], singleton: true
end
end
scope "/", PhilomenaWeb do