Merge branch 'improvements' into 'master'
Some various improvements See merge request framasoft/mobilizon!936
This commit is contained in:
commit
f97fe9403c
|
@ -66,6 +66,7 @@ config :mobilizon, Mobilizon.Web.Upload,
|
||||||
uploader: Mobilizon.Web.Upload.Uploader.Local,
|
uploader: Mobilizon.Web.Upload.Uploader.Local,
|
||||||
filters: [
|
filters: [
|
||||||
Mobilizon.Web.Upload.Filter.Dedupe,
|
Mobilizon.Web.Upload.Filter.Dedupe,
|
||||||
|
Mobilizon.Web.Upload.Filter.AnalyzeMetadata,
|
||||||
Mobilizon.Web.Upload.Filter.Optimize
|
Mobilizon.Web.Upload.Filter.Optimize
|
||||||
],
|
],
|
||||||
allow_list_mime_types: ["image/gif", "image/jpeg", "image/png", "image/webp"],
|
allow_list_mime_types: ["image/gif", "image/jpeg", "image/png", "image/webp"],
|
||||||
|
|
|
@ -30,6 +30,7 @@
|
||||||
"@tiptap/starter-kit": "^2.0.0-beta.37",
|
"@tiptap/starter-kit": "^2.0.0-beta.37",
|
||||||
"@tiptap/vue-2": "^2.0.0-beta.21",
|
"@tiptap/vue-2": "^2.0.0-beta.21",
|
||||||
"apollo-absinthe-upload-link": "^1.5.0",
|
"apollo-absinthe-upload-link": "^1.5.0",
|
||||||
|
"blurhash": "^1.1.3",
|
||||||
"buefy": "^0.9.0",
|
"buefy": "^0.9.0",
|
||||||
"bulma-divider": "^0.2.0",
|
"bulma-divider": "^0.2.0",
|
||||||
"core-js": "^3.6.4",
|
"core-js": "^3.6.4",
|
||||||
|
|
|
@ -16,6 +16,7 @@ import { IMember } from "@/types/actor/member.model";
|
||||||
import { IComment } from "@/types/comment.model";
|
import { IComment } from "@/types/comment.model";
|
||||||
import { IEvent } from "@/types/event.model";
|
import { IEvent } from "@/types/event.model";
|
||||||
import { IActivity } from "@/types/activity.model";
|
import { IActivity } from "@/types/activity.model";
|
||||||
|
import uniqBy from "lodash/uniqBy";
|
||||||
|
|
||||||
type possibleTypes = { name: string };
|
type possibleTypes = { name: string };
|
||||||
type schemaType = {
|
type schemaType = {
|
||||||
|
@ -58,7 +59,7 @@ export const typePolicies: TypePolicies = {
|
||||||
Event: {
|
Event: {
|
||||||
fields: {
|
fields: {
|
||||||
participants: paginatedLimitPagination<IParticipant>(["roles"]),
|
participants: paginatedLimitPagination<IParticipant>(["roles"]),
|
||||||
commnents: pageLimitPagination<IComment>(),
|
comments: pageLimitPagination<IComment>(),
|
||||||
relatedEvents: pageLimitPagination<IEvent>(),
|
relatedEvents: pageLimitPagination<IEvent>(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -124,10 +125,6 @@ export function pageLimitPagination<T = Reference>(
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
merge(existing, incoming, { args }) {
|
merge(existing, incoming, { args }) {
|
||||||
console.log("pageLimitPagination");
|
|
||||||
console.log("existing", existing);
|
|
||||||
console.log("incoming", incoming);
|
|
||||||
// console.log("args", args);
|
|
||||||
if (!incoming) return existing;
|
if (!incoming) return existing;
|
||||||
if (!existing) return incoming; // existing will be empty the first time
|
if (!existing) return incoming; // existing will be empty the first time
|
||||||
|
|
||||||
|
@ -144,9 +141,6 @@ export function paginatedLimitPagination<T = Paginate<any>>(
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
merge(existing, incoming, { args }) {
|
merge(existing, incoming, { args }) {
|
||||||
console.log("paginatedLimitPagination");
|
|
||||||
console.log("existing", existing);
|
|
||||||
console.log("incoming", incoming);
|
|
||||||
if (!incoming) return existing;
|
if (!incoming) return existing;
|
||||||
if (!existing) return incoming; // existing will be empty the first time
|
if (!existing) return incoming; // existing will be empty the first time
|
||||||
|
|
||||||
|
@ -168,7 +162,6 @@ function doMerge<T = any>(
|
||||||
if (args) {
|
if (args) {
|
||||||
// Assume an page of 1 if args.page omitted.
|
// Assume an page of 1 if args.page omitted.
|
||||||
const { page = 1, limit = 10 } = args;
|
const { page = 1, limit = 10 } = args;
|
||||||
console.log("args, selected", { page, limit });
|
|
||||||
for (let i = 0; i < incoming.length; ++i) {
|
for (let i = 0; i < incoming.length; ++i) {
|
||||||
merged[(page - 1) * limit + i] = incoming[i];
|
merged[(page - 1) * limit + i] = incoming[i];
|
||||||
}
|
}
|
||||||
|
@ -179,7 +172,8 @@ function doMerge<T = any>(
|
||||||
// exception here, instead of recovering by appending incoming
|
// exception here, instead of recovering by appending incoming
|
||||||
// onto the existing array.
|
// onto the existing array.
|
||||||
res = [...merged, ...incoming];
|
res = [...merged, ...incoming];
|
||||||
|
// eslint-disable-next-line no-underscore-dangle
|
||||||
|
res = uniqBy(res, (elem: any) => elem.__ref);
|
||||||
}
|
}
|
||||||
console.log("doMerge returns", res);
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,14 +64,11 @@ $color-black: #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
// background: #f7f8fa;
|
|
||||||
background: $body-background-color;
|
background: $body-background-color;
|
||||||
font-family: BlinkMacSystemFont, Roboto, Oxygen, Ubuntu, Cantarell, "Segoe UI",
|
font-family: BlinkMacSystemFont, Roboto, Oxygen, Ubuntu, Cantarell, "Segoe UI",
|
||||||
"Fira Sans", "Droid Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
"Fira Sans", "Droid Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||||
|
|
||||||
/*main {*/
|
overflow-x: hidden;
|
||||||
/* margin: 1rem auto 0;*/
|
|
||||||
/*}*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#mobilizon > .container > .message {
|
#mobilizon > .container > .message {
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
<template>
|
<template>
|
||||||
<li :class="{ reply: comment.inReplyToComment }">
|
<li
|
||||||
<article
|
:class="{
|
||||||
class="media"
|
reply: comment.inReplyToComment,
|
||||||
:class="{ selected: commentSelected }"
|
announcement: comment.isAnnouncement,
|
||||||
:id="commentId"
|
selected: commentSelected,
|
||||||
>
|
}"
|
||||||
|
class="comment-element"
|
||||||
|
>
|
||||||
|
<article class="media" :id="commentId">
|
||||||
<popover-actor-card
|
<popover-actor-card
|
||||||
:actor="comment.actor"
|
:actor="comment.actor"
|
||||||
:inline="true"
|
:inline="true"
|
||||||
|
@ -33,14 +36,12 @@
|
||||||
<strong :class="{ organizer: commentFromOrganizer }">{{
|
<strong :class="{ organizer: commentFromOrganizer }">{{
|
||||||
comment.actor.name
|
comment.actor.name
|
||||||
}}</strong>
|
}}</strong>
|
||||||
<small class="has-text-grey">{{
|
<small>{{ usernameWithDomain(comment.actor) }}</small>
|
||||||
usernameWithDomain(comment.actor)
|
|
||||||
}}</small>
|
|
||||||
</span>
|
</span>
|
||||||
<a v-else class="comment-link has-text-grey" :href="commentURL">
|
<a v-else class="comment-link" :href="commentURL">
|
||||||
<span>{{ $t("[deleted]") }}</span>
|
<span>{{ $t("[deleted]") }}</span>
|
||||||
</a>
|
</a>
|
||||||
<a class="comment-link has-text-grey" :href="commentURL">
|
<a class="comment-link" :href="commentURL">
|
||||||
<small>{{
|
<small>{{
|
||||||
formatDistanceToNow(new Date(comment.updatedAt), {
|
formatDistanceToNow(new Date(comment.updatedAt), {
|
||||||
locale: $dateFnsLocale,
|
locale: $dateFnsLocale,
|
||||||
|
@ -265,7 +266,7 @@ export default class Comment extends Vue {
|
||||||
}
|
}
|
||||||
|
|
||||||
get commentSelected(): boolean {
|
get commentSelected(): boolean {
|
||||||
return this.commentId === this.$route.hash;
|
return `#${this.commentId}` === this.$route.hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
get commentFromOrganizer(): boolean {
|
get commentFromOrganizer(): boolean {
|
||||||
|
@ -276,13 +277,13 @@ export default class Comment extends Vue {
|
||||||
|
|
||||||
get commentId(): string {
|
get commentId(): string {
|
||||||
if (this.comment.originComment)
|
if (this.comment.originComment)
|
||||||
return `#comment-${this.comment.originComment.uuid}-${this.comment.uuid}`;
|
return `comment-${this.comment.originComment.uuid}-${this.comment.uuid}`;
|
||||||
return `#comment-${this.comment.uuid}`;
|
return `comment-${this.comment.uuid}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
get commentURL(): string {
|
get commentURL(): string {
|
||||||
if (!this.comment.local && this.comment.url) return this.comment.url;
|
if (!this.comment.local && this.comment.url) return this.comment.url;
|
||||||
return this.commentId;
|
return `#${this.commentId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
reportModal(): void {
|
reportModal(): void {
|
||||||
|
@ -368,6 +369,7 @@ form.reply {
|
||||||
a.comment-link {
|
a.comment-link {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
|
color: $text;
|
||||||
&:hover {
|
&:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
@ -378,6 +380,41 @@ a.comment-link {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.comment-element {
|
||||||
|
padding: 0.25rem;
|
||||||
|
border-radius: 5px;
|
||||||
|
|
||||||
|
&.announcement {
|
||||||
|
background: $purple-2;
|
||||||
|
|
||||||
|
small {
|
||||||
|
color: hsl(0, 0%, 21%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
background-color: $violet-1;
|
||||||
|
color: $white;
|
||||||
|
.reply-btn,
|
||||||
|
small,
|
||||||
|
strong,
|
||||||
|
.icons button {
|
||||||
|
color: $white;
|
||||||
|
}
|
||||||
|
a.comment-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
text-decoration-color: $white;
|
||||||
|
small {
|
||||||
|
color: $purple-3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-left {
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.root-comment .replies {
|
.root-comment .replies {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
|
@ -402,6 +439,7 @@ a.comment-link {
|
||||||
}
|
}
|
||||||
|
|
||||||
.media .media-content {
|
.media .media-content {
|
||||||
|
overflow-x: initial;
|
||||||
.content .editor-line {
|
.content .editor-line {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -433,16 +471,12 @@ a.comment-link {
|
||||||
|
|
||||||
.level-item.reply-btn {
|
.level-item.reply-btn {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: $primary;
|
color: $violet-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
article {
|
article {
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
|
|
||||||
&.selected {
|
|
||||||
background-color: lighten($secondary, 30%);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment-replies {
|
.comment-replies {
|
||||||
|
|
|
@ -74,7 +74,7 @@
|
||||||
@delete-comment="deleteComment"
|
@delete-comment="deleteComment"
|
||||||
/>
|
/>
|
||||||
</transition-group>
|
</transition-group>
|
||||||
<div class="no-comments" key="no-comments">
|
<div v-else class="no-comments" key="no-comments">
|
||||||
<span>{{ $t("No comments yet") }}</span>
|
<span>{{ $t("No comments yet") }}</span>
|
||||||
</div>
|
</div>
|
||||||
</transition-group>
|
</transition-group>
|
||||||
|
@ -311,7 +311,18 @@ export default class CommentTree extends Vue {
|
||||||
return this.comments
|
return this.comments
|
||||||
.filter((comment) => comment.inReplyToComment == null)
|
.filter((comment) => comment.inReplyToComment == null)
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
if (a.updatedAt && b.updatedAt) {
|
if (a.isAnnouncement !== b.isAnnouncement) {
|
||||||
|
return (
|
||||||
|
(b.isAnnouncement === true ? 1 : 0) -
|
||||||
|
(a.isAnnouncement === true ? 1 : 0)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (a.publishedAt && b.publishedAt) {
|
||||||
|
return (
|
||||||
|
new Date(b.publishedAt).getTime() -
|
||||||
|
new Date(a.publishedAt).getTime()
|
||||||
|
);
|
||||||
|
} else if (a.updatedAt && b.updatedAt) {
|
||||||
return (
|
return (
|
||||||
new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
|
new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
|
||||||
);
|
);
|
||||||
|
|
|
@ -12,9 +12,17 @@
|
||||||
</docs>
|
</docs>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<time class="datetime-container" :datetime="dateObj.getUTCSeconds()">
|
<time
|
||||||
<span class="month">{{ month }}</span>
|
class="datetime-container"
|
||||||
<span class="day">{{ day }}</span>
|
:class="{ small }"
|
||||||
|
:datetime="dateObj.getUTCSeconds()"
|
||||||
|
:style="`--small: ${smallStyle}`"
|
||||||
|
>
|
||||||
|
<div class="datetime-container-header" />
|
||||||
|
<div class="datetime-container-content">
|
||||||
|
<span class="day">{{ day }}</span>
|
||||||
|
<span class="month">{{ month }}</span>
|
||||||
|
</div>
|
||||||
</time>
|
</time>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -26,6 +34,7 @@ export default class DateCalendarIcon extends Vue {
|
||||||
* `date` can be a string or an actual date object.
|
* `date` can be a string or an actual date object.
|
||||||
*/
|
*/
|
||||||
@Prop({ required: true }) date!: string;
|
@Prop({ required: true }) date!: string;
|
||||||
|
@Prop({ required: false, default: false }) small!: boolean;
|
||||||
|
|
||||||
get dateObj(): Date {
|
get dateObj(): Date {
|
||||||
return new Date(this.$props.date);
|
return new Date(this.$props.date);
|
||||||
|
@ -38,28 +47,41 @@ export default class DateCalendarIcon extends Vue {
|
||||||
get day(): string {
|
get day(): string {
|
||||||
return this.dateObj.toLocaleString(undefined, { day: "numeric" });
|
return this.dateObj.toLocaleString(undefined, { day: "numeric" });
|
||||||
}
|
}
|
||||||
|
get smallStyle(): string {
|
||||||
|
return this.small ? "1.2" : "2";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
time.datetime-container {
|
time.datetime-container {
|
||||||
background: $backgrounds;
|
|
||||||
border: 1px solid $borders;
|
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
/*height: 50px;*/
|
|
||||||
width: 50px;
|
|
||||||
padding: 8px;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
overflow-y: hidden;
|
||||||
|
overflow-x: hidden;
|
||||||
|
align-items: stretch;
|
||||||
|
width: calc(40px * var(--small));
|
||||||
|
box-shadow: 0 0 12px rgba(0, 0, 0, 0.2);
|
||||||
|
height: calc(40px * var(--small));
|
||||||
|
background: #fff;
|
||||||
|
|
||||||
|
.datetime-container-header {
|
||||||
|
height: calc(10px * var(--small));
|
||||||
|
background: #f3425f;
|
||||||
|
}
|
||||||
|
.datetime-container-content {
|
||||||
|
height: calc(30px * var(--small));
|
||||||
|
}
|
||||||
|
|
||||||
span {
|
span {
|
||||||
display: block;
|
display: block;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
color: $violet-3;
|
||||||
|
|
||||||
&.month {
|
&.month {
|
||||||
color: $danger;
|
|
||||||
padding: 2px 0;
|
padding: 2px 0;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 12px;
|
line-height: 12px;
|
||||||
|
@ -67,9 +89,8 @@ time.datetime-container {
|
||||||
}
|
}
|
||||||
|
|
||||||
&.day {
|
&.day {
|
||||||
color: $violet-3;
|
font-size: calc(1rem * var(--small));
|
||||||
font-size: 20px;
|
line-height: calc(1rem * var(--small));
|
||||||
line-height: 20px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
33
js/src/components/Event/EventBanner.vue
Normal file
33
js/src/components/Event/EventBanner.vue
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
<template>
|
||||||
|
<div class="banner-container">
|
||||||
|
<lazy-image-wrapper :picture="picture" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { IMedia } from "@/types/media.model";
|
||||||
|
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||||
|
import LazyImageWrapper from "../Image/LazyImageWrapper.vue";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
components: {
|
||||||
|
LazyImageWrapper,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
export default class EventBanner extends Vue {
|
||||||
|
@Prop({ required: true, default: null })
|
||||||
|
picture!: IMedia | null;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.banner-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
height: 30vh;
|
||||||
|
}
|
||||||
|
::v-deep img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
object-position: 50% 50%;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -4,12 +4,11 @@
|
||||||
:to="{ name: 'Event', params: { uuid: event.uuid } }"
|
:to="{ name: 'Event', params: { uuid: event.uuid } }"
|
||||||
>
|
>
|
||||||
<div class="card-image">
|
<div class="card-image">
|
||||||
<figure
|
<figure class="image is-16by9">
|
||||||
class="image is-16by9"
|
<lazy-image-wrapper
|
||||||
:style="`background-image: url('${
|
:picture="event.picture"
|
||||||
event.picture ? event.picture.url : '/img/mobilizon_default_card.png'
|
style="height: 100%; position: absolute; top: 0; left: 0; width: 100%"
|
||||||
}')`"
|
/>
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
class="tag-container"
|
class="tag-container"
|
||||||
v-if="event.tags || event.status !== EventStatus.CONFIRMED"
|
v-if="event.tags || event.status !== EventStatus.CONFIRMED"
|
||||||
|
@ -34,6 +33,7 @@
|
||||||
<div class="media">
|
<div class="media">
|
||||||
<div class="media-left">
|
<div class="media-left">
|
||||||
<date-calendar-icon
|
<date-calendar-icon
|
||||||
|
:small="true"
|
||||||
v-if="!mergedOptions.hideDate"
|
v-if="!mergedOptions.hideDate"
|
||||||
:date="event.beginsOn"
|
:date="event.beginsOn"
|
||||||
/>
|
/>
|
||||||
|
@ -103,6 +103,7 @@
|
||||||
import { IEvent, IEventCardOptions } from "@/types/event.model";
|
import { IEvent, IEventCardOptions } from "@/types/event.model";
|
||||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||||
import DateCalendarIcon from "@/components/Event/DateCalendarIcon.vue";
|
import DateCalendarIcon from "@/components/Event/DateCalendarIcon.vue";
|
||||||
|
import LazyImageWrapper from "@/components/Image/LazyImageWrapper.vue";
|
||||||
import { Actor, Person } from "@/types/actor";
|
import { Actor, Person } from "@/types/actor";
|
||||||
import { EventStatus, ParticipantRole } from "@/types/enums";
|
import { EventStatus, ParticipantRole } from "@/types/enums";
|
||||||
import RouteName from "../../router/name";
|
import RouteName from "../../router/name";
|
||||||
|
@ -110,6 +111,7 @@ import RouteName from "../../router/name";
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
DateCalendarIcon,
|
DateCalendarIcon,
|
||||||
|
LazyImageWrapper,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class EventCard extends Vue {
|
export default class EventCard extends Vue {
|
||||||
|
@ -220,6 +222,22 @@ a.card {
|
||||||
.card-content {
|
.card-content {
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
|
|
||||||
|
& > .media {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
& > .media-left {
|
||||||
|
margin-top: -15px;
|
||||||
|
height: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
align-self: flex-start;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
margin-left: 0rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.event-title {
|
.event-title {
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
line-height: 1.25rem;
|
line-height: 1.25rem;
|
||||||
|
|
|
@ -7,11 +7,14 @@
|
||||||
{{ displayNameAndUsername(participation.actor) }}
|
{{ displayNameAndUsername(participation.actor) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="list-card">
|
<div class="list-card">
|
||||||
|
<div class="date-component">
|
||||||
|
<date-calendar-icon
|
||||||
|
:date="participation.event.beginsOn"
|
||||||
|
:small="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div class="title-wrapper">
|
<div class="title-wrapper">
|
||||||
<div class="date-component">
|
|
||||||
<date-calendar-icon :date="participation.event.beginsOn" />
|
|
||||||
</div>
|
|
||||||
<router-link
|
<router-link
|
||||||
:to="{
|
:to="{
|
||||||
name: RouteName.EVENT,
|
name: RouteName.EVENT,
|
||||||
|
@ -21,7 +24,7 @@
|
||||||
<h3 class="title">{{ participation.event.title }}</h3>
|
<h3 class="title">{{ participation.event.title }}</h3>
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
<div class="participation-actor has-text-grey">
|
<div class="participation-actor">
|
||||||
<span>
|
<span>
|
||||||
<b-icon
|
<b-icon
|
||||||
icon="earth"
|
icon="earth"
|
||||||
|
@ -47,17 +50,20 @@
|
||||||
"
|
"
|
||||||
>{{ participation.event.physicalAddress.locality }} -</span
|
>{{ participation.event.physicalAddress.locality }} -</span
|
||||||
>
|
>
|
||||||
<span>
|
<i18n
|
||||||
<i18n tag="span" path="Organized by {name}">
|
tag="span"
|
||||||
<popover-actor-card
|
path="Organized by {name}"
|
||||||
slot="name"
|
v-if="organizerActor.id !== currentActor.id"
|
||||||
:actor="organizerActor"
|
>
|
||||||
:inline="true"
|
<popover-actor-card
|
||||||
>
|
slot="name"
|
||||||
{{ organizerActor.displayName() }}
|
:actor="organizerActor"
|
||||||
</popover-actor-card>
|
:inline="true"
|
||||||
</i18n>
|
>
|
||||||
</span>
|
{{ organizerActor.displayName() }}
|
||||||
|
</popover-actor-card>
|
||||||
|
</i18n>
|
||||||
|
<span v-else>{{ $t("Organized by you") }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span
|
<span
|
||||||
|
@ -113,7 +119,9 @@
|
||||||
$tc(
|
$tc(
|
||||||
"{count} requests waiting",
|
"{count} requests waiting",
|
||||||
participation.event.participantStats.notApproved,
|
participation.event.participantStats.notApproved,
|
||||||
{ count: participation.event.participantStats.notApproved }
|
{
|
||||||
|
count: participation.event.participantStats.notApproved,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</b-button>
|
</b-button>
|
||||||
|
@ -344,6 +352,7 @@ article.box {
|
||||||
.list-card {
|
.list-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
padding: 0 6px;
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
padding-right: 7.5px;
|
padding-right: 7.5px;
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
<div class="content column">
|
<div class="content column">
|
||||||
<div class="title-wrapper">
|
<div class="title-wrapper">
|
||||||
<div class="date-component">
|
<div class="date-component">
|
||||||
<date-calendar-icon :date="event.beginsOn" />
|
<date-calendar-icon :date="event.beginsOn" :small="true" />
|
||||||
</div>
|
</div>
|
||||||
<router-link
|
<router-link
|
||||||
:to="{ name: RouteName.EVENT, params: { uuid: event.uuid } }"
|
:to="{ name: RouteName.EVENT, params: { uuid: event.uuid } }"
|
||||||
|
|
|
@ -23,7 +23,7 @@ export default class EventMetadataBlock extends Vue {
|
||||||
h2 {
|
h2 {
|
||||||
font-size: 1.8rem;
|
font-size: 1.8rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #f7ba30;
|
color: $violet;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.eventMetadataBlock {
|
div.eventMetadataBlock {
|
||||||
|
|
|
@ -3,7 +3,11 @@
|
||||||
class="event-minimalist-card-wrapper"
|
class="event-minimalist-card-wrapper"
|
||||||
:to="{ name: RouteName.EVENT, params: { uuid: event.uuid } }"
|
:to="{ name: RouteName.EVENT, params: { uuid: event.uuid } }"
|
||||||
>
|
>
|
||||||
<date-calendar-icon class="calendar-icon" :date="event.beginsOn" />
|
<date-calendar-icon
|
||||||
|
class="calendar-icon"
|
||||||
|
:date="event.beginsOn"
|
||||||
|
:small="true"
|
||||||
|
/>
|
||||||
<div class="title-info-wrapper">
|
<div class="title-info-wrapper">
|
||||||
<p class="event-minimalist-title">{{ event.title }}</p>
|
<p class="event-minimalist-title">{{ event.title }}</p>
|
||||||
<p v-if="event.physicalAddress" class="has-text-grey">
|
<p v-if="event.physicalAddress" class="has-text-grey">
|
||||||
|
|
|
@ -66,7 +66,9 @@ export default class OrganizerPicker extends Vue {
|
||||||
return this.value;
|
return this.value;
|
||||||
}
|
}
|
||||||
if (this.currentActor) {
|
if (this.currentActor) {
|
||||||
return this.currentActor;
|
return this.identities.find(
|
||||||
|
(identity) => identity.id === this.currentActor.id
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
|
@ -110,6 +110,7 @@ import { IActor, IGroup, IPerson, usernameWithDomain } from "../../types/actor";
|
||||||
import OrganizerPicker from "./OrganizerPicker.vue";
|
import OrganizerPicker from "./OrganizerPicker.vue";
|
||||||
import {
|
import {
|
||||||
CURRENT_ACTOR_CLIENT,
|
CURRENT_ACTOR_CLIENT,
|
||||||
|
IDENTITIES,
|
||||||
LOGGED_USER_MEMBERSHIPS,
|
LOGGED_USER_MEMBERSHIPS,
|
||||||
} from "../../graphql/actor";
|
} from "../../graphql/actor";
|
||||||
import { Paginate } from "../../types/paginate";
|
import { Paginate } from "../../types/paginate";
|
||||||
|
@ -152,6 +153,7 @@ const MEMBER_ROLES = [
|
||||||
},
|
},
|
||||||
update: (data) => data.loggedUser.memberships,
|
update: (data) => data.loggedUser.memberships,
|
||||||
},
|
},
|
||||||
|
identities: IDENTITIES,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class OrganizerPickerWrapper extends Vue {
|
export default class OrganizerPickerWrapper extends Vue {
|
||||||
|
@ -161,6 +163,8 @@ export default class OrganizerPickerWrapper extends Vue {
|
||||||
|
|
||||||
currentActor!: IPerson;
|
currentActor!: IPerson;
|
||||||
|
|
||||||
|
identities!: IPerson[];
|
||||||
|
|
||||||
isComponentModalActive = false;
|
isComponentModalActive = false;
|
||||||
|
|
||||||
@Prop({ type: Array, required: false, default: () => [] })
|
@Prop({ type: Array, required: false, default: () => [] })
|
||||||
|
@ -200,7 +204,9 @@ export default class OrganizerPickerWrapper extends Vue {
|
||||||
return this.value;
|
return this.value;
|
||||||
}
|
}
|
||||||
if (this.currentActor) {
|
if (this.currentActor) {
|
||||||
return this.currentActor;
|
return this.identities.find(
|
||||||
|
(identity) => identity.id === this.currentActor.id
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
|
@ -115,12 +115,13 @@ footer.footer {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
max-width: 40rem;
|
max-width: 40rem;
|
||||||
@include mobile {
|
@include mobile {
|
||||||
max-width: 400px;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
div.content {
|
div.content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
padding-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
|
@ -131,6 +132,7 @@ footer.footer {
|
||||||
li {
|
li {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
margin: auto 5px;
|
margin: auto 5px;
|
||||||
|
padding: 2px 0;
|
||||||
a {
|
a {
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
}
|
}
|
||||||
|
@ -143,9 +145,12 @@ footer.footer {
|
||||||
text-decoration-color: $secondary;
|
text-decoration-color: $secondary;
|
||||||
}
|
}
|
||||||
|
|
||||||
::v-deep span.select select {
|
::v-deep span.select {
|
||||||
background: $background-color;
|
select,
|
||||||
color: $white;
|
option {
|
||||||
|
background: $background-color;
|
||||||
|
color: $white;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
34
js/src/components/Image/BlurhashImg.vue
Normal file
34
js/src/components/Image/BlurhashImg.vue
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
<template>
|
||||||
|
<canvas ref="canvas" width="32" height="32" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { decode } from "blurhash";
|
||||||
|
import { Component, Prop, Ref, Vue } from "vue-property-decorator";
|
||||||
|
|
||||||
|
@Component
|
||||||
|
export default class extends Vue {
|
||||||
|
@Prop({ type: String, required: true }) hash!: string;
|
||||||
|
@Prop({ type: Number, default: 1 }) aspectRatio!: string;
|
||||||
|
|
||||||
|
@Ref("canvas") readonly canvas!: any;
|
||||||
|
|
||||||
|
mounted(): void {
|
||||||
|
const pixels = decode(this.hash, 32, 32);
|
||||||
|
const imageData = new ImageData(pixels, 32, 32);
|
||||||
|
const context = this.canvas.getContext("2d");
|
||||||
|
context.putImageData(imageData, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
canvas {
|
||||||
|
position: absolute;
|
||||||
|
top: 0px;
|
||||||
|
right: 0px;
|
||||||
|
bottom: 0px;
|
||||||
|
left: 0px;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
117
js/src/components/Image/LazyImage.vue
Normal file
117
js/src/components/Image/LazyImage.vue
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
<template>
|
||||||
|
<div ref="wrapper" class="wrapper" v-bind="$attrs">
|
||||||
|
<div class="relative container">
|
||||||
|
<!-- Show the placeholder as background -->
|
||||||
|
<blurhash-img
|
||||||
|
v-if="blurhash"
|
||||||
|
:hash="blurhash"
|
||||||
|
:aspect-ratio="height / width"
|
||||||
|
class="top-0 left-0 transition-opacity duration-500"
|
||||||
|
:class="isLoaded ? 'opacity-0' : 'opacity-100'"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Show the real image on the top and fade in after loading -->
|
||||||
|
<img
|
||||||
|
ref="image"
|
||||||
|
:width="width"
|
||||||
|
:height="height"
|
||||||
|
class="absolute top-0 left-0 transition-opacity duration-500"
|
||||||
|
:class="isLoaded ? 'opacity-100' : 'opacity-0'"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Prop, Component, Vue, Ref, Watch } from "vue-property-decorator";
|
||||||
|
import BlurhashImg from "./BlurhashImg.vue";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
components: {
|
||||||
|
BlurhashImg,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
export default class LazyImage extends Vue {
|
||||||
|
@Prop({ type: String, required: true }) src!: string;
|
||||||
|
@Prop({ type: String, required: false, default: null }) blurhash!: string;
|
||||||
|
@Prop({ type: Number, default: 1 }) width!: number;
|
||||||
|
@Prop({ type: Number, default: 1 }) height!: number;
|
||||||
|
|
||||||
|
inheritAttrs = false;
|
||||||
|
isLoaded = false;
|
||||||
|
|
||||||
|
observer!: IntersectionObserver;
|
||||||
|
|
||||||
|
@Ref("wrapper") readonly wrapper!: any;
|
||||||
|
@Ref("image") image!: any;
|
||||||
|
|
||||||
|
mounted(): void {
|
||||||
|
this.observer = new IntersectionObserver((entries) => {
|
||||||
|
if (entries[0].isIntersecting) {
|
||||||
|
this.onEnter();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.observer.observe(this.wrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
unmounted(): void {
|
||||||
|
this.observer.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
onEnter(): void {
|
||||||
|
// Image is visible (means: has entered the viewport),
|
||||||
|
// so start loading by setting the src attribute
|
||||||
|
this.image.src = this.src;
|
||||||
|
|
||||||
|
this.image.onload = () => {
|
||||||
|
// Image is loaded, so start fading in
|
||||||
|
this.isLoaded = true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Watch("src")
|
||||||
|
updateImageWithSrcChange(): void {
|
||||||
|
this.onEnter();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.relative {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.absolute {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
.top-0 {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
.left-0 {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
.opacity-100 {
|
||||||
|
opacity: 100%;
|
||||||
|
}
|
||||||
|
.opacity-0 {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
.transition-opacity {
|
||||||
|
transition-property: opacity;
|
||||||
|
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
.duration-500 {
|
||||||
|
transition-duration: 0.5s;
|
||||||
|
}
|
||||||
|
.wrapper,
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
object-position: 50% 50%;
|
||||||
|
}
|
||||||
|
</style>
|
40
js/src/components/Image/LazyImageWrapper.vue
Normal file
40
js/src/components/Image/LazyImageWrapper.vue
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
<template>
|
||||||
|
<lazy-image
|
||||||
|
v-if="pictureOrDefault.url !== undefined"
|
||||||
|
:src="pictureOrDefault.url"
|
||||||
|
:width="pictureOrDefault.metadata.width"
|
||||||
|
:height="pictureOrDefault.metadata.height"
|
||||||
|
:blurhash="pictureOrDefault.metadata.blurhash"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { IMedia } from "@/types/media.model";
|
||||||
|
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||||
|
import LazyImage from "../Image/LazyImage.vue";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
components: {
|
||||||
|
LazyImage,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
export default class LazyImageWrapper extends Vue {
|
||||||
|
@Prop({ required: true, default: null })
|
||||||
|
picture!: IMedia | null;
|
||||||
|
|
||||||
|
get pictureOrDefault(): Partial<IMedia> {
|
||||||
|
return {
|
||||||
|
url:
|
||||||
|
this?.picture?.url === null
|
||||||
|
? "/img/mobilizon_default_card.png"
|
||||||
|
: this?.picture?.url,
|
||||||
|
metadata: {
|
||||||
|
width: this?.picture?.metadata?.width || 630,
|
||||||
|
height: this?.picture?.metadata?.height || 350,
|
||||||
|
blurhash:
|
||||||
|
this?.picture?.metadata?.blurhash ||
|
||||||
|
"MCHKI4El-P-U}+={R-WWoes,Iu-P=?R,xD",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -1,35 +1,40 @@
|
||||||
import gql from "graphql-tag";
|
import gql from "graphql-tag";
|
||||||
|
|
||||||
const $addressFragment = `
|
export const ADDRESS_FRAGMENT = gql`
|
||||||
id,
|
fragment AdressFragment on Address {
|
||||||
description,
|
id
|
||||||
geom,
|
description
|
||||||
street,
|
geom
|
||||||
locality,
|
street
|
||||||
postalCode,
|
locality
|
||||||
region,
|
postalCode
|
||||||
country,
|
region
|
||||||
type,
|
country
|
||||||
url,
|
type
|
||||||
originId
|
url
|
||||||
|
originId
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const ADDRESS = gql`
|
export const ADDRESS = gql`
|
||||||
query($query:String!, $locale: String, $type: AddressSearchType) {
|
query ($query: String!, $locale: String, $type: AddressSearchType) {
|
||||||
searchAddress(
|
searchAddress(query: $query, locale: $locale, type: $type) {
|
||||||
query: $query,
|
...AdressFragment
|
||||||
locale: $locale,
|
|
||||||
type: $type
|
|
||||||
) {
|
|
||||||
${$addressFragment}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
${ADDRESS_FRAGMENT}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const REVERSE_GEOCODE = gql`
|
export const REVERSE_GEOCODE = gql`
|
||||||
query($latitude: Float!, $longitude: Float!, $zoom: Int, $locale: String) {
|
query ($latitude: Float!, $longitude: Float!, $zoom: Int, $locale: String) {
|
||||||
reverseGeocode(latitude: $latitude, longitude: $longitude, zoom: $zoom, locale: $locale) {
|
reverseGeocode(
|
||||||
${$addressFragment}
|
latitude: $latitude
|
||||||
}
|
longitude: $longitude
|
||||||
|
zoom: $zoom
|
||||||
|
locale: $locale
|
||||||
|
) {
|
||||||
|
...AdressFragment
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
${ADDRESS_FRAGMENT}
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -1,179 +1,189 @@
|
||||||
import gql from "graphql-tag";
|
import gql from "graphql-tag";
|
||||||
|
import { ADDRESS_FRAGMENT } from "./address";
|
||||||
|
import { TAG_FRAGMENT } from "./tags";
|
||||||
|
|
||||||
const participantQuery = `
|
const PARTICIPANT_QUERY_FRAGMENT = gql`
|
||||||
role,
|
fragment ParticipantQuery on Participant {
|
||||||
id,
|
role
|
||||||
actor {
|
id
|
||||||
preferredUsername,
|
actor {
|
||||||
avatar {
|
preferredUsername
|
||||||
|
avatar {
|
||||||
|
id
|
||||||
|
url
|
||||||
|
}
|
||||||
|
name
|
||||||
id
|
id
|
||||||
url
|
domain
|
||||||
},
|
}
|
||||||
name,
|
event {
|
||||||
id,
|
id
|
||||||
domain
|
uuid
|
||||||
},
|
}
|
||||||
event {
|
metadata {
|
||||||
id,
|
cancellationToken
|
||||||
uuid
|
message
|
||||||
},
|
}
|
||||||
metadata {
|
insertedAt
|
||||||
cancellationToken,
|
|
||||||
message
|
|
||||||
},
|
|
||||||
insertedAt
|
|
||||||
`;
|
|
||||||
|
|
||||||
const participantsQuery = `
|
|
||||||
total,
|
|
||||||
elements {
|
|
||||||
${participantQuery}
|
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const physicalAddressQuery = `
|
const PARTICIPANTS_QUERY_FRAGMENT = gql`
|
||||||
description,
|
fragment ParticipantsQuery on PaginatedParticipantList {
|
||||||
street,
|
total
|
||||||
locality,
|
elements {
|
||||||
postalCode,
|
...ParticipantQuery
|
||||||
region,
|
}
|
||||||
country,
|
}
|
||||||
geom,
|
${PARTICIPANT_QUERY_FRAGMENT}
|
||||||
type,
|
|
||||||
id,
|
|
||||||
originId
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const tagsQuery = `
|
const EVENT_OPTIONS_FRAGMENT = gql`
|
||||||
id,
|
fragment EventOptions on EventOptions {
|
||||||
slug,
|
maximumAttendeeCapacity
|
||||||
title
|
remainingAttendeeCapacity
|
||||||
|
showRemainingAttendeeCapacity
|
||||||
|
anonymousParticipation
|
||||||
|
showStartTime
|
||||||
|
showEndTime
|
||||||
|
offers {
|
||||||
|
price
|
||||||
|
priceCurrency
|
||||||
|
url
|
||||||
|
}
|
||||||
|
participationConditions {
|
||||||
|
title
|
||||||
|
content
|
||||||
|
url
|
||||||
|
}
|
||||||
|
attendees
|
||||||
|
program
|
||||||
|
commentModeration
|
||||||
|
showParticipationPrice
|
||||||
|
hideOrganizerWhenGroupEvent
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const optionsQuery = `
|
const FULL_EVENT_FRAGMENT = gql`
|
||||||
maximumAttendeeCapacity,
|
fragment FullEvent on Event {
|
||||||
remainingAttendeeCapacity,
|
id
|
||||||
showRemainingAttendeeCapacity,
|
uuid
|
||||||
anonymousParticipation,
|
|
||||||
showStartTime,
|
|
||||||
showEndTime,
|
|
||||||
offers {
|
|
||||||
price,
|
|
||||||
priceCurrency,
|
|
||||||
url
|
url
|
||||||
},
|
local
|
||||||
participationConditions {
|
title
|
||||||
title,
|
description
|
||||||
content,
|
beginsOn
|
||||||
url
|
endsOn
|
||||||
},
|
status
|
||||||
attendees,
|
visibility
|
||||||
program,
|
joinOptions
|
||||||
commentModeration,
|
draft
|
||||||
showParticipationPrice,
|
picture {
|
||||||
hideOrganizerWhenGroupEvent,
|
id
|
||||||
__typename
|
url
|
||||||
`;
|
name
|
||||||
|
metadata {
|
||||||
export const FETCH_EVENT = gql`
|
width
|
||||||
query FetchEvent($uuid:UUID!) {
|
height
|
||||||
event(uuid: $uuid) {
|
blurhash
|
||||||
id,
|
}
|
||||||
uuid,
|
}
|
||||||
url,
|
publishAt
|
||||||
local,
|
onlineAddress
|
||||||
title,
|
phoneAddress
|
||||||
description,
|
physicalAddress {
|
||||||
beginsOn,
|
...AdressFragment
|
||||||
endsOn,
|
}
|
||||||
status,
|
organizerActor {
|
||||||
visibility,
|
avatar {
|
||||||
joinOptions,
|
id
|
||||||
draft,
|
url
|
||||||
|
}
|
||||||
|
preferredUsername
|
||||||
|
domain
|
||||||
|
name
|
||||||
|
url
|
||||||
|
id
|
||||||
|
summary
|
||||||
|
}
|
||||||
|
contacts {
|
||||||
|
avatar {
|
||||||
|
id
|
||||||
|
url
|
||||||
|
}
|
||||||
|
preferredUsername
|
||||||
|
name
|
||||||
|
summary
|
||||||
|
domain
|
||||||
|
url
|
||||||
|
id
|
||||||
|
}
|
||||||
|
attributedTo {
|
||||||
|
avatar {
|
||||||
|
id
|
||||||
|
url
|
||||||
|
}
|
||||||
|
preferredUsername
|
||||||
|
name
|
||||||
|
summary
|
||||||
|
domain
|
||||||
|
url
|
||||||
|
id
|
||||||
|
}
|
||||||
|
participantStats {
|
||||||
|
going
|
||||||
|
notApproved
|
||||||
|
participant
|
||||||
|
}
|
||||||
|
tags {
|
||||||
|
...TagFragment
|
||||||
|
}
|
||||||
|
relatedEvents {
|
||||||
|
id
|
||||||
|
uuid
|
||||||
|
title
|
||||||
|
beginsOn
|
||||||
picture {
|
picture {
|
||||||
id
|
id
|
||||||
url
|
url
|
||||||
name
|
name
|
||||||
},
|
metadata {
|
||||||
publishAt,
|
width
|
||||||
onlineAddress,
|
height
|
||||||
phoneAddress,
|
blurhash
|
||||||
|
}
|
||||||
|
}
|
||||||
physicalAddress {
|
physicalAddress {
|
||||||
${physicalAddressQuery}
|
id
|
||||||
|
description
|
||||||
}
|
}
|
||||||
organizerActor {
|
organizerActor {
|
||||||
|
id
|
||||||
avatar {
|
avatar {
|
||||||
id
|
id
|
||||||
url
|
url
|
||||||
},
|
|
||||||
preferredUsername,
|
|
||||||
domain,
|
|
||||||
name,
|
|
||||||
url,
|
|
||||||
id,
|
|
||||||
summary
|
|
||||||
},
|
|
||||||
contacts {
|
|
||||||
avatar {
|
|
||||||
id
|
|
||||||
url,
|
|
||||||
}
|
}
|
||||||
preferredUsername,
|
preferredUsername
|
||||||
name,
|
domain
|
||||||
summary,
|
name
|
||||||
domain,
|
|
||||||
url,
|
|
||||||
id
|
|
||||||
},
|
|
||||||
attributedTo {
|
|
||||||
avatar {
|
|
||||||
id
|
|
||||||
url,
|
|
||||||
}
|
|
||||||
preferredUsername,
|
|
||||||
name,
|
|
||||||
summary,
|
|
||||||
domain,
|
|
||||||
url,
|
|
||||||
id
|
|
||||||
},
|
|
||||||
participantStats {
|
|
||||||
going,
|
|
||||||
notApproved,
|
|
||||||
participant
|
|
||||||
},
|
|
||||||
tags {
|
|
||||||
${tagsQuery}
|
|
||||||
},
|
|
||||||
relatedEvents {
|
|
||||||
id
|
|
||||||
uuid,
|
|
||||||
title,
|
|
||||||
beginsOn,
|
|
||||||
picture {
|
|
||||||
id,
|
|
||||||
url
|
|
||||||
}
|
|
||||||
physicalAddress {
|
|
||||||
id
|
|
||||||
description
|
|
||||||
},
|
|
||||||
organizerActor {
|
|
||||||
id
|
|
||||||
avatar {
|
|
||||||
id
|
|
||||||
url,
|
|
||||||
},
|
|
||||||
preferredUsername,
|
|
||||||
domain,
|
|
||||||
name,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
options {
|
|
||||||
${optionsQuery}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
options {
|
||||||
|
...EventOptions
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
${ADDRESS_FRAGMENT}
|
||||||
|
${TAG_FRAGMENT}
|
||||||
|
${EVENT_OPTIONS_FRAGMENT}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const FETCH_EVENT = gql`
|
||||||
|
query FetchEvent($uuid: UUID!) {
|
||||||
|
event(uuid: $uuid) {
|
||||||
|
...FullEvent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
${FULL_EVENT_FRAGMENT}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const FETCH_EVENT_BASIC = gql`
|
export const FETCH_EVENT_BASIC = gql`
|
||||||
|
@ -240,252 +250,129 @@ export const FETCH_EVENTS = gql`
|
||||||
# },
|
# },
|
||||||
category
|
category
|
||||||
tags {
|
tags {
|
||||||
slug
|
...TagFragment
|
||||||
title
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
${TAG_FRAGMENT}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const CREATE_EVENT = gql`
|
export const CREATE_EVENT = gql`
|
||||||
mutation createEvent(
|
mutation createEvent(
|
||||||
$organizerActorId: ID!,
|
$organizerActorId: ID!
|
||||||
$attributedToId: ID,
|
$attributedToId: ID
|
||||||
$title: String!,
|
$title: String!
|
||||||
$description: String!,
|
$description: String!
|
||||||
$beginsOn: DateTime!,
|
$beginsOn: DateTime!
|
||||||
$endsOn: DateTime,
|
$endsOn: DateTime
|
||||||
$status: EventStatus,
|
$status: EventStatus
|
||||||
$visibility: EventVisibility,
|
$visibility: EventVisibility
|
||||||
$joinOptions: EventJoinOptions,
|
$joinOptions: EventJoinOptions
|
||||||
$draft: Boolean,
|
$draft: Boolean
|
||||||
$tags: [String],
|
$tags: [String]
|
||||||
$picture: MediaInput,
|
$picture: MediaInput
|
||||||
$onlineAddress: String,
|
$onlineAddress: String
|
||||||
$phoneAddress: String,
|
$phoneAddress: String
|
||||||
$category: String,
|
$category: String
|
||||||
$physicalAddress: AddressInput,
|
$physicalAddress: AddressInput
|
||||||
$options: EventOptionsInput,
|
$options: EventOptionsInput
|
||||||
$contacts: [Contact]
|
$contacts: [Contact]
|
||||||
) {
|
) {
|
||||||
createEvent(
|
createEvent(
|
||||||
organizerActorId: $organizerActorId,
|
organizerActorId: $organizerActorId
|
||||||
attributedToId: $attributedToId,
|
attributedToId: $attributedToId
|
||||||
title: $title,
|
title: $title
|
||||||
description: $description,
|
description: $description
|
||||||
beginsOn: $beginsOn,
|
beginsOn: $beginsOn
|
||||||
endsOn: $endsOn,
|
endsOn: $endsOn
|
||||||
status: $status,
|
status: $status
|
||||||
visibility: $visibility,
|
visibility: $visibility
|
||||||
joinOptions: $joinOptions,
|
joinOptions: $joinOptions
|
||||||
draft: $draft,
|
draft: $draft
|
||||||
tags: $tags,
|
tags: $tags
|
||||||
picture: $picture,
|
picture: $picture
|
||||||
onlineAddress: $onlineAddress,
|
onlineAddress: $onlineAddress
|
||||||
phoneAddress: $phoneAddress,
|
phoneAddress: $phoneAddress
|
||||||
category: $category,
|
category: $category
|
||||||
physicalAddress: $physicalAddress
|
physicalAddress: $physicalAddress
|
||||||
options: $options,
|
options: $options
|
||||||
contacts: $contacts
|
contacts: $contacts
|
||||||
) {
|
) {
|
||||||
id,
|
...FullEvent
|
||||||
uuid,
|
|
||||||
title,
|
|
||||||
url,
|
|
||||||
local,
|
|
||||||
description,
|
|
||||||
beginsOn,
|
|
||||||
endsOn,
|
|
||||||
status,
|
|
||||||
visibility,
|
|
||||||
joinOptions,
|
|
||||||
draft,
|
|
||||||
picture {
|
|
||||||
id
|
|
||||||
url
|
|
||||||
},
|
|
||||||
publishAt,
|
|
||||||
category,
|
|
||||||
onlineAddress,
|
|
||||||
phoneAddress,
|
|
||||||
physicalAddress {
|
|
||||||
${physicalAddressQuery}
|
|
||||||
},
|
|
||||||
attributedTo {
|
|
||||||
id,
|
|
||||||
domain,
|
|
||||||
name,
|
|
||||||
url,
|
|
||||||
preferredUsername,
|
|
||||||
avatar {
|
|
||||||
id
|
|
||||||
url
|
|
||||||
}
|
|
||||||
},
|
|
||||||
organizerActor {
|
|
||||||
avatar {
|
|
||||||
id
|
|
||||||
url
|
|
||||||
},
|
|
||||||
preferredUsername,
|
|
||||||
domain,
|
|
||||||
name,
|
|
||||||
url,
|
|
||||||
id,
|
|
||||||
},
|
|
||||||
contacts {
|
|
||||||
avatar {
|
|
||||||
id
|
|
||||||
url
|
|
||||||
},
|
|
||||||
preferredUsername,
|
|
||||||
domain,
|
|
||||||
name,
|
|
||||||
url,
|
|
||||||
id,
|
|
||||||
},
|
|
||||||
participantStats {
|
|
||||||
going,
|
|
||||||
notApproved,
|
|
||||||
participant
|
|
||||||
},
|
|
||||||
tags {
|
|
||||||
${tagsQuery}
|
|
||||||
},
|
|
||||||
options {
|
|
||||||
${optionsQuery}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
${FULL_EVENT_FRAGMENT}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const EDIT_EVENT = gql`
|
export const EDIT_EVENT = gql`
|
||||||
mutation updateEvent(
|
mutation updateEvent(
|
||||||
$id: ID!,
|
$id: ID!
|
||||||
$title: String,
|
$title: String
|
||||||
$description: String,
|
$description: String
|
||||||
$beginsOn: DateTime,
|
$beginsOn: DateTime
|
||||||
$endsOn: DateTime,
|
$endsOn: DateTime
|
||||||
$status: EventStatus,
|
$status: EventStatus
|
||||||
$visibility: EventVisibility,
|
$visibility: EventVisibility
|
||||||
$joinOptions: EventJoinOptions,
|
$joinOptions: EventJoinOptions
|
||||||
$draft: Boolean,
|
$draft: Boolean
|
||||||
$tags: [String],
|
$tags: [String]
|
||||||
$picture: MediaInput,
|
$picture: MediaInput
|
||||||
$onlineAddress: String,
|
$onlineAddress: String
|
||||||
$phoneAddress: String,
|
$phoneAddress: String
|
||||||
$organizerActorId: ID,
|
$organizerActorId: ID
|
||||||
$attributedToId: ID,
|
$attributedToId: ID
|
||||||
$category: String,
|
$category: String
|
||||||
$physicalAddress: AddressInput,
|
$physicalAddress: AddressInput
|
||||||
$options: EventOptionsInput,
|
$options: EventOptionsInput
|
||||||
$contacts: [Contact]
|
$contacts: [Contact]
|
||||||
) {
|
) {
|
||||||
updateEvent(
|
updateEvent(
|
||||||
eventId: $id,
|
eventId: $id
|
||||||
title: $title,
|
title: $title
|
||||||
description: $description,
|
description: $description
|
||||||
beginsOn: $beginsOn,
|
beginsOn: $beginsOn
|
||||||
endsOn: $endsOn,
|
endsOn: $endsOn
|
||||||
status: $status,
|
status: $status
|
||||||
visibility: $visibility,
|
visibility: $visibility
|
||||||
joinOptions: $joinOptions,
|
joinOptions: $joinOptions
|
||||||
draft: $draft,
|
draft: $draft
|
||||||
tags: $tags,
|
tags: $tags
|
||||||
picture: $picture,
|
picture: $picture
|
||||||
onlineAddress: $onlineAddress,
|
onlineAddress: $onlineAddress
|
||||||
phoneAddress: $phoneAddress,
|
phoneAddress: $phoneAddress
|
||||||
organizerActorId: $organizerActorId,
|
organizerActorId: $organizerActorId
|
||||||
attributedToId: $attributedToId,
|
attributedToId: $attributedToId
|
||||||
category: $category,
|
category: $category
|
||||||
physicalAddress: $physicalAddress
|
physicalAddress: $physicalAddress
|
||||||
options: $options,
|
options: $options
|
||||||
contacts: $contacts
|
contacts: $contacts
|
||||||
) {
|
) {
|
||||||
id,
|
...FullEvent
|
||||||
uuid,
|
|
||||||
title,
|
|
||||||
url,
|
|
||||||
local,
|
|
||||||
description,
|
|
||||||
beginsOn,
|
|
||||||
endsOn,
|
|
||||||
status,
|
|
||||||
visibility,
|
|
||||||
joinOptions,
|
|
||||||
draft,
|
|
||||||
picture {
|
|
||||||
id
|
|
||||||
url
|
|
||||||
},
|
|
||||||
publishAt,
|
|
||||||
category,
|
|
||||||
onlineAddress,
|
|
||||||
phoneAddress,
|
|
||||||
physicalAddress {
|
|
||||||
${physicalAddressQuery}
|
|
||||||
},
|
|
||||||
attributedTo {
|
|
||||||
id,
|
|
||||||
domain,
|
|
||||||
name,
|
|
||||||
url,
|
|
||||||
preferredUsername,
|
|
||||||
avatar {
|
|
||||||
id
|
|
||||||
url
|
|
||||||
}
|
|
||||||
},
|
|
||||||
contacts {
|
|
||||||
avatar {
|
|
||||||
id
|
|
||||||
url
|
|
||||||
},
|
|
||||||
preferredUsername,
|
|
||||||
domain,
|
|
||||||
name,
|
|
||||||
url,
|
|
||||||
id,
|
|
||||||
},
|
|
||||||
organizerActor {
|
|
||||||
avatar {
|
|
||||||
id
|
|
||||||
url
|
|
||||||
},
|
|
||||||
preferredUsername,
|
|
||||||
domain,
|
|
||||||
name,
|
|
||||||
url,
|
|
||||||
id,
|
|
||||||
},
|
|
||||||
participantStats {
|
|
||||||
going,
|
|
||||||
notApproved,
|
|
||||||
participant
|
|
||||||
},
|
|
||||||
tags {
|
|
||||||
${tagsQuery}
|
|
||||||
},
|
|
||||||
options {
|
|
||||||
${optionsQuery}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
${FULL_EVENT_FRAGMENT}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const JOIN_EVENT = gql`
|
export const JOIN_EVENT = gql`
|
||||||
mutation JoinEvent($eventId: ID!, $actorId: ID!, $email: String, $message: String, $locale: String) {
|
mutation JoinEvent(
|
||||||
|
$eventId: ID!
|
||||||
|
$actorId: ID!
|
||||||
|
$email: String
|
||||||
|
$message: String
|
||||||
|
$locale: String
|
||||||
|
) {
|
||||||
joinEvent(
|
joinEvent(
|
||||||
eventId: $eventId,
|
eventId: $eventId
|
||||||
actorId: $actorId,
|
actorId: $actorId
|
||||||
email: $email,
|
email: $email
|
||||||
message: $message,
|
message: $message
|
||||||
locale: $locale
|
locale: $locale
|
||||||
) {
|
) {
|
||||||
${participantQuery}
|
...ParticipantsQuery
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
${PARTICIPANTS_QUERY_FRAGMENT}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const LEAVE_EVENT = gql`
|
export const LEAVE_EVENT = gql`
|
||||||
|
@ -534,20 +421,21 @@ export const DELETE_EVENT = gql`
|
||||||
export const PARTICIPANTS = gql`
|
export const PARTICIPANTS = gql`
|
||||||
query Participants($uuid: UUID!, $page: Int, $limit: Int, $roles: String) {
|
query Participants($uuid: UUID!, $page: Int, $limit: Int, $roles: String) {
|
||||||
event(uuid: $uuid) {
|
event(uuid: $uuid) {
|
||||||
id,
|
id
|
||||||
uuid,
|
uuid
|
||||||
title,
|
title
|
||||||
participants(page: $page, limit: $limit, roles: $roles) {
|
participants(page: $page, limit: $limit, roles: $roles) {
|
||||||
${participantsQuery}
|
...ParticipantsQuery
|
||||||
},
|
}
|
||||||
participantStats {
|
participantStats {
|
||||||
going,
|
going
|
||||||
notApproved,
|
notApproved
|
||||||
rejected,
|
rejected
|
||||||
participant
|
participant
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
${PARTICIPANTS_QUERY_FRAGMENT}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const EVENT_PERSON_PARTICIPATION = gql`
|
export const EVENT_PERSON_PARTICIPATION = gql`
|
||||||
|
|
|
@ -150,7 +150,7 @@ export default class EventMixin extends mixins(Vue) {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async deleteEvent(event: IEvent) {
|
private async deleteEvent(event: IEvent) {
|
||||||
const eventTitle = event.title;
|
const { title: eventTitle, id: eventId } = event;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.$apollo.mutate<IParticipant>({
|
await this.$apollo.mutate<IParticipant>({
|
||||||
|
@ -159,6 +159,9 @@ export default class EventMixin extends mixins(Vue) {
|
||||||
eventId: event.id,
|
eventId: event.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const cache = this.$apollo.getClient().cache as InMemoryCache;
|
||||||
|
cache.evict({ id: `Event:${eventId}` });
|
||||||
|
cache.gc();
|
||||||
/**
|
/**
|
||||||
* When the event corresponding has been deleted (by the organizer).
|
* When the event corresponding has been deleted (by the organizer).
|
||||||
* A notification is already triggered.
|
* A notification is already triggered.
|
||||||
|
|
|
@ -3,6 +3,7 @@ export interface IMedia {
|
||||||
url: string;
|
url: string;
|
||||||
name: string;
|
name: string;
|
||||||
alt: string;
|
alt: string;
|
||||||
|
metadata: IMediaMetadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IMediaUpload {
|
export interface IMediaUpload {
|
||||||
|
@ -10,3 +11,9 @@ export interface IMediaUpload {
|
||||||
name: string;
|
name: string;
|
||||||
alt: string | null;
|
alt: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IMediaMetadata {
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
blurhash?: string;
|
||||||
|
}
|
||||||
|
|
|
@ -58,6 +58,7 @@ $danger-invert: findColorInvert($danger);
|
||||||
$link: $primary;
|
$link: $primary;
|
||||||
$link-invert: $primary-invert;
|
$link-invert: $primary-invert;
|
||||||
$text: $violet-1;
|
$text: $violet-1;
|
||||||
|
$grey: #757575;
|
||||||
|
|
||||||
$colors: map-merge(
|
$colors: map-merge(
|
||||||
$colors,
|
$colors,
|
||||||
|
|
|
@ -782,25 +782,18 @@ export default class EditEvent extends Vue {
|
||||||
*/
|
*/
|
||||||
private postCreateOrUpdate(store: any, updateEvent: IEvent) {
|
private postCreateOrUpdate(store: any, updateEvent: IEvent) {
|
||||||
const resultEvent: IEvent = { ...updateEvent };
|
const resultEvent: IEvent = { ...updateEvent };
|
||||||
resultEvent.organizerActor = this.event.organizerActor;
|
console.log(resultEvent);
|
||||||
resultEvent.relatedEvents = [];
|
|
||||||
|
|
||||||
store.writeQuery({
|
|
||||||
query: FETCH_EVENT,
|
|
||||||
variables: { uuid: updateEvent.uuid },
|
|
||||||
data: { event: resultEvent },
|
|
||||||
});
|
|
||||||
if (!updateEvent.draft) {
|
if (!updateEvent.draft) {
|
||||||
store.writeQuery({
|
store.writeQuery({
|
||||||
query: EVENT_PERSON_PARTICIPATION,
|
query: EVENT_PERSON_PARTICIPATION,
|
||||||
variables: {
|
variables: {
|
||||||
eventId: updateEvent.id,
|
eventId: resultEvent.id,
|
||||||
name: this.event.organizerActor?.preferredUsername,
|
name: resultEvent.organizerActor?.preferredUsername,
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
person: {
|
person: {
|
||||||
__typename: "Person",
|
__typename: "Person",
|
||||||
id: this.event?.organizerActor?.id,
|
id: resultEvent?.organizerActor?.id,
|
||||||
participations: {
|
participations: {
|
||||||
__typename: "PaginatedParticipantList",
|
__typename: "PaginatedParticipantList",
|
||||||
total: 1,
|
total: 1,
|
||||||
|
@ -811,11 +804,11 @@ export default class EditEvent extends Vue {
|
||||||
role: ParticipantRole.CREATOR,
|
role: ParticipantRole.CREATOR,
|
||||||
actor: {
|
actor: {
|
||||||
__typename: "Actor",
|
__typename: "Actor",
|
||||||
id: this.event?.organizerActor?.id,
|
id: resultEvent?.organizerActor?.id,
|
||||||
},
|
},
|
||||||
event: {
|
event: {
|
||||||
__typename: "Event",
|
__typename: "Event",
|
||||||
id: updateEvent.id,
|
id: resultEvent.id,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -859,7 +852,7 @@ export default class EditEvent extends Vue {
|
||||||
* Build variables for Event GraphQL creation query
|
* Build variables for Event GraphQL creation query
|
||||||
*/
|
*/
|
||||||
private async buildVariables() {
|
private async buildVariables() {
|
||||||
let res = this.event.toEditJSON();
|
let res = new EventModel(this.event).toEditJSON();
|
||||||
const organizerActor = this.event.organizerActor?.id
|
const organizerActor = this.event.organizerActor?.id
|
||||||
? this.event.organizerActor
|
? this.event.organizerActor
|
||||||
: this.organizerActor;
|
: this.organizerActor;
|
||||||
|
|
|
@ -1,299 +1,296 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<transition appear name="fade" mode="out-in">
|
<transition appear name="fade" mode="out-in">
|
||||||
<div>
|
<div class="wrapper">
|
||||||
<div
|
<event-banner :picture="event.picture" />
|
||||||
class="header-picture"
|
<div class="intro-wrapper">
|
||||||
v-if="event.picture"
|
<div class="date-calendar-icon-wrapper">
|
||||||
:style="`background-image: url('${event.picture.url}')`"
|
<date-calendar-icon :date="event.beginsOn" />
|
||||||
/>
|
</div>
|
||||||
<div class="header-picture-default" v-else />
|
<section class="intro">
|
||||||
<section class="section intro">
|
<div class="columns">
|
||||||
<div class="columns">
|
<div class="column">
|
||||||
<div class="column is-1-tablet">
|
<h1 class="title" style="margin: 0">{{ event.title }}</h1>
|
||||||
<date-calendar-icon :date="event.beginsOn" />
|
<div class="organizer">
|
||||||
</div>
|
<span v-if="event.organizerActor && !event.attributedTo">
|
||||||
<div class="column">
|
<popover-actor-card
|
||||||
<h1 class="title" style="margin: 0">{{ event.title }}</h1>
|
:actor="event.organizerActor"
|
||||||
<div class="organizer">
|
:inline="true"
|
||||||
<span v-if="event.organizerActor && !event.attributedTo">
|
>
|
||||||
<popover-actor-card
|
<span>
|
||||||
:actor="event.organizerActor"
|
{{
|
||||||
:inline="true"
|
$t("By @{username}", {
|
||||||
|
username: usernameWithDomain(event.organizerActor),
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</popover-actor-card>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-else-if="
|
||||||
|
event.attributedTo &&
|
||||||
|
event.options.hideOrganizerWhenGroupEvent
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<span>
|
|
||||||
{{
|
|
||||||
$t("By @{username}", {
|
|
||||||
username: usernameWithDomain(event.organizerActor),
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
</span>
|
|
||||||
</popover-actor-card>
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
v-else-if="
|
|
||||||
event.attributedTo &&
|
|
||||||
event.options.hideOrganizerWhenGroupEvent
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<popover-actor-card
|
|
||||||
:actor="event.attributedTo"
|
|
||||||
:inline="true"
|
|
||||||
>
|
|
||||||
{{
|
|
||||||
$t("By @{group}", {
|
|
||||||
group: usernameWithDomain(event.attributedTo),
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
</popover-actor-card>
|
|
||||||
</span>
|
|
||||||
<span v-else-if="event.organizerActor && event.attributedTo">
|
|
||||||
<i18n path="By {group}">
|
|
||||||
<popover-actor-card
|
<popover-actor-card
|
||||||
:actor="event.attributedTo"
|
:actor="event.attributedTo"
|
||||||
slot="group"
|
|
||||||
:inline="true"
|
:inline="true"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
$t("By @{group}", {
|
||||||
|
group: usernameWithDomain(event.attributedTo),
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</popover-actor-card>
|
||||||
|
</span>
|
||||||
|
<span v-else-if="event.organizerActor && event.attributedTo">
|
||||||
|
<i18n path="By {group}">
|
||||||
|
<popover-actor-card
|
||||||
|
:actor="event.attributedTo"
|
||||||
|
slot="group"
|
||||||
|
:inline="true"
|
||||||
|
>
|
||||||
|
<router-link
|
||||||
|
:to="{
|
||||||
|
name: RouteName.GROUP,
|
||||||
|
params: {
|
||||||
|
preferredUsername: usernameWithDomain(
|
||||||
|
event.attributedTo
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
$t("@{group}", {
|
||||||
|
group: usernameWithDomain(event.attributedTo),
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</router-link>
|
||||||
|
</popover-actor-card>
|
||||||
|
</i18n>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p class="tags" v-if="event.tags && event.tags.length > 0">
|
||||||
|
<router-link
|
||||||
|
v-for="tag in event.tags"
|
||||||
|
:key="tag.title"
|
||||||
|
:to="{ name: RouteName.TAG, params: { tag: tag.title } }"
|
||||||
|
>
|
||||||
|
<tag>{{ tag.title }}</tag>
|
||||||
|
</router-link>
|
||||||
|
</p>
|
||||||
|
<b-tag type="is-warning" size="is-medium" v-if="event.draft"
|
||||||
|
>{{ $t("Draft") }}
|
||||||
|
</b-tag>
|
||||||
|
<span
|
||||||
|
class="event-status"
|
||||||
|
v-if="event.status !== EventStatus.CONFIRMED"
|
||||||
|
>
|
||||||
|
<b-tag
|
||||||
|
type="is-warning"
|
||||||
|
v-if="event.status === EventStatus.TENTATIVE"
|
||||||
|
>{{ $t("Event to be confirmed") }}</b-tag
|
||||||
|
>
|
||||||
|
<b-tag
|
||||||
|
type="is-danger"
|
||||||
|
v-if="event.status === EventStatus.CANCELLED"
|
||||||
|
>{{ $t("Event cancelled") }}</b-tag
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="column is-3-tablet">
|
||||||
|
<participation-section
|
||||||
|
:participation="participations[0]"
|
||||||
|
:event="event"
|
||||||
|
:anonymousParticipation="anonymousParticipation"
|
||||||
|
@join-event="joinEvent"
|
||||||
|
@join-modal="isJoinModalActive = true"
|
||||||
|
@join-event-with-confirmation="joinEventWithConfirmation"
|
||||||
|
@confirm-leave="confirmLeave"
|
||||||
|
@cancel-anonymous-participation="cancelAnonymousParticipation"
|
||||||
|
/>
|
||||||
|
<div class="has-text-right">
|
||||||
|
<template class="visibility" v-if="!event.draft">
|
||||||
|
<p v-if="event.visibility === EventVisibility.PUBLIC">
|
||||||
|
{{ $t("Public event") }}
|
||||||
|
<b-icon icon="earth" />
|
||||||
|
</p>
|
||||||
|
<p v-if="event.visibility === EventVisibility.UNLISTED">
|
||||||
|
{{ $t("Private event") }}
|
||||||
|
<b-icon icon="link" />
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
<template v-if="!event.local && organizer">
|
||||||
|
<a :href="event.url">
|
||||||
|
<tag>{{ organizer.domain }}</tag>
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
<p>
|
||||||
|
<router-link
|
||||||
|
class="participations-link"
|
||||||
|
v-if="actorIsOrganizer && event.draft === false"
|
||||||
|
:to="{
|
||||||
|
name: RouteName.PARTICIPATIONS,
|
||||||
|
params: { eventId: event.uuid },
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<!-- We retire one because of the event creator who is a participant -->
|
||||||
|
<span v-if="event.options.maximumAttendeeCapacity">
|
||||||
|
{{
|
||||||
|
$tc(
|
||||||
|
"{available}/{capacity} available places",
|
||||||
|
event.options.maximumAttendeeCapacity -
|
||||||
|
event.participantStats.participant,
|
||||||
|
{
|
||||||
|
available:
|
||||||
|
event.options.maximumAttendeeCapacity -
|
||||||
|
event.participantStats.participant,
|
||||||
|
capacity: event.options.maximumAttendeeCapacity,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
<span v-else>
|
||||||
|
{{
|
||||||
|
$tc(
|
||||||
|
"No one is participating|One person participating|{going} people participating",
|
||||||
|
event.participantStats.participant,
|
||||||
|
{
|
||||||
|
going: event.participantStats.participant,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</router-link>
|
||||||
|
<span v-else>
|
||||||
|
<span v-if="event.options.maximumAttendeeCapacity">
|
||||||
|
{{
|
||||||
|
$tc(
|
||||||
|
"{available}/{capacity} available places",
|
||||||
|
event.options.maximumAttendeeCapacity -
|
||||||
|
event.participantStats.participant,
|
||||||
|
{
|
||||||
|
available:
|
||||||
|
event.options.maximumAttendeeCapacity -
|
||||||
|
event.participantStats.participant,
|
||||||
|
capacity: event.options.maximumAttendeeCapacity,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
<span v-else>
|
||||||
|
{{
|
||||||
|
$tc(
|
||||||
|
"No one is participating|One person participating|{going} people participating",
|
||||||
|
event.participantStats.participant,
|
||||||
|
{
|
||||||
|
going: event.participantStats.participant,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<b-tooltip
|
||||||
|
type="is-dark"
|
||||||
|
v-if="!event.local"
|
||||||
|
:label="
|
||||||
|
$t(
|
||||||
|
'The actual number of participants may differ, as this event is hosted on another instance.'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<b-icon size="is-small" icon="help-circle-outline" />
|
||||||
|
</b-tooltip>
|
||||||
|
<b-icon icon="ticket-confirmation-outline" />
|
||||||
|
</p>
|
||||||
|
<b-dropdown position="is-bottom-left" aria-role="list">
|
||||||
|
<b-button
|
||||||
|
slot="trigger"
|
||||||
|
role="button"
|
||||||
|
icon-right="dots-horizontal"
|
||||||
|
>
|
||||||
|
{{ $t("Actions") }}
|
||||||
|
</b-button>
|
||||||
|
<b-dropdown-item
|
||||||
|
aria-role="listitem"
|
||||||
|
has-link
|
||||||
|
v-if="actorIsOrganizer || event.draft"
|
||||||
>
|
>
|
||||||
<router-link
|
<router-link
|
||||||
:to="{
|
:to="{
|
||||||
name: RouteName.GROUP,
|
name: RouteName.EDIT_EVENT,
|
||||||
params: {
|
params: { eventId: event.uuid },
|
||||||
preferredUsername: usernameWithDomain(
|
|
||||||
event.attributedTo
|
|
||||||
),
|
|
||||||
},
|
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
{{
|
{{ $t("Edit") }}
|
||||||
$t("@{group}", {
|
<b-icon icon="pencil" />
|
||||||
group: usernameWithDomain(event.attributedTo),
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
</router-link>
|
</router-link>
|
||||||
</popover-actor-card>
|
</b-dropdown-item>
|
||||||
</i18n>
|
<b-dropdown-item
|
||||||
</span>
|
aria-role="listitem"
|
||||||
</div>
|
has-link
|
||||||
<p class="tags" v-if="event.tags && event.tags.length > 0">
|
v-if="actorIsOrganizer || event.draft"
|
||||||
<router-link
|
|
||||||
v-for="tag in event.tags"
|
|
||||||
:key="tag.title"
|
|
||||||
:to="{ name: RouteName.TAG, params: { tag: tag.title } }"
|
|
||||||
>
|
|
||||||
<tag>{{ tag.title }}</tag>
|
|
||||||
</router-link>
|
|
||||||
</p>
|
|
||||||
<b-tag type="is-warning" size="is-medium" v-if="event.draft"
|
|
||||||
>{{ $t("Draft") }}
|
|
||||||
</b-tag>
|
|
||||||
<span
|
|
||||||
class="event-status"
|
|
||||||
v-if="event.status !== EventStatus.CONFIRMED"
|
|
||||||
>
|
|
||||||
<b-tag
|
|
||||||
type="is-warning"
|
|
||||||
v-if="event.status === EventStatus.TENTATIVE"
|
|
||||||
>{{ $t("Event to be confirmed") }}</b-tag
|
|
||||||
>
|
|
||||||
<b-tag
|
|
||||||
type="is-danger"
|
|
||||||
v-if="event.status === EventStatus.CANCELLED"
|
|
||||||
>{{ $t("Event cancelled") }}</b-tag
|
|
||||||
>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="column is-3-tablet">
|
|
||||||
<participation-section
|
|
||||||
:participation="participations[0]"
|
|
||||||
:event="event"
|
|
||||||
:anonymousParticipation="anonymousParticipation"
|
|
||||||
@join-event="joinEvent"
|
|
||||||
@join-modal="isJoinModalActive = true"
|
|
||||||
@join-event-with-confirmation="joinEventWithConfirmation"
|
|
||||||
@confirm-leave="confirmLeave"
|
|
||||||
@cancel-anonymous-participation="cancelAnonymousParticipation"
|
|
||||||
/>
|
|
||||||
<div class="has-text-right">
|
|
||||||
<template class="visibility" v-if="!event.draft">
|
|
||||||
<p v-if="event.visibility === EventVisibility.PUBLIC">
|
|
||||||
{{ $t("Public event") }}
|
|
||||||
<b-icon icon="earth" />
|
|
||||||
</p>
|
|
||||||
<p v-if="event.visibility === EventVisibility.UNLISTED">
|
|
||||||
{{ $t("Private event") }}
|
|
||||||
<b-icon icon="link" />
|
|
||||||
</p>
|
|
||||||
</template>
|
|
||||||
<template v-if="!event.local && organizer">
|
|
||||||
<a :href="event.url">
|
|
||||||
<tag>{{ organizer.domain }}</tag>
|
|
||||||
</a>
|
|
||||||
</template>
|
|
||||||
<p>
|
|
||||||
<router-link
|
|
||||||
class="participations-link"
|
|
||||||
v-if="actorIsOrganizer && event.draft === false"
|
|
||||||
:to="{
|
|
||||||
name: RouteName.PARTICIPATIONS,
|
|
||||||
params: { eventId: event.uuid },
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<!-- We retire one because of the event creator who is a participant -->
|
|
||||||
<span v-if="event.options.maximumAttendeeCapacity">
|
|
||||||
{{
|
|
||||||
$tc(
|
|
||||||
"{available}/{capacity} available places",
|
|
||||||
event.options.maximumAttendeeCapacity -
|
|
||||||
event.participantStats.participant,
|
|
||||||
{
|
|
||||||
available:
|
|
||||||
event.options.maximumAttendeeCapacity -
|
|
||||||
event.participantStats.participant,
|
|
||||||
capacity: event.options.maximumAttendeeCapacity,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</span>
|
|
||||||
<span v-else>
|
|
||||||
{{
|
|
||||||
$tc(
|
|
||||||
"No one is participating|One person participating|{going} people participating",
|
|
||||||
event.participantStats.participant,
|
|
||||||
{
|
|
||||||
going: event.participantStats.participant,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</span>
|
|
||||||
</router-link>
|
|
||||||
<span v-else>
|
|
||||||
<span v-if="event.options.maximumAttendeeCapacity">
|
|
||||||
{{
|
|
||||||
$tc(
|
|
||||||
"{available}/{capacity} available places",
|
|
||||||
event.options.maximumAttendeeCapacity -
|
|
||||||
event.participantStats.participant,
|
|
||||||
{
|
|
||||||
available:
|
|
||||||
event.options.maximumAttendeeCapacity -
|
|
||||||
event.participantStats.participant,
|
|
||||||
capacity: event.options.maximumAttendeeCapacity,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</span>
|
|
||||||
<span v-else>
|
|
||||||
{{
|
|
||||||
$tc(
|
|
||||||
"No one is participating|One person participating|{going} people participating",
|
|
||||||
event.participantStats.participant,
|
|
||||||
{
|
|
||||||
going: event.participantStats.participant,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<b-tooltip
|
|
||||||
type="is-dark"
|
|
||||||
v-if="!event.local"
|
|
||||||
:label="
|
|
||||||
$t(
|
|
||||||
'The actual number of participants may differ, as this event is hosted on another instance.'
|
|
||||||
)
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<b-icon size="is-small" icon="help-circle-outline" />
|
|
||||||
</b-tooltip>
|
|
||||||
<b-icon icon="ticket-confirmation-outline" />
|
|
||||||
</p>
|
|
||||||
<b-dropdown position="is-bottom-left" aria-role="list">
|
|
||||||
<b-button
|
|
||||||
slot="trigger"
|
|
||||||
role="button"
|
|
||||||
icon-right="dots-horizontal"
|
|
||||||
>
|
|
||||||
{{ $t("Actions") }}
|
|
||||||
</b-button>
|
|
||||||
<b-dropdown-item
|
|
||||||
aria-role="listitem"
|
|
||||||
has-link
|
|
||||||
v-if="actorIsOrganizer || event.draft"
|
|
||||||
>
|
|
||||||
<router-link
|
|
||||||
:to="{
|
|
||||||
name: RouteName.EDIT_EVENT,
|
|
||||||
params: { eventId: event.uuid },
|
|
||||||
}"
|
|
||||||
>
|
>
|
||||||
{{ $t("Edit") }}
|
<router-link
|
||||||
<b-icon icon="pencil" />
|
:to="{
|
||||||
</router-link>
|
name: RouteName.DUPLICATE_EVENT,
|
||||||
</b-dropdown-item>
|
params: { eventId: event.uuid },
|
||||||
<b-dropdown-item
|
}"
|
||||||
aria-role="listitem"
|
>
|
||||||
has-link
|
{{ $t("Duplicate") }}
|
||||||
v-if="actorIsOrganizer || event.draft"
|
<b-icon icon="content-duplicate" />
|
||||||
>
|
</router-link>
|
||||||
<router-link
|
</b-dropdown-item>
|
||||||
:to="{
|
<b-dropdown-item
|
||||||
name: RouteName.DUPLICATE_EVENT,
|
aria-role="listitem"
|
||||||
params: { eventId: event.uuid },
|
v-if="actorIsOrganizer || event.draft"
|
||||||
}"
|
@click="openDeleteEventModalWrapper"
|
||||||
>
|
>
|
||||||
{{ $t("Duplicate") }}
|
{{ $t("Delete") }}
|
||||||
<b-icon icon="content-duplicate" />
|
<b-icon icon="delete" />
|
||||||
</router-link>
|
</b-dropdown-item>
|
||||||
</b-dropdown-item>
|
|
||||||
<b-dropdown-item
|
|
||||||
aria-role="listitem"
|
|
||||||
v-if="actorIsOrganizer || event.draft"
|
|
||||||
@click="openDeleteEventModalWrapper"
|
|
||||||
>
|
|
||||||
{{ $t("Delete") }}
|
|
||||||
<b-icon icon="delete" />
|
|
||||||
</b-dropdown-item>
|
|
||||||
|
|
||||||
<hr
|
<hr
|
||||||
class="dropdown-divider"
|
class="dropdown-divider"
|
||||||
aria-role="menuitem"
|
aria-role="menuitem"
|
||||||
v-if="actorIsOrganizer || event.draft"
|
v-if="actorIsOrganizer || event.draft"
|
||||||
/>
|
/>
|
||||||
<b-dropdown-item
|
<b-dropdown-item
|
||||||
aria-role="listitem"
|
aria-role="listitem"
|
||||||
v-if="!event.draft"
|
v-if="!event.draft"
|
||||||
@click="triggerShare()"
|
@click="triggerShare()"
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
{{ $t("Share this event") }}
|
{{ $t("Share this event") }}
|
||||||
<b-icon icon="share" />
|
<b-icon icon="share" />
|
||||||
</span>
|
</span>
|
||||||
</b-dropdown-item>
|
</b-dropdown-item>
|
||||||
<b-dropdown-item
|
<b-dropdown-item
|
||||||
aria-role="listitem"
|
aria-role="listitem"
|
||||||
@click="downloadIcsEvent()"
|
@click="downloadIcsEvent()"
|
||||||
v-if="!event.draft"
|
v-if="!event.draft"
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
{{ $t("Add to my calendar") }}
|
{{ $t("Add to my calendar") }}
|
||||||
<b-icon icon="calendar-plus" />
|
<b-icon icon="calendar-plus" />
|
||||||
</span>
|
</span>
|
||||||
</b-dropdown-item>
|
</b-dropdown-item>
|
||||||
<b-dropdown-item
|
<b-dropdown-item
|
||||||
aria-role="listitem"
|
aria-role="listitem"
|
||||||
v-if="ableToReport"
|
v-if="ableToReport"
|
||||||
@click="isReportModalActive = true"
|
@click="isReportModalActive = true"
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
{{ $t("Report") }}
|
{{ $t("Report") }}
|
||||||
<b-icon icon="flag" />
|
<b-icon icon="flag" />
|
||||||
</span>
|
</span>
|
||||||
</b-dropdown-item>
|
</b-dropdown-item>
|
||||||
</b-dropdown>
|
</b-dropdown>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</section>
|
</div>
|
||||||
<div class="event-description-wrapper">
|
<div class="event-description-wrapper">
|
||||||
<aside class="event-metadata">
|
<aside class="event-metadata">
|
||||||
<div class="sticky">
|
<div class="sticky">
|
||||||
|
@ -662,6 +659,7 @@ import { IConfig } from "../../types/config.model";
|
||||||
import Subtitle from "../../components/Utils/Subtitle.vue";
|
import Subtitle from "../../components/Utils/Subtitle.vue";
|
||||||
import Tag from "../../components/Tag.vue";
|
import Tag from "../../components/Tag.vue";
|
||||||
import EventMetadataBlock from "../../components/Event/EventMetadataBlock.vue";
|
import EventMetadataBlock from "../../components/Event/EventMetadataBlock.vue";
|
||||||
|
import EventBanner from "../../components/Event/EventBanner.vue";
|
||||||
import ActorCard from "../../components/Account/ActorCard.vue";
|
import ActorCard from "../../components/Account/ActorCard.vue";
|
||||||
import PopoverActorCard from "../../components/Account/PopoverActorCard.vue";
|
import PopoverActorCard from "../../components/Account/PopoverActorCard.vue";
|
||||||
import { IParticipant } from "../../types/participant.model";
|
import { IParticipant } from "../../types/participant.model";
|
||||||
|
@ -683,6 +681,7 @@ import { ApolloCache, FetchResult, InMemoryCache } from "@apollo/client/core";
|
||||||
Tag,
|
Tag,
|
||||||
ActorCard,
|
ActorCard,
|
||||||
PopoverActorCard,
|
PopoverActorCard,
|
||||||
|
EventBanner,
|
||||||
"map-leaflet": () =>
|
"map-leaflet": () =>
|
||||||
import(/* webpackChunkName: "map" */ "../../components/Map.vue"),
|
import(/* webpackChunkName: "map" */ "../../components/Map.vue"),
|
||||||
ShareEventModal: () =>
|
ShareEventModal: () =>
|
||||||
|
@ -1308,18 +1307,6 @@ export default class Event extends EventMixin {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-picture,
|
|
||||||
.header-picture-default {
|
|
||||||
height: 400px;
|
|
||||||
background-size: cover;
|
|
||||||
background-position: center;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-picture-default {
|
|
||||||
background-image: url("../../../public/img/mobilizon_default_card.png");
|
|
||||||
}
|
|
||||||
|
|
||||||
div.sidebar {
|
div.sidebar {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
@ -1353,7 +1340,7 @@ div.sidebar {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.intro.section {
|
.intro {
|
||||||
background: white;
|
background: white;
|
||||||
|
|
||||||
.is-3-tablet {
|
.is-3-tablet {
|
||||||
|
@ -1570,4 +1557,30 @@ a.participations-link {
|
||||||
border: 0;
|
border: 0;
|
||||||
cursor: auto;
|
cursor: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wrapper,
|
||||||
|
.intro-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-wrapper {
|
||||||
|
position: relative;
|
||||||
|
padding: 0 16px 16px;
|
||||||
|
background: #fff;
|
||||||
|
|
||||||
|
.date-calendar-icon-wrapper {
|
||||||
|
margin-top: 16px;
|
||||||
|
height: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
align-self: flex-start;
|
||||||
|
margin-bottom: 7px;
|
||||||
|
margin-left: 0rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -189,10 +189,7 @@
|
||||||
}}</b-message>
|
}}</b-message>
|
||||||
</section>
|
</section>
|
||||||
<!-- Your upcoming events -->
|
<!-- Your upcoming events -->
|
||||||
<section
|
<section v-if="canShowMyUpcomingEvents" class="container">
|
||||||
v-if="currentActor.id && goingToEvents.size > 0"
|
|
||||||
class="container"
|
|
||||||
>
|
|
||||||
<h3 class="title">{{ $t("Your upcoming events") }}</h3>
|
<h3 class="title">{{ $t("Your upcoming events") }}</h3>
|
||||||
<b-loading :active.sync="$apollo.loading" />
|
<b-loading :active.sync="$apollo.loading" />
|
||||||
<div v-for="row of goingToEvents" class="upcoming-events" :key="row[0]">
|
<div v-for="row of goingToEvents" class="upcoming-events" :key="row[0]">
|
||||||
|
@ -236,7 +233,7 @@
|
||||||
</span>
|
</span>
|
||||||
</section>
|
</section>
|
||||||
<!-- Last week events -->
|
<!-- Last week events -->
|
||||||
<section v-if="currentActor && lastWeekEvents.length > 0">
|
<section v-if="canShowLastWeekEvents">
|
||||||
<h3 class="title">{{ $t("Last week") }}</h3>
|
<h3 class="title">{{ $t("Last week") }}</h3>
|
||||||
<b-loading :active.sync="$apollo.loading" />
|
<b-loading :active.sync="$apollo.loading" />
|
||||||
<div>
|
<div>
|
||||||
|
@ -250,7 +247,7 @@
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<!-- Events close to you -->
|
<!-- Events close to you -->
|
||||||
<section class="events-close" v-if="closeEvents.total > 0">
|
<section class="events-close" v-if="canShowCloseEvents">
|
||||||
<h2 class="is-size-2 has-text-weight-bold">
|
<h2 class="is-size-2 has-text-weight-bold">
|
||||||
{{ $t("Events nearby") }}
|
{{ $t("Events nearby") }}
|
||||||
</h2>
|
</h2>
|
||||||
|
@ -285,7 +282,12 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<hr class="home-separator" />
|
<hr
|
||||||
|
class="home-separator"
|
||||||
|
v-if="
|
||||||
|
canShowMyUpcomingEvents || canShowLastWeekEvents || canShowCloseEvents
|
||||||
|
"
|
||||||
|
/>
|
||||||
<section class="events-recent">
|
<section class="events-recent">
|
||||||
<h2 class="is-size-2 has-text-weight-bold">
|
<h2 class="is-size-2 has-text-weight-bold">
|
||||||
{{ $t("Last published events") }}
|
{{ $t("Last published events") }}
|
||||||
|
@ -586,6 +588,18 @@ export default class Home extends Vue {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get canShowMyUpcomingEvents(): boolean {
|
||||||
|
return this.currentActor.id != undefined && this.goingToEvents.size > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
get canShowLastWeekEvents(): boolean {
|
||||||
|
return this.currentActor && this.lastWeekEvents.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
get canShowCloseEvents(): boolean {
|
||||||
|
return this.closeEvents.total > 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ import { CommentModeration } from "@/types/enums";
|
||||||
import { IEvent } from "@/types/event.model";
|
import { IEvent } from "@/types/event.model";
|
||||||
import {
|
import {
|
||||||
eventCommentThreadsMock,
|
eventCommentThreadsMock,
|
||||||
|
eventNoCommentThreadsMock,
|
||||||
newCommentForEventMock,
|
newCommentForEventMock,
|
||||||
newCommentForEventResponse,
|
newCommentForEventResponse,
|
||||||
} from "../../mocks/event";
|
} from "../../mocks/event";
|
||||||
|
@ -35,7 +36,7 @@ const eventData = {
|
||||||
};
|
};
|
||||||
describe("CommentTree", () => {
|
describe("CommentTree", () => {
|
||||||
let wrapper: Wrapper<Vue>;
|
let wrapper: Wrapper<Vue>;
|
||||||
let mockClient: MockApolloClient;
|
let mockClient: MockApolloClient | null;
|
||||||
let apolloProvider;
|
let apolloProvider;
|
||||||
let requestHandlers: Record<string, RequestHandler>;
|
let requestHandlers: Record<string, RequestHandler>;
|
||||||
const cache = new InMemoryCache({ addTypename: false });
|
const cache = new InMemoryCache({ addTypename: false });
|
||||||
|
@ -83,24 +84,10 @@ describe("CommentTree", () => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
it("renders an empty comment tree", async () => {
|
|
||||||
generateWrapper();
|
|
||||||
|
|
||||||
expect(wrapper.exists()).toBe(true);
|
|
||||||
expect(wrapper.find(".loading").text()).toBe("Loading comments…");
|
|
||||||
|
|
||||||
await wrapper.vm.$nextTick();
|
|
||||||
await wrapper.vm.$nextTick(); // because of the <transition>
|
|
||||||
|
|
||||||
expect(wrapper.find(".no-comments").text()).toBe("No comments yet");
|
|
||||||
expect(wrapper.html()).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders a comment tree with comments", async () => {
|
it("renders a comment tree with comments", async () => {
|
||||||
generateWrapper();
|
generateWrapper();
|
||||||
|
|
||||||
await wrapper.vm.$nextTick();
|
await flushPromises();
|
||||||
await wrapper.vm.$nextTick(); // because of the <transition>
|
|
||||||
|
|
||||||
expect(wrapper.exists()).toBe(true);
|
expect(wrapper.exists()).toBe(true);
|
||||||
expect(
|
expect(
|
||||||
|
@ -150,4 +137,21 @@ describe("CommentTree", () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("renders an empty comment tree", async () => {
|
||||||
|
generateWrapper({
|
||||||
|
eventCommentThreadsQueryHandler: jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue(eventNoCommentThreadsMock),
|
||||||
|
});
|
||||||
|
expect(requestHandlers.eventCommentThreadsQueryHandler).toHaveBeenCalled();
|
||||||
|
|
||||||
|
expect(wrapper.exists()).toBe(true);
|
||||||
|
expect(wrapper.find(".loading").text()).toBe("Loading comments…");
|
||||||
|
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(wrapper.find(".no-comments").text()).toBe("No comments yet");
|
||||||
|
expect(wrapper.html()).toMatchSnapshot();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,25 +2,62 @@
|
||||||
|
|
||||||
exports[`CommentTree renders a comment tree with comments 1`] = `
|
exports[`CommentTree renders a comment tree with comments 1`] = `
|
||||||
<div>
|
<div>
|
||||||
<!---->
|
<form class="new-comment">
|
||||||
|
<!---->
|
||||||
|
<article class="media">
|
||||||
|
<figure class="media-left">
|
||||||
|
<identity-picker-wrapper-stub value="[object Object]"></identity-picker-wrapper-stub>
|
||||||
|
</figure>
|
||||||
|
<div class="media-content">
|
||||||
|
<div class="field">
|
||||||
|
<div class="field">
|
||||||
|
<p class="control">
|
||||||
|
<editor-stub mode="comment" value=""></editor-stub>
|
||||||
|
</p>
|
||||||
|
<!---->
|
||||||
|
</div>
|
||||||
|
<!---->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="send-comment">
|
||||||
|
<b-button-stub type="is-primary" iconleft="send" nativetype="submit" tag="button" aria-label="Post a comment" class="comment-button-submit"></b-button-stub>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</form>
|
||||||
<transition-group-stub name="comment-empty-list" mode="out-in">
|
<transition-group-stub name="comment-empty-list" mode="out-in">
|
||||||
<transition-group-stub tag="ul" name="comment-list" class="comment-list">
|
<transition-group-stub tag="ul" name="comment-list" class="comment-list">
|
||||||
<comment-stub comment="[object Object]" event="[object Object]" class="root-comment"></comment-stub>
|
<comment-stub comment="[object Object]" event="[object Object]" class="root-comment"></comment-stub>
|
||||||
<comment-stub comment="[object Object]" event="[object Object]" class="root-comment"></comment-stub>
|
<comment-stub comment="[object Object]" event="[object Object]" class="root-comment"></comment-stub>
|
||||||
</transition-group-stub>
|
</transition-group-stub>
|
||||||
<div class="no-comments"><span>No comments yet</span></div>
|
|
||||||
</transition-group-stub>
|
</transition-group-stub>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`CommentTree renders an empty comment tree 1`] = `
|
exports[`CommentTree renders an empty comment tree 1`] = `
|
||||||
<div>
|
<div>
|
||||||
<!---->
|
<form class="new-comment">
|
||||||
|
<!---->
|
||||||
|
<article class="media">
|
||||||
|
<figure class="media-left">
|
||||||
|
<identity-picker-wrapper-stub value="[object Object]"></identity-picker-wrapper-stub>
|
||||||
|
</figure>
|
||||||
|
<div class="media-content">
|
||||||
|
<div class="field">
|
||||||
|
<div class="field">
|
||||||
|
<p class="control">
|
||||||
|
<editor-stub mode="comment" value=""></editor-stub>
|
||||||
|
</p>
|
||||||
|
<!---->
|
||||||
|
</div>
|
||||||
|
<!---->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="send-comment">
|
||||||
|
<b-button-stub type="is-primary" iconleft="send" nativetype="submit" tag="button" aria-label="Post a comment" class="comment-button-submit"></b-button-stub>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</form>
|
||||||
<transition-group-stub name="comment-empty-list" mode="out-in">
|
<transition-group-stub name="comment-empty-list" mode="out-in">
|
||||||
<transition-group-stub tag="ul" name="comment-list" class="comment-list">
|
|
||||||
<comment-stub comment="[object Object]" event="[object Object]" class="root-comment"></comment-stub>
|
|
||||||
<comment-stub comment="[object Object]" event="[object Object]" class="root-comment"></comment-stub>
|
|
||||||
</transition-group-stub>
|
|
||||||
<div class="no-comments"><span>No comments yet</span></div>
|
<div class="no-comments"><span>No comments yet</span></div>
|
||||||
</transition-group-stub>
|
</transition-group-stub>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -63,6 +63,17 @@ export const joinEventMock = {
|
||||||
locale: "en_US",
|
locale: "en_US",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const eventNoCommentThreadsMock = {
|
||||||
|
data: {
|
||||||
|
event: {
|
||||||
|
__typename: "Event",
|
||||||
|
id: "1",
|
||||||
|
uuid: "f37910ea-fd5a-4756-9679-00971f3f4106",
|
||||||
|
comments: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const eventCommentThreadsMock = {
|
export const eventCommentThreadsMock = {
|
||||||
data: {
|
data: {
|
||||||
event: {
|
event: {
|
||||||
|
|
|
@ -3416,6 +3416,11 @@ bluebird@^3.1.1, bluebird@^3.7.2:
|
||||||
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
|
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
|
||||||
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
|
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
|
||||||
|
|
||||||
|
blurhash@^1.1.3:
|
||||||
|
version "1.1.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/blurhash/-/blurhash-1.1.3.tgz#dc325af7da836d07a0861d830bdd63694382483e"
|
||||||
|
integrity sha512-yUhPJvXexbqbyijCIE/T2NCXcj9iNPhWmOKbPTuR/cm7Q5snXYIfnVnz6m7MWOXxODMz/Cr3UcVkRdHiuDVRDw==
|
||||||
|
|
||||||
body-parser@1.19.0:
|
body-parser@1.19.0:
|
||||||
version "1.19.0"
|
version "1.19.0"
|
||||||
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a"
|
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a"
|
||||||
|
|
|
@ -352,18 +352,14 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
|
||||||
end
|
end
|
||||||
|
|
||||||
def make_media_data(media) when is_map(media) do
|
def make_media_data(media) when is_map(media) do
|
||||||
with {:ok, %{"url" => [%{"href" => url, "mediaType" => content_type}], "size" => size}} <-
|
with {:ok, %{url: url} = uploaded} <-
|
||||||
Mobilizon.Web.Upload.store(media.file),
|
Mobilizon.Web.Upload.store(media.file),
|
||||||
{:media_exists, nil} <- {:media_exists, Mobilizon.Medias.get_media_by_url(url)},
|
{:media_exists, nil} <- {:media_exists, Mobilizon.Medias.get_media_by_url(url)},
|
||||||
{:ok, %Media{file: _file} = media} <-
|
{:ok, %Media{file: _file} = media} <-
|
||||||
Mobilizon.Medias.create_media(%{
|
Mobilizon.Medias.create_media(%{
|
||||||
"file" => %{
|
file: Map.take(uploaded, [:url, :name, :content_type, :size]),
|
||||||
"url" => url,
|
metadata: Map.take(uploaded, [:width, :height, :blurhash]),
|
||||||
"name" => media.name,
|
actor_id: media.actor_id
|
||||||
"content_type" => content_type,
|
|
||||||
"size" => size
|
|
||||||
},
|
|
||||||
"actor_id" => media.actor_id
|
|
||||||
}) do
|
}) do
|
||||||
Converter.Media.model_to_as(media)
|
Converter.Media.model_to_as(media)
|
||||||
else
|
else
|
||||||
|
|
|
@ -143,7 +143,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Actor do
|
||||||
when code in 200..299 <- RemoteMediaDownloaderClient.get(url),
|
when code in 200..299 <- RemoteMediaDownloaderClient.get(url),
|
||||||
name <- name || Parser.get_filename_from_response(response_headers, url) || default_name,
|
name <- name || Parser.get_filename_from_response(response_headers, url) || default_name,
|
||||||
{:ok, file} <- Upload.store(%{body: body, name: name}) do
|
{:ok, file} <- Upload.store(%{body: body, name: name}) do
|
||||||
file
|
Map.take(file, [:content_type, :name, :url, :size])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -40,17 +40,13 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Media do
|
||||||
)
|
)
|
||||||
when is_binary(media_url) do
|
when is_binary(media_url) do
|
||||||
with {:ok, %{body: body}} <- Tesla.get(media_url, opts: @http_options),
|
with {:ok, %{body: body}} <- Tesla.get(media_url, opts: @http_options),
|
||||||
{:ok, %{name: name, url: url, content_type: content_type, size: size}} <-
|
{:ok, %{url: url} = uploaded} <-
|
||||||
Upload.store(%{body: body, name: name}),
|
Upload.store(%{body: body, name: name}),
|
||||||
{:media_exists, nil} <- {:media_exists, Medias.get_media_by_url(url)} do
|
{:media_exists, nil} <- {:media_exists, Medias.get_media_by_url(url)} do
|
||||||
Medias.create_media(%{
|
Medias.create_media(%{
|
||||||
"file" => %{
|
file: Map.take(uploaded, [:url, :name, :content_type, :size]),
|
||||||
"url" => url,
|
metadata: Map.take(uploaded, [:width, :height, :blurhash]),
|
||||||
"name" => name,
|
actor_id: actor_id
|
||||||
"content_type" => content_type,
|
|
||||||
"size" => size
|
|
||||||
},
|
|
||||||
"actor_id" => actor_id
|
|
||||||
})
|
})
|
||||||
else
|
else
|
||||||
{:media_exists, %MediaModel{file: _file} = media} ->
|
{:media_exists, %MediaModel{file: _file} = media} ->
|
||||||
|
|
|
@ -144,7 +144,7 @@ defmodule Mobilizon.Federation.WebFinger do
|
||||||
@spec find_webfinger_endpoint(String.t()) :: String.t()
|
@spec find_webfinger_endpoint(String.t()) :: String.t()
|
||||||
def find_webfinger_endpoint(domain) when is_binary(domain) do
|
def find_webfinger_endpoint(domain) when is_binary(domain) do
|
||||||
with {:ok, %{body: body}} <- fetch_document("http://#{domain}/.well-known/host-meta"),
|
with {:ok, %{body: body}} <- fetch_document("http://#{domain}/.well-known/host-meta"),
|
||||||
link_template <- find_link_from_template(body) do
|
link_template when is_binary(link_template) <- find_link_from_template(body) do
|
||||||
{:ok, link_template}
|
{:ok, link_template}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -203,6 +203,9 @@ defmodule Mobilizon.Federation.WebFinger do
|
||||||
xpath(doc, ~x"//Link[@rel=\"lrdd\"][@type=\"application/json\"]/@template"s),
|
xpath(doc, ~x"//Link[@rel=\"lrdd\"][@type=\"application/json\"]/@template"s),
|
||||||
res when res in [nil, ""] <- xpath(doc, ~x"//Link[@rel=\"lrdd\"]/@template"s),
|
res when res in [nil, ""] <- xpath(doc, ~x"//Link[@rel=\"lrdd\"]/@template"s),
|
||||||
do: {:error, :link_not_found}
|
do: {:error, :link_not_found}
|
||||||
|
catch
|
||||||
|
:exit, _e ->
|
||||||
|
{:error, :link_not_found}
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec fetch_document(String.t()) :: Tesla.Env.result()
|
@spec fetch_document(String.t()) :: Tesla.Env.result()
|
||||||
|
|
|
@ -52,13 +52,16 @@ defmodule Mobilizon.GraphQL.API.Events do
|
||||||
defp process_picture(%{media_id: _picture_id} = args, _), do: args
|
defp process_picture(%{media_id: _picture_id} = args, _), do: args
|
||||||
|
|
||||||
defp process_picture(%{media: media}, %Actor{id: actor_id}) do
|
defp process_picture(%{media: media}, %Actor{id: actor_id}) do
|
||||||
%{
|
with uploaded when is_map(uploaded) <-
|
||||||
file:
|
media
|
||||||
media
|
|> Map.get(:file)
|
||||||
|> Map.get(:file)
|
|> Utils.make_media_data(description: Map.get(media, :name)) do
|
||||||
|> Utils.make_media_data(description: Map.get(media, :name)),
|
%{
|
||||||
actor_id: actor_id
|
file: Map.take(uploaded, [:url, :name, :content_type, :size]),
|
||||||
}
|
metadata: Map.take(uploaded, [:width, :height, :blurhash]),
|
||||||
|
actor_id: actor_id
|
||||||
|
}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec extract_pictures_from_event_body(map(), Actor.t()) :: map()
|
@spec extract_pictures_from_event_body(map(), Actor.t()) :: map()
|
||||||
|
|
|
@ -47,7 +47,13 @@ defmodule Mobilizon.GraphQL.Resolvers.Media do
|
||||||
%{context: %{current_user: %User{} = user}}
|
%{context: %{current_user: %User{} = user}}
|
||||||
) do
|
) do
|
||||||
with %Actor{id: actor_id} <- Users.get_actor_for_user(user),
|
with %Actor{id: actor_id} <- Users.get_actor_for_user(user),
|
||||||
{:ok, %{name: _name, url: url, content_type: content_type, size: size}} <-
|
{:ok,
|
||||||
|
%{
|
||||||
|
name: _name,
|
||||||
|
url: url,
|
||||||
|
content_type: content_type,
|
||||||
|
size: size
|
||||||
|
} = uploaded} <-
|
||||||
Mobilizon.Web.Upload.store(file),
|
Mobilizon.Web.Upload.store(file),
|
||||||
args <-
|
args <-
|
||||||
args
|
args
|
||||||
|
@ -55,7 +61,11 @@ defmodule Mobilizon.GraphQL.Resolvers.Media do
|
||||||
|> Map.put(:size, size)
|
|> Map.put(:size, size)
|
||||||
|> Map.put(:content_type, content_type),
|
|> Map.put(:content_type, content_type),
|
||||||
{:ok, media = %Media{}} <-
|
{:ok, media = %Media{}} <-
|
||||||
Medias.create_media(%{"file" => args, "actor_id" => actor_id}) do
|
Medias.create_media(%{
|
||||||
|
file: args,
|
||||||
|
actor_id: actor_id,
|
||||||
|
metadata: Map.take(uploaded, [:width, :height, :blurhash])
|
||||||
|
}) do
|
||||||
{:ok, transform_media(media)}
|
{:ok, transform_media(media)}
|
||||||
else
|
else
|
||||||
{:error, :mime_type_not_allowed} ->
|
{:error, :mime_type_not_allowed} ->
|
||||||
|
@ -124,13 +134,14 @@ defmodule Mobilizon.GraphQL.Resolvers.Media do
|
||||||
def user_size(_parent, _args, _resolution), do: {:error, :unauthenticated}
|
def user_size(_parent, _args, _resolution), do: {:error, :unauthenticated}
|
||||||
|
|
||||||
@spec transform_media(Media.t()) :: map()
|
@spec transform_media(Media.t()) :: map()
|
||||||
defp transform_media(%Media{id: id, file: file}) do
|
defp transform_media(%Media{id: id, file: file, metadata: metadata}) do
|
||||||
%{
|
%{
|
||||||
name: file.name,
|
name: file.name,
|
||||||
url: file.url,
|
url: file.url,
|
||||||
id: id,
|
id: id,
|
||||||
content_type: file.content_type,
|
content_type: file.content_type,
|
||||||
size: file.size
|
size: file.size,
|
||||||
|
metadata: metadata
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -215,13 +215,16 @@ defmodule Mobilizon.GraphQL.Resolvers.Post do
|
||||||
defp process_picture(%{media_id: _picture_id} = args, _), do: args
|
defp process_picture(%{media_id: _picture_id} = args, _), do: args
|
||||||
|
|
||||||
defp process_picture(%{media: media}, %Actor{id: actor_id}) do
|
defp process_picture(%{media: media}, %Actor{id: actor_id}) do
|
||||||
%{
|
with uploaded when is_map(uploaded) <-
|
||||||
file:
|
media
|
||||||
media
|
|> Map.get(:file)
|
||||||
|> Map.get(:file)
|
|> Utils.make_media_data(description: Map.get(media, :name)) do
|
||||||
|> Utils.make_media_data(description: Map.get(media, :name)),
|
%{
|
||||||
actor_id: actor_id
|
file: Map.take(uploaded, [:url, :name, :content_type, :size]),
|
||||||
}
|
metadata: Map.take(uploaded, [:width, :height, :blurhash]),
|
||||||
|
actor_id: actor_id
|
||||||
|
}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec extract_pictures_from_post_body(map(), String.t()) :: map()
|
@spec extract_pictures_from_post_body(map(), String.t()) :: map()
|
||||||
|
|
|
@ -14,6 +14,7 @@ defmodule Mobilizon.GraphQL.Schema.MediaType do
|
||||||
field(:url, :string, description: "The media's full URL")
|
field(:url, :string, description: "The media's full URL")
|
||||||
field(:content_type, :string, description: "The media's detected content type")
|
field(:content_type, :string, description: "The media's detected content type")
|
||||||
field(:size, :integer, description: "The media's size")
|
field(:size, :integer, description: "The media's size")
|
||||||
|
field(:metadata, :media_metadata, description: "The media's metadata")
|
||||||
end
|
end
|
||||||
|
|
||||||
@desc """
|
@desc """
|
||||||
|
@ -24,6 +25,15 @@ defmodule Mobilizon.GraphQL.Schema.MediaType do
|
||||||
field(:total, :integer, description: "The total number of medias in the list")
|
field(:total, :integer, description: "The total number of medias in the list")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@desc """
|
||||||
|
Some metadata associated with a media
|
||||||
|
"""
|
||||||
|
object :media_metadata do
|
||||||
|
field(:width, :integer, description: "The media width (if a picture)")
|
||||||
|
field(:height, :integer, description: "The media width (if a height)")
|
||||||
|
field(:blurhash, :string, description: "The media blurhash (if a picture")
|
||||||
|
end
|
||||||
|
|
||||||
@desc "An attached media or a link to a media"
|
@desc "An attached media or a link to a media"
|
||||||
input_object :media_input do
|
input_object :media_input do
|
||||||
# Either a full media object
|
# Either a full media object
|
||||||
|
|
|
@ -80,11 +80,12 @@ defmodule Mobilizon.Discussions do
|
||||||
# However, it also excludes all top-level comments with deleted replies from being selected
|
# However, it also excludes all top-level comments with deleted replies from being selected
|
||||||
# |> where([_, r], is_nil(r.deleted_at))
|
# |> where([_, r], is_nil(r.deleted_at))
|
||||||
|> group_by([c], c.id)
|
|> group_by([c], c.id)
|
||||||
|
|> order_by([c], desc: :is_announcement, asc: :published_at)
|
||||||
|> select([c, r], %{c | total_replies: count(r.id)})
|
|> select([c, r], %{c | total_replies: count(r.id)})
|
||||||
end
|
end
|
||||||
|
|
||||||
def query(Comment, _) do
|
def query(Comment, _) do
|
||||||
order_by(Comment, [c], asc: :published_at)
|
order_by(Comment, [c], asc: :is_announcement, asc: :published_at)
|
||||||
end
|
end
|
||||||
|
|
||||||
def query(queryable, _) do
|
def query(queryable, _) do
|
||||||
|
|
|
@ -256,7 +256,7 @@ defmodule Mobilizon.Events.Event do
|
||||||
|
|
||||||
# In case it's a new picture
|
# In case it's a new picture
|
||||||
defp put_picture(%Changeset{} = changeset, _attrs) do
|
defp put_picture(%Changeset{} = changeset, _attrs) do
|
||||||
cast_assoc(changeset, :picture)
|
cast_assoc(changeset, :picture, with: &Media.changeset/2)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Created or updated with draft parameter: don't publish
|
# Created or updated with draft parameter: don't publish
|
||||||
|
|
|
@ -5,21 +5,32 @@ defmodule Mobilizon.Medias.Media do
|
||||||
|
|
||||||
use Ecto.Schema
|
use Ecto.Schema
|
||||||
|
|
||||||
import Ecto.Changeset, only: [cast: 3, cast_embed: 2]
|
import Ecto.Changeset, only: [cast: 3, cast_embed: 2, cast_embed: 3]
|
||||||
|
|
||||||
alias Mobilizon.Actors.Actor
|
alias Mobilizon.Actors.Actor
|
||||||
alias Mobilizon.Discussions.Comment
|
alias Mobilizon.Discussions.Comment
|
||||||
alias Mobilizon.Events.Event
|
alias Mobilizon.Events.Event
|
||||||
alias Mobilizon.Medias.File
|
alias Mobilizon.Medias.File
|
||||||
|
alias Mobilizon.Medias.Media.Metadata
|
||||||
alias Mobilizon.Posts.Post
|
alias Mobilizon.Posts.Post
|
||||||
|
|
||||||
@type t :: %__MODULE__{
|
@type t :: %__MODULE__{
|
||||||
file: File.t(),
|
file: File.t(),
|
||||||
|
metadata: Metadata.t(),
|
||||||
actor: Actor.t()
|
actor: Actor.t()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@metadata_attrs [:height, :width, :blurhash]
|
||||||
|
|
||||||
schema "medias" do
|
schema "medias" do
|
||||||
embeds_one(:file, File, on_replace: :update)
|
embeds_one(:file, File, on_replace: :update)
|
||||||
|
|
||||||
|
embeds_one :metadata, Metadata, on_replace: :update do
|
||||||
|
field(:height, :integer)
|
||||||
|
field(:width, :integer)
|
||||||
|
field(:blurhash, :string)
|
||||||
|
end
|
||||||
|
|
||||||
belongs_to(:actor, Actor)
|
belongs_to(:actor, Actor)
|
||||||
has_many(:event_picture, Event, foreign_key: :picture_id)
|
has_many(:event_picture, Event, foreign_key: :picture_id)
|
||||||
many_to_many(:events, Event, join_through: "events_medias")
|
many_to_many(:events, Event, join_through: "events_medias")
|
||||||
|
@ -36,5 +47,13 @@ defmodule Mobilizon.Medias.Media do
|
||||||
media
|
media
|
||||||
|> cast(attrs, [:actor_id])
|
|> cast(attrs, [:actor_id])
|
||||||
|> cast_embed(:file)
|
|> cast_embed(:file)
|
||||||
|
|> cast_embed(:metadata, with: &metadata_changeset/2)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc false
|
||||||
|
@spec changeset(struct(), map) :: Ecto.Changeset.t()
|
||||||
|
def metadata_changeset(metadata, attrs) do
|
||||||
|
metadata
|
||||||
|
|> cast(attrs, @metadata_attrs)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
47
lib/web/upload/filter/analyze_metadata.ex
Normal file
47
lib/web/upload/filter/analyze_metadata.ex
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
# Portions of this file are derived from Pleroma:
|
||||||
|
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
# Upstream: https://git.pleroma.social/pleroma/pleroma/-/blob/develop/lib/pleroma/upload/filter/analyze_metadata.ex
|
||||||
|
|
||||||
|
defmodule Mobilizon.Web.Upload.Filter.AnalyzeMetadata do
|
||||||
|
@moduledoc """
|
||||||
|
Extracts metadata about the upload, such as width/height
|
||||||
|
"""
|
||||||
|
require Logger
|
||||||
|
alias Mobilizon.Web.Upload
|
||||||
|
|
||||||
|
@behaviour Mobilizon.Web.Upload.Filter
|
||||||
|
|
||||||
|
@spec filter(Upload.t()) ::
|
||||||
|
{:ok, :filtered, Upload.t()} | {:ok, :noop} | {:error, String.t()}
|
||||||
|
def filter(%Upload{tempfile: file, content_type: "image" <> _} = upload) do
|
||||||
|
image =
|
||||||
|
file
|
||||||
|
|> Mogrify.open()
|
||||||
|
|> Mogrify.verbose()
|
||||||
|
|
||||||
|
upload =
|
||||||
|
upload
|
||||||
|
|> Map.put(:width, image.width)
|
||||||
|
|> Map.put(:height, image.height)
|
||||||
|
|> Map.put(:blurhash, get_blurhash(file))
|
||||||
|
|
||||||
|
{:ok, :filtered, upload}
|
||||||
|
rescue
|
||||||
|
e in ErlangError ->
|
||||||
|
Logger.warn("#{__MODULE__}: #{inspect(e)}")
|
||||||
|
{:ok, :noop}
|
||||||
|
end
|
||||||
|
|
||||||
|
def filter(_), do: {:ok, :noop}
|
||||||
|
|
||||||
|
defp get_blurhash(file) do
|
||||||
|
case :eblurhash.magick(to_charlist(file)) do
|
||||||
|
{:ok, blurhash} ->
|
||||||
|
to_string(blurhash)
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -73,12 +73,9 @@ defmodule Mobilizon.Web.Upload do
|
||||||
{:ok, upload} <- Filter.filter(opts.filters, upload),
|
{:ok, upload} <- Filter.filter(opts.filters, upload),
|
||||||
{:ok, url_spec} <- Uploader.put_file(opts.uploader, upload) do
|
{:ok, url_spec} <- Uploader.put_file(opts.uploader, upload) do
|
||||||
{:ok,
|
{:ok,
|
||||||
%{
|
upload
|
||||||
name: Map.get(opts, :description) || upload.name,
|
|> Map.put(:name, Map.get(opts, :description) || upload.name)
|
||||||
url: url_from_spec(upload, opts.base_url, url_spec),
|
|> Map.put(:url, url_from_spec(upload, opts.base_url, url_spec))}
|
||||||
content_type: upload.content_type,
|
|
||||||
size: upload.size
|
|
||||||
}}
|
|
||||||
else
|
else
|
||||||
{:error, error} ->
|
{:error, error} ->
|
||||||
Logger.error(
|
Logger.error(
|
||||||
|
|
3
mix.exs
3
mix.exs
|
@ -162,6 +162,9 @@ defmodule Mobilizon.Mixfile do
|
||||||
{:sweet_xml, "~> 0.6.6"},
|
{:sweet_xml, "~> 0.6.6"},
|
||||||
{:web_push_encryption,
|
{:web_push_encryption,
|
||||||
git: "https://github.com/tcitworld/elixir-web-push-encryption", branch: "otp-24"},
|
git: "https://github.com/tcitworld/elixir-web-push-encryption", branch: "otp-24"},
|
||||||
|
{:eblurhash,
|
||||||
|
git: "https://github.com/zotonic/eblurhash",
|
||||||
|
ref: "04a0b76eadf4de1be17726f39b6313b88708fd12"},
|
||||||
# Dev and test dependencies
|
# Dev and test dependencies
|
||||||
{:phoenix_live_reload, "~> 1.2", only: [:dev, :e2e]},
|
{:phoenix_live_reload, "~> 1.2", only: [:dev, :e2e]},
|
||||||
{:ex_machina, "~> 2.3", only: [:dev, :test]},
|
{:ex_machina, "~> 2.3", only: [:dev, :test]},
|
||||||
|
|
1
mix.lock
1
mix.lock
|
@ -25,6 +25,7 @@
|
||||||
"dialyxir": {:hex, :dialyxir, "1.1.0", "c5aab0d6e71e5522e77beff7ba9e08f8e02bad90dfbeffae60eaf0cb47e29488", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "07ea8e49c45f15264ebe6d5b93799d4dd56a44036cf42d0ad9c960bc266c0b9a"},
|
"dialyxir": {:hex, :dialyxir, "1.1.0", "c5aab0d6e71e5522e77beff7ba9e08f8e02bad90dfbeffae60eaf0cb47e29488", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "07ea8e49c45f15264ebe6d5b93799d4dd56a44036cf42d0ad9c960bc266c0b9a"},
|
||||||
"earmark": {:hex, :earmark, "1.4.15", "2c7f924bf495ec1f65bd144b355d0949a05a254d0ec561740308a54946a67888", [:mix], [{:earmark_parser, ">= 1.4.13", [hex: :earmark_parser, repo: "hexpm", optional: false]}], "hexpm", "3b1209b85bc9f3586f370f7c363f6533788fb4e51db23aa79565875e7f9999ee"},
|
"earmark": {:hex, :earmark, "1.4.15", "2c7f924bf495ec1f65bd144b355d0949a05a254d0ec561740308a54946a67888", [:mix], [{:earmark_parser, ">= 1.4.13", [hex: :earmark_parser, repo: "hexpm", optional: false]}], "hexpm", "3b1209b85bc9f3586f370f7c363f6533788fb4e51db23aa79565875e7f9999ee"},
|
||||||
"earmark_parser": {:hex, :earmark_parser, "1.4.13", "0c98163e7d04a15feb62000e1a891489feb29f3d10cb57d4f845c405852bbef8", [:mix], [], "hexpm", "d602c26af3a0af43d2f2645613f65841657ad6efc9f0e361c3b6c06b578214ba"},
|
"earmark_parser": {:hex, :earmark_parser, "1.4.13", "0c98163e7d04a15feb62000e1a891489feb29f3d10cb57d4f845c405852bbef8", [:mix], [], "hexpm", "d602c26af3a0af43d2f2645613f65841657ad6efc9f0e361c3b6c06b578214ba"},
|
||||||
|
"eblurhash": {:git, "https://github.com/zotonic/eblurhash", "04a0b76eadf4de1be17726f39b6313b88708fd12", [ref: "04a0b76eadf4de1be17726f39b6313b88708fd12"]},
|
||||||
"ecto": {:hex, :ecto, "3.6.2", "efdf52acfc4ce29249bab5417415bd50abd62db7b0603b8bab0d7b996548c2bc", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "efad6dfb04e6f986b8a3047822b0f826d9affe8e4ebdd2aeedbfcb14fd48884e"},
|
"ecto": {:hex, :ecto, "3.6.2", "efdf52acfc4ce29249bab5417415bd50abd62db7b0603b8bab0d7b996548c2bc", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "efad6dfb04e6f986b8a3047822b0f826d9affe8e4ebdd2aeedbfcb14fd48884e"},
|
||||||
"ecto_autoslug_field": {:hex, :ecto_autoslug_field, "2.0.1", "2177c1c253f6dd3efd4b56d1cb76104d0a6ef044c6b9a7a0ad6d32665c4111e5", [:mix], [{:ecto, ">= 2.1.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:slugger, ">= 0.2.0", [hex: :slugger, repo: "hexpm", optional: false]}], "hexpm", "a3cc73211f2e75b89a03332183812ebe1ac08be2e25a1df5aa3d1422f92c45c3"},
|
"ecto_autoslug_field": {:hex, :ecto_autoslug_field, "2.0.1", "2177c1c253f6dd3efd4b56d1cb76104d0a6ef044c6b9a7a0ad6d32665c4111e5", [:mix], [{:ecto, ">= 2.1.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:slugger, ">= 0.2.0", [hex: :slugger, repo: "hexpm", optional: false]}], "hexpm", "a3cc73211f2e75b89a03332183812ebe1ac08be2e25a1df5aa3d1422f92c45c3"},
|
||||||
"ecto_enum": {:hex, :ecto_enum, "1.4.0", "d14b00e04b974afc69c251632d1e49594d899067ee2b376277efd8233027aec8", [:mix], [{:ecto, ">= 3.0.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "> 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:mariaex, ">= 0.0.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "8fb55c087181c2b15eee406519dc22578fa60dd82c088be376d0010172764ee4"},
|
"ecto_enum": {:hex, :ecto_enum, "1.4.0", "d14b00e04b974afc69c251632d1e49594d899067ee2b376277efd8233027aec8", [:mix], [{:ecto, ">= 3.0.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "> 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:mariaex, ">= 0.0.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "8fb55c087181c2b15eee406519dc22578fa60dd82c088be376d0010172764ee4"},
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
defmodule Mobilizon.Storage.Repo.Migrations.AddMetadataToMedia do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def change do
|
||||||
|
alter table(:medias) do
|
||||||
|
add(:metadata, :map)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
20
test/web/upload/filter/analyze_metadata_test.exs
Normal file
20
test/web/upload/filter/analyze_metadata_test.exs
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
# Portions of this file are derived from Pleroma:
|
||||||
|
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
# Upstream: https://git.pleroma.social/pleroma/pleroma/-/blob/develop/test/pleroma/upload/filter/analyze_metadata_test.exs
|
||||||
|
|
||||||
|
defmodule Mobilizon.Web.Upload.Filter.AnalyzeMetadataTest do
|
||||||
|
use Mobilizon.DataCase, async: true
|
||||||
|
alias Mobilizon.Web.Upload.Filter.AnalyzeMetadata
|
||||||
|
|
||||||
|
test "adds the image dimensions" do
|
||||||
|
upload = %Mobilizon.Web.Upload{
|
||||||
|
name: "an… image.jpg",
|
||||||
|
content_type: "image/jpeg",
|
||||||
|
path: Path.absname("test/fixtures/image.jpg"),
|
||||||
|
tempfile: Path.absname("test/fixtures/image.jpg")
|
||||||
|
}
|
||||||
|
|
||||||
|
assert {:ok, :filtered, %{width: 266, height: 67}} = AnalyzeMetadata.filter(upload)
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in a new issue