Merge branch 'emoji-reaction-extensions-2' into 'develop'

Emoji Reactions: In the API, sort them by first emoji insertion date

See merge request pleroma/pleroma!2126
This commit is contained in:
rinpatch 2020-01-22 14:11:01 +00:00
commit 34aa0c542b
9 changed files with 77 additions and 46 deletions

View file

@ -95,6 +95,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Captcha: Enable by default - Captcha: Enable by default
- Mastodon API: Add support for `account_id` param to filter notifications by the account - Mastodon API: Add support for `account_id` param to filter notifications by the account
- Mastodon API: Add `emoji_reactions` property to Statuses - Mastodon API: Add `emoji_reactions` property to Statuses
- Mastodon API: Change emoji reaction reply format
</details> </details>
### Fixed ### Fixed

View file

@ -29,7 +29,7 @@ Has these additional fields under the `pleroma` object:
- `spoiler_text`: a map consisting of alternate representations of the `spoiler_text` property with the key being it's mimetype. Currently the only alternate representation supported is `text/plain` - `spoiler_text`: a map consisting of alternate representations of the `spoiler_text` property with the key being it's mimetype. Currently the only alternate representation supported is `text/plain`
- `expires_at`: a datetime (iso8601) that states when the post will expire (be deleted automatically), or empty if the post won't expire - `expires_at`: a datetime (iso8601) that states when the post will expire (be deleted automatically), or empty if the post won't expire
- `thread_muted`: true if the thread the post belongs to is muted - `thread_muted`: true if the thread the post belongs to is muted
- `emoji_reactions`: An object with all the emoji reactions with count. Contains no information about the reacting users, for that use the `emoji_reactions_by` endpoint. - `emoji_reactions`: A list with emoji / reaction count tuples. Contains no information about the reacting users, for that use the `emoji_reactions_by` endpoint.
## Attachments ## Attachments

View file

@ -451,11 +451,11 @@ Emoji reactions work a lot like favourites do. They make it possible to react to
* Method: `GET` * Method: `GET`
* Authentication: optional * Authentication: optional
* Params: None * Params: None
* Response: JSON, a map of emoji to account list mappings. * Response: JSON, a list of emoji/account list tuples, sorted by emoji insertion date, in ascending order, e.g, the first emoji in the list is the oldest.
* Example Response: * Example Response:
```json ```json
{ [
"😀" => [{"id" => "xyz.."...}, {"id" => "zyx..."}], ["😀", [{"id" => "xyz.."...}, {"id" => "zyx..."}]],
"🗡" => [{"id" => "abc..."}] ["☕", [{"id" => "abc..."}]]
} ]
``` ```

View file

@ -312,19 +312,12 @@ def make_emoji_reaction_data(user, object, emoji, activity_id) do
|> Map.put("content", emoji) |> Map.put("content", emoji)
end end
@spec update_element_in_object(String.t(), list(any), Object.t()) :: @spec update_element_in_object(String.t(), list(any), Object.t(), integer() | nil) ::
{:ok, Object.t()} | {:error, Ecto.Changeset.t()} {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
def update_element_in_object(property, element, object) do def update_element_in_object(property, element, object, count \\ nil) do
length = length =
if is_map(element) do count ||
element length(element)
|> Map.values()
|> List.flatten()
|> length()
else
element
|> length()
end
data = data =
Map.merge( Map.merge(
@ -344,29 +337,52 @@ def add_emoji_reaction_to_object(
%Activity{data: %{"content" => emoji, "actor" => actor}}, %Activity{data: %{"content" => emoji, "actor" => actor}},
object object
) do ) do
reactions = object.data["reactions"] || %{} reactions = object.data["reactions"] || []
emoji_actors = reactions[emoji] || []
new_emoji_actors = [actor | emoji_actors] |> Enum.uniq() new_reactions =
new_reactions = Map.put(reactions, emoji, new_emoji_actors) case Enum.find_index(reactions, fn [candidate, _] -> emoji == candidate end) do
update_element_in_object("reaction", new_reactions, object) nil ->
reactions ++ [[emoji, [actor]]]
index ->
List.update_at(
reactions,
index,
fn [emoji, users] -> [emoji, Enum.uniq([actor | users])] end
)
end
count = emoji_count(new_reactions)
update_element_in_object("reaction", new_reactions, object, count)
end
def emoji_count(reactions_list) do
Enum.reduce(reactions_list, 0, fn [_, users], acc -> acc + length(users) end)
end end
def remove_emoji_reaction_from_object( def remove_emoji_reaction_from_object(
%Activity{data: %{"content" => emoji, "actor" => actor}}, %Activity{data: %{"content" => emoji, "actor" => actor}},
object object
) do ) do
reactions = object.data["reactions"] || %{} reactions = object.data["reactions"] || []
emoji_actors = reactions[emoji] || []
new_emoji_actors = List.delete(emoji_actors, actor)
new_reactions = new_reactions =
if new_emoji_actors == [] do case Enum.find_index(reactions, fn [candidate, _] -> emoji == candidate end) do
Map.delete(reactions, emoji) nil ->
else reactions
Map.put(reactions, emoji, new_emoji_actors)
index ->
List.update_at(
reactions,
index,
fn [emoji, users] -> [emoji, List.delete(users, actor)] end
)
|> Enum.reject(fn [_, users] -> Enum.empty?(users) end)
end end
update_element_in_object("reaction", new_reactions, object) count = emoji_count(new_reactions)
update_element_in_object("reaction", new_reactions, object, count)
end end
@spec add_like_to_object(Activity.t(), Object.t()) :: @spec add_like_to_object(Activity.t(), Object.t()) ::

View file

@ -255,12 +255,11 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity}
emoji_reactions = emoji_reactions =
with %{data: %{"reactions" => emoji_reactions}} <- object do with %{data: %{"reactions" => emoji_reactions}} <- object do
Enum.map(emoji_reactions, fn {emoji, users} -> Enum.map(emoji_reactions, fn [emoji, users] ->
{emoji, length(users)} [emoji, length(users)]
end) end)
|> Enum.into(%{})
else else
_ -> %{} _ -> []
end end
%{ %{

View file

@ -43,21 +43,21 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
def emoji_reactions_by(%{assigns: %{user: user}} = conn, %{"id" => activity_id}) do def emoji_reactions_by(%{assigns: %{user: user}} = conn, %{"id" => activity_id}) do
with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id), with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
%Object{data: %{"reactions" => emoji_reactions}} <- Object.normalize(activity) do %Object{data: %{"reactions" => emoji_reactions}} when is_list(emoji_reactions) <-
Object.normalize(activity) do
reactions = reactions =
emoji_reactions emoji_reactions
|> Enum.map(fn {emoji, users} -> |> Enum.map(fn [emoji, users] ->
users = Enum.map(users, &User.get_cached_by_ap_id/1) users = Enum.map(users, &User.get_cached_by_ap_id/1)
{emoji, AccountView.render("index.json", %{users: users, for: user, as: :user})} {emoji, AccountView.render("index.json", %{users: users, for: user, as: :user})}
end) end)
|> Enum.into(%{})
conn conn
|> json(reactions) |> json(reactions)
else else
_e -> _e ->
conn conn
|> json(%{}) |> json([])
end end
end end

View file

@ -867,6 +867,8 @@ test "returns reblogs for users for whom reblogs have not been muted" do
test "adds an emoji reaction activity to the db" do test "adds an emoji reaction activity to the db" do
user = insert(:user) user = insert(:user)
reactor = insert(:user) reactor = insert(:user)
third_user = insert(:user)
fourth_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "YASSSS queen slay"}) {:ok, activity} = CommonAPI.post(user, %{"status" => "YASSSS queen slay"})
assert object = Object.normalize(activity) assert object = Object.normalize(activity)
@ -881,7 +883,21 @@ test "adds an emoji reaction activity to the db" do
assert reaction_activity.data["to"] == [User.ap_followers(reactor), activity.data["actor"]] assert reaction_activity.data["to"] == [User.ap_followers(reactor), activity.data["actor"]]
assert reaction_activity.data["context"] == object.data["context"] assert reaction_activity.data["context"] == object.data["context"]
assert object.data["reaction_count"] == 1 assert object.data["reaction_count"] == 1
assert object.data["reactions"]["🔥"] == [reactor.ap_id] assert object.data["reactions"] == [["🔥", [reactor.ap_id]]]
{:ok, _reaction_activity, object} = ActivityPub.react_with_emoji(third_user, object, "")
assert object.data["reaction_count"] == 2
assert object.data["reactions"] == [["🔥", [reactor.ap_id]], ["", [third_user.ap_id]]]
{:ok, _reaction_activity, object} = ActivityPub.react_with_emoji(fourth_user, object, "🔥")
assert object.data["reaction_count"] == 3
assert object.data["reactions"] == [
["🔥", [fourth_user.ap_id, reactor.ap_id]],
["", [third_user.ap_id]]
]
end end
end end
@ -919,7 +935,7 @@ test "adds an undo activity to the db" do
object = Object.get_by_ap_id(object.data["id"]) object = Object.get_by_ap_id(object.data["id"])
assert object.data["reaction_count"] == 0 assert object.data["reaction_count"] == 0
assert object.data["reactions"] == %{} assert object.data["reactions"] == []
end end
end end

View file

@ -31,13 +31,12 @@ test "has an emoji reaction list" do
{:ok, activity} = CommonAPI.post(user, %{"status" => "dae cofe??"}) {:ok, activity} = CommonAPI.post(user, %{"status" => "dae cofe??"})
{:ok, _, _} = CommonAPI.react_with_emoji(activity.id, user, "") {:ok, _, _} = CommonAPI.react_with_emoji(activity.id, user, "")
{:ok, _, _} = CommonAPI.react_with_emoji(activity.id, other_user, "")
{:ok, _, _} = CommonAPI.react_with_emoji(activity.id, third_user, "🍵") {:ok, _, _} = CommonAPI.react_with_emoji(activity.id, third_user, "🍵")
{:ok, _, _} = CommonAPI.react_with_emoji(activity.id, other_user, "")
activity = Repo.get(Activity, activity.id) activity = Repo.get(Activity, activity.id)
status = StatusView.render("show.json", activity: activity) status = StatusView.render("show.json", activity: activity)
assert status[:pleroma][:emoji_reactions]["🍵"] == 1 assert status[:pleroma][:emoji_reactions] == [["", 2], ["🍵", 1]]
assert status[:pleroma][:emoji_reactions][""] == 2
end end
test "loads and returns the direct conversation id when given the `with_direct_conversation_id` option" do test "loads and returns the direct conversation id when given the `with_direct_conversation_id` option" do
@ -189,7 +188,7 @@ test "a note activity" do
expires_at: nil, expires_at: nil,
direct_conversation_id: nil, direct_conversation_id: nil,
thread_muted: false, thread_muted: false,
emoji_reactions: %{} emoji_reactions: []
} }
} }

View file

@ -62,7 +62,7 @@ test "GET /api/v1/pleroma/statuses/:id/emoji_reactions_by", %{conn: conn} do
|> get("/api/v1/pleroma/statuses/#{activity.id}/emoji_reactions_by") |> get("/api/v1/pleroma/statuses/#{activity.id}/emoji_reactions_by")
|> json_response(200) |> json_response(200)
assert result == %{} assert result == []
{:ok, _, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅") {:ok, _, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅")
@ -71,7 +71,7 @@ test "GET /api/v1/pleroma/statuses/:id/emoji_reactions_by", %{conn: conn} do
|> get("/api/v1/pleroma/statuses/#{activity.id}/emoji_reactions_by") |> get("/api/v1/pleroma/statuses/#{activity.id}/emoji_reactions_by")
|> json_response(200) |> json_response(200)
[represented_user] = result["🎅"] [["🎅", [represented_user]]] = result
assert represented_user["id"] == other_user.id assert represented_user["id"] == other_user.id
end end