From 16f90254a0308d36504e89399b8048b393d9b141 Mon Sep 17 00:00:00 2001 From: Thomas Citharel Date: Thu, 5 Aug 2021 16:17:57 +0200 Subject: [PATCH 1/5] Revert "Try to add --cache to kaniko" This reverts commit 3eb56b57aa54cb0728e88fe0e650b36ceb03d5e9. --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 91d8d1c89..28f6f4443 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -201,7 +201,7 @@ pages: - mkdir -p /kaniko/.docker - echo "{\"auths\":{\"$CI_REGISTRY\":{\"auth\":\"$CI_REGISTRY_AUTH\",\"email\":\"$CI_REGISTRY_EMAIL\"}}}" > /kaniko/.docker/config.json script: - - /kaniko/executor --cache=true --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/docker/production/Dockerfile --destination $DOCKER_IMAGE_NAME --build-arg VCS_REF=$CI_VCS_REF --build-arg BUILD_DATE=$CI_JOB_TIMESTAMP + - /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/docker/production/Dockerfile --destination $DOCKER_IMAGE_NAME --build-arg VCS_REF=$CI_VCS_REF --build-arg BUILD_DATE=$CI_JOB_TIMESTAMP build-docker-master: <<: *docker From bfc9dd7ea3d2877d356cf9be7a6ada9fae33c106 Mon Sep 17 00:00:00 2001 From: Thomas Citharel Date: Mon, 9 Aug 2021 08:59:46 +0200 Subject: [PATCH 2/5] Redirect from Login page to Homepage if already logged-in Signed-off-by: Thomas Citharel --- js/src/views/User/Login.vue | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/js/src/views/User/Login.vue b/js/src/views/User/Login.vue index 7d68efe42..4965d98d2 100644 --- a/js/src/views/User/Login.vue +++ b/js/src/views/User/Login.vue @@ -118,7 +118,7 @@ From 9243be24484245e3782b5f389942549048a586d7 Mon Sep 17 00:00:00 2001 From: Thomas Citharel Date: Mon, 9 Aug 2021 14:24:38 +0200 Subject: [PATCH 3/5] Fix apollo cache merge issue Signed-off-by: Thomas Citharel --- js/src/apollo/utils.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/js/src/apollo/utils.ts b/js/src/apollo/utils.ts index d8c3bdb2b..d47680ba5 100644 --- a/js/src/apollo/utils.ts +++ b/js/src/apollo/utils.ts @@ -165,12 +165,13 @@ function doMerge( args: Record | null ): Array { const merged = existing && Array.isArray(existing) ? existing.slice(0) : []; + const previous = incoming && Array.isArray(incoming) ? incoming.slice(0) : []; let res; if (args) { // Assume an page of 1 if args.page omitted. const { page = 1, limit = 10 } = args; - for (let i = 0; i < incoming.length; ++i) { - merged[(page - 1) * limit + i] = incoming[i]; + for (let i = 0; i < previous.length; ++i) { + merged[(page - 1) * limit + i] = previous[i]; } res = merged; } else { @@ -178,7 +179,7 @@ function doMerge( // to receive any arguments, so you might prefer to throw an // exception here, instead of recovering by appending incoming // onto the existing array. - res = [...merged, ...incoming]; + res = [...merged, ...previous]; // eslint-disable-next-line no-underscore-dangle res = uniqBy(res, (elem: any) => elem.__ref); } From 33bf8334fe775cc224c18907d1c6280551c3c977 Mon Sep 17 00:00:00 2001 From: Thomas Citharel Date: Mon, 9 Aug 2021 14:24:54 +0200 Subject: [PATCH 4/5] Allow all rel values for event & post links in descriptions Signed-off-by: Thomas Citharel --- js/src/components/Editor.vue | 4 +++- lib/service/formatter/default_scrubbler.ex | 5 +++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/js/src/components/Editor.vue b/js/src/components/Editor.vue index f0b5a0eac..3e6491b76 100644 --- a/js/src/components/Editor.vue +++ b/js/src/components/Editor.vue @@ -250,7 +250,9 @@ export default class EditorComponent extends Vue { Mention.configure(MentionOptions), CustomImage, Underline, - Link, + Link.configure({ + HTMLAttributes: { target: "_blank", rel: "noopener noreferrer ugc" }, + }), CharacterCount.configure({ limit: this.maxSize, }), diff --git a/lib/service/formatter/default_scrubbler.ex b/lib/service/formatter/default_scrubbler.ex index 001207216..70372d5b8 100644 --- a/lib/service/formatter/default_scrubbler.ex +++ b/lib/service/formatter/default_scrubbler.ex @@ -36,6 +36,11 @@ defmodule Mobilizon.Service.Formatter.DefaultScrubbler do "ugc" ]) + # Rel attributes are separated by spaces + Meta.allow_tag_with_this_attribute_values(:a, "rel", [ + "noopener noreferrer ugc" + ]) + Meta.allow_tag_with_these_attributes(:a, ["name", "title", "target"]) Meta.allow_tag_with_these_attributes(:abbr, ["title"]) From 5f3d1f89dff6cf43d1e6b47aeffc70bd89011274 Mon Sep 17 00:00:00 2001 From: Thomas Citharel Date: Mon, 9 Aug 2021 14:26:11 +0200 Subject: [PATCH 5/5] Allow to add metadata to an event Signed-off-by: Thomas Citharel --- js/public/img/fediverse_monochrome.svg | 1 + js/public/img/peertube_monochrome.svg | 1 + js/public/img/sign_language_monochrome.svg | 1 + .../components/Event/EventMetadataBlock.vue | 20 +- js/src/components/Event/EventMetadataItem.vue | 140 ++++++ js/src/components/Event/EventMetadataList.vue | 206 ++++++++ .../components/Event/EventMetadataSidebar.vue | 450 ++++++++++++++++++ .../Event/Integrations/PeerTube.vue | 55 +++ .../components/Event/Integrations/Twitch.vue | 56 +++ .../components/Event/Integrations/YouTube.vue | 56 +++ js/src/graphql/event.ts | 8 + js/src/i18n/en_US.json | 52 +- js/src/i18n/fr_FR.json | 52 +- js/src/mixins/event.ts | 9 - js/src/services/EventMetadata.ts | 212 +++++++++ js/src/types/enums.ts | 24 + js/src/types/event-metadata.ts | 23 + js/src/types/event.model.ts | 12 + js/src/views/Event/Edit.vue | 11 + js/src/views/Event/Event.vue | 382 +++------------ lib/graphql/schema/event.ex | 23 + lib/mobilizon/events/event.ex | 3 + lib/mobilizon/events/event_metadata.ex | 45 ++ ...20210805142745_add_metadata_for_events.exs | 9 + 24 files changed, 1512 insertions(+), 339 deletions(-) create mode 100644 js/public/img/fediverse_monochrome.svg create mode 100644 js/public/img/peertube_monochrome.svg create mode 100644 js/public/img/sign_language_monochrome.svg create mode 100644 js/src/components/Event/EventMetadataItem.vue create mode 100644 js/src/components/Event/EventMetadataList.vue create mode 100644 js/src/components/Event/EventMetadataSidebar.vue create mode 100644 js/src/components/Event/Integrations/PeerTube.vue create mode 100644 js/src/components/Event/Integrations/Twitch.vue create mode 100644 js/src/components/Event/Integrations/YouTube.vue create mode 100644 js/src/services/EventMetadata.ts create mode 100644 js/src/types/event-metadata.ts create mode 100644 lib/mobilizon/events/event_metadata.ex create mode 100644 priv/repo/migrations/20210805142745_add_metadata_for_events.exs diff --git a/js/public/img/fediverse_monochrome.svg b/js/public/img/fediverse_monochrome.svg new file mode 100644 index 000000000..ada84802c --- /dev/null +++ b/js/public/img/fediverse_monochrome.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/js/public/img/peertube_monochrome.svg b/js/public/img/peertube_monochrome.svg new file mode 100644 index 000000000..90a90f6f6 --- /dev/null +++ b/js/public/img/peertube_monochrome.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/js/public/img/sign_language_monochrome.svg b/js/public/img/sign_language_monochrome.svg new file mode 100644 index 000000000..33256bedf --- /dev/null +++ b/js/public/img/sign_language_monochrome.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/js/src/components/Event/EventMetadataBlock.vue b/js/src/components/Event/EventMetadataBlock.vue index be72e52bf..48ab50fbf 100644 --- a/js/src/components/Event/EventMetadataBlock.vue +++ b/js/src/components/Event/EventMetadataBlock.vue @@ -2,7 +2,18 @@

{{ title }}

- + + + + +

@@ -36,6 +47,13 @@ div.eventMetadataBlock { &.padding-left { padding: 0 20px; + + a { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } } } } diff --git a/js/src/components/Event/EventMetadataItem.vue b/js/src/components/Event/EventMetadataItem.vue new file mode 100644 index 000000000..9a950b860 --- /dev/null +++ b/js/src/components/Event/EventMetadataItem.vue @@ -0,0 +1,140 @@ + + + diff --git a/js/src/components/Event/EventMetadataList.vue b/js/src/components/Event/EventMetadataList.vue new file mode 100644 index 000000000..b5779def6 --- /dev/null +++ b/js/src/components/Event/EventMetadataList.vue @@ -0,0 +1,206 @@ + + diff --git a/js/src/components/Event/EventMetadataSidebar.vue b/js/src/components/Event/EventMetadataSidebar.vue new file mode 100644 index 000000000..af567ee76 --- /dev/null +++ b/js/src/components/Event/EventMetadataSidebar.vue @@ -0,0 +1,450 @@ + + + diff --git a/js/src/components/Event/Integrations/PeerTube.vue b/js/src/components/Event/Integrations/PeerTube.vue new file mode 100644 index 000000000..db4ccaed2 --- /dev/null +++ b/js/src/components/Event/Integrations/PeerTube.vue @@ -0,0 +1,55 @@ + + + diff --git a/js/src/components/Event/Integrations/Twitch.vue b/js/src/components/Event/Integrations/Twitch.vue new file mode 100644 index 000000000..a89f740c4 --- /dev/null +++ b/js/src/components/Event/Integrations/Twitch.vue @@ -0,0 +1,56 @@ + + + diff --git a/js/src/components/Event/Integrations/YouTube.vue b/js/src/components/Event/Integrations/YouTube.vue new file mode 100644 index 000000000..2dc188691 --- /dev/null +++ b/js/src/components/Event/Integrations/YouTube.vue @@ -0,0 +1,56 @@ + + + diff --git a/js/src/graphql/event.ts b/js/src/graphql/event.ts index f563e8a97..600ea4db2 100644 --- a/js/src/graphql/event.ts +++ b/js/src/graphql/event.ts @@ -171,6 +171,12 @@ const FULL_EVENT_FRAGMENT = gql` options { ...EventOptions } + metadata { + key + title + value + type + } } ${ADDRESS_FRAGMENT} ${TAG_FRAGMENT} @@ -326,6 +332,7 @@ export const EDIT_EVENT = gql` $physicalAddress: AddressInput $options: EventOptionsInput $contacts: [Contact] + $metadata: EventMetadataInput ) { updateEvent( eventId: $id @@ -347,6 +354,7 @@ export const EDIT_EVENT = gql` physicalAddress: $physicalAddress options: $options contacts: $contacts + metadata: $metadata ) { ...FullEvent } diff --git a/js/src/i18n/en_US.json b/js/src/i18n/en_US.json index 55450901f..3d054a646 100644 --- a/js/src/i18n/en_US.json +++ b/js/src/i18n/en_US.json @@ -1072,5 +1072,55 @@ "+ Create a post": "+ Create a post", "Edited {relative_time} ago": "Edited {relative_time} ago", "Members-only post": "Members-only post", - "This post is accessible only for members. You have access to it for moderation purposes only because you are an instance moderator.": "This post is accessible only for members. You have access to it for moderation purposes only because you are an instance moderator." + "This post is accessible only for members. You have access to it for moderation purposes only because you are an instance moderator.": "This post is accessible only for members. You have access to it for moderation purposes only because you are an instance moderator.", + "Find or add an element": "Find or add an element", + "e.g. Accessibility, Twitch, PeerTube": "e.g. Accessibility, Twitch, PeerTube", + "Add new…": "Add new…", + "No results for {search}": "No results for {search}", + "Wheelchair accessibility": "Wheelchair accessibility", + "Whether the event is accessible with a wheelchair": "Whether the event is accessible with a wheelchair", + "Not accessible with a wheelchair": "Not accessible with a wheelchair", + "Partially accessible with a wheelchair": "Partially accessible with a wheelchair", + "Fully accessible with a wheelchair": "Fully accessible with a wheelchair", + "YouTube replay": "YouTube replay", + "The URL where the event live can be watched again after it has ended": "The URL where the event live can be watched again after it has ended", + "Twitch replay": "Twitch replay", + "PeerTube replay": "PeerTube replay", + "PeerTube live": "PeerTube live", + "The URL where the event can be watched live": "The URL where the event can be watched live", + "Twitch live": "Twitch live", + "YouTube live": "YouTube live", + "Event metadata": "Event metadata", + "Framadate poll": "Framadate poll", + "The URL of a poll where the choice for the event date is happening": "The URL of a poll where the choice for the event date is happening", + "View account on {hostname} (in a new window)": "View account on {hostname} (in a new window)", + "Twitter account": "Twitter account", + "A twitter account handle to follow for event updates": "A twitter account handle to follow for event updates", + "Fediverse account": "Fediverse account", + "A fediverse account URL to follow for event updates": "A fediverse account URL to follow for event updates", + "Element title": "Element title", + "Element value": "Element value", + "Subtitles": "Subtitles", + "Whether the event live video is subtitled": "Whether the event live video is subtitled", + "The event live video contains subtitles": "The event live video contains subtitles", + "The event live video does not contain subtitles": "The event live video does not contain subtitles", + "Sign Language": "Sign Language", + "Whether the event is interpreted in sign language": "Whether the event is interpreted in sign language", + "The event has a sign language interpreter": "The event has a sign language interpreter", + "The event hasn't got a sign language interpreter": "The event hasn't got a sign language interpreter", + "Online ticketing": "Online ticketing", + "An URL to an external ticketing platform": "An URL to an external ticketing platform", + "Price sheet": "Price sheet", + "A link to a page presenting the price options": "A link to a page presenting the price options", + "Integrate this event with 3rd-party tools and show metadata for the event.": "Integrate this event with 3rd-party tools and show metadata for the event.", + "This URL doesn't seem to be valid": "This URL doesn't seem to be valid", + "Schedule": "Schedule", + "A link to a page presenting the event schedule": "A link to a page presenting the event schedule", + "Accessibility": "Accessibility", + "Live": "Live", + "Replay": "Replay", + "Tools": "Tools", + "Social": "Social", + "Details": "Details", + "Booking": "Booking" } diff --git a/js/src/i18n/fr_FR.json b/js/src/i18n/fr_FR.json index cc125b8f9..58630776b 100644 --- a/js/src/i18n/fr_FR.json +++ b/js/src/i18n/fr_FR.json @@ -1163,5 +1163,55 @@ "+ Create a post": "+ Créer un billet", "Edited {relative_time} ago": "Édité il y a {relative_time}", "Members-only post": "Billet reservé aux membres", - "This post is accessible only for members. You have access to it for moderation purposes only because you are an instance moderator.": "Ce billet est accessible uniquement aux membres. Vous y avez accès à des fins de modération car vous êtes modérateur⋅ice de l'instance." + "This post is accessible only for members. You have access to it for moderation purposes only because you are an instance moderator.": "Ce billet est accessible uniquement aux membres. Vous y avez accès à des fins de modération car vous êtes modérateur⋅ice de l'instance.", + "Find or add an element": "Trouver ou ajouter un élément", + "e.g. Accessibility, Twitch, PeerTube": "par ex. Accessibilité, Framadate, PeerTube", + "Add new…": "Ajouter un nouvel élément…", + "No results for {search}": "Pas de résultats pour {search}", + "Wheelchair accessibility": "Accessibilité aux fauteuils roulants", + "Whether the event is accessible with a wheelchair": "Si l'événement est accessible avec un fauteuil roulant", + "Not accessible with a wheelchair": "Non accessible avec un fauteuil roulant", + "Partially accessible with a wheelchair": "Partiellement accessible avec un fauteuil roulant", + "Fully accessible with a wheelchair": "Entièrement accessible avec un fauteuil roulant", + "YouTube replay": "Replay sur YouTube", + "The URL where the event live can be watched again after it has ended": "L'URL où le direct de l'événement peut être visionné à nouveau une fois terminé", + "Twitch replay": "Replay sur Twitch", + "PeerTube replay": "Replay sur PeerTube", + "PeerTube live": "Direct sur PeerTube", + "The URL where the event can be watched live": "L'URL où l'événement peut être visionné en direct", + "Twitch live": "Direct sur Twitch", + "YouTube live": "Direct sur YouTube", + "Event metadata": "Métadonnées de l'événement", + "Framadate poll": "Sondage Framadate", + "The URL of a poll where the choice for the event date is happening": "L'URL d'un sondage où la date de l'événement doit être choisie", + "View account on {hostname} (in a new window)": "Voir le compte sur {hostname} (dans une nouvelle fenêtre)", + "Twitter account": "Compte Twitter", + "A twitter account handle to follow for event updates": "Un compte sur Twitter à suivre pour les mises à jour de l'événement", + "Fediverse account": "Compte fediverse", + "A fediverse account URL to follow for event updates": "Un compte sur le fediverse à suivre pour les mises à jour de l'événement", + "Element title": "Titre de l'élement", + "Element value": "Valeur de l'élement", + "Subtitles": "Sous-titres", + "Whether the event live video is subtitled": "Si le direct vidéo de l'événement est sous-titré", + "The event live video contains subtitles": "Le direct vidéo de l'événement contient des sous-titres", + "The event live video does not contain subtitles": "Le direct vidéo de l'événement ne contient pas de sous-titres", + "Sign Language": "Langue des signes", + "Whether the event is interpreted in sign language": "Si l'événement est interprété en langue des signes", + "The event has a sign language interpreter": "L'événement a un interprète en langue des signes", + "The event hasn't got a sign language interpreter": "L'événement n'a pas d'interprète en langue des signes", + "Online ticketing": "Billetterie en ligne", + "An URL to an external ticketing platform": "Une URL vers une plateforme de billetterie externe", + "Price sheet": "Feuille des prix", + "A link to a page presenting the price options": "Un lien vers une page présentant la tarification", + "Integrate this event with 3rd-party tools and show metadata for the event.": "Intégrer cet événement avec des outils tiers et afficher des métadonnées pour l'événement.", + "This URL doesn't seem to be valid": "Cette URL ne semble pas être valide", + "Schedule": "Programme", + "A link to a page presenting the event schedule": "Un lien vers une page présentant le programme de l'événement", + "Accessibility": "Accessibilité", + "Live": "Direct", + "Replay": "Rattrapage", + "Tools": "Outils", + "Social": "Social", + "Details": "Détails", + "Booking": "Réservations" } diff --git a/js/src/mixins/event.ts b/js/src/mixins/event.ts index 213db15e3..5f0b28822 100644 --- a/js/src/mixins/event.ts +++ b/js/src/mixins/event.ts @@ -193,13 +193,4 @@ export default class EventMixin extends mixins(Vue) { console.error(error); } } - - // eslint-disable-next-line class-methods-use-this - urlToHostname(url: string): string | null { - try { - return new URL(url).hostname; - } catch (e) { - return null; - } - } } diff --git a/js/src/services/EventMetadata.ts b/js/src/services/EventMetadata.ts new file mode 100644 index 000000000..a6b899d85 --- /dev/null +++ b/js/src/services/EventMetadata.ts @@ -0,0 +1,212 @@ +import { + EventMetadataType, + EventMetadataKeyType, + EventMetadataCategories, +} from "@/types/enums"; +import { IEventMetadataDescription } from "@/types/event-metadata"; +import { i18n } from "@/utils/i18n"; + +export const eventMetaDataList: IEventMetadataDescription[] = [ + { + icon: "wheelchair-accessibility", + key: "mz:accessibility:wheelchairAccessible", + label: i18n.t("Wheelchair accessibility") as string, + description: i18n.t( + "Whether the event is accessible with a wheelchair" + ) as string, + value: "", + type: EventMetadataType.STRING, + keyType: EventMetadataKeyType.CHOICE, + choices: { + no: i18n.t("Not accessible with a wheelchair") as string, + partially: i18n.t("Partially accessible with a wheelchair") as string, + fully: i18n.t("Fully accessible with a wheelchair") as string, + }, + category: EventMetadataCategories.ACCESSIBILITY, + }, + { + icon: "subtitles", + key: "mz:accessibility:live:subtitle", + label: i18n.t("Subtitles") as string, + description: i18n.t("Whether the event live video is subtitled") as string, + value: "", + type: EventMetadataType.BOOLEAN, + keyType: EventMetadataKeyType.PLAIN, + choices: { + true: i18n.t("The event live video contains subtitles") as string, + false: i18n.t( + "The event live video does not contain subtitles" + ) as string, + }, + category: EventMetadataCategories.ACCESSIBILITY, + }, + { + icon: "mz:icon:sign_language", + key: "mz:accessibility:live:sign_language", + label: i18n.t("Sign Language") as string, + description: i18n.t( + "Whether the event is interpreted in sign language" + ) as string, + value: "", + type: EventMetadataType.BOOLEAN, + keyType: EventMetadataKeyType.PLAIN, + choices: { + true: i18n.t("The event has a sign language interpreter") as string, + false: i18n.t( + "The event hasn't got a sign language interpreter" + ) as string, + }, + category: EventMetadataCategories.ACCESSIBILITY, + }, + { + icon: "youtube", + key: "mz:replay:youtube:url", + label: i18n.t("YouTube replay") as string, + description: i18n.t( + "The URL where the event live can be watched again after it has ended" + ) as string, + value: "", + type: EventMetadataType.STRING, + keyType: EventMetadataKeyType.URL, + pattern: + /http(?:s?):\/\/(?:www\.)?youtu(?:be\.com\/watch\?v=|\.be\/)([\w\-_]*)(&(amp;)?‌[\w?‌=]*)?/, + category: EventMetadataCategories.REPLAY, + }, + // { + // icon: "twitch", + // key: "mz:replay:twitch:url", + // label: i18n.t("Twitch replay") as string, + // description: i18n.t( + // "The URL where the event live can be watched again after it has ended" + // ) as string, + // value: "", + // type: EventMetadataType.STRING, + // }, + { + icon: "mz:icon:peertube", + key: "mz:replay:peertube:url", + label: i18n.t("PeerTube replay") as string, + description: i18n.t( + "The URL where the event live can be watched again after it has ended" + ) as string, + value: "", + type: EventMetadataType.STRING, + keyType: EventMetadataKeyType.URL, + pattern: /^https?:\/\/([^/]+)\/(?:videos\/(?:watch|embed)|w)\/([^/]+)$/, + category: EventMetadataCategories.REPLAY, + }, + { + icon: "mz:icon:peertube", + key: "mz:live:peertube:url", + label: i18n.t("PeerTube live") as string, + description: i18n.t( + "The URL where the event can be watched live" + ) as string, + value: "", + type: EventMetadataType.STRING, + keyType: EventMetadataKeyType.URL, + pattern: /^https?:\/\/([^/]+)\/(?:videos\/(?:watch|embed)|w)\/([^/]+)$/, + category: EventMetadataCategories.LIVE, + }, + { + icon: "twitch", + key: "mz:live:twitch:url", + label: i18n.t("Twitch live") as string, + description: i18n.t( + "The URL where the event can be watched live" + ) as string, + value: "", + type: EventMetadataType.STRING, + keyType: EventMetadataKeyType.URL, + placeholder: "https://www.twitch.tv/", + pattern: /^(?:https?:\/\/)?(?:www\.|go\.)?twitch\.tv\/([a-z0-9_]+)($|\?)/, + category: EventMetadataCategories.LIVE, + }, + { + icon: "youtube", + key: "mz:live:youtube:url", + label: i18n.t("YouTube live") as string, + description: i18n.t( + "The URL where the event can be watched live" + ) as string, + value: "", + type: EventMetadataType.STRING, + keyType: EventMetadataKeyType.URL, + pattern: + /http(?:s?):\/\/(?:www\.)?youtu(?:be\.com\/watch\?v=|\.be\/)([\w\-_]*)(&(amp;)?‌[\w?‌=]*)?/, + category: EventMetadataCategories.LIVE, + }, + { + icon: "calendar-check", + key: "mz:poll:framadate:url", + label: i18n.t("Framadate poll") as string, + description: i18n.t( + "The URL of a poll where the choice for the event date is happening" + ) as string, + value: "", + placeholder: "https://framadate.org/", + type: EventMetadataType.STRING, + keyType: EventMetadataKeyType.URL, + category: EventMetadataCategories.TOOLS, + }, + { + icon: "twitter", + key: "mz:social:twitter:account", + label: i18n.t("Twitter account") as string, + description: i18n.t( + "A twitter account handle to follow for event updates" + ) as string, + value: "", + placeholder: "@JoinMobilizon", + type: EventMetadataType.STRING, + keyType: EventMetadataKeyType.HANDLE, + category: EventMetadataCategories.SOCIAL, + }, + { + icon: "mz:icon:fediverse", + key: "mz:social:fediverse:account_url", + label: i18n.t("Fediverse account") as string, + description: i18n.t( + "A fediverse account URL to follow for event updates" + ) as string, + value: "", + placeholder: "https://framapiaf.org/@mobilizon", + type: EventMetadataType.STRING, + keyType: EventMetadataKeyType.URL, + category: EventMetadataCategories.SOCIAL, + }, + { + icon: "ticket-confirmation", + key: "mz:ticket:external_url", + label: i18n.t("Online ticketing") as string, + description: i18n.t("An URL to an external ticketing platform") as string, + value: "", + type: EventMetadataType.STRING, + keyType: EventMetadataKeyType.URL, + category: EventMetadataCategories.BOOKING, + }, + { + icon: "cash", + key: "mz:ticket:price_url", + label: i18n.t("Price sheet") as string, + description: i18n.t( + "A link to a page presenting the price options" + ) as string, + value: "", + type: EventMetadataType.STRING, + keyType: EventMetadataKeyType.URL, + category: EventMetadataCategories.DETAILS, + }, + { + icon: "calendar-text", + key: "mz:schedule_url", + label: i18n.t("Schedule") as string, + description: i18n.t( + "A link to a page presenting the event schedule" + ) as string, + value: "", + type: EventMetadataType.STRING, + keyType: EventMetadataKeyType.URL, + category: EventMetadataCategories.DETAILS, + }, +]; diff --git a/js/src/types/enums.ts b/js/src/types/enums.ts index f05e5262c..b5c738b7c 100644 --- a/js/src/types/enums.ts +++ b/js/src/types/enums.ts @@ -251,3 +251,27 @@ export enum SortDirection { ASC = "ASC", DESC = "DESC", } + +export enum EventMetadataType { + STRING = "STRING", + INTEGER = "INTEGER", + FLOAT = "FLOAT", + BOOLEAN = "BOOLEAN", +} + +export enum EventMetadataKeyType { + PLAIN = "PLAIN", + URL = "URL", + CHOICE = "CHOICE", + HANDLE = "HANDLE", +} + +export enum EventMetadataCategories { + ACCESSIBILITY = "ACCESSIBILITY", + LIVE = "LIVE", + REPLAY = "REPLAY", + SOCIAL = "SOCIAL", + TOOLS = "TOOLS", + DETAILS = "DETAILS", + BOOKING = "BOOKING", +} diff --git a/js/src/types/event-metadata.ts b/js/src/types/event-metadata.ts new file mode 100644 index 000000000..776ed7fd7 --- /dev/null +++ b/js/src/types/event-metadata.ts @@ -0,0 +1,23 @@ +import { + EventMetadataCategories, + EventMetadataKeyType, + EventMetadataType, +} from "./enums"; + +export interface IEventMetadata { + key: string; + title?: string; + value: string; + type: EventMetadataType; +} + +export interface IEventMetadataDescription extends IEventMetadata { + icon?: string; + placeholder?: string; + description: string; + choices?: Record; + keyType: EventMetadataKeyType; + pattern?: RegExp; + label: string; + category: EventMetadataCategories; +} diff --git a/js/src/types/event.model.ts b/js/src/types/event.model.ts index 3dcf496e0..eb3c02469 100644 --- a/js/src/types/event.model.ts +++ b/js/src/types/event.model.ts @@ -10,6 +10,7 @@ import type { IParticipant } from "./participant.model"; import { EventOptions } from "./event-options.model"; import type { IEventOptions } from "./event-options.model"; import { EventJoinOptions, EventStatus, EventVisibility } from "./enums"; +import { IEventMetadata } from "./event-metadata"; export interface IEventCardOptions { hideDate: boolean; @@ -49,6 +50,7 @@ interface IEventEditJSON { tags: string[]; options: IEventOptions; contacts: { id?: string }[]; + metadata: IEventMetadata[]; } export interface IEvent { @@ -84,6 +86,7 @@ export interface IEvent { tags: ITag[]; options: IEventOptions; + metadata: IEventMetadata[]; contacts: IActor[]; toEditJSON(): IEventEditJSON; @@ -153,6 +156,8 @@ export class EventModel implements IEvent { options: IEventOptions = new EventOptions(); + metadata: IEventMetadata[] = []; + constructor(hash?: IEvent) { if (!hash) return; @@ -193,6 +198,7 @@ export class EventModel implements IEvent { this.contacts = hash.contacts; this.tags = hash.tags; + this.metadata = hash.metadata; if (hash.options) this.options = hash.options; } @@ -212,6 +218,12 @@ export class EventModel implements IEvent { phoneAddress: this.phoneAddress, physicalAddress: this.removeTypeName(this.physicalAddress), options: this.removeTypeName(this.options), + metadata: this.metadata.map(({ key, value, type, title }) => ({ + key, + value, + type, + title, + })), attributedToId: this.attributedTo && this.attributedTo.id ? this.attributedTo.id : null, contacts: this.contacts.map(({ id }) => ({ diff --git a/js/src/views/Event/Edit.vue b/js/src/views/Event/Edit.vue index 9144809a9..eaacac57b 100644 --- a/js/src/views/Event/Edit.vue +++ b/js/src/views/Event/Edit.vue @@ -122,6 +122,15 @@

+ {{ $t("Event metadata") }} +

+ {{ + $t( + "Integrate this event with 3rd-party tools and show metadata for the event." + ) + }} +

+ {{ $t("Who can view this event and participate") }}
@@ -408,6 +315,14 @@ />
+
+ +
{{ $t("Comments") }} @@ -531,80 +446,6 @@
- - -
@@ -618,8 +459,6 @@ import { EventVisibility, MemberRole, ParticipantRole, - RoutingTransportationType, - RoutingType, } from "@/types/enums"; import { EVENT_PERSON_PARTICIPATION, @@ -636,7 +475,6 @@ import { IActor, IPerson, Person, usernameWithDomain } from "../../types/actor"; import { GRAPHQL_API_ENDPOINT } from "../../api/_entrypoint"; import DateCalendarIcon from "../../components/Event/DateCalendarIcon.vue"; import EventCard from "../../components/Event/EventCard.vue"; -import EventFullDate from "../../components/Event/EventFullDate.vue"; import ReportModal from "../../components/Report/ReportModal.vue"; import { IReport } from "../../types/report.model"; import { CREATE_REPORT } from "../../graphql/report"; @@ -644,7 +482,6 @@ import EventMixin from "../../mixins/event"; import IdentityPicker from "../Account/IdentityPicker.vue"; import ParticipationSection from "../../components/Participation/ParticipationSection.vue"; import RouteName from "../../router/name"; -import { Address } from "../../types/address.model"; import CommentTree from "../../components/Comment/CommentTree.vue"; import "intersection-observer"; import { CONFIG } from "../../graphql/config"; @@ -657,19 +494,18 @@ import { import { IConfig } from "../../types/config.model"; import Subtitle from "../../components/Utils/Subtitle.vue"; import Tag from "../../components/Tag.vue"; -import EventMetadataBlock from "../../components/Event/EventMetadataBlock.vue"; +import EventMetadataSidebar from "../../components/Event/EventMetadataSidebar.vue"; import EventBanner from "../../components/Event/EventBanner.vue"; -import ActorCard from "../../components/Account/ActorCard.vue"; import PopoverActorCard from "../../components/Account/PopoverActorCard.vue"; import { IParticipant } from "../../types/participant.model"; import { ApolloCache, FetchResult } from "@apollo/client/core"; +import { IEventMetadataDescription } from "@/types/event-metadata"; +import { eventMetaDataList } from "../../services/EventMetadata"; // noinspection TypeScriptValidateTypes @Component({ components: { - EventMetadataBlock, Subtitle, - EventFullDate, EventCard, BIcon, DateCalendarIcon, @@ -678,15 +514,25 @@ import { ApolloCache, FetchResult } from "@apollo/client/core"; ParticipationSection, CommentTree, Tag, - ActorCard, PopoverActorCard, EventBanner, - "map-leaflet": () => - import(/* webpackChunkName: "map" */ "../../components/Map.vue"), + EventMetadataSidebar, ShareEventModal: () => import( /* webpackChunkName: "shareEventModal" */ "../../components/Event/ShareEventModal.vue" ), + "integration-twitch": () => + import( + /* webpackChunkName: "twitchIntegration" */ "../../components/Event/Integrations/Twitch.vue" + ), + "integration-peertube": () => + import( + /* webpackChunkName: "PeerTubeIntegration" */ "../../components/Event/Integrations/PeerTube.vue" + ), + "integration-youtube": () => + import( + /* webpackChunkName: "YouTubeIntegration" */ "../../components/Event/Integrations/YouTube.vue" + ), }, apollo: { event: { @@ -783,8 +629,6 @@ export default class Event extends EventMixin { oldParticipationRole!: string; - showMap = false; - isReportModalActive = false; isShareModalActive = false; @@ -813,65 +657,6 @@ export default class Event extends EventMixin { messageForConfirmation = ""; - RoutingParamType = { - [RoutingType.OPENSTREETMAP]: { - [RoutingTransportationType.FOOT]: "engine=fossgis_osrm_foot", - [RoutingTransportationType.BIKE]: "engine=fossgis_osrm_bike", - [RoutingTransportationType.TRANSIT]: null, - [RoutingTransportationType.CAR]: "engine=fossgis_osrm_car", - }, - [RoutingType.GOOGLE_MAPS]: { - [RoutingTransportationType.FOOT]: "dirflg=w", - [RoutingTransportationType.BIKE]: "dirflg=b", - [RoutingTransportationType.TRANSIT]: "dirflg=r", - [RoutingTransportationType.CAR]: "driving", - }, - }; - - makeNavigationPath( - transportationType: RoutingTransportationType - ): string | undefined { - const geometry = this.physicalAddress?.geom; - if (geometry) { - const routingType = this.config.maps.routing.type; - /** - * build urls to routing map - */ - if (!this.RoutingParamType[routingType][transportationType]) { - return; - } - - const urlGeometry = geometry.split(";").reverse().join(","); - - switch (routingType) { - case RoutingType.GOOGLE_MAPS: - return `https://maps.google.com/?saddr=Current+Location&daddr=${urlGeometry}&${this.RoutingParamType[routingType][transportationType]}`; - case RoutingType.OPENSTREETMAP: - default: { - const bboxX = geometry.split(";").reverse()[0]; - const bboxY = geometry.split(";").reverse()[1]; - return `https://www.openstreetmap.org/directions?from=&to=${urlGeometry}&${this.RoutingParamType[routingType][transportationType]}#map=14/${bboxX}/${bboxY}`; - } - } - } - } - - get addressLinkToRouteByCar(): undefined | string { - return this.makeNavigationPath(RoutingTransportationType.CAR); - } - - get addressLinkToRouteByBike(): undefined | string { - return this.makeNavigationPath(RoutingTransportationType.BIKE); - } - - get addressLinkToRouteByFeet(): undefined | string { - return this.makeNavigationPath(RoutingTransportationType.FOOT); - } - - get addressLinkToRouteByTransit(): undefined | string { - return this.makeNavigationPath(RoutingTransportationType.TRANSIT); - } - get eventTitle(): undefined | string { if (!this.event) return undefined; return this.event.title; @@ -1262,12 +1047,6 @@ export default class Event extends EventMixin { ); } - get physicalAddress(): Address | null { - if (!this.event.physicalAddress) return null; - - return new Address(this.event.physicalAddress); - } - async anonymousParticipationConfirmed(): Promise { return isParticipatingInThisEvent(this.uuid); } @@ -1302,6 +1081,32 @@ export default class Event extends EventMixin { } return null; } + + metadataToComponent: Record = { + "mz:live:twitch:url": "integration-twitch", + "mz:live:peertube:url": "integration-peertube", + "mz:live:youtube:url": "integration-youtube", + }; + + get integrations(): Record { + return this.event.metadata + .map((val) => { + const def = eventMetaDataList.find((dat) => dat.key === val.key); + return { + ...def, + ...val, + }; + }) + .reduce((acc: Record, metadata) => { + const component = this.metadataToComponent[metadata.key]; + if (component !== undefined) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + acc[component] = metadata; + } + return acc; + }, {}); + } }