Merge branch 'bugs' into 'master'

Add dir="auto" to most user generated content

See merge request framasoft/mobilizon!1100
This commit is contained in:
Thomas Citharel 2021-11-07 17:40:13 +00:00
commit 7a30c92651
29 changed files with 154 additions and 77 deletions

View File

@ -1,27 +1,25 @@
<template> <template>
<div> <div class="media" style="align-items: top" dir="auto">
<div class="media" style="align-items: top"> <div class="media-left">
<div class="media-left"> <figure class="image is-32x32" v-if="actor.avatar">
<figure class="image is-32x32" v-if="actor.avatar"> <img class="is-rounded" :src="actor.avatar.url" alt="" />
<img class="is-rounded" :src="actor.avatar.url" alt="" /> </figure>
</figure> <b-icon v-else size="is-medium" icon="account-circle" />
<b-icon v-else size="is-medium" icon="account-circle" /> </div>
</div>
<div class="media-content"> <div class="media-content">
<p> <p>
{{ actor.name || `@${usernameWithDomain(actor)}` }} {{ actor.name || `@${usernameWithDomain(actor)}` }}
</p> </p>
<p class="has-text-grey-dark" v-if="actor.name"> <p class="has-text-grey-dark" v-if="actor.name">
@{{ usernameWithDomain(actor) }} @{{ usernameWithDomain(actor) }}
</p> </p>
<div <div
v-if="full" v-if="full"
class="summary" class="summary"
:class="{ limit: limit }" :class="{ limit: limit }"
v-html="actor.summary" v-html="actor.summary"
/> />
</div>
</div> </div>
</div> </div>
</template> </template>
@ -54,6 +52,14 @@ export default class ActorCard extends Vue {
<style lang="scss"> <style lang="scss">
@use "@/styles/_mixins" as *; @use "@/styles/_mixins" as *;
.media {
.media-left {
margin-right: initial;
@include margin-right(1rem);
}
}
.tooltip { .tooltip {
display: block !important; display: block !important;
z-index: 10000; z-index: 10000;

View File

@ -1,5 +1,5 @@
<template> <template>
<address> <address dir="auto">
<b-icon <b-icon
v-if="showIcon" v-if="showIcon"
:icon="address.poiInfos.poiIcon.icon" :icon="address.poiInfos.poiIcon.icon"

View File

@ -7,7 +7,7 @@
}" }"
class="comment-element" class="comment-element"
> >
<article class="media" :id="commentId"> <article class="media" :id="commentId" dir="auto">
<popover-actor-card <popover-actor-card
:actor="comment.actor" :actor="comment.actor"
:inline="true" :inline="true"
@ -32,11 +32,11 @@
</div> </div>
<div class="media-content"> <div class="media-content">
<div class="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 }">{{ <strong :class="{ organizer: commentFromOrganizer }">{{
comment.actor.name comment.actor.name
}}</strong> }}</strong>
<small>{{ usernameWithDomain(comment.actor) }}</small> <small>@{{ usernameWithDomain(comment.actor) }}</small>
</span> </span>
<a v-else class="comment-link" :href="commentURL"> <a v-else class="comment-link" :href="commentURL">
<span>{{ $t("[deleted]") }}</span> <span>{{ $t("[deleted]") }}</span>
@ -63,7 +63,7 @@
</button> </button>
</span> </span>
<br /> <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 v-else>{{ $t("[This comment has been deleted]") }}</div>
<div class="load-replies" v-if="comment.totalReplies"> <div class="load-replies" v-if="comment.totalReplies">
<p v-if="!showReplies" @click="fetchReplies"> <p v-if="!showReplies" @click="fetchReplies">
@ -446,9 +446,12 @@ a.comment-link {
.media .media-content { .media .media-content {
overflow-x: initial; overflow-x: initial;
.content .editor-line { .content {
display: flex; text-align: start;
align-items: center; .editor-line {
display: flex;
align-items: center;
}
} }
.icons { .icons {

View File

@ -59,7 +59,7 @@
> >
{{ $t("Loading comments…") }} {{ $t("Loading comments…") }}
</p> </p>
<transition-group name="comment-empty-list" mode="out-in" v-else> <transition-group tag="div" name="comment-empty-list" v-else>
<transition-group <transition-group
key="list" key="list"
name="comment-list" name="comment-list"

View File

@ -10,7 +10,7 @@
<b-icon v-else size="is-large" icon="account-circle" /> <b-icon v-else size="is-large" icon="account-circle" />
</div> </div>
<div class="body"> <div class="body">
<div class="meta"> <div class="meta" dir="auto">
<span <span
class="first-line name" class="first-line name"
v-if="comment.actor && !comment.deletedAt" v-if="comment.actor && !comment.deletedAt"
@ -64,7 +64,11 @@
> >
</div> </div>
</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> <div class="description-content" v-html="comment.text"></div>
<p <p
v-if=" v-if="

View File

@ -1,6 +1,7 @@
<template> <template>
<router-link <router-link
class="discussion-minimalist-card-wrapper" class="discussion-minimalist-card-wrapper"
dir="auto"
:to="{ :to="{
name: RouteName.DISCUSSION, name: RouteName.DISCUSSION,
params: { slug: discussion.slug, id: discussion.id }, params: { slug: discussion.slug, id: discussion.id },
@ -37,6 +38,7 @@
</div> </div>
<div <div
class="ellipsis has-text-grey-dark" class="ellipsis has-text-grey-dark"
dir="auto"
v-if="!discussion.lastComment.deletedAt" v-if="!discussion.lastComment.deletedAt"
> >
{{ htmlTextEllipsis }} {{ htmlTextEllipsis }}

View File

@ -211,6 +211,7 @@ import ListItem from "@tiptap/extension-list-item";
import Underline from "@tiptap/extension-underline"; import Underline from "@tiptap/extension-underline";
import Link from "@tiptap/extension-link"; import Link from "@tiptap/extension-link";
import CharacterCount from "@tiptap/extension-character-count"; import CharacterCount from "@tiptap/extension-character-count";
import { AutoDir } from "./Editor/Autodir";
@Component({ @Component({
components: { EditorContent, BubbleMenu }, components: { EditorContent, BubbleMenu },
@ -274,6 +275,7 @@ export default class EditorComponent extends Vue {
ListItem, ListItem,
Mention.configure(MentionOptions), Mention.configure(MentionOptions),
CustomImage, CustomImage,
AutoDir,
Underline, Underline,
Link.configure({ Link.configure({
HTMLAttributes: { target: "_blank", rel: "noopener noreferrer ugc" }, HTMLAttributes: { target: "_blank", rel: "noopener noreferrer ugc" },

View 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",
},
},
},
];
},
});

View File

@ -27,6 +27,7 @@ const debouncedFetchItems = pDebounce(fetchItems, 200);
const mentionOptions: Partial<any> = { const mentionOptions: Partial<any> = {
HTMLAttributes: { HTMLAttributes: {
class: "mention", class: "mention",
dir: "ltr",
}, },
suggestion: { suggestion: {
items: async (query: string): Promise<IPerson[]> => { items: async (query: string): Promise<IPerson[]> => {

View File

@ -12,6 +12,7 @@
expanded expanded
@select="updateSelected" @select="updateSelected"
v-bind="$attrs" v-bind="$attrs"
dir="auto"
> >
<template #default="{ option }"> <template #default="{ option }">
<b-icon :icon="option.poiInfos.poiIcon.icon" /> <b-icon :icon="option.poiInfos.poiIcon.icon" />

View File

@ -24,7 +24,7 @@
v-for="tag in (event.tags || []).slice(0, 3)" v-for="tag in (event.tags || []).slice(0, 3)"
:key="tag.slug" :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> </router-link>
</div> </div>
</figure> </figure>
@ -39,9 +39,11 @@
/> />
</div> </div>
<div class="media-content"> <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="content-end">
<div class="event-organizer"> <div class="event-organizer" dir="auto">
<figure <figure
class="image is-24x24" class="image is-24x24"
v-if="organizer(event) && organizer(event).avatar" v-if="organizer(event) && organizer(event).avatar"
@ -58,12 +60,14 @@
</span> </span>
</div> </div>
<inline-address <inline-address
dir="auto"
v-if="event.physicalAddress" v-if="event.physicalAddress"
class="event-subtitle" class="event-subtitle"
:physical-address="event.physicalAddress" :physical-address="event.physicalAddress"
/> />
<div <div
class="event-subtitle" class="event-subtitle"
dir="auto"
v-else-if="event.options && event.options.isOnline" v-else-if="event.options && event.options.isOnline"
> >
<b-icon icon="video" /> <b-icon icon="video" />

View File

@ -44,6 +44,7 @@ div.eventMetadataBlock {
.content-wrapper { .content-wrapper {
overflow: hidden; overflow: hidden;
width: 100%;
&.padding-left { &.padding-left {
padding: 0 20px; padding: 0 20px;

View File

@ -1,6 +1,6 @@
<template> <template>
<article class="box mb-5 mt-4"> <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"> <figure class="image is-24x24" v-if="participation.actor.avatar">
<img <img
class="is-rounded" class="is-rounded"
@ -43,7 +43,7 @@
</router-link> </router-link>
</div> </div>
</div> </div>
<div class="list-card-content"> <div class="list-card-content" dir="auto">
<div class="title-wrapper"> <div class="title-wrapper">
<router-link <router-link
:to="{ :to="{

View File

@ -23,13 +23,15 @@
<b-icon v-else size="is-large" icon="account-group" /> <b-icon v-else size="is-large" icon="account-group" />
</div> </div>
<div class="media-content"> <div class="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"> <span class="is-6 has-text-grey-dark group-federated-username">
{{ `@${usernameWithDomain(group)}` }} {{ `@${usernameWithDomain(group)}` }}
</span> </span>
</div> </div>
</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"> <div class="card-custom-footer">
<inline-address <inline-address
class="has-text-grey-dark" class="has-text-grey-dark"

View File

@ -1,13 +1,13 @@
<template> <template>
<div class="card"> <div class="card">
<div class="identity-header"> <div class="identity-header" dir="auto">
<figure class="image is-24x24" v-if="member.actor.avatar"> <figure class="image is-24x24" v-if="member.actor.avatar">
<img class="is-rounded" :src="member.actor.avatar.url" alt="" /> <img class="is-rounded" :src="member.actor.avatar.url" alt="" />
</figure> </figure>
<b-icon v-else icon="account-circle" /> <b-icon v-else icon="account-circle" />
{{ displayNameAndUsername(member.actor) }} {{ displayNameAndUsername(member.actor) }}
</div> </div>
<div class="card-content"> <div class="card-content" dir="auto">
<div> <div>
<div class="media"> <div class="media">
<div class="media-left"> <div class="media-left">
@ -16,7 +16,7 @@
</figure> </figure>
<b-icon v-else size="is-large" icon="account-group" /> <b-icon v-else size="is-large" icon="account-group" />
</div> </div>
<div class="media-content"> <div class="media-content" dir="auto">
<router-link <router-link
:to="{ :to="{
name: RouteName.GROUP, name: RouteName.GROUP,
@ -27,10 +27,7 @@
> >
<h2>{{ member.parent.name }}</h2> <h2>{{ member.parent.name }}</h2>
<p class="is-6 has-text-grey-dark"> <p class="is-6 has-text-grey-dark">
<span v-if="member.parent.domain">{{ <span>{{ `@${usernameWithDomain(member.parent)}` }}</span>
`@${member.parent.preferredUsername}@${member.parent.domain}`
}}</span>
<span v-else>{{ `@${member.parent.preferredUsername}` }}</span>
<b-taglist> <b-taglist>
<b-tag <b-tag
type="is-info" type="is-info"

View File

@ -18,6 +18,7 @@
class="absolute top-0 left-0 transition-opacity duration-500" class="absolute top-0 left-0 transition-opacity duration-500"
:class="{ isLoaded: isLoaded ? 'opacity-100' : 'opacity-0', rounded }" :class="{ isLoaded: isLoaded ? 'opacity-100' : 'opacity-0', rounded }"
alt="" alt=""
src=""
/> />
</div> </div>
</div> </div>

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="resource-wrapper"> <div class="resource-wrapper" dir="auto">
<router-link <router-link
:to="{ :to="{
name: RouteName.RESOURCE_FOLDER, name: RouteName.RESOURCE_FOLDER,

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="resource-wrapper"> <div class="resource-wrapper" dir="auto">
<a :href="resource.resourceUrl" target="_blank"> <a :href="resource.resourceUrl" target="_blank">
<div class="preview"> <div class="preview">
<div <div

View File

@ -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.", "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", "View full profile": "View full profile",
"Any type": "Any type", "Any type": "Any type",
"In person": "In person" "In person": "In person",
"In the past": "In the past"
} }

View File

@ -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.", "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", "View full profile": "Voir le profil complet",
"Any type": "N'importe quel type", "Any type": "N'importe quel type",
"In person": "En personne" "In person": "En personne",
"In the past": "Dans le passé"
} }

View File

@ -47,6 +47,7 @@
v-model="identity.name" v-model="identity.name"
@input="autoUpdateUsername($event)" @input="autoUpdateUsername($event)"
id="identity-display-name" id="identity-display-name"
dir="auto"
/> />
</b-field> </b-field>
@ -64,6 +65,7 @@
required required
v-model="identity.preferredUsername" v-model="identity.preferredUsername"
:disabled="isUpdate" :disabled="isUpdate"
dir="auto"
:use-html5-validation="!isUpdate" :use-html5-validation="!isUpdate"
pattern="[a-z0-9_]+" pattern="[a-z0-9_]+"
id="identity-username" id="identity-username"
@ -82,6 +84,7 @@
> >
<b-input <b-input
type="textarea" type="textarea"
dir="auto"
aria-required="false" aria-required="false"
v-model="identity.summary" v-model="identity.summary"
id="identity-summary" id="identity-summary"
@ -190,7 +193,8 @@
</div> </div>
</template> </template>
<style scoped type="scss"> <style scoped lang="scss">
@use "@/styles/_mixins" as *;
h1 { h1 {
display: flex; display: flex;
justify-content: center; justify-content: center;

View File

@ -45,7 +45,7 @@
{{ error }} {{ error }}
</b-message> </b-message>
<section> <section>
<div class="discussion-title"> <div class="discussion-title" dir="auto">
<h1 class="title" v-if="discussion.title && !editTitleMode"> <h1 class="title" v-if="discussion.title && !editTitleMode">
{{ discussion.title }} {{ discussion.title }}
</h1> </h1>

View File

@ -65,7 +65,7 @@
{{ $t("There's no discussions yet") }} {{ $t("There's no discussions yet") }}
</empty-content> </empty-content>
</section> </section>
<section class="section" v-else> <section class="section" v-else-if="!$apollo.loading">
<empty-content icon="chat"> <empty-content icon="chat">
{{ $t("Only group members can access discussions") }} {{ $t("Only group members can access discussions") }}
<template #desc> <template #desc>

View File

@ -10,9 +10,11 @@
<section class="intro"> <section class="intro">
<div class="columns"> <div class="columns">
<div class="column"> <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"> <div class="organizer">
<span v-if="event.organizerActor && !event.attributedTo"> <div v-if="event.organizerActor && !event.attributedTo">
<popover-actor-card <popover-actor-card
:actor="event.organizerActor" :actor="event.organizerActor"
:inline="true" :inline="true"
@ -25,7 +27,7 @@
}} }}
</span> </span>
</popover-actor-card> </popover-actor-card>
</span> </div>
<span <span
v-else-if=" v-else-if="
event.attributedTo && event.attributedTo &&
@ -70,7 +72,11 @@
</i18n> </i18n>
</span> </span>
</div> </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 <router-link
v-for="tag in event.tags" v-for="tag in event.tags"
:key="tag.title" :key="tag.title"
@ -316,6 +322,7 @@
</p> </p>
<div v-else> <div v-else>
<div <div
dir="auto"
class="description-content" class="description-content"
ref="eventDescriptionElement" ref="eventDescriptionElement"
v-html="event.description" v-html="event.description"

View File

@ -5,7 +5,7 @@
<lazy-image-wrapper :picture="post.picture" /> <lazy-image-wrapper :picture="post.picture" />
</div> </div>
<div class="heading-section"> <div class="heading-section">
<div class="heading-wrapper"> <div class="heading-wrapper" dir="auto">
<div class="title-metadata"> <div class="title-metadata">
<div class="title-wrapper"> <div class="title-wrapper">
<b-tag <b-tag
@ -165,8 +165,8 @@
}} }}
</b-message> </b-message>
<section v-html="post.body" class="content" /> <section v-html="post.body" dir="auto" class="content" />
<section class="tags"> <section class="tags" dir="auto">
<router-link <router-link
v-for="tag in post.tags" v-for="tag in post.tags"
:key="tag.title" :key="tag.title"

View File

@ -213,7 +213,7 @@ import debounce from "lodash/debounce";
interface ISearchTimeOption { interface ISearchTimeOption {
label: string; label: string;
start?: Date; start?: Date | null;
end?: Date | null; end?: Date | null;
} }
@ -292,6 +292,11 @@ export default class Search extends Vue {
location: IAddress = new Address(); location: IAddress = new Address();
dateOptions: Record<string, ISearchTimeOption> = { dateOptions: Record<string, ISearchTimeOption> = {
past: {
label: this.$t("In the past") as string,
start: null,
end: new Date(),
},
today: { today: {
label: this.$t("Today") as string, label: this.$t("Today") as string,
start: new Date(), start: new Date(),
@ -346,7 +351,7 @@ export default class Search extends Vue {
data(): Record<string, unknown> { data(): Record<string, unknown> {
return { 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]) { if (this.dateOptions[this.when]) {
return this.dateOptions[this.when].start; return this.dateOptions[this.when].start;
} }

View File

@ -24,7 +24,7 @@ exports[`CommentTree renders a comment tree with comments 1`] = `
</div> </div>
</article> </article>
</form> </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"> <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>
<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> </div>
</article> </article>
</form> </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> <div class="no-comments"><span>No comments yet</span></div>
</transition-group-stub> </transition-group-stub>
</div> </div>

View File

@ -1258,23 +1258,27 @@ defmodule Mobilizon.Events do
defp events_for_begins_on(query, args) do defp events_for_begins_on(query, args) do
begins_on = Map.get(args, :begins_on, DateTime.utc_now()) begins_on = Map.get(args, :begins_on, DateTime.utc_now())
query if is_nil(begins_on) do
|> where([q], q.begins_on >= ^begins_on) query
else
where(query, [q], q.begins_on >= ^begins_on)
end
end end
@spec events_for_ends_on(Ecto.Queryable.t(), map()) :: Ecto.Query.t() @spec events_for_ends_on(Ecto.Queryable.t(), map()) :: Ecto.Query.t()
defp events_for_ends_on(query, args) do defp events_for_ends_on(query, args) do
ends_on = Map.get(args, :ends_on) ends_on = Map.get(args, :ends_on)
if is_nil(ends_on), if is_nil(ends_on) do
do: query, query
else: else
where( where(
query, query,
[q], [q],
(is_nil(q.ends_on) and q.begins_on <= ^ends_on) or (is_nil(q.ends_on) and q.begins_on <= ^ends_on) or
q.ends_on <= ^ends_on q.ends_on <= ^ends_on
) )
end
end end
@spec events_for_tags(Ecto.Queryable.t(), map()) :: Ecto.Query.t() @spec events_for_tags(Ecto.Queryable.t(), map()) :: Ecto.Query.t()

View File

@ -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, "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(:span, ["data-user"])
Meta.allow_tag_with_these_attributes(:h1, []) Meta.allow_tag_with_these_attributes(:h1, [])