Introduce activity filters
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
parent
aa2c79d312
commit
d8e4d6c24f
|
@ -326,6 +326,7 @@ export const GROUP_TIMELINE = gql`
|
||||||
query GroupTimeline(
|
query GroupTimeline(
|
||||||
$preferredUsername: String!
|
$preferredUsername: String!
|
||||||
$type: ActivityType
|
$type: ActivityType
|
||||||
|
$author: ActivityAuthor
|
||||||
$page: Int
|
$page: Int
|
||||||
$limit: Int
|
$limit: Int
|
||||||
) {
|
) {
|
||||||
|
@ -334,7 +335,7 @@ export const GROUP_TIMELINE = gql`
|
||||||
preferredUsername
|
preferredUsername
|
||||||
domain
|
domain
|
||||||
name
|
name
|
||||||
activity(type: $type, page: $page, limit: $limit) {
|
activity(type: $type, author: $author, page: $page, limit: $limit) {
|
||||||
total
|
total
|
||||||
elements {
|
elements {
|
||||||
id
|
id
|
||||||
|
|
|
@ -960,5 +960,8 @@
|
||||||
"@{username}'s follow request was accepted": "@{username}'s follow request was accepted",
|
"@{username}'s follow request was accepted": "@{username}'s follow request was accepted",
|
||||||
"Delete this discussion": "Delete this discussion",
|
"Delete this discussion": "Delete this discussion",
|
||||||
"Are you sure you want to delete this entire discussion?": "Are you sure you want to delete this entire discussion?",
|
"Are you sure you want to delete this entire discussion?": "Are you sure you want to delete this entire discussion?",
|
||||||
"Delete discussion": "Delete discussion"
|
"Delete discussion": "Delete discussion",
|
||||||
|
"All activities": "All activities",
|
||||||
|
"From yourself": "From yourself",
|
||||||
|
"By others": "By others"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1054,5 +1054,8 @@
|
||||||
"@{username}'s follow request was accepted": "@{username}'s follow request was accepted",
|
"@{username}'s follow request was accepted": "@{username}'s follow request was accepted",
|
||||||
"Delete this discussion": "Supprimer cette discussion",
|
"Delete this discussion": "Supprimer cette discussion",
|
||||||
"Are you sure you want to delete this entire discussion?": "Êtes-vous certain⋅e de vouloir supprimer l'entièreté de cette discussion ?",
|
"Are you sure you want to delete this entire discussion?": "Êtes-vous certain⋅e de vouloir supprimer l'entièreté de cette discussion ?",
|
||||||
"Delete discussion": "Supprimer la discussion"
|
"Delete discussion": "Supprimer la discussion",
|
||||||
|
"All activities": "Toutes les activités",
|
||||||
|
"From yourself": "De vous",
|
||||||
|
"By others": "Des autres"
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,74 @@
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
<section class="timeline">
|
<section class="timeline">
|
||||||
|
<b-field>
|
||||||
|
<b-radio-button v-model="activityType" :native-value="undefined">
|
||||||
|
<b-icon icon="timeline-text"></b-icon>
|
||||||
|
{{ $t("All activities") }}</b-radio-button
|
||||||
|
>
|
||||||
|
<b-radio-button
|
||||||
|
v-model="activityType"
|
||||||
|
:native-value="ActivityType.MEMBER"
|
||||||
|
>
|
||||||
|
<b-icon icon="account-multiple-plus"></b-icon>
|
||||||
|
{{ $t("Members") }}</b-radio-button
|
||||||
|
>
|
||||||
|
<b-radio-button
|
||||||
|
v-model="activityType"
|
||||||
|
:native-value="ActivityType.GROUP"
|
||||||
|
>
|
||||||
|
<b-icon icon="cog"></b-icon>
|
||||||
|
{{ $t("Settings") }}</b-radio-button
|
||||||
|
>
|
||||||
|
<b-radio-button
|
||||||
|
v-model="activityType"
|
||||||
|
:native-value="ActivityType.EVENT"
|
||||||
|
>
|
||||||
|
<b-icon icon="calendar"></b-icon>
|
||||||
|
{{ $t("Events") }}</b-radio-button
|
||||||
|
>
|
||||||
|
<b-radio-button
|
||||||
|
v-model="activityType"
|
||||||
|
:native-value="ActivityType.POST"
|
||||||
|
>
|
||||||
|
<b-icon icon="bullhorn"></b-icon>
|
||||||
|
{{ $t("Posts") }}</b-radio-button
|
||||||
|
>
|
||||||
|
<b-radio-button
|
||||||
|
v-model="activityType"
|
||||||
|
:native-value="ActivityType.DISCUSSION"
|
||||||
|
>
|
||||||
|
<b-icon icon="chat"></b-icon>
|
||||||
|
{{ $t("Discussions") }}</b-radio-button
|
||||||
|
>
|
||||||
|
<b-radio-button
|
||||||
|
v-model="activityType"
|
||||||
|
:native-value="ActivityType.RESOURCE"
|
||||||
|
>
|
||||||
|
<b-icon icon="link"></b-icon>
|
||||||
|
{{ $t("Resources") }}</b-radio-button
|
||||||
|
>
|
||||||
|
</b-field>
|
||||||
|
<b-field>
|
||||||
|
<b-radio-button v-model="activityAuthor" :native-value="undefined">
|
||||||
|
<b-icon icon="timeline-text"></b-icon>
|
||||||
|
{{ $t("All activities") }}</b-radio-button
|
||||||
|
>
|
||||||
|
<b-radio-button
|
||||||
|
v-model="activityAuthor"
|
||||||
|
:native-value="ActivityAuthorFilter.SELF"
|
||||||
|
>
|
||||||
|
<b-icon icon="account"></b-icon>
|
||||||
|
{{ $t("From yourself") }}</b-radio-button
|
||||||
|
>
|
||||||
|
<b-radio-button
|
||||||
|
v-model="activityAuthor"
|
||||||
|
:native-value="ActivityAuthorFilter.BY"
|
||||||
|
>
|
||||||
|
<b-icon icon="account-multiple"></b-icon>
|
||||||
|
{{ $t("By others") }}</b-radio-button
|
||||||
|
>
|
||||||
|
</b-field>
|
||||||
<transition-group name="timeline-list" tag="div">
|
<transition-group name="timeline-list" tag="div">
|
||||||
<div
|
<div
|
||||||
class="day"
|
class="day"
|
||||||
|
@ -100,12 +168,20 @@ import { IActivity } from "../../types/activity.model";
|
||||||
import Observer from "../../components/Utils/Observer.vue";
|
import Observer from "../../components/Utils/Observer.vue";
|
||||||
import SkeletonActivityItem from "../../components/Activity/SkeletonActivityItem.vue";
|
import SkeletonActivityItem from "../../components/Activity/SkeletonActivityItem.vue";
|
||||||
import RouteName from "../../router/name";
|
import RouteName from "../../router/name";
|
||||||
|
import { Location } from "vue-router";
|
||||||
|
|
||||||
const PAGINATION_LIMIT = 25;
|
const PAGINATION_LIMIT = 25;
|
||||||
const SKELETON_DAY_ITEMS = 2;
|
const SKELETON_DAY_ITEMS = 2;
|
||||||
const SKELETON_ITEMS_PER_DAY = 5;
|
const SKELETON_ITEMS_PER_DAY = 5;
|
||||||
type IActivitySkeleton = IActivity | { skeleton: string };
|
type IActivitySkeleton = IActivity | { skeleton: string };
|
||||||
|
|
||||||
|
enum ActivityAuthorFilter {
|
||||||
|
SELF = "SELF",
|
||||||
|
BY = "BY",
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ActivityFilter = ActivityType | ActivityAuthorFilter | null;
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
apollo: {
|
apollo: {
|
||||||
group: {
|
group: {
|
||||||
|
@ -116,6 +192,8 @@ type IActivitySkeleton = IActivity | { skeleton: string };
|
||||||
preferredUsername: this.preferredUsername,
|
preferredUsername: this.preferredUsername,
|
||||||
page: 1,
|
page: 1,
|
||||||
limit: PAGINATION_LIMIT,
|
limit: PAGINATION_LIMIT,
|
||||||
|
type: this.activityType,
|
||||||
|
author: this.activityAuthor,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -157,6 +235,44 @@ export default class Timeline extends Vue {
|
||||||
|
|
||||||
usernameWithDomain = usernameWithDomain;
|
usernameWithDomain = usernameWithDomain;
|
||||||
|
|
||||||
|
ActivityType = ActivityType;
|
||||||
|
|
||||||
|
ActivityAuthorFilter = ActivityAuthorFilter;
|
||||||
|
|
||||||
|
get activityType(): ActivityType | undefined {
|
||||||
|
if (this.$route?.query?.type) {
|
||||||
|
return this.$route?.query?.type as ActivityType;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
set activityType(type: ActivityType | undefined) {
|
||||||
|
this.$router.push({
|
||||||
|
name: RouteName.TIMELINE,
|
||||||
|
params: {
|
||||||
|
preferredUsername: this.preferredUsername,
|
||||||
|
},
|
||||||
|
query: { ...this.$route.query, type },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get activityAuthor(): ActivityAuthorFilter | undefined {
|
||||||
|
if (this.$route?.query?.author) {
|
||||||
|
return this.$route?.query?.author as ActivityAuthorFilter;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
set activityAuthor(author: ActivityAuthorFilter | undefined) {
|
||||||
|
this.$router.push({
|
||||||
|
name: RouteName.TIMELINE,
|
||||||
|
params: {
|
||||||
|
preferredUsername: this.preferredUsername,
|
||||||
|
},
|
||||||
|
query: { ...this.$route.query, author },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
get activity(): Paginate<IActivitySkeleton> {
|
get activity(): Paginate<IActivitySkeleton> {
|
||||||
if (this.group) {
|
if (this.group) {
|
||||||
return this.group.activity;
|
return this.group.activity;
|
||||||
|
|
|
@ -13,13 +13,19 @@ defmodule Mobilizon.GraphQL.Resolvers.Activity do
|
||||||
|
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
def group_activity(%Actor{type: :Group, id: group_id}, %{page: page, limit: limit}, %{
|
def group_activity(%Actor{type: :Group, id: group_id}, %{page: page, limit: limit} = args, %{
|
||||||
context: %{current_user: %User{role: role} = user}
|
context: %{current_user: %User{role: role} = user}
|
||||||
}) do
|
}) do
|
||||||
with {:actor, %Actor{id: actor_id} = _actor} <- {:actor, Users.get_actor_for_user(user)},
|
with {:actor, %Actor{id: actor_id} = _actor} <- {:actor, Users.get_actor_for_user(user)},
|
||||||
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id) or is_moderator(role)} do
|
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id) or is_moderator(role)} do
|
||||||
%Page{total: total, elements: elements} =
|
%Page{total: total, elements: elements} =
|
||||||
Activities.list_group_activities_for_member(group_id, actor_id, [], page, limit)
|
Activities.list_group_activities_for_member(
|
||||||
|
group_id,
|
||||||
|
actor_id,
|
||||||
|
[type: Map.get(args, :type), author: Map.get(args, :author)],
|
||||||
|
page,
|
||||||
|
limit
|
||||||
|
)
|
||||||
|
|
||||||
elements =
|
elements =
|
||||||
Enum.map(elements, fn %Activity{} = activity ->
|
Enum.map(elements, fn %Activity{} = activity ->
|
||||||
|
|
|
@ -19,6 +19,11 @@ defmodule Mobilizon.GraphQL.Schema.ActivityType do
|
||||||
value(:member, description: "Activities concerning members")
|
value(:member, description: "Activities concerning members")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
enum :activity_author do
|
||||||
|
value(:self, description: "Activities created by the current actor")
|
||||||
|
value(:by, description: "Activities created by others")
|
||||||
|
end
|
||||||
|
|
||||||
object :activity_param_item do
|
object :activity_param_item do
|
||||||
field(:key, :string)
|
field(:key, :string)
|
||||||
field(:value, :string)
|
field(:value, :string)
|
||||||
|
|
|
@ -153,6 +153,7 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do
|
||||||
arg(:limit, :integer, default_value: 10, description: "The limit of activity items per page")
|
arg(:limit, :integer, default_value: 10, description: "The limit of activity items per page")
|
||||||
|
|
||||||
arg(:type, :activity_type, description: "Filter by type of activity")
|
arg(:type, :activity_type, description: "Filter by type of activity")
|
||||||
|
arg(:author, :activity_author, description: "Filter by activity author")
|
||||||
resolve(&Activity.group_activity/3)
|
resolve(&Activity.group_activity/3)
|
||||||
description("The group activity")
|
description("The group activity")
|
||||||
end
|
end
|
||||||
|
|
|
@ -93,6 +93,7 @@ defmodule Mobilizon.Activities do
|
||||||
)
|
)
|
||||||
|> where([a, m], a.inserted_at >= m.member_since)
|
|> where([a, m], a.inserted_at >= m.member_since)
|
||||||
|> filter_object_type(Keyword.get(filters, :type))
|
|> filter_object_type(Keyword.get(filters, :type))
|
||||||
|
|> filter_author(Keyword.get(filters, :author), actor_asking_id)
|
||||||
|> order_by(desc: :inserted_at)
|
|> order_by(desc: :inserted_at)
|
||||||
|> preload([:author, :group])
|
|> preload([:author, :group])
|
||||||
|> Page.build_page(page, limit)
|
|> Page.build_page(page, limit)
|
||||||
|
@ -158,12 +159,21 @@ defmodule Mobilizon.Activities do
|
||||||
|
|
||||||
def activity_types, do: @activity_types
|
def activity_types, do: @activity_types
|
||||||
|
|
||||||
@spec filter_object_type(Query.t(), atom()) :: Query.t()
|
@spec filter_object_type(Query.t(), atom() | nil) :: Query.t()
|
||||||
defp filter_object_type(query, :type) do
|
defp filter_object_type(query, nil), do: query
|
||||||
where(query, [q], q.type == ^:type)
|
|
||||||
|
defp filter_object_type(query, type) do
|
||||||
|
where(query, [q], q.type == ^type)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp filter_object_type(query, _) do
|
@spec filter_author(Query.t(), atom() | nil, integer() | String.t()) :: Query.t()
|
||||||
query
|
defp filter_author(query, nil, _), do: query
|
||||||
|
|
||||||
|
defp filter_author(query, :self, actor_asking_id) do
|
||||||
|
where(query, [q], q.author_id == ^actor_asking_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp filter_author(query, :by, actor_asking_id) do
|
||||||
|
where(query, [q], q.author_id != ^actor_asking_id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -21,6 +21,7 @@ defmodule Mobilizon.GraphQL.Resolvers.ActivityTest do
|
||||||
query GroupTimeline(
|
query GroupTimeline(
|
||||||
$preferredUsername: String!
|
$preferredUsername: String!
|
||||||
$type: ActivityType
|
$type: ActivityType
|
||||||
|
$author: ActivityAuthor
|
||||||
$page: Int
|
$page: Int
|
||||||
$limit: Int
|
$limit: Int
|
||||||
) {
|
) {
|
||||||
|
@ -29,7 +30,7 @@ defmodule Mobilizon.GraphQL.Resolvers.ActivityTest do
|
||||||
preferredUsername
|
preferredUsername
|
||||||
domain
|
domain
|
||||||
name
|
name
|
||||||
activity(type: $type, page: $page, limit: $limit) {
|
activity(type: $type, author: $author, page: $page, limit: $limit) {
|
||||||
total
|
total
|
||||||
elements {
|
elements {
|
||||||
id
|
id
|
||||||
|
@ -210,6 +211,92 @@ defmodule Mobilizon.GraphQL.Resolvers.ActivityTest do
|
||||||
event.uuid
|
event.uuid
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "group_activity/3 list group activities filtered by type", %{
|
||||||
|
conn: conn,
|
||||||
|
group: %Actor{preferred_username: preferred_username, id: group_id} = group
|
||||||
|
} do
|
||||||
|
user = insert(:user)
|
||||||
|
actor = insert(:actor, user: user)
|
||||||
|
|
||||||
|
insert(:member,
|
||||||
|
parent: group,
|
||||||
|
actor: actor,
|
||||||
|
role: :member,
|
||||||
|
member_since: DateTime.truncate(DateTime.utc_now(), :second)
|
||||||
|
)
|
||||||
|
|
||||||
|
event = insert(:event, attributed_to: group, organizer_actor: actor)
|
||||||
|
post = insert(:post, author: actor, attributed_to: group)
|
||||||
|
EventActivity.insert_activity(event, subject: "event_created")
|
||||||
|
PostActivity.insert_activity(post, subject: "post_created")
|
||||||
|
assert %{success: 2, failure: 0} == Oban.drain_queue(queue: :activity)
|
||||||
|
assert Activities.list_activities() |> length() == 2
|
||||||
|
|
||||||
|
res =
|
||||||
|
conn
|
||||||
|
|> auth_conn(user)
|
||||||
|
|> AbsintheHelpers.graphql_query(
|
||||||
|
query: @group_activities_query,
|
||||||
|
variables: %{preferredUsername: preferred_username, type: "POST"}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert res["errors"] == nil
|
||||||
|
assert res["data"]["group"]["id"] == to_string(group_id)
|
||||||
|
assert res["data"]["group"]["activity"]["total"] == 1
|
||||||
|
activity = hd(res["data"]["group"]["activity"]["elements"])
|
||||||
|
assert activity["object"]["id"] == to_string(post.id)
|
||||||
|
assert activity["subject"] == "post_created"
|
||||||
|
|
||||||
|
assert Enum.find(activity["subjectParams"], &(&1["key"] == "post_title"))["value"] ==
|
||||||
|
post.title
|
||||||
|
|
||||||
|
assert Enum.find(activity["subjectParams"], &(&1["key"] == "post_slug"))["value"] ==
|
||||||
|
post.slug
|
||||||
|
end
|
||||||
|
|
||||||
|
test "group_activity/3 list group activities filtered by author", %{
|
||||||
|
conn: conn,
|
||||||
|
group: %Actor{preferred_username: preferred_username, id: group_id} = group
|
||||||
|
} do
|
||||||
|
user = insert(:user)
|
||||||
|
actor = insert(:actor, user: user)
|
||||||
|
|
||||||
|
insert(:member,
|
||||||
|
parent: group,
|
||||||
|
actor: actor,
|
||||||
|
role: :member,
|
||||||
|
member_since: DateTime.truncate(DateTime.utc_now(), :second)
|
||||||
|
)
|
||||||
|
|
||||||
|
event = insert(:event, attributed_to: group, organizer_actor: actor)
|
||||||
|
post = insert(:post, attributed_to: group)
|
||||||
|
EventActivity.insert_activity(event, subject: "event_created")
|
||||||
|
PostActivity.insert_activity(post, subject: "post_created")
|
||||||
|
assert %{success: 2, failure: 0} == Oban.drain_queue(queue: :activity)
|
||||||
|
assert Activities.list_activities() |> length() == 2
|
||||||
|
|
||||||
|
res =
|
||||||
|
conn
|
||||||
|
|> auth_conn(user)
|
||||||
|
|> AbsintheHelpers.graphql_query(
|
||||||
|
query: @group_activities_query,
|
||||||
|
variables: %{preferredUsername: preferred_username, author: "BY"}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert res["errors"] == nil
|
||||||
|
assert res["data"]["group"]["id"] == to_string(group_id)
|
||||||
|
assert res["data"]["group"]["activity"]["total"] == 1
|
||||||
|
activity = hd(res["data"]["group"]["activity"]["elements"])
|
||||||
|
assert activity["object"]["id"] == to_string(post.id)
|
||||||
|
assert activity["subject"] == "post_created"
|
||||||
|
|
||||||
|
assert Enum.find(activity["subjectParams"], &(&1["key"] == "post_title"))["value"] ==
|
||||||
|
post.title
|
||||||
|
|
||||||
|
assert Enum.find(activity["subjectParams"], &(&1["key"] == "post_slug"))["value"] ==
|
||||||
|
post.slug
|
||||||
|
end
|
||||||
|
|
||||||
test "group_activity/3 list group activities from deleted object", %{
|
test "group_activity/3 list group activities from deleted object", %{
|
||||||
conn: conn,
|
conn: conn,
|
||||||
group: %Actor{preferred_username: preferred_username, id: group_id} = group
|
group: %Actor{preferred_username: preferred_username, id: group_id} = group
|
||||||
|
|
Loading…
Reference in a new issue