From 49a5725da3bee3dedb08abea4388b95b950b1136 Mon Sep 17 00:00:00 2001 From: Thomas Citharel Date: Tue, 29 Sep 2020 09:53:48 +0200 Subject: [PATCH] Improve and activate groups Signed-off-by: Thomas Citharel --- config/config.exs | 5 +- js/src/App.vue | 2 +- js/src/apollo/utils.ts | 13 - js/src/components/Admin/Followers.vue | 49 +- js/src/components/Admin/Followings.vue | 49 +- js/src/components/Comment/Comment.vue | 17 +- js/src/components/Comment/CommentTree.vue | 123 +-- js/src/components/Editor.vue | 46 +- js/src/components/Editor/MaxSize.ts | 33 + js/src/components/Event/OrganizerPicker.vue | 102 +++ .../Event/OrganizerPickerWrapper.vue | 199 +++++ js/src/components/Footer.vue | 2 +- js/src/components/Group/GroupMemberCard.vue | 84 +- js/src/components/Group/GroupPicker.vue | 19 +- .../components/Group/GroupPickerWrapper.vue | 17 +- js/src/components/Group/Invitations.vue | 45 +- .../Participation/ConfirmParticipation.vue | 7 +- .../ParticipationWithoutAccount.vue | 25 +- js/src/components/PictureUpload.vue | 18 +- js/src/components/Resource/FolderItem.vue | 30 +- .../Settings/NotificationsOnboarding.vue | 22 +- js/src/components/Todo/CompactTodo.vue | 23 +- js/src/components/Todo/FullTodo.vue | 25 +- js/src/graphql/actor.ts | 47 ++ js/src/graphql/config.ts | 4 +- js/src/graphql/event.ts | 35 + js/src/graphql/resources.ts | 1 + js/src/graphql/todos.ts | 2 + js/src/i18n/en_US.json | 55 +- js/src/i18n/fr_FR.json | 16 +- js/src/mixins/event.ts | 5 +- js/src/router/actor.ts | 8 - js/src/router/guards/register-guard.ts | 2 +- js/src/types/actor/actor.model.ts | 4 +- js/src/types/config.model.ts | 2 +- js/src/types/event.model.ts | 9 + js/src/types/login-error-code.model.ts | 2 +- js/src/typings/intl.d.ts | 17 + js/src/utils/errors.ts | 91 --- js/src/utils/i18n.ts | 8 + js/src/views/About/AboutInstance.vue | 4 +- .../views/Account/children/EditIdentity.vue | 24 +- js/src/views/Admin/AdminGroupProfile.vue | 25 +- js/src/views/Discussions/DiscussionsList.vue | 2 +- js/src/views/Event/Edit.vue | 113 ++- js/src/views/Event/Event.vue | 26 +- js/src/views/Event/GroupEvents.vue | 2 +- js/src/views/Group/Create.vue | 28 +- js/src/views/Group/Group.vue | 87 +- js/src/views/Group/GroupList.vue | 66 -- js/src/views/Group/GroupMembers.vue | 14 +- js/src/views/Group/GroupSettings.vue | 80 +- js/src/views/Group/MyGroups.vue | 40 +- js/src/views/Group/Settings.vue | 6 +- js/src/views/Posts/Post.vue | 15 +- js/src/views/Resources/ResourceFolder.vue | 2 +- js/src/views/Settings/AccountSettings.vue | 41 +- js/src/views/Todos/Todo.vue | 2 +- js/src/views/Todos/TodoList.vue | 2 +- js/src/views/Todos/TodoLists.vue | 2 +- js/src/views/User/Login.vue | 44 +- js/src/views/User/Register.vue | 4 +- js/src/views/User/SendPasswordReset.vue | 20 +- js/src/vue-apollo.ts | 24 +- lib/federation/activity_pub/types/events.ex | 7 +- .../activity_stream/converter/utils.ex | 9 +- lib/graphql/api/groups.ex | 4 +- lib/graphql/resolvers/actor.ex | 23 +- lib/graphql/resolvers/admin.ex | 15 +- lib/graphql/resolvers/comment.ex | 15 +- lib/graphql/resolvers/config.ex | 2 +- lib/graphql/resolvers/discussion.ex | 10 +- lib/graphql/resolvers/event.ex | 30 +- lib/graphql/resolvers/feed_token.ex | 15 +- lib/graphql/resolvers/group.ex | 62 +- lib/graphql/resolvers/member.ex | 20 +- lib/graphql/resolvers/participant.ex | 63 +- lib/graphql/resolvers/person.ex | 44 +- lib/graphql/resolvers/picture.ex | 7 +- lib/graphql/resolvers/post.ex | 30 +- lib/graphql/resolvers/report.ex | 25 +- lib/graphql/resolvers/resource.ex | 27 +- lib/graphql/resolvers/todos.ex | 41 +- lib/graphql/resolvers/user.ex | 82 +- lib/graphql/schema/config.ex | 2 +- lib/graphql/schema/event.ex | 8 + lib/mobilizon/actors/actors.ex | 3 +- lib/mobilizon/config.ex | 8 +- lib/mobilizon/events/event.ex | 5 +- lib/mobilizon/events/events.ex | 5 +- lib/mobilizon/users/user.ex | 15 +- lib/web/endpoint.ex | 2 + lib/web/plugs/set_locale_plug.ex | 67 ++ lib/web/proxy/reverse_proxy.ex | 2 +- priv/gettext/ar/LC_MESSAGES/errors.po | 731 +++++++++++++++++ priv/gettext/be/LC_MESSAGES/errors.po | 731 +++++++++++++++++ priv/gettext/ca/LC_MESSAGES/errors.po | 731 +++++++++++++++++ priv/gettext/cs/LC_MESSAGES/errors.po | 731 +++++++++++++++++ priv/gettext/de/LC_MESSAGES/errors.po | 731 +++++++++++++++++ priv/gettext/en/LC_MESSAGES/errors.po | 731 +++++++++++++++++ priv/gettext/errors.pot | 731 +++++++++++++++++ priv/gettext/es/LC_MESSAGES/default.po | 432 +++++----- priv/gettext/es/LC_MESSAGES/errors.po | 731 +++++++++++++++++ priv/gettext/fi/LC_MESSAGES/errors.po | 731 +++++++++++++++++ priv/gettext/fr/LC_MESSAGES/default.po | 2 +- priv/gettext/fr/LC_MESSAGES/errors.po | 761 +++++++++++++++++- priv/gettext/it/LC_MESSAGES/default.po | 432 +++++----- priv/gettext/it/LC_MESSAGES/errors.po | 741 ++++++++++++++++- priv/gettext/ja/LC_MESSAGES/default.po | 432 +++++----- priv/gettext/ja/LC_MESSAGES/errors.po | 731 +++++++++++++++++ priv/gettext/nl/LC_MESSAGES/errors.po | 731 +++++++++++++++++ priv/gettext/oc/LC_MESSAGES/errors.po | 731 +++++++++++++++++ priv/gettext/pl/LC_MESSAGES/errors.po | 731 +++++++++++++++++ priv/gettext/pt/LC_MESSAGES/errors.po | 731 +++++++++++++++++ priv/gettext/pt_BR/LC_MESSAGES/errors.po | 731 +++++++++++++++++ priv/gettext/ru/LC_MESSAGES/errors.po | 731 +++++++++++++++++ priv/gettext/sv/LC_MESSAGES/default.po | 432 +++++----- priv/gettext/sv/LC_MESSAGES/errors.po | 731 +++++++++++++++++ .../20200928150725_add_contacts_to_events.exs | 10 + test/graphql/resolvers/comment_test.exs | 2 +- test/graphql/resolvers/event_test.exs | 4 +- test/graphql/resolvers/group_test.exs | 4 +- test/graphql/resolvers/member_test.exs | 8 +- test/graphql/resolvers/participant_test.exs | 4 +- test/graphql/resolvers/person_test.exs | 10 +- test/graphql/resolvers/post_test.exs | 2 +- test/graphql/resolvers/report_test.exs | 2 +- test/graphql/resolvers/resource_test.exs | 6 +- test/graphql/resolvers/user_test.exs | 24 +- test/support/factory.ex | 3 +- test/web/plugs/set_locale_plug_test.exs | 47 ++ 131 files changed, 16440 insertions(+), 1929 deletions(-) create mode 100644 js/src/components/Editor/MaxSize.ts create mode 100644 js/src/components/Event/OrganizerPicker.vue create mode 100644 js/src/components/Event/OrganizerPickerWrapper.vue create mode 100644 js/src/typings/intl.d.ts delete mode 100644 js/src/utils/errors.ts delete mode 100644 js/src/views/Group/GroupList.vue create mode 100644 lib/web/plugs/set_locale_plug.ex create mode 100644 priv/repo/migrations/20200928150725_add_contacts_to_events.exs create mode 100644 test/web/plugs/set_locale_plug_test.exs diff --git a/config/config.exs b/config/config.exs index 9eeb600c9..37f6a25d4 100644 --- a/config/config.exs +++ b/config/config.exs @@ -17,7 +17,7 @@ config :mobilizon, :instance, description: "Change this to a proper description of your instance", hostname: "localhost", registrations_open: false, - registration_email_whitelist: [], + registration_email_allowlist: [], demo: false, repository: Mix.Project.config()[:source_url], allow_relay: true, @@ -29,8 +29,7 @@ config :mobilizon, :instance, email_from: "noreply@localhost", email_reply_to: "noreply@localhost" -# Groups are to be activated with Mobilizon 1.0.0 -config :mobilizon, :groups, enabled: false +config :mobilizon, :groups, enabled: true config :mobilizon, :events, creation: true diff --git a/js/src/App.vue b/js/src/App.vue index 03d84ad5d..5761b33c0 100644 --- a/js/src/App.vue +++ b/js/src/App.vue @@ -76,7 +76,7 @@ export default class App extends Vue { currentUser!: ICurrentUser; - async created() { + async created(): Promise { if (await this.initializeCurrentUser()) { await initializeCurrentActor(this.$apollo.provider.defaultClient); } diff --git a/js/src/apollo/utils.ts b/js/src/apollo/utils.ts index 5482d6f72..ff7c317a7 100644 --- a/js/src/apollo/utils.ts +++ b/js/src/apollo/utils.ts @@ -1,5 +1,4 @@ import { IntrospectionFragmentMatcher, NormalizedCacheObject } from "apollo-cache-inmemory"; -import { IError, errors, defaultError, refreshSuggestion } from "@/utils/errors"; import { AUTH_ACCESS_TOKEN, AUTH_REFRESH_TOKEN } from "@/constants"; import { REFRESH_TOKEN } from "@/graphql/auth"; import { saveTokenData } from "@/utils/auth"; @@ -24,18 +23,6 @@ export const fragmentMatcher = new IntrospectionFragmentMatcher({ }, }); -export const computeErrorMessage = (message: any) => { - const error: IError = errors.reduce((acc, errorLocal) => { - if (RegExp(errorLocal.match).test(message)) { - return errorLocal; - } - return acc; - }, defaultError); - - if (error.value === null) return null; - return error.suggestRefresh === false ? error.value : `${error.value}
${refreshSuggestion}`; -}; - export async function refreshAccessToken( apolloClient: ApolloClient ): Promise { diff --git a/js/src/components/Admin/Followers.vue b/js/src/components/Admin/Followers.vue index 16a29fd2a..73d65c186 100644 --- a/js/src/components/Admin/Followers.vue +++ b/js/src/components/Admin/Followers.vue @@ -101,6 +101,7 @@ diff --git a/js/src/components/Comment/Comment.vue b/js/src/components/Comment/Comment.vue index b98330ac8..799a7010a 100644 --- a/js/src/components/Comment/Comment.vue +++ b/js/src/components/Comment/Comment.vue @@ -126,6 +126,7 @@ import { Component, Prop, Vue, Ref } from "vue-property-decorator"; import EditorComponent from "@/components/Editor.vue"; import TimeAgo from "javascript-time-ago"; +import { SnackbarProgrammatic as Snackbar } from "buefy"; import { CommentModel, IComment } from "../../types/comment.model"; import { CURRENT_ACTOR_CLIENT } from "../../graphql/actor"; import { IPerson, usernameWithDomain } from "../../types/actor"; @@ -171,7 +172,7 @@ export default class Comment extends Vue { usernameWithDomain = usernameWithDomain; - async mounted() { + async mounted(): Promise { const localeName = this.$i18n.locale; const locale = await import(`javascript-time-ago/locale/${localeName}`); TimeAgo.addLocale(locale); @@ -183,7 +184,7 @@ export default class Comment extends Vue { } } - async createReplyToComment(comment: IComment) { + async createReplyToComment(comment: IComment): Promise { if (this.replyTo) { this.replyTo = false; this.newComment = new CommentModel(); @@ -196,7 +197,7 @@ export default class Comment extends Vue { this.commentEditor.replyToComment(comment); } - replyToComment() { + replyToComment(): void { this.newComment.inReplyToComment = this.comment; this.newComment.originComment = this.comment.originComment || this.comment; this.newComment.actor = this.currentActor; @@ -205,7 +206,7 @@ export default class Comment extends Vue { this.replyTo = false; } - async fetchReplies() { + async fetchReplies(): Promise { const parentId = this.comment.id; const { data } = await this.$apollo.query<{ thread: IComment[] }>({ query: FETCH_THREAD_REPLIES, @@ -267,7 +268,7 @@ export default class Comment extends Vue { return this.commentId; } - reportModal() { + reportModal(): void { if (!this.comment.actor) return; this.$buefy.modal.open({ parent: this, @@ -281,7 +282,7 @@ export default class Comment extends Vue { }); } - async reportComment(content: string, forward: boolean) { + async reportComment(content: string, forward: boolean): Promise { try { if (!this.comment.actor) return; await this.$apollo.mutate({ @@ -303,8 +304,8 @@ export default class Comment extends Vue { position: "is-bottom-right", duration: 5000, }); - } catch (error) { - console.error(error); + } catch (e) { + Snackbar.open({ message: e.message, type: "is-danger", position: "is-bottom" }); } } } diff --git a/js/src/components/Comment/CommentTree.vue b/js/src/components/Comment/CommentTree.vue index 48b51be2e..32f726682 100644 --- a/js/src/components/Comment/CommentTree.vue +++ b/js/src/components/Comment/CommentTree.vue @@ -51,6 +51,7 @@ import { Prop, Vue, Component, Watch } from "vue-property-decorator"; import Comment from "@/components/Comment/Comment.vue"; import IdentityPickerWrapper from "@/views/Account/IdentityPickerWrapper.vue"; +import { SnackbarProgrammatic as Snackbar } from "buefy"; import { CommentModel, IComment } from "../../types/comment.model"; import { CREATE_COMMENT_FROM_EVENT, @@ -100,11 +101,11 @@ export default class CommentTree extends Vue { CommentModeration = CommentModeration; @Watch("currentActor") - watchCurrentActor(currentActor: IPerson) { + watchCurrentActor(currentActor: IPerson): void { this.newComment.actor = currentActor; } - async createCommentForEvent(comment: IComment) { + async createCommentForEvent(comment: IComment): Promise { try { if (!comment.actor) return; await this.$apollo.mutate({ @@ -190,74 +191,78 @@ export default class CommentTree extends Vue { // and reset the new comment field this.newComment = new CommentModel(); } catch (e) { - console.error(e); + Snackbar.open({ message: e.message, type: "is-danger", position: "is-bottom" }); } } - async deleteComment(comment: IComment) { - await this.$apollo.mutate({ - mutation: DELETE_COMMENT, - variables: { - commentId: comment.id, - actorId: this.currentActor.id, - }, - update: (store, { data }) => { - if (data == null) return; - const deletedCommentId = data.deleteComment.id; + async deleteComment(comment: IComment): Promise { + try { + await this.$apollo.mutate({ + mutation: DELETE_COMMENT, + variables: { + commentId: comment.id, + actorId: this.currentActor.id, + }, + update: (store, { data }) => { + if (data == null) return; + const deletedCommentId = data.deleteComment.id; - const commentsData = store.readQuery<{ event: IEvent }>({ - query: COMMENTS_THREADS, - variables: { - eventUUID: this.event.uuid, - }, - }); - if (!commentsData) return; - const { event } = commentsData; - const { comments: oldComments } = event; - - if (comment.originComment) { - // we have deleted a reply to a thread - const localData = store.readQuery<{ thread: IComment[] }>({ - query: FETCH_THREAD_REPLIES, + const commentsData = store.readQuery<{ event: IEvent }>({ + query: COMMENTS_THREADS, variables: { - threadId: comment.originComment.id, + eventUUID: this.event.uuid, }, }); - if (!localData) return; - const { thread: oldReplyList } = localData; - const replies = oldReplyList.filter((reply) => reply.id !== deletedCommentId); + if (!commentsData) return; + const { event } = commentsData; + const { comments: oldComments } = event; + + if (comment.originComment) { + // we have deleted a reply to a thread + const localData = store.readQuery<{ thread: IComment[] }>({ + query: FETCH_THREAD_REPLIES, + variables: { + threadId: comment.originComment.id, + }, + }); + if (!localData) return; + const { thread: oldReplyList } = localData; + const replies = oldReplyList.filter((reply) => reply.id !== deletedCommentId); + store.writeQuery({ + query: FETCH_THREAD_REPLIES, + variables: { + threadId: comment.originComment.id, + }, + data: { thread: replies }, + }); + + const { originComment } = comment; + + const parentCommentIndex = oldComments.findIndex( + (oldComment) => oldComment.id === originComment.id + ); + const parentComment = oldComments[parentCommentIndex]; + parentComment.replies = replies; + parentComment.totalReplies -= 1; + oldComments.splice(parentCommentIndex, 1, parentComment); + event.comments = oldComments; + } else { + // we have deleted a thread itself + event.comments = oldComments.filter((reply) => reply.id !== deletedCommentId); + } store.writeQuery({ - query: FETCH_THREAD_REPLIES, + query: COMMENTS_THREADS, variables: { - threadId: comment.originComment.id, + eventUUID: this.event.uuid, }, - data: { thread: replies }, + data: { event }, }); - - const { originComment } = comment; - - const parentCommentIndex = oldComments.findIndex( - (oldComment) => oldComment.id === originComment.id - ); - const parentComment = oldComments[parentCommentIndex]; - parentComment.replies = replies; - parentComment.totalReplies -= 1; - oldComments.splice(parentCommentIndex, 1, parentComment); - event.comments = oldComments; - } else { - // we have deleted a thread itself - event.comments = oldComments.filter((reply) => reply.id !== deletedCommentId); - } - store.writeQuery({ - query: COMMENTS_THREADS, - variables: { - eventUUID: this.event.uuid, - }, - data: { event }, - }); - }, - }); - // this.comments = this.comments.filter(commentItem => commentItem.id !== comment.id); + }, + }); + // this.comments = this.comments.filter(commentItem => commentItem.id !== comment.id); + } catch (e) { + Snackbar.open({ message: e.message, type: "is-danger", position: "is-bottom" }); + } } get orderedComments(): IComment[] { diff --git a/js/src/components/Editor.vue b/js/src/components/Editor.vue index 68982a291..16e362cb5 100644 --- a/js/src/components/Editor.vue +++ b/js/src/components/Editor.vue @@ -2,7 +2,7 @@
@@ -211,6 +211,7 @@ import tippy, { Instance, sticky } from "tippy.js"; import { SEARCH_PERSONS } from "../graphql/search"; import { Actor, IActor, IPerson } from "../types/actor"; import Image from "./Editor/Image"; +import MaxSize from "./Editor/MaxSize"; import { UPLOAD_PICTURE } from "../graphql/upload"; import { listenFileUpload } from "../utils/upload"; import { CURRENT_ACTOR_CLIENT } from "../graphql/actor"; @@ -229,6 +230,8 @@ export default class EditorComponent extends Vue { @Prop({ required: false, default: "description" }) mode!: string; + @Prop({ required: false, default: 100_000_000 }) maxSize!: number; + currentActor!: IPerson; editor: Editor | null = null; @@ -254,6 +257,10 @@ export default class EditorComponent extends Vue { return this.mode === "comment"; } + get isShortMode(): boolean { + return this.isBasicMode; + } + get hasResults(): boolean { return this.filteredActors.length > 0; } @@ -386,6 +393,7 @@ export default class EditorComponent extends Vue { showOnlyWhenEditable: false, }), new Image(), + new MaxSize({ maxSize: this.maxSize }), ], onUpdate: ({ getHTML }: { getHTML: Function }) => { this.$emit("input", getHTML()); @@ -395,7 +403,7 @@ export default class EditorComponent extends Vue { } @Watch("value") - onValueChanged(val: string) { + onValueChanged(val: string): void { if (!this.editor) return; if (val !== this.editor.getHTML()) { this.editor.setContent(val); @@ -420,7 +428,7 @@ export default class EditorComponent extends Vue { return undefined; } - upHandler() { + upHandler(): void { this.navigatedActorIndex = (this.navigatedActorIndex + this.filteredActors.length - 1) % this.filteredActors.length; } @@ -429,11 +437,11 @@ export default class EditorComponent extends Vue { * navigate to the next item * if it's the last item, navigate to the first one */ - downHandler() { + downHandler(): void { this.navigatedActorIndex = (this.navigatedActorIndex + 1) % this.filteredActors.length; } - enterHandler() { + enterHandler(): void { const actor = this.filteredActors[this.navigatedActorIndex]; if (actor) { this.selectActor(actor); @@ -445,7 +453,7 @@ export default class EditorComponent extends Vue { * so it's important to pass also the position of your suggestion text * @param actor IActor */ - selectActor(actor: IActor) { + selectActor(actor: IActor): void { const actorModel = new Actor(actor); this.insertMention({ range: this.suggestionRange, @@ -460,7 +468,7 @@ export default class EditorComponent extends Vue { } /** We use this to programatically insert an actor mention when creating a reply to comment */ - replyToComment(comment: IComment) { + replyToComment(comment: IComment): void { if (!comment.actor) return; const actorModel = new Actor(comment.actor); if (!this.editor) return; @@ -476,11 +484,12 @@ export default class EditorComponent extends Vue { * tiptap provides a virtualNode object for using popper.js (or tippy.js) for popups * @param node */ - renderPopup(node: Element) { + renderPopup(node: Element): void { if (this.popup) { return; } this.popup = tippy("#mobilizon", { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore getReferenceClientRect: node.getBoundingClientRect, appendTo: () => document.body, @@ -497,8 +506,9 @@ export default class EditorComponent extends Vue { }) as Instance[]; } - destroyPopup() { + destroyPopup(): void { if (this.popup) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore this.popup[0].destroy(); this.popup = null; @@ -512,7 +522,7 @@ export default class EditorComponent extends Vue { * Show a file prompt, upload picture and insert it into editor * @param command */ - async showImagePrompt(command: Function) { + async showImagePrompt(command: Function): Promise { const image = await listenFileUpload(); const { data } = await this.$apollo.mutate({ mutation: UPLOAD_PICTURE, @@ -527,7 +537,7 @@ export default class EditorComponent extends Vue { } } - beforeDestroy() { + beforeDestroy(): void { if (!this.editor) return; this.destroyPopup(); this.editor.destroy(); @@ -577,9 +587,19 @@ $color-white: #eee; font-style: italic; } - &.mode_description { + .editor__content div.ProseMirror { + min-height: 10rem; + } + + &.short_mode { div.ProseMirror { - min-height: 10rem; + min-height: 5rem; + } + } + + &.comment_mode { + div.ProseMirror { + min-height: 2rem; } } diff --git a/js/src/components/Editor/MaxSize.ts b/js/src/components/Editor/MaxSize.ts new file mode 100644 index 000000000..a1d113704 --- /dev/null +++ b/js/src/components/Editor/MaxSize.ts @@ -0,0 +1,33 @@ +// @ts-nocheck +import { Extension, Plugin } from "tiptap"; + +export default class MaxSize extends Extension { + get name() { + return "maxSize"; + } + + get defaultOptions() { + return { + maxSize: null, + }; + } + + get plugins() { + return [ + new Plugin({ + appendTransaction: (transactions, oldState, newState) => { + const max = this.options.maxSize; + const oldLength = oldState.doc.content.size; + const newLength = newState.doc.content.size; + + if (newLength > max && newLength > oldLength) { + let newTr = newState.tr; + newTr.insertText("", max + 1, newLength); + + return newTr; + } + }, + }), + ]; + } +} diff --git a/js/src/components/Event/OrganizerPicker.vue b/js/src/components/Event/OrganizerPicker.vue new file mode 100644 index 000000000..be35489b3 --- /dev/null +++ b/js/src/components/Event/OrganizerPicker.vue @@ -0,0 +1,102 @@ + + + diff --git a/js/src/components/Event/OrganizerPickerWrapper.vue b/js/src/components/Event/OrganizerPickerWrapper.vue new file mode 100644 index 000000000..abc94ea66 --- /dev/null +++ b/js/src/components/Event/OrganizerPickerWrapper.vue @@ -0,0 +1,199 @@ + + + diff --git a/js/src/components/Footer.vue b/js/src/components/Footer.vue index d9b32510a..27105abb3 100644 --- a/js/src/components/Footer.vue +++ b/js/src/components/Footer.vue @@ -10,7 +10,7 @@ {{ $t("Terms") }}
  • - + {{ $t("License") }}
  • diff --git a/js/src/components/Group/GroupMemberCard.vue b/js/src/components/Group/GroupMemberCard.vue index 13ddb2550..f00689077 100644 --- a/js/src/components/Group/GroupMemberCard.vue +++ b/js/src/components/Group/GroupMemberCard.vue @@ -1,32 +1,52 @@ @@ -88,11 +97,11 @@ export default class GroupPickerWrapper extends Vue { groupMemberships: Paginate = { elements: [], total: 0 }; @Watch("value") - updateCurrentGroup(value: IGroup) { + updateCurrentGroup(value: IGroup): void { this.currentGroup = value; } - relay(group: IGroup) { + relay(group: IGroup): void { this.currentGroup = group; this.$emit("input", group); this.isComponentModalActive = false; diff --git a/js/src/components/Group/Invitations.vue b/js/src/components/Group/Invitations.vue index 9dadff141..092596852 100644 --- a/js/src/components/Group/Invitations.vue +++ b/js/src/components/Group/Invitations.vue @@ -14,6 +14,7 @@ import { ACCEPT_INVITATION, REJECT_INVITATION } from "@/graphql/member"; import { IMember } from "@/types/actor"; import { Component, Prop, Vue } from "vue-property-decorator"; import InvitationCard from "@/components/Group/InvitationCard.vue"; +import { SnackbarProgrammatic as Snackbar } from "buefy"; @Component({ components: { @@ -23,27 +24,35 @@ import InvitationCard from "@/components/Group/InvitationCard.vue"; export default class Invitations extends Vue { @Prop({ required: true, type: Array }) invitations!: IMember; - async acceptInvitation(id: string) { - const { data } = await this.$apollo.mutate<{ acceptInvitation: IMember }>({ - mutation: ACCEPT_INVITATION, - variables: { - id, - }, - }); - if (data) { - this.$emit("acceptInvitation", data.acceptInvitation); + async acceptInvitation(id: string): Promise { + try { + const { data } = await this.$apollo.mutate<{ acceptInvitation: IMember }>({ + mutation: ACCEPT_INVITATION, + variables: { + id, + }, + }); + if (data) { + this.$emit("acceptInvitation", data.acceptInvitation); + } + } catch (e) { + Snackbar.open({ message: e.message, type: "is-danger", position: "is-bottom" }); } } - async rejectInvitation(id: string) { - const { data } = await this.$apollo.mutate<{ rejectInvitation: IMember }>({ - mutation: REJECT_INVITATION, - variables: { - id, - }, - }); - if (data) { - this.$emit("rejectInvitation", data.rejectInvitation); + async rejectInvitation(id: string): Promise { + try { + const { data } = await this.$apollo.mutate<{ rejectInvitation: IMember }>({ + mutation: REJECT_INVITATION, + variables: { + id, + }, + }); + if (data) { + this.$emit("rejectInvitation", data.rejectInvitation); + } + } catch (e) { + Snackbar.open({ message: e.message, type: "is-danger", position: "is-bottom" }); } } } diff --git a/js/src/components/Participation/ConfirmParticipation.vue b/js/src/components/Participation/ConfirmParticipation.vue index 1276a69ed..e6f7e4a36 100644 --- a/js/src/components/Participation/ConfirmParticipation.vue +++ b/js/src/components/Participation/ConfirmParticipation.vue @@ -18,6 +18,7 @@ diff --git a/js/src/components/PictureUpload.vue b/js/src/components/PictureUpload.vue index 3e72ad4fa..2fb40a9e7 100644 --- a/js/src/components/PictureUpload.vue +++ b/js/src/components/PictureUpload.vue @@ -1,7 +1,7 @@ diff --git a/js/src/components/Todo/CompactTodo.vue b/js/src/components/Todo/CompactTodo.vue index 9e4761c96..8386e9f68 100644 --- a/js/src/components/Todo/CompactTodo.vue +++ b/js/src/components/Todo/CompactTodo.vue @@ -21,6 +21,7 @@ diff --git a/js/src/components/Todo/FullTodo.vue b/js/src/components/Todo/FullTodo.vue index b9ea52fa3..5573b3ec2 100644 --- a/js/src/components/Todo/FullTodo.vue +++ b/js/src/components/Todo/FullTodo.vue @@ -19,6 +19,7 @@ diff --git a/js/src/graphql/actor.ts b/js/src/graphql/actor.ts index c1be5e61a..b9c907b41 100644 --- a/js/src/graphql/actor.ts +++ b/js/src/graphql/actor.ts @@ -359,6 +359,53 @@ export const PERSON_MEMBERSHIPS = gql` } `; +export const PERSON_MEMBERSHIPS_WITH_MEMBERS = gql` + query PersonMembershipsWithMembers($id: ID!) { + person(id: $id) { + id + memberships { + total + elements { + id + role + parent { + id + preferredUsername + name + domain + avatar { + url + } + members { + total + elements { + id + role + actor { + id + preferredUsername + name + domain + avatar { + url + } + } + } + } + } + invitedBy { + id + preferredUsername + name + } + insertedAt + updatedAt + } + } + } + } +`; + export const CREATE_PERSON = gql` mutation CreatePerson( $preferredUsername: String! diff --git a/js/src/graphql/config.ts b/js/src/graphql/config.ts index d9eb57d6c..a163d438b 100644 --- a/js/src/graphql/config.ts +++ b/js/src/graphql/config.ts @@ -6,7 +6,7 @@ export const CONFIG = gql` name description registrationsOpen - registrationsWhitelist + registrationsAllowlist demoMode countryCode anonymous { @@ -94,7 +94,7 @@ export const ABOUT = gql` longDescription contact registrationsOpen - registrationsWhitelist + registrationsAllowlist anonymous { participation { allowed diff --git a/js/src/graphql/event.ts b/js/src/graphql/event.ts index 41e8274be..f3c6acfc8 100644 --- a/js/src/graphql/event.ts +++ b/js/src/graphql/event.ts @@ -111,6 +111,17 @@ export const FETCH_EVENT = gql` id, summary }, + contacts { + avatar { + url, + } + preferredUsername, + name, + summary, + domain, + url, + id + }, attributedTo { avatar { url, @@ -229,6 +240,7 @@ export const CREATE_EVENT = gql` $category: String, $physicalAddress: AddressInput, $options: EventOptionsInput, + $contacts: [Contact] ) { createEvent( organizerActorId: $organizerActorId, @@ -248,6 +260,7 @@ export const CREATE_EVENT = gql` category: $category, physicalAddress: $physicalAddress options: $options, + contacts: $contacts ) { id, uuid, @@ -292,6 +305,16 @@ export const CREATE_EVENT = gql` url, id, }, + contacts { + avatar { + url + }, + preferredUsername, + domain, + name, + url, + id, + }, participantStats { going, notApproved, @@ -327,6 +350,7 @@ export const EDIT_EVENT = gql` $category: String, $physicalAddress: AddressInput, $options: EventOptionsInput, + $contacts: [Contact] ) { updateEvent( eventId: $id, @@ -347,6 +371,7 @@ export const EDIT_EVENT = gql` category: $category, physicalAddress: $physicalAddress options: $options, + contacts: $contacts ) { id, uuid, @@ -381,6 +406,16 @@ export const EDIT_EVENT = gql` url } }, + contacts { + avatar { + url + }, + preferredUsername, + domain, + name, + url, + id, + }, organizerActor { avatar { url diff --git a/js/src/graphql/resources.ts b/js/src/graphql/resources.ts index 5b1e90525..e614e9c77 100644 --- a/js/src/graphql/resources.ts +++ b/js/src/graphql/resources.ts @@ -35,6 +35,7 @@ export const GET_RESOURCE = gql` actor { id preferredUsername + name domain } children { diff --git a/js/src/graphql/todos.ts b/js/src/graphql/todos.ts index 2c8c879df..fed3a6f2c 100644 --- a/js/src/graphql/todos.ts +++ b/js/src/graphql/todos.ts @@ -11,6 +11,7 @@ export const GET_TODO = gql` actor { id preferredUsername + name } title id @@ -51,6 +52,7 @@ export const FETCH_TODO_LIST = gql` id preferredUsername domain + name } } } diff --git a/js/src/i18n/en_US.json b/js/src/i18n/en_US.json index 1ca564181..2720a547f 100644 --- a/js/src/i18n/en_US.json +++ b/js/src/i18n/en_US.json @@ -22,7 +22,6 @@ "All the places have already been taken": "All the places have been taken|One place is still available|{places} places are still available", "Allow all comments": "Allow all comments", "Allow registrations": "Allow registrations", - "An error has occurred.": "An error has occurred.", "Anonymous participant": "Anonymous participant", "Anonymous participants will be asked to confirm their participation through e-mail.": "Anonymous participants will be asked to confirm their participation through e-mail.", "Anonymous participations": "Anonymous participations", @@ -38,7 +37,6 @@ "Back to previous page": "Back to previous page", "Before you can login, you need to click on the link inside it to validate your account.": "Before you can login, you need to click on the link inside it to validate your account.", "By @{username}": "By @{username}", - "By {username} and {group}": "By {username} and {group}", "Cancel anonymous participation": "Cancel anonymous participation", "Cancel creation": "Cancel creation", "Cancel edition": "Cancel edition", @@ -120,8 +118,6 @@ "Ends on…": "Ends on…", "Enter the link URL": "Enter the link URL", "Error while changing email": "Error while changing email", - "Error while communicating with the server.": "Error while communicating with the server.", - "Error while saving report.": "Error while saving report.", "Error while validating account": "Error while validating account", "Error while validating participation": "Error while validating participation", "Event already passed": "Event already passed", @@ -129,7 +125,6 @@ "Event creation": "Event creation", "Event edition": "Event edition", "Event list": "Event list", - "Event not found.": "Event not found.", "Event page settings": "Event page settings", "Event to be confirmed": "Event to be confirmed", "Event {eventTitle} deleted": "Event {eventTitle} deleted", @@ -158,7 +153,6 @@ "Getting location": "Getting location", "Go": "Go", "Going as {name}": "Going as {name}", - "Group List": "Group List", "Group name": "Group name", "Group {displayName} created": "Group {displayName} created", "Groups": "Groups", @@ -177,7 +171,6 @@ "If an account with this email exists, we just sent another confirmation email to {email}": "If an account with this email exists, we just sent another confirmation email to {email}", "If this identity is the only administrator of some groups, you need to delete them before being able to delete this identity.": "If this identity is the only administrator of some groups, you need to delete them before being able to delete this identity.", "If you want, you may send a message to the event organizer here.": "If you want, you may send a message to the event organizer here.", - "Impossible to login, your email or password seems incorrect.": "Impossible to login, your email or password seems incorrect.", "In the meantime, please consider that the software is not (yet) finished. More information {onBlog}.": "In the meantime, please consider that the software is not (yet) finished. More information {onBlog}.", "Installing Mobilizon will allow communities to free themselves from the services of tech giants by creating their own event platform.": "Installing Mobilizon will allow communities to free themselves from the services of tech giants by creating their own event platform.", "Instance Name": "Instance Name", @@ -240,7 +233,6 @@ "No participant to reject|Reject participant|Reject {number} participants": "No participant to reject|Reject participant|Reject {number} participants", "No resolved reports yet": "No resolved reports yet", "No results for \"{queryText}\"": "No results for \"{queryText}\"", - "No user account with this email was found. Maybe you made a typo?": "No user account with this email was found. Maybe you made a typo?", "Notes": "Notes", "Email notifications": "Email notifications", "Number of places": "Number of places", @@ -261,10 +253,7 @@ "Other software may also support this.": "Other software may also support this.", "Otherwise this identity will just be removed from the group administrators.": "Otherwise this identity will just be removed from the group administrators.", "Page limited to my group (asks for auth)": "Page limited to my group (asks for auth)", - "Page not found": "Page not found", "Page": "Page", - "Participant already was rejected.": "Participant already was rejected.", - "Participant has already been approved as participant.": "Participant has already been approved as participant.", "Participant": "Participant", "Participants": "Participants", "Participate using your email address": "Participate using your email address", @@ -282,7 +271,6 @@ "Please contact this instance's Mobilizon admin if you think this is a mistake.": "Please contact this instance's Mobilizon admin if you think this is a mistake.", "Please enter your password to confirm this action.": "Please enter your password to confirm this action.", "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 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", @@ -309,7 +297,6 @@ "Registration is allowed, anyone can register.": "Registration is allowed, anyone can register.", "Registration is closed.": "Registration is closed.", "Registration is currently closed.": "Registration is currently closed.", - "Registrations are restricted by whitelisting.": "Registrations are restricted by whitelisting.", "Reject": "Reject", "Rejected": "Rejected", "Reopen": "Reopen", @@ -323,7 +310,6 @@ "Reported identity": "Reported identity", "Reported": "Reported", "Reports": "Reports", - "Resend confirmation email": "Resend confirmation email", "Reset my password": "Reset my password", "Resolved": "Resolved", "Resource provided is not an URL": "Resource provided is not an URL", @@ -352,8 +338,6 @@ "The account's email address was changed. Check your emails to verify it.": "The account's email address was changed. Check your emails to verify it.", "The actual number of participants may differ, as this event is hosted on another instance.": "The actual number of participants may differ, as this event is hosted on another instance.", "The content came from another server. Transfer an anonymous copy of the report?": "The content came from another server. Transfer an anonymous copy of the report ?", - "The current identity doesn't have any permission on this event. You should probably change it.": "The current identity doesn't have any permission on this event. You should probably change it.", - "The current password is invalid": "The current password is invalid", "The draft event has been updated": "The draft event has been updated", "The event has been created as a draft": "The event has been created as a draft", "The event has been published": "The event has been published", @@ -363,20 +347,14 @@ "The event organizer didn't add any description.": "The event organizer didn't add any description.", "The event organizer manually approves participations. Since you've chosen to participate without an account, please explain why you want to participate to this event.": "The event organizer manually approves participations. Since you've chosen to participate without an account, please explain why you want to participate to this event.", "The event title will be ellipsed.": "The event title will be ellipsed.", - "The new email doesn't seem to be valid": "The new email doesn't seem to be valid", - "The new email must be different": "The new email must be different", - "The new password must be different": "The new password must be different", "The page you're looking for doesn't exist.": "The page you're looking for doesn't exist.", - "The password provided is invalid": "The password provided is invalid", "The password was successfully changed": "The password was successfully changed", "The report will be sent to the moderators of your instance. You can explain why you report this content below.": "The report will be sent to the moderators of your instance. You can explain why you report this content below.", - "The user account you're trying to login as has not been confirmed yet. Check your email inbox and eventually your spam folder.": "The user account you're trying to login as has not been confirmed yet. Check your email inbox and eventually your spam folder.", "The {default_terms} will be used. They will be translated in the user's language.": "The {default_terms} will be used. They will be translated in the user's language.", "There are {participants} participants.": "There are {participants} participants.", "There will be no way to recover your data.": "There will be no way to recover your data.", "These events may interest you": "These events may interest you", "This Mobilizon instance and this event organizer allows anonymous participations, but requires validation through email confirmation.": "This Mobilizon instance and this event organizer allows anonymous participations, but requires validation through email confirmation.", - "This email is already registered as participant for this event": "This email is already registered as participant for this event", "This information is saved only on your computer. Click for details": "This information is saved only on your computer. Click for details", "This instance isn't opened to registrations, but you can register on other instances.": "This instance isn't opened to registrations, but you can register on other instances.", "This is a demonstration site to test the beta version of Mobilizon.": "This is a demonstration site to test the beta version of Mobilizon.", @@ -417,19 +395,16 @@ "Who can view this event and participate": "Who can view this event and participate", "World map": "World map", "Write something…": "Write something…", - "You are already a participant of this event.": "You are already a participant of this event.", "You are participating in this event anonymously but didn't confirm participation": "You are participating in this event anonymously but didn't confirm participation", "You are participating in this event anonymously": "You are participating in this event anonymously", "You can add tags by hitting the Enter key or by adding a comma": "You can add tags by hitting the Enter key or by adding a comma", "You can try another search term or drag and drop the marker on the map": "You can try another search term or drag and drop the marker on the map", - "You can't remove your last identity.": "You can't remove your last identity.", "You don't follow any instances yet.": "You don't follow any instances yet.", "You have been disconnected": "You have been disconnected", "You have cancelled your participation": "You have cancelled your participation", "You have one event in {days} days.": "You have no events in {days} days | You have one event in {days} days. | You have {count} events in {days} days", "You have one event today.": "You have no events today | You have one event today. | You have {count} events today", "You have one event tomorrow.": "You have no events tomorrow | You have one event tomorrow. | You have {count} events tomorrow", - "You may also ask to {resend_confirmation_email}.": "You may also ask to {resend_confirmation_email}.", "You need to login.": "You need to login.", "You will be redirected to the original instance": "You will be redirected to the original instance", "You wish to participate to the following event": "You wish to participate to the following event", @@ -441,7 +416,6 @@ "Your current email is {email}. You use it to log in.": "Your current email is {email}. You use it to log in.", "Your email has been changed": "Your email has been changed", "Your email is being changed": "Your email is being changed", - "Your email is not whitelisted, you can't register.": "Your email is not whitelisted, you can't register.", "Your email will only be used to confirm that you're a real person and send you eventual updates for this event. It will NOT be transmitted to other instances or to the event organizer.": "Your email will only be used to confirm that you're a real person and send you eventual updates for this event. It will NOT be transmitted to other instances or to the event organizer.", "Your federated identity": "Your federated identity", "Your participation has been confirmed": "Your participation has been confirmed", @@ -462,7 +436,6 @@ "its source code is public": "its source code is public", "on our blog": "on our blog", "profile@instance": "profile@instance", - "resend confirmation email": "resend confirmation email", "respect of the fundamental freedoms": "respect of the fundamental freedoms", "with another identity…": "with another identity…", "{approved} / {total} seats": "{approved} / {total} seats", @@ -470,7 +443,6 @@ "{count} requests waiting": "{count} requests waiting", "© The OpenStreetMap Contributors": "© The OpenStreetMap Contributors", "@{username} ({role})": "@{username} ({role})", - "@{username}": "@{username}", "@{group}": "@{group}", "{title} ({count} todos)": "{title} ({count} todos)", "Pick a group": "Pick a group", @@ -483,8 +455,6 @@ "Organizers": "Organizers", "Hide the organizer": "Hide the organizer", "Don't show @{organizer} as event host alongside @{group}": "Don't show @{organizer} as event host alongside @{group}", - "Group": "Group", - "Ongoing tasks": "Ongoing tasks", "You need to create the group before you create an event.": "You need to create a group before you create an event.", "This identity is not a member of any group.": "This identity is not a member of any group.", "(Masked)": "(Masked)", @@ -499,7 +469,7 @@ "Decline": "Decline", "Rename": "Rename", "Move": "Move", - "Contact": "Contact", + "Contact": "Contact|Contacts", "Website": "Website", "Actor": "Actor", "Statut": "Statut", @@ -612,7 +582,6 @@ "terms of service": "terms of service", "Please read the instance's {fullRules}": "Please read the instance's {fullRules}", "I agree to the {instanceRules} and {termsOfService}": "I agree to the {instanceRules} and {termsOfService}", - "This email is already used.": "This email is already used.", "Powered by {mobilizon}. © 2018 - {date} The Mobilizon Contributors - Made with the financial support of {contributors}.": "Powered by {mobilizon}. © 2018 - {date} The Mobilizon Contributors - Made with the financial support of {contributors}.", "more than 1360 contributors": "more than 1360 contributors", "{moderator} has unsuspended profile {profile}": "{moderator} has unsuspended profile {profile}", @@ -693,8 +662,6 @@ "You can't change your password because you are registered through {provider}.": "You can't change your password because you are registered through {provider}.", "Error while login with {provider}. Retry or login another way.": "Error while login with {provider}. Retry or login another way.", "Error while login with {provider}. This login provider doesn't exist.": "Error while login with {provider}. This login provider doesn't exist.", - "This user has been disabled": "This user has been disabled", - "You can't reset your password because you use a 3rd-party auth provider to login.": "You can't reset your password because you use a 3rd-party auth provider to login.", "Post": "Post", "By {author}": "By {author}", "Right now": "Right now", @@ -738,13 +705,10 @@ "{count} team members": "{count} team members", "No resources yet": "No resources yet", "No posts yet": "No posts yet", - "No ongoing todos": "No ongoing todos", "No discussions yet": "No discussions yet", "Add / Remove…": "Add / Remove…", - "No public posts": "No public posts", "You have been removed from this group's members.": "You have been removed from this group's members.", "Since you are a new member, private content can take a few minutes to appear.": "Since you are a new member, private content can take a few minutes to appear.", - "Leave group": "Leave group", "Remove": "Remove", "Update": "Update", "Search…": "Search…", @@ -765,7 +729,6 @@ "Federated Group Name": "Federated Group Name", "This is like your federated username ({username}) for groups. It will allow you to be found on the federation, and is guaranteed to be unique.": "This is like your federated username ({username}) for groups. It will allow you to be found on the federation, and is guaranteed to be unique.", "Banner": "Banner", - "A group with this name already exists": "A group with this name already exists", "Create or join an group and start organizing with other people": "Create or join an group and start organizing with other people", "From a birthday party with friends and family to a march for climate change, right now, our gatherings are trapped inside the tech giants’ platforms. How can we organize, how can we click “Attend,” without providing private data to Facebook or locking ourselves inside MeetUp?": "From a birthday party with friends and family to a march for climate change, right now, our gatherings are trapped inside the tech giants’ platforms. How can we organize, how can we click “Attend,” without providing private data to Facebook or locking ourselves inside MeetUp?", "We want to develop a digital common that everyone can make their own, one which respects privacy and activism by design.": "We want to develop a digital common that everyone can make their own, one which respects privacy and activism by design.", @@ -785,7 +748,6 @@ "Bio": "Bio", "+ Start a discussion": "+ Start a discussion", "+ Add a resource": "+ Add a resource", - "+ Add a todo": "+ Add a todo", "+ Create an event": "+ Create an event", "+ Post a public message": "+ Post a public message", "A cookie is a small file containing information that is sent to your computer when you visit a website. When you visit the site again, the cookie allows that site to recognize your browser. Cookies may store user preferences and other information. You can configure your browser to refuse all cookies. However, this may result in some website features or services partially working. Local storage works the same way but allows you to store more data.": "A cookie is a small file containing information that is sent to your computer when you visit a website. When you visit the site again, the cookie allows that site to recognize your browser. Cookies may store user preferences and other information. You can configure your browser to refuse all cookies. However, this may result in some website features or services partially working. Local storage works the same way but allows you to store more data.", @@ -793,5 +755,18 @@ "No posts found": "No posts found", "Last sign-in": "Last sign-in", "Last IP adress": "Last IP adress", - "You'll need to transmit the group URL so people may access the group's profile. The group won't be findable in Mobilizon's search or regular search engines.": "You'll need to transmit the group URL so people may access the group's profile. The group won't be findable in Mobilizon's search or regular search engines." + "You'll need to transmit the group URL so people may access the group's profile. The group won't be findable in Mobilizon's search or regular search engines.": "You'll need to transmit the group URL so people may access the group's profile. The group won't be findable in Mobilizon's search or regular search engines.", + "Leave": "Leave", + "Group settings saved": "Group settings saved", + "Error": "Error", + "Registrations are restricted by allowlisting.": "Registrations are restricted by allowlisting.", + "Resend confirmation email": "Resend confirmation email", + "By {group}": "By {group}", + "Pick a profile or a group": "Pick a profile or a group", + "Add a contact": "Add a contact", + "Your profile will be shown as contact.": "Your profile will be shown as contact.", + "Pick": "Pick", + "The event will show as attributed to your personal profile.": "The event will show as attributed to your personal profile.", + "The event will show as attributed to this group.": "The event will show as attributed to this group.", + "{contact} will be displayed as contact.": "{contact} will be displayed as contact.|{contact} will be displayed as contacts." } diff --git a/js/src/i18n/fr_FR.json b/js/src/i18n/fr_FR.json index 20e064ced..65ecf0406 100644 --- a/js/src/i18n/fr_FR.json +++ b/js/src/i18n/fr_FR.json @@ -101,7 +101,7 @@ "Confirmed": "Confirmé·e", "Confirmed at": "Confirmé·e à", "Confirmed: Will happen": "Confirmé : aura lieu", - "Contact": "Contact", + "Contact": "Contact|Contacts", "Continue editing": "Continuer la modification", "Cookies and Local storage": "Cookies et stockage local", "Country": "Pays", @@ -793,5 +793,17 @@ "No posts found": "Aucun billet trouvé", "Last sign-in": "Dernière connexion", "Last IP adress": "Dernière addresse IP", - "You'll need to transmit the group URL so people may access the group's profile. The group won't be findable in Mobilizon's search or regular search engines.": "Vous aurez besoin de transmettre l'URL du groupe pour que d'autres personnes accèdent au profil du groupe. Le groupe ne sera pas trouvable dans la recherche de Mobilizon ni dans les moteurs de recherche habituels." + "You'll need to transmit the group URL so people may access the group's profile. The group won't be findable in Mobilizon's search or regular search engines.": "Vous aurez besoin de transmettre l'URL du groupe pour que d'autres personnes accèdent au profil du groupe. Le groupe ne sera pas trouvable dans la recherche de Mobilizon ni dans les moteurs de recherche habituels.", + "Pick a profile or a group": "Choisir un profil ou groupe", + "Add a contact": "Ajouter un contact", + "Your profile will be shown as contact.": "Votre profil sera affiché en tant que contact.", + "Pick": "Choisir", + "Leave": "Quitter", + "The event will show as attributed to your personal profile.": "L'événement sera affiché comme étant attribué à votre profil.", + "The event will show as attributed to this group.": "L'événement sera affiché comme étant attribué à ce groupe.", + "By {group}": "Par {group}", + "Group settings saved": "Paramètres du groupe sauvegardés", + "Error": "Erreur", + "Registrations are restricted by allowlisting.": "Les inscriptions sont restreintes par liste d'accès.", + "{contact} will be displayed as contact.": "{contact} sera affiché·e comme contact.|{contact} seront affiché·es comme contacts." } diff --git a/js/src/mixins/event.ts b/js/src/mixins/event.ts index b571579ab..57e631973 100644 --- a/js/src/mixins/event.ts +++ b/js/src/mixins/event.ts @@ -7,7 +7,7 @@ import { FETCH_EVENT, LEAVE_EVENT, } from "../graphql/event"; -import RouteName from "../router/name"; +import { SnackbarProgrammatic as Snackbar } from "buefy"; import { IPerson } from "../types/actor"; @Component @@ -80,6 +80,7 @@ export default class EventMixin extends mixins(Vue) { this.participationCancelledMessage(); } } catch (error) { + Snackbar.open({ message: error.message, type: "is-danger", position: "is-bottom" }); console.error(error); } } @@ -143,6 +144,8 @@ export default class EventMixin extends mixins(Vue) { duration: 5000, }); } catch (error) { + Snackbar.open({ message: error.message, type: "is-danger", position: "is-bottom" }); + console.error(error); } } diff --git a/js/src/router/actor.ts b/js/src/router/actor.ts index c961dea5b..012dc67ae 100644 --- a/js/src/router/actor.ts +++ b/js/src/router/actor.ts @@ -1,11 +1,9 @@ import { RouteConfig } from "vue-router"; -import GroupList from "@/views/Group/GroupList.vue"; import CreateGroup from "@/views/Group/Create.vue"; import Group from "@/views/Group/Group.vue"; import MyGroups from "@/views/Group/MyGroups.vue"; export enum ActorRouteName { - GROUP_LIST = "GroupList", GROUP = "Group", CREATE_GROUP = "CreateGroup", PROFILE = "Profile", @@ -13,12 +11,6 @@ export enum ActorRouteName { } export const actorRoutes: RouteConfig[] = [ - { - path: "/groups", - name: ActorRouteName.GROUP_LIST, - component: GroupList, - meta: { requiredAuth: false }, - }, { path: "/groups/create", name: ActorRouteName.CREATE_GROUP, diff --git a/js/src/router/guards/register-guard.ts b/js/src/router/guards/register-guard.ts index afeec4d05..b78acb63e 100644 --- a/js/src/router/guards/register-guard.ts +++ b/js/src/router/guards/register-guard.ts @@ -11,7 +11,7 @@ export const beforeRegisterGuard: NavigationGuard = async (to, from, next) => { const { config } = data; - if (!config.registrationsOpen && !config.registrationsWhitelist) { + if (!config.registrationsOpen && !config.registrationsAllowlist) { return next({ name: "Error", query: { code: ErrorCode.REGISTRATION_CLOSED }, diff --git a/js/src/types/actor/actor.model.ts b/js/src/types/actor/actor.model.ts index f0b43f97e..676d1aeb5 100644 --- a/js/src/types/actor/actor.model.ts +++ b/js/src/types/actor/actor.model.ts @@ -16,8 +16,8 @@ export interface IActor { summary: string; preferredUsername: string; suspended: boolean; - avatar: IPicture | null; - banner: IPicture | null; + avatar?: IPicture | null; + banner?: IPicture | null; type: ActorType; } diff --git a/js/src/types/config.model.ts b/js/src/types/config.model.ts index fa1b4922e..43a1177e5 100644 --- a/js/src/types/config.model.ts +++ b/js/src/types/config.model.ts @@ -8,7 +8,7 @@ export interface IConfig { contact: string; registrationsOpen: boolean; - registrationsWhitelist: boolean; + registrationsAllowlist: boolean; demoMode: boolean; countryCode: string; location: { diff --git a/js/src/types/event.model.ts b/js/src/types/event.model.ts index 2e5c22b0c..09fb2998a 100644 --- a/js/src/types/event.model.ts +++ b/js/src/types/event.model.ts @@ -151,6 +151,7 @@ export interface IEvent { tags: ITag[]; options: IEventOptions; + contacts: IActor[]; toEditJSON(): IEventEditJSON; } @@ -259,6 +260,8 @@ export class EventModel implements IEvent { tags: ITag[] = []; + contacts: IActor[] = []; + options: IEventOptions = new EventOptions(); constructor(hash?: IEvent) { @@ -296,6 +299,8 @@ export class EventModel implements IEvent { this.physicalAddress = hash.physicalAddress ? new Address(hash.physicalAddress) : undefined; this.participantStats = hash.participantStats; + this.contacts = hash.contacts; + this.tags = hash.tags; if (hash.options) this.options = hash.options; } @@ -319,6 +324,9 @@ export class EventModel implements IEvent { options: this.options, // organizerActorId: this.organizerActor && this.organizerActor.id ? this.organizerActor.id : null, attributedToId: this.attributedTo && this.attributedTo.id ? this.attributedTo.id : null, + contacts: this.contacts.map(({ id }) => ({ + id, + })), }; } } @@ -340,4 +348,5 @@ interface IEventEditJSON { physicalAddress?: IAddress; tags: string[]; options: IEventOptions; + contacts: { id?: string }[]; } diff --git a/js/src/types/login-error-code.model.ts b/js/src/types/login-error-code.model.ts index 4c812ec18..e691534a7 100644 --- a/js/src/types/login-error-code.model.ts +++ b/js/src/types/login-error-code.model.ts @@ -1,5 +1,5 @@ export enum LoginErrorCode { - NEED_TO_LOGIN = "rouge", + NEED_TO_LOGIN = "need_to_login", } export enum LoginError { diff --git a/js/src/typings/intl.d.ts b/js/src/typings/intl.d.ts new file mode 100644 index 000000000..388dd5731 --- /dev/null +++ b/js/src/typings/intl.d.ts @@ -0,0 +1,17 @@ +declare namespace Intl { + type Locale = string; + type Locales = Locale[]; + type Type = "conjunction" | "disjunction" | "unit"; + type Style = "long" | "short" | "narrow"; + type LocaleMatcher = "lookup" | "best fit"; + interface ListFormatOptions { + type: Type; + style: Style; + localeMatcher: LocaleMatcher; + } + + class ListFormat { + constructor(locales?: Locale | Locales | undefined, options?: Partial); + public format: (items: string[]) => string; + } +} diff --git a/js/src/utils/errors.ts b/js/src/utils/errors.ts deleted file mode 100644 index 752a0ffe6..000000000 --- a/js/src/utils/errors.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { i18n } from "@/utils/i18n"; - -export const refreshSuggestion = i18n.t("Please refresh the page and retry.") as string; - -export const defaultError: IError = { - match: / /, - value: i18n.t("An error has occurred.") as string, -}; - -export interface IError { - match: RegExp; - value: string | null; - suggestRefresh?: boolean; -} - -export const errors: IError[] = [ - { - match: /^Event with UUID .* not found$/, - value: i18n.t("Page not found") as string, - suggestRefresh: false, - }, - { - match: /^Event not found$/, - value: i18n.t("Event not found.") as string, - }, - { - match: /^Event with this ID .* doesn't exist$/, - value: i18n.t("Event not found.") as string, - }, - { - match: /^Error while saving report$/, - value: i18n.t("Error while saving report.") as string, - }, - { - match: /^Participant already has role rejected$/, - value: i18n.t("Participant already was rejected.") as string, - }, - { - match: /^Participant already has role participant$/, - value: i18n.t("Participant has already been approved as participant.") as string, - }, - { - match: /^You are already a participant of this event$/, - value: i18n.t("You are already a participant of this event.") as string, - }, - { - match: /NetworkError when attempting to fetch resource.$/, - value: i18n.t("Error while communicating with the server.") as string, - }, - { - match: /Provided moderator actor ID doesn't have permission on this event$/, - value: i18n.t( - "The current identity doesn't have any permission on this event. You should probably change it." - ) as string, - suggestRefresh: false, - }, - { - match: /Your email is not on the whitelist$/, - value: i18n.t("Your email is not whitelisted, you can't register.") as string, - suggestRefresh: false, - }, - { - match: /Cannot remove the last identity of a user/, - value: i18n.t("You can't remove your last identity.") as string, - suggestRefresh: false, - }, - { - match: /^No user with this email was found$/, - value: null, - }, - { - match: /^Username is already taken$/, - value: null, - }, - { - match: /^Impossible to authenticate, either your email or password are invalid.$/, - value: null, - }, - { - match: /^No user to validate with this email was found$/, - value: null, - }, - { - match: /^This email is already used.$/, - value: i18n.t("This email is already used.") as string, - }, - { - match: /^User account not confirmed$/, - value: null, - }, -]; diff --git a/js/src/utils/i18n.ts b/js/src/utils/i18n.ts index a19c3ab63..a6c867c10 100644 --- a/js/src/utils/i18n.ts +++ b/js/src/utils/i18n.ts @@ -13,3 +13,11 @@ export const i18n = new VueI18n({ messages, // set locale messages fallbackLocale: "en_US", }); + +export function formatList(list: string[]): string { + if (window.Intl && Intl.ListFormat) { + const formatter = new Intl.ListFormat(undefined, { style: "long", type: "conjunction" }); + return formatter.format(list); + } + return list.join(","); +} diff --git a/js/src/views/About/AboutInstance.vue b/js/src/views/About/AboutInstance.vue index 755a0aca8..9d569ca50 100644 --- a/js/src/views/About/AboutInstance.vue +++ b/js/src/views/About/AboutInstance.vue @@ -45,10 +45,10 @@ {{ $t("Registrations") }} - + {{ $t("Restricted") }} - + {{ $t("Open") }} {{ $t("Closed") }} diff --git a/js/src/views/Account/children/EditIdentity.vue b/js/src/views/Account/children/EditIdentity.vue index e5def75b4..f33bf4347 100644 --- a/js/src/views/Account/children/EditIdentity.vue +++ b/js/src/views/Account/children/EditIdentity.vue @@ -124,6 +124,7 @@ h1 { diff --git a/js/src/views/Group/Group.vue b/js/src/views/Group/Group.vue index 70e3a739b..3df6f06d7 100644 --- a/js/src/views/Group/Group.vue +++ b/js/src/views/Group/Group.vue @@ -33,8 +33,8 @@
    -
    - +
    +
    @@ -56,13 +56,6 @@ class="button is-outlined" >{{ $t("Group settings") }} - {{ $t("Leave group") }}
    @@ -114,6 +107,7 @@ >{{ $t("Show map") }} +
    @@ -186,50 +180,6 @@ > - - - - -
    @@ -349,7 +299,7 @@ import { Component, Prop, Vue, Watch } from "vue-property-decorator"; import EventCard from "@/components/Event/EventCard.vue"; import { CURRENT_ACTOR_CLIENT, PERSON_MEMBERSHIPS } from "@/graphql/actor"; -import { FETCH_GROUP, LEAVE_GROUP } from "@/graphql/group"; +import { FETCH_GROUP } from "@/graphql/group"; import { IActor, IGroup, @@ -369,7 +319,6 @@ import FolderItem from "@/components/Resource/FolderItem.vue"; import { Address } from "@/types/address.model"; import Invitations from "@/components/Group/Invitations.vue"; import addMinutes from "date-fns/addMinutes"; -import { Route } from "vue-router"; import GroupSection from "../../components/Group/GroupSection.vue"; import RouteName from "../../router/name"; @@ -451,16 +400,6 @@ export default class Group extends Vue { } } - async leaveGroup(): Promise { - await this.$apollo.mutate({ - mutation: LEAVE_GROUP, - variables: { - groupId: this.group.id, - }, - }); - return this.$router.push({ name: RouteName.MY_GROUPS }); - } - acceptInvitation(): void { if (this.groupMember) { const index = this.person.memberships.elements.findIndex( @@ -477,7 +416,7 @@ export default class Group extends Vue { get groupTitle(): undefined | string { if (!this.group) return undefined; - return this.group.preferredUsername; + return this.group.name || this.group.preferredUsername; } get groupSummary(): undefined | string { @@ -583,6 +522,8 @@ div.container { &.presentation { border: 2px solid $purple-2; padding: 10px 0; + position: relative; + overflow: hidden; h1 { color: $purple-1; @@ -593,6 +534,20 @@ div.container { .button.is-outlined { border-color: $purple-2; } + + & > *:not(img) { + position: relative; + z-index: 2; + } + + & > img { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: auto; + opacity: 0.3; + } } .members { diff --git a/js/src/views/Group/GroupList.vue b/js/src/views/Group/GroupList.vue deleted file mode 100644 index b70566a70..000000000 --- a/js/src/views/Group/GroupList.vue +++ /dev/null @@ -1,66 +0,0 @@ - - - diff --git a/js/src/views/Group/GroupMembers.vue b/js/src/views/Group/GroupMembers.vue index 2788c4638..c51c02b45 100644 --- a/js/src/views/Group/GroupMembers.vue +++ b/js/src/views/Group/GroupMembers.vue @@ -210,14 +210,14 @@ export default class GroupMembers extends Vue { usernameWithDomain = usernameWithDomain; - mounted() { + mounted(): void { const roleQuery = this.$route.query.role as string; if (Object.values(MemberRole).includes(roleQuery as MemberRole)) { this.roles = roleQuery as MemberRole; } } - async inviteMember() { + async inviteMember(): Promise { await this.$apollo.mutate<{ inviteMember: IMember }>({ mutation: INVITE_MEMBER, variables: { @@ -253,7 +253,7 @@ export default class GroupMembers extends Vue { } @Watch("page") - loadMoreMembers() { + loadMoreMembers(): void { this.$apollo.queries.event.fetchMore({ // New variables variables: { @@ -279,7 +279,7 @@ export default class GroupMembers extends Vue { }); } - async removeMember(memberId: string) { + async removeMember(memberId: string): Promise { await this.$apollo.mutate<{ removeMember: IMember }>({ mutation: REMOVE_MEMBER, variables: { @@ -310,15 +310,15 @@ export default class GroupMembers extends Vue { }); } - promoteMember(memberId: string) { + promoteMember(memberId: string): Promise { return this.updateMember(memberId, MemberRole.ADMINISTRATOR); } - demoteMember(memberId: string) { + demoteMember(memberId: string): Promise { return this.updateMember(memberId, MemberRole.MEMBER); } - async updateMember(memberId: string, role: MemberRole) { + async updateMember(memberId: string, role: MemberRole): Promise { await this.$apollo.mutate<{ updateMember: IMember }>({ mutation: UPDATE_MEMBER, variables: { diff --git a/js/src/views/Group/GroupSettings.vue b/js/src/views/Group/GroupSettings.vue index 0ad4ea423..056febd82 100644 --- a/js/src/views/Group/GroupSettings.vue +++ b/js/src/views/Group/GroupSettings.vue @@ -37,8 +37,23 @@ - + + + + + + +

    {{ $t("Group visibility") }}

    import("../../components/Editor.vue"), }, }) @@ -141,6 +158,10 @@ export default class GroupSettings extends Vue { newMemberUsername = ""; + avatarFile: File | null = null; + + bannerFile: File | null = null; + usernameWithDomain = usernameWithDomain; GroupVisibility = { @@ -151,19 +172,12 @@ export default class GroupSettings extends Vue { showCopiedTooltip = false; async updateGroup(): Promise { - const variables = { ...this.group }; - // eslint-disable-next-line - // @ts-ignore - delete variables.__typename; - if (variables.physicalAddress) { - // eslint-disable-next-line - // @ts-ignore - delete variables.physicalAddress.__typename; - } + const variables = this.buildVariables(); await this.$apollo.mutate<{ updateGroup: IGroup }>({ mutation: UPDATE_GROUP, variables, }); + this.$notifier.success(this.$t("Group settings saved") as string); } confirmDeleteGroup(): void { @@ -198,6 +212,52 @@ export default class GroupSettings extends Vue { }, 2000); } + private buildVariables() { + let avatarObj = {}; + let bannerObj = {}; + const variables = { ...this.group }; + + // eslint-disable-next-line + // @ts-ignore + delete variables.__typename; + if (variables.physicalAddress) { + // eslint-disable-next-line + // @ts-ignore + delete variables.physicalAddress.__typename; + } + delete variables.avatar; + delete variables.banner; + + if (this.avatarFile) { + avatarObj = { + avatar: { + picture: { + name: this.avatarFile.name, + alt: `${this.group.preferredUsername}'s avatar`, + file: this.avatarFile, + }, + }, + }; + } + + if (this.bannerFile) { + bannerObj = { + banner: { + picture: { + name: this.bannerFile.name, + alt: `${this.group.preferredUsername}'s banner`, + file: this.bannerFile, + }, + }, + }; + } + return { + ...variables, + ...avatarObj, + ...bannerObj, + }; + } + // eslint-disable-next-line class-methods-use-this get canShowCopyButton(): boolean { return window.isSecureContext; diff --git a/js/src/views/Group/MyGroups.vue b/js/src/views/Group/MyGroups.vue index 86271c83e..06051349d 100644 --- a/js/src/views/Group/MyGroups.vue +++ b/js/src/views/Group/MyGroups.vue @@ -8,7 +8,11 @@ ) }}

    - {{ $t("Create group") }} +
    + {{ + $t("Create group") + }} +
    - +
    {{ $t("No groups found") }} @@ -27,10 +37,13 @@