--- /dev/null
+defmodule Pleroma.Bookmark do
+ use Ecto.Schema
+
+ import Ecto.Changeset
+ import Ecto.Query
+
+ alias Pleroma.Activity
+ alias Pleroma.Bookmark
+ alias Pleroma.FlakeId
+ alias Pleroma.Repo
+ alias Pleroma.User
+
+ @type t :: %__MODULE__{}
+
+ schema "bookmarks" do
+ belongs_to(:user, User, type: FlakeId)
+ belongs_to(:activity, Activity, type: FlakeId)
+
+ timestamps()
+ end
+
+ @spec create(FlakeId.t(), FlakeId.t()) :: {:ok, Bookmark.t()} | {:error, Changeset.t()}
+ def create(user_id, activity_id) do
+ attrs = %{
+ user_id: user_id,
+ activity_id: activity_id
+ }
+
+ %Bookmark{}
+ |> cast(attrs, [:user_id, :activity_id])
+ |> validate_required([:user_id, :activity_id])
+ |> unique_constraint(:activity_id, name: :bookmarks_user_id_activity_id_index)
+ |> Repo.insert()
+ end
+
+ @spec for_user_query(FlakeId.t()) :: Ecto.Query.t()
+ def for_user_query(user_id) do
+ Bookmark
+ |> where(user_id: ^user_id)
+ |> join(:inner, [b], activity in assoc(b, :activity))
+ |> preload([b, a], activity: a)
+ end
+
+ @spec destroy(FlakeId.t(), FlakeId.t()) :: {:ok, Bookmark.t()} | {:error, Changeset.t()}
+ def destroy(user_id, activity_id) do
+ from(b in Bookmark,
+ where: b.user_id == ^user_id,
+ where: b.activity_id == ^activity_id
+ )
+ |> Repo.one()
+ |> Repo.delete()
+ end
+end
alias Comeonin.Pbkdf2
alias Pleroma.Activity
+ alias Pleroma.Bookmark
alias Pleroma.Formatter
alias Pleroma.Notification
alias Pleroma.Object
field(:search_rank, :float, virtual: true)
field(:search_type, :integer, virtual: true)
field(:tags, {:array, :string}, default: [])
- field(:bookmarks, {:array, :string}, default: [])
field(:last_refreshed_at, :naive_datetime_usec)
+ has_many(:bookmarks, Bookmark)
has_many(:notifications, Notification)
has_many(:registrations, Registration)
embeds_one(:info, Pleroma.User.Info)
updated_user
end
- def bookmark(%User{} = user, status_id) do
- bookmarks = Enum.uniq(user.bookmarks ++ [status_id])
- update_bookmarks(user, bookmarks)
- end
-
- def unbookmark(%User{} = user, status_id) do
- bookmarks = Enum.uniq(user.bookmarks -- [status_id])
- update_bookmarks(user, bookmarks)
- end
-
- def update_bookmarks(%User{} = user, bookmarks) do
- user
- |> change(%{bookmarks: bookmarks})
- |> update_and_set_cache
- end
-
defp normalize_tags(tags) do
[tags]
|> List.flatten()
use Pleroma.Web, :controller
alias Ecto.Changeset
alias Pleroma.Activity
+ alias Pleroma.Bookmark
alias Pleroma.Config
alias Pleroma.Filter
alias Pleroma.Notification
|> ActivityPub.contain_timeline(user)
|> Enum.reverse()
+ user = Repo.preload(user, :bookmarks)
+
conn
|> add_link_headers(:home_timeline, activities)
|> put_view(StatusView)
|> ActivityPub.fetch_public_activities()
|> Enum.reverse()
+ user = Repo.preload(user, :bookmarks)
+
conn
|> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
|> put_view(StatusView)
end
def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
- with %User{} = user <- User.get_cached_by_id(params["id"]) do
+ with %User{} = user <- User.get_cached_by_id(params["id"]),
+ reading_user <- Repo.preload(reading_user, :bookmarks) do
activities = ActivityPub.fetch_user_activities(user, reading_user, params)
conn
|> ActivityPub.fetch_activities_query(params)
|> Pagination.fetch_paginated(params)
+ user = Repo.preload(user, :bookmarks)
+
conn
|> add_link_headers(:dm_timeline, activities)
|> put_view(StatusView)
%Object{} = object <- Object.normalize(activity),
%User{} = user <- User.get_cached_by_nickname(user.nickname),
true <- Visibility.visible_for_user?(activity, user),
- {:ok, user} <- User.bookmark(user, object.data["id"]) do
+ {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
+ user = Repo.preload(user, :bookmarks)
+
conn
|> put_view(StatusView)
|> try_render("status.json", %{activity: activity, for: user, as: :activity})
%Object{} = object <- Object.normalize(activity),
%User{} = user <- User.get_cached_by_nickname(user.nickname),
true <- Visibility.visible_for_user?(activity, user),
- {:ok, user} <- User.unbookmark(user, object.data["id"]) do
+ {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
+ user = Repo.preload(user, :bookmarks)
+
conn
|> put_view(StatusView)
|> try_render("status.json", %{activity: activity, for: user, as: :activity})
end
end
- def bookmarks(%{assigns: %{user: user}} = conn, _) do
+ def bookmarks(%{assigns: %{user: user}} = conn, params) do
user = User.get_cached_by_id(user.id)
+ user = Repo.preload(user, :bookmarks)
+
+ bookmarks =
+ Bookmark.for_user_query(user.id)
+ |> Pagination.fetch_paginated(params)
activities =
- user.bookmarks
- |> Enum.map(fn id -> Activity.get_create_by_object_ap_id(id) end)
- |> Enum.reverse()
+ bookmarks
+ |> Enum.map(fn b -> b.activity end)
conn
+ |> add_link_headers(:bookmarks, bookmarks)
|> put_view(StatusView)
|> render("index.json", %{activities: activities, for: user, as: :activity})
end
|> ActivityPub.fetch_activities_bounded(following, params)
|> Enum.reverse()
+ user = Repo.preload(user, :bookmarks)
+
conn
|> put_view(StatusView)
|> render("index.json", %{activities: activities, for: user, as: :activity})
favorited = opts[:for] && opts[:for].ap_id in (object.data["likes"] || [])
- bookmarked = opts[:for] && object.data["id"] in opts[:for].bookmarks
+ bookmarked =
+ opts[:for] && Ecto.assoc_loaded?(opts[:for].bookmarks) &&
+ Enum.any?(opts[:for].bookmarks, fn b -> b.activity_id == activity.id end)
attachment_data = object.data["attachment"] || []
attachments = render_many(attachment_data, StatusView, "attachment.json", as: :attachment)
--- /dev/null
+defmodule Pleroma.Repo.Migrations.CreateBookmarks do
+ use Ecto.Migration
+
+ def change do
+ create table(:bookmarks) do
+ add(:user_id, references(:users, type: :uuid, on_delete: :delete_all))
+ add(:activity_id, references(:activities, type: :uuid, on_delete: :delete_all))
+
+ timestamps()
+ end
+
+ create(unique_index(:bookmarks, [:user_id, :activity_id]))
+ end
+end
--- /dev/null
+defmodule Pleroma.BookmarkTest do
+ use Pleroma.DataCase
+ import Pleroma.Factory
+ alias Pleroma.Bookmark
+ alias Pleroma.Web.CommonAPI
+
+ describe "create/2" do
+ test "with valid params" do
+ user = insert(:user)
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "Some cool information"})
+ {:ok, bookmark} = Bookmark.create(user.id, activity.id)
+ assert bookmark.user_id == user.id
+ assert bookmark.activity_id == activity.id
+ end
+
+ test "with invalid params" do
+ {:error, changeset} = Bookmark.create(nil, "")
+ refute changeset.valid?
+
+ assert changeset.errors == [
+ user_id: {"can't be blank", [validation: :required]},
+ activity_id: {"can't be blank", [validation: :required]}
+ ]
+ end
+ end
+
+ describe "destroy/2" do
+ test "with valid params" do
+ user = insert(:user)
+
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "Some cool information"})
+ {:ok, _bookmark} = Bookmark.create(user.id, activity.id)
+
+ {:ok, _deleted_bookmark} = Bookmark.destroy(user.id, activity.id)
+ end
+ end
+end
end
end
- test "bookmarks" do
- user = insert(:user)
-
- {:ok, activity1} =
- CommonAPI.post(user, %{
- "status" => "heweoo!"
- })
-
- id1 = Object.normalize(activity1).data["id"]
-
- {:ok, activity2} =
- CommonAPI.post(user, %{
- "status" => "heweoo!"
- })
-
- id2 = Object.normalize(activity2).data["id"]
-
- assert {:ok, user_state1} = User.bookmark(user, id1)
- assert user_state1.bookmarks == [id1]
-
- assert {:ok, user_state2} = User.unbookmark(user, id1)
- assert user_state2.bookmarks == []
-
- assert {:ok, user_state3} = User.bookmark(user, id2)
- assert user_state3.bookmarks == [id2]
- end
-
test "follower count is updated when a follower is blocked" do
user = insert(:user)
follower = insert(:user)