diff --git a/.graphqlconfig.yaml b/.graphqlconfig.yaml index 97d01151f..6058dcaba 100644 --- a/.graphqlconfig.yaml +++ b/.graphqlconfig.yaml @@ -3,4 +3,6 @@ projects: schemaPath: schema.graphql extensions: endpoints: - dev: 'http://localhost:4000/api' + dev: + url: 'http://localhost:4001/api' + introspect: true diff --git a/js/src/App.vue b/js/src/App.vue index 5df5c8f2b..2d05f65b5 100644 --- a/js/src/App.vue +++ b/js/src/App.vue @@ -79,6 +79,7 @@ export default class App extends Vue { @import "~bulma/sass/elements/tag.sass"; @import "~bulma/sass/components/navbar.sass"; @import "~bulma/sass/components/modal.sass"; + @import "~bulma/sass/components/media.sass"; @import "~bulma/sass/grid/_all.sass"; @import "~bulma/sass/layout/section.sass"; @import "~bulma/sass/layout/footer.sass"; diff --git a/js/src/components/Account/CreateIdentity.vue b/js/src/components/Account/CreateIdentity.vue new file mode 100644 index 000000000..ec5bdd7fe --- /dev/null +++ b/js/src/components/Account/CreateIdentity.vue @@ -0,0 +1,44 @@ +<template> + <!-- TODO --> +</template> + +<script lang="ts"> + import { Component, Vue } from 'vue-property-decorator'; + import { CREATE_PERSON, LOGGED_PERSON } from '../../graphql/actor'; + import { IPerson } from '@/types/actor'; + + @Component({ + apollo: { + loggedPerson: { + query: LOGGED_PERSON, + }, + }, + }) + export default class Identities extends Vue { + loggedPerson!: IPerson; + errors: string[] = []; + newPerson!: IPerson; + + async createProfile(e) { + e.preventDefault(); + + try { + await this.$apollo.mutate({ + mutation: CREATE_PERSON, + variables: this.newPerson, + }); + + this.$apollo.queries.identities.refresh(); + } catch (err) { + console.error(err); + err.graphQLErrors.forEach(({ message }) => { + this.errors.push(message); + }); + } + } + + host() { + return `@${window.location.host}`; + } + } +</script> diff --git a/js/src/components/Account/Identities.vue b/js/src/components/Account/Identities.vue new file mode 100644 index 000000000..d1337db96 --- /dev/null +++ b/js/src/components/Account/Identities.vue @@ -0,0 +1,82 @@ +<template> + <section> + <h1 class="title"> + <translate>My identities</translate> + </h1> + + <ul class="identities"> + <li v-for="identity in identities" :key="identity.id"> + <div class="media identity" v-bind:class="{ 'is-current-identity': isCurrentIdentity(identity) }"> + <div class="media-left"> + <figure class="image is-48x48"> + <img class="is-rounded" :src="identity.avatarUrl"> + </figure> + </div> + + <div class="media-content"> + {{ identity.displayName() }} + </div> + </div> + </li> + </ul> + + <a class="button create-identity is-primary"> + <translate>Create a new identity</translate> + </a> + </section> +</template> + +<style lang="scss" scoped> + .identities { + border-right: 1px solid grey; + + padding: 15px 0; + } + + .media.identity { + align-items: center; + font-size: 1.3rem; + padding-bottom: 0; + margin-bottom: 15px; + + &.is-current-identity { + background-color: rgba(0, 0, 0, 0.1); + } + } + + .title { + margin-bottom: 30px; + } +</style> + +<script lang="ts"> + import { Component, Vue } from 'vue-property-decorator'; + import { IDENTITIES, LOGGED_PERSON } from '@/graphql/actor'; + import { IPerson, Person } from '@/types/actor'; + + @Component({ + apollo: { + loggedPerson: { + query: LOGGED_PERSON, + }, + }, + }) + export default class Identities extends Vue { + identities: Person[] = []; + loggedPerson!: IPerson; + errors: string[] = []; + + async mounted() { + const result = await this.$apollo.query({ + query: IDENTITIES, + }); + + this.identities = result.data.identities + .map(i => new Person(i)); + } + + isCurrentIdentity(identity: IPerson) { + return identity.preferredUsername === this.loggedPerson.preferredUsername; + } + } +</script> diff --git a/js/src/components/Event/EventCard.vue b/js/src/components/Event/EventCard.vue index 1fcd23f72..e551cbcca 100644 --- a/js/src/components/Event/EventCard.vue +++ b/js/src/components/Event/EventCard.vue @@ -47,7 +47,7 @@ import { IEvent, ParticipantRole } from '@/types/event.model'; import { Component, Prop, Vue } from 'vue-property-decorator'; import DateCalendarIcon from '@/components/Event/DateCalendarIcon.vue'; -import { IActor, IPerson, Person } from '@/types/actor.model'; +import { IActor, IPerson, Person } from '@/types/actor'; const lineClamp = require('line-clamp'); export interface IEventCardOptions { diff --git a/js/src/components/Group/GroupCard.vue b/js/src/components/Group/GroupCard.vue index f5d2336d4..cb8137624 100644 --- a/js/src/components/Group/GroupCard.vue +++ b/js/src/components/Group/GroupCard.vue @@ -20,7 +20,7 @@ <script lang="ts"> import { Component, Prop, Vue } from 'vue-property-decorator'; -import { Group } from '@/types/actor.model'; +import { Group } from '@/types/actor'; import { RouteName } from '@/router'; @Component diff --git a/js/src/components/NavBar.vue b/js/src/components/NavBar.vue index 3963a26e0..1a393482e 100644 --- a/js/src/components/NavBar.vue +++ b/js/src/components/NavBar.vue @@ -36,9 +36,9 @@ </div> <div class="navbar-item has-dropdown is-hoverable" v-else> <router-link - class="navbar-link" - v-if="currentUser.isLoggedIn && loggedPerson" - :to="{ name: 'Profile', params: { name: loggedPerson.preferredUsername} }" + class="navbar-link" + v-if="currentUser.isLoggedIn && loggedPerson" + :to="{ name: 'MyAccount' }" > <figure class="image is-24x24"> <img :src="loggedPerson.avatarUrl"> @@ -47,8 +47,13 @@ </router-link> <div class="navbar-dropdown"> - <a class="navbar-item"><translate>My account</translate></a> - <a class="navbar-item" v-on:click="logout()"><translate>Log out</translate></a> + <router-link :to="{ name: 'MyAccount' }" class="navbar-item"> + <translate>My account</translate> + </router-link> + + <a class="navbar-item" v-on:click="logout()"> + <translate>Log out</translate> + </a> </div> </div> </div> @@ -63,7 +68,7 @@ import { CURRENT_USER_CLIENT, UPDATE_CURRENT_USER_CLIENT } from '@/graphql/user' import { onLogout } from '@/vue-apollo'; import { deleteUserData } from '@/utils/auth'; import { LOGGED_PERSON } from '@/graphql/actor'; -import { IPerson } from '@/types/actor.model'; +import { IPerson } from '@/types/actor'; import { CONFIG } from '@/graphql/config'; import { IConfig } from '@/types/config.model'; import { ICurrentUser } from '@/types/current-user.model'; diff --git a/js/src/router/actor.ts b/js/src/router/actor.ts index a8dd4f904..0bb02beb4 100644 --- a/js/src/router/actor.ts +++ b/js/src/router/actor.ts @@ -1,25 +1,19 @@ import Profile from '@/views/Account/Profile.vue'; +import MyAccount from '@/views/Account/MyAccount.vue'; import CreateGroup from '@/views/Group/Create.vue'; import Group from '@/views/Group/Group.vue'; import GroupList from '@/views/Group/GroupList.vue'; -import Identities from '@/views/Account/Identities.vue'; import { RouteConfig } from 'vue-router'; export enum ActorRouteName { - IDENTITIES = 'Identities', GROUP_LIST = 'GroupList', GROUP = 'Group', CREATE_GROUP = 'CreateGroup', PROFILE = 'Profile', + MY_ACCOUNT = 'MyAccount', } export const actorRoutes: RouteConfig[] = [ - { - path: '/identities', - name: ActorRouteName.IDENTITIES, - component: Identities, - meta: { requiredAuth: true }, - }, { path: '/groups', name: ActorRouteName.GROUP_LIST, @@ -46,4 +40,11 @@ export const actorRoutes: RouteConfig[] = [ props: true, meta: { requiredAuth: false }, }, + { + path: '/my-account', + name: ActorRouteName.MY_ACCOUNT, + component: MyAccount, + props: true, + meta: { requiredAuth: true }, + }, ]; diff --git a/js/src/types/actor.model.ts b/js/src/types/actor.model.ts deleted file mode 100644 index d08aa8f06..000000000 --- a/js/src/types/actor.model.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { ICurrentUser } from '@/types/current-user.model'; -import { IEvent } from '@/types/event.model'; - -export interface IActor { - id?: string; - url: string; - name: string; - domain: string|null; - summary: string; - preferredUsername: string; - suspended: boolean; - avatarUrl: string; - bannerUrl: string; -} - -export class Actor implements IActor { - avatarUrl: string = ''; - bannerUrl: string = ''; - domain: string | null = null; - name: string = ''; - preferredUsername: string = ''; - summary: string = ''; - suspended: boolean = false; - url: string = ''; - - get displayNameAndUsername(): string { - return `${this.name} (${this.usernameWithDomain})`; - } - - public usernameWithDomain(): string { - const domain = this.domain ? `@${this.domain}` : ''; - return `@${this.preferredUsername}${domain}`; - } - - public displayName(): string { - return this.name != null && this.name !== '' ? this.name : this.usernameWithDomain(); - } -} - -export interface IPerson extends IActor { - feedTokens: IFeedToken[]; - goingToEvents: IEvent[]; -} - -export interface IGroup extends IActor { - members: IMember[]; -} - -export class Person extends Actor implements IPerson { - feedTokens: IFeedToken[] = []; - goingToEvents: IEvent[] = []; -} - -export class Group extends Actor implements IGroup { - members: IMember[] = []; -} - -export interface IFeedToken { - token: string; - actor?: IPerson; - user: ICurrentUser; -} - -export enum MemberRole { - PENDING, - MEMBER, - MODERATOR, - ADMIN, -} - -export interface IMember { - role: MemberRole; - parent: IGroup; - actor: IActor; -} diff --git a/js/src/types/actor/actor.model.ts b/js/src/types/actor/actor.model.ts new file mode 100644 index 000000000..c2ae01efb --- /dev/null +++ b/js/src/types/actor/actor.model.ts @@ -0,0 +1,39 @@ +export interface IActor { + id?: string; + url: string; + name: string; + domain: string|null; + summary: string; + preferredUsername: string; + suspended: boolean; + avatarUrl: string; + bannerUrl: string; +} + +export class Actor implements IActor { + avatarUrl: string = ''; + bannerUrl: string = ''; + domain: string | null = null; + name: string = ''; + preferredUsername: string = ''; + summary: string = ''; + suspended: boolean = false; + url: string = ''; + + constructor (hash: IActor | {} = {}) { + Object.assign(this, hash); + } + + get displayNameAndUsername(): string { + return `${this.name} (${this.usernameWithDomain})`; + } + + usernameWithDomain(): string { + const domain = this.domain ? `@${this.domain}` : ''; + return `@${this.preferredUsername}${domain}`; + } + + displayName(): string { + return this.name != null && this.name !== '' ? this.name : this.usernameWithDomain(); + } +} diff --git a/js/src/types/actor/group.model.ts b/js/src/types/actor/group.model.ts new file mode 100644 index 000000000..d7dcd0566 --- /dev/null +++ b/js/src/types/actor/group.model.ts @@ -0,0 +1,22 @@ +import { Actor, IActor } from '@/types/actor/actor.model'; + +export enum MemberRole { + PENDING, + MEMBER, + MODERATOR, + ADMIN, +} + +export interface IGroup extends IActor { + members: IMember[]; +} + +export interface IMember { + role: MemberRole; + parent: IGroup; + actor: IActor; +} + +export class Group extends Actor implements IGroup { + members: IMember[] = []; +} diff --git a/js/src/types/actor/index.ts b/js/src/types/actor/index.ts new file mode 100644 index 000000000..a35dc2de3 --- /dev/null +++ b/js/src/types/actor/index.ts @@ -0,0 +1,3 @@ +export * from './actor.model'; +export * from './group.model'; +export * from './person.model'; diff --git a/js/src/types/actor/person.model.ts b/js/src/types/actor/person.model.ts new file mode 100644 index 000000000..d94e10649 --- /dev/null +++ b/js/src/types/actor/person.model.ts @@ -0,0 +1,25 @@ +import { ICurrentUser } from '@/types/current-user.model'; +import { IEvent } from '@/types/event.model'; +import { Actor, IActor } from '@/types/actor/actor.model'; + +export interface IFeedToken { + token: string; + actor?: IPerson; + user: ICurrentUser; +} + +export interface IPerson extends IActor { + feedTokens: IFeedToken[]; + goingToEvents: IEvent[]; +} + +export class Person extends Actor implements IPerson { + feedTokens: IFeedToken[] = []; + goingToEvents: IEvent[] = []; + + constructor(hash: IPerson | {} = {}) { + super(hash); + + Object.assign(this, hash); + } +} diff --git a/js/src/types/event.model.ts b/js/src/types/event.model.ts index c5f9cafbb..ca6ec65a8 100644 --- a/js/src/types/event.model.ts +++ b/js/src/types/event.model.ts @@ -1,6 +1,5 @@ -import { Actor, IActor } from './actor.model'; +import { Actor, IActor } from './actor'; import { IAddress } from '@/types/address.model'; -import { ITag } from '@/types/tag.model'; export enum EventStatus { TENTATIVE, diff --git a/js/src/types/search.model.ts b/js/src/types/search.model.ts index 615f41dab..1bd731439 100644 --- a/js/src/types/search.model.ts +++ b/js/src/types/search.model.ts @@ -1,4 +1,4 @@ -import { IGroup } from '@/types/actor.model'; +import { IGroup } from '@/types/actor'; import { IEvent } from '@/types/event.model'; export interface SearchEvent { diff --git a/js/src/utils/html.ts b/js/src/utils/html.ts new file mode 100644 index 000000000..036b1c0e3 --- /dev/null +++ b/js/src/utils/html.ts @@ -0,0 +1,3 @@ +export function nl2br(text: string) { + return text.replace(/(?:\r\n|\r|\n)/g, '<br>'); +} diff --git a/js/src/views/Account/Identities.vue b/js/src/views/Account/Identities.vue deleted file mode 100644 index d196486c2..000000000 --- a/js/src/views/Account/Identities.vue +++ /dev/null @@ -1,92 +0,0 @@ -<template> - <section> - <b-loading :active.sync="$apollo.loading"></b-loading> - <h1 class="title"> - <translate>Identities</translate> - </h1> - <a class="button is-primary" @click="showCreateProfileForm = true"> - <translate>Add a new profile</translate> - </a> - <div class="columns" v-if="showCreateProfileForm"> - <form @submit="createProfile" class="column is-half"> - <b-message title="Error" type="is-danger" v-for="error in errors" :key="error">{{ error }}</b-message> - <b-field label="Username"> - <b-input aria-required="true" required v-model="newPerson.preferredUsername"/> - </b-field> - <button class="button is-primary"> - <translate>Register</translate> - </button> - </form> - </div> - <ul> - <li v-for="identity in identities" :key="identity.id"> - <hr> - <div class="media"> - <div class="media-left"> - <figure class="image is-48x48"> - <img :src="identity.avatarUrl"> - </figure> - </div> - <div class="media-content"> - <p class="title is-5"> - {{ identity.name }} - <span - v-if="identity.preferredUsername === loggedPerson.preferredUsername" - class="tag is-primary" - > - <translate>Current</translate> - </span> - </p> - <p class="subtitle is-6">@{{ identity.preferredUsername }}</p> - </div> - </div> - </li> - </ul> - </section> -</template> - -<script lang="ts"> -import { Component, Vue } from 'vue-property-decorator'; -import { IDENTITIES, LOGGED_PERSON, CREATE_PERSON } from '../../graphql/actor'; -import { IPerson } from '@/types/actor.model'; - -@Component({ - apollo: { - identities: { - query: IDENTITIES, - }, - loggedPerson: { - query: LOGGED_PERSON, - }, - }, -}) -export default class Identities extends Vue { - identities: IPerson[] = []; - loggedPerson!: IPerson; - newPerson!: IPerson; - showCreateProfileForm: boolean = false; - errors: string[] = []; - - async createProfile(e) { - e.preventDefault(); - - try { - await this.$apollo.mutate({ - mutation: CREATE_PERSON, - variables: this.newPerson, - }); - this.showCreateProfileForm = false; - this.$apollo.queries.identities.refresh(); - } catch (err) { - console.error(err); - err.graphQLErrors.forEach(({ message }) => { - this.errors.push(message); - }); - } - } - - host() { - return `@${window.location.host}`; - } -} -</script> diff --git a/js/src/views/Account/MyAccount.vue b/js/src/views/Account/MyAccount.vue new file mode 100644 index 000000000..f4d39d400 --- /dev/null +++ b/js/src/views/Account/MyAccount.vue @@ -0,0 +1,68 @@ +<template> + <section class="container"> + <div v-if="person"> + <div class="header"> + <figure v-if="person.bannerUrl" class="image is-3by1"> + <img :src="person.bannerUrl" alt="banner"> + </figure> + </div> + + <div class="columns"> + <div class="identities column is-4"> + <identities></identities> + </div> + </div> + </div> + </section> +</template> + +<style lang="scss"> + .header { + padding-bottom: 30px; + } + + .identities { + padding-right: 45px; + margin-right: 45px; + } +</style> + +<script lang="ts"> +import { LOGGED_PERSON } from '@/graphql/actor'; +import { Component, Vue } from 'vue-property-decorator'; +import EventCard from '@/components/Event/EventCard.vue'; +import { IPerson } from '@/types/actor'; +import { CURRENT_USER_CLIENT } from '@/graphql/user'; +import Identities from '@/components/Account/Identities.vue'; + +@Component({ + apollo: { + currentUser: { + query: CURRENT_USER_CLIENT, + }, + loggedPerson: { + query: LOGGED_PERSON, + }, + }, + components: { + EventCard, + Identities, + }, +}) +export default class MyAccount extends Vue { + person: IPerson | null = null; + + async mounted () { + const result = await this.$apollo.query({ + query: LOGGED_PERSON, + }); + + this.person = result.data.loggedPerson; + } +} +</script> +<style lang="scss"> + @import "../../variables"; + @import "~bulma/sass/utilities/_all"; + @import "~bulma/sass/components/dropdown.sass"; +</style> diff --git a/js/src/views/Account/Profile.vue b/js/src/views/Account/Profile.vue index 9ecacfacb..f5c7a5b1a 100644 --- a/js/src/views/Account/Profile.vue +++ b/js/src/views/Account/Profile.vue @@ -1,103 +1,102 @@ <template> - <section class="container"> - <div v-if="person"> - <div class="card-image" v-if="person.bannerUrl"> - <figure class="image"> - <img :src="person.bannerUrl"> - </figure> - </div> - <div class="card-content"> - <div class="media"> - <div class="media-left"> - <figure class="image is-48x48"> - <img :src="person.avatarUrl"> - </figure> - </div> - <div class="media-content"> - <p class="title">{{ person.name }}</p> - <p class="subtitle">@{{ person.preferredUsername }}</p> - </div> - </div> - - <div class="content"> - <vue-simple-markdown :source="person.summary"></vue-simple-markdown> - </div> - - <b-dropdown hoverable has-link aria-role="list"> - <button class="button is-primary" slot="trigger"> - <translate>Public feeds</translate> - <b-icon icon="menu-down"></b-icon> - </button> - - <b-dropdown-item aria-role="listitem"> - <a :href="feedUrls('atom', true)"> - <translate>Public RSS/Atom Feed</translate> - </a> - </b-dropdown-item> - <b-dropdown-item aria-role="listitem"> - <a :href="feedUrls('ics', true)"> - <translate>Public iCal Feed</translate> - </a> - </b-dropdown-item> - </b-dropdown> - - <b-dropdown hoverable has-link aria-role="list" v-if="person.feedTokens.length > 0"> - <button class="button is-info" slot="trigger"> - <translate>Private feeds</translate> - <b-icon icon="menu-down"></b-icon> - </button> - - <b-dropdown-item aria-role="listitem"> - <a :href="feedUrls('atom', false)"> - <translate>RSS/Atom Feed</translate> - </a> - </b-dropdown-item> - <b-dropdown-item aria-role="listitem"> - <a :href="feedUrls('ics', false)"> - <translate>iCal Feed</translate> - </a> - </b-dropdown-item> - </b-dropdown> - <a class="button" v-else-if="loggedPerson" @click="createToken"> - <translate>Create token</translate> - </a> - </div> - <section v-if="person.organizedEvents.length > 0"> - <h2 class="subtitle"> - <translate>Organized</translate> - </h2> - <div class="columns"> - <EventCard - v-for="event in person.organizedEvents" - :event="event" - :options="{ hideDetails: true, organizerActor: person }" - :key="event.uuid" - class="column is-one-third" - /> - </div> - <div class="field is-grouped"> - <p class="control"> - <a - class="button" - @click="deleteProfile()" - v-if="loggedPerson && loggedPerson.id === person.id" - > - <translate>Delete</translate> - </a> - </p> - </div> - </section> + <section class="container"> + <div v-if="person"> + <div class="card-image" v-if="person.bannerUrl"> + <figure class="image"> + <img :src="person.bannerUrl"> + </figure> + </div> + <div class="card-content"> + <div class="media"> + <div class="media-left"> + <figure class="image is-48x48"> + <img :src="person.avatarUrl"> + </figure> + </div> + <div class="media-content"> + <p class="title">{{ person.name }}</p> + <p class="subtitle">@{{ person.preferredUsername }}</p> + </div> </div> - </section> + + <div class="content"> + <vue-simple-markdown :source="person.summary"></vue-simple-markdown> + </div> + + <b-dropdown hoverable has-link aria-role="list"> + <button class="button is-primary" slot="trigger"> + <translate>Public feeds</translate> + <b-icon icon="menu-down"></b-icon> + </button> + + <b-dropdown-item aria-role="listitem"> + <a :href="feedUrls('atom', true)"> + <translate>Public RSS/Atom Feed</translate> + </a> + </b-dropdown-item> + <b-dropdown-item aria-role="listitem"> + <a :href="feedUrls('ics', true)"> + <translate>Public iCal Feed</translate> + </a> + </b-dropdown-item> + </b-dropdown> + + <b-dropdown hoverable has-link aria-role="list" v-if="person.feedTokens.length > 0"> + <button class="button is-info" slot="trigger"> + <translate>Private feeds</translate> + <b-icon icon="menu-down"></b-icon> + </button> + + <b-dropdown-item aria-role="listitem"> + <a :href="feedUrls('atom', false)"> + <translate>RSS/Atom Feed</translate> + </a> + </b-dropdown-item> + <b-dropdown-item aria-role="listitem"> + <a :href="feedUrls('ics', false)"> + <translate>iCal Feed</translate> + </a> + </b-dropdown-item> + </b-dropdown> + <a class="button" v-else-if="loggedPerson" @click="createToken"> + <translate>Create token</translate> + </a> + </div> + <section v-if="person.organizedEvents.length > 0"> + <h2 class="subtitle"> + <translate>Organized</translate> + </h2> + <div class="columns"> + <EventCard + v-for="event in person.organizedEvents" + :event="event" + :options="{ hideDetails: true, organizerActor: person }" + :key="event.uuid" + class="column is-one-third" + /> + </div> + <div class="field is-grouped"> + <p class="control"> + <a + class="button" + @click="deleteProfile()" + v-if="loggedPerson && loggedPerson.id === person.id" + > + <translate>Delete</translate> + </a> + </p> + </div> + </section> + </div> + </section> </template> <script lang="ts"> import { FETCH_PERSON, LOGGED_PERSON } from '@/graphql/actor'; import { Component, Prop, Vue, Watch } from 'vue-property-decorator'; import EventCard from '@/components/Event/EventCard.vue'; -import { RouteName } from '@/router'; import { MOBILIZON_INSTANCE_HOST } from '@/api/_entrypoint'; -import { IPerson } from '@/types/actor.model'; +import { IPerson } from '@/types/actor'; import { CREATE_FEED_TOKEN_ACTOR } from '@/graphql/feed_tokens'; @Component({ @@ -118,19 +117,15 @@ import { CREATE_FEED_TOKEN_ACTOR } from '@/graphql/feed_tokens'; EventCard, }, }) -export default class Profile extends Vue { +export default class MyAccount extends Vue { @Prop({ type: String, required: true }) name!: string; person!: IPerson; - // call again the method if the route changes + // call again the method if the route changes @Watch('$route') - onRouteChange() { - // this.fetchData() - } - - nl2br(text) { - return text.replace(/(?:\r\n|\r|\n)/g, '<br>'); + onRouteChange() { + // this.fetchData() } feedUrls(format, isPublic = true): string { @@ -155,7 +150,7 @@ export default class Profile extends Vue { } </script> <style lang="scss"> -@import "../../variables"; -@import "~bulma/sass/utilities/_all"; -@import "~bulma/sass/components/dropdown.sass"; + @import "../../variables"; + @import "~bulma/sass/utilities/_all"; + @import "~bulma/sass/components/dropdown.sass"; </style> diff --git a/js/src/views/Account/Register.vue b/js/src/views/Account/Register.vue index 3c125d9b6..6c461d0b1 100644 --- a/js/src/views/Account/Register.vue +++ b/js/src/views/Account/Register.vue @@ -71,7 +71,7 @@ <script lang="ts"> import { Component, Prop, Vue } from 'vue-property-decorator'; -import { IPerson } from '@/types/actor.model'; +import { IPerson } from '@/types/actor'; import { REGISTER_PERSON } from '@/graphql/actor'; import { MOBILIZON_INSTANCE_HOST } from '@/api/_entrypoint'; import { RouteName } from '@/router'; diff --git a/js/src/views/Event/Create.vue b/js/src/views/Event/Create.vue index 968d0cd3b..e65a29aae 100644 --- a/js/src/views/Event/Create.vue +++ b/js/src/views/Event/Create.vue @@ -40,7 +40,7 @@ import { EventModel, } from '@/types/event.model'; import { LOGGED_PERSON } from '@/graphql/actor'; -import { IPerson, Person } from '@/types/actor.model'; +import { IPerson, Person } from '@/types/actor'; @Component({ apollo: { diff --git a/js/src/views/Event/Event.vue b/js/src/views/Event/Event.vue index b0a8eb697..10231052e 100644 --- a/js/src/views/Event/Event.vue +++ b/js/src/views/Event/Event.vue @@ -233,7 +233,7 @@ import { DELETE_EVENT, FETCH_EVENT, JOIN_EVENT, LEAVE_EVENT } from '@/graphql/ev import { Component, Prop, Vue } from 'vue-property-decorator'; import { LOGGED_PERSON } from '@/graphql/actor'; import { EventVisibility, IEvent, IParticipant } from '@/types/event.model'; -import { IPerson } from '@/types/actor.model'; +import { IPerson } from '@/types/actor'; import { RouteName } from '@/router'; import 'vue-simple-markdown/dist/vue-simple-markdown.css'; import { GRAPHQL_API_ENDPOINT } from '@/api/_entrypoint'; diff --git a/js/src/views/Group/Group.vue b/js/src/views/Group/Group.vue index 0c03a19c5..3d391caff 100644 --- a/js/src/views/Group/Group.vue +++ b/js/src/views/Group/Group.vue @@ -59,7 +59,7 @@ import { Component, Prop, Vue, Watch } from 'vue-property-decorator'; import EventCard from '@/components/Event/EventCard.vue'; import { FETCH_GROUP, LOGGED_PERSON } from '@/graphql/actor'; -import { IGroup } from '@/types/actor.model'; +import { IGroup } from '@/types/actor'; @Component({ apollo: { diff --git a/js/src/views/Home.vue b/js/src/views/Home.vue index c81faff9b..eed078f9d 100644 --- a/js/src/views/Home.vue +++ b/js/src/views/Home.vue @@ -88,7 +88,7 @@ import { FETCH_EVENTS } from '@/graphql/event'; import { Component, Vue } from 'vue-property-decorator'; import EventCard from '@/components/Event/EventCard.vue'; import { LOGGED_PERSON_WITH_GOING_TO_EVENTS } from '@/graphql/actor'; -import { IPerson, Person } from '@/types/actor.model'; +import { IPerson, Person } from '@/types/actor'; import { ICurrentUser } from '@/types/current-user.model'; import { CURRENT_USER_CLIENT } from '@/graphql/user'; import { RouteName } from '@/router'; diff --git a/js/src/views/Search.vue b/js/src/views/Search.vue index 21a52bf0d..7bcc92c39 100644 --- a/js/src/views/Search.vue +++ b/js/src/views/Search.vue @@ -48,7 +48,7 @@ import { SEARCH_EVENTS, SEARCH_GROUPS } from '@/graphql/search'; import { RouteName } from '@/router'; import EventCard from '@/components/Event/EventCard.vue'; import GroupCard from '@/components/Group/GroupCard.vue'; -import { Group, IGroup } from '@/types/actor.model'; +import { Group, IGroup } from '@/types/actor'; import { SearchEvent, SearchGroup } from '@/types/search.model'; enum SearchTabs { diff --git a/js/tslint.json b/js/tslint.json index cb6227b2b..61db46360 100644 --- a/js/tslint.json +++ b/js/tslint.json @@ -3,6 +3,7 @@ "rules": { "max-line-length": [ true, 140 ], "import-name": false, - "ter-arrow-parens": false + "ter-arrow-parens": false, + "no-boolean-literal-compare": false } } diff --git a/schema.graphql b/schema.graphql new file mode 100644 index 000000000..f9f2fe6fa --- /dev/null +++ b/schema.graphql @@ -0,0 +1,781 @@ +# source: http://localhost:4001/api +# timestamp: Fri Apr 26 2019 14:47:01 GMT+0200 (heure d’été d’Europe centrale) + +schema { + query: RootQueryType + mutation: RootMutationType +} + +"""An ActivityPub actor""" +interface Actor { + """The actor's avatar url""" + avatarUrl: String + + """The actor's banner url""" + bannerUrl: String + + """The actor's domain if (null if it's this instance)""" + domain: String + + """List of followers""" + followers: [Follower] + + """Number of followers for this actor""" + followersCount: Int + + """List of followings""" + following: [Follower] + + """Number of actors following this actor""" + followingCount: Int + + """Internal ID for this actor""" + id: Int + + """The actors RSA Keys""" + keys: String + + """If the actor is from this instance""" + local: Boolean + + """Whether the actors manually approves followers""" + manuallyApprovesFollowers: Boolean + + """The actor's displayed name""" + name: String + + """A list of the events this actor has organized""" + organizedEvents: [Event] + + """The actor's preferred username""" + preferredUsername: String + + """The actor's summary""" + summary: String + + """If the actor is suspended""" + suspended: Boolean + + """The type of Actor (Person, Group,…)""" + type: ActorType + + """The ActivityPub actor's URL""" + url: String +} + +"""The list of types an actor can be""" +enum ActorType { + """An ActivityPub Application""" + APPLICATION + + """An ActivityPub Group""" + GROUP + + """An ActivityPub Organization""" + ORGANIZATION + + """An ActivityPub Person""" + PERSON + + """An ActivityPub Service""" + SERVICE +} + +type Address { + country: String + description: String + + """The floor this event is at""" + floor: String + + """The geocoordinates for the point where this address is""" + geom: Point + + """The address's locality""" + locality: String + postalCode: String + region: String + + """The address's street name (with number)""" + street: String +} + +"""A comment""" +type Comment { + """Internal ID for this comment""" + id: ID + local: Boolean + primaryLanguage: String + replies: [Comment] + text: String + threadLanguages: [String]! + url: String + uuid: UUID + visibility: CommentVisibility +} + +"""The list of visibility options for a comment""" +enum CommentVisibility { + """visible only to people invited""" + INVITE + + """Visible only after a moderator accepted""" + MODERATED + + """Visible only to people members of the group or followers of the person""" + PRIVATE + + """Publically listed and federated. Can be shared.""" + PUBLIC + + """Visible only to people with the link - or invited""" + UNLISTED +} + +"""A config object""" +type Config { + description: String + name: String + registrationsOpen: Boolean +} + +""" +The `DateTime` scalar type represents a date and time in the UTC +timezone. The DateTime appears in a JSON response as an ISO8601 formatted +string, including UTC timezone ("Z"). The parsed date and time string will +be converted to UTC and any UTC offset other than 0 will be rejected. +""" +scalar DateTime + +"""Represents a deleted feed_token""" +type DeletedFeedToken { + actor: DeletedObject + user: DeletedObject +} + +"""Represents a deleted member""" +type DeletedMember { + actor: DeletedObject + parent: DeletedObject +} + +"""A struct containing the id of the deleted object""" +type DeletedObject { + id: Int +} + +"""Represents a deleted participant""" +type DeletedParticipant { + actor: DeletedObject + event: DeletedObject +} + +"""An event""" +type Event { + """Who the event is attributed to (often a group)""" + attributedTo: Actor + + """Datetime for when the event begins""" + beginsOn: DateTime + + """The event's category""" + category: String + + """When the event was created""" + createdAt: DateTime + + """The event's description""" + description: String + + """Datetime for when the event ends""" + endsOn: DateTime + + """Internal ID for this event""" + id: Int + + """A large picture for the event""" + largeImage: String + + """Whether the event is local or not""" + local: Boolean + + """Online address of the event""" + onlineAddress: OnlineAddress + + """The event's organizer (as a person)""" + organizerActor: Actor + + """The event's participants""" + participants: [Participant] + + """Phone address for the event""" + phoneAddress: PhoneAddress + + """The type of the event's address""" + physicalAddress: Address + + """When the event was published""" + publishAt: DateTime + + """Events related to this one""" + relatedEvents: [Event] + + """The event's description's slug""" + slug: String + + """Status of the event""" + status: EventStatus + + """The event's tags""" + tags: [Tag] + + """A thumbnail picture for the event""" + thumbnail: String + + """The event's title""" + title: String + + """When the event was last updated""" + updatedAt: DateTime + + """The ActivityPub Event URL""" + url: String + + """The Event UUID""" + uuid: UUID + + """The event's visibility""" + visibility: EventVisibility +} + +"""Search events result""" +type Events { + """Event elements""" + elements: [Event]! + + """Total elements""" + total: Int! +} + +"""The list of possible options for the event's status""" +enum EventStatus { + """The event is cancelled""" + CANCELLED + + """The event is confirmed""" + CONFIRMED + + """The event is tentative""" + TENTATIVE +} + +"""The list of visibility options for an event""" +enum EventVisibility { + """visible only to people invited""" + INVITE + + """Visible only after a moderator accepted""" + MODERATED + + """Visible only to people members of the group or followers of the person""" + PRIVATE + + """Publically listed and federated. Can be shared.""" + PUBLIC + + """Visible only to people with the link - or invited""" + UNLISTED +} + +"""Represents a participant to an event""" +type FeedToken { + """The event which the actor participates in""" + actor: Actor + + """The role of this actor at this event""" + token: String + + """The actor that participates to the event""" + user: User +} + +""" +Represents an actor's follower + +""" +type Follower { + """Which profile follows""" + actor: Actor + + """Whether the follow has been approved by the target actor""" + approved: Boolean + + """What or who the profile follows""" + targetActor: Actor +} + +""" +Represents a group of actors + +""" +type Group implements Actor { + """The actor's avatar url""" + avatarUrl: String + + """The actor's banner url""" + bannerUrl: String + + """The actor's domain if (null if it's this instance)""" + domain: String + + """List of followers""" + followers: [Follower] + + """Number of followers for this actor""" + followersCount: Int + + """List of followings""" + following: [Follower] + + """Number of actors following this actor""" + followingCount: Int + + """Internal ID for this group""" + id: Int + + """The actors RSA Keys""" + keys: String + + """If the actor is from this instance""" + local: Boolean + + """Whether the actors manually approves followers""" + manuallyApprovesFollowers: Boolean + + """List of group members""" + members: [Member]! + + """The actor's displayed name""" + name: String + + """Whether the group is opened to all or has restricted access""" + openness: Openness + + """A list of the events this actor has organized""" + organizedEvents: [Event] + + """The actor's preferred username""" + preferredUsername: String + + """The actor's summary""" + summary: String + + """If the actor is suspended""" + suspended: Boolean + + """The type of Actor (Person, Group,…)""" + type: ActorType + + """The type of group : Group, Community,…""" + types: GroupType + + """The ActivityPub actor's URL""" + url: String +} + +"""Search groups result""" +type Groups { + """Group elements""" + elements: [Group]! + + """Total elements""" + total: Int! +} + +""" +The types of Group that exist + +""" +enum GroupType { + """A public group of many actors""" + COMMUNITY + + """A private group of persons""" + GROUP +} + +"""A JWT and the associated user ID""" +type Login { + """A JWT Token for this session""" + token: String! + + """The user associated to this session""" + user: User! +} + +""" +Represents a member of a group + +""" +type Member { + """Which profile is member of""" + actor: Person + + """Of which the profile is member""" + parent: Group + + """The role of this membership""" + role: Int +} + +type OnlineAddress { + info: String + url: String +} + +""" +Describes how an actor is opened to follows + +""" +enum Openness { + """The actor can only be followed by invitation""" + INVITE_ONLY + + """The actor needs to accept the following before it's effective""" + MODERATED + + """The actor is open to followings""" + OPEN +} + +"""Represents a participant to an event""" +type Participant { + """The actor that participates to the event""" + actor: Actor + + """The event which the actor participates in""" + event: Event + + """The role of this actor at this event""" + role: Int +} + +""" +Represents a person identity + +""" +type Person implements Actor { + """The actor's avatar url""" + avatarUrl: String + + """The actor's banner url""" + bannerUrl: String + + """The actor's domain if (null if it's this instance)""" + domain: String + + """A list of the feed tokens for this person""" + feedTokens: [FeedToken] + + """List of followers""" + followers: [Follower] + + """Number of followers for this actor""" + followersCount: Int + + """List of followings""" + following: [Follower] + + """Number of actors following this actor""" + followingCount: Int + + """The list of events this person goes to""" + goingToEvents: [Event] + + """Internal ID for this person""" + id: Int + + """The actors RSA Keys""" + keys: String + + """If the actor is from this instance""" + local: Boolean + + """Whether the actors manually approves followers""" + manuallyApprovesFollowers: Boolean + + """The list of groups this person is member of""" + memberOf: [Member] + + """The actor's displayed name""" + name: String + + """A list of the events this actor has organized""" + organizedEvents: [Event] + + """The actor's preferred username""" + preferredUsername: String + + """The actor's summary""" + summary: String + + """If the actor is suspended""" + suspended: Boolean + + """The type of Actor (Person, Group,…)""" + type: ActorType + + """The ActivityPub actor's URL""" + url: String + + """The user this actor is associated to""" + user: User +} + +"""Search persons result""" +type Persons { + """Person elements""" + elements: [Person]! + + """Total elements""" + total: Int! +} + +type PhoneAddress { + info: String + phone: String +} + +""" +The `Point` scalar type represents Point geographic information compliant string data, +represented as floats separated by a semi-colon. The geodetic system is WGS 84 +""" +scalar Point + +type RootMutationType { + """Change default actor for user""" + changeDefaultActor(preferredUsername: String!): User + + """Create a comment""" + createComment(actorUsername: String!, text: String!): Comment + + """Create an event""" + createEvent(beginsOn: DateTime!, category: String!, description: String!, endsOn: DateTime, largeImage: String, onlineAddress: String, organizerActorId: ID!, phoneAddress: String, public: Boolean, publishAt: DateTime, state: Int, status: Int, thumbnail: String, title: String!): Event + + """Create a Feed Token""" + createFeedToken(actorId: Int): FeedToken + + """Create a group""" + createGroup( + """ + The actor's username which will be the admin (otherwise user's default one) + """ + adminActorUsername: String + + """The summary for the group""" + description: String = "" + + """The displayed name for the group""" + name: String + + """The name for the group""" + preferredUsername: String! + ): Group + + """Create a new person for user""" + createPerson( + """The displayed name for the new profile""" + name: String = "" + preferredUsername: String! + + """The summary for the new profile""" + summary: String = "" + ): Person + + """Create an user""" + createUser(email: String!, password: String!): User + + """Delete an event""" + deleteEvent(actorId: Int!, eventId: Int!): DeletedObject + + """Delete a feed token""" + deleteFeedToken(token: String!): DeletedFeedToken + + """Delete a group""" + deleteGroup(actorId: Int!, groupId: Int!): DeletedObject + + """Join an event""" + joinEvent(actorId: Int!, eventId: Int!): Participant + + """Join a group""" + joinGroup(actorId: Int!, groupId: Int!): Member + + """Leave an event""" + leaveEvent(actorId: Int!, eventId: Int!): DeletedParticipant + + """Leave an event""" + leaveGroup(actorId: Int!, groupId: Int!): DeletedMember + + """Login an user""" + login(email: String!, password: String!): Login + + """Register a first profile on registration""" + registerPerson( + """The email from the user previously created""" + email: String! + + """The displayed name for the new profile""" + name: String = "" + preferredUsername: String! + + """The summary for the new profile""" + summary: String = "" + ): Person + + """Resend registration confirmation token""" + resendConfirmationEmail(email: String!, locale: String = "en"): String + + """Reset user password""" + resetPassword(locale: String = "en", password: String!, token: String!): Login + + """Send a link through email to reset user password""" + sendResetPassword(email: String!, locale: String = "en"): String + + """Validate an user after registration""" + validateUser(token: String!): Login +} + +""" +Root Query + +""" +type RootQueryType { + """Get the instance config""" + config: Config + + """Get an event by uuid""" + event(uuid: UUID!): Event + + """Get all events""" + events(limit: Int = 10, page: Int = 1): [Event] + + """Get a group by it's preferred username""" + group(preferredUsername: String!): Group + + """Get all groups""" + groups(limit: Int = 10, page: Int = 1): [Group] + + """Get the persons for an user""" + identities: [Person] + + """Get the current actor for the logged-in user""" + loggedPerson: Person + + """Get the current user""" + loggedUser: User + + """Get all participants for an event uuid""" + participants(limit: Int = 10, page: Int = 1, uuid: UUID!): [Participant] + + """Get a person by it's preferred username""" + person(preferredUsername: String!): Person + + """Reverse geocode coordinates""" + reverseGeocode(latitude: Float!, longitude: Float!): [Address] + + """Search for an address""" + searchAddress(query: String!): [Address] + + """Search events""" + searchEvents(limit: Int = 10, page: Int = 1, search: String!): Events + + """Search groups""" + searchGroups(limit: Int = 10, page: Int = 1, search: String!): Groups + + """Search persons""" + searchPersons(limit: Int = 10, page: Int = 1, search: String!): Persons + + """Get the list of tags""" + tags(limit: Int = 10, page: Int = 1): [Tag]! + + """Get an user""" + user(id: ID!): User + + """List instance users""" + users(direction: SortDirection = DESC, limit: Int = 10, page: Int = 1, sort: SortableUserField = ID): Users +} + +"""The list of possible options for the event's status""" +enum SortableUserField { + ID +} + +"""Available sort directions""" +enum SortDirection { + ASC + DESC +} + +"""A tag""" +type Tag { + """The tag's ID""" + id: ID + + """Related tags to this tag""" + related: [Tag] + + """The tags's slug""" + slug: String + + """The tag's title""" + title: String +} + +"""A local user of Mobilizon""" +type User { + """The datetime the last activation/confirmation token was sent""" + confirmationSentAt: DateTime + + """The account activation/confirmation token""" + confirmationToken: String + + """The datetime when the user was confirmed/activated""" + confirmedAt: DateTime + + """The user's default actor""" + defaultActor: Person + + """The user's email""" + email: String! + + """A list of the feed tokens for this user""" + feedTokens: [FeedToken] + + """The user's ID""" + id: ID! + + """The user's list of profiles (identities)""" + profiles: [Person]! + + """The datetime last reset password email was sent""" + resetPasswordSentAt: DateTime + + """The token sent when requesting password token""" + resetPasswordToken: String +} + +"""Users list""" +type Users { + """User elements""" + elements: [User]! + + """Total elements""" + total: Int! +} + +""" +The `UUID` scalar type represents UUID4 compliant string data, represented as UTF-8 +character sequences. The UUID4 type is most often used to represent unique +human-readable ID strings. +""" +scalar UUID