@@ -27,7 +27,7 @@
|
||||
"@absinthe/socket": "^0.2.1",
|
||||
"@absinthe/socket-apollo-link": "^0.2.1",
|
||||
"@apollo/client": "^3.3.16",
|
||||
"@oruga-ui/oruga-next": "^0.6.0",
|
||||
"@oruga-ui/oruga-next": "^0.7.0",
|
||||
"@sentry/tracing": "^7.1",
|
||||
"@sentry/vue": "^7.1",
|
||||
"@tiptap/core": "^2.0.0-beta.41",
|
||||
@@ -114,7 +114,7 @@
|
||||
"@vitest/coverage-v8": "^0.34.1",
|
||||
"@vitest/ui": "^0.34.1",
|
||||
"@vue/eslint-config-prettier": "^8.0.0",
|
||||
"@vue/eslint-config-typescript": "^11.0.0",
|
||||
"@vue/eslint-config-typescript": "^12.0.0",
|
||||
"@vue/test-utils": "^2.0.2",
|
||||
"eslint": "^8.21.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
@@ -131,7 +131,7 @@
|
||||
"prettier-eslint": "^15.0.1",
|
||||
"rollup-plugin-visualizer": "^5.7.1",
|
||||
"sass": "^1.34.1",
|
||||
"typescript": "~5.1.3",
|
||||
"typescript": "~5.2.2",
|
||||
"vite": "^4.0.4",
|
||||
"vite-plugin-pwa": "^0.16.4",
|
||||
"vitest": "^0.34.1",
|
||||
|
||||
@@ -138,6 +138,7 @@ interval.value = window.setInterval(async () => {
|
||||
}, 60000) as unknown as number;
|
||||
|
||||
onBeforeMount(async () => {
|
||||
console.debug("Before mount App");
|
||||
if (initializeCurrentUser()) {
|
||||
try {
|
||||
await initializeCurrentActor();
|
||||
@@ -150,6 +151,8 @@ onBeforeMount(async () => {
|
||||
userAlreadyActivated: "true",
|
||||
},
|
||||
});
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -202,20 +205,24 @@ onUnmounted(() => {
|
||||
const { mutate: updateCurrentUser } = useMutation(UPDATE_CURRENT_USER_CLIENT);
|
||||
|
||||
const initializeCurrentUser = () => {
|
||||
console.debug("Initializing current user");
|
||||
const userId = localStorage.getItem(AUTH_USER_ID);
|
||||
const userEmail = localStorage.getItem(AUTH_USER_EMAIL);
|
||||
const accessToken = localStorage.getItem(AUTH_ACCESS_TOKEN);
|
||||
const role = localStorage.getItem(AUTH_USER_ROLE);
|
||||
|
||||
if (userId && userEmail && accessToken && role) {
|
||||
updateCurrentUser({
|
||||
const userData = {
|
||||
id: userId,
|
||||
email: userEmail,
|
||||
isLoggedIn: true,
|
||||
role,
|
||||
});
|
||||
};
|
||||
updateCurrentUser(userData);
|
||||
console.debug("Initialized current user", userData);
|
||||
return true;
|
||||
}
|
||||
console.debug("Failed to initialize current user");
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
@@ -45,6 +45,11 @@ export const typePolicies: TypePolicies = {
|
||||
comments: paginatedLimitPagination<IComment>(),
|
||||
},
|
||||
},
|
||||
Conversation: {
|
||||
fields: {
|
||||
comments: paginatedLimitPagination<IComment>(),
|
||||
},
|
||||
},
|
||||
Group: {
|
||||
fields: {
|
||||
organizedEvents: paginatedLimitPagination([
|
||||
|
||||
77
js/src/components/Account/ActorAutoComplete.vue
Normal file
77
js/src/components/Account/ActorAutoComplete.vue
Normal file
@@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<o-inputitems
|
||||
:modelValue="modelValue"
|
||||
@update:modelValue="(val: IActor[]) => $emit('update:modelValue', val)"
|
||||
:data="availableActors"
|
||||
:allow-autocomplete="true"
|
||||
:allow-new="false"
|
||||
:open-on-focus="false"
|
||||
field="displayName"
|
||||
placeholder="Add a recipient"
|
||||
@typing="getActors"
|
||||
>
|
||||
<template #default="props">
|
||||
<ActorInline :actor="props.option" />
|
||||
</template>
|
||||
</o-inputitems>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { SEARCH_PERSON_AND_GROUPS } from "@/graphql/search";
|
||||
import { IActor, IGroup, IPerson, displayName } from "@/types/actor";
|
||||
import { Paginate } from "@/types/paginate";
|
||||
import { useLazyQuery } from "@vue/apollo-composable";
|
||||
import { ref } from "vue";
|
||||
import ActorInline from "./ActorInline.vue";
|
||||
|
||||
defineProps<{
|
||||
modelValue: IActor[];
|
||||
}>();
|
||||
|
||||
defineEmits<{
|
||||
"update:modelValue": [value: IActor[]];
|
||||
}>();
|
||||
|
||||
const {
|
||||
load: loadSearchPersonsAndGroupsQuery,
|
||||
refetch: refetchSearchPersonsAndGroupsQuery,
|
||||
} = useLazyQuery<
|
||||
{ searchPersons: Paginate<IPerson>; searchGroups: Paginate<IGroup> },
|
||||
{ searchText: string }
|
||||
>(SEARCH_PERSON_AND_GROUPS);
|
||||
|
||||
const availableActors = ref<IActor[]>([]);
|
||||
|
||||
const getActors = async (text: string) => {
|
||||
availableActors.value = await fetchActors(text);
|
||||
};
|
||||
|
||||
const fetchActors = async (text: string): Promise<IActor[]> => {
|
||||
if (text === "") return [];
|
||||
try {
|
||||
const res =
|
||||
(await loadSearchPersonsAndGroupsQuery(SEARCH_PERSON_AND_GROUPS, {
|
||||
searchText: text,
|
||||
})) ||
|
||||
(
|
||||
await refetchSearchPersonsAndGroupsQuery({
|
||||
searchText: text,
|
||||
})
|
||||
)?.data;
|
||||
if (!res) return [];
|
||||
return [
|
||||
...res.searchPersons.elements.map((person) => ({
|
||||
...person,
|
||||
displayName: displayName(person),
|
||||
})),
|
||||
...res.searchGroups.elements.map((group) => ({
|
||||
...group,
|
||||
displayName: displayName(group),
|
||||
})),
|
||||
];
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -39,6 +39,9 @@
|
||||
v-html="actor.summary"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex pr-2">
|
||||
<Email />
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div
|
||||
class="p-4 bg-white rounded-lg shadow-md sm:p-8 flex items-center space-x-4"
|
||||
@@ -81,6 +84,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { displayName, IActor, usernameWithDomain } from "../../types/actor";
|
||||
import AccountCircle from "vue-material-design-icons/AccountCircle.vue";
|
||||
import Email from "vue-material-design-icons/Email.vue";
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
|
||||
@@ -25,7 +25,7 @@ import { IActor } from "@/types/actor";
|
||||
import { ActorType } from "@/types/enums";
|
||||
|
||||
const avatarUrl = ref<string>(
|
||||
"https://framapiaf.s3.framasoft.org/framapiaf/accounts/avatars/000/000/399/original/aa56a445efb72803.jpg"
|
||||
"https://stockage.framapiaf.org/framapiaf/accounts/avatars/000/000/399/original/52b08a3e80b43d40.jpg"
|
||||
);
|
||||
|
||||
const stateLocal = reactive<IActor>({
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div
|
||||
class="inline-flex items-start bg-white dark:bg-violet-1 dark:text-white p-2 rounded-md"
|
||||
class="inline-flex items-start gap-2 bg-white dark:bg-violet-1 dark:text-white p-2 rounded-md"
|
||||
>
|
||||
<div class="flex-none mr-2">
|
||||
<div class="flex-none">
|
||||
<figure v-if="actor.avatar">
|
||||
<img
|
||||
class="rounded-xl"
|
||||
@@ -24,11 +24,15 @@
|
||||
@{{ usernameWithDomain(actor) }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex pr-2 self-center">
|
||||
<Email />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { displayName, IActor, usernameWithDomain } from "../../types/actor";
|
||||
import AccountCircle from "vue-material-design-icons/AccountCircle.vue";
|
||||
import Email from "vue-material-design-icons/Email.vue";
|
||||
|
||||
defineProps<{
|
||||
actor: IActor;
|
||||
|
||||
@@ -49,7 +49,7 @@ const group = {
|
||||
domain: "mobilizon.fr",
|
||||
avatar: {
|
||||
...baseActorAvatar,
|
||||
url: "https://framapiaf.s3.framasoft.org/framapiaf/accounts/avatars/000/000/399/original/aa56a445efb72803.jpg",
|
||||
url: "https://stockage.framapiaf.org/framapiaf/accounts/avatars/000/000/399/original/52b08a3e80b43d40.jpg",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
160
js/src/components/Conversations/ConversationListItem.vue
Normal file
160
js/src/components/Conversations/ConversationListItem.vue
Normal file
@@ -0,0 +1,160 @@
|
||||
<template>
|
||||
<router-link
|
||||
class="flex gap-2 w-full items-center px-2 py-4 border-b-stone-200 border-b bg-white dark:bg-transparent"
|
||||
dir="auto"
|
||||
:to="{
|
||||
name: RouteName.CONVERSATION,
|
||||
params: { id: conversation.conversationParticipantId },
|
||||
}"
|
||||
>
|
||||
<div class="relative">
|
||||
<figure
|
||||
class="w-12 h-12"
|
||||
v-if="
|
||||
conversation.lastComment?.actor &&
|
||||
conversation.lastComment.actor.avatar
|
||||
"
|
||||
>
|
||||
<img
|
||||
class="rounded-full"
|
||||
:src="conversation.lastComment.actor.avatar.url"
|
||||
alt=""
|
||||
width="48"
|
||||
height="48"
|
||||
/>
|
||||
</figure>
|
||||
<account-circle :size="48" v-else />
|
||||
<div class="flex absolute -bottom-2 left-6">
|
||||
<template
|
||||
v-for="extraParticipant in nonLastCommenterParticipants.slice(0, 2)"
|
||||
:key="extraParticipant.id"
|
||||
>
|
||||
<figure class="w-6 h-6 -mr-3">
|
||||
<img
|
||||
v-if="extraParticipant && extraParticipant.avatar"
|
||||
class="rounded-full h-6"
|
||||
:src="extraParticipant.avatar.url"
|
||||
alt=""
|
||||
width="24"
|
||||
height="24"
|
||||
/>
|
||||
<account-circle :size="24" v-else />
|
||||
</figure>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overflow-hidden flex-1">
|
||||
<div class="flex items-center justify-between">
|
||||
<i18n-t
|
||||
keypath="With {participants}"
|
||||
tag="p"
|
||||
class="truncate flex-1"
|
||||
v-if="formattedListOfParticipants"
|
||||
>
|
||||
<template #participants>
|
||||
<span v-html="formattedListOfParticipants" />
|
||||
</template>
|
||||
</i18n-t>
|
||||
<p v-else>{{ t("With unknown participants") }}</p>
|
||||
<div class="inline-flex items-center px-1.5">
|
||||
<span
|
||||
v-if="conversation.unread"
|
||||
class="bg-primary rounded-full inline-block h-2.5 w-2.5 mx-2"
|
||||
>
|
||||
</span>
|
||||
<time
|
||||
class="whitespace-nowrap"
|
||||
:datetime="actualDate.toString()"
|
||||
:title="formatDateTimeString(actualDate)"
|
||||
>
|
||||
{{ distanceToNow }}</time
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="line-clamp-2 my-1"
|
||||
dir="auto"
|
||||
v-if="!conversation.lastComment?.deletedAt"
|
||||
>
|
||||
{{ htmlTextEllipsis }}
|
||||
</div>
|
||||
<div v-else class="">
|
||||
{{ t("[This comment has been deleted]") }}
|
||||
</div>
|
||||
</div>
|
||||
</router-link>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { formatDistanceToNowStrict } from "date-fns";
|
||||
import { IConversation } from "../../types/conversation";
|
||||
import RouteName from "../../router/name";
|
||||
import { computed, inject } from "vue";
|
||||
import { formatDateTimeString } from "../../filters/datetime";
|
||||
import type { Locale } from "date-fns";
|
||||
import AccountCircle from "vue-material-design-icons/AccountCircle.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { formatList } from "@/utils/i18n";
|
||||
import { displayName } from "@/types/actor";
|
||||
import { useCurrentActorClient } from "@/composition/apollo/actor";
|
||||
|
||||
const props = defineProps<{
|
||||
conversation: IConversation;
|
||||
}>();
|
||||
|
||||
const conversation = computed(() => props.conversation);
|
||||
|
||||
const dateFnsLocale = inject<Locale>("dateFnsLocale");
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
const distanceToNow = computed(() => {
|
||||
return (
|
||||
formatDistanceToNowStrict(new Date(actualDate.value), {
|
||||
locale: dateFnsLocale,
|
||||
}) ?? t("Right now")
|
||||
);
|
||||
});
|
||||
|
||||
const htmlTextEllipsis = computed((): string => {
|
||||
const element = document.createElement("div");
|
||||
if (conversation.value.lastComment && conversation.value.lastComment.text) {
|
||||
element.innerHTML = conversation.value.lastComment.text
|
||||
.replace(/<br\s*\/?>/gi, " ")
|
||||
.replace(/<p>/gi, " ");
|
||||
}
|
||||
return element.innerText;
|
||||
});
|
||||
|
||||
const actualDate = computed((): string => {
|
||||
if (
|
||||
conversation.value.updatedAt === conversation.value.insertedAt &&
|
||||
conversation.value.lastComment?.publishedAt
|
||||
) {
|
||||
return conversation.value.lastComment.publishedAt;
|
||||
}
|
||||
return conversation.value.updatedAt;
|
||||
});
|
||||
|
||||
const formattedListOfParticipants = computed(() => {
|
||||
return formatList(
|
||||
otherParticipants.value.map(
|
||||
(participant) => `<b>${displayName(participant)}</b>`
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
const { currentActor } = useCurrentActorClient();
|
||||
|
||||
const otherParticipants = computed(
|
||||
() =>
|
||||
conversation.value?.participants.filter(
|
||||
(participant) => participant.id !== currentActor.value?.id
|
||||
) ?? []
|
||||
);
|
||||
|
||||
const nonLastCommenterParticipants = computed(() =>
|
||||
otherParticipants.value.filter(
|
||||
(participant) =>
|
||||
participant.id !== conversation.value.lastComment?.actor?.id
|
||||
)
|
||||
);
|
||||
</script>
|
||||
69
js/src/components/Conversations/EventConversations.vue
Normal file
69
js/src/components/Conversations/EventConversations.vue
Normal file
@@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<div class="container mx-auto section">
|
||||
<breadcrumbs-nav :links="[]" />
|
||||
<section>
|
||||
<h1>{{ t("Conversations") }}</h1>
|
||||
<!-- <o-button
|
||||
tag="router-link"
|
||||
:to="{
|
||||
name: RouteName.CREATE_CONVERSATION,
|
||||
params: { uuid: event.uuid },
|
||||
}"
|
||||
>{{ t("New private message") }}</o-button
|
||||
> -->
|
||||
<div v-if="conversations.elements.length > 0">
|
||||
<conversation-list-item
|
||||
:conversation="conversation"
|
||||
v-for="conversation in conversations.elements"
|
||||
:key="conversation.id"
|
||||
/>
|
||||
<o-pagination
|
||||
v-show="conversations.total > CONVERSATIONS_PER_PAGE"
|
||||
class="conversation-pagination"
|
||||
:total="conversations.total"
|
||||
v-model:current="page"
|
||||
:per-page="CONVERSATIONS_PER_PAGE"
|
||||
:aria-next-label="t('Next page')"
|
||||
:aria-previous-label="t('Previous page')"
|
||||
:aria-page-label="t('Page')"
|
||||
:aria-current-label="t('Current page')"
|
||||
>
|
||||
</o-pagination>
|
||||
</div>
|
||||
<empty-content v-else icon="chat">
|
||||
{{ t("There's no conversations yet") }}
|
||||
</empty-content>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import ConversationListItem from "../../components/Conversations/ConversationListItem.vue";
|
||||
// import RouteName from "../../router/name";
|
||||
import EmptyContent from "../../components/Utils/EmptyContent.vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useRouteQuery, integerTransformer } from "vue-use-route-query";
|
||||
import { computed } from "vue";
|
||||
import { IEvent } from "../../types/event.model";
|
||||
import { EVENT_CONVERSATIONS } from "../../graphql/event";
|
||||
import { useQuery } from "@vue/apollo-composable";
|
||||
|
||||
const page = useRouteQuery("page", 1, integerTransformer);
|
||||
const CONVERSATIONS_PER_PAGE = 10;
|
||||
|
||||
const props = defineProps<{ event: IEvent }>();
|
||||
const event = computed(() => props.event);
|
||||
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
const { result: conversationsResult } = useQuery<{
|
||||
event: Pick<IEvent, "conversations">;
|
||||
}>(EVENT_CONVERSATIONS, () => ({
|
||||
uuid: event.value.uuid,
|
||||
page: page.value,
|
||||
}));
|
||||
|
||||
const conversations = computed(
|
||||
() =>
|
||||
conversationsResult.value?.event.conversations || { elements: [], total: 0 }
|
||||
);
|
||||
</script>
|
||||
137
js/src/components/Conversations/NewConversation.vue
Normal file
137
js/src/components/Conversations/NewConversation.vue
Normal file
@@ -0,0 +1,137 @@
|
||||
<template>
|
||||
<form @submit="sendForm" class="flex flex-col">
|
||||
<ActorAutoComplete v-model="actorMentions" />
|
||||
<Editor
|
||||
v-model="text"
|
||||
mode="basic"
|
||||
:aria-label="t('Message body')"
|
||||
v-if="currentActor"
|
||||
:currentActor="currentActor"
|
||||
:placeholder="t('Write a new message')"
|
||||
/>
|
||||
<footer class="flex gap-2 py-3 mx-2 justify-end">
|
||||
<o-button :disabled="!canSend" nativeType="submit">{{
|
||||
t("Send")
|
||||
}}</o-button>
|
||||
</footer>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { IActor, IPerson, usernameWithDomain } from "@/types/actor";
|
||||
import { computed, defineAsyncComponent, provide, ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import ActorAutoComplete from "../../components/Account/ActorAutoComplete.vue";
|
||||
import {
|
||||
DefaultApolloClient,
|
||||
provideApolloClient,
|
||||
useMutation,
|
||||
} from "@vue/apollo-composable";
|
||||
import { apolloClient } from "@/vue-apollo";
|
||||
import { PROFILE_CONVERSATIONS } from "@/graphql/event";
|
||||
import { POST_PRIVATE_MESSAGE_MUTATION } from "@/graphql/conversations";
|
||||
import { IConversation } from "@/types/conversation";
|
||||
import { useCurrentActorClient } from "@/composition/apollo/actor";
|
||||
import { useRouter } from "vue-router";
|
||||
import RouteName from "@/router/name";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
mentions?: IActor[];
|
||||
}>(),
|
||||
{ mentions: () => [] }
|
||||
);
|
||||
|
||||
provide(DefaultApolloClient, apolloClient);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const emit = defineEmits(["close"]);
|
||||
|
||||
const actorMentions = ref(props.mentions);
|
||||
|
||||
const textMentions = computed(() =>
|
||||
(props.mentions ?? []).map((actor) => usernameWithDomain(actor)).join(" ")
|
||||
);
|
||||
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
const text = ref(textMentions.value);
|
||||
|
||||
const Editor = defineAsyncComponent(
|
||||
() => import("../../components/TextEditor.vue")
|
||||
);
|
||||
|
||||
const { currentActor } = provideApolloClient(apolloClient)(() => {
|
||||
return useCurrentActorClient();
|
||||
});
|
||||
|
||||
const canSend = computed(() => {
|
||||
return actorMentions.value.length > 0 || /@.+/.test(text.value);
|
||||
});
|
||||
|
||||
const { mutate: postPrivateMessageMutate } = provideApolloClient(apolloClient)(
|
||||
() =>
|
||||
useMutation<
|
||||
{
|
||||
postPrivateMessage: IConversation;
|
||||
},
|
||||
{
|
||||
text: string;
|
||||
actorId: string;
|
||||
language?: string;
|
||||
mentions?: string[];
|
||||
attributedToId?: string;
|
||||
}
|
||||
>(POST_PRIVATE_MESSAGE_MUTATION, {
|
||||
update(cache, result) {
|
||||
if (!result.data?.postPrivateMessage) return;
|
||||
const cachedData = cache.readQuery<{
|
||||
loggedPerson: Pick<IPerson, "conversations" | "id">;
|
||||
}>({
|
||||
query: PROFILE_CONVERSATIONS,
|
||||
variables: {
|
||||
page: 1,
|
||||
},
|
||||
});
|
||||
if (!cachedData) return;
|
||||
cache.writeQuery({
|
||||
query: PROFILE_CONVERSATIONS,
|
||||
variables: {
|
||||
page: 1,
|
||||
},
|
||||
data: {
|
||||
loggedPerson: {
|
||||
...cachedData?.loggedPerson,
|
||||
conversations: {
|
||||
...cachedData.loggedPerson.conversations,
|
||||
total: (cachedData.loggedPerson.conversations?.total ?? 0) + 1,
|
||||
elements: [
|
||||
...(cachedData.loggedPerson.conversations?.elements ?? []),
|
||||
result.data.postPrivateMessage,
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const sendForm = async (e: Event) => {
|
||||
e.preventDefault();
|
||||
console.debug("Sending new private message");
|
||||
if (!currentActor.value?.id) return;
|
||||
const result = await postPrivateMessageMutate({
|
||||
actorId: currentActor.value.id,
|
||||
text: text.value,
|
||||
mentions: actorMentions.value.map((actor) => usernameWithDomain(actor)),
|
||||
});
|
||||
if (!result?.data?.postPrivateMessage.conversationParticipantId) return;
|
||||
router.push({
|
||||
name: RouteName.CONVERSATION,
|
||||
params: { id: result?.data?.postPrivateMessage.conversationParticipantId },
|
||||
});
|
||||
emit("close");
|
||||
};
|
||||
</script>
|
||||
@@ -1,5 +1,7 @@
|
||||
<template>
|
||||
<article class="flex gap-2 bg-white dark:bg-transparent">
|
||||
<article
|
||||
class="flex gap-2 bg-white dark:bg-transparent border rounded-md p-2 mt-2"
|
||||
>
|
||||
<div class="">
|
||||
<figure class="" v-if="comment.actor && comment.actor.avatar">
|
||||
<img
|
||||
@@ -29,12 +31,12 @@
|
||||
v-if="
|
||||
comment.actor &&
|
||||
!comment.deletedAt &&
|
||||
comment.actor.id === currentActor?.id
|
||||
(comment.actor.id === currentActor.id || canReport)
|
||||
"
|
||||
>
|
||||
<o-dropdown aria-role="list" position="bottom-left">
|
||||
<template #trigger>
|
||||
<o-icon role="button" icon="dots-horizontal" />
|
||||
<DotsHorizontal class="cursor-pointer" />
|
||||
</template>
|
||||
|
||||
<o-dropdown-item
|
||||
@@ -53,10 +55,14 @@
|
||||
<o-icon icon="delete"></o-icon>
|
||||
{{ t("Delete") }}
|
||||
</o-dropdown-item>
|
||||
<!-- <o-dropdown-item aria-role="listitem" @click="isReportModalActive = true">
|
||||
<o-dropdown-item
|
||||
v-if="canReport"
|
||||
aria-role="listitem"
|
||||
@click="isReportModalActive = true"
|
||||
>
|
||||
<o-icon icon="flag" />
|
||||
{{ t("Report") }}
|
||||
</o-dropdown-item> -->
|
||||
</o-dropdown-item>
|
||||
</o-dropdown>
|
||||
</span>
|
||||
<div class="self-center">
|
||||
@@ -124,6 +130,20 @@
|
||||
</form>
|
||||
</div>
|
||||
</article>
|
||||
<o-modal
|
||||
v-model:active="isReportModalActive"
|
||||
has-modal-card
|
||||
ref="reportModal"
|
||||
:close-button-aria-label="t('Close')"
|
||||
:autoFocus="false"
|
||||
:trapFocus="false"
|
||||
>
|
||||
<ReportModal
|
||||
:on-confirm="reportComment"
|
||||
:title="t('Report this comment')"
|
||||
:outside-domain="comment.actor?.domain"
|
||||
/>
|
||||
</o-modal>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
@@ -132,17 +152,26 @@ import { IPerson, usernameWithDomain } from "../../types/actor";
|
||||
import { computed, defineAsyncComponent, inject, ref } from "vue";
|
||||
import { formatDateTimeString } from "@/filters/datetime";
|
||||
import AccountCircle from "vue-material-design-icons/AccountCircle.vue";
|
||||
import DotsHorizontal from "vue-material-design-icons/DotsHorizontal.vue";
|
||||
import type { Locale } from "date-fns";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useCreateReport } from "@/composition/apollo/report";
|
||||
import { Snackbar } from "@/plugins/snackbar";
|
||||
import { useProgrammatic } from "@oruga-ui/oruga-next";
|
||||
import ReportModal from "@/components/Report/ReportModal.vue";
|
||||
|
||||
const Editor = defineAsyncComponent(
|
||||
() => import("@/components/TextEditor.vue")
|
||||
);
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: IComment;
|
||||
currentActor: IPerson;
|
||||
}>();
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue: IComment;
|
||||
currentActor: IPerson;
|
||||
canReport: boolean;
|
||||
}>(),
|
||||
{ canReport: false }
|
||||
);
|
||||
|
||||
const emit = defineEmits(["update:modelValue", "deleteComment"]);
|
||||
|
||||
@@ -156,7 +185,7 @@ const updatedComment = ref("");
|
||||
|
||||
const dateFnsLocale = inject<Locale>("dateFnsLocale");
|
||||
|
||||
// isReportModalActive: boolean = false;
|
||||
const isReportModalActive = ref(false);
|
||||
|
||||
const toggleEditMode = (): void => {
|
||||
updatedComment.value = comment.value.text;
|
||||
@@ -170,6 +199,51 @@ const updateComment = (): void => {
|
||||
});
|
||||
toggleEditMode();
|
||||
};
|
||||
|
||||
const {
|
||||
mutate: createReportMutation,
|
||||
onError: onCreateReportError,
|
||||
onDone: oneCreateReportDone,
|
||||
} = useCreateReport();
|
||||
|
||||
const reportComment = async (
|
||||
content: string,
|
||||
forward: boolean
|
||||
): Promise<void> => {
|
||||
if (!props.modelValue.actor) return;
|
||||
createReportMutation({
|
||||
reportedId: props.modelValue.actor?.id ?? "",
|
||||
commentsIds: [props.modelValue.id ?? ""],
|
||||
content,
|
||||
forward,
|
||||
});
|
||||
};
|
||||
|
||||
const snackbar = inject<Snackbar>("snackbar");
|
||||
const { oruga } = useProgrammatic();
|
||||
|
||||
onCreateReportError((e) => {
|
||||
isReportModalActive.value = false;
|
||||
if (e.message) {
|
||||
snackbar?.open({
|
||||
message: e.message,
|
||||
variant: "danger",
|
||||
position: "bottom",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
oneCreateReportDone(() => {
|
||||
isReportModalActive.value = false;
|
||||
oruga.notification.open({
|
||||
message: t("Comment from {'@'}{username} reported", {
|
||||
username: props.modelValue.actor?.preferredUsername,
|
||||
}),
|
||||
variant: "success",
|
||||
position: "bottom-right",
|
||||
duration: 5000,
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@use "@/styles/_mixins" as *;
|
||||
|
||||
@@ -2,28 +2,29 @@ import { SEARCH_PERSONS } from "@/graphql/search";
|
||||
import { VueRenderer } from "@tiptap/vue-3";
|
||||
import tippy from "tippy.js";
|
||||
import MentionList from "./MentionList.vue";
|
||||
import { apolloClient, waitApolloQuery } from "@/vue-apollo";
|
||||
import { apolloClient } from "@/vue-apollo";
|
||||
import { IPerson } from "@/types/actor";
|
||||
import pDebounce from "p-debounce";
|
||||
import { MentionOptions } from "@tiptap/extension-mention";
|
||||
import { Editor } from "@tiptap/core";
|
||||
import { provideApolloClient, useQuery } from "@vue/apollo-composable";
|
||||
import { provideApolloClient, useLazyQuery } from "@vue/apollo-composable";
|
||||
import { Paginate } from "@/types/paginate";
|
||||
|
||||
const fetchItems = async (query: string): Promise<IPerson[]> => {
|
||||
try {
|
||||
if (query === "") return [];
|
||||
const res = await waitApolloQuery(
|
||||
provideApolloClient(apolloClient)(() => {
|
||||
return useQuery<
|
||||
{ searchPersons: Paginate<IPerson> },
|
||||
{ searchText: string }
|
||||
>(SEARCH_PERSONS, () => ({
|
||||
searchText: query,
|
||||
}));
|
||||
})
|
||||
);
|
||||
return res.data.searchPersons.elements;
|
||||
const res = await provideApolloClient(apolloClient)(async () => {
|
||||
const { load: loadSearchPersonsQuery } = useLazyQuery<
|
||||
{ searchPersons: Paginate<IPerson> },
|
||||
{ searchText: string }
|
||||
>(SEARCH_PERSONS);
|
||||
|
||||
return await loadSearchPersonsQuery(SEARCH_PERSONS, {
|
||||
searchText: query,
|
||||
});
|
||||
});
|
||||
if (!res) return [];
|
||||
return res.searchPersons.elements;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return [];
|
||||
|
||||
@@ -318,18 +318,10 @@ const debounceDelay = computed(() =>
|
||||
geocodingAutocomplete.value === true ? 200 : 2000
|
||||
);
|
||||
|
||||
const { onResult: onAddressSearchResult, load: searchAddress } = useLazyQuery<{
|
||||
const { load: searchAddress } = useLazyQuery<{
|
||||
searchAddress: IAddress[];
|
||||
}>(ADDRESS);
|
||||
|
||||
onAddressSearchResult((result) => {
|
||||
if (result.loading) return;
|
||||
const { data } = result;
|
||||
console.debug("onAddressSearchResult", data.searchAddress);
|
||||
addressData.value = data.searchAddress;
|
||||
isFetching.value = false;
|
||||
});
|
||||
|
||||
const asyncData = async (query: string): Promise<void> => {
|
||||
console.debug("Finding addresses");
|
||||
if (!query.length) {
|
||||
@@ -345,11 +337,21 @@ const asyncData = async (query: string): Promise<void> => {
|
||||
|
||||
isFetching.value = true;
|
||||
|
||||
searchAddress(undefined, {
|
||||
query,
|
||||
locale: locale,
|
||||
type: props.resultType,
|
||||
});
|
||||
try {
|
||||
const result = await searchAddress(undefined, {
|
||||
query,
|
||||
locale: locale,
|
||||
type: props.resultType,
|
||||
});
|
||||
|
||||
if (!result) return;
|
||||
console.debug("onAddressSearchResult", result.searchAddress);
|
||||
addressData.value = result.searchAddress;
|
||||
isFetching.value = false;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const selectedAddressText = computed(() => {
|
||||
@@ -393,24 +395,9 @@ const locateMe = async (): Promise<void> => {
|
||||
gettingLocation.value = false;
|
||||
};
|
||||
|
||||
const { onResult: onReverseGeocodeResult, load: loadReverseGeocode } =
|
||||
useReverseGeocode();
|
||||
const { load: loadReverseGeocode } = useReverseGeocode();
|
||||
|
||||
onReverseGeocodeResult((result) => {
|
||||
if (result.loading !== false) return;
|
||||
const { data } = result;
|
||||
addressData.value = data.reverseGeocode;
|
||||
|
||||
if (addressData.value.length > 0) {
|
||||
const foundAddress = addressData.value[0];
|
||||
Object.assign(selected, foundAddress);
|
||||
console.debug("reverse geocode succeded, setting new address");
|
||||
queryTextWithDefault.value = addressFullName(foundAddress);
|
||||
emit("update:modelValue", selected);
|
||||
}
|
||||
});
|
||||
|
||||
const reverseGeoCode = (e: LatLng, zoom: number) => {
|
||||
const reverseGeoCode = async (e: LatLng, zoom: number) => {
|
||||
console.debug("reverse geocode");
|
||||
|
||||
// If the details is opened, just update coords, don't reverse geocode
|
||||
@@ -423,12 +410,26 @@ const reverseGeoCode = (e: LatLng, zoom: number) => {
|
||||
// If the position has been updated through autocomplete selection, no need to geocode it!
|
||||
if (!e || checkCurrentPosition(e)) return;
|
||||
|
||||
loadReverseGeocode(undefined, {
|
||||
latitude: e.lat,
|
||||
longitude: e.lng,
|
||||
zoom,
|
||||
locale: locale as unknown as string,
|
||||
});
|
||||
try {
|
||||
const result = await loadReverseGeocode(undefined, {
|
||||
latitude: e.lat,
|
||||
longitude: e.lng,
|
||||
zoom,
|
||||
locale: locale as unknown as string,
|
||||
});
|
||||
if (!result) return;
|
||||
addressData.value = result.reverseGeocode;
|
||||
|
||||
if (addressData.value.length > 0) {
|
||||
const foundAddress = addressData.value[0];
|
||||
Object.assign(selected, foundAddress);
|
||||
console.debug("reverse geocode succeded, setting new address");
|
||||
queryTextWithDefault.value = addressFullName(foundAddress);
|
||||
emit("update:modelValue", selected);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to load reverse geocode", err);
|
||||
}
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
|
||||
@@ -40,10 +40,10 @@ const basicGroup: IGroup = {
|
||||
const groupWithMedia: IGroup = {
|
||||
...basicGroup,
|
||||
banner: {
|
||||
url: "https://mobilizon.fr/media/7b340fe641e7ad711ebb6f8821b5ce824992db08701e37ebb901c175436aaafc.jpg?name=framasoft%27s%20banner.jpg",
|
||||
url: "https://mobilizon.fr/media/a8227a16cc80b3d20ff5ee549a29c1b20a0ca1547f8861129aae9f00c3c69d12.jpg?name=framasoft%27s%20banner.jpg",
|
||||
},
|
||||
avatar: {
|
||||
url: "https://mobilizon.fr/media/ff5b2d425fb73e17fcbb56a1a032359ee0b21453c11af59e103e783817a32fdf.png?name=framasoft%27s%20avatar.png",
|
||||
url: "https://mobilizon.fr/media/890f5396ef80081a6b1b18a5db969746cf8bb340e8a4e657d665e41f6646c539.jpg?name=framasoft%27s%20avatar.jpg",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -122,8 +122,8 @@ const events = computed(
|
||||
() => eventsResult.value?.searchEvents ?? { elements: [], total: 0 }
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
load();
|
||||
onMounted(async () => {
|
||||
await load();
|
||||
});
|
||||
|
||||
const loading = computed(() => props.doingGeoloc || loadingEvents.value);
|
||||
|
||||
@@ -13,7 +13,24 @@
|
||||
>
|
||||
<MobilizonLogo class="w-40" />
|
||||
</router-link>
|
||||
<div class="flex items-center md:order-2 ml-auto" v-if="currentActor?.id">
|
||||
<div
|
||||
class="flex items-center md:order-2 ml-auto gap-2"
|
||||
v-if="currentActor?.id"
|
||||
>
|
||||
<router-link
|
||||
:to="{ name: RouteName.CONVERSATION_LIST }"
|
||||
class="flex sm:mr-3 text-sm md:mr-0 relative"
|
||||
id="conversations-menu-button"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<span class="sr-only">{{ t("Open conversations") }}</span>
|
||||
<Inbox :size="32" />
|
||||
<span
|
||||
v-show="unreadConversationsCount > 0"
|
||||
class="absolute bottom-0.5 -left-2 bg-primary rounded-full inline-block h-3 w-3 mx-2"
|
||||
>
|
||||
</span>
|
||||
</router-link>
|
||||
<o-dropdown position="bottom-left">
|
||||
<template #trigger>
|
||||
<button
|
||||
@@ -202,22 +219,28 @@
|
||||
import MobilizonLogo from "@/components/MobilizonLogo.vue";
|
||||
import { ICurrentUserRole } from "@/types/enums";
|
||||
import { logout } from "../utils/auth";
|
||||
import { displayName } from "../types/actor";
|
||||
import { IPerson, displayName } from "../types/actor";
|
||||
import RouteName from "../router/name";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { computed, onMounted, ref, watch } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import AccountCircle from "vue-material-design-icons/AccountCircle.vue";
|
||||
import Inbox from "vue-material-design-icons/Inbox.vue";
|
||||
import { useCurrentUserClient } from "@/composition/apollo/user";
|
||||
import {
|
||||
useCurrentActorClient,
|
||||
useCurrentUserIdentities,
|
||||
} from "@/composition/apollo/actor";
|
||||
import { useMutation } from "@vue/apollo-composable";
|
||||
import { useLazyQuery, useMutation } from "@vue/apollo-composable";
|
||||
import { UPDATE_DEFAULT_ACTOR } from "@/graphql/actor";
|
||||
import { changeIdentity } from "@/utils/identity";
|
||||
import { useRegistrationConfig } from "@/composition/apollo/config";
|
||||
import { useProgrammatic } from "@oruga-ui/oruga-next";
|
||||
import {
|
||||
UNREAD_ACTOR_CONVERSATIONS,
|
||||
UNREAD_ACTOR_CONVERSATIONS_SUBSCRIPTION,
|
||||
} from "@/graphql/user";
|
||||
import { ICurrentUser } from "@/types/current-user.model";
|
||||
|
||||
const { currentUser } = useCurrentUserClient();
|
||||
const { currentActor } = useCurrentActorClient();
|
||||
@@ -239,6 +262,61 @@ const canRegister = computed(() => {
|
||||
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
const unreadConversationsCount = computed(
|
||||
() =>
|
||||
unreadActorConversationsResult.value?.loggedUser.defaultActor
|
||||
?.unreadConversationsCount ?? 0
|
||||
);
|
||||
|
||||
const {
|
||||
result: unreadActorConversationsResult,
|
||||
load: loadUnreadConversations,
|
||||
subscribeToMore,
|
||||
} = useLazyQuery<{
|
||||
loggedUser: Pick<ICurrentUser, "id" | "defaultActor">;
|
||||
}>(UNREAD_ACTOR_CONVERSATIONS);
|
||||
|
||||
watch(currentActor, async (currentActorValue, previousActorValue) => {
|
||||
if (
|
||||
currentActorValue?.id &&
|
||||
currentActorValue.preferredUsername !==
|
||||
previousActorValue?.preferredUsername
|
||||
) {
|
||||
await loadUnreadConversations();
|
||||
|
||||
subscribeToMore<
|
||||
{ personId: string },
|
||||
{ personUnreadConversationsCount: number }
|
||||
>({
|
||||
document: UNREAD_ACTOR_CONVERSATIONS_SUBSCRIPTION,
|
||||
variables: {
|
||||
personId: currentActor.value?.id as string,
|
||||
},
|
||||
updateQuery: (previousResult, { subscriptionData }) => {
|
||||
console.debug(
|
||||
"Updating actor unread conversations count query after subscribe to more update",
|
||||
subscriptionData?.data?.personUnreadConversationsCount
|
||||
);
|
||||
return {
|
||||
...previousResult,
|
||||
loggedUser: {
|
||||
id: previousResult.loggedUser.id,
|
||||
defaultActor: {
|
||||
...previousResult.loggedUser.defaultActor,
|
||||
unreadConversationsCount:
|
||||
subscriptionData?.data?.personUnreadConversationsCount ??
|
||||
previousResult.loggedUser.defaultActor
|
||||
?.unreadConversationsCount,
|
||||
} as IPerson, // no idea why,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {});
|
||||
|
||||
watch(identities, () => {
|
||||
// If we don't have any identities, the user has validated their account,
|
||||
// is logging for the first time but didn't create an identity somehow
|
||||
|
||||
109
js/src/components/Participation/NewPrivateMessage.vue
Normal file
109
js/src/components/Participation/NewPrivateMessage.vue
Normal file
@@ -0,0 +1,109 @@
|
||||
<template>
|
||||
<form @submit="sendForm">
|
||||
<Editor
|
||||
v-model="text"
|
||||
mode="basic"
|
||||
:aria-label="t('Message body')"
|
||||
v-if="currentActor"
|
||||
:currentActor="currentActor"
|
||||
:placeholder="t('Write a new message')"
|
||||
/>
|
||||
<o-button class="mt-3" nativeType="submit">{{ t("Send") }}</o-button>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useCurrentActorClient } from "@/composition/apollo/actor";
|
||||
import { SEND_EVENT_PRIVATE_MESSAGE_MUTATION } from "@/graphql/conversations";
|
||||
import { EVENT_CONVERSATIONS } from "@/graphql/event";
|
||||
import { IConversation } from "@/types/conversation";
|
||||
import { ParticipantRole } from "@/types/enums";
|
||||
import { IEvent } from "@/types/event.model";
|
||||
import { useMutation } from "@vue/apollo-composable";
|
||||
import { computed, defineAsyncComponent, ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
const props = defineProps<{
|
||||
event: IEvent;
|
||||
}>();
|
||||
|
||||
const event = computed(() => props.event);
|
||||
|
||||
const text = ref("");
|
||||
const {
|
||||
mutate: eventPrivateMessageMutate,
|
||||
onDone: onEventPrivateMessageMutated,
|
||||
} = useMutation<
|
||||
{
|
||||
sendEventPrivateMessage: IConversation;
|
||||
},
|
||||
{
|
||||
text: string;
|
||||
actorId: string;
|
||||
eventId: string;
|
||||
roles?: string;
|
||||
inReplyToActorId?: ParticipantRole[];
|
||||
language?: string;
|
||||
}
|
||||
>(SEND_EVENT_PRIVATE_MESSAGE_MUTATION, {
|
||||
update(cache, result) {
|
||||
if (!result.data?.sendEventPrivateMessage) return;
|
||||
const cachedData = cache.readQuery<{
|
||||
event: Pick<IEvent, "conversations" | "id" | "uuid">;
|
||||
}>({
|
||||
query: EVENT_CONVERSATIONS,
|
||||
variables: {
|
||||
uuid: event.value.uuid,
|
||||
page: 1,
|
||||
},
|
||||
});
|
||||
if (!cachedData) return;
|
||||
cache.writeQuery({
|
||||
query: EVENT_CONVERSATIONS,
|
||||
variables: {
|
||||
uuid: event.value.uuid,
|
||||
page: 1,
|
||||
},
|
||||
data: {
|
||||
event: {
|
||||
...cachedData?.event,
|
||||
conversations: {
|
||||
...cachedData.event.conversations,
|
||||
total: cachedData.event.conversations.total + 1,
|
||||
elements: [
|
||||
...cachedData.event.conversations.elements,
|
||||
result.data.sendEventPrivateMessage,
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { currentActor } = useCurrentActorClient();
|
||||
|
||||
const sendForm = (e: Event) => {
|
||||
e.preventDefault();
|
||||
console.debug("Sending new private message");
|
||||
if (!currentActor.value?.id || !event.value.id) return;
|
||||
eventPrivateMessageMutate({
|
||||
text: text.value,
|
||||
actorId:
|
||||
event.value?.attributedTo?.id ??
|
||||
event.value.organizerActor?.id ??
|
||||
currentActor.value?.id,
|
||||
eventId: event.value.id,
|
||||
});
|
||||
};
|
||||
|
||||
onEventPrivateMessageMutated(() => {
|
||||
text.value = "";
|
||||
});
|
||||
|
||||
const Editor = defineAsyncComponent(
|
||||
() => import("../../components/TextEditor.vue")
|
||||
);
|
||||
</script>
|
||||
@@ -5,7 +5,9 @@
|
||||
</header>
|
||||
|
||||
<section>
|
||||
<div class="flex gap-1 flex-row mb-3">
|
||||
<div
|
||||
class="flex gap-1 flex-row mb-3 bg-mbz-yellow p-3 rounded items-center"
|
||||
>
|
||||
<o-icon
|
||||
icon="alert"
|
||||
variant="warning"
|
||||
|
||||
@@ -273,7 +273,7 @@ import Placeholder from "@tiptap/extension-placeholder";
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue: string;
|
||||
mode?: string;
|
||||
mode?: "description" | "comment" | "basic";
|
||||
maxSize?: number;
|
||||
ariaLabel?: string;
|
||||
currentActor: IPerson;
|
||||
@@ -305,12 +305,6 @@ const isBasicMode = computed((): boolean => {
|
||||
return props.mode === "basic";
|
||||
});
|
||||
|
||||
// const insertMention = (obj: { range: any; attrs: any }) => {
|
||||
// console.debug("initialize Mention");
|
||||
// };
|
||||
|
||||
// const observer = ref<MutationObserver | null>(null);
|
||||
|
||||
const transformPastedHTML = (html: string): string => {
|
||||
// When using comment mode, limit to acceptable tags
|
||||
if (isCommentMode.value) {
|
||||
|
||||
46
js/src/composition/apollo/members.ts
Normal file
46
js/src/composition/apollo/members.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { GROUP_MEMBERS } from "@/graphql/member";
|
||||
import { IGroup } from "@/types/actor";
|
||||
import { MemberRole } from "@/types/enums";
|
||||
import { useQuery } from "@vue/apollo-composable";
|
||||
import { computed } from "vue";
|
||||
import type { Ref } from "vue";
|
||||
|
||||
type useGroupMembersOptions = {
|
||||
membersPage?: number;
|
||||
membersLimit?: number;
|
||||
roles?: MemberRole[];
|
||||
enabled?: Ref<boolean>;
|
||||
name?: string;
|
||||
};
|
||||
|
||||
export function useGroupMembers(
|
||||
groupName: Ref<string>,
|
||||
options: useGroupMembersOptions = {}
|
||||
) {
|
||||
console.debug("useGroupMembers", options);
|
||||
const { result, error, loading, onResult, onError, refetch, fetchMore } =
|
||||
useQuery<
|
||||
{
|
||||
group: IGroup;
|
||||
},
|
||||
{
|
||||
name: string;
|
||||
membersPage?: number;
|
||||
membersLimit?: number;
|
||||
}
|
||||
>(
|
||||
GROUP_MEMBERS,
|
||||
() => ({
|
||||
groupName: groupName.value,
|
||||
page: options.membersPage,
|
||||
limit: options.membersLimit,
|
||||
name: options.name,
|
||||
}),
|
||||
() => ({
|
||||
enabled: !!groupName.value && options.enabled?.value,
|
||||
fetchPolicy: "cache-and-network",
|
||||
})
|
||||
);
|
||||
const members = computed(() => result.value?.group?.members);
|
||||
return { members, error, loading, onResult, onError, refetch, fetchMore };
|
||||
}
|
||||
@@ -1,18 +1,22 @@
|
||||
import { FILTER_TAGS } from "@/graphql/tags";
|
||||
import { ITag } from "@/types/tag.model";
|
||||
import { apolloClient, waitApolloQuery } from "@/vue-apollo";
|
||||
import { provideApolloClient, useQuery } from "@vue/apollo-composable";
|
||||
import { apolloClient } from "@/vue-apollo";
|
||||
import { provideApolloClient, useLazyQuery } from "@vue/apollo-composable";
|
||||
|
||||
export async function fetchTags(text: string): Promise<ITag[]> {
|
||||
try {
|
||||
const res = await waitApolloQuery(
|
||||
provideApolloClient(apolloClient)(() =>
|
||||
useQuery<{ tags: ITag[] }, { filter: string }>(FILTER_TAGS, {
|
||||
filter: text,
|
||||
})
|
||||
)
|
||||
const { load: loadFetchTagsQuery } = useLazyQuery<
|
||||
{ tags: ITag[] },
|
||||
{ filter: string }
|
||||
>(FILTER_TAGS);
|
||||
|
||||
const res = await provideApolloClient(apolloClient)(() =>
|
||||
loadFetchTagsQuery(FILTER_TAGS, {
|
||||
filter: text,
|
||||
})
|
||||
);
|
||||
return res.data.tags;
|
||||
if (!res) return [];
|
||||
return res.tags;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return [];
|
||||
|
||||
@@ -17,6 +17,7 @@ export const COMMENT_FIELDS_FRAGMENT = gql`
|
||||
insertedAt
|
||||
updatedAt
|
||||
deletedAt
|
||||
publishedAt
|
||||
isAnnouncement
|
||||
language
|
||||
}
|
||||
|
||||
166
js/src/graphql/conversations.ts
Normal file
166
js/src/graphql/conversations.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
import gql from "graphql-tag";
|
||||
import { ACTOR_FRAGMENT } from "./actor";
|
||||
import { COMMENT_FIELDS_FRAGMENT } from "./comment";
|
||||
|
||||
export const CONVERSATION_QUERY_FRAGMENT = gql`
|
||||
fragment ConversationQuery on Conversation {
|
||||
id
|
||||
conversationParticipantId
|
||||
actor {
|
||||
...ActorFragment
|
||||
}
|
||||
lastComment {
|
||||
...CommentFields
|
||||
}
|
||||
participants {
|
||||
...ActorFragment
|
||||
}
|
||||
event {
|
||||
id
|
||||
uuid
|
||||
title
|
||||
picture {
|
||||
id
|
||||
url
|
||||
name
|
||||
metadata {
|
||||
width
|
||||
height
|
||||
blurhash
|
||||
}
|
||||
}
|
||||
}
|
||||
unread
|
||||
insertedAt
|
||||
updatedAt
|
||||
}
|
||||
${ACTOR_FRAGMENT}
|
||||
${COMMENT_FIELDS_FRAGMENT}
|
||||
`;
|
||||
|
||||
export const CONVERSATIONS_QUERY_FRAGMENT = gql`
|
||||
fragment ConversationsQuery on PaginatedConversationList {
|
||||
total
|
||||
elements {
|
||||
...ConversationQuery
|
||||
}
|
||||
}
|
||||
${CONVERSATION_QUERY_FRAGMENT}
|
||||
`;
|
||||
|
||||
export const SEND_EVENT_PRIVATE_MESSAGE_MUTATION = gql`
|
||||
mutation SendEventPrivateMessageMutation(
|
||||
$text: String!
|
||||
$actorId: ID!
|
||||
$eventId: ID!
|
||||
$roles: [ParticipantRoleEnum]
|
||||
$attributedToId: ID
|
||||
$language: String
|
||||
) {
|
||||
sendEventPrivateMessage(
|
||||
text: $text
|
||||
actorId: $actorId
|
||||
eventId: $eventId
|
||||
roles: $roles
|
||||
attributedToId: $attributedToId
|
||||
language: $language
|
||||
) {
|
||||
...ConversationQuery
|
||||
}
|
||||
}
|
||||
${CONVERSATION_QUERY_FRAGMENT}
|
||||
`;
|
||||
|
||||
export const GET_CONVERSATION = gql`
|
||||
query GetConversation($id: ID!, $page: Int, $limit: Int) {
|
||||
conversation(id: $id) {
|
||||
...ConversationQuery
|
||||
comments(page: $page, limit: $limit) @connection(key: "comments") {
|
||||
total
|
||||
elements {
|
||||
id
|
||||
text
|
||||
actor {
|
||||
...ActorFragment
|
||||
}
|
||||
insertedAt
|
||||
updatedAt
|
||||
deletedAt
|
||||
publishedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${CONVERSATION_QUERY_FRAGMENT}
|
||||
`;
|
||||
|
||||
export const POST_PRIVATE_MESSAGE_MUTATION = gql`
|
||||
mutation PostPrivateMessageMutation(
|
||||
$text: String!
|
||||
$actorId: ID!
|
||||
$language: String
|
||||
$mentions: [String]
|
||||
) {
|
||||
postPrivateMessage(
|
||||
text: $text
|
||||
actorId: $actorId
|
||||
language: $language
|
||||
mentions: $mentions
|
||||
) {
|
||||
...ConversationQuery
|
||||
}
|
||||
}
|
||||
${CONVERSATION_QUERY_FRAGMENT}
|
||||
`;
|
||||
|
||||
export const REPLY_TO_PRIVATE_MESSAGE_MUTATION = gql`
|
||||
mutation ReplyToPrivateMessageMutation(
|
||||
$text: String!
|
||||
$actorId: ID!
|
||||
$attributedToId: ID
|
||||
$language: String
|
||||
$conversationId: ID!
|
||||
$mentions: [String]
|
||||
) {
|
||||
postPrivateMessage(
|
||||
text: $text
|
||||
actorId: $actorId
|
||||
attributedToId: $attributedToId
|
||||
language: $language
|
||||
conversationId: $conversationId
|
||||
mentions: $mentions
|
||||
) {
|
||||
...ConversationQuery
|
||||
}
|
||||
}
|
||||
${CONVERSATION_QUERY_FRAGMENT}
|
||||
`;
|
||||
|
||||
export const CONVERSATION_COMMENT_CHANGED = gql`
|
||||
subscription ConversationCommentChanged($id: ID!) {
|
||||
conversationCommentChanged(id: $id) {
|
||||
id
|
||||
lastComment {
|
||||
id
|
||||
text
|
||||
updatedAt
|
||||
insertedAt
|
||||
deletedAt
|
||||
publishedAt
|
||||
actor {
|
||||
...ActorFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${ACTOR_FRAGMENT}
|
||||
`;
|
||||
|
||||
export const MARK_CONVERSATION_AS_READ = gql`
|
||||
mutation MarkConversationAsRead($id: ID!, $read: Boolean!) {
|
||||
updateConversation(conversationId: $id, read: $read) {
|
||||
...ConversationQuery
|
||||
}
|
||||
}
|
||||
${CONVERSATION_QUERY_FRAGMENT}
|
||||
`;
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
PARTICIPANT_QUERY_FRAGMENT,
|
||||
} from "./participant";
|
||||
import { TAG_FRAGMENT } from "./tags";
|
||||
import { CONVERSATIONS_QUERY_FRAGMENT } from "./conversations";
|
||||
|
||||
const FULL_EVENT_FRAGMENT = gql`
|
||||
fragment FullEvent on Event {
|
||||
@@ -375,9 +376,16 @@ export const PARTICIPANTS = gql`
|
||||
rejected
|
||||
participant
|
||||
}
|
||||
organizerActor {
|
||||
...ActorFragment
|
||||
}
|
||||
attributedTo {
|
||||
...ActorFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
${PARTICIPANTS_QUERY_FRAGMENT}
|
||||
${ACTOR_FRAGMENT}
|
||||
`;
|
||||
|
||||
export const EVENT_PERSON_PARTICIPATION = gql`
|
||||
@@ -494,3 +502,41 @@ export const EXPORT_EVENT_PARTICIPATIONS = gql`
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const EVENT_CONVERSATIONS = gql`
|
||||
query EventConversations($uuid: UUID!, $page: Int, $limit: Int) {
|
||||
event(uuid: $uuid) {
|
||||
id
|
||||
uuid
|
||||
title
|
||||
conversations(page: $page, limit: $limit) {
|
||||
...ConversationsQuery
|
||||
}
|
||||
}
|
||||
}
|
||||
${CONVERSATIONS_QUERY_FRAGMENT}
|
||||
`;
|
||||
|
||||
export const USER_CONVERSATIONS = gql`
|
||||
query UserConversations($page: Int, $limit: Int) {
|
||||
loggedUser {
|
||||
id
|
||||
conversations(page: $page, limit: $limit) {
|
||||
...ConversationsQuery
|
||||
}
|
||||
}
|
||||
}
|
||||
${CONVERSATIONS_QUERY_FRAGMENT}
|
||||
`;
|
||||
|
||||
export const PROFILE_CONVERSATIONS = gql`
|
||||
query ProfileConversations($page: Int, $limit: Int) {
|
||||
loggedPerson {
|
||||
id
|
||||
conversations(page: $page, limit: $limit) {
|
||||
...ConversationsQuery
|
||||
}
|
||||
}
|
||||
}
|
||||
${CONVERSATIONS_QUERY_FRAGMENT}
|
||||
`;
|
||||
|
||||
@@ -85,6 +85,12 @@ const REPORT_FRAGMENT = gql`
|
||||
uuid
|
||||
title
|
||||
}
|
||||
conversation {
|
||||
id
|
||||
participants {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
notes {
|
||||
id
|
||||
|
||||
@@ -247,6 +247,34 @@ export const SEARCH_PERSONS = gql`
|
||||
${ACTOR_FRAGMENT}
|
||||
`;
|
||||
|
||||
export const SEARCH_PERSON_AND_GROUPS = gql`
|
||||
query SearchPersonsAndGroups($searchText: String!, $page: Int, $limit: Int) {
|
||||
searchPersons(term: $searchText, page: $page, limit: $limit) {
|
||||
total
|
||||
elements {
|
||||
...ActorFragment
|
||||
}
|
||||
}
|
||||
searchGroups(term: $searchText, page: $page, limit: $limit) {
|
||||
total
|
||||
elements {
|
||||
...ActorFragment
|
||||
banner {
|
||||
id
|
||||
url
|
||||
}
|
||||
membersCount
|
||||
followersCount
|
||||
physicalAddress {
|
||||
...AdressFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${ADDRESS_FRAGMENT}
|
||||
${ACTOR_FRAGMENT}
|
||||
`;
|
||||
|
||||
export const INTERACT = gql`
|
||||
query Interact($uri: String!) {
|
||||
interact(uri: $uri) {
|
||||
|
||||
@@ -312,3 +312,21 @@ export const FEED_TOKENS_LOGGED_USER = gql`
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const UNREAD_ACTOR_CONVERSATIONS = gql`
|
||||
query LoggedUserUnreadConversations {
|
||||
loggedUser {
|
||||
id
|
||||
defaultActor {
|
||||
id
|
||||
unreadConversationsCount
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const UNREAD_ACTOR_CONVERSATIONS_SUBSCRIPTION = gql`
|
||||
subscription OnUreadActorConversationsChanged($personId: ID!) {
|
||||
personUnreadConversationsCount(personId: $personId)
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -1610,5 +1610,20 @@
|
||||
"External registration": "External registration",
|
||||
"I want to manage the registration with an external provider": "I want to manage the registration with an external provider",
|
||||
"External provider URL": "External provider URL",
|
||||
"Members will also access private sections like discussions, resources and restricted posts.": "Members will also access private sections like discussions, resources and restricted posts."
|
||||
"Members will also access private sections like discussions, resources and restricted posts.": "Members will also access private sections like discussions, resources and restricted posts.",
|
||||
"With unknown participants": "With unknown participants",
|
||||
"With {participants}": "With {participants}",
|
||||
"Conversations": "Conversations",
|
||||
"New private message": "New private message",
|
||||
"There's no conversations yet": "There's no conversations yet",
|
||||
"Open conversations": "Open conversations",
|
||||
"List of conversations": "List of conversations",
|
||||
"Conversation with {participants}": "Conversation with {participants}",
|
||||
"Delete this conversation": "Delete this conversation",
|
||||
"Are you sure you want to delete this entire conversation?": "Are you sure you want to delete this entire conversation?",
|
||||
"This is a announcement from the organizers of event {event}. You can't reply to it, but you can send a private message to event organizers.": "This is a announcement from the organizers of event {event}. You can't reply to it, but you can send a private message to event organizers.",
|
||||
"You have access to this conversation as a member of the {group} group": "You have access to this conversation as a member of the {group} group",
|
||||
"Comment from an event announcement": "Comment from an event announcement",
|
||||
"Comment from a private conversation": "Comment from a private conversation",
|
||||
"I've been mentionned in a conversation": "I've been mentionned in a conversation"
|
||||
}
|
||||
@@ -31,7 +31,7 @@
|
||||
"A post has been updated": "Un billet a été mis à jour",
|
||||
"A practical tool": "Un outil pratique",
|
||||
"A resource has been created or updated": "Une resource a été créée ou mise à jour",
|
||||
"A short tagline for your instance homepage. Defaults to \"Gather ⋅ Organize ⋅ Mobilize\"": "Un court slogan pour la page d'accueil de votre instance. La valeur par défaut est « Rassembler ⋅ Organiser ⋅ Mobiliser »",
|
||||
"A short tagline for your instance homepage. Defaults to \"Gather · Organize · Mobilize\"": "Un court slogan pour la page d'accueil de votre instance. La valeur par défaut est « Rassembler · Organiser · Mobiliser »",
|
||||
"A twitter account handle to follow for event updates": "Un compte sur Twitter à suivre pour les mises à jour de l'événement",
|
||||
"A user-friendly, emancipatory and ethical tool for gathering, organising, and mobilising.": "Un outil convivial, émancipateur et éthique pour se rassembler, s'organiser et se mobiliser.",
|
||||
"A validation email was sent to {email}": "Un email de validation a été envoyé à {email}",
|
||||
@@ -103,7 +103,7 @@
|
||||
"An URL to an external ticketing platform": "Une URL vers une plateforme de billetterie externe",
|
||||
"An anonymous profile joined the event {event}.": "Un profil anonyme a rejoint l'événement {event}.",
|
||||
"An error has occured while refreshing the page.": "Une erreur est survenue lors du rafraîchissement de la page.",
|
||||
"An error has occured. Sorry about that. You may try to reload the page.": "Une erreur est survenue. Nous en sommes désolé⋅es. Vous pouvez essayer de rafraîchir la page.",
|
||||
"An error has occured. Sorry about that. You may try to reload the page.": "Une erreur est survenue. Nous en sommes désolé·es. Vous pouvez essayer de rafraîchir la page.",
|
||||
"An ethical alternative": "Une alternative éthique",
|
||||
"An event I'm going to has been updated": "Un événement auquel je participe a été mis à jour",
|
||||
"An event I'm going to has posted an announcement": "Un événement auquel je participe a posté une annonce",
|
||||
@@ -118,7 +118,7 @@
|
||||
"And {number} comments": "Et {number} commentaires",
|
||||
"Announcements": "Annonces",
|
||||
"Announcements and mentions notifications are always sent straight away.": "Les notifications d'annonces et de mentions sont toujours envoyées directement.",
|
||||
"Anonymous participant": "Participant⋅e anonyme",
|
||||
"Anonymous participant": "Participant·e anonyme",
|
||||
"Anonymous participants will be asked to confirm their participation through e-mail.": "Les participants anonymes devront confirmer leur participation par email.",
|
||||
"Anonymous participations": "Participations anonymes",
|
||||
"Any category": "N'importe quelle catégorie",
|
||||
@@ -126,7 +126,7 @@
|
||||
"Any distance": "N'importe quelle distance",
|
||||
"Any type": "N'importe quel type",
|
||||
"Anyone can join freely": "N'importe qui peut rejoindre",
|
||||
"Anyone can request being a member, but an administrator needs to approve the membership.": "N'importe qui peut demander à être membre, mais un⋅e administrateur⋅ice devra approuver leur adhésion.",
|
||||
"Anyone can request being a member, but an administrator needs to approve the membership.": "N'importe qui peut demander à être membre, mais un·e administrateur·ice devra approuver leur adhésion.",
|
||||
"Anyone wanting to be a member from your group will be able to from your group page.": "N'importe qui voulant devenir membre pourra le faire depuis votre page de groupe.",
|
||||
"Application": "Application",
|
||||
"Application authorized": "Application autorisée",
|
||||
@@ -135,26 +135,26 @@
|
||||
"Apply filters": "Appliquer les filtres",
|
||||
"Approve member": "Approuver le ou la membre",
|
||||
"Apps": "Applications",
|
||||
"Are you really sure you want to delete your whole account? You'll lose everything. Identities, settings, events created, messages and participations will be gone forever.": "Êtes-vous vraiment certain⋅e de vouloir supprimer votre compte ? Vous allez tout perdre. Identités, paramètres, événements créés, messages et participations disparaîtront pour toujours.",
|
||||
"Are you really sure you want to delete your whole account? You'll lose everything. Identities, settings, events created, messages and participations will be gone forever.": "Êtes-vous vraiment certain·e de vouloir supprimer votre compte ? Vous allez tout perdre. Identités, paramètres, événements créés, messages et participations disparaîtront pour toujours.",
|
||||
"Are you sure you want to <b>completely delete</b> this group? All members - including remote ones - will be notified and removed from the group, and <b>all of the group data (events, posts, discussions, todos…) will be irretrievably destroyed</b>.": "Êtes-vous certain·e de vouloir <b>complètement supprimer</b> ce groupe ? Tous les membres - y compris ceux·elles sur d'autres instances - seront notifié·e·s et supprimé·e·s du groupe, et <b>toutes les données associées au groupe (événements, billets, discussions, todos…) seront irrémédiablement détruites</b>.",
|
||||
"Are you sure you want to <b>delete</b> this comment? <b>This action cannot be undone</b>.": "Êtes-vous certain⋅e de vouloir <b>supprimer</b> ce commentaire ? <b>Cette action ne peut pas être annulée.</b>",
|
||||
"Are you sure you want to <b>delete</b> this comment? This action cannot be undone.": "Êtes-vous certain⋅e de vouloir <b>supprimer</b> ce commentaire ? Cette action ne peut pas être annulée.",
|
||||
"Are you sure you want to <b>delete</b> this event? <b>This action cannot be undone</b>. You may want to engage the discussion with the event creator and ask them to edit their event instead.": "Êtes-vous certain⋅e de vouloir <b>supprimer</b> cet événement ? Cette action n'est pas réversible. Vous voulez peut-être engager la discussion avec le créateur de l'événement et lui demander de modifier son événement à la place.",
|
||||
"Are you sure you want to <b>delete</b> this event? This action cannot be undone. You may want to engage the discussion with the event creator or edit its event instead.": "Êtes-vous certain⋅e de vouloir <b>supprimer</b> cet événement ? Cette action n'est pas réversible. Vous voulez peut-être engager la discussion avec le créateur de l'événement ou bien modifier son événement à la place.",
|
||||
"Are you sure you want to <b>delete</b> this comment? <b>This action cannot be undone</b>.": "Êtes-vous certain·e de vouloir <b>supprimer</b> ce commentaire ? <b>Cette action ne peut pas être annulée.</b>",
|
||||
"Are you sure you want to <b>delete</b> this comment? This action cannot be undone.": "Êtes-vous certain·e de vouloir <b>supprimer</b> ce commentaire ? Cette action ne peut pas être annulée.",
|
||||
"Are you sure you want to <b>delete</b> this event? <b>This action cannot be undone</b>. You may want to engage the discussion with the event creator and ask them to edit their event instead.": "Êtes-vous certain·e de vouloir <b>supprimer</b> cet événement ? Cette action n'est pas réversible. Vous voulez peut-être engager la discussion avec le créateur de l'événement et lui demander de modifier son événement à la place.",
|
||||
"Are you sure you want to <b>delete</b> this event? This action cannot be undone. You may want to engage the discussion with the event creator or edit its event instead.": "Êtes-vous certain·e de vouloir <b>supprimer</b> cet événement ? Cette action n'est pas réversible. Vous voulez peut-être engager la discussion avec le créateur de l'événement ou bien modifier son événement à la place.",
|
||||
"Are you sure you want to <b>suspend</b> this group? All members - including remote ones - will be notified and removed from the group, and <b>all of the group data (events, posts, discussions, todos…) will be irretrievably destroyed</b>.": "Êtes-vous certain·e de vouloir <b>suspendre</b> ce groupe ? Tous les membres - y compris ceux·elles sur d'autres instances - seront notifié·e·s et supprimé·e·s du groupe, et <b>toutes les données associées au groupe (événements, billets, discussions, todos…) seront irrémédiablement détruites</b>.",
|
||||
"Are you sure you want to <b>suspend</b> this group? As this group originates from instance {instance}, this will only remove local members and delete the local data, as well as rejecting all the future data.": "Êtes-vous certain·e de vouloir <b>suspendre</b> ce groupe ? Comme ce groupe provient de l'instance {instance}, cela supprimera seulement les membres locaux et supprimera les données locales, et rejettera également toutes les données futures.",
|
||||
"Are you sure you want to cancel the event creation? You'll lose all modifications.": "Êtes-vous certain⋅e de vouloir annuler la création de l'événement ? Vous allez perdre toutes vos modifications.",
|
||||
"Are you sure you want to cancel the event edition? You'll lose all modifications.": "Êtes-vous certain⋅e de vouloir annuler la modification de l'événement ? Vous allez perdre toutes vos modifications.",
|
||||
"Are you sure you want to cancel your participation at event \"{title}\"?": "Êtes-vous certain⋅e de vouloir annuler votre participation à l'événement « {title} » ?",
|
||||
"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 event? This action cannot be reverted.": "Êtes-vous certain⋅e de vouloir supprimer cet événement ? Cette action ne peut être annulée.",
|
||||
"Are you sure you want to cancel the event creation? You'll lose all modifications.": "Êtes-vous certain·e de vouloir annuler la création de l'événement ? Vous allez perdre toutes vos modifications.",
|
||||
"Are you sure you want to cancel the event edition? You'll lose all modifications.": "Êtes-vous certain·e de vouloir annuler la modification de l'événement ? Vous allez perdre toutes vos modifications.",
|
||||
"Are you sure you want to cancel your participation at event \"{title}\"?": "Êtes-vous certain·e de vouloir annuler votre participation à l'événement « {title} » ?",
|
||||
"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 event? This action cannot be reverted.": "Êtes-vous certain·e de vouloir supprimer cet événement ? Cette action ne peut être annulée.",
|
||||
"Are you sure you want to delete this post? This action cannot be reverted.": "Voulez-vous vraiment supprimer ce billet ? Cette action ne peut pas être annulée.",
|
||||
"Are you sure you want to leave the group {groupName}? You'll loose access to this group's private content. This action cannot be undone.": "Êtes-vous sûr⋅e de vouloir quitter le groupe {groupName} ? Vous perdrez accès au contenu privé de ce groupe. Cette action ne peut pas être annulée.",
|
||||
"Are you sure you want to leave the group {groupName}? You'll loose access to this group's private content. This action cannot be undone.": "Êtes-vous sûr·e de vouloir quitter le groupe {groupName} ? Vous perdrez accès au contenu privé de ce groupe. Cette action ne peut pas être annulée.",
|
||||
"As the event organizer has chosen to manually validate participation requests, your participation will be really confirmed only once you receive an email stating it's being accepted.": "L'organisateur de l'événement ayant choisi de valider manuellement les demandes de participation, votre participation ne sera réellement confirmée que lorsque vous recevrez un courriel indiquant qu'elle est acceptée.",
|
||||
"Ask your instance admin to {enable_feature}.": "Demandez à l'administrateur⋅ice de votre instance d'{enable_feature}.",
|
||||
"Ask your instance admin to {enable_feature}.": "Demandez à l'administrateur·ice de votre instance d'{enable_feature}.",
|
||||
"Assigned to": "Assigné à",
|
||||
"Atom feed for events and posts": "Flux Atom pour les événements et les billets",
|
||||
"Attending": "Participant⋅e",
|
||||
"Attending": "Participant·e",
|
||||
"Authorize": "Autoriser",
|
||||
"Authorize application": "Autoriser l'application",
|
||||
"Authorized on {authorization_date}": "Autorisée le {authorization_date}",
|
||||
@@ -165,7 +165,7 @@
|
||||
"Back to previous page": "Retour à la page précédente",
|
||||
"Back to profile list": "Retour à la liste des profiles",
|
||||
"Back to top": "Retour en haut",
|
||||
"Back to user list": "Retour à la liste des utilisateur⋅ices",
|
||||
"Back to user list": "Retour à la liste des utilisateur·ices",
|
||||
"Banner": "Bannière",
|
||||
"Become part of the community and start organizing events": "Faites partie de la communauté et commencez à organiser des événements",
|
||||
"Before you can login, you need to click on the link inside it to validate your account.": "Avant que vous puissiez vous enregistrer, vous devez cliquer sur le lien à l'intérieur pour valider votre compte.",
|
||||
@@ -207,7 +207,7 @@
|
||||
"Change role": "Changer le role",
|
||||
"Change the filters.": "Changez les filtres.",
|
||||
"Change timezone": "Changer de fuseau horaire",
|
||||
"Change user email": "Modifier l'email de l'utilisateur⋅ice",
|
||||
"Change user email": "Modifier l'email de l'utilisateur·ice",
|
||||
"Change user role": "Changer le role de l'utilisateur",
|
||||
"Check your device to continue. You may now close this window.": "Vérifiez votre appareil pour continuer. Vous pouvez maintenant fermer cette fenêtre.",
|
||||
"Check your inbox (and your junk mail folder).": "Vérifiez votre boîte de réception (et votre dossier des indésirables).",
|
||||
@@ -223,7 +223,7 @@
|
||||
"Click for more information": "Cliquez pour plus d'informations",
|
||||
"Click to upload": "Cliquez pour téléverser",
|
||||
"Close": "Fermer",
|
||||
"Close comments for all (except for admins)": "Fermer les commentaires à tout le monde (excepté les administrateur⋅rice·s)",
|
||||
"Close comments for all (except for admins)": "Fermer les commentaires à tout le monde (excepté les administrateur·rice·s)",
|
||||
"Close map": "Fermer la carte",
|
||||
"Closed": "Fermé",
|
||||
"Comment body": "Corps du commentaire",
|
||||
@@ -238,7 +238,7 @@
|
||||
"Confirm my participation": "Confirmer ma participation",
|
||||
"Confirm my particpation": "Confirmer ma participation",
|
||||
"Confirm participation": "Confirmer la participation",
|
||||
"Confirm user": "Confirmer l'utilisateur⋅ice",
|
||||
"Confirm user": "Confirmer l'utilisateur·ice",
|
||||
"Confirmed": "Confirmé·e",
|
||||
"Confirmed at": "Confirmé·e à",
|
||||
"Confirmed: Will happen": "Confirmé : aura lieu",
|
||||
@@ -343,7 +343,7 @@
|
||||
"Distance": "Distance",
|
||||
"Do not receive any mail": "Ne pas recevoir d'e-mail",
|
||||
"Do you really want to suspend the account « {emailAccount} » ?": "Voulez-vous vraiment suspendre le compte « {emailAccount} » ?",
|
||||
"Do you really want to suspend this account? All of the user's profiles will be deleted.": "Voulez-vous vraiment suspendre ce compte ? Tous les profils de cet⋅te utilisateur⋅ice seront supprimés.",
|
||||
"Do you really want to suspend this account? All of the user's profiles will be deleted.": "Voulez-vous vraiment suspendre ce compte ? Tous les profils de cet·te utilisateur·ice seront supprimés.",
|
||||
"Do you really want to suspend this profile? All of the profiles content will be deleted.": "Voulez-vous vraiment suspendre ce profil ? Tout le contenu du profil sera supprimé.",
|
||||
"Do you wish to {create_event} or {explore_events}?": "Voulez-vous {create_event} ou {explore_events} ?",
|
||||
"Do you wish to {create_group} or {explore_groups}?": "Voulez-vous {create_group} ou {explore_groups} ?",
|
||||
@@ -356,7 +356,7 @@
|
||||
"Edit": "Modifier",
|
||||
"Edit post": "Éditer le billet",
|
||||
"Edit profile {profile}": "Éditer le profil {profile}",
|
||||
"Edit user email": "Éditer l'email de l'utilisateur⋅ice",
|
||||
"Edit user email": "Éditer l'email de l'utilisateur·ice",
|
||||
"Edited {ago}": "Édité il y a {ago}",
|
||||
"Edited {relative_time} ago": "Édité il y a {relative_time}",
|
||||
"Eg: Stockholm, Dance, Chess…": "Par exemple : Lyon, Danse, Bridge…",
|
||||
@@ -444,15 +444,15 @@
|
||||
"Follow a new instance": "Suivre une nouvelle instance",
|
||||
"Follow instance": "Suivre l'instance",
|
||||
"Follow request pending approval": "Demande de suivi en attente d'approbation",
|
||||
"Follow requests will be approved by a group moderator": "Les demandes de suivi seront approuvées par un⋅e modérateur⋅ice du groupe",
|
||||
"Follow requests will be approved by a group moderator": "Les demandes de suivi seront approuvées par un·e modérateur·ice du groupe",
|
||||
"Follow status": "Statut du suivi",
|
||||
"Followed": "Suivies",
|
||||
"Followed, pending response": "Suivie, en attente de la réponse",
|
||||
"Follower": "Abonné⋅es",
|
||||
"Followers": "Abonné⋅es",
|
||||
"Followers will receive new public events and posts.": "Les abonnée⋅s recevront les nouveaux événements et billets publics.",
|
||||
"Follower": "Abonné·es",
|
||||
"Followers": "Abonné·es",
|
||||
"Followers will receive new public events and posts.": "Les abonnée·s recevront les nouveaux événements et billets publics.",
|
||||
"Following": "Suivantes",
|
||||
"Following the group will allow you to be informed of the {group_upcoming_public_events}, whereas joining the group means you will {access_to_group_private_content_as_well}, including group discussions, group resources and members-only posts.": "Suivre le groupe vous permettra d'être informé⋅e des {group_upcoming_public_events}, alors que rejoindre le groupe signfie que vous {access_to_group_private_content_as_well}, y compris les discussion de groupe, les resources du groupe et les billets réservés au groupe.",
|
||||
"Following the group will allow you to be informed of the {group_upcoming_public_events}, whereas joining the group means you will {access_to_group_private_content_as_well}, including group discussions, group resources and members-only posts.": "Suivre le groupe vous permettra d'être informé·e des {group_upcoming_public_events}, alors que rejoindre le groupe signfie que vous {access_to_group_private_content_as_well}, y compris les discussion de groupe, les resources du groupe et les billets réservés au groupe.",
|
||||
"Followings": "Abonnements",
|
||||
"Follows us": "Nous suit",
|
||||
"Follows us, pending approval": "Nous suit, en attente de validation",
|
||||
@@ -467,13 +467,13 @@
|
||||
"From the {startDate} to the {endDate}": "Du {startDate} au {endDate}",
|
||||
"From yourself": "De vous",
|
||||
"Fully accessible with a wheelchair": "Entièrement accessible avec un fauteuil roulant",
|
||||
"Gather ⋅ Organize ⋅ Mobilize": "Rassembler ⋅ Organiser ⋅ Mobiliser",
|
||||
"Gather · Organize · Mobilize": "Rassembler · Organiser · Mobiliser",
|
||||
"General": "Général",
|
||||
"General information": "Informations générales",
|
||||
"General settings": "Paramètres généraux",
|
||||
"Geolocate me": "Me géolocaliser",
|
||||
"Geolocation was not determined in time.": "La localisation n'a pas été déterminée à temps.",
|
||||
"Get informed of the upcoming public events": "Soyez informé⋅e des événements publics à venir",
|
||||
"Get informed of the upcoming public events": "Soyez informé·e des événements publics à venir",
|
||||
"Getting location": "Récupération de la position",
|
||||
"Getting there": "S'y rendre",
|
||||
"Glossary": "Glossaire",
|
||||
@@ -482,7 +482,7 @@
|
||||
"Go!": "Go !",
|
||||
"Google Meet": "Google Meet",
|
||||
"Group": "Groupe",
|
||||
"Group Followers": "Abonné⋅es au groupe",
|
||||
"Group Followers": "Abonné·es au groupe",
|
||||
"Group Members": "Membres du groupe",
|
||||
"Group URL": "URL du groupe",
|
||||
"Group activity": "Activité des groupes",
|
||||
@@ -520,8 +520,8 @@
|
||||
"I participate": "Je participe",
|
||||
"I want to allow people to participate without an account.": "Je veux permettre aux gens de participer sans avoir un compte.",
|
||||
"I want to approve every participation request": "Je veux approuver chaque demande de participation",
|
||||
"I've been mentionned in a comment under an event": "J'ai été mentionné⋅e dans un commentaire sous un événement",
|
||||
"I've been mentionned in a group discussion": "J'ai été mentionné⋅e dans une discussion d'un groupe",
|
||||
"I've been mentionned in a comment under an event": "J'ai été mentionné·e dans un commentaire sous un événement",
|
||||
"I've been mentionned in a group discussion": "J'ai été mentionné·e dans une discussion d'un groupe",
|
||||
"I've clicked on X, then on Y": "J'ai cliqué sur X, puis sur Y",
|
||||
"ICS feed for events": "Flux ICS pour les événements",
|
||||
"ICS/WebCal Feed": "Flux ICS/WebCal",
|
||||
@@ -535,7 +535,7 @@
|
||||
"If this identity is the only administrator of some groups, you need to delete them before being able to delete this identity.": "Si cette identité est la seule administratrice de certains groupes, vous devez les supprimer avant de pouvoir supprimer cette identité.",
|
||||
"If you are being asked for your federated indentity, it's composed of your username and your instance. For instance, the federated identity for your first profile is:": "Si l'on vous demande votre identité fédérée, elle est composée de votre nom d'utilisateur·ice et de votre instance. Par exemple, l'identité fédérée de votre premier profil est :",
|
||||
"If you have opted for manual validation of participants, Mobilizon will send you an email to inform you of new participations to be processed. You can choose the frequency of these notifications below.": "Si vous avez opté pour la validation manuelle des participantes, Mobilizon vous enverra un email pour vous informer des nouvelles participations à traiter. Vous pouvez choisir la fréquence de ces notifications ci-dessous.",
|
||||
"If you want, you may send a message to the event organizer here.": "Si vous le désirez, vous pouvez laisser un message pour l'organisateur⋅ice de l'événement ci-dessous.",
|
||||
"If you want, you may send a message to the event organizer here.": "Si vous le désirez, vous pouvez laisser un message pour l'organisateur·ice de l'événement ci-dessous.",
|
||||
"Ignore": "Ignorer",
|
||||
"Illustration picture for “{category}” by {author} on {source} ({license})": "Image d'illustration pour “{category}” par {author} sur {source} ({license})",
|
||||
"In person": "En personne",
|
||||
@@ -640,7 +640,7 @@
|
||||
"Member": "Membre",
|
||||
"Members": "Membres",
|
||||
"Members-only post": "Billet reservé aux membres",
|
||||
"Membership requests will be approved by a group moderator": "Les demandes d'adhésion seront approuvées par un⋅e modérateur⋅ice du groupe",
|
||||
"Membership requests will be approved by a group moderator": "Les demandes d'adhésion seront approuvées par un·e modérateur·ice du groupe",
|
||||
"Memberships": "Adhésions",
|
||||
"Mentions": "Mentions",
|
||||
"Message": "Message",
|
||||
@@ -704,7 +704,7 @@
|
||||
"No event found at this address": "Aucun événement trouvé à cette addresse",
|
||||
"No events found": "Aucun événement trouvé",
|
||||
"No events found for {search}": "Aucun événement trouvé pour {search}",
|
||||
"No follower matches the filters": "Aucun⋅e abonné⋅e ne correspond aux filtres",
|
||||
"No follower matches the filters": "Aucun·e abonné·e ne correspond aux filtres",
|
||||
"No group found": "Aucun groupe trouvé",
|
||||
"No group matches the filters": "Aucun groupe ne correspond aux filtres",
|
||||
"No group member found": "Aucun membre du groupe trouvé",
|
||||
@@ -719,7 +719,7 @@
|
||||
"No instances match this filter. Try resetting filter fields?": "Aucune instance ne correspond à ce filtre. Essayer de remettre à zéro les champs des filtres ?",
|
||||
"No languages found": "Aucune langue trouvée",
|
||||
"No member matches the filters": "Aucun·e membre ne correspond aux filtres",
|
||||
"No members found": "Aucun⋅e membre trouvé⋅e",
|
||||
"No members found": "Aucun·e membre trouvé·e",
|
||||
"No memberships found": "Aucune adhésion trouvée",
|
||||
"No message": "Pas de message",
|
||||
"No moderation logs yet": "Pas encore de journaux de modération",
|
||||
@@ -729,8 +729,8 @@
|
||||
"No organized events found": "Aucun événement organisé trouvé",
|
||||
"No organized events listed": "Aucun événement organisé listé",
|
||||
"No participant matches the filters": "Aucun·e participant·e ne correspond aux filtres",
|
||||
"No participant to approve|Approve participant|Approve {number} participants": "Aucun⋅e participant⋅e à valider|Valider le ou la participant⋅e|Valider {number} participant⋅es",
|
||||
"No participant to reject|Reject participant|Reject {number} participants": "Aucun⋅e participant⋅e à refuser|Refuser le ou la participant⋅e|Refuser {number} participant⋅es",
|
||||
"No participant to approve|Approve participant|Approve {number} participants": "Aucun·e participant·e à valider|Valider le ou la participant·e|Valider {number} participant·es",
|
||||
"No participant to reject|Reject participant|Reject {number} participants": "Aucun·e participant·e à refuser|Refuser le ou la participant·e|Refuser {number} participant·es",
|
||||
"No participations listed": "Aucune participation listée",
|
||||
"No posts found": "Aucun billet trouvé",
|
||||
"No posts yet": "Pas encore de billets",
|
||||
@@ -745,8 +745,8 @@
|
||||
"No results found": "Aucun résultat trouvé",
|
||||
"No results found for {search}": "Aucun résultat trouvé pour {search}",
|
||||
"No rules defined yet.": "Pas de règles définies pour le moment.",
|
||||
"No user matches the filter": "Aucun⋅e utilisateur⋅ice ne correspond au filtre",
|
||||
"No user matches the filters": "Aucun⋅e utilisateur⋅ice ne correspond aux filtres",
|
||||
"No user matches the filter": "Aucun·e utilisateur·ice ne correspond au filtre",
|
||||
"No user matches the filters": "Aucun·e utilisateur·ice ne correspond aux filtres",
|
||||
"None": "Aucun",
|
||||
"Not accessible with a wheelchair": "Non accessible avec un fauteuil roulant",
|
||||
"Not approved": "Non approuvé·e·s",
|
||||
@@ -757,7 +757,7 @@
|
||||
"Notification settings": "Paramètres des notifications",
|
||||
"Notifications": "Notifications",
|
||||
"Notifications for manually approved participations to an event": "Notifications pour l'approbation manuelle des participations à un événement",
|
||||
"Notify participants": "Notifier les participant⋅es",
|
||||
"Notify participants": "Notifier les participant·es",
|
||||
"Notify the user of the change": "Notifier l'utilisateur du changement",
|
||||
"Now, create your first profile:": "Maintenant, créez votre premier profil :",
|
||||
"Number of members": "Nombre de membres",
|
||||
@@ -780,13 +780,13 @@
|
||||
"Only accessible through link (private)": "Uniquement accessible par lien (privé)",
|
||||
"Only accessible to members of the group": "Accessible uniquement aux membres du groupe",
|
||||
"Only alphanumeric lowercased characters and underscores are supported.": "Seuls les caractères alphanumériques minuscules et les tirets bas sont acceptés.",
|
||||
"Only group members can access discussions": "Seul⋅es les membres du groupes peuvent accéder aux discussions",
|
||||
"Only group moderators can create, edit and delete events.": "Seule⋅s les modérateur⋅ices de groupe peuvent créer, éditer et supprimer des événements.",
|
||||
"Only group members can access discussions": "Seul·es les membres du groupes peuvent accéder aux discussions",
|
||||
"Only group moderators can create, edit and delete events.": "Seule·s les modérateur·ices de groupe peuvent créer, éditer et supprimer des événements.",
|
||||
"Only group moderators can create, edit and delete posts.": "Seul·e·s les modérateur·rice·s du groupe peuvent créer, éditer et supprimer des billets.",
|
||||
"Only registered users may fetch remote events from their URL.": "Seul⋅es les utilisateur⋅ices enregistré⋅es peuvent récupérer des événements depuis leur URL.",
|
||||
"Only registered users may fetch remote events from their URL.": "Seul·es les utilisateur·ices enregistré·es peuvent récupérer des événements depuis leur URL.",
|
||||
"Open": "Ouvert",
|
||||
"Open a topic on our forum": "Ouvrir un sujet sur notre forum",
|
||||
"Open an issue on our bug tracker (advanced users)": "Ouvrir un ticket sur notre système de suivi des bugs (utilisateur⋅ices avancé⋅es)",
|
||||
"Open an issue on our bug tracker (advanced users)": "Ouvrir un ticket sur notre système de suivi des bugs (utilisateur·ices avancé·es)",
|
||||
"Open main menu": "Ouvrir le menu principal",
|
||||
"Open user menu": "Ouvrir le menu utilisateur",
|
||||
"Opened reports": "Signalements ouverts",
|
||||
@@ -796,24 +796,24 @@
|
||||
"Organized by": "Organisé par",
|
||||
"Organized by {name}": "Organisé par {name}",
|
||||
"Organized events": "Événements organisés",
|
||||
"Organizer": "Organisateur⋅ice",
|
||||
"Organizer": "Organisateur·ice",
|
||||
"Organizer notifications": "Notifications pour organisateur·rice",
|
||||
"Organizers": "Organisateur⋅ices",
|
||||
"Organizers": "Organisateur·ices",
|
||||
"Other": "Autre",
|
||||
"Other actions": "Autres actions",
|
||||
"Other notification options:": "Autres options de notification :",
|
||||
"Other software may also support this.": "D'autres logiciels peuvent également supporter cette fonctionnalité.",
|
||||
"Other users with the same IP address": "Autres utilisateur⋅ices avec la même adresse IP",
|
||||
"Other users with the same email domain": "Autres utilisateur⋅ices avec le même domaine de courriel",
|
||||
"Otherwise this identity will just be removed from the group administrators.": "Sinon cette identité sera juste supprimée des administrateur⋅rice·s du groupe.",
|
||||
"Other users with the same IP address": "Autres utilisateur·ices avec la même adresse IP",
|
||||
"Other users with the same email domain": "Autres utilisateur·ices avec le même domaine de courriel",
|
||||
"Otherwise this identity will just be removed from the group administrators.": "Sinon cette identité sera juste supprimée des administrateur·rice·s du groupe.",
|
||||
"Owncast": "Owncast",
|
||||
"Page": "Page",
|
||||
"Page limited to my group (asks for auth)": "Accès limité à mon groupe (demande authentification)",
|
||||
"Page not found": "Page non trouvée",
|
||||
"Parent folder": "Dossier parent",
|
||||
"Partially accessible with a wheelchair": "Partiellement accessible avec un fauteuil roulant",
|
||||
"Participant": "Participant⋅e",
|
||||
"Participants": "Participant⋅e⋅s",
|
||||
"Participant": "Participant·e",
|
||||
"Participants": "Participant·e·s",
|
||||
"Participants to {eventTitle}": "Participant·es à {eventTitle}",
|
||||
"Participate": "Participer",
|
||||
"Participate using your email address": "Participer en utilisant votre adresse email",
|
||||
@@ -839,7 +839,7 @@
|
||||
"Pick an instance": "Choisir une instance",
|
||||
"Please add as many details as possible to help identify the problem.": "Merci d'ajouter un maximum de détails afin d'aider à identifier le problème.",
|
||||
"Please check your spam folder if you didn't receive the email.": "Merci de vérifier votre dossier des indésirables si vous n'avez pas reçu l'email.",
|
||||
"Please contact this instance's Mobilizon admin if you think this is a mistake.": "Veuillez contacter l'administrateur⋅rice de cette instance Mobilizon si vous pensez qu’il s’agit d’une erreur.",
|
||||
"Please contact this instance's Mobilizon admin if you think this is a mistake.": "Veuillez contacter l'administrateur·rice de cette instance Mobilizon si vous pensez qu’il s’agit d’une erreur.",
|
||||
"Please do not use it in any real way.": "Merci de ne pas en faire une utilisation réelle.",
|
||||
"Please enter your password to confirm this action.": "Merci d'entrer votre mot de passe pour confirmer cette action.",
|
||||
"Please make sure the address is correct and that the page hasn't been moved.": "Assurez‐vous que l’adresse est correcte et que la page n’a pas été déplacée.",
|
||||
@@ -1055,7 +1055,7 @@
|
||||
"The URL where the event live can be watched again after it has ended": "L'URL où le direct de l'événement peut être visionné à nouveau une fois terminé",
|
||||
"The Zoom video teleconference URL": "L'URL de visio-conférence Zoom",
|
||||
"The account's email address was changed. Check your emails to verify it.": "L'adresse email du compte a été modifiée. Vérifiez vos emails pour confirmer le changement.",
|
||||
"The actual number of participants may differ, as this event is hosted on another instance.": "Le nombre réel de participant⋅e⋅s peut être différent, car cet événement provient d'une autre instance.",
|
||||
"The actual number of participants may differ, as this event is hosted on another instance.": "Le nombre réel de participant·e·s peut être différent, car cet événement provient d'une autre instance.",
|
||||
"The calc will be created on {service}": "Le calc sera créé sur {service}",
|
||||
"The content came from another server. Transfer an anonymous copy of the report?": "Le contenu provient d'une autre instance. Transférer une copie anonyme du signalement ?",
|
||||
"The device code is incorrect or no longer valid.": "Le code de l'appareil est incorrect ou n'est plus valide.",
|
||||
@@ -1069,9 +1069,9 @@
|
||||
"The event is fully online": "L'événement est entièrement en ligne",
|
||||
"The event live video contains subtitles": "Le direct vidéo de l'événement contient des sous-titres",
|
||||
"The event live video does not contain subtitles": "Le direct vidéo de l'événement ne contient pas de sous-titres",
|
||||
"The event organiser has chosen to validate manually participations. Do you want to add a little note to explain why you want to participate to this event?": "L'organisateur⋅ice de l'événement a choisi de valider manuellement les demandes de participation. Voulez-vous ajouter un petit message pour expliquer pourquoi vous souhaitez participer à cet événement ?",
|
||||
"The event organizer didn't add any description.": "L'organisateur⋅ice de l'événement n'a pas ajouté de description.",
|
||||
"The event organizer manually approves participations. Since you've chosen to participate without an account, please explain why you want to participate to this event.": "L'organisateur⋅ice de l'événement valide les participations manuellement. Comme vous avez choisi de participer sans compte, merci d'expliquer pourquoi vous voulez participer à cet événement.",
|
||||
"The event organiser has chosen to validate manually participations. Do you want to add a little note to explain why you want to participate to this event?": "L'organisateur·ice de l'événement a choisi de valider manuellement les demandes de participation. Voulez-vous ajouter un petit message pour expliquer pourquoi vous souhaitez participer à cet événement ?",
|
||||
"The event organizer didn't add any description.": "L'organisateur·ice de l'événement n'a pas ajouté de description.",
|
||||
"The event organizer manually approves participations. Since you've chosen to participate without an account, please explain why you want to participate to this event.": "L'organisateur·ice de l'événement valide les participations manuellement. Comme vous avez choisi de participer sans compte, merci d'expliquer pourquoi vous voulez participer à cet événement.",
|
||||
"The event title will be ellipsed.": "Le titre de l'événement sera ellipsé.",
|
||||
"The event will show as attributed to this group.": "L'événement sera affiché comme étant attribué à ce groupe.",
|
||||
"The event will show as attributed to this profile.": "L'événement sera affiché comme attribué à ce profil.",
|
||||
@@ -1082,7 +1082,7 @@
|
||||
"The events you created are not shown here.": "Les événements que vous avez créé ne s'affichent pas ici.",
|
||||
"The following user's profiles will be deleted, with all their data:": "Les profils suivants de l'utilisateur·ice seront supprimés, avec toutes leurs données :",
|
||||
"The geolocation prompt was denied.": "La demande de localisation a été refusée.",
|
||||
"The group can now be joined by anyone, but new members need to be approved by an administrator.": "Le groupe peut maintenant être rejoint par n'importe qui, mais les nouvelles et nouveaux membres doivent être approuvées par un⋅e modérateur⋅ice.",
|
||||
"The group can now be joined by anyone, but new members need to be approved by an administrator.": "Le groupe peut maintenant être rejoint par n'importe qui, mais les nouvelles et nouveaux membres doivent être approuvées par un·e modérateur·ice.",
|
||||
"The group can now be joined by anyone.": "Le groupe peut maintenant être rejoint par n'importe qui.",
|
||||
"The group can now only be joined with an invite.": "Le groupe peut maintenant être rejoint uniquement sur invitation.",
|
||||
"The group will be publicly listed in search results and may be suggested in the explore section. Only public informations will be shown on it's page.": "Le groupe sera listé publiquement dans les résultats de recherche et pourra être suggéré sur la page « Explorer ». Seules les informations publiques seront affichées sur sa page.",
|
||||
@@ -1104,15 +1104,15 @@
|
||||
"The post {post} was updated by {profile}.": "Le billet {post} a été mis à jour par {profile}.",
|
||||
"The provided application was not found.": "L'application fournie n'a pas été trouvée.",
|
||||
"The report contents (eventual comments and event) and the reported profile details will be transmitted to Akismet.": "Les contenus du signalement (les éventuels commentaires et événement) et les détails du profil signalé seront transmis à Akismet.",
|
||||
"The report will be sent to the moderators of your instance. You can explain why you report this content below.": "Le signalement sera envoyé aux modérateur⋅ices de votre instance. Vous pouvez expliquer pourquoi vous signalez ce contenu ci-dessous.",
|
||||
"The report will be sent to the moderators of your instance. You can explain why you report this content below.": "Le signalement sera envoyé aux modérateur·ices de votre instance. Vous pouvez expliquer pourquoi vous signalez ce contenu ci-dessous.",
|
||||
"The selected picture is too heavy. You need to select a file smaller than {size}.": "L'image sélectionnée est trop lourde. Vous devez sélectionner un fichier de moins de {size}.",
|
||||
"The technical details of the error can help developers solve the problem more easily. Please add them to your feedback.": "Les détails techniques de l'erreur peuvent aider les développeur⋅ices à résoudre le problème plus facilement. Merci de les inclure dans vos retours.",
|
||||
"The user has been disabled": "L'utilisateur⋅ice a été désactivé",
|
||||
"The technical details of the error can help developers solve the problem more easily. Please add them to your feedback.": "Les détails techniques de l'erreur peuvent aider les développeur·ices à résoudre le problème plus facilement. Merci de les inclure dans vos retours.",
|
||||
"The user has been disabled": "L'utilisateur·ice a été désactivé",
|
||||
"The videoconference will be created on {service}": "La visio-conférence sera créée sur {service}",
|
||||
"The {default_privacy_policy} will be used. They will be translated in the user's language.": "La {default_privacy_policy} sera utilisée. Elle sera traduite dans la langue de l'utilisateur·rice.",
|
||||
"The {default_terms} will be used. They will be translated in the user's language.": "Les {default_terms} seront utilisées. Elles seront traduites dans la langue de l'utilisateur⋅rice.",
|
||||
"The {default_terms} will be used. They will be translated in the user's language.": "Les {default_terms} seront utilisées. Elles seront traduites dans la langue de l'utilisateur·rice.",
|
||||
"Theme": "Thème",
|
||||
"There are {participants} participants.": "Il n'y a qu'un⋅e participant⋅e. | Il y a {participants} participant⋅es.",
|
||||
"There are {participants} participants.": "Il n'y a qu'un·e participant·e. | Il y a {participants} participant·es.",
|
||||
"There is no activity yet. Start doing some things to see activity appear here.": "Il n'y a pas encore d'activité. Commencez par effectuer des actions pour voir des éléments s'afficher ici.",
|
||||
"There will be no way to recover your data.": "Il n'y aura aucun moyen de récupérer vos données.",
|
||||
"There will be no way to restore the profile's data!": "Il n'y aura aucun moyen de restorer les données du profil !",
|
||||
@@ -1120,9 +1120,9 @@
|
||||
"There's no discussions yet": "Il n'y a pas encore de discussions",
|
||||
"These apps can access your account through the API. If you see here apps that you don't recognize, that don't work as expected or that you don't use anymore, you can revoke their access.": "Ces applications peuvent accéder à votre compte via l'API. Si vous voyez ici des applications que vous ne reconnaissez pas, qui ne fonctionnent pas comme prévu ou que vous n'utilisez plus, vous pouvez révoquer leur accès.",
|
||||
"These events may interest you": "Ces événements peuvent vous intéresser",
|
||||
"These feeds contain event data for the events for which any of your profiles is a participant or creator. You should keep these private. You can find feeds for specific profiles on each profile edition page.": "Ces flux contiennent des informations sur les événements pour lesquels n'importe lequel de vos profils est un⋅e participant⋅e ou un⋅e créateur⋅ice. Vous devriez les garder privés. Vous pouvez trouver des flux spécifiques à chaque profil sur la page d'édition des profils.",
|
||||
"These feeds contain event data for the events for which this specific profile is a participant or creator. You should keep these private. You can find feeds for all of your profiles into your notification settings.": "Ces flux contiennent des informations sur les événements pour lesquels ce profil spécifique est un⋅e participant⋅e ou un⋅e créateur⋅ice. Vous devriez les garder privés. Vous pouvez trouver des flux pour l'ensemble de vos profils dans vos paramètres de notification.",
|
||||
"This Mobilizon instance and this event organizer allows anonymous participations, but requires validation through email confirmation.": "Cette instance Mobilizon et l'organisateur⋅ice de l'événement autorise les participations anonymes, mais requiert une validation à travers une confirmation par email.",
|
||||
"These feeds contain event data for the events for which any of your profiles is a participant or creator. You should keep these private. You can find feeds for specific profiles on each profile edition page.": "Ces flux contiennent des informations sur les événements pour lesquels n'importe lequel de vos profils est un·e participant·e ou un·e créateur·ice. Vous devriez les garder privés. Vous pouvez trouver des flux spécifiques à chaque profil sur la page d'édition des profils.",
|
||||
"These feeds contain event data for the events for which this specific profile is a participant or creator. You should keep these private. You can find feeds for all of your profiles into your notification settings.": "Ces flux contiennent des informations sur les événements pour lesquels ce profil spécifique est un·e participant·e ou un·e créateur·ice. Vous devriez les garder privés. Vous pouvez trouver des flux pour l'ensemble de vos profils dans vos paramètres de notification.",
|
||||
"This Mobilizon instance and this event organizer allows anonymous participations, but requires validation through email confirmation.": "Cette instance Mobilizon et l'organisateur·ice de l'événement autorise les participations anonymes, mais requiert une validation à travers une confirmation par email.",
|
||||
"This URL doesn't seem to be valid": "Cette URL ne semble pas être valide",
|
||||
"This URL is not supported": "Cette URL n'est pas supportée",
|
||||
"This application asks for the following permissions:": "Cette application demande les autorisations suivantes :",
|
||||
@@ -1191,14 +1191,14 @@
|
||||
"This is like your federated username (<code>{username}</code>) for groups. It will allow the group to be found on the federation, and is guaranteed to be unique.": "C'est comme votre adresse fédérée (<code>{username}</code>) pour les groupes. Cela permettra au groupe d'être trouvable sur la fédération, et est garanti d'être unique.",
|
||||
"This is like your federated username ({username}) for groups. It will allow the group to be found on the federation, and is guaranteed to be unique.": "C'est comme votre adresse fédérée ({username}) pour les groupes. Cela permettra au groupe d'être trouvable sur la fédération, et est garanti d'être unique.",
|
||||
"This month": "Ce mois-ci",
|
||||
"This post is accessible only for members. You have access to it for moderation purposes only because you are an instance moderator.": "Ce billet est accessible uniquement aux membres. Vous y avez accès à des fins de modération car vous êtes modérateur⋅ice de l'instance.",
|
||||
"This post is accessible only for members. You have access to it for moderation purposes only because you are an instance moderator.": "Ce billet est accessible uniquement aux membres. Vous y avez accès à des fins de modération car vous êtes modérateur·ice de l'instance.",
|
||||
"This post is accessible only through it's link. Be careful where you post this link.": "Ce billet est accessible uniquement à travers son lien. Faites attention où vous le diffusez.",
|
||||
"This profile is from another instance, the informations shown here may be incomplete.": "Ce profil provient d'une autre instance, les informations montrées ici peuvent être incomplètes.",
|
||||
"This profile is located on this instance, so you need to {access_the_corresponding_account} to suspend it.": "Ce profil se situe sur cette instance, vous devez donc {access_the_corresponding_account} afin de le suspendre.",
|
||||
"This profile was not found": "Ce profil n'a pas été trouvé",
|
||||
"This setting will be used to display the website and send you emails in the correct language.": "Ce paramètre sera utilisé pour l'affichage du site et pour vous envoyer des courriels dans la bonne langue.",
|
||||
"This user doesn't have any profiles": "Cet utilisateur⋅ice n'a aucun profil",
|
||||
"This user was not found": "Cet utilisateur⋅ice n'a pas été trouvé⋅e",
|
||||
"This user doesn't have any profiles": "Cet utilisateur·ice n'a aucun profil",
|
||||
"This user was not found": "Cet utilisateur·ice n'a pas été trouvé·e",
|
||||
"This website isn't moderated and the data that you enter will be automatically destroyed every day at 00:01 (Paris timezone).": "Ce site n’est pas modéré et les données que vous y rentrerez seront automatiquement détruites tous les jours à 00:01 (heure de Paris).",
|
||||
"This week": "Cette semaine",
|
||||
"This weekend": "Ce week-end",
|
||||
@@ -1243,12 +1243,12 @@
|
||||
"Underline": "Souligné",
|
||||
"Undo": "Annuler",
|
||||
"Unfollow": "Ne plus suivre",
|
||||
"Unfortunately, your participation request was rejected by the organizers.": "Malheureusement, votre demande de participation a été refusée par les organisateur⋅ices.",
|
||||
"Unfortunately, your participation request was rejected by the organizers.": "Malheureusement, votre demande de participation a été refusée par les organisateur·ices.",
|
||||
"Unknown": "Inconnu",
|
||||
"Unknown actor": "Acteur inconnu",
|
||||
"Unknown error.": "Erreur inconnue.",
|
||||
"Unknown value for the openness setting.": "Valeur inconnue pour le paramètre d'ouverture.",
|
||||
"Unlogged participation": "Participation non connecté⋅e",
|
||||
"Unlogged participation": "Participation non connecté·e",
|
||||
"Unsaved changes": "Modifications non enregistrées",
|
||||
"Unsubscribe to browser push notifications": "Se désinscrire des notifications push du navigateur",
|
||||
"Unsuspend": "Annuler la suspension",
|
||||
@@ -1275,10 +1275,10 @@
|
||||
"Uploaded media total size": "Taille totale des médias téléversés",
|
||||
"Use my location": "Utiliser ma position",
|
||||
"User": "Utilisateur·rice",
|
||||
"User settings": "Paramètres utilisateur⋅ices",
|
||||
"User settings": "Paramètres utilisateur·ices",
|
||||
"User suspended and report resolved": "Utilisateur suspendu et signalement résolu",
|
||||
"Username": "Identifiant",
|
||||
"Users": "Utilisateur⋅rice⋅s",
|
||||
"Users": "Utilisateur·rice·s",
|
||||
"Validating account": "Validation du compte",
|
||||
"Validating email": "Validation de l'email",
|
||||
"Video Conference": "Visio-conférence",
|
||||
@@ -1377,7 +1377,7 @@
|
||||
"You deleted the post {post}.": "Vous avez supprimé le billet {post}.",
|
||||
"You deleted the resource {resource}.": "Vous avez supprimé la ressource {resource}.",
|
||||
"You demoted the member {member} to an unknown role.": "Vous avez rétrogradé le membre {member} à un role inconnu.",
|
||||
"You demoted {member} to moderator.": "Vous avez rétrogradé {member} en tant que modérateur⋅ice.",
|
||||
"You demoted {member} to moderator.": "Vous avez rétrogradé {member} en tant que modérateur·ice.",
|
||||
"You demoted {member} to simple member.": "Vous avez rétrogradé {member} en tant que simple membre.",
|
||||
"You didn't create or join any event yet.": "Vous n'avez pas encore créé ou rejoint d'événement.",
|
||||
"You don't follow any instances yet.": "Vous ne suivez aucune instance pour le moment.",
|
||||
@@ -1386,7 +1386,7 @@
|
||||
"You have attended {count} events in the past.": "Vous n'avez participé à aucun événement par le passé.|Vous avez participé à un événement par le passé.|Vous avez participé à {count} événements par le passé.",
|
||||
"You have been invited by {invitedBy} to the following group:": "Vous avez été invité par {invitedBy} à rejoindre le groupe suivant :",
|
||||
"You have been logged-out": "Vous avez été déconnecté·e",
|
||||
"You have been removed from this group's members.": "Vous avez été exclu⋅e des membres de ce groupe.",
|
||||
"You have been removed from this group's members.": "Vous avez été exclu·e des membres de ce groupe.",
|
||||
"You have cancelled your participation": "Vous avez annulé votre participation",
|
||||
"You have one event in {days} days.": "Vous n'avez pas d'événements dans {days} jours | Vous avez un événement dans {days} jours. | Vous avez {count} événements dans {days} jours",
|
||||
"You have one event today.": "Vous n'avez pas d'événement aujourd'hui | Vous avez un événement aujourd'hui. | Vous avez {count} événements aujourd'hui",
|
||||
@@ -1398,7 +1398,7 @@
|
||||
"You may clear all participation information for this device with the buttons below.": "Vous pouvez effacer toutes les informations de participation pour cet appareil avec les boutons ci-dessous.",
|
||||
"You may now close this page or {return_to_the_homepage}.": "Vous pouvez maintenant fermer cette page ou {return_to_the_homepage}.",
|
||||
"You may now close this window, or {return_to_event}.": "Vous pouvez maintenant fermer cette fenêtre, ou bien {return_to_event}.",
|
||||
"You may show some members as contacts.": "Vous pouvez afficher certain⋅es membres en tant que contacts.",
|
||||
"You may show some members as contacts.": "Vous pouvez afficher certain·es membres en tant que contacts.",
|
||||
"You moved the folder {resource} into {new_path}.": "Vous avez déplacé le dossier {resource} dans {new_path}.",
|
||||
"You moved the folder {resource} to the root folder.": "Vous avez déplacé le dossier {resource} dans le dossier racine.",
|
||||
"You moved the resource {resource} into {new_path}.": "Vous avez déplacé la ressource {resource} dans {new_path}.",
|
||||
@@ -1407,8 +1407,8 @@
|
||||
"You need to provide the following code to your application. It will only be valid for a few minutes.": "Vous devez fournir le code suivant à votre application. Il sera seulement valide pendant quelques minutes.",
|
||||
"You posted a comment on the event {event}.": "Vous avez posté un commentaire sur l'événement {event}.",
|
||||
"You promoted the member {member} to an unknown role.": "Vous avez promu le ou la membre {member} à un role inconnu.",
|
||||
"You promoted {member} to administrator.": "Vous avez promu {member} en tant qu'adminstrateur⋅ice.",
|
||||
"You promoted {member} to moderator.": "Vous avez promu {member} en tant que modérateur⋅ice.",
|
||||
"You promoted {member} to administrator.": "Vous avez promu {member} en tant qu'adminstrateur·ice.",
|
||||
"You promoted {member} to moderator.": "Vous avez promu {member} en tant que modérateur·ice.",
|
||||
"You rejected {member}'s membership request.": "Vous avez rejeté la demande d'adhésion de {member}.",
|
||||
"You renamed the discussion from {old_discussion} to {discussion}.": "Vous avez renommé la discussion {old_discussion} en {discussion}.",
|
||||
"You renamed the folder from {old_resource_title} to {resource}.": "Vous avez renommé le dossier {old_resource_title} en {resource}.",
|
||||
@@ -1420,14 +1420,14 @@
|
||||
"You updated the group {group}.": "Vous avez mis à jour le groupe {group}.",
|
||||
"You updated the member {member}.": "Vous avez mis à jour le ou la membre {member}.",
|
||||
"You updated the post {post}.": "Vous avez mis à jour le billet {post}.",
|
||||
"You were demoted to an unknown role by {profile}.": "Vous avez été rétrogradé⋅e à un role inconnu par {profile}.",
|
||||
"You were demoted to moderator by {profile}.": "Vous avez été rétrogradé⋅e modérateur⋅ice par {profile}.",
|
||||
"You were demoted to simple member by {profile}.": "Vous avez été rétrogradé⋅e simple membre par {profile}.",
|
||||
"You were promoted to administrator by {profile}.": "Vous avez été promu⋅e administrateur⋅ice par {profile}.",
|
||||
"You were promoted to an unknown role by {profile}.": "Vous avez été promu⋅e à un role inconnu par {profile}.",
|
||||
"You were promoted to moderator by {profile}.": "Vous avez été promu⋅e modérateur⋅ice par {profile}.",
|
||||
"You were demoted to an unknown role by {profile}.": "Vous avez été rétrogradé·e à un role inconnu par {profile}.",
|
||||
"You were demoted to moderator by {profile}.": "Vous avez été rétrogradé·e modérateur·ice par {profile}.",
|
||||
"You were demoted to simple member by {profile}.": "Vous avez été rétrogradé·e simple membre par {profile}.",
|
||||
"You were promoted to administrator by {profile}.": "Vous avez été promu·e administrateur·ice par {profile}.",
|
||||
"You were promoted to an unknown role by {profile}.": "Vous avez été promu·e à un role inconnu par {profile}.",
|
||||
"You were promoted to moderator by {profile}.": "Vous avez été promu·e modérateur·ice par {profile}.",
|
||||
"You will be able to add an avatar and set other options in your account settings.": "Vous pourrez ajouter un avatar et définir d'autres options dans les paramètres de votre compte.",
|
||||
"You will be redirected to the original instance": "Vous allez être redirigé⋅e vers l'instance d'origine",
|
||||
"You will be redirected to the original instance": "Vous allez être redirigé·e vers l'instance d'origine",
|
||||
"You will find here all the events you have created or of which you are a participant, as well as events organized by groups you follow or are a member of.": "Vous trouverez ici tous les événements que vous avez créé ou dont vous êtes un·e participant·e, ainsi que les événements organisés par les groupes que vous suivez ou dont vous êtes membre.",
|
||||
"You will receive notifications about this group's public activity depending on %{notification_settings}.": "Vous recevrez des notifications à propos de l'activité publique de ce groupe en fonction de %{notification_settings}.",
|
||||
"You wish to participate to the following event": "Vous souhaitez participer à l'événement suivant",
|
||||
@@ -1472,7 +1472,7 @@
|
||||
"Zoom": "Zoom",
|
||||
"Zoom in": "Zoomer",
|
||||
"Zoom out": "Dézoomer",
|
||||
"[This comment has been deleted by it's author]": "[Ce commentaire a été supprimé par son auteur⋅rice]",
|
||||
"[This comment has been deleted by it's author]": "[Ce commentaire a été supprimé par son auteur·rice]",
|
||||
"[This comment has been deleted]": "[Ce commentaire a été supprimé]",
|
||||
"[deleted]": "[supprimé]",
|
||||
"a non-existent report": "un signalement non-existant",
|
||||
@@ -1517,9 +1517,9 @@
|
||||
"{available}/{capacity} available places": "Pas de places restantes|{available}/{capacity} places restantes|{available}/{capacity} places restantes",
|
||||
"{count} events": "{count} événements",
|
||||
"{count} km": "{count} km",
|
||||
"{count} members": "Aucun membre|Un⋅e membre|{count} membres",
|
||||
"{count} members or followers": "Aucun⋅e membre ou abonné⋅e|Un⋅e membre ou abonné⋅e|{count} membres ou abonné⋅es",
|
||||
"{count} participants": "Aucun⋅e participant⋅e | Un⋅e participant⋅e | {count} participant⋅e⋅s",
|
||||
"{count} members": "Aucun membre|Un·e membre|{count} membres",
|
||||
"{count} members or followers": "Aucun·e membre ou abonné·e|Un·e membre ou abonné·e|{count} membres ou abonné·es",
|
||||
"{count} participants": "Aucun·e participant·e | Un·e participant·e | {count} participant·e·s",
|
||||
"{count} requests waiting": "Une demande en attente|{count} demandes en attente",
|
||||
"{eventsCount} events found": "Aucun événement trouvé|Un événement trouvé|{eventsCount} événements trouvés",
|
||||
"{folder} - Resources": "{folder} - Ressources",
|
||||
@@ -1536,7 +1536,7 @@
|
||||
"{member} joined the group.": "{member} a rejoint le groupe.",
|
||||
"{member} rejected the invitation to join the group.": "{member} a refusé l'invitation à se joindre au groupe.",
|
||||
"{member} requested to join the group.": "{member} a demandé à rejoindre le groupe.",
|
||||
"{member} was invited by {profile}.": "{member} a été invité⋅e par {profile}.",
|
||||
"{member} was invited by {profile}.": "{member} a été invité·e par {profile}.",
|
||||
"{moderator} added a note on {report}": "{moderator} a ajouté une note sur {report}",
|
||||
"{moderator} closed {report}": "{moderator} a fermé {report}",
|
||||
"{moderator} deleted an event named \"{title}\"": "{moderator} a supprimé un événement nommé \"{title}\"",
|
||||
@@ -1554,7 +1554,7 @@
|
||||
"{numberOfCategories} selected": "{numberOfCategories} sélectionnées",
|
||||
"{numberOfLanguages} selected": "{numberOfLanguages} sélectionnées",
|
||||
"{number} kilometers": "{number} kilomètres",
|
||||
"{number} members": "Aucun⋅e membre|Un⋅e membre|{number} membres",
|
||||
"{number} members": "Aucun·e membre|Un·e membre|{number} membres",
|
||||
"{number} memberships": "{number} adhésions",
|
||||
"{number} organized events": "Aucun événement organisé|Un événement organisé|{number} événements organisés",
|
||||
"{number} participations": "Aucune participation|Une participation|{number} participations",
|
||||
@@ -1574,7 +1574,7 @@
|
||||
"{profile} deleted the folder {resource}.": "{profile} a supprimé le dossier {resource}.",
|
||||
"{profile} deleted the resource {resource}.": "{profile} a supprimé la ressource {resource}.",
|
||||
"{profile} demoted {member} to an unknown role.": "{profile} a rétrogradé {member} à un role inconnu.",
|
||||
"{profile} demoted {member} to moderator.": "{profile} a rétrogradé {member} en tant que modérateur⋅ice.",
|
||||
"{profile} demoted {member} to moderator.": "{profile} a rétrogradé {member} en tant que modérateur·ice.",
|
||||
"{profile} demoted {member} to simple member.": "{profile} a rétrogradé {member} en tant que simple membre.",
|
||||
"{profile} excluded member {member}.": "{profile} a exclu le ou la membre {member}.",
|
||||
"{profile} joined the the event {event}.": "{profile} a rejoint l'événement {event}.",
|
||||
@@ -1583,9 +1583,9 @@
|
||||
"{profile} moved the resource {resource} into {new_path}.": "{profile} a déplacé la ressource {resource} dans {new_path}.",
|
||||
"{profile} moved the resource {resource} to the root folder.": "{profile} a déplacé la ressource {resource} dans le dossier racine.",
|
||||
"{profile} posted a comment on the event {event}.": "{profile} a posté un commentaire sur l'événement {event}.",
|
||||
"{profile} promoted {member} to administrator.": "{profile} a promu {member} en tant qu'administrateur⋅ice.",
|
||||
"{profile} promoted {member} to administrator.": "{profile} a promu {member} en tant qu'administrateur·ice.",
|
||||
"{profile} promoted {member} to an unknown role.": "{profile} a promu {member} à un role inconnu.",
|
||||
"{profile} promoted {member} to moderator.": "{profile} a promu {member} en tant que modérateur⋅ice.",
|
||||
"{profile} promoted {member} to moderator.": "{profile} a promu {member} en tant que modérateur·ice.",
|
||||
"{profile} quit the group.": "{profile} a quitté le groupe.",
|
||||
"{profile} rejected {member}'s membership request.": "{profile} a rejeté la demande d'adhésion de {member}.",
|
||||
"{profile} renamed the discussion from {old_discussion} to {discussion}.": "{profile} a renommé la discussion {old_discussion} en {discussion}.",
|
||||
@@ -1601,10 +1601,25 @@
|
||||
"{username} was invited to {group}": "{username} a été invité à {group}",
|
||||
"{user}'s follow request was accepted": "La demande de suivi de {user} a été acceptée",
|
||||
"{user}'s follow request was rejected": "La demande de suivi de {user} a été rejetée",
|
||||
"© The OpenStreetMap Contributors": "© Les Contributeur⋅ices OpenStreetMap",
|
||||
"© The OpenStreetMap Contributors": "© Les Contributeur·ices OpenStreetMap",
|
||||
"Go to booking": "Aller à la réservation",
|
||||
"External registration": "Inscription externe",
|
||||
"I want to manage the registration with an external provider": "Je souhaite gérer l'enregistrement auprès d'un fournisseur externe",
|
||||
"External provider URL": "URL du fournisseur externe",
|
||||
"Members will also access private sections like discussions, resources and restricted posts.": "Les membres auront également accès aux section privées comme les discussions, les ressources et les billets restreints."
|
||||
"Members will also access private sections like discussions, resources and restricted posts.": "Les membres auront également accès aux section privées comme les discussions, les ressources et les billets restreints.",
|
||||
"With unknown participants": "Avec des participant·es inconnu·es",
|
||||
"With {participants}": "Avec {participants}",
|
||||
"Conversations": "Conversations",
|
||||
"New private message": "Nouveau message privé",
|
||||
"There's no conversations yet": "Il n'y a pas encore de conversations",
|
||||
"Open conversations": "Ouvrir les conversations",
|
||||
"List of conversations": "Liste des conversations",
|
||||
"Conversation with {participants}": "Conversation avec {participants}",
|
||||
"Delete this conversation": "Supprimer cette conversation",
|
||||
"Are you sure you want to delete this entire conversation?": "Êtes-vous sûr·e de vouloir supprimer l'entièreté de cette conversation ?",
|
||||
"This is a announcement from the organizers of event {event}. You can't reply to it, but you can send a private message to event organizers.": "Ceci est une annonce des organisateur·ices de cet événement {event}. Vous ne pouvez pas y répondre, mais vous pouvez envoyer un nouveau message aux organisateur·ices de l'événement.",
|
||||
"You have access to this conversation as a member of the {group} group": "Vous avez accès à cette conversation en tant que membre du groupe {group}",
|
||||
"Comment from an event announcement": "Commentaire d'une annonce d'événement",
|
||||
"Comment from a private conversation": "Commentaire d'une conversation privée",
|
||||
"I've been mentionned in a conversation": "J'ai été mentionnée dans une conversation"
|
||||
}
|
||||
|
||||
33
js/src/router/conversation.ts
Normal file
33
js/src/router/conversation.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { RouteRecordRaw } from "vue-router";
|
||||
import { i18n } from "@/utils/i18n";
|
||||
|
||||
const t = i18n.global.t;
|
||||
|
||||
export enum ConversationRouteName {
|
||||
CONVERSATION_LIST = "DISCUSSION_LIST",
|
||||
CONVERSATION = "CONVERSATION",
|
||||
}
|
||||
|
||||
export const conversationRoutes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: "/conversations",
|
||||
name: ConversationRouteName.CONVERSATION_LIST,
|
||||
component: (): Promise<any> =>
|
||||
import("@/views/Conversations/ConversationListView.vue"),
|
||||
props: true,
|
||||
meta: {
|
||||
requiredAuth: true,
|
||||
announcer: {
|
||||
message: (): string => t("List of conversations") as string,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/conversations/:id/:comment_id?",
|
||||
name: ConversationRouteName.CONVERSATION,
|
||||
component: (): Promise<any> =>
|
||||
import("@/views/Conversations/ConversationView.vue"),
|
||||
props: true,
|
||||
meta: { requiredAuth: true, announcer: { skip: true } },
|
||||
},
|
||||
];
|
||||
@@ -8,6 +8,7 @@ import { authGuardIfNeeded } from "./guards/auth-guard";
|
||||
import { settingsRoutes } from "./settings";
|
||||
import { groupsRoutes } from "./groups";
|
||||
import { discussionRoutes } from "./discussion";
|
||||
import { conversationRoutes } from "./conversation";
|
||||
import { userRoutes } from "./user";
|
||||
import RouteName from "./name";
|
||||
import { AVAILABLE_LANGUAGES, i18n } from "@/utils/i18n";
|
||||
@@ -36,6 +37,7 @@ export const routes = [
|
||||
...actorRoutes,
|
||||
...groupsRoutes,
|
||||
...discussionRoutes,
|
||||
...conversationRoutes,
|
||||
...errorRoutes,
|
||||
{
|
||||
path: "/search",
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ErrorRouteName } from "./error";
|
||||
import { SettingsRouteName } from "./settings";
|
||||
import { GroupsRouteName } from "./groups";
|
||||
import { DiscussionRouteName } from "./discussion";
|
||||
import { ConversationRouteName } from "./conversation";
|
||||
import { UserRouteName } from "./user";
|
||||
|
||||
enum GlobalRouteName {
|
||||
@@ -31,5 +32,6 @@ export default {
|
||||
...SettingsRouteName,
|
||||
...GroupsRouteName,
|
||||
...DiscussionRouteName,
|
||||
...ConversationRouteName,
|
||||
...ErrorRouteName,
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { IParticipant } from "../participant.model";
|
||||
import type { IMember } from "./member.model";
|
||||
import type { IFeedToken } from "../feedtoken.model";
|
||||
import { IFollower } from "./follower.model";
|
||||
import { IConversation } from "../conversation";
|
||||
|
||||
export interface IPerson extends IActor {
|
||||
feedTokens: IFeedToken[];
|
||||
@@ -16,6 +17,8 @@ export interface IPerson extends IActor {
|
||||
follows?: Paginate<IFollower>;
|
||||
user?: ICurrentUser;
|
||||
organizedEvents?: Paginate<IEvent>;
|
||||
conversations?: Paginate<IConversation>;
|
||||
unreadConversationsCount?: number;
|
||||
}
|
||||
|
||||
export class Person extends Actor implements IPerson {
|
||||
@@ -28,6 +31,7 @@ export class Person extends Actor implements IPerson {
|
||||
memberships!: Paginate<IMember>;
|
||||
|
||||
organizedEvents!: Paginate<IEvent>;
|
||||
conversations!: Paginate<IConversation>;
|
||||
|
||||
user!: ICurrentUser;
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { IPerson, Person } from "@/types/actor";
|
||||
import type { IEvent } from "@/types/event.model";
|
||||
import { EventModel } from "@/types/event.model";
|
||||
import { IConversation } from "./conversation";
|
||||
|
||||
export interface IComment {
|
||||
id?: string;
|
||||
@@ -20,6 +21,7 @@ export interface IComment {
|
||||
publishedAt?: string;
|
||||
isAnnouncement: boolean;
|
||||
language?: string;
|
||||
conversation?: IConversation;
|
||||
}
|
||||
|
||||
export class CommentModel implements IComment {
|
||||
|
||||
17
js/src/types/conversation.ts
Normal file
17
js/src/types/conversation.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { IActor } from "@/types/actor";
|
||||
import type { IComment } from "@/types/comment.model";
|
||||
import type { Paginate } from "@/types/paginate";
|
||||
import { IEvent } from "./event.model";
|
||||
|
||||
export interface IConversation {
|
||||
conversationParticipantId?: string;
|
||||
id?: string;
|
||||
actor?: IActor;
|
||||
lastComment?: IComment;
|
||||
comments: Paginate<IComment>;
|
||||
participants: IActor[];
|
||||
updatedAt: string;
|
||||
insertedAt: string;
|
||||
unread: boolean;
|
||||
event?: IEvent;
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import { PictureInformation } from "./picture";
|
||||
import { IMember } from "./actor/member.model";
|
||||
import { IFeedToken } from "./feedtoken.model";
|
||||
import { IApplicationToken } from "./application.model";
|
||||
import { IConversation } from "./conversation";
|
||||
|
||||
export interface ICurrentUser {
|
||||
id: string;
|
||||
@@ -69,4 +70,5 @@ export interface IUser extends ICurrentUser {
|
||||
memberships: Paginate<IMember>;
|
||||
feedTokens: IFeedToken[];
|
||||
authAuthorizedApplications: IApplicationToken[];
|
||||
conversations: Paginate<IConversation>;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { EventOptions } from "./event-options.model";
|
||||
import type { IEventOptions } from "./event-options.model";
|
||||
import { EventJoinOptions, EventStatus, EventVisibility } from "./enums";
|
||||
import { IEventMetadata, IEventMetadataDescription } from "./event-metadata";
|
||||
import { IConversation } from "./conversation";
|
||||
|
||||
export interface IEventCardOptions {
|
||||
hideDate?: boolean;
|
||||
@@ -85,6 +86,7 @@ export interface IEvent {
|
||||
|
||||
relatedEvents: IEvent[];
|
||||
comments: IComment[];
|
||||
conversations: Paginate<IConversation>;
|
||||
|
||||
onlineAddress?: string;
|
||||
phoneAddress?: string;
|
||||
@@ -161,6 +163,8 @@ export class EventModel implements IEvent {
|
||||
|
||||
comments: IComment[] = [];
|
||||
|
||||
conversations!: Paginate<IConversation>;
|
||||
|
||||
attributedTo?: IGroup = new Group();
|
||||
|
||||
organizerActor?: IActor = new Actor();
|
||||
|
||||
@@ -49,17 +49,17 @@ export function deleteUserData(): void {
|
||||
});
|
||||
}
|
||||
|
||||
export async function logout(performServerLogout = true): Promise<void> {
|
||||
const { mutate: logoutMutation } = provideApolloClient(apolloClient)(() =>
|
||||
useMutation(LOGOUT)
|
||||
);
|
||||
const { mutate: cleanUserClient } = provideApolloClient(apolloClient)(() =>
|
||||
useMutation(UPDATE_CURRENT_USER_CLIENT)
|
||||
);
|
||||
const { mutate: cleanActorClient } = provideApolloClient(apolloClient)(() =>
|
||||
useMutation(UPDATE_CURRENT_ACTOR_CLIENT)
|
||||
);
|
||||
const { mutate: logoutMutation } = provideApolloClient(apolloClient)(() =>
|
||||
useMutation(LOGOUT)
|
||||
);
|
||||
const { mutate: cleanUserClient } = provideApolloClient(apolloClient)(() =>
|
||||
useMutation(UPDATE_CURRENT_USER_CLIENT)
|
||||
);
|
||||
const { mutate: cleanActorClient } = provideApolloClient(apolloClient)(() =>
|
||||
useMutation(UPDATE_CURRENT_ACTOR_CLIENT)
|
||||
);
|
||||
|
||||
export async function logout(performServerLogout = true): Promise<void> {
|
||||
if (performServerLogout) {
|
||||
logoutMutation({
|
||||
refreshToken: localStorage.getItem(AUTH_REFRESH_TOKEN),
|
||||
|
||||
@@ -16,21 +16,31 @@ function saveActorData(obj: IPerson): void {
|
||||
localStorage.setItem(AUTH_USER_ACTOR_ID, `${obj.id}`);
|
||||
}
|
||||
|
||||
const {
|
||||
mutate: updateCurrentActorClient,
|
||||
onDone: onUpdateCurrentActorClientDone,
|
||||
} = provideApolloClient(apolloClient)(() =>
|
||||
useMutation(UPDATE_CURRENT_ACTOR_CLIENT)
|
||||
);
|
||||
|
||||
export async function changeIdentity(identity: IPerson): Promise<void> {
|
||||
if (!identity.id) return;
|
||||
const { mutate: updateCurrentActorClient } = provideApolloClient(
|
||||
apolloClient
|
||||
)(() => useMutation(UPDATE_CURRENT_ACTOR_CLIENT));
|
||||
console.debug("Changing identity", identity);
|
||||
|
||||
updateCurrentActorClient(identity);
|
||||
if (identity.id) {
|
||||
console.debug("Saving actor data");
|
||||
saveActorData(identity);
|
||||
}
|
||||
|
||||
onUpdateCurrentActorClientDone(() => {
|
||||
console.debug("Updating current actor client");
|
||||
});
|
||||
}
|
||||
|
||||
const { onResult: setIdentities, load: loadIdentities } = provideApolloClient(
|
||||
apolloClient
|
||||
)(() => useLazyQuery<{ loggedUser: Pick<ICurrentUser, "actors"> }>(IDENTITIES));
|
||||
const { load: loadIdentities } = provideApolloClient(apolloClient)(() =>
|
||||
useLazyQuery<{ loggedUser: Pick<ICurrentUser, "actors"> }>(IDENTITIES)
|
||||
);
|
||||
|
||||
/**
|
||||
* We fetch from localStorage the latest actor ID used,
|
||||
@@ -39,11 +49,14 @@ const { onResult: setIdentities, load: loadIdentities } = provideApolloClient(
|
||||
*/
|
||||
export async function initializeCurrentActor(): Promise<void> {
|
||||
const actorId = localStorage.getItem(AUTH_USER_ACTOR_ID);
|
||||
console.debug("Initializing current actor", actorId);
|
||||
|
||||
loadIdentities();
|
||||
try {
|
||||
const result = await loadIdentities();
|
||||
if (!result) return;
|
||||
|
||||
setIdentities(async ({ data }) => {
|
||||
const identities = computed(() => data?.loggedUser?.actors);
|
||||
console.debug("got identities", result);
|
||||
const identities = computed(() => result.loggedUser?.actors);
|
||||
console.debug(
|
||||
"initializing current actor based on identities",
|
||||
identities.value
|
||||
@@ -61,5 +74,7 @@ export async function initializeCurrentActor(): Promise<void> {
|
||||
if (activeIdentity) {
|
||||
await changeIdentity(activeIdentity);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Failed to initialize current Actor", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,7 +169,7 @@ const { mutate: acceptInstance, onError: onAcceptInstanceError } = useMutation(
|
||||
() => ({
|
||||
update(cache: ApolloCache<any>) {
|
||||
cache.writeFragment({
|
||||
id: cache.identify(instance as unknown as Reference),
|
||||
id: cache.identify(instance.value as unknown as Reference),
|
||||
fragment: gql`
|
||||
fragment InstanceFollowerStatus on Instance {
|
||||
followerStatus
|
||||
|
||||
94
js/src/views/Conversations/ConversationListView.vue
Normal file
94
js/src/views/Conversations/ConversationListView.vue
Normal file
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<div class="container mx-auto" v-if="conversations">
|
||||
<breadcrumbs-nav
|
||||
:links="[
|
||||
{
|
||||
name: RouteName.CONVERSATION_LIST,
|
||||
text: t('Conversations'),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
<o-notification v-if="error" variant="danger">
|
||||
{{ error }}
|
||||
</o-notification>
|
||||
<section>
|
||||
<h1>{{ t("Conversations") }}</h1>
|
||||
<o-button @click="openNewMessageModal">{{
|
||||
t("New private message")
|
||||
}}</o-button>
|
||||
<div v-if="conversations.elements.length > 0" class="my-2">
|
||||
<conversation-list-item
|
||||
:conversation="conversation"
|
||||
v-for="conversation in conversations.elements"
|
||||
:key="conversation.id"
|
||||
/>
|
||||
<o-pagination
|
||||
v-show="conversations.total > CONVERSATIONS_PER_PAGE"
|
||||
class="conversation-pagination"
|
||||
:total="conversations.total"
|
||||
v-model:current="page"
|
||||
:per-page="CONVERSATIONS_PER_PAGE"
|
||||
:aria-next-label="t('Next page')"
|
||||
:aria-previous-label="t('Previous page')"
|
||||
:aria-page-label="t('Page')"
|
||||
:aria-current-label="t('Current page')"
|
||||
>
|
||||
</o-pagination>
|
||||
</div>
|
||||
<empty-content v-else icon="chat">
|
||||
{{ t("There's no conversations yet") }}
|
||||
</empty-content>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import RouteName from "../../router/name";
|
||||
import { useQuery } from "@vue/apollo-composable";
|
||||
import { computed, defineAsyncComponent, ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { integerTransformer, useRouteQuery } from "vue-use-route-query";
|
||||
import { PROFILE_CONVERSATIONS } from "@/graphql/event";
|
||||
import ConversationListItem from "../../components/Conversations/ConversationListItem.vue";
|
||||
import EmptyContent from "../../components/Utils/EmptyContent.vue";
|
||||
import { useHead } from "@vueuse/head";
|
||||
import { IPerson } from "@/types/actor";
|
||||
import { useProgrammatic } from "@oruga-ui/oruga-next";
|
||||
|
||||
const page = useRouteQuery("page", 1, integerTransformer);
|
||||
const CONVERSATIONS_PER_PAGE = 10;
|
||||
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
useHead({
|
||||
title: computed(() => t("List of conversations")),
|
||||
});
|
||||
|
||||
const error = ref(false);
|
||||
|
||||
const { result: conversationsResult } = useQuery<{
|
||||
loggedPerson: Pick<IPerson, "conversations">;
|
||||
}>(PROFILE_CONVERSATIONS, () => ({
|
||||
page: page.value,
|
||||
}));
|
||||
|
||||
const conversations = computed(
|
||||
() =>
|
||||
conversationsResult.value?.loggedPerson.conversations || {
|
||||
elements: [],
|
||||
total: 0,
|
||||
}
|
||||
);
|
||||
|
||||
const { oruga } = useProgrammatic();
|
||||
|
||||
const NewConversation = defineAsyncComponent(
|
||||
() => import("@/components/Conversations/NewConversation.vue")
|
||||
);
|
||||
|
||||
const openNewMessageModal = () => {
|
||||
oruga.modal.open({
|
||||
component: NewConversation,
|
||||
trapFocus: true,
|
||||
});
|
||||
};
|
||||
</script>
|
||||
527
js/src/views/Conversations/ConversationView.vue
Normal file
527
js/src/views/Conversations/ConversationView.vue
Normal file
@@ -0,0 +1,527 @@
|
||||
<template>
|
||||
<div class="container mx-auto" v-if="conversation">
|
||||
<breadcrumbs-nav
|
||||
:links="[
|
||||
{
|
||||
name: RouteName.CONVERSATION_LIST,
|
||||
text: t('Conversations'),
|
||||
},
|
||||
{
|
||||
name: RouteName.CONVERSATION,
|
||||
params: { id: conversation.id },
|
||||
text: title,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
<div
|
||||
v-if="conversation.event"
|
||||
class="bg-mbz-yellow p-6 mb-6 rounded flex gap-2 items-center"
|
||||
>
|
||||
<Calendar :size="36" />
|
||||
<i18n-t
|
||||
tag="p"
|
||||
keypath="This is a announcement from the organizers of event {event}"
|
||||
>
|
||||
<template #event>
|
||||
<b>
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.EVENT,
|
||||
params: { uuid: conversation.event.uuid },
|
||||
}"
|
||||
>{{ conversation.event.title }}</router-link
|
||||
>
|
||||
</b>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</div>
|
||||
<div
|
||||
v-if="currentActor && currentActor.id !== conversation.actor?.id"
|
||||
class="bg-mbz-info p-6 rounded flex gap-2 items-center my-3"
|
||||
>
|
||||
<i18n-t
|
||||
keypath="You have access to this conversation as a member of the {group} group"
|
||||
tag="p"
|
||||
>
|
||||
<template #group>
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.GROUP,
|
||||
params: {
|
||||
preferredUsername: usernameWithDomain(conversation.actor),
|
||||
},
|
||||
}"
|
||||
><b>{{ displayName(conversation.actor) }}</b></router-link
|
||||
>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</div>
|
||||
<o-notification v-if="error" variant="danger">
|
||||
{{ error }}
|
||||
</o-notification>
|
||||
<section v-if="currentActor">
|
||||
<discussion-comment
|
||||
v-for="comment in conversation.comments.elements"
|
||||
:key="comment.id"
|
||||
:model-value="comment"
|
||||
:current-actor="currentActor"
|
||||
:can-report="true"
|
||||
@update:modelValue="
|
||||
(comment: IComment) =>
|
||||
updateComment({
|
||||
commentId: comment.id as string,
|
||||
text: comment.text,
|
||||
})
|
||||
"
|
||||
@delete-comment="
|
||||
(comment: IComment) =>
|
||||
deleteComment({
|
||||
commentId: comment.id as string,
|
||||
})
|
||||
"
|
||||
/>
|
||||
<o-button
|
||||
v-if="
|
||||
conversation.comments.elements.length < conversation.comments.total
|
||||
"
|
||||
@click="loadMoreComments"
|
||||
>{{ t("Fetch more") }}</o-button
|
||||
>
|
||||
<form @submit.prevent="reply" v-if="!error && !conversation.event">
|
||||
<o-field :label="t('Text')">
|
||||
<Editor
|
||||
v-model="newComment"
|
||||
:aria-label="t('Message body')"
|
||||
v-if="currentActor"
|
||||
:currentActor="currentActor"
|
||||
:placeholder="t('Write a new message')"
|
||||
/>
|
||||
</o-field>
|
||||
<o-button
|
||||
class="my-2"
|
||||
native-type="submit"
|
||||
:disabled="['<p></p>', ''].includes(newComment)"
|
||||
variant="primary"
|
||||
>{{ t("Reply") }}</o-button
|
||||
>
|
||||
</form>
|
||||
<div
|
||||
v-else-if="conversation.event"
|
||||
class="bg-mbz-yellow p-6 rounded flex gap-2 items-center mt-6"
|
||||
>
|
||||
<Calendar :size="36" />
|
||||
<i18n-t
|
||||
tag="p"
|
||||
keypath="This is a announcement from the organizers of event {event}. You can't reply to it, but you can send a private message to event organizers."
|
||||
>
|
||||
<template #event>
|
||||
<b>
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.EVENT,
|
||||
params: { uuid: conversation.event.uuid },
|
||||
}"
|
||||
>{{ conversation.event.title }}</router-link
|
||||
>
|
||||
</b>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
CONVERSATION_COMMENT_CHANGED,
|
||||
GET_CONVERSATION,
|
||||
MARK_CONVERSATION_AS_READ,
|
||||
REPLY_TO_PRIVATE_MESSAGE_MUTATION,
|
||||
} from "../../graphql/conversations";
|
||||
import DiscussionComment from "../../components/Discussion/DiscussionComment.vue";
|
||||
import { DELETE_COMMENT, UPDATE_COMMENT } from "../../graphql/comment";
|
||||
import RouteName from "../../router/name";
|
||||
import { IComment } from "../../types/comment.model";
|
||||
import {
|
||||
ApolloCache,
|
||||
FetchResult,
|
||||
InMemoryCache,
|
||||
gql,
|
||||
} from "@apollo/client/core";
|
||||
import { useMutation, useQuery } from "@vue/apollo-composable";
|
||||
import {
|
||||
defineAsyncComponent,
|
||||
ref,
|
||||
computed,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
} from "vue";
|
||||
import { useHead } from "@vueuse/head";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useCurrentActorClient } from "../../composition/apollo/actor";
|
||||
import { AbsintheGraphQLError } from "../../types/errors.model";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { IConversation } from "@/types/conversation";
|
||||
import { usernameWithDomain, displayName } from "@/types/actor";
|
||||
import { formatList } from "@/utils/i18n";
|
||||
import throttle from "lodash/throttle";
|
||||
import Calendar from "vue-material-design-icons/Calendar.vue";
|
||||
import { ActorType } from "@/types/enums";
|
||||
|
||||
const props = defineProps<{ id: string }>();
|
||||
|
||||
const conversationId = computed(() => props.id);
|
||||
|
||||
const page = ref(1);
|
||||
const COMMENTS_PER_PAGE = 10;
|
||||
|
||||
const { currentActor } = useCurrentActorClient();
|
||||
|
||||
const {
|
||||
result: conversationResult,
|
||||
onResult: onConversationResult,
|
||||
onError: onConversationError,
|
||||
subscribeToMore,
|
||||
fetchMore,
|
||||
} = useQuery<{ conversation: IConversation }>(
|
||||
GET_CONVERSATION,
|
||||
() => ({
|
||||
id: conversationId.value,
|
||||
page: page.value,
|
||||
limit: COMMENTS_PER_PAGE,
|
||||
}),
|
||||
() => ({
|
||||
enabled: conversationId.value !== undefined,
|
||||
})
|
||||
);
|
||||
|
||||
subscribeToMore({
|
||||
document: CONVERSATION_COMMENT_CHANGED,
|
||||
variables: {
|
||||
id: conversationId.value,
|
||||
},
|
||||
updateQuery(
|
||||
previousResult: any,
|
||||
{ subscriptionData }: { subscriptionData: any }
|
||||
) {
|
||||
const previousConversation = previousResult.conversation;
|
||||
const lastComment =
|
||||
subscriptionData.data.conversationCommentChanged.lastComment;
|
||||
hasMoreComments.value = !previousConversation.comments.elements.some(
|
||||
(comment: IComment) => comment.id === lastComment.id
|
||||
);
|
||||
if (hasMoreComments.value) {
|
||||
return {
|
||||
conversation: {
|
||||
...previousConversation,
|
||||
lastComment: lastComment,
|
||||
comments: {
|
||||
elements: [
|
||||
...previousConversation.comments.elements.filter(
|
||||
({ id }: { id: string }) => id !== lastComment.id
|
||||
),
|
||||
lastComment,
|
||||
],
|
||||
total: previousConversation.comments.total + 1,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return previousConversation;
|
||||
},
|
||||
});
|
||||
|
||||
const conversation = computed(() => conversationResult.value?.conversation);
|
||||
const otherParticipants = computed(
|
||||
() =>
|
||||
conversation.value?.participants.filter(
|
||||
(participant) => participant.id !== currentActor.value?.id
|
||||
) ?? []
|
||||
);
|
||||
|
||||
const Editor = defineAsyncComponent(
|
||||
() => import("../../components/TextEditor.vue")
|
||||
);
|
||||
|
||||
const { t } = useI18n({ useScope: "global" });
|
||||
|
||||
const title = computed(() =>
|
||||
t("Conversation with {participants}", {
|
||||
participants: formatList(
|
||||
otherParticipants.value.map((participant) => displayName(participant))
|
||||
),
|
||||
})
|
||||
);
|
||||
|
||||
useHead({
|
||||
title: title.value,
|
||||
});
|
||||
|
||||
const newComment = ref("");
|
||||
// const newTitle = ref("");
|
||||
// const editTitleMode = ref(false);
|
||||
const hasMoreComments = ref(true);
|
||||
const error = ref<string | null>(null);
|
||||
|
||||
const { mutate: replyToConversationMutation } = useMutation<
|
||||
{
|
||||
postPrivateMessage: IConversation;
|
||||
},
|
||||
{
|
||||
text: string;
|
||||
actorId: string;
|
||||
language?: string;
|
||||
conversationId: string;
|
||||
mentions?: string[];
|
||||
attributedToId?: string;
|
||||
}
|
||||
>(REPLY_TO_PRIVATE_MESSAGE_MUTATION, () => ({
|
||||
update: (store: ApolloCache<InMemoryCache>, { data }) => {
|
||||
console.debug("update after reply to", [conversationId.value, page.value]);
|
||||
const conversationData = store.readQuery<{
|
||||
conversation: IConversation;
|
||||
}>({
|
||||
query: GET_CONVERSATION,
|
||||
variables: {
|
||||
id: conversationId.value,
|
||||
},
|
||||
});
|
||||
console.debug("update after reply to", conversationData);
|
||||
if (!conversationData) return;
|
||||
const { conversation: conversationCached } = conversationData;
|
||||
|
||||
console.debug("got cache", conversationCached);
|
||||
|
||||
store.writeQuery({
|
||||
query: GET_CONVERSATION,
|
||||
variables: {
|
||||
id: conversationId.value,
|
||||
},
|
||||
data: {
|
||||
conversation: {
|
||||
...conversationCached,
|
||||
lastComment: data?.postPrivateMessage.lastComment,
|
||||
comments: {
|
||||
elements: [
|
||||
...conversationCached.comments.elements,
|
||||
data?.postPrivateMessage.lastComment,
|
||||
],
|
||||
total: conversationCached.comments.total + 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
||||
const reply = () => {
|
||||
if (
|
||||
newComment.value === "" ||
|
||||
!conversation.value?.id ||
|
||||
!currentActor.value?.id
|
||||
)
|
||||
return;
|
||||
|
||||
replyToConversationMutation({
|
||||
conversationId: conversation.value?.id,
|
||||
text: newComment.value,
|
||||
actorId: currentActor.value?.id,
|
||||
mentions: otherParticipants.value.map((participant) =>
|
||||
usernameWithDomain(participant)
|
||||
),
|
||||
attributedToId:
|
||||
conversation.value?.actor?.type === ActorType.GROUP
|
||||
? conversation.value?.actor.id
|
||||
: undefined,
|
||||
});
|
||||
|
||||
newComment.value = "";
|
||||
};
|
||||
|
||||
const { mutate: updateComment } = useMutation<
|
||||
{ updateComment: IComment },
|
||||
{ commentId: string; text: string }
|
||||
>(UPDATE_COMMENT, () => ({
|
||||
update: (
|
||||
store: ApolloCache<{ deleteComment: IComment }>,
|
||||
{ data }: FetchResult
|
||||
) => {
|
||||
if (!data || !data.deleteComment) return;
|
||||
const discussionData = store.readQuery<{
|
||||
conversation: IConversation;
|
||||
}>({
|
||||
query: GET_CONVERSATION,
|
||||
variables: {
|
||||
id: conversationId.value,
|
||||
page: page.value,
|
||||
},
|
||||
});
|
||||
if (!discussionData) return;
|
||||
const { conversation: discussionCached } = discussionData;
|
||||
const index = discussionCached.comments.elements.findIndex(
|
||||
({ id }) => id === data.deleteComment.id
|
||||
);
|
||||
if (index > -1) {
|
||||
discussionCached.comments.elements.splice(index, 1);
|
||||
discussionCached.comments.total -= 1;
|
||||
}
|
||||
store.writeQuery({
|
||||
query: GET_CONVERSATION,
|
||||
variables: { id: conversationId.value, page: page.value },
|
||||
data: { conversation: discussionCached },
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
||||
const { mutate: deleteComment } = useMutation<
|
||||
{ deleteComment: { id: string } },
|
||||
{ commentId: string }
|
||||
>(DELETE_COMMENT, () => ({
|
||||
update: (store: ApolloCache<{ deleteComment: IComment }>, { data }) => {
|
||||
const id = data?.deleteComment?.id;
|
||||
if (!id) return;
|
||||
store.writeFragment({
|
||||
id: `Comment:${id}`,
|
||||
fragment: gql`
|
||||
fragment CommentDeleted on Comment {
|
||||
deletedAt
|
||||
actor {
|
||||
id
|
||||
}
|
||||
text
|
||||
}
|
||||
`,
|
||||
data: {
|
||||
deletedAt: new Date(),
|
||||
text: "",
|
||||
actor: null,
|
||||
},
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
||||
const loadMoreComments = async (): Promise<void> => {
|
||||
if (!hasMoreComments.value) return;
|
||||
console.debug("Loading more comments");
|
||||
page.value++;
|
||||
try {
|
||||
await fetchMore({
|
||||
// New variables
|
||||
variables: () => ({
|
||||
id: conversationId.value,
|
||||
page: page.value,
|
||||
limit: COMMENTS_PER_PAGE,
|
||||
}),
|
||||
});
|
||||
hasMoreComments.value = !conversation.value?.comments.elements
|
||||
.map(({ id }) => id)
|
||||
.includes(conversation.value?.lastComment?.id);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
// const dialog = inject<Dialog>("dialog");
|
||||
|
||||
// const openDeleteDiscussionConfirmation = (): void => {
|
||||
// dialog?.confirm({
|
||||
// variant: "danger",
|
||||
// title: t("Delete this conversation"),
|
||||
// message: t("Are you sure you want to delete this entire conversation?"),
|
||||
// confirmText: t("Delete conversation"),
|
||||
// cancelText: t("Cancel"),
|
||||
// onConfirm: () =>
|
||||
// deleteConversation({
|
||||
// discussionId: conversation.value?.id,
|
||||
// }),
|
||||
// });
|
||||
// };
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
// const { mutate: deleteConversation, onDone: deleteConversationDone } =
|
||||
// useMutation(DELETE_DISCUSSION);
|
||||
|
||||
// deleteConversationDone(() => {
|
||||
// if (conversation.value?.actor) {
|
||||
// router.push({
|
||||
// name: RouteName.DISCUSSION_LIST,
|
||||
// params: {
|
||||
// preferredUsername: usernameWithDomain(conversation.value.actor),
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
// });
|
||||
|
||||
onConversationError((discussionError) =>
|
||||
handleErrors(discussionError.graphQLErrors as AbsintheGraphQLError[])
|
||||
);
|
||||
|
||||
onConversationResult(({ data }) => {
|
||||
if (
|
||||
page.value === 1 &&
|
||||
data?.conversation?.comments?.total &&
|
||||
data?.conversation?.comments?.total < COMMENTS_PER_PAGE
|
||||
) {
|
||||
markConversationAsRead();
|
||||
}
|
||||
});
|
||||
|
||||
const handleErrors = async (errors: AbsintheGraphQLError[]): Promise<void> => {
|
||||
if (errors[0].code === "not_found") {
|
||||
await router.push({ name: RouteName.PAGE_NOT_FOUND });
|
||||
}
|
||||
if (errors[0].code === "unauthorized") {
|
||||
error.value = errors[0].message;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener("scroll", handleScroll);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener("scroll", handleScroll);
|
||||
});
|
||||
|
||||
const { mutate: markConversationAsRead } = useMutation<
|
||||
{
|
||||
updateConversation: IConversation;
|
||||
},
|
||||
{
|
||||
id: string;
|
||||
read: boolean;
|
||||
}
|
||||
>(MARK_CONVERSATION_AS_READ, {
|
||||
variables: {
|
||||
id: conversationId.value,
|
||||
read: true,
|
||||
},
|
||||
});
|
||||
|
||||
const loadMoreCommentsThrottled = throttle(async () => {
|
||||
console.log("Throttled");
|
||||
await loadMoreComments();
|
||||
if (!hasMoreComments.value && conversation.value?.unread) {
|
||||
console.debug("marking as read");
|
||||
markConversationAsRead();
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
const handleScroll = (): void => {
|
||||
const scrollTop =
|
||||
(document.documentElement && document.documentElement.scrollTop) ||
|
||||
document.body.scrollTop;
|
||||
const scrollHeight =
|
||||
(document.documentElement && document.documentElement.scrollHeight) ||
|
||||
document.body.scrollHeight;
|
||||
const clientHeight =
|
||||
document.documentElement.clientHeight || window.innerHeight;
|
||||
const scrolledToBottom =
|
||||
Math.ceil(scrollTop + clientHeight + 800) >= scrollHeight;
|
||||
if (scrolledToBottom) {
|
||||
console.debug("Scrolled to bottom");
|
||||
loadMoreCommentsThrottled();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -250,6 +250,8 @@
|
||||
</div>
|
||||
</template>
|
||||
</o-table>
|
||||
<EventConversations :event="event" class="my-6" />
|
||||
<NewPrivateMessage :event="event" />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -283,6 +285,8 @@ import EmptyContent from "@/components/Utils/EmptyContent.vue";
|
||||
import { Notifier } from "@/plugins/notifier";
|
||||
import Tag from "@/components/TagElement.vue";
|
||||
import { useHead } from "@vueuse/head";
|
||||
import EventConversations from "../../components/Conversations/EventConversations.vue";
|
||||
import NewPrivateMessage from "../../components/Participation/NewPrivateMessage.vue";
|
||||
|
||||
const PARTICIPANTS_PER_PAGE = 10;
|
||||
const MESSAGE_ELLIPSIS_LENGTH = 130;
|
||||
|
||||
@@ -44,9 +44,9 @@
|
||||
<div class="flex flex-wrap justify-center flex-col md:flex-row">
|
||||
<div
|
||||
class="flex flex-col items-center flex-1 m-0"
|
||||
v-if="isCurrentActorAGroupMember && !previewPublic"
|
||||
v-if="isCurrentActorAGroupMember && !previewPublic && members"
|
||||
>
|
||||
<div class="flex gap-1">
|
||||
<div class="flex">
|
||||
<figure
|
||||
:title="
|
||||
t(`{'@'}{username} ({role})`, {
|
||||
@@ -54,11 +54,12 @@
|
||||
role: member.role,
|
||||
})
|
||||
"
|
||||
v-for="member in members"
|
||||
v-for="member in members.elements"
|
||||
:key="member.actor.id"
|
||||
class="-mr-3"
|
||||
>
|
||||
<img
|
||||
class="rounded-full"
|
||||
class="rounded-full h-8"
|
||||
:src="member.actor.avatar.url"
|
||||
v-if="member.actor.avatar"
|
||||
alt=""
|
||||
@@ -698,6 +699,7 @@ import Events from "@/components/Group/Sections/EventsSection.vue";
|
||||
import { Dialog } from "@/plugins/dialog";
|
||||
import { Notifier } from "@/plugins/notifier";
|
||||
import { useGroupResourcesList } from "@/composition/apollo/resources";
|
||||
import { useGroupMembers } from "@/composition/apollo/members";
|
||||
|
||||
const props = defineProps<{
|
||||
preferredUsername: string;
|
||||
@@ -1050,18 +1052,18 @@ const isCurrentActorOnADifferentDomainThanGroup = computed((): boolean => {
|
||||
return group.value?.domain !== null;
|
||||
});
|
||||
|
||||
const members = computed((): IMember[] => {
|
||||
return (
|
||||
(group.value?.members?.elements ?? []).filter(
|
||||
(member: IMember) =>
|
||||
![
|
||||
MemberRole.INVITED,
|
||||
MemberRole.REJECTED,
|
||||
MemberRole.NOT_APPROVED,
|
||||
].includes(member.role)
|
||||
) ?? []
|
||||
);
|
||||
});
|
||||
// const members = computed((): IMember[] => {
|
||||
// return (
|
||||
// (group.value?.members?.elements ?? []).filter(
|
||||
// (member: IMember) =>
|
||||
// ![
|
||||
// MemberRole.INVITED,
|
||||
// MemberRole.REJECTED,
|
||||
// MemberRole.NOT_APPROVED,
|
||||
// ].includes(member.role)
|
||||
// ) ?? []
|
||||
// );
|
||||
// });
|
||||
|
||||
const physicalAddress = computed((): Address | null => {
|
||||
if (!group.value?.physicalAddress) return null;
|
||||
@@ -1179,6 +1181,10 @@ const hasCurrentActorThisRole = (givenRole: string | string[]): boolean => {
|
||||
);
|
||||
};
|
||||
|
||||
const { members } = useGroupMembers(preferredUsername, {
|
||||
enabled: computed(() => isCurrentActorAGroupMember.value),
|
||||
});
|
||||
|
||||
watch(isCurrentActorAGroupMember, () => {
|
||||
refetchGroup();
|
||||
});
|
||||
|
||||
@@ -257,25 +257,65 @@
|
||||
<h2 class="mb-1">{{ t("Reported content") }}</h2>
|
||||
<ul v-for="comment in report.comments" :key="comment.id">
|
||||
<li>
|
||||
<i18n-t keypath="Comment under event {eventTitle}" tag="p">
|
||||
<template #eventTitle>
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.EVENT,
|
||||
params: { uuid: comment.event?.uuid },
|
||||
}"
|
||||
>
|
||||
<b>{{ comment.event?.title }}</b>
|
||||
</router-link>
|
||||
</template>
|
||||
</i18n-t>
|
||||
<EventComment
|
||||
:root-comment="true"
|
||||
:comment="comment"
|
||||
:event="comment.event as IEvent"
|
||||
:current-actor="currentActor as IPerson"
|
||||
:readOnly="true"
|
||||
/>
|
||||
<template v-if="comment.conversation && comment.event">
|
||||
<i18n-t keypath="Comment from an event announcement" tag="p">
|
||||
<template #eventTitle>
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.EVENT,
|
||||
params: { uuid: comment.event?.uuid },
|
||||
}"
|
||||
>
|
||||
<b>{{ comment.event?.title }}</b>
|
||||
</router-link>
|
||||
</template>
|
||||
</i18n-t>
|
||||
<DiscussionComment
|
||||
:modelValue="comment"
|
||||
:current-actor="currentActor as IPerson"
|
||||
:readOnly="true"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="comment.conversation">
|
||||
<i18n-t keypath="Comment from a private conversation" tag="p">
|
||||
<template #eventTitle>
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.EVENT,
|
||||
params: { uuid: comment.event?.uuid },
|
||||
}"
|
||||
>
|
||||
<b>{{ comment.event?.title }}</b>
|
||||
</router-link>
|
||||
</template>
|
||||
</i18n-t>
|
||||
<DiscussionComment
|
||||
:modelValue="comment"
|
||||
:current-actor="currentActor as IPerson"
|
||||
:readOnly="true"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<i18n-t keypath="Comment under event {eventTitle}" tag="p">
|
||||
<template #eventTitle>
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.EVENT,
|
||||
params: { uuid: comment.event?.uuid },
|
||||
}"
|
||||
>
|
||||
<b>{{ comment.event?.title }}</b>
|
||||
</router-link>
|
||||
</template>
|
||||
</i18n-t>
|
||||
<EventComment
|
||||
:root-comment="true"
|
||||
:comment="comment"
|
||||
:event="comment.event as IEvent"
|
||||
:current-actor="currentActor as IPerson"
|
||||
:readOnly="true"
|
||||
/>
|
||||
</template>
|
||||
<o-button
|
||||
v-if="!comment.deletedAt"
|
||||
variant="danger"
|
||||
@@ -389,10 +429,10 @@ import { useFeatures } from "@/composition/apollo/config";
|
||||
import { IEvent } from "@/types/event.model";
|
||||
import EmptyContent from "@/components/Utils/EmptyContent.vue";
|
||||
import EventComment from "@/components/Comment/EventComment.vue";
|
||||
import DiscussionComment from "@/components/Discussion/DiscussionComment.vue";
|
||||
import { SUSPEND_PROFILE } from "@/graphql/actor";
|
||||
import { GET_USER, SUSPEND_USER } from "@/graphql/user";
|
||||
import { IUser } from "@/types/current-user.model";
|
||||
import { waitApolloQuery } from "@/vue-apollo";
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
@@ -721,7 +761,10 @@ const { mutate: doSuspendUser, onDone: onSuspendUserDone } = useMutation<
|
||||
{ userId: string }
|
||||
>(SUSPEND_USER);
|
||||
|
||||
const userLazyQuery = useLazyQuery<{ user: IUser }, { id: string }>(GET_USER);
|
||||
const { load: loadUserLazyQuery } = useLazyQuery<
|
||||
{ user: IUser },
|
||||
{ id: string }
|
||||
>(GET_USER);
|
||||
|
||||
const suspendProfile = async (actorId: string): Promise<void> => {
|
||||
dialog?.confirm({
|
||||
@@ -761,15 +804,13 @@ const cachedReportedUser = ref<IUser | undefined>();
|
||||
const suspendUser = async (user: IUser): Promise<void> => {
|
||||
try {
|
||||
if (!cachedReportedUser.value) {
|
||||
userLazyQuery.load(GET_USER, { id: user.id });
|
||||
|
||||
const userLazyQueryResult = await waitApolloQuery<
|
||||
{ user: IUser },
|
||||
{ id: string }
|
||||
>(userLazyQuery);
|
||||
console.debug("data", userLazyQueryResult);
|
||||
|
||||
cachedReportedUser.value = userLazyQueryResult.data.user;
|
||||
try {
|
||||
const result = await loadUserLazyQuery(GET_USER, { id: user.id });
|
||||
if (!result) return;
|
||||
cachedReportedUser.value = result.user;
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
dialog?.confirm({
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
<tr v-for="subType in notificationType.subtypes" :key="subType.id">
|
||||
<td v-for="(method, key) in notificationMethods" :key="key">
|
||||
<o-checkbox
|
||||
:modelValue="notificationValues[subType.id][key].enabled"
|
||||
:modelValue="notificationValues?.[subType.id]?.[key]?.enabled"
|
||||
@update:modelValue="
|
||||
(e: boolean) =>
|
||||
updateNotificationValue({
|
||||
@@ -82,7 +82,7 @@
|
||||
enabled: e,
|
||||
})
|
||||
"
|
||||
:disabled="notificationValues[subType.id][key].disabled"
|
||||
:disabled="notificationValues?.[subType.id]?.[key]?.disabled"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
@@ -104,7 +104,7 @@
|
||||
>
|
||||
<o-select
|
||||
v-model="groupNotifications"
|
||||
@input="updateSetting({ groupNotifications })"
|
||||
@update:modelValue="updateSetting({ groupNotifications })"
|
||||
id="groupNotifications"
|
||||
>
|
||||
<option
|
||||
@@ -450,6 +450,10 @@ const defaultNotificationValues = {
|
||||
email: { enabled: true, disabled: false },
|
||||
push: { enabled: true, disabled: false },
|
||||
},
|
||||
conversation_mention: {
|
||||
email: { enabled: true, disabled: false },
|
||||
push: { enabled: true, disabled: false },
|
||||
},
|
||||
discussion_mention: {
|
||||
email: { enabled: true, disabled: false },
|
||||
push: { enabled: false, disabled: false },
|
||||
@@ -464,6 +468,10 @@ const notificationTypes: NotificationType[] = [
|
||||
{
|
||||
label: t("Mentions") as string,
|
||||
subtypes: [
|
||||
{
|
||||
id: "conversation_mention",
|
||||
label: t("I've been mentionned in a conversation") as string,
|
||||
},
|
||||
{
|
||||
id: "event_comment_mention",
|
||||
label: t("I've been mentionned in a comment under an event") as string,
|
||||
|
||||
@@ -249,6 +249,8 @@ onCurrentUserMutationDone(async () => {
|
||||
userAlreadyActivated: "true",
|
||||
},
|
||||
});
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -37,22 +37,23 @@ const {
|
||||
{ id: string; email: string; isLoggedIn: boolean; role: ICurrentUserRole }
|
||||
>(UPDATE_CURRENT_USER_CLIENT);
|
||||
|
||||
const { onResult: onLoggedUserResult, load: loadUser } = useLazyQuery<{
|
||||
const { load: loadUser } = useLazyQuery<{
|
||||
loggedUser: IUser;
|
||||
}>(LOGGED_USER);
|
||||
|
||||
onUpdateCurrentUserClientDone(async () => {
|
||||
loadUser();
|
||||
});
|
||||
|
||||
onLoggedUserResult(async (result) => {
|
||||
if (result.loading) return;
|
||||
const loggedUser = result.data.loggedUser;
|
||||
if (loggedUser.defaultActor) {
|
||||
await changeIdentity(loggedUser.defaultActor);
|
||||
await router.push({ name: RouteName.HOME });
|
||||
} else {
|
||||
// No need to push to REGISTER_PROFILE, the navbar will do it for us
|
||||
try {
|
||||
const result = await loadUser();
|
||||
if (!result) return;
|
||||
const loggedUser = result.loggedUser;
|
||||
if (loggedUser.defaultActor) {
|
||||
await changeIdentity(loggedUser.defaultActor);
|
||||
await router.push({ name: RouteName.HOME });
|
||||
} else {
|
||||
// No need to push to REGISTER_PROFILE, the navbar will do it for us
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
import {
|
||||
ApolloClient,
|
||||
ApolloQueryResult,
|
||||
NormalizedCacheObject,
|
||||
OperationVariables,
|
||||
} from "@apollo/client/core";
|
||||
import { ApolloClient, NormalizedCacheObject } from "@apollo/client/core";
|
||||
import buildCurrentUserResolver from "@/apollo/user";
|
||||
import { cache } from "./apollo/memory";
|
||||
import { fullLink } from "./apollo/link";
|
||||
import { UseQueryReturn } from "@vue/apollo-composable";
|
||||
|
||||
export const apolloClient = new ApolloClient<NormalizedCacheObject>({
|
||||
cache,
|
||||
@@ -15,24 +9,3 @@ export const apolloClient = new ApolloClient<NormalizedCacheObject>({
|
||||
connectToDevTools: true,
|
||||
resolvers: buildCurrentUserResolver(cache),
|
||||
});
|
||||
|
||||
export function waitApolloQuery<
|
||||
TResult = any,
|
||||
TVariables extends OperationVariables = OperationVariables,
|
||||
>({
|
||||
onResult,
|
||||
onError,
|
||||
}: UseQueryReturn<TResult, TVariables>): Promise<ApolloQueryResult<TResult>> {
|
||||
return new Promise((res, rej) => {
|
||||
const { off: offResult } = onResult((result) => {
|
||||
if (result.loading === false) {
|
||||
offResult();
|
||||
res(result);
|
||||
}
|
||||
});
|
||||
const { off: offError } = onError((error) => {
|
||||
offError();
|
||||
rej(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
1881
js/yarn.lock
1881
js/yarn.lock
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user