forked from AkkomaGang/akkoma
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:
commit
34aa0c542b
9 changed files with 77 additions and 46 deletions
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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..."}]]
|
||||||
}
|
]
|
||||||
```
|
```
|
||||||
|
|
|
@ -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()) ::
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
%{
|
%{
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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: []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue