Merge branch 'session-issues' into 'master'

Various issues

Closes #823

See merge request framasoft/mobilizon!1025
This commit is contained in:
Thomas Citharel 2021-08-12 09:30:42 +00:00
commit d2fed8f91a
15 changed files with 184 additions and 108 deletions

View file

@ -66,10 +66,11 @@ config :mime, :types, %{
config :mobilizon, Mobilizon.Web.Upload,
uploader: Mobilizon.Web.Upload.Uploader.Local,
filters: [
Mobilizon.Web.Upload.Filter.Dedupe,
Mobilizon.Web.Upload.Filter.AnalyzeMetadata,
Mobilizon.Web.Upload.Filter.Resize,
Mobilizon.Web.Upload.Filter.Optimize
Mobilizon.Web.Upload.Filter.Optimize,
Mobilizon.Web.Upload.Filter.BlurHash,
Mobilizon.Web.Upload.Filter.Dedupe
],
allow_list_mime_types: ["image/gif", "image/jpeg", "image/png", "image/webp"],
link_name: true,

View file

@ -5,6 +5,7 @@
</template>
<script lang="ts">
import { IMedia } from "@/types/media.model";
import { PropType } from "vue";
import { Component, Prop, Vue } from "vue-property-decorator";
import LazyImageWrapper from "../Image/LazyImageWrapper.vue";
@ -14,7 +15,7 @@ import LazyImageWrapper from "../Image/LazyImageWrapper.vue";
},
})
export default class EventBanner extends Vue {
@Prop({ required: true, default: null })
@Prop({ required: true, default: null, type: Object as PropType<IMedia> })
picture!: IMedia | null;
}
</script>

View file

@ -7,17 +7,21 @@ import { decode } from "blurhash";
import { Component, Prop, Ref, Vue } from "vue-property-decorator";
@Component
export default class extends Vue {
export default class BlurhashImg extends Vue {
@Prop({ type: String, required: true }) hash!: string;
@Prop({ type: Number, default: 1 }) aspectRatio!: string;
@Ref("canvas") readonly canvas!: any;
mounted(): void {
const pixels = decode(this.hash, 32, 32);
const imageData = new ImageData(pixels, 32, 32);
const context = this.canvas.getContext("2d");
context.putImageData(imageData, 0, 0);
try {
const pixels = decode(this.hash, 32, 32);
const imageData = new ImageData(pixels, 32, 32);
const context = this.canvas.getContext("2d");
context.putImageData(imageData, 0, 0);
} catch (e) {
console.error(e);
}
}
}
</script>

View file

@ -9,6 +9,7 @@
</template>
<script lang="ts">
import { IMedia } from "@/types/media.model";
import { PropType } from "vue";
import { Component, Prop, Vue } from "vue-property-decorator";
import LazyImage from "../Image/LazyImage.vue";
@ -31,7 +32,7 @@ const DEFAULT_PICTURE = {
},
})
export default class LazyImageWrapper extends Vue {
@Prop({ required: true })
@Prop({ required: false, type: Object as PropType<IMedia | null> })
picture!: IMedia | null;
get pictureOrDefault(): Partial<IMedia> {

View file

@ -7,12 +7,18 @@
:center="[lat, lon]"
@click="clickMap"
@update:zoom="updateZoom"
:options="{ zoomControl: false }"
>
<l-tile-layer
:url="config.maps.tiles.endpoint"
:attribution="attribution"
>
</l-tile-layer>
<l-control-zoom
position="topleft"
:zoomInTitle="$t('Zoom in')"
:zoomOutTitle="$t('Zoom out')"
></l-control-zoom>
<v-locatecontrol :options="{ icon: 'mdi mdi-map-marker' }" />
<l-marker
:lat-lng="[lat, lon]"
@ -34,7 +40,14 @@
import { Icon, LatLng, LeafletMouseEvent, LeafletEvent } from "leaflet";
import "leaflet/dist/leaflet.css";
import { Component, Prop, Vue } from "vue-property-decorator";
import { LMap, LTileLayer, LMarker, LPopup, LIcon } from "vue2-leaflet";
import {
LMap,
LTileLayer,
LMarker,
LPopup,
LIcon,
LControlZoom,
} from "vue2-leaflet";
import Vue2LeafletLocateControl from "@/components/Map/Vue2LeafletLocateControl.vue";
import { CONFIG } from "../graphql/config";
import { IConfig } from "../types/config.model";
@ -46,6 +59,7 @@ import { IConfig } from "../types/config.model";
LMarker,
LPopup,
LIcon,
LControlZoom,
"v-locatecontrol": Vue2LeafletLocateControl,
},
apollo: {

View file

@ -10,9 +10,9 @@
* to try to trigger location manually (not done ATM)
*/
import L, { DomEvent } from "leaflet";
import { DomEvent } from "leaflet";
import { findRealParent, propsBinder } from "vue2-leaflet";
import "leaflet.locatecontrol";
import Locatecontrol from "leaflet.locatecontrol";
import { Component, Prop, Vue } from "vue-property-decorator";
@Component({
@ -37,12 +37,20 @@ export default class Vue2LeafletLocateControl extends Vue {
parentContainer: any;
mounted(): void {
this.mapObject = L.control.locate(this.options);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
this.mapObject = new Locatecontrol({
...this.options,
strings: { title: this.$t("Show me where I am") as string },
});
DomEvent.on(this.mapObject, this.$listeners as any);
propsBinder(this, this.mapObject, this.$props);
this.ready = true;
this.parentContainer = findRealParent(this.$parent);
this.mapObject.addTo(this.parentContainer.mapObject, !this.visible);
this.$nextTick(() => {
this.$emit("ready", this.mapObject);
});
}
public locate(): void {

View file

@ -1125,5 +1125,8 @@
"Booking": "Booking",
"Filter by profile or group name": "Filter by profile or group name",
"Filter by name": "Filter by name",
"Redirecting in progress…": "Redirecting in progress…"
"Redirecting in progress…": "Redirecting in progress…",
"Zoom in": "Zoom in",
"Zoom out": "Zoom out",
"Show me where I am": "Show me where I am"
}

View file

@ -99,7 +99,7 @@
"Are you sure you want to <b>delete</b> this event? This action cannot be undone. You may want to engage the discussion with the event creator or edit its event instead.": "Êtes-vous certain⋅e de vouloir <b>supprimer</b> cet événement ? Cette action n'est pas réversible. Vous voulez peut-être engager la discussion avec le créateur de l'événement ou bien modifier son événement à la place.",
"Are you sure you want to <b>suspend</b> this group? All members - including remote ones - will be notified and removed from the group, and <b>all of the group data (events, posts, discussions, todos…) will be irretrievably destroyed</b>.": "Êtes-vous certain·e de vouloir <b>suspendre</b> ce groupe ? Tous les membres - y compris ceux·elles sur d'autres instances - seront notifié·e·s et supprimé·e·s du groupe, et <b>toutes les données associées au groupe (événements, billets, discussions, todos…) seront irrémédiablement détruites</b>.",
"Are you sure you want to <b>suspend</b> this group? As this group originates from instance {instance}, this will only remove local members and delete the local data, as well as rejecting all the future data.": "Êtes-vous certain·e de vouloir <b>suspendre</b> ce groupe ? Comme ce groupe provient de l'instance {instance}, cela supprimera seulement les membres locaux et supprimera les données locales, et rejettera également toutes les données futures.",
"Are you sure you want to cancel the event creation? You'll lose all modifications.": "Étes-vous certain⋅e de vouloir annuler la création de l'événement ? Vous allez perdre toutes vos modifications.",
"Are you sure you want to cancel the event creation? You'll lose all modifications.": "Êtes-vous certain⋅e de vouloir annuler la création de l'événement ? Vous allez perdre toutes vos modifications.",
"Are you sure you want to cancel the event edition? You'll lose all modifications.": "Êtes-vous certain⋅e de vouloir annuler la modification de l'événement ? Vous allez perdre toutes vos modifications.",
"Are you sure you want to cancel your participation at event \"{title}\"?": "Êtes-vous certain⋅e de vouloir annuler votre participation à l'événement « {title} » ?",
"Are you sure you want to delete this entire discussion?": "Êtes-vous certain⋅e de vouloir supprimer l'entièreté de cette discussion ?",
@ -1216,5 +1216,8 @@
"Booking": "Réservations",
"Filter by profile or group name": "Filter par nom du profil ou du groupe",
"Filter by name": "Filtrer par nom",
"Redirecting in progress…": "Redirection en cours…"
"Redirecting in progress…": "Redirection en cours…",
"Zoom in": "Zoomer",
"Zoom out": "Dézoomer",
"Show me where I am": "Afficher ma position"
}

View file

@ -1,5 +1,3 @@
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from "vue";
import Buefy from "buefy";
import Component from "vue-class-component";

View file

@ -203,41 +203,47 @@ export class EventModel implements IEvent {
}
toEditJSON(): IEventEditJSON {
return {
id: this.id,
title: this.title,
description: this.description,
beginsOn: this.beginsOn.toISOString(),
endsOn: this.endsOn ? this.endsOn.toISOString() : null,
status: this.status,
visibility: this.visibility,
joinOptions: this.joinOptions,
draft: this.draft,
tags: this.tags.map((t) => t.title),
onlineAddress: this.onlineAddress,
phoneAddress: this.phoneAddress,
physicalAddress: this.removeTypeName(this.physicalAddress),
options: this.removeTypeName(this.options),
metadata: this.metadata.map(({ key, value, type, title }) => ({
key,
value,
type,
title,
})),
attributedToId:
this.attributedTo && this.attributedTo.id ? this.attributedTo.id : null,
contacts: this.contacts.map(({ id }) => ({
id,
})),
};
}
private removeTypeName(entity: any): any {
if (entity?.__typename) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { __typename, ...purgedEntity } = entity;
return purgedEntity;
}
return entity;
return toEditJSON(this);
}
}
function removeTypeName(entity: any): any {
if (entity?.__typename) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { __typename, ...purgedEntity } = entity;
return purgedEntity;
}
return entity;
}
export function toEditJSON(event: IEvent): IEventEditJSON {
return {
id: event.id,
title: event.title,
description: event.description,
beginsOn: event.beginsOn.toISOString(),
endsOn: event.endsOn ? event.endsOn.toISOString() : null,
status: event.status,
visibility: event.visibility,
joinOptions: event.joinOptions,
draft: event.draft,
tags: event.tags.map((t) => t.title),
onlineAddress: event.onlineAddress,
phoneAddress: event.phoneAddress,
physicalAddress: removeTypeName(event.physicalAddress),
options: removeTypeName(event.options),
metadata: event.metadata.map(({ key, value, type, title }) => ({
key,
value,
type,
title,
})),
attributedToId:
event.attributedTo && event.attributedTo.id
? event.attributedTo.id
: null,
contacts: event.contacts.map(({ id }) => ({
id,
})),
};
}

View file

@ -463,7 +463,7 @@ import FullAddressAutoComplete from "@/components/Event/FullAddressAutoComplete.
import EventMetadataList from "@/components/Event/EventMetadataList.vue";
import IdentityPickerWrapper from "@/views/Account/IdentityPickerWrapper.vue";
import Subtitle from "@/components/Utils/Subtitle.vue";
import { Route } from "vue-router";
import { RawLocation, Route } from "vue-router";
import { formatList } from "@/utils/i18n";
import {
ActorType,
@ -481,7 +481,7 @@ import {
EVENT_PERSON_PARTICIPATION,
FETCH_EVENT,
} from "../../graphql/event";
import { EventModel, IEvent } from "../../types/event.model";
import { EventModel, IEvent, toEditJSON } from "../../types/event.model";
import {
CURRENT_ACTOR_CLIENT,
IDENTITIES,
@ -586,6 +586,8 @@ export default class EditEvent extends Vue {
event: IEvent = new EventModel();
unmodifiedEvent: IEvent = new EventModel();
identities: IActor[] = [];
person!: IPerson;
@ -687,12 +689,13 @@ export default class EditEvent extends Vue {
if (!(this.isUpdate || this.isDuplicate)) {
this.initializeEvent();
} else {
this.event = {
this.event = new EventModel({
...this.event,
options: cloneDeep(this.event.options),
description: this.event.description || "",
};
});
}
this.unmodifiedEvent = cloneDeep(this.event);
}
createOrUpdateDraft(e: Event): void {
@ -813,8 +816,8 @@ export default class EditEvent extends Vue {
}
get updateEventMessage(): string {
// if (this.unmodifiedEvent.draft && !this.event.draft)
// return this.$i18n.t("The event has been updated and published") as string;
if (this.unmodifiedEvent.draft && !this.event.draft)
return this.$i18n.t("The event has been updated and published") as string;
return (
this.event.draft
? this.$i18n.t("The draft event has been updated")
@ -910,7 +913,7 @@ export default class EditEvent extends Vue {
* Build variables for Event GraphQL creation query
*/
private async buildVariables() {
let res = new EventModel(this.event).toEditJSON();
let res = toEditJSON(new EventModel(this.event));
const organizerActor = this.event.organizerActor?.id
? this.event.organizerActor
: this.organizerActor;
@ -984,10 +987,12 @@ export default class EditEvent extends Vue {
/**
* Confirm cancel
*/
confirmGoElsewhere(callback: () => any): void {
if (!this.isEventModified) {
callback();
}
confirmGoElsewhere(): Promise<boolean> {
// TODO: Make calculation of changes work again and bring this back
// If the event wasn't modified, no need to warn
// if (!this.isEventModified) {
// return Promise.resolve(true);
// }
const title: string = this.isUpdate
? (this.$t("Cancel edition") as string)
: (this.$t("Cancel creation") as string);
@ -1001,14 +1006,17 @@ export default class EditEvent extends Vue {
{ title: this.event.title }
) as string);
this.$buefy.dialog.confirm({
title,
message,
confirmText: this.$t("Abandon editing") as string,
cancelText: this.$t("Continue editing") as string,
type: "is-warning",
hasIcon: true,
onConfirm: callback,
return new Promise((resolve) => {
this.$buefy.dialog.confirm({
title,
message,
confirmText: this.$t("Abandon editing") as string,
cancelText: this.$t("Continue editing") as string,
type: "is-warning",
hasIcon: true,
onConfirm: () => resolve(true),
onCancel: () => resolve(false),
});
});
}
@ -1016,21 +1024,29 @@ export default class EditEvent extends Vue {
* Confirm cancel
*/
confirmGoBack(): void {
this.confirmGoElsewhere(() => this.$router.go(-1));
this.$router.go(-1);
}
// eslint-disable-next-line consistent-return
beforeRouteLeave(to: Route, from: Route, next: () => void): void {
async beforeRouteLeave(
to: Route,
from: Route,
next: (to?: RawLocation | false | ((vm: any) => void)) => void
): Promise<void> {
if (to.name === RouteName.EVENT) return next();
this.confirmGoElsewhere(() => next());
if (await this.confirmGoElsewhere()) {
return next();
}
return next(false);
}
get isEventModified(): boolean {
// return (
// JSON.stringify(this.event.toEditJSON()) !==
// JSON.stringify(this.unmodifiedEvent)
// );
return false;
return (
this.event &&
this.unmodifiedEvent &&
JSON.stringify(toEditJSON(this.event)) !==
JSON.stringify(this.unmodifiedEvent)
);
}
get beginsOn(): Date {

View file

@ -20,13 +20,7 @@ defmodule Mobilizon.Web.Upload.Filter.AnalyzeMetadata do
|> Mogrify.open()
|> Mogrify.verbose()
upload =
upload
|> Map.put(:width, image.width)
|> Map.put(:height, image.height)
|> Map.put(:blurhash, get_blurhash(file))
{:ok, :filtered, upload}
{:ok, :filtered, %Upload{upload | width: image.width, height: image.height}}
rescue
e in ErlangError ->
Logger.warn("#{__MODULE__}: #{inspect(e)}")
@ -34,14 +28,4 @@ defmodule Mobilizon.Web.Upload.Filter.AnalyzeMetadata do
end
def filter(_), do: {:ok, :noop}
defp get_blurhash(file) do
case :eblurhash.magick(to_charlist(file)) do
{:ok, blurhash} ->
to_string(blurhash)
_ ->
nil
end
end
end

View file

@ -0,0 +1,31 @@
defmodule Mobilizon.Web.Upload.Filter.BlurHash do
@moduledoc """
Computes blurhash from the upload
"""
require Logger
alias Mobilizon.Web.Upload
@behaviour Mobilizon.Web.Upload.Filter
@spec filter(Upload.t()) ::
{:ok, :filtered, Upload.t()} | {:ok, :noop} | {:error, String.t()}
def filter(%Upload{tempfile: file, content_type: "image" <> _} = upload) do
{:ok, :filtered, %Upload{upload | blurhash: generate_blurhash(file)}}
rescue
e in ErlangError ->
Logger.warn("#{__MODULE__}: #{inspect(e)}")
{:ok, :noop}
end
def filter(_), do: {:ok, :noop}
defp generate_blurhash(file) do
case :eblurhash.magick(to_charlist(file)) do
{:ok, blurhash} ->
to_string(blurhash)
_ ->
nil
end
end
end

View file

@ -6,22 +6,27 @@ defmodule Mobilizon.Web.Upload.Filter.Resize do
"""
@behaviour Mobilizon.Web.Upload.Filter
alias Mobilizon.Web.Upload
@maximum_width 1_920
@maximum_height 1_080
def filter(%Mobilizon.Web.Upload{
tempfile: file,
content_type: "image" <> _,
width: width,
height: height
}) do
def filter(
%Upload{
tempfile: file,
content_type: "image" <> _,
width: width,
height: height
} = upload
) do
{new_width, new_height} = sizes = limit_sizes({width, height})
file
|> Mogrify.open()
|> Mogrify.resize(string(limit_sizes({width, height})))
|> Mogrify.resize(string(sizes))
|> Mogrify.save(in_place: true)
{:ok, :filtered}
{:ok, :filtered, %Upload{upload | width: new_width, height: new_height}}
end
def filter(_), do: {:ok, :noop}

View file

@ -62,9 +62,10 @@ defmodule Mobilizon.Web.Upload do
path: String.t(),
size: integer(),
width: integer(),
height: integer()
height: integer(),
blurhash: String.t()
}
defstruct [:id, :name, :tempfile, :content_type, :path, :size, :width, :height]
defstruct [:id, :name, :tempfile, :content_type, :path, :size, :width, :height, :blurhash]
@spec store(source, options :: [option()]) :: {:ok, map()} | {:error, any()}
def store(upload, opts \\ []) do