Merge branch 'fixes' into 'main'
Various fixes Closes #997 See merge request framasoft/mobilizon!1199
This commit is contained in:
commit
0fac23cc4b
@ -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
|
||||
|
||||
|
@ -5,33 +5,48 @@
|
||||
:placeholder="$t('Filter by profile or group name')"
|
||||
v-model="actorFilter"
|
||||
/>
|
||||
<b-radio-button
|
||||
v-model="selectedActor"
|
||||
:native-value="availableActor"
|
||||
class="list-item"
|
||||
v-for="availableActor in actualFilteredAvailableActors"
|
||||
:key="availableActor.id"
|
||||
<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"
|
||||
>
|
||||
<div class="media" dir="auto">
|
||||
<figure class="image is-48x48" v-if="availableActor.avatar">
|
||||
<img
|
||||
class="image is-rounded"
|
||||
:src="availableActor.avatar.url"
|
||||
alt=""
|
||||
/>
|
||||
</figure>
|
||||
<b-icon
|
||||
class="media-left"
|
||||
v-else
|
||||
size="is-large"
|
||||
icon="account-circle"
|
||||
<li
|
||||
class="relative focus-within:shadow-lg"
|
||||
v-for="availableActor in actualFilteredAvailableActors"
|
||||
:key="availableActor.id"
|
||||
>
|
||||
<input
|
||||
class="sr-only peer"
|
||||
type="radio"
|
||||
:value="availableActor"
|
||||
name="availableActors"
|
||||
v-model="selectedActor"
|
||||
:id="`availableActor-${availableActor.id}`"
|
||||
/>
|
||||
<div class="media-content">
|
||||
<h3>{{ availableActor.name }}</h3>
|
||||
<small>{{ `@${availableActor.preferredUsername}` }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</b-radio-button>
|
||||
<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"
|
||||
:src="availableActor.avatar.url"
|
||||
alt=""
|
||||
/>
|
||||
</figure>
|
||||
<b-icon v-else size="is-large" icon="account-circle" />
|
||||
<div>
|
||||
<h3>{{ availableActor.name }}</h3>
|
||||
<small>{{ `@${availableActor.preferredUsername}` }}</small>
|
||||
</div>
|
||||
</label>
|
||||
</li>
|
||||
</transition-group>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
|
@ -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()));
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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!
|
||||
|
@ -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!
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
"""
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user