Refactor media upload
Use Upload Media logic from Pleroma Backend changes for picture upload Move AS <-> Model conversion to separate module Front changes Downgrade apollo-client: https://github.com/Akryum/vue-apollo/issues/577 Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
parent
9724bc8e9f
commit
f90089e1bf
@ -100,7 +100,7 @@
|
||||
#
|
||||
{Credo.Check.Refactor.CondStatements, []},
|
||||
{Credo.Check.Refactor.CyclomaticComplexity, []},
|
||||
{Credo.Check.Refactor.FunctionArity, []},
|
||||
{Credo.Check.Refactor.FunctionArity, [max_arity: 9]},
|
||||
{Credo.Check.Refactor.LongQuoteBlocks, []},
|
||||
{Credo.Check.Refactor.MatchInCondition, []},
|
||||
{Credo.Check.Refactor.NegatedConditionsInUnless, []},
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -28,6 +28,8 @@ priv/data/*
|
||||
!priv/data/.gitkeep
|
||||
.vscode/
|
||||
cover/
|
||||
test/fixtures/image_tmp.jpg
|
||||
test/uploads/
|
||||
uploads/*
|
||||
!uploads/.gitkeep
|
||||
.idea
|
||||
|
@ -4,5 +4,5 @@ projects:
|
||||
extensions:
|
||||
endpoints:
|
||||
dev:
|
||||
url: 'http://localhost:4001/api'
|
||||
url: 'http://localhost:4000/api'
|
||||
introspect: true
|
||||
|
@ -1,7 +1,7 @@
|
||||
FROM bitwalker/alpine-elixir:latest
|
||||
|
||||
RUN apk add inotify-tools postgresql-client yarn
|
||||
RUN apk add --no-cache make gcc libc-dev argon2
|
||||
RUN apk add --no-cache make gcc libc-dev argon2 imagemagick
|
||||
|
||||
RUN mix local.hex --force && mix local.rebar --force
|
||||
|
||||
|
@ -14,7 +14,11 @@ config :mobilizon, :instance,
|
||||
description: System.get_env("MOBILIZON_INSTANCE_DESCRIPTION") || "This is a Mobilizon instance",
|
||||
version: "1.0.0-dev",
|
||||
registrations_open: System.get_env("MOBILIZON_INSTANCE_REGISTRATIONS_OPEN") || false,
|
||||
repository: Mix.Project.config()[:source_url]
|
||||
repository: Mix.Project.config()[:source_url],
|
||||
remote_limit: 100_000,
|
||||
upload_limit: 16_000_000,
|
||||
avatar_upload_limit: 2_000_000,
|
||||
banner_upload_limit: 4_000_000
|
||||
|
||||
config :mime, :types, %{
|
||||
"application/activity+json" => ["activity-json"],
|
||||
@ -31,6 +35,34 @@ config :mobilizon, MobilizonWeb.Endpoint,
|
||||
email_from: "noreply@localhost",
|
||||
email_to: "noreply@localhost"
|
||||
|
||||
# Upload configuration
|
||||
config :mobilizon, MobilizonWeb.Upload,
|
||||
uploader: MobilizonWeb.Uploaders.Local,
|
||||
filters: [MobilizonWeb.Upload.Filter.Dedupe],
|
||||
link_name: true,
|
||||
proxy_remote: false,
|
||||
proxy_opts: [
|
||||
redirect_on_failure: false,
|
||||
max_body_length: 25 * 1_048_576,
|
||||
http: [
|
||||
follow_redirect: true,
|
||||
pool: :upload
|
||||
]
|
||||
]
|
||||
|
||||
config :mobilizon, MobilizonWeb.Uploaders.Local, uploads: "uploads"
|
||||
|
||||
config :mobilizon, :media_proxy,
|
||||
enabled: false,
|
||||
proxy_opts: [
|
||||
redirect_on_failure: false,
|
||||
max_body_length: 25 * 1_048_576,
|
||||
http: [
|
||||
follow_redirect: true,
|
||||
pool: :media
|
||||
]
|
||||
]
|
||||
|
||||
# Configures Elixir's Logger
|
||||
config :logger, :console,
|
||||
format: "$time $metadata[$level] $message\n",
|
||||
@ -62,9 +94,6 @@ config :geolix,
|
||||
}
|
||||
]
|
||||
|
||||
config :arc,
|
||||
storage: Arc.Storage.Local
|
||||
|
||||
config :phoenix, :format_encoders, json: Jason, "activity-json": Jason
|
||||
|
||||
config :mobilizon, Mobilizon.Service.Geospatial.Nominatim,
|
||||
|
@ -8,11 +8,10 @@ use Mix.Config
|
||||
# with brunch.io to recompile .js and .css sources.
|
||||
config :mobilizon, MobilizonWeb.Endpoint,
|
||||
http: [
|
||||
port: System.get_env("MOBILIZON_INSTANCE_PORT") || 4001
|
||||
port: System.get_env("MOBILIZON_INSTANCE_PORT") || 4000
|
||||
],
|
||||
url: [
|
||||
host: System.get_env("MOBILIZON_INSTANCE_HOST") || "mobilizon.local",
|
||||
port: System.get_env("MOBILIZON_INSTANCE_PORT") || 4001
|
||||
host: System.get_env("MOBILIZON_INSTANCE_HOST") || "mobilizon.local"
|
||||
],
|
||||
debug_errors: true,
|
||||
code_reloader: true,
|
||||
|
@ -1,28 +1,10 @@
|
||||
use Mix.Config
|
||||
|
||||
# For production, we often load configuration from external
|
||||
# sources, such as your system environment. For this reason,
|
||||
# you won't find the :http configuration below, but set inside
|
||||
# MobilizonWeb.Endpoint.init/2 when load_from_system_env is
|
||||
# true. Any dynamic configuration should be done there.
|
||||
#
|
||||
# Don't forget to configure the url host to something meaningful,
|
||||
# Phoenix uses this information when generating URLs.
|
||||
#
|
||||
# Finally, we also include the path to a cache manifest
|
||||
# containing the digested version of static files. This
|
||||
# manifest is generated by the mix phx.digest task
|
||||
# which you typically run after static files are built.
|
||||
config :mobilizon, MobilizonWeb.Endpoint,
|
||||
load_from_system_env: true,
|
||||
http: [:inet6, port: System.get_env("MOBILIZON_INSTANCE_PORT") || 4000],
|
||||
url: [
|
||||
host: System.get_env("MOBILIZON_INSTANCE_HOST") || "mobilizon.me",
|
||||
scheme: "https",
|
||||
port: 443
|
||||
],
|
||||
http: [
|
||||
ip: {127, 0, 0, 1},
|
||||
port: System.get_env("MOBILIZON_INSTANCE_PORT") || 4000
|
||||
port: 80
|
||||
],
|
||||
secret_key_base:
|
||||
System.get_env("MOBILIZON_SECRET") || "ThisShouldBeAVeryStrongStringPleaseReplaceMe",
|
||||
|
@ -33,6 +33,10 @@ config :mobilizon, Mobilizon.Repo,
|
||||
|
||||
config :mobilizon, Mobilizon.Mailer, adapter: Bamboo.TestAdapter
|
||||
|
||||
config :mobilizon, MobilizonWeb.Upload, filters: [], link_name: false
|
||||
|
||||
config :mobilizon, MobilizonWeb.Uploaders.Local, uploads: "test/uploads"
|
||||
|
||||
config :exvcr,
|
||||
vcr_cassette_library_dir: "test/fixtures/vcr_cassettes"
|
||||
|
||||
|
@ -14,7 +14,7 @@
|
||||
"dependencies": {
|
||||
"apollo-absinthe-upload-link": "^1.5.0",
|
||||
"apollo-cache-inmemory": "^1.5.1",
|
||||
"apollo-client": "^2.5.1",
|
||||
"apollo-client": "2.5.1",
|
||||
"apollo-link": "^1.2.11",
|
||||
"apollo-link-http": "^1.5.14",
|
||||
"apollo-link-state": "^0.4.2",
|
||||
|
@ -91,6 +91,7 @@ export default class App extends Vue {
|
||||
@import "~buefy/src/scss/components/form";
|
||||
@import "~buefy/src/scss/components/modal";
|
||||
@import "~buefy/src/scss/components/tag";
|
||||
@import "~buefy/src/scss/components/upload";
|
||||
@import "~buefy/src/scss/utils/_all";
|
||||
|
||||
.router-enter-active,
|
||||
|
@ -8,8 +8,8 @@
|
||||
<li v-for="identity in identities" :key="identity.id">
|
||||
<div class="media identity" v-bind:class="{ 'is-current-identity': isCurrentIdentity(identity) }">
|
||||
<div class="media-left">
|
||||
<figure class="image is-48x48">
|
||||
<img class="is-rounded" :src="identity.avatarUrl">
|
||||
<figure class="image is-48x48" v-if="identity.avatar">
|
||||
<img class="is-rounded" :src="identity.avatar.url">
|
||||
</figure>
|
||||
</div>
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
<router-link class="card" :to="{ name: 'Event', params: { uuid: event.uuid } }">
|
||||
<div class="card-image" v-if="!event.image">
|
||||
<figure class="image is-16by9">
|
||||
<div class="tag-container">
|
||||
<div class="tag-container" v-if="event.tags">
|
||||
<b-tag v-for="tag in event.tags.slice(0, 3)" :key="tag.slug" type="is-secondary">{{ tag.title }}</b-tag>
|
||||
</div>
|
||||
<img src="https://picsum.photos/g/400/225/?random">
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="card">
|
||||
<div class="card-image" v-if="!group.bannerUrl">
|
||||
<div class="card-image" v-if="!group.banner">
|
||||
<figure class="image is-4by3">
|
||||
<img src="https://picsum.photos/g/400/200/">
|
||||
</figure>
|
||||
|
@ -40,8 +40,8 @@
|
||||
v-if="currentUser.isLoggedIn && loggedPerson"
|
||||
:to="{ name: 'MyAccount' }"
|
||||
>
|
||||
<figure class="image is-24x24">
|
||||
<img :src="loggedPerson.avatarUrl">
|
||||
<figure class="image is-24x24" v-if="loggedPerson.avatar">
|
||||
<img :src="loggedPerson.avatar.url">
|
||||
</figure>
|
||||
<span>{{ loggedPerson.preferredUsername }}</span>
|
||||
</router-link>
|
||||
|
29
js/src/components/PictureUpload.vue
Normal file
29
js/src/components/PictureUpload.vue
Normal file
@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<b-field class="file">
|
||||
<b-upload v-model="pictureFile" @input="onFileChanged">
|
||||
<a class="button is-primary">
|
||||
<b-icon icon="upload"></b-icon>
|
||||
<span>Click to upload</span>
|
||||
</a>
|
||||
</b-upload>
|
||||
<span class="file-name" v-if="pictureFile">
|
||||
{{ pictureFile.name }}
|
||||
</span>
|
||||
</b-field>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
import { IPictureUpload } from '@/types/picture.model';
|
||||
|
||||
@Component
|
||||
export default class PictureUpload extends Vue {
|
||||
picture!: IPictureUpload;
|
||||
pictureFile: File|null = null;
|
||||
|
||||
onFileChanged(file: File) {
|
||||
this.picture = { file, name: file.name, alt: '' };
|
||||
this.$emit('change', this.picture);
|
||||
}
|
||||
}
|
||||
</script>
|
@ -10,8 +10,12 @@ query($name:String!) {
|
||||
summary,
|
||||
preferredUsername,
|
||||
suspended,
|
||||
avatarUrl,
|
||||
bannerUrl,
|
||||
avatar {
|
||||
url
|
||||
},
|
||||
banner {
|
||||
url
|
||||
},
|
||||
feedTokens {
|
||||
token
|
||||
},
|
||||
@ -28,7 +32,9 @@ export const LOGGED_PERSON = gql`
|
||||
query {
|
||||
loggedPerson {
|
||||
id,
|
||||
avatarUrl,
|
||||
avatar {
|
||||
url
|
||||
},
|
||||
preferredUsername,
|
||||
}
|
||||
}`;
|
||||
@ -37,7 +43,9 @@ export const LOGGED_PERSON_WITH_GOING_TO_EVENTS = gql`
|
||||
query {
|
||||
loggedPerson {
|
||||
id,
|
||||
avatarUrl,
|
||||
avatar {
|
||||
url
|
||||
},
|
||||
preferredUsername,
|
||||
goingToEvents {
|
||||
uuid,
|
||||
@ -56,7 +64,9 @@ query {
|
||||
export const IDENTITIES = gql`
|
||||
query {
|
||||
identities {
|
||||
avatarUrl,
|
||||
avatar {
|
||||
url
|
||||
},
|
||||
preferredUsername,
|
||||
name
|
||||
}
|
||||
@ -72,7 +82,9 @@ mutation CreatePerson($preferredUsername: String!) {
|
||||
preferredUsername,
|
||||
name,
|
||||
summary,
|
||||
avatarUrl
|
||||
avatar {
|
||||
url
|
||||
},
|
||||
}
|
||||
}
|
||||
`;
|
||||
@ -91,7 +103,9 @@ mutation ($preferredUsername: String!, $name: String!, $summary: String!, $email
|
||||
preferredUsername,
|
||||
name,
|
||||
summary,
|
||||
avatarUrl,
|
||||
avatar {
|
||||
url
|
||||
},
|
||||
}
|
||||
}
|
||||
`;
|
||||
@ -106,8 +120,12 @@ query($name:String!) {
|
||||
summary,
|
||||
preferredUsername,
|
||||
suspended,
|
||||
avatarUrl,
|
||||
bannerUrl,
|
||||
avatar {
|
||||
url
|
||||
},
|
||||
banner {
|
||||
url
|
||||
}
|
||||
organizedEvents {
|
||||
uuid,
|
||||
title,
|
||||
|
@ -4,7 +4,9 @@ const participantQuery = `
|
||||
role,
|
||||
actor {
|
||||
preferredUsername,
|
||||
avatarUrl,
|
||||
avatar {
|
||||
url
|
||||
},
|
||||
name,
|
||||
id
|
||||
}
|
||||
@ -24,8 +26,10 @@ export const FETCH_EVENT = gql`
|
||||
endsOn,
|
||||
status,
|
||||
visibility,
|
||||
thumbnail,
|
||||
largeImage,
|
||||
picture {
|
||||
id
|
||||
url
|
||||
},
|
||||
publishAt,
|
||||
category,
|
||||
# online_address,
|
||||
@ -35,19 +39,23 @@ export const FETCH_EVENT = gql`
|
||||
floor,
|
||||
street,
|
||||
locality,
|
||||
postal_code,
|
||||
postalCode,
|
||||
region,
|
||||
country,
|
||||
geom
|
||||
}
|
||||
organizerActor {
|
||||
avatarUrl,
|
||||
avatar {
|
||||
url
|
||||
},
|
||||
preferredUsername,
|
||||
domain,
|
||||
name,
|
||||
},
|
||||
# attributedTo {
|
||||
# # avatarUrl,
|
||||
# avatar {
|
||||
# url,
|
||||
# }
|
||||
# preferredUsername,
|
||||
# name,
|
||||
# },
|
||||
@ -66,7 +74,9 @@ export const FETCH_EVENT = gql`
|
||||
description
|
||||
},
|
||||
organizerActor {
|
||||
avatarUrl,
|
||||
avatar {
|
||||
url,
|
||||
},
|
||||
preferredUsername,
|
||||
domain,
|
||||
name,
|
||||
@ -89,8 +99,10 @@ export const FETCH_EVENTS = gql`
|
||||
endsOn,
|
||||
status,
|
||||
visibility,
|
||||
thumbnail,
|
||||
largeImage,
|
||||
picture {
|
||||
id
|
||||
url
|
||||
},
|
||||
publishAt,
|
||||
# online_address,
|
||||
# phone_address,
|
||||
@ -99,12 +111,16 @@ export const FETCH_EVENTS = gql`
|
||||
locality
|
||||
}
|
||||
organizerActor {
|
||||
avatarUrl,
|
||||
avatar {
|
||||
url
|
||||
},
|
||||
preferredUsername,
|
||||
name,
|
||||
},
|
||||
attributedTo {
|
||||
avatarUrl,
|
||||
avatar {
|
||||
url
|
||||
},
|
||||
preferredUsername,
|
||||
name,
|
||||
},
|
||||
@ -124,20 +140,31 @@ export const CREATE_EVENT = gql`
|
||||
mutation CreateEvent(
|
||||
$title: String!,
|
||||
$description: String!,
|
||||
$organizerActorId: String!,
|
||||
$organizerActorId: ID!,
|
||||
$category: String!,
|
||||
$beginsOn: DateTime!
|
||||
$beginsOn: DateTime!,
|
||||
$picture_file: Upload,
|
||||
$picture_name: String,
|
||||
) {
|
||||
createEvent(
|
||||
title: $title,
|
||||
description: $description,
|
||||
beginsOn: $beginsOn,
|
||||
organizerActorId: $organizerActorId,
|
||||
category: $category
|
||||
category: $category,
|
||||
picture: {
|
||||
picture: {
|
||||
file: $picture_file,
|
||||
name: $picture_name,
|
||||
}
|
||||
}
|
||||
) {
|
||||
id,
|
||||
uuid,
|
||||
title
|
||||
title,
|
||||
picture {
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
@ -4,14 +4,16 @@ export const LOGGED_PERSON = gql`
|
||||
query {
|
||||
loggedPerson {
|
||||
id,
|
||||
avatarUrl,
|
||||
avatar {
|
||||
url
|
||||
},
|
||||
preferredUsername,
|
||||
}
|
||||
}`;
|
||||
|
||||
export const CREATE_FEED_TOKEN_ACTOR = gql`
|
||||
mutation createFeedToken($actor_id: Int!) {
|
||||
createFeedToken(actor_id: $actor_id) {
|
||||
createFeedToken(actorId: $actor_id) {
|
||||
token,
|
||||
actor {
|
||||
id
|
||||
|
@ -23,7 +23,9 @@ query SearchGroups($searchText: String!) {
|
||||
searchGroups(search: $searchText) {
|
||||
total,
|
||||
elements {
|
||||
avatarUrl,
|
||||
avatar {
|
||||
url
|
||||
},
|
||||
domain,
|
||||
preferredUsername,
|
||||
name,
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { IPicture } from '@/types/picture.model';
|
||||
|
||||
export interface IActor {
|
||||
id?: string;
|
||||
url: string;
|
||||
@ -6,13 +8,13 @@ export interface IActor {
|
||||
summary: string;
|
||||
preferredUsername: string;
|
||||
suspended: boolean;
|
||||
avatarUrl: string;
|
||||
bannerUrl: string;
|
||||
avatar: IPicture | null;
|
||||
banner: IPicture | null;
|
||||
}
|
||||
|
||||
export class Actor implements IActor {
|
||||
avatarUrl: string = '';
|
||||
bannerUrl: string = '';
|
||||
avatar: IPicture | null = null;
|
||||
banner: IPicture | null = null;
|
||||
domain: string | null = null;
|
||||
name: string = '';
|
||||
preferredUsername: string = '';
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { Actor, IActor } from './actor';
|
||||
import { IAddress } from '@/types/address.model';
|
||||
import { ITag } from '@/types/tag.model';
|
||||
import { IAbstractPicture, IPicture } from '@/types/picture.model';
|
||||
|
||||
export enum EventStatus {
|
||||
TENTATIVE,
|
||||
@ -62,8 +64,7 @@ export interface IEvent {
|
||||
|
||||
joinOptions: EventJoinOptions;
|
||||
|
||||
thumbnail: string;
|
||||
largeImage: string;
|
||||
picture: IAbstractPicture|null;
|
||||
|
||||
organizerActor: IActor;
|
||||
attributedTo: IActor;
|
||||
@ -84,12 +85,10 @@ export class EventModel implements IEvent {
|
||||
description: string = '';
|
||||
endsOn: Date = new Date();
|
||||
joinOptions: EventJoinOptions = EventJoinOptions.FREE;
|
||||
largeImage: string = '';
|
||||
local: boolean = true;
|
||||
participants: IParticipant[] = [];
|
||||
publishAt: Date = new Date();
|
||||
status: EventStatus = EventStatus.CONFIRMED;
|
||||
thumbnail: string = '';
|
||||
title: string = '';
|
||||
url: string = '';
|
||||
uuid: string = '';
|
||||
@ -99,4 +98,5 @@ export class EventModel implements IEvent {
|
||||
relatedEvents: IEvent[] = [];
|
||||
onlineAddress: string = '';
|
||||
phoneAddress: string = '';
|
||||
picture: IAbstractPicture|null = null;
|
||||
}
|
||||
|
16
js/src/types/picture.model.ts
Normal file
16
js/src/types/picture.model.ts
Normal file
@ -0,0 +1,16 @@
|
||||
export interface IAbstractPicture {
|
||||
name;
|
||||
alt;
|
||||
}
|
||||
|
||||
export interface IPicture {
|
||||
url;
|
||||
name;
|
||||
alt;
|
||||
}
|
||||
|
||||
export interface IPictureUpload {
|
||||
file: File;
|
||||
name: String;
|
||||
alt: String|null;
|
||||
}
|
@ -2,8 +2,8 @@
|
||||
<section class="container">
|
||||
<div v-if="person">
|
||||
<div class="header">
|
||||
<figure v-if="person.bannerUrl" class="image is-3by1">
|
||||
<img :src="person.bannerUrl" alt="banner">
|
||||
<figure v-if="person.banner" class="image is-3by1">
|
||||
<img :src="person.banner.url" alt="banner">
|
||||
</figure>
|
||||
</div>
|
||||
|
||||
|
@ -1,16 +1,16 @@
|
||||
<template>
|
||||
<section class="container">
|
||||
<div v-if="person">
|
||||
<div class="card-image" v-if="person.bannerUrl">
|
||||
<div class="card-image" v-if="person.banner">
|
||||
<figure class="image">
|
||||
<img :src="person.bannerUrl">
|
||||
<img :src="person.banner.url">
|
||||
</figure>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="media">
|
||||
<div class="media-left">
|
||||
<figure class="image is-48x48">
|
||||
<img :src="person.avatarUrl">
|
||||
<figure class="image is-48x48" v-if="person.avatar">
|
||||
<img :src="person.avatar.url">
|
||||
</figure>
|
||||
</div>
|
||||
<div class="media-content">
|
||||
@ -18,7 +18,6 @@
|
||||
<p class="subtitle">@{{ person.preferredUsername }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<vue-simple-markdown :source="person.summary"></vue-simple-markdown>
|
||||
</div>
|
||||
@ -58,7 +57,7 @@
|
||||
</a>
|
||||
</b-dropdown-item>
|
||||
</b-dropdown>
|
||||
<a class="button" v-else-if="loggedPerson" @click="createToken">
|
||||
<a class="button" v-if="loggedPerson.id === person.id" @click="createToken">
|
||||
<translate>Create token</translate>
|
||||
</a>
|
||||
</div>
|
||||
|
@ -81,7 +81,7 @@ export default class Register extends Vue {
|
||||
@Prop({ type: String, required: true }) email!: string;
|
||||
@Prop({ type: Boolean, required: false, default: false }) userAlreadyActivated!: boolean;
|
||||
|
||||
host: string = MOBILIZON_INSTANCE_HOST;
|
||||
host?: string = MOBILIZON_INSTANCE_HOST;
|
||||
|
||||
person: IPerson = {
|
||||
preferredUsername: '',
|
||||
@ -90,8 +90,8 @@ export default class Register extends Vue {
|
||||
id: '',
|
||||
url: '',
|
||||
suspended: false,
|
||||
avatarUrl: '',
|
||||
bannerUrl: '',
|
||||
avatar: null,
|
||||
banner: null,
|
||||
domain: null,
|
||||
feedTokens: [],
|
||||
goingToEvents: [],
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<section>
|
||||
<section class="container">
|
||||
<h1 class="title">
|
||||
<translate>Create a new event</translate>
|
||||
</h1>
|
||||
@ -22,6 +22,8 @@
|
||||
</b-select>
|
||||
</b-field>
|
||||
|
||||
<picture-upload @change="handlePictureUploadChange" />
|
||||
|
||||
<button class="button is-primary">
|
||||
<translate>Create my event</translate>
|
||||
</button>
|
||||
@ -41,8 +43,11 @@ import {
|
||||
} from '@/types/event.model';
|
||||
import { LOGGED_PERSON } from '@/graphql/actor';
|
||||
import { IPerson, Person } from '@/types/actor';
|
||||
import PictureUpload from '@/components/PictureUpload.vue';
|
||||
import { IPictureUpload } from '@/types/picture.model';
|
||||
|
||||
@Component({
|
||||
components: { PictureUpload },
|
||||
apollo: {
|
||||
loggedPerson: {
|
||||
query: LOGGED_PERSON,
|
||||
@ -55,6 +60,8 @@ export default class CreateEvent extends Vue {
|
||||
loggedPerson: IPerson = new Person();
|
||||
categories: string[] = Object.keys(Category);
|
||||
event: IEvent = new EventModel();
|
||||
pictureFile?: File;
|
||||
pictureName?: String;
|
||||
|
||||
createEvent(e: Event) {
|
||||
e.preventDefault();
|
||||
@ -62,15 +69,18 @@ export default class CreateEvent extends Vue {
|
||||
this.event.attributedTo = this.loggedPerson;
|
||||
|
||||
if (this.event.uuid === '') {
|
||||
console.log('event', this.event);
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: CREATE_EVENT,
|
||||
variables: {
|
||||
title: this.event.title,
|
||||
description: this.event.description,
|
||||
beginsOn: this.event.beginsOn,
|
||||
beginsOn: this.event.beginsOn.toISOString(),
|
||||
category: this.event.category,
|
||||
organizerActorId: this.event.organizerActor.id,
|
||||
picture_file: this.pictureFile,
|
||||
picture_name: this.pictureName,
|
||||
},
|
||||
})
|
||||
.then(data => {
|
||||
@ -100,6 +110,12 @@ export default class CreateEvent extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
handlePictureUploadChange(picture: IPictureUpload) {
|
||||
console.log('picture upload change', picture);
|
||||
this.pictureFile = picture.file;
|
||||
this.pictureName = picture.name;
|
||||
}
|
||||
|
||||
// getAddressData(addressData) {
|
||||
// if (addressData !== null) {
|
||||
// this.event.address = {
|
||||
|
@ -3,7 +3,10 @@
|
||||
<b-loading :active.sync="$apollo.loading"></b-loading>
|
||||
<div v-if="event">
|
||||
<div class="header-picture container">
|
||||
<figure class="image is-3by1">
|
||||
<figure class="image is-3by1" v-if="event.picture">
|
||||
<img :src="event.picture.url">
|
||||
</figure>
|
||||
<figure class="image is-3by1" v-else>
|
||||
<img src="https://picsum.photos/600/200/">
|
||||
</figure>
|
||||
</div>
|
||||
@ -95,10 +98,10 @@
|
||||
<translate
|
||||
:translate-params="{name: event.organizerActor.name ? event.organizerActor.name : event.organizerActor.preferredUsername}"
|
||||
v-if="event.organizerActor">By %{ name }</translate>
|
||||
<figure v-if="event.organizerActor.avatarUrl" class="image is-48x48">
|
||||
<figure v-if="event.organizerActor.avatar" class="image is-48x48">
|
||||
<img
|
||||
class="is-rounded"
|
||||
:src="event.organizerActor.avatarUrl"
|
||||
:src="event.organizerActor.avatar.url"
|
||||
:alt="$gettextInterpolate('%{actor}\'s avatar', {actor: event.organizerActor.preferredUsername})" />
|
||||
</figure>
|
||||
</router-link>
|
||||
@ -185,8 +188,8 @@
|
||||
<!-- >-->
|
||||
<!-- <div>-->
|
||||
<!-- <figure>-->
|
||||
<!-- <img v-if="!participant.actor.avatarUrl" src="https://picsum.photos/125/125/">-->
|
||||
<!-- <img v-else :src="participant.actor.avatarUrl">-->
|
||||
<!-- <img v-if="!participant.actor.avatar.url" src="https://picsum.photos/125/125/">-->
|
||||
<!-- <img v-else :src="participant.actor.avatar.url">-->
|
||||
<!-- </figure>-->
|
||||
<!-- <span>{{ participant.actor.preferredUsername }}</span>-->
|
||||
<!-- </div>-->
|
||||
|
@ -1,16 +1,16 @@
|
||||
<template>
|
||||
<section class="container">
|
||||
<div v-if="group">
|
||||
<div class="card-image" v-if="group.bannerUrl">
|
||||
<div class="card-image" v-if="group.banner.url">
|
||||
<figure class="image">
|
||||
<img :src="group.bannerUrl">
|
||||
<img :src="group.banner.url">
|
||||
</figure>
|
||||
</div>
|
||||
<div class="box">
|
||||
<div class="media">
|
||||
<div class="media-left">
|
||||
<figure class="image is-48x48">
|
||||
<img :src="group.avatarUrl">
|
||||
<img :src="group.avatar.url">
|
||||
</figure>
|
||||
</div>
|
||||
<div class="media-content">
|
||||
|
867
js/yarn.lock
867
js/yarn.lock
File diff suppressed because it is too large
Load Diff
@ -33,6 +33,7 @@ defmodule Mobilizon.Actors.Actor do
|
||||
alias Mobilizon.Users.User
|
||||
alias Mobilizon.Actors.{Actor, Follower, Member}
|
||||
alias Mobilizon.Events.{Event, FeedToken}
|
||||
alias Mobilizon.Media.File
|
||||
|
||||
alias MobilizonWeb.Router.Helpers, as: Routes
|
||||
alias MobilizonWeb.Endpoint
|
||||
@ -62,8 +63,6 @@ defmodule Mobilizon.Actors.Actor do
|
||||
field(:openness, Mobilizon.Actors.ActorOpennessEnum, default: :moderated)
|
||||
field(:visibility, Mobilizon.Actors.ActorVisibilityEnum, default: :private)
|
||||
field(:suspended, :boolean, default: false)
|
||||
field(:avatar_url, :string)
|
||||
field(:banner_url, :string)
|
||||
# field(:openness, Mobilizon.Actors.ActorOpennessEnum, default: :moderated)
|
||||
has_many(:followers, Follower, foreign_key: :target_actor_id)
|
||||
has_many(:followings, Follower, foreign_key: :actor_id)
|
||||
@ -71,6 +70,8 @@ defmodule Mobilizon.Actors.Actor do
|
||||
many_to_many(:memberships, Actor, join_through: Member)
|
||||
belongs_to(:user, User)
|
||||
has_many(:feed_tokens, FeedToken, foreign_key: :actor_id)
|
||||
embeds_one(:avatar, File)
|
||||
embeds_one(:banner, File)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
@ -93,11 +94,11 @@ defmodule Mobilizon.Actors.Actor do
|
||||
:keys,
|
||||
:manually_approves_followers,
|
||||
:suspended,
|
||||
:avatar_url,
|
||||
:banner_url,
|
||||
:user_id
|
||||
])
|
||||
|> build_urls()
|
||||
|> cast_embed(:avatar)
|
||||
|> cast_embed(:banner)
|
||||
|> unique_username_validator()
|
||||
|> validate_required([:preferred_username, :keys, :suspended, :url])
|
||||
|> unique_constraint(:preferred_username, name: :actors_preferred_username_domain_type_index)
|
||||
@ -119,10 +120,11 @@ defmodule Mobilizon.Actors.Actor do
|
||||
:suspended,
|
||||
:url,
|
||||
:type,
|
||||
:avatar_url,
|
||||
:user_id
|
||||
])
|
||||
|> build_urls()
|
||||
|> cast_embed(:avatar)
|
||||
|> cast_embed(:banner)
|
||||
# Needed because following constraint can't work for domain null values (local)
|
||||
|> unique_username_validator()
|
||||
|> unique_constraint(:preferred_username, name: :actors_preferred_username_domain_type_index)
|
||||
@ -152,9 +154,7 @@ defmodule Mobilizon.Actors.Actor do
|
||||
:summary,
|
||||
:preferred_username,
|
||||
:keys,
|
||||
:manually_approves_followers,
|
||||
:avatar_url,
|
||||
:banner_url
|
||||
:manually_approves_followers
|
||||
])
|
||||
|> validate_required([
|
||||
:url,
|
||||
@ -165,6 +165,8 @@ defmodule Mobilizon.Actors.Actor do
|
||||
:preferred_username,
|
||||
:keys
|
||||
])
|
||||
|> cast_embed(:avatar)
|
||||
|> cast_embed(:banner)
|
||||
# Needed because following constraint can't work for domain null values (local)
|
||||
|> unique_username_validator()
|
||||
|> unique_constraint(:preferred_username, name: :actors_preferred_username_domain_type_index)
|
||||
@ -193,10 +195,10 @@ defmodule Mobilizon.Actors.Actor do
|
||||
:name,
|
||||
:domain,
|
||||
:summary,
|
||||
:preferred_username,
|
||||
:avatar_url,
|
||||
:banner_url
|
||||
:preferred_username
|
||||
])
|
||||
|> cast_embed(:avatar)
|
||||
|> cast_embed(:banner)
|
||||
|> build_urls(:Group)
|
||||
|> put_change(:domain, nil)
|
||||
|> put_change(:keys, Actors.create_keys())
|
||||
|
@ -11,7 +11,7 @@ defmodule Mobilizon.Actors do
|
||||
alias Mobilizon.Actors.{Actor, Bot, Member, Follower}
|
||||
|
||||
alias Mobilizon.Service.ActivityPub
|
||||
# import Exgravatar
|
||||
require Logger
|
||||
|
||||
@doc false
|
||||
def data() do
|
||||
@ -57,9 +57,12 @@ defmodule Mobilizon.Actors do
|
||||
end
|
||||
|
||||
# Get actor by ID and preload organized events, followers and followings
|
||||
@spec get_actor_with_everything(integer()) :: Ecto.Query
|
||||
@spec get_actor_with_everything(integer()) :: Ecto.Query.t()
|
||||
defp do_get_actor_with_everything(id) do
|
||||
from(a in Actor, where: a.id == ^id, preload: [:organized_events, :followers, :followings])
|
||||
from(a in Actor,
|
||||
where: a.id == ^id,
|
||||
preload: [:organized_events, :followers, :followings]
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@ -239,24 +242,29 @@ defmodule Mobilizon.Actors do
|
||||
"""
|
||||
@spec insert_or_update_actor(map(), boolean()) :: {:ok, Actor.t()}
|
||||
def insert_or_update_actor(data, preload \\ false) do
|
||||
cs = Actor.remote_actor_creation(data)
|
||||
cs =
|
||||
data
|
||||
|> Actor.remote_actor_creation()
|
||||
|
||||
{:ok, actor} =
|
||||
Repo.insert(
|
||||
cs,
|
||||
on_conflict: [
|
||||
set: [
|
||||
keys: data.keys,
|
||||
avatar_url: data.avatar_url,
|
||||
banner_url: data.banner_url,
|
||||
name: data.name,
|
||||
summary: data.summary
|
||||
]
|
||||
],
|
||||
conflict_target: [:url]
|
||||
)
|
||||
|
||||
if preload, do: {:ok, Repo.preload(actor, [:followers])}, else: {:ok, actor}
|
||||
with {:ok, actor} <-
|
||||
Repo.insert(
|
||||
cs,
|
||||
on_conflict: [
|
||||
set: [
|
||||
keys: data.keys,
|
||||
name: data.name,
|
||||
summary: data.summary
|
||||
]
|
||||
],
|
||||
conflict_target: [:url]
|
||||
) do
|
||||
actor = if preload, do: Repo.preload(actor, [:followers]), else: actor
|
||||
{:ok, actor}
|
||||
else
|
||||
err ->
|
||||
Logger.error(inspect(err))
|
||||
{:error, err}
|
||||
end
|
||||
end
|
||||
|
||||
# def increase_event_count(%Actor{} = actor) do
|
||||
@ -291,7 +299,8 @@ defmodule Mobilizon.Actors do
|
||||
{:error, :actor_not_found}
|
||||
|
||||
actor ->
|
||||
if preload, do: {:ok, Repo.preload(actor, [:followers])}, else: {:ok, actor}
|
||||
actor = if preload, do: Repo.preload(actor, [:followers]), else: actor
|
||||
{:ok, actor}
|
||||
end
|
||||
end
|
||||
|
||||
@ -371,7 +380,11 @@ defmodule Mobilizon.Actors do
|
||||
"""
|
||||
@spec get_local_actor_by_name(String.t()) :: Actor.t() | nil
|
||||
def get_local_actor_by_name(name) do
|
||||
Repo.one(from(a in Actor, where: a.preferred_username == ^name and is_nil(a.domain)))
|
||||
Repo.one(
|
||||
from(a in Actor,
|
||||
where: a.preferred_username == ^name and is_nil(a.domain)
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@ -435,6 +448,7 @@ defmodule Mobilizon.Actors do
|
||||
{:ok, actor}
|
||||
|
||||
_ ->
|
||||
Logger.error("Could not fetch by AP id")
|
||||
{:error, "Could not fetch by AP id"}
|
||||
end
|
||||
end
|
||||
|
@ -6,6 +6,9 @@ defmodule Mobilizon.Application do
|
||||
import Cachex.Spec
|
||||
alias Mobilizon.Service.Export.{Feed, ICalendar}
|
||||
|
||||
@name Mix.Project.config()[:name]
|
||||
@version Mix.Project.config()[:version]
|
||||
|
||||
# See https://hexdocs.pm/elixir/Application.html
|
||||
# for more information on OTP Applications
|
||||
def start(_type, _args) do
|
||||
@ -82,4 +85,13 @@ defmodule Mobilizon.Application do
|
||||
MobilizonWeb.Endpoint.config_change(changed, removed)
|
||||
:ok
|
||||
end
|
||||
|
||||
def named_version, do: @name <> " " <> @version
|
||||
|
||||
def user_agent do
|
||||
info =
|
||||
"#{MobilizonWeb.Endpoint.url()} <#{Mobilizon.CommonConfig.get([:instance, :email], "")}>"
|
||||
|
||||
named_version() <> "; " <> info
|
||||
end
|
||||
end
|
||||
|
@ -22,4 +22,45 @@ defmodule Mobilizon.CommonConfig do
|
||||
defp instance_config(), do: Application.get_env(:mobilizon, :instance)
|
||||
|
||||
defp to_bool(v), do: v == true or v == "true" or v == "True"
|
||||
|
||||
def get(key), do: get(key, nil)
|
||||
|
||||
def get([key], default), do: get(key, default)
|
||||
|
||||
def get([parent_key | keys], default) do
|
||||
case :mobilizon
|
||||
|> Application.get_env(parent_key)
|
||||
|> get_in(keys) do
|
||||
nil -> default
|
||||
any -> any
|
||||
end
|
||||
end
|
||||
|
||||
def get(key, default) do
|
||||
Application.get_env(:mobilizon, key, default)
|
||||
end
|
||||
|
||||
def get!(key) do
|
||||
value = get(key, nil)
|
||||
|
||||
if value == nil do
|
||||
raise("Missing configuration value: #{inspect(key)}")
|
||||
else
|
||||
value
|
||||
end
|
||||
end
|
||||
|
||||
def put([key], value), do: put(key, value)
|
||||
|
||||
def put([parent_key | keys], value) do
|
||||
parent =
|
||||
Application.get_env(:mobilizon, parent_key)
|
||||
|> put_in(keys, value)
|
||||
|
||||
Application.put_env(:mobilizon, parent_key, parent)
|
||||
end
|
||||
|
||||
def put(key, value) do
|
||||
Application.put_env(:mobilizon, key, value)
|
||||
end
|
||||
end
|
||||
|
@ -35,6 +35,7 @@ defmodule Mobilizon.Events.Event do
|
||||
import Ecto.Changeset
|
||||
alias Mobilizon.Events.{Event, Participant, Tag, Session, Track}
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Media.Picture
|
||||
alias Mobilizon.Addresses.Address
|
||||
|
||||
schema "events" do
|
||||
@ -48,8 +49,6 @@ defmodule Mobilizon.Events.Event do
|
||||
field(:status, Mobilizon.Events.EventStatusEnum, default: :confirmed)
|
||||
field(:visibility, Mobilizon.Events.EventVisibilityEnum, default: :public)
|
||||
field(:join_options, Mobilizon.Events.JoinOptionsEnum, default: :free)
|
||||
field(:thumbnail, :string)
|
||||
field(:large_image, :string)
|
||||
field(:publish_at, :utc_datetime)
|
||||
field(:uuid, Ecto.UUID, default: Ecto.UUID.generate())
|
||||
field(:online_address, :string)
|
||||
@ -62,6 +61,7 @@ defmodule Mobilizon.Events.Event do
|
||||
has_many(:tracks, Track)
|
||||
has_many(:sessions, Session)
|
||||
belongs_to(:physical_address, Address)
|
||||
belongs_to(:picture, Picture)
|
||||
|
||||
timestamps(type: :utc_datetime)
|
||||
end
|
||||
@ -80,12 +80,11 @@ defmodule Mobilizon.Events.Event do
|
||||
:category,
|
||||
:status,
|
||||
:visibility,
|
||||
:thumbnail,
|
||||
:large_image,
|
||||
:publish_at,
|
||||
:online_address,
|
||||
:phone_address,
|
||||
:uuid
|
||||
:uuid,
|
||||
:picture_id
|
||||
])
|
||||
|> cast_assoc(:tags)
|
||||
|> cast_assoc(:physical_address)
|
||||
|
@ -178,7 +178,8 @@ defmodule Mobilizon.Events do
|
||||
:tracks,
|
||||
:tags,
|
||||
:participants,
|
||||
:physical_address
|
||||
:physical_address,
|
||||
:picture
|
||||
]
|
||||
)
|
||||
|> Repo.one()
|
||||
@ -692,7 +693,7 @@ defmodule Mobilizon.Events do
|
||||
on: p.actor_id == a.id,
|
||||
on: p.event_id == e.id,
|
||||
where: a.id == ^id and p.role != ^:not_approved,
|
||||
preload: [:tags]
|
||||
preload: [:picture, :tags]
|
||||
)
|
||||
|> paginate(page, limit)
|
||||
)
|
||||
@ -1239,11 +1240,7 @@ defmodule Mobilizon.Events do
|
||||
|
||||
"""
|
||||
def get_feed_token(token) do
|
||||
from(
|
||||
tk in FeedToken,
|
||||
where: tk.token == ^token,
|
||||
preload: [:actor, :user]
|
||||
)
|
||||
from(ftk in FeedToken, where: ftk.token == ^token, preload: [:actor, :user])
|
||||
|> Repo.one()
|
||||
end
|
||||
|
||||
|
115
lib/mobilizon/media.ex
Normal file
115
lib/mobilizon/media.ex
Normal file
@ -0,0 +1,115 @@
|
||||
defmodule Mobilizon.Media do
|
||||
@moduledoc """
|
||||
The Media context.
|
||||
"""
|
||||
|
||||
import Ecto.Query, warn: false
|
||||
alias Mobilizon.Repo
|
||||
|
||||
alias Mobilizon.Media.Picture
|
||||
|
||||
@doc false
|
||||
def data() do
|
||||
Dataloader.Ecto.new(Mobilizon.Repo, query: &query/2)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def query(queryable, _params) do
|
||||
queryable
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a single picture.
|
||||
|
||||
Raises `Ecto.NoResultsError` if the Picture does not exist.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> get_picture!(123)
|
||||
%Picture{}
|
||||
|
||||
iex> get_picture!(456)
|
||||
** (Ecto.NoResultsError)
|
||||
|
||||
"""
|
||||
def get_picture!(id), do: Repo.get!(Picture, id)
|
||||
|
||||
def get_picture(id), do: Repo.get(Picture, id)
|
||||
|
||||
@doc """
|
||||
Get a picture by it's URL
|
||||
"""
|
||||
@spec get_picture_by_url(String.t()) :: Picture.t() | nil
|
||||
def get_picture_by_url(url) do
|
||||
from(
|
||||
p in Picture,
|
||||
where: fragment("? @> ?", p.file, ~s|{"url": "#{url}"}|)
|
||||
)
|
||||
|> Repo.one()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a picture.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> create_picture(%{field: value})
|
||||
{:ok, %Picture{}}
|
||||
|
||||
iex> create_picture(%{field: bad_value})
|
||||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def create_picture(attrs \\ %{}) do
|
||||
%Picture{}
|
||||
|> Picture.changeset(attrs)
|
||||