Merge branch 'fixes' into 'master'

Fix overflow on group membership cards

See merge request framasoft/mobilizon!940
This commit is contained in:
Thomas Citharel 2021-06-16 15:40:14 +00:00
commit 5eea5e2c81
44 changed files with 2245 additions and 1826 deletions

View File

@ -2,8 +2,9 @@
FROM node:16-alpine as assets FROM node:16-alpine as assets
RUN apk add --no-cache python3 build-base libwebp-tools bash imagemagick ncurses RUN apk add --no-cache python3 build-base libwebp-tools bash imagemagick ncurses
WORKDIR /build
COPY js . COPY js .
RUN ls -a
RUN yarn install \ RUN yarn install \
&& yarn run build && yarn run build
@ -24,7 +25,7 @@ COPY config/config.exs config/prod.exs ./config/
COPY config/docker.exs ./config/runtime.exs COPY config/docker.exs ./config/runtime.exs
COPY rel ./rel COPY rel ./rel
COPY support ./support COPY support ./support
COPY --from=assets ./priv/static ./priv/static COPY --from=assets ./build/priv/static ./priv/static
RUN mix phx.digest \ RUN mix phx.digest \
&& mix release && mix release

View File

@ -73,14 +73,14 @@
"@types/vuedraggable": "^2.23.0", "@types/vuedraggable": "^2.23.0",
"@typescript-eslint/eslint-plugin": "^4.18.0", "@typescript-eslint/eslint-plugin": "^4.18.0",
"@typescript-eslint/parser": "^4.18.0", "@typescript-eslint/parser": "^4.18.0",
"@vue/cli-plugin-babel": "~5.0.0-beta.0", "@vue/cli-plugin-babel": "~5.0.0-beta.2",
"@vue/cli-plugin-e2e-cypress": "~5.0.0-beta.0", "@vue/cli-plugin-e2e-cypress": "~5.0.0-beta.2",
"@vue/cli-plugin-eslint": "~5.0.0-beta.0", "@vue/cli-plugin-eslint": "~5.0.0-beta.2",
"@vue/cli-plugin-pwa": "~5.0.0-beta.0", "@vue/cli-plugin-pwa": "~5.0.0-beta.2",
"@vue/cli-plugin-router": "~5.0.0-beta.0", "@vue/cli-plugin-router": "~5.0.0-beta.2",
"@vue/cli-plugin-typescript": "~5.0.0-beta.0", "@vue/cli-plugin-typescript": "~5.0.0-beta.2",
"@vue/cli-plugin-unit-jest": "~5.0.0-beta.0", "@vue/cli-plugin-unit-jest": "~5.0.0-beta.2",
"@vue/cli-service": "~5.0.0-beta.0", "@vue/cli-service": "~5.0.0-beta.2",
"@vue/eslint-config-prettier": "^6.0.0", "@vue/eslint-config-prettier": "^6.0.0",
"@vue/eslint-config-typescript": "^7.0.0", "@vue/eslint-config-typescript": "^7.0.0",
"@vue/test-utils": "^1.1.0", "@vue/test-utils": "^1.1.0",

View File

@ -0,0 +1,48 @@
<template>
<div class="actor-inline">
<div class="actor-avatar">
<figure class="image is-24x24" v-if="actor.avatar">
<img class="is-rounded" :src="actor.avatar.url" alt="" />
</figure>
<b-icon v-else size="is-medium" icon="account-circle" />
</div>
<div class="actor-name">
<p>
{{ actor.name || `@${usernameWithDomain(actor)}` }}
</p>
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue, Prop } from "vue-property-decorator";
import { IActor, usernameWithDomain } from "../../types/actor";
@Component
export default class ActorInline extends Vue {
@Prop({ required: true, type: Object }) actor!: IActor;
usernameWithDomain = usernameWithDomain;
}
</script>
<style lang="scss" scoped>
div.actor-inline {
align-items: flex-start;
display: inline-flex;
text-align: inherit;
align-items: top;
div.actor-avatar {
flex-basis: auto;
flex-grow: 0;
flex-shrink: 0;
margin-right: 0.5rem;
}
div.actor-name {
flex-basis: auto;
flex-grow: 1;
flex-shrink: 1;
text-align: inherit;
}
}
</style>

View File

@ -41,7 +41,7 @@
></popover-actor-card ></popover-actor-card
></i18n ></i18n
> >
<small class="has-text-grey activity-date">{{ <small class="has-text-grey-dark activity-date">{{
activity.insertedAt | formatTimeString activity.insertedAt | formatTimeString
}}</small> }}</small>
</div> </div>

View File

@ -27,7 +27,7 @@
></popover-actor-card ></popover-actor-card
></i18n ></i18n
> >
<small class="has-text-grey activity-date">{{ <small class="has-text-grey-dark activity-date">{{
activity.insertedAt | formatTimeString activity.insertedAt | formatTimeString
}}</small> }}</small>
</div> </div>

View File

@ -34,7 +34,7 @@
v-for="detail in details" v-for="detail in details"
:key="detail" :key="detail"
tag="p" tag="p"
class="has-text-grey" class="has-text-grey-dark"
> >
<popover-actor-card <popover-actor-card
:actor="activity.author" :actor="activity.author"
@ -63,7 +63,7 @@
subjectParams.old_group_name subjectParams.old_group_name
}}</b> }}</b>
</i18n> </i18n>
<small class="has-text-grey activity-date">{{ <small class="has-text-grey-dark activity-date">{{
activity.insertedAt | formatTimeString activity.insertedAt | formatTimeString
}}</small> }}</small>
</div> </div>

View File

@ -34,7 +34,7 @@
></popover-actor-card ></popover-actor-card
></i18n ></i18n
> >
<small class="has-text-grey activity-date">{{ <small class="has-text-grey-dark activity-date">{{
activity.insertedAt | formatTimeString activity.insertedAt | formatTimeString
}}</small> }}</small>
</div> </div>

View File

@ -27,7 +27,7 @@
></popover-actor-card ></popover-actor-card
></i18n ></i18n
> >
<small class="has-text-grey activity-date">{{ <small class="has-text-grey-dark activity-date">{{
activity.insertedAt | formatTimeString activity.insertedAt | formatTimeString
}}</small> }}</small>
</div> </div>

View File

@ -37,7 +37,7 @@
></popover-actor-card ></popover-actor-card
></i18n ></i18n
> >
<small class="has-text-grey activity-date">{{ <small class="has-text-grey-dark activity-date">{{
activity.insertedAt | formatTimeString activity.insertedAt | formatTimeString
}}</small> }}</small>
</div> </div>

View File

@ -32,10 +32,10 @@
}}</span }}</span
> >
</div> </div>
<div class="has-text-grey" v-if="!discussion.lastComment.deletedAt"> <div class="has-text-grey-dark" v-if="!discussion.lastComment.deletedAt">
{{ htmlTextEllipsis }} {{ htmlTextEllipsis }}
</div> </div>
<div v-else class="has-text-grey"> <div v-else class="has-text-grey-dark">
{{ $t("[This comment has been deleted]") }} {{ $t("[This comment has been deleted]") }}
</div> </div>
</div> </div>
@ -98,10 +98,9 @@ export default class DiscussionListItem extends Vue {
.discussion-minimalist-title { .discussion-minimalist-title {
color: #3c376e; color: #3c376e;
font-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica, font-family: Roboto, Helvetica, Arial, serif;
Arial, serif; font-size: 16px;
font-size: 1.25rem; font-weight: 500;
font-weight: 700;
flex: 1; flex: 1;
} }
} }

View File

@ -99,7 +99,9 @@ export default class GroupMemberCard extends Vue {
} }
.media-content { .media-content {
overflow: hidden; ::v-deep .tags {
margin-bottom: 0;
}
} }
} }

View File

@ -55,20 +55,21 @@ section {
} }
div.group-section-title { div.group-section-title {
--title-color: $violet-2;
display: flex; display: flex;
align-items: stretch; align-items: stretch;
background: $secondary; background: $secondary;
color: #3a384c; color: var(--title-color);
&.privateSection { &.privateSection {
color: $violet-2; color: $purple-3;
background: $purple-2; background: $violet-2;
} }
::v-deep & > a { ::v-deep & > a {
align-self: center; align-self: center;
margin-right: 5px; margin-right: 5px;
color: $orange-3; color: var(--title-color);
} }
h2 { h2 {

View File

@ -30,6 +30,17 @@ import { IGroup } from "@/types/actor";
}, },
}, },
}, },
metaInfo() {
return {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
title: this.$t("Join group {group}", {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
group: this.groupTitle,
}) as string,
};
},
}) })
export default class JoinGroupWithAccount extends Vue { export default class JoinGroupWithAccount extends Vue {
@Prop({ type: String, required: true }) preferredUsername!: string; @Prop({ type: String, required: true }) preferredUsername!: string;
@ -40,6 +51,10 @@ export default class JoinGroupWithAccount extends Vue {
return this.group?.url; return this.group?.url;
} }
get groupTitle(): undefined | string {
return this.group?.name || this.group?.preferredUsername;
}
sentence = this.$t( sentence = this.$t(
"We will redirect you to your instance in order to interact with this group" "We will redirect you to your instance in order to interact with this group"
); );

View File

@ -0,0 +1,138 @@
<template>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">{{ $t("Share this group") }}</p>
</header>
<section class="modal-card-body is-flex" v-if="group">
<div class="container has-text-centered">
<b-notification
type="is-warning"
v-if="group.visibility !== GroupVisibility.PUBLIC"
:closable="false"
>
{{
$t(
"This group is accessible only through it's link. Be careful where you post this link."
)
}}
</b-notification>
<b-field>
<b-input ref="groupURLInput" :value="group.url" expanded />
<p class="control">
<b-tooltip
:label="$t('URL copied to clipboard')"
:active="showCopiedTooltip"
always
type="is-success"
position="is-left"
>
<b-button
type="is-primary"
icon-right="content-paste"
native-type="button"
@click="copyURL"
@keyup.enter="copyURL"
/>
</b-tooltip>
</p>
</b-field>
<div>
<!-- <b-icon icon="mastodon" size="is-large" type="is-primary" />-->
<a :href="twitterShareUrl" target="_blank" rel="nofollow noopener"
><b-icon icon="twitter" size="is-large" type="is-primary"
/></a>
<a :href="facebookShareUrl" target="_blank" rel="nofollow noopener"
><b-icon icon="facebook" size="is-large" type="is-primary"
/></a>
<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"
>
<span data-v-5e15e80a="" class="icon has-text-primary is-large">
<DiasporaLogo alt="diaspora-logo" />
</span>
</a>
<a :href="emailShareUrl" target="_blank" rel="nofollow noopener"
><b-icon icon="email" size="is-large" type="is-primary"
/></a>
</div>
</div>
</section>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue, Ref } from "vue-property-decorator";
import { GroupVisibility } from "@/types/enums";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import DiasporaLogo from "../../assets/diaspora-icon.svg?inline";
import { displayName, IGroup } from "@/types/actor";
@Component({
components: {
DiasporaLogo,
},
})
export default class ShareGroupModal extends Vue {
@Prop({ type: Object, required: true }) group!: IGroup;
@Ref("groupURLInput") readonly groupURLInput!: any;
GroupVisibility = GroupVisibility;
showCopiedTooltip = false;
get twitterShareUrl(): string {
return `https://twitter.com/intent/tweet?url=${encodeURIComponent(
this.group.url
)}&text=${displayName(this.group)}`;
}
get facebookShareUrl(): string {
return `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(
this.group.url
)}`;
}
get linkedInShareUrl(): string {
return `https://www.linkedin.com/shareArticle?mini=true&url=${encodeURIComponent(
this.group.url
)}&title=${displayName(this.group)}`;
}
get emailShareUrl(): string {
return `mailto:?to=&body=${this.group.url}&subject=${displayName(
this.group
)}`;
}
get diasporaShareUrl(): string {
return `https://share.diasporafoundation.org/?title=${encodeURIComponent(
displayName(this.group)
)}&url=${encodeURIComponent(this.group.url)}`;
}
copyURL(): void {
this.groupURLInput.$refs.input.select();
document.execCommand("copy");
this.showCopiedTooltip = true;
setTimeout(() => {
this.showCopiedTooltip = false;
}, 2000);
}
}
</script>
<style lang="scss" scoped>
.diaspora span svg {
height: 2rem;
width: 2rem;
}
</style>

View File

@ -12,27 +12,38 @@ import { IMedia } from "@/types/media.model";
import { Component, Prop, Vue } from "vue-property-decorator"; import { Component, Prop, Vue } from "vue-property-decorator";
import LazyImage from "../Image/LazyImage.vue"; import LazyImage from "../Image/LazyImage.vue";
const DEFAULT_CARD_URL = "/img/mobilizon_default_card.png";
const DEFAULT_BLURHASH = "MCHKI4El-P-U}+={R-WWoes,Iu-P=?R,xD";
const DEFAULT_WIDTH = 630;
const DEFAULT_HEIGHT = 350;
const DEFAULT_PICTURE = {
url: DEFAULT_CARD_URL,
metadata: {
width: DEFAULT_WIDTH,
height: DEFAULT_HEIGHT,
blurhash: DEFAULT_BLURHASH,
},
};
@Component({ @Component({
components: { components: {
LazyImage, LazyImage,
}, },
}) })
export default class LazyImageWrapper extends Vue { export default class LazyImageWrapper extends Vue {
@Prop({ required: true, default: null }) @Prop({ required: true })
picture!: IMedia | null; picture!: IMedia;
get pictureOrDefault(): Partial<IMedia> { get pictureOrDefault(): Partial<IMedia> {
if (this.picture === null) {
return DEFAULT_PICTURE;
}
return { return {
url: url: this?.picture?.url,
this?.picture === null
? "/img/mobilizon_default_card.png"
: this?.picture?.url,
metadata: { metadata: {
width: this?.picture?.metadata?.width || 630, width: this?.picture?.metadata?.width,
height: this?.picture?.metadata?.height || 350, height: this?.picture?.metadata?.height,
blurhash: blurhash: this?.picture?.metadata?.blurhash,
this?.picture?.metadata?.blurhash ||
"MCHKI4El-P-U}+={R-WWoes,Iu-P=?R,xD",
}, },
}; };
} }

View File

@ -112,7 +112,12 @@
<span @click="setIdentity(identity)"> <span @click="setIdentity(identity)">
<div class="media-left"> <div class="media-left">
<figure class="image is-32x32" v-if="identity.avatar"> <figure class="image is-32x32" v-if="identity.avatar">
<img class="is-rounded" :src="identity.avatar.url" alt /> <img
class="is-rounded"
loading="lazy"
:src="identity.avatar.url"
alt
/>
</figure> </figure>
<b-icon v-else size="is-medium" icon="account-circle" /> <b-icon v-else size="is-medium" icon="account-circle" />
</div> </div>
@ -133,11 +138,6 @@
:to="{ name: RouteName.UPDATE_IDENTITY }" :to="{ name: RouteName.UPDATE_IDENTITY }"
>{{ $t("My account") }}</b-navbar-item >{{ $t("My account") }}</b-navbar-item
> >
<!-- <b-navbar-item tag="router-link" :to="{ name: RouteName.CREATE_GROUP }">-->
<!-- {{ $t('Create group') }}-->
<!-- </b-navbar-item>-->
<b-navbar-item <b-navbar-item
v-if="currentUser.role === ICurrentUserRole.ADMINISTRATOR" v-if="currentUser.role === ICurrentUserRole.ADMINISTRATOR"
tag="router-link" tag="router-link"

View File

@ -5,7 +5,7 @@
> >
<div class="title-info-wrapper"> <div class="title-info-wrapper">
<p class="post-minimalist-title">{{ post.title }}</p> <p class="post-minimalist-title">{{ post.title }}</p>
<small class="has-text-grey">{{ <small class="has-text-grey-dark">{{
formatDistanceToNow(new Date(post.publishAt || post.insertedAt), { formatDistanceToNow(new Date(post.publishAt || post.insertedAt), {
locale: $dateFnsLocale, locale: $dateFnsLocale,
addSuffix: true, addSuffix: true,
@ -43,10 +43,9 @@ export default class PostListItem extends Vue {
.post-minimalist-title { .post-minimalist-title {
color: #3c376e; color: #3c376e;
font-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica, Arial, font-family: Roboto, Helvetica, Arial, serif;
serif; font-size: 16px;
font-size: 1rem; font-weight: 500;
font-weight: 700;
overflow: hidden; overflow: hidden;
display: -webkit-box; display: -webkit-box;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;

View File

@ -23,6 +23,7 @@
<div class="title-wrapper"> <div class="title-wrapper">
<img <img
class="favicon" class="favicon"
alt=""
v-if="resource.metadata && resource.metadata.faviconUrl" v-if="resource.metadata && resource.metadata.faviconUrl"
:src="resource.metadata.faviconUrl" :src="resource.metadata.faviconUrl"
/> />
@ -31,7 +32,8 @@
<div class="metadata-wrapper"> <div class="metadata-wrapper">
<span class="host" v-if="!inline || preview">{{ urlHostname }}</span> <span class="host" v-if="!inline || preview">{{ urlHostname }}</span>
<span <span
class="published-at is-hidden-mobile" class="published-at"
:class="{ 'is-hidden-mobile': !inline }"
v-if="resource.updatedAt || resource.publishedAt" v-if="resource.updatedAt || resource.publishedAt"
>{{ >{{
(resource.updatedAt || resource.publishedAt) (resource.updatedAt || resource.publishedAt)

View File

@ -369,10 +369,10 @@ export const JOIN_EVENT = gql`
message: $message message: $message
locale: $locale locale: $locale
) { ) {
...ParticipantsQuery ...ParticipantQuery
} }
} }
${PARTICIPANTS_QUERY_FRAGMENT} ${PARTICIPANT_QUERY_FRAGMENT}
`; `;
export const LEAVE_EVENT = gql` export const LEAVE_EVENT = gql`

View File

@ -80,10 +80,22 @@ export const GROUP_FIELDS_FRAGMENTS = gql`
avatar { avatar {
id id
url url
name
metadata {
width
height
blurhash
}
} }
banner { banner {
id id
url url
name
metadata {
width
height
blurhash
}
} }
organizedEvents( organizedEvents(
afterDatetime: $afterDateTime afterDatetime: $afterDateTime

View File

@ -41,6 +41,11 @@ export const POST_FRAGMENT = gql`
id id
url url
name name
metadata {
height
width
blurhash
}
} }
} }
${TAG_FRAGMENT} ${TAG_FRAGMENT}

View File

@ -1043,5 +1043,9 @@
"User settings": "User settings", "User settings": "User settings",
"You changed your email or password": "You changed your email or password", "You changed your email or password": "You changed your email or password",
"Organized by you": "Organized by you", "Organized by you": "Organized by you",
"Move resource to the root folder": "Move resource to the root folder" "Move resource to the root folder": "Move resource to the root folder",
"Share this group": "Share this group",
"This group is accessible only through it's link. Be careful where you post this link.": "This group is accessible only through it's link. Be careful where you post this link.",
"{count} members": "No members|One member|{count} members",
"Share": "Share"
} }

View File

@ -1134,5 +1134,9 @@
"{username} was invited to {group}": "{username} a été invité à {group}", "{username} was invited to {group}": "{username} a été invité à {group}",
"© The OpenStreetMap Contributors": "© Les Contributeur⋅ices OpenStreetMap", "© The OpenStreetMap Contributors": "© Les Contributeur⋅ices OpenStreetMap",
"Organized by you": "Organisé par vous", "Organized by you": "Organisé par vous",
"Move resource to the root folder": "Déplacer la resource dans le dossier racine" "Move resource to the root folder": "Déplacer la resource dans le dossier racine",
"Share this group": "Partager ce groupe",
"This group is accessible only through it's link. Be careful where you post this link.": "Ce groupe est accessible uniquement à travers son lien. Faites attention où vous le diffusez.",
"{count} members": "Aucun membre|Un⋅e membre|{count} membres",
"Share": "Partager"
} }

View File

@ -5,13 +5,7 @@ import {
} from "@/graphql/actor"; } from "@/graphql/actor";
import { FETCH_GROUP } from "@/graphql/group"; import { FETCH_GROUP } from "@/graphql/group";
import RouteName from "@/router/name"; import RouteName from "@/router/name";
import { import { IActor, IGroup, IPerson, usernameWithDomain } from "@/types/actor";
Group,
IActor,
IGroup,
IPerson,
usernameWithDomain,
} from "@/types/actor";
import { MemberRole } from "@/types/enums"; import { MemberRole } from "@/types/enums";
import { Component, Vue } from "vue-property-decorator"; import { Component, Vue } from "vue-property-decorator";
@ -50,14 +44,14 @@ const now = new Date();
variables() { variables() {
return { return {
actorId: this.currentActor.id, actorId: this.currentActor.id,
group: this.group.preferredUsername, group: this.group?.preferredUsername,
}; };
}, },
skip() { skip() {
return ( return (
!this.currentActor || !this.currentActor ||
!this.currentActor.id || !this.currentActor.id ||
!this.group.preferredUsername !this.group?.preferredUsername
); );
}, },
}, },
@ -65,7 +59,7 @@ const now = new Date();
return ( return (
!this.currentActor || !this.currentActor ||
!this.currentActor.id || !this.currentActor.id ||
!this.group.preferredUsername !this.group?.preferredUsername
); );
}, },
}, },
@ -73,7 +67,7 @@ const now = new Date();
}, },
}) })
export default class GroupMixin extends Vue { export default class GroupMixin extends Vue {
group: IGroup = new Group(); group!: IGroup;
currentActor!: IActor; currentActor!: IActor;

View File

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

View File

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

View File

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

View File

@ -1,15 +1,15 @@
import { RouteConfig, Route } from "vue-router"; import { RouteConfig, Route } from "vue-router";
import { EsModuleComponent } from "vue/types/options"; import { ImportedComponent } from "vue/types/options";
const participations = (): Promise<EsModuleComponent> => const participations = (): Promise<ImportedComponent> =>
import( import(
/* webpackChunkName: "participations" */ "@/views/Event/Participants.vue" /* webpackChunkName: "participations" */ "@/views/Event/Participants.vue"
); );
const editEvent = (): Promise<EsModuleComponent> => const editEvent = (): Promise<ImportedComponent> =>
import(/* webpackChunkName: "edit-event" */ "@/views/Event/Edit.vue"); import(/* webpackChunkName: "edit-event" */ "@/views/Event/Edit.vue");
const event = (): Promise<EsModuleComponent> => const event = (): Promise<ImportedComponent> =>
import(/* webpackChunkName: "event" */ "@/views/Event/Event.vue"); import(/* webpackChunkName: "event" */ "@/views/Event/Event.vue");
const myEvents = (): Promise<EsModuleComponent> => const myEvents = (): Promise<ImportedComponent> =>
import(/* webpackChunkName: "my-events" */ "@/views/Event/MyEvents.vue"); import(/* webpackChunkName: "my-events" */ "@/views/Event/MyEvents.vue");
export enum EventRouteName { export enum EventRouteName {
@ -31,7 +31,7 @@ export const eventRoutes: RouteConfig[] = [
{ {
path: "/events/list/:location?", path: "/events/list/:location?",
name: EventRouteName.EVENT_LIST, name: EventRouteName.EVENT_LIST,
component: (): Promise<EsModuleComponent> => component: (): Promise<ImportedComponent> =>
import(/* webpackChunkName: "EventList" */ "@/views/Event/EventList.vue"), import(/* webpackChunkName: "EventList" */ "@/views/Event/EventList.vue"),
meta: { requiredAuth: false }, meta: { requiredAuth: false },
}, },
@ -83,35 +83,35 @@ export const eventRoutes: RouteConfig[] = [
{ {
path: "/events/:uuid/participate", path: "/events/:uuid/participate",
name: EventRouteName.EVENT_PARTICIPATE_LOGGED_OUT, name: EventRouteName.EVENT_PARTICIPATE_LOGGED_OUT,
component: (): Promise<EsModuleComponent> => component: (): Promise<ImportedComponent> =>
import("../components/Participation/UnloggedParticipation.vue"), import("../components/Participation/UnloggedParticipation.vue"),
props: true, props: true,
}, },
{ {
path: "/events/:uuid/participate/with-account", path: "/events/:uuid/participate/with-account",
name: EventRouteName.EVENT_PARTICIPATE_WITH_ACCOUNT, name: EventRouteName.EVENT_PARTICIPATE_WITH_ACCOUNT,
component: (): Promise<EsModuleComponent> => component: (): Promise<ImportedComponent> =>
import("../components/Participation/ParticipationWithAccount.vue"), import("../components/Participation/ParticipationWithAccount.vue"),
props: true, props: true,
}, },
{ {
path: "/events/:uuid/participate/without-account", path: "/events/:uuid/participate/without-account",
name: EventRouteName.EVENT_PARTICIPATE_WITHOUT_ACCOUNT, name: EventRouteName.EVENT_PARTICIPATE_WITHOUT_ACCOUNT,
component: (): Promise<EsModuleComponent> => component: (): Promise<ImportedComponent> =>
import("../components/Participation/ParticipationWithoutAccount.vue"), import("../components/Participation/ParticipationWithoutAccount.vue"),
props: true, props: true,
}, },
{ {
path: "/participation/email/confirm/:token", path: "/participation/email/confirm/:token",
name: EventRouteName.EVENT_PARTICIPATE_CONFIRM, name: EventRouteName.EVENT_PARTICIPATE_CONFIRM,
component: (): Promise<EsModuleComponent> => component: (): Promise<ImportedComponent> =>
import("../components/Participation/ConfirmParticipation.vue"), import("../components/Participation/ConfirmParticipation.vue"),
props: true, props: true,
}, },
{ {
path: "/tag/:tag", path: "/tag/:tag",
name: EventRouteName.TAG, name: EventRouteName.TAG,
component: (): Promise<EsModuleComponent> => component: (): Promise<ImportedComponent> =>
import(/* webpackChunkName: "Search" */ "@/views/Search.vue"), import(/* webpackChunkName: "Search" */ "@/views/Search.vue"),
props: true, props: true,
meta: { requiredAuth: false }, meta: { requiredAuth: false },

View File

@ -1,5 +1,5 @@
import { RouteConfig, Route } from "vue-router"; import { RouteConfig, Route } from "vue-router";
import { EsModuleComponent } from "vue/types/options"; import { ImportedComponent } from "vue/types/options";
export enum GroupsRouteName { export enum GroupsRouteName {
TODO_LISTS = "TODO_LISTS", TODO_LISTS = "TODO_LISTS",
@ -21,16 +21,16 @@ export enum GroupsRouteName {
TIMELINE = "TIMELINE", TIMELINE = "TIMELINE",
} }
const resourceFolder = (): Promise<EsModuleComponent> => const resourceFolder = (): Promise<ImportedComponent> =>
import("@/views/Resources/ResourceFolder.vue"); import("@/views/Resources/ResourceFolder.vue");
const groupEvents = (): Promise<EsModuleComponent> => const groupEvents = (): Promise<ImportedComponent> =>
import(/* webpackChunkName: "groupEvents" */ "@/views/Event/GroupEvents.vue"); import(/* webpackChunkName: "groupEvents" */ "@/views/Event/GroupEvents.vue");
export const groupsRoutes: RouteConfig[] = [ export const groupsRoutes: RouteConfig[] = [
{ {
path: "/@:preferredUsername/todo-lists", path: "/@:preferredUsername/todo-lists",
name: GroupsRouteName.TODO_LISTS, name: GroupsRouteName.TODO_LISTS,
component: (): Promise<EsModuleComponent> => component: (): Promise<ImportedComponent> =>
import("@/views/Todos/TodoLists.vue"), import("@/views/Todos/TodoLists.vue"),
props: true, props: true,
meta: { requiredAuth: true }, meta: { requiredAuth: true },
@ -38,7 +38,7 @@ export const groupsRoutes: RouteConfig[] = [
{ {
path: "/todo-lists/:id", path: "/todo-lists/:id",
name: GroupsRouteName.TODO_LIST, name: GroupsRouteName.TODO_LIST,
component: (): Promise<EsModuleComponent> => component: (): Promise<ImportedComponent> =>
import("@/views/Todos/TodoList.vue"), import("@/views/Todos/TodoList.vue"),
props: true, props: true,
meta: { requiredAuth: true }, meta: { requiredAuth: true },
@ -46,7 +46,7 @@ export const groupsRoutes: RouteConfig[] = [
{ {
path: "/todo/:todoId", path: "/todo/:todoId",
name: GroupsRouteName.TODO, name: GroupsRouteName.TODO,
component: (): Promise<EsModuleComponent> => component: (): Promise<ImportedComponent> =>
import("@/views/Todos/Todo.vue"), import("@/views/Todos/Todo.vue"),
props: true, props: true,
meta: { requiredAuth: true }, meta: { requiredAuth: true },
@ -67,7 +67,7 @@ export const groupsRoutes: RouteConfig[] = [
}, },
{ {
path: "/@:preferredUsername/settings", path: "/@:preferredUsername/settings",
component: (): Promise<EsModuleComponent> => component: (): Promise<ImportedComponent> =>
import("@/views/Group/Settings.vue"), import("@/views/Group/Settings.vue"),
props: true, props: true,
meta: { requiredAuth: true }, meta: { requiredAuth: true },
@ -77,20 +77,20 @@ export const groupsRoutes: RouteConfig[] = [
{ {
path: "public", path: "public",
name: GroupsRouteName.GROUP_PUBLIC_SETTINGS, name: GroupsRouteName.GROUP_PUBLIC_SETTINGS,
component: (): Promise<EsModuleComponent> => component: (): Promise<ImportedComponent> =>
import("../views/Group/GroupSettings.vue"), import("../views/Group/GroupSettings.vue"),
}, },
{ {
path: "members", path: "members",
name: GroupsRouteName.GROUP_MEMBERS_SETTINGS, name: GroupsRouteName.GROUP_MEMBERS_SETTINGS,
component: (): Promise<EsModuleComponent> => component: (): Promise<ImportedComponent> =>
import("../views/Group/GroupMembers.vue"), import("../views/Group/GroupMembers.vue"),
props: true, props: true,
}, },
{ {
path: "followers", path: "followers",
name: GroupsRouteName.GROUP_FOLLOWERS_SETTINGS, name: GroupsRouteName.GROUP_FOLLOWERS_SETTINGS,
component: (): Promise<EsModuleComponent> => component: (): Promise<ImportedComponent> =>
import("../views/Group/GroupFollowers.vue"), import("../views/Group/GroupFollowers.vue"),
props: true, props: true,
}, },
@ -98,7 +98,7 @@ export const groupsRoutes: RouteConfig[] = [
}, },
{ {
path: "/@:preferredUsername/p/new", path: "/@:preferredUsername/p/new",
component: (): Promise<EsModuleComponent> => component: (): Promise<ImportedComponent> =>
import("@/views/Posts/Edit.vue"), import("@/views/Posts/Edit.vue"),
props: true, props: true,
name: GroupsRouteName.POST_CREATE, name: GroupsRouteName.POST_CREATE,
@ -106,7 +106,7 @@ export const groupsRoutes: RouteConfig[] = [
}, },
{ {
path: "/p/:slug/edit", path: "/p/:slug/edit",
component: (): Promise<EsModuleComponent> => component: (): Promise<ImportedComponent> =>
import("@/views/Posts/Edit.vue"), import("@/views/Posts/Edit.vue"),
props: (route: Route): Record<string, unknown> => ({ props: (route: Route): Record<string, unknown> => ({
...route.params, ...route.params,
@ -117,7 +117,7 @@ export const groupsRoutes: RouteConfig[] = [
}, },
{ {
path: "/p/:slug", path: "/p/:slug",
component: (): Promise<EsModuleComponent> => component: (): Promise<ImportedComponent> =>
import("@/views/Posts/Post.vue"), import("@/views/Posts/Post.vue"),
props: true, props: true,
name: GroupsRouteName.POST, name: GroupsRouteName.POST,
@ -125,7 +125,7 @@ export const groupsRoutes: RouteConfig[] = [
}, },
{ {
path: "/@:preferredUsername/p", path: "/@:preferredUsername/p",
component: (): Promise<EsModuleComponent> => component: (): Promise<ImportedComponent> =>
import("@/views/Posts/List.vue"), import("@/views/Posts/List.vue"),
props: true, props: true,
name: GroupsRouteName.POSTS, name: GroupsRouteName.POSTS,
@ -140,7 +140,7 @@ export const groupsRoutes: RouteConfig[] = [
}, },
{ {
path: "/@:preferredUsername/join", path: "/@:preferredUsername/join",
component: (): Promise<EsModuleComponent> => component: (): Promise<ImportedComponent> =>
import("@/components/Group/JoinGroupWithAccount.vue"), import("@/components/Group/JoinGroupWithAccount.vue"),
props: true, props: true,
name: GroupsRouteName.GROUP_JOIN, name: GroupsRouteName.GROUP_JOIN,
@ -149,7 +149,7 @@ export const groupsRoutes: RouteConfig[] = [
{ {
path: "/@:preferredUsername/timeline", path: "/@:preferredUsername/timeline",
name: GroupsRouteName.TIMELINE, name: GroupsRouteName.TIMELINE,
component: (): Promise<EsModuleComponent> => component: (): Promise<ImportedComponent> =>
import("@/views/Group/Timeline.vue"), import("@/views/Group/Timeline.vue"),
props: true, props: true,
meta: { requiredAuth: true }, meta: { requiredAuth: true },

View File

@ -2,7 +2,7 @@ import Vue from "vue";
import Router, { Route } from "vue-router"; import Router, { Route } from "vue-router";
import VueScrollTo from "vue-scrollto"; import VueScrollTo from "vue-scrollto";
import { PositionResult } from "vue-router/types/router.d"; import { PositionResult } from "vue-router/types/router.d";
import { EsModuleComponent } from "vue/types/options"; import { ImportedComponent } from "vue/types/options";
import Home from "../views/Home.vue"; import Home from "../views/Home.vue";
import { eventRoutes } from "./event"; import { eventRoutes } from "./event";
import { actorRoutes } from "./actor"; import { actorRoutes } from "./actor";
@ -46,7 +46,7 @@ export const routes = [
{ {
path: "/search", path: "/search",
name: RouteName.SEARCH, name: RouteName.SEARCH,
component: (): Promise<EsModuleComponent> => component: (): Promise<ImportedComponent> =>
import(/* webpackChunkName: "Search" */ "@/views/Search.vue"), import(/* webpackChunkName: "Search" */ "@/views/Search.vue"),
props: true, props: true,
meta: { requiredAuth: false }, meta: { requiredAuth: false },
@ -60,7 +60,7 @@ export const routes = [
{ {
path: "/about", path: "/about",
name: RouteName.ABOUT, name: RouteName.ABOUT,
component: (): Promise<EsModuleComponent> => component: (): Promise<ImportedComponent> =>
import(/* webpackChunkName: "about" */ "@/views/About.vue"), import(/* webpackChunkName: "about" */ "@/views/About.vue"),
meta: { requiredAuth: false }, meta: { requiredAuth: false },
redirect: { name: RouteName.ABOUT_INSTANCE }, redirect: { name: RouteName.ABOUT_INSTANCE },
@ -68,7 +68,7 @@ export const routes = [
{ {
path: "instance", path: "instance",
name: RouteName.ABOUT_INSTANCE, name: RouteName.ABOUT_INSTANCE,
component: (): Promise<EsModuleComponent> => component: (): Promise<ImportedComponent> =>
import( import(
/* webpackChunkName: "about" */ "@/views/About/AboutInstance.vue" /* webpackChunkName: "about" */ "@/views/About/AboutInstance.vue"
), ),
@ -76,28 +76,28 @@ export const routes = [
{ {
path: "/terms", path: "/terms",
name: RouteName.TERMS, name: RouteName.TERMS,
component: (): Promise<EsModuleComponent> => component: (): Promise<ImportedComponent> =>
import(/* webpackChunkName: "cookies" */ "@/views/About/Terms.vue"), import(/* webpackChunkName: "cookies" */ "@/views/About/Terms.vue"),
meta: { requiredAuth: false }, meta: { requiredAuth: false },
}, },
{ {
path: "/privacy", path: "/privacy",
name: RouteName.PRIVACY, name: RouteName.PRIVACY,
component: (): Promise<EsModuleComponent> => component: (): Promise<ImportedComponent> =>
import(/* webpackChunkName: "cookies" */ "@/views/About/Privacy.vue"), import(/* webpackChunkName: "cookies" */ "@/views/About/Privacy.vue"),
meta: { requiredAuth: false }, meta: { requiredAuth: false },
}, },
{ {
path: "/rules", path: "/rules",
name: RouteName.RULES, name: RouteName.RULES,
component: (): Promise<EsModuleComponent> => component: (): Promise<ImportedComponent> =>
import(/* webpackChunkName: "cookies" */ "@/views/About/Rules.vue"), import(/* webpackChunkName: "cookies" */ "@/views/About/Rules.vue"),
meta: { requiredAuth: false }, meta: { requiredAuth: false },
}, },
{ {
path: "/glossary", path: "/glossary",
name: RouteName.GLOSSARY, name: RouteName.GLOSSARY,
component: (): Promise<EsModuleComponent> => component: (): Promise<ImportedComponent> =>
import( import(
/* webpackChunkName: "cookies" */ "@/views/About/Glossary.vue" /* webpackChunkName: "cookies" */ "@/views/About/Glossary.vue"
), ),
@ -108,14 +108,14 @@ export const routes = [
{ {
path: "/interact", path: "/interact",
name: RouteName.INTERACT, name: RouteName.INTERACT,
component: (): Promise<EsModuleComponent> => component: (): Promise<ImportedComponent> =>
import(/* webpackChunkName: "interact" */ "@/views/Interact.vue"), import(/* webpackChunkName: "interact" */ "@/views/Interact.vue"),
meta: { requiredAuth: false }, meta: { requiredAuth: false },
}, },
{ {
path: "/auth/:provider/callback", path: "/auth/:provider/callback",
name: "auth-callback", name: "auth-callback",
component: (): Promise<EsModuleComponent> => component: (): Promise<ImportedComponent> =>
import( import(
/* webpackChunkName: "ProviderValidation" */ "@/views/User/ProviderValidation.vue" /* webpackChunkName: "ProviderValidation" */ "@/views/User/ProviderValidation.vue"
), ),
@ -123,7 +123,7 @@ export const routes = [
{ {
path: "/welcome/:step?", path: "/welcome/:step?",
name: RouteName.WELCOME_SCREEN, name: RouteName.WELCOME_SCREEN,
component: (): Promise<EsModuleComponent> => component: (): Promise<ImportedComponent> =>
import( import(
/* webpackChunkName: "WelcomeScreen" */ "@/views/User/SettingsOnboard.vue" /* webpackChunkName: "WelcomeScreen" */ "@/views/User/SettingsOnboard.vue"
), ),
@ -139,7 +139,7 @@ export const routes = [
{ {
path: "/404", path: "/404",
name: RouteName.PAGE_NOT_FOUND, name: RouteName.PAGE_NOT_FOUND,
component: (): Promise<EsModuleComponent> => component: (): Promise<ImportedComponent> =>
import( import(
/* webpackChunkName: "PageNotFound" */ "../views/PageNotFound.vue" /* webpackChunkName: "PageNotFound" */ "../views/PageNotFound.vue"
), ),

View File

@ -1,5 +1,5 @@
import { Route, RouteConfig } from "vue-router"; import { Route, RouteConfig } from "vue-router";
import { EsModuleComponent } from "vue/types/options"; import { ImportedComponent } from "vue/types/options";
export enum SettingsRouteName { export enum SettingsRouteName {
SETTINGS = "SETTINGS", SETTINGS = "SETTINGS",
@ -31,7 +31,7 @@ export enum SettingsRouteName {
export const settingsRoutes: RouteConfig[] = [ export const settingsRoutes: RouteConfig[] = [
{ {
path: "/settings", path: "/settings",
component: (): Promise<EsModuleComponent> => component: (): Promise<ImportedComponent> =>
import(/* webpackChunkName: "Settings" */ "@/views/Settings.vue"), import(/* webpackChunkName: "Settings" */ "@/views/Settings.vue"),
props: true, props: true,
meta: { requiredAuth: true }, meta: { requiredAuth: true },
@ -47,7 +47,7 @@ export const settingsRoutes: RouteConfig[] = [
{ {
path: "account/general", path: "account/general",
name: SettingsRouteName.ACCOUNT_SETTINGS_GENERAL, name: SettingsRouteName.ACCOUNT_SETTINGS_GENERAL,
component: (): Promise<EsModuleComponent> => component: (): Promise<ImportedComponent> =>
import( import(
/* webpackChunkName: "AccountSettings" */ "@/views/Settings/AccountSettings.vue" /* webpackChunkName: "AccountSettings" */ "@/views/Settings/AccountSettings.vue"
), ),
@ -57,7 +57,7 @@ export const settingsRoutes: RouteConfig[] = [
{ {
path: "preferences", path: "preferences",
name: SettingsRouteName.PREFERENCES, name: SettingsRouteName.PREFERENCES,
component: (): Promise<EsModuleComponent> => component: (): Promise<ImportedComponent> =>
import( import(
/* webpackChunkName: "Preferences" */ "@/views/Settings/Preferences.vue" /* webpackChunkName: "Preferences" */ "@/views/Settings/Preferences.vue"
), ),
@ -67,7 +67,7 @@ export const settingsRoutes: RouteConfig[] = [
{ {
path: "notifications", path: "notifications",
name: SettingsRouteName.NOTIFICATIONS, name: SettingsRouteName.NOTIFICATIONS,
component: (): Promise<EsModuleComponent> => component: (): Promise<ImportedComponent> =>
import( import(
/* webpackChunkName: "Notifications" */ "@/views/Settings/Notifications.vue" /* webpackChunkName: "Notifications" */ "@/views/Settings/Notifications.vue"
), ),
@ -83,7 +83,7 @@ export const settingsRoutes: RouteConfig[] = [
{ {
path: "admin/dashboard", path: "admin/dashboard",
name: SettingsRouteName.ADMIN_DASHBOARD, name: SettingsRouteName.ADMIN_DASHBOARD,
component: (): Promise<EsModuleComponent> => component: (): Promise<ImportedComponent> =>
import( import(
/* webpackChunkName: "Dashboard" */ "@/views/Admin/Dashboard.vue" /* webpackChunkName: "Dashboard" */ "@/views/Admin/Dashboard.vue"
), ),
@ -92,7 +92,7 @@ export const settingsRoutes: RouteConfig[] = [
{ {
path: "admin/settings", path: "admin/settings",
name: SettingsRouteName.ADMIN_SETTINGS, name: SettingsRouteName.ADMIN_SETTINGS,
component: (): Promise<EsModuleComponent> => component: (): Promise<ImportedComponent> =>
import( import(
/* webpackChunkName: "AdminSettings" */ "@/views/Admin/Settings.vue" /* webpackChunkName: "AdminSettings" */ "@/views/Admin/Settings.vue"
), ),
@ -102,7 +102,7 @@ export const settingsRoutes: RouteConfig[] = [
{ {
path: "admin/users", path: "admin/users",
name: SettingsRouteName.USERS, name: SettingsRouteName.USERS,
component: (): Promise<EsModuleComponent> => component: (): Promise<ImportedComponent> =>
import(/* webpackChunkName: "Users" */ "@/views/Admin/Users.vue"), import(/* webpackChunkName: "Users" */ "@/views/Admin/Users.vue"),
props: true, props: true,
meta: { requiredAuth: true }, meta: { requiredAuth: true },
@ -110,7 +110,7 @@ export const settingsRoutes: RouteConfig[] = [
{ {
path: "admin/users/:id", path: "admin/users/:id",
name: SettingsRouteName.ADMIN_USER_PROFILE, name: SettingsRouteName.ADMIN_USER_PROFILE,
component: (): Promise<EsModuleComponent> => component: (): Promise<ImportedComponent> =>
import( import(
/* webpackChunkName: "AdminUserProfile" */ "@/views/Admin/AdminUserProfile.vue" /* webpackChunkName: "AdminUserProfile" */ "@/views/Admin/AdminUserProfile.vue"
), ),
@ -120,7 +120,7 @@ export const settingsRoutes: RouteConfig[] = [
{ {
path: "admin/profiles", path: "admin/profiles",
name: SettingsRouteName.PROFILES, name: SettingsRouteName.PROFILES,
component: (): Promise<EsModuleComponent> => component: (): Promise<ImportedComponent> =>
import( import(
/* webpackChunkName: "AdminProfiles" */ "@/views/Admin/Profiles.vue" /* webpackChunkName: "AdminProfiles" */ "@/views/Admin/Profiles.vue"
), ),
@ -130,7 +130,7 @@ export const settingsRoutes: RouteConfig[] = [
{ {
path: "admin/profiles/:id", path: "admin/profiles/:id",
name: SettingsRouteName.ADMIN_PROFILE, name: SettingsRouteName.ADMIN_PROFILE,
component: (): Promise<EsModuleComponent> => component: (): Promise<ImportedComponent> =>
import( import(
/* webpackChunkName: "AdminProfile" */ "@/views/Admin/AdminProfile.vue" /* webpackChunkName: "AdminProfile" */ "@/views/Admin/AdminProfile.vue"
), ),
@ -140,7 +140,7 @@ export const settingsRoutes: RouteConfig[] = [
{ {
path: "admin/groups", path: "admin/groups",
name: SettingsRouteName.ADMIN_GROUPS, name: SettingsRouteName.ADMIN_GROUPS,
component: (): Promise<EsModuleComponent> => component: (): Promise<ImportedComponent> =>
import( import(
/* webpackChunkName: "GroupProfiles" */ "@/views/Admin/GroupProfiles.vue" /* webpackChunkName: "GroupProfiles" */ "@/views/Admin/GroupProfiles.vue"
), ),
@ -150,7 +150,7 @@ export const settingsRoutes: RouteConfig[] = [
{ {
path: "admin/groups/:id", path: "admin/groups/:id",
name: SettingsRouteName.ADMIN_GROUP_PROFILE, name: SettingsRouteName.ADMIN_GROUP_PROFILE,
component: (): Promise<EsModuleComponent> => component: (): Promise<ImportedComponent> =>
import( import(
/* webpackChunkName: "AdminGroupProfile" */ "@/views/Admin/AdminGroupProfile.vue" /* webpackChunkName: "AdminGroupProfile" */ "@/views/Admin/AdminGroupProfile.vue"
), ),
@ -161,14 +161,14 @@ export const settingsRoutes: RouteConfig[] = [
path: "admin/relays", path: "admin/relays",
name: SettingsRouteName.RELAYS, name: SettingsRouteName.RELAYS,
redirect: { name: SettingsRouteName.RELAY_FOLLOWINGS }, redirect: { name: SettingsRouteName.RELAY_FOLLOWINGS },
component: (): Promise<EsModuleComponent> => component: (): Promise<ImportedComponent> =>
import(/* webpackChunkName: "Follows" */ "@/views/Admin/Follows.vue"), import(/* webpackChunkName: "Follows" */ "@/views/Admin/Follows.vue"),
meta: { requiredAuth: true }, meta: { requiredAuth: true },
children: [ children: [
{ {
path: "followings", path: "followings",
name: SettingsRouteName.RELAY_FOLLOWINGS, name: SettingsRouteName.RELAY_FOLLOWINGS,
component: (): Promise<EsModuleComponent> => component: (): Promise<ImportedComponent> =>
import( import(
/* webpackChunkName: "Followings" */ "@/components/Admin/Followings.vue" /* webpackChunkName: "Followings" */ "@/components/Admin/Followings.vue"
), ),
@ -177,7 +177,7 @@ export const settingsRoutes: RouteConfig[] = [
{ {
path: "followers", path: "followers",
name: SettingsRouteName.RELAY_FOLLOWERS, name: SettingsRouteName.RELAY_FOLLOWERS,
component: (): Promise<EsModuleComponent> => component: (): Promise<ImportedComponent> =>
import( import(
/* webpackChunkName: "Followers" */ "@/components/Admin/Followers.vue" /* webpackChunkName: "Followers" */ "@/components/Admin/Followers.vue"
), ),
@ -195,7 +195,7 @@ export const settingsRoutes: RouteConfig[] = [
{ {
path: "/moderation/reports/:filter?", path: "/moderation/reports/:filter?",
name: SettingsRouteName.REPORTS, name: SettingsRouteName.REPORTS,
component: (): Promise<EsModuleComponent> => component: (): Promise<ImportedComponent> =>
import( import(
/* webpackChunkName: "ReportList" */ "@/views/Moderation/ReportList.vue" /* webpackChunkName: "ReportList" */ "@/views/Moderation/ReportList.vue"
), ),
@ -205,7 +205,7 @@ export const settingsRoutes: RouteConfig[] = [
{ {
path: "/moderation/report/:reportId", path: "/moderation/report/:reportId",
name: SettingsRouteName.REPORT, name: SettingsRouteName.REPORT,
component: (): Promise<EsModuleComponent> => component: (): Promise<ImportedComponent> =>
import( import(
/* webpackChunkName: "Report" */ "@/views/Moderation/Report.vue" /* webpackChunkName: "Report" */ "@/views/Moderation/Report.vue"
), ),
@ -215,7 +215,7 @@ export const settingsRoutes: RouteConfig[] = [
{ {
path: "/moderation/logs", path: "/moderation/logs",
name: SettingsRouteName.REPORT_LOGS, name: SettingsRouteName.REPORT_LOGS,
component: (): Promise<EsModuleComponent> => component: (): Promise<ImportedComponent> =>
import( import(
/* webpackChunkName: "ModerationLogs" */ "@/views/Moderation/Logs.vue" /* webpackChunkName: "ModerationLogs" */ "@/views/Moderation/Logs.vue"
), ),
@ -231,7 +231,7 @@ export const settingsRoutes: RouteConfig[] = [
{ {
path: "/identity/create", path: "/identity/create",
name: SettingsRouteName.CREATE_IDENTITY, name: SettingsRouteName.CREATE_IDENTITY,
component: (): Promise<EsModuleComponent> => component: (): Promise<ImportedComponent> =>
import( import(
/* webpackChunkName: "EditIdentity" */ "@/views/Account/children/EditIdentity.vue" /* webpackChunkName: "EditIdentity" */ "@/views/Account/children/EditIdentity.vue"
), ),
@ -244,7 +244,7 @@ export const settingsRoutes: RouteConfig[] = [
{ {
path: "/identity/update/:identityName?", path: "/identity/update/:identityName?",
name: SettingsRouteName.UPDATE_IDENTITY, name: SettingsRouteName.UPDATE_IDENTITY,
component: (): Promise<EsModuleComponent> => component: (): Promise<ImportedComponent> =>
import( import(
/* webpackChunkName: "EditIdentity" */ "@/views/Account/children/EditIdentity.vue" /* webpackChunkName: "EditIdentity" */ "@/views/Account/children/EditIdentity.vue"
), ),

View File

@ -1,6 +1,6 @@
import { beforeRegisterGuard } from "@/router/guards/register-guard"; import { beforeRegisterGuard } from "@/router/guards/register-guard";
import { Route, RouteConfig } from "vue-router"; import { Route, RouteConfig } from "vue-router";
import { EsModuleComponent } from "vue/types/options"; import { ImportedComponent } from "vue/types/options";
export enum UserRouteName { export enum UserRouteName {
REGISTER = "Register", REGISTER = "Register",
@ -17,7 +17,7 @@ export const userRoutes: RouteConfig[] = [
{ {
path: "/register/user", path: "/register/user",
name: UserRouteName.REGISTER, name: UserRouteName.REGISTER,
component: (): Promise<EsModuleComponent> => component: (): Promise<ImportedComponent> =>
import( import(
/* webpackChunkName: "RegisterUser" */ "@/views/User/Register.vue" /* webpackChunkName: "RegisterUser" */ "@/views/User/Register.vue"
), ),
@ -28,7 +28,7 @@ export const userRoutes: RouteConfig[] = [
{ {
path: "/register/profile", path: "/register/profile",
name: UserRouteName.REGISTER_PROFILE, name: UserRouteName.REGISTER_PROFILE,
component: (): Promise<EsModuleComponent> => component: (): Promise<ImportedComponent> =>
import( import(
/* webpackChunkName: "RegisterProfile" */ "@/views/Account/Register.vue" /* webpackChunkName: "RegisterProfile" */ "@/views/Account/Register.vue"
), ),
@ -42,7 +42,7 @@ export const userRoutes: RouteConfig[] = [
{ {
path: "/resend-instructions", path: "/resend-instructions",
name: UserRouteName.RESEND_CONFIRMATION, name: UserRouteName.RESEND_CONFIRMATION,
component: (): Promise<EsModuleComponent> => component: (): Promise<ImportedComponent> =>
import( import(
/* webpackChunkName: "ResendConfirmation" */ "@/views/User/ResendConfirmation.vue" /* webpackChunkName: "ResendConfirmation" */ "@/views/User/ResendConfirmation.vue"
), ),
@ -52,7 +52,7 @@ export const userRoutes: RouteConfig[] = [
{ {
path: "/password-reset/send", path: "/password-reset/send",
name: UserRouteName.SEND_PASSWORD_RESET, name: UserRouteName.SEND_PASSWORD_RESET,
component: (): Promise<EsModuleComponent> => component: (): Promise<ImportedComponent> =>
import( import(
/* webpackChunkName: "SendPasswordReset" */ "@/views/User/SendPasswordReset.vue" /* webpackChunkName: "SendPasswordReset" */ "@/views/User/SendPasswordReset.vue"
), ),
@ -62,7 +62,7 @@ export const userRoutes: RouteConfig[] = [
{ {
path: "/password-reset/:token", path: "/password-reset/:token",
name: UserRouteName.PASSWORD_RESET, name: UserRouteName.PASSWORD_RESET,
component: (): Promise<EsModuleComponent> => component: (): Promise<ImportedComponent> =>
import( import(
/* webpackChunkName: "PasswordReset" */ "@/views/User/PasswordReset.vue" /* webpackChunkName: "PasswordReset" */ "@/views/User/PasswordReset.vue"
), ),
@ -72,7 +72,7 @@ export const userRoutes: RouteConfig[] = [
{ {
path: "/validate/email/:token", path: "/validate/email/:token",
name: UserRouteName.EMAIL_VALIDATE, name: UserRouteName.EMAIL_VALIDATE,
component: (): Promise<EsModuleComponent> => component: (): Promise<ImportedComponent> =>
import( import(
/* webpackChunkName: "EmailValidate" */ "@/views/User/EmailValidate.vue" /* webpackChunkName: "EmailValidate" */ "@/views/User/EmailValidate.vue"
), ),
@ -82,7 +82,7 @@ export const userRoutes: RouteConfig[] = [
{ {
path: "/validate/:token", path: "/validate/:token",
name: UserRouteName.VALIDATE, name: UserRouteName.VALIDATE,
component: (): Promise<EsModuleComponent> => component: (): Promise<ImportedComponent> =>
import(/* webpackChunkName: "Validate" */ "@/views/User/Validate.vue"), import(/* webpackChunkName: "Validate" */ "@/views/User/Validate.vue"),
props: true, props: true,
meta: { requiresAuth: false }, meta: { requiresAuth: false },
@ -90,7 +90,7 @@ export const userRoutes: RouteConfig[] = [
{ {
path: "/login", path: "/login",
name: UserRouteName.LOGIN, name: UserRouteName.LOGIN,
component: (): Promise<EsModuleComponent> => component: (): Promise<ImportedComponent> =>
import(/* webpackChunkName: "Login" */ "@/views/User/Login.vue"), import(/* webpackChunkName: "Login" */ "@/views/User/Login.vue"),
props: true, props: true,
meta: { requiredAuth: false }, meta: { requiredAuth: false },

View File

@ -57,7 +57,8 @@ export class Actor implements IActor {
} }
export function usernameWithDomain(actor: IActor, force = false): string { export function usernameWithDomain(actor: IActor, force = false): string {
if (actor.domain) { if (!actor) return "";
if (actor?.domain) {
return `${actor.preferredUsername}@${actor.domain}`; return `${actor.preferredUsername}@${actor.domain}`;
} }
if (force) { if (force) {

View File

@ -7,7 +7,7 @@ import type { IDiscussion } from "../discussions";
import type { IPost } from "../post.model"; import type { IPost } from "../post.model";
import type { IAddress } from "../address.model"; import type { IAddress } from "../address.model";
import { Address } from "../address.model"; import { Address } from "../address.model";
import { ActorType, Openness } from "../enums"; import { ActorType, GroupVisibility, Openness } from "../enums";
import type { IMember } from "./member.model"; import type { IMember } from "./member.model";
import type { ITodoList } from "../todolist"; import type { ITodoList } from "../todolist";
import { IActivity } from "../activity.model"; import { IActivity } from "../activity.model";
@ -20,6 +20,7 @@ export interface IGroup extends IActor {
organizedEvents: Paginate<IEvent>; organizedEvents: Paginate<IEvent>;
physicalAddress: IAddress; physicalAddress: IAddress;
openness: Openness; openness: Openness;
visibility: GroupVisibility;
manuallyApprovesFollowers: boolean; manuallyApprovesFollowers: boolean;
activity: Paginate<IActivity>; activity: Paginate<IActivity>;
} }
@ -43,6 +44,7 @@ export class Group extends Actor implements IGroup {
this.patch(hash); this.patch(hash);
} }
visibility: GroupVisibility = GroupVisibility.PUBLIC;
activity: Paginate<IActivity> = { elements: [], total: 0 }; activity: Paginate<IActivity> = { elements: [], total: 0 };
openness: Openness = Openness.INVITE_ONLY; openness: Openness = Openness.INVITE_ONLY;

View File

@ -1443,8 +1443,9 @@ div.sidebar {
width: 100%; width: 100%;
.media-content { .media-content {
width: calc(100% - 32px - 1rem); width: calc(100% - 32px - 1rem);
max-width: 80vw;
p.has-text-grey { p.has-text-grey-dark {
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
} }

View File

@ -135,7 +135,7 @@ const EVENTS_PAGE_LIMIT = 10;
const { group } = this; const { group } = this;
return { return {
title: this.$t("{group} events", { title: this.$t("{group} events", {
group: group.name || usernameWithDomain(group), group: group?.name || usernameWithDomain(group),
}) as string, }) as string,
}; };
}, },

View File

@ -10,7 +10,7 @@
</li> </li>
<li class="is-active"> <li class="is-active">
<router-link <router-link
v-if="group.preferredUsername" v-if="group && group.preferredUsername"
:to="{ :to="{
name: RouteName.GROUP, name: RouteName.GROUP,
params: { preferredUsername: usernameWithDomain(group) }, params: { preferredUsername: usernameWithDomain(group) },
@ -44,105 +44,34 @@
) )
}} }}
</b-message> </b-message>
<header class="block-container presentation"> <header class="block-container presentation" v-if="group">
<div class="block-column media"> <div class="banner-container">
<div class="media-left"> <lazy-image-wrapper :picture="group.banner" />
</div>
<div class="header">
<div class="avatar-container">
<figure class="image is-128x128" v-if="group.avatar"> <figure class="image is-128x128" v-if="group.avatar">
<img class="is-rounded" :src="group.avatar.url" alt="" /> <img class="is-rounded" :src="group.avatar.url" alt="" />
</figure> </figure>
<b-icon v-else size="is-large" icon="account-group" /> <b-icon v-else size="is-large" icon="account-group" />
</div> </div>
<div class="media-content"> <div class="title-container">
<h1 v-if="group.name">{{ group.name }}</h1> <h1 v-if="group.name">{{ group.name }}</h1>
<b-skeleton v-else :animated="true" /> <b-skeleton v-else :animated="true" />
<small class="has-text-grey" v-if="group.preferredUsername" <small class="has-text-grey-dark" v-if="group.preferredUsername"
>@{{ usernameWithDomain(group) }}</small >@{{ usernameWithDomain(group) }}</small
> >
<b-skeleton v-else :animated="true" /> <b-skeleton v-else :animated="true" />
<br /> <br />
<div class="buttons">
<b-button
outlined
icon-left="timeline-text"
v-if="isCurrentActorAGroupMember"
tag="router-link"
:to="{
name: RouteName.TIMELINE,
params: { preferredUsername: usernameWithDomain(group) },
}"
>{{ $t("Activity") }}</b-button
>
<b-button
outlined
icon-left="cog"
v-if="isCurrentActorAGroupAdmin"
tag="router-link"
:to="{
name: RouteName.GROUP_PUBLIC_SETTINGS,
params: { preferredUsername: usernameWithDomain(group) },
}"
>{{ $t("Group settings") }}</b-button
>
<b-dropdown
class="menu-dropdown"
aria-role="list"
v-if="isCurrentActorAGroupMember"
position="is-bottom-left"
>
<b-button
slot="trigger"
outlined
role="button"
icon-right="dots-horizontal"
>
</b-button>
<b-dropdown-item
aria-role="listitem"
v-if="ableToReport"
@click="isReportModalActive = true"
>
<span>
<b-icon icon="flag" />
{{ $t("Report") }}
</span>
</b-dropdown-item>
<b-dropdown-item
aria-role="listitem"
v-if="isCurrentActorAGroupMember"
@click="leaveGroup"
>
<span>
<b-icon icon="exit-to-app" />
{{ $t("Leave") }}
</span>
</b-dropdown-item>
<hr class="dropdown-divider" />
<b-dropdown-item has-link aria-role="listitem">
<a
:href="`@${preferredUsername}/feed/atom`"
:title="$t('Atom feed for events and posts')"
>
<b-icon icon="rss" />
{{ $t("RSS/Atom Feed") }}
</a>
</b-dropdown-item>
<b-dropdown-item has-link aria-role="listitem">
<a
:href="`@${preferredUsername}/feed/ics`"
:title="$t('ICS feed for events')"
>
<b-icon icon="calendar-sync" />
{{ $t("ICS/WebCal Feed") }}
</a>
</b-dropdown-item>
</b-dropdown>
</div> </div>
</div> <div class="group-metadata">
</div> <div
<div class="block-column members" v-if="isCurrentActorAGroupMember"> class="block-column members"
v-if="isCurrentActorAGroupMember && !previewPublic"
>
<div> <div>
<figure <figure
class="image is-48x48" class="image is-32x32"
:title=" :title="
$t(`@{username} ({role})`, { $t(`@{username} ({role})`, {
username: usernameWithDomain(member.actor), username: usernameWithDomain(member.actor),
@ -158,11 +87,15 @@
v-if="member.actor.avatar" v-if="member.actor.avatar"
alt alt
/> />
<b-icon v-else size="is-large" icon="account-circle" /> <b-icon v-else size="is-medium" icon="account-circle" />
</figure> </figure>
</div> </div>
<p> <p>
{{ $t("{count} team members", { count: group.members.total }) }} {{
$tc("{count} members", group.members.total, {
count: group.members.total,
})
}}
<router-link <router-link
v-if="isCurrentActorAGroupAdmin" v-if="isCurrentActorAGroupAdmin"
:to="{ :to="{
@ -173,7 +106,7 @@
> >
</p> </p>
</div> </div>
<div class="block-column address" v-else> <!-- <div class="block-column address">
<address v-if="physicalAddress"> <address v-if="physicalAddress">
<p <p
class="addressDescription" class="addressDescription"
@ -257,15 +190,152 @@
</b-dropdown-item> </b-dropdown-item>
</b-dropdown> </b-dropdown>
</p> </p>
</div> </div> -->
<img <div class="buttons">
v-if="group.banner && group.banner.url" <b-button
:src="group.banner.url" outlined
alt="" icon-left="timeline-text"
v-if="isCurrentActorAGroupMember && !previewPublic"
tag="router-link"
:to="{
name: RouteName.TIMELINE,
params: { preferredUsername: usernameWithDomain(group) },
}"
>{{ $t("Activity") }}</b-button
>
<b-button
outlined
icon-left="cog"
v-if="isCurrentActorAGroupAdmin && !previewPublic"
tag="router-link"
:to="{
name: RouteName.GROUP_PUBLIC_SETTINGS,
params: { preferredUsername: usernameWithDomain(group) },
}"
>{{ $t("Group settings") }}</b-button
>
<b-tooltip
v-if="
(!isCurrentActorAGroupMember || previewPublic) &&
group.openness !== Openness.OPEN
"
:label="$t('This group is invite-only')"
position="is-bottom"
>
<b-button disabled type="is-primary">{{
$t("Join group")
}}</b-button></b-tooltip
>
<b-button
v-else-if="
(!isCurrentActorAGroupMember || previewPublic) &&
currentActor.id
"
@click="joinGroup"
type="is-primary"
:disabled="previewPublic"
>{{ $t("Join group") }}</b-button
>
<b-button
tag="router-link"
:to="{
name: RouteName.GROUP_JOIN,
params: { preferredUsername: usernameWithDomain(group) },
}"
v-else-if="!isCurrentActorAGroupMember || previewPublic"
:disabled="previewPublic"
type="is-primary"
>{{ $t("Join group") }}</b-button
>
<b-button
outlined
icon-left="share"
@click="triggerShare()"
v-if="!isCurrentActorAGroupMember || previewPublic"
>
{{ $t("Share") }}
</b-button>
<b-dropdown
class="menu-dropdown"
v-if="isCurrentActorAGroupMember || previewPublic"
position="is-bottom-left"
aria-role="menu"
>
<b-button
slot="trigger"
outlined
role="button"
icon-left="dots-horizontal"
aria-label="Other actions"
/> />
<b-dropdown-item aria-role="menuitem">
<b-switch v-model="previewPublic">{{
$t("Public preview")
}}</b-switch>
</b-dropdown-item>
<b-dropdown-item
v-if="!previewPublic"
aria-role="menuitem"
@click="triggerShare()"
>
<span>
<b-icon icon="share" />
{{ $t("Share") }}
</span>
</b-dropdown-item>
<hr
class="dropdown-divider"
v-if="isCurrentActorAGroupMember"
/>
<b-dropdown-item has-link aria-role="menuitem">
<a
:href="`@${preferredUsername}/feed/atom`"
:title="$t('Atom feed for events and posts')"
>
<b-icon icon="rss" />
{{ $t("RSS/Atom Feed") }}
</a>
</b-dropdown-item>
<b-dropdown-item has-link aria-role="menuitem">
<a
:href="`@${preferredUsername}/feed/ics`"
:title="$t('ICS feed for events')"
>
<b-icon icon="calendar-sync" />
{{ $t("ICS/WebCal Feed") }}
</a>
</b-dropdown-item>
<hr class="dropdown-divider" />
<b-dropdown-item
v-if="ableToReport"
aria-role="menuitem"
@click="isReportModalActive = true"
>
<span>
<b-icon icon="flag" />
{{ $t("Report") }}
</span>
</b-dropdown-item>
<b-dropdown-item
aria-role="menuitem"
v-if="isCurrentActorAGroupMember && !previewPublic"
@click="leaveGroup"
>
<span>
<b-icon icon="exit-to-app" />
{{ $t("Leave") }}
</span>
</b-dropdown-item>
</b-dropdown>
</div>
</div>
</div>
</header> </header>
</div> </div>
<div v-if="isCurrentActorAGroupMember" class="block-container"> <div
v-if="isCurrentActorAGroupMember && !previewPublic"
class="block-container"
>
<!-- Private things --> <!-- Private things -->
<div class="block-column"> <div class="block-column">
<!-- Group discussions --> <!-- Group discussions -->
@ -285,9 +355,9 @@
:discussion="discussion" :discussion="discussion"
/> />
</div> </div>
<div v-else class="content has-text-grey has-text-centered"> <empty-content v-else icon="chat" :inline="true">
<p>{{ $t("No discussions yet") }}</p> {{ $t("No discussions yet") }}
</div> </empty-content>
</template> </template>
<template v-slot:create> <template v-slot:create>
<router-link <router-link
@ -328,12 +398,9 @@
/> />
</div> </div>
</div> </div>
<div <empty-content v-else icon="link" :inline="true">
v-else-if="group" {{ $t("No resources yet") }}
class="content has-text-grey has-text-centered" </empty-content>
>
<p>{{ $t("No resources yet") }}</p>
</div>
</template> </template>
<template v-slot:create> <template v-slot:create>
<router-link <router-link
@ -351,7 +418,7 @@
<div class="block-column"> <div class="block-column">
<!-- Events --> <!-- Events -->
<group-section <group-section
:title="$t('Upcoming events')" :title="$t('Events')"
icon="calendar" icon="calendar"
:privateSection="false" :privateSection="false"
:route="{ :route="{
@ -371,12 +438,9 @@
class="organized-event" class="organized-event"
/> />
</div> </div>
<div <empty-content v-else-if="group" icon="calendar" :inline="true">
v-else-if="group" {{ $t("No public upcoming events") }}
class="content has-text-grey has-text-centered" </empty-content>
>
<p>{{ $t("No public upcoming events") }}</p>
</div>
<b-skeleton animated v-else></b-skeleton> <b-skeleton animated v-else></b-skeleton>
</template> </template>
<template v-slot:create> <template v-slot:create>
@ -409,12 +473,9 @@
:post="post" :post="post"
/> />
</div> </div>
<div <empty-content v-else-if="group" icon="bullhorn" :inline="true">
v-else-if="group" {{ $t("No posts yet") }}
class="content has-text-grey has-text-centered" </empty-content>
>
<p>{{ $t("No posts yet") }}</p>
</div>
</template> </template>
<template v-slot:create> <template v-slot:create>
<router-link <router-link
@ -433,16 +494,62 @@
<b-message v-else-if="!group && $apollo.loading === false" type="is-danger"> <b-message v-else-if="!group && $apollo.loading === false" type="is-danger">
{{ $t("No group found") }} {{ $t("No group found") }}
</b-message> </b-message>
<div v-else class="public-container"> <div v-else-if="group" class="public-container">
<aside class="group-metadata">
<div class="sticky">
<event-metadata-block :title="$t('Members')" icon="account-group">
{{
$tc("{count} members", group.members.total, {
count: group.members.total,
})
}}
</event-metadata-block>
<event-metadata-block
v-if="physicalAddress"
:title="$t('Location')"
:icon="
physicalAddress ? physicalAddress.poiInfos.poiIcon.icon : 'earth'
"
>
<div class="address-wrapper">
<span v-if="!physicalAddress">{{
$t("No address defined")
}}</span>
<div class="address" v-if="physicalAddress">
<div>
<address>
<p
class="addressDescription"
:title="physicalAddress.poiInfos.name"
>
{{ physicalAddress.poiInfos.name }}
</p>
<p class="has-text-grey-dark">
{{ physicalAddress.poiInfos.alternativeName }}
</p>
</address>
</div>
<span
class="map-show-button"
@click="showMap = !showMap"
v-if="physicalAddress.geom"
>{{ $t("Show map") }}</span
>
</div>
</div>
</event-metadata-block>
</div>
</aside>
<div class="main-content">
<section> <section>
<subtitle>{{ $t("About") }}</subtitle> <subtitle>{{ $t("About") }}</subtitle>
<div <div
v-html="group.summary" v-html="group.summary"
v-if="group.summary && group.summary !== '<p></p>'" v-if="group.summary && group.summary !== '<p></p>'"
/> />
<div v-else-if="group" class="content has-text-grey has-text-centered"> <empty-content v-else-if="group" icon="image-text" :inline="true">
<p>{{ $t("This group doesn't have a description yet.") }}</p> {{ $t("This group doesn't have a description yet.") }}
</div> </empty-content>
</section> </section>
<section> <section>
<subtitle>{{ $t("Upcoming events") }}</subtitle> <subtitle>{{ $t("Upcoming events") }}</subtitle>
@ -457,15 +564,9 @@
class="organized-event" class="organized-event"
/> />
</div> </div>
<div <empty-content v-else-if="group" icon="calendar" :inline="true">
v-else-if="group && group.organizedEvents.elements.length == 0" {{ $t("No public upcoming events") }}
class="content has-text-grey has-text-centered" </empty-content>
>
<p>{{ $t("No public upcoming events") }}</p>
</div>
<div v-else-if="group" class="content has-text-grey has-text-centered">
<p>{{ $t("No public upcoming events") }}</p>
</div>
<b-skeleton animated v-else-if="$apollo.loading"></b-skeleton> <b-skeleton animated v-else-if="$apollo.loading"></b-skeleton>
<router-link <router-link
v-if="group.organizedEvents.total > 0" v-if="group.organizedEvents.total > 0"
@ -486,9 +587,9 @@
:post="post" :post="post"
/> />
</div> </div>
<div v-else-if="group" class="content has-text-grey has-text-centered"> <empty-content v-else-if="group" icon="bullhorn" :inline="true">
<p>{{ $t("No posts yet") }}</p> {{ $t("No posts yet") }}
</div> </empty-content>
<b-skeleton animated v-else-if="$apollo.loading"></b-skeleton> <b-skeleton animated v-else-if="$apollo.loading"></b-skeleton>
<router-link <router-link
v-if="group.posts.total > 0" v-if="group.posts.total > 0"
@ -499,6 +600,7 @@
>{{ $t("View all posts") }}</router-link >{{ $t("View all posts") }}</router-link
> >
</section> </section>
</div>
<b-modal <b-modal
v-if="physicalAddress && physicalAddress.geom" v-if="physicalAddress && physicalAddress.geom"
:active.sync="showMap" :active.sync="showMap"
@ -518,6 +620,7 @@
:active.sync="isReportModalActive" :active.sync="isReportModalActive"
has-modal-card has-modal-card
ref="reportModal" ref="reportModal"
v-if="group"
> >
<report-modal <report-modal
:on-confirm="reportGroup" :on-confirm="reportGroup"
@ -526,13 +629,21 @@
@close="$refs.reportModal.close()" @close="$refs.reportModal.close()"
/> />
</b-modal> </b-modal>
<b-modal
v-if="group"
:active.sync="isShareModalActive"
has-modal-card
ref="shareModal"
>
<share-group-modal :group="group" />
</b-modal>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Prop, Watch } from "vue-property-decorator"; import { Component, Prop, Watch } from "vue-property-decorator";
import EventCard from "@/components/Event/EventCard.vue"; import EventCard from "@/components/Event/EventCard.vue";
import { IActor, usernameWithDomain } from "@/types/actor"; import { displayName, IActor, usernameWithDomain } from "@/types/actor";
import Subtitle from "@/components/Utils/Subtitle.vue"; import Subtitle from "@/components/Utils/Subtitle.vue";
import CompactTodo from "@/components/Todo/CompactTodo.vue"; import CompactTodo from "@/components/Todo/CompactTodo.vue";
import EventMinimalistCard from "@/components/Event/EventMinimalistCard.vue"; import EventMinimalistCard from "@/components/Event/EventMinimalistCard.vue";
@ -557,6 +668,9 @@ import GroupSection from "../../components/Group/GroupSection.vue";
import ReportModal from "../../components/Report/ReportModal.vue"; import ReportModal from "../../components/Report/ReportModal.vue";
import { PERSON_MEMBERSHIP_GROUP } from "@/graphql/actor"; import { PERSON_MEMBERSHIP_GROUP } from "@/graphql/actor";
import { LEAVE_GROUP } from "@/graphql/group"; import { LEAVE_GROUP } from "@/graphql/group";
import LazyImageWrapper from "../../components/Image/LazyImageWrapper.vue";
import EventMetadataBlock from "../../components/Event/EventMetadataBlock.vue";
import EmptyContent from "../../components/Utils/EmptyContent.vue";
@Component({ @Component({
apollo: { apollo: {
@ -574,8 +688,15 @@ import { LEAVE_GROUP } from "@/graphql/group";
GroupSection, GroupSection,
Invitations, Invitations,
ReportModal, ReportModal,
LazyImageWrapper,
EventMetadataBlock,
EmptyContent,
"map-leaflet": () => "map-leaflet": () =>
import(/* webpackChunkName: "map" */ "../../components/Map.vue"), import(/* webpackChunkName: "map" */ "../../components/Map.vue"),
ShareGroupModal: () =>
import(
/* webpackChunkName: "shareGroupModal" */ "../../components/Group/ShareGroupModal.vue"
),
}, },
metaInfo() { metaInfo() {
return { return {
@ -607,6 +728,10 @@ export default class Group extends mixins(GroupMixin) {
isReportModalActive = false; isReportModalActive = false;
isShareModalActive = false;
previewPublic = false;
@Watch("currentActor") @Watch("currentActor")
watchCurrentActor(currentActor: IActor, oldActor: IActor): void { watchCurrentActor(currentActor: IActor, oldActor: IActor): void {
if (currentActor.id && oldActor && currentActor.id !== oldActor.id) { if (currentActor.id && oldActor && currentActor.id !== oldActor.id) {
@ -717,14 +842,33 @@ export default class Group extends mixins(GroupMixin) {
} }
} }
triggerShare(): void {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-start
if (navigator.share) {
navigator
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
.share({
title: displayName(this.group),
url: this.group.url,
})
.then(() => console.log("Successful share"))
.catch((error: any) => console.log("Error sharing", error));
} else {
this.isShareModalActive = true;
// send popup
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore-end
}
get groupTitle(): undefined | string { get groupTitle(): undefined | string {
if (!this.group) return undefined; return this.group?.name || this.group?.preferredUsername;
return this.group.name || this.group.preferredUsername;
} }
get groupSummary(): undefined | string { get groupSummary(): undefined | string {
if (!this.group) return undefined; return this.group?.summary;
return this.group.summary;
} }
get groupMember(): IMember | undefined { get groupMember(): IMember | undefined {
@ -821,18 +965,26 @@ export default class Group extends mixins(GroupMixin) {
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "~bulma/sass/utilities/mixins.sass";
div.container { div.container {
background: white;
margin-bottom: 3rem; margin-bottom: 3rem;
padding: 2rem 0;
.header, .header,
.public-container { .public-container {
margin: auto 1rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.header {
background: $white;
padding-top: 1rem;
}
.header .breadcrumb {
margin-bottom: 0.5rem;
margin-left: 0.5rem;
}
.block-container { .block-container {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@ -840,8 +992,9 @@ div.container {
&.presentation { &.presentation {
border: 2px solid $purple-2; border: 2px solid $purple-2;
padding: 10px 0; padding: 0 0 10px;
position: relative; position: relative;
flex-direction: column;
h1 { h1 {
color: $purple-1; color: $purple-1;
@ -858,29 +1011,17 @@ div.container {
z-index: 2; z-index: 2;
} }
& > img { & > .banner-container {
position: absolute; display: flex;
left: 0; justify-content: center;
top: 0; height: 30vh;
::v-deep img {
width: 100%; width: 100%;
height: 100%; height: 100%;
opacity: 0.3;
object-fit: cover; object-fit: cover;
object-position: 50% 50%; object-position: 50% 50%;
} }
} }
.members {
display: flex;
flex-direction: column;
div {
display: flex;
}
figure:not(:first-child) {
margin-left: -10px;
}
} }
div.address { div.address {
@ -925,9 +1066,23 @@ div.container {
.block-column { .block-column {
flex: 1; flex: 1;
margin: 0 1rem; margin: 0;
max-width: 576px;
@include desktop {
margin: 0 0.5rem;
&:first-child {
margin-left: 0;
}
&:last-child {
margin-right: 0;
}
}
section { section {
background: $white;
.posts-wrapper { .posts-wrapper {
padding-bottom: 1rem; padding-bottom: 1rem;
} }
@ -965,11 +1120,134 @@ div.container {
} }
} }
} }
.header {
display: flex;
flex-wrap: wrap;
justify-content: center;
flex-direction: column;
flex: 1;
margin: 0;
align-items: center;
.avatar-container {
display: flex;
align-self: center;
height: 0;
margin-top: 16px;
align-items: flex-end;
::v-deep .icon {
border-radius: 290486px;
border: 1px solid #cdcaea;
background: white;
height: 5rem;
width: 5rem;
i::before {
font-size: 60px;
}
}
figure {
position: relative;
img {
position: absolute;
background: #fff;
}
}
}
.title-container {
flex: 1;
display: flex;
flex-direction: column;
text-align: center;
h1 {
font-size: 32px;
line-height: 38px;
}
}
.group-metadata {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
& > .buttons {
justify-content: center;
::v-deep .b-tooltip {
padding-right: 0.5em;
}
}
.members {
display: flex;
flex-direction: column;
min-width: 300px;
align-items: center;
div {
display: flex;
}
figure:not(:first-child) {
margin-left: -10px;
}
}
}
}
} }
.public-container { .public-container {
display: flex;
flex-wrap: wrap;
flex-direction: row-reverse;
padding: 0;
margin-top: 1rem;
.group-metadata {
min-width: 20rem;
flex: 1;
padding-left: 1rem;
@include mobile {
padding-left: 0;
}
.sticky {
position: sticky;
background: white;
top: 50px;
padding: 1rem;
}
}
.main-content {
min-width: 20rem;
flex: 2;
background: white;
@include desktop {
padding: 10px;
}
@include mobile {
margin-top: 1rem;
}
h2 {
margin: 0 auto 10px;
}
}
section { section {
margin-top: 2rem; margin-top: 0;
.posts-wrapper {
margin-bottom: 1rem;
}
} }
} }

View File

@ -4,11 +4,12 @@
<ul> <ul>
<li> <li>
<router-link <router-link
v-if="group"
:to="{ :to="{
name: RouteName.GROUP, name: RouteName.GROUP,
params: { preferredUsername: usernameWithDomain(group) }, params: { preferredUsername: usernameWithDomain(group) },
}" }"
>{{ group.name }}</router-link >{{ group.name || usernameWithDomain(group) }}</router-link
> >
</li> </li>
<li> <li>
@ -37,10 +38,10 @@
> >
<form @submit.prevent="updateGroup"> <form @submit.prevent="updateGroup">
<b-field :label="$t('Group name')"> <b-field :label="$t('Group name')">
<b-input v-model="group.name" /> <b-input v-model="editableGroup.name" />
</b-field> </b-field>
<b-field :label="$t('Group short description')"> <b-field :label="$t('Group short description')">
<editor mode="basic" v-model="group.summary" :maxSize="500" <editor mode="basic" v-model="editableGroup.summary" :maxSize="500"
/></b-field> /></b-field>
<b-field :label="$t('Avatar')"> <b-field :label="$t('Avatar')">
<picture-upload <picture-upload
@ -62,7 +63,7 @@
<p class="label">{{ $t("Group visibility") }}</p> <p class="label">{{ $t("Group visibility") }}</p>
<div class="field"> <div class="field">
<b-radio <b-radio
v-model="group.visibility" v-model="editableGroup.visibility"
name="groupVisibility" name="groupVisibility"
:native-value="GroupVisibility.PUBLIC" :native-value="GroupVisibility.PUBLIC"
> >
@ -76,9 +77,9 @@
</div> </div>
<div class="field"> <div class="field">
<b-radio <b-radio
v-model="group.visibility" v-model="editableGroup.visibility"
name="groupVisibility" name="groupVisibility"
:native-value="GroupVisibility.PRIVATE" :native-value="GroupVisibility.UNLISTED"
>{{ $t("Only accessible through link") }}<br /> >{{ $t("Only accessible through link") }}<br />
<small>{{ <small>{{
$t( $t(
@ -110,7 +111,7 @@
<p class="label">{{ $t("New members") }}</p> <p class="label">{{ $t("New members") }}</p>
<div class="field"> <div class="field">
<b-radio <b-radio
v-model="group.openness" v-model="editableGroup.openness"
name="groupOpenness" name="groupOpenness"
:native-value="Openness.OPEN" :native-value="Openness.OPEN"
> >
@ -124,7 +125,7 @@
</div> </div>
<div class="field"> <div class="field">
<b-radio <b-radio
v-model="group.openness" v-model="editableGroup.openness"
name="groupOpenness" name="groupOpenness"
:native-value="Openness.INVITE_ONLY" :native-value="Openness.INVITE_ONLY"
>{{ $t("Manually invite new members") }}<br /> >{{ $t("Manually invite new members") }}<br />
@ -140,14 +141,14 @@
:label="$t('Followers')" :label="$t('Followers')"
:message="$t('Followers will receive new public events and posts.')" :message="$t('Followers will receive new public events and posts.')"
> >
<b-checkbox v-model="group.manuallyApprovesFollowers"> <b-checkbox v-model="editableGroup.manuallyApprovesFollowers">
{{ $t("Manually approve new followers") }} {{ $t("Manually approve new followers") }}
</b-checkbox> </b-checkbox>
</b-field> </b-field>
<full-address-auto-complete <full-address-auto-complete
:label="$t('Group address')" :label="$t('Group address')"
v-model="group.physicalAddress" v-model="editableGroup.physicalAddress"
:value="currentAddress" :value="currentAddress"
/> />
@ -171,14 +172,13 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component } from "vue-property-decorator"; import { Component, Watch } from "vue-property-decorator";
import FullAddressAutoComplete from "@/components/Event/FullAddressAutoComplete.vue"; import FullAddressAutoComplete from "@/components/Event/FullAddressAutoComplete.vue";
import { Route } from "vue-router"; import { Route } from "vue-router";
import PictureUpload from "@/components/PictureUpload.vue"; import PictureUpload from "@/components/PictureUpload.vue";
import { mixins } from "vue-class-component"; import { mixins } from "vue-class-component";
import GroupMixin from "@/mixins/group"; import GroupMixin from "@/mixins/group";
import { GroupVisibility, Openness } from "@/types/enums"; import { GroupVisibility, Openness } from "@/types/enums";
import RouteName from "../../router/name";
import { UPDATE_GROUP, DELETE_GROUP } from "../../graphql/group"; import { UPDATE_GROUP, DELETE_GROUP } from "../../graphql/group";
import { IGroup, usernameWithDomain } from "../../types/actor"; import { IGroup, usernameWithDomain } from "../../types/actor";
import { Address, IAddress } from "../../types/address.model"; import { Address, IAddress } from "../../types/address.model";
@ -186,6 +186,8 @@ import { CONFIG } from "@/graphql/config";
import { IConfig } from "@/types/config.model"; import { IConfig } from "@/types/config.model";
import { ServerParseError } from "@apollo/client/link/http"; import { ServerParseError } from "@apollo/client/link/http";
import { ErrorResponse } from "@apollo/client/link/error"; import { ErrorResponse } from "@apollo/client/link/error";
import RouteName from "@/router/name";
import { buildFileFromIMedia } from "@/utils/image";
@Component({ @Component({
components: { components: {
@ -225,9 +227,12 @@ export default class GroupSettings extends mixins(GroupMixin) {
showCopiedTooltip = false; showCopiedTooltip = false;
editableGroup!: IGroup;
async updateGroup(): Promise<void> { async updateGroup(): Promise<void> {
try { try {
const variables = this.buildVariables(); const variables = this.buildVariables();
console.log(variables);
await this.$apollo.mutate<{ updateGroup: IGroup }>({ await this.$apollo.mutate<{ updateGroup: IGroup }>({
mutation: UPDATE_GROUP, mutation: UPDATE_GROUP,
variables, variables,
@ -270,18 +275,38 @@ export default class GroupSettings extends mixins(GroupMixin) {
}, 2000); }, 2000);
} }
@Watch("group")
async watchUpdateGroup(oldGroup: IGroup, newGroup: IGroup): Promise<void> {
if (
oldGroup?.avatar !== undefined &&
oldGroup?.avatar !== newGroup?.avatar
) {
this.avatarFile = await buildFileFromIMedia(this.group.avatar);
}
if (
oldGroup?.banner !== undefined &&
oldGroup?.banner !== newGroup?.banner
) {
this.bannerFile = await buildFileFromIMedia(this.group.banner);
}
this.editableGroup = { ...this.group };
}
private buildVariables() { private buildVariables() {
let avatarObj = {}; let avatarObj = {};
let bannerObj = {}; let bannerObj = {};
const variables = { ...this.group }; const variables = { ...this.editableGroup };
const physicalAddress = {
...variables.physicalAddress,
};
// eslint-disable-next-line // eslint-disable-next-line
// @ts-ignore // @ts-ignore
delete variables.__typename; delete variables.__typename;
if (variables.physicalAddress) { if (physicalAddress) {
// eslint-disable-next-line // eslint-disable-next-line
// @ts-ignore // @ts-ignore
delete variables.physicalAddress.__typename; delete physicalAddress.__typename;
} }
delete variables.avatar; delete variables.avatar;
delete variables.banner; delete variables.banner;
@ -291,7 +316,7 @@ export default class GroupSettings extends mixins(GroupMixin) {
avatar: { avatar: {
media: { media: {
name: this.avatarFile.name, name: this.avatarFile.name,
alt: `${this.group.preferredUsername}'s avatar`, alt: `${this.editableGroup.preferredUsername}'s avatar`,
file: this.avatarFile, file: this.avatarFile,
}, },
}, },
@ -303,14 +328,20 @@ export default class GroupSettings extends mixins(GroupMixin) {
banner: { banner: {
media: { media: {
name: this.bannerFile.name, name: this.bannerFile.name,
alt: `${this.group.preferredUsername}'s banner`, alt: `${this.editableGroup.preferredUsername}'s banner`,
file: this.bannerFile, file: this.bannerFile,
}, },
}, },
}; };
} }
return { return {
...variables, id: this.group.id,
name: this.editableGroup.name,
summary: this.editableGroup.summary,
visibility: this.editableGroup.visibility,
openness: this.editableGroup.openness,
manuallyApprovesFollowers: this.editableGroup.manuallyApprovesFollowers,
physicalAddress,
...avatarObj, ...avatarObj,
...bannerObj, ...bannerObj,
}; };
@ -322,7 +353,7 @@ export default class GroupSettings extends mixins(GroupMixin) {
} }
get currentAddress(): IAddress { get currentAddress(): IAddress {
return new Address(this.group.physicalAddress); return new Address(this.editableGroup.physicalAddress);
} }
get avatarMaxSize(): number | undefined { get avatarMaxSize(): number | undefined {

View File

@ -49,7 +49,7 @@
<picture-upload <picture-upload
v-model="pictureFile" v-model="pictureFile"
:textFallback="$t('Headline picture')" :textFallback="$t('Headline picture')"
:defaultImage="post.picture" :defaultImage="editablePost.picture"
/> />
<b-field <b-field
@ -61,21 +61,21 @@
size="is-large" size="is-large"
aria-required="true" aria-required="true"
required required
v-model="post.title" v-model="editablePost.title"
/> />
</b-field> </b-field>
<tag-input v-model="post.tags" :data="tags" path="title" /> <tag-input v-model="editablePost.tags" :data="tags" path="title" />
<div class="field"> <div class="field">
<label class="label">{{ $t("Post") }}</label> <label class="label">{{ $t("Post") }}</label>
<p v-if="errors.body" class="help is-danger">{{ errors.body }}</p> <p v-if="errors.body" class="help is-danger">{{ errors.body }}</p>
<editor v-model="post.body" /> <editor v-model="editablePost.body" />
</div> </div>
<subtitle>{{ $t("Who can view this post") }}</subtitle> <subtitle>{{ $t("Who can view this post") }}</subtitle>
<div class="field"> <div class="field">
<b-radio <b-radio
v-model="post.visibility" v-model="editablePost.visibility"
name="postVisibility" name="postVisibility"
:native-value="PostVisibility.PUBLIC" :native-value="PostVisibility.PUBLIC"
>{{ $t("Visible everywhere on the web") }}</b-radio >{{ $t("Visible everywhere on the web") }}</b-radio
@ -83,7 +83,7 @@
</div> </div>
<div class="field"> <div class="field">
<b-radio <b-radio
v-model="post.visibility" v-model="editablePost.visibility"
name="postVisibility" name="postVisibility"
:native-value="PostVisibility.UNLISTED" :native-value="PostVisibility.UNLISTED"
>{{ $t("Only accessible through link") }}</b-radio >{{ $t("Only accessible through link") }}</b-radio
@ -91,7 +91,7 @@
</div> </div>
<div class="field"> <div class="field">
<b-radio <b-radio
v-model="post.visibility" v-model="editablePost.visibility"
name="postVisibility" name="postVisibility"
:native-value="PostVisibility.PRIVATE" :native-value="PostVisibility.PRIVATE"
>{{ $t("Only accessible to members of the group") }}</b-radio >{{ $t("Only accessible to members of the group") }}</b-radio
@ -166,7 +166,7 @@ import {
import { IPost } from "../../types/post.model"; import { IPost } from "../../types/post.model";
import Editor from "../../components/Editor.vue"; import Editor from "../../components/Editor.vue";
import { IActor, IGroup, usernameWithDomain } from "../../types/actor"; import { IActor, usernameWithDomain } from "../../types/actor";
import TagInput from "../../components/Event/TagInput.vue"; import TagInput from "../../components/Event/TagInput.vue";
import RouteName from "../../router/name"; import RouteName from "../../router/name";
import Subtitle from "../../components/Utils/Subtitle.vue"; import Subtitle from "../../components/Utils/Subtitle.vue";
@ -249,8 +249,6 @@ export default class EditPost extends mixins(GroupMixin) {
tags: [], tags: [],
}; };
group!: IGroup;
PostVisibility = PostVisibility; PostVisibility = PostVisibility;
pictureFile: File | null = null; pictureFile: File | null = null;
@ -259,6 +257,8 @@ export default class EditPost extends mixins(GroupMixin) {
RouteName = RouteName; RouteName = RouteName;
editablePost!: IPost;
usernameWithDomain = usernameWithDomain; usernameWithDomain = usernameWithDomain;
async mounted(): Promise<void> { async mounted(): Promise<void> {
@ -270,6 +270,7 @@ export default class EditPost extends mixins(GroupMixin) {
if (oldPost.picture !== newPost.picture) { if (oldPost.picture !== newPost.picture) {
this.pictureFile = await buildFileFromIMedia(this.post.picture); this.pictureFile = await buildFileFromIMedia(this.post.picture);
} }
this.editablePost = { ...this.post };
} }
// eslint-disable-next-line consistent-return // eslint-disable-next-line consistent-return
@ -280,11 +281,11 @@ export default class EditPost extends mixins(GroupMixin) {
const { data } = await this.$apollo.mutate({ const { data } = await this.$apollo.mutate({
mutation: UPDATE_POST, mutation: UPDATE_POST,
variables: { variables: {
id: this.post.id, id: this.editablePost.id,
title: this.post.title, title: this.editablePost.title,
body: this.post.body, body: this.editablePost.body,
tags: (this.post.tags || []).map(({ title }) => title), tags: (this.editablePost.tags || []).map(({ title }) => title),
visibility: this.post.visibility, visibility: this.editablePost.visibility,
draft, draft,
...(await this.buildPicture()), ...(await this.buildPicture()),
}, },
@ -300,9 +301,9 @@ export default class EditPost extends mixins(GroupMixin) {
const { data } = await this.$apollo.mutate({ const { data } = await this.$apollo.mutate({
mutation: CREATE_POST, mutation: CREATE_POST,
variables: { variables: {
...this.post, ...this.editablePost,
...(await this.buildPicture()), ...(await this.buildPicture()),
tags: (this.post.tags || []).map(({ title }) => title), tags: (this.editablePost.tags || []).map(({ title }) => title),
attributedToId: this.actualGroup.id, attributedToId: this.actualGroup.id,
draft, draft,
}, },
@ -362,16 +363,16 @@ export default class EditPost extends mixins(GroupMixin) {
obj = { ...obj, ...pictureObj }; obj = { ...obj, ...pictureObj };
} }
try { try {
if (this.post.picture && this.pictureFile) { if (this.editablePost.picture && this.pictureFile) {
const oldPictureFile = (await buildFileFromIMedia( const oldPictureFile = (await buildFileFromIMedia(
this.post.picture this.editablePost.picture
)) as File; )) as File;
const oldPictureFileContent = await readFileAsync(oldPictureFile); const oldPictureFileContent = await readFileAsync(oldPictureFile);
const newPictureFileContent = await readFileAsync( const newPictureFileContent = await readFileAsync(
this.pictureFile as File this.pictureFile as File
); );
if (oldPictureFileContent === newPictureFileContent) { if (oldPictureFileContent === newPictureFileContent) {
obj.picture = { mediaId: this.post.picture.id }; obj.picture = { mediaId: this.editablePost.picture.id };
} }
} }
} catch (e) { } catch (e) {
@ -381,7 +382,7 @@ export default class EditPost extends mixins(GroupMixin) {
} }
get actualGroup(): IActor { get actualGroup(): IActor {
if (!this.group.id) { if (!this.group?.id) {
return this.post.attributedTo as IActor; return this.post.attributedTo as IActor;
} }
return this.group; return this.group;

View File

@ -86,7 +86,7 @@ import { IMember } from "@/types/actor/member.model";
import { FETCH_GROUP_POSTS } from "../../graphql/post"; import { FETCH_GROUP_POSTS } from "../../graphql/post";
import { Paginate } from "../../types/paginate"; import { Paginate } from "../../types/paginate";
import { IPost } from "../../types/post.model"; import { IPost } from "../../types/post.model";
import { IGroup, IPerson, usernameWithDomain } from "../../types/actor"; import { usernameWithDomain } from "../../types/actor";
import RouteName from "../../router/name"; import RouteName from "../../router/name";
import PostElementItem from "../../components/Post/PostElementItem.vue"; import PostElementItem from "../../components/Post/PostElementItem.vue";
@ -138,14 +138,10 @@ const POSTS_PAGE_LIMIT = 10;
export default class PostList extends mixins(GroupMixin) { export default class PostList extends mixins(GroupMixin) {
@Prop({ required: true, type: String }) preferredUsername!: string; @Prop({ required: true, type: String }) preferredUsername!: string;
group!: IGroup;
posts!: Paginate<IPost>; posts!: Paginate<IPost>;
memberships!: IMember[]; memberships!: IMember[];
currentActor!: IPerson;
postsPage = 1; postsPage = 1;
RouteName = RouteName; RouteName = RouteName;

View File

@ -1,9 +1,20 @@
<template> <template>
<div>
<article class="container" v-if="post"> <article class="container" v-if="post">
<section class="heading-section"> <header>
<div class="banner-container">
<lazy-image
v-if="post.picture"
:src="post.picture.url"
:width="post.picture.metadata.width"
:height="post.picture.metadata.height"
:blurhash="post.picture.metadata.blurhash"
/>
</div>
<div class="heading-section">
<div class="heading-wrapper">
<div class="title-metadata">
<h1 class="title">{{ post.title }}</h1> <h1 class="title">{{ post.title }}</h1>
<i18n tag="span" path="By {author}" class="authors"> <p class="metadata">
<router-link <router-link
slot="author" slot="author"
:to="{ :to="{
@ -12,21 +23,26 @@
preferredUsername: usernameWithDomain(post.attributedTo), preferredUsername: usernameWithDomain(post.attributedTo),
}, },
}" }"
>{{ post.attributedTo.name }}</router-link
> >
</i18n> <actor-inline :actor="post.attributedTo" />
<p class="published has-text-grey-dark" v-if="!post.draft"> </router-link>
<span class="published has-text-grey-dark" v-if="!post.draft">
<b-icon icon="clock" size="is-small" />
{{ post.publishAt | formatDateTimeString }} {{ post.publishAt | formatDateTimeString }}
</p> </span>
<small <span
v-if="post.visibility === PostVisibility.PRIVATE" v-if="post.visibility === PostVisibility.PRIVATE"
class="has-text-grey-dark" class="has-text-grey-dark"
> >
<b-icon icon="lock" size="is-small" /> <b-icon icon="lock" size="is-small" />
{{ {{
$t("Accessible only to members", { group: post.attributedTo.name }) $t("Accessible only to members", {
group: post.attributedTo.name,
})
}} }}
</small> </span>
</p>
</div>
<p class="buttons" v-if="isCurrentActorMember"> <p class="buttons" v-if="isCurrentActorMember">
<b-tag type="is-warning" size="is-medium" v-if="post.draft">{{ <b-tag type="is-warning" size="is-medium" v-if="post.draft">{{
$t("Draft") $t("Draft")
@ -42,8 +58,10 @@
>{{ $t("Edit") }}</router-link >{{ $t("Edit") }}</router-link
> >
</p> </p>
<img v-if="post.picture" :src="post.picture.url" alt="" /> </div>
</section> </div>
</header>
<section v-html="post.body" class="content" /> <section v-html="post.body" class="content" />
<section class="tags"> <section class="tags">
<router-link <router-link
@ -55,7 +73,6 @@
</router-link> </router-link>
</section> </section>
</article> </article>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
@ -66,11 +83,12 @@ import { PostVisibility } from "@/types/enums";
import { IMember } from "@/types/actor/member.model"; import { IMember } from "@/types/actor/member.model";
import { CURRENT_ACTOR_CLIENT, PERSON_MEMBERSHIPS } from "../../graphql/actor"; import { CURRENT_ACTOR_CLIENT, PERSON_MEMBERSHIPS } from "../../graphql/actor";
import { FETCH_POST } from "../../graphql/post"; import { FETCH_POST } from "../../graphql/post";
import { IPost } from "../../types/post.model"; import { IPost } from "../../types/post.model";
import { usernameWithDomain } from "../../types/actor"; import { usernameWithDomain } from "../../types/actor";
import RouteName from "../../router/name"; import RouteName from "../../router/name";
import Tag from "../../components/Tag.vue"; import Tag from "../../components/Tag.vue";
import LazyImage from "../../components/Image/LazyImage.vue";
import ActorInline from "../../components/Account/ActorInline.vue";
@Component({ @Component({
apollo: { apollo: {
@ -106,6 +124,8 @@ import Tag from "../../components/Tag.vue";
}, },
components: { components: {
Tag, Tag,
LazyImage,
ActorInline,
}, },
metaInfo() { metaInfo() {
return { return {
@ -148,63 +168,80 @@ export default class Post extends mixins(GroupMixin) {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
article { article {
section.heading-section { background: $white !important;
text-align: center; header {
display: flex;
flex-direction: column;
.banner-container {
display: flex;
justify-content: center;
height: 30vh;
}
.heading-section {
position: relative; position: relative;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
margin: auto -3rem 2rem; margin-bottom: 2rem;
.heading-wrapper {
padding: 15px 10px;
display: flex;
flex-wrap: wrap;
justify-content: center;
.title-metadata {
min-width: 300px;
flex: 20;
p.metadata {
margin-top: 16px;
display: flex;
justify-content: flex-start;
flex-wrap: wrap;
*:not(:first-child) {
padding-left: 5px;
}
}
}
p.buttons {
flex: 1;
}
}
h1.title { h1.title {
margin: 0 auto; margin: 0;
padding-top: 3rem; font-weight: 500;
font-size: 3rem; font-size: 38px;
font-weight: 700; font-family: "Roboto", "Helvetica", "Arial", serif;
} }
.authors { .authors {
margin-top: 2rem;
display: inline-block; display: inline-block;
} }
.published {
margin-top: 1rem;
}
&::after { &::after {
height: 0.2rem; height: 0.2rem;
content: " "; content: " ";
display: block; display: block;
width: 100%;
background-color: $purple-1; background-color: $purple-1;
margin-top: 1rem;
} }
.buttons { .buttons {
justify-content: center; justify-content: center;
} }
& > * {
z-index: 10;
}
& > img {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
opacity: 0.3;
object-fit: cover;
object-position: 50% 50%;
z-index: 0;
} }
} }
section.content { & > section {
margin: 0 2rem;
&.content {
font-size: 1.1rem; font-size: 1.1rem;
} }
section.tags { &.tags {
padding-bottom: 5rem; padding-bottom: 5rem;
a { a {
@ -216,10 +253,8 @@ article {
} }
} }
} }
}
background: $white;
max-width: 700px;
margin: 0 auto; margin: 0 auto;
padding: 0 3rem;
} }
</style> </style>

File diff suppressed because it is too large Load Diff

View File

@ -76,14 +76,19 @@ defmodule Mobilizon.Federation.ActivityPub.Actor do
@doc """ @doc """
Find an actor in our local database or call WebFinger to find what's its AP ID is and then fetch it Find an actor in our local database or call WebFinger to find what's its AP ID is and then fetch it
""" """
@spec find_or_make_actor_from_nickname(String.t(), atom() | nil) :: tuple() @spec find_or_make_actor_from_nickname(String.t(), atom() | nil) ::
{:ok, Actor.t()} | {:error, any()}
def find_or_make_actor_from_nickname(nickname, type \\ nil) do def find_or_make_actor_from_nickname(nickname, type \\ nil) do
case Actors.get_actor_by_name(nickname, type) do case Actors.get_actor_by_name_with_preload(nickname, type) do
%Actor{} = actor -> %Actor{url: actor_url} = actor ->
if Actors.needs_update?(actor) do
make_actor_from_url(actor_url, true)
else
{:ok, actor} {:ok, actor}
end
nil -> nil ->
make_actor_from_nickname(nickname) make_actor_from_nickname(nickname, true)
end end
end end
@ -94,10 +99,10 @@ defmodule Mobilizon.Federation.ActivityPub.Actor do
Create an actor inside our database from username, using WebFinger to find out its AP ID and then fetch it Create an actor inside our database from username, using WebFinger to find out its AP ID and then fetch it
""" """
@spec make_actor_from_nickname(String.t()) :: {:ok, %Actor{}} | {:error, any()} @spec make_actor_from_nickname(String.t()) :: {:ok, %Actor{}} | {:error, any()}
def make_actor_from_nickname(nickname) do def make_actor_from_nickname(nickname, preload \\ false) do
case WebFinger.finger(nickname) do case WebFinger.finger(nickname) do
{:ok, url} when is_binary(url) -> {:ok, url} when is_binary(url) ->
make_actor_from_url(url) make_actor_from_url(url, preload)
_e -> _e ->
{:error, "No ActivityPub URL found in WebFinger"} {:error, "No ActivityPub URL found in WebFinger"}

View File

@ -4,10 +4,11 @@ defmodule Mobilizon.Web.Cache.ActivityPub do
""" """
alias Mobilizon.{Actors, Discussions, Events, Posts, Resources, Todos, Tombstone} alias Mobilizon.{Actors, Discussions, Events, Posts, Resources, Todos, Tombstone}
alias Mobilizon.Actors.{Actor, Member} alias Mobilizon.Actors.Actor, as: ActorModel
alias Mobilizon.Actors.Member
alias Mobilizon.Discussions.{Comment, Discussion} alias Mobilizon.Discussions.{Comment, Discussion}
alias Mobilizon.Events.Event alias Mobilizon.Events.Event
alias Mobilizon.Federation.ActivityPub.Relay alias Mobilizon.Federation.ActivityPub.{Actor, Relay}
alias Mobilizon.Posts.Post alias Mobilizon.Posts.Post
alias Mobilizon.Resources.Resource alias Mobilizon.Resources.Resource
alias Mobilizon.Todos.{Todo, TodoList} alias Mobilizon.Todos.{Todo, TodoList}
@ -23,8 +24,8 @@ defmodule Mobilizon.Web.Cache.ActivityPub do
{:commit, Actor.t()} | {:ignore, nil} {:commit, Actor.t()} | {:ignore, nil}
def get_actor_by_name(name) do def get_actor_by_name(name) do
Cachex.fetch(@cache, "actor_" <> name, fn "actor_" <> name -> Cachex.fetch(@cache, "actor_" <> name, fn "actor_" <> name ->
case Actors.get_actor_by_name_with_preload(name) do case Actor.find_or_make_actor_from_nickname(name) do
%Actor{} = actor -> {:ok, %ActorModel{} = actor} ->
{:commit, actor} {:commit, actor}
nil -> nil ->
@ -41,7 +42,7 @@ defmodule Mobilizon.Web.Cache.ActivityPub do
def get_local_actor_by_name(name) do def get_local_actor_by_name(name) do
Cachex.fetch(@cache, "local_actor_" <> name, fn "local_actor_" <> name -> Cachex.fetch(@cache, "local_actor_" <> name, fn "local_actor_" <> name ->
case Actors.get_local_actor_by_name(name) do case Actors.get_local_actor_by_name(name) do
%Actor{} = actor -> %ActorModel{} = actor ->
{:commit, actor} {:commit, actor}
nil -> nil ->