2019-06-17 17:15:27 +02:00
|
|
|
<template>
|
|
|
|
<div class="root">
|
|
|
|
<h1 class="title">
|
|
|
|
<span v-if="isUpdate">{{ identity.displayName() }}</span>
|
2019-09-12 11:34:01 +02:00
|
|
|
<span v-else>{{ $t('I create an identity') }}</span>
|
2019-06-17 17:15:27 +02:00
|
|
|
</h1>
|
|
|
|
|
|
|
|
<picture-upload v-model="avatarFile" class="picture-upload"></picture-upload>
|
|
|
|
|
2019-09-12 11:34:01 +02:00
|
|
|
<b-field :label="$t('Display name')">
|
2019-06-17 17:15:27 +02:00
|
|
|
<b-input aria-required="true" required v-model="identity.name" @input="autoUpdateUsername($event)"/>
|
|
|
|
</b-field>
|
|
|
|
|
2019-09-12 11:34:01 +02:00
|
|
|
<b-field :label="$t('Username')">
|
2019-06-17 17:15:27 +02:00
|
|
|
<b-field>
|
|
|
|
<b-input aria-required="true" required v-model="identity.preferredUsername" :disabled="isUpdate"/>
|
|
|
|
|
|
|
|
<p class="control">
|
|
|
|
<span class="button is-static">@{{ getInstanceHost() }}</span>
|
|
|
|
</p>
|
|
|
|
</b-field>
|
|
|
|
</b-field>
|
|
|
|
|
2019-09-12 11:34:01 +02:00
|
|
|
<b-field :label="$t('Description')">
|
2019-06-17 17:15:27 +02:00
|
|
|
<b-input type="textarea" aria-required="false" v-model="identity.summary"/>
|
|
|
|
</b-field>
|
|
|
|
|
2019-09-11 09:59:01 +02:00
|
|
|
<b-notification
|
|
|
|
type="is-danger"
|
|
|
|
has-icon
|
|
|
|
aria-close-label="Close notification"
|
|
|
|
role="alert"
|
2019-09-18 17:32:37 +02:00
|
|
|
:key="error"
|
2019-09-11 09:59:01 +02:00
|
|
|
v-for="error in errors"
|
|
|
|
>
|
|
|
|
{{ error }}
|
|
|
|
</b-notification>
|
|
|
|
|
2019-06-17 17:15:27 +02:00
|
|
|
<b-field class="submit">
|
|
|
|
<div class="control">
|
2019-09-12 11:34:01 +02:00
|
|
|
<button type="button" class="button is-primary" @click="submit()">
|
|
|
|
{{ $t('Save') }}
|
2019-06-17 17:15:27 +02:00
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
</b-field>
|
|
|
|
|
|
|
|
<div class="delete-identity" v-if="isUpdate">
|
2019-09-12 11:34:01 +02:00
|
|
|
<span @click="openDeleteIdentityConfirmation()">
|
|
|
|
{{ $t('Delete this identity') }}
|
2019-06-17 17:15:27 +02:00
|
|
|
</span>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</template>
|
|
|
|
|
|
|
|
<style scoped type="scss">
|
|
|
|
h1 {
|
|
|
|
display: flex;
|
|
|
|
justify-content: center;
|
|
|
|
}
|
|
|
|
|
|
|
|
.picture-upload {
|
|
|
|
margin: 30px 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
.submit,
|
|
|
|
.delete-identity {
|
|
|
|
display: flex;
|
|
|
|
justify-content: center;
|
|
|
|
}
|
|
|
|
|
|
|
|
.submit {
|
|
|
|
margin: 30px 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
.delete-identity {
|
|
|
|
text-decoration: underline;
|
|
|
|
cursor: pointer;
|
|
|
|
margin-top: 15px;
|
|
|
|
}
|
|
|
|
</style>
|
|
|
|
|
|
|
|
<script lang="ts">
|
|
|
|
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
|
2019-09-11 09:59:01 +02:00
|
|
|
import {
|
|
|
|
CREATE_PERSON,
|
|
|
|
CURRENT_ACTOR_CLIENT,
|
|
|
|
DELETE_PERSON,
|
|
|
|
FETCH_PERSON,
|
|
|
|
IDENTITIES,
|
|
|
|
UPDATE_PERSON,
|
|
|
|
} from '@/graphql/actor';
|
2019-06-17 17:15:27 +02:00
|
|
|
import { IPerson, Person } from '@/types/actor';
|
|
|
|
import PictureUpload from '@/components/PictureUpload.vue';
|
|
|
|
import { MOBILIZON_INSTANCE_HOST } from '@/api/_entrypoint';
|
|
|
|
import { Dialog } from 'buefy/dist/components/dialog';
|
|
|
|
import { RouteName } from '@/router';
|
2019-10-08 19:32:48 +02:00
|
|
|
import { buildFileFromIPicture, buildFileVariable, readFileAsync } from '@/utils/image';
|
2019-10-04 18:28:25 +02:00
|
|
|
import { changeIdentity } from '@/utils/auth';
|
2019-06-17 17:15:27 +02:00
|
|
|
|
|
|
|
@Component({
|
|
|
|
components: {
|
|
|
|
PictureUpload,
|
|
|
|
Dialog,
|
|
|
|
},
|
2019-09-11 09:59:01 +02:00
|
|
|
apollo: {
|
|
|
|
currentActor: {
|
|
|
|
query: CURRENT_ACTOR_CLIENT,
|
|
|
|
},
|
|
|
|
},
|
2019-06-17 17:15:27 +02:00
|
|
|
})
|
|
|
|
export default class EditIdentity extends Vue {
|
|
|
|
@Prop({ type: Boolean }) isUpdate!: boolean;
|
|
|
|
|
|
|
|
errors: string[] = [];
|
|
|
|
|
|
|
|
identityName!: string | undefined;
|
|
|
|
avatarFile: File | null = null;
|
|
|
|
identity = new Person();
|
|
|
|
|
|
|
|
private oldDisplayName: string | null = null;
|
2019-09-11 09:59:01 +02:00
|
|
|
private currentActor: IPerson | null = null;
|
2019-06-17 17:15:27 +02:00
|
|
|
|
|
|
|
@Watch('isUpdate')
|
|
|
|
async isUpdateChanged () {
|
|
|
|
this.resetFields();
|
|
|
|
}
|
|
|
|
|
|
|
|
@Watch('$route.params.identityName', { immediate: true })
|
|
|
|
async onIdentityParamChanged (val: string) {
|
|
|
|
// Only used when we update the identity
|
|
|
|
if (this.isUpdate !== true) return;
|
|
|
|
|
|
|
|
await this.redirectIfNoIdentitySelected(val);
|
|
|
|
|
|
|
|
this.resetFields();
|
|
|
|
this.identityName = val;
|
|
|
|
|
|
|
|
if (this.identityName) {
|
|
|
|
this.identity = await this.getIdentity();
|
|
|
|
|
2019-09-02 14:35:50 +02:00
|
|
|
this.avatarFile = await buildFileFromIPicture(this.identity.avatar);
|
2019-06-17 17:15:27 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
submit() {
|
|
|
|
if (this.isUpdate) return this.updateIdentity();
|
|
|
|
|
|
|
|
return this.createIdentity();
|
|
|
|
}
|
|
|
|
|
|
|
|
autoUpdateUsername(newDisplayName: string | null) {
|
|
|
|
const oldUsername = this.convertToUsername(this.oldDisplayName);
|
|
|
|
|
|
|
|
if (this.identity.preferredUsername === oldUsername) {
|
|
|
|
this.identity.preferredUsername = this.convertToUsername(newDisplayName);
|
|
|
|
}
|
|
|
|
|
|
|
|
this.oldDisplayName = newDisplayName;
|
|
|
|
}
|
|
|
|
|
2019-09-11 09:59:01 +02:00
|
|
|
/**
|
|
|
|
* Delete an identity
|
|
|
|
*/
|
2019-06-17 17:15:27 +02:00
|
|
|
async deleteIdentity() {
|
|
|
|
try {
|
|
|
|
await this.$apollo.mutate({
|
|
|
|
mutation: DELETE_PERSON,
|
2019-10-04 18:28:25 +02:00
|
|
|
variables: {
|
|
|
|
id: this.identity.id,
|
|
|
|
},
|
2019-06-17 17:15:27 +02:00
|
|
|
update: (store) => {
|
|
|
|
const data = store.readQuery<{ identities: IPerson[] }>({ query: IDENTITIES });
|
|
|
|
|
|
|
|
if (data) {
|
|
|
|
data.identities = data.identities.filter(i => i.id !== this.identity.id);
|
|
|
|
|
|
|
|
store.writeQuery({ query: IDENTITIES, data });
|
|
|
|
}
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
this.$notifier.success(
|
2019-09-12 11:34:01 +02:00
|
|
|
this.$t('Identity {displayName} deleted', { displayName: this.identity.displayName() }) as string,
|
2019-06-17 17:15:27 +02:00
|
|
|
);
|
2019-09-11 09:59:01 +02:00
|
|
|
/**
|
|
|
|
* If we just deleted the current identity, we need to change it to the next one
|
|
|
|
*/
|
|
|
|
const data = this.$apollo.provider.defaultClient.readQuery<{ identities: IPerson[] }>({ query: IDENTITIES });
|
|
|
|
if (data) {
|
|
|
|
await this.maybeUpdateCurrentActorCache(data.identities[0]);
|
2019-06-17 17:15:27 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
await this.redirectIfNoIdentitySelected();
|
|
|
|
} catch (err) {
|
|
|
|
this.handleError(err);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async updateIdentity() {
|
|
|
|
try {
|
2019-10-08 19:32:48 +02:00
|
|
|
const variables = await this.buildVariables();
|
|
|
|
|
2019-06-17 17:15:27 +02:00
|
|
|
await this.$apollo.mutate({
|
|
|
|
mutation: UPDATE_PERSON,
|
2019-10-08 19:32:48 +02:00
|
|
|
variables,
|
2019-06-17 17:15:27 +02:00
|
|
|
update: (store, { data: { updatePerson } }) => {
|
|
|
|
const data = store.readQuery<{ identities: IPerson[] }>({ query: IDENTITIES });
|
|
|
|
|
|
|
|
if (data) {
|
|
|
|
const index = data.identities.findIndex(i => i.id === this.identity.id);
|
|
|
|
|
|
|
|
this.$set(data.identities, index, updatePerson);
|
2019-09-11 09:59:01 +02:00
|
|
|
this.maybeUpdateCurrentActorCache(updatePerson);
|
2019-06-17 17:15:27 +02:00
|
|
|
|
|
|
|
store.writeQuery({ query: IDENTITIES, data });
|
|
|
|
}
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
this.$notifier.success(
|
2019-09-12 11:34:01 +02:00
|
|
|
this.$t('Identity {displayName} updated', { displayName: this.identity.displayName() }) as string,
|
2019-06-17 17:15:27 +02:00
|
|
|
);
|
|
|
|
} catch (err) {
|
|
|
|
this.handleError(err);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async createIdentity() {
|
|
|
|
try {
|
2019-10-08 19:32:48 +02:00
|
|
|
const variables = await this.buildVariables();
|
|
|
|
|
2019-06-17 17:15:27 +02:00
|
|
|
await this.$apollo.mutate({
|
|
|
|
mutation: CREATE_PERSON,
|
2019-10-08 19:32:48 +02:00
|
|
|
variables,
|
2019-06-17 17:15:27 +02:00
|
|
|
update: (store, { data: { createPerson } }) => {
|
|
|
|
const data = store.readQuery<{ identities: IPerson[] }>({ query: IDENTITIES });
|
|
|
|
|
|
|
|
if (data) {
|
|
|
|
data.identities.push(createPerson);
|
|
|
|
|
|
|
|
store.writeQuery({ query: IDENTITIES, data });
|
|
|
|
}
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
this.$notifier.success(
|
2019-09-12 11:34:01 +02:00
|
|
|
this.$t('Identity {displayName} created', { displayName: this.identity.displayName() }) as string,
|
2019-06-17 17:15:27 +02:00
|
|
|
);
|
2019-09-11 09:59:01 +02:00
|
|
|
|
|
|
|
await this.$router.push({ name: RouteName.UPDATE_IDENTITY, params: { identityName: this.identity.preferredUsername } });
|
2019-06-17 17:15:27 +02:00
|
|
|
} catch (err) {
|
|
|
|
this.handleError(err);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
getInstanceHost() {
|
|
|
|
return MOBILIZON_INSTANCE_HOST;
|
|
|
|
}
|
|
|
|
|
|
|
|
openDeleteIdentityConfirmation() {
|
2019-08-21 11:25:09 +02:00
|
|
|
this.$buefy.dialog.prompt({
|
2019-06-17 17:15:27 +02:00
|
|
|
type: 'is-danger',
|
2019-09-12 11:34:01 +02:00
|
|
|
title: this.$t('Delete your identity') as string,
|
|
|
|
message: `${this.$t('This will delete / anonymize all content (events, comments, messages, participations…) created from this identity.')}
|
|
|
|
<br /><br />
|
|
|
|
${this.$t('If this identity is the only administrator of some groups, you need to delete them before being able to delete this identity.')}
|
|
|
|
${this.$t('Otherwise this identity will just be removed from the group administrators.')}
|
|
|
|
<br /><br />
|
|
|
|
${this.$t('To confirm, type your identity username "{preferredUsername}"', { preferredUsername: this.identity.preferredUsername })}`,
|
|
|
|
confirmText: this.$t(
|
|
|
|
'Delete {preferredUsername}',
|
2019-06-17 17:15:27 +02:00
|
|
|
{ preferredUsername: this.identity.preferredUsername },
|
2019-09-12 11:34:01 +02:00
|
|
|
) as string,
|
2019-06-17 17:15:27 +02:00
|
|
|
inputAttrs: {
|
|
|
|
placeholder: this.identity.preferredUsername,
|
|
|
|
pattern: this.identity.preferredUsername,
|
|
|
|
},
|
|
|
|
|
|
|
|
onConfirm: () => this.deleteIdentity(),
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
private async getIdentity() {
|
|
|
|
const result = await this.$apollo.query({
|
|
|
|
query: FETCH_PERSON,
|
|
|
|
variables: {
|
2019-10-04 18:28:25 +02:00
|
|
|
username: this.identityName,
|
2019-06-17 17:15:27 +02:00
|
|
|
},
|
|
|
|
});
|
|
|
|
|
2019-10-04 18:28:25 +02:00
|
|
|
return new Person(result.data.fetchPerson);
|
2019-06-17 17:15:27 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
private handleError(err: any) {
|
|
|
|
console.error(err);
|
|
|
|
|
2019-09-11 09:59:01 +02:00
|
|
|
if (err.graphQLErrors !== undefined) {
|
|
|
|
err.graphQLErrors.forEach(({ message }) => {
|
|
|
|
this.$notifier.error(message);
|
|
|
|
});
|
|
|
|
}
|
2019-06-17 17:15:27 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
private convertToUsername(value: string | null) {
|
|
|
|
if (!value) return '';
|
|
|
|
|
2019-10-12 18:11:10 +02:00
|
|
|
// https://stackoverflow.com/a/37511463
|
|
|
|
return value.toLocaleLowerCase()
|
|
|
|
.normalize('NFD')
|
|
|
|
.replace(/[\u0300-\u036f]/g, '')
|
2019-06-17 17:15:27 +02:00
|
|
|
.replace(/ /g, '_')
|
2019-10-12 18:11:10 +02:00
|
|
|
.replace(/[^a-z0-9._]/g, '')
|
|
|
|
;
|
2019-06-17 17:15:27 +02:00
|
|
|
}
|
|
|
|
|
2019-10-08 19:32:48 +02:00
|
|
|
private async buildVariables() {
|
|
|
|
const avatarObj = buildFileVariable(this.avatarFile, 'avatar', `${this.identity.preferredUsername}'s avatar`);
|
|
|
|
const res = Object.assign({}, this.identity, avatarObj);
|
|
|
|
/**
|
|
|
|
* If the avatar didn't change, no need to try reuploading it
|
|
|
|
*/
|
2019-10-08 20:00:26 +02:00
|
|
|
if (this.identity.avatar) {
|
|
|
|
const oldAvatarFile = await buildFileFromIPicture(this.identity.avatar) as File;
|
|
|
|
const oldAvatarFileContent = await readFileAsync(oldAvatarFile);
|
|
|
|
const newAvatarFileContent = await readFileAsync(this.avatarFile as File);
|
|
|
|
if (oldAvatarFileContent === newAvatarFileContent) {
|
|
|
|
res.avatar = null;
|
|
|
|
}
|
2019-10-08 19:32:48 +02:00
|
|
|
}
|
|
|
|
return res;
|
2019-06-17 17:15:27 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
private async redirectIfNoIdentitySelected (identityParam?: string) {
|
|
|
|
if (!!identityParam) return;
|
|
|
|
|
|
|
|
await this.loadLoggedPersonIfNeeded();
|
|
|
|
|
2019-09-11 09:59:01 +02:00
|
|
|
if (!!this.currentActor) {
|
|
|
|
await this.$router.push({ params: { identityName: this.currentActor.preferredUsername } });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private async maybeUpdateCurrentActorCache(identity: IPerson) {
|
|
|
|
if (this.currentActor) {
|
|
|
|
if (this.currentActor.preferredUsername === this.identity.preferredUsername) {
|
|
|
|
await changeIdentity(this.$apollo.provider.defaultClient, identity);
|
|
|
|
}
|
|
|
|
this.currentActor = identity;
|
2019-06-17 17:15:27 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private async loadLoggedPersonIfNeeded (bypassCache = false) {
|
2019-09-11 09:59:01 +02:00
|
|
|
if (this.currentActor) return;
|
2019-06-17 17:15:27 +02:00
|
|
|
|
|
|
|
const result = await this.$apollo.query({
|
2019-09-11 09:59:01 +02:00
|
|
|
query: CURRENT_ACTOR_CLIENT,
|
2019-06-17 17:15:27 +02:00
|
|
|
fetchPolicy: bypassCache ? 'network-only' : undefined,
|
|
|
|
});
|
|
|
|
|
2019-09-11 09:59:01 +02:00
|
|
|
this.currentActor = result.data.currentActor;
|
2019-06-17 17:15:27 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
private resetFields () {
|
|
|
|
this.identityName = undefined;
|
|
|
|
this.identity = new Person();
|
|
|
|
this.oldDisplayName = null;
|
|
|
|
this.avatarFile = null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
</script>
|