diff --git a/js/src/graphql/group.ts b/js/src/graphql/group.ts index 6b0baf8a0..5f5b27584 100644 --- a/js/src/graphql/group.ts +++ b/js/src/graphql/group.ts @@ -326,6 +326,7 @@ export const GROUP_TIMELINE = gql` query GroupTimeline( $preferredUsername: String! $type: ActivityType + $author: ActivityAuthor $page: Int $limit: Int ) { @@ -334,7 +335,7 @@ export const GROUP_TIMELINE = gql` preferredUsername domain name - activity(type: $type, page: $page, limit: $limit) { + activity(type: $type, author: $author, page: $page, limit: $limit) { total elements { id diff --git a/js/src/i18n/en_US.json b/js/src/i18n/en_US.json index 8a58bda0e..fda891bd5 100644 --- a/js/src/i18n/en_US.json +++ b/js/src/i18n/en_US.json @@ -960,5 +960,8 @@ "@{username}'s follow request was accepted": "@{username}'s follow request was accepted", "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?", - "Delete discussion": "Delete discussion" + "Delete discussion": "Delete discussion", + "All activities": "All activities", + "From yourself": "From yourself", + "By others": "By others" } diff --git a/js/src/i18n/fr_FR.json b/js/src/i18n/fr_FR.json index d52a037c8..4c5777190 100644 --- a/js/src/i18n/fr_FR.json +++ b/js/src/i18n/fr_FR.json @@ -1054,5 +1054,8 @@ "@{username}'s follow request was accepted": "@{username}'s follow request was accepted", "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 ?", - "Delete discussion": "Supprimer la discussion" + "Delete discussion": "Supprimer la discussion", + "All activities": "Toutes les activités", + "From yourself": "De vous", + "By others": "Des autres" } diff --git a/js/src/views/Group/Timeline.vue b/js/src/views/Group/Timeline.vue index bab7d7b80..ff89edd43 100644 --- a/js/src/views/Group/Timeline.vue +++ b/js/src/views/Group/Timeline.vue @@ -23,6 +23,74 @@ </ul> </nav> <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"> <div class="day" @@ -100,12 +168,20 @@ import { IActivity } from "../../types/activity.model"; import Observer from "../../components/Utils/Observer.vue"; import SkeletonActivityItem from "../../components/Activity/SkeletonActivityItem.vue"; import RouteName from "../../router/name"; +import { Location } from "vue-router"; const PAGINATION_LIMIT = 25; const SKELETON_DAY_ITEMS = 2; const SKELETON_ITEMS_PER_DAY = 5; type IActivitySkeleton = IActivity | { skeleton: string }; +enum ActivityAuthorFilter { + SELF = "SELF", + BY = "BY", +} + +export type ActivityFilter = ActivityType | ActivityAuthorFilter | null; + @Component({ apollo: { group: { @@ -116,6 +192,8 @@ type IActivitySkeleton = IActivity | { skeleton: string }; preferredUsername: this.preferredUsername, page: 1, limit: PAGINATION_LIMIT, + type: this.activityType, + author: this.activityAuthor, }; }, }, @@ -157,6 +235,44 @@ export default class Timeline extends Vue { 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> { if (this.group) { return this.group.activity; diff --git a/lib/graphql/resolvers/activity.ex b/lib/graphql/resolvers/activity.ex index e76de34f7..9b7f589a6 100644 --- a/lib/graphql/resolvers/activity.ex +++ b/lib/graphql/resolvers/activity.ex @@ -13,13 +13,19 @@ defmodule Mobilizon.GraphQL.Resolvers.Activity do 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} }) do 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 %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 = Enum.map(elements, fn %Activity{} = activity -> diff --git a/lib/graphql/schema/activity.ex b/lib/graphql/schema/activity.ex index 9a03b3f79..7901c87d5 100644 --- a/lib/graphql/schema/activity.ex +++ b/lib/graphql/schema/activity.ex @@ -19,6 +19,11 @@ defmodule Mobilizon.GraphQL.Schema.ActivityType do value(:member, description: "Activities concerning members") 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 field(:key, :string) field(:value, :string) diff --git a/lib/graphql/schema/actors/group.ex b/lib/graphql/schema/actors/group.ex index 83c2f9b6d..59d4690f1 100644 --- a/lib/graphql/schema/actors/group.ex +++ b/lib/graphql/schema/actors/group.ex @@ -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(:type, :activity_type, description: "Filter by type of activity") + arg(:author, :activity_author, description: "Filter by activity author") resolve(&Activity.group_activity/3) description("The group activity") end diff --git a/lib/mobilizon/activities/activities.ex b/lib/mobilizon/activities/activities.ex index bb263c798..8c04a4e66 100644 --- a/lib/mobilizon/activities/activities.ex +++ b/lib/mobilizon/activities/activities.ex @@ -93,6 +93,7 @@ defmodule Mobilizon.Activities do ) |> where([a, m], a.inserted_at >= m.member_since) |> filter_object_type(Keyword.get(filters, :type)) + |> filter_author(Keyword.get(filters, :author), actor_asking_id) |> order_by(desc: :inserted_at) |> preload([:author, :group]) |> Page.build_page(page, limit) @@ -158,12 +159,21 @@ defmodule Mobilizon.Activities do def activity_types, do: @activity_types - @spec filter_object_type(Query.t(), atom()) :: Query.t() - defp filter_object_type(query, :type) do - where(query, [q], q.type == ^:type) + @spec filter_object_type(Query.t(), atom() | nil) :: Query.t() + defp filter_object_type(query, nil), do: query + + defp filter_object_type(query, type) do + where(query, [q], q.type == ^type) end - defp filter_object_type(query, _) do - query + @spec filter_author(Query.t(), atom() | nil, integer() | String.t()) :: Query.t() + 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 diff --git a/test/graphql/resolvers/activity_test.exs b/test/graphql/resolvers/activity_test.exs index b3d149fea..437c0e55d 100644 --- a/test/graphql/resolvers/activity_test.exs +++ b/test/graphql/resolvers/activity_test.exs @@ -21,6 +21,7 @@ defmodule Mobilizon.GraphQL.Resolvers.ActivityTest do query GroupTimeline( $preferredUsername: String! $type: ActivityType + $author: ActivityAuthor $page: Int $limit: Int ) { @@ -29,7 +30,7 @@ defmodule Mobilizon.GraphQL.Resolvers.ActivityTest do preferredUsername domain name - activity(type: $type, page: $page, limit: $limit) { + activity(type: $type, author: $author, page: $page, limit: $limit) { total elements { id @@ -210,6 +211,92 @@ defmodule Mobilizon.GraphQL.Resolvers.ActivityTest do event.uuid 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", %{ conn: conn, group: %Actor{preferred_username: preferred_username, id: group_id} = group