From c91f3c618b54afe8ab38b6caa52a3e2c1d785b2f Mon Sep 17 00:00:00 2001 From: Thomas Citharel Date: Fri, 15 Nov 2019 18:36:47 +0100 Subject: [PATCH] Introduce comments below events Also add tomstones Signed-off-by: Thomas Citharel --- config/dev.exs | 2 +- js/package.json | 4 +- js/src/assets/undraw_just_saying.svg | 1 + js/src/components/Comment/Comment.vue | 351 ++++++ js/src/components/Comment/CommentTree.vue | 328 +++++ js/src/components/Editor.vue | 88 +- js/src/components/Event/DateTimePicker.vue | 8 +- js/src/components/Event/EventCard.vue | 2 +- js/src/components/Report/ReportCard.vue | 11 +- js/src/components/Report/ReportModal.vue | 39 +- js/src/filters/index.ts | 2 + js/src/filters/utils.ts | 9 + js/src/graphql/comment.ts | 83 ++ js/src/graphql/event.ts | 2 + js/src/graphql/report.ts | 25 +- js/src/i18n/en_US.json | 14 +- js/src/i18n/fr_FR.json | 17 +- js/src/main.ts | 2 + js/src/mixins/event.ts | 3 +- js/src/router/index.ts | 10 +- js/src/shims-vue.d.ts | 4 + js/src/types/comment.model.ts | 50 + js/src/types/event.model.ts | 5 +- js/src/types/report.model.ts | 3 + js/src/typings/tiptap.d.ts | 3 + js/src/variables.scss | 3 - js/src/views/Account/IdentityPicker.vue | 3 +- .../views/Account/IdentityPickerWrapper.vue | 30 +- js/src/views/Event/Edit.vue | 38 +- js/src/views/Event/Event.vue | 72 +- js/src/views/Moderation/Report.vue | 176 +-- js/src/views/Moderation/ReportList.vue | 4 +- js/yarn.lock | 1051 +++++++++-------- lib/mobilizon/events/comment.ex | 68 +- lib/mobilizon/events/event.ex | 2 + lib/mobilizon/events/events.ex | 85 +- lib/mobilizon/reports/report.ex | 33 +- lib/mobilizon/tombstone.ex | 45 + lib/mobilizon_web/api/comments.ex | 13 +- lib/mobilizon_web/api/reports.ex | 53 +- lib/mobilizon_web/resolvers/admin.ex | 11 +- lib/mobilizon_web/resolvers/comment.ex | 58 +- lib/mobilizon_web/resolvers/report.ex | 4 +- lib/mobilizon_web/router.ex | 6 +- lib/mobilizon_web/schema.ex | 3 +- lib/mobilizon_web/schema/admin.ex | 6 +- lib/mobilizon_web/schema/comment.ex | 34 +- lib/mobilizon_web/schema/event.ex | 6 +- lib/mobilizon_web/schema/report.ex | 4 +- lib/service/activity_pub/activity_pub.ex | 82 +- lib/service/activity_pub/converter/comment.ex | 17 +- lib/service/activity_pub/converter/flag.ex | 24 +- lib/service/activity_pub/transmogrifier.ex | 14 +- lib/service/activity_pub/visibility.ex | 2 + lib/service/formatter/formatter.ex | 7 +- lib/service/workers/build_search_worker.ex | 32 +- mix.exs | 1 - ...20181108165151_fix_comments_references.exs | 4 +- ...0191114155534_cascade_comment_deletion.exs | 23 + ...91122154018_add_deleted_at_on_comments.exs | 21 + ...91126173510_add_local_field_to_reports.exs | 19 + .../20191127163737_create_tombstones.exs | 14 + schema.graphql | 22 +- test/mobilizon/events/events_test.exs | 2 +- .../activity_pub/activity_pub_test.exs | 3 +- .../activity_pub/transmogrifier_test.exs | 60 +- .../service/formatter/formatter_test.exs | 17 +- test/mobilizon_web/api/report_test.exs | 42 +- .../resolvers/comment_resolver_test.exs | 231 +++- .../resolvers/report_resolver_test.exs | 8 +- test/support/factory.ex | 2 +- 71 files changed, 2642 insertions(+), 879 deletions(-) create mode 100644 js/src/assets/undraw_just_saying.svg create mode 100644 js/src/components/Comment/Comment.vue create mode 100644 js/src/components/Comment/CommentTree.vue create mode 100644 js/src/filters/utils.ts create mode 100644 js/src/graphql/comment.ts create mode 100644 js/src/types/comment.model.ts create mode 100644 lib/mobilizon/tombstone.ex create mode 100644 priv/repo/migrations/20191114155534_cascade_comment_deletion.exs create mode 100644 priv/repo/migrations/20191122154018_add_deleted_at_on_comments.exs create mode 100644 priv/repo/migrations/20191126173510_add_local_field_to_reports.exs create mode 100644 priv/repo/migrations/20191127163737_create_tombstones.exs diff --git a/config/dev.exs b/config/dev.exs index 8b9d4f621..ac5f5a921 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -52,7 +52,7 @@ config :mobilizon, MobilizonWeb.Endpoint, # Do not include metadata nor timestamps in development logs config :logger, :console, format: "[$level] $message\n", level: :debug -config :mobilizon, Mobilizon.Service.Geospatial, service: Mobilizon.Service.Geospatial.Mimirsbrunn +config :mobilizon, Mobilizon.Service.Geospatial, service: Mobilizon.Service.Geospatial.Nominatim # Set a higher stacktrace during development. Avoid configuring such # in production as building large stacktraces may be expensive. diff --git a/js/package.json b/js/package.json index 9d6781b17..583f38f95 100644 --- a/js/package.json +++ b/js/package.json @@ -25,12 +25,13 @@ "graphql": "^14.5.8", "graphql-tag": "^2.10.1", "intersection-observer": "^0.7.0", + "javascript-time-ago": "^2.0.4", "leaflet": "^1.4.0", "leaflet.locatecontrol": "^0.68.0", "lodash": "^4.17.11", "ngeohash": "^0.6.3", "register-service-worker": "^1.6.2", - "tippy.js": "^5.0.2", + "tippy.js": "4.3.5", "tiptap": "^1.26.0", "tiptap-extensions": "^1.28.0", "vue": "^2.6.10", @@ -40,6 +41,7 @@ "vue-meta": "^2.3.1", "vue-property-decorator": "^8.1.0", "vue-router": "^3.0.6", + "vue-scrollto": "^2.17.1", "vue2-leaflet": "^2.0.3" }, "devDependencies": { diff --git a/js/src/assets/undraw_just_saying.svg b/js/src/assets/undraw_just_saying.svg new file mode 100644 index 000000000..a9ceaf767 --- /dev/null +++ b/js/src/assets/undraw_just_saying.svg @@ -0,0 +1 @@ +just_saying \ No newline at end of file diff --git a/js/src/components/Comment/Comment.vue b/js/src/components/Comment/Comment.vue new file mode 100644 index 000000000..ce31e8ff2 --- /dev/null +++ b/js/src/components/Comment/Comment.vue @@ -0,0 +1,351 @@ + + + diff --git a/js/src/components/Comment/CommentTree.vue b/js/src/components/Comment/CommentTree.vue new file mode 100644 index 000000000..df20bfec4 --- /dev/null +++ b/js/src/components/Comment/CommentTree.vue @@ -0,0 +1,328 @@ + + + + + diff --git a/js/src/components/Editor.vue b/js/src/components/Editor.vue index f44469bd8..3bc2ca9c6 100644 --- a/js/src/components/Editor.vue +++ b/js/src/components/Editor.vue @@ -1,7 +1,7 @@
- No actors found + {{ $t('No actors found') }}
@@ -165,11 +192,12 @@ import { } from 'tiptap-extensions'; import tippy, { Instance } from 'tippy.js'; import { SEARCH_PERSONS } from '@/graphql/search'; -import { IActor, IPerson } from '@/types/actor'; +import { Actor, IActor, IPerson } from '@/types/actor'; import Image from '@/components/Editor/Image'; import { UPLOAD_PICTURE } from '@/graphql/upload'; import { listenFileUpload } from '@/utils/upload'; import { CURRENT_ACTOR_CLIENT } from '@/graphql/actor'; +import { IComment } from '@/types/comment.model'; @Component({ components: { EditorContent, EditorMenuBar, EditorMenuBubble }, @@ -181,6 +209,7 @@ import { CURRENT_ACTOR_CLIENT } from '@/graphql/actor'; }) export default class EditorComponent extends Vue { @Prop({ required: true }) value!: string; + @Prop({ required: false, default: 'description' }) mode!: string; currentActor!: IPerson; @@ -192,9 +221,17 @@ export default class EditorComponent extends Vue { query!: string|null; filteredActors: IActor[] = []; suggestionRange!: object|null; - navigatedUserIndex: number = 0; + navigatedActorIndex: number = 0; popup!: Instance|null; + get isDescriptionMode() { + return this.mode === 'description'; + } + + get isCommentMode() { + return this.mode === 'comment'; + } + get hasResults() { return this.filteredActors.length; } @@ -232,7 +269,7 @@ export default class EditorComponent extends Vue { this.query = query; this.filteredActors = items; this.suggestionRange = range; - this.navigatedUserIndex = 0; + this.navigatedActorIndex = 0; this.renderPopup(virtualNode); }, @@ -244,7 +281,7 @@ export default class EditorComponent extends Vue { this.query = null; this.filteredActors = []; this.suggestionRange = null; - this.navigatedUserIndex = 0; + this.navigatedActorIndex = 0; this.destroyPopup(); }, @@ -335,7 +372,7 @@ export default class EditorComponent extends Vue { } upHandler() { - this.navigatedUserIndex = ((this.navigatedUserIndex + this.filteredActors.length) - 1) % this.filteredActors.length; + this.navigatedActorIndex = ((this.navigatedActorIndex + this.filteredActors.length) - 1) % this.filteredActors.length; } /** @@ -343,11 +380,11 @@ export default class EditorComponent extends Vue { * if it's the last item, navigate to the first one */ downHandler() { - this.navigatedUserIndex = (this.navigatedUserIndex + 1) % this.filteredActors.length; + this.navigatedActorIndex = (this.navigatedActorIndex + 1) % this.filteredActors.length; } enterHandler() { - const actor = this.filteredActors[this.navigatedUserIndex]; + const actor = this.filteredActors[this.navigatedActorIndex]; if (actor) { this.selectActor(actor); } @@ -359,17 +396,26 @@ export default class EditorComponent extends Vue { * @param actor IActor */ selectActor(actor: IActor) { + const actorModel = new Actor(actor); this.insertMention({ range: this.suggestionRange, attrs: { - id: actor.id, - label: actor.name, + id: actorModel.id, + label: actorModel.usernameWithDomain().substring(1), // usernameWithDomain returns with a @ prefix and tiptap adds one itself }, }); if (!this.editor) return; this.editor.focus(); } + replyToComment(comment: IComment) { + console.log('called replyToComment', comment); + const actorModel = new Actor(comment.actor); + if (!this.editor) return; + this.editor.commands.mention({ id: actorModel.id, label: actorModel.usernameWithDomain().substring(1) }); + this.editor.focus(); + } + /** * renders a popup with suggestions * tiptap provides a virtualNode object for using popper.js (or tippy.js) for popups @@ -443,6 +489,8 @@ export default class EditorComponent extends Vue { } diff --git a/js/src/components/Event/EventCard.vue b/js/src/components/Event/EventCard.vue index 8ad8357fe..c17a2be64 100644 --- a/js/src/components/Event/EventCard.vue +++ b/js/src/components/Event/EventCard.vue @@ -108,7 +108,7 @@ export default class EventCard extends Vue { } - \ No newline at end of file + + .modal-card-body { + .media-content { + .box { + .media { + padding-top: 0; + border-top: none; + } + } + + & > p { + margin-bottom: 2rem; + } + } + } + diff --git a/js/src/filters/index.ts b/js/src/filters/index.ts index 89a7267a5..4f106c2e5 100644 --- a/js/src/filters/index.ts +++ b/js/src/filters/index.ts @@ -1,9 +1,11 @@ import { formatDateString, formatTimeString, formatDateTimeString } from './datetime'; +import { nl2br } from '@/filters/utils'; export default { install(vue) { vue.filter('formatDateString', formatDateString); vue.filter('formatTimeString', formatTimeString); vue.filter('formatDateTimeString', formatDateTimeString); + vue.filter('nl2br', nl2br); }, }; diff --git a/js/src/filters/utils.ts b/js/src/filters/utils.ts new file mode 100644 index 000000000..4b81b878b --- /dev/null +++ b/js/src/filters/utils.ts @@ -0,0 +1,9 @@ +/** + * New Line to
+ * + * @param {string} str Input text + * @return {string} Filtered text + */ +export function nl2br(str: String): String { + return `${str}`.replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, '$1
'); +} diff --git a/js/src/graphql/comment.ts b/js/src/graphql/comment.ts new file mode 100644 index 000000000..c2e171ba1 --- /dev/null +++ b/js/src/graphql/comment.ts @@ -0,0 +1,83 @@ +import gql from 'graphql-tag'; + +export const COMMENT_FIELDS_FRAGMENT_NAME = 'CommentFields'; +export const COMMENT_FIELDS_FRAGMENT = gql` + fragment CommentFields on Comment { + id, + uuid, + url, + text, + visibility, + actor { + avatar { + url + }, + id, + preferredUsername, + name + }, + totalReplies, + updatedAt, + deletedAt + }, +`; + +export const COMMENT_RECURSIVE_FRAGMENT = gql` + fragment CommentRecursive on Comment { + ...CommentFields + inReplyToComment { + ...CommentFields + }, + originComment { + ...CommentFields + }, + replies { + ...CommentFields + replies { + ...CommentFields + } + }, + }, + ${COMMENT_FIELDS_FRAGMENT} +`; + +export const FETCH_THREAD_REPLIES = gql` + query($threadId: ID!) { + thread(id: $threadId) { + ...CommentRecursive + } + } + ${COMMENT_RECURSIVE_FRAGMENT} +`; + + +export const COMMENTS_THREADS = gql` + query($eventUUID: UUID!) { + event(uuid: $eventUUID) { + id, + uuid, + comments { + ...CommentFields, + } + } + } + ${COMMENT_RECURSIVE_FRAGMENT} +`; + +export const CREATE_COMMENT_FROM_EVENT = gql` + mutation CreateCommentFromEvent($eventId: ID!, $actorId: ID!, $text: String!, $inReplyToCommentId: ID) { + createComment(eventId: $eventId, actorId: $actorId, text: $text, inReplyToCommentId: $inReplyToCommentId) { + ...CommentRecursive + } + } + ${COMMENT_RECURSIVE_FRAGMENT} +`; + + +export const DELETE_COMMENT = gql` + mutation DeleteComment($commentId: ID!, $actorId: ID!) { + deleteComment(commentId: $commentId, actorId: $actorId) { + id + } + } +`; diff --git a/js/src/graphql/event.ts b/js/src/graphql/event.ts index 72009bd33..0722c65ef 100644 --- a/js/src/graphql/event.ts +++ b/js/src/graphql/event.ts @@ -1,4 +1,5 @@ import gql from 'graphql-tag'; +import { COMMENT_FIELDS_FRAGMENT } from '@/graphql/comment'; const participantQuery = ` role, @@ -135,6 +136,7 @@ export const FETCH_EVENT = gql` } } } + ${COMMENT_FIELDS_FRAGMENT} `; export const FETCH_EVENTS = gql` diff --git a/js/src/graphql/report.ts b/js/src/graphql/report.ts index 46b72fae3..8c0557d0d 100644 --- a/js/src/graphql/report.ts +++ b/js/src/graphql/report.ts @@ -29,7 +29,8 @@ export const REPORTS = gql` url } }, - status + status, + content } } `; @@ -63,10 +64,23 @@ const REPORT_FRAGMENT = gql` url } }, + comments { + id, + text, + actor { + id, + preferredUsername, + name, + avatar { + url + } + } + } notes { id, content moderator { + id, preferredUsername, name, avatar { @@ -94,11 +108,12 @@ export const REPORT = gql` export const CREATE_REPORT = gql` mutation CreateReport( $eventId: ID!, - $reporterActorId: ID!, - $reportedActorId: ID!, - $content: String + $reporterId: ID!, + $reportedId: ID!, + $content: String, + $commentsIds: [ID] ) { - createReport(eventId: $eventId, reporterActorId: $reporterActorId, reportedActorId: $reportedActorId, content: $content) { + createReport(eventId: $eventId, reporterId: $reporterId, reportedId: $reportedId, content: $content, commentsIds: $commentsIds) { id } } diff --git a/js/src/i18n/en_US.json b/js/src/i18n/en_US.json index f37ce303d..e37a969cf 100644 --- a/js/src/i18n/en_US.json +++ b/js/src/i18n/en_US.json @@ -40,7 +40,8 @@ "Click to select": "Click to select", "Click to upload": "Click to upload", "Close comments for all (except for admins)": "Close comments for all (except for admins)", - "Comments on the event page": "Comments on the event page", + "Comment from @{username} reported": "Comment from @{username} reported", + "Comments have been closed.": "Comments have been closed.", "Comments": "Comments", "Confirm my particpation": "Confirm my particpation", "Confirmed: Will happen": "Confirmed: Will happen", @@ -117,6 +118,7 @@ "Group {displayName} created": "Group {displayName} created", "Groups": "Groups", "Headline picture": "Headline picture", + "Hide replies": "Hide replies", "I create an identity": "I create an identity", "I participate": "I participate", "I want to approve every participation request": "I want to approve every participation request", @@ -155,7 +157,9 @@ "My identities": "My identities", "Name": "Name", "New password": "New password", + "No actors found": "No actors found", "No address defined": "No address defined", + "No comments yet": "No comments yet", "No end date": "No end date", "No events found": "No events found", "No group found": "No group found", @@ -196,6 +200,8 @@ "Please make sure the address is correct and that the page hasn't been moved.": "Please make sure the address is correct and that the page hasn't been moved.", "Please read the full rules": "Please read the full rules", "Please refresh the page and retry.": "Please refresh the page and retry.", + "Post a comment": "Post a comment", + "Post a reply": "Post a reply", "Postal Code": "Postal Code", "Private event": "Private event", "Private feeds": "Private feeds", @@ -216,6 +222,7 @@ "Reject": "Reject", "Rejected participations": "Rejected participations", "Rejected": "Rejected", + "Report this comment": "Report this comment", "Report this event": "Report this event", "Report": "Report", "Requests": "Requests", @@ -276,6 +283,7 @@ "User accounts and every other data is currently deleted every 48 hours, so you may want to register again.": "User accounts and every other data is currently deleted every 48 hours, so you may want to register again.", "Username": "Username", "Users": "Users", + "View a reply": "|View one reply|View {totalReplies} replies", "View event page": "View event page", "View everything": "View everything", "View page on {hostname} (in a new window)": "View page on {hostname} (in a new window)", @@ -313,6 +321,8 @@ "Your local administrator resumed its policy:": "Your local administrator resumed its policy:", "Your participation has been confirmed": "Your participation has been confirmed", "Your participation has been requested": "Your participation has been requested", + "[This comment has been deleted]": "[This comment has been deleted]", + "[deleted]": "[deleted]", "a decentralised federation protocol": "a decentralised federation protocol", "e.g. 10 Rue Jangot": "e.g. 10 Rue Jangot", "firstDayOfWeek": "0", @@ -330,4 +340,4 @@ "{license} guarantees {respect} of the people who will use it. Since {source}, anyone can audit it, which guarantees its transparency.": "{license} guarantees {respect} of the people who will use it. Since {source}, anyone can audit it, which guarantees its transparency.", "© The Mobilizon Contributors {date} - Made with Elixir, Phoenix, VueJS & with some love and some weeks": "© The Mobilizon Contributors {date} - Made with Elixir, Phoenix, VueJS & with some love and some weeks", "© The OpenStreetMap Contributors": "© The OpenStreetMap Contributors" -} +} \ No newline at end of file diff --git a/js/src/i18n/fr_FR.json b/js/src/i18n/fr_FR.json index e7b23d25b..6eee6eb43 100644 --- a/js/src/i18n/fr_FR.json +++ b/js/src/i18n/fr_FR.json @@ -40,6 +40,8 @@ "Click to select": "Cliquez pour sélectionner", "Click to upload": "Cliquez pour uploader", "Close comments for all (except for admins)": "Fermer les commentaires à tout le monde (excepté les administrateurs)", + "Comment from @{username} reported": "Commentaire de @{username} signalé", + "Comments have been closed.": "Les commentaires sont fermés.", "Comments on the event page": "Commentaires sur la page de l'événement", "Comments": "Commentaires", "Confirm my particpation": "Confirmer ma participation", @@ -59,6 +61,7 @@ "Create": "Créer", "Creator": "Créateur", "Current identity has been changed to {identityName} in order to manage this event.": "L'identité actuelle a été changée à {identityName} pour pouvoir gérer cet événement.", + "Dashboard": "Tableau de bord", "Date and time settings": "Paramètres de date et d'heure", "Date parameters": "Paramètres de date", "Delete event": "Supprimer un événement", @@ -117,6 +120,7 @@ "Group {displayName} created": "Groupe {displayName} créé", "Groups": "Groupes", "Headline picture": "Image à la une", + "Hide replies": "Masquer les réponses", "I create an identity": "Je crée une identité", "I participate": "Je participe", "I want to approve every participation request": "Je veux approuver chaque demande de participation", @@ -155,7 +159,9 @@ "My identities": "Mes identités", "Name": "Nom", "New password": "Nouveau mot de passe", + "No actors found": "Aucun acteur trouvé", "No address defined": "Aucune adresse définie", + "No comments yet": "Pas encore de commentaires", "No end date": "Pas de date de fin", "No events found": "Aucun événement trouvé", "No group found": "Aucun groupe trouvé", @@ -197,6 +203,8 @@ "Please read the full rules": "Merci de lire les règles complètes", "Please refresh the page and retry.": "Merci de rafraîchir la page puis réessayer.", "Please type at least 5 characters": "Merci d'entrer au moins 5 caractères", + "Post a comment": "Ajouter un commentaire", + "Post a reply": "Envoyer une réponse", "Postal Code": "Code postal", "Private event": "Événement privé", "Private feeds": "Flux privés", @@ -217,8 +225,10 @@ "Reject": "Rejetter", "Rejected participations": "Participations rejetées", "Rejected": "Rejetés", + "Report this comment": "Signaler ce commentaire", "Report this event": "Signaler cet événement", - "Report": "Signaler", + "Report": "Signalement", + "Reports": "Signalements", "Requests": "Requêtes", "Resend confirmation email": "Envoyer à nouveau l'email de confirmation", "Reset my password": "Réinitialiser mon mot de passe", @@ -277,6 +287,7 @@ "User accounts and every other data is currently deleted every 48 hours, so you may want to register again.": "Les comptes utilisateurs et toutes les autres données sont actuellement supprimées toutes les 48 heures, donc vous voulez peut-être vous inscrire à nouveau.", "Username": "Pseudo", "Users": "Utilisateurs", + "View a reply": "Aucune réponse | Voir une réponse | Voir {totalReplies} réponses", "View event page": "Voir la page de l'événement", "View everything": "Voir tout", "View page on {hostname} (in a new window)": "Voir la page sur {hostname} (dans une nouvelle fenêtre)", @@ -314,6 +325,8 @@ "Your local administrator resumed its policy:": "Votre administrateur local a résumé sa politique ainsi :", "Your participation has been confirmed": "Votre participation a été confirmée", "Your participation has been requested": "Votre participation a été demandée", + "[This comment has been deleted]": "[Ce commentaire a été supprimé]", + "[deleted]": "[supprimé]", "a decentralised federation protocol": "un protocole de fédération décentralisée", "e.g. 10 Rue Jangot": "par exemple : 10 Rue Jangot", "firstDayOfWeek": "1", @@ -331,4 +344,4 @@ "{license} guarantees {respect} of the people who will use it. Since {source}, anyone can audit it, which guarantees its transparency.": "{license} garantit {respect} des personnes qui l'utiliseront. Puisque {source}, il est publiquement auditable, ce qui garantit sa transparence.", "© The Mobilizon Contributors {date} - Made with Elixir, Phoenix, VueJS & with some love and some weeks": "© Les contributeurs de Mobilizon {date} - Fait avec Elixir, Phoenix, VueJS & et de l'amour et des semaines", "© The OpenStreetMap Contributors": "© Les Contributeur⋅ices OpenStreetMap" -} +} \ No newline at end of file diff --git a/js/src/main.ts b/js/src/main.ts index 27a36e03f..3987364f0 100644 --- a/js/src/main.ts +++ b/js/src/main.ts @@ -3,6 +3,7 @@ import Vue from 'vue'; import Buefy from 'buefy'; import Component from 'vue-class-component'; +import VueScrollTo from 'vue-scrollto'; import App from '@/App.vue'; import router from '@/router'; import { apolloProvider } from './vue-apollo'; @@ -17,6 +18,7 @@ Vue.use(Buefy); Vue.use(NotifierPlugin); Vue.use(filters); Vue.use(VueMeta); +Vue.use(VueScrollTo); // Register the router hooks with their names Component.registerHooks([ diff --git a/js/src/mixins/event.ts b/js/src/mixins/event.ts index f35c3aeac..9d77e3d84 100644 --- a/js/src/mixins/event.ts +++ b/js/src/mixins/event.ts @@ -7,7 +7,8 @@ import { IPerson } from '@/types/actor'; @Component export default class EventMixin extends mixins(Vue) { - async openDeleteEventModal (event: IEvent, currentActor: IPerson) { + + async openDeleteEventModal(event: IEvent, currentActor: IPerson) { const participantsLength = event.participantStats.participant; const prefix = participantsLength ? this.$tc('There are {participants} participants.', event.participantStats.participant, { diff --git a/js/src/router/index.ts b/js/src/router/index.ts index 857b54011..807aea98a 100644 --- a/js/src/router/index.ts +++ b/js/src/router/index.ts @@ -1,5 +1,6 @@ import Vue from 'vue'; import Router from 'vue-router'; +import VueScrollTo from 'vue-scrollto'; import PageNotFound from '@/views/PageNotFound.vue'; import Home from '@/views/Home.vue'; import { UserRouteName, userRoutes } from './user'; @@ -20,13 +21,18 @@ enum GlobalRouteName { SEARCH = 'Search', } -function scrollBehavior(to) { +function scrollBehavior(to, from, savedPosition) { if (to.hash) { + VueScrollTo.scrollTo(to.hash, 700); return { selector: to.hash, - // , offset: { x: 0, y: 10 } + offset: { x: 0, y: 10 }, }; } + if (savedPosition) { + return savedPosition; + } + return { x: 0, y: 0 }; } diff --git a/js/src/shims-vue.d.ts b/js/src/shims-vue.d.ts index 8f6f41026..7f74dd240 100644 --- a/js/src/shims-vue.d.ts +++ b/js/src/shims-vue.d.ts @@ -1,4 +1,8 @@ +import { Vue } from 'vue/types/vue'; + declare module '*.vue' { import Vue from 'vue'; export default Vue; } + +type Refs = Vue['$refs'] & T; diff --git a/js/src/types/comment.model.ts b/js/src/types/comment.model.ts new file mode 100644 index 000000000..bed61c4b5 --- /dev/null +++ b/js/src/types/comment.model.ts @@ -0,0 +1,50 @@ +import { Actor, IActor } from '@/types/actor'; +import { EventModel, IEvent } from '@/types/event.model'; + +export interface IComment { + id?: string; + uuid?: string; + url?: string; + text: string; + actor: IActor; + inReplyToComment?: IComment; + originComment?: IComment; + replies: IComment[]; + event?: IEvent; + updatedAt?: Date; + deletedAt?: Date; + totalReplies: number; +} + +export class CommentModel implements IComment { + actor: IActor = new Actor(); + id?: string; + text: string = ''; + url?: string; + uuid?: string; + inReplyToComment?: IComment = undefined; + originComment?: IComment = undefined; + replies: IComment[] = []; + event?: IEvent = undefined; + updatedAt?: Date = undefined; + deletedAt?: Date = undefined; + totalReplies: number = 0; + + constructor(hash?: IComment) { + if (!hash) return; + + this.id = hash.id; + this.uuid = hash.uuid; + this.url = hash.url; + this.text = hash.text; + this.inReplyToComment = hash.inReplyToComment; + this.originComment = hash.originComment; + this.actor = new Actor(hash.actor); + this.event = new EventModel(hash.event); + this.replies = hash.replies; + this.updatedAt = hash.updatedAt; + this.deletedAt = hash.deletedAt; + this.totalReplies = hash.totalReplies; + } + +} diff --git a/js/src/types/event.model.ts b/js/src/types/event.model.ts index 3c29374d1..6f041aaa4 100644 --- a/js/src/types/event.model.ts +++ b/js/src/types/event.model.ts @@ -2,6 +2,7 @@ import { Actor, IActor, IPerson } from './actor'; import { Address, IAddress } from '@/types/address.model'; import { ITag } from '@/types/tag.model'; import { IPicture } from '@/types/picture.model'; +import { IComment } from '@/types/comment.model'; export enum EventStatus { TENTATIVE = 'TENTATIVE', @@ -129,6 +130,7 @@ export interface IEvent { participants: IParticipant[]; relatedEvents: IEvent[]; + comments: IComment[]; onlineAddress?: string; phoneAddress?: string; @@ -199,9 +201,10 @@ export class EventModel implements IEvent { participants: IParticipant[] = []; relatedEvents: IEvent[] = []; + comments: IComment[] = []; attributedTo = new Actor(); - organizerActor?: IActor; + organizerActor?: IActor = new Actor(); tags: ITag[] = []; options: IEventOptions = new EventOptions(); diff --git a/js/src/types/report.model.ts b/js/src/types/report.model.ts index b1cea059f..69c070853 100644 --- a/js/src/types/report.model.ts +++ b/js/src/types/report.model.ts @@ -1,5 +1,6 @@ import { IActor, IPerson } from '@/types/actor'; import { IEvent } from '@/types/event.model'; +import { IComment } from '@/types/comment.model'; export enum ReportStatusEnum { OPEN = 'OPEN', @@ -12,6 +13,7 @@ export interface IReport extends IActionLogObject { reported: IActor; reporter: IPerson; event?: IEvent; + comments: IComment[]; content: string; notes: IReportNote[]; insertedAt: Date; @@ -36,6 +38,7 @@ export enum ActionLogAction { REPORT_UPDATE_OPENED = 'REPORT_UPDATE_OPENED', REPORT_UPDATE_RESOLVED = 'REPORT_UPDATE_RESOLVED', EVENT_DELETION = 'EVENT_DELETION', + COMMENT_DELETION = 'COMMENT_DELETION', } export interface IActionLog { diff --git a/js/src/typings/tiptap.d.ts b/js/src/typings/tiptap.d.ts index 789eb3a24..f07c9fc9c 100644 --- a/js/src/typings/tiptap.d.ts +++ b/js/src/typings/tiptap.d.ts @@ -2,6 +2,9 @@ declare module 'tiptap' { import Vue from 'vue'; export class Editor { public constructor({}); + public commands: { + mention: Function, + }; public setOptions({}): void; public setContent(content: string): void; diff --git a/js/src/variables.scss b/js/src/variables.scss index 5921c99cc..6493dc980 100644 --- a/js/src/variables.scss +++ b/js/src/variables.scss @@ -32,6 +32,3 @@ $navbar-height: 4rem; // Footer $footer-padding: 3rem 1.5rem 4rem; $footer-background-color: $primary; - -// Card -$card-background-color: $secondary; diff --git a/js/src/views/Account/IdentityPicker.vue b/js/src/views/Account/IdentityPicker.vue index 126b8a777..d0d70edc5 100644 --- a/js/src/views/Account/IdentityPicker.vue +++ b/js/src/views/Account/IdentityPicker.vue @@ -7,7 +7,8 @@