Merge branch 'search-fixes' into 'main'

Fix event card background color behind picture

Closes #1174, #1172 et #1171

See merge request framasoft/mobilizon!1301
This commit is contained in:
Thomas Citharel 2022-10-25 17:44:40 +00:00
commit 3b7f0f8acf
19 changed files with 691 additions and 753 deletions

View File

@ -142,7 +142,7 @@ body {
} }
.dropdown-item-active { .dropdown-item-active {
@apply bg-white text-black; @apply bg-white dark:bg-zinc-700 dark:text-zinc-100 text-black;
} }
.dropdown-button { .dropdown-button {
@apply inline-flex gap-1; @apply inline-flex gap-1;

View File

@ -1,10 +1,9 @@
<template> <template>
<div> <div>
<form <form
class=""
v-if="isAbleToComment" v-if="isAbleToComment"
@submit.prevent="createCommentForEvent(newComment)" @submit.prevent="createCommentForEvent(newComment)"
@keyup.ctrl.enter="createCommentForEvent(newComment)" class="mt-2"
> >
<o-notification <o-notification
v-if="isEventOrganiser && !areCommentsClosed" v-if="isEventOrganiser && !areCommentsClosed"
@ -26,6 +25,7 @@
mode="comment" mode="comment"
v-model="newComment.text" v-model="newComment.text"
:aria-label="t('Comment body')" :aria-label="t('Comment body')"
@submit="createCommentForEvent(newComment)"
/> />
<p class="" v-if="emptyCommentError"> <p class="" v-if="emptyCommentError">
{{ t("Comment text can't be empty") }} {{ t("Comment text can't be empty") }}
@ -53,7 +53,7 @@
<p v-if="commentsLoading" class="text-center"> <p v-if="commentsLoading" class="text-center">
{{ t("Loading comments…") }} {{ t("Loading comments…") }}
</p> </p>
<transition-group tag="div" name="comment-empty-list" v-else> <transition-group tag="div" name="comment-empty-list" v-else class="mt-2">
<transition-group <transition-group
key="list" key="list"
name="comment-list" name="comment-list"
@ -61,18 +61,17 @@
class="comment-list" class="comment-list"
tag="ul" tag="ul"
> >
<comment <event-comment
class="root-comment" class="root-comment my-2"
:comment="comment" :comment="comment"
:event="event" :event="event"
:currentActor="currentActor" :currentActor="currentActor"
v-for="comment in filteredOrderedComments" v-for="comment in filteredOrderedComments"
:key="comment.id" :key="comment.id"
@create-comment="createCommentForEvent" @create-comment="createCommentForEvent"
@delete-comment=" @delete-comment="commentToDelete => deleteComment({
deleteComment({ commentId: commentToDelete.id as string,
commentId: comment.id as string, originCommentId: commentToDelete.originComment?.id,
originCommentId: comment.originComment?.id,
}) })
" "
/> />
@ -85,7 +84,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import Comment from "@/components/Comment/EventComment.vue"; import EventComment from "@/components/Comment/EventComment.vue";
import IdentityPickerWrapper from "@/views/Account/IdentityPickerWrapper.vue"; import IdentityPickerWrapper from "@/views/Account/IdentityPickerWrapper.vue";
import { CommentModeration } from "@/types/enums"; import { CommentModeration } from "@/types/enums";
import { CommentModel, IComment } from "../../types/comment.model"; import { CommentModel, IComment } from "../../types/comment.model";

View File

@ -6,9 +6,9 @@
'bg-violet-1': commentSelected, 'bg-violet-1': commentSelected,
'shadow-none': !rootComment, 'shadow-none': !rootComment,
}" }"
class="mbz-card p-2" class="bg-white dark:bg-zinc-900 rounded p-2"
> >
<article :id="commentId" dir="auto"> <article :id="commentId" dir="auto" class="mbz-comment">
<div> <div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div class="flex items-center gap-1" v-if="actorComment"> <div class="flex items-center gap-1" v-if="actorComment">
@ -36,9 +36,9 @@
> >
</div> </div>
<a v-else :href="commentURL"> <p v-else :href="commentURL">
<span>{{ t("[deleted]") }}</span> <span>{{ t("[deleted]") }}</span>
</a> </p>
<a :href="commentURL"> <a :href="commentURL">
<small v-if="comment.updatedAt">{{ <small v-if="comment.updatedAt">{{
formatDistanceToNow(new Date(comment.updatedAt), { formatDistanceToNow(new Date(comment.updatedAt), {
@ -47,19 +47,6 @@
}) })
}}</small> }}</small>
</a> </a>
<div v-if="!comment.deletedAt" class="flex">
<button
v-if="actorComment?.id === currentActor?.id"
@click="deleteComment"
>
<Delete :size="16" />
<span class="sr-only">{{ t("Delete") }}</span>
</button>
<button @click="reportModal">
<Alert :size="16" />
<span class="sr-only">{{ t("Report") }}</span>
</button>
</div>
</div> </div>
<div <div
v-if="!comment.deletedAt" v-if="!comment.deletedAt"
@ -68,11 +55,53 @@
:lang="comment.language" :lang="comment.language"
/> />
<div v-else>{{ t("[This comment has been deleted]") }}</div> <div v-else>{{ t("[This comment has been deleted]") }}</div>
<nav class="flex gap-1 mt-1" v-if="!comment.deletedAt">
<button
class="cursor-pointer flex hover:bg-zinc-300 dark:hover:bg-zinc-600 rounded p-1"
v-if="
currentActor?.id &&
event.options.commentModeration !== CommentModeration.CLOSED &&
!comment.deletedAt
"
@click="createReplyToComment()"
>
<Reply />
<span>{{ t("Reply") }}</span>
</button>
<o-dropdown aria-role="list">
<template #trigger>
<button
class="cursor-pointer flex hover:bg-zinc-300 dark:hover:bg-zinc-600 rounded p-1"
>
<DotsHorizontal />
<span class="sr-only">{{ t("More options") }}</span>
</button>
</template>
<o-dropdown-item
aria-role="listitem"
v-if="actorComment?.id === currentActor?.id"
>
<button class="flex items-center gap-1" @click="deleteComment">
<Delete :size="16" />
<span>{{ t("Delete") }}</span>
</button>
</o-dropdown-item>
<o-dropdown-item aria-role="listitem">
<button
@click="isReportModalActive = true"
class="flex items-center gap-1"
>
<Alert :size="16" />
<span>{{ t("Report") }}</span>
</button>
</o-dropdown-item>
</o-dropdown>
</nav>
<div class="" v-if="comment.totalReplies"> <div class="" v-if="comment.totalReplies">
<p <button
v-if="!showReplies" v-if="!showReplies"
@click="showReplies = true" @click="showReplies = true"
class="flex cursor-pointer" class="flex cursor-pointer hover:bg-zinc-300 dark:hover:bg-zinc-600 rounded p-1"
> >
<ChevronDown /> <ChevronDown />
<span>{{ <span>{{
@ -84,28 +113,16 @@
comment.totalReplies comment.totalReplies
) )
}}</span> }}</span>
</p> </button>
<p <button
v-else-if="comment.totalReplies && showReplies" v-else-if="comment.totalReplies && showReplies"
@click="showReplies = false" @click="showReplies = false"
class="flex cursor-pointer" class="flex cursor-pointer hover:bg-zinc-300 dark:hover:bg-zinc-600 rounded p-1"
> >
<ChevronUp /> <ChevronUp />
<span>{{ t("Hide replies") }}</span> <span>{{ t("Hide replies") }}</span>
</p> </button>
</div> </div>
<nav
v-if="
currentActor?.id &&
event.options.commentModeration !== CommentModeration.CLOSED &&
!comment.deletedAt
"
@click="createReplyToComment()"
class="flex gap-1 cursor-pointer"
>
<Reply />
<span>{{ t("Reply") }}</span>
</nav>
</div> </div>
</article> </article>
<form <form
@ -137,6 +154,7 @@
:current-actor="currentActor" :current-actor="currentActor"
:aria-label="t('Comment body')" :aria-label="t('Comment body')"
class="flex-1" class="flex-1"
@submit="replyToComment"
/> />
<o-button <o-button
:disabled="newComment.text.trim().length === 0" :disabled="newComment.text.trim().length === 0"
@ -149,17 +167,13 @@
</div> </div>
</article> </article>
</form> </form>
<div>
<div>
<div @click="showReplies = false" />
</div>
<transition-group <transition-group
name="comment-replies" name="comment-replies"
v-if="showReplies" v-if="showReplies"
tag="ul" tag="ul"
class="flex flex-col gap-2" class="flex flex-col gap-2"
> >
<Comment <EventComment
v-for="reply in comment.replies" v-for="reply in comment.replies"
:key="reply.id" :key="reply.id"
:comment="reply" :comment="reply"
@ -169,9 +183,21 @@
@create-comment="emit('create-comment', $event)" @create-comment="emit('create-comment', $event)"
@delete-comment="emit('delete-comment', $event)" @delete-comment="emit('delete-comment', $event)"
@report-comment="emit('report-comment', $event)" @report-comment="emit('report-comment', $event)"
class="ml-2"
/> />
</transition-group> </transition-group>
</div> <o-modal
v-model:active="isReportModalActive"
has-modal-card
ref="reportModal"
:close-button-aria-label="t('Close')"
>
<ReportModal
:on-confirm="reportComment"
:title="t('Report this comment')"
:outside-domain="comment.actor?.domain"
/>
</o-modal>
</li> </li>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -197,8 +223,13 @@ import Delete from "vue-material-design-icons/Delete.vue";
import Alert from "vue-material-design-icons/Alert.vue"; import Alert from "vue-material-design-icons/Alert.vue";
import ChevronUp from "vue-material-design-icons/ChevronUp.vue"; import ChevronUp from "vue-material-design-icons/ChevronUp.vue";
import ChevronDown from "vue-material-design-icons/ChevronDown.vue"; import ChevronDown from "vue-material-design-icons/ChevronDown.vue";
import DotsHorizontal from "vue-material-design-icons/DotsHorizontal.vue";
import Reply from "vue-material-design-icons/Reply.vue"; import Reply from "vue-material-design-icons/Reply.vue";
import type { Locale } from "date-fns"; import type { Locale } from "date-fns";
import ReportModal from "@/components/Report/ReportModal.vue";
import { useCreateReport } from "@/composition/apollo/report";
import { Snackbar } from "@/plugins/snackbar";
import { useProgrammatic } from "@oruga-ui/oruga-next";
const Editor = defineAsyncComponent( const Editor = defineAsyncComponent(
() => import("@/components/TextEditor.vue") () => import("@/components/TextEditor.vue")
@ -214,26 +245,20 @@ const props = withDefaults(
{ rootComment: true } { rootComment: true }
); );
const emit = defineEmits([ const emit = defineEmits<{
"create-comment", (e: "create-comment", comment: IComment): void;
"delete-comment", (e: "delete-comment", comment: IComment): void;
"report-comment", (e: "report-comment", comment: IComment): void;
]); }>();
const commentEditor = ref<typeof EditorComponent | null>(null); const commentEditor = ref<typeof EditorComponent | null>(null);
// Hack because Vue only exports it's own interface.
// See https://github.com/kaorun343/vue-property-decorator/issues/257
// @Ref() readonly commentEditor!: EditorComponent & {
// replyToComment: (comment: IComment) => void;
// focus: () => void;
// };
const newComment = ref<IComment>(new CommentModel()); const newComment = ref<IComment>(new CommentModel());
const replyTo = ref(false); const replyTo = ref(false);
const showReplies = ref(false); const showReplies = ref(false);
const route = useRoute(); const route = useRoute();
const { t } = useI18n({ useScope: "global" }); const { t } = useI18n({ useScope: "global" });
const isReportModalActive = ref(false);
onMounted(() => { onMounted(() => {
if (route?.hash.includes(`#comment-${props.comment.uuid}`)) { if (route?.hash.includes(`#comment-${props.comment.uuid}`)) {
@ -310,219 +335,57 @@ const reportModal = (): void => {
// }); // });
}; };
// const reportComment = async ( const {
// content: string, mutate: createReportMutation,
// forward: boolean onError: onCreateReportError,
// ): Promise<void> => { onDone: oneCreateReportDone,
// try { } = useCreateReport();
// if (!props.comment.actor) return;
// const { onError, onDone } = useMutation(CREATE_REPORT, () => ({ const reportComment = async (
// variables: { content: string,
// eventId: props.event.id, forward: boolean
// reportedId: props.comment.actor?.id, ): Promise<void> => {
// commentsIds: [props.comment.id], if (!props.comment.actor) return;
// content, createReportMutation({
// forward, eventId: props.event.id,
// }, reportedId: props.comment.actor?.id ?? "",
// })); commentsIds: [props.comment.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.comment.actor?.preferredUsername,
}),
variant: "success",
position: "bottom-right",
duration: 5000,
});
});
// // this.$buefy.notification.open({
// // message: this.t("Comment from @{username} reported", {
// // username: this.comment.actor.preferredUsername,
// // }) as string,
// // type: "is-success",
// // position: "is-bottom-right",
// // duration: 5000,
// // });
// } catch (e: any) {
// if (e.message) {
// // Snackbar.open({
// // message: e.message,
// // type: "is-danger",
// // position: "is-bottom",
// // });
// }
// }
// };
const actorComment = computed(() => props.comment.actor); const actorComment = computed(() => props.comment.actor);
const dateFnsLocale = inject<Locale>("dateFnsLocale"); const dateFnsLocale = inject<Locale>("dateFnsLocale");
</script> </script>
<style lang="scss" scoped> <style>
@use "@/styles/_mixins" as *; article.mbz-comment .mention.h-card {
form.reply { @apply inline-block border border-zinc-600 dark:border-zinc-300 rounded py-0.5 px-1;
padding-bottom: 1rem;
}
.first-line {
margin-bottom: 3px;
* {
padding: 0 5px 0 0;
}
strong.organizer {
border-radius: 12px;
color: white;
padding: 0 6px;
}
// & > small {
// @include margin-left(0.3rem);
// }
}
.editor-line {
display: flex;
max-width: calc(80rem - 64px);
.editor {
flex: 1;
// @include padding-right(10px);
margin-bottom: 0;
}
}
a.comment-link {
text-decoration: none;
// @include margin-left(5px);
color: text;
&:hover {
text-decoration: underline;
}
small {
&:hover {
color: hsl(0, 0%, 21%);
}
}
}
.comment-element {
padding: 0.25rem;
border-radius: 5px;
&.announcement {
small {
color: hsl(0, 0%, 21%);
}
}
&.selected {
.reply-btn,
small,
span,
strong,
a.comment-link:hover {
text-decoration: underline;
}
}
// .media-left {
// @include margin-right(5px);
// }
}
.root-comment .replies {
display: flex;
.left {
display: flex;
flex-direction: column;
align-items: center;
// @include margin-right(10px);
.vertical-border {
width: 3px;
height: 100%;
background-color: rgba(0, 0, 0, 0.05);
margin: 10px calc(1rem + 1px);
cursor: pointer;
&:hover {
background-color: rgba(0, 0, 0, 0.1);
}
}
}
}
.media .media-content {
overflow-x: initial;
.content {
text-align: start;
.editor-line {
display: flex;
align-items: center;
}
}
.icons {
display: none;
}
}
.media:hover .media-content .icons {
display: inline;
button {
cursor: pointer;
border: none;
background: none;
}
}
.load-replies {
cursor: pointer;
& > p > span {
font-weight: bold;
}
}
.level-item.reply-btn {
font-weight: bold;
}
article {
border-radius: 4px;
margin-bottom: 5px;
}
.comment-replies {
flex-grow: 1;
}
.comment-replies-enter-active,
.comment-replies-leave-active,
.comment-replies-move {
transition: 500ms cubic-bezier(0.59, 0.12, 0.34, 0.95);
transition-property: opacity, transform;
}
.comment-replies-enter {
opacity: 0;
transform: translateX(50px) scaleY(0.5);
}
.comment-replies-enter-to {
opacity: 1;
transform: translateX(0) scaleY(1);
}
.comment-replies-leave-active {
position: absolute;
}
.comment-replies-leave-to {
opacity: 0;
transform: scaleY(0);
transform-origin: center top;
}
// .reply-action .icon {
// @include padding-right(0.4rem);
// }
.visually-hidden {
display: none;
} }
</style> </style>

View File

@ -0,0 +1,22 @@
import { Extension } from "@tiptap/vue-3";
export interface RichEditorKeyboardSubmitOptions {
submit: () => void;
}
export default Extension.create<RichEditorKeyboardSubmitOptions>({
name: "RichEditorKeyboardSubmit",
addOptions() {
return {
submit: () => ({}),
};
},
addKeyboardShortcuts() {
return {
"Ctrl-Enter": () => {
this.options.submit();
return true;
},
};
},
});

View File

@ -9,7 +9,7 @@
:isInternal="isInternal" :isInternal="isInternal"
> >
<div <div
class="bg-secondary rounded-lg" class="rounded-lg"
:class="{ 'sm:w-full sm:max-w-[20rem]': mode === 'row' }" :class="{ 'sm:w-full sm:max-w-[20rem]': mode === 'row' }"
> >
<figure class="block relative pt-40"> <figure class="block relative pt-40">

View File

@ -1,23 +1,35 @@
<template> <template>
<div class="dark:bg-zinc-700 p-2 rounded" v-if="report"> <div
class="bg-mbz-yellow-alt-50 hover:bg-mbz-yellow-alt-100 dark:bg-zinc-700 hover:dark:bg-zinc-600 rounded"
v-if="report"
>
<div class="flex justify-between gap-1 border-b p-2">
<div class="flex gap-1"> <div class="flex gap-1">
<figure class="" v-if="report.reported.avatar"> <figure class="" v-if="report.reported.avatar">
<img <img
alt="" alt=""
:src="report.reported.avatar.url" :src="report.reported.avatar.url"
class="rounded-full" class="rounded-full"
width="48" width="24"
height="48" height="24"
/> />
</figure> </figure>
<AccountCircle v-else :size="48" /> <AccountCircle v-else :size="24" />
<div class=""> <div class="">
<p class="" v-if="report.reported.name">{{ report.reported.name }}</p> <p class="" v-if="report.reported.name">{{ report.reported.name }}</p>
<p class="">@{{ usernameWithDomain(report.reported) }}</p> <p class="text-zinc-700 dark:text-zinc-100 text-sm">
@{{ usernameWithDomain(report.reported) }}
</p>
</div>
</div>
<div>
<p v-if="report.reported.suspended" class="text-red-700 font-bold">
{{ t("Suspended") }}
</p>
</div> </div>
</div> </div>
<div class="reported_by"> <div class="p-2">
<div class=""> <div class="">
<span v-if="report.reporter.type === ActorType.APPLICATION"> <span v-if="report.reporter.type === ActorType.APPLICATION">
{{ {{
@ -26,6 +38,14 @@
}) })
}} }}
</span> </span>
<span
v-if="
report.reporter.preferredUsername === 'anonymous' &&
!report.reporter.domain
"
>
{{ t("Reported by someone anonymously") }}
</span>
<span v-else> <span v-else>
{{ {{
t("Reported by {reporter}", { t("Reported by {reporter}", {

View File

@ -90,6 +90,7 @@ import { computed, ref } from "vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { IComment } from "../../types/comment.model"; import { IComment } from "../../types/comment.model";
import { usernameWithDomain } from "@/types/actor"; import { usernameWithDomain } from "@/types/actor";
import AccountCircle from "vue-material-design-icons/AccountCircle.vue";
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{

View File

@ -208,6 +208,7 @@ import Gapcursor from "@tiptap/extension-gapcursor";
import History from "@tiptap/extension-history"; import History from "@tiptap/extension-history";
import { IActor, IPerson, usernameWithDomain } from "../types/actor"; import { IActor, IPerson, usernameWithDomain } from "../types/actor";
import CustomImage from "./Editor/Image"; import CustomImage from "./Editor/Image";
import RichEditorKeyboardSubmit from "./Editor/RichEditorKeyboardSubmit";
import { UPLOAD_MEDIA } from "../graphql/upload"; import { UPLOAD_MEDIA } from "../graphql/upload";
import { listenFileUpload } from "../utils/upload"; import { listenFileUpload } from "../utils/upload";
import Mention from "@tiptap/extension-mention"; import Mention from "@tiptap/extension-mention";
@ -252,7 +253,7 @@ const props = withDefaults(
} }
); );
const emit = defineEmits(["update:modelValue"]); const emit = defineEmits(["update:modelValue", "submit"]);
const isDescriptionMode = computed((): boolean => { const isDescriptionMode = computed((): boolean => {
return props.mode === "description" || isBasicMode.value; return props.mode === "description" || isBasicMode.value;
@ -297,7 +298,7 @@ const editor = useEditor({
"aria-label": props.ariaLabel ?? "", "aria-label": props.ariaLabel ?? "",
role: "textbox", role: "textbox",
class: class:
"prose dark:prose-invert prose-sm sm:prose lg:prose-lg xl:prose-xl m-5 focus:outline-none !max-w-full", "prose dark:prose-invert prose-sm lg:prose-lg xl:prose-xl bg-zinc-50 dark:bg-zinc-700 focus:outline-none !max-w-full",
}, },
transformPastedHTML: transformPastedHTML, transformPastedHTML: transformPastedHTML,
}, },
@ -323,6 +324,9 @@ const editor = useEditor({
Link.configure({ Link.configure({
HTMLAttributes: { target: "_blank", rel: "noopener noreferrer ugc" }, HTMLAttributes: { target: "_blank", rel: "noopener noreferrer ugc" },
}), }),
RichEditorKeyboardSubmit.configure({
submit: () => emit("submit"),
}),
], ],
injectCSS: false, injectCSS: false,
content: props.modelValue, content: props.modelValue,
@ -480,9 +484,7 @@ onBeforeUnmount(() => {
div.ProseMirror { div.ProseMirror {
min-height: 2.5rem; min-height: 2.5rem;
box-shadow: inset 0 1px 2px rgba(10, 10, 10, 0.1); box-shadow: inset 0 1px 2px rgba(10, 10, 10, 0.1);
background-color: white;
border-radius: 4px; border-radius: 4px;
color: #363636;
border: 1px solid #dbdbdb; border: 1px solid #dbdbdb;
padding: 12px 6px; padding: 12px 6px;

View File

@ -61,7 +61,7 @@ export function useAnonymousReportsConfig() {
}>(ANONYMOUS_REPORTS_CONFIG, undefined, { fetchPolicy: "cache-only" }); }>(ANONYMOUS_REPORTS_CONFIG, undefined, { fetchPolicy: "cache-only" });
const anonymousReportsConfig = computed( const anonymousReportsConfig = computed(
() => configResult.value?.config?.anonymous?.participation () => configResult.value?.config?.anonymous?.reports
); );
return { anonymousReportsConfig, error, loading }; return { anonymousReportsConfig, error, loading };
} }

View File

@ -14,9 +14,11 @@ export const REPORTS = gql`
id id
reported { reported {
...ActorFragment ...ActorFragment
suspended
} }
reporter { reporter {
...ActorFragment ...ActorFragment
suspended
} }
event { event {
id id

View File

@ -52,7 +52,7 @@
"Close": "Close", "Close": "Close",
"Closed": "Closed", "Closed": "Closed",
"Comment deleted": "Comment deleted", "Comment deleted": "Comment deleted",
"Comment from @{username} reported": "Comment from @{username} reported", "Comment from {'@'}{username} reported": "Comment from {'@'}{username} reported",
"Comments": "Comments", "Comments": "Comments",
"Confirm my participation": "Confirm my participation", "Confirm my participation": "Confirm my participation",
"Confirm my particpation": "Confirm my particpation", "Confirm my particpation": "Confirm my particpation",

View File

@ -192,7 +192,7 @@
"Closed": "Fermé", "Closed": "Fermé",
"Comment body": "Corps du commentaire", "Comment body": "Corps du commentaire",
"Comment deleted": "Commentaire supprimé", "Comment deleted": "Commentaire supprimé",
"Comment from @{username} reported": "Commentaire de @{username} signalé", "Comment from {'@'}{username} reported": "Commentaire de {'@'}{username} signalé",
"Comment text can't be empty": "Le texte du commentaire ne peut être vide", "Comment text can't be empty": "Le texte du commentaire ne peut être vide",
"Comments": "Commentaires", "Comments": "Commentaires",
"Comments are closed for everybody else.": "Les commentaires sont fermés pour tou·te·s les autres.", "Comments are closed for everybody else.": "Les commentaires sont fermés pour tou·te·s les autres.",

View File

@ -123,7 +123,7 @@ const { result: membershipsResult } = useQuery<{
() => ({ enabled: currentActor.value?.id !== undefined }) () => ({ enabled: currentActor.value?.id !== undefined })
); );
const memberships = computed( const memberships = computed(
() => membershipsResult.value?.person.memberships.elements () => membershipsResult.value?.person.memberships?.elements
); );
const route = useRoute(); const route = useRoute();

View File

@ -543,7 +543,7 @@
:to="{ :to="{
name: RouteName.GROUP_EVENTS, name: RouteName.GROUP_EVENTS,
params: { preferredUsername: usernameWithDomain(group) }, params: { preferredUsername: usernameWithDomain(group) },
query: { future: false }, query: { showPassedEvents: true },
}" }"
>{{ t("View past events") }}</o-button >{{ t("View past events") }}</o-button
> >
@ -559,7 +559,9 @@
:to="{ :to="{
name: RouteName.GROUP_EVENTS, name: RouteName.GROUP_EVENTS,
params: { preferredUsername: usernameWithDomain(group) }, params: { preferredUsername: usernameWithDomain(group) },
query: { future: organizedEvents.elements.length > 0 }, query: {
showPassedEvents: organizedEvents.elements.length === 0,
},
}" }"
>{{ t("View all events") }}</o-button >{{ t("View all events") }}</o-button
> >
@ -696,7 +698,7 @@ const {
group, group,
loading: groupLoading, loading: groupLoading,
refetch: refetchGroup, refetch: refetchGroup,
} = useGroup(props.preferredUsername); } = useGroup(props.preferredUsername, { afterDateTime: new Date() });
const router = useRouter(); const router = useRouter();
const { t } = useI18n({ useScope: "global" }); const { t } = useI18n({ useScope: "global" });
@ -913,35 +915,41 @@ const toggleFollowNotify = () => {
}); });
}; };
const reportGroup = async (content: string, forward: boolean) => {
isReportModalActive.value = false;
reportModalRef.value.close();
const { const {
mutate: createReportMutation, mutate: createReportMutation,
onError: onCreateReportError, onError: onCreateReportError,
onDone: oneCreateReportDone, onDone: onCreateReportDone,
} = useCreateReport(); } = useCreateReport();
const reportGroup = (content: string, forward: boolean) => {
isReportModalActive.value = false;
console.debug("report group", {
reportedId: group.value?.id ?? "",
content,
forward,
});
createReportMutation({ createReportMutation({
reportedId: group.value?.id ?? "", reportedId: group.value?.id ?? "",
content, content,
forward, forward,
}); });
};
oneCreateReportDone(() => { onCreateReportDone(() => {
notifier?.success(t("Group {groupTitle} reported", { groupTitle })); notifier?.success(
t("Group {groupTitle} reported", { groupTitle: groupTitle.value })
);
}); });
onCreateReportError((error: any) => { onCreateReportError((error: any) => {
console.error(error); console.error(error);
notifier?.error( notifier?.error(
t("Error while reporting group {groupTitle}", { t("Error while reporting group {groupTitle}", {
groupTitle, groupTitle: groupTitle.value,
}) })
); );
}); });
};
const triggerShare = (): void => { const triggerShare = (): void => {
if (navigator.share) { if (navigator.share) {
@ -1030,8 +1038,8 @@ const physicalAddress = computed((): Address | null => {
return new Address(group.value?.physicalAddress); return new Address(group.value?.physicalAddress);
}); });
const ableToReport = computed((): boolean | undefined => { const ableToReport = computed((): boolean => {
return anonymousReportsConfig.value?.allowed; return anonymousReportsConfig.value?.allowed === true;
}); });
const organizedEvents = computed((): Paginate<IEvent> => { const organizedEvents = computed((): Paginate<IEvent> => {

View File

@ -14,14 +14,25 @@
/> />
<section v-if="actionLogs.total > 0 && actionLogs.elements.length > 0"> <section v-if="actionLogs.total > 0 && actionLogs.elements.length > 0">
<ul> <ul>
<li v-for="log in actionLogs.elements" :key="log.id"> <li
<div class="box"> v-for="log in actionLogs.elements"
:key="log.id"
class="bg-mbz-yellow-alt-50 hover:bg-mbz-yellow-alt-100 dark:bg-zinc-700 hover:dark:bg-zinc-600 rounded p-2 my-1"
>
<div class="flex gap-1">
<div class="flex gap-1">
<figure class="" v-if="log.actor?.avatar">
<img <img
class="image" alt=""
:src="log.actor.avatar.url" :src="log.actor.avatar?.url"
:alt="log.actor.avatar.alt || ''" class="rounded-full"
v-if="log.actor.avatar" width="36"
height="36"
/> />
</figure>
<AccountCircle v-else :size="36" />
</div>
<div>
<i18n-t <i18n-t
v-if="log.action === ActionLogAction.REPORT_UPDATE_CLOSED" v-if="log.action === ActionLogAction.REPORT_UPDATE_CLOSED"
tag="span" tag="span"
@ -29,15 +40,17 @@
> >
<template #moderator> <template #moderator>
<router-link <router-link
class="underline"
:to="{ :to="{
name: RouteName.ADMIN_PROFILE, name: RouteName.ADMIN_PROFILE,
params: { id: log.actor.id }, params: { id: log.actor.id },
}" }"
>@{{ log.actor.preferredUsername }}</router-link >{{ displayName(log.actor) }}</router-link
> >
</template> </template>
<template #report> <template #report>
<router-link <router-link
class="underline"
:to="{ :to="{
name: RouteName.REPORT, name: RouteName.REPORT,
params: { reportId: log.object.id }, params: { reportId: log.object.id },
@ -57,15 +70,17 @@
> >
<template #moderator> <template #moderator>
<router-link <router-link
class="underline"
:to="{ :to="{
name: RouteName.ADMIN_PROFILE, name: RouteName.ADMIN_PROFILE,
params: { id: log.actor.id }, params: { id: log.actor.id },
}" }"
>@{{ log.actor.preferredUsername }}</router-link >{{ displayName(log.actor) }}</router-link
> >
</template> </template>
<template #report> <template #report>
<router-link <router-link
class="underline"
:to="{ :to="{
name: RouteName.REPORT, name: RouteName.REPORT,
params: { reportId: log.object.id }, params: { reportId: log.object.id },
@ -79,21 +94,25 @@
</template> </template>
</i18n-t> </i18n-t>
<i18n-t <i18n-t
v-else-if="log.action === ActionLogAction.REPORT_UPDATE_RESOLVED" v-else-if="
log.action === ActionLogAction.REPORT_UPDATE_RESOLVED
"
tag="span" tag="span"
keypath="{moderator} marked {report} as resolved" keypath="{moderator} marked {report} as resolved"
> >
<template #moderator> <template #moderator>
<router-link <router-link
class="underline"
:to="{ :to="{
name: RouteName.ADMIN_PROFILE, name: RouteName.ADMIN_PROFILE,
params: { id: log.actor.id }, params: { id: log.actor.id },
}" }"
>@{{ log.actor.preferredUsername }}</router-link >{{ displayName(log.actor) }}</router-link
> >
</template> </template>
<template #report> <template #report>
<router-link <router-link
class="underline"
:to="{ :to="{
name: RouteName.REPORT, name: RouteName.REPORT,
params: { reportId: log.object.id }, params: { reportId: log.object.id },
@ -113,15 +132,17 @@
> >
<template #moderator> <template #moderator>
<router-link <router-link
class="underline"
:to="{ :to="{
name: RouteName.ADMIN_PROFILE, name: RouteName.ADMIN_PROFILE,
params: { id: log.actor.id }, params: { id: log.actor.id },
}" }"
>@{{ log.actor.preferredUsername }}</router-link >{{ displayName(log.actor) }}</router-link
> >
</template> </template>
<template #report> <template #report>
<router-link <router-link
class="underline"
v-if="log.object.report" v-if="log.object.report"
:to="{ :to="{
name: RouteName.REPORT, name: RouteName.REPORT,
@ -143,11 +164,12 @@
> >
<template #moderator> <template #moderator>
<router-link <router-link
class="underline"
:to="{ :to="{
name: RouteName.ADMIN_PROFILE, name: RouteName.ADMIN_PROFILE,
params: { id: log.actor.id }, params: { id: log.actor.id },
}" }"
>@{{ log.actor.preferredUsername }}</router-link >{{ displayName(log.actor) }}</router-link
> >
</template> </template>
<template #title> <template #title>
@ -164,15 +186,17 @@
> >
<template #moderator> <template #moderator>
<router-link <router-link
class="underline"
:to="{ :to="{
name: RouteName.ADMIN_PROFILE, name: RouteName.ADMIN_PROFILE,
params: { id: log.actor.id }, params: { id: log.actor.id },
}" }"
>@{{ log.actor.preferredUsername }}</router-link >{{ displayName(log.actor) }}</router-link
> >
</template> </template>
<template #profile> <template #profile>
<router-link <router-link
class="underline"
:to="{ :to="{
name: RouteName.ADMIN_PROFILE, name: RouteName.ADMIN_PROFILE,
params: { id: log.object.id }, params: { id: log.object.id },
@ -191,15 +215,17 @@
> >
<template #moderator> <template #moderator>
<router-link <router-link
class="underline"
:to="{ :to="{
name: RouteName.ADMIN_PROFILE, name: RouteName.ADMIN_PROFILE,
params: { id: log.actor.id }, params: { id: log.actor.id },
}" }"
>@{{ log.actor.preferredUsername }}</router-link >{{ displayName(log.actor) }}</router-link
> >
</template> </template>
<template #profile> <template #profile>
<router-link <router-link
class="underline"
:to="{ :to="{
name: RouteName.ADMIN_PROFILE, name: RouteName.ADMIN_PROFILE,
params: { id: log.object.id }, params: { id: log.object.id },
@ -218,15 +244,17 @@
> >
<template #moderator> <template #moderator>
<router-link <router-link
class="underline"
:to="{ :to="{
name: RouteName.ADMIN_PROFILE, name: RouteName.ADMIN_PROFILE,
params: { id: log.actor.id }, params: { id: log.actor.id },
}" }"
>@{{ log.actor.preferredUsername }}</router-link >{{ displayName(log.actor) }}</router-link
> >
</template> </template>
<template #profile> <template #profile>
<router-link <router-link
class="underline"
:to="{ :to="{
name: RouteName.ADMIN_GROUP_PROFILE, name: RouteName.ADMIN_GROUP_PROFILE,
params: { id: log.object.id }, params: { id: log.object.id },
@ -245,15 +273,17 @@
> >
<template #moderator> <template #moderator>
<router-link <router-link
class="underline"
:to="{ :to="{
name: RouteName.ADMIN_PROFILE, name: RouteName.ADMIN_PROFILE,
params: { id: log.actor.id }, params: { id: log.actor.id },
}" }"
>@{{ log.actor.preferredUsername }}</router-link >{{ displayName(log.actor) }}</router-link
> >
</template> </template>
<template #profile> <template #profile>
<router-link <router-link
class="underline"
:to="{ :to="{
name: RouteName.ADMIN_GROUP_PROFILE, name: RouteName.ADMIN_GROUP_PROFILE,
params: { id: log.object.id }, params: { id: log.object.id },
@ -269,15 +299,17 @@
> >
<template #moderator> <template #moderator>
<router-link <router-link
class="underline"
:to="{ :to="{
name: RouteName.ADMIN_PROFILE, name: RouteName.ADMIN_PROFILE,
params: { id: log.actor.id }, params: { id: log.actor.id },
}" }"
>@{{ log.actor.preferredUsername }}</router-link >{{ displayName(log.actor) }}</router-link
> >
</template> </template>
<template #user> <template #user>
<router-link <router-link
class="underline"
v-if="log.object.confirmedAt" v-if="log.object.confirmedAt"
:to="{ :to="{
name: RouteName.ADMIN_USER_PROFILE, name: RouteName.ADMIN_USER_PROFILE,
@ -300,15 +332,17 @@
> >
<template #moderator> <template #moderator>
<router-link <router-link
class="underline"
:to="{ :to="{
name: RouteName.ADMIN_PROFILE, name: RouteName.ADMIN_PROFILE,
params: { id: log.actor.id }, params: { id: log.actor.id },
}" }"
>@{{ log.actor.preferredUsername }}</router-link >{{ displayName(log.actor) }}</router-link
> >
</template> </template>
<template #event> <template #event>
<router-link <router-link
class="underline"
v-if="log.object.event && log.object.event.uuid" v-if="log.object.event && log.object.event.uuid"
:to="{ :to="{
name: RouteName.EVENT, name: RouteName.EVENT,
@ -320,6 +354,7 @@
</template> </template>
<template #author> <template #author>
<router-link <router-link
class="underline"
:to="{ :to="{
name: RouteName.ADMIN_PROFILE, name: RouteName.ADMIN_PROFILE,
params: { id: log.object.actor.id }, params: { id: log.object.actor.id },
@ -337,15 +372,17 @@
> >
<template #moderator> <template #moderator>
<router-link <router-link
class="underline"
:to="{ :to="{
name: RouteName.ADMIN_PROFILE, name: RouteName.ADMIN_PROFILE,
params: { id: log.actor.id }, params: { id: log.actor.id },
}" }"
>@{{ log.actor.preferredUsername }}</router-link >{{ displayName(log.actor) }}</router-link
> >
</template> </template>
<template #author> <template #author>
<router-link <router-link
class="underline"
:to="{ :to="{
name: RouteName.ADMIN_PROFILE, name: RouteName.ADMIN_PROFILE,
params: { id: log.object.actor.id }, params: { id: log.object.actor.id },
@ -363,17 +400,19 @@
> >
<template #moderator> <template #moderator>
<router-link <router-link
class="underline"
:to="{ :to="{
name: RouteName.ADMIN_PROFILE, name: RouteName.ADMIN_PROFILE,
params: { id: log.actor.id }, params: { id: log.actor.id },
}" }"
>@{{ log.actor.preferredUsername }}</router-link >{{ displayName(log.actor) }}</router-link
> >
</template> </template>
</i18n-t> </i18n-t>
<br /> <br />
<small>{{ formatDateTimeString(log.insertedAt) }}</small> <small>{{ formatDateTimeString(log.insertedAt) }}</small>
</div> </div>
</div>
</li> </li>
</ul> </ul>
<o-pagination <o-pagination
@ -399,7 +438,7 @@ import { IActionLog } from "@/types/report.model";
import { LOGS } from "@/graphql/report"; import { LOGS } from "@/graphql/report";
import { ActionLogAction } from "@/types/enums"; import { ActionLogAction } from "@/types/enums";
import RouteName from "../../router/name"; import RouteName from "../../router/name";
import { displayNameAndUsername } from "../../types/actor"; import { displayNameAndUsername, displayName } from "../../types/actor";
import { Paginate } from "@/types/paginate"; import { Paginate } from "@/types/paginate";
import { useQuery } from "@vue/apollo-composable"; import { useQuery } from "@vue/apollo-composable";
import { integerTransformer, useRouteQuery } from "vue-use-route-query"; import { integerTransformer, useRouteQuery } from "vue-use-route-query";
@ -407,6 +446,7 @@ import { useHead } from "@vueuse/head";
import { computed } from "vue"; import { computed } from "vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { formatDateTimeString } from "@/filters/datetime"; import { formatDateTimeString } from "@/filters/datetime";
import AccountCircle from "vue-material-design-icons/AccountCircle.vue";
const LOGS_PER_PAGE = 10; const LOGS_PER_PAGE = 10;
@ -436,10 +476,6 @@ img.image {
vertical-align: text-bottom; vertical-align: text-bottom;
} }
a {
text-decoration: none;
}
section ul li { section ul li {
margin: 0.5rem auto; margin: 0.5rem auto;
} }

View File

@ -119,7 +119,10 @@ const { result: reportsResult } = useQuery<{ reports: Paginate<IReport> }>(
status: status.value, status: status.value,
limit: REPORT_PAGE_LIMIT, limit: REPORT_PAGE_LIMIT,
domain: filterDomain.value, domain: filterDomain.value,
}) }),
{
fetchPolicy: "cache-and-network",
}
); );
const reports = computed( const reports = computed(

View File

@ -136,8 +136,9 @@
</tr> </tr>
<tr v-if="report.event && report.comments.length > 0"> <tr v-if="report.event && report.comments.length > 0">
<td>{{ t("Event") }}</td> <td>{{ t("Event") }}</td>
<td> <td class="flex gap-2 items-center">
<router-link <router-link
class="underline"
:to="{ :to="{
name: RouteName.EVENT, name: RouteName.EVENT,
params: { uuid: report.event.uuid }, params: { uuid: report.event.uuid },
@ -145,41 +146,32 @@
> >
{{ report.event.title }} {{ report.event.title }}
</router-link> </router-link>
<span>
<!-- <o-button-->
<!-- tag="router-link"-->
<!-- variant="primary"-->
<!-- :to="{ name: RouteName.EDIT_EVENT, params: {eventId: report.event.uuid } }"-->
<!-- icon-left="pencil"-->
<!-- size="small">{{ t('Edit') }}</o-button>-->
<o-button <o-button
variant="danger" variant="danger"
@click="confirmEventDelete()" @click="confirmEventDelete()"
icon-left="delete" icon-left="delete"
size="small"
>{{ t("Delete") }}</o-button >{{ t("Delete") }}</o-button
> >
</span>
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</section> </section>
<section> <section class="bg-white dark:bg-zinc-700 rounded px-2 pt-1 pb-2 my-3">
<h2>{{ t("Report reason") }}</h2> <h2 class="mb-1">{{ t("Report reason") }}</h2>
<div class="dark:bg-zinc-700 p-2 rounded my-2"> <div class="">
<div class="flex gap-1"> <div class="flex gap-1">
<figure class="" v-if="report.reported.avatar"> <figure class="" v-if="report.reported.avatar">
<img <img
alt="" alt=""
:src="report.reported.avatar.url" :src="report.reported.avatar.url"
class="rounded-full" class="rounded-full"
width="48" width="36"
height="48" height="36"
/> />
</figure> </figure>
<AccountCircle v-else :size="48" /> <AccountCircle v-else :size="36" />
<div class=""> <div class="">
<p class="" v-if="report.reported.name"> <p class="" v-if="report.reported.name">
{{ report.reported.name }} {{ report.reported.name }}
@ -196,15 +188,12 @@
</div> </div>
</section> </section>
<section class="" v-if="report.event && report.comments.length === 0"> <section
<h2>{{ t("Reported content") }}</h2> class="bg-white dark:bg-zinc-700 rounded px-2 pt-1 pb-2 my-3"
<EventCard :event="report.event" mode="row" class="my-2" /> v-if="report.event && report.comments.length === 0"
<!-- <o-button--> >
<!-- tag="router-link"--> <h2 class="mb-1">{{ t("Reported content") }}</h2>
<!-- variant="primary"--> <EventCard :event="report.event" mode="row" class="my-2 max-w-4xl" />
<!-- :to="{ name: RouteName.EDIT_EVENT, params: {eventId: report.event.uuid } }"-->
<!-- icon-left="pencil"-->
<!-- size="small">{{ t('Edit') }}</o-button>-->
<o-button <o-button
variant="danger" variant="danger"
@click="confirmEventDelete()" @click="confirmEventDelete()"
@ -214,32 +203,35 @@
> >
</section> </section>
<div v-if="report.comments.length > 0"> <section
class="bg-white dark:bg-zinc-700 rounded px-2 pt-1 pb-2 my-3"
v-if="report.comments.length > 0"
>
<h2 class="mb-1">{{ t("Reported content") }}</h2>
<ul v-for="comment in report.comments" :key="comment.id"> <ul v-for="comment in report.comments" :key="comment.id">
<li> <li>
<div class="" v-if="comment"> <div class="" v-if="comment">
<article class="flex gap-1"> <article>
<div class=""> <div class="flex gap-1">
<figure class="" v-if="comment.actor && comment.actor.avatar"> <figure class="" v-if="comment.actor?.avatar">
<img <img
:src="comment.actor.avatar.url"
alt="" alt=""
width="48" :src="comment.actor.avatar?.url"
height="48" class="rounded-full"
width="36"
height="36"
/> />
</figure> </figure>
<AccountCircle :size="48" v-else /> <AccountCircle v-else :size="36" />
<div>
<div v-if="comment.actor">
<p>{{ comment.actor.name }}</p>
<p>@{{ comment.actor.preferredUsername }}</p>
</div> </div>
<div class="">
<div class="prose dark:prose-invert">
<span v-if="comment.actor">
<strong>{{ comment.actor.name }}</strong>
<small>@{{ comment.actor.preferredUsername }}</small>
</span>
<span v-else>{{ t("Unknown actor") }}</span> <span v-else>{{ t("Unknown actor") }}</span>
<br />
<p v-html="comment.text" />
</div> </div>
</div>
<div class="prose dark:prose-invert" v-html="comment.text" />
<o-button <o-button
variant="danger" variant="danger"
@click="confirmCommentDelete(comment)" @click="confirmCommentDelete(comment)"
@ -247,17 +239,16 @@
size="small" size="small"
>{{ t("Delete") }}</o-button >{{ t("Delete") }}</o-button
> >
</div>
</article> </article>
</div> </div>
</li> </li>
</ul> </ul>
</div> </section>
<section> <section class="bg-white dark:bg-zinc-700 rounded px-2 pt-1 pb-2 my-3">
<h2>{{ t("Notes") }}</h2> <h2 class="mb-1">{{ t("Notes") }}</h2>
<div <div
class="box note" class=""
v-for="note in report.notes" v-for="note in report.notes"
:id="`note-${note.id}`" :id="`note-${note.id}`"
:key="note.id" :key="note.id"
@ -536,8 +527,4 @@ tbody td img.image,
text-decoration: none; text-decoration: none;
color: inherit; color: inherit;
} }
td > a {
text-decoration: none;
}
</style> </style>

View File

@ -529,6 +529,16 @@
class="my-4" class="my-4"
/> />
</div> </div>
<o-notification v-else-if="searchLoading === false" variant="info">
<p>{{ t("No events found") }}</p>
<p v-if="searchIsUrl && !currentUser?.id">
{{
t(
"Only registered users may fetch remote events from their URL."
)
}}
</p>
</o-notification>
<o-pagination <o-pagination
v-if=" v-if="
(searchEvents && searchEvents?.total > EVENT_PAGE_LIMIT) || (searchEvents && searchEvents?.total > EVENT_PAGE_LIMIT) ||
@ -544,16 +554,6 @@
:aria-page-label="t('Page')" :aria-page-label="t('Page')"
:aria-current-label="t('Current page')" :aria-current-label="t('Current page')"
/> />
<o-notification v-else-if="searchLoading === false" variant="info">
<p>{{ t("No events found") }}</p>
<p v-if="searchIsUrl && !currentUser?.id">
{{
t(
"Only registered users may fetch remote events from their URL."
)
}}
</p>
</o-notification>
</template> </template>
<template v-else-if="contentType === ContentType.EVENTS"> <template v-else-if="contentType === ContentType.EVENTS">
<template v-if="searchEvents && searchEvents.total > 0"> <template v-if="searchEvents && searchEvents.total > 0">

View File

@ -1272,16 +1272,11 @@ defmodule Mobilizon.Events do
end end
end end
# @spec events_for_search_query(String.t()) :: Ecto.Query.t()
# defp events_for_search_query("") do
# Event
# |> join: rank in fragment("")
# end
defp events_for_search_query(search_string) do defp events_for_search_query(search_string) do
from(event in Event, Event
join: id_and_rank in matching_event_ids_and_ranks(search_string), |> distinct(:id)
on: id_and_rank.id == event.id |> join(:inner, [e], id_and_rank in matching_event_ids_and_ranks(search_string),
on: id_and_rank.id == e.id
) )
end end