Merge branch 'fixes' into 'main'

Various fixes

Closes #997

See merge request framasoft/mobilizon!1199
This commit is contained in:
Thomas Citharel 2022-04-04 14:27:16 +00:00
commit 0fac23cc4b
13 changed files with 190 additions and 72 deletions

View File

@ -212,7 +212,8 @@ config :mobilizon, :activitypub,
# One day
actor_stale_period: 3_600 * 48,
actor_key_rotation_delay: 3_600 * 48,
sign_object_fetches: true
sign_object_fetches: true,
stale_actor_search_exclusion_after: 3_600 * 24 * 7
config :mobilizon, Mobilizon.Service.Geospatial, service: Mobilizon.Service.Geospatial.Nominatim

View File

@ -5,14 +5,33 @@
:placeholder="$t('Filter by profile or group name')"
v-model="actorFilter"
/>
<b-radio-button
v-model="selectedActor"
:native-value="availableActor"
class="list-item"
<transition-group
tag="ul"
class="grid grid-cols-1 gap-y-3 m-5 max-w-md mx-auto"
enter-active-class="duration-300 ease-out"
enter-from-class="transform opacity-0"
enter-to-class="opacity-100"
leave-active-class="duration-200 ease-in"
leave-from-class="opacity-100"
leave-to-class="transform opacity-0"
>
<li
class="relative focus-within:shadow-lg"
v-for="availableActor in actualFilteredAvailableActors"
:key="availableActor.id"
>
<div class="media" dir="auto">
<input
class="sr-only peer"
type="radio"
:value="availableActor"
name="availableActors"
v-model="selectedActor"
:id="`availableActor-${availableActor.id}`"
/>
<label
class="flex flex-wrap p-3 bg-white border border-gray-300 rounded-lg cursor-pointer hover:bg-gray-50 peer-checked:ring-primary peer-checked:ring-2 peer-checked:border-transparent"
:for="`availableActor-${availableActor.id}`"
>
<figure class="image is-48x48" v-if="availableActor.avatar">
<img
class="image is-rounded"
@ -20,18 +39,14 @@
alt=""
/>
</figure>
<b-icon
class="media-left"
v-else
size="is-large"
icon="account-circle"
/>
<div class="media-content">
<b-icon v-else size="is-large" icon="account-circle" />
<div>
<h3>{{ availableActor.name }}</h3>
<small>{{ `@${availableActor.preferredUsername}` }}</small>
</div>
</div>
</b-radio-button>
</label>
</li>
</transition-group>
</div>
</template>
<script lang="ts">

View File

@ -1,5 +1,8 @@
<template>
<div class="organizer-picker" v-if="selectedActor">
<div
class="bg-white border border-gray-300 rounded-lg cursor-pointer"
v-if="selectedActor"
>
<!-- If we have a current actor (inline) -->
<div
v-if="inline && selectedActor.id"
@ -69,7 +72,8 @@
<p>{{ $t("Add a contact") }}</p>
<b-input
:placeholder="$t('Filter by name')"
v-model="contactFilter"
:value="contactFilter"
@input="debounceSetFilterByName"
dir="auto"
/>
<div v-if="actorMembers.length > 0">
@ -144,11 +148,12 @@ import EmptyContent from "../Utils/EmptyContent.vue";
import {
CURRENT_ACTOR_CLIENT,
IDENTITIES,
LOGGED_USER_MEMBERSHIPS,
PERSON_GROUP_MEMBERSHIPS,
} from "../../graphql/actor";
import { Paginate } from "../../types/paginate";
import { GROUP_MEMBERS } from "@/graphql/member";
import { ActorType, MemberRole } from "@/types/enums";
import debounce from "lodash/debounce";
const MEMBER_ROLES = [
MemberRole.CREATOR,
@ -179,15 +184,17 @@ const MEMBER_ROLES = [
},
},
currentActor: CURRENT_ACTOR_CLIENT,
userMemberships: {
query: LOGGED_USER_MEMBERSHIPS,
personMemberships: {
query: PERSON_GROUP_MEMBERSHIPS,
variables() {
return {
id: this.currentActor?.id,
page: 1,
limit: 10,
groupId: this.$route.query?.actorId,
};
},
update: (data) => data.loggedUser.memberships,
update: (data) => data.person.memberships,
},
identities: IDENTITIES,
},
@ -197,6 +204,9 @@ export default class OrganizerPickerWrapper extends Vue {
@Prop({ default: true, type: Boolean }) inline!: boolean;
@Prop({ type: Array, required: false, default: () => [] })
contacts!: IActor[];
currentActor!: IPerson;
identities!: IPerson[];
@ -207,13 +217,17 @@ export default class OrganizerPickerWrapper extends Vue {
usernameWithDomain = usernameWithDomain;
@Prop({ type: Array, required: false, default: () => [] })
contacts!: IActor[];
members: Paginate<IMember> = { elements: [], total: 0 };
membersPage = 1;
userMemberships: Paginate<IMember> = { elements: [], total: 0 };
personMemberships: Paginate<IMember> = { elements: [], total: 0 };
data(): Record<string, unknown> {
return {
debounceSetFilterByName: debounce(this.setContactFilter, 1000),
};
}
get actualContacts(): (string | undefined)[] {
return this.contacts.map(({ id }) => id);
@ -226,15 +240,17 @@ export default class OrganizerPickerWrapper extends Vue {
);
}
@Watch("userMemberships")
setContactFilter(contactFilter: string) {
this.contactFilter = contactFilter;
}
@Watch("personMemberships")
setInitialActor(): void {
if (this.$route.query?.actorId) {
const actorId = this.$route.query?.actorId as string;
const actor = this.userMemberships.elements.find(
({ parent: { id }, role }) =>
actorId === id && MEMBER_ROLES.includes(role)
)?.parent as IActor;
this.selectedActor = actor;
if (
this.personMemberships?.elements[0]?.parent?.id ===
this.$route.query?.actorId
) {
this.selectedActor = this.personMemberships?.elements[0]?.parent;
}
}
@ -276,7 +292,7 @@ export default class OrganizerPickerWrapper extends Vue {
actor.preferredUsername.toLowerCase(),
actor.name?.toLowerCase(),
actor.domain?.toLowerCase(),
].some((match) => match?.includes(this.contactFilter.toLowerCase()));
];
});
}

View File

@ -351,6 +351,30 @@ export const PERSON_STATUS_GROUP = gql`
${ACTOR_FRAGMENT}
`;
export const PERSON_GROUP_MEMBERSHIPS = gql`
query PersonGroupMemberships($id: ID!, $groupId: ID!) {
person(id: $id) {
id
memberships(groupId: $groupId) {
total
elements {
id
role
parent {
...ActorFragment
}
invitedBy {
...ActorFragment
}
insertedAt
updatedAt
}
}
}
}
${ACTOR_FRAGMENT}
`;
export const GROUP_MEMBERSHIP_SUBSCRIPTION_CHANGED = gql`
subscription GroupMembershipSubscriptionChanged(
$actorId: ID!

View File

@ -226,6 +226,15 @@ export const FETCH_GROUP = gql`
${RESOURCE_METADATA_BASIC_FIELDS_FRAGMENT}
`;
export const FETCH_GROUP_BY_ID = gql`
query FetchGroupById($id: ID!) {
groupById(id: $name) {
...GroupFullFields
}
}
${GROUP_FIELDS_FRAGMENTS}
`;
export const GET_GROUP = gql`
query GetGroup(
$id: ID!

View File

@ -95,7 +95,7 @@
>
<b-button
size="is-small"
v-if="!user.confirmedAt || !user.disabled"
v-if="!user.confirmedAt || user.disabled"
@click="isConfirmationModalActive = true"
type="is-text"
icon-left="check"

View File

@ -49,7 +49,8 @@ defmodule Mobilizon.GraphQL.API.Search do
location: Map.get(args, :location),
minimum_visibility: Map.get(args, :minimum_visibility, :public),
current_actor_id: Map.get(args, :current_actor_id),
exclude_my_groups: Map.get(args, :exclude_my_groups, false)
exclude_my_groups: Map.get(args, :exclude_my_groups, false),
exclude_stale_actors: true
],
page,
limit

View File

@ -16,15 +16,15 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
require Logger
@doc """
Find a group
"""
@spec find_group(
any,
%{:preferred_username => binary, optional(any) => any},
Absinthe.Resolution.t()
) ::
{:error, :group_not_found} | {:ok, Actor.t()}
@doc """
Find a group
"""
def find_group(
parent,
%{preferred_username: name} = args,
@ -45,7 +45,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
{:ok, %Actor{}} ->
{:error, :group_not_found}
{:error, _err} ->
{:error, err} ->
Logger.debug("Unable to find group, #{inspect(err)}")
{:error, :group_not_found}
end
end
@ -59,11 +60,30 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
{:ok, %Actor{}} ->
{:error, :group_not_found}
{:error, _err} ->
{:error, err} ->
Logger.debug("Unable to find group, #{inspect(err)}")
{:error, :group_not_found}
end
end
def find_group_by_id(_parent, %{id: id} = args, %{
context: %{
current_actor: %Actor{id: actor_id}
}
}) do
with %Actor{suspended: false, id: group_id} = group <- Actors.get_actor_with_preload(id),
true <- Actors.is_member?(actor_id, group_id) do
{:ok, group}
else
_ ->
{:error, :group_not_found}
end
end
def find_group_by_id(_parent, _args, _resolution) do
{:error, :group_not_found}
end
@doc """
Get a group
"""

View File

@ -358,11 +358,11 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
Returns this person's group memberships
"""
@spec person_memberships(Actor.t(), map(), map()) :: {:ok, Page.t()} | {:error, String.t()}
def person_memberships(%Actor{id: actor_id} = person, %{group: group}, %{
def person_memberships(%Actor{id: actor_id} = person, args, %{
context: %{current_user: %User{} = user}
}) do
if user_can_access_person_details?(person, user) do
with {:group, %Actor{id: group_id}} <- {:group, Actors.get_actor_by_name(group, :Group)},
with {:group, %Actor{id: group_id}} <- {:group, group_from_args(args)},
{:ok, %Member{} = membership} <- Actors.get_member(actor_id, group_id) do
{:ok,
%Page{
@ -373,6 +373,21 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
{:error, :member_not_found} ->
{:ok, %Page{total: 0, elements: []}}
{:group, :none} ->
with {:can_get_memberships, true} <-
{:can_get_memberships, user_can_access_person_details?(person, user)},
memberships <-
Actors.list_members_for_actor(
person,
Map.get(args, :page, 1),
Map.get(args, :limit, 10)
) do
{:ok, memberships}
else
{:can_get_memberships, _} ->
{:error, dgettext("errors", "Profile is not owned by authenticated user")}
end
{:group, nil} ->
{:error, :group_not_found}
end
@ -381,23 +396,6 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
end
end
def person_memberships(
%Actor{} = person,
%{page: page, limit: limit},
%{
context: %{current_user: %User{} = user}
}
) do
with {:can_get_memberships, true} <-
{:can_get_memberships, user_can_access_person_details?(person, user)},
memberships <- Actors.list_members_for_actor(person, page, limit) do
{:ok, memberships}
else
{:can_get_memberships, _} ->
{:error, dgettext("errors", "Profile is not owned by authenticated user")}
end
end
@doc """
Returns this person's group follows
"""
@ -498,4 +496,17 @@ defmodule Mobilizon.GraphQL.Resolvers.Person do
do: actor_user_id == user_id
defp user_can_access_person_details?(_, _), do: false
@spec group_from_args(map()) :: Actor.t() | nil
defp group_from_args(%{group: group}) do
Actors.get_actor_by_name(group, :Group)
end
defp group_from_args(%{group_id: group_id}) do
Actors.get_actor(group_id)
end
defp group_from_args(_) do
:none
end
end

View File

@ -244,6 +244,13 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do
resolve(&Group.find_group/3)
end
@desc "Get a group by its preferred username"
field :group_by_id, :group do
arg(:id, non_null(:id), description: "The group local ID")
resolve(&Group.find_group_by_id/3)
end
end
object :group_mutations do

View File

@ -88,11 +88,12 @@ defmodule Mobilizon.GraphQL.Schema.Actors.PersonType do
resolve(&Person.person_participations/3)
end
@desc "The list of group this person is member of"
@desc "The list of groups this person is member of"
field(:memberships, :paginated_member_list,
description: "The list of group this person is member of"
) do
arg(:group, :string, description: "Filter by group federated username")
arg(:group_id, :id, description: "Filter by group ID")
arg(:page, :integer,
default_value: 1,

View File

@ -461,6 +461,7 @@ defmodule Mobilizon.Actors do
) do
term
|> build_actors_by_username_or_name_page_query(options)
|> maybe_exclude_stale_actors(Keyword.get(options, :exclude_stale_actors, false))
|> maybe_exclude_my_groups(
Keyword.get(options, :exclude_my_groups, false),
Keyword.get(options, :current_actor_id)
@ -477,6 +478,17 @@ defmodule Mobilizon.Actors do
defp maybe_exclude_my_groups(query, _, _), do: query
@spec maybe_exclude_stale_actors(Ecto.Queryable.t(), boolean()) :: Ecto.Query.t()
defp maybe_exclude_stale_actors(query, true) do
actor_stale_period =
Application.get_env(:mobilizon, :activitypub)[:stale_actor_search_exclusion_after]
stale_date = DateTime.utc_now() |> DateTime.add(-actor_stale_period)
where(query, [a], is_nil(a.domain) or a.last_refreshed_at >= ^stale_date)
end
defp maybe_exclude_stale_actors(query, false), do: query
@spec build_actors_by_username_or_name_page_query(
String.t(),
Keyword.t()

View File

@ -53,7 +53,8 @@ defmodule Mobilizon.GraphQL.API.SearchTest do
location: nil,
minimum_visibility: :public,
current_actor_id: nil,
exclude_my_groups: false
exclude_my_groups: false,
exclude_stale_actors: true
],
1,
10