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
RUN apk add --no-cache python3 build-base libwebp-tools bash imagemagick ncurses
WORKDIR /build
COPY js .
RUN ls -a
RUN yarn install \
&& yarn run build
@ -24,7 +25,7 @@ COPY config/config.exs config/prod.exs ./config/
COPY config/docker.exs ./config/runtime.exs
COPY rel ./rel
COPY support ./support
COPY --from=assets ./priv/static ./priv/static
COPY --from=assets ./build/priv/static ./priv/static
RUN mix phx.digest \
&& mix release

View file

@ -73,14 +73,14 @@
"@types/vuedraggable": "^2.23.0",
"@typescript-eslint/eslint-plugin": "^4.18.0",
"@typescript-eslint/parser": "^4.18.0",
"@vue/cli-plugin-babel": "~5.0.0-beta.0",
"@vue/cli-plugin-e2e-cypress": "~5.0.0-beta.0",
"@vue/cli-plugin-eslint": "~5.0.0-beta.0",
"@vue/cli-plugin-pwa": "~5.0.0-beta.0",
"@vue/cli-plugin-router": "~5.0.0-beta.0",
"@vue/cli-plugin-typescript": "~5.0.0-beta.0",
"@vue/cli-plugin-unit-jest": "~5.0.0-beta.0",
"@vue/cli-service": "~5.0.0-beta.0",
"@vue/cli-plugin-babel": "~5.0.0-beta.2",
"@vue/cli-plugin-e2e-cypress": "~5.0.0-beta.2",
"@vue/cli-plugin-eslint": "~5.0.0-beta.2",
"@vue/cli-plugin-pwa": "~5.0.0-beta.2",
"@vue/cli-plugin-router": "~5.0.0-beta.2",
"@vue/cli-plugin-typescript": "~5.0.0-beta.2",
"@vue/cli-plugin-unit-jest": "~5.0.0-beta.2",
"@vue/cli-service": "~5.0.0-beta.2",
"@vue/eslint-config-prettier": "^6.0.0",
"@vue/eslint-config-typescript": "^7.0.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
></i18n
>
<small class="has-text-grey activity-date">{{
<small class="has-text-grey-dark activity-date">{{
activity.insertedAt | formatTimeString
}}</small>
</div>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -55,20 +55,21 @@ section {
}
div.group-section-title {
--title-color: $violet-2;
display: flex;
align-items: stretch;
background: $secondary;
color: #3a384c;
color: var(--title-color);
&.privateSection {
color: $violet-2;
background: $purple-2;
color: $purple-3;
background: $violet-2;
}
::v-deep & > a {
align-self: center;
margin-right: 5px;
color: $orange-3;
color: var(--title-color);
}
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 {
@Prop({ type: String, required: true }) preferredUsername!: string;
@ -40,6 +51,10 @@ export default class JoinGroupWithAccount extends Vue {
return this.group?.url;
}
get groupTitle(): undefined | string {
return this.group?.name || this.group?.preferredUsername;
}
sentence = this.$t(
"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 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({
components: {
LazyImage,
},
})
export default class LazyImageWrapper extends Vue {
@Prop({ required: true, default: null })
picture!: IMedia | null;
@Prop({ required: true })
picture!: IMedia;
get pictureOrDefault(): Partial<IMedia> {
if (this.picture === null) {
return DEFAULT_PICTURE;
}
return {
url:
this?.picture === null
? "/img/mobilizon_default_card.png"
: this?.picture?.url,
url: this?.picture?.url,
metadata: {
width: this?.picture?.metadata?.width || 630,
height: this?.picture?.metadata?.height || 350,
blurhash:
this?.picture?.metadata?.blurhash ||
"MCHKI4El-P-U}+={R-WWoes,Iu-P=?R,xD",
width: this?.picture?.metadata?.width,
height: this?.picture?.metadata?.height,
blurhash: this?.picture?.metadata?.blurhash,
},
};
}

View file

@ -112,7 +112,12 @@
<span @click="setIdentity(identity)">
<div class="media-left">
<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>
<b-icon v-else size="is-medium" icon="account-circle" />
</div>
@ -133,11 +138,6 @@
:to="{ name: RouteName.UPDATE_IDENTITY }"
>{{ $t("My account") }}</b-navbar-item
>
<!-- <b-navbar-item tag="router-link" :to="{ name: RouteName.CREATE_GROUP }">-->
<!-- {{ $t('Create group') }}-->
<!-- </b-navbar-item>-->
<b-navbar-item
v-if="currentUser.role === ICurrentUserRole.ADMINISTRATOR"
tag="router-link"

View file

@ -5,7 +5,7 @@
>
<div class="title-info-wrapper">
<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), {
locale: $dateFnsLocale,
addSuffix: true,
@ -43,10 +43,9 @@ export default class PostListItem extends Vue {
.post-minimalist-title {
color: #3c376e;
font-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica, Arial,
serif;
font-size: 1rem;
font-weight: 700;
font-family: Roboto, Helvetica, Arial, serif;
font-size: 16px;
font-weight: 500;
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;

View file

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

View file

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

View file

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

View file

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

View file

@ -1043,5 +1043,9 @@
"User settings": "User settings",
"You changed your email or password": "You changed your email or password",
"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}",
"© The OpenStreetMap Contributors": "© Les Contributeur⋅ices OpenStreetMap",
"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";
import { FETCH_GROUP } from "@/graphql/group";
import RouteName from "@/router/name";
import {
Group,
IActor,
IGroup,
IPerson,
usernameWithDomain,
} from "@/types/actor";
import { IActor, IGroup, IPerson, usernameWithDomain } from "@/types/actor";
import { MemberRole } from "@/types/enums";
import { Component, Vue } from "vue-property-decorator";
@ -50,14 +44,14 @@ const now = new Date();
variables() {
return {
actorId: this.currentActor.id,
group: this.group.preferredUsername,
group: this.group?.preferredUsername,
};
},
skip() {
return (
!this.currentActor ||
!this.currentActor.id ||
!this.group.preferredUsername
!this.group?.preferredUsername
);
},
},
@ -65,7 +59,7 @@ const now = new Date();
return (
!this.currentActor ||
!this.currentActor.id ||
!this.group.preferredUsername
!this.group?.preferredUsername
);
},
},
@ -73,7 +67,7 @@ const now = new Date();
},
})
export default class GroupMixin extends Vue {
group: IGroup = new Group();
group!: IGroup;
currentActor!: IActor;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

@ -76,14 +76,19 @@ defmodule Mobilizon.Federation.ActivityPub.Actor do
@doc """
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
case Actors.get_actor_by_name(nickname, type) do
%Actor{} = actor ->
{:ok, actor}
case Actors.get_actor_by_name_with_preload(nickname, type) do
%Actor{url: actor_url} = actor ->
if Actors.needs_update?(actor) do
make_actor_from_url(actor_url, true)
else
{:ok, actor}
end
nil ->
make_actor_from_nickname(nickname)
make_actor_from_nickname(nickname, true)
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
"""
@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
{:ok, url} when is_binary(url) ->
make_actor_from_url(url)
make_actor_from_url(url, preload)
_e ->
{: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.{Actor, Member}
alias Mobilizon.Actors.Actor, as: ActorModel
alias Mobilizon.Actors.Member
alias Mobilizon.Discussions.{Comment, Discussion}
alias Mobilizon.Events.Event
alias Mobilizon.Federation.ActivityPub.Relay
alias Mobilizon.Federation.ActivityPub.{Actor, Relay}
alias Mobilizon.Posts.Post
alias Mobilizon.Resources.Resource
alias Mobilizon.Todos.{Todo, TodoList}
@ -23,8 +24,8 @@ defmodule Mobilizon.Web.Cache.ActivityPub do
{:commit, Actor.t()} | {:ignore, nil}
def get_actor_by_name(name) do
Cachex.fetch(@cache, "actor_" <> name, fn "actor_" <> name ->
case Actors.get_actor_by_name_with_preload(name) do
%Actor{} = actor ->
case Actor.find_or_make_actor_from_nickname(name) do
{:ok, %ActorModel{} = actor} ->
{:commit, actor}
nil ->
@ -41,7 +42,7 @@ defmodule Mobilizon.Web.Cache.ActivityPub do
def get_local_actor_by_name(name) do
Cachex.fetch(@cache, "local_actor_" <> name, fn "local_actor_" <> name ->
case Actors.get_local_actor_by_name(name) do
%Actor{} = actor ->
%ActorModel{} = actor ->
{:commit, actor}
nil ->