Merge branch 'upgrade-deps' into 'master'

Upgrade deps

See merge request framasoft/mobilizon!731
This commit is contained in:
Thomas Citharel 2020-11-30 18:34:45 +01:00
commit c39a771fd5
212 changed files with 5006 additions and 2477 deletions

View File

@ -158,17 +158,14 @@ config :geolix,
} }
] ]
config :auto_linker, config :mobilizon, Mobilizon.Service.Formatter,
opts: [
scheme: true,
extra: true,
# TODO: Set to :no_scheme when it works properly
validate_tld: true,
class: false, class: false,
strip_prefix: false, rel: "noopener noreferrer ugc",
new_window: true, new_window: true,
rel: "noopener noreferrer ugc" truncate: false,
] strip_prefix: false,
extra: true,
validate_tld: :no_scheme
config :tesla, adapter: Tesla.Adapter.Hackney config :tesla, adapter: Tesla.Adapter.Hackney

View File

@ -4,4 +4,4 @@ indent_size = 2
end_of_line = lf end_of_line = lf
trim_trailing_whitespace = true trim_trailing_whitespace = true
insert_final_newline = true insert_final_newline = true
max_line_length = 100 max_line_length = 80

View File

@ -38,8 +38,11 @@ module.exports = {
"error", "error",
{ {
ignoreStrings: true, ignoreStrings: true,
ignoreHTMLTextContents: true,
ignoreTemplateLiterals: true,
ignoreComments: true,
template: 170, template: 170,
code: 100, code: 80,
}, },
], ],
"prettier/prettier": "error", "prettier/prettier": "error",
@ -48,13 +51,18 @@ module.exports = {
"import/prefer-default-export": "off", "import/prefer-default-export": "off",
"import/extensions": "off", "import/extensions": "off",
"import/no-unresolved": "off", "import/no-unresolved": "off",
"no-shadow": "off",
"@typescript-eslint/no-shadow": ["error"],
}, },
ignorePatterns: ["src/typings/*.d.ts", "vue.config.js"], ignorePatterns: ["src/typings/*.d.ts", "vue.config.js"],
overrides: [ overrides: [
{ {
files: ["**/__tests__/*.{j,t}s?(x)", "**/tests/unit/**/*.spec.{j,t}s?(x)"], files: [
"**/__tests__/*.{j,t}s?(x)",
"**/tests/unit/**/*.spec.{j,t}s?(x)",
],
env: { env: {
mocha: true, mocha: true,
}, },

View File

@ -24,7 +24,9 @@ fetch(`http://localhost:4000/api`, {
.then((result) => result.json()) .then((result) => result.json())
.then((result) => { .then((result) => {
// here we're filtering out any type information unrelated to unions or interfaces // here we're filtering out any type information unrelated to unions or interfaces
const filteredData = result.data.__schema.types.filter((type) => type.possibleTypes !== null); const filteredData = result.data.__schema.types.filter(
(type) => type.possibleTypes !== null
);
result.data.__schema.types = filteredData; result.data.__schema.types = filteredData;
fs.writeFile("./fragmentTypes.json", JSON.stringify(result.data), (err) => { fs.writeFile("./fragmentTypes.json", JSON.stringify(result.data), (err) => {
if (err) { if (err) {

View File

@ -63,13 +63,13 @@
"@types/vuedraggable": "^2.23.0", "@types/vuedraggable": "^2.23.0",
"@typescript-eslint/eslint-plugin": "^4.0.1", "@typescript-eslint/eslint-plugin": "^4.0.1",
"@typescript-eslint/parser": "^4.0.1", "@typescript-eslint/parser": "^4.0.1",
"@vue/cli-plugin-babel": "~4.5.8", "@vue/cli-plugin-babel": "~4.5.9",
"@vue/cli-plugin-e2e-cypress": "~4.5.8", "@vue/cli-plugin-e2e-cypress": "~4.5.9",
"@vue/cli-plugin-eslint": "~4.5.8", "@vue/cli-plugin-eslint": "~4.5.9",
"@vue/cli-plugin-pwa": "~4.5.8", "@vue/cli-plugin-pwa": "~4.5.9",
"@vue/cli-plugin-router": "~4.5.8", "@vue/cli-plugin-router": "~4.5.9",
"@vue/cli-plugin-typescript": "~4.5.8", "@vue/cli-plugin-typescript": "~4.5.9",
"@vue/cli-service": "~4.5.8", "@vue/cli-service": "~4.5.9",
"@vue/eslint-config-airbnb": "^5.0.2", "@vue/eslint-config-airbnb": "^5.0.2",
"@vue/eslint-config-prettier": "^6.0.0", "@vue/eslint-config-prettier": "^6.0.0",
"@vue/eslint-config-typescript": "^7.0.0", "@vue/eslint-config-typescript": "^7.0.0",
@ -79,11 +79,11 @@
"eslint-plugin-import": "^2.20.2", "eslint-plugin-import": "^2.20.2",
"eslint-plugin-prettier": "^3.1.3", "eslint-plugin-prettier": "^3.1.3",
"eslint-plugin-vue": "^7.0.0", "eslint-plugin-vue": "^7.0.0",
"prettier": "2.1.2", "prettier": "2.2.1",
"prettier-eslint": "^11.0.0", "prettier-eslint": "^12.0.0",
"sass": "^1.29.0", "sass": "^1.29.0",
"sass-loader": "^10.0.1", "sass-loader": "^10.0.1",
"typescript": "~4.0.2", "typescript": "~4.1.2",
"vue-cli-plugin-svg": "~0.1.3", "vue-cli-plugin-svg": "~0.1.3",
"vue-i18n-extract": "^1.0.2", "vue-i18n-extract": "^1.0.2",
"vue-template-compiler": "^2.6.11", "vue-template-compiler": "^2.6.11",

18
js/src/@types/dom.d.ts vendored Normal file
View File

@ -0,0 +1,18 @@
declare global {
interface GeolocationCoordinates {
readonly accuracy: number;
readonly altitude: number | null;
readonly altitudeAccuracy: number | null;
readonly heading: number | null;
readonly latitude: number;
readonly longitude: number;
readonly speed: number | null;
}
interface GeolocationPosition {
readonly coords: GeolocationCoordinates;
readonly timestamp: number;
}
}
export {};

View File

@ -32,8 +32,16 @@
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-property-decorator"; import { Component, Vue } from "vue-property-decorator";
import NavBar from "./components/NavBar.vue"; import NavBar from "./components/NavBar.vue";
import { AUTH_ACCESS_TOKEN, AUTH_USER_EMAIL, AUTH_USER_ID, AUTH_USER_ROLE } from "./constants"; import {
import { CURRENT_USER_CLIENT, UPDATE_CURRENT_USER_CLIENT } from "./graphql/user"; AUTH_ACCESS_TOKEN,
AUTH_USER_EMAIL,
AUTH_USER_ID,
AUTH_USER_ROLE,
} from "./constants";
import {
CURRENT_USER_CLIENT,
UPDATE_CURRENT_USER_CLIENT,
} from "./graphql/user";
import Footer from "./components/Footer.vue"; import Footer from "./components/Footer.vue";
import Logo from "./components/Logo.vue"; import Logo from "./components/Logo.vue";
import { initializeCurrentActor } from "./utils/auth"; import { initializeCurrentActor } from "./utils/auth";

View File

@ -1,8 +1,11 @@
import { ICurrentUserRole } from "@/types/enums";
import { ApolloCache } from "apollo-cache"; import { ApolloCache } from "apollo-cache";
import { NormalizedCacheObject } from "apollo-cache-inmemory"; import { NormalizedCacheObject } from "apollo-cache-inmemory";
import { ICurrentUserRole } from "@/types/current-user.model"; import { Resolvers } from "apollo-client/core/types";
export default function buildCurrentUserResolver(cache: ApolloCache<NormalizedCacheObject>) { export default function buildCurrentUserResolver(
cache: ApolloCache<NormalizedCacheObject>
): Resolvers {
cache.writeData({ cache.writeData({
data: { data: {
currentUser: { currentUser: {
@ -53,7 +56,12 @@ export default function buildCurrentUserResolver(cache: ApolloCache<NormalizedCa
preferredUsername, preferredUsername,
avatar, avatar,
name, name,
}: { id: string; preferredUsername: string; avatar: string; name: string }, }: {
id: string;
preferredUsername: string;
avatar: string;
name: string;
},
{ cache: localCache }: { cache: ApolloCache<NormalizedCacheObject> } { cache: localCache }: { cache: ApolloCache<NormalizedCacheObject> }
) => { ) => {
const data = { const data = {

View File

@ -1,4 +1,7 @@
import { IntrospectionFragmentMatcher, NormalizedCacheObject } from "apollo-cache-inmemory"; import {
IntrospectionFragmentMatcher,
NormalizedCacheObject,
} from "apollo-cache-inmemory";
import { AUTH_ACCESS_TOKEN, AUTH_REFRESH_TOKEN } from "@/constants"; import { AUTH_ACCESS_TOKEN, AUTH_REFRESH_TOKEN } from "@/constants";
import { REFRESH_TOKEN } from "@/graphql/auth"; import { REFRESH_TOKEN } from "@/graphql/auth";
import { saveTokenData } from "@/utils/auth"; import { saveTokenData } from "@/utils/auth";
@ -11,7 +14,11 @@ export const fragmentMatcher = new IntrospectionFragmentMatcher({
{ {
kind: "UNION", kind: "UNION",
name: "SearchResult", name: "SearchResult",
possibleTypes: [{ name: "Event" }, { name: "Person" }, { name: "Group" }], possibleTypes: [
{ name: "Event" },
{ name: "Person" },
{ name: "Group" },
],
}, },
{ {
kind: "INTERFACE", kind: "INTERFACE",

View File

@ -13,7 +13,12 @@
<template slot-scope="props"> <template slot-scope="props">
<div class="media"> <div class="media">
<div class="media-left"> <div class="media-left">
<img width="32" :src="props.option.avatar.url" v-if="props.option.avatar" alt="" /> <img
width="32"
:src="props.option.avatar.url"
v-if="props.option.avatar"
alt=""
/>
<b-icon v-else icon="account-circle" /> <b-icon v-else icon="account-circle" />
</div> </div>
<div class="media-content"> <div class="media-content">
@ -21,7 +26,9 @@
{{ props.option.name }} {{ props.option.name }}
<br /> <br />
<small>{{ `@${props.option.preferredUsername}` }}</small> <small>{{ `@${props.option.preferredUsername}` }}</small>
<small v-if="props.option.domain">{{ `@${props.option.domain}` }}</small> <small v-if="props.option.domain">{{
`@${props.option.domain}`
}}</small>
</span> </span>
<span v-else> <span v-else>
{{ `@${props.option.preferredUsername}` }} {{ `@${props.option.preferredUsername}` }}
@ -53,7 +60,9 @@ export default class ActorAutoComplete extends Vue {
selected: IPerson | null = this.defaultSelected; selected: IPerson | null = this.defaultSelected;
name: string = this.defaultSelected ? this.defaultSelected.preferredUsername : ""; name: string = this.defaultSelected
? this.defaultSelected.preferredUsername
: "";
page = 1; page = 1;

View File

@ -12,8 +12,15 @@
<p> <p>
{{ actor.name || `@${usernameWithDomain(actor)}` }} {{ actor.name || `@${usernameWithDomain(actor)}` }}
</p> </p>
<p class="has-text-grey" v-if="actor.name">@{{ usernameWithDomain(actor) }}</p> <p class="has-text-grey" v-if="actor.name">
<div v-if="full" class="summary" :class="{ limit: limit }" v-html="actor.summary" /> @{{ usernameWithDomain(actor) }}
</p>
<div
v-if="full"
class="summary"
:class="{ limit: limit }"
v-html="actor.summary"
/>
</div> </div>
</div> </div>
</div> </div>

View File

@ -7,7 +7,10 @@
<ul class="identities"> <ul class="identities">
<li v-for="identity in identities" :key="identity.id"> <li v-for="identity in identities" :key="identity.id">
<router-link <router-link
:to="{ name: 'UpdateIdentity', params: { identityName: identity.preferredUsername } }" :to="{
name: 'UpdateIdentity',
params: { identityName: identity.preferredUsername },
}"
class="media identity" class="media identity"
v-bind:class="{ 'is-current-identity': isCurrentIdentity(identity) }" v-bind:class="{ 'is-current-identity': isCurrentIdentity(identity) }"
> >
@ -24,7 +27,10 @@
</li> </li>
</ul> </ul>
<router-link :to="{ name: 'CreateIdentity' }" class="button create-identity is-primary"> <router-link
:to="{ name: 'CreateIdentity' }"
class="button create-identity is-primary"
>
{{ $t("Create a new identity") }} {{ $t("Create a new identity") }}
</router-link> </router-link>
</section> </section>
@ -53,7 +59,7 @@ export default class Identities extends Vue {
errors: string[] = []; errors: string[] = [];
isCurrentIdentity(identity: IPerson) { isCurrentIdentity(identity: IPerson): boolean {
return identity.preferredUsername === this.currentIdentityName; return identity.preferredUsername === this.currentIdentityName;
} }
} }

View File

@ -1,94 +0,0 @@
<docs>
```vue
<participant-card :participant="{ actor: { preferredUsername: 'user1', name: 'someoneIDontLike' }, role: 'REJECTED' }" />
```
```vue
<participant-card :participant="{ actor: { preferredUsername: 'user2', name: 'someoneWhoWillWait' }, role: 'NOT_APPROVED' }" />
```
```vue
<participant-card :participant="{ actor: { preferredUsername: 'user3', name: 'a_participant' }, role: 'PARTICIPANT' }" />
```
```vue
<participant-card :participant="{ actor: { preferredUsername: 'me', name: 'myself' }, role: 'CREATOR' }" />
```
</docs>
<template>
<article class="card">
<div class="card-content">
<div class="media">
<div class="media-left" v-if="participant.actor.avatar">
<figure class="image is-48x48">
<img :src="participant.actor.avatar.url" />
</figure>
</div>
<div class="media-content">
<span ref="title">{{ actorDisplayName }}</span
><br />
<small class="has-text-grey" v-if="participant.actor.domain"
>@{{ participant.actor.preferredUsername }}@{{ participant.actor.domain }}</small
>
<small class="has-text-grey" v-else>@{{ participant.actor.preferredUsername }}</small>
</div>
</div>
</div>
<footer class="card-footer">
<b-button
v-if="[ParticipantRole.NOT_APPROVED, ParticipantRole.REJECTED].includes(participant.role)"
@click="accept(participant)"
type="is-success"
class="card-footer-item"
>{{ $t("Approve") }}</b-button
>
<b-button
v-if="participant.role === ParticipantRole.NOT_APPROVED"
@click="reject(participant)"
type="is-danger"
class="card-footer-item"
>{{ $t("Reject") }}</b-button
>
<b-button
v-if="participant.role === ParticipantRole.PARTICIPANT"
@click="exclude(participant)"
type="is-danger"
class="card-footer-item"
>{{ $t("Exclude") }}</b-button
>
<span v-if="participant.role === ParticipantRole.CREATOR" class="card-footer-item">{{
$t("Creator")
}}</span>
</footer>
</article>
</template>
<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
import { IParticipant, ParticipantRole } from "../../types/participant.model";
import { IPerson, Person } from "../../types/actor";
@Component
export default class ParticipantCard extends Vue {
@Prop({ required: true }) participant!: IParticipant;
@Prop({ type: Function }) accept!: Function;
@Prop({ type: Function }) reject!: Function;
@Prop({ type: Function }) exclude!: Function;
ParticipantRole = ParticipantRole;
get actorDisplayName(): string {
const actor = new Person(this.participant.actor as IPerson);
return actor.displayName();
}
}
</script>
<style lang="scss">
.card-footer-item {
height: $control-height;
}
</style>

View File

@ -12,8 +12,9 @@
</v-popover> </v-popover>
</template> </template>
<script lang="ts"> <script lang="ts">
import { ActorType } from "@/types/enums";
import { Component, Vue, Prop } from "vue-property-decorator"; import { Component, Vue, Prop } from "vue-property-decorator";
import { IActor, ActorType } from "../../types/actor"; import { IActor } from "../../types/actor";
import ActorCard from "./ActorCard.vue"; import ActorCard from "./ActorCard.vue";
@Component({ @Component({

View File

@ -16,11 +16,21 @@
checkable checkable
checkbox-position="left" checkbox-position="left"
> >
<b-table-column field="actor.id" label="ID" width="40" numeric v-slot="props">{{ <b-table-column
props.row.actor.id field="actor.id"
}}</b-table-column> label="ID"
width="40"
numeric
v-slot="props"
>{{ props.row.actor.id }}</b-table-column
>
<b-table-column field="actor.type" :label="$t('Type')" width="80" v-slot="props"> <b-table-column
field="actor.type"
:label="$t('Type')"
width="80"
v-slot="props"
>
<b-icon icon="lan" v-if="RelayMixin.isInstance(props.row.actor)" /> <b-icon icon="lan" v-if="RelayMixin.isInstance(props.row.actor)" />
<b-icon icon="account-circle" v-else /> <b-icon icon="account-circle" v-else />
</b-table-column> </b-table-column>
@ -33,26 +43,39 @@
centered centered
v-slot="props" v-slot="props"
> >
<span :class="`tag ${props.row.approved ? 'is-success' : 'is-danger'}`">{{ <span
props.row.approved ? $t("Accepted") : $t("Pending") :class="`tag ${props.row.approved ? 'is-success' : 'is-danger'}`"
}}</span> >{{ props.row.approved ? $t("Accepted") : $t("Pending") }}</span
>
</b-table-column> </b-table-column>
<b-table-column field="actor.domain" :label="$t('Domain')" sortable> <b-table-column field="actor.domain" :label="$t('Domain')" sortable>
<template v-slot:default="props"> <template v-slot:default="props">
<a @click="toggle(props.row)" v-if="RelayMixin.isInstance(props.row.actor)">{{ <a
props.row.actor.domain @click="toggle(props.row)"
}}</a> v-if="RelayMixin.isInstance(props.row.actor)"
>{{ props.row.actor.domain }}</a
>
<a @click="toggle(props.row)" v-else>{{ <a @click="toggle(props.row)" v-else>{{
`${props.row.actor.preferredUsername}@${props.row.actor.domain}` `${props.row.actor.preferredUsername}@${props.row.actor.domain}`
}}</a> }}</a>
</template> </template>
</b-table-column> </b-table-column>
<b-table-column field="targetActor.updatedAt" :label="$t('Date')" sortable v-slot="props"> <b-table-column
<span :title="$options.filters.formatDateTimeString(props.row.updatedAt)">{{ field="targetActor.updatedAt"
formatDistanceToNow(new Date(props.row.updatedAt), { locale: $dateFnsLocale }) :label="$t('Date')"
}}</span></b-table-column sortable
v-slot="props"
>
<span
:title="$options.filters.formatDateTimeString(props.row.updatedAt)"
>{{
formatDistanceToNow(new Date(props.row.updatedAt), {
locale: $dateFnsLocale,
})
}}</span
></b-table-column
> >
<template slot="detail" slot-scope="props"> <template slot="detail" slot-scope="props">
@ -143,7 +166,11 @@ export default class Followers extends Mixins(RelayMixin) {
await this.$apollo.queries.relayFollowers.refetch(); await this.$apollo.queries.relayFollowers.refetch();
this.checkedRows = []; this.checkedRows = [];
} catch (e) { } catch (e) {
Snackbar.open({ message: e.message, type: "is-danger", position: "is-bottom" }); Snackbar.open({
message: e.message,
type: "is-danger",
position: "is-bottom",
});
} }
} }
@ -158,7 +185,11 @@ export default class Followers extends Mixins(RelayMixin) {
await this.$apollo.queries.relayFollowers.refetch(); await this.$apollo.queries.relayFollowers.refetch();
this.checkedRows = []; this.checkedRows = [];
} catch (e) { } catch (e) {
Snackbar.open({ message: e.message, type: "is-danger", position: "is-bottom" }); Snackbar.open({
message: e.message,
type: "is-danger",
position: "is-bottom",
});
} }
} }

View File

@ -1,13 +1,22 @@
<template> <template>
<div> <div>
<form @submit="followRelay"> <form @submit="followRelay">
<b-field :label="$t('Add an instance')" custom-class="add-relay" horizontal> <b-field
:label="$t('Add an instance')"
custom-class="add-relay"
horizontal
>
<b-field grouped expanded size="is-large"> <b-field grouped expanded size="is-large">
<p class="control"> <p class="control">
<b-input v-model="newRelayAddress" :placeholder="$t('Ex: mobilizon.fr')" /> <b-input
v-model="newRelayAddress"
:placeholder="$t('Ex: mobilizon.fr')"
/>
</p> </p>
<p class="control"> <p class="control">
<b-button type="is-primary" native-type="submit">{{ $t("Add an instance") }}</b-button> <b-button type="is-primary" native-type="submit">{{
$t("Add an instance")
}}</b-button>
</p> </p>
</b-field> </b-field>
</b-field> </b-field>
@ -29,12 +38,25 @@
checkable checkable
checkbox-position="left" checkbox-position="left"
> >
<b-table-column field="targetActor.id" label="ID" width="40" numeric v-slot="props">{{ <b-table-column
props.row.targetActor.id field="targetActor.id"
}}</b-table-column> label="ID"
width="40"
numeric
v-slot="props"
>{{ props.row.targetActor.id }}</b-table-column
>
<b-table-column field="targetActor.type" :label="$t('Type')" width="80" v-slot="props"> <b-table-column
<b-icon icon="lan" v-if="RelayMixin.isInstance(props.row.targetActor)" /> field="targetActor.type"
:label="$t('Type')"
width="80"
v-slot="props"
>
<b-icon
icon="lan"
v-if="RelayMixin.isInstance(props.row.targetActor)"
/>
<b-icon icon="account-circle" v-else /> <b-icon icon="account-circle" v-else />
</b-table-column> </b-table-column>
@ -46,26 +68,39 @@
centered centered
v-slot="props" v-slot="props"
> >
<span :class="`tag ${props.row.approved ? 'is-success' : 'is-danger'}`">{{ <span
props.row.approved ? $t("Accepted") : $t("Pending") :class="`tag ${props.row.approved ? 'is-success' : 'is-danger'}`"
}}</span> >{{ props.row.approved ? $t("Accepted") : $t("Pending") }}</span
>
</b-table-column> </b-table-column>
<b-table-column field="targetActor.domain" :label="$t('Domain')" sortable> <b-table-column field="targetActor.domain" :label="$t('Domain')" sortable>
<template v-slot:default="props"> <template v-slot:default="props">
<a @click="toggle(props.row)" v-if="RelayMixin.isInstance(props.row.targetActor)">{{ <a
props.row.targetActor.domain @click="toggle(props.row)"
}}</a> v-if="RelayMixin.isInstance(props.row.targetActor)"
>{{ props.row.targetActor.domain }}</a
>
<a @click="toggle(props.row)" v-else>{{ <a @click="toggle(props.row)" v-else>{{
`${props.row.targetActor.preferredUsername}@${props.row.targetActor.domain}` `${props.row.targetActor.preferredUsername}@${props.row.targetActor.domain}`
}}</a> }}</a>
</template> </template>
</b-table-column> </b-table-column>
<b-table-column field="targetActor.updatedAt" :label="$t('Date')" sortable v-slot="props"> <b-table-column
<span :title="$options.filters.formatDateTimeString(props.row.updatedAt)">{{ field="targetActor.updatedAt"
formatDistanceToNow(new Date(props.row.updatedAt), { locale: $dateFnsLocale }) :label="$t('Date')"
}}</span></b-table-column sortable
v-slot="props"
>
<span
:title="$options.filters.formatDateTimeString(props.row.updatedAt)"
>{{
formatDistanceToNow(new Date(props.row.updatedAt), {
locale: $dateFnsLocale,
})
}}</span
></b-table-column
> >
<template slot="detail" slot-scope="props"> <template slot="detail" slot-scope="props">
@ -103,7 +138,6 @@ import { SnackbarProgrammatic as Snackbar } from "buefy";
import { formatDistanceToNow } from "date-fns"; import { formatDistanceToNow } from "date-fns";
import { ADD_RELAY, REMOVE_RELAY } from "../../graphql/admin"; import { ADD_RELAY, REMOVE_RELAY } from "../../graphql/admin";
import { IFollower } from "../../types/actor/follower.model"; import { IFollower } from "../../types/actor/follower.model";
import { Paginate } from "../../types/paginate";
import RelayMixin from "../../mixins/relay"; import RelayMixin from "../../mixins/relay";
@Component({ @Component({
@ -133,13 +167,19 @@ export default class Followings extends Mixins(RelayMixin) {
await this.$apollo.queries.relayFollowings.refetch(); await this.$apollo.queries.relayFollowings.refetch();
this.newRelayAddress = ""; this.newRelayAddress = "";
} catch (err) { } catch (err) {
Snackbar.open({ message: err.message, type: "is-danger", position: "is-bottom" }); Snackbar.open({
message: err.message,
type: "is-danger",
position: "is-bottom",
});
} }
} }
async removeRelays(): Promise<void> { async removeRelays(): Promise<void> {
await this.checkedRows.forEach((row: IFollower) => { await this.checkedRows.forEach((row: IFollower) => {
this.removeRelay(`${row.targetActor.preferredUsername}@${row.targetActor.domain}`); this.removeRelay(
`${row.targetActor.preferredUsername}@${row.targetActor.domain}`
);
}); });
} }
@ -154,7 +194,11 @@ export default class Followings extends Mixins(RelayMixin) {
await this.$apollo.queries.relayFollowings.refetch(); await this.$apollo.queries.relayFollowings.refetch();
this.checkedRows = []; this.checkedRows = [];
} catch (e) { } catch (e) {
Snackbar.open({ message: e.message, type: "is-danger", position: "is-bottom" }); Snackbar.open({
message: e.message,
type: "is-danger",
position: "is-bottom",
});
} }
} }
} }

View File

@ -1,19 +1,34 @@
<template> <template>
<li :class="{ reply: comment.inReplyToComment }"> <li :class="{ reply: comment.inReplyToComment }">
<article class="media" :class="{ selected: commentSelected }" :id="commentId"> <article
class="media"
:class="{ selected: commentSelected }"
:id="commentId"
>
<popover-actor-card <popover-actor-card
class="media-left" class="media-left"
:actor="comment.actor" :actor="comment.actor"
:inline="true" :inline="true"
v-if="comment.actor" v-if="comment.actor"
> >
<figure class="image is-48x48" v-if="!comment.deletedAt && comment.actor.avatar"> <figure
class="image is-48x48"
v-if="!comment.deletedAt && comment.actor.avatar"
>
<img class="is-rounded" :src="comment.actor.avatar.url" alt="" /> <img class="is-rounded" :src="comment.actor.avatar.url" alt="" />
</figure> </figure>
<b-icon class="media-left" v-else size="is-large" icon="account-circle" /> <b-icon
class="media-left"
v-else
size="is-large"
icon="account-circle"
/>
</popover-actor-card> </popover-actor-card>
<div v-else class="media-left"> <div v-else class="media-left">
<figure class="image is-48x48" v-if="!comment.deletedAt && comment.actor.avatar"> <figure
class="image is-48x48"
v-if="!comment.deletedAt && comment.actor.avatar"
>
<img class="is-rounded" :src="comment.actor.avatar.url" alt="" /> <img class="is-rounded" :src="comment.actor.avatar.url" alt="" />
</figure> </figure>
<b-icon v-else size="is-large" icon="account-circle" /> <b-icon v-else size="is-large" icon="account-circle" />
@ -21,7 +36,9 @@
<div class="media-content"> <div class="media-content">
<div class="content"> <div class="content">
<span class="first-line" v-if="!comment.deletedAt"> <span class="first-line" v-if="!comment.deletedAt">
<strong :class="{ organizer: commentFromOrganizer }">{{ comment.actor.name }}</strong> <strong :class="{ organizer: commentFromOrganizer }">{{
comment.actor.name
}}</strong>
<small>@{{ usernameWithDomain(comment.actor) }}</small> <small>@{{ usernameWithDomain(comment.actor) }}</small>
<a class="comment-link has-text-grey" :href="commentURL"> <a class="comment-link has-text-grey" :href="commentURL">
<small>{{ <small>{{
@ -54,10 +71,15 @@
<div class="load-replies" v-if="comment.totalReplies"> <div class="load-replies" v-if="comment.totalReplies">
<p v-if="!showReplies" @click="fetchReplies"> <p v-if="!showReplies" @click="fetchReplies">
<b-icon icon="chevron-down" /><span>{{ <b-icon icon="chevron-down" /><span>{{
$tc("View a reply", comment.totalReplies, { totalReplies: comment.totalReplies }) $tc("View a reply", comment.totalReplies, {
totalReplies: comment.totalReplies,
})
}}</span> }}</span>
</p> </p>
<p v-else-if="comment.totalReplies && showReplies" @click="showReplies = false"> <p
v-else-if="comment.totalReplies && showReplies"
@click="showReplies = false"
>
<b-icon icon="chevron-up" /> <b-icon icon="chevron-up" />
<span>{{ $t("Hide replies") }}</span> <span>{{ $t("Hide replies") }}</span>
</p> </p>
@ -86,14 +108,24 @@
</nav> </nav>
</div> </div>
</article> </article>
<form class="reply" @submit.prevent="replyToComment" v-if="currentActor.id" v-show="replyTo"> <form
class="reply"
@submit.prevent="replyToComment"
v-if="currentActor.id"
v-show="replyTo"
>
<article class="media reply"> <article class="media reply">
<figure class="media-left" v-if="currentActor.avatar"> <figure class="media-left" v-if="currentActor.avatar">
<p class="image is-48x48"> <p class="image is-48x48">
<img :src="currentActor.avatar.url" alt="" /> <img :src="currentActor.avatar.url" alt="" />
</p> </p>
</figure> </figure>
<b-icon class="media-left" v-else size="is-large" icon="account-circle" /> <b-icon
class="media-left"
v-else
size="is-large"
icon="account-circle"
/>
<div class="media-content"> <div class="media-content">
<div class="content"> <div class="content">
<span class="first-line"> <span class="first-line">
@ -102,7 +134,12 @@
</span> </span>
<br /> <br />
<span class="editor-line"> <span class="editor-line">
<editor class="editor" ref="commentEditor" v-model="newComment.text" mode="comment" /> <editor
class="editor"
ref="commentEditor"
v-model="newComment.text"
mode="comment"
/>
<b-button <b-button
:disabled="newComment.text.trim().length === 0" :disabled="newComment.text.trim().length === 0"
native-type="submit" native-type="submit"
@ -118,7 +155,12 @@
<div class="left"> <div class="left">
<div class="vertical-border" @click="showReplies = false" /> <div class="vertical-border" @click="showReplies = false" />
</div> </div>
<transition-group name="comment-replies" v-if="showReplies" class="comment-replies" tag="ul"> <transition-group
name="comment-replies"
v-if="showReplies"
class="comment-replies"
tag="ul"
>
<comment <comment
class="reply" class="reply"
v-for="reply in comment.replies" v-for="reply in comment.replies"
@ -137,7 +179,7 @@ import { Component, Prop, Vue, Ref } from "vue-property-decorator";
import EditorComponent from "@/components/Editor.vue"; import EditorComponent from "@/components/Editor.vue";
import { SnackbarProgrammatic as Snackbar } from "buefy"; import { SnackbarProgrammatic as Snackbar } from "buefy";
import { formatDistanceToNow } from "date-fns"; import { formatDistanceToNow } from "date-fns";
import { CommentModeration } from "../../types/event-options.model"; import { CommentModeration } from "@/types/enums";
import { CommentModel, IComment } from "../../types/comment.model"; import { CommentModel, IComment } from "../../types/comment.model";
import { CURRENT_ACTOR_CLIENT } from "../../graphql/actor"; import { CURRENT_ACTOR_CLIENT } from "../../graphql/actor";
import { IPerson, usernameWithDomain } from "../../types/actor"; import { IPerson, usernameWithDomain } from "../../types/actor";
@ -155,7 +197,8 @@ import PopoverActorCard from "../Account/PopoverActorCard.vue";
}, },
}, },
components: { components: {
editor: () => import(/* webpackChunkName: "editor" */ "@/components/Editor.vue"), editor: () =>
import(/* webpackChunkName: "editor" */ "@/components/Editor.vue"),
comment: () => import(/* webpackChunkName: "comment" */ "./Comment.vue"), comment: () => import(/* webpackChunkName: "comment" */ "./Comment.vue"),
PopoverActorCard, PopoverActorCard,
}, },
@ -167,7 +210,9 @@ export default class Comment extends Vue {
// Hack because Vue only exports it's own interface. // Hack because Vue only exports it's own interface.
// See https://github.com/kaorun343/vue-property-decorator/issues/257 // See https://github.com/kaorun343/vue-property-decorator/issues/257
@Ref() readonly commentEditor!: EditorComponent & { replyToComment: (comment: IComment) => void }; @Ref() readonly commentEditor!: EditorComponent & {
replyToComment: (comment: IComment) => void;
};
currentActor!: IPerson; currentActor!: IPerson;
@ -231,7 +276,9 @@ export default class Comment extends Vue {
if (!eventData) return; if (!eventData) return;
const { event } = eventData; const { event } = eventData;
const { comments } = event; const { comments } = event;
const parentCommentIndex = comments.findIndex((oldComment) => oldComment.id === parentId); const parentCommentIndex = comments.findIndex(
(oldComment) => oldComment.id === parentId
);
const parentComment = comments[parentCommentIndex]; const parentComment = comments[parentCommentIndex];
if (!parentComment) return; if (!parentComment) return;
parentComment.replies = thread; parentComment.replies = thread;
@ -303,7 +350,11 @@ export default class Comment extends Vue {
duration: 5000, duration: 5000,
}); });
} catch (e) { } catch (e) {
Snackbar.open({ message: e.message, type: "is-danger", position: "is-bottom" }); Snackbar.open({
message: e.message,
type: "is-danger",
position: "is-bottom",
});
} }
} }
} }

View File

@ -6,9 +6,11 @@
@submit.prevent="createCommentForEvent(newComment)" @submit.prevent="createCommentForEvent(newComment)"
@keyup.ctrl.enter="createCommentForEvent(newComment)" @keyup.ctrl.enter="createCommentForEvent(newComment)"
> >
<b-notification v-if="isEventOrganiser && !areCommentsClosed" :closable="false">{{ <b-notification
$t("Comments are closed for everybody else.") v-if="isEventOrganiser && !areCommentsClosed"
}}</b-notification> :closable="false"
>{{ $t("Comments are closed for everybody else.") }}</b-notification
>
<article class="media"> <article class="media">
<figure class="media-left"> <figure class="media-left">
<identity-picker-wrapper :inline="false" v-model="newComment.actor" /> <identity-picker-wrapper :inline="false" v-model="newComment.actor" />
@ -16,11 +18,17 @@
<div class="media-content"> <div class="media-content">
<div class="field"> <div class="field">
<p class="control"> <p class="control">
<editor ref="commenteditor" mode="comment" v-model="newComment.text" /> <editor
ref="commenteditor"
mode="comment"
v-model="newComment.text"
/>
</p> </p>
</div> </div>
<div class="send-comment"> <div class="send-comment">
<b-button native-type="submit" type="is-primary">{{ $t("Post a comment") }}</b-button> <b-button native-type="submit" type="is-primary">{{
$t("Post a comment")
}}</b-button>
</div> </div>
</div> </div>
</article> </article>
@ -29,7 +37,12 @@
$t("The organiser has chosen to close comments.") $t("The organiser has chosen to close comments.")
}}</b-notification> }}</b-notification>
<transition name="comment-empty-list" mode="out-in"> <transition name="comment-empty-list" mode="out-in">
<transition-group name="comment-list" v-if="comments.length" class="comment-list" tag="ul"> <transition-group
name="comment-list"
v-if="comments.length"
class="comment-list"
tag="ul"
>
<comment <comment
class="root-comment" class="root-comment"
:comment="comment" :comment="comment"
@ -51,7 +64,7 @@
import { Prop, Vue, Component, Watch } from "vue-property-decorator"; import { Prop, Vue, Component, Watch } from "vue-property-decorator";
import Comment from "@/components/Comment/Comment.vue"; import Comment from "@/components/Comment/Comment.vue";
import IdentityPickerWrapper from "@/views/Account/IdentityPickerWrapper.vue"; import IdentityPickerWrapper from "@/views/Account/IdentityPickerWrapper.vue";
import { CommentModeration } from "../../types/event-options.model"; import { CommentModeration } from "@/types/enums";
import { CommentModel, IComment } from "../../types/comment.model"; import { CommentModel, IComment } from "../../types/comment.model";
import { import {
CREATE_COMMENT_FROM_EVENT, CREATE_COMMENT_FROM_EVENT,
@ -76,7 +89,9 @@ import { IEvent } from "../../types/event.model";
}; };
}, },
update(data) { update(data) {
return data.event.comments.map((comment: IComment) => new CommentModel(comment)); return data.event.comments.map(
(comment: IComment) => new CommentModel(comment)
);
}, },
skip() { skip() {
return !this.event.uuid; return !this.event.uuid;
@ -86,7 +101,8 @@ import { IEvent } from "../../types/event.model";
components: { components: {
Comment, Comment,
IdentityPickerWrapper, IdentityPickerWrapper,
editor: () => import(/* webpackChunkName: "editor" */ "@/components/Editor.vue"), editor: () =>
import(/* webpackChunkName: "editor" */ "@/components/Editor.vue"),
}, },
}) })
export default class CommentTree extends Vue { export default class CommentTree extends Vue {
@ -113,7 +129,9 @@ export default class CommentTree extends Vue {
variables: { variables: {
eventId: this.event.id, eventId: this.event.id,
text: comment.text, text: comment.text,
inReplyToCommentId: comment.inReplyToComment ? comment.inReplyToComment.id : null, inReplyToCommentId: comment.inReplyToComment
? comment.inReplyToComment.id
: null,
}, },
update: (store, { data }) => { update: (store, { data }) => {
if (data == null) return; if (data == null) return;
@ -228,7 +246,9 @@ export default class CommentTree extends Vue {
}); });
if (!localData) return; if (!localData) return;
const { thread: oldReplyList } = localData; const { thread: oldReplyList } = localData;
const replies = oldReplyList.filter((reply) => reply.id !== deletedCommentId); const replies = oldReplyList.filter(
(reply) => reply.id !== deletedCommentId
);
store.writeQuery({ store.writeQuery({
query: FETCH_THREAD_REPLIES, query: FETCH_THREAD_REPLIES,
variables: { variables: {
@ -249,7 +269,9 @@ export default class CommentTree extends Vue {
event.comments = oldComments; event.comments = oldComments;
} else { } else {
// we have deleted a thread itself // we have deleted a thread itself
event.comments = oldComments.filter((reply) => reply.id !== deletedCommentId); event.comments = oldComments.filter(
(reply) => reply.id !== deletedCommentId
);
} }
store.writeQuery({ store.writeQuery({
query: COMMENTS_THREADS, query: COMMENTS_THREADS,
@ -274,14 +296,18 @@ export default class CommentTree extends Vue {
.filter((comment) => comment.inReplyToComment == null) .filter((comment) => comment.inReplyToComment == null)
.sort((a, b) => { .sort((a, b) => {
if (a.updatedAt && b.updatedAt) { if (a.updatedAt && b.updatedAt) {
return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(); return (
new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
);
} }
return 0; return 0;
}); });
} }
get filteredOrderedComments(): IComment[] { get filteredOrderedComments(): IComment[] {
return this.orderedComments.filter((comment) => !comment.deletedAt || comment.totalReplies > 0); return this.orderedComments.filter(
(comment) => !comment.deletedAt || comment.totalReplies > 0
);
} }
get isEventOrganiser(): boolean { get isEventOrganiser(): boolean {

View File

@ -15,7 +15,10 @@
<span v-else class="name comment-link has-text-grey"> <span v-else class="name comment-link has-text-grey">
{{ $t("[deleted]") }} {{ $t("[deleted]") }}
</span> </span>
<span class="icons" v-if="!comment.deletedAt && comment.actor.id === currentActor.id"> <span
class="icons"
v-if="!comment.deletedAt && comment.actor.id === currentActor.id"
>
<b-dropdown aria-role="list"> <b-dropdown aria-role="list">
<b-icon slot="trigger" role="button" icon="dots-horizontal" /> <b-icon slot="trigger" role="button" icon="dots-horizontal" />
@ -44,8 +47,9 @@
<div class="post-infos"> <div class="post-infos">
<span :title="comment.insertedAt | formatDateTimeString"> <span :title="comment.insertedAt | formatDateTimeString">
{{ {{
formatDistanceToNow(new Date(comment.updatedAt), { locale: $dateFnsLocale }) || formatDistanceToNow(new Date(comment.updatedAt), {
$t("Right now") locale: $dateFnsLocale,
}) || $t("Right now")
}}</span }}</span
> >
</div> </div>
@ -77,7 +81,9 @@
type="is-primary" type="is-primary"
>{{ $t("Update") }}</b-button >{{ $t("Update") }}</b-button
> >
<b-button native-type="button" @click="toggleEditMode">{{ $t("Cancel") }}</b-button> <b-button native-type="button" @click="toggleEditMode">{{
$t("Cancel")
}}</b-button>
</div> </div>
</form> </form>
</div> </div>
@ -95,7 +101,8 @@ import { CURRENT_ACTOR_CLIENT } from "../../graphql/actor";
currentActor: CURRENT_ACTOR_CLIENT, currentActor: CURRENT_ACTOR_CLIENT,
}, },
components: { components: {
editor: () => import(/* webpackChunkName: "editor" */ "@/components/Editor.vue"), editor: () =>
import(/* webpackChunkName: "editor" */ "@/components/Editor.vue"),
}, },
}) })
export default class DiscussionComment extends Vue { export default class DiscussionComment extends Vue {

View File

@ -1,14 +1,23 @@
<template> <template>
<router-link <router-link
class="discussion-minimalist-card-wrapper" class="discussion-minimalist-card-wrapper"
:to="{ name: RouteName.DISCUSSION, params: { slug: discussion.slug, id: discussion.id } }" :to="{
name: RouteName.DISCUSSION,
params: { slug: discussion.slug, id: discussion.id },
}"
> >
<div class="media-left"> <div class="media-left">
<figure <figure
class="image is-32x32" class="image is-32x32"
v-if="discussion.lastComment.actor && discussion.lastComment.actor.avatar" v-if="
discussion.lastComment.actor && discussion.lastComment.actor.avatar
"
> >
<img class="is-rounded" :src="discussion.lastComment.actor.avatar.url" alt /> <img
class="is-rounded"
:src="discussion.lastComment.actor.avatar.url"
alt
/>
</figure> </figure>
<b-icon v-else size="is-medium" icon="account-circle" /> <b-icon v-else size="is-medium" icon="account-circle" />
</div> </div>
@ -17,15 +26,18 @@
<p class="discussion-minimalist-title">{{ discussion.title }}</p> <p class="discussion-minimalist-title">{{ discussion.title }}</p>
<span :title="actualDate | formatDateTimeString"> <span :title="actualDate | formatDateTimeString">
{{ {{
formatDistanceToNowStrict(new Date(actualDate), { locale: $dateFnsLocale }) || formatDistanceToNowStrict(new Date(actualDate), {
$t("Right now") locale: $dateFnsLocale,
}) || $t("Right now")
}}</span }}</span
> >
</div> </div>
<div class="has-text-grey" v-if="!discussion.lastComment.deletedAt"> <div class="has-text-grey" v-if="!discussion.lastComment.deletedAt">
{{ htmlTextEllipsis }} {{ htmlTextEllipsis }}
</div> </div>
<div v-else class="has-text-grey">{{ $t("[This comment has been deleted]") }}</div> <div v-else class="has-text-grey">
{{ $t("[This comment has been deleted]") }}
</div>
</div> </div>
</router-link> </router-link>
</template> </template>
@ -54,7 +66,10 @@ export default class DiscussionListItem extends Vue {
} }
get actualDate(): string | Date | undefined { get actualDate(): string | Date | undefined {
if (this.discussion.updatedAt === this.discussion.insertedAt && this.discussion.lastComment) { if (
this.discussion.updatedAt === this.discussion.insertedAt &&
this.discussion.lastComment
) {
return this.discussion.lastComment.publishedAt; return this.discussion.lastComment.publishedAt;
} }
return this.discussion.updatedAt; return this.discussion.updatedAt;
@ -83,7 +98,8 @@ export default class DiscussionListItem extends Vue {
.discussion-minimalist-title { .discussion-minimalist-title {
color: #3c376e; color: #3c376e;
font-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica, Arial, serif; font-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica,
Arial, serif;
font-size: 1.25rem; font-size: 1.25rem;
font-weight: 700; font-weight: 700;
flex: 1; flex: 1;

View File

@ -117,11 +117,21 @@
<b-icon icon="format-quote-close" /> <b-icon icon="format-quote-close" />
</button> </button>
<button v-if="!isBasicMode" class="menubar__button" @click="commands.undo" type="button"> <button
v-if="!isBasicMode"
class="menubar__button"
@click="commands.undo"
type="button"
>
<b-icon icon="undo" /> <b-icon icon="undo" />
</button> </button>
<button v-if="!isBasicMode" class="menubar__button" @click="commands.redo" type="button"> <button
v-if="!isBasicMode"
class="menubar__button"
@click="commands.redo"
type="button"
>
<b-icon icon="redo" /> <b-icon icon="redo" />
</button> </button>
</div> </div>
@ -181,7 +191,9 @@
</div> </div>
</div> </div>
</template> </template>
<div v-else class="suggestion-list__item is-empty">{{ $t("No profiles found") }}</div> <div v-else class="suggestion-list__item is-empty">
{{ $t("No profiles found") }}
</div>
</div> </div>
</div> </div>
</template> </template>
@ -395,15 +407,7 @@ export default class EditorComponent extends Vue {
new Image(), new Image(),
new MaxSize({ maxSize: this.maxSize }), new MaxSize({ maxSize: this.maxSize }),
], ],
onUpdate: ({ onUpdate: ({ getHTML }: { getHTML: Function }) => {
getHTML,
transaction,
getJSON,
}: {
getHTML: Function;
getJSON: Function;
transaction: unknown;
}) => {
this.$emit("input", getHTML()); this.$emit("input", getHTML());
}, },
}); });
@ -438,7 +442,8 @@ export default class EditorComponent extends Vue {
upHandler(): void { upHandler(): void {
this.navigatedActorIndex = this.navigatedActorIndex =
(this.navigatedActorIndex + this.filteredActors.length - 1) % this.filteredActors.length; (this.navigatedActorIndex + this.filteredActors.length - 1) %
this.filteredActors.length;
} }
/** /**
@ -446,7 +451,8 @@ export default class EditorComponent extends Vue {
* if it's the last item, navigate to the first one * if it's the last item, navigate to the first one
*/ */
downHandler(): void { downHandler(): void {
this.navigatedActorIndex = (this.navigatedActorIndex + 1) % this.filteredActors.length; this.navigatedActorIndex =
(this.navigatedActorIndex + 1) % this.filteredActors.length;
} }
enterHandler(): void { enterHandler(): void {
@ -541,7 +547,10 @@ export default class EditorComponent extends Vue {
}, },
}); });
if (data.uploadMedia && data.uploadMedia.url) { if (data.uploadMedia && data.uploadMedia.url) {
command({ src: data.uploadMedia.url, "data-media-id": data.uploadMedia.id }); command({
src: data.uploadMedia.url,
"data-media-id": data.uploadMedia.id,
});
} }
} catch (error) { } catch (error) {
console.error(error); console.error(error);

View File

@ -48,9 +48,14 @@ export default class Image extends Node {
} }
commands({ type }: { type: NodeType }): any { commands({ type }: { type: NodeType }): any {
return (attrs: { [key: string]: string }) => (state: EditorState, dispatch: DispatchFn) => { return (attrs: { [key: string]: string }) => (
state: EditorState,
dispatch: DispatchFn
) => {
const { selection }: { selection: TextSelection } = state; const { selection }: { selection: TextSelection } = state;
const position = selection.$cursor ? selection.$cursor.pos : selection.$to.pos; const position = selection.$cursor
? selection.$cursor.pos
: selection.$to.pos;
const node = type.create(attrs); const node = type.create(attrs);
const transaction = state.tr.insert(position, node); const transaction = state.tr.insert(position, node);
dispatch(transaction); dispatch(transaction);
@ -75,7 +80,8 @@ export default class Image extends Node {
} }
const images = Array.from(realEvent.dataTransfer.files).filter( const images = Array.from(realEvent.dataTransfer.files).filter(
(file: any) => /image/i.test(file.type) && !/svg/i.test(file.type) (file: any) =>
/image/i.test(file.type) && !/svg/i.test(file.type)
); );
if (images.length === 0) { if (images.length === 0) {
@ -105,7 +111,10 @@ export default class Image extends Node {
src: data.uploadMedia.url, src: data.uploadMedia.url,
"data-media-id": data.uploadMedia.id, "data-media-id": data.uploadMedia.id,
}); });
const transaction = view.state.tr.insert(coordinates.pos, node); const transaction = view.state.tr.insert(
coordinates.pos,
node
);
view.dispatch(transaction); view.dispatch(transaction);
}); });
return true; return true;

View File

@ -1,11 +1,14 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck // @ts-nocheck
import { Extension, Plugin } from "tiptap"; import { Extension, Plugin } from "tiptap";
export default class MaxSize extends Extension { export default class MaxSize extends Extension {
// eslint-disable-next-line class-methods-use-this
get name() { get name() {
return "maxSize"; return "maxSize";
} }
// eslint-disable-next-line class-methods-use-this
get defaultOptions() { get defaultOptions() {
return { return {
maxSize: null, maxSize: null,
@ -21,7 +24,7 @@ export default class MaxSize extends Extension {
const newLength = newState.doc.content.size; const newLength = newState.doc.content.size;
if (newLength > max && newLength > oldLength) { if (newLength > max && newLength > oldLength) {
let newTr = newState.tr; const newTr = newState.tr;
newTr.insertText("", max + 1, newLength); newTr.insertText("", max + 1, newLength);
return newTr; return newTr;

View File

@ -21,9 +21,13 @@
</b-autocomplete> </b-autocomplete>
</b-field> </b-field>
<b-field v-if="isSecureContext()"> <b-field v-if="isSecureContext()">
<b-button type="is-text" v-if="!gettingLocation" icon-right="target" @click="locateMe">{{ <b-button
$t("Use my location") type="is-text"
}}</b-button> v-if="!gettingLocation"
icon-right="target"
@click="locateMe"
>{{ $t("Use my location") }}</b-button
>
<span v-else>{{ $t("Getting location") }}</span> <span v-else>{{ $t("Getting location") }}</span>
</b-field> </b-field>
<!-- <!--
@ -50,7 +54,7 @@
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue, Watch } from "vue-property-decorator"; import { Component, Prop, Vue, Watch } from "vue-property-decorator";
import { LatLng } from "leaflet"; import { LatLng } from "leaflet";
import { debounce } from "lodash"; import { debounce, DebouncedFunc } from "lodash";
import { Address, IAddress } from "../../types/address.model"; import { Address, IAddress } from "../../types/address.model";
import { ADDRESS, REVERSE_GEOCODE } from "../../graphql/address"; import { ADDRESS, REVERSE_GEOCODE } from "../../graphql/address";
import { CONFIG } from "../../graphql/config"; import { CONFIG } from "../../graphql/config";
@ -58,7 +62,8 @@ import { IConfig } from "../../types/config.model";
@Component({ @Component({
components: { components: {
"map-leaflet": () => import(/* webpackChunkName: "map" */ "@/components/Map.vue"), "map-leaflet": () =>
import(/* webpackChunkName: "map" */ "@/components/Map.vue"),
}, },
apollo: { apollo: {
config: CONFIG, config: CONFIG,
@ -81,7 +86,7 @@ export default class AddressAutoComplete extends Vue {
private gettingLocation = false; private gettingLocation = false;
private location!: Position; private location!: GeolocationPosition;
private gettingLocationError: any; private gettingLocationError: any;
@ -89,7 +94,7 @@ export default class AddressAutoComplete extends Vue {
config!: IConfig; config!: IConfig;
fetchAsyncData!: Function; fetchAsyncData!: DebouncedFunc<(query: string) => Promise<void>>;
// We put this in data because of issues like // We put this in data because of issues like
// https://github.com/vuejs/vue-class-component/issues/263 // https://github.com/vuejs/vue-class-component/issues/263
@ -121,7 +126,9 @@ export default class AddressAutoComplete extends Vue {
}, },
}); });
this.addressData = result.data.searchAddress.map((address: IAddress) => new Address(address)); this.addressData = result.data.searchAddress.map(
(address: IAddress) => new Address(address)
);
this.isFetching = false; this.isFetching = false;
} }
@ -174,7 +181,9 @@ export default class AddressAutoComplete extends Vue {
}, },
}); });
this.addressData = result.data.reverseGeocode.map((address: IAddress) => new Address(address)); this.addressData = result.data.reverseGeocode.map(
(address: IAddress) => new Address(address)
);
if (this.addressData.length > 0) { if (this.addressData.length > 0) {
const defaultAddress = new Address(this.addressData[0]); const defaultAddress = new Address(this.addressData[0]);
this.selected = defaultAddress; this.selected = defaultAddress;
@ -197,7 +206,10 @@ export default class AddressAutoComplete extends Vue {
this.location = await AddressAutoComplete.getLocation(); this.location = await AddressAutoComplete.getLocation();
this.mapDefaultZoom = 12; this.mapDefaultZoom = 12;
this.reverseGeoCode( this.reverseGeoCode(
new LatLng(this.location.coords.latitude, this.location.coords.longitude), new LatLng(
this.location.coords.latitude,
this.location.coords.longitude
),
12 12
); );
} catch (e) { } catch (e) {
@ -207,7 +219,7 @@ export default class AddressAutoComplete extends Vue {
this.gettingLocation = false; this.gettingLocation = false;
} }
static async getLocation(): Promise<Position> { static async getLocation(): Promise<GeolocationPosition> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (!("geolocation" in navigator)) { if (!("geolocation" in navigator)) {
reject(new Error("Geolocation is not available.")); reject(new Error("Geolocation is not available."));

View File

@ -102,14 +102,20 @@ export default class DateTimePicker extends Vue {
} }
get minTime(): Date | null { get minTime(): Date | null {
if (this.minDatetime && this.datesAreOnSameDay(this.dateWithTime, this.minDatetime)) { if (
this.minDatetime &&
this.datesAreOnSameDay(this.dateWithTime, this.minDatetime)
) {
return this.minDatetime; return this.minDatetime;
} }
return null; return null;
} }
get maxTime(): Date | null { get maxTime(): Date | null {
if (this.maxDatetime && this.datesAreOnSameDay(this.dateWithTime, this.maxDatetime)) { if (
this.maxDatetime &&
this.datesAreOnSameDay(this.dateWithTime, this.maxDatetime)
) {
return this.maxDatetime; return this.maxDatetime;
} }
return null; return null;

View File

@ -1,5 +1,8 @@
<template> <template>
<router-link class="card" :to="{ name: 'Event', params: { uuid: event.uuid } }"> <router-link
class="card"
:to="{ name: 'Event', params: { uuid: event.uuid } }"
>
<div class="card-image"> <div class="card-image">
<figure <figure
class="image is-16by9" class="image is-16by9"
@ -21,14 +24,18 @@
<div class="card-content"> <div class="card-content">
<div class="media"> <div class="media">
<div class="media-left"> <div class="media-left">
<date-calendar-icon v-if="!mergedOptions.hideDate" :date="event.beginsOn" /> <date-calendar-icon
v-if="!mergedOptions.hideDate"
:date="event.beginsOn"
/>
</div> </div>
<div class="media-content"> <div class="media-content">
<p class="event-title">{{ event.title }}</p> <p class="event-title">{{ event.title }}</p>
<div class="event-subtitle" v-if="event.physicalAddress"> <div class="event-subtitle" v-if="event.physicalAddress">
<!-- <p>{{ $t('By @{username}', { username: actor.preferredUsername }) }}</p>--> <!-- <p>{{ $t('By @{username}', { username: actor.preferredUsername }) }}</p>-->
<span> <span>
{{ event.physicalAddress.description }}, {{ event.physicalAddress.locality }} {{ event.physicalAddress.description }},
{{ event.physicalAddress.locality }}
</span> </span>
</div> </div>
</div> </div>
@ -77,7 +84,7 @@ import { IEvent, IEventCardOptions } from "@/types/event.model";
import { Component, Prop, Vue } from "vue-property-decorator"; import { Component, Prop, Vue } from "vue-property-decorator";
import DateCalendarIcon from "@/components/Event/DateCalendarIcon.vue"; import DateCalendarIcon from "@/components/Event/DateCalendarIcon.vue";
import { Actor, Person } from "@/types/actor"; import { Actor, Person } from "@/types/actor";
import { ParticipantRole } from "../../types/participant.model"; import { ParticipantRole } from "@/types/enums";
import RouteName from "../../router/name"; import RouteName from "../../router/name";
@Component({ @Component({

View File

@ -18,7 +18,9 @@
</docs> </docs>
<template> <template>
<span v-if="!endsOn">{{ beginsOn | formatDateTimeString(showStartTime) }}</span> <span v-if="!endsOn">{{
beginsOn | formatDateTimeString(showStartTime)
}}</span>
<span v-else-if="isSameDay() && showStartTime && showEndTime"> <span v-else-if="isSameDay() && showStartTime && showEndTime">
{{ {{
$t("On {date} from {startTime} to {endTime}", { $t("On {date} from {startTime} to {endTime}", {
@ -44,7 +46,9 @@
}) })
}} }}
</span> </span>
<span v-else-if="isSameDay()">{{ $t("On {date}", { date: formatDate(beginsOn) }) }}</span> <span v-else-if="isSameDay()">{{
$t("On {date}", { date: formatDate(beginsOn) })
}}</span>
<span v-else-if="endsOn && showStartTime && showEndTime"> <span v-else-if="endsOn && showStartTime && showEndTime">
{{ {{
$t("From the {startDate} at {startTime} to the {endDate} at {endTime}", { $t("From the {startDate} at {startTime} to the {endDate} at {endTime}", {
@ -97,7 +101,9 @@ export default class EventFullDate extends Vue {
} }
isSameDay(): boolean { isSameDay(): boolean {
const sameDay = new Date(this.beginsOn).toDateString() === new Date(this.endsOn).toDateString(); const sameDay =
new Date(this.beginsOn).toDateString() ===
new Date(this.endsOn).toDateString();
return this.endsOn !== undefined && sameDay; return this.endsOn !== undefined && sameDay;
} }
} }

View File

@ -6,25 +6,38 @@
<div class="date-component"> <div class="date-component">
<date-calendar-icon :date="participation.event.beginsOn" /> <date-calendar-icon :date="participation.event.beginsOn" />
</div> </div>
<router-link :to="{ name: RouteName.EVENT, params: { uuid: participation.event.uuid } }"> <router-link
:to="{
name: RouteName.EVENT,
params: { uuid: participation.event.uuid },
}"
>
<h3 class="title">{{ participation.event.title }}</h3> <h3 class="title">{{ participation.event.title }}</h3>
</router-link> </router-link>
</div> </div>
<div class="participation-actor has-text-grey"> <div class="participation-actor has-text-grey">
<span> <span>
<b-icon icon="earth" v-if="participation.event.visibility === EventVisibility.PUBLIC" /> <b-icon
icon="earth"
v-if="participation.event.visibility === EventVisibility.PUBLIC"
/>
<b-icon <b-icon
icon="link" icon="link"
v-else-if="participation.event.visibility === EventVisibility.UNLISTED" v-else-if="
participation.event.visibility === EventVisibility.UNLISTED
"
/> />
<b-icon <b-icon
icon="lock" icon="lock"
v-else-if="participation.event.visibility === EventVisibility.PRIVATE" v-else-if="
participation.event.visibility === EventVisibility.PRIVATE
"
/> />
</span> </span>
<span <span
v-if=" v-if="
participation.event.physicalAddress && participation.event.physicalAddress.locality participation.event.physicalAddress &&
participation.event.physicalAddress.locality
" "
>{{ participation.event.physicalAddress.locality }} -</span >{{ participation.event.physicalAddress.locality }} -</span
> >
@ -43,7 +56,11 @@
path="Going as {name}" path="Going as {name}"
tag="span" tag="span"
> >
<popover-actor-card slot="name" :actor="participation.actor" :inline="true"> <popover-actor-card
slot="name"
:actor="participation.actor"
:inline="true"
>
{{ participation.actor.displayName() }} {{ participation.actor.displayName() }}
</popover-actor-card> </popover-actor-card>
</i18n> </i18n>
@ -53,12 +70,15 @@
<span <span
class="participant-stats" class="participant-stats"
v-if=" v-if="
![ParticipantRole.PARTICIPANT, ParticipantRole.NOT_APPROVED].includes( ![
participation.role ParticipantRole.PARTICIPANT,
) ParticipantRole.NOT_APPROVED,
].includes(participation.role)
" "
> >
<span v-if="participation.event.options.maximumAttendeeCapacity !== 0"> <span
v-if="participation.event.options.maximumAttendeeCapacity !== 0"
>
{{ {{
$tc( $tc(
"{available}/{capacity} available places", "{available}/{capacity} available places",
@ -68,16 +88,21 @@
available: available:
participation.event.options.maximumAttendeeCapacity - participation.event.options.maximumAttendeeCapacity -
participation.event.participantStats.participant, participation.event.participantStats.participant,
capacity: participation.event.options.maximumAttendeeCapacity, capacity:
participation.event.options.maximumAttendeeCapacity,
} }
) )
}} }}
</span> </span>
<span v-else> <span v-else>
{{ {{
$tc("{count} participants", participation.event.participantStats.participant, { $tc(
"{count} participants",
participation.event.participantStats.participant,
{
count: participation.event.participantStats.participant, count: participation.event.participantStats.participant,
}) }
)
}} }}
</span> </span>
<span v-if="participation.event.participantStats.notApproved > 0"> <span v-if="participation.event.participantStats.notApproved > 0">
@ -107,9 +132,10 @@
<ul> <ul>
<li <li
v-if=" v-if="
![ParticipantRole.PARTICIPANT, ParticipantRole.NOT_APPROVED].includes( ![
participation.role ParticipantRole.PARTICIPANT,
) ParticipantRole.NOT_APPROVED,
].includes(participation.role)
" "
> >
<b-button <b-button
@ -140,19 +166,23 @@
</li> </li>
<li <li
v-if=" v-if="
![ParticipantRole.PARTICIPANT, ParticipantRole.NOT_APPROVED].includes( ![
participation.role ParticipantRole.PARTICIPANT,
) ParticipantRole.NOT_APPROVED,
].includes(participation.role)
" "
@click="openDeleteEventModalWrapper" @click="openDeleteEventModalWrapper"
> >
<b-button type="is-text" icon-left="delete">{{ $t("Delete") }}</b-button> <b-button type="is-text" icon-left="delete">{{
$t("Delete")
}}</b-button>
</li> </li>
<li <li
v-if=" v-if="
![ParticipantRole.PARTICIPANT, ParticipantRole.NOT_APPROVED].includes( ![
participation.role ParticipantRole.PARTICIPANT,
) ParticipantRole.NOT_APPROVED,
].includes(participation.role)
" "
> >
<b-button <b-button
@ -172,7 +202,10 @@
tag="router-link" tag="router-link"
icon-left="view-compact" icon-left="view-compact"
type="is-text" type="is-text"
:to="{ name: RouteName.EVENT, params: { uuid: participation.event.uuid } }" :to="{
name: RouteName.EVENT,
params: { uuid: participation.event.uuid },
}"
>{{ $t("View event page") }}</b-button >{{ $t("View event page") }}</b-button
> >
</li> </li>
@ -187,8 +220,9 @@ import { Component, Prop } from "vue-property-decorator";
import DateCalendarIcon from "@/components/Event/DateCalendarIcon.vue"; import DateCalendarIcon from "@/components/Event/DateCalendarIcon.vue";
import { mixins } from "vue-class-component"; import { mixins } from "vue-class-component";
import { RawLocation, Route } from "vue-router"; import { RawLocation, Route } from "vue-router";
import { IParticipant, ParticipantRole } from "../../types/participant.model"; import { EventVisibility, ParticipantRole } from "@/types/enums";
import { EventVisibility, IEventCardOptions } from "../../types/event.model"; import { IParticipant } from "../../types/participant.model";
import { IEventCardOptions } from "../../types/event.model";
import { IPerson } from "../../types/actor"; import { IPerson } from "../../types/actor";
import ActorMixin from "../../mixins/actor"; import ActorMixin from "../../mixins/actor";
import { CURRENT_ACTOR_CLIENT } from "../../graphql/actor"; import { CURRENT_ACTOR_CLIENT } from "../../graphql/actor";
@ -249,8 +283,14 @@ export default class EventListCard extends mixins(ActorMixin, EventMixin) {
await this.openDeleteEventModal(this.participation.event); await this.openDeleteEventModal(this.participation.event);
} }
async gotToWithCheck(participation: IParticipant, route: RawLocation): Promise<Route> { async gotToWithCheck(
if (participation.actor.id !== this.currentActor.id && participation.event.organizerActor) { participation: IParticipant,
route: RawLocation
): Promise<Route> {
if (
participation.actor.id !== this.currentActor.id &&
participation.event.organizerActor
) {
const organizer = participation.event.organizerActor as IPerson; const organizer = participation.event.organizerActor as IPerson;
await changeIdentity(this.$apollo.provider.defaultClient, organizer); await changeIdentity(this.$apollo.provider.defaultClient, organizer);
this.$buefy.notification.open({ this.$buefy.notification.open({

View File

@ -6,7 +6,9 @@
<div class="date-component"> <div class="date-component">
<date-calendar-icon :date="event.beginsOn" /> <date-calendar-icon :date="event.beginsOn" />
</div> </div>
<router-link :to="{ name: RouteName.EVENT, params: { uuid: event.uuid } }"> <router-link
:to="{ name: RouteName.EVENT, params: { uuid: event.uuid } }"
>
<h2 class="title">{{ event.title }}</h2> <h2 class="title">{{ event.title }}</h2>
</router-link> </router-link>
</div> </div>
@ -15,17 +17,34 @@
{{ event.physicalAddress.locality }} {{ event.physicalAddress.locality }}
</span> </span>
<span v-if="event.attributedTo && options.memberofGroup"> <span v-if="event.attributedTo && options.memberofGroup">
{{ $t("Created by {name}", { name: usernameWithDomain(event.organizerActor) }) }} {{
$t("Created by {name}", {
name: usernameWithDomain(event.organizerActor),
})
}}
</span> </span>
<span v-else-if="options.memberofGroup"> <span v-else-if="options.memberofGroup">
{{ $t("Organized by {name}", { name: usernameWithDomain(event.organizerActor) }) }} {{
$t("Organized by {name}", {
name: usernameWithDomain(event.organizerActor),
})
}}
</span> </span>
</div> </div>
<div class="columns"> <div class="columns">
<span class="column is-narrow"> <span class="column is-narrow">
<b-icon icon="earth" v-if="event.visibility === EventVisibility.PUBLIC" /> <b-icon
<b-icon icon="link" v-if="event.visibility === EventVisibility.UNLISTED" /> icon="earth"
<b-icon icon="lock" v-if="event.visibility === EventVisibility.PRIVATE" /> v-if="event.visibility === EventVisibility.PUBLIC"
/>
<b-icon
icon="link"
v-if="event.visibility === EventVisibility.UNLISTED"
/>
<b-icon
icon="lock"
v-if="event.visibility === EventVisibility.PRIVATE"
/>
</span> </span>
<span class="column is-narrow participant-stats"> <span class="column is-narrow participant-stats">
<span v-if="event.options.maximumAttendeeCapacity !== 0"> <span v-if="event.options.maximumAttendeeCapacity !== 0">
@ -38,9 +57,13 @@
</span> </span>
<span v-else> <span v-else>
{{ {{
$tc("{count} participants", event.participantStats.participant, { $tc(
"{count} participants",
event.participantStats.participant,
{
count: event.participantStats.participant, count: event.participantStats.participant,
}) }
)
}} }}
</span> </span>
</span> </span>
@ -51,7 +74,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { EventVisibility, IEventCardOptions, IEvent } from "@/types/event.model"; import { IEventCardOptions, IEvent } from "@/types/event.model";
import { Component, Prop } from "vue-property-decorator"; import { Component, Prop } from "vue-property-decorator";
import DateCalendarIcon from "@/components/Event/DateCalendarIcon.vue"; import DateCalendarIcon from "@/components/Event/DateCalendarIcon.vue";
import { IPerson, usernameWithDomain } from "@/types/actor"; import { IPerson, usernameWithDomain } from "@/types/actor";
@ -59,7 +82,7 @@ import { mixins } from "vue-class-component";
import ActorMixin from "@/mixins/actor"; import ActorMixin from "@/mixins/actor";
import { CURRENT_ACTOR_CLIENT } from "@/graphql/actor"; import { CURRENT_ACTOR_CLIENT } from "@/graphql/actor";
import EventMixin from "@/mixins/event"; import EventMixin from "@/mixins/event";
import { ParticipantRole } from "../../types/participant.model"; import { EventVisibility, ParticipantRole } from "@/types/enums";
import RouteName from "../../router/name"; import RouteName from "../../router/name";
const defaultOptions: IEventCardOptions = { const defaultOptions: IEventCardOptions = {

View File

@ -14,10 +14,12 @@
{{ {{
$tc( $tc(
"{available}/{capacity} available places", "{available}/{capacity} available places",
event.options.maximumAttendeeCapacity - event.participantStats.participant, event.options.maximumAttendeeCapacity -
event.participantStats.participant,
{ {
available: available:
event.options.maximumAttendeeCapacity - event.participantStats.participant, event.options.maximumAttendeeCapacity -
event.participantStats.participant,
capacity: event.options.maximumAttendeeCapacity, capacity: event.options.maximumAttendeeCapacity,
} }
) )
@ -42,9 +44,13 @@
" "
> >
{{ {{
$tc("{count} requests waiting", event.participantStats.notApproved, { $tc(
"{count} requests waiting",
event.participantStats.notApproved,
{
count: event.participantStats.notApproved, count: event.participantStats.notApproved,
}) }
)
}} }}
</b-button> </b-button>
</span> </span>
@ -56,7 +62,7 @@
import { Component, Prop, Vue } from "vue-property-decorator"; import { Component, Prop, Vue } from "vue-property-decorator";
import { IEvent } from "@/types/event.model"; import { IEvent } from "@/types/event.model";
import DateCalendarIcon from "@/components/Event/DateCalendarIcon.vue"; import DateCalendarIcon from "@/components/Event/DateCalendarIcon.vue";
import { ParticipantRole } from "../../types/participant.model"; import { ParticipantRole } from "@/types/enums";
import RouteName from "../../router/name"; import RouteName from "../../router/name";
@Component({ @Component({
@ -88,7 +94,8 @@ export default class EventMinimalistCard extends Vue {
.event-minimalist-title { .event-minimalist-title {
color: #3c376e; color: #3c376e;
font-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica, Arial, serif; font-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica, Arial,
serif;
font-size: 1.25rem; font-size: 1.25rem;
font-weight: 700; font-weight: 700;
} }

View File

@ -33,9 +33,12 @@
<div v-else-if="queryText.length >= 3" class="is-enabled"> <div v-else-if="queryText.length >= 3" class="is-enabled">
<span>{{ $t('No results for "{queryText}"') }}</span> <span>{{ $t('No results for "{queryText}"') }}</span>
<span>{{ <span>{{
$t("You can try another search term or drag and drop the marker on the map", { $t(
"You can try another search term or drag and drop the marker on the map",
{
queryText, queryText,
}) }
)
}}</span> }}</span>
<!-- <p class="control" @click="openNewAddressModal">--> <!-- <p class="control" @click="openNewAddressModal">-->
<!-- <button type="button" class="button is-primary">{{ $t('Add') }}</button>--> <!-- <button type="button" class="button is-primary">{{ $t('Add') }}</button>-->
@ -102,7 +105,7 @@
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue, Watch } from "vue-property-decorator"; import { Component, Prop, Vue, Watch } from "vue-property-decorator";
import { LatLng } from "leaflet"; import { LatLng } from "leaflet";
import { debounce } from "lodash"; import { debounce, DebouncedFunc } from "lodash";
import { Address, IAddress } from "../../types/address.model"; import { Address, IAddress } from "../../types/address.model";
import { ADDRESS, REVERSE_GEOCODE } from "../../graphql/address"; import { ADDRESS, REVERSE_GEOCODE } from "../../graphql/address";
import { CONFIG } from "../../graphql/config"; import { CONFIG } from "../../graphql/config";
@ -110,7 +113,8 @@ import { IConfig } from "../../types/config.model";
@Component({ @Component({
components: { components: {
"map-leaflet": () => import(/* webpackChunkName: "map" */ "@/components/Map.vue"), "map-leaflet": () =>
import(/* webpackChunkName: "map" */ "@/components/Map.vue"),
}, },
apollo: { apollo: {
config: CONFIG, config: CONFIG,
@ -133,7 +137,7 @@ export default class FullAddressAutoComplete extends Vue {
private gettingLocation = false; private gettingLocation = false;
private location!: Position; private location!: GeolocationPosition;
private gettingLocationError: any; private gettingLocationError: any;
@ -141,11 +145,11 @@ export default class FullAddressAutoComplete extends Vue {
config!: IConfig; config!: IConfig;
fetchAsyncData!: Function; fetchAsyncData!: DebouncedFunc<(query: string) => Promise<void>>;
// We put this in data because of issues like // We put this in data because of issues like
// https://github.com/vuejs/vue-class-component/issues/263 // https://github.com/vuejs/vue-class-component/issues/263
data() { data(): Record<string, unknown> {
return { return {
fetchAsyncData: debounce(this.asyncData, 200), fetchAsyncData: debounce(this.asyncData, 200),
}; };
@ -173,7 +177,9 @@ export default class FullAddressAutoComplete extends Vue {
}, },
}); });
this.addressData = result.data.searchAddress.map((address: IAddress) => new Address(address)); this.addressData = result.data.searchAddress.map(
(address: IAddress) => new Address(address)
);
this.isFetching = false; this.isFetching = false;
} }
@ -224,7 +230,9 @@ export default class FullAddressAutoComplete extends Vue {
}, },
}); });
this.addressData = result.data.reverseGeocode.map((address: IAddress) => new Address(address)); this.addressData = result.data.reverseGeocode.map(
(address: IAddress) => new Address(address)
);
if (this.addressData.length > 0) { if (this.addressData.length > 0) {
const defaultAddress = new Address(this.addressData[0]); const defaultAddress = new Address(this.addressData[0]);
this.selected = defaultAddress; this.selected = defaultAddress;
@ -248,7 +256,10 @@ export default class FullAddressAutoComplete extends Vue {
this.location = await FullAddressAutoComplete.getLocation(); this.location = await FullAddressAutoComplete.getLocation();
this.mapDefaultZoom = 12; this.mapDefaultZoom = 12;
this.reverseGeoCode( this.reverseGeoCode(
new LatLng(this.location.coords.latitude, this.location.coords.longitude), new LatLng(
this.location.coords.latitude,
this.location.coords.longitude
),
12 12
); );
} catch (e) { } catch (e) {
@ -266,7 +277,7 @@ export default class FullAddressAutoComplete extends Vue {
return window.isSecureContext; return window.isSecureContext;
} }
static async getLocation(): Promise<Position> { static async getLocation(): Promise<GeolocationPosition> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (!("geolocation" in navigator)) { if (!("geolocation" in navigator)) {
reject(new Error("Geolocation is not available.")); reject(new Error("Geolocation is not available."));

View File

@ -10,9 +10,18 @@
> >
<div class="media"> <div class="media">
<figure class="image is-48x48" v-if="availableActor.avatar"> <figure class="image is-48x48" v-if="availableActor.avatar">
<img class="media-left is-rounded" :src="availableActor.avatar.url" alt="" /> <img
class="media-left is-rounded"
:src="availableActor.avatar.url"
alt=""
/>
</figure> </figure>
<b-icon class="media-left" v-else size="is-large" icon="account-circle" /> <b-icon
class="media-left"
v-else
size="is-large"
icon="account-circle"
/>
<div class="media-content"> <div class="media-content">
<h3>{{ availableActor.name }}</h3> <h3>{{ availableActor.name }}</h3>
<small>{{ `@${availableActor.preferredUsername}` }}</small> <small>{{ `@${availableActor.preferredUsername}` }}</small>
@ -23,9 +32,11 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue, Watch } from "vue-property-decorator"; import { Component, Prop, Vue, Watch } from "vue-property-decorator";
import { IMember, IPerson, MemberRole, IActor, Actor } from "@/types/actor"; import { IPerson, IActor, Actor } from "@/types/actor";
import { PERSON_MEMBERSHIPS } from "@/graphql/actor"; import { PERSON_MEMBERSHIPS } from "@/graphql/actor";
import { Paginate } from "@/types/paginate"; import { Paginate } from "@/types/paginate";
import { IMember } from "@/types/actor/member.model";
import { MemberRole } from "@/types/enums";
@Component({ @Component({
apollo: { apollo: {
@ -59,16 +70,21 @@ export default class OrganizerPicker extends Vue {
get actualMemberships(): IMember[] { get actualMemberships(): IMember[] {
if (this.restrictModeratorLevel) { if (this.restrictModeratorLevel) {
return this.groupMemberships.elements.filter((membership: IMember) => return this.groupMemberships.elements.filter((membership: IMember) =>
[MemberRole.ADMINISTRATOR, MemberRole.MODERATOR, MemberRole.CREATOR].includes( [
membership.role MemberRole.ADMINISTRATOR,
) MemberRole.MODERATOR,
MemberRole.CREATOR,
].includes(membership.role)
); );
} }
return this.groupMemberships.elements; return this.groupMemberships.elements;
} }
get actualAvailableActors(): IActor[] { get actualAvailableActors(): IActor[] {
return [this.identity, ...this.actualMemberships.map((member) => member.parent)]; return [
this.identity,
...this.actualMemberships.map((member) => member.parent),
];
} }
@Watch("currentActor") @Watch("currentActor")

View File

@ -1,7 +1,11 @@
<template> <template>
<div class="organizer-picker"> <div class="organizer-picker">
<!-- If we have a current actor (inline) --> <!-- If we have a current actor (inline) -->
<div v-if="inline && currentActor.id" class="inline box" @click="isComponentModalActive = true"> <div
v-if="inline && currentActor.id"
class="inline box"
@click="isComponentModalActive = true"
>
<div class="media"> <div class="media">
<div class="media-left"> <div class="media-left">
<figure class="image is-48x48" v-if="currentActor.avatar"> <figure class="image is-48x48" v-if="currentActor.avatar">
@ -15,7 +19,9 @@
</div> </div>
<div class="media-content" v-if="currentActor.name"> <div class="media-content" v-if="currentActor.name">
<p class="is-4">{{ currentActor.name }}</p> <p class="is-4">{{ currentActor.name }}</p>
<p class="is-6 has-text-grey">{{ `@${currentActor.preferredUsername}` }}</p> <p class="is-6 has-text-grey">
{{ `@${currentActor.preferredUsername}` }}
</p>
</div> </div>
<div class="media-content" v-else> <div class="media-content" v-else>
{{ `@${currentActor.preferredUsername}` }} {{ `@${currentActor.preferredUsername}` }}
@ -26,7 +32,11 @@
</div> </div>
</div> </div>
<!-- If we have a current actor --> <!-- If we have a current actor -->
<span v-else-if="currentActor.id" class="block" @click="isComponentModalActive = true"> <span
v-else-if="currentActor.id"
class="block"
@click="isComponentModalActive = true"
>
<img <img
class="image is-48x48" class="image is-48x48"
v-if="currentActor.avatar" v-if="currentActor.avatar"
@ -40,13 +50,19 @@
<div class="media"> <div class="media">
<div class="media-left"> <div class="media-left">
<figure class="image is-48x48" v-if="identity.avatar"> <figure class="image is-48x48" v-if="identity.avatar">
<img class="image is-rounded" :src="identity.avatar.url" :alt="identity.avatar.alt" /> <img
class="image is-rounded"
:src="identity.avatar.url"
:alt="identity.avatar.alt"
/>
</figure> </figure>
<b-icon v-else size="is-large" icon="account-circle" /> <b-icon v-else size="is-large" icon="account-circle" />
</div> </div>
<div class="media-content" v-if="identity.name"> <div class="media-content" v-if="identity.name">
<p class="is-4">{{ identity.name }}</p> <p class="is-4">{{ identity.name }}</p>
<p class="is-6 has-text-grey">{{ `@${identity.preferredUsername}` }}</p> <p class="is-6 has-text-grey">
{{ `@${identity.preferredUsername}` }}
</p>
</div> </div>
<div class="media-content" v-else> <div class="media-content" v-else>
{{ `@${identity.preferredUsername}` }} {{ `@${identity.preferredUsername}` }}
@ -74,7 +90,11 @@
<div class="column"> <div class="column">
<div v-if="actorMembersForCurrentActor.length > 0"> <div v-if="actorMembersForCurrentActor.length > 0">
<p>{{ $t("Add a contact") }}</p> <p>{{ $t("Add a contact") }}</p>
<p class="field" v-for="actor in actorMembersForCurrentActor" :key="actor.id"> <p
class="field"
v-for="actor in actorMembersForCurrentActor"
:key="actor.id"
>
<b-checkbox v-model="actualContacts" :native-value="actor.id"> <b-checkbox v-model="actualContacts" :native-value="actor.id">
<div class="media"> <div class="media">
<div class="media-left"> <div class="media-left">
@ -89,7 +109,9 @@
</div> </div>
<div class="media-content" v-if="actor.name"> <div class="media-content" v-if="actor.name">
<p class="is-4">{{ actor.name }}</p> <p class="is-4">{{ actor.name }}</p>
<p class="is-6 has-text-grey">{{ `@${actor.preferredUsername}` }}</p> <p class="is-6 has-text-grey">
{{ `@${actor.preferredUsername}` }}
</p>
</div> </div>
<div class="media-content" v-else> <div class="media-content" v-else>
{{ `@${actor.preferredUsername}` }} {{ `@${actor.preferredUsername}` }}
@ -115,7 +137,8 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue, Watch } from "vue-property-decorator"; import { Component, Prop, Vue, Watch } from "vue-property-decorator";
import { IActor, IGroup, IMember, IPerson } from "../../types/actor"; import { IMember } from "@/types/actor/member.model";
import { IActor, IGroup, IPerson } from "../../types/actor";
import OrganizerPicker from "./OrganizerPicker.vue"; import OrganizerPicker from "./OrganizerPicker.vue";
import { PERSON_MEMBERSHIPS_WITH_MEMBERS } from "../../graphql/actor"; import { PERSON_MEMBERSHIPS_WITH_MEMBERS } from "../../graphql/actor";
import { Paginate } from "../../types/paginate"; import { Paginate } from "../../types/paginate";
@ -150,7 +173,8 @@ export default class OrganizerPickerWrapper extends Vue {
groupMemberships: Paginate<IMember> = { elements: [], total: 0 }; groupMemberships: Paginate<IMember> = { elements: [], total: 0 };
@Prop({ type: Array, required: false, default: () => [] }) contacts!: IActor[]; @Prop({ type: Array, required: false, default: () => [] })
contacts!: IActor[];
actualContacts: (string | undefined)[] = this.contacts.map(({ id }) => id); actualContacts: (string | undefined)[] = this.contacts.map(({ id }) => id);
@ -171,7 +195,9 @@ export default class OrganizerPickerWrapper extends Vue {
pickActor(): void { pickActor(): void {
this.$emit( this.$emit(
"update:contacts", "update:contacts",
this.actorMembersForCurrentActor.filter(({ id }) => this.actualContacts.includes(id)) this.actorMembersForCurrentActor.filter(({ id }) =>
this.actualContacts.includes(id)
)
); );
this.$emit("input", this.currentActor); this.$emit("input", this.currentActor);
this.isComponentModalActive = false; this.isComponentModalActive = false;
@ -182,7 +208,9 @@ export default class OrganizerPickerWrapper extends Vue {
({ parent: { id } }) => id === this.currentActor.id ({ parent: { id } }) => id === this.currentActor.id
); );
if (currentMembership) { if (currentMembership) {
return currentMembership.parent.members.elements.map(({ actor }) => actor); return currentMembership.parent.members.elements.map(
({ actor }: { actor: IActor }) => actor
);
} }
return []; return [];
} }

View File

@ -47,8 +47,16 @@ A button to set your participation
> >
</b-dropdown> </b-dropdown>
<div v-else-if="participation && participation.role === ParticipantRole.NOT_APPROVED"> <div
<b-dropdown aria-role="list" position="is-bottom-left" class="dropdown-disabled"> v-else-if="
participation && participation.role === ParticipantRole.NOT_APPROVED
"
>
<b-dropdown
aria-role="list"
position="is-bottom-left"
class="dropdown-disabled"
>
<button class="button is-success is-large" type="button" slot="trigger"> <button class="button is-success is-large" type="button" slot="trigger">
<b-icon icon="timer-sand-empty" /> <b-icon icon="timer-sand-empty" />
<template> <template>
@ -74,9 +82,17 @@ A button to set your participation
<small>{{ $t("Waiting for organization team approval.") }}</small> <small>{{ $t("Waiting for organization team approval.") }}</small>
</div> </div>
<div v-else-if="participation && participation.role === ParticipantRole.REJECTED"> <div
v-else-if="
participation && participation.role === ParticipantRole.REJECTED
"
>
<span> <span>
{{ $t("Unfortunately, your participation request was rejected by the organizers.") }} {{
$t(
"Unfortunately, your participation request was rejected by the organizers."
)
}}
</span> </span>
</div> </div>
@ -92,7 +108,11 @@ A button to set your participation
<b-icon icon="menu-down" /> <b-icon icon="menu-down" />
</button> </button>
<b-dropdown-item :value="true" aria-role="listitem" @click="joinEvent(currentActor)"> <b-dropdown-item
:value="true"
aria-role="listitem"
@click="joinEvent(currentActor)"
>
<div class="media"> <div class="media">
<div class="media-left"> <div class="media-left">
<figure class="image is-32x32" v-if="currentActor.avatar"> <figure class="image is-32x32" v-if="currentActor.avatar">
@ -103,7 +123,8 @@ A button to set your participation
<span> <span>
{{ {{
$t("as {identity}", { $t("as {identity}", {
identity: currentActor.name || `@${currentActor.preferredUsername}`, identity:
currentActor.name || `@${currentActor.preferredUsername}`,
}) })
}} }}
</span> </span>
@ -121,7 +142,10 @@ A button to set your participation
</b-dropdown> </b-dropdown>
<b-button <b-button
tag="router-link" tag="router-link"
:to="{ name: RouteName.EVENT_PARTICIPATE_LOGGED_OUT, params: { uuid: event.uuid } }" :to="{
name: RouteName.EVENT_PARTICIPATE_LOGGED_OUT,
params: { uuid: event.uuid },
}"
v-else-if="!participation && hasAnonymousParticipationMethods" v-else-if="!participation && hasAnonymousParticipationMethods"
type="is-primary" type="is-primary"
size="is-large" size="is-large"
@ -130,7 +154,10 @@ A button to set your participation
> >
<b-button <b-button
tag="router-link" tag="router-link"
:to="{ name: RouteName.EVENT_PARTICIPATE_WITH_ACCOUNT, params: { uuid: event.uuid } }" :to="{
name: RouteName.EVENT_PARTICIPATE_WITH_ACCOUNT,
params: { uuid: event.uuid },
}"
v-else-if="!currentActor.id" v-else-if="!currentActor.id"
type="is-primary" type="is-primary"
size="is-large" size="is-large"
@ -142,8 +169,9 @@ A button to set your participation
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator"; import { Component, Prop, Vue } from "vue-property-decorator";
import { IParticipant, ParticipantRole } from "../../types/participant.model"; import { EventJoinOptions, ParticipantRole } from "@/types/enums";
import { EventJoinOptions, IEvent } from "../../types/event.model"; import { IParticipant } from "../../types/participant.model";
import { IEvent } from "../../types/event.model";
import { IPerson, Person } from "../../types/actor"; import { IPerson, Person } from "../../types/actor";
import { CURRENT_ACTOR_CLIENT, IDENTITIES } from "../../graphql/actor"; import { CURRENT_ACTOR_CLIENT, IDENTITIES } from "../../graphql/actor";
import { CURRENT_USER_CLIENT } from "../../graphql/user"; import { CURRENT_USER_CLIENT } from "../../graphql/user";
@ -161,7 +189,9 @@ import RouteName from "../../router/name";
identities: { identities: {
query: IDENTITIES, query: IDENTITIES,
update: ({ identities }) => update: ({ identities }) =>
identities ? identities.map((identity: IPerson) => new Person(identity)) : [], identities
? identities.map((identity: IPerson) => new Person(identity))
: [],
skip() { skip() {
return this.currentUser.isLoggedIn === false; return this.currentUser.isLoggedIn === false;
}, },

View File

@ -59,7 +59,12 @@
<a :href="linkedInShareUrl" target="_blank" rel="nofollow noopener" <a :href="linkedInShareUrl" target="_blank" rel="nofollow noopener"
><b-icon icon="linkedin" size="is-large" type="is-primary" ><b-icon icon="linkedin" size="is-large" type="is-primary"
/></a> /></a>
<a :href="diasporaShareUrl" class="diaspora" target="_blank" rel="nofollow noopener"> <a
:href="diasporaShareUrl"
class="diaspora"
target="_blank"
rel="nofollow noopener"
>
<span data-v-5e15e80a="" class="icon has-text-primary is-large"> <span data-v-5e15e80a="" class="icon has-text-primary is-large">
<DiasporaLogo alt="diaspora-logo" /> <DiasporaLogo alt="diaspora-logo" />
</span> </span>
@ -76,7 +81,9 @@
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue, Ref } from "vue-property-decorator"; import { Component, Prop, Vue, Ref } from "vue-property-decorator";
import { IEvent, EventVisibility, EventStatus } from "../../types/event.model"; import { EventStatus, EventVisibility } from "@/types/enums";
import { IEvent } from "../../types/event.model";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
import DiasporaLogo from "../../assets/diaspora-icon.svg?inline"; import DiasporaLogo from "../../assets/diaspora-icon.svg?inline";
@ -88,7 +95,8 @@ import DiasporaLogo from "../../assets/diaspora-icon.svg?inline";
export default class ShareEventModal extends Vue { export default class ShareEventModal extends Vue {
@Prop({ type: Object, required: true }) event!: IEvent; @Prop({ type: Object, required: true }) event!: IEvent;
@Prop({ type: Boolean, required: false, default: true }) eventCapacityOK!: boolean; @Prop({ type: Boolean, required: false, default: true })
eventCapacityOK!: boolean;
@Ref("eventURLInput") readonly eventURLInput!: any; @Ref("eventURLInput") readonly eventURLInput!: any;
@ -99,13 +107,15 @@ export default class ShareEventModal extends Vue {
showCopiedTooltip = false; showCopiedTooltip = false;
get twitterShareUrl(): string { get twitterShareUrl(): string {
return `https://twitter.com/intent/tweet?url=${encodeURIComponent(this.event.url)}&text=${ return `https://twitter.com/intent/tweet?url=${encodeURIComponent(
this.event.title this.event.url
}`; )}&text=${this.event.title}`;
} }
get facebookShareUrl(): string { get facebookShareUrl(): string {
return `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(this.event.url)}`; return `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(
this.event.url
)}`;
} }
get linkedInShareUrl(): string { get linkedInShareUrl(): string {
@ -124,7 +134,7 @@ export default class ShareEventModal extends Vue {
)}&url=${encodeURIComponent(this.event.url)}`; )}&url=${encodeURIComponent(this.event.url)}`;
} }
copyURL() { copyURL(): void {
this.eventURLInput.$refs.input.select(); this.eventURLInput.$refs.input.select();
document.execCommand("copy"); document.execCommand("copy");
this.showCopiedTooltip = true; this.showCopiedTooltip = true;

View File

@ -4,7 +4,9 @@
{{ $t("Add some tags") }} {{ $t("Add some tags") }}
<b-tooltip <b-tooltip
type="is-dark" type="is-dark"
:label="$t('You can add tags by hitting the Enter key or by adding a comma')" :label="
$t('You can add tags by hitting the Enter key or by adding a comma')
"
> >
<b-icon size="is-small" icon="help-circle-outline"></b-icon> <b-icon size="is-small" icon="help-circle-outline"></b-icon>
</b-tooltip> </b-tooltip>
@ -40,7 +42,6 @@ import { ITag } from "../../types/tag.model";
if (typeof tag !== "string") { if (typeof tag !== "string") {
return tag; return tag;
} }
// @ts-ignore
return { title: tag, slug: tag } as ITag; return { title: tag, slug: tag } as ITag;
}); });
this.$emit("input", tagEntities); this.$emit("input", tagEntities);
@ -57,14 +58,14 @@ export default class TagInput extends Vue {
filteredTags: ITag[] = []; filteredTags: ITag[] = [];
getFilteredTags(text: string) { getFilteredTags(text: string): void {
this.filteredTags = differenceBy(this.data, this.value, "id").filter( this.filteredTags = differenceBy(this.data, this.value, "id").filter(
(option) => get(option, this.path).toString().toLowerCase().indexOf(text.toLowerCase()) >= 0 (option) =>
get(option, this.path)
.toString()
.toLowerCase()
.indexOf(text.toLowerCase()) >= 0
); );
} }
static isTag(x: any): x is ITag {
return x.slug !== undefined;
}
} }
</script> </script>

View File

@ -3,13 +3,20 @@
<img :src="`/img/pics/footer_${random}.jpg`" alt="" /> <img :src="`/img/pics/footer_${random}.jpg`" alt="" />
<ul> <ul>
<li> <li>
<router-link :to="{ name: RouteName.ABOUT }">{{ $t("About") }}</router-link> <router-link :to="{ name: RouteName.ABOUT }">{{
$t("About")
}}</router-link>
</li> </li>
<li> <li>
<router-link :to="{ name: RouteName.TERMS }">{{ $t("Terms") }}</router-link> <router-link :to="{ name: RouteName.TERMS }">{{
$t("Terms")
}}</router-link>
</li> </li>
<li> <li>
<a hreflang="en" href="https://framagit.org/framasoft/mobilizon/blob/master/LICENSE"> <a
hreflang="en"
href="https://framagit.org/framasoft/mobilizon/blob/master/LICENSE"
>
{{ $t("License") }} {{ $t("License") }}
</a> </a>
</li> </li>
@ -19,7 +26,9 @@
tag="span" tag="span"
path="Powered by {mobilizon}. © 2018 - {date} The Mobilizon Contributors - Made with the financial support of {contributors}." path="Powered by {mobilizon}. © 2018 - {date} The Mobilizon Contributors - Made with the financial support of {contributors}."
> >
<a slot="mobilizon" href="https://joinmobilizon.org">{{ $t("Mobilizon") }}</a> <a slot="mobilizon" href="https://joinmobilizon.org">{{
$t("Mobilizon")
}}</a>
<span slot="date">{{ new Date().getFullYear() }}</span> <span slot="date">{{ new Date().getFullYear() }}</span>
<a href="https://joinmobilizon.org/hall-of-fame" slot="contributors">{{ <a href="https://joinmobilizon.org/hall-of-fame" slot="contributors">{{
$t("more than 1360 contributors") $t("more than 1360 contributors")

View File

@ -17,7 +17,9 @@
> >
<h3>{{ group.name }}</h3> <h3>{{ group.name }}</h3>
<p class="is-6 has-text-grey"> <p class="is-6 has-text-grey">
<span v-if="group.domain">{{ `@${group.preferredUsername}@${group.domain}` }}</span> <span v-if="group.domain">{{
`@${group.preferredUsername}@${group.domain}`
}}</span>
<span v-else>{{ `@${group.preferredUsername}` }}</span> <span v-else>{{ `@${group.preferredUsername}` }}</span>
</p> </p>
</router-link> </router-link>

View File

@ -13,7 +13,9 @@
<router-link <router-link
:to="{ :to="{
name: RouteName.GROUP, name: RouteName.GROUP,
params: { preferredUsername: usernameWithDomain(member.parent) }, params: {
preferredUsername: usernameWithDomain(member.parent),
},
}" }"
> >
<h3>{{ member.parent.name }}</h3> <h3>{{ member.parent.name }}</h3>
@ -23,12 +25,16 @@
}}</span> }}</span>
<span v-else>{{ `@${member.parent.preferredUsername}` }}</span> <span v-else>{{ `@${member.parent.preferredUsername}` }}</span>
<b-taglist> <b-taglist>
<b-tag type="is-info" v-if="member.role === MemberRole.ADMINISTRATOR">{{ <b-tag
$t("Administrator") type="is-info"
}}</b-tag> v-if="member.role === MemberRole.ADMINISTRATOR"
<b-tag type="is-info" v-else-if="member.role === MemberRole.MODERATOR">{{ >{{ $t("Administrator") }}</b-tag
$t("Moderator") >
}}</b-tag> <b-tag
type="is-info"
v-else-if="member.role === MemberRole.MODERATOR"
>{{ $t("Moderator") }}</b-tag
>
</b-taglist> </b-taglist>
</p> </p>
</router-link> </router-link>
@ -54,7 +60,9 @@
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator"; import { Component, Prop, Vue } from "vue-property-decorator";
import { IMember, MemberRole, usernameWithDomain } from "@/types/actor"; import { usernameWithDomain } from "@/types/actor";
import { IMember } from "@/types/actor/member.model";
import { MemberRole } from "@/types/enums";
import RouteName from "../../router/name"; import RouteName from "../../router/name";
@Component @Component

View File

@ -8,7 +8,9 @@
<a <a
class="list-item" class="list-item"
v-for="groupMembership in actualMemberships" v-for="groupMembership in actualMemberships"
:class="{ 'is-active': groupMembership.parent.id === currentGroup.id }" :class="{
'is-active': groupMembership.parent.id === currentGroup.id,
}"
@click="changeCurrentGroup(groupMembership.parent)" @click="changeCurrentGroup(groupMembership.parent)"
:key="groupMembership.id" :key="groupMembership.id"
> >
@ -19,14 +21,25 @@
:src="groupMembership.parent.avatar.url" :src="groupMembership.parent.avatar.url"
alt="" alt=""
/> />
<b-icon class="media-left" v-else size="is-large" icon="account-circle" /> <b-icon
class="media-left"
v-else
size="is-large"
icon="account-circle"
/>
<div class="media-content"> <div class="media-content">
<h3>@{{ groupMembership.parent.name }}</h3> <h3>@{{ groupMembership.parent.name }}</h3>
<small>{{ `@${groupMembership.parent.preferredUsername}` }}</small> <small>{{
`@${groupMembership.parent.preferredUsername}`
}}</small>
</div> </div>
</div> </div>
</a> </a>
<a class="list-item" @click="changeCurrentGroup(new Group())" v-if="currentGroup.id"> <a
class="list-item"
@click="changeCurrentGroup(new Group())"
v-if="currentGroup.id"
>
<h3>{{ $t("Unset group") }}</h3> <h3>{{ $t("Unset group") }}</h3>
</a> </a>
</div> </div>
@ -36,9 +49,11 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator"; import { Component, Prop, Vue } from "vue-property-decorator";
import { IGroup, IMember, IPerson, Group, MemberRole } from "@/types/actor"; import { IGroup, IPerson, Group } from "@/types/actor";
import { PERSON_MEMBERSHIPS } from "@/graphql/actor"; import { PERSON_MEMBERSHIPS } from "@/graphql/actor";
import { Paginate } from "@/types/paginate"; import { Paginate } from "@/types/paginate";
import { IMember } from "@/types/actor/member.model";
import { MemberRole } from "@/types/enums";
@Component({ @Component({
apollo: { apollo: {
@ -77,9 +92,11 @@ export default class GroupPicker extends Vue {
get actualMemberships(): IMember[] { get actualMemberships(): IMember[] {
if (this.restrictModeratorLevel) { if (this.restrictModeratorLevel) {
return this.groupMemberships.elements.filter((membership: IMember) => return this.groupMemberships.elements.filter((membership: IMember) =>
[MemberRole.ADMINISTRATOR, MemberRole.MODERATOR, MemberRole.CREATOR].includes( [
membership.role MemberRole.ADMINISTRATOR,
) MemberRole.MODERATOR,
MemberRole.CREATOR,
].includes(membership.role)
); );
} }
return this.groupMemberships.elements; return this.groupMemberships.elements;

View File

@ -10,7 +10,11 @@
{{ $t("The event will show the group as organizer.") }} {{ $t("The event will show the group as organizer.") }}
</p> </p>
</div> </div>
<div v-if="inline && currentGroup.id" class="inline box" @click="isComponentModalActive = true"> <div
v-if="inline && currentGroup.id"
class="inline box"
@click="isComponentModalActive = true"
>
<div class="media"> <div class="media">
<div class="media-left"> <div class="media-left">
<figure class="image is-48x48" v-if="currentGroup.avatar"> <figure class="image is-48x48" v-if="currentGroup.avatar">
@ -24,7 +28,9 @@
</div> </div>
<div class="media-content" v-if="currentGroup.name"> <div class="media-content" v-if="currentGroup.name">
<p class="is-4">{{ currentGroup.name }}</p> <p class="is-4">{{ currentGroup.name }}</p>
<p class="is-6 has-text-grey">{{ `@${currentGroup.preferredUsername}` }}</p> <p class="is-6 has-text-grey">
{{ `@${currentGroup.preferredUsername}` }}
</p>
</div> </div>
<div class="media-content" v-else> <div class="media-content" v-else>
{{ `@${currentGroup.preferredUsername}` }} {{ `@${currentGroup.preferredUsername}` }}
@ -34,7 +40,11 @@
</b-button> </b-button>
</div> </div>
</div> </div>
<span v-else-if="currentGroup.id" class="block" @click="isComponentModalActive = true"> <span
v-else-if="currentGroup.id"
class="block"
@click="isComponentModalActive = true"
>
<img <img
class="image is-48x48" class="image is-48x48"
v-if="currentGroup.avatar" v-if="currentGroup.avatar"
@ -44,7 +54,9 @@
<b-icon v-else size="is-large" icon="account-circle" /> <b-icon v-else size="is-large" icon="account-circle" />
</span> </span>
<div v-if="groupMemberships.total === 0" class="box"> <div v-if="groupMemberships.total === 0" class="box">
<p class="is-4">{{ $t("This identity is not a member of any group.") }}</p> <p class="is-4">
{{ $t("This identity is not a member of any group.") }}
</p>
<p class="is-6 is-size-6 has-text-grey"> <p class="is-6 is-size-6 has-text-grey">
{{ $t("You need to create the group before you create an event.") }} {{ $t("You need to create the group before you create an event.") }}
</p> </p>
@ -61,7 +73,8 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue, Watch } from "vue-property-decorator"; import { Component, Prop, Vue, Watch } from "vue-property-decorator";
import { IGroup, IMember, IPerson } from "../../types/actor"; import { IMember } from "@/types/actor/member.model";
import { IGroup, IPerson } from "../../types/actor";
import GroupPicker from "./GroupPicker.vue"; import GroupPicker from "./GroupPicker.vue";
import { PERSON_MEMBERSHIPS } from "../../graphql/actor"; import { PERSON_MEMBERSHIPS } from "../../graphql/actor";
import { Paginate } from "../../types/paginate"; import { Paginate } from "../../types/paginate";

View File

@ -26,7 +26,8 @@ export default class GroupSection extends Vue {
@Prop({ required: true, type: String }) icon!: string; @Prop({ required: true, type: String }) icon!: string;
@Prop({ required: false, type: Boolean, default: true }) privateSection!: boolean; @Prop({ required: false, type: Boolean, default: true })
privateSection!: boolean;
@Prop({ required: true, type: Object }) route!: Route; @Prop({ required: true, type: Object }) route!: Route;
} }
@ -76,7 +77,8 @@ div.group-section-title {
::v-deep span { ::v-deep span {
display: inline; display: inline;
padding: 3px 8px; padding: 3px 8px;
font-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica, Arial, serif; font-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica, Arial,
serif;
font-weight: 500; font-weight: 500;
font-size: 30px; font-size: 30px;
flex: 1; flex: 1;

View File

@ -2,7 +2,10 @@
<div class="media"> <div class="media">
<div class="media-content"> <div class="media-content">
<div class="content"> <div class="content">
<i18n tag="p" path="You have been invited by {invitedBy} to the following group:"> <i18n
tag="p"
path="You have been invited by {invitedBy} to the following group:"
>
<b slot="invitedBy">{{ member.invitedBy.name }}</b> <b slot="invitedBy">{{ member.invitedBy.name }}</b>
</i18n> </i18n>
</div> </div>
@ -20,15 +23,21 @@
<router-link <router-link
:to="{ :to="{
name: RouteName.GROUP, name: RouteName.GROUP,
params: { preferredUsername: usernameWithDomain(member.parent) }, params: {
preferredUsername: usernameWithDomain(member.parent),
},
}" }"
> >
<h3>{{ member.parent.name }}</h3> <h3>{{ member.parent.name }}</h3>
<p class="is-6 has-text-grey"> <p class="is-6 has-text-grey">
<span v-if="member.parent.domain"> <span v-if="member.parent.domain">
{{ `@${member.parent.preferredUsername}@${member.parent.domain}` }} {{
`@${member.parent.preferredUsername}@${member.parent.domain}`
}}
</span> </span>
<span v-else>{{ `@${member.parent.preferredUsername}` }}</span> <span v-else>{{
`@${member.parent.preferredUsername}`
}}</span>
</p> </p>
</router-link> </router-link>
</div> </div>
@ -54,7 +63,8 @@
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator"; import { Component, Prop, Vue } from "vue-property-decorator";
import { IMember, usernameWithDomain } from "@/types/actor"; import { usernameWithDomain } from "@/types/actor";
import { IMember } from "@/types/actor/member.model";
import RouteName from "../../router/name"; import RouteName from "../../router/name";
@Component @Component

View File

@ -11,10 +11,10 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { ACCEPT_INVITATION, REJECT_INVITATION } from "@/graphql/member"; import { ACCEPT_INVITATION, REJECT_INVITATION } from "@/graphql/member";
import { IMember } from "@/types/actor";
import { Component, Prop, Vue } from "vue-property-decorator"; import { Component, Prop, Vue } from "vue-property-decorator";
import InvitationCard from "@/components/Group/InvitationCard.vue"; import InvitationCard from "@/components/Group/InvitationCard.vue";
import { LOGGED_USER_MEMBERSHIPS } from "@/graphql/actor"; import { LOGGED_USER_MEMBERSHIPS } from "@/graphql/actor";
import { IMember } from "@/types/actor/member.model";
@Component({ @Component({
components: { components: {
@ -26,13 +26,15 @@ export default class Invitations extends Vue {
async acceptInvitation(id: string): Promise<void> { async acceptInvitation(id: string): Promise<void> {
try { try {
const { data } = await this.$apollo.mutate<{ acceptInvitation: IMember }>({ const { data } = await this.$apollo.mutate<{ acceptInvitation: IMember }>(
{
mutation: ACCEPT_INVITATION, mutation: ACCEPT_INVITATION,
variables: { variables: {
id, id,
}, },
refetchQueries: [{ query: LOGGED_USER_MEMBERSHIPS }], refetchQueries: [{ query: LOGGED_USER_MEMBERSHIPS }],
}); }
);
if (data) { if (data) {
this.$emit("accept-invitation", data.acceptInvitation); this.$emit("accept-invitation", data.acceptInvitation);
} }
@ -46,13 +48,15 @@ export default class Invitations extends Vue {
async rejectInvitation(id: string): Promise<void> { async rejectInvitation(id: string): Promise<void> {
try { try {
const { data } = await this.$apollo.mutate<{ rejectInvitation: IMember }>({ const { data } = await this.$apollo.mutate<{ rejectInvitation: IMember }>(
{
mutation: REJECT_INVITATION, mutation: REJECT_INVITATION,
variables: { variables: {
id, id,
}, },
refetchQueries: [{ query: LOGGED_USER_MEMBERSHIPS }], refetchQueries: [{ query: LOGGED_USER_MEMBERSHIPS }],
}); }
);
if (data) { if (data) {
this.$emit("reject-invitation", data.rejectInvitation); this.$emit("reject-invitation", data.rejectInvitation);
} }

View File

@ -25,6 +25,8 @@ export default class JoinGroupWithAccount extends Vue {
}`; }`;
} }
sentence = this.$t("We will redirect you to your instance in order to interact with this group"); sentence = this.$t(
"We will redirect you to your instance in order to interact with this group"
);
} }
</script> </script>

View File

@ -8,7 +8,11 @@
@click="clickMap" @click="clickMap"
@update:zoom="updateZoom" @update:zoom="updateZoom"
> >
<l-tile-layer :url="config.maps.tiles.endpoint" :attribution="attribution"> </l-tile-layer> <l-tile-layer
:url="config.maps.tiles.endpoint"
:attribution="attribution"
>
</l-tile-layer>
<v-locatecontrol :options="{ icon: 'mdi mdi-map-marker' }" /> <v-locatecontrol :options="{ icon: 'mdi mdi-map-marker' }" />
<l-marker <l-marker
:lat-lng="[lat, lon]" :lat-lng="[lat, lon]"
@ -17,7 +21,9 @@
:draggable="!readOnly" :draggable="!readOnly"
> >
<l-popup v-if="popupMultiLine"> <l-popup v-if="popupMultiLine">
<span v-for="line in popupMultiLine" :key="line">{{ line }}<br /></span> <span v-for="line in popupMultiLine" :key="line"
>{{ line }}<br
/></span>
</l-popup> </l-popup>
</l-marker> </l-marker>
</l-map> </l-map>
@ -51,12 +57,15 @@ export default class Map extends Vue {
@Prop({ type: String, required: true }) coords!: string; @Prop({ type: String, required: true }) coords!: string;
@Prop({ type: Object, required: false }) marker!: { text: string | string[]; icon: string }; @Prop({ type: Object, required: false }) marker!: {
text: string | string[];
icon: string;
};
@Prop({ type: Object, required: false }) options!: object; @Prop({ type: Object, required: false }) options!: Record<string, unknown>;
@Prop({ type: Function, required: false }) @Prop({ type: Function, required: false })
updateDraggableMarkerCallback!: Function; updateDraggableMarkerCallback!: (latlng: LatLng, zoom: number) => void;
defaultOptions: { defaultOptions: {
zoom: number; zoom: number;
@ -86,45 +95,48 @@ export default class Map extends Vue {
} }
/* eslint-enable */ /* eslint-enable */
openPopup(event: LeafletEvent) { openPopup(event: LeafletEvent): void {
this.$nextTick(() => { this.$nextTick(() => {
event.target.openPopup(); event.target.openPopup();
}); });
} }
get mergedOptions(): object { get mergedOptions(): Record<string, unknown> {
return { ...this.defaultOptions, ...this.options }; return { ...this.defaultOptions, ...this.options };
} }
get lat() { get lat(): number {
return this.$props.coords.split(";")[1]; return this.$props.coords.split(";")[1];
} }
get lon() { get lon(): number {
return this.$props.coords.split(";")[0]; return this.$props.coords.split(";")[0];
} }
get popupMultiLine() { get popupMultiLine(): Array<string> {
if (Array.isArray(this.marker.text)) { if (Array.isArray(this.marker.text)) {
return this.marker.text; return this.marker.text;
} }
return [this.marker.text]; return [this.marker.text];
} }
clickMap(event: LeafletMouseEvent) { clickMap(event: LeafletMouseEvent): void {
this.updateDraggableMarkerPosition(event.latlng); this.updateDraggableMarkerPosition(event.latlng);
} }
updateDraggableMarkerPosition(e: LatLng) { updateDraggableMarkerPosition(e: LatLng): void {
this.updateDraggableMarkerCallback(e, this.zoom); this.updateDraggableMarkerCallback(e, this.zoom);
} }
updateZoom(zoom: number) { updateZoom(zoom: number): void {
this.zoom = zoom; this.zoom = zoom;
} }
get attribution() { get attribution(): string {
return this.config.maps.tiles.attribution || this.$t("© The OpenStreetMap Contributors"); return (
this.config.maps.tiles.attribution ||
(this.$t("© The OpenStreetMap Contributors") as string)
);
} }
} }
</script> </script>

View File

@ -17,13 +17,16 @@ import { Component, Prop, Vue } from "vue-property-decorator";
@Component({ @Component({
beforeDestroy() { beforeDestroy() {
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
this.parentContainer.removeLayer(this); this.parentContainer.removeLayer(this);
}, },
}) })
export default class Vue2LeafletLocateControl extends Vue { export default class Vue2LeafletLocateControl extends Vue {
@Prop({ type: Object, default: () => ({}) }) options!: object; @Prop({ type: Object, default: () => ({}) }) options!: Record<
string,
unknown
>;
@Prop({ type: Boolean, default: true }) visible = true; @Prop({ type: Boolean, default: true }) visible = true;
@ -33,7 +36,7 @@ export default class Vue2LeafletLocateControl extends Vue {
parentContainer: any; parentContainer: any;
mounted() { mounted(): void {
this.mapObject = L.control.locate(this.options); this.mapObject = L.control.locate(this.options);
DomEvent.on(this.mapObject, this.$listeners as any); DomEvent.on(this.mapObject, this.$listeners as any);
propsBinder(this, this.mapObject, this.$props); propsBinder(this, this.mapObject, this.$props);
@ -42,7 +45,7 @@ export default class Vue2LeafletLocateControl extends Vue {
this.mapObject.addTo(this.parentContainer.mapObject, !this.visible); this.mapObject.addTo(this.parentContainer.mapObject, !this.visible);
} }
public locate() { public locate(): void {
this.mapObject.start(); this.mapObject.start();
} }
} }

View File

@ -1,7 +1,15 @@
<template> <template>
<b-navbar type="is-secondary" wrapper-class="container" :active.sync="mobileNavbarActive"> <b-navbar
type="is-secondary"
wrapper-class="container"
:active.sync="mobileNavbarActive"
>
<template slot="brand"> <template slot="brand">
<b-navbar-item tag="router-link" :to="{ name: RouteName.HOME }" :aria-label="$t('Home')"> <b-navbar-item
tag="router-link"
:to="{ name: RouteName.HOME }"
:aria-label="$t('Home')"
>
<logo /> <logo />
</b-navbar-item> </b-navbar-item>
</template> </template>
@ -19,9 +27,12 @@
>{{ $t("My groups") }}</b-navbar-item >{{ $t("My groups") }}</b-navbar-item
> >
<b-navbar-item tag="span" v-if="config && config.features.eventCreation"> <b-navbar-item tag="span" v-if="config && config.features.eventCreation">
<b-button tag="router-link" :to="{ name: RouteName.CREATE_EVENT }" type="is-primary">{{ <b-button
$t("Create") tag="router-link"
}}</b-button> :to="{ name: RouteName.CREATE_EVENT }"
type="is-primary"
>{{ $t("Create") }}</b-button
>
</b-navbar-item> </b-navbar-item>
</template> </template>
<template slot="end"> <template slot="end">
@ -30,9 +41,17 @@
</b-navbar-item> </b-navbar-item>
<b-navbar-dropdown v-if="currentActor.id && currentUser.isLoggedIn" right> <b-navbar-dropdown v-if="currentActor.id && currentUser.isLoggedIn" right>
<template slot="label" v-if="currentActor" class="navbar-dropdown-profile"> <template
slot="label"
v-if="currentActor"
class="navbar-dropdown-profile"
>
<figure class="image is-32x32" v-if="currentActor.avatar"> <figure class="image is-32x32" v-if="currentActor.avatar">
<img class="is-rounded" alt="avatarUrl" :src="currentActor.avatar.url" /> <img
class="is-rounded"
alt="avatarUrl"
:src="currentActor.avatar.url"
/>
</figure> </figure>
<b-icon v-else icon="account-circle" /> <b-icon v-else icon="account-circle" />
</template> </template>
@ -65,9 +84,11 @@
<hr class="navbar-divider" /> <hr class="navbar-divider" />
</b-navbar-item> </b-navbar-item>
<b-navbar-item tag="router-link" :to="{ name: RouteName.UPDATE_IDENTITY }">{{ <b-navbar-item
$t("My account") tag="router-link"
}}</b-navbar-item> :to="{ name: RouteName.UPDATE_IDENTITY }"
>{{ $t("My account") }}</b-navbar-item
>
<!-- <b-navbar-item tag="router-link" :to="{ name: RouteName.CREATE_GROUP }">--> <!-- <b-navbar-item tag="router-link" :to="{ name: RouteName.CREATE_GROUP }">-->
<!-- {{ $t('Create group') }}--> <!-- {{ $t('Create group') }}-->
@ -95,9 +116,11 @@
<strong>{{ $t("Sign up") }}</strong> <strong>{{ $t("Sign up") }}</strong>
</router-link> </router-link>
<router-link class="button is-light" :to="{ name: RouteName.LOGIN }">{{ <router-link
$t("Log in") class="button is-light"
}}</router-link> :to="{ name: RouteName.LOGIN }"
>{{ $t("Log in") }}</router-link
>
</div> </div>
</b-navbar-item> </b-navbar-item>
</template> </template>
@ -109,13 +132,18 @@ import { Component, Vue, Watch } from "vue-property-decorator";
import Logo from "@/components/Logo.vue"; import Logo from "@/components/Logo.vue";
import { GraphQLError } from "graphql"; import { GraphQLError } from "graphql";
import { loadLanguageAsync } from "@/utils/i18n"; import { loadLanguageAsync } from "@/utils/i18n";
import { ICurrentUserRole } from "@/types/enums";
import { CURRENT_USER_CLIENT, USER_SETTINGS } from "../graphql/user"; import { CURRENT_USER_CLIENT, USER_SETTINGS } from "../graphql/user";
import { changeIdentity, logout } from "../utils/auth"; import { changeIdentity, logout } from "../utils/auth";
import { CURRENT_ACTOR_CLIENT, IDENTITIES, UPDATE_DEFAULT_ACTOR } from "../graphql/actor"; import {
CURRENT_ACTOR_CLIENT,
IDENTITIES,
UPDATE_DEFAULT_ACTOR,
} from "../graphql/actor";
import { IPerson, Person } from "../types/actor"; import { IPerson, Person } from "../types/actor";
import { CONFIG } from "../graphql/config"; import { CONFIG } from "../graphql/config";
import { IConfig } from "../types/config.model"; import { IConfig } from "../types/config.model";
import { ICurrentUser, ICurrentUserRole, IUser } from "../types/current-user.model"; import { ICurrentUser, IUser } from "../types/current-user.model";
import SearchField from "./SearchField.vue"; import SearchField from "./SearchField.vue";
import RouteName from "../router/name"; import RouteName from "../router/name";
@ -130,7 +158,9 @@ import RouteName from "../router/name";
identities: { identities: {
query: IDENTITIES, query: IDENTITIES,
update: ({ identities }) => update: ({ identities }) =>
identities ? identities.map((identity: IPerson) => new Person(identity)) : [], identities
? identities.map((identity: IPerson) => new Person(identity))
: [],
skip() { skip() {
return this.currentUser.isLoggedIn === false; return this.currentUser.isLoggedIn === false;
}, },
@ -201,7 +231,8 @@ export default class NavBar extends Vue {
async handleErrors(errors: GraphQLError[]): Promise<void> { async handleErrors(errors: GraphQLError[]): Promise<void> {
if ( if (
errors.length > 0 && errors.length > 0 &&
errors[0].message === "You need to be logged-in to view your list of identities" errors[0].message ===
"You need to be logged-in to view your list of identities"
) { ) {
await this.logout(); await this.logout();
} }

View File

@ -1,9 +1,14 @@
<template> <template>
<section class="section container"> <section class="section container">
<h1 class="title" v-if="loading">{{ $t("Your participation request is being validated") }}</h1> <h1 class="title" v-if="loading">
{{ $t("Your participation request is being validated") }}
</h1>
<div v-else> <div v-else>
<div v-if="failed"> <div v-if="failed">
<b-message :title="$t('Error while validating participation request')" type="is-danger"> <b-message
:title="$t('Error while validating participation request')"
type="is-danger"
>
{{ {{
$t( $t(
"Either the participation request has already been validated, either the validation token is incorrect." "Either the participation request has already been validated, either the validation token is incorrect."
@ -12,9 +17,16 @@
</b-message> </b-message>
</div> </div>
<div v-else> <div v-else>
<h1 class="title">{{ $t("Your participation request has been validated") }}</h1> <h1 class="title">
<p class="content" v-if="participation.event.joinOptions == EventJoinOptions.RESTRICTED"> {{ $t("Your participation request has been validated") }}
{{ $t("Your participation still has to be approved by the organisers.") }} </h1>
<p
class="content"
v-if="participation.event.joinOptions == EventJoinOptions.RESTRICTED"
>
{{
$t("Your participation still has to be approved by the organisers.")
}}
</p> </p>
<div class="columns has-text-centered"> <div class="columns has-text-centered">
<div class="column"> <div class="column">
@ -38,9 +50,9 @@
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator"; import { Component, Prop, Vue } from "vue-property-decorator";
import { confirmLocalAnonymousParticipation } from "@/services/AnonymousParticipationStorage"; import { confirmLocalAnonymousParticipation } from "@/services/AnonymousParticipationStorage";
import { EventJoinOptions } from "@/types/enums";
import { IParticipant } from "../../types/participant.model"; import { IParticipant } from "../../types/participant.model";
import RouteName from "../../router/name"; import RouteName from "../../router/name";
import { EventJoinOptions } from "../../types/event.model";
import { CONFIRM_PARTICIPATION } from "../../graphql/event"; import { CONFIRM_PARTICIPATION } from "../../graphql/event";
@Component @Component

View File

@ -1,5 +1,9 @@
<template> <template>
<redirect-with-account :uri="uri" :pathAfterLogin="`/events/${uuid}`" :sentence="sentence" /> <redirect-with-account
:uri="uri"
:pathAfterLogin="`/events/${uuid}`"
:sentence="sentence"
/>
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator"; import { Component, Prop, Vue } from "vue-property-decorator";
@ -21,6 +25,8 @@ export default class ParticipationWithAccount extends Vue {
}`; }`;
} }
sentence = this.$t("We will redirect you to your instance in order to interact with this event"); sentence = this.$t(
"We will redirect you to your instance in order to interact with this event"
);
} }
</script> </script>

View File

@ -33,7 +33,13 @@
) )
}} }}
</p> </p>
<p v-else>{{ $t("If you want, you may send a message to the event organizer here.") }}</p> <p v-else>
{{
$t(
"If you want, you may send a message to the event organizer here."
)
}}
</p>
<b-field :label="$t('Message')"> <b-field :label="$t('Message')">
<b-input <b-input
type="textarea" type="textarea"
@ -54,18 +60,29 @@
</p> </p>
</b-checkbox> </b-checkbox>
</b-field> </b-field>
<b-button :disabled="sendingForm" type="is-primary" native-type="submit">{{ <b-button
$t("Send email") :disabled="sendingForm"
}}</b-button> type="is-primary"
native-type="submit"
>{{ $t("Send email") }}</b-button
>
<div class="has-text-centered"> <div class="has-text-centered">
<b-button native-type="button" tag="a" type="is-text" @click="$router.go(-1)">{{ <b-button
$t("Back to previous page") native-type="button"
}}</b-button> tag="a"
type="is-text"
@click="$router.go(-1)"
>{{ $t("Back to previous page") }}</b-button
>
</div> </div>
</form> </form>
<div v-else> <div v-else>
<h1 class="title">{{ $t("Request for participation confirmation sent") }}</h1> <h1 class="title">
<p class="content">{{ $t("Check your inbox (and your junk mail folder).") }}</p> {{ $t("Request for participation confirmation sent") }}
</h1>
<p class="content">
{{ $t("Check your inbox (and your junk mail folder).") }}
</p>
<p class="content">{{ $t("You may now close this window.") }}</p> <p class="content">{{ $t("You may now close this window.") }}</p>
</div> </div>
</div> </div>
@ -74,12 +91,13 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator"; import { Component, Prop, Vue } from "vue-property-decorator";
import { EventModel, IEvent, EventJoinOptions } from "@/types/event.model"; import { EventModel, IEvent } from "@/types/event.model";
import { FETCH_EVENT, JOIN_EVENT } from "@/graphql/event"; import { FETCH_EVENT, JOIN_EVENT } from "@/graphql/event";
import { IConfig } from "@/types/config.model"; import { IConfig } from "@/types/config.model";
import { CONFIG } from "@/graphql/config"; import { CONFIG } from "@/graphql/config";
import { addLocalUnconfirmedAnonymousParticipation } from "@/services/AnonymousParticipationStorage"; import { addLocalUnconfirmedAnonymousParticipation } from "@/services/AnonymousParticipationStorage";
import { IParticipant, ParticipantRole } from "../../types/participant.model"; import { EventJoinOptions, ParticipantRole } from "@/types/enums";
import { IParticipant } from "../../types/participant.model";
@Component({ @Component({
apollo: { apollo: {
@ -101,7 +119,11 @@ import { IParticipant, ParticipantRole } from "../../types/participant.model";
export default class ParticipationWithoutAccount extends Vue { export default class ParticipationWithoutAccount extends Vue {
@Prop({ type: String, required: true }) uuid!: string; @Prop({ type: String, required: true }) uuid!: string;
anonymousParticipation: { email: string; message: string; saveParticipation: boolean } = { anonymousParticipation: {
email: string;
message: string;
saveParticipation: boolean;
} = {
email: "", email: "",
message: "", message: "",
saveParticipation: true, saveParticipation: true,
@ -133,7 +155,9 @@ export default class ParticipationWithoutAccount extends Vue {
}, },
update: (store, { data: updateData }) => { update: (store, { data: updateData }) => {
if (updateData == null) { if (updateData == null) {
console.error("Cannot update event participant cache, because of data null value."); console.error(
"Cannot update event participant cache, because of data null value."
);
return; return;
} }
@ -142,12 +166,16 @@ export default class ParticipationWithoutAccount extends Vue {
variables: { uuid: this.event.uuid }, variables: { uuid: this.event.uuid },
}); });
if (cachedData == null) { if (cachedData == null) {
console.error("Cannot update event participant cache, because of cached null value."); console.error(
"Cannot update event participant cache, because of cached null value."
);
return; return;
} }
const { event } = cachedData; const { event } = cachedData;
if (event === null) { if (event === null) {
console.error("Cannot update event participant cache, because of null value."); console.error(
"Cannot update event participant cache, because of null value."
);
return; return;
} }

View File

@ -2,22 +2,34 @@
<section class="section container hero"> <section class="section container hero">
<div class="hero-body" v-if="event"> <div class="hero-body" v-if="event">
<div class="container"> <div class="container">
<subtitle>{{ $t("You wish to participate to the following event") }}</subtitle> <subtitle>{{
$t("You wish to participate to the following event")
}}</subtitle>
<EventListViewCard v-if="event" :event="event" /> <EventListViewCard v-if="event" :event="event" />
<div class="columns has-text-centered"> <div class="columns has-text-centered">
<div class="column"> <div class="column">
<router-link :to="{ name: RouteName.EVENT_PARTICIPATE_WITH_ACCOUNT }"> <router-link
:to="{ name: RouteName.EVENT_PARTICIPATE_WITH_ACCOUNT }"
>
<figure class="image is-128x128"> <figure class="image is-128x128">
<img src="../../assets/undraw_profile.svg" alt="Profile illustration" /> <img
src="../../assets/undraw_profile.svg"
alt="Profile illustration"
/>
</figure> </figure>
<b-button type="is-primary">{{ $t("I have a Mobilizon account") }}</b-button> <b-button type="is-primary">{{
$t("I have a Mobilizon account")
}}</b-button>
</router-link> </router-link>
<p> <p>
<small> <small>
{{ {{
$t("Either on the {instance} instance or on another instance.", { $t(
"Either on the {instance} instance or on another instance.",
{
instance: host, instance: host,
}) }
)
}} }}
</small> </small>
<b-tooltip <b-tooltip
@ -32,25 +44,41 @@
</b-tooltip> </b-tooltip>
</p> </p>
</div> </div>
<vertical-divider :content="$t('Or')" v-if="anonymousParticipationAllowed" /> <vertical-divider
:content="$t('Or')"
v-if="anonymousParticipationAllowed"
/>
<div <div
class="column" class="column"
v-if="anonymousParticipationAllowed && hasAnonymousEmailParticipationMethod" v-if="
anonymousParticipationAllowed &&
hasAnonymousEmailParticipationMethod
"
> >
<router-link <router-link
:to="{ name: RouteName.EVENT_PARTICIPATE_WITHOUT_ACCOUNT }" :to="{ name: RouteName.EVENT_PARTICIPATE_WITHOUT_ACCOUNT }"
v-if="event.local" v-if="event.local"
> >
<figure class="image is-128x128"> <figure class="image is-128x128">
<img src="../../assets/undraw_mail_2.svg" alt="Privacy illustration" /> <img
src="../../assets/undraw_mail_2.svg"
alt="Privacy illustration"
/>
</figure> </figure>
<b-button type="is-primary">{{ $t("I don't have a Mobilizon account") }}</b-button> <b-button type="is-primary">{{
$t("I don't have a Mobilizon account")
}}</b-button>
</router-link> </router-link>
<a :href="`${event.url}/participate/without-account`" v-else> <a :href="`${event.url}/participate/without-account`" v-else>
<figure class="image is-128x128"> <figure class="image is-128x128">
<img src="../../assets/undraw_mail_2.svg" alt="Privacy illustration" /> <img
src="../../assets/undraw_mail_2.svg"
alt="Privacy illustration"
/>
</figure> </figure>
<b-button type="is-primary">{{ $t("I don't have a Mobilizon account") }}</b-button> <b-button type="is-primary">{{
$t("I don't have a Mobilizon account")
}}</b-button>
</a> </a>
<p> <p>
<small>{{ $t("Participate using your email address") }}</small> <small>{{ $t("Participate using your email address") }}</small>

View File

@ -69,7 +69,11 @@ export default class PictureUpload extends Vue {
@Prop({ type: Object, required: false }) defaultImage!: IMedia; @Prop({ type: Object, required: false }) defaultImage!: IMedia;
@Prop({ type: String, required: false, default: "image/gif,image/png,image/jpeg,image/webp" }) @Prop({
type: String,
required: false,
default: "image/gif,image/png,image/jpeg,image/webp",
})
accept!: string; accept!: string;
@Prop({ @Prop({
@ -95,13 +99,11 @@ export default class PictureUpload extends Vue {
@Watch("pictureFile") @Watch("pictureFile")
onPictureFileChanged(val: File): void { onPictureFileChanged(val: File): void {
console.log("onPictureFileChanged", val);
this.updatePreview(val); this.updatePreview(val);
} }
@Watch("defaultImage") @Watch("defaultImage")
onDefaultImageChange(defaultImage: IMedia): void { onDefaultImageChange(defaultImage: IMedia): void {
console.log("onDefaultImageChange", defaultImage);
this.imageSrc = defaultImage ? defaultImage.url : null; this.imageSrc = defaultImage ? defaultImage.url : null;
} }

View File

@ -14,26 +14,46 @@
<div class="media-content"> <div class="media-content">
<p class="post-minimalist-title">{{ post.title }}</p> <p class="post-minimalist-title">{{ post.title }}</p>
<div class="metadata"> <div class="metadata">
<b-tag type="is-warning" size="is-small" v-if="post.draft">{{ $t("Draft") }}</b-tag> <b-tag type="is-warning" size="is-small" v-if="post.draft">{{
$t("Draft")
}}</b-tag>
<small <small
v-if="post.visibility === PostVisibility.PUBLIC && isCurrentActorMember" v-if="
post.visibility === PostVisibility.PUBLIC &&
isCurrentActorMember
"
class="has-text-grey" class="has-text-grey"
> >
<b-icon icon="earth" size="is-small" />{{ $t("Public") }}</small <b-icon icon="earth" size="is-small" />{{ $t("Public") }}</small
> >
<small v-else-if="post.visibility === PostVisibility.UNLISTED" class="has-text-grey"> <small
<b-icon icon="link" size="is-small" />{{ $t("Accessible through link") }}</small v-else-if="post.visibility === PostVisibility.UNLISTED"
class="has-text-grey"
>
<b-icon icon="link" size="is-small" />{{
$t("Accessible through link")
}}</small
>
<small
v-else-if="post.visibility === PostVisibility.PRIVATE"
class="has-text-grey"
> >
<small v-else-if="post.visibility === PostVisibility.PRIVATE" class="has-text-grey">
<b-icon icon="lock" size="is-small" />{{ <b-icon icon="lock" size="is-small" />{{
$t("Accessible only to members", { group: post.attributedTo.name }) $t("Accessible only to members", {
group: post.attributedTo.name,
})
}}</small }}</small
> >
<small class="has-text-grey">{{ <small class="has-text-grey">{{
$options.filters.formatDateTimeString(new Date(post.insertedAt), false) $options.filters.formatDateTimeString(
new Date(post.insertedAt),
false
)
}}</small> }}</small>
<small class="has-text-grey" v-if="isCurrentActorMember">{{ <small class="has-text-grey" v-if="isCurrentActorMember">{{
$t("Created by {username}", { username: `@${usernameWithDomain(post.author)}` }) $t("Created by {username}", {
username: `@${usernameWithDomain(post.author)}`,
})
}}</small> }}</small>
</div> </div>
</div> </div>
@ -43,15 +63,17 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { usernameWithDomain } from "@/types/actor"; import { usernameWithDomain } from "@/types/actor";
import { PostVisibility } from "@/types/enums";
import { Component, Prop, Vue } from "vue-property-decorator"; import { Component, Prop, Vue } from "vue-property-decorator";
import RouteName from "../../router/name"; import RouteName from "../../router/name";
import { IPost, PostVisibility } from "../../types/post.model"; import { IPost } from "../../types/post.model";
@Component @Component
export default class PostElementItem extends Vue { export default class PostElementItem extends Vue {
@Prop({ required: true, type: Object }) post!: IPost; @Prop({ required: true, type: Object }) post!: IPost;
@Prop({ required: false, type: Boolean, default: false }) isCurrentActorMember!: boolean; @Prop({ required: false, type: Boolean, default: false })
isCurrentActorMember!: boolean;
RouteName = RouteName; RouteName = RouteName;
@ -74,7 +96,8 @@ export default class PostElementItem extends Vue {
.post-minimalist-title { .post-minimalist-title {
color: #3c376e; color: #3c376e;
font-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica, Arial, serif; font-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica, Arial,
serif;
font-size: 1rem; font-size: 1rem;
font-weight: 700; font-weight: 700;
overflow: hidden; overflow: hidden;

View File

@ -43,7 +43,8 @@ export default class PostListItem extends Vue {
.post-minimalist-title { .post-minimalist-title {
color: #3c376e; color: #3c376e;
font-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica, Arial, serif; font-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica, Arial,
serif;
font-size: 1rem; font-size: 1rem;
font-weight: 700; font-weight: 700;
overflow: hidden; overflow: hidden;

View File

@ -22,10 +22,18 @@
<div class="content columns"> <div class="content columns">
<div class="column is-one-quarter-desktop"> <div class="column is-one-quarter-desktop">
<span v-if="report.reporter.type === ActorType.APPLICATION"> <span v-if="report.reporter.type === ActorType.APPLICATION">
{{ $t("Reported by someone on {domain}", { domain: report.reporter.domain }) }} {{
$t("Reported by someone on {domain}", {
domain: report.reporter.domain,
})
}}
</span> </span>
<span v-else> <span v-else>
{{ $t("Reported by {reporter}", { reporter: report.reporter.preferredUsername }) }} {{
$t("Reported by {reporter}", {
reporter: report.reporter.preferredUsername,
})
}}
</span> </span>
</div> </div>
<div class="column" v-if="report.content" v-html="report.content" /> <div class="column" v-if="report.content" v-html="report.content" />
@ -36,7 +44,7 @@
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator"; import { Component, Prop, Vue } from "vue-property-decorator";
import { IReport } from "@/types/report.model"; import { IReport } from "@/types/report.model";
import { ActorType } from "@/types/actor"; import { ActorType } from "@/types/enums";
@Component @Component
export default class ReportCard extends Vue { export default class ReportCard extends Vue {

View File

@ -4,7 +4,10 @@
<p class="modal-card-title">{{ title }}</p> <p class="modal-card-title">{{ title }}</p>
</header> </header>
<section class="modal-card-body is-flex" :class="{ 'is-titleless': !title }"> <section
class="modal-card-body is-flex"
:class="{ 'is-titleless': !title }"
>
<div class="media"> <div class="media">
<div class="media-left"> <div class="media-left">
<b-icon icon="alert" type="is-warning" size="is-large" /> <b-icon icon="alert" type="is-warning" size="is-large" />
@ -16,7 +19,12 @@
<figure class="image is-48x48" v-if="comment.actor.avatar"> <figure class="image is-48x48" v-if="comment.actor.avatar">
<img :src="comment.actor.avatar.url" alt="Image" /> <img :src="comment.actor.avatar.url" alt="Image" />
</figure> </figure>
<b-icon class="media-left" v-else size="is-large" icon="account-circle" /> <b-icon
class="media-left"
v-else
size="is-large"
icon="account-circle"
/>
</div> </div>
<div class="media-content"> <div class="media-content">
<div class="content"> <div class="content">
@ -82,7 +90,10 @@ import { IComment } from "../../types/comment.model";
}, },
}) })
export default class ReportModal extends Vue { export default class ReportModal extends Vue {
@Prop({ type: Function }) onConfirm!: Function; @Prop({ type: Function }) onConfirm!: (
content: string,
forward: boolean
) => void;
@Prop({ type: String }) title!: string; @Prop({ type: String }) title!: string;

View File

@ -14,7 +14,9 @@
</div> </div>
<div class="body"> <div class="body">
<h3>{{ resource.title }}</h3> <h3>{{ resource.title }}</h3>
<span class="host" v-if="inline">{{ resource.updatedAt | formatDateTimeString }}</span> <span class="host" v-if="inline">{{
resource.updatedAt | formatDateTimeString
}}</span>
</div> </div>
<draggable <draggable
v-if="!inline" v-if="!inline"
@ -93,21 +95,27 @@ export default class FolderItem extends Mixins(ResourceMixin) {
async moveResource(resource: IResource): Promise<IResource | undefined> { async moveResource(resource: IResource): Promise<IResource | undefined> {
try { try {
const { data } = await this.$apollo.mutate<{ updateResource: IResource }>({ const { data } = await this.$apollo.mutate<{ updateResource: IResource }>(
{
mutation: UPDATE_RESOURCE, mutation: UPDATE_RESOURCE,
variables: { variables: {
id: resource.id, id: resource.id,
path: `${this.resource.path}/${resource.title}`, path: `${this.resource.path}/${resource.title}`,
parentId: this.resource.id, parentId: this.resource.id,
}, },
}); }
);
if (!data) { if (!data) {
console.error("Error while updating resource"); console.error("Error while updating resource");
return undefined; return undefined;
} }
return data.updateResource; return data.updateResource;
} catch (e) { } catch (e) {
Snackbar.open({ message: e.message, type: "is-danger", position: "is-bottom" }); Snackbar.open({
message: e.message,
type: "is-danger",
position: "is-bottom",
});
return undefined; return undefined;
} }
} }

View File

@ -17,7 +17,7 @@
</b-dropdown> </b-dropdown>
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Vue, Prop } from "vue-property-decorator"; import { Component, Vue } from "vue-property-decorator";
@Component @Component
export default class ResourceDropdown extends Vue {} export default class ResourceDropdown extends Vue {}

View File

@ -2,7 +2,12 @@
<div class="resource-wrapper"> <div class="resource-wrapper">
<a :href="resource.resourceUrl" target="_blank"> <a :href="resource.resourceUrl" target="_blank">
<div class="preview"> <div class="preview">
<div v-if="resource.type && Object.keys(mapServiceTypeToIcon).includes(resource.type)"> <div
v-if="
resource.type &&
Object.keys(mapServiceTypeToIcon).includes(resource.type)
"
>
<b-icon :icon="mapServiceTypeToIcon[resource.type]" size="is-large" /> <b-icon :icon="mapServiceTypeToIcon[resource.type]" size="is-large" />
</div> </div>
<div <div
@ -21,7 +26,9 @@
:src="resource.metadata.faviconUrl" :src="resource.metadata.faviconUrl"
/> />
<h3>{{ resource.title }}</h3> <h3>{{ resource.title }}</h3>
<span class="host" v-if="inline">{{ resource.updatedAt | formatDateTimeString }}</span> <span class="host" v-if="inline">{{
resource.updatedAt | formatDateTimeString
}}</span>
<span class="host" v-else>{{ urlHostname }}</span> <span class="host" v-else>{{ urlHostname }}</span>
</div> </div>
</a> </a>

View File

@ -2,9 +2,15 @@
<div v-if="resource"> <div v-if="resource">
<article class="panel is-primary"> <article class="panel is-primary">
<p class="panel-heading"> <p class="panel-heading">
{{ $t('Move "{resourceName}"', { resourceName: initialResource.title }) }} {{
$t('Move "{resourceName}"', { resourceName: initialResource.title })
}}
</p> </p>
<a class="panel-block clickable" @click="resource = resource.parent" v-if="resource.parent"> <a
class="panel-block clickable"
@click="resource = resource.parent"
v-if="resource.parent"
>
<span class="panel-icon"> <span class="panel-icon">
<b-icon icon="chevron-up" size="is-small" /> <b-icon icon="chevron-up" size="is-small" />
</span> </span>
@ -23,12 +29,19 @@
<a <a
class="panel-block" class="panel-block"
v-for="element in resource.children.elements" v-for="element in resource.children.elements"
:class="{ clickable: element.type === 'folder' && element.id !== initialResource.id }" :class="{
clickable:
element.type === 'folder' && element.id !== initialResource.id,
}"
:key="element.id" :key="element.id"
@click="goDown(element)" @click="goDown(element)"
> >
<span class="panel-icon"> <span class="panel-icon">
<b-icon icon="folder" size="is-small" v-if="element.type === 'folder'" /> <b-icon
icon="folder"
size="is-small"
v-if="element.type === 'folder'"
/>
<b-icon icon="link" size="is-small" v-else /> <b-icon icon="link" size="is-small" v-else />
</span> </span>
{{ element.title }} {{ element.title }}
@ -44,10 +57,17 @@
{{ $t("No resources in this folder") }} {{ $t("No resources in this folder") }}
</p> </p>
</article> </article>
<b-button type="is-primary" @click="updateResource" :disabled="moveDisabled">{{ <b-button
type="is-primary"
@click="updateResource"
:disabled="moveDisabled"
>{{
$t("Move resource to {folder}", { folder: resource.title }) $t("Move resource to {folder}", { folder: resource.title })
}}</b-button
>
<b-button type="is-text" @click="$emit('close-move-modal')">{{
$t("Cancel")
}}</b-button> }}</b-button>
<b-button type="is-text" @click="$emit('closeMoveModal')">{{ $t("Cancel") }}</b-button>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
@ -81,31 +101,34 @@ export default class ResourceSelector extends Vue {
resource: IResource | undefined = this.initialResource.parent; resource: IResource | undefined = this.initialResource.parent;
goDown(element: IResource) { goDown(element: IResource): void {
if (element.type === "folder" && element.id !== this.initialResource.id) { if (element.type === "folder" && element.id !== this.initialResource.id) {
this.resource = element; this.resource = element;
} }
} }
updateResource() { updateResource(): void {
this.$emit( this.$emit(
"updateResource", "update-resource",
{ {
id: this.initialResource.id, id: this.initialResource.id,
title: this.initialResource.title, title: this.initialResource.title,
parent: this.resource && this.resource.path === "/" ? null : this.resource, parent:
this.resource && this.resource.path === "/" ? null : this.resource,
path: this.initialResource.path, path: this.initialResource.path,
}, },
this.initialResource.parent this.initialResource.parent
); );
} }
get moveDisabled() { get moveDisabled(): boolean | undefined {
return ( return (
(this.initialResource.parent && (this.initialResource.parent &&
this.resource && this.resource &&
this.initialResource.parent.path === this.resource.path) || this.initialResource.parent.path === this.resource.path) ||
(this.initialResource.parent == undefined && this.resource && this.resource.path === "/") (this.initialResource.parent === undefined &&
this.resource &&
this.resource.path === "/")
); );
} }
} }

View File

@ -23,9 +23,9 @@ export default class SearchField extends Vue {
search = ""; search = "";
enter() { async enter(): Promise<void> {
this.$emit("navbar-search"); this.$emit("navbar-search");
this.$router.push({ await this.$router.push({
name: RouteName.SEARCH, name: RouteName.SEARCH,
query: { term: this.search }, query: { term: this.search },
}); });

View File

@ -15,16 +15,27 @@
</p> </p>
</div> </div>
<div class="field"> <div class="field">
<b-checkbox v-model="notificationOnDay" @input="updateSetting({ notificationOnDay })"> <b-checkbox
v-model="notificationOnDay"
@input="updateSetting({ notificationOnDay })"
>
<strong>{{ $t("Notification on the day of the event") }}</strong> <strong>{{ $t("Notification on the day of the event") }}</strong>
<p> <p>
{{ {{
$t("We'll use your timezone settings to send a recap of the morning of the event.") $t(
"We'll use your timezone settings to send a recap of the morning of the event."
)
}} }}
</p> </p>
</b-checkbox> </b-checkbox>
</div> </div>
<p>{{ $t("To activate more notifications, head over to the notification settings.") }}</p> <p>
{{
$t(
"To activate more notifications, head over to the notification settings."
)
}}
</p>
</section> </section>
</div> </div>
</template> </template>
@ -50,7 +61,11 @@ export default class NotificationsOnboarding extends mixins(Onboarding) {
try { try {
this.doUpdateSetting(variables); this.doUpdateSetting(variables);
} catch (e) { } catch (e) {
Snackbar.open({ message: e.message, type: "is-danger", position: "is-bottom" }); Snackbar.open({
message: e.message,
type: "is-danger",
position: "is-bottom",
});
} }
} }
} }

View File

@ -16,7 +16,7 @@ export default class SettingMenuItem extends Vue {
@Prop({ required: true, type: Object }) to!: Route; @Prop({ required: true, type: Object }) to!: Route;
get isActive() { get isActive(): boolean {
if (!this.to) return false; if (!this.to) return false;
if (this.to.name === this.$route.name) { if (this.to.name === this.$route.name) {
if (this.to.params) { if (this.to.params) {

View File

@ -20,11 +20,12 @@ export default class SettingMenuSection extends Vue {
@Prop({ required: true, type: Object }) to!: Route; @Prop({ required: true, type: Object }) to!: Route;
get sectionActive() { get sectionActive(): boolean {
if (this.$slots.default) { if (this.$slots.default) {
return this.$slots.default.some( return this.$slots.default.some(
({ ({
componentOptions: { componentOptions: {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
propsData: { to }, propsData: { to },
}, },

View File

@ -1,18 +1,27 @@
<template> <template>
<aside> <aside>
<ul> <ul>
<SettingMenuSection :title="$t('Account')" :to="{ name: RouteName.ACCOUNT_SETTINGS }"> <SettingMenuSection
:title="$t('Account')"
:to="{ name: RouteName.ACCOUNT_SETTINGS }"
>
<SettingMenuItem <SettingMenuItem
:title="this.$t('General')" :title="this.$t('General')"
:to="{ name: RouteName.ACCOUNT_SETTINGS_GENERAL }" :to="{ name: RouteName.ACCOUNT_SETTINGS_GENERAL }"
/> />
<SettingMenuItem :title="$t('Preferences')" :to="{ name: RouteName.PREFERENCES }" /> <SettingMenuItem
:title="$t('Preferences')"
:to="{ name: RouteName.PREFERENCES }"
/>
<SettingMenuItem <SettingMenuItem
:title="this.$t('Email notifications')" :title="this.$t('Email notifications')"
:to="{ name: RouteName.NOTIFICATIONS }" :to="{ name: RouteName.NOTIFICATIONS }"
/> />
</SettingMenuSection> </SettingMenuSection>
<SettingMenuSection :title="$t('Profiles')" :to="{ name: RouteName.IDENTITIES }"> <SettingMenuSection
:title="$t('Profiles')"
:to="{ name: RouteName.IDENTITIES }"
>
<SettingMenuItem <SettingMenuItem
v-for="profile in identities" v-for="profile in identities"
:key="profile.preferredUsername" :key="profile.preferredUsername"
@ -22,7 +31,10 @@
params: { identityName: profile.preferredUsername }, params: { identityName: profile.preferredUsername },
}" }"
/> />
<SettingMenuItem :title="$t('New profile')" :to="{ name: RouteName.CREATE_IDENTITY }" /> <SettingMenuItem
:title="$t('New profile')"
:to="{ name: RouteName.CREATE_IDENTITY }"
/>
</SettingMenuSection> </SettingMenuSection>
<SettingMenuSection <SettingMenuSection
v-if=" v-if="
@ -33,35 +45,54 @@
:title="$t('Moderation')" :title="$t('Moderation')"
:to="{ name: RouteName.MODERATION }" :to="{ name: RouteName.MODERATION }"
> >
<SettingMenuItem :title="$t('Reports')" :to="{ name: RouteName.REPORTS }" /> <SettingMenuItem
<SettingMenuItem :title="$t('Moderation log')" :to="{ name: RouteName.REPORT_LOGS }" /> :title="$t('Reports')"
:to="{ name: RouteName.REPORTS }"
/>
<SettingMenuItem
:title="$t('Moderation log')"
:to="{ name: RouteName.REPORT_LOGS }"
/>
<SettingMenuItem :title="$t('Users')" :to="{ name: RouteName.USERS }" /> <SettingMenuItem :title="$t('Users')" :to="{ name: RouteName.USERS }" />
<SettingMenuItem :title="$t('Profiles')" :to="{ name: RouteName.PROFILES }" /> <SettingMenuItem
<SettingMenuItem :title="$t('Groups')" :to="{ name: RouteName.ADMIN_GROUPS }" /> :title="$t('Profiles')"
:to="{ name: RouteName.PROFILES }"
/>
<SettingMenuItem
:title="$t('Groups')"
:to="{ name: RouteName.ADMIN_GROUPS }"
/>
</SettingMenuSection> </SettingMenuSection>
<SettingMenuSection <SettingMenuSection
v-if="this.currentUser.role == ICurrentUserRole.ADMINISTRATOR" v-if="this.currentUser.role == ICurrentUserRole.ADMINISTRATOR"
:title="$t('Admin')" :title="$t('Admin')"
:to="{ name: RouteName.ADMIN }" :to="{ name: RouteName.ADMIN }"
> >
<SettingMenuItem :title="$t('Dashboard')" :to="{ name: RouteName.ADMIN_DASHBOARD }" /> <SettingMenuItem
:title="$t('Dashboard')"
:to="{ name: RouteName.ADMIN_DASHBOARD }"
/>
<SettingMenuItem <SettingMenuItem
:title="$t('Instance settings')" :title="$t('Instance settings')"
:to="{ name: RouteName.ADMIN_SETTINGS }" :to="{ name: RouteName.ADMIN_SETTINGS }"
/> />
<SettingMenuItem :title="$t('Federation')" :to="{ name: RouteName.RELAYS }" /> <SettingMenuItem
:title="$t('Federation')"
:to="{ name: RouteName.RELAYS }"
/>
</SettingMenuSection> </SettingMenuSection>
</ul> </ul>
</aside> </aside>
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator"; import { Component, Vue } from "vue-property-decorator";
import { ICurrentUserRole } from "@/types/enums";
import SettingMenuSection from "./SettingMenuSection.vue"; import SettingMenuSection from "./SettingMenuSection.vue";
import SettingMenuItem from "./SettingMenuItem.vue"; import SettingMenuItem from "./SettingMenuItem.vue";
import { IDENTITIES } from "../../graphql/actor"; import { IDENTITIES } from "../../graphql/actor";
import { IPerson, Person } from "../../types/actor"; import { IPerson, Person } from "../../types/actor";
import { CURRENT_USER_CLIENT } from "../../graphql/user"; import { CURRENT_USER_CLIENT } from "../../graphql/user";
import { ICurrentUser, ICurrentUserRole } from "../../types/current-user.model"; import { ICurrentUser } from "../../types/current-user.model";
import RouteName from "../../router/name"; import RouteName from "../../router/name";
@ -70,7 +101,8 @@ import RouteName from "../../router/name";
apollo: { apollo: {
identities: { identities: {
query: IDENTITIES, query: IDENTITIES,
update: (data) => data.identities.map((identity: IPerson) => new Person(identity)), update: (data) =>
data.identities.map((identity: IPerson) => new Person(identity)),
}, },
currentUser: CURRENT_USER_CLIENT, currentUser: CURRENT_USER_CLIENT,
}, },

View File

@ -39,7 +39,10 @@
timezone, timezone,
}) })
}} }}
<b-message type="is-danger" v-if="!$apollo.loading && !supportedTimezone"> <b-message
type="is-danger"
v-if="!$apollo.loading && !supportedTimezone"
>
{{ $t("Your timezone {timezone} isn't supported.", { timezone }) }} {{ $t("Your timezone {timezone} isn't supported.", { timezone }) }}
</b-message> </b-message>
</p> </p>

View File

@ -2,9 +2,10 @@
<div class="card" v-if="todo"> <div class="card" v-if="todo">
<div class="card-content"> <div class="card-content">
<b-checkbox v-model="status" /> <b-checkbox v-model="status" />
<router-link :to="{ name: RouteName.TODO, params: { todoId: todo.id } }">{{ <router-link
todo.title :to="{ name: RouteName.TODO, params: { todoId: todo.id } }"
}}</router-link> >{{ todo.title }}</router-link
>
<span class="details has-text-grey"> <span class="details has-text-grey">
<span v-if="todo.dueDate" class="due_date"> <span v-if="todo.dueDate" class="due_date">
<b-icon icon="calendar" /> <b-icon icon="calendar" />
@ -13,7 +14,9 @@
<span v-if="todo.assignedTo" class="assigned_to"> <span v-if="todo.assignedTo" class="assigned_to">
<b-icon icon="account" /> <b-icon icon="account" />
{{ `@${todo.assignedTo.preferredUsername}` }} {{ `@${todo.assignedTo.preferredUsername}` }}
<span v-if="todo.assignedTo.domain">{{ `@${todo.assignedTo.domain}` }}</span> <span v-if="todo.assignedTo.domain">{{
`@${todo.assignedTo.domain}`
}}</span>
</span> </span>
</span> </span>
</div> </div>
@ -53,7 +56,11 @@ export default class Todo extends Vue {
}); });
this.editMode = false; this.editMode = false;
} catch (e) { } catch (e) {
Snackbar.open({ message: e.message, type: "is-danger", position: "is-bottom" }); Snackbar.open({
message: e.message,
type: "is-danger",
position: "is-bottom",
});
} }
} }
} }

View File

@ -18,7 +18,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator"; import { Component, Prop, Vue } from "vue-property-decorator";
import { debounce } from "lodash"; import { debounce, DebouncedFunc } from "lodash";
import { SnackbarProgrammatic as Snackbar } from "buefy"; import { SnackbarProgrammatic as Snackbar } from "buefy";
import { ITodo } from "../../types/todos"; import { ITodo } from "../../types/todos";
import RouteName from "../../router/name"; import RouteName from "../../router/name";
@ -36,7 +36,9 @@ export default class Todo extends Vue {
editMode = false; editMode = false;
debounceUpdateTodo!: Function; debounceUpdateTodo!: DebouncedFunc<
(obj: Record<string, unknown>) => Promise<void>
>;
// We put this in data because of issues like // We put this in data because of issues like
// https://github.com/vuejs/vue-class-component/issues/263 // https://github.com/vuejs/vue-class-component/issues/263
@ -89,7 +91,11 @@ export default class Todo extends Vue {
}); });
this.editMode = false; this.editMode = false;
} catch (e) { } catch (e) {
Snackbar.open({ message: e.message, type: "is-danger", position: "is-bottom" }); Snackbar.open({
message: e.message,
type: "is-danger",
position: "is-bottom",
});
} }
} }
} }

View File

@ -7,7 +7,11 @@
<b-icon :icon="oauthProvider.id" /> <b-icon :icon="oauthProvider.id" />
<span>{{ SELECTED_PROVIDERS[oauthProvider.id] }}</span></a <span>{{ SELECTED_PROVIDERS[oauthProvider.id] }}</span></a
> >
<a class="button is-light" :href="`/auth/${oauthProvider.id}`" v-else-if="isProviderSelected"> <a
class="button is-light"
:href="`/auth/${oauthProvider.id}`"
v-else-if="isProviderSelected"
>
<b-icon icon="lock" /> <b-icon icon="lock" />
<span>{{ oauthProvider.label }}</span> <span>{{ oauthProvider.label }}</span>
</a> </a>

View File

@ -20,7 +20,9 @@
</div> </div>
<vertical-divider :content="$t('Or')" /> <vertical-divider :content="$t('Or')" />
<div class="column"> <div class="column">
<subtitle>{{ $t("I have an account on another Mobilizon instance.") }}</subtitle> <subtitle>{{
$t("I have an account on another Mobilizon instance.")
}}</subtitle>
<p>{{ $t("Other software may also support this.") }}</p> <p>{{ $t("Other software may also support this.") }}</p>
<p>{{ sentence }}</p> <p>{{ sentence }}</p>
<form @submit.prevent="redirectToInstance"> <form @submit.prevent="redirectToInstance">
@ -34,7 +36,9 @@
:placeholder="$t('profile@instance')" :placeholder="$t('profile@instance')"
></b-input> ></b-input>
<p class="control"> <p class="control">
<button class="button is-primary" type="submit">{{ $t("Go") }}</button> <button class="button is-primary" type="submit">
{{ $t("Go") }}
</button>
</p> </p>
</b-field> </b-field>
</b-field> </b-field>
@ -54,7 +58,7 @@
import { Component, Prop, Vue } from "vue-property-decorator"; import { Component, Prop, Vue } from "vue-property-decorator";
import VerticalDivider from "@/components/Utils/VerticalDivider.vue"; import VerticalDivider from "@/components/Utils/VerticalDivider.vue";
import Subtitle from "@/components/Utils/Subtitle.vue"; import Subtitle from "@/components/Utils/Subtitle.vue";
import { LoginErrorCode } from "@/types/login-error-code.model"; import { LoginErrorCode } from "@/types/enums";
import RouteName from "../../router/name"; import RouteName from "../../router/name";
@Component({ @Component({
@ -80,14 +84,22 @@ export default class RedirectWithAccount extends Vue {
async redirectToInstance(): Promise<void> { async redirectToInstance(): Promise<void> {
const [, host] = this.remoteActorAddress.split("@", 2); const [, host] = this.remoteActorAddress.split("@", 2);
const remoteInteractionURI = await this.webFingerFetch(host, this.remoteActorAddress); const remoteInteractionURI = await this.webFingerFetch(
host,
this.remoteActorAddress
);
window.open(remoteInteractionURI); window.open(remoteInteractionURI);
} }
private async webFingerFetch(hostname: string, identity: string): Promise<string> { private async webFingerFetch(
hostname: string,
identity: string
): Promise<string> {
const scheme = process.env.NODE_ENV === "production" ? "https" : "http"; const scheme = process.env.NODE_ENV === "production" ? "https" : "http";
const data = await ( const data = await (
await fetch(`${scheme}://${hostname}/.well-known/webfinger?resource=acct:${identity}`) await fetch(
`${scheme}://${hostname}/.well-known/webfinger?resource=acct:${identity}`
)
).json(); ).json();
if (data && Array.isArray(data.links)) { if (data && Array.isArray(data.links)) {
const link: { template: string } = data.links.find( const link: { template: string } = data.links.find(

View File

@ -21,7 +21,8 @@ h2 {
display: inline; display: inline;
padding: 3px 8px; padding: 3px 8px;
color: #3a384c; color: #3a384c;
font-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica, Arial, serif; font-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica, Arial,
serif;
font-weight: 400; font-weight: 400;
font-size: 32px; font-size: 32px;
} }

View File

@ -14,7 +14,10 @@ function formatDateString(value: string): string {
} }
function formatTimeString(value: string): string { function formatTimeString(value: string): string {
return parseDateTime(value).toLocaleTimeString(undefined, { hour: "numeric", minute: "numeric" }); return parseDateTime(value).toLocaleTimeString(undefined, {
hour: "numeric",
minute: "numeric",
});
} }
function formatDateTimeString(value: string, showTime = true): string { function formatDateTimeString(value: string, showTime = true): string {

View File

@ -1,5 +1,9 @@
import nl2br from "@/filters/utils"; import nl2br from "@/filters/utils";
import { formatDateString, formatTimeString, formatDateTimeString } from "./datetime"; import {
formatDateString,
formatTimeString,
formatDateTimeString,
} from "./datetime";
export default { export default {
install(vue: any): void { install(vue: any): void {

View File

@ -65,7 +65,10 @@ export const GET_PERSON = gql`
feedTokens { feedTokens {
token token
} }
organizedEvents(page: $organizedEventsPage, limit: $organizedEventsLimit) { organizedEvents(
page: $organizedEventsPage
limit: $organizedEventsLimit
) {
total total
elements { elements {
id id
@ -442,7 +445,12 @@ export const CREATE_PERSON = gql`
`; `;
export const UPDATE_PERSON = gql` export const UPDATE_PERSON = gql`
mutation UpdatePerson($id: ID!, $name: String, $summary: String, $avatar: MediaInput) { mutation UpdatePerson(
$id: ID!
$name: String
$summary: String
$avatar: MediaInput
) {
updatePerson(id: $id, name: $name, summary: $summary, avatar: $avatar) { updatePerson(id: $id, name: $name, summary: $summary, avatar: $avatar) {
id id
preferredUsername preferredUsername
@ -469,7 +477,12 @@ export const DELETE_PERSON = gql`
* Prefer CREATE_PERSON when creating another identity * Prefer CREATE_PERSON when creating another identity
*/ */
export const REGISTER_PERSON = gql` export const REGISTER_PERSON = gql`
mutation($preferredUsername: String!, $name: String!, $summary: String!, $email: String!) { mutation(
$preferredUsername: String!
$name: String!
$summary: String!
$email: String!
) {
registerPerson( registerPerson(
preferredUsername: $preferredUsername preferredUsername: $preferredUsername
name: $name name: $name

View File

@ -68,8 +68,16 @@ export const COMMENTS_THREADS = gql`
`; `;
export const CREATE_COMMENT_FROM_EVENT = gql` export const CREATE_COMMENT_FROM_EVENT = gql`
mutation CreateCommentFromEvent($eventId: ID!, $text: String!, $inReplyToCommentId: ID) { mutation CreateCommentFromEvent(
createComment(eventId: $eventId, text: $text, inReplyToCommentId: $inReplyToCommentId) { $eventId: ID!
$text: String!
$inReplyToCommentId: ID
) {
createComment(
eventId: $eventId
text: $text
inReplyToCommentId: $inReplyToCommentId
) {
...CommentRecursive ...CommentRecursive
} }
} }

View File

@ -42,8 +42,20 @@ export const SEARCH_EVENTS = gql`
`; `;
export const SEARCH_GROUPS = gql` export const SEARCH_GROUPS = gql`
query SearchGroups($term: String, $location: String, $radius: Float, $page: Int, $limit: Int) { query SearchGroups(
searchGroups(term: $term, location: $location, radius: $radius, page: $page, limit: $limit) { $term: String
$location: String
$radius: Float
$page: Int
$limit: Int
) {
searchGroups(
term: $term
location: $location
radius: $radius
page: $page
limit: $limit
) {
total total
elements { elements {
id id

View File

@ -108,7 +108,12 @@ export const UPDATE_CURRENT_USER_CLIENT = gql`
$isLoggedIn: Boolean! $isLoggedIn: Boolean!
$role: UserRole! $role: UserRole!
) { ) {
updateCurrentUser(id: $id, email: $email, isLoggedIn: $isLoggedIn, role: $role) @client updateCurrentUser(
id: $id
email: $email
isLoggedIn: $isLoggedIn
role: $role
) @client
} }
`; `;

View File

@ -4,9 +4,11 @@ import { Component, Vue } from "vue-property-decorator";
@Component @Component
export default class ActorMixin extends Vue { export default class ActorMixin extends Vue {
static actorIsOrganizer(actor: IActor, event: IEvent) { static actorIsOrganizer(actor: IActor, event: IEvent): boolean {
console.log("actorIsOrganizer actor", actor.id); console.log("actorIsOrganizer actor", actor.id);
console.log("actorIsOrganizer event", event); console.log("actorIsOrganizer event", event);
return event.organizerActor && actor.id === event.organizerActor.id; return (
event.organizerActor !== undefined && actor.id === event.organizerActor.id
);
} }
} }

View File

@ -1,7 +1,8 @@
import { mixins } from "vue-class-component"; import { mixins } from "vue-class-component";
import { Component, Vue } from "vue-property-decorator"; import { Component, Vue } from "vue-property-decorator";
import { SnackbarProgrammatic as Snackbar } from "buefy"; import { SnackbarProgrammatic as Snackbar } from "buefy";
import { IParticipant, ParticipantRole } from "../types/participant.model"; import { ParticipantRole } from "@/types/enums";
import { IParticipant } from "../types/participant.model";
import { IEvent } from "../types/event.model"; import { IEvent } from "../types/event.model";
import { import {
DELETE_EVENT, DELETE_EVENT,
@ -20,7 +21,9 @@ export default class EventMixin extends mixins(Vue) {
anonymousParticipationConfirmed: boolean | null = null anonymousParticipationConfirmed: boolean | null = null
): Promise<void> { ): Promise<void> {
try { try {
const { data: resultData } = await this.$apollo.mutate<{ leaveEvent: IParticipant }>({ const { data: resultData } = await this.$apollo.mutate<{
leaveEvent: IParticipant;
}>({
mutation: LEAVE_EVENT, mutation: LEAVE_EVENT,
variables: { variables: {
eventId: event.id, eventId: event.id,
@ -32,14 +35,18 @@ export default class EventMixin extends mixins(Vue) {
let participation; let participation;
if (!token) { if (!token) {
const participationCachedData = store.readQuery<{ person: IPerson }>({ const participationCachedData = store.readQuery<{
person: IPerson;
}>({
query: EVENT_PERSON_PARTICIPATION, query: EVENT_PERSON_PARTICIPATION,
variables: { eventId: event.id, actorId }, variables: { eventId: event.id, actorId },
}); });
if (participationCachedData == null) return; if (participationCachedData == null) return;
const { person } = participationCachedData; const { person } = participationCachedData;
if (person === null) { if (person === null) {
console.error("Cannot update participation cache, because of null value."); console.error(
"Cannot update participation cache, because of null value."
);
return; return;
} }
[participation] = person.participations.elements; [participation] = person.participations.elements;
@ -62,7 +69,10 @@ export default class EventMixin extends mixins(Vue) {
console.error("Cannot update event cache, because of null value."); console.error("Cannot update event cache, because of null value.");
return; return;
} }
if (participation && participation.role === ParticipantRole.NOT_APPROVED) { if (
participation &&
participation.role === ParticipantRole.NOT_APPROVED
) {
eventCached.participantStats.notApproved -= 1; eventCached.participantStats.notApproved -= 1;
} else if (anonymousParticipationConfirmed === false) { } else if (anonymousParticipationConfirmed === false) {
eventCached.participantStats.notConfirmed -= 1; eventCached.participantStats.notConfirmed -= 1;
@ -81,13 +91,19 @@ export default class EventMixin extends mixins(Vue) {
this.participationCancelledMessage(); this.participationCancelledMessage();
} }
} catch (error) { } catch (error) {
Snackbar.open({ message: error.message, type: "is-danger", position: "is-bottom" }); Snackbar.open({
message: error.message,
type: "is-danger",
position: "is-bottom",
});
console.error(error); console.error(error);
} }
} }
private participationCancelledMessage() { private participationCancelledMessage() {
this.$notifier.success(this.$t("You have cancelled your participation") as string); this.$notifier.success(
this.$t("You have cancelled your participation") as string
);
} }
protected async openDeleteEventModal(event: IEvent): Promise<void> { protected async openDeleteEventModal(event: IEvent): Promise<void> {
@ -96,21 +112,29 @@ export default class EventMixin extends mixins(Vue) {
} }
const participantsLength = event.participantStats.participant; const participantsLength = event.participantStats.participant;
const prefix = participantsLength const prefix = participantsLength
? this.$tc("There are {participants} participants.", event.participantStats.participant, { ? this.$tc(
"There are {participants} participants.",
event.participantStats.participant,
{
participants: event.participantStats.participant, participants: event.participantStats.participant,
}) }
)
: ""; : "";
this.$buefy.dialog.prompt({ this.$buefy.dialog.prompt({
type: "is-danger", type: "is-danger",
title: this.$t("Delete event") as string, title: this.$t("Delete event") as string,
message: `${prefix} message: `${prefix}
${this.$t("Are you sure you want to delete this event? This action cannot be reverted.")} ${this.$t(
"Are you sure you want to delete this event? This action cannot be reverted."
)}
<br><br> <br><br>
${this.$t('To confirm, type your event title "{eventTitle}"', { ${this.$t('To confirm, type your event title "{eventTitle}"', {
eventTitle: event.title, eventTitle: event.title,
})}`, })}`,
confirmText: this.$t("Delete {eventTitle}", { eventTitle: event.title }) as string, confirmText: this.$t("Delete {eventTitle}", {
eventTitle: event.title,
}) as string,
inputAttrs: { inputAttrs: {
placeholder: event.title, placeholder: event.title,
pattern: escapeRegExp(event.title), pattern: escapeRegExp(event.title),
@ -138,13 +162,19 @@ export default class EventMixin extends mixins(Vue) {
this.$emit("event-deleted", event.id); this.$emit("event-deleted", event.id);
this.$buefy.notification.open({ this.$buefy.notification.open({
message: this.$t("Event {eventTitle} deleted", { eventTitle }) as string, message: this.$t("Event {eventTitle} deleted", {
eventTitle,
}) as string,
type: "is-success", type: "is-success",
position: "is-bottom-right", position: "is-bottom-right",
duration: 5000, duration: 5000,
}); });
} catch (error) { } catch (error) {
Snackbar.open({ message: error.message, type: "is-danger", position: "is-bottom" }); Snackbar.open({
message: error.message,
type: "is-danger",
position: "is-bottom",
});
console.error(error); console.error(error);
} }

View File

@ -2,7 +2,8 @@ import { PERSON_MEMBERSHIPS, CURRENT_ACTOR_CLIENT } from "@/graphql/actor";
import { GROUP_MEMBERSHIP_SUBSCRIPTION_CHANGED } from "@/graphql/event"; import { GROUP_MEMBERSHIP_SUBSCRIPTION_CHANGED } from "@/graphql/event";
import { FETCH_GROUP } from "@/graphql/group"; import { FETCH_GROUP } from "@/graphql/group";
import RouteName from "@/router/name"; import RouteName from "@/router/name";
import { Group, IActor, IGroup, IPerson, MemberRole } from "@/types/actor"; import { Group, IActor, IGroup, IPerson } from "@/types/actor";
import { MemberRole } from "@/types/enums";
import { Component, Vue } from "vue-property-decorator"; import { Component, Vue } from "vue-property-decorator";
@Component({ @Component({
@ -60,7 +61,10 @@ export default class GroupMixin extends Vue {
} }
get isCurrentActorAGroupModerator(): boolean { get isCurrentActorAGroupModerator(): boolean {
return this.hasCurrentActorThisRole([MemberRole.MODERATOR, MemberRole.ADMINISTRATOR]); return this.hasCurrentActorThisRole([
MemberRole.MODERATOR,
MemberRole.ADMINISTRATOR,
]);
} }
hasCurrentActorThisRole(givenRole: string | string[]): boolean { hasCurrentActorThisRole(givenRole: string | string[]): boolean {
@ -68,7 +72,8 @@ export default class GroupMixin extends Vue {
return ( return (
this.person && this.person &&
this.person.memberships.elements.some( this.person.memberships.elements.some(
({ parent: { id }, role }) => id === this.group.id && roles.includes(role) ({ parent: { id }, role }) =>
id === this.group.id && roles.includes(role)
) )
); );
} }

View File

@ -9,10 +9,14 @@ export default class IdentityEditionMixin extends Mixins(Vue) {
oldDisplayName: string | null = null; oldDisplayName: string | null = null;
autoUpdateUsername(newDisplayName: string | null): void { autoUpdateUsername(newDisplayName: string | null): void {
const oldUsername = IdentityEditionMixin.convertToUsername(this.oldDisplayName); const oldUsername = IdentityEditionMixin.convertToUsername(
this.oldDisplayName
);
if (this.identity.preferredUsername === oldUsername) { if (this.identity.preferredUsername === oldUsername) {
this.identity.preferredUsername = IdentityEditionMixin.convertToUsername(newDisplayName); this.identity.preferredUsername = IdentityEditionMixin.convertToUsername(
newDisplayName
);
} }
this.oldDisplayName = newDisplayName; this.oldDisplayName = newDisplayName;

View File

@ -13,7 +13,9 @@ export default class Onboarding extends Vue {
RouteName = RouteName; RouteName = RouteName;
protected async doUpdateSetting(variables: Record<string, unknown>): Promise<void> { protected async doUpdateSetting(
variables: Record<string, unknown>
): Promise<void> {
await this.$apollo.mutate<{ setUserSettings: string }>({ await this.$apollo.mutate<{ setUserSettings: string }>({
mutation: SET_USER_SETTINGS, mutation: SET_USER_SETTINGS,
variables, variables,

View File

@ -1,8 +1,9 @@
import { Component, Vue, Ref } from "vue-property-decorator"; import { Component, Vue, Ref } from "vue-property-decorator";
import { ActorType, IActor } from "@/types/actor"; import { IActor } from "@/types/actor";
import { IFollower } from "@/types/actor/follower.model"; import { IFollower } from "@/types/actor/follower.model";
import { RELAY_FOLLOWERS, RELAY_FOLLOWINGS } from "@/graphql/admin"; import { RELAY_FOLLOWERS, RELAY_FOLLOWINGS } from "@/graphql/admin";
import { Paginate } from "@/types/paginate"; import { Paginate } from "@/types/paginate";
import { ActorType } from "@/types/enums";
@Component({ @Component({
apollo: { apollo: {
@ -62,7 +63,10 @@ export default class RelayMixin extends Vue {
relayFollowings: { relayFollowings: {
__typename: previousResult.relayFollowings.__typename, __typename: previousResult.relayFollowings.__typename,
total: previousResult.relayFollowings.total, total: previousResult.relayFollowings.total,
elements: [...previousResult.relayFollowings.elements, ...newFollowings], elements: [
...previousResult.relayFollowings.elements,
...newFollowings,
],
}, },
}; };
}, },
@ -87,7 +91,10 @@ export default class RelayMixin extends Vue {
relayFollowers: { relayFollowers: {
__typename: previousResult.relayFollowers.__typename, __typename: previousResult.relayFollowers.__typename,
total: previousResult.relayFollowers.total, total: previousResult.relayFollowers.total,
elements: [...previousResult.relayFollowers.elements, ...newFollowers], elements: [
...previousResult.relayFollowers.elements,
...newFollowers,
],
}, },
}; };
}, },
@ -100,7 +107,8 @@ export default class RelayMixin extends Vue {
static isInstance(actor: IActor): boolean { static isInstance(actor: IActor): boolean {
return ( return (
actor.type === ActorType.APPLICATION && actor.type === ActorType.APPLICATION &&
(actor.preferredUsername === "relay" || actor.preferredUsername === actor.domain) (actor.preferredUsername === "relay" ||
actor.preferredUsername === actor.domain)
); );
} }
} }

View File

@ -7,7 +7,10 @@ declare module "vue/types/vue" {
} }
} }
export function DateFnsPlugin(vue: typeof VueInstance, { locale }: { locale: string }): void { export function DateFnsPlugin(
vue: typeof VueInstance,
{ locale }: { locale: string }
): void {
import(`date-fns/locale/${locale}/index.js`).then((localeEntity) => { import(`date-fns/locale/${locale}/index.js`).then((localeEntity) => {
VueInstance.prototype.$dateFnsLocale = localeEntity; VueInstance.prototype.$dateFnsLocale = localeEntity;
}); });

View File

@ -23,7 +23,9 @@ if (process.env.NODE_ENV === "production") {
console.log("New content is available; please refresh."); console.log("New content is available; please refresh.");
}, },
offline() { offline() {
console.log("No internet connection found. App is running in offline mode."); console.log(
"No internet connection found. App is running in offline mode."
);
}, },
error(error) { error(error) {
console.error("Error during service worker registration:", error); console.error("Error during service worker registration:", error);

View File

@ -1,4 +1,5 @@
import { RouteConfig } from "vue-router"; import { RouteConfig } from "vue-router";
import { EsModuleComponent } from "vue/types/options";
export enum ActorRouteName { export enum ActorRouteName {
GROUP = "Group", GROUP = "Group",
@ -11,20 +12,23 @@ export const actorRoutes: RouteConfig[] = [
{ {
path: "/groups/create", path: "/groups/create",
name: ActorRouteName.CREATE_GROUP, name: ActorRouteName.CREATE_GROUP,
component: () => import(/* webpackChunkName: "CreateGroup" */ "@/views/Group/Create.vue"), component: (): Promise<EsModuleComponent> =>
import(/* webpackChunkName: "CreateGroup" */ "@/views/Group/Create.vue"),
meta: { requiredAuth: true }, meta: { requiredAuth: true },
}, },
{ {
path: "/@:preferredUsername", path: "/@:preferredUsername",
name: ActorRouteName.GROUP, name: ActorRouteName.GROUP,
component: () => import(/* webpackChunkName: "Group" */ "@/views/Group/Group.vue"), component: (): Promise<EsModuleComponent> =>
import(/* webpackChunkName: "Group" */ "@/views/Group/Group.vue"),
props: true, props: true,
meta: { requiredAuth: false }, meta: { requiredAuth: false },
}, },
{ {
path: "/groups/me", path: "/groups/me",
name: ActorRouteName.MY_GROUPS, name: ActorRouteName.MY_GROUPS,
component: () => import(/* webpackChunkName: "MyGroups" */ "@/views/Group/MyGroups.vue"), component: (): Promise<EsModuleComponent> =>
import(/* webpackChunkName: "MyGroups" */ "@/views/Group/MyGroups.vue"),
meta: { requiredAuth: true }, meta: { requiredAuth: true },
}, },
]; ];

View File

@ -1,4 +1,5 @@
import { RouteConfig } from "vue-router"; import { RouteConfig } from "vue-router";
import { EsModuleComponent } from "vue/types/options";
export enum DiscussionRouteName { export enum DiscussionRouteName {
DISCUSSION_LIST = "DISCUSSION_LIST", DISCUSSION_LIST = "DISCUSSION_LIST",
@ -10,24 +11,30 @@ export const discussionRoutes: RouteConfig[] = [
{ {
path: "/@:preferredUsername/discussions", path: "/@:preferredUsername/discussions",
name: DiscussionRouteName.DISCUSSION_LIST, name: DiscussionRouteName.DISCUSSION_LIST,
component: () => component: (): Promise<EsModuleComponent> =>
import(/* webpackChunkName: "DiscussionsList" */ "@/views/Discussions/DiscussionsList.vue"), import(
/* webpackChunkName: "DiscussionsList" */ "@/views/Discussions/DiscussionsList.vue"
),
props: true, props: true,
meta: { requiredAuth: true }, meta: { requiredAuth: true },
}, },
{ {
path: "/@:preferredUsername/discussions/new", path: "/@:preferredUsername/discussions/new",
name: DiscussionRouteName.CREATE_DISCUSSION, name: DiscussionRouteName.CREATE_DISCUSSION,
component: () => component: (): Promise<EsModuleComponent> =>
import(/* webpackChunkName: "CreateDiscussion" */ "@/views/Discussions/Create.vue"), import(
/* webpackChunkName: "CreateDiscussion" */ "@/views/Discussions/Create.vue"
),
props: true, props: true,
meta: { requiredAuth: true }, meta: { requiredAuth: true },
}, },
{ {
path: "/@:preferredUsername/c/:slug/:comment_id?", path: "/@:preferredUsername/c/:slug/:comment_id?",
name: DiscussionRouteName.DISCUSSION, name: DiscussionRouteName.DISCUSSION,
component: () => component: (): Promise<EsModuleComponent> =>
import(/* webpackChunkName: "Discussion" */ "@/views/Discussions/Discussion.vue"), import(
/* webpackChunkName: "Discussion" */ "@/views/Discussions/Discussion.vue"
),
props: true, props: true,
meta: { requiredAuth: true }, meta: { requiredAuth: true },
}, },

View File

@ -1,5 +1,6 @@
import { beforeRegisterGuard } from "@/router/guards/register-guard"; import { beforeRegisterGuard } from "@/router/guards/register-guard";
import { RouteConfig } from "vue-router"; import { RouteConfig } from "vue-router";
import { EsModuleComponent } from "vue/types/options";
export enum ErrorRouteName { export enum ErrorRouteName {
ERROR = "Error", ERROR = "Error",
@ -9,7 +10,8 @@ export const errorRoutes: RouteConfig[] = [
{ {
path: "/error", path: "/error",
name: ErrorRouteName.ERROR, name: ErrorRouteName.ERROR,
component: () => import(/* webpackChunkName: "Error" */ "../views/Error.vue"), component: (): Promise<EsModuleComponent> =>
import(/* webpackChunkName: "Error" */ "../views/Error.vue"),
beforeEnter: beforeRegisterGuard, beforeEnter: beforeRegisterGuard,
}, },
]; ];

View File

@ -1,10 +1,16 @@
import { RouteConfig, Route } from "vue-router"; import { RouteConfig, Route } from "vue-router";
import { EsModuleComponent } from "vue/types/options";
const participations = () => const participations = (): Promise<EsModuleComponent> =>
import(/* webpackChunkName: "participations" */ "@/views/Event/Participants.vue"); import(
const editEvent = () => import(/* webpackChunkName: "edit-event" */ "@/views/Event/Edit.vue"); /* webpackChunkName: "participations" */ "@/views/Event/Participants.vue"
const event = () => import(/* webpackChunkName: "event" */ "@/views/Event/Event.vue"); );
const myEvents = () => import(/* webpackChunkName: "my-events" */ "@/views/Event/MyEvents.vue"); const editEvent = (): Promise<EsModuleComponent> =>
import(/* webpackChunkName: "edit-event" */ "@/views/Event/Edit.vue");
const event = (): Promise<EsModuleComponent> =>
import(/* webpackChunkName: "event" */ "@/views/Event/Event.vue");
const myEvents = (): Promise<EsModuleComponent> =>
import(/* webpackChunkName: "my-events" */ "@/views/Event/MyEvents.vue");
export enum EventRouteName { export enum EventRouteName {
EVENT_LIST = "EventList", EVENT_LIST = "EventList",
@ -18,7 +24,6 @@ export enum EventRouteName {
EVENT_PARTICIPATE_WITHOUT_ACCOUNT = "EVENT_PARTICIPATE_WITHOUT_ACCOUNT", EVENT_PARTICIPATE_WITHOUT_ACCOUNT = "EVENT_PARTICIPATE_WITHOUT_ACCOUNT",
EVENT_PARTICIPATE_LOGGED_OUT = "EVENT_PARTICIPATE_LOGGED_OUT", EVENT_PARTICIPATE_LOGGED_OUT = "EVENT_PARTICIPATE_LOGGED_OUT",
EVENT_PARTICIPATE_CONFIRM = "EVENT_PARTICIPATE_CONFIRM", EVENT_PARTICIPATE_CONFIRM = "EVENT_PARTICIPATE_CONFIRM",
LOCATION = "Location",
TAG = "Tag", TAG = "Tag",
} }
@ -26,7 +31,8 @@ export const eventRoutes: RouteConfig[] = [
{ {
path: "/events/list/:location?", path: "/events/list/:location?",
name: EventRouteName.EVENT_LIST, name: EventRouteName.EVENT_LIST,
component: () => import(/* webpackChunkName: "EventList" */ "@/views/Event/EventList.vue"), component: (): Promise<EsModuleComponent> =>
import(/* webpackChunkName: "EventList" */ "@/views/Event/EventList.vue"),
meta: { requiredAuth: false }, meta: { requiredAuth: false },
}, },
{ {
@ -46,14 +52,19 @@ export const eventRoutes: RouteConfig[] = [
name: EventRouteName.EDIT_EVENT, name: EventRouteName.EDIT_EVENT,
component: editEvent, component: editEvent,
meta: { requiredAuth: true }, meta: { requiredAuth: true },
props: (route: Route) => ({ ...route.params, ...{ isUpdate: true } }), props: (route: Route): Record<string, unknown> => {
return { ...route.params, ...{ isUpdate: true } };
},
}, },
{ {
path: "/events/duplicate/:eventId", path: "/events/duplicate/:eventId",
name: EventRouteName.DUPLICATE_EVENT, name: EventRouteName.DUPLICATE_EVENT,
component: editEvent, component: editEvent,
meta: { requiredAuth: true }, meta: { requiredAuth: true },
props: (route: Route) => ({ ...route.params, ...{ isDuplicate: true } }), props: (route: Route): Record<string, unknown> => ({
...route.params,
...{ isDuplicate: true },
}),
}, },
{ {
path: "/events/:eventId/participations", path: "/events/:eventId/participations",
@ -62,12 +73,6 @@ export const eventRoutes: RouteConfig[] = [
meta: { requiredAuth: true }, meta: { requiredAuth: true },
props: true, props: true,
}, },
{
path: "/location/new",
name: EventRouteName.LOCATION,
component: () => import(/* webpackChunkName: "Location" */ "@/views/Location.vue"),
meta: { requiredAuth: true },
},
{ {
path: "/events/:uuid", path: "/events/:uuid",
name: EventRouteName.EVENT, name: EventRouteName.EVENT,
@ -78,31 +83,36 @@ export const eventRoutes: RouteConfig[] = [
{ {
path: "/events/:uuid/participate", path: "/events/:uuid/participate",
name: EventRouteName.EVENT_PARTICIPATE_LOGGED_OUT, name: EventRouteName.EVENT_PARTICIPATE_LOGGED_OUT,
component: () => import("../components/Participation/UnloggedParticipation.vue"), component: (): Promise<EsModuleComponent> =>
import("../components/Participation/UnloggedParticipation.vue"),
props: true, props: true,
}, },
{ {
path: "/events/:uuid/participate/with-account", path: "/events/:uuid/participate/with-account",
name: EventRouteName.EVENT_PARTICIPATE_WITH_ACCOUNT, name: EventRouteName.EVENT_PARTICIPATE_WITH_ACCOUNT,
component: () => import("../components/Participation/ParticipationWithAccount.vue"), component: (): Promise<EsModuleComponent> =>
import("../components/Participation/ParticipationWithAccount.vue"),
props: true, props: true,
}, },
{ {
path: "/events/:uuid/participate/without-account", path: "/events/:uuid/participate/without-account",
name: EventRouteName.EVENT_PARTICIPATE_WITHOUT_ACCOUNT, name: EventRouteName.EVENT_PARTICIPATE_WITHOUT_ACCOUNT,
component: () => import("../components/Participation/ParticipationWithoutAccount.vue"), component: (): Promise<EsModuleComponent> =>
import("../components/Participation/ParticipationWithoutAccount.vue"),
props: true, props: true,
}, },
{ {
path: "/participation/email/confirm/:token", path: "/participation/email/confirm/:token",
name: EventRouteName.EVENT_PARTICIPATE_CONFIRM, name: EventRouteName.EVENT_PARTICIPATE_CONFIRM,
component: () => import("../components/Participation/ConfirmParticipation.vue"), component: (): Promise<EsModuleComponent> =>
import("../components/Participation/ConfirmParticipation.vue"),
props: true, props: true,
}, },
{ {
path: "/tag/:tag", path: "/tag/:tag",
name: EventRouteName.TAG, name: EventRouteName.TAG,
component: () => import(/* webpackChunkName: "Search" */ "@/views/Search.vue"), component: (): Promise<EsModuleComponent> =>
import(/* webpackChunkName: "Search" */ "@/views/Search.vue"),
props: true, props: true,
meta: { requiredAuth: false }, meta: { requiredAuth: false },
}, },

View File

@ -1,4 +1,5 @@
import { RouteConfig, Route } from "vue-router"; import { RouteConfig, Route } from "vue-router";
import { EsModuleComponent } from "vue/types/options";
export enum GroupsRouteName { export enum GroupsRouteName {
TODO_LISTS = "TODO_LISTS", TODO_LISTS = "TODO_LISTS",
@ -18,29 +19,33 @@ export enum GroupsRouteName {
GROUP_JOIN = "GROUP_JOIN", GROUP_JOIN = "GROUP_JOIN",
} }
const resourceFolder = () => import("@/views/Resources/ResourceFolder.vue"); const resourceFolder = (): Promise<EsModuleComponent> =>
const groupEvents = () => import("@/views/Resources/ResourceFolder.vue");
const groupEvents = (): Promise<EsModuleComponent> =>
import(/* webpackChunkName: "groupEvents" */ "@/views/Event/GroupEvents.vue"); import(/* webpackChunkName: "groupEvents" */ "@/views/Event/GroupEvents.vue");
export const groupsRoutes: RouteConfig[] = [ export const groupsRoutes: RouteConfig[] = [
{ {
path: "/@:preferredUsername/todo-lists", path: "/@:preferredUsername/todo-lists",
name: GroupsRouteName.TODO_LISTS, name: GroupsRouteName.TODO_LISTS,
component: () => import("@/views/Todos/TodoLists.vue"), component: (): Promise<EsModuleComponent> =>
import("@/views/Todos/TodoLists.vue"),
props: true, props: true,
meta: { requiredAuth: true }, meta: { requiredAuth: true },
}, },
{ {
path: "/todo-lists/:id", path: "/todo-lists/:id",
name: GroupsRouteName.TODO_LIST, name: GroupsRouteName.TODO_LIST,
component: () => import("@/views/Todos/TodoList.vue"), component: (): Promise<EsModuleComponent> =>
import("@/views/Todos/TodoList.vue"),
props: true, props: true,
meta: { requiredAuth: true }, meta: { requiredAuth: true },
}, },
{ {
path: "/todo/:todoId", path: "/todo/:todoId",
name: GroupsRouteName.TODO, name: GroupsRouteName.TODO,
component: () => import("@/views/Todos/Todo.vue"), component: (): Promise<EsModuleComponent> =>
import("@/views/Todos/Todo.vue"),
props: true, props: true,
meta: { requiredAuth: true }, meta: { requiredAuth: true },
}, },
@ -60,7 +65,8 @@ export const groupsRoutes: RouteConfig[] = [
}, },
{ {
path: "/@:preferredUsername/settings", path: "/@:preferredUsername/settings",
component: () => import("@/views/Group/Settings.vue"), component: (): Promise<EsModuleComponent> =>
import("@/views/Group/Settings.vue"),
props: true, props: true,
meta: { requiredAuth: true }, meta: { requiredAuth: true },
redirect: { name: GroupsRouteName.GROUP_PUBLIC_SETTINGS }, redirect: { name: GroupsRouteName.GROUP_PUBLIC_SETTINGS },
@ -69,40 +75,49 @@ export const groupsRoutes: RouteConfig[] = [
{ {
path: "public", path: "public",
name: GroupsRouteName.GROUP_PUBLIC_SETTINGS, name: GroupsRouteName.GROUP_PUBLIC_SETTINGS,
component: () => import("../views/Group/GroupSettings.vue"), component: (): Promise<EsModuleComponent> =>
import("../views/Group/GroupSettings.vue"),
}, },
{ {
path: "members", path: "members",
name: GroupsRouteName.GROUP_MEMBERS_SETTINGS, name: GroupsRouteName.GROUP_MEMBERS_SETTINGS,
component: () => import("../views/Group/GroupMembers.vue"), component: (): Promise<EsModuleComponent> =>
import("../views/Group/GroupMembers.vue"),
props: true, props: true,
}, },
], ],
}, },
{ {
path: "/@:preferredUsername/p/new", path: "/@:preferredUsername/p/new",
component: () => import("@/views/Posts/Edit.vue"), component: (): Promise<EsModuleComponent> =>
import("@/views/Posts/Edit.vue"),
props: true, props: true,
name: GroupsRouteName.POST_CREATE, name: GroupsRouteName.POST_CREATE,
meta: { requiredAuth: true }, meta: { requiredAuth: true },
}, },
{ {
path: "/p/:slug/edit", path: "/p/:slug/edit",
component: () => import("@/views/Posts/Edit.vue"), component: (): Promise<EsModuleComponent> =>
props: (route: Route) => ({ ...route.params, ...{ isUpdate: true } }), import("@/views/Posts/Edit.vue"),
props: (route: Route): Record<string, unknown> => ({
...route.params,
...{ isUpdate: true },
}),
name: GroupsRouteName.POST_EDIT, name: GroupsRouteName.POST_EDIT,
meta: { requiredAuth: true }, meta: { requiredAuth: true },
}, },
{ {
path: "/p/:slug", path: "/p/:slug",
component: () => import("@/views/Posts/Post.vue"), component: (): Promise<EsModuleComponent> =>
import("@/views/Posts/Post.vue"),
props: true, props: true,
name: GroupsRouteName.POST, name: GroupsRouteName.POST,
meta: { requiredAuth: false }, meta: { requiredAuth: false },
}, },
{ {
path: "/@:preferredUsername/p", path: "/@:preferredUsername/p",
component: () => import("@/views/Posts/List.vue"), component: (): Promise<EsModuleComponent> =>
import("@/views/Posts/List.vue"),
props: true, props: true,
name: GroupsRouteName.POSTS, name: GroupsRouteName.POSTS,
meta: { requiredAuth: false }, meta: { requiredAuth: false },
@ -116,7 +131,8 @@ export const groupsRoutes: RouteConfig[] = [
}, },
{ {
path: "/@:preferredUsername/join", path: "/@:preferredUsername/join",
component: () => import("@/components/Group/JoinGroupWithAccount.vue"), component: (): Promise<EsModuleComponent> =>
import("@/components/Group/JoinGroupWithAccount.vue"),
props: true, props: true,
name: GroupsRouteName.GROUP_JOIN, name: GroupsRouteName.GROUP_JOIN,
meta: { requiredAuth: false }, meta: { requiredAuth: false },

View File

@ -1,7 +1,7 @@
import { NavigationGuard } from "vue-router"; import { NavigationGuard } from "vue-router";
import { UserRouteName } from "@/router/user"; import { UserRouteName } from "@/router/user";
import { LoginErrorCode } from "@/types/login-error-code.model";
import { AUTH_ACCESS_TOKEN } from "@/constants"; import { AUTH_ACCESS_TOKEN } from "@/constants";
import { LoginErrorCode } from "@/types/enums";
// eslint-disable-next-line import/prefer-default-export // eslint-disable-next-line import/prefer-default-export
export const authGuardIfNeeded: NavigationGuard = async (to, from, next) => { export const authGuardIfNeeded: NavigationGuard = async (to, from, next) => {

View File

@ -1,6 +1,6 @@
import { ErrorCode } from "@/types/enums";
import { NavigationGuard } from "vue-router"; import { NavigationGuard } from "vue-router";
import { CONFIG } from "../../graphql/config"; import { CONFIG } from "../../graphql/config";
import { ErrorCode } from "../../types/error-code.model";
import apolloProvider from "../../vue-apollo"; import apolloProvider from "../../vue-apollo";
// eslint-disable-next-line import/prefer-default-export // eslint-disable-next-line import/prefer-default-export

View File

@ -49,7 +49,8 @@ const router = new Router({
{ {
path: "/search", path: "/search",
name: RouteName.SEARCH, name: RouteName.SEARCH,
component: () => import(/* webpackChunkName: "search" */ "../views/Search.vue"), component: () =>
import(/* webpackChunkName: "search" */ "../views/Search.vue"),
props: true, props: true,
meta: { requiredAuth: false }, meta: { requiredAuth: false },
}, },
@ -62,7 +63,8 @@ const router = new Router({
{ {
path: "/about", path: "/about",
name: RouteName.ABOUT, name: RouteName.ABOUT,
component: () => import(/* webpackChunkName: "about" */ "@/views/About.vue"), component: () =>
import(/* webpackChunkName: "about" */ "@/views/About.vue"),
meta: { requiredAuth: false }, meta: { requiredAuth: false },
redirect: { name: RouteName.ABOUT_INSTANCE }, redirect: { name: RouteName.ABOUT_INSTANCE },
children: [ children: [
@ -70,30 +72,40 @@ const router = new Router({
path: "instance", path: "instance",
name: RouteName.ABOUT_INSTANCE, name: RouteName.ABOUT_INSTANCE,
component: () => component: () =>
import(/* webpackChunkName: "about" */ "@/views/About/AboutInstance.vue"), import(
/* webpackChunkName: "about" */ "@/views/About/AboutInstance.vue"
),
}, },
{ {
path: "/terms", path: "/terms",
name: RouteName.TERMS, name: RouteName.TERMS,
component: () => import(/* webpackChunkName: "cookies" */ "@/views/About/Terms.vue"), component: () =>
import(/* webpackChunkName: "cookies" */ "@/views/About/Terms.vue"),
meta: { requiredAuth: false }, meta: { requiredAuth: false },
}, },
{ {
path: "/privacy", path: "/privacy",
name: RouteName.PRIVACY, name: RouteName.PRIVACY,
component: () => import(/* webpackChunkName: "cookies" */ "@/views/About/Privacy.vue"), component: () =>
import(
/* webpackChunkName: "cookies" */ "@/views/About/Privacy.vue"
),
meta: { requiredAuth: false }, meta: { requiredAuth: false },
}, },
{ {
path: "/rules", path: "/rules",
name: RouteName.RULES, name: RouteName.RULES,
component: () => import(/* webpackChunkName: "cookies" */ "@/views/About/Rules.vue"), component: () =>
import(/* webpackChunkName: "cookies" */ "@/views/About/Rules.vue"),
meta: { requiredAuth: false }, meta: { requiredAuth: false },
}, },
{ {
path: "/glossary", path: "/glossary",
name: RouteName.GLOSSARY, name: RouteName.GLOSSARY,
component: () => import(/* webpackChunkName: "cookies" */ "@/views/About/Glossary.vue"), component: () =>
import(
/* webpackChunkName: "cookies" */ "@/views/About/Glossary.vue"
),
meta: { requiredAuth: false }, meta: { requiredAuth: false },
}, },
], ],
@ -101,20 +113,25 @@ const router = new Router({
{ {
path: "/interact", path: "/interact",
name: RouteName.INTERACT, name: RouteName.INTERACT,
component: () => import(/* webpackChunkName: "cookies" */ "@/views/Interact.vue"), component: () =>
import(/* webpackChunkName: "cookies" */ "@/views/Interact.vue"),
meta: { requiredAuth: false }, meta: { requiredAuth: false },
}, },
{ {
path: "/auth/:provider/callback", path: "/auth/:provider/callback",
name: "auth-callback", name: "auth-callback",
component: () => component: () =>
import(/* webpackChunkName: "ProviderValidation" */ "@/views/User/ProviderValidation.vue"), import(
/* webpackChunkName: "ProviderValidation" */ "@/views/User/ProviderValidation.vue"
),
}, },
{ {
path: "/welcome/:step?", path: "/welcome/:step?",
name: RouteName.WELCOME_SCREEN, name: RouteName.WELCOME_SCREEN,
component: () => component: () =>
import(/* webpackChunkName: "WelcomeScreen" */ "@/views/User/SettingsOnboard.vue"), import(
/* webpackChunkName: "WelcomeScreen" */ "@/views/User/SettingsOnboard.vue"
),
meta: { requiredAuth: true }, meta: { requiredAuth: true },
props: (route) => { props: (route) => {
const step = Number.parseInt(route.params.step, 10); const step = Number.parseInt(route.params.step, 10);
@ -127,7 +144,8 @@ const router = new Router({
{ {
path: "/404", path: "/404",
name: RouteName.PAGE_NOT_FOUND, name: RouteName.PAGE_NOT_FOUND,
component: () => import(/* webpackChunkName: "search" */ "../views/PageNotFound.vue"), component: () =>
import(/* webpackChunkName: "search" */ "../views/PageNotFound.vue"),
meta: { requiredAuth: false }, meta: { requiredAuth: false },
}, },
{ {

View File

@ -1,4 +1,5 @@
import { RouteConfig } from "vue-router"; import { Route, RouteConfig } from "vue-router";
import { EsModuleComponent } from "vue/types/options";
export enum SettingsRouteName { export enum SettingsRouteName {
SETTINGS = "SETTINGS", SETTINGS = "SETTINGS",
@ -30,7 +31,8 @@ export enum SettingsRouteName {
export const settingsRoutes: RouteConfig[] = [ export const settingsRoutes: RouteConfig[] = [
{ {
path: "/settings", path: "/settings",
component: () => import(/* webpackChunkName: "Settings" */ "@/views/Settings.vue"), component: (): Promise<EsModuleComponent> =>
import(/* webpackChunkName: "Settings" */ "@/views/Settings.vue"),
props: true, props: true,
meta: { requiredAuth: true }, meta: { requiredAuth: true },
redirect: { name: SettingsRouteName.ACCOUNT_SETTINGS }, redirect: { name: SettingsRouteName.ACCOUNT_SETTINGS },
@ -45,24 +47,30 @@ export const settingsRoutes: RouteConfig[] = [
{ {
path: "account/general", path: "account/general",
name: SettingsRouteName.ACCOUNT_SETTINGS_GENERAL, name: SettingsRouteName.ACCOUNT_SETTINGS_GENERAL,
component: () => component: (): Promise<EsModuleComponent> =>
import(/* webpackChunkName: "AccountSettings" */ "@/views/Settings/AccountSettings.vue"), import(
/* webpackChunkName: "AccountSettings" */ "@/views/Settings/AccountSettings.vue"
),
props: true, props: true,
meta: { requiredAuth: true }, meta: { requiredAuth: true },
}, },
{ {
path: "preferences", path: "preferences",
name: SettingsRouteName.PREFERENCES, name: SettingsRouteName.PREFERENCES,
component: () => component: (): Promise<EsModuleComponent> =>
import(/* webpackChunkName: "Preferences" */ "@/views/Settings/Preferences.vue"), import(
/* webpackChunkName: "Preferences" */ "@/views/Settings/Preferences.vue"
),
props: true, props: true,
meta: { requiredAuth: true }, meta: { requiredAuth: true },
}, },
{ {
path: "notifications", path: "notifications",
name: SettingsRouteName.NOTIFICATIONS, name: SettingsRouteName.NOTIFICATIONS,
component: () => component: (): Promise<EsModuleComponent> =>
import(/* webpackChunkName: "Notifications" */ "@/views/Settings/Notifications.vue"), import(
/* webpackChunkName: "Notifications" */ "@/views/Settings/Notifications.vue"
),
props: true, props: true,
meta: { requiredAuth: true }, meta: { requiredAuth: true },
}, },
@ -75,61 +83,77 @@ export const settingsRoutes: RouteConfig[] = [
{ {
path: "admin/dashboard", path: "admin/dashboard",
name: SettingsRouteName.ADMIN_DASHBOARD, name: SettingsRouteName.ADMIN_DASHBOARD,
component: () => import(/* webpackChunkName: "Dashboard" */ "@/views/Admin/Dashboard.vue"), component: (): Promise<EsModuleComponent> =>
import(
/* webpackChunkName: "Dashboard" */ "@/views/Admin/Dashboard.vue"
),
meta: { requiredAuth: true }, meta: { requiredAuth: true },
}, },
{ {
path: "admin/settings", path: "admin/settings",
name: SettingsRouteName.ADMIN_SETTINGS, name: SettingsRouteName.ADMIN_SETTINGS,
component: () => component: (): Promise<EsModuleComponent> =>
import(/* webpackChunkName: "AdminSettings" */ "@/views/Admin/Settings.vue"), import(
/* webpackChunkName: "AdminSettings" */ "@/views/Admin/Settings.vue"
),
props: true, props: true,
meta: { requiredAuth: true }, meta: { requiredAuth: true },
}, },
{ {
path: "admin/users", path: "admin/users",
name: SettingsRouteName.USERS, name: SettingsRouteName.USERS,
component: () => import(/* webpackChunkName: "Users" */ "@/views/Admin/Users.vue"), component: (): Promise<EsModuleComponent> =>
import(/* webpackChunkName: "Users" */ "@/views/Admin/Users.vue"),
props: true, props: true,
meta: { requiredAuth: true }, meta: { requiredAuth: true },
}, },
{ {
path: "admin/users/:id", path: "admin/users/:id",
name: SettingsRouteName.ADMIN_USER_PROFILE, name: SettingsRouteName.ADMIN_USER_PROFILE,
component: () => component: (): Promise<EsModuleComponent> =>
import(/* webpackChunkName: "AdminUserProfile" */ "@/views/Admin/AdminUserProfile.vue"), import(
/* webpackChunkName: "AdminUserProfile" */ "@/views/Admin/AdminUserProfile.vue"
),
props: true, props: true,
meta: { requiredAuth: true }, meta: { requiredAuth: true },
}, },
{ {
path: "admin/profiles", path: "admin/profiles",
name: SettingsRouteName.PROFILES, name: SettingsRouteName.PROFILES,
component: () => component: (): Promise<EsModuleComponent> =>
import(/* webpackChunkName: "AdminProfiles" */ "@/views/Admin/Profiles.vue"), import(
/* webpackChunkName: "AdminProfiles" */ "@/views/Admin/Profiles.vue"
),
props: true, props: true,
meta: { requiredAuth: true }, meta: { requiredAuth: true },
}, },
{ {
path: "admin/profiles/:id", path: "admin/profiles/:id",
name: SettingsRouteName.ADMIN_PROFILE, name: SettingsRouteName.ADMIN_PROFILE,
component: () => component: (): Promise<EsModuleComponent> =>
import(/* webpackChunkName: "AdminProfile" */ "@/views/Admin/AdminProfile.vue"), import(
/* webpackChunkName: "AdminProfile" */ "@/views/Admin/AdminProfile.vue"
),
props: true, props: true,
meta: { requiredAuth: true }, meta: { requiredAuth: true },
}, },
{ {
path: "admin/groups", path: "admin/groups",
name: SettingsRouteName.ADMIN_GROUPS, name: SettingsRouteName.ADMIN_GROUPS,
component: () => component: (): Promise<EsModuleComponent> =>
import(/* webpackChunkName: "GroupProfiles" */ "@/views/Admin/GroupProfiles.vue"), import(
/* webpackChunkName: "GroupProfiles" */ "@/views/Admin/GroupProfiles.vue"
),
props: true, props: true,
meta: { requiredAuth: true }, meta: { requiredAuth: true },
}, },
{ {
path: "admin/groups/:id", path: "admin/groups/:id",
name: SettingsRouteName.ADMIN_GROUP_PROFILE, name: SettingsRouteName.ADMIN_GROUP_PROFILE,
component: () => component: (): Promise<EsModuleComponent> =>
import(/* webpackChunkName: "AdminGroupProfile" */ "@/views/Admin/AdminGroupProfile.vue"), import(
/* webpackChunkName: "AdminGroupProfile" */ "@/views/Admin/AdminGroupProfile.vue"
),
props: true, props: true,
meta: { requiredAuth: true }, meta: { requiredAuth: true },
}, },
@ -137,21 +161,26 @@ export const settingsRoutes: RouteConfig[] = [
path: "admin/relays", path: "admin/relays",
name: SettingsRouteName.RELAYS, name: SettingsRouteName.RELAYS,
redirect: { name: SettingsRouteName.RELAY_FOLLOWINGS }, redirect: { name: SettingsRouteName.RELAY_FOLLOWINGS },
component: () => import(/* webpackChunkName: "Follows" */ "@/views/Admin/Follows.vue"), component: (): Promise<EsModuleComponent> =>
import(/* webpackChunkName: "Follows" */ "@/views/Admin/Follows.vue"),
meta: { requiredAuth: true }, meta: { requiredAuth: true },
children: [ children: [
{ {
path: "followings", path: "followings",
name: SettingsRouteName.RELAY_FOLLOWINGS, name: SettingsRouteName.RELAY_FOLLOWINGS,
component: () => component: (): Promise<EsModuleComponent> =>
import(/* webpackChunkName: "Followings" */ "@/components/Admin/Followings.vue"), import(
/* webpackChunkName: "Followings" */ "@/components/Admin/Followings.vue"
),
meta: { requiredAuth: true }, meta: { requiredAuth: true },
}, },
{ {
path: "followers", path: "followers",
name: SettingsRouteName.RELAY_FOLLOWERS, name: SettingsRouteName.RELAY_FOLLOWERS,
component: () => component: (): Promise<EsModuleComponent> =>
import(/* webpackChunkName: "Followers" */ "@/components/Admin/Followers.vue"), import(
/* webpackChunkName: "Followers" */ "@/components/Admin/Followers.vue"
),
meta: { requiredAuth: true }, meta: { requiredAuth: true },
}, },
], ],
@ -166,23 +195,30 @@ export const settingsRoutes: RouteConfig[] = [
{ {
path: "/moderation/reports/:filter?", path: "/moderation/reports/:filter?",
name: SettingsRouteName.REPORTS, name: SettingsRouteName.REPORTS,
component: () => component: (): Promise<EsModuleComponent> =>
import(/* webpackChunkName: "ReportList" */ "@/views/Moderation/ReportList.vue"), import(
/* webpackChunkName: "ReportList" */ "@/views/Moderation/ReportList.vue"
),
props: true, props: true,
meta: { requiredAuth: true }, meta: { requiredAuth: true },
}, },
{ {
path: "/moderation/report/:reportId", path: "/moderation/report/:reportId",
name: SettingsRouteName.REPORT, name: SettingsRouteName.REPORT,
component: () => import(/* webpackChunkName: "Report" */ "@/views/Moderation/Report.vue"), component: (): Promise<EsModuleComponent> =>
import(
/* webpackChunkName: "Report" */ "@/views/Moderation/Report.vue"
),
props: true, props: true,
meta: { requiredAuth: true }, meta: { requiredAuth: true },
}, },
{ {
path: "/moderation/logs", path: "/moderation/logs",
name: SettingsRouteName.REPORT_LOGS, name: SettingsRouteName.REPORT_LOGS,
component: () => component: (): Promise<EsModuleComponent> =>
import(/* webpackChunkName: "ModerationLogs" */ "@/views/Moderation/Logs.vue"), import(
/* webpackChunkName: "ModerationLogs" */ "@/views/Moderation/Logs.vue"
),
props: true, props: true,
meta: { requiredAuth: true }, meta: { requiredAuth: true },
}, },
@ -195,21 +231,27 @@ export const settingsRoutes: RouteConfig[] = [
{ {
path: "/identity/create", path: "/identity/create",
name: SettingsRouteName.CREATE_IDENTITY, name: SettingsRouteName.CREATE_IDENTITY,
component: () => component: (): Promise<EsModuleComponent> =>
import( import(
/* webpackChunkName: "EditIdentity" */ "@/views/Account/children/EditIdentity.vue" /* webpackChunkName: "EditIdentity" */ "@/views/Account/children/EditIdentity.vue"
), ),
props: (route) => ({ identityName: route.params.identityName, isUpdate: false }), props: (route: Route): Record<string, unknown> => ({
identityName: route.params.identityName,
isUpdate: false,
}),
meta: { requiredAuth: true }, meta: { requiredAuth: true },
}, },
{ {
path: "/identity/update/:identityName?", path: "/identity/update/:identityName?",
name: SettingsRouteName.UPDATE_IDENTITY, name: SettingsRouteName.UPDATE_IDENTITY,
component: () => component: (): Promise<EsModuleComponent> =>
import( import(
/* webpackChunkName: "EditIdentity" */ "@/views/Account/children/EditIdentity.vue" /* webpackChunkName: "EditIdentity" */ "@/views/Account/children/EditIdentity.vue"
), ),
props: (route) => ({ identityName: route.params.identityName, isUpdate: true }), props: (route: Route): Record<string, unknown> => ({
identityName: route.params.identityName,
isUpdate: true,
}),
meta: { requiredAuth: true }, meta: { requiredAuth: true },
}, },
], ],

View File

@ -1,5 +1,6 @@
import { beforeRegisterGuard } from "@/router/guards/register-guard"; import { beforeRegisterGuard } from "@/router/guards/register-guard";
import { RouteConfig } from "vue-router"; import { Route, RouteConfig } from "vue-router";
import { EsModuleComponent } from "vue/types/options";
export enum UserRouteName { export enum UserRouteName {
REGISTER = "Register", REGISTER = "Register",
@ -16,7 +17,10 @@ export const userRoutes: RouteConfig[] = [
{ {
path: "/register/user", path: "/register/user",
name: UserRouteName.REGISTER, name: UserRouteName.REGISTER,
component: () => import(/* webpackChunkName: "RegisterUser" */ "@/views/User/Register.vue"), component: (): Promise<EsModuleComponent> =>
import(
/* webpackChunkName: "RegisterUser" */ "@/views/User/Register.vue"
),
props: true, props: true,
meta: { requiredAuth: false }, meta: { requiredAuth: false },
beforeEnter: beforeRegisterGuard, beforeEnter: beforeRegisterGuard,
@ -24,10 +28,12 @@ export const userRoutes: RouteConfig[] = [
{ {
path: "/register/profile", path: "/register/profile",
name: UserRouteName.REGISTER_PROFILE, name: UserRouteName.REGISTER_PROFILE,
component: () => component: (): Promise<EsModuleComponent> =>
import(/* webpackChunkName: "RegisterProfile" */ "@/views/Account/Register.vue"), import(
/* webpackChunkName: "RegisterProfile" */ "@/views/Account/Register.vue"
),
// We can only pass string values through params, therefore // We can only pass string values through params, therefore
props: (route) => ({ props: (route: Route): Record<string, unknown> => ({
email: route.params.email, email: route.params.email,
userAlreadyActivated: route.params.userAlreadyActivated === "true", userAlreadyActivated: route.params.userAlreadyActivated === "true",
}), }),
@ -36,46 +42,56 @@ export const userRoutes: RouteConfig[] = [
{ {
path: "/resend-instructions", path: "/resend-instructions",
name: UserRouteName.RESEND_CONFIRMATION, name: UserRouteName.RESEND_CONFIRMATION,
component: () => component: (): Promise<EsModuleComponent> =>
import(/* webpackChunkName: "ResendConfirmation" */ "@/views/User/ResendConfirmation.vue"), import(
/* webpackChunkName: "ResendConfirmation" */ "@/views/User/ResendConfirmation.vue"
),
props: true, props: true,
meta: { requiresAuth: false }, meta: { requiresAuth: false },
}, },
{ {
path: "/password-reset/send", path: "/password-reset/send",
name: UserRouteName.SEND_PASSWORD_RESET, name: UserRouteName.SEND_PASSWORD_RESET,
component: () => component: (): Promise<EsModuleComponent> =>
import(/* webpackChunkName: "SendPasswordReset" */ "@/views/User/SendPasswordReset.vue"), import(
/* webpackChunkName: "SendPasswordReset" */ "@/views/User/SendPasswordReset.vue"
),
props: true, props: true,
meta: { requiresAuth: false }, meta: { requiresAuth: false },
}, },
{ {
path: "/password-reset/:token", path: "/password-reset/:token",
name: UserRouteName.PASSWORD_RESET, name: UserRouteName.PASSWORD_RESET,
component: () => component: (): Promise<EsModuleComponent> =>
import(/* webpackChunkName: "PasswordReset" */ "@/views/User/PasswordReset.vue"), import(
/* webpackChunkName: "PasswordReset" */ "@/views/User/PasswordReset.vue"
),
meta: { requiresAuth: false }, meta: { requiresAuth: false },
props: true, props: true,
}, },
{ {
path: "/validate/email/:token", path: "/validate/email/:token",
name: UserRouteName.EMAIL_VALIDATE, name: UserRouteName.EMAIL_VALIDATE,
component: () => component: (): Promise<EsModuleComponent> =>
import(/* webpackChunkName: "EmailValidate" */ "@/views/User/EmailValidate.vue"), import(
/* webpackChunkName: "EmailValidate" */ "@/views/User/EmailValidate.vue"
),
props: true, props: true,
meta: { requiresAuth: false }, meta: { requiresAuth: false },
}, },
{ {
path: "/validate/:token", path: "/validate/:token",
name: UserRouteName.VALIDATE, name: UserRouteName.VALIDATE,
component: () => import(/* webpackChunkName: "Validate" */ "@/views/User/Validate.vue"), component: (): Promise<EsModuleComponent> =>
import(/* webpackChunkName: "Validate" */ "@/views/User/Validate.vue"),
props: true, props: true,
meta: { requiresAuth: false }, meta: { requiresAuth: false },
}, },
{ {
path: "/login", path: "/login",
name: UserRouteName.LOGIN, name: UserRouteName.LOGIN,
component: () => import(/* webpackChunkName: "Login" */ "@/views/User/Login.vue"), component: (): Promise<EsModuleComponent> =>
import(/* webpackChunkName: "Login" */ "@/views/User/Login.vue"),
props: true, props: true,
meta: { requiredAuth: false }, meta: { requiredAuth: false },
}, },

View File

@ -16,20 +16,25 @@ class AnonymousParticipationNotFoundError extends Error {
} }
} }
/** function jsonToMap(jsonStr: string): Map<string, IAnonymousParticipation> {
* Fetch existing anonymous participations saved inside this browser return new Map(JSON.parse(jsonStr));
*/
function getLocalAnonymousParticipations(): Map<string, IAnonymousParticipation> {
return jsonToMap(
localStorage.getItem(ANONYMOUS_PARTICIPATIONS_LOCALSTORAGE_KEY) || mapToJson(new Map())
);
} }
function mapToJson(map: Map<any, any>): string { function mapToJson(map: Map<any, any>): string {
return JSON.stringify([...map]); return JSON.stringify([...map]);
} }
function jsonToMap(jsonStr: string): Map<string, IAnonymousParticipation> {
return new Map(JSON.parse(jsonStr)); /**
* Fetch existing anonymous participations saved inside this browser
*/
function getLocalAnonymousParticipations(): Map<
string,
IAnonymousParticipation
> {
return jsonToMap(
localStorage.getItem(ANONYMOUS_PARTICIPATIONS_LOCALSTORAGE_KEY) ||
mapToJson(new Map())
);
} }
/** /**
@ -39,6 +44,7 @@ function jsonToMap(jsonStr: string): Map<string, IAnonymousParticipation> {
function purgeOldParticipations( function purgeOldParticipations(
participations: Map<string, IAnonymousParticipation> participations: Map<string, IAnonymousParticipation>
): Map<string, IAnonymousParticipation> { ): Map<string, IAnonymousParticipation> {
// eslint-disable-next-line no-restricted-syntax
for (const [hashedUUID, { expiration }] of participations) { for (const [hashedUUID, { expiration }] of participations) {
if (expiration < new Date()) { if (expiration < new Date()) {
participations.delete(hashedUUID); participations.delete(hashedUUID);
@ -56,9 +62,14 @@ function insertLocalAnonymousParticipation(
hashedUUID: string, hashedUUID: string,
participation: IAnonymousParticipation participation: IAnonymousParticipation
) { ) {
const participations = purgeOldParticipations(getLocalAnonymousParticipations()); const participations = purgeOldParticipations(
getLocalAnonymousParticipations()
);
participations.set(hashedUUID, participation); participations.set(hashedUUID, participation);
localStorage.setItem(ANONYMOUS_PARTICIPATIONS_LOCALSTORAGE_KEY, mapToJson(participations)); localStorage.setItem(
ANONYMOUS_PARTICIPATIONS_LOCALSTORAGE_KEY,
mapToJson(participations)
);
} }
function buildExpiration(event: IEvent): Date { function buildExpiration(event: IEvent): Date {
@ -67,59 +78,6 @@ function buildExpiration(event: IEvent): Date {
return expiration; return expiration;
} }
async function addLocalUnconfirmedAnonymousParticipation(event: IEvent, cancellationToken: string) {
/**
* We hash the event UUID so that we can't know which events an anonymous user goes by looking up it's localstorage
*/
const hashedUUID = await digestMessage(event.uuid);
/**
* We round expiration to first day of next 3 months so that it's difficult to find event from date
*/
const expiration = buildExpiration(event);
insertLocalAnonymousParticipation(hashedUUID, {
token: cancellationToken,
expiration,
confirmed: false,
});
}
async function confirmLocalAnonymousParticipation(uuid: string) {
const participations = purgeOldParticipations(getLocalAnonymousParticipations());
const hashedUUID = await digestMessage(uuid);
const participation = participations.get(hashedUUID);
if (participation) {
participation.confirmed = true;
participations.set(hashedUUID, participation);
localStorage.setItem(ANONYMOUS_PARTICIPATIONS_LOCALSTORAGE_KEY, mapToJson(participations));
}
}
async function isParticipatingInThisEvent(eventUUID: string): Promise<boolean> {
const participation = await getParticipation(eventUUID);
return participation !== undefined && participation.confirmed;
}
async function getParticipation(eventUUID: string): Promise<IAnonymousParticipation> {
const hashedUUID = await digestMessage(eventUUID);
const participation = purgeOldParticipations(getLocalAnonymousParticipations()).get(hashedUUID);
if (participation) {
return participation;
}
throw new AnonymousParticipationNotFoundError("Participation not found");
}
async function getLeaveTokenForParticipation(eventUUID: string): Promise<string> {
return (await getParticipation(eventUUID)).token;
}
async function removeAnonymousParticipation(eventUUID: string): Promise<void> {
const hashedUUID = await digestMessage(eventUUID);
const participations = purgeOldParticipations(getLocalAnonymousParticipations());
participations.delete(hashedUUID);
localStorage.setItem(ANONYMOUS_PARTICIPATIONS_LOCALSTORAGE_KEY, mapToJson(participations));
}
async function digestMessage(message: string): Promise<string> { async function digestMessage(message: string): Promise<string> {
const encoder = new TextEncoder(); const encoder = new TextEncoder();
const data = encoder.encode(message); const data = encoder.encode(message);
@ -128,6 +86,80 @@ async function digestMessage(message: string): Promise<string> {
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join(""); return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
} }
async function addLocalUnconfirmedAnonymousParticipation(
event: IEvent,
cancellationToken: string
): Promise<void> {
/**
* We hash the event UUID so that we can't know which events
* an anonymous user goes by looking up it's localstorage
*/
const hashedUUID = await digestMessage(event.uuid);
/**
* We round expiration to first day of next 3 months so that
* it's difficult to find event from date
*/
const expiration = buildExpiration(event);
insertLocalAnonymousParticipation(hashedUUID, {
token: cancellationToken,
expiration,
confirmed: false,
});
}
async function confirmLocalAnonymousParticipation(uuid: string): Promise<void> {
const participations = purgeOldParticipations(
getLocalAnonymousParticipations()
);
const hashedUUID = await digestMessage(uuid);
const participation = participations.get(hashedUUID);
if (participation) {
participation.confirmed = true;
participations.set(hashedUUID, participation);
localStorage.setItem(
ANONYMOUS_PARTICIPATIONS_LOCALSTORAGE_KEY,
mapToJson(participations)
);
}
}
async function getParticipation(
eventUUID: string
): Promise<IAnonymousParticipation> {
const hashedUUID = await digestMessage(eventUUID);
const participation = purgeOldParticipations(
getLocalAnonymousParticipations()
).get(hashedUUID);
if (participation) {
return participation;
}
throw new AnonymousParticipationNotFoundError("Participation not found");
}
async function isParticipatingInThisEvent(eventUUID: string): Promise<boolean> {
const participation = await getParticipation(eventUUID);
return participation !== undefined && participation.confirmed;
}
async function getLeaveTokenForParticipation(
eventUUID: string
): Promise<string> {
return (await getParticipation(eventUUID)).token;
}
async function removeAnonymousParticipation(eventUUID: string): Promise<void> {
const hashedUUID = await digestMessage(eventUUID);
const participations = purgeOldParticipations(
getLocalAnonymousParticipations()
);
participations.delete(hashedUUID);
localStorage.setItem(
ANONYMOUS_PARTICIPATIONS_LOCALSTORAGE_KEY,
mapToJson(participations)
);
}
export { export {
addLocalUnconfirmedAnonymousParticipation, addLocalUnconfirmedAnonymousParticipation,
confirmLocalAnonymousParticipation, confirmLocalAnonymousParticipation,

View File

@ -1,12 +1,5 @@
import { IMedia } from "@/types/media.model"; import type { IMedia } from "@/types/media.model";
import { ActorType } from "../enums";
export enum ActorType {
PERSON = "PERSON",
APPLICATION = "APPLICATION",
GROUP = "GROUP",
ORGANISATION = "ORGANISATION",
SERVICE = "SERVICE",
}
export interface IActor { export interface IActor {
id?: string; id?: string;
@ -59,7 +52,9 @@ export class Actor implements IActor {
} }
public displayName(): string { public displayName(): string {
return this.name != null && this.name !== "" ? this.name : this.usernameWithDomain(); return this.name != null && this.name !== ""
? this.name
: this.usernameWithDomain();
} }
} }

View File

@ -1,4 +1,4 @@
import { IActor } from "@/types/actor/actor.model"; import type { IActor } from "@/types/actor/actor.model";
export interface IFollower { export interface IFollower {
id?: string; id?: string;

View File

@ -1,38 +1,15 @@
import { Actor, ActorType, IActor } from "./actor.model"; import type { IActor } from "./actor.model";
import { Paginate } from "../paginate"; import { Actor } from "./actor.model";
import { IResource } from "../resource"; import type { Paginate } from "../paginate";
import { ITodoList } from "../todos"; import type { IResource } from "../resource";
import { IEvent } from "../event.model"; import type { IEvent } from "../event.model";
import { IDiscussion } from "../discussions"; import type { IDiscussion } from "../discussions";
import { IPerson } from "./person.model"; import type { IPost } from "../post.model";
import { IPost } from "../post.model"; import type { IAddress } from "../address.model";
import { IAddress, Address } from "../address.model"; import { Address } from "../address.model";
import { ActorType, Openness } from "../enums";
export enum MemberRole { import type { IMember } from "./member.model";
NOT_APPROVED = "NOT_APPROVED", import type { ITodoList } from "../todolist";
INVITED = "INVITED",
MEMBER = "MEMBER",
MODERATOR = "MODERATOR",
ADMINISTRATOR = "ADMINISTRATOR",
CREATOR = "CREATOR",
REJECTED = "REJECTED",
}
export enum Openness {
INVITE_ONLY = "INVITE_ONLY",
MODERATED = "MODERATED",
OPEN = "OPEN",
}
export interface IMember {
id?: string;
role: MemberRole;
parent: IGroup;
actor: IActor;
invitedBy?: IPerson;
insertedAt: string;
updatedAt: string;
}
export interface IGroup extends IActor { export interface IGroup extends IActor {
members: Paginate<IMember>; members: Paginate<IMember>;

Some files were not shown because too many files have changed in this diff Show More