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,
opts: [
scheme: true,
extra: true,
# TODO: Set to :no_scheme when it works properly
validate_tld: true,
class: false,
strip_prefix: false,
new_window: true,
rel: "noopener noreferrer ugc"
]
config :mobilizon, Mobilizon.Service.Formatter,
class: false,
rel: "noopener noreferrer ugc",
new_window: true,
truncate: false,
strip_prefix: false,
extra: true,
validate_tld: :no_scheme
config :tesla, adapter: Tesla.Adapter.Hackney

View file

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

View file

@ -38,8 +38,11 @@ module.exports = {
"error",
{
ignoreStrings: true,
ignoreHTMLTextContents: true,
ignoreTemplateLiterals: true,
ignoreComments: true,
template: 170,
code: 100,
code: 80,
},
],
"prettier/prettier": "error",
@ -48,13 +51,18 @@ module.exports = {
"import/prefer-default-export": "off",
"import/extensions": "off",
"import/no-unresolved": "off",
"no-shadow": "off",
"@typescript-eslint/no-shadow": ["error"],
},
ignorePatterns: ["src/typings/*.d.ts", "vue.config.js"],
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: {
mocha: true,
},

View file

@ -24,7 +24,9 @@ fetch(`http://localhost:4000/api`, {
.then((result) => result.json())
.then((result) => {
// 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;
fs.writeFile("./fragmentTypes.json", JSON.stringify(result.data), (err) => {
if (err) {

View file

@ -63,13 +63,13 @@
"@types/vuedraggable": "^2.23.0",
"@typescript-eslint/eslint-plugin": "^4.0.1",
"@typescript-eslint/parser": "^4.0.1",
"@vue/cli-plugin-babel": "~4.5.8",
"@vue/cli-plugin-e2e-cypress": "~4.5.8",
"@vue/cli-plugin-eslint": "~4.5.8",
"@vue/cli-plugin-pwa": "~4.5.8",
"@vue/cli-plugin-router": "~4.5.8",
"@vue/cli-plugin-typescript": "~4.5.8",
"@vue/cli-service": "~4.5.8",
"@vue/cli-plugin-babel": "~4.5.9",
"@vue/cli-plugin-e2e-cypress": "~4.5.9",
"@vue/cli-plugin-eslint": "~4.5.9",
"@vue/cli-plugin-pwa": "~4.5.9",
"@vue/cli-plugin-router": "~4.5.9",
"@vue/cli-plugin-typescript": "~4.5.9",
"@vue/cli-service": "~4.5.9",
"@vue/eslint-config-airbnb": "^5.0.2",
"@vue/eslint-config-prettier": "^6.0.0",
"@vue/eslint-config-typescript": "^7.0.0",
@ -79,11 +79,11 @@
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-prettier": "^3.1.3",
"eslint-plugin-vue": "^7.0.0",
"prettier": "2.1.2",
"prettier-eslint": "^11.0.0",
"prettier": "2.2.1",
"prettier-eslint": "^12.0.0",
"sass": "^1.29.0",
"sass-loader": "^10.0.1",
"typescript": "~4.0.2",
"typescript": "~4.1.2",
"vue-cli-plugin-svg": "~0.1.3",
"vue-i18n-extract": "^1.0.2",
"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">
import { Component, Vue } from "vue-property-decorator";
import NavBar from "./components/NavBar.vue";
import { 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 {
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 Logo from "./components/Logo.vue";
import { initializeCurrentActor } from "./utils/auth";

View file

@ -1,8 +1,11 @@
import { ICurrentUserRole } from "@/types/enums";
import { ApolloCache } from "apollo-cache";
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({
data: {
currentUser: {
@ -53,7 +56,12 @@ export default function buildCurrentUserResolver(cache: ApolloCache<NormalizedCa
preferredUsername,
avatar,
name,
}: { id: string; preferredUsername: string; avatar: string; name: string },
}: {
id: string;
preferredUsername: string;
avatar: string;
name: string;
},
{ cache: localCache }: { cache: ApolloCache<NormalizedCacheObject> }
) => {
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 { REFRESH_TOKEN } from "@/graphql/auth";
import { saveTokenData } from "@/utils/auth";
@ -11,7 +14,11 @@ export const fragmentMatcher = new IntrospectionFragmentMatcher({
{
kind: "UNION",
name: "SearchResult",
possibleTypes: [{ name: "Event" }, { name: "Person" }, { name: "Group" }],
possibleTypes: [
{ name: "Event" },
{ name: "Person" },
{ name: "Group" },
],
},
{
kind: "INTERFACE",

View file

@ -13,7 +13,12 @@
<template slot-scope="props">
<div class="media">
<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" />
</div>
<div class="media-content">
@ -21,7 +26,9 @@
{{ props.option.name }}
<br />
<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 v-else>
{{ `@${props.option.preferredUsername}` }}
@ -53,7 +60,9 @@ export default class ActorAutoComplete extends Vue {
selected: IPerson | null = this.defaultSelected;
name: string = this.defaultSelected ? this.defaultSelected.preferredUsername : "";
name: string = this.defaultSelected
? this.defaultSelected.preferredUsername
: "";
page = 1;

View file

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

View file

@ -7,7 +7,10 @@
<ul class="identities">
<li v-for="identity in identities" :key="identity.id">
<router-link
:to="{ name: 'UpdateIdentity', params: { identityName: identity.preferredUsername } }"
:to="{
name: 'UpdateIdentity',
params: { identityName: identity.preferredUsername },
}"
class="media identity"
v-bind:class="{ 'is-current-identity': isCurrentIdentity(identity) }"
>
@ -24,7 +27,10 @@
</li>
</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") }}
</router-link>
</section>
@ -53,7 +59,7 @@ export default class Identities extends Vue {
errors: string[] = [];
isCurrentIdentity(identity: IPerson) {
isCurrentIdentity(identity: IPerson): boolean {
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>
</template>
<script lang="ts">
import { ActorType } from "@/types/enums";
import { Component, Vue, Prop } from "vue-property-decorator";
import { IActor, ActorType } from "../../types/actor";
import { IActor } from "../../types/actor";
import ActorCard from "./ActorCard.vue";
@Component({

View file

@ -16,11 +16,21 @@
checkable
checkbox-position="left"
>
<b-table-column field="actor.id" label="ID" width="40" numeric v-slot="props">{{
props.row.actor.id
}}</b-table-column>
<b-table-column
field="actor.id"
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="account-circle" v-else />
</b-table-column>
@ -33,26 +43,39 @@
centered
v-slot="props"
>
<span :class="`tag ${props.row.approved ? 'is-success' : 'is-danger'}`">{{
props.row.approved ? $t("Accepted") : $t("Pending")
}}</span>
<span
:class="`tag ${props.row.approved ? 'is-success' : 'is-danger'}`"
>{{ props.row.approved ? $t("Accepted") : $t("Pending") }}</span
>
</b-table-column>
<b-table-column field="actor.domain" :label="$t('Domain')" sortable>
<template v-slot:default="props">
<a @click="toggle(props.row)" v-if="RelayMixin.isInstance(props.row.actor)">{{
props.row.actor.domain
}}</a>
<a
@click="toggle(props.row)"
v-if="RelayMixin.isInstance(props.row.actor)"
>{{ props.row.actor.domain }}</a
>
<a @click="toggle(props.row)" v-else>{{
`${props.row.actor.preferredUsername}@${props.row.actor.domain}`
}}</a>
</template>
</b-table-column>
<b-table-column field="targetActor.updatedAt" :label="$t('Date')" sortable v-slot="props">
<span :title="$options.filters.formatDateTimeString(props.row.updatedAt)">{{
formatDistanceToNow(new Date(props.row.updatedAt), { locale: $dateFnsLocale })
}}</span></b-table-column
<b-table-column
field="targetActor.updatedAt"
:label="$t('Date')"
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">
@ -143,7 +166,11 @@ export default class Followers extends Mixins(RelayMixin) {
await this.$apollo.queries.relayFollowers.refetch();
this.checkedRows = [];
} 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();
this.checkedRows = [];
} 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>
<div>
<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">
<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 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>
</b-field>
</b-field>
@ -29,12 +38,25 @@
checkable
checkbox-position="left"
>
<b-table-column field="targetActor.id" label="ID" width="40" numeric v-slot="props">{{
props.row.targetActor.id
}}</b-table-column>
<b-table-column
field="targetActor.id"
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-icon icon="lan" v-if="RelayMixin.isInstance(props.row.targetActor)" />
<b-table-column
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-table-column>
@ -46,26 +68,39 @@
centered
v-slot="props"
>
<span :class="`tag ${props.row.approved ? 'is-success' : 'is-danger'}`">{{
props.row.approved ? $t("Accepted") : $t("Pending")
}}</span>
<span
:class="`tag ${props.row.approved ? 'is-success' : 'is-danger'}`"
>{{ props.row.approved ? $t("Accepted") : $t("Pending") }}</span
>
</b-table-column>
<b-table-column field="targetActor.domain" :label="$t('Domain')" sortable>
<template v-slot:default="props">
<a @click="toggle(props.row)" v-if="RelayMixin.isInstance(props.row.targetActor)">{{
props.row.targetActor.domain
}}</a>
<a
@click="toggle(props.row)"
v-if="RelayMixin.isInstance(props.row.targetActor)"
>{{ props.row.targetActor.domain }}</a
>
<a @click="toggle(props.row)" v-else>{{
`${props.row.targetActor.preferredUsername}@${props.row.targetActor.domain}`
}}</a>
</template>
</b-table-column>
<b-table-column field="targetActor.updatedAt" :label="$t('Date')" sortable v-slot="props">
<span :title="$options.filters.formatDateTimeString(props.row.updatedAt)">{{
formatDistanceToNow(new Date(props.row.updatedAt), { locale: $dateFnsLocale })
}}</span></b-table-column
<b-table-column
field="targetActor.updatedAt"
:label="$t('Date')"
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">
@ -103,7 +138,6 @@ import { SnackbarProgrammatic as Snackbar } from "buefy";
import { formatDistanceToNow } from "date-fns";
import { ADD_RELAY, REMOVE_RELAY } from "../../graphql/admin";
import { IFollower } from "../../types/actor/follower.model";
import { Paginate } from "../../types/paginate";
import RelayMixin from "../../mixins/relay";
@Component({
@ -133,13 +167,19 @@ export default class Followings extends Mixins(RelayMixin) {
await this.$apollo.queries.relayFollowings.refetch();
this.newRelayAddress = "";
} 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> {
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();
this.checkedRows = [];
} 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>
<li :class="{ reply: comment.inReplyToComment }">
<article class="media" :class="{ selected: commentSelected }" :id="commentId">
<article
class="media"
:class="{ selected: commentSelected }"
:id="commentId"
>
<popover-actor-card
class="media-left"
:actor="comment.actor"
:inline="true"
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="" />
</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>
<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="" />
</figure>
<b-icon v-else size="is-large" icon="account-circle" />
@ -21,7 +36,9 @@
<div class="media-content">
<div class="content">
<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>
<a class="comment-link has-text-grey" :href="commentURL">
<small>{{
@ -54,10 +71,15 @@
<div class="load-replies" v-if="comment.totalReplies">
<p v-if="!showReplies" @click="fetchReplies">
<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>
</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" />
<span>{{ $t("Hide replies") }}</span>
</p>
@ -86,14 +108,24 @@
</nav>
</div>
</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">
<figure class="media-left" v-if="currentActor.avatar">
<p class="image is-48x48">
<img :src="currentActor.avatar.url" alt="" />
</p>
</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="content">
<span class="first-line">
@ -102,7 +134,12 @@
</span>
<br />
<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
:disabled="newComment.text.trim().length === 0"
native-type="submit"
@ -118,7 +155,12 @@
<div class="left">
<div class="vertical-border" @click="showReplies = false" />
</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
class="reply"
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 { SnackbarProgrammatic as Snackbar } from "buefy";
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 { CURRENT_ACTOR_CLIENT } from "../../graphql/actor";
import { IPerson, usernameWithDomain } from "../../types/actor";
@ -155,7 +197,8 @@ import PopoverActorCard from "../Account/PopoverActorCard.vue";
},
},
components: {
editor: () => import(/* webpackChunkName: "editor" */ "@/components/Editor.vue"),
editor: () =>
import(/* webpackChunkName: "editor" */ "@/components/Editor.vue"),
comment: () => import(/* webpackChunkName: "comment" */ "./Comment.vue"),
PopoverActorCard,
},
@ -167,7 +210,9 @@ export default class Comment extends Vue {
// Hack because Vue only exports it's own interface.
// 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;
@ -231,7 +276,9 @@ export default class Comment extends Vue {
if (!eventData) return;
const { event } = eventData;
const { comments } = event;
const parentCommentIndex = comments.findIndex((oldComment) => oldComment.id === parentId);
const parentCommentIndex = comments.findIndex(
(oldComment) => oldComment.id === parentId
);
const parentComment = comments[parentCommentIndex];
if (!parentComment) return;
parentComment.replies = thread;
@ -303,7 +350,11 @@ export default class Comment extends Vue {
duration: 5000,
});
} 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)"
@keyup.ctrl.enter="createCommentForEvent(newComment)"
>
<b-notification v-if="isEventOrganiser && !areCommentsClosed" :closable="false">{{
$t("Comments are closed for everybody else.")
}}</b-notification>
<b-notification
v-if="isEventOrganiser && !areCommentsClosed"
:closable="false"
>{{ $t("Comments are closed for everybody else.") }}</b-notification
>
<article class="media">
<figure class="media-left">
<identity-picker-wrapper :inline="false" v-model="newComment.actor" />
@ -16,11 +18,17 @@
<div class="media-content">
<div class="field">
<p class="control">
<editor ref="commenteditor" mode="comment" v-model="newComment.text" />
<editor
ref="commenteditor"
mode="comment"
v-model="newComment.text"
/>
</p>
</div>
<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>
</article>
@ -29,7 +37,12 @@
$t("The organiser has chosen to close comments.")
}}</b-notification>
<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
class="root-comment"
:comment="comment"
@ -51,7 +64,7 @@
import { Prop, Vue, Component, Watch } from "vue-property-decorator";
import Comment from "@/components/Comment/Comment.vue";
import IdentityPickerWrapper from "@/views/Account/IdentityPickerWrapper.vue";
import { CommentModeration } from "../../types/event-options.model";
import { CommentModeration } from "@/types/enums";
import { CommentModel, IComment } from "../../types/comment.model";
import {
CREATE_COMMENT_FROM_EVENT,
@ -76,7 +89,9 @@ import { IEvent } from "../../types/event.model";
};
},
update(data) {
return data.event.comments.map((comment: IComment) => new CommentModel(comment));
return data.event.comments.map(
(comment: IComment) => new CommentModel(comment)
);
},
skip() {
return !this.event.uuid;
@ -86,7 +101,8 @@ import { IEvent } from "../../types/event.model";
components: {
Comment,
IdentityPickerWrapper,
editor: () => import(/* webpackChunkName: "editor" */ "@/components/Editor.vue"),
editor: () =>
import(/* webpackChunkName: "editor" */ "@/components/Editor.vue"),
},
})
export default class CommentTree extends Vue {
@ -113,7 +129,9 @@ export default class CommentTree extends Vue {
variables: {
eventId: this.event.id,
text: comment.text,
inReplyToCommentId: comment.inReplyToComment ? comment.inReplyToComment.id : null,
inReplyToCommentId: comment.inReplyToComment
? comment.inReplyToComment.id
: null,
},
update: (store, { data }) => {
if (data == null) return;
@ -228,7 +246,9 @@ export default class CommentTree extends Vue {
});
if (!localData) return;
const { thread: oldReplyList } = localData;
const replies = oldReplyList.filter((reply) => reply.id !== deletedCommentId);
const replies = oldReplyList.filter(
(reply) => reply.id !== deletedCommentId
);
store.writeQuery({
query: FETCH_THREAD_REPLIES,
variables: {
@ -249,7 +269,9 @@ export default class CommentTree extends Vue {
event.comments = oldComments;
} else {
// we have deleted a thread itself
event.comments = oldComments.filter((reply) => reply.id !== deletedCommentId);
event.comments = oldComments.filter(
(reply) => reply.id !== deletedCommentId
);
}
store.writeQuery({
query: COMMENTS_THREADS,
@ -274,14 +296,18 @@ export default class CommentTree extends Vue {
.filter((comment) => comment.inReplyToComment == null)
.sort((a, b) => {
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;
});
}
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 {

View file

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

View file

@ -1,14 +1,23 @@
<template>
<router-link
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">
<figure
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>
<b-icon v-else size="is-medium" icon="account-circle" />
</div>
@ -17,15 +26,18 @@
<p class="discussion-minimalist-title">{{ discussion.title }}</p>
<span :title="actualDate | formatDateTimeString">
{{
formatDistanceToNowStrict(new Date(actualDate), { locale: $dateFnsLocale }) ||
$t("Right now")
formatDistanceToNowStrict(new Date(actualDate), {
locale: $dateFnsLocale,
}) || $t("Right now")
}}</span
>
</div>
<div class="has-text-grey" v-if="!discussion.lastComment.deletedAt">
{{ htmlTextEllipsis }}
</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>
</router-link>
</template>
@ -54,7 +66,10 @@ export default class DiscussionListItem extends Vue {
}
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.updatedAt;
@ -83,7 +98,8 @@ export default class DiscussionListItem extends Vue {
.discussion-minimalist-title {
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-weight: 700;
flex: 1;

View file

@ -117,11 +117,21 @@
<b-icon icon="format-quote-close" />
</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" />
</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" />
</button>
</div>
@ -181,7 +191,9 @@
</div>
</div>
</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>
</template>
@ -395,15 +407,7 @@ export default class EditorComponent extends Vue {
new Image(),
new MaxSize({ maxSize: this.maxSize }),
],
onUpdate: ({
getHTML,
transaction,
getJSON,
}: {
getHTML: Function;
getJSON: Function;
transaction: unknown;
}) => {
onUpdate: ({ getHTML }: { getHTML: Function }) => {
this.$emit("input", getHTML());
},
});
@ -438,7 +442,8 @@ export default class EditorComponent extends Vue {
upHandler(): void {
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
*/
downHandler(): void {
this.navigatedActorIndex = (this.navigatedActorIndex + 1) % this.filteredActors.length;
this.navigatedActorIndex =
(this.navigatedActorIndex + 1) % this.filteredActors.length;
}
enterHandler(): void {
@ -541,7 +547,10 @@ export default class EditorComponent extends Vue {
},
});
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) {
console.error(error);

View file

@ -48,9 +48,14 @@ export default class Image extends Node {
}
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 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 transaction = state.tr.insert(position, node);
dispatch(transaction);
@ -75,7 +80,8 @@ export default class Image extends Node {
}
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) {
@ -105,7 +111,10 @@ export default class Image extends Node {
src: data.uploadMedia.url,
"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);
});
return true;

View file

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

View file

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

View file

@ -102,14 +102,20 @@ export default class DateTimePicker extends Vue {
}
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 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 null;

View file

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

View file

@ -18,7 +18,9 @@
</docs>
<template>
<span v-if="!endsOn">{{ beginsOn | formatDateTimeString(showStartTime) }}</span>
<span v-if="!endsOn">{{
beginsOn | formatDateTimeString(showStartTime)
}}</span>
<span v-else-if="isSameDay() && showStartTime && showEndTime">
{{
$t("On {date} from {startTime} to {endTime}", {
@ -44,7 +46,9 @@
})
}}
</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">
{{
$t("From the {startDate} at {startTime} to the {endDate} at {endTime}", {
@ -97,7 +101,9 @@ export default class EventFullDate extends Vue {
}
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;
}
}

View file

@ -6,25 +6,38 @@
<div class="date-component">
<date-calendar-icon :date="participation.event.beginsOn" />
</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>
</router-link>
</div>
<div class="participation-actor has-text-grey">
<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
icon="link"
v-else-if="participation.event.visibility === EventVisibility.UNLISTED"
v-else-if="
participation.event.visibility === EventVisibility.UNLISTED
"
/>
<b-icon
icon="lock"
v-else-if="participation.event.visibility === EventVisibility.PRIVATE"
v-else-if="
participation.event.visibility === EventVisibility.PRIVATE
"
/>
</span>
<span
v-if="
participation.event.physicalAddress && participation.event.physicalAddress.locality
participation.event.physicalAddress &&
participation.event.physicalAddress.locality
"
>{{ participation.event.physicalAddress.locality }} -</span
>
@ -43,7 +56,11 @@
path="Going as {name}"
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() }}
</popover-actor-card>
</i18n>
@ -53,12 +70,15 @@
<span
class="participant-stats"
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(
"{available}/{capacity} available places",
@ -68,16 +88,21 @@
available:
participation.event.options.maximumAttendeeCapacity -
participation.event.participantStats.participant,
capacity: participation.event.options.maximumAttendeeCapacity,
capacity:
participation.event.options.maximumAttendeeCapacity,
}
)
}}
</span>
<span v-else>
{{
$tc("{count} participants", participation.event.participantStats.participant, {
count: participation.event.participantStats.participant,
})
$tc(
"{count} participants",
participation.event.participantStats.participant,
{
count: participation.event.participantStats.participant,
}
)
}}
</span>
<span v-if="participation.event.participantStats.notApproved > 0">
@ -107,9 +132,10 @@
<ul>
<li
v-if="
![ParticipantRole.PARTICIPANT, ParticipantRole.NOT_APPROVED].includes(
participation.role
)
![
ParticipantRole.PARTICIPANT,
ParticipantRole.NOT_APPROVED,
].includes(participation.role)
"
>
<b-button
@ -140,19 +166,23 @@
</li>
<li
v-if="
![ParticipantRole.PARTICIPANT, ParticipantRole.NOT_APPROVED].includes(
participation.role
)
![
ParticipantRole.PARTICIPANT,
ParticipantRole.NOT_APPROVED,
].includes(participation.role)
"
@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
v-if="
![ParticipantRole.PARTICIPANT, ParticipantRole.NOT_APPROVED].includes(
participation.role
)
![
ParticipantRole.PARTICIPANT,
ParticipantRole.NOT_APPROVED,
].includes(participation.role)
"
>
<b-button
@ -172,7 +202,10 @@
tag="router-link"
icon-left="view-compact"
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
>
</li>
@ -187,8 +220,9 @@ import { Component, Prop } from "vue-property-decorator";
import DateCalendarIcon from "@/components/Event/DateCalendarIcon.vue";
import { mixins } from "vue-class-component";
import { RawLocation, Route } from "vue-router";
import { IParticipant, ParticipantRole } from "../../types/participant.model";
import { EventVisibility, IEventCardOptions } from "../../types/event.model";
import { EventVisibility, ParticipantRole } from "@/types/enums";
import { IParticipant } from "../../types/participant.model";
import { IEventCardOptions } from "../../types/event.model";
import { IPerson } from "../../types/actor";
import ActorMixin from "../../mixins/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);
}
async gotToWithCheck(participation: IParticipant, route: RawLocation): Promise<Route> {
if (participation.actor.id !== this.currentActor.id && participation.event.organizerActor) {
async gotToWithCheck(
participation: IParticipant,
route: RawLocation
): Promise<Route> {
if (
participation.actor.id !== this.currentActor.id &&
participation.event.organizerActor
) {
const organizer = participation.event.organizerActor as IPerson;
await changeIdentity(this.$apollo.provider.defaultClient, organizer);
this.$buefy.notification.open({

View file

@ -6,7 +6,9 @@
<div class="date-component">
<date-calendar-icon :date="event.beginsOn" />
</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>
</router-link>
</div>
@ -15,17 +17,34 @@
{{ event.physicalAddress.locality }}
</span>
<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 v-else-if="options.memberofGroup">
{{ $t("Organized by {name}", { name: usernameWithDomain(event.organizerActor) }) }}
{{
$t("Organized by {name}", {
name: usernameWithDomain(event.organizerActor),
})
}}
</span>
</div>
<div class="columns">
<span class="column is-narrow">
<b-icon icon="earth" 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" />
<b-icon
icon="earth"
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 class="column is-narrow participant-stats">
<span v-if="event.options.maximumAttendeeCapacity !== 0">
@ -38,9 +57,13 @@
</span>
<span v-else>
{{
$tc("{count} participants", event.participantStats.participant, {
count: event.participantStats.participant,
})
$tc(
"{count} participants",
event.participantStats.participant,
{
count: event.participantStats.participant,
}
)
}}
</span>
</span>
@ -51,7 +74,7 @@
</template>
<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 DateCalendarIcon from "@/components/Event/DateCalendarIcon.vue";
import { IPerson, usernameWithDomain } from "@/types/actor";
@ -59,7 +82,7 @@ import { mixins } from "vue-class-component";
import ActorMixin from "@/mixins/actor";
import { CURRENT_ACTOR_CLIENT } from "@/graphql/actor";
import EventMixin from "@/mixins/event";
import { ParticipantRole } from "../../types/participant.model";
import { EventVisibility, ParticipantRole } from "@/types/enums";
import RouteName from "../../router/name";
const defaultOptions: IEventCardOptions = {

View file

@ -14,10 +14,12 @@
{{
$tc(
"{available}/{capacity} available places",
event.options.maximumAttendeeCapacity - event.participantStats.participant,
event.options.maximumAttendeeCapacity -
event.participantStats.participant,
{
available:
event.options.maximumAttendeeCapacity - event.participantStats.participant,
event.options.maximumAttendeeCapacity -
event.participantStats.participant,
capacity: event.options.maximumAttendeeCapacity,
}
)
@ -42,9 +44,13 @@
"
>
{{
$tc("{count} requests waiting", event.participantStats.notApproved, {
count: event.participantStats.notApproved,
})
$tc(
"{count} requests waiting",
event.participantStats.notApproved,
{
count: event.participantStats.notApproved,
}
)
}}
</b-button>
</span>
@ -56,7 +62,7 @@
import { Component, Prop, Vue } from "vue-property-decorator";
import { IEvent } from "@/types/event.model";
import DateCalendarIcon from "@/components/Event/DateCalendarIcon.vue";
import { ParticipantRole } from "../../types/participant.model";
import { ParticipantRole } from "@/types/enums";
import RouteName from "../../router/name";
@Component({
@ -88,7 +94,8 @@ export default class EventMinimalistCard extends Vue {
.event-minimalist-title {
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-weight: 700;
}

View file

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

View file

@ -10,9 +10,18 @@
>
<div class="media">
<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>
<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">
<h3>{{ availableActor.name }}</h3>
<small>{{ `@${availableActor.preferredUsername}` }}</small>
@ -23,9 +32,11 @@
</template>
<script lang="ts">
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 { Paginate } from "@/types/paginate";
import { IMember } from "@/types/actor/member.model";
import { MemberRole } from "@/types/enums";
@Component({
apollo: {
@ -59,16 +70,21 @@ export default class OrganizerPicker extends Vue {
get actualMemberships(): IMember[] {
if (this.restrictModeratorLevel) {
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;
}
get actualAvailableActors(): IActor[] {
return [this.identity, ...this.actualMemberships.map((member) => member.parent)];
return [
this.identity,
...this.actualMemberships.map((member) => member.parent),
];
}
@Watch("currentActor")

View file

@ -1,7 +1,11 @@
<template>
<div class="organizer-picker">
<!-- 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-left">
<figure class="image is-48x48" v-if="currentActor.avatar">
@ -15,7 +19,9 @@
</div>
<div class="media-content" v-if="currentActor.name">
<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 class="media-content" v-else>
{{ `@${currentActor.preferredUsername}` }}
@ -26,7 +32,11 @@
</div>
</div>
<!-- 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
class="image is-48x48"
v-if="currentActor.avatar"
@ -40,13 +50,19 @@
<div class="media">
<div class="media-left">
<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>
<b-icon v-else size="is-large" icon="account-circle" />
</div>
<div class="media-content" v-if="identity.name">
<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 class="media-content" v-else>
{{ `@${identity.preferredUsername}` }}
@ -74,7 +90,11 @@
<div class="column">
<div v-if="actorMembersForCurrentActor.length > 0">
<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">
<div class="media">
<div class="media-left">
@ -89,7 +109,9 @@
</div>
<div class="media-content" v-if="actor.name">
<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 class="media-content" v-else>
{{ `@${actor.preferredUsername}` }}
@ -115,7 +137,8 @@
</template>
<script lang="ts">
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 { PERSON_MEMBERSHIPS_WITH_MEMBERS } from "../../graphql/actor";
import { Paginate } from "../../types/paginate";
@ -150,7 +173,8 @@ export default class OrganizerPickerWrapper extends Vue {
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);
@ -171,7 +195,9 @@ export default class OrganizerPickerWrapper extends Vue {
pickActor(): void {
this.$emit(
"update:contacts",
this.actorMembersForCurrentActor.filter(({ id }) => this.actualContacts.includes(id))
this.actorMembersForCurrentActor.filter(({ id }) =>
this.actualContacts.includes(id)
)
);
this.$emit("input", this.currentActor);
this.isComponentModalActive = false;
@ -182,7 +208,9 @@ export default class OrganizerPickerWrapper extends Vue {
({ parent: { id } }) => id === this.currentActor.id
);
if (currentMembership) {
return currentMembership.parent.members.elements.map(({ actor }) => actor);
return currentMembership.parent.members.elements.map(
({ actor }: { actor: IActor }) => actor
);
}
return [];
}

View file

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

View file

@ -59,7 +59,12 @@
<a :href="linkedInShareUrl" target="_blank" rel="nofollow noopener"
><b-icon icon="linkedin" size="is-large" type="is-primary"
/></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">
<DiasporaLogo alt="diaspora-logo" />
</span>
@ -76,7 +81,9 @@
<script lang="ts">
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
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 {
@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;
@ -99,13 +107,15 @@ export default class ShareEventModal extends Vue {
showCopiedTooltip = false;
get twitterShareUrl(): string {
return `https://twitter.com/intent/tweet?url=${encodeURIComponent(this.event.url)}&text=${
this.event.title
}`;
return `https://twitter.com/intent/tweet?url=${encodeURIComponent(
this.event.url
)}&text=${this.event.title}`;
}
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 {
@ -124,7 +134,7 @@ export default class ShareEventModal extends Vue {
)}&url=${encodeURIComponent(this.event.url)}`;
}
copyURL() {
copyURL(): void {
this.eventURLInput.$refs.input.select();
document.execCommand("copy");
this.showCopiedTooltip = true;

View file

@ -4,7 +4,9 @@
{{ $t("Add some tags") }}
<b-tooltip
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-tooltip>
@ -40,7 +42,6 @@ import { ITag } from "../../types/tag.model";
if (typeof tag !== "string") {
return tag;
}
// @ts-ignore
return { title: tag, slug: tag } as ITag;
});
this.$emit("input", tagEntities);
@ -57,14 +58,14 @@ export default class TagInput extends Vue {
filteredTags: ITag[] = [];
getFilteredTags(text: string) {
getFilteredTags(text: string): void {
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>

View file

@ -3,13 +3,20 @@
<img :src="`/img/pics/footer_${random}.jpg`" alt="" />
<ul>
<li>
<router-link :to="{ name: RouteName.ABOUT }">{{ $t("About") }}</router-link>
<router-link :to="{ name: RouteName.ABOUT }">{{
$t("About")
}}</router-link>
</li>
<li>
<router-link :to="{ name: RouteName.TERMS }">{{ $t("Terms") }}</router-link>
<router-link :to="{ name: RouteName.TERMS }">{{
$t("Terms")
}}</router-link>
</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") }}
</a>
</li>
@ -19,7 +26,9 @@
tag="span"
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>
<a href="https://joinmobilizon.org/hall-of-fame" slot="contributors">{{
$t("more than 1360 contributors")

View file

@ -17,7 +17,9 @@
>
<h3>{{ group.name }}</h3>
<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>
</p>
</router-link>

View file

@ -13,7 +13,9 @@
<router-link
:to="{
name: RouteName.GROUP,
params: { preferredUsername: usernameWithDomain(member.parent) },
params: {
preferredUsername: usernameWithDomain(member.parent),
},
}"
>
<h3>{{ member.parent.name }}</h3>
@ -23,12 +25,16 @@
}}</span>
<span v-else>{{ `@${member.parent.preferredUsername}` }}</span>
<b-taglist>
<b-tag type="is-info" v-if="member.role === MemberRole.ADMINISTRATOR">{{
$t("Administrator")
}}</b-tag>
<b-tag type="is-info" v-else-if="member.role === MemberRole.MODERATOR">{{
$t("Moderator")
}}</b-tag>
<b-tag
type="is-info"
v-if="member.role === MemberRole.ADMINISTRATOR"
>{{ $t("Administrator") }}</b-tag
>
<b-tag
type="is-info"
v-else-if="member.role === MemberRole.MODERATOR"
>{{ $t("Moderator") }}</b-tag
>
</b-taglist>
</p>
</router-link>
@ -54,7 +60,9 @@
<script lang="ts">
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";
@Component

View file

@ -8,7 +8,9 @@
<a
class="list-item"
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)"
:key="groupMembership.id"
>
@ -19,14 +21,25 @@
:src="groupMembership.parent.avatar.url"
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">
<h3>@{{ groupMembership.parent.name }}</h3>
<small>{{ `@${groupMembership.parent.preferredUsername}` }}</small>
<small>{{
`@${groupMembership.parent.preferredUsername}`
}}</small>
</div>
</div>
</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>
</a>
</div>
@ -36,9 +49,11 @@
</template>
<script lang="ts">
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 { Paginate } from "@/types/paginate";
import { IMember } from "@/types/actor/member.model";
import { MemberRole } from "@/types/enums";
@Component({
apollo: {
@ -77,9 +92,11 @@ export default class GroupPicker extends Vue {
get actualMemberships(): IMember[] {
if (this.restrictModeratorLevel) {
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;

View file

@ -10,7 +10,11 @@
{{ $t("The event will show the group as organizer.") }}
</p>
</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-left">
<figure class="image is-48x48" v-if="currentGroup.avatar">
@ -24,7 +28,9 @@
</div>
<div class="media-content" v-if="currentGroup.name">
<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 class="media-content" v-else>
{{ `@${currentGroup.preferredUsername}` }}
@ -34,7 +40,11 @@
</b-button>
</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
class="image is-48x48"
v-if="currentGroup.avatar"
@ -44,7 +54,9 @@
<b-icon v-else size="is-large" icon="account-circle" />
</span>
<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">
{{ $t("You need to create the group before you create an event.") }}
</p>
@ -61,7 +73,8 @@
</template>
<script lang="ts">
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 { PERSON_MEMBERSHIPS } from "../../graphql/actor";
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: false, type: Boolean, default: true }) privateSection!: boolean;
@Prop({ required: false, type: Boolean, default: true })
privateSection!: boolean;
@Prop({ required: true, type: Object }) route!: Route;
}
@ -76,7 +77,8 @@ div.group-section-title {
::v-deep span {
display: inline;
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-size: 30px;
flex: 1;

View file

@ -2,7 +2,10 @@
<div class="media">
<div class="media-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>
</i18n>
</div>
@ -20,15 +23,21 @@
<router-link
:to="{
name: RouteName.GROUP,
params: { preferredUsername: usernameWithDomain(member.parent) },
params: {
preferredUsername: usernameWithDomain(member.parent),
},
}"
>
<h3>{{ member.parent.name }}</h3>
<p class="is-6 has-text-grey">
<span v-if="member.parent.domain">
{{ `@${member.parent.preferredUsername}@${member.parent.domain}` }}
{{
`@${member.parent.preferredUsername}@${member.parent.domain}`
}}
</span>
<span v-else>{{ `@${member.parent.preferredUsername}` }}</span>
<span v-else>{{
`@${member.parent.preferredUsername}`
}}</span>
</p>
</router-link>
</div>
@ -54,7 +63,8 @@
<script lang="ts">
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";
@Component

View file

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

View file

@ -8,7 +8,11 @@
@click="clickMap"
@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' }" />
<l-marker
:lat-lng="[lat, lon]"
@ -17,7 +21,9 @@
:draggable="!readOnly"
>
<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-marker>
</l-map>
@ -51,12 +57,15 @@ export default class Map extends Vue {
@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 })
updateDraggableMarkerCallback!: Function;
updateDraggableMarkerCallback!: (latlng: LatLng, zoom: number) => void;
defaultOptions: {
zoom: number;
@ -86,45 +95,48 @@ export default class Map extends Vue {
}
/* eslint-enable */
openPopup(event: LeafletEvent) {
openPopup(event: LeafletEvent): void {
this.$nextTick(() => {
event.target.openPopup();
});
}
get mergedOptions(): object {
get mergedOptions(): Record<string, unknown> {
return { ...this.defaultOptions, ...this.options };
}
get lat() {
get lat(): number {
return this.$props.coords.split(";")[1];
}
get lon() {
get lon(): number {
return this.$props.coords.split(";")[0];
}
get popupMultiLine() {
get popupMultiLine(): Array<string> {
if (Array.isArray(this.marker.text)) {
return this.marker.text;
}
return [this.marker.text];
}
clickMap(event: LeafletMouseEvent) {
clickMap(event: LeafletMouseEvent): void {
this.updateDraggableMarkerPosition(event.latlng);
}
updateDraggableMarkerPosition(e: LatLng) {
updateDraggableMarkerPosition(e: LatLng): void {
this.updateDraggableMarkerCallback(e, this.zoom);
}
updateZoom(zoom: number) {
updateZoom(zoom: number): void {
this.zoom = zoom;
}
get attribution() {
return this.config.maps.tiles.attribution || this.$t("© The OpenStreetMap Contributors");
get attribution(): string {
return (
this.config.maps.tiles.attribution ||
(this.$t("© The OpenStreetMap Contributors") as string)
);
}
}
</script>

View file

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

View file

@ -1,7 +1,15 @@
<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">
<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 />
</b-navbar-item>
</template>
@ -19,9 +27,12 @@
>{{ $t("My groups") }}</b-navbar-item
>
<b-navbar-item tag="span" v-if="config && config.features.eventCreation">
<b-button tag="router-link" :to="{ name: RouteName.CREATE_EVENT }" type="is-primary">{{
$t("Create")
}}</b-button>
<b-button
tag="router-link"
:to="{ name: RouteName.CREATE_EVENT }"
type="is-primary"
>{{ $t("Create") }}</b-button
>
</b-navbar-item>
</template>
<template slot="end">
@ -30,9 +41,17 @@
</b-navbar-item>
<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">
<img class="is-rounded" alt="avatarUrl" :src="currentActor.avatar.url" />
<img
class="is-rounded"
alt="avatarUrl"
:src="currentActor.avatar.url"
/>
</figure>
<b-icon v-else icon="account-circle" />
</template>
@ -65,9 +84,11 @@
<hr class="navbar-divider" />
</b-navbar-item>
<b-navbar-item tag="router-link" :to="{ name: RouteName.UPDATE_IDENTITY }">{{
$t("My account")
}}</b-navbar-item>
<b-navbar-item
tag="router-link"
:to="{ name: RouteName.UPDATE_IDENTITY }"
>{{ $t("My account") }}</b-navbar-item
>
<!-- <b-navbar-item tag="router-link" :to="{ name: RouteName.CREATE_GROUP }">-->
<!-- {{ $t('Create group') }}-->
@ -95,9 +116,11 @@
<strong>{{ $t("Sign up") }}</strong>
</router-link>
<router-link class="button is-light" :to="{ name: RouteName.LOGIN }">{{
$t("Log in")
}}</router-link>
<router-link
class="button is-light"
:to="{ name: RouteName.LOGIN }"
>{{ $t("Log in") }}</router-link
>
</div>
</b-navbar-item>
</template>
@ -109,13 +132,18 @@ import { Component, Vue, Watch } from "vue-property-decorator";
import Logo from "@/components/Logo.vue";
import { GraphQLError } from "graphql";
import { loadLanguageAsync } from "@/utils/i18n";
import { ICurrentUserRole } from "@/types/enums";
import { CURRENT_USER_CLIENT, USER_SETTINGS } from "../graphql/user";
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 { CONFIG } from "../graphql/config";
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 RouteName from "../router/name";
@ -130,7 +158,9 @@ import RouteName from "../router/name";
identities: {
query: IDENTITIES,
update: ({ identities }) =>
identities ? identities.map((identity: IPerson) => new Person(identity)) : [],
identities
? identities.map((identity: IPerson) => new Person(identity))
: [],
skip() {
return this.currentUser.isLoggedIn === false;
},
@ -201,7 +231,8 @@ export default class NavBar extends Vue {
async handleErrors(errors: GraphQLError[]): Promise<void> {
if (
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();
}

View file

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

View file

@ -1,5 +1,9 @@
<template>
<redirect-with-account :uri="uri" :pathAfterLogin="`/events/${uuid}`" :sentence="sentence" />
<redirect-with-account
:uri="uri"
:pathAfterLogin="`/events/${uuid}`"
:sentence="sentence"
/>
</template>
<script lang="ts">
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>

View file

@ -33,7 +33,13 @@
)
}}
</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-input
type="textarea"
@ -54,18 +60,29 @@
</p>
</b-checkbox>
</b-field>
<b-button :disabled="sendingForm" type="is-primary" native-type="submit">{{
$t("Send email")
}}</b-button>
<b-button
:disabled="sendingForm"
type="is-primary"
native-type="submit"
>{{ $t("Send email") }}</b-button
>
<div class="has-text-centered">
<b-button native-type="button" tag="a" type="is-text" @click="$router.go(-1)">{{
$t("Back to previous page")
}}</b-button>
<b-button
native-type="button"
tag="a"
type="is-text"
@click="$router.go(-1)"
>{{ $t("Back to previous page") }}</b-button
>
</div>
</form>
<div v-else>
<h1 class="title">{{ $t("Request for participation confirmation sent") }}</h1>
<p class="content">{{ $t("Check your inbox (and your junk mail folder).") }}</p>
<h1 class="title">
{{ $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>
</div>
</div>
@ -74,12 +91,13 @@
</template>
<script lang="ts">
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 { IConfig } from "@/types/config.model";
import { CONFIG } from "@/graphql/config";
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({
apollo: {
@ -101,7 +119,11 @@ import { IParticipant, ParticipantRole } from "../../types/participant.model";
export default class ParticipationWithoutAccount extends Vue {
@Prop({ type: String, required: true }) uuid!: string;
anonymousParticipation: { email: string; message: string; saveParticipation: boolean } = {
anonymousParticipation: {
email: string;
message: string;
saveParticipation: boolean;
} = {
email: "",
message: "",
saveParticipation: true,
@ -133,7 +155,9 @@ export default class ParticipationWithoutAccount extends Vue {
},
update: (store, { data: updateData }) => {
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;
}
@ -142,12 +166,16 @@ export default class ParticipationWithoutAccount extends Vue {
variables: { uuid: this.event.uuid },
});
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;
}
const { event } = cachedData;
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;
}

View file

@ -2,22 +2,34 @@
<section class="section container hero">
<div class="hero-body" v-if="event">
<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" />
<div class="columns has-text-centered">
<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">
<img src="../../assets/undraw_profile.svg" alt="Profile illustration" />
<img
src="../../assets/undraw_profile.svg"
alt="Profile illustration"
/>
</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>
<p>
<small>
{{
$t("Either on the {instance} instance or on another instance.", {
instance: host,
})
$t(
"Either on the {instance} instance or on another instance.",
{
instance: host,
}
)
}}
</small>
<b-tooltip
@ -32,25 +44,41 @@
</b-tooltip>
</p>
</div>
<vertical-divider :content="$t('Or')" v-if="anonymousParticipationAllowed" />
<vertical-divider
:content="$t('Or')"
v-if="anonymousParticipationAllowed"
/>
<div
class="column"
v-if="anonymousParticipationAllowed && hasAnonymousEmailParticipationMethod"
v-if="
anonymousParticipationAllowed &&
hasAnonymousEmailParticipationMethod
"
>
<router-link
:to="{ name: RouteName.EVENT_PARTICIPATE_WITHOUT_ACCOUNT }"
v-if="event.local"
>
<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>
<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>
<a :href="`${event.url}/participate/without-account`" v-else>
<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>
<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>
<p>
<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: 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;
@Prop({
@ -95,13 +99,11 @@ export default class PictureUpload extends Vue {
@Watch("pictureFile")
onPictureFileChanged(val: File): void {
console.log("onPictureFileChanged", val);
this.updatePreview(val);
}
@Watch("defaultImage")
onDefaultImageChange(defaultImage: IMedia): void {
console.log("onDefaultImageChange", defaultImage);
this.imageSrc = defaultImage ? defaultImage.url : null;
}

View file

@ -14,26 +14,46 @@
<div class="media-content">
<p class="post-minimalist-title">{{ post.title }}</p>
<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
v-if="post.visibility === PostVisibility.PUBLIC && isCurrentActorMember"
v-if="
post.visibility === PostVisibility.PUBLIC &&
isCurrentActorMember
"
class="has-text-grey"
>
<b-icon icon="earth" size="is-small" />{{ $t("Public") }}</small
>
<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.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" />{{
$t("Accessible only to members", { group: post.attributedTo.name })
$t("Accessible only to members", {
group: post.attributedTo.name,
})
}}</small
>
<small class="has-text-grey">{{
$options.filters.formatDateTimeString(new Date(post.insertedAt), false)
$options.filters.formatDateTimeString(
new Date(post.insertedAt),
false
)
}}</small>
<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>
</div>
</div>
@ -43,15 +63,17 @@
</template>
<script lang="ts">
import { usernameWithDomain } from "@/types/actor";
import { PostVisibility } from "@/types/enums";
import { Component, Prop, Vue } from "vue-property-decorator";
import RouteName from "../../router/name";
import { IPost, PostVisibility } from "../../types/post.model";
import { IPost } from "../../types/post.model";
@Component
export default class PostElementItem extends Vue {
@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;
@ -74,7 +96,8 @@ export default class PostElementItem extends Vue {
.post-minimalist-title {
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-weight: 700;
overflow: hidden;

View file

@ -43,7 +43,8 @@ export default class PostListItem extends Vue {
.post-minimalist-title {
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-weight: 700;
overflow: hidden;

View file

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

View file

@ -4,7 +4,10 @@
<p class="modal-card-title">{{ title }}</p>
</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-left">
<b-icon icon="alert" type="is-warning" size="is-large" />
@ -16,7 +19,12 @@
<figure class="image is-48x48" v-if="comment.actor.avatar">
<img :src="comment.actor.avatar.url" alt="Image" />
</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 class="media-content">
<div class="content">
@ -82,7 +90,10 @@ import { IComment } from "../../types/comment.model";
},
})
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;

View file

@ -14,7 +14,9 @@
</div>
<div class="body">
<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>
<draggable
v-if="!inline"
@ -93,21 +95,27 @@ export default class FolderItem extends Mixins(ResourceMixin) {
async moveResource(resource: IResource): Promise<IResource | undefined> {
try {
const { data } = await this.$apollo.mutate<{ updateResource: IResource }>({
mutation: UPDATE_RESOURCE,
variables: {
id: resource.id,
path: `${this.resource.path}/${resource.title}`,
parentId: this.resource.id,
},
});
const { data } = await this.$apollo.mutate<{ updateResource: IResource }>(
{
mutation: UPDATE_RESOURCE,
variables: {
id: resource.id,
path: `${this.resource.path}/${resource.title}`,
parentId: this.resource.id,
},
}
);
if (!data) {
console.error("Error while updating resource");
return undefined;
}
return data.updateResource;
} 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;
}
}

View file

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

View file

@ -2,7 +2,12 @@
<div class="resource-wrapper">
<a :href="resource.resourceUrl" target="_blank">
<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" />
</div>
<div
@ -21,7 +26,9 @@
:src="resource.metadata.faviconUrl"
/>
<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>
</div>
</a>

View file

@ -2,9 +2,15 @@
<div v-if="resource">
<article class="panel is-primary">
<p class="panel-heading">
{{ $t('Move "{resourceName}"', { resourceName: initialResource.title }) }}
{{
$t('Move "{resourceName}"', { resourceName: initialResource.title })
}}
</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">
<b-icon icon="chevron-up" size="is-small" />
</span>
@ -23,12 +29,19 @@
<a
class="panel-block"
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"
@click="goDown(element)"
>
<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 />
</span>
{{ element.title }}
@ -44,10 +57,17 @@
{{ $t("No resources in this folder") }}
</p>
</article>
<b-button type="is-primary" @click="updateResource" :disabled="moveDisabled">{{
$t("Move resource to {folder}", { folder: resource.title })
<b-button
type="is-primary"
@click="updateResource"
:disabled="moveDisabled"
>{{
$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 type="is-text" @click="$emit('closeMoveModal')">{{ $t("Cancel") }}</b-button>
</div>
</template>
<script lang="ts">
@ -81,31 +101,34 @@ export default class ResourceSelector extends Vue {
resource: IResource | undefined = this.initialResource.parent;
goDown(element: IResource) {
goDown(element: IResource): void {
if (element.type === "folder" && element.id !== this.initialResource.id) {
this.resource = element;
}
}
updateResource() {
updateResource(): void {
this.$emit(
"updateResource",
"update-resource",
{
id: this.initialResource.id,
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,
},
this.initialResource.parent
);
}
get moveDisabled() {
get moveDisabled(): boolean | undefined {
return (
(this.initialResource.parent &&
this.resource &&
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 = "";
enter() {
async enter(): Promise<void> {
this.$emit("navbar-search");
this.$router.push({
await this.$router.push({
name: RouteName.SEARCH,
query: { term: this.search },
});

View file

@ -15,16 +15,27 @@
</p>
</div>
<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>
<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>
</b-checkbox>
</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>
</div>
</template>
@ -50,7 +61,11 @@ export default class NotificationsOnboarding extends mixins(Onboarding) {
try {
this.doUpdateSetting(variables);
} 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;
get isActive() {
get isActive(): boolean {
if (!this.to) return false;
if (this.to.name === this.$route.name) {
if (this.to.params) {

View file

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

View file

@ -1,18 +1,27 @@
<template>
<aside>
<ul>
<SettingMenuSection :title="$t('Account')" :to="{ name: RouteName.ACCOUNT_SETTINGS }">
<SettingMenuSection
:title="$t('Account')"
:to="{ name: RouteName.ACCOUNT_SETTINGS }"
>
<SettingMenuItem
:title="this.$t('General')"
:to="{ name: RouteName.ACCOUNT_SETTINGS_GENERAL }"
/>
<SettingMenuItem :title="$t('Preferences')" :to="{ name: RouteName.PREFERENCES }" />
<SettingMenuItem
:title="$t('Preferences')"
:to="{ name: RouteName.PREFERENCES }"
/>
<SettingMenuItem
:title="this.$t('Email notifications')"
:to="{ name: RouteName.NOTIFICATIONS }"
/>
</SettingMenuSection>
<SettingMenuSection :title="$t('Profiles')" :to="{ name: RouteName.IDENTITIES }">
<SettingMenuSection
:title="$t('Profiles')"
:to="{ name: RouteName.IDENTITIES }"
>
<SettingMenuItem
v-for="profile in identities"
:key="profile.preferredUsername"
@ -22,7 +31,10 @@
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
v-if="
@ -33,35 +45,54 @@
:title="$t('Moderation')"
:to="{ name: RouteName.MODERATION }"
>
<SettingMenuItem :title="$t('Reports')" :to="{ name: RouteName.REPORTS }" />
<SettingMenuItem :title="$t('Moderation log')" :to="{ name: RouteName.REPORT_LOGS }" />
<SettingMenuItem
: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('Profiles')" :to="{ name: RouteName.PROFILES }" />
<SettingMenuItem :title="$t('Groups')" :to="{ name: RouteName.ADMIN_GROUPS }" />
<SettingMenuItem
:title="$t('Profiles')"
:to="{ name: RouteName.PROFILES }"
/>
<SettingMenuItem
:title="$t('Groups')"
:to="{ name: RouteName.ADMIN_GROUPS }"
/>
</SettingMenuSection>
<SettingMenuSection
v-if="this.currentUser.role == ICurrentUserRole.ADMINISTRATOR"
:title="$t('Admin')"
:to="{ name: RouteName.ADMIN }"
>
<SettingMenuItem :title="$t('Dashboard')" :to="{ name: RouteName.ADMIN_DASHBOARD }" />
<SettingMenuItem
:title="$t('Dashboard')"
:to="{ name: RouteName.ADMIN_DASHBOARD }"
/>
<SettingMenuItem
:title="$t('Instance settings')"
:to="{ name: RouteName.ADMIN_SETTINGS }"
/>
<SettingMenuItem :title="$t('Federation')" :to="{ name: RouteName.RELAYS }" />
<SettingMenuItem
:title="$t('Federation')"
:to="{ name: RouteName.RELAYS }"
/>
</SettingMenuSection>
</ul>
</aside>
</template>
<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 SettingMenuItem from "./SettingMenuItem.vue";
import { IDENTITIES } from "../../graphql/actor";
import { IPerson, Person } from "../../types/actor";
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";
@ -70,7 +101,8 @@ import RouteName from "../../router/name";
apollo: {
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,
},

View file

@ -39,7 +39,10 @@
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 }) }}
</b-message>
</p>

View file

@ -2,9 +2,10 @@
<div class="card" v-if="todo">
<div class="card-content">
<b-checkbox v-model="status" />
<router-link :to="{ name: RouteName.TODO, params: { todoId: todo.id } }">{{
todo.title
}}</router-link>
<router-link
:to="{ name: RouteName.TODO, params: { todoId: todo.id } }"
>{{ todo.title }}</router-link
>
<span class="details has-text-grey">
<span v-if="todo.dueDate" class="due_date">
<b-icon icon="calendar" />
@ -13,7 +14,9 @@
<span v-if="todo.assignedTo" class="assigned_to">
<b-icon icon="account" />
{{ `@${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>
</div>
@ -53,7 +56,11 @@ export default class Todo extends Vue {
});
this.editMode = false;
} 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>
<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
import { debounce } from "lodash";
import { debounce, DebouncedFunc } from "lodash";
import { SnackbarProgrammatic as Snackbar } from "buefy";
import { ITodo } from "../../types/todos";
import RouteName from "../../router/name";
@ -36,7 +36,9 @@ export default class Todo extends Vue {
editMode = false;
debounceUpdateTodo!: Function;
debounceUpdateTodo!: DebouncedFunc<
(obj: Record<string, unknown>) => Promise<void>
>;
// We put this in data because of issues like
// https://github.com/vuejs/vue-class-component/issues/263
@ -89,7 +91,11 @@ export default class Todo extends Vue {
});
this.editMode = false;
} 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" />
<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" />
<span>{{ oauthProvider.label }}</span>
</a>

View file

@ -20,7 +20,9 @@
</div>
<vertical-divider :content="$t('Or')" />
<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>{{ sentence }}</p>
<form @submit.prevent="redirectToInstance">
@ -34,7 +36,9 @@
:placeholder="$t('profile@instance')"
></b-input>
<p class="control">
<button class="button is-primary" type="submit">{{ $t("Go") }}</button>
<button class="button is-primary" type="submit">
{{ $t("Go") }}
</button>
</p>
</b-field>
</b-field>
@ -54,7 +58,7 @@
import { Component, Prop, Vue } from "vue-property-decorator";
import VerticalDivider from "@/components/Utils/VerticalDivider.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";
@Component({
@ -80,14 +84,22 @@ export default class RedirectWithAccount extends Vue {
async redirectToInstance(): Promise<void> {
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);
}
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 data = await (
await fetch(`${scheme}://${hostname}/.well-known/webfinger?resource=acct:${identity}`)
await fetch(
`${scheme}://${hostname}/.well-known/webfinger?resource=acct:${identity}`
)
).json();
if (data && Array.isArray(data.links)) {
const link: { template: string } = data.links.find(

View file

@ -21,7 +21,8 @@ h2 {
display: inline;
padding: 3px 8px;
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-size: 32px;
}

View file

@ -14,7 +14,10 @@ function formatDateString(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 {

View file

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

View file

@ -65,7 +65,10 @@ export const GET_PERSON = gql`
feedTokens {
token
}
organizedEvents(page: $organizedEventsPage, limit: $organizedEventsLimit) {
organizedEvents(
page: $organizedEventsPage
limit: $organizedEventsLimit
) {
total
elements {
id
@ -442,7 +445,12 @@ export const CREATE_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) {
id
preferredUsername
@ -469,7 +477,12 @@ export const DELETE_PERSON = gql`
* Prefer CREATE_PERSON when creating another identity
*/
export const REGISTER_PERSON = gql`
mutation($preferredUsername: String!, $name: String!, $summary: String!, $email: String!) {
mutation(
$preferredUsername: String!
$name: String!
$summary: String!
$email: String!
) {
registerPerson(
preferredUsername: $preferredUsername
name: $name

View file

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

View file

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

View file

@ -108,7 +108,12 @@ export const UPDATE_CURRENT_USER_CLIENT = gql`
$isLoggedIn: Boolean!
$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
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 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 { Component, Vue } from "vue-property-decorator";
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 {
DELETE_EVENT,
@ -20,7 +21,9 @@ export default class EventMixin extends mixins(Vue) {
anonymousParticipationConfirmed: boolean | null = null
): Promise<void> {
try {
const { data: resultData } = await this.$apollo.mutate<{ leaveEvent: IParticipant }>({
const { data: resultData } = await this.$apollo.mutate<{
leaveEvent: IParticipant;
}>({
mutation: LEAVE_EVENT,
variables: {
eventId: event.id,
@ -32,14 +35,18 @@ export default class EventMixin extends mixins(Vue) {
let participation;
if (!token) {
const participationCachedData = store.readQuery<{ person: IPerson }>({
const participationCachedData = store.readQuery<{
person: IPerson;
}>({
query: EVENT_PERSON_PARTICIPATION,
variables: { eventId: event.id, actorId },
});
if (participationCachedData == null) return;
const { person } = participationCachedData;
if (person === null) {
console.error("Cannot update participation cache, because of null value.");
console.error(
"Cannot update participation cache, because of null value."
);
return;
}
[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.");
return;
}
if (participation && participation.role === ParticipantRole.NOT_APPROVED) {
if (
participation &&
participation.role === ParticipantRole.NOT_APPROVED
) {
eventCached.participantStats.notApproved -= 1;
} else if (anonymousParticipationConfirmed === false) {
eventCached.participantStats.notConfirmed -= 1;
@ -81,13 +91,19 @@ export default class EventMixin extends mixins(Vue) {
this.participationCancelledMessage();
}
} 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);
}
}
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> {
@ -96,21 +112,29 @@ export default class EventMixin extends mixins(Vue) {
}
const participantsLength = event.participantStats.participant;
const prefix = participantsLength
? this.$tc("There are {participants} participants.", event.participantStats.participant, {
participants: event.participantStats.participant,
})
? this.$tc(
"There are {participants} participants.",
event.participantStats.participant,
{
participants: event.participantStats.participant,
}
)
: "";
this.$buefy.dialog.prompt({
type: "is-danger",
title: this.$t("Delete event") as string,
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>
${this.$t('To confirm, type your event title "{eventTitle}"', {
eventTitle: event.title,
})}`,
confirmText: this.$t("Delete {eventTitle}", { eventTitle: event.title }) as string,
confirmText: this.$t("Delete {eventTitle}", {
eventTitle: event.title,
}) as string,
inputAttrs: {
placeholder: event.title,
pattern: escapeRegExp(event.title),
@ -138,13 +162,19 @@ export default class EventMixin extends mixins(Vue) {
this.$emit("event-deleted", event.id);
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",
position: "is-bottom-right",
duration: 5000,
});
} 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);
}

View file

@ -2,7 +2,8 @@ import { PERSON_MEMBERSHIPS, CURRENT_ACTOR_CLIENT } from "@/graphql/actor";
import { GROUP_MEMBERSHIP_SUBSCRIPTION_CHANGED } from "@/graphql/event";
import { FETCH_GROUP } from "@/graphql/group";
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";
@Component({
@ -60,7 +61,10 @@ export default class GroupMixin extends Vue {
}
get isCurrentActorAGroupModerator(): boolean {
return this.hasCurrentActorThisRole([MemberRole.MODERATOR, MemberRole.ADMINISTRATOR]);
return this.hasCurrentActorThisRole([
MemberRole.MODERATOR,
MemberRole.ADMINISTRATOR,
]);
}
hasCurrentActorThisRole(givenRole: string | string[]): boolean {
@ -68,7 +72,8 @@ export default class GroupMixin extends Vue {
return (
this.person &&
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;
autoUpdateUsername(newDisplayName: string | null): void {
const oldUsername = IdentityEditionMixin.convertToUsername(this.oldDisplayName);
const oldUsername = IdentityEditionMixin.convertToUsername(
this.oldDisplayName
);
if (this.identity.preferredUsername === oldUsername) {
this.identity.preferredUsername = IdentityEditionMixin.convertToUsername(newDisplayName);
this.identity.preferredUsername = IdentityEditionMixin.convertToUsername(
newDisplayName
);
}
this.oldDisplayName = newDisplayName;

View file

@ -13,7 +13,9 @@ export default class Onboarding extends Vue {
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 }>({
mutation: SET_USER_SETTINGS,
variables,

View file

@ -1,8 +1,9 @@
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 { RELAY_FOLLOWERS, RELAY_FOLLOWINGS } from "@/graphql/admin";
import { Paginate } from "@/types/paginate";
import { ActorType } from "@/types/enums";
@Component({
apollo: {
@ -62,7 +63,10 @@ export default class RelayMixin extends Vue {
relayFollowings: {
__typename: previousResult.relayFollowings.__typename,
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: {
__typename: previousResult.relayFollowers.__typename,
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 {
return (
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) => {
VueInstance.prototype.$dateFnsLocale = localeEntity;
});

View file

@ -23,7 +23,9 @@ if (process.env.NODE_ENV === "production") {
console.log("New content is available; please refresh.");
},
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) {
console.error("Error during service worker registration:", error);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
import { NavigationGuard } from "vue-router";
import { UserRouteName } from "@/router/user";
import { LoginErrorCode } from "@/types/login-error-code.model";
import { AUTH_ACCESS_TOKEN } from "@/constants";
import { LoginErrorCode } from "@/types/enums";
// eslint-disable-next-line import/prefer-default-export
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 { CONFIG } from "../../graphql/config";
import { ErrorCode } from "../../types/error-code.model";
import apolloProvider from "../../vue-apollo";
// eslint-disable-next-line import/prefer-default-export

View file

@ -49,7 +49,8 @@ const router = new Router({
{
path: "/search",
name: RouteName.SEARCH,
component: () => import(/* webpackChunkName: "search" */ "../views/Search.vue"),
component: () =>
import(/* webpackChunkName: "search" */ "../views/Search.vue"),
props: true,
meta: { requiredAuth: false },
},
@ -62,7 +63,8 @@ const router = new Router({
{
path: "/about",
name: RouteName.ABOUT,
component: () => import(/* webpackChunkName: "about" */ "@/views/About.vue"),
component: () =>
import(/* webpackChunkName: "about" */ "@/views/About.vue"),
meta: { requiredAuth: false },
redirect: { name: RouteName.ABOUT_INSTANCE },
children: [
@ -70,30 +72,40 @@ const router = new Router({
path: "instance",
name: RouteName.ABOUT_INSTANCE,
component: () =>
import(/* webpackChunkName: "about" */ "@/views/About/AboutInstance.vue"),
import(
/* webpackChunkName: "about" */ "@/views/About/AboutInstance.vue"
),
},
{
path: "/terms",
name: RouteName.TERMS,
component: () => import(/* webpackChunkName: "cookies" */ "@/views/About/Terms.vue"),
component: () =>
import(/* webpackChunkName: "cookies" */ "@/views/About/Terms.vue"),
meta: { requiredAuth: false },
},
{
path: "/privacy",
name: RouteName.PRIVACY,
component: () => import(/* webpackChunkName: "cookies" */ "@/views/About/Privacy.vue"),
component: () =>
import(
/* webpackChunkName: "cookies" */ "@/views/About/Privacy.vue"
),
meta: { requiredAuth: false },
},
{
path: "/rules",
name: RouteName.RULES,
component: () => import(/* webpackChunkName: "cookies" */ "@/views/About/Rules.vue"),
component: () =>
import(/* webpackChunkName: "cookies" */ "@/views/About/Rules.vue"),
meta: { requiredAuth: false },
},
{
path: "/glossary",
name: RouteName.GLOSSARY,
component: () => import(/* webpackChunkName: "cookies" */ "@/views/About/Glossary.vue"),
component: () =>
import(
/* webpackChunkName: "cookies" */ "@/views/About/Glossary.vue"
),
meta: { requiredAuth: false },
},
],
@ -101,20 +113,25 @@ const router = new Router({
{
path: "/interact",
name: RouteName.INTERACT,
component: () => import(/* webpackChunkName: "cookies" */ "@/views/Interact.vue"),
component: () =>
import(/* webpackChunkName: "cookies" */ "@/views/Interact.vue"),
meta: { requiredAuth: false },
},
{
path: "/auth/:provider/callback",
name: "auth-callback",
component: () =>
import(/* webpackChunkName: "ProviderValidation" */ "@/views/User/ProviderValidation.vue"),
import(
/* webpackChunkName: "ProviderValidation" */ "@/views/User/ProviderValidation.vue"
),
},
{
path: "/welcome/:step?",
name: RouteName.WELCOME_SCREEN,
component: () =>
import(/* webpackChunkName: "WelcomeScreen" */ "@/views/User/SettingsOnboard.vue"),
import(
/* webpackChunkName: "WelcomeScreen" */ "@/views/User/SettingsOnboard.vue"
),
meta: { requiredAuth: true },
props: (route) => {
const step = Number.parseInt(route.params.step, 10);
@ -127,7 +144,8 @@ const router = new Router({
{
path: "/404",
name: RouteName.PAGE_NOT_FOUND,
component: () => import(/* webpackChunkName: "search" */ "../views/PageNotFound.vue"),
component: () =>
import(/* webpackChunkName: "search" */ "../views/PageNotFound.vue"),
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 {
SETTINGS = "SETTINGS",
@ -30,7 +31,8 @@ export enum SettingsRouteName {
export const settingsRoutes: RouteConfig[] = [
{
path: "/settings",
component: () => import(/* webpackChunkName: "Settings" */ "@/views/Settings.vue"),
component: (): Promise<EsModuleComponent> =>
import(/* webpackChunkName: "Settings" */ "@/views/Settings.vue"),
props: true,
meta: { requiredAuth: true },
redirect: { name: SettingsRouteName.ACCOUNT_SETTINGS },
@ -45,24 +47,30 @@ export const settingsRoutes: RouteConfig[] = [
{
path: "account/general",
name: SettingsRouteName.ACCOUNT_SETTINGS_GENERAL,
component: () =>
import(/* webpackChunkName: "AccountSettings" */ "@/views/Settings/AccountSettings.vue"),
component: (): Promise<EsModuleComponent> =>
import(
/* webpackChunkName: "AccountSettings" */ "@/views/Settings/AccountSettings.vue"
),
props: true,
meta: { requiredAuth: true },
},
{
path: "preferences",
name: SettingsRouteName.PREFERENCES,
component: () =>
import(/* webpackChunkName: "Preferences" */ "@/views/Settings/Preferences.vue"),
component: (): Promise<EsModuleComponent> =>
import(
/* webpackChunkName: "Preferences" */ "@/views/Settings/Preferences.vue"
),
props: true,
meta: { requiredAuth: true },
},
{
path: "notifications",
name: SettingsRouteName.NOTIFICATIONS,
component: () =>
import(/* webpackChunkName: "Notifications" */ "@/views/Settings/Notifications.vue"),
component: (): Promise<EsModuleComponent> =>
import(
/* webpackChunkName: "Notifications" */ "@/views/Settings/Notifications.vue"
),
props: true,
meta: { requiredAuth: true },
},
@ -75,61 +83,77 @@ export const settingsRoutes: RouteConfig[] = [
{
path: "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 },
},
{
path: "admin/settings",
name: SettingsRouteName.ADMIN_SETTINGS,
component: () =>
import(/* webpackChunkName: "AdminSettings" */ "@/views/Admin/Settings.vue"),
component: (): Promise<EsModuleComponent> =>
import(
/* webpackChunkName: "AdminSettings" */ "@/views/Admin/Settings.vue"
),
props: true,
meta: { requiredAuth: true },
},
{
path: "admin/users",
name: SettingsRouteName.USERS,
component: () => import(/* webpackChunkName: "Users" */ "@/views/Admin/Users.vue"),
component: (): Promise<EsModuleComponent> =>
import(/* webpackChunkName: "Users" */ "@/views/Admin/Users.vue"),
props: true,
meta: { requiredAuth: true },
},
{
path: "admin/users/:id",
name: SettingsRouteName.ADMIN_USER_PROFILE,
component: () =>
import(/* webpackChunkName: "AdminUserProfile" */ "@/views/Admin/AdminUserProfile.vue"),
component: (): Promise<EsModuleComponent> =>
import(
/* webpackChunkName: "AdminUserProfile" */ "@/views/Admin/AdminUserProfile.vue"
),
props: true,
meta: { requiredAuth: true },
},
{
path: "admin/profiles",
name: SettingsRouteName.PROFILES,
component: () =>
import(/* webpackChunkName: "AdminProfiles" */ "@/views/Admin/Profiles.vue"),
component: (): Promise<EsModuleComponent> =>
import(
/* webpackChunkName: "AdminProfiles" */ "@/views/Admin/Profiles.vue"
),
props: true,
meta: { requiredAuth: true },
},
{
path: "admin/profiles/:id",
name: SettingsRouteName.ADMIN_PROFILE,
component: () =>
import(/* webpackChunkName: "AdminProfile" */ "@/views/Admin/AdminProfile.vue"),
component: (): Promise<EsModuleComponent> =>
import(
/* webpackChunkName: "AdminProfile" */ "@/views/Admin/AdminProfile.vue"
),
props: true,
meta: { requiredAuth: true },
},
{
path: "admin/groups",
name: SettingsRouteName.ADMIN_GROUPS,
component: () =>
import(/* webpackChunkName: "GroupProfiles" */ "@/views/Admin/GroupProfiles.vue"),
component: (): Promise<EsModuleComponent> =>
import(
/* webpackChunkName: "GroupProfiles" */ "@/views/Admin/GroupProfiles.vue"
),
props: true,
meta: { requiredAuth: true },
},
{
path: "admin/groups/:id",
name: SettingsRouteName.ADMIN_GROUP_PROFILE,
component: () =>
import(/* webpackChunkName: "AdminGroupProfile" */ "@/views/Admin/AdminGroupProfile.vue"),
component: (): Promise<EsModuleComponent> =>
import(
/* webpackChunkName: "AdminGroupProfile" */ "@/views/Admin/AdminGroupProfile.vue"
),
props: true,
meta: { requiredAuth: true },
},
@ -137,21 +161,26 @@ export const settingsRoutes: RouteConfig[] = [
path: "admin/relays",
name: SettingsRouteName.RELAYS,
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 },
children: [
{
path: "followings",
name: SettingsRouteName.RELAY_FOLLOWINGS,
component: () =>
import(/* webpackChunkName: "Followings" */ "@/components/Admin/Followings.vue"),
component: (): Promise<EsModuleComponent> =>
import(
/* webpackChunkName: "Followings" */ "@/components/Admin/Followings.vue"
),
meta: { requiredAuth: true },
},
{
path: "followers",
name: SettingsRouteName.RELAY_FOLLOWERS,
component: () =>
import(/* webpackChunkName: "Followers" */ "@/components/Admin/Followers.vue"),
component: (): Promise<EsModuleComponent> =>
import(
/* webpackChunkName: "Followers" */ "@/components/Admin/Followers.vue"
),
meta: { requiredAuth: true },
},
],
@ -166,23 +195,30 @@ export const settingsRoutes: RouteConfig[] = [
{
path: "/moderation/reports/:filter?",
name: SettingsRouteName.REPORTS,
component: () =>
import(/* webpackChunkName: "ReportList" */ "@/views/Moderation/ReportList.vue"),
component: (): Promise<EsModuleComponent> =>
import(
/* webpackChunkName: "ReportList" */ "@/views/Moderation/ReportList.vue"
),
props: true,
meta: { requiredAuth: true },
},
{
path: "/moderation/report/:reportId",
name: SettingsRouteName.REPORT,
component: () => import(/* webpackChunkName: "Report" */ "@/views/Moderation/Report.vue"),
component: (): Promise<EsModuleComponent> =>
import(
/* webpackChunkName: "Report" */ "@/views/Moderation/Report.vue"
),
props: true,
meta: { requiredAuth: true },
},
{
path: "/moderation/logs",
name: SettingsRouteName.REPORT_LOGS,
component: () =>
import(/* webpackChunkName: "ModerationLogs" */ "@/views/Moderation/Logs.vue"),
component: (): Promise<EsModuleComponent> =>
import(
/* webpackChunkName: "ModerationLogs" */ "@/views/Moderation/Logs.vue"
),
props: true,
meta: { requiredAuth: true },
},
@ -195,21 +231,27 @@ export const settingsRoutes: RouteConfig[] = [
{
path: "/identity/create",
name: SettingsRouteName.CREATE_IDENTITY,
component: () =>
component: (): Promise<EsModuleComponent> =>
import(
/* 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 },
},
{
path: "/identity/update/:identityName?",
name: SettingsRouteName.UPDATE_IDENTITY,
component: () =>
component: (): Promise<EsModuleComponent> =>
import(
/* 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 },
},
],

View file

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

View file

@ -16,20 +16,25 @@ class AnonymousParticipationNotFoundError extends Error {
}
}
/**
* Fetch existing anonymous participations saved inside this browser
*/
function getLocalAnonymousParticipations(): Map<string, IAnonymousParticipation> {
return jsonToMap(
localStorage.getItem(ANONYMOUS_PARTICIPATIONS_LOCALSTORAGE_KEY) || mapToJson(new Map())
);
function jsonToMap(jsonStr: string): Map<string, IAnonymousParticipation> {
return new Map(JSON.parse(jsonStr));
}
function mapToJson(map: Map<any, any>): string {
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(
participations: Map<string, IAnonymousParticipation>
): Map<string, IAnonymousParticipation> {
// eslint-disable-next-line no-restricted-syntax
for (const [hashedUUID, { expiration }] of participations) {
if (expiration < new Date()) {
participations.delete(hashedUUID);
@ -56,9 +62,14 @@ function insertLocalAnonymousParticipation(
hashedUUID: string,
participation: IAnonymousParticipation
) {
const participations = purgeOldParticipations(getLocalAnonymousParticipations());
const participations = purgeOldParticipations(
getLocalAnonymousParticipations()
);
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 {
@ -67,59 +78,6 @@ function buildExpiration(event: IEvent): Date {
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> {
const encoder = new TextEncoder();
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("");
}
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 {
addLocalUnconfirmedAnonymousParticipation,
confirmLocalAnonymousParticipation,

View file

@ -1,12 +1,5 @@
import { IMedia } from "@/types/media.model";
export enum ActorType {
PERSON = "PERSON",
APPLICATION = "APPLICATION",
GROUP = "GROUP",
ORGANISATION = "ORGANISATION",
SERVICE = "SERVICE",
}
import type { IMedia } from "@/types/media.model";
import { ActorType } from "../enums";
export interface IActor {
id?: string;
@ -59,7 +52,9 @@ export class Actor implements IActor {
}
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 {
id?: string;

View file

@ -1,38 +1,15 @@
import { Actor, ActorType, IActor } from "./actor.model";
import { Paginate } from "../paginate";
import { IResource } from "../resource";
import { ITodoList } from "../todos";
import { IEvent } from "../event.model";
import { IDiscussion } from "../discussions";
import { IPerson } from "./person.model";
import { IPost } from "../post.model";
import { IAddress, Address } from "../address.model";
export enum MemberRole {
NOT_APPROVED = "NOT_APPROVED",
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;
}
import type { IActor } from "./actor.model";
import { Actor } from "./actor.model";
import type { Paginate } from "../paginate";
import type { IResource } from "../resource";
import type { IEvent } from "../event.model";
import type { IDiscussion } from "../discussions";
import type { IPost } from "../post.model";
import type { IAddress } from "../address.model";
import { Address } from "../address.model";
import { ActorType, Openness } from "../enums";
import type { IMember } from "./member.model";
import type { ITodoList } from "../todolist";
export interface IGroup extends IActor {
members: Paginate<IMember>;

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