Add admin interface to manage instances subscriptions
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
parent
0a96d70348
commit
334d66bf5d
1
.gitignore
vendored
1
.gitignore
vendored
@ -19,6 +19,7 @@ erl_crash.dump
|
||||
.env.test
|
||||
/.env
|
||||
.env.2
|
||||
.env.1
|
||||
|
||||
/setup_db.psql
|
||||
|
||||
|
@ -22,7 +22,7 @@ config :mobilizon, :instance,
|
||||
repository: Mix.Project.config()[:source_url],
|
||||
allow_relay: true,
|
||||
# Federation is to be activated with Mobilizon 1.0.0-beta.2
|
||||
federating: false,
|
||||
federating: true,
|
||||
remote_limit: 100_000,
|
||||
upload_limit: 10_000_000,
|
||||
avatar_upload_limit: 2_000_000,
|
||||
@ -63,7 +63,7 @@ config :mobilizon, MobilizonWeb.Upload,
|
||||
config :mobilizon, MobilizonWeb.Uploaders.Local, uploads: "uploads"
|
||||
|
||||
config :mobilizon, :media_proxy,
|
||||
enabled: false,
|
||||
enabled: true,
|
||||
proxy_opts: [
|
||||
redirect_on_failure: false,
|
||||
max_body_length: 25 * 1_048_576,
|
||||
@ -107,7 +107,9 @@ config :auto_linker,
|
||||
# TODO: Set to :no_scheme when it works properly
|
||||
validate_tld: true,
|
||||
class: false,
|
||||
strip_prefix: false
|
||||
strip_prefix: false,
|
||||
new_window: true,
|
||||
rel: "noopener noreferrer ugc"
|
||||
]
|
||||
|
||||
config :phoenix, :format_encoders, json: Jason, "activity-json": Jason
|
||||
@ -120,6 +122,8 @@ config :ex_cldr,
|
||||
config :http_signatures,
|
||||
adapter: Mobilizon.Service.HTTPSignatures.Signature
|
||||
|
||||
config :mobilizon, :activitypub, sign_object_fetches: true
|
||||
|
||||
config :mobilizon, Mobilizon.Service.Geospatial.Nominatim,
|
||||
endpoint:
|
||||
System.get_env("GEOSPATIAL_NOMINATIM_ENDPOINT") || "https://nominatim.openstreetmap.org",
|
||||
@ -155,7 +159,7 @@ config :mobilizon, :maps,
|
||||
config :mobilizon, Oban,
|
||||
repo: Mobilizon.Storage.Repo,
|
||||
prune: {:maxlen, 10_000},
|
||||
queues: [default: 10, search: 20]
|
||||
queues: [default: 10, search: 20, background: 5]
|
||||
|
||||
# Import environment specific config. This must remain at the bottom
|
||||
# of this file so it overrides the configuration defined above.
|
||||
|
104
docs/contribute/activity_pub.md
Normal file
104
docs/contribute/activity_pub.md
Normal file
@ -0,0 +1,104 @@
|
||||
# Federation
|
||||
|
||||
## ActivityPub
|
||||
|
||||
Mobilizon uses [ActivityPub](http://activitypub.rocks/) to federate content between instances. It only supports the server-to-server part of [the ActivityPub spec](https://www.w3.org/TR/activitypub/).
|
||||
|
||||
It implements the [HTTP signatures spec](https://tools.ietf.org/html/draft-cavage-http-signatures-12) for authentication of inbox deliveries, but doesn't implement Linked Data Signatures for forwarded payloads, and instead fetches content when needed.
|
||||
|
||||
To match usernames to actors, Mobilizon uses [WebFinger](https://tools.ietf.org/html/rfc7033).
|
||||
|
||||
## Instance subscriptions
|
||||
|
||||
Instances subscribe to each other through an internal actor named `relay@instance.tld` that publishes (through `Announce`) every created content to it's followers. Each content creation share is saved so that updates and deletes are correctly sent to every
|
||||
|
||||
## Activities
|
||||
|
||||
Supported Activity | Supported Object
|
||||
------------ | -------------
|
||||
`Accept` | `Follow`, `Join`
|
||||
`Announce` | `Object`
|
||||
`Create` | `Note`, `Event`
|
||||
`Delete` | `Object`
|
||||
`Flag` | `Object`
|
||||
`Follow` | `Object`
|
||||
`Reject` | `Follow`, `Join`
|
||||
`Remove` | `Note`, `Event`
|
||||
`Undo` | `Announce`, `Follow`
|
||||
`Update` | `Object`
|
||||
|
||||
## Extensions
|
||||
|
||||
### Event
|
||||
|
||||
The vocabulary for Event is based on [the Event object in ActivityStreams](https://www.w3.org/TR/activitystreams-vocabulary/#dfn-event), extended with :
|
||||
|
||||
* the [Event Schema](https://schema.org/Event) from Schema.org
|
||||
* some properties from [iCalendar](https://tools.ietf.org/html/rfc5545), such as `ical:status` (see [this issue](https://framagit.org/framasoft/mobilizon/issues/320))
|
||||
|
||||
The following properties are added.
|
||||
|
||||
#### repliesModeration
|
||||
|
||||
Disabling replies is [an ongoing issue with ActivityPub](https://github.com/w3c/activitypub/issues/319) so we use a temporary property.
|
||||
|
||||
See [the corresponding issue](https://framagit.org/framasoft/mobilizon/issues/321).
|
||||
|
||||
Accepted values: `allow_all`, `closed`, `moderated` (not used at the moment)
|
||||
|
||||
Example:
|
||||
```json
|
||||
{
|
||||
"@context": [
|
||||
"...",
|
||||
{
|
||||
"mz": "https://joinmobilizon.org/ns#",
|
||||
"repliesModerationOption": {
|
||||
"@id": "mz:repliesModerationOption",
|
||||
"@type": "mz:repliesModerationOptionType"
|
||||
},
|
||||
"repliesModerationOptionType": {
|
||||
"@id": "mz:repliesModerationOptionType",
|
||||
"@type": "rdfs:Class"
|
||||
}
|
||||
}
|
||||
],
|
||||
"...": "...",
|
||||
"repliesModerationOption": "allow_all",
|
||||
"type": "Event",
|
||||
"url": "http://mobilizon1.com/events/8cf76e9f-c426-4912-9cd6-c7030b969611"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
#### joinMode
|
||||
|
||||
Indicator of how new members may be able to join.
|
||||
|
||||
See [the corresponding issue](https://framagit.org/framasoft/mobilizon/issues/321).
|
||||
|
||||
Accepted values: `free`, `restricted`, `invite` (not used at the moment)
|
||||
|
||||
Example:
|
||||
```json
|
||||
{
|
||||
"@context": [
|
||||
"...",
|
||||
{
|
||||
"mz": "https://joinmobilizon.org/ns#",
|
||||
"joinMode": {
|
||||
"@id": "mz:joinMode",
|
||||
"@type": "mz:joinModeType"
|
||||
},
|
||||
"joinModeType": {
|
||||
"@id": "mz:joinModeType",
|
||||
"@type": "rdfs:Class"
|
||||
}
|
||||
}
|
||||
],
|
||||
"...": "...",
|
||||
"joinMode": "restricted",
|
||||
"type": "Event",
|
||||
"url": "http://mobilizon1.com/events/8cf76e9f-c426-4912-9cd6-c7030b969611"
|
||||
}
|
||||
```
|
@ -12,15 +12,21 @@
|
||||
"dev": "vue-cli-service build --watch",
|
||||
"styleguide": "vue-cli-service styleguidist",
|
||||
"styleguide:build": "vue-cli-service styleguidist:build",
|
||||
"vue-i18n-extract": "vue-i18n-extract"
|
||||
"vue-i18n-extract": "vue-i18n-extract",
|
||||
"graphql:get-schema": "graphql get-schema",
|
||||
"i18n-extract": "vue-i18n-extract report -v './src/**/*.?(ts|vue)' -l './src/i18n/en_US.json' -o output.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@absinthe/socket": "^0.2.1",
|
||||
"@absinthe/socket-apollo-link": "^0.2.1",
|
||||
"@mdi/font": "^4.5.95",
|
||||
"apollo-absinthe-upload-link": "^1.5.0",
|
||||
"apollo-cache-inmemory": "^1.5.1",
|
||||
"apollo-client": "^2.5.1",
|
||||
"apollo-link": "^1.2.11",
|
||||
"apollo-link-http": "^1.5.16",
|
||||
"apollo-link-ws": "^1.0.19",
|
||||
"apollo-utilities": "^1.3.2",
|
||||
"buefy": "^0.8.2",
|
||||
"graphql": "^14.5.8",
|
||||
"graphql-tag": "^2.10.1",
|
||||
@ -30,6 +36,7 @@
|
||||
"leaflet.locatecontrol": "^0.68.0",
|
||||
"lodash": "^4.17.11",
|
||||
"ngeohash": "^0.6.3",
|
||||
"phoenix": "^1.4.11",
|
||||
"register-service-worker": "^1.6.2",
|
||||
"tippy.js": "4.3.5",
|
||||
"tiptap": "^1.26.0",
|
||||
|
@ -26,7 +26,8 @@
|
||||
</div>
|
||||
<div class="media-content">
|
||||
<span class="title" ref="title">{{ actorDisplayName }}</span><br>
|
||||
<small class="has-text-grey">@{{ participant.actor.preferredUsername }}</small>
|
||||
<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>
|
||||
@ -41,7 +42,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import { IActor, IPerson, Person } from '@/types/actor';
|
||||
import { Person } from '@/types/actor';
|
||||
import { IParticipant, ParticipantRole } from '@/types/event.model';
|
||||
|
||||
@Component
|
||||
|
141
js/src/components/Admin/Followers.vue
Normal file
141
js/src/components/Admin/Followers.vue
Normal file
@ -0,0 +1,141 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-table
|
||||
v-show="relayFollowers.elements.length > 0"
|
||||
:data="relayFollowers.elements"
|
||||
:loading="$apollo.queries.relayFollowers.loading"
|
||||
ref="table"
|
||||
:checked-rows.sync="checkedRows"
|
||||
:is-row-checkable="(row) => row.id !== 3"
|
||||
detailed
|
||||
:show-detail-icon="false"
|
||||
paginated
|
||||
backend-pagination
|
||||
:total="relayFollowers.total"
|
||||
:per-page="perPage"
|
||||
@page-change="onPageChange"
|
||||
checkable
|
||||
checkbox-position="left">
|
||||
<template slot-scope="props">
|
||||
<b-table-column field="actor.id" label="ID" width="40" numeric>
|
||||
{{ props.row.actor.id }}
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column field="actor.type" :label="$t('Type')" width="80">
|
||||
<b-icon icon="lan" v-if="isInstance(props.row.actor)" />
|
||||
<b-icon icon="account-circle" v-else />
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column field="approved" :label="$t('Status')" width="100" sortable centered>
|
||||
<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>
|
||||
<a @click="toggle(props.row)" v-if="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="actor.updatedAt" :label="$t('Date')" sortable>
|
||||
{{ props.row.updatedAt | formatDateTimeString }}
|
||||
</b-table-column>
|
||||
</template>
|
||||
|
||||
<template slot="detail" slot-scope="props">
|
||||
<article>
|
||||
<div class="content">
|
||||
<strong>{{ props.row.actor.domain }}</strong>
|
||||
<small>@{{ props.row.actor.preferredUsername }}</small>
|
||||
<small>31m</small>
|
||||
<br>
|
||||
<p v-html="props.row.actor.summary" />
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<template slot="bottom-left" v-if="checkedRows.length > 0">
|
||||
<div class="buttons">
|
||||
<b-button @click="acceptRelays" type="is-success" v-if="checkedRowsHaveAtLeastOneToApprove">
|
||||
{{ $tc('No instance to approve|Approve instance|Approve {number} instances', checkedRows.length, { number: checkedRows.length }) }}
|
||||
</b-button>
|
||||
<b-button @click="rejectRelays" type="is-danger">
|
||||
{{ $tc('No instance to reject|Reject instance|Reject {number} instances', checkedRows.length, { number: checkedRows.length }) }}
|
||||
</b-button>
|
||||
</div>
|
||||
</template>
|
||||
</b-table>
|
||||
<b-message type="is-danger" v-if="relayFollowers.elements.length === 0">
|
||||
{{ $t("No instance follows your instance yet.") }}
|
||||
</b-message>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Mixins } from 'vue-property-decorator';
|
||||
import { ACCEPT_RELAY, REJECT_RELAY, RELAY_FOLLOWERS } from '@/graphql/admin';
|
||||
import { Paginate } from '@/types/paginate';
|
||||
import { IFollower } from '@/types/actor/follower.model';
|
||||
import RelayMixin from '@/mixins/relay';
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
relayFollowers: {
|
||||
query: RELAY_FOLLOWERS,
|
||||
fetchPolicy: 'cache-and-network',
|
||||
},
|
||||
},
|
||||
metaInfo() {
|
||||
return {
|
||||
title: this.$t('Followers') as string,
|
||||
titleTemplate: '%s | Mobilizon',
|
||||
};
|
||||
},
|
||||
})
|
||||
export default class Followers extends Mixins(RelayMixin) {
|
||||
relayFollowers: Paginate<IFollower> = { elements: [], total: 0 };
|
||||
|
||||
async acceptRelays() {
|
||||
await this.checkedRows.forEach((row: IFollower) => {
|
||||
this.acceptRelay(`${row.actor.preferredUsername}@${row.actor.domain}`);
|
||||
});
|
||||
}
|
||||
|
||||
async rejectRelays() {
|
||||
await this.checkedRows.forEach((row: IFollower) => {
|
||||
this.rejectRelay(`${row.actor.preferredUsername}@${row.actor.domain}`);
|
||||
});
|
||||
}
|
||||
|
||||
async acceptRelay(address: String) {
|
||||
await this.$apollo.mutate({
|
||||
mutation: ACCEPT_RELAY,
|
||||
variables: {
|
||||
address,
|
||||
},
|
||||
});
|
||||
await this.$apollo.queries.relayFollowers.refetch();
|
||||
this.checkedRows = [];
|
||||
}
|
||||
|
||||
async rejectRelay(address: String) {
|
||||
await this.$apollo.mutate({
|
||||
mutation: REJECT_RELAY,
|
||||
variables: {
|
||||
address,
|
||||
},
|
||||
});
|
||||
await this.$apollo.queries.relayFollowers.refetch();
|
||||
this.checkedRows = [];
|
||||
}
|
||||
|
||||
get checkedRowsHaveAtLeastOneToApprove(): boolean {
|
||||
return this.checkedRows.some(checkedRow => !checkedRow.approved);
|
||||
}
|
||||
}
|
||||
</script>
|
142
js/src/components/Admin/Followings.vue
Normal file
142
js/src/components/Admin/Followings.vue
Normal file
@ -0,0 +1,142 @@
|
||||
<template>
|
||||
<div>
|
||||
<form @submit="followRelay">
|
||||
<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: test.mobilizon.org')" />
|
||||
</p>
|
||||
<p class="control">
|
||||
<b-button type="is-primary" native-type="submit">{{ $t('Add an instance') }}</b-button>
|
||||
</p>
|
||||
</b-field>
|
||||
</b-field>
|
||||
</form>
|
||||
<b-table
|
||||
v-show="relayFollowings.elements.length > 0"
|
||||
:data="relayFollowings.elements"
|
||||
:loading="$apollo.queries.relayFollowings.loading"
|
||||
ref="table"
|
||||
:checked-rows.sync="checkedRows"
|
||||
:is-row-checkable="(row) => row.id !== 3"
|
||||
detailed
|
||||
:show-detail-icon="false"
|
||||
paginated
|
||||
backend-pagination
|
||||
:total="relayFollowings.total"
|
||||
:per-page="perPage"
|
||||
@page-change="onPageChange"
|
||||
checkable
|
||||
checkbox-position="left">
|
||||
<template slot-scope="props">
|
||||
<b-table-column field="targetActor.id" label="ID" width="40" numeric>
|
||||
{{ props.row.targetActor.id }}
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column field="targetActor.type" :label="$t('Type')" width="80">
|
||||
<b-icon icon="lan" v-if="isInstance(props.row.targetActor)" />
|
||||
<b-icon icon="account-circle" v-else />
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column field="approved" :label="$t('Status')" width="100" sortable centered>
|
||||
<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>
|
||||
<a @click="toggle(props.row)" v-if="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>
|
||||
{{ props.row.updatedAt | formatDateTimeString }}
|
||||
</b-table-column>
|
||||
</template>
|
||||
|
||||
<template slot="detail" slot-scope="props">
|
||||
<article>
|
||||
<div class="content">
|
||||
<strong>{{ props.row.targetActor.domain }}</strong>
|
||||
<small>@{{ props.row.targetActor.preferredUsername }}</small>
|
||||
<small>31m</small>
|
||||
<br>
|
||||
<p v-html="props.row.targetActor.summary" />
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<template slot="bottom-left" v-if="checkedRows.length > 0">
|
||||
<b-button @click="removeRelays" type="is-danger">
|
||||
{{ $tc('No instance to remove|Remove instance|Remove {number} instances', checkedRows.length, { number: checkedRows.length }) }}
|
||||
</b-button>
|
||||
</template>
|
||||
</b-table>
|
||||
<b-message type="is-danger" v-if="relayFollowings.elements.length === 0">
|
||||
{{ $t("You don't follow any instances yet.") }}
|
||||
</b-message>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Mixins } from 'vue-property-decorator';
|
||||
import { ADD_RELAY, RELAY_FOLLOWINGS, REMOVE_RELAY } from '@/graphql/admin';
|
||||
import { IFollower } from '@/types/actor/follower.model';
|
||||
import { Paginate } from '@/types/paginate';
|
||||
import RelayMixin from '@/mixins/relay';
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
relayFollowings: {
|
||||
query: RELAY_FOLLOWINGS,
|
||||
fetchPolicy: 'cache-and-network',
|
||||
},
|
||||
},
|
||||
metaInfo() {
|
||||
return {
|
||||
title: this.$t('Followings') as string,
|
||||
titleTemplate: '%s | Mobilizon',
|
||||
};
|
||||
},
|
||||
})
|
||||
export default class Followings extends Mixins(RelayMixin) {
|
||||
|
||||
relayFollowings: Paginate<IFollower> = { elements: [], total: 0 };
|
||||
newRelayAddress: String = '';
|
||||
|
||||
async followRelay(e) {
|
||||
e.preventDefault();
|
||||
await this.$apollo.mutate({
|
||||
mutation: ADD_RELAY,
|
||||
variables: {
|
||||
address: this.newRelayAddress,
|
||||
},
|
||||
// TODO: Handle cache update properly without refreshing
|
||||
});
|
||||
await this.$apollo.queries.relayFollowings.refetch();
|
||||
this.newRelayAddress = '';
|
||||
}
|
||||
|
||||
async removeRelays() {
|
||||
await this.checkedRows.forEach((row: IFollower) => {
|
||||
this.removeRelay(`${row.targetActor.preferredUsername}@${row.targetActor.domain}`);
|
||||
});
|
||||
}
|
||||
|
||||
async removeRelay(address: String) {
|
||||
await this.$apollo.mutate({
|
||||
mutation: REMOVE_RELAY,
|
||||
variables: {
|
||||
address,
|
||||
},
|
||||
});
|
||||
await this.$apollo.queries.relayFollowings.refetch();
|
||||
this.checkedRows = [];
|
||||
}
|
||||
}
|
||||
</script>
|
@ -11,7 +11,8 @@
|
||||
<div class="content">
|
||||
<span class="first-line" v-if="!comment.deletedAt">
|
||||
<strong>{{ comment.actor.name }}</strong>
|
||||
<small>@{{ comment.actor.preferredUsername }}</small>
|
||||
<small v-if="comment.actor.domain">@{{ comment.actor.preferredUsername }}@{{ comment.actor.domain }}</small>
|
||||
<small v-else>@{{ comment.actor.preferredUsername }}</small>
|
||||
<a class="comment-link has-text-grey" :href="commentId">
|
||||
<small>{{ timeago(new Date(comment.updatedAt)) }}</small>
|
||||
</a>
|
||||
@ -202,7 +203,7 @@ export default class Comment extends Vue {
|
||||
|
||||
timeago(dateTime): String {
|
||||
if (this.timeAgoInstance != null) {
|
||||
// @ts-ignore
|
||||
// @ts-ignore
|
||||
return this.timeAgoInstance.format(dateTime);
|
||||
}
|
||||
return '';
|
||||
@ -213,7 +214,7 @@ export default class Comment extends Vue {
|
||||
}
|
||||
|
||||
get commentFromOrganizer(): boolean {
|
||||
return this.event.organizerActor !== undefined && this.comment.actor.id === this.event.organizerActor.id;
|
||||
return this.event.organizerActor !== undefined && this.comment.actor && this.comment.actor.id === this.event.organizerActor.id;
|
||||
}
|
||||
|
||||
get commentId(): String {
|
||||
@ -230,6 +231,7 @@ export default class Comment extends Vue {
|
||||
title: this.$t('Report this comment'),
|
||||
comment: this.comment,
|
||||
onConfirm: this.reportComment,
|
||||
outsideDomain: this.comment.actor.domain,
|
||||
},
|
||||
});
|
||||
}
|
||||
@ -244,6 +246,7 @@ export default class Comment extends Vue {
|
||||
reportedId: this.comment.actor.id,
|
||||
commentsIds: [this.comment.id],
|
||||
content,
|
||||
forward,
|
||||
},
|
||||
});
|
||||
this.$buefy.notification.open({
|
||||
|
@ -221,7 +221,7 @@ export default class CommentTree extends Vue {
|
||||
data: { thread: replies },
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
// @ts-ignore
|
||||
const parentCommentIndex = oldComments.findIndex(oldComment => oldComment.id === comment.originComment.id);
|
||||
const parentComment = oldComments[parentCommentIndex];
|
||||
parentComment.replies = replies;
|
||||
|
@ -409,9 +409,9 @@ export default class EditorComponent extends Vue {
|
||||
}
|
||||
|
||||
replyToComment(comment: IComment) {
|
||||
console.log('called replyToComment', comment);
|
||||
const actorModel = new Actor(comment.actor);
|
||||
if (!this.editor) return;
|
||||
console.log(this.editor.commands);
|
||||
this.editor.commands.mention({ id: actorModel.id, label: actorModel.usernameWithDomain().substring(1) });
|
||||
this.editor.focus();
|
||||
}
|
||||
|
@ -112,7 +112,7 @@ export default class AddressAutoComplete extends Vue {
|
||||
addressData: IAddress[] = [];
|
||||
selected: IAddress = new Address();
|
||||
isFetching: boolean = false;
|
||||
queryText: string = this.value && (new Address(this.value)).fullName || '';
|
||||
queryText: string = (this.value && (new Address(this.value)).fullName) || '';
|
||||
addressModalActive: boolean = false;
|
||||
private gettingLocation: boolean = false;
|
||||
private location!: Position;
|
||||
@ -164,6 +164,7 @@ export default class AddressAutoComplete extends Vue {
|
||||
|
||||
@Watch('value')
|
||||
updateEditing() {
|
||||
if (!(this.value && this.value.id)) return;
|
||||
this.selected = this.value;
|
||||
const address = new Address(this.selected);
|
||||
this.queryText = `${address.poiInfos.name} ${address.poiInfos.alternativeName}`;
|
||||
|
@ -26,11 +26,11 @@ A button to set your participation
|
||||
<div class="participation-button">
|
||||
<b-dropdown aria-role="list" position="is-bottom-left" v-if="participation && participation.role === ParticipantRole.PARTICIPANT">
|
||||
<button class="button is-success" type="button" slot="trigger">
|
||||
<b-icon icon="check"></b-icon>
|
||||
<b-icon icon="check" />
|
||||
<template>
|
||||
<span>{{ $t('I participate') }}</span>
|
||||
</template>
|
||||
<b-icon icon="menu-down"></b-icon>
|
||||
<b-icon icon="menu-down" />
|
||||
</button>
|
||||
|
||||
<!-- <b-dropdown-item :value="false" aria-role="listitem">-->
|
||||
@ -45,11 +45,11 @@ A button to set your participation
|
||||
<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" type="button" slot="trigger">
|
||||
<b-icon icon="timer-sand-empty"></b-icon>
|
||||
<b-icon icon="timer-sand-empty" />
|
||||
<template>
|
||||
<span>{{ $t('I participate') }}</span>
|
||||
</template>
|
||||
<b-icon icon="menu-down"></b-icon>
|
||||
<b-icon icon="menu-down" />
|
||||
</button>
|
||||
|
||||
<!-- <b-dropdown-item :value="false" aria-role="listitem">-->
|
||||
@ -73,7 +73,7 @@ A button to set your participation
|
||||
<template>
|
||||
<span>{{ $t('Participate') }}</span>
|
||||
</template>
|
||||
<b-icon icon="menu-down"></b-icon>
|
||||
<b-icon icon="menu-down" />
|
||||
</button>
|
||||
|
||||
<b-dropdown-item :value="true" aria-role="listitem" @click="joinEvent(currentActor)">
|
||||
@ -84,12 +84,12 @@ A button to set your participation
|
||||
</figure>
|
||||
</div>
|
||||
<div class="media-content">
|
||||
<span>{{ $t('with {identity}', {identity: currentActor.preferredUsername }) }}</span>
|
||||
<span>{{ $t('as {identity}', {identity: currentActor.preferredUsername }) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</b-dropdown-item>
|
||||
|
||||
<b-dropdown-item :value="false" aria-role="listitem" @click="joinModal">
|
||||
<b-dropdown-item :value="false" aria-role="listitem" @click="joinModal" v-if="identities.length > 1">
|
||||
{{ $t('with another identity…')}}
|
||||
</b-dropdown-item>
|
||||
</b-dropdown>
|
||||
@ -99,14 +99,32 @@ A button to set your participation
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import { IParticipant, ParticipantRole } from '@/types/event.model';
|
||||
import { IPerson } from '@/types/actor';
|
||||
import { IPerson, Person } from '@/types/actor';
|
||||
import { IDENTITIES } from '@/graphql/actor';
|
||||
import { CURRENT_USER_CLIENT } from '@/graphql/user';
|
||||
import { ICurrentUser } from '@/types/current-user.model';
|
||||
|
||||
@Component
|
||||
@Component({
|
||||
apollo: {
|
||||
currentUser: {
|
||||
query: CURRENT_USER_CLIENT,
|
||||
},
|
||||
identities: {
|
||||
query: IDENTITIES,
|
||||
update: ({ identities }) => identities ? identities.map(identity => new Person(identity)) : [],
|
||||
skip() {
|
||||
return this.currentUser.isLoggedIn === false;
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
export default class ParticipationButton extends Vue {
|
||||
@Prop({ required: true }) participation!: IParticipant;
|
||||
@Prop({ required: true }) currentActor!: IPerson;
|
||||
|
||||
ParticipantRole = ParticipantRole;
|
||||
currentUser!: ICurrentUser;
|
||||
identities: IPerson[] = [];
|
||||
|
||||
joinEvent(actor: IPerson) {
|
||||
this.$emit('joinEvent', actor);
|
||||
|
@ -16,7 +16,7 @@ import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
|
||||
@Component({
|
||||
beforeDestroy() {
|
||||
// @ts-ignore
|
||||
// @ts-ignore
|
||||
this.parentContainer.removeLayer(this);
|
||||
},
|
||||
})
|
||||
|
@ -20,7 +20,14 @@
|
||||
</div>
|
||||
|
||||
<div class="content columns">
|
||||
<div class="column is-one-quarter-desktop">Reported by <img v-if="report.reporter.avatar" class="image" :src="report.reporter.avatar.url" /> @{{ report.reporter.preferredUsername }}</div>
|
||||
<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}) }}
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ $t('Reported by {reporter}', { reporter: report.reporter.preferredUsername}) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="column" v-if="report.content">{{ report.content }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -29,10 +36,13 @@
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import { IReport } from '@/types/report.model';
|
||||
import { ActorType } from '@/types/actor';
|
||||
|
||||
@Component
|
||||
export default class ReportCard extends Vue {
|
||||
@Prop({ required: true }) report!: IReport;
|
||||
|
||||
ActorType = ActorType;
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
|
@ -44,11 +44,8 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p v-if="outsideDomain">
|
||||
{{ $t('The content came from another server. Transfer an anonymous copy of the report?') }}
|
||||
</p>
|
||||
|
||||
<div class="control" v-if="outsideDomain">
|
||||
<p>{{ $t('The content came from another server. Transfer an anonymous copy of the report?') }}</p>
|
||||
<b-switch v-model="forward">{{ $t('Transfer to {outsideDomain}', { outsideDomain }) }}</b-switch>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -19,3 +19,87 @@ export const DASHBOARD = gql`
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const RELAY_FRAGMENT = gql`
|
||||
fragment relayFragment on Follower {
|
||||
actor {
|
||||
id,
|
||||
preferredUsername,
|
||||
name,
|
||||
domain,
|
||||
type,
|
||||
summary
|
||||
},
|
||||
targetActor {
|
||||
id,
|
||||
preferredUsername,
|
||||
name,
|
||||
domain,
|
||||
type,
|
||||
summary
|
||||
},
|
||||
approved,
|
||||
insertedAt,
|
||||
updatedAt
|
||||
}
|
||||
`;
|
||||
|
||||
export const RELAY_FOLLOWERS = gql`
|
||||
query relayFollowers($page: Int, $limit: Int) {
|
||||
relayFollowers(page: $page, limit: $limit) {
|
||||
elements {
|
||||
...relayFragment
|
||||
},
|
||||
total
|
||||
}
|
||||
}
|
||||
${RELAY_FRAGMENT}
|
||||
`;
|
||||
|
||||
export const RELAY_FOLLOWINGS = gql`
|
||||
query relayFollowings($page: Int, $limit: Int) {
|
||||
relayFollowings(page: $page, limit: $limit) {
|
||||
elements {
|
||||
...relayFragment
|
||||
},
|
||||
total
|
||||
}
|
||||
}
|
||||
${RELAY_FRAGMENT}
|
||||
`;
|
||||
|
||||
export const ADD_RELAY = gql`
|
||||
mutation addRelay($address: String!) {
|
||||
addRelay(address: $address) {
|
||||
...relayFragment
|
||||
}
|
||||
}
|
||||
${RELAY_FRAGMENT}
|
||||
`;
|
||||
|
||||
export const REMOVE_RELAY = gql`
|
||||
mutation removeRelay($address: String!) {
|
||||
removeRelay(address: $address) {
|
||||
...relayFragment
|
||||
}
|
||||
}
|
||||
${RELAY_FRAGMENT}
|
||||
`;
|
||||
|
||||
export const ACCEPT_RELAY = gql`
|
||||
mutation acceptRelay($address: String!) {
|
||||
acceptRelay(address: $address) {
|
||||
...relayFragment
|
||||
}
|
||||
}
|
||||
${RELAY_FRAGMENT}
|
||||
`;
|
||||
|
||||
export const REJECT_RELAY = gql`
|
||||
mutation rejectRelay($address: String!) {
|
||||
rejectRelay(address: $address) {
|
||||
...relayFragment
|
||||
}
|
||||
}
|
||||
${RELAY_FRAGMENT}
|
||||
`;
|
||||
|
@ -13,6 +13,7 @@ export const COMMENT_FIELDS_FRAGMENT = gql`
|
||||
url
|
||||
},
|
||||
id,
|
||||
domain,
|
||||
preferredUsername,
|
||||
name
|
||||
},
|
||||
|
@ -10,7 +10,8 @@ const participantQuery = `
|
||||
url
|
||||
},
|
||||
name,
|
||||
id
|
||||
id,
|
||||
domain
|
||||
},
|
||||
event {
|
||||
id
|
||||
@ -441,3 +442,21 @@ export const EVENT_PERSON_PARTICIPATION = gql`
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const EVENT_PERSON_PARTICIPATION_SUBSCRIPTION_CHANGED = gql`
|
||||
subscription ($actorId: ID!, $eventId: ID!) {
|
||||
eventPersonParticipationChanged(personId: $actorId) {
|
||||
id,
|
||||
participations(eventId: $eventId) {
|
||||
id,
|
||||
role,
|
||||
actor {
|
||||
id
|
||||
},
|
||||
event {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
@ -18,7 +18,9 @@ export const REPORTS = gql`
|
||||
name,
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
},
|
||||
domain,
|
||||
type
|
||||
},
|
||||
event {
|
||||
id,
|
||||
@ -52,7 +54,9 @@ const REPORT_FRAGMENT = gql`
|
||||
name,
|
||||
avatar {
|
||||
url
|
||||
}
|
||||
},
|
||||
domain,
|
||||
type
|
||||
},
|
||||
event {
|
||||
id,
|
||||
@ -111,9 +115,10 @@ export const CREATE_REPORT = gql`
|
||||
$reporterId: ID!,
|
||||
$reportedId: ID!,
|
||||
$content: String,
|
||||
$commentsIds: [ID]
|
||||
$commentsIds: [ID],
|
||||
$forward: Boolean
|
||||
) {
|
||||
createReport(eventId: $eventId, reporterId: $reporterId, reportedId: $reportedId, content: $content, commentsIds: $commentsIds) {
|
||||
createReport(eventId: $eventId, reporterId: $reporterId, reportedId: $reportedId, content: $content, commentsIds: $commentsIds, forward: $forward) {
|
||||
id
|
||||
}
|
||||
}
|
||||
|
@ -322,7 +322,6 @@
|
||||
"resend confirmation email": "Bestätigungsmail erneut senden",
|
||||
"respect of the fundamental freedoms": "Respekt für die fundamentalen Freiheiten",
|
||||
"with another identity…": "mit einer anderen Identität.…",
|
||||
"with {identity}": "mit {identity}",
|
||||
"{approved} / {total} seats": "{approved} / {total} Plätze",
|
||||
"{count} participants": "Noch keine Teilnehmer | Ein Teilnehmer | {count} Teilnehmer",
|
||||
"{count} requests waiting": "{count} Anfragen ausstehend",
|
||||
|
@ -333,11 +333,59 @@
|
||||
"resend confirmation email": "resend confirmation email",
|
||||
"respect of the fundamental freedoms": "respect of the fundamental freedoms",
|
||||
"with another identity…": "with another identity…",
|
||||
"with {identity}": "with {identity}",
|
||||
"as {identity}": "as {identity}",
|
||||
"{approved} / {total} seats": "{approved} / {total} seats",
|
||||
"{count} participants": "No participants yet | One participant | {count} participants",
|
||||
"{count} requests waiting": "{count} requests waiting",
|
||||
"{license} guarantees {respect} of the people who will use it. Since {source}, anyone can audit it, which guarantees its transparency.": "{license} guarantees {respect} of the people who will use it. Since {source}, anyone can audit it, which guarantees its transparency.",
|
||||
"© The Mobilizon Contributors {date} - Made with Elixir, Phoenix, VueJS & with some love and some weeks": "© The Mobilizon Contributors {date} - Made with Elixir, Phoenix, VueJS & with some love and some weeks",
|
||||
"© The OpenStreetMap Contributors": "© The OpenStreetMap Contributors"
|
||||
"© The OpenStreetMap Contributors": "© The OpenStreetMap Contributors",
|
||||
"Reply": "Reply",
|
||||
"Accepted": "Accepted",
|
||||
"Pending": "Pending",
|
||||
"No instance to remove|Remove instance|Remove {number} instances": "No instances to remove|Remove instance|Remove {number} instances",
|
||||
"Dashboard": "Dashboard",
|
||||
"Reports": "Reports",
|
||||
"Mark as resolved": "Mark as resolved",
|
||||
"Reopen": "Reopen",
|
||||
"Close": "Close",
|
||||
"Reported identity": "Reported identity",
|
||||
"Reported by": "Reported by",
|
||||
"Reported": "Reported",
|
||||
"Updated": "Updated",
|
||||
"Open": "Open",
|
||||
"Closed": "Closed",
|
||||
"Resolved": "Resolved",
|
||||
"Unknown": "Unknown",
|
||||
"No comment": "No comment",
|
||||
"Notes": "Notes",
|
||||
"New note": "New note",
|
||||
"Add a note": "Add a note",
|
||||
"Deleting event": "Deleting event",
|
||||
"Are you sure you want to <b>delete</b> this event? This action cannot be undone. You may want to engage the conversation with the event creator or edit its event instead.": "Are you sure you want to <b>delete</b> this event? This action cannot be undone. You may want to engage the conversation with the event creator or edit its event instead.",
|
||||
"Delete Event": "Delete Event",
|
||||
"Type": "Type",
|
||||
"Domain": "Domain",
|
||||
"Date": "Date",
|
||||
"No instance to approve|Approve instance|Approve {number} instances": "No instance to approve|Approve instance|Approve {number} instances",
|
||||
"No instance to reject|Reject instance|Reject {number} instances": "No instance to reject|Reject instance|Reject {number} instances",
|
||||
"No instance follows your instance yet.": "No instance follows your instance yet.",
|
||||
"Followers": "Followers",
|
||||
"Add an instance": "Add an instance",
|
||||
"Ex: test.mobilizon.org": "Ex: test.mobilizon.org",
|
||||
"You don't follow any instances yet.": "You don't follow any instances yet.",
|
||||
"Followings": "Followings",
|
||||
"Instances": "Instances",
|
||||
"Reported by {reporter}": "Reported by {reporter}",
|
||||
"No open reports yet": "No open reports yet",
|
||||
"No resolved reports yet": "No resolved reports yet",
|
||||
"No closed reports yet": "No closed reports yet",
|
||||
"Reported by someone on {domain}": "Reported by someone on {domain}",
|
||||
"Your participation has been rejected": "Your participation has been rejected",
|
||||
"Your participation status has been changed": "Your participation status has been changed",
|
||||
"Unknown actor": "Unknown actor",
|
||||
"Deleting comment": "Deleting comment",
|
||||
"Are you sure you want to <b>delete</b> this comment? This action cannot be undone.": "Are you sure you want to <b>delete</b> this comment? This action cannot be undone.",
|
||||
"Delete Comment": "Delete Comment",
|
||||
"Comment deleted": "Comment deleted"
|
||||
}
|
@ -286,7 +286,7 @@
|
||||
"Update my event": "Éditer mon événement",
|
||||
"User accounts and every other data is currently deleted every 48 hours, so you may want to register again.": "Les comptes utilisateurs et toutes les autres données sont actuellement supprimées toutes les 48 heures, donc vous voulez peut-être vous inscrire à nouveau.",
|
||||
"Username": "Pseudo",
|
||||
"Users": "Utilisateurs",
|
||||
"Users": "Utilisateur⋅ice⋅s",
|
||||
"View a reply": "Aucune réponse | Voir une réponse | Voir {totalReplies} réponses",
|
||||
"View event page": "Voir la page de l'événement",
|
||||
"View everything": "Voir tout",
|
||||
@ -337,11 +337,57 @@
|
||||
"resend confirmation email": "réenvoyer l'email de confirmation",
|
||||
"respect of the fundamental freedoms": "le respect des libertés fondamentales",
|
||||
"with another identity…": "avec une autre identité…",
|
||||
"with {identity}": "avec {identity}",
|
||||
"as {identity}": "en tant que {identity}",
|
||||
"{approved} / {total} seats": "{approved} / {total} places",
|
||||
"{count} participants": "Aucun⋅e participant⋅e | Un⋅e participant⋅e | {count} participant⋅e⋅s",
|
||||
"{count} requests waiting": "Une demande en attente|{count} demandes en attente",
|
||||
"{license} guarantees {respect} of the people who will use it. Since {source}, anyone can audit it, which guarantees its transparency.": "{license} garantit {respect} des personnes qui l'utiliseront. Puisque {source}, il est publiquement auditable, ce qui garantit sa transparence.",
|
||||
"© The Mobilizon Contributors {date} - Made with Elixir, Phoenix, VueJS & with some love and some weeks": "© Les contributeurs de Mobilizon {date} - Fait avec Elixir, Phoenix, VueJS & et de l'amour et des semaines",
|
||||
"© The OpenStreetMap Contributors": "© Les Contributeur⋅ices OpenStreetMap"
|
||||
"© The OpenStreetMap Contributors": "© Les Contributeur⋅ices OpenStreetMap",
|
||||
"Reply": "Répondre",
|
||||
"Accepted": "Accepté",
|
||||
"Pending": "En attente",
|
||||
"No instance to remove|Remove instance|Remove {number} instances": "Pas d'instances à supprimer|Supprimer une instance|Supprimer {number} instances",
|
||||
"Mark as resolved": "Marquer comme résolu",
|
||||
"Reopen": "Réouvrir",
|
||||
"Close": "Fermé",
|
||||
"Reported identity": "Identité signalée",
|
||||
"Reported by": "Signalée par",
|
||||
"Reported": "Signalée",
|
||||
"Updated": "Mis à jour",
|
||||
"Open": "Ouvert",
|
||||
"Closed": "Fermé",
|
||||
"Resolved": "Résolu",
|
||||
"Unknown": "Inconnu",
|
||||
"No comment": "Pas de commentaire",
|
||||
"Notes": "Notes",
|
||||
"New note": "Nouvelle note",
|
||||
"Add a note": "Ajouter une note",
|
||||
"Deleting event": "Suppression de l'événement",
|
||||
"Are you sure you want to <b>delete</b> this event? This action cannot be undone. You may want to engage the conversation with the event creator or edit its event instead.": "Êtes-vous certain⋅e de vouloir <b>supprimer</b> cet événement ? Cette action n'est pas réversible. Vous voulez peut-être engager la conversation avec le créateur de l'événement ou bien éditer son événement à la place.",
|
||||
"Delete Event": "Supprimer l'événement",
|
||||
"Type": "Type",
|
||||
"Domain": "Domaine",
|
||||
"Date": "Date",
|
||||
"No instance to approve|Approve instance|Approve {number} instances": "Aucune instance à approuver|Approuver une instance|Approuver {number} instances",
|
||||
"No instance to reject|Reject instance|Reject {number} instances": "Aucune instance à rejetter|Rejetter une instance|Rejetter {number} instances",
|
||||
"No instance follows your instance yet.": "Aucune instance ne suit votre instance pour le moment.",
|
||||
"Followers": "Abonnés",
|
||||
"Add an instance": "Ajouter une instance",
|
||||
"Ex: test.mobilizon.org": "Ex: test.mobilizon.org",
|
||||
"You don't follow any instances yet.": "Vous ne suivez aucune instance pour le moment.",
|
||||
"Followings": "Abonnements",
|
||||
"Instances": "Instances",
|
||||
"Reported by {reporter}": "Signalé par {reporter}",
|
||||
"No open reports yet": "Aucun signalement ouvert pour le moment",
|
||||
"No resolved reports yet": "Aucun signalement résolu pour le moment",
|
||||
"No closed reports yet": "Aucun signalement fermé pour le moment",
|
||||
"Reported by someone on {domain}": "Signalé par quelqu'un depuis {domain}",
|
||||
"Your participation has been rejected": "Votre participation a été rejettée",
|
||||
"Your participation status has been changed": "Le statut de votre participation a été mis à jour",
|
||||
"Unknown actor": "Acteur inconnu",
|
||||
"Deleting comment": "Suppression du commentaire en cours",
|
||||
"Are you sure you want to <b>delete</b> this comment? This action cannot be undone.": "Êtes-vous certain⋅e de vouloir <b>supprimer</b> ce commentaire? Cette action ne peut pas être annulée.",
|
||||
"Delete Comment": "Supprimer le commentaire",
|
||||
"Comment deleted": "Commentaire supprimé"
|
||||
}
|
@ -321,7 +321,6 @@
|
||||
"resend confirmation email": "bevestigingsemail opnieuw versturen",
|
||||
"respect of the fundamental freedoms": "respect voor de fundamentele vrijheden",
|
||||
"with another identity…": "met een andere identiteit…",
|
||||
"with {identity}": "met {identity}",
|
||||
"{approved} / {total} seats": "{approved} / {total} plaatsen",
|
||||
"{count} participants": "Nog geen deelnemers | Eén deelnemer | {count} deelnemers",
|
||||
"{count} requests waiting": "{count} aanvragen in afwachting",
|
||||
|
@ -368,7 +368,6 @@
|
||||
"resend confirmation email": "tornar enviar lo messatge de confirmacion",
|
||||
"respect of the fundamental freedoms": "lo respet de las libertats fondamentalas",
|
||||
"with another identity…": "amb una autra identitat…",
|
||||
"with {identity}": "amb {identity}",
|
||||
"{actor}'s avatar": "Avatar de {actor}",
|
||||
"{approved} / {total} seats": "{approved} / {total} plaças",
|
||||
"{count} participants": "Cap de participacion pel moment|Un participant|{count} participants",
|
||||
|
@ -324,7 +324,6 @@
|
||||
"resend confirmation email": "skicka bekräftelsemail igen",
|
||||
"respect of the fundamental freedoms": "respektera våra grundläggande friheter",
|
||||
"with another identity…": "med en annan identitet…",
|
||||
"with {identity}": "med {identity}",
|
||||
"{approved} / {total} seats": "{approved} / {total} platser",
|
||||
"{count} participants": "Inga deltagande ännu|En deltagande|{count} deltagande",
|
||||
"{count} requests waiting": "{count} förfrågningar väntar",
|
||||
|
44
js/src/mixins/relay.ts
Normal file
44
js/src/mixins/relay.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
import { Refs } from '@/shims-vue';
|
||||
import { ActorType, IActor } from '@/types/actor';
|
||||
import { IFollower } from '@/types/actor/follower.model';
|
||||
|
||||
@Component
|
||||
export default class RelayMixin extends Vue {
|
||||
$refs!: Refs<{
|
||||
table: any,
|
||||
}>;
|
||||
|
||||
checkedRows: IFollower[] = [];
|
||||
page: number = 1;
|
||||
perPage: number = 2;
|
||||
|
||||
toggle(row) {
|
||||
this.$refs.table.toggleDetails(row);
|
||||
}
|
||||
|
||||
async onPageChange(page: number) {
|
||||
this.page = page;
|
||||
await this.$apollo.queries.relayFollowings.fetchMore({
|
||||
variables: {
|
||||
page: this.page,
|
||||
limit: this.perPage,
|
||||
},
|
||||
updateQuery: (previousResult, { fetchMoreResult }) => {
|
||||
if (!fetchMoreResult) return previousResult;
|
||||
const newFollowings = fetchMoreResult.relayFollowings.elements;
|
||||
return {
|
||||
relayFollowings: {
|
||||
__typename: previousResult.relayFollowings.__typename,
|
||||
total: previousResult.relayFollowings.total,
|
||||
elements: [...previousResult.relayFollowings.elements, ...newFollowings],
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
isInstance(actor: IActor): boolean {
|
||||
return actor.type === ActorType.APPLICATION && actor.preferredUsername === 'relay';
|
||||
}
|
||||
}
|
@ -1,10 +1,12 @@
|
||||
import Vue from 'vue';
|
||||
import { ColorModifiers } from 'buefy/types/helpers';
|
||||
|
||||
declare module 'vue/types/vue' {
|
||||
interface Vue {
|
||||
$notifier: {
|
||||
success: (message: string) => void;
|
||||
error: (message: string) => void;
|
||||
info: (message: string) => void;
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -17,21 +19,23 @@ export class Notifier {
|
||||
}
|
||||
|
||||
success(message: string) {
|
||||
this.vue.prototype.$buefy.notification.open({
|
||||
message,
|
||||
duration: 5000,
|
||||
position: 'is-bottom-right',
|
||||
type: 'is-success',
|
||||
hasIcon: true,
|
||||
});
|
||||
this.notification(message, 'is-success');
|
||||
}
|
||||
|
||||
error(message: string) {
|
||||
this.notification(message, 'is-danger');
|
||||
}
|
||||
|
||||
info(message: string) {
|
||||
this.notification(message, 'is-info');
|
||||
}
|
||||
|
||||
private notification(message: string, type: ColorModifiers) {
|
||||
this.vue.prototype.$buefy.notification.open({
|
||||
message,
|
||||
duration: 5000,
|
||||
position: 'is-bottom-right',
|
||||
type: 'is-danger',
|
||||
type,
|
||||
hasIcon: true,
|
||||
});
|
||||
}
|
||||
|
@ -1,8 +1,14 @@
|
||||
import { RouteConfig } from 'vue-router';
|
||||
import Dashboard from '@/views/Admin/Dashboard.vue';
|
||||
import Follows from '@/views/Admin/Follows.vue';
|
||||
import Followings from '@/components/Admin/Followings.vue';
|
||||
import Followers from '@/components/Admin/Followers.vue';
|
||||
|
||||
export enum AdminRouteName {
|
||||
DASHBOARD = 'Dashboard',
|
||||
RELAYS = 'Relays',
|
||||
RELAY_FOLLOWINGS = 'Followings',
|
||||
RELAY_FOLLOWERS = 'Followers',
|
||||
}
|
||||
|
||||
export const adminRoutes: RouteConfig[] = [
|
||||
@ -13,4 +19,24 @@ export const adminRoutes: RouteConfig[] = [
|
||||
props: true,
|
||||
meta: { requiredAuth: true },
|
||||
},
|
||||
{
|
||||
path: '/admin/relays',
|
||||
name: AdminRouteName.RELAYS,
|
||||
redirect: { name: AdminRouteName.RELAY_FOLLOWINGS },
|
||||
component: Follows,
|
||||
children: [
|
||||
{
|
||||
path: 'followings',
|
||||
name: AdminRouteName.RELAY_FOLLOWINGS,
|
||||
component: Followings,
|
||||
},
|
||||
{
|
||||
path: 'followers',
|
||||
name: AdminRouteName.RELAY_FOLLOWERS,
|
||||
component: Followers,
|
||||
},
|
||||
],
|
||||
props: true,
|
||||
meta: { requiredAuth: true },
|
||||
},
|
||||
];
|
||||
|
@ -1,5 +1,13 @@
|
||||
import { IPicture } from '@/types/picture.model';
|
||||
|
||||
export enum ActorType {
|
||||
PERSON = 'PERSON',
|
||||
APPLICATION = 'APPLICATION',
|
||||
GROUP = 'GROUP',
|
||||
ORGANISATION = 'ORGANISATION',
|
||||
SERVICE = 'SERVICE',
|
||||
}
|
||||
|
||||
export interface IActor {
|
||||
id?: number;
|
||||
url: string;
|
||||
@ -10,6 +18,7 @@ export interface IActor {
|
||||
suspended: boolean;
|
||||
avatar: IPicture | null;
|
||||
banner: IPicture | null;
|
||||
type: ActorType;
|
||||
}
|
||||
|
||||
export class Actor implements IActor {
|
||||
@ -22,6 +31,7 @@ export class Actor implements IActor {
|
||||
summary: string = '';
|
||||
suspended: boolean = false;
|
||||
url: string = '';
|
||||
type: ActorType = ActorType.PERSON;
|
||||
|
||||
constructor (hash: IActor | {} = {}) {
|
||||
Object.assign(this, hash);
|
||||
|
8
js/src/types/actor/follower.model.ts
Normal file
8
js/src/types/actor/follower.model.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { IActor } from '@/types/actor/actor.model';
|
||||
|
||||
export interface IFollower {
|
||||