diff --git a/js/package.json b/js/package.json index 0cbbe9b0e..6bd24a65f 100644 --- a/js/package.json +++ b/js/package.json @@ -67,14 +67,14 @@ "@types/vuedraggable": "^2.23.0", "@typescript-eslint/eslint-plugin": "^2.26.0", "@typescript-eslint/parser": "^2.26.0", - "@vue/cli-plugin-babel": "~4.5.2", - "@vue/cli-plugin-e2e-cypress": "~4.5.2", - "@vue/cli-plugin-eslint": "~4.5.2", - "@vue/cli-plugin-pwa": "~4.5.2", - "@vue/cli-plugin-router": "~4.5.2", - "@vue/cli-plugin-typescript": "~4.5.2", - "@vue/cli-plugin-unit-mocha": "~4.5.2", - "@vue/cli-service": "~4.5.2", + "@vue/cli-plugin-babel": "~4.5.3", + "@vue/cli-plugin-e2e-cypress": "~4.5.3", + "@vue/cli-plugin-eslint": "~4.5.3", + "@vue/cli-plugin-pwa": "~4.5.3", + "@vue/cli-plugin-router": "~4.5.3", + "@vue/cli-plugin-typescript": "~4.5.3", + "@vue/cli-plugin-unit-mocha": "~4.5.3", + "@vue/cli-service": "~4.5.3", "@vue/eslint-config-airbnb": "^5.0.2", "@vue/eslint-config-prettier": "^6.0.0", "@vue/eslint-config-typescript": "^5.0.2", diff --git a/js/src/components/Comment/Comment.vue b/js/src/components/Comment/Comment.vue index e2b7be14f..c1a73eb2e 100644 --- a/js/src/components/Comment/Comment.vue +++ b/js/src/components/Comment/Comment.vue @@ -255,7 +255,7 @@ export default class Comment extends Vue { get commentFromOrganizer(): boolean { return ( this.event.organizerActor !== undefined && - this.comment.actor && + this.comment.actor != null && this.comment.actor.id === this.event.organizerActor.id ); } @@ -272,6 +272,7 @@ export default class Comment extends Vue { } reportModal() { + if (!this.comment.actor) return; this.$buefy.modal.open({ parent: this, component: ReportModal, @@ -286,6 +287,7 @@ export default class Comment extends Vue { async reportComment(content: string, forward: boolean) { try { + if (!this.comment.actor) return; await this.$apollo.mutate({ mutation: CREATE_REPORT, variables: { diff --git a/js/src/components/Comment/CommentTree.vue b/js/src/components/Comment/CommentTree.vue index f92869be0..48b51be2e 100644 --- a/js/src/components/Comment/CommentTree.vue +++ b/js/src/components/Comment/CommentTree.vue @@ -106,6 +106,7 @@ export default class CommentTree extends Vue { async createCommentForEvent(comment: IComment) { try { + if (!comment.actor) return; await this.$apollo.mutate({ mutation: CREATE_COMMENT_FROM_EVENT, variables: { diff --git a/js/src/components/Discussion/DiscussionComment.vue b/js/src/components/Discussion/DiscussionComment.vue index 6724dc4d6..e1329e630 100644 --- a/js/src/components/Discussion/DiscussionComment.vue +++ b/js/src/components/Discussion/DiscussionComment.vue @@ -8,26 +8,102 @@
-
- @{{ comment.actor.preferredUsername }} -
+ + {{ comment.actor.name }} + @{{ usernameWithDomain(comment.actor) }} + + + {{ $t("[deleted]") }} + + + + + + + + {{ $t("Edit") }} + + + + {{ $t("Delete") }} + + + + {{ $t("Report") }} + + +
-
+
+
{{ $t("[This comment has been deleted]") }}
+
+ +
+ {{ $t("Update") }} + {{ $t("Cancel") }} +
+
diff --git a/js/src/components/Discussion/DiscussionListItem.vue b/js/src/components/Discussion/DiscussionListItem.vue index 5f254af10..280704893 100644 --- a/js/src/components/Discussion/DiscussionListItem.vue +++ b/js/src/components/Discussion/DiscussionListItem.vue @@ -4,14 +4,25 @@ :to="{ name: RouteName.DISCUSSION, params: { slug: discussion.slug, id: discussion.id } }" >
-
+
-

{{ discussion.title }}

-
{{ htmlTextEllipsis }}
+
+

{{ discussion.title }}

+ + {{ $timeAgo.format(new Date(discussion.updatedAt), "twitter") || $t("Right now") }} +
+
+ {{ htmlTextEllipsis }} +
+
{{ $t("[This comment has been deleted]") }}
@@ -28,7 +39,7 @@ export default class DiscussionListItem extends Vue { get htmlTextEllipsis() { const element = document.createElement("div"); - if (this.discussion.lastComment) { + if (this.discussion.lastComment && this.discussion.lastComment.text) { element.innerHTML = this.discussion.lastComment.text .replace(//gi, " ") .replace(/

/gi, " "); @@ -53,11 +64,17 @@ export default class DiscussionListItem extends Vue { .title-info-wrapper { flex: 2; - .discussion-minimalist-title { - color: #3c376e; - font-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica, Arial, serif; - font-size: 1.25rem; - font-weight: 700; + .title-and-date { + display: flex; + align-items: center; + + .discussion-minimalist-title { + color: #3c376e; + font-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica, Arial, serif; + font-size: 1.25rem; + font-weight: 700; + flex: 1; + } } div.has-text-grey { diff --git a/js/src/components/Editor.vue b/js/src/components/Editor.vue index 5c509fd6d..d6a077998 100644 --- a/js/src/components/Editor.vue +++ b/js/src/components/Editor.vue @@ -446,6 +446,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) { + if (!comment.actor) return; const actorModel = new Actor(comment.actor); if (!this.editor) return; this.editor.commands.mention({ diff --git a/js/src/components/Group/InvitationCard.vue b/js/src/components/Group/InvitationCard.vue index afcbbd23e..5c3de92b5 100644 --- a/js/src/components/Group/InvitationCard.vue +++ b/js/src/components/Group/InvitationCard.vue @@ -2,13 +2,9 @@

-

- {{ - $t("You have been invited by {invitedBy} to the following group:", { - invitedBy: member.invitedBy.name, - }) - }} -

+ + {{ member.invitedBy.name }} +
@@ -43,7 +39,7 @@
- + {{ $t("Decline") }}
diff --git a/js/src/components/Group/Invitations.vue b/js/src/components/Group/Invitations.vue new file mode 100644 index 000000000..9dadff141 --- /dev/null +++ b/js/src/components/Group/Invitations.vue @@ -0,0 +1,50 @@ + + diff --git a/js/src/graphql/actor.ts b/js/src/graphql/actor.ts index 0d9df8208..c1be5e61a 100644 --- a/js/src/graphql/actor.ts +++ b/js/src/graphql/actor.ts @@ -1,7 +1,4 @@ import gql from "graphql-tag"; -import { DISCUSSION_BASIC_FIELDS_FRAGMENT } from "@/graphql/discussion"; -import { RESOURCE_METADATA_BASIC_FIELDS_FRAGMENT } from "@/graphql/resources"; -import { POST_BASIC_FIELDS } from "./post"; export const FETCH_PERSON = gql` query($username: String!) { @@ -349,6 +346,13 @@ export const PERSON_MEMBERSHIPS = gql` url } } + invitedBy { + id + preferredUsername + name + } + insertedAt + updatedAt } } } @@ -424,209 +428,6 @@ export const REGISTER_PERSON = gql` } `; -export const LIST_GROUPS = gql` - query { - groups { - elements { - id - url - name - domain - summary - preferredUsername - suspended - avatar { - url - } - banner { - url - } - organizedEvents { - elements { - uuid - title - beginsOn - } - total - } - } - total - } - } -`; - -export const FETCH_GROUP = gql` - query($name: String!) { - group(preferredUsername: $name) { - id - url - name - domain - summary - preferredUsername - suspended - visibility - physicalAddress { - description - street - locality - postalCode - region - country - geom - type - id - originId - } - avatar { - url - } - banner { - url - } - organizedEvents { - elements { - id - uuid - title - beginsOn - } - total - } - discussions { - total - elements { - ...DiscussionBasicFields - } - } - posts { - total - elements { - ...PostBasicFields - } - } - members { - elements { - role - actor { - id - name - domain - preferredUsername - avatar { - url - } - } - insertedAt - } - total - } - resources(page: 1, limit: 3) { - elements { - id - title - resourceUrl - summary - updatedAt - type - path - metadata { - ...ResourceMetadataBasicFields - } - } - total - } - todoLists { - elements { - id - title - todos { - elements { - id - title - status - dueDate - assignedTo { - id - preferredUsername - } - } - total - } - } - total - } - } - } - ${DISCUSSION_BASIC_FIELDS_FRAGMENT} - ${POST_BASIC_FIELDS} - ${RESOURCE_METADATA_BASIC_FIELDS_FRAGMENT} -`; - -export const CREATE_GROUP = gql` - mutation CreateGroup( - $creatorActorId: ID! - $preferredUsername: String! - $name: String! - $summary: String - $avatar: PictureInput - $banner: PictureInput - ) { - createGroup( - creatorActorId: $creatorActorId - preferredUsername: $preferredUsername - name: $name - summary: $summary - banner: $banner - avatar: $avatar - ) { - id - preferredUsername - name - summary - avatar { - url - } - banner { - url - } - } - } -`; - -export const UPDATE_GROUP = gql` - mutation UpdateGroup( - $id: ID! - $name: String - $summary: String - $avatar: PictureInput - $banner: PictureInput - $visibility: GroupVisibility - $physicalAddress: AddressInput - ) { - updateGroup( - id: $id - name: $name - summary: $summary - banner: $banner - avatar: $avatar - visibility: $visibility - physicalAddress: $physicalAddress - ) { - id - preferredUsername - name - summary - avatar { - url - } - banner { - url - } - } - } -`; - export const SUSPEND_PROFILE = gql` mutation SuspendProfile($id: ID!) { suspendProfile(id: $id) { diff --git a/js/src/graphql/comment.ts b/js/src/graphql/comment.ts index c00c28e5a..bb1be002c 100644 --- a/js/src/graphql/comment.ts +++ b/js/src/graphql/comment.ts @@ -86,8 +86,8 @@ export const CREATE_COMMENT_FROM_EVENT = gql` `; export const DELETE_COMMENT = gql` - mutation DeleteComment($commentId: ID!, $actorId: ID!) { - deleteComment(commentId: $commentId, actorId: $actorId) { + mutation DeleteComment($commentId: ID!) { + deleteComment(commentId: $commentId) { id } } @@ -99,4 +99,5 @@ export const UPDATE_COMMENT = gql` ...CommentFields } } + ${COMMENT_FIELDS_FRAGMENT} `; diff --git a/js/src/graphql/discussion.ts b/js/src/graphql/discussion.ts index 035aa83db..dbfd9df55 100644 --- a/js/src/graphql/discussion.ts +++ b/js/src/graphql/discussion.ts @@ -5,6 +5,7 @@ export const DISCUSSION_BASIC_FIELDS_FRAGMENT = gql` id title slug + updatedAt lastComment { id text @@ -15,6 +16,7 @@ export const DISCUSSION_BASIC_FIELDS_FRAGMENT = gql` url } } + deletedAt } } `; @@ -110,6 +112,7 @@ export const GET_DISCUSSION = gql` } insertedAt updatedAt + deletedAt } } ...DiscussionFields diff --git a/js/src/graphql/group.ts b/js/src/graphql/group.ts new file mode 100644 index 000000000..7f45ccf83 --- /dev/null +++ b/js/src/graphql/group.ts @@ -0,0 +1,215 @@ +import gql from "graphql-tag"; +import { DISCUSSION_BASIC_FIELDS_FRAGMENT } from "./discussion"; +import { RESOURCE_METADATA_BASIC_FIELDS_FRAGMENT } from "./resources"; +import { POST_BASIC_FIELDS } from "./post"; + +export const LIST_GROUPS = gql` + query { + groups { + elements { + id + url + name + domain + summary + preferredUsername + suspended + avatar { + url + } + banner { + url + } + organizedEvents { + elements { + uuid + title + beginsOn + } + total + } + } + total + } + } +`; + +export const FETCH_GROUP = gql` + query($name: String!) { + group(preferredUsername: $name) { + id + url + name + domain + summary + preferredUsername + suspended + visibility + physicalAddress { + description + street + locality + postalCode + region + country + geom + type + id + originId + } + avatar { + url + } + banner { + url + } + organizedEvents { + elements { + id + uuid + title + beginsOn + } + total + } + discussions { + total + elements { + ...DiscussionBasicFields + } + } + posts { + total + elements { + ...PostBasicFields + } + } + members { + elements { + role + actor { + id + name + domain + preferredUsername + avatar { + url + } + } + insertedAt + } + total + } + resources(page: 1, limit: 3) { + elements { + id + title + resourceUrl + summary + updatedAt + type + path + metadata { + ...ResourceMetadataBasicFields + } + } + total + } + todoLists { + elements { + id + title + todos { + elements { + id + title + status + dueDate + assignedTo { + id + preferredUsername + } + } + total + } + } + total + } + } + } + ${DISCUSSION_BASIC_FIELDS_FRAGMENT} + ${POST_BASIC_FIELDS} + ${RESOURCE_METADATA_BASIC_FIELDS_FRAGMENT} +`; + +export const CREATE_GROUP = gql` + mutation CreateGroup( + $creatorActorId: ID! + $preferredUsername: String! + $name: String! + $summary: String + $avatar: PictureInput + $banner: PictureInput + ) { + createGroup( + creatorActorId: $creatorActorId + preferredUsername: $preferredUsername + name: $name + summary: $summary + banner: $banner + avatar: $avatar + ) { + id + preferredUsername + name + summary + avatar { + url + } + banner { + url + } + } + } +`; + +export const UPDATE_GROUP = gql` + mutation UpdateGroup( + $id: ID! + $name: String + $summary: String + $avatar: PictureInput + $banner: PictureInput + $visibility: GroupVisibility + $physicalAddress: AddressInput + ) { + updateGroup( + id: $id + name: $name + summary: $summary + banner: $banner + avatar: $avatar + visibility: $visibility + physicalAddress: $physicalAddress + ) { + id + preferredUsername + name + summary + avatar { + url + } + banner { + url + } + } + } +`; + +export const LEAVE_GROUP = gql` + mutation LeaveGroup($groupId: ID!) { + leaveGroup(groupId: $groupId) { + id + } + } +`; diff --git a/js/src/graphql/member.ts b/js/src/graphql/member.ts index 2331cc35d..fe2e704bf 100644 --- a/js/src/graphql/member.ts +++ b/js/src/graphql/member.ts @@ -1,23 +1,52 @@ import gql from "graphql-tag"; +export const MEMBER_FRAGMENT = gql` + fragment MemberFragment on Member { + id + role + parent { + id + preferredUsername + domain + name + avatar { + url + } + } + actor { + id + preferredUsername + domain + name + avatar { + url + } + } + insertedAt + } +`; + export const INVITE_MEMBER = gql` mutation InviteMember($groupId: ID!, $targetActorUsername: String!) { inviteMember(groupId: $groupId, targetActorUsername: $targetActorUsername) { - id - role - parent { - id - } - actor { - id - } + ...MemberFragment } } + ${MEMBER_FRAGMENT} `; export const ACCEPT_INVITATION = gql` mutation AcceptInvitation($id: ID!) { acceptInvitation(id: $id) { + ...MemberFragment + } + } + ${MEMBER_FRAGMENT} +`; + +export const REJECT_INVITATION = gql` + mutation RejectInvitation($id: ID!) { + rejectInvitation(id: $id) { id } } @@ -33,6 +62,7 @@ export const GROUP_MEMBERS = gql` preferredUsername members(page: $page, limit: $limit, roles: $roles) { elements { + id role actor { id @@ -50,3 +80,11 @@ export const GROUP_MEMBERS = gql` } } `; + +export const REMOVE_MEMBER = gql` + mutation RemoveMember($groupId: ID!, $memberId: ID!) { + removeMember(groupId: $groupId, memberId: $memberId) { + id + } + } +`; diff --git a/js/src/i18n/en_US.json b/js/src/i18n/en_US.json index 42639605c..6fbe6f0dc 100644 --- a/js/src/i18n/en_US.json +++ b/js/src/i18n/en_US.json @@ -756,5 +756,9 @@ "No ongoing todos": "No ongoing todos", "No discussions yet": "No discussions yet", "Add / Remove…": "Add / Remove…", - "No public posts": "No public posts" + "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" } diff --git a/js/src/i18n/fr_FR.json b/js/src/i18n/fr_FR.json index 1d65246f5..f02a5eedd 100644 --- a/js/src/i18n/fr_FR.json +++ b/js/src/i18n/fr_FR.json @@ -757,5 +757,9 @@ "No ongoing todos": "Pas de todos en cours", "No discussions yet": "Pas encore de discussions", "Add / Remove…": "Ajouter / Supprimer…", - "No public posts": "Pas de billets publics" + "No public posts": "Pas de billets publics", + "You have been removed from this group's members.": "Vous avez été exclu des membres de ce groupe.", + "Since you are a new member, private content can take a few minutes to appear.": "Étant donné que vous êtes un·e nouveau·elle membre, le contenu privé peut mettre quelques minutes à arriver.", + "Leave group": "Quitter le groupe", + "Remove": "Exclure" } diff --git a/js/src/types/actor/group.model.ts b/js/src/types/actor/group.model.ts index 759d8191c..374dade95 100644 --- a/js/src/types/actor/group.model.ts +++ b/js/src/types/actor/group.model.ts @@ -33,6 +33,8 @@ export interface IMember { parent: IGroup; actor: IActor; invitedBy?: IPerson; + insertedAt: string; + updatedAt: string; } export class Group extends Actor implements IGroup { diff --git a/js/src/types/comment.model.ts b/js/src/types/comment.model.ts index 6e94df524..f9315378b 100644 --- a/js/src/types/comment.model.ts +++ b/js/src/types/comment.model.ts @@ -7,7 +7,7 @@ export interface IComment { url?: string; text: string; local: boolean; - actor: IActor; + actor: IActor | null; inReplyToComment?: IComment; originComment?: IComment; replies: IComment[]; @@ -56,7 +56,7 @@ export class CommentModel implements IComment { this.text = hash.text; this.inReplyToComment = hash.inReplyToComment; this.originComment = hash.originComment; - this.actor = new Actor(hash.actor); + this.actor = hash.actor ? new Actor(hash.actor) : new Actor(); this.event = new EventModel(hash.event); this.replies = hash.replies; this.updatedAt = hash.updatedAt; diff --git a/js/src/views/Discussions/Create.vue b/js/src/views/Discussions/Create.vue index e79ce3ade..3330c829e 100644 --- a/js/src/views/Discussions/Create.vue +++ b/js/src/views/Discussions/Create.vue @@ -19,7 +19,8 @@ diff --git a/js/src/views/Group/GroupSettings.vue b/js/src/views/Group/GroupSettings.vue index 9eb28fb13..7e75f6234 100644 --- a/js/src/views/Group/GroupSettings.vue +++ b/js/src/views/Group/GroupSettings.vue @@ -100,7 +100,7 @@ diff --git a/js/src/views/Group/Settings.vue b/js/src/views/Group/Settings.vue index 31fb716ab..144498ea4 100644 --- a/js/src/views/Group/Settings.vue +++ b/js/src/views/Group/Settings.vue @@ -26,7 +26,7 @@ import { Component, Vue, Watch } from "vue-property-decorator"; import { Route } from "vue-router"; import { IGroup, IPerson } from "@/types/actor"; -import { FETCH_GROUP } from "@/graphql/actor"; +import { FETCH_GROUP } from "@/graphql/group"; import RouteName from "../../router/name"; import SettingMenuSection from "../../components/Settings/SettingMenuSection.vue"; import SettingMenuItem from "../../components/Settings/SettingMenuItem.vue"; diff --git a/js/src/views/Posts/Edit.vue b/js/src/views/Posts/Edit.vue index 1118e7125..ea32807ef 100644 --- a/js/src/views/Posts/Edit.vue +++ b/js/src/views/Posts/Edit.vue @@ -79,7 +79,8 @@