Merge branch 'bugs' into 'master'
Add dir="auto" to most user generated content See merge request framasoft/mobilizon!1100
This commit is contained in:
commit
7a30c92651
@ -1,27 +1,25 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="media" style="align-items: top">
|
||||
<div class="media-left">
|
||||
<figure class="image is-32x32" 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="media" style="align-items: top" dir="auto">
|
||||
<div class="media-left">
|
||||
<figure class="image is-32x32" 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="media-content">
|
||||
<p>
|
||||
{{ actor.name || `@${usernameWithDomain(actor)}` }}
|
||||
</p>
|
||||
<p class="has-text-grey-dark" v-if="actor.name">
|
||||
@{{ usernameWithDomain(actor) }}
|
||||
</p>
|
||||
<div
|
||||
v-if="full"
|
||||
class="summary"
|
||||
:class="{ limit: limit }"
|
||||
v-html="actor.summary"
|
||||
/>
|
||||
</div>
|
||||
<div class="media-content">
|
||||
<p>
|
||||
{{ actor.name || `@${usernameWithDomain(actor)}` }}
|
||||
</p>
|
||||
<p class="has-text-grey-dark" v-if="actor.name">
|
||||
@{{ usernameWithDomain(actor) }}
|
||||
</p>
|
||||
<div
|
||||
v-if="full"
|
||||
class="summary"
|
||||
:class="{ limit: limit }"
|
||||
v-html="actor.summary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -54,6 +52,14 @@ export default class ActorCard extends Vue {
|
||||
|
||||
<style lang="scss">
|
||||
@use "@/styles/_mixins" as *;
|
||||
|
||||
.media {
|
||||
.media-left {
|
||||
margin-right: initial;
|
||||
@include margin-right(1rem);
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
display: block !important;
|
||||
z-index: 10000;
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<address>
|
||||
<address dir="auto">
|
||||
<b-icon
|
||||
v-if="showIcon"
|
||||
:icon="address.poiInfos.poiIcon.icon"
|
||||
|
@ -7,7 +7,7 @@
|
||||
}"
|
||||
class="comment-element"
|
||||
>
|
||||
<article class="media" :id="commentId">
|
||||
<article class="media" :id="commentId" dir="auto">
|
||||
<popover-actor-card
|
||||
:actor="comment.actor"
|
||||
:inline="true"
|
||||
@ -32,11 +32,11 @@
|
||||
</div>
|
||||
<div class="media-content">
|
||||
<div class="content">
|
||||
<span class="first-line" v-if="!comment.deletedAt">
|
||||
<span class="first-line" v-if="!comment.deletedAt" dir="auto">
|
||||
<strong :class="{ organizer: commentFromOrganizer }">{{
|
||||
comment.actor.name
|
||||
}}</strong>
|
||||
<small>{{ usernameWithDomain(comment.actor) }}</small>
|
||||
<small>@{{ usernameWithDomain(comment.actor) }}</small>
|
||||
</span>
|
||||
<a v-else class="comment-link" :href="commentURL">
|
||||
<span>{{ $t("[deleted]") }}</span>
|
||||
@ -63,7 +63,7 @@
|
||||
</button>
|
||||
</span>
|
||||
<br />
|
||||
<div v-if="!comment.deletedAt" v-html="comment.text" />
|
||||
<div v-if="!comment.deletedAt" v-html="comment.text" dir="auto" />
|
||||
<div v-else>{{ $t("[This comment has been deleted]") }}</div>
|
||||
<div class="load-replies" v-if="comment.totalReplies">
|
||||
<p v-if="!showReplies" @click="fetchReplies">
|
||||
@ -446,9 +446,12 @@ a.comment-link {
|
||||
|
||||
.media .media-content {
|
||||
overflow-x: initial;
|
||||
.content .editor-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.content {
|
||||
text-align: start;
|
||||
.editor-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.icons {
|
||||
|
@ -59,7 +59,7 @@
|
||||
>
|
||||
{{ $t("Loading comments…") }}
|
||||
</p>
|
||||
<transition-group name="comment-empty-list" mode="out-in" v-else>
|
||||
<transition-group tag="div" name="comment-empty-list" v-else>
|
||||
<transition-group
|
||||
key="list"
|
||||
name="comment-list"
|
||||
|
@ -10,7 +10,7 @@
|
||||
<b-icon v-else size="is-large" icon="account-circle" />
|
||||
</div>
|
||||
<div class="body">
|
||||
<div class="meta">
|
||||
<div class="meta" dir="auto">
|
||||
<span
|
||||
class="first-line name"
|
||||
v-if="comment.actor && !comment.deletedAt"
|
||||
@ -64,7 +64,11 @@
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!editMode && !comment.deletedAt" class="text-wrapper">
|
||||
<div
|
||||
v-if="!editMode && !comment.deletedAt"
|
||||
class="text-wrapper"
|
||||
dir="auto"
|
||||
>
|
||||
<div class="description-content" v-html="comment.text"></div>
|
||||
<p
|
||||
v-if="
|
||||
|
@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<router-link
|
||||
class="discussion-minimalist-card-wrapper"
|
||||
dir="auto"
|
||||
:to="{
|
||||
name: RouteName.DISCUSSION,
|
||||
params: { slug: discussion.slug, id: discussion.id },
|
||||
@ -37,6 +38,7 @@
|
||||
</div>
|
||||
<div
|
||||
class="ellipsis has-text-grey-dark"
|
||||
dir="auto"
|
||||
v-if="!discussion.lastComment.deletedAt"
|
||||
>
|
||||
{{ htmlTextEllipsis }}
|
||||
|
@ -211,6 +211,7 @@ import ListItem from "@tiptap/extension-list-item";
|
||||
import Underline from "@tiptap/extension-underline";
|
||||
import Link from "@tiptap/extension-link";
|
||||
import CharacterCount from "@tiptap/extension-character-count";
|
||||
import { AutoDir } from "./Editor/Autodir";
|
||||
|
||||
@Component({
|
||||
components: { EditorContent, BubbleMenu },
|
||||
@ -274,6 +275,7 @@ export default class EditorComponent extends Vue {
|
||||
ListItem,
|
||||
Mention.configure(MentionOptions),
|
||||
CustomImage,
|
||||
AutoDir,
|
||||
Underline,
|
||||
Link.configure({
|
||||
HTMLAttributes: { target: "_blank", rel: "noopener noreferrer ugc" },
|
||||
|
30
js/src/components/Editor/Autodir.ts
Normal file
30
js/src/components/Editor/Autodir.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { Extension } from "@tiptap/core";
|
||||
|
||||
/**
|
||||
* Allows to set dir="auto" on top nodes
|
||||
* Taken from https://github.com/ueberdosis/tiptap/issues/1621#issuecomment-918990408
|
||||
*/
|
||||
export const AutoDir = Extension.create({
|
||||
name: "AutoDir",
|
||||
addGlobalAttributes() {
|
||||
return [
|
||||
{
|
||||
types: [
|
||||
"heading",
|
||||
"paragraph",
|
||||
"bulletList",
|
||||
"orderedList",
|
||||
"blockquote",
|
||||
],
|
||||
attributes: {
|
||||
autoDir: {
|
||||
renderHTML: () => ({
|
||||
dir: "auto",
|
||||
}),
|
||||
parseHTML: (element) => element.dir || "auto",
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
});
|
@ -27,6 +27,7 @@ const debouncedFetchItems = pDebounce(fetchItems, 200);
|
||||
const mentionOptions: Partial<any> = {
|
||||
HTMLAttributes: {
|
||||
class: "mention",
|
||||
dir: "ltr",
|
||||
},
|
||||
suggestion: {
|
||||
items: async (query: string): Promise<IPerson[]> => {
|
||||
|
@ -12,6 +12,7 @@
|
||||
expanded
|
||||
@select="updateSelected"
|
||||
v-bind="$attrs"
|
||||
dir="auto"
|
||||
>
|
||||
<template #default="{ option }">
|
||||
<b-icon :icon="option.poiInfos.poiIcon.icon" />
|
||||
|
@ -24,7 +24,7 @@
|
||||
v-for="tag in (event.tags || []).slice(0, 3)"
|
||||
:key="tag.slug"
|
||||
>
|
||||
<b-tag type="is-light">{{ tag.title }}</b-tag>
|
||||
<b-tag type="is-light" dir="auto">{{ tag.title }}</b-tag>
|
||||
</router-link>
|
||||
</div>
|
||||
</figure>
|
||||
@ -39,9 +39,11 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="media-content">
|
||||
<h3 class="event-title" :title="event.title">{{ event.title }}</h3>
|
||||
<h3 class="event-title" :title="event.title" dir="auto">
|
||||
{{ event.title }}
|
||||
</h3>
|
||||
<div class="content-end">
|
||||
<div class="event-organizer">
|
||||
<div class="event-organizer" dir="auto">
|
||||
<figure
|
||||
class="image is-24x24"
|
||||
v-if="organizer(event) && organizer(event).avatar"
|
||||
@ -58,12 +60,14 @@
|
||||
</span>
|
||||
</div>
|
||||
<inline-address
|
||||
dir="auto"
|
||||
v-if="event.physicalAddress"
|
||||
class="event-subtitle"
|
||||
:physical-address="event.physicalAddress"
|
||||
/>
|
||||
<div
|
||||
class="event-subtitle"
|
||||
dir="auto"
|
||||
v-else-if="event.options && event.options.isOnline"
|
||||
>
|
||||
<b-icon icon="video" />
|
||||
|
@ -44,6 +44,7 @@ div.eventMetadataBlock {
|
||||
|
||||
.content-wrapper {
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
|
||||
&.padding-left {
|
||||
padding: 0 20px;
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<article class="box mb-5 mt-4">
|
||||
<div class="identity-header">
|
||||
<div class="identity-header" dir="auto">
|
||||
<figure class="image is-24x24" v-if="participation.actor.avatar">
|
||||
<img
|
||||
class="is-rounded"
|
||||
@ -43,7 +43,7 @@
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-card-content">
|
||||
<div class="list-card-content" dir="auto">
|
||||
<div class="title-wrapper">
|
||||
<router-link
|
||||
:to="{
|
||||
|
@ -23,13 +23,15 @@
|
||||
<b-icon v-else size="is-large" icon="account-group" />
|
||||
</div>
|
||||
<div class="media-content">
|
||||
<h3 class="is-size-5 group-title">{{ displayName(group) }}</h3>
|
||||
<h3 class="is-size-5 group-title" dir="auto">
|
||||
{{ displayName(group) }}
|
||||
</h3>
|
||||
<span class="is-6 has-text-grey-dark group-federated-username">
|
||||
{{ `@${usernameWithDomain(group)}` }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content mb-2" v-html="group.summary" />
|
||||
<div class="content mb-2" dir="auto" v-html="group.summary" />
|
||||
<div class="card-custom-footer">
|
||||
<inline-address
|
||||
class="has-text-grey-dark"
|
||||
|
@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<div class="card">
|
||||
<div class="identity-header">
|
||||
<div class="identity-header" dir="auto">
|
||||
<figure class="image is-24x24" v-if="member.actor.avatar">
|
||||
<img class="is-rounded" :src="member.actor.avatar.url" alt="" />
|
||||
</figure>
|
||||
<b-icon v-else icon="account-circle" />
|
||||
{{ displayNameAndUsername(member.actor) }}
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="card-content" dir="auto">
|
||||
<div>
|
||||
<div class="media">
|
||||
<div class="media-left">
|
||||
@ -16,7 +16,7 @@
|
||||
</figure>
|
||||
<b-icon v-else size="is-large" icon="account-group" />
|
||||
</div>
|
||||
<div class="media-content">
|
||||
<div class="media-content" dir="auto">
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.GROUP,
|
||||
@ -27,10 +27,7 @@
|
||||
>
|
||||
<h2>{{ member.parent.name }}</h2>
|
||||
<p class="is-6 has-text-grey-dark">
|
||||
<span v-if="member.parent.domain">{{
|
||||
`@${member.parent.preferredUsername}@${member.parent.domain}`
|
||||
}}</span>
|
||||
<span v-else>{{ `@${member.parent.preferredUsername}` }}</span>
|
||||
<span>{{ `@${usernameWithDomain(member.parent)}` }}</span>
|
||||
<b-taglist>
|
||||
<b-tag
|
||||
type="is-info"
|
||||
|
@ -18,6 +18,7 @@
|
||||
class="absolute top-0 left-0 transition-opacity duration-500"
|
||||
:class="{ isLoaded: isLoaded ? 'opacity-100' : 'opacity-0', rounded }"
|
||||
alt=""
|
||||
src=""
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="resource-wrapper">
|
||||
<div class="resource-wrapper" dir="auto">
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.RESOURCE_FOLDER,
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="resource-wrapper">
|
||||
<div class="resource-wrapper" dir="auto">
|
||||
<a :href="resource.resourceUrl" target="_blank">
|
||||
<div class="preview">
|
||||
<div
|
||||
|
@ -1232,5 +1232,6 @@
|
||||
"This profile is from another instance, the informations shown here may be incomplete.": "This profile is from another instance, the informations shown here may be incomplete.",
|
||||
"View full profile": "View full profile",
|
||||
"Any type": "Any type",
|
||||
"In person": "In person"
|
||||
"In person": "In person",
|
||||
"In the past": "In the past"
|
||||
}
|
||||
|
@ -1336,5 +1336,6 @@
|
||||
"This profile is from another instance, the informations shown here may be incomplete.": "Ce profil provient d'une autre instance, les informations montrées ici peuvent être incomplètes.",
|
||||
"View full profile": "Voir le profil complet",
|
||||
"Any type": "N'importe quel type",
|
||||
"In person": "En personne"
|
||||
"In person": "En personne",
|
||||
"In the past": "Dans le passé"
|
||||
}
|
||||
|
@ -47,6 +47,7 @@
|
||||
v-model="identity.name"
|
||||
@input="autoUpdateUsername($event)"
|
||||
id="identity-display-name"
|
||||
dir="auto"
|
||||
/>
|
||||
</b-field>
|
||||
|
||||
@ -64,6 +65,7 @@
|
||||
required
|
||||
v-model="identity.preferredUsername"
|
||||
:disabled="isUpdate"
|
||||
dir="auto"
|
||||
:use-html5-validation="!isUpdate"
|
||||
pattern="[a-z0-9_]+"
|
||||
id="identity-username"
|
||||
@ -82,6 +84,7 @@
|
||||
>
|
||||
<b-input
|
||||
type="textarea"
|
||||
dir="auto"
|
||||
aria-required="false"
|
||||
v-model="identity.summary"
|
||||
id="identity-summary"
|
||||
@ -190,7 +193,8 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped type="scss">
|
||||
<style scoped lang="scss">
|
||||
@use "@/styles/_mixins" as *;
|
||||
h1 {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
@ -45,7 +45,7 @@
|
||||
{{ error }}
|
||||
</b-message>
|
||||
<section>
|
||||
<div class="discussion-title">
|
||||
<div class="discussion-title" dir="auto">
|
||||
<h1 class="title" v-if="discussion.title && !editTitleMode">
|
||||
{{ discussion.title }}
|
||||
</h1>
|
||||
|
@ -65,7 +65,7 @@
|
||||
{{ $t("There's no discussions yet") }}
|
||||
</empty-content>
|
||||
</section>
|
||||
<section class="section" v-else>
|
||||
<section class="section" v-else-if="!$apollo.loading">
|
||||
<empty-content icon="chat">
|
||||
{{ $t("Only group members can access discussions") }}
|
||||
<template #desc>
|
||||
|
@ -10,9 +10,11 @@
|
||||
<section class="intro">
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<h1 class="title" style="margin: 0">{{ event.title }}</h1>
|
||||
<h1 class="title" style="margin: 0" dir="auto">
|
||||
{{ event.title }}
|
||||
</h1>
|
||||
<div class="organizer">
|
||||
<span v-if="event.organizerActor && !event.attributedTo">
|
||||
<div v-if="event.organizerActor && !event.attributedTo">
|
||||
<popover-actor-card
|
||||
:actor="event.organizerActor"
|
||||
:inline="true"
|
||||
@ -25,7 +27,7 @@
|
||||
}}
|
||||
</span>
|
||||
</popover-actor-card>
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
v-else-if="
|
||||
event.attributedTo &&
|
||||
@ -70,7 +72,11 @@
|
||||
</i18n>
|
||||
</span>
|
||||
</div>
|
||||
<p class="tags" v-if="event.tags && event.tags.length > 0">
|
||||
<p
|
||||
class="tags"
|
||||
v-if="event.tags && event.tags.length > 0"
|
||||
dir="auto"
|
||||
>
|
||||
<router-link
|
||||
v-for="tag in event.tags"
|
||||
:key="tag.title"
|
||||
@ -316,6 +322,7 @@
|
||||
</p>
|
||||
<div v-else>
|
||||
<div
|
||||
dir="auto"
|
||||
class="description-content"
|
||||
ref="eventDescriptionElement"
|
||||
v-html="event.description"
|
||||
|
@ -5,7 +5,7 @@
|
||||
<lazy-image-wrapper :picture="post.picture" />
|
||||
</div>
|
||||
<div class="heading-section">
|
||||
<div class="heading-wrapper">
|
||||
<div class="heading-wrapper" dir="auto">
|
||||
<div class="title-metadata">
|
||||
<div class="title-wrapper">
|
||||
<b-tag
|
||||
@ -165,8 +165,8 @@
|
||||
}}
|
||||
</b-message>
|
||||
|
||||
<section v-html="post.body" class="content" />
|
||||
<section class="tags">
|
||||
<section v-html="post.body" dir="auto" class="content" />
|
||||
<section class="tags" dir="auto">
|
||||
<router-link
|
||||
v-for="tag in post.tags"
|
||||
:key="tag.title"
|
||||
|
@ -213,7 +213,7 @@ import debounce from "lodash/debounce";
|
||||
|
||||
interface ISearchTimeOption {
|
||||
label: string;
|
||||
start?: Date;
|
||||
start?: Date | null;
|
||||
end?: Date | null;
|
||||
}
|
||||
|
||||
@ -292,6 +292,11 @@ export default class Search extends Vue {
|
||||
location: IAddress = new Address();
|
||||
|
||||
dateOptions: Record<string, ISearchTimeOption> = {
|
||||
past: {
|
||||
label: this.$t("In the past") as string,
|
||||
start: null,
|
||||
end: new Date(),
|
||||
},
|
||||
today: {
|
||||
label: this.$t("Today") as string,
|
||||
start: new Date(),
|
||||
@ -346,7 +351,7 @@ export default class Search extends Vue {
|
||||
|
||||
data(): Record<string, unknown> {
|
||||
return {
|
||||
debouncedUpdateSearchQuery: debounce(this.updateSearchQuery, 200),
|
||||
debouncedUpdateSearchQuery: debounce(this.updateSearchQuery, 500),
|
||||
};
|
||||
}
|
||||
|
||||
@ -525,7 +530,7 @@ export default class Search extends Vue {
|
||||
}
|
||||
};
|
||||
|
||||
get start(): Date | undefined {
|
||||
get start(): Date | undefined | null {
|
||||
if (this.dateOptions[this.when]) {
|
||||
return this.dateOptions[this.when].start;
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ exports[`CommentTree renders a comment tree with comments 1`] = `
|
||||
</div>
|
||||
</article>
|
||||
</form>
|
||||
<transition-group-stub name="comment-empty-list" mode="out-in">
|
||||
<transition-group-stub tag="div" name="comment-empty-list">
|
||||
<transition-group-stub tag="ul" name="comment-list" class="comment-list">
|
||||
<comment-stub comment="[object Object]" event="[object Object]" class="root-comment"></comment-stub>
|
||||
<comment-stub comment="[object Object]" event="[object Object]" class="root-comment"></comment-stub>
|
||||
@ -66,7 +66,7 @@ exports[`CommentTree renders an empty comment tree 1`] = `
|
||||
</div>
|
||||
</article>
|
||||
</form>
|
||||
<transition-group-stub name="comment-empty-list" mode="out-in">
|
||||
<transition-group-stub tag="div" name="comment-empty-list">
|
||||
<div class="no-comments"><span>No comments yet</span></div>
|
||||
</transition-group-stub>
|
||||
</div>
|
||||
|
@ -1258,23 +1258,27 @@ defmodule Mobilizon.Events do
|
||||
defp events_for_begins_on(query, args) do
|
||||
begins_on = Map.get(args, :begins_on, DateTime.utc_now())
|
||||
|
||||
query
|
||||
|> where([q], q.begins_on >= ^begins_on)
|
||||
if is_nil(begins_on) do
|
||||
query
|
||||
else
|
||||
where(query, [q], q.begins_on >= ^begins_on)
|
||||
end
|
||||
end
|
||||
|
||||
@spec events_for_ends_on(Ecto.Queryable.t(), map()) :: Ecto.Query.t()
|
||||
defp events_for_ends_on(query, args) do
|
||||
ends_on = Map.get(args, :ends_on)
|
||||
|
||||
if is_nil(ends_on),
|
||||
do: query,
|
||||
else:
|
||||
where(
|
||||
query,
|
||||
[q],
|
||||
(is_nil(q.ends_on) and q.begins_on <= ^ends_on) or
|
||||
q.ends_on <= ^ends_on
|
||||
)
|
||||
if is_nil(ends_on) do
|
||||
query
|
||||
else
|
||||
where(
|
||||
query,
|
||||
[q],
|
||||
(is_nil(q.ends_on) and q.begins_on <= ^ends_on) or
|
||||
q.ends_on <= ^ends_on
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@spec events_for_tags(Ecto.Queryable.t(), map()) :: Ecto.Query.t()
|
||||
|
@ -71,6 +71,7 @@ defmodule Mobilizon.Service.Formatter.DefaultScrubbler do
|
||||
])
|
||||
|
||||
Meta.allow_tag_with_this_attribute_values(:span, "class", ["h-card", "mention"])
|
||||
Meta.allow_tag_with_this_attribute_values(:span, "dir", ["ltr", "rtl", "auto"])
|
||||
Meta.allow_tag_with_these_attributes(:span, ["data-user"])
|
||||
|
||||
Meta.allow_tag_with_these_attributes(:h1, [])
|
||||
|
Loading…
Reference in New Issue
Block a user