forked from AkkomaGang/akkoma-fe
Merge branch 'develop' into 'themeApply'
# Conflicts: # CHANGELOG.md
This commit is contained in:
commit
370f1e55ad
83 changed files with 4489 additions and 1043 deletions
4
.babelrc
4
.babelrc
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"presets": ["@babel/preset-env"],
|
||||
"plugins": ["@babel/plugin-transform-runtime", "lodash", "@vue/babel-plugin-transform-vue-jsx"],
|
||||
"presets": ["@babel/preset-env", "@vue/babel-preset-jsx"],
|
||||
"plugins": ["@babel/plugin-transform-runtime", "lodash"],
|
||||
"comments": false
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ All notable changes to this project will be documented in this file.
|
|||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
## [Unreleased]
|
||||
## [2.4.0] - 2021-08-08
|
||||
### Added
|
||||
- Added a quick settings to timeline header for easier access
|
||||
- Added option to mark posts as sensitive by default
|
||||
|
@ -12,6 +12,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
- Implemented user option to hide floating shout panel
|
||||
- Implemented "edit profile" button if viewing own profile which opens profile settings
|
||||
- Added Apply and Reset buttons to the bottom of theme tab to minimize UI travel
|
||||
- Implemented user option to always show floating New Post button (normally mobile-only)
|
||||
|
||||
### Fixed
|
||||
- Fixed follow request count showing in the wrong location in mobile view
|
||||
|
|
|
@ -47,8 +47,8 @@
|
|||
"@babel/preset-env": "^7.7.6",
|
||||
"@babel/register": "^7.7.4",
|
||||
"@ungap/event-target": "^0.1.0",
|
||||
"@vue/babel-helper-vue-jsx-merge-props": "^1.0.0",
|
||||
"@vue/babel-plugin-transform-vue-jsx": "^1.1.2",
|
||||
"@vue/babel-helper-vue-jsx-merge-props": "^1.2.1",
|
||||
"@vue/babel-preset-jsx": "^1.2.4",
|
||||
"@vue/test-utils": "^1.0.0-beta.26",
|
||||
"autoprefixer": "^6.4.0",
|
||||
"babel-eslint": "^7.0.0",
|
||||
|
|
|
@ -73,6 +73,9 @@ export default {
|
|||
this.$store.state.instance.instanceSpecificPanelContent
|
||||
},
|
||||
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
|
||||
shoutboxPosition () {
|
||||
return this.$store.getters.mergedConfig.showNewPostButton || false
|
||||
},
|
||||
hideShoutbox () {
|
||||
return this.$store.getters.mergedConfig.hideShoutbox
|
||||
},
|
||||
|
|
|
@ -88,6 +88,10 @@ a {
|
|||
font-family: sans-serif;
|
||||
font-family: var(--interfaceFont, sans-serif);
|
||||
|
||||
&.-sublime {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
i[class*=icon-],
|
||||
.svg-inline--fa {
|
||||
color: $fallback--text;
|
||||
|
|
|
@ -53,6 +53,7 @@
|
|||
v-if="currentUser && shout && !hideShoutbox"
|
||||
:floating="true"
|
||||
class="floating-shout mobile-hidden"
|
||||
:class="{ 'left': shoutboxPosition }"
|
||||
/>
|
||||
<MobilePostStatusButton />
|
||||
<UserReportingModal />
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import UserCard from '../user_card/user_card.vue'
|
||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
import RichContent from 'src/components/rich_content/rich_content.jsx'
|
||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||
|
||||
const BasicUserCard = {
|
||||
|
@ -13,7 +14,8 @@ const BasicUserCard = {
|
|||
},
|
||||
components: {
|
||||
UserCard,
|
||||
UserAvatar
|
||||
UserAvatar,
|
||||
RichContent
|
||||
},
|
||||
methods: {
|
||||
toggleUserExpanded () {
|
||||
|
|
|
@ -25,17 +25,11 @@
|
|||
:title="user.name"
|
||||
class="basic-user-card-user-name"
|
||||
>
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<span
|
||||
v-if="user.name_html"
|
||||
<RichContent
|
||||
class="basic-user-card-user-name-value"
|
||||
v-html="user.name_html"
|
||||
:html="user.name"
|
||||
:emoji="user.emoji"
|
||||
/>
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
<span
|
||||
v-else
|
||||
class="basic-user-card-user-name-value"
|
||||
>{{ user.name }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<router-link
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { mapState } from 'vuex'
|
||||
import StatusContent from '../status_content/status_content.vue'
|
||||
import StatusBody from '../status_content/status_content.vue'
|
||||
import fileType from 'src/services/file_type/file_type.service'
|
||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
import AvatarList from '../avatar_list/avatar_list.vue'
|
||||
|
@ -16,7 +16,7 @@ const ChatListItem = {
|
|||
AvatarList,
|
||||
Timeago,
|
||||
ChatTitle,
|
||||
StatusContent
|
||||
StatusBody
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
|
@ -38,12 +38,14 @@ const ChatListItem = {
|
|||
},
|
||||
messageForStatusContent () {
|
||||
const message = this.chat.lastMessage
|
||||
const messageEmojis = message ? message.emojis : []
|
||||
const isYou = message && message.account_id === this.currentUser.id
|
||||
const content = message ? (this.attachmentInfo || message.content) : ''
|
||||
const messagePreview = isYou ? `<i>${this.$t('chats.you')}</i> ${content}` : content
|
||||
return {
|
||||
summary: '',
|
||||
statusnet_html: messagePreview,
|
||||
emojis: messageEmojis,
|
||||
raw_html: messagePreview,
|
||||
text: messagePreview,
|
||||
attachments: []
|
||||
}
|
||||
|
|
|
@ -77,18 +77,15 @@
|
|||
border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
|
||||
}
|
||||
|
||||
.StatusContent {
|
||||
img.emoji {
|
||||
width: 1.4em;
|
||||
height: 1.4em;
|
||||
}
|
||||
.chat-preview-body {
|
||||
--emoji-size: 1.4em;
|
||||
}
|
||||
|
||||
.time-wrapper {
|
||||
line-height: 1.4em;
|
||||
}
|
||||
|
||||
.single-line {
|
||||
.chat-preview-body {
|
||||
padding-right: 1em;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,7 +29,8 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="chat-preview">
|
||||
<StatusContent
|
||||
<StatusBody
|
||||
class="chat-preview-body"
|
||||
:status="messageForStatusContent"
|
||||
:single-line="true"
|
||||
/>
|
||||
|
|
|
@ -57,8 +57,9 @@ const ChatMessage = {
|
|||
messageForStatusContent () {
|
||||
return {
|
||||
summary: '',
|
||||
statusnet_html: this.message.content,
|
||||
text: this.message.content,
|
||||
emojis: this.message.emojis,
|
||||
raw_html: this.message.content || '',
|
||||
text: this.message.content || '',
|
||||
attachments: this.message.attachments
|
||||
}
|
||||
},
|
||||
|
|
|
@ -89,8 +89,9 @@
|
|||
}
|
||||
|
||||
.without-attachment {
|
||||
.status-content {
|
||||
&::after {
|
||||
.message-content {
|
||||
// TODO figure out how to do it properly
|
||||
.RichContent::after {
|
||||
margin-right: 5.4em;
|
||||
content: " ";
|
||||
display: inline-block;
|
||||
|
@ -162,6 +163,7 @@
|
|||
.visible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.chat-message-date-separator {
|
||||
|
|
|
@ -71,6 +71,7 @@
|
|||
</Popover>
|
||||
</div>
|
||||
<StatusContent
|
||||
class="message-content"
|
||||
:status="messageForStatusContent"
|
||||
:full-content="true"
|
||||
>
|
||||
|
|
36
src/components/hashtag_link/hashtag_link.js
Normal file
36
src/components/hashtag_link/hashtag_link.js
Normal file
|
@ -0,0 +1,36 @@
|
|||
import { extractTagFromUrl } from 'src/services/matcher/matcher.service.js'
|
||||
|
||||
const HashtagLink = {
|
||||
name: 'HashtagLink',
|
||||
props: {
|
||||
url: {
|
||||
required: true,
|
||||
type: String
|
||||
},
|
||||
content: {
|
||||
required: true,
|
||||
type: String
|
||||
},
|
||||
tag: {
|
||||
required: false,
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onClick () {
|
||||
const tag = this.tag || extractTagFromUrl(this.url)
|
||||
if (tag) {
|
||||
const link = this.generateTagLink(tag)
|
||||
this.$router.push(link)
|
||||
} else {
|
||||
window.open(this.url, '_blank')
|
||||
}
|
||||
},
|
||||
generateTagLink (tag) {
|
||||
return `/tag/${tag}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default HashtagLink
|
6
src/components/hashtag_link/hashtag_link.scss
Normal file
6
src/components/hashtag_link/hashtag_link.scss
Normal file
|
@ -0,0 +1,6 @@
|
|||
.HashtagLink {
|
||||
position: relative;
|
||||
white-space: normal;
|
||||
display: inline-block;
|
||||
color: var(--link);
|
||||
}
|
19
src/components/hashtag_link/hashtag_link.vue
Normal file
19
src/components/hashtag_link/hashtag_link.vue
Normal file
|
@ -0,0 +1,19 @@
|
|||
<template>
|
||||
<span
|
||||
class="HashtagLink"
|
||||
>
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<a
|
||||
:href="url"
|
||||
class="original"
|
||||
target="_blank"
|
||||
@click.prevent="onClick"
|
||||
v-html="content"
|
||||
/>
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script src="./hashtag_link.js"/>
|
||||
|
||||
<style lang="scss" src="./hashtag_link.scss"/>
|
95
src/components/mention_link/mention_link.js
Normal file
95
src/components/mention_link/mention_link.js
Normal file
|
@ -0,0 +1,95 @@
|
|||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||
import { mapGetters, mapState } from 'vuex'
|
||||
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faAt
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
faAt
|
||||
)
|
||||
|
||||
const MentionLink = {
|
||||
name: 'MentionLink',
|
||||
props: {
|
||||
url: {
|
||||
required: true,
|
||||
type: String
|
||||
},
|
||||
content: {
|
||||
required: true,
|
||||
type: String
|
||||
},
|
||||
userId: {
|
||||
required: false,
|
||||
type: String
|
||||
},
|
||||
userScreenName: {
|
||||
required: false,
|
||||
type: String
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onClick () {
|
||||
const link = generateProfileLink(
|
||||
this.userId || this.user.id,
|
||||
this.userScreenName || this.user.screen_name
|
||||
)
|
||||
this.$router.push(link)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
user () {
|
||||
return this.url && this.$store && this.$store.getters.findUserByUrl(this.url)
|
||||
},
|
||||
isYou () {
|
||||
// FIXME why user !== currentUser???
|
||||
return this.user && this.user.id === this.currentUser.id
|
||||
},
|
||||
userName () {
|
||||
return this.user && this.userNameFullUi.split('@')[0]
|
||||
},
|
||||
userNameFull () {
|
||||
return this.user && this.user.screen_name
|
||||
},
|
||||
userNameFullUi () {
|
||||
return this.user && this.user.screen_name_ui
|
||||
},
|
||||
highlight () {
|
||||
return this.user && this.mergedConfig.highlight[this.user.screen_name]
|
||||
},
|
||||
highlightType () {
|
||||
return this.highlight && ('-' + this.highlight.type)
|
||||
},
|
||||
highlightClass () {
|
||||
if (this.highlight) return highlightClass(this.user)
|
||||
},
|
||||
style () {
|
||||
if (this.highlight) {
|
||||
const {
|
||||
backgroundColor,
|
||||
backgroundPosition,
|
||||
backgroundImage,
|
||||
...rest
|
||||
} = highlightStyle(this.highlight)
|
||||
return rest
|
||||
}
|
||||
},
|
||||
classnames () {
|
||||
return [
|
||||
{
|
||||
'-you': this.isYou,
|
||||
'-highlighted': this.highlight
|
||||
},
|
||||
this.highlightType
|
||||
]
|
||||
},
|
||||
...mapGetters(['mergedConfig']),
|
||||
...mapState({
|
||||
currentUser: state => state.users.currentUser
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default MentionLink
|
91
src/components/mention_link/mention_link.scss
Normal file
91
src/components/mention_link/mention_link.scss
Normal file
|
@ -0,0 +1,91 @@
|
|||
.MentionLink {
|
||||
position: relative;
|
||||
white-space: normal;
|
||||
display: inline-block;
|
||||
color: var(--link);
|
||||
|
||||
& .new,
|
||||
& .original {
|
||||
display: inline-block;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.full {
|
||||
position: absolute;
|
||||
display: inline-block;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
word-wrap: normal;
|
||||
white-space: nowrap;
|
||||
transition: opacity 0.2s ease;
|
||||
z-index: 1;
|
||||
margin-top: 0.25em;
|
||||
padding: 0.5em;
|
||||
user-select: all;
|
||||
}
|
||||
|
||||
.short {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
& .short,
|
||||
& .full {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.new {
|
||||
&.-you {
|
||||
& .shortName,
|
||||
& .full {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.at {
|
||||
color: var(--link);
|
||||
opacity: 0.8;
|
||||
display: inline-block;
|
||||
height: 50%;
|
||||
line-height: 1;
|
||||
padding: 0 0.1em;
|
||||
vertical-align: -25%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&.-striped {
|
||||
& .userName,
|
||||
& .full {
|
||||
background-image:
|
||||
repeating-linear-gradient(
|
||||
135deg,
|
||||
var(--____highlight-tintColor),
|
||||
var(--____highlight-tintColor) 5px,
|
||||
var(--____highlight-tintColor2) 5px,
|
||||
var(--____highlight-tintColor2) 10px
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
&.-solid {
|
||||
& .userName,
|
||||
& .full {
|
||||
background-image: linear-gradient(var(--____highlight-tintColor2), var(--____highlight-tintColor2));
|
||||
}
|
||||
}
|
||||
|
||||
&.-side {
|
||||
& .userName,
|
||||
& .userNameFull {
|
||||
box-shadow: 0 -5px 3px -4px inset var(--____highlight-solidColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .new .full {
|
||||
opacity: 1;
|
||||
pointer-events: initial;
|
||||
}
|
||||
}
|
56
src/components/mention_link/mention_link.vue
Normal file
56
src/components/mention_link/mention_link.vue
Normal file
|
@ -0,0 +1,56 @@
|
|||
<template>
|
||||
<span
|
||||
class="MentionLink"
|
||||
>
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<a
|
||||
v-if="!user"
|
||||
:href="url"
|
||||
class="original"
|
||||
target="_blank"
|
||||
v-html="content"
|
||||
/>
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
<span
|
||||
v-if="user"
|
||||
class="new"
|
||||
:style="style"
|
||||
:class="classnames"
|
||||
>
|
||||
<a
|
||||
class="short button-unstyled"
|
||||
:href="url"
|
||||
@click.prevent="onClick"
|
||||
>
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<FAIcon
|
||||
size="sm"
|
||||
icon="at"
|
||||
class="at"
|
||||
/><span class="shortName"><span
|
||||
class="userName"
|
||||
v-html="userName"
|
||||
/></span>
|
||||
<span
|
||||
v-if="isYou"
|
||||
class="you"
|
||||
>{{ $t('status.you') }}</span>
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
</a>
|
||||
<span
|
||||
v-if="userName !== userNameFull"
|
||||
class="full popover-default"
|
||||
:class="[highlightType]"
|
||||
>
|
||||
<span
|
||||
class="userNameFull"
|
||||
v-text="'@' + userNameFull"
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script src="./mention_link.js"/>
|
||||
|
||||
<style lang="scss" src="./mention_link.scss"/>
|
37
src/components/mentions_line/mentions_line.js
Normal file
37
src/components/mentions_line/mentions_line.js
Normal file
|
@ -0,0 +1,37 @@
|
|||
import MentionLink from 'src/components/mention_link/mention_link.vue'
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
export const MENTIONS_LIMIT = 5
|
||||
|
||||
const MentionsLine = {
|
||||
name: 'MentionsLine',
|
||||
props: {
|
||||
mentions: {
|
||||
required: true,
|
||||
type: Array
|
||||
}
|
||||
},
|
||||
data: () => ({ expanded: false }),
|
||||
components: {
|
||||
MentionLink
|
||||
},
|
||||
computed: {
|
||||
mentionsComputed () {
|
||||
return this.mentions.slice(0, MENTIONS_LIMIT)
|
||||
},
|
||||
extraMentions () {
|
||||
return this.mentions.slice(MENTIONS_LIMIT)
|
||||
},
|
||||
manyMentions () {
|
||||
return this.extraMentions.length > 0
|
||||
},
|
||||
...mapGetters(['mergedConfig'])
|
||||
},
|
||||
methods: {
|
||||
toggleShowMore () {
|
||||
this.expanded = !this.expanded
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default MentionsLine
|
11
src/components/mentions_line/mentions_line.scss
Normal file
11
src/components/mentions_line/mentions_line.scss
Normal file
|
@ -0,0 +1,11 @@
|
|||
.MentionsLine {
|
||||
.showMoreLess {
|
||||
white-space: normal;
|
||||
color: var(--link);
|
||||
}
|
||||
|
||||
.fullExtraMentions,
|
||||
.mention-link:not(:last-child) {
|
||||
margin-right: 0.25em;
|
||||
}
|
||||
}
|
43
src/components/mentions_line/mentions_line.vue
Normal file
43
src/components/mentions_line/mentions_line.vue
Normal file
|
@ -0,0 +1,43 @@
|
|||
<template>
|
||||
<span class="MentionsLine">
|
||||
<MentionLink
|
||||
v-for="mention in mentionsComputed"
|
||||
:key="mention.index"
|
||||
class="mention-link"
|
||||
:content="mention.content"
|
||||
:url="mention.url"
|
||||
:first-mention="false"
|
||||
/><span
|
||||
v-if="manyMentions"
|
||||
class="extraMentions"
|
||||
>
|
||||
<span
|
||||
v-if="expanded"
|
||||
class="fullExtraMentions"
|
||||
>
|
||||
<MentionLink
|
||||
v-for="mention in extraMentions"
|
||||
:key="mention.index"
|
||||
class="mention-link"
|
||||
:content="mention.content"
|
||||
:url="mention.url"
|
||||
:first-mention="false"
|
||||
/>
|
||||
</span><button
|
||||
v-if="!expanded"
|
||||
class="button-unstyled showMoreLess"
|
||||
@click="toggleShowMore"
|
||||
>
|
||||
{{ $t('status.plus_more', { number: extraMentions.length }) }}
|
||||
</button><button
|
||||
v-if="expanded"
|
||||
class="button-unstyled showMoreLess"
|
||||
@click="toggleShowMore"
|
||||
>
|
||||
{{ $t('general.show_less') }}
|
||||
</button>
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
<script src="./mentions_line.js" ></script>
|
||||
<style lang="scss" src="./mentions_line.scss" />
|
|
@ -44,6 +44,9 @@ const MobilePostStatusButton = {
|
|||
|
||||
return this.autohideFloatingPostButton && (this.hidden || this.inputActive)
|
||||
},
|
||||
isPersistent () {
|
||||
return !!this.$store.getters.mergedConfig.showNewPostButton
|
||||
},
|
||||
autohideFloatingPostButton () {
|
||||
return !!this.$store.getters.mergedConfig.autohideFloatingPostButton
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<div v-if="isLoggedIn">
|
||||
<button
|
||||
class="button-default new-status-button"
|
||||
:class="{ 'hidden': isHidden }"
|
||||
:class="{ 'hidden': isHidden, 'always-show': isPersistent }"
|
||||
@click="openPostForm"
|
||||
>
|
||||
<FAIcon icon="pen" />
|
||||
|
@ -47,7 +47,7 @@
|
|||
}
|
||||
|
||||
@media all and (min-width: 801px) {
|
||||
.new-status-button {
|
||||
.new-status-button:not(.always-show) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import Status from '../status/status.vue'
|
|||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
import UserCard from '../user_card/user_card.vue'
|
||||
import Timeago from '../timeago/timeago.vue'
|
||||
import RichContent from 'src/components/rich_content/rich_content.jsx'
|
||||
import { isStatusNotification } from '../../services/notification_utils/notification_utils.js'
|
||||
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
|
||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||
|
@ -44,7 +45,8 @@ const Notification = {
|
|||
UserAvatar,
|
||||
UserCard,
|
||||
Timeago,
|
||||
Status
|
||||
Status,
|
||||
RichContent
|
||||
},
|
||||
methods: {
|
||||
toggleUserExpanded () {
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
// TODO Copypaste from Status, should unify it somehow
|
||||
.Notification {
|
||||
--emoji-size: 14px;
|
||||
|
||||
&.-muted {
|
||||
padding: 0.25em 0.6em;
|
||||
height: 1.2em;
|
||||
|
|
|
@ -51,12 +51,14 @@
|
|||
<span class="notification-details">
|
||||
<div class="name-and-action">
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<bdi
|
||||
v-if="!!notification.from_profile.name_html"
|
||||
<bdi v-if="!!notification.from_profile.name_html">
|
||||
<RichContent
|
||||
class="username"
|
||||
:title="'@'+notification.from_profile.screen_name_ui"
|
||||
v-html="notification.from_profile.name_html"
|
||||
:html="notification.from_profile.name_html"
|
||||
:emoji="notification.from_profile.emoji"
|
||||
/>
|
||||
</bdi>
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
<span
|
||||
v-else
|
||||
|
|
|
@ -148,13 +148,6 @@
|
|||
max-width: 100%;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
img {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
vertical-align: middle;
|
||||
object-fit: contain
|
||||
}
|
||||
}
|
||||
|
||||
.timeago {
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
import Timeago from '../timeago/timeago.vue'
|
||||
import Timeago from 'components/timeago/timeago.vue'
|
||||
import RichContent from 'components/rich_content/rich_content.jsx'
|
||||
import { forEach, map } from 'lodash'
|
||||
|
||||
export default {
|
||||
name: 'Poll',
|
||||
props: ['basePoll'],
|
||||
components: { Timeago },
|
||||
props: ['basePoll', 'emoji'],
|
||||
components: {
|
||||
Timeago,
|
||||
RichContent
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
loading: false,
|
||||
|
|
|
@ -17,8 +17,11 @@
|
|||
<span class="result-percentage">
|
||||
{{ percentageForOption(option.votes_count) }}%
|
||||
</span>
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<span v-html="option.title_html" />
|
||||
<RichContent
|
||||
:html="option.title_html"
|
||||
:handle-links="false"
|
||||
:emoji="emoji"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="result-fill"
|
||||
|
@ -42,8 +45,11 @@
|
|||
:value="index"
|
||||
>
|
||||
<label class="option-vote">
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<div v-html="option.title_html" />
|
||||
<RichContent
|
||||
:html="option.title_html"
|
||||
:handle-links="false"
|
||||
:emoji="emoji"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
|
327
src/components/rich_content/rich_content.jsx
Normal file
327
src/components/rich_content/rich_content.jsx
Normal file
|
@ -0,0 +1,327 @@
|
|||
import Vue from 'vue'
|
||||
import { unescape, flattenDeep } from 'lodash'
|
||||
import { getTagName, processTextForEmoji, getAttrs } from 'src/services/html_converter/utility.service.js'
|
||||
import { convertHtmlToTree } from 'src/services/html_converter/html_tree_converter.service.js'
|
||||
import { convertHtmlToLines } from 'src/services/html_converter/html_line_converter.service.js'
|
||||
import StillImage from 'src/components/still-image/still-image.vue'
|
||||
import MentionsLine, { MENTIONS_LIMIT } from 'src/components/mentions_line/mentions_line.vue'
|
||||
import HashtagLink from 'src/components/hashtag_link/hashtag_link.vue'
|
||||
|
||||
import './rich_content.scss'
|
||||
|
||||
/**
|
||||
* RichContent, The Über-powered component for rendering Post HTML.
|
||||
*
|
||||
* This takes post HTML and does multiple things to it:
|
||||
* - Groups all mentions into <MentionsLine>, this affects all mentions regardles
|
||||
* of where they are (beginning/middle/end), even single mentions are converted
|
||||
* to a <MentionsLine> containing single <MentionLink>.
|
||||
* - Replaces emoji shortcodes with <StillImage>'d images.
|
||||
*
|
||||
* There are two problems with this component's architecture:
|
||||
* 1. Parsing HTML and rendering are inseparable. Attempts to separate the two
|
||||
* proven to be a massive overcomplication due to amount of things done here.
|
||||
* 2. We need to output both render and some extra data, which seems to be imp-
|
||||
* possible in vue. Current solution is to emit 'parseReady' event when parsing
|
||||
* is done within render() function.
|
||||
*
|
||||
* Apart from that one small hiccup with emit in render this _should_ be vue3-ready
|
||||
*/
|
||||
export default Vue.component('RichContent', {
|
||||
name: 'RichContent',
|
||||
props: {
|
||||
// Original html content
|
||||
html: {
|
||||
required: true,
|
||||
type: String
|
||||
},
|
||||
attentions: {
|
||||
required: false,
|
||||
default: () => []
|
||||
},
|
||||
// Emoji object, as in status.emojis, note the "s" at the end...
|
||||
emoji: {
|
||||
required: true,
|
||||
type: Array
|
||||
},
|
||||
// Whether to handle links or not (posts: yes, everything else: no)
|
||||
handleLinks: {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
// Meme arrows
|
||||
greentext: {
|
||||
required: false,
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
// NEVER EVER TOUCH DATA INSIDE RENDER
|
||||
render (h) {
|
||||
// Pre-process HTML
|
||||
const { newHtml: html } = preProcessPerLine(this.html, this.greentext)
|
||||
let currentMentions = null // Current chain of mentions, we group all mentions together
|
||||
// This is used to recover spacing removed when parsing mentions
|
||||
let lastSpacing = ''
|
||||
|
||||
const lastTags = [] // Tags that appear at the end of post body
|
||||
const writtenMentions = [] // All mentions that appear in post body
|
||||
const invisibleMentions = [] // All mentions that go beyond the limiter (see MentionsLine)
|
||||
// to collapse too many mentions in a row
|
||||
const writtenTags = [] // All tags that appear in post body
|
||||
// unique index for vue "tag" property
|
||||
let mentionIndex = 0
|
||||
let tagsIndex = 0
|
||||
|
||||
const renderImage = (tag) => {
|
||||
return <StillImage
|
||||
{...{ attrs: getAttrs(tag) }}
|
||||
class="img"
|
||||
/>
|
||||
}
|
||||
|
||||
const renderHashtag = (attrs, children, encounteredTextReverse) => {
|
||||
const linkData = getLinkData(attrs, children, tagsIndex++)
|
||||
writtenTags.push(linkData)
|
||||
if (!encounteredTextReverse) {
|
||||
lastTags.push(linkData)
|
||||
}
|
||||
return <HashtagLink {...{ props: linkData }}/>
|
||||
}
|
||||
|
||||
const renderMention = (attrs, children) => {
|
||||
const linkData = getLinkData(attrs, children, mentionIndex++)
|
||||
linkData.notifying = this.attentions.some(a => a.statusnet_profile_url === linkData.url)
|
||||
writtenMentions.push(linkData)
|
||||
if (currentMentions === null) {
|
||||
currentMentions = []
|
||||
}
|
||||
currentMentions.push(linkData)
|
||||
if (currentMentions.length > MENTIONS_LIMIT) {
|
||||
invisibleMentions.push(linkData)
|
||||
}
|
||||
if (currentMentions.length === 1) {
|
||||
return <MentionsLine mentions={ currentMentions } />
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
// Processor to use with html_tree_converter
|
||||
const processItem = (item, index, array, what) => {
|
||||
// Handle text nodes - just add emoji
|
||||
if (typeof item === 'string') {
|
||||
const emptyText = item.trim() === ''
|
||||
if (item.includes('\n')) {
|
||||
currentMentions = null
|
||||
}
|
||||
if (emptyText) {
|
||||
// don't include spaces when processing mentions - we'll include them
|
||||
// in MentionsLine
|
||||
lastSpacing = item
|
||||
return currentMentions !== null ? item.trim() : item
|
||||
}
|
||||
|
||||
currentMentions = null
|
||||
if (item.includes(':')) {
|
||||
item = ['', processTextForEmoji(
|
||||
item,
|
||||
this.emoji,
|
||||
({ shortcode, url }) => {
|
||||
return <StillImage
|
||||
class="emoji img"
|
||||
src={url}
|
||||
title={`:${shortcode}:`}
|
||||
alt={`:${shortcode}:`}
|
||||
/>
|
||||
}
|
||||
)]
|
||||
}
|
||||
return item
|
||||
}
|
||||
|
||||
// Handle tag nodes
|
||||
if (Array.isArray(item)) {
|
||||
const [opener, children, closer] = item
|
||||
const Tag = getTagName(opener)
|
||||
const attrs = getAttrs(opener)
|
||||
const previouslyMentions = currentMentions !== null
|
||||
/* During grouping of mentions we trim all the empty text elements
|
||||
* This padding is added to recover last space removed in case
|
||||
* we have a tag right next to mentions
|
||||
*/
|
||||
const mentionsLinePadding =
|
||||
// Padding is only needed if we just finished parsing mentions
|
||||
previouslyMentions &&
|
||||
// Don't add padding if content is string and has padding already
|
||||
!(children && typeof children[0] === 'string' && children[0].match(/^\s/))
|
||||
? lastSpacing
|
||||
: ''
|
||||
switch (Tag) {
|
||||
case 'br':
|
||||
currentMentions = null
|
||||
break
|
||||
case 'img': // replace images with StillImage
|
||||
return ['', [mentionsLinePadding, renderImage(opener)], '']
|
||||
case 'a': // replace mentions with MentionLink
|
||||
if (!this.handleLinks) break
|
||||
if (attrs['class'] && attrs['class'].includes('mention')) {
|
||||
// Handling mentions here
|
||||
return renderMention(attrs, children)
|
||||
} else {
|
||||
currentMentions = null
|
||||
break
|
||||
}
|
||||
case 'span':
|
||||
if (this.handleLinks && attrs['class'] && attrs['class'].includes('h-card')) {
|
||||
return ['', children.map(processItem), '']
|
||||
}
|
||||
}
|
||||
|
||||
if (children !== undefined) {
|
||||
return [
|
||||
'',
|
||||
[
|
||||
mentionsLinePadding,
|
||||
[opener, children.map(processItem), closer]
|
||||
],
|
||||
''
|
||||
]
|
||||
} else {
|
||||
return ['', [mentionsLinePadding, item], '']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Processor for back direction (for finding "last" stuff, just easier this way)
|
||||
let encounteredTextReverse = false
|
||||
const processItemReverse = (item, index, array, what) => {
|
||||
// Handle text nodes - just add emoji
|
||||
if (typeof item === 'string') {
|
||||
const emptyText = item.trim() === ''
|
||||
if (emptyText) return item
|
||||
if (!encounteredTextReverse) encounteredTextReverse = true
|
||||
return unescape(item)
|
||||
} else if (Array.isArray(item)) {
|
||||
// Handle tag nodes
|
||||
const [opener, children] = item
|
||||
const Tag = opener === '' ? '' : getTagName(opener)
|
||||
switch (Tag) {
|
||||
case 'a': // replace mentions with MentionLink
|
||||
if (!this.handleLinks) break
|
||||
const attrs = getAttrs(opener)
|
||||
// should only be this
|
||||
if (
|
||||
(attrs['class'] && attrs['class'].includes('hashtag')) || // Pleroma style
|
||||
(attrs['rel'] === 'tag') // Mastodon style
|
||||
) {
|
||||
return renderHashtag(attrs, children, encounteredTextReverse)
|
||||
} else {
|
||||
attrs.target = '_blank'
|
||||
const newChildren = [...children].reverse().map(processItemReverse).reverse()
|
||||
|
||||
return <a {...{ attrs }}>
|
||||
{ newChildren }
|
||||
</a>
|
||||
}
|
||||
case '':
|
||||
return [...children].reverse().map(processItemReverse).reverse()
|
||||
}
|
||||
|
||||
// Render tag as is
|
||||
if (children !== undefined) {
|
||||
const newChildren = Array.isArray(children)
|
||||
? [...children].reverse().map(processItemReverse).reverse()
|
||||
: children
|
||||
return <Tag {...{ attrs: getAttrs(opener) }}>
|
||||
{ newChildren }
|
||||
</Tag>
|
||||
} else {
|
||||
return <Tag/>
|
||||
}
|
||||
}
|
||||
return item
|
||||
}
|
||||
|
||||
const pass1 = convertHtmlToTree(html).map(processItem)
|
||||
const pass2 = [...pass1].reverse().map(processItemReverse).reverse()
|
||||
// DO NOT USE SLOTS they cause a re-render feedback loop here.
|
||||
// slots updated -> rerender -> emit -> update up the tree -> rerender -> ...
|
||||
// at least until vue3?
|
||||
const result = <span class="RichContent">
|
||||
{ pass2 }
|
||||
</span>
|
||||
|
||||
const event = {
|
||||
lastTags,
|
||||
writtenMentions,
|
||||
writtenTags,
|
||||
invisibleMentions
|
||||
}
|
||||
|
||||
// DO NOT MOVE TO UPDATE. BAD IDEA.
|
||||
this.$emit('parseReady', event)
|
||||
|
||||
return result
|
||||
}
|
||||
})
|
||||
|
||||
const getLinkData = (attrs, children, index) => {
|
||||
const stripTags = (item) => {
|
||||
if (typeof item === 'string') {
|
||||
return item
|
||||
} else {
|
||||
return item[1].map(stripTags).join('')
|
||||
}
|
||||
}
|
||||
const textContent = children.map(stripTags).join('')
|
||||
return {
|
||||
index,
|
||||
url: attrs.href,
|
||||
tag: attrs['data-tag'],
|
||||
content: flattenDeep(children).join(''),
|
||||
textContent
|
||||
}
|
||||
}
|
||||
|
||||
/** Pre-processing HTML
|
||||
*
|
||||
* Currently this does one thing:
|
||||
* - add green/cyantexting
|
||||
*
|
||||
* @param {String} html - raw HTML to process
|
||||
* @param {Boolean} greentext - whether to enable greentexting or not
|
||||
*/
|
||||
export const preProcessPerLine = (html, greentext) => {
|
||||
const greentextHandle = new Set(['p', 'div'])
|
||||
|
||||
const lines = convertHtmlToLines(html)
|
||||
const newHtml = lines.reverse().map((item, index, array) => {
|
||||
if (!item.text) return item
|
||||
const string = item.text
|
||||
|
||||
// Greentext stuff
|
||||
if (
|
||||
// Only if greentext is engaged
|
||||
greentext &&
|
||||
// Only handle p's and divs. Don't want to affect blockquotes, code etc
|
||||
item.level.every(l => greentextHandle.has(l)) &&
|
||||
// Only if line begins with '>' or '<'
|
||||
(string.includes('>') || string.includes('<'))
|
||||
) {
|
||||
const cleanedString = string.replace(/<[^>]+?>/gi, '') // remove all tags
|
||||
.replace(/@\w+/gi, '') // remove mentions (even failed ones)
|
||||
.trim()
|
||||
if (cleanedString.startsWith('>')) {
|
||||
return `<span class='greentext'>${string}</span>`
|
||||
} else if (cleanedString.startsWith('<')) {
|
||||
return `<span class='cyantext'>${string}</span>`
|
||||
}
|
||||
}
|
||||
|
||||
return string
|
||||
}).reverse().join('')
|
||||
|
||||
return { newHtml }
|
||||
}
|
64
src/components/rich_content/rich_content.scss
Normal file
64
src/components/rich_content/rich_content.scss
Normal file
|
@ -0,0 +1,64 @@
|
|||
.RichContent {
|
||||
blockquote {
|
||||
margin: 0.2em 0 0.2em 2em;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
pre {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
code,
|
||||
samp,
|
||||
kbd,
|
||||
var,
|
||||
pre {
|
||||
font-family: var(--postCodeFont, monospace);
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0 0 1em 0;
|
||||
}
|
||||
|
||||
p:last-child {
|
||||
margin: 0 0 0 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.1em;
|
||||
line-height: 1.2em;
|
||||
margin: 1.4em 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.1em;
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1em;
|
||||
margin: 1.2em 0;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 1.1em 0;
|
||||
}
|
||||
|
||||
.img {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.emoji {
|
||||
display: inline-block;
|
||||
width: var(--emoji-size, 32px);
|
||||
height: var(--emoji-size, 32px);
|
||||
}
|
||||
|
||||
.img,
|
||||
video {
|
||||
max-width: 100%;
|
||||
max-height: 400px;
|
||||
vertical-align: middle;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
|
@ -122,6 +122,11 @@
|
|||
{{ $t('settings.sensitive_by_default') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="alwaysShowNewPostButton">
|
||||
{{ $t('settings.always_show_post_button') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="autohideFloatingPostButton">
|
||||
{{ $t('settings.autohide_floating_post_button') }}
|
||||
|
|
|
@ -475,7 +475,7 @@ export default {
|
|||
this.loadThemeFromLocalStorage(false, true)
|
||||
break
|
||||
case 'file':
|
||||
console.err('Forcing snapshout from file is not supported yet')
|
||||
console.error('Forcing snapshot from file is not supported yet')
|
||||
break
|
||||
}
|
||||
this.dismissWarning()
|
||||
|
|
|
@ -79,12 +79,19 @@
|
|||
|
||||
.floating-shout {
|
||||
position: fixed;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
z-index: 1000;
|
||||
max-width: 25em;
|
||||
}
|
||||
|
||||
.floating-shout.left {
|
||||
left: 0px;
|
||||
}
|
||||
|
||||
.floating-shout:not(.left) {
|
||||
right: 0px;
|
||||
}
|
||||
|
||||
.shout-panel {
|
||||
.shout-heading {
|
||||
cursor: pointer;
|
||||
|
|
|
@ -49,6 +49,7 @@ const SideDrawer = {
|
|||
currentUser () {
|
||||
return this.$store.state.users.currentUser
|
||||
},
|
||||
shout () { return this.$store.state.shout.channel.state === 'joined' },
|
||||
unseenNotifications () {
|
||||
return unseenNotificationsFromStore(this.$store)
|
||||
},
|
||||
|
|
|
@ -106,10 +106,10 @@
|
|||
</router-link>
|
||||
</li>
|
||||
<li
|
||||
v-if="chat"
|
||||
v-if="shout"
|
||||
@click="toggleDrawer"
|
||||
>
|
||||
<router-link :to="{ name: 'chat-panel' }">
|
||||
<router-link :to="{ name: 'shout-panel' }">
|
||||
<FAIcon
|
||||
fixed-width
|
||||
class="fa-scale-110 fa-old-padding"
|
||||
|
|
|
@ -9,9 +9,12 @@ import UserAvatar from '../user_avatar/user_avatar.vue'
|
|||
import AvatarList from '../avatar_list/avatar_list.vue'
|
||||
import Timeago from '../timeago/timeago.vue'
|
||||
import StatusContent from '../status_content/status_content.vue'
|
||||
import RichContent from 'src/components/rich_content/rich_content.jsx'
|
||||
import StatusPopover from '../status_popover/status_popover.vue'
|
||||
import UserListPopover from '../user_list_popover/user_list_popover.vue'
|
||||
import EmojiReactions from '../emoji_reactions/emoji_reactions.vue'
|
||||
import MentionsLine from 'src/components/mentions_line/mentions_line.vue'
|
||||
import MentionLink from 'src/components/mention_link/mention_link.vue'
|
||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
|
||||
import { muteWordHits } from '../../services/status_parser/status_parser.js'
|
||||
|
@ -68,7 +71,10 @@ const Status = {
|
|||
StatusPopover,
|
||||
UserListPopover,
|
||||
EmojiReactions,
|
||||
StatusContent
|
||||
StatusContent,
|
||||
RichContent,
|
||||
MentionLink,
|
||||
MentionsLine
|
||||
},
|
||||
props: [
|
||||
'statusoid',
|
||||
|
@ -92,7 +98,8 @@ const Status = {
|
|||
userExpanded: false,
|
||||
mediaPlaying: [],
|
||||
suspendable: true,
|
||||
error: null
|
||||
error: null,
|
||||
headTailLinks: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -132,12 +139,15 @@ const Status = {
|
|||
},
|
||||
replyProfileLink () {
|
||||
if (this.isReply) {
|
||||
return this.generateUserProfileLink(this.status.in_reply_to_user_id, this.replyToName)
|
||||
const user = this.$store.getters.findUser(this.status.in_reply_to_user_id)
|
||||
// FIXME Why user not found sometimes???
|
||||
return user ? user.statusnet_profile_url : 'NOT_FOUND'
|
||||
}
|
||||
},
|
||||
retweet () { return !!this.statusoid.retweeted_status },
|
||||
retweeterUser () { return this.statusoid.user },
|
||||
retweeter () { return this.statusoid.user.name || this.statusoid.user.screen_name_ui },
|
||||
retweeterHtml () { return this.statusoid.user.name_html },
|
||||
retweeterHtml () { return this.statusoid.user.name },
|
||||
retweeterProfileLink () { return this.generateUserProfileLink(this.statusoid.user.id, this.statusoid.user.screen_name) },
|
||||
status () {
|
||||
if (this.retweet) {
|
||||
|
@ -156,6 +166,25 @@ const Status = {
|
|||
muteWordHits () {
|
||||
return muteWordHits(this.status, this.muteWords)
|
||||
},
|
||||
mentionsLine () {
|
||||
if (!this.headTailLinks) return []
|
||||
const writtenSet = new Set(this.headTailLinks.writtenMentions.map(_ => _.url))
|
||||
return this.status.attentions.filter(attn => {
|
||||
// no reply user
|
||||
return attn.id !== this.status.in_reply_to_user_id &&
|
||||
// no self-replies
|
||||
attn.statusnet_profile_url !== this.status.user.statusnet_profile_url &&
|
||||
// don't include if mentions is written
|
||||
!writtenSet.has(attn.statusnet_profile_url)
|
||||
}).map(attn => ({
|
||||
url: attn.statusnet_profile_url,
|
||||
content: attn.screen_name,
|
||||
userId: attn.id
|
||||
}))
|
||||
},
|
||||
hasMentionsLine () {
|
||||
return this.mentionsLine.length > 0
|
||||
},
|
||||
muted () {
|
||||
if (this.statusoid.user.id === this.currentUser.id) return false
|
||||
const { status } = this
|
||||
|
@ -303,6 +332,9 @@ const Status = {
|
|||
},
|
||||
removeMediaPlaying (id) {
|
||||
this.mediaPlaying = this.mediaPlaying.filter(mediaId => mediaId !== id)
|
||||
},
|
||||
setHeadTailLinks (headTailLinks) {
|
||||
this.headTailLinks = headTailLinks
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
|
||||
@import '../../_variables.scss';
|
||||
|
||||
$status-margin: 0.75em;
|
||||
|
||||
.Status {
|
||||
min-width: 0;
|
||||
white-space: normal;
|
||||
|
||||
&:hover {
|
||||
--_still-image-img-visibility: visible;
|
||||
|
@ -93,12 +93,8 @@ $status-margin: 0.75em;
|
|||
margin-right: 0.4em;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
.emoji {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
vertical-align: middle;
|
||||
object-fit: contain;
|
||||
}
|
||||
--_still_image-label-scale: 0.25;
|
||||
--emoji-size: 14px;
|
||||
}
|
||||
|
||||
.status-favicon {
|
||||
|
@ -155,35 +151,24 @@ $status-margin: 0.75em;
|
|||
}
|
||||
}
|
||||
|
||||
.glued-label {
|
||||
display: inline-flex;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.timeago {
|
||||
margin-right: 0.2em;
|
||||
}
|
||||
|
||||
.heading-reply-row {
|
||||
& .heading-reply-row {
|
||||
position: relative;
|
||||
align-content: baseline;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
line-height: 160%;
|
||||
max-width: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.reply-to-and-accountname {
|
||||
display: flex;
|
||||
height: 18px;
|
||||
margin-right: 0.5em;
|
||||
max-width: 100%;
|
||||
|
||||
.reply-to-link {
|
||||
white-space: nowrap;
|
||||
word-break: break-word;
|
||||
text-overflow: ellipsis;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
& .reply-to-popover,
|
||||
& .reply-to-no-popover {
|
||||
min-width: 0;
|
||||
|
@ -220,21 +205,27 @@ $status-margin: 0.75em;
|
|||
}
|
||||
}
|
||||
|
||||
.reply-to {
|
||||
& .mentions,
|
||||
& .reply-to {
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
padding-right: 0.25em;
|
||||
}
|
||||
|
||||
.reply-to-text {
|
||||
& .mentions-text,
|
||||
& .reply-to-text {
|
||||
color: var(--faint);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.replies-separator {
|
||||
margin-left: 0.4em;
|
||||
.mentions-line {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.replies {
|
||||
margin-top: 0.25em;
|
||||
line-height: 18px;
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<template>
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<div
|
||||
v-if="!hideStatus"
|
||||
class="Status"
|
||||
|
@ -89,8 +88,12 @@
|
|||
<router-link
|
||||
v-if="retweeterHtml"
|
||||
:to="retweeterProfileLink"
|
||||
v-html="retweeterHtml"
|
||||
>
|
||||
<RichContent
|
||||
:html="retweeterHtml"
|
||||
:emoji="retweeterUser.emoji"
|
||||
/>
|
||||
</router-link>
|
||||
<router-link
|
||||
v-else
|
||||
:to="retweeterProfileLink"
|
||||
|
@ -145,8 +148,12 @@
|
|||
v-if="status.user.name_html"
|
||||
class="status-username"
|
||||
:title="status.user.name"
|
||||
v-html="status.user.name_html"
|
||||
>
|
||||
<RichContent
|
||||
:html="status.user.name"
|
||||
:emoji="status.user.emoji"
|
||||
/>
|
||||
</h4>
|
||||
<h4
|
||||
v-else
|
||||
class="status-username"
|
||||
|
@ -214,11 +221,13 @@
|
|||
</button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="heading-reply-row">
|
||||
<div
|
||||
v-if="isReply || hasMentionsLine"
|
||||
class="heading-reply-row"
|
||||
>
|
||||
<span
|
||||
v-if="isReply"
|
||||
class="reply-to-and-accountname"
|
||||
class="glued-label"
|
||||
>
|
||||
<StatusPopover
|
||||
v-if="!isPreview"
|
||||
|
@ -238,7 +247,7 @@
|
|||
flip="horizontal"
|
||||
/>
|
||||
<span
|
||||
class="faint-link reply-to-text"
|
||||
class="reply-to-text"
|
||||
>
|
||||
{{ $t('status.reply_to') }}
|
||||
</span>
|
||||
|
@ -251,20 +260,57 @@
|
|||
>
|
||||
<span class="reply-to-text">{{ $t('status.reply_to') }}</span>
|
||||
</span>
|
||||
<router-link
|
||||
class="reply-to-link"
|
||||
:title="replyToName"
|
||||
:to="replyProfileLink"
|
||||
>
|
||||
{{ replyToName }}
|
||||
</router-link>
|
||||
<span
|
||||
v-if="replies && replies.length"
|
||||
class="faint replies-separator"
|
||||
>
|
||||
-
|
||||
<MentionLink
|
||||
:content="replyToName"
|
||||
:url="replyProfileLink"
|
||||
:user-id="status.in_reply_to_user_id"
|
||||
:user-screen-name="status.in_reply_to_screen_name"
|
||||
:first-mention="false"
|
||||
/>
|
||||
</span>
|
||||
|
||||
<!-- This little wrapper is made for sole purpose of "gluing" -->
|
||||
<!-- "Mentions" label to the first mention -->
|
||||
<span
|
||||
v-if="hasMentionsLine"
|
||||
class="glued-label"
|
||||
>
|
||||
<span
|
||||
class="mentions"
|
||||
:aria-label="$t('tool_tip.mentions')"
|
||||
@click.prevent="gotoOriginal(status.in_reply_to_status_id)"
|
||||
>
|
||||
<span
|
||||
class="mentions-text"
|
||||
>
|
||||
{{ $t('status.mentions') }}
|
||||
</span>
|
||||
</span>
|
||||
<MentionsLine
|
||||
v-if="hasMentionsLine"
|
||||
:mentions="mentionsLine.slice(0, 1)"
|
||||
class="mentions-line-first"
|
||||
/>
|
||||
</span>
|
||||
<MentionsLine
|
||||
v-if="hasMentionsLine"
|
||||
:mentions="mentionsLine.slice(1)"
|
||||
class="mentions-line"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<StatusContent
|
||||
ref="content"
|
||||
:status="status"
|
||||
:no-heading="noHeading"
|
||||
:highlight="highlight"
|
||||
:focused="isFocused"
|
||||
@mediaplay="addMediaPlaying($event)"
|
||||
@mediapause="removeMediaPlaying($event)"
|
||||
@parseReady="setHeadTailLinks"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="inConversation && !isPreview && replies && replies.length"
|
||||
class="replies"
|
||||
|
@ -283,17 +329,6 @@
|
|||
</button>
|
||||
</StatusPopover>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<StatusContent
|
||||
:status="status"
|
||||
:no-heading="noHeading"
|
||||
:highlight="highlight"
|
||||
:focused="isFocused"
|
||||
@mediaplay="addMediaPlaying($event)"
|
||||
@mediapause="removeMediaPlaying($event)"
|
||||
/>
|
||||
|
||||
<transition name="fade">
|
||||
<div
|
||||
|
@ -402,7 +437,6 @@
|
|||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
</template>
|
||||
|
||||
<script src="./status.js" ></script>
|
||||
|
|
127
src/components/status_body/status_body.js
Normal file
127
src/components/status_body/status_body.js
Normal file
|
@ -0,0 +1,127 @@
|
|||
import fileType from 'src/services/file_type/file_type.service'
|
||||
import RichContent from 'src/components/rich_content/rich_content.jsx'
|
||||
import { mapGetters } from 'vuex'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faFile,
|
||||
faMusic,
|
||||
faImage,
|
||||
faLink,
|
||||
faPollH
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
faFile,
|
||||
faMusic,
|
||||
faImage,
|
||||
faLink,
|
||||
faPollH
|
||||
)
|
||||
|
||||
const StatusContent = {
|
||||
name: 'StatusContent',
|
||||
props: [
|
||||
'status',
|
||||
'focused',
|
||||
'noHeading',
|
||||
'fullContent',
|
||||
'singleLine'
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
showingTall: this.fullContent || (this.inConversation && this.focused),
|
||||
showingLongSubject: false,
|
||||
// not as computed because it sets the initial state which will be changed later
|
||||
expandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject,
|
||||
postLength: this.status.text.length,
|
||||
parseReadyDone: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
localCollapseSubjectDefault () {
|
||||
return this.mergedConfig.collapseMessageWithSubject
|
||||
},
|
||||
// This is a bit hacky, but we want to approximate post height before rendering
|
||||
// so we count newlines (masto uses <p> for paragraphs, GS uses <br> between them)
|
||||
// as well as approximate line count by counting characters and approximating ~80
|
||||
// per line.
|
||||
//
|
||||
// Using max-height + overflow: auto for status components resulted in false positives
|
||||
// very often with japanese characters, and it was very annoying.
|
||||
tallStatus () {
|
||||
const lengthScore = this.status.raw_html.split(/<p|<br/).length + this.postLength / 80
|
||||
return lengthScore > 20
|
||||
},
|
||||
longSubject () {
|
||||
return this.status.summary.length > 240
|
||||
},
|
||||
// When a status has a subject and is also tall, we should only have one show more/less button. If the default is to collapse statuses with subjects, we just treat it like a status with a subject; otherwise, we just treat it like a tall status.
|
||||
mightHideBecauseSubject () {
|
||||
return !!this.status.summary && this.localCollapseSubjectDefault
|
||||
},
|
||||
mightHideBecauseTall () {
|
||||
return this.tallStatus && !(this.status.summary && this.localCollapseSubjectDefault)
|
||||
},
|
||||
hideSubjectStatus () {
|
||||
return this.mightHideBecauseSubject && !this.expandingSubject
|
||||
},
|
||||
hideTallStatus () {
|
||||
return this.mightHideBecauseTall && !this.showingTall
|
||||
},
|
||||
showingMore () {
|
||||
return (this.mightHideBecauseTall && this.showingTall) || (this.mightHideBecauseSubject && this.expandingSubject)
|
||||
},
|
||||
attachmentTypes () {
|
||||
return this.status.attachments.map(file => fileType.fileType(file.mimetype))
|
||||
},
|
||||
...mapGetters(['mergedConfig'])
|
||||
},
|
||||
components: {
|
||||
RichContent
|
||||
},
|
||||
mounted () {
|
||||
this.status.attentions && this.status.attentions.forEach(attn => {
|
||||
const { id } = attn
|
||||
this.$store.dispatch('fetchUserIfMissing', id)
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
onParseReady (event) {
|
||||
if (this.parseReadyDone) return
|
||||
this.parseReadyDone = true
|
||||
this.$emit('parseReady', event)
|
||||
const { writtenMentions, invisibleMentions } = event
|
||||
writtenMentions
|
||||
.filter(mention => !mention.notifying)
|
||||
.forEach(mention => {
|
||||
const { content, url } = mention
|
||||
const cleanedString = content.replace(/<[^>]+?>/gi, '') // remove all tags
|
||||
if (!cleanedString.startsWith('@')) return
|
||||
const handle = cleanedString.slice(1)
|
||||
const host = url.replace(/^https?:\/\//, '').replace(/\/.+?$/, '')
|
||||
this.$store.dispatch('fetchUserIfMissing', `${handle}@${host}`)
|
||||
})
|
||||
/* This is a bit of a hack to make current tall status detector work
|
||||
* with rich mentions. Invisible mentions are detected at RichContent level
|
||||
* and also we generate plaintext version of mentions by stripping tags
|
||||
* so here we subtract from post length by each mention that became invisible
|
||||
* via MentionsLine
|
||||
*/
|
||||
this.postLength = invisibleMentions.reduce((acc, mention) => {
|
||||
return acc - mention.textContent.length - 1
|
||||
}, this.postLength)
|
||||
},
|
||||
toggleShowMore () {
|
||||
if (this.mightHideBecauseTall) {
|
||||
this.showingTall = !this.showingTall
|
||||
} else if (this.mightHideBecauseSubject) {
|
||||
this.expandingSubject = !this.expandingSubject
|
||||
}
|
||||
},
|
||||
generateTagLink (tag) {
|
||||
return `/tag/${tag}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default StatusContent
|
118
src/components/status_body/status_body.scss
Normal file
118
src/components/status_body/status_body.scss
Normal file
|
@ -0,0 +1,118 @@
|
|||
@import '../../_variables.scss';
|
||||
|
||||
.StatusBody {
|
||||
|
||||
.emoji {
|
||||
--_still_image-label-scale: 0.5;
|
||||
}
|
||||
|
||||
& .text,
|
||||
& .summary {
|
||||
font-family: var(--postFont, sans-serif);
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
line-height: 1.4em;
|
||||
}
|
||||
|
||||
.summary {
|
||||
display: block;
|
||||
font-style: italic;
|
||||
padding-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.text {
|
||||
&.-single-line {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
height: 1.4em;
|
||||
}
|
||||
}
|
||||
|
||||
.summary-wrapper {
|
||||
margin-bottom: 0.5em;
|
||||
border-style: solid;
|
||||
border-width: 0 0 1px 0;
|
||||
border-color: var(--border, $fallback--border);
|
||||
flex-grow: 0;
|
||||
|
||||
&.-tall {
|
||||
position: relative;
|
||||
|
||||
.summary {
|
||||
max-height: 2em;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.text-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
|
||||
&.-tall-status {
|
||||
position: relative;
|
||||
height: 220px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: hidden;
|
||||
z-index: 1;
|
||||
|
||||
.media-body {
|
||||
min-height: 0;
|
||||
mask:
|
||||
linear-gradient(to top, white, transparent) bottom/100% 70px no-repeat,
|
||||
linear-gradient(to top, white, white);
|
||||
|
||||
/* Autoprefixed seem to ignore this one, and also syntax is different */
|
||||
-webkit-mask-composite: xor;
|
||||
mask-composite: exclude;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& .tall-status-hider,
|
||||
& .tall-subject-hider,
|
||||
& .status-unhider,
|
||||
& .cw-status-hider {
|
||||
display: inline-block;
|
||||
word-break: break-all;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tall-status-hider {
|
||||
position: absolute;
|
||||
height: 70px;
|
||||
margin-top: 150px;
|
||||
line-height: 110px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.tall-subject-hider {
|
||||
// position: absolute;
|
||||
padding-bottom: 0.5em;
|
||||
}
|
||||
|
||||
& .status-unhider,
|
||||
& .cw-status-hider {
|
||||
word-break: break-all;
|
||||
|
||||
svg {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.greentext {
|
||||
color: $fallback--cGreen;
|
||||
color: var(--postGreentext, $fallback--cGreen);
|
||||
}
|
||||
|
||||
.cyantext {
|
||||
color: var(--postCyantext, $fallback--cBlue);
|
||||
}
|
||||
}
|
97
src/components/status_body/status_body.vue
Normal file
97
src/components/status_body/status_body.vue
Normal file
|
@ -0,0 +1,97 @@
|
|||
<template>
|
||||
<div class="StatusBody">
|
||||
<div class="body">
|
||||
<div
|
||||
v-if="status.summary_raw_html"
|
||||
class="summary-wrapper"
|
||||
:class="{ '-tall': (longSubject && !showingLongSubject) }"
|
||||
>
|
||||
<RichContent
|
||||
class="media-body summary"
|
||||
:html="status.summary_raw_html"
|
||||
:emoji="status.emojis"
|
||||
/>
|
||||
<button
|
||||
v-if="longSubject && showingLongSubject"
|
||||
class="button-unstyled -link tall-subject-hider"
|
||||
@click.prevent="showingLongSubject=false"
|
||||
>
|
||||
{{ $t("status.hide_full_subject") }}
|
||||
</button>
|
||||
<button
|
||||
v-else-if="longSubject"
|
||||
class="button-unstyled -link tall-subject-hider"
|
||||
@click.prevent="showingLongSubject=true"
|
||||
>
|
||||
{{ $t("status.show_full_subject") }}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
:class="{'-tall-status': hideTallStatus}"
|
||||
class="text-wrapper"
|
||||
>
|
||||
<button
|
||||
v-if="hideTallStatus"
|
||||
class="button-unstyled -link tall-status-hider"
|
||||
:class="{ '-focused': focused }"
|
||||
@click.prevent="toggleShowMore"
|
||||
>
|
||||
{{ $t("general.show_more") }}
|
||||
</button>
|
||||
<RichContent
|
||||
v-if="!hideSubjectStatus && !(singleLine && status.summary_raw_html)"
|
||||
:class="{ '-single-line': singleLine }"
|
||||
class="text media-body"
|
||||
:html="status.raw_html"
|
||||
:emoji="status.emojis"
|
||||
:handle-links="true"
|
||||
:greentext="mergedConfig.greentext"
|
||||
:attentions="status.attentions"
|
||||
@parseReady="onParseReady"
|
||||
/>
|
||||
|
||||
<button
|
||||
v-if="hideSubjectStatus"
|
||||
class="button-unstyled -link cw-status-hider"
|
||||
@click.prevent="toggleShowMore"
|
||||
>
|
||||
{{ $t("status.show_content") }}
|
||||
<FAIcon
|
||||
v-if="attachmentTypes.includes('image')"
|
||||
icon="image"
|
||||
/>
|
||||
<FAIcon
|
||||
v-if="attachmentTypes.includes('video')"
|
||||
icon="video"
|
||||
/>
|
||||
<FAIcon
|
||||
v-if="attachmentTypes.includes('audio')"
|
||||
icon="music"
|
||||
/>
|
||||
<FAIcon
|
||||
v-if="attachmentTypes.includes('unknown')"
|
||||
icon="file"
|
||||
/>
|
||||
<FAIcon
|
||||
v-if="status.poll && status.poll.options"
|
||||
icon="poll-h"
|
||||
/>
|
||||
<FAIcon
|
||||
v-if="status.card"
|
||||
icon="link"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
v-if="showingMore && !fullContent"
|
||||
class="button-unstyled -link status-unhider"
|
||||
@click.prevent="toggleShowMore"
|
||||
>
|
||||
{{ tallStatus ? $t("general.show_less") : $t("status.hide_content") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<slot v-if="!hideSubjectStatus" />
|
||||
</div>
|
||||
</template>
|
||||
<script src="./status_body.js" ></script>
|
||||
<style lang="scss" src="./status_body.scss" />
|
|
@ -1,11 +1,9 @@
|
|||
import Attachment from '../attachment/attachment.vue'
|
||||
import Poll from '../poll/poll.vue'
|
||||
import Gallery from '../gallery/gallery.vue'
|
||||
import StatusBody from 'src/components/status_body/status_body.vue'
|
||||
import LinkPreview from '../link-preview/link-preview.vue'
|
||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||
import fileType from 'src/services/file_type/file_type.service'
|
||||
import { processHtml } from 'src/services/tiny_post_html_processor/tiny_post_html_processor.service.js'
|
||||
import { mentionMatchesUrl, extractTagFromUrl } from 'src/services/matcher/matcher.service.js'
|
||||
import { mapGetters, mapState } from 'vuex'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
|
@ -35,52 +33,11 @@ const StatusContent = {
|
|||
'fullContent',
|
||||
'singleLine'
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
showingTall: this.fullContent || (this.inConversation && this.focused),
|
||||
showingLongSubject: false,
|
||||
// not as computed because it sets the initial state which will be changed later
|
||||
expandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
localCollapseSubjectDefault () {
|
||||
return this.mergedConfig.collapseMessageWithSubject
|
||||
},
|
||||
hideAttachments () {
|
||||
return (this.mergedConfig.hideAttachments && !this.inConversation) ||
|
||||
(this.mergedConfig.hideAttachmentsInConv && this.inConversation)
|
||||
},
|
||||
// This is a bit hacky, but we want to approximate post height before rendering
|
||||
// so we count newlines (masto uses <p> for paragraphs, GS uses <br> between them)
|
||||
// as well as approximate line count by counting characters and approximating ~80
|
||||
// per line.
|
||||
//
|
||||
// Using max-height + overflow: auto for status components resulted in false positives
|
||||
// very often with japanese characters, and it was very annoying.
|
||||
tallStatus () {
|
||||
const lengthScore = this.status.statusnet_html.split(/<p|<br/).length + this.status.text.length / 80
|
||||
return lengthScore > 20
|
||||
},
|
||||
longSubject () {
|
||||
return this.status.summary.length > 240
|
||||
},
|
||||
// When a status has a subject and is also tall, we should only have one show more/less button. If the default is to collapse statuses with subjects, we just treat it like a status with a subject; otherwise, we just treat it like a tall status.
|
||||
mightHideBecauseSubject () {
|
||||
return !!this.status.summary && this.localCollapseSubjectDefault
|
||||
},
|
||||
mightHideBecauseTall () {
|
||||
return this.tallStatus && !(this.status.summary && this.localCollapseSubjectDefault)
|
||||
},
|
||||
hideSubjectStatus () {
|
||||
return this.mightHideBecauseSubject && !this.expandingSubject
|
||||
},
|
||||
hideTallStatus () {
|
||||
return this.mightHideBecauseTall && !this.showingTall
|
||||
},
|
||||
showingMore () {
|
||||
return (this.mightHideBecauseTall && this.showingTall) || (this.mightHideBecauseSubject && this.expandingSubject)
|
||||
},
|
||||
nsfwClickthrough () {
|
||||
if (!this.status.nsfw) {
|
||||
return false
|
||||
|
@ -118,45 +75,11 @@ const StatusContent = {
|
|||
file => !fileType.fileMatchesSomeType(this.galleryTypes, file)
|
||||
)
|
||||
},
|
||||
attachmentTypes () {
|
||||
return this.status.attachments.map(file => fileType.fileType(file.mimetype))
|
||||
},
|
||||
maxThumbnails () {
|
||||
return this.mergedConfig.maxThumbnails
|
||||
},
|
||||
postBodyHtml () {
|
||||
const html = this.status.statusnet_html
|
||||
|
||||
if (this.mergedConfig.greentext) {
|
||||
try {
|
||||
if (html.includes('>')) {
|
||||
// This checks if post has '>' at the beginning, excluding mentions so that @mention >impying works
|
||||
return processHtml(html, (string) => {
|
||||
if (string.includes('>') &&
|
||||
string
|
||||
.replace(/<[^>]+?>/gi, '') // remove all tags
|
||||
.replace(/@\w+/gi, '') // remove mentions (even failed ones)
|
||||
.trim()
|
||||
.startsWith('>')) {
|
||||
return `<span class='greentext'>${string}</span>`
|
||||
} else {
|
||||
return string
|
||||
}
|
||||
})
|
||||
} else {
|
||||
return html
|
||||
}
|
||||
} catch (e) {
|
||||
console.err('Failed to process status html', e)
|
||||
return html
|
||||
}
|
||||
} else {
|
||||
return html
|
||||
}
|
||||
},
|
||||
...mapGetters(['mergedConfig']),
|
||||
...mapState({
|
||||
betterShadow: state => state.interface.browserSupport.cssFilter,
|
||||
currentUser: state => state.users.currentUser
|
||||
})
|
||||
},
|
||||
|
@ -164,48 +87,10 @@ const StatusContent = {
|
|||
Attachment,
|
||||
Poll,
|
||||
Gallery,
|
||||
LinkPreview
|
||||
LinkPreview,
|
||||
StatusBody
|
||||
},
|
||||
methods: {
|
||||
linkClicked (event) {
|
||||
const target = event.target.closest('.status-content a')
|
||||
if (target) {
|
||||
if (target.className.match(/mention/)) {
|
||||
const href = target.href
|
||||
const attn = this.status.attentions.find(attn => mentionMatchesUrl(attn, href))
|
||||
if (attn) {
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
const link = this.generateUserProfileLink(attn.id, attn.screen_name)
|
||||
this.$router.push(link)
|
||||
return
|
||||
}
|
||||
}
|
||||
if (target.rel.match(/(?:^|\s)tag(?:$|\s)/) || target.className.match(/hashtag/)) {
|
||||
// Extract tag name from dataset or link url
|
||||
const tag = target.dataset.tag || extractTagFromUrl(target.href)
|
||||
if (tag) {
|
||||
const link = this.generateTagLink(tag)
|
||||
this.$router.push(link)
|
||||
return
|
||||
}
|
||||
}
|
||||
window.open(target.href, '_blank')
|
||||
}
|
||||
},
|
||||
toggleShowMore () {
|
||||
if (this.mightHideBecauseTall) {
|
||||
this.showingTall = !this.showingTall
|
||||
} else if (this.mightHideBecauseSubject) {
|
||||
this.expandingSubject = !this.expandingSubject
|
||||
}
|
||||
},
|
||||
generateUserProfileLink (id, name) {
|
||||
return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames)
|
||||
},
|
||||
generateTagLink (tag) {
|
||||
return `/tag/${tag}`
|
||||
},
|
||||
setMedia () {
|
||||
const attachments = this.attachmentSize === 'hide' ? this.status.attachments : this.galleryAttachments
|
||||
return () => this.$store.dispatch('setMedia', attachments)
|
||||
|
|
|
@ -1,98 +1,20 @@
|
|||
<template>
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<div class="StatusContent">
|
||||
<slot name="header" />
|
||||
<div
|
||||
v-if="status.summary_html"
|
||||
class="summary-wrapper"
|
||||
:class="{ 'tall-subject': (longSubject && !showingLongSubject) }"
|
||||
<StatusBody
|
||||
:status="status"
|
||||
:single-line="singleLine"
|
||||
@parseReady="$emit('parseReady', $event)"
|
||||
>
|
||||
<div
|
||||
class="media-body summary"
|
||||
@click.prevent="linkClicked"
|
||||
v-html="status.summary_html"
|
||||
<div v-if="status.poll && status.poll.options">
|
||||
<Poll
|
||||
:base-poll="status.poll"
|
||||
:emoji="status.emojis"
|
||||
/>
|
||||
<button
|
||||
v-if="longSubject && showingLongSubject"
|
||||
class="button-unstyled -link tall-subject-hider"
|
||||
@click.prevent="showingLongSubject=false"
|
||||
>
|
||||
{{ $t("status.hide_full_subject") }}
|
||||
</button>
|
||||
<button
|
||||
v-else-if="longSubject"
|
||||
class="button-unstyled -link tall-subject-hider"
|
||||
:class="{ 'tall-subject-hider_focused': focused }"
|
||||
@click.prevent="showingLongSubject=true"
|
||||
>
|
||||
{{ $t("status.show_full_subject") }}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
:class="{'tall-status': hideTallStatus}"
|
||||
class="status-content-wrapper"
|
||||
>
|
||||
<button
|
||||
v-if="hideTallStatus"
|
||||
class="button-unstyled -link tall-status-hider"
|
||||
:class="{ 'tall-status-hider_focused': focused }"
|
||||
@click.prevent="toggleShowMore"
|
||||
>
|
||||
{{ $t("general.show_more") }}
|
||||
</button>
|
||||
<div
|
||||
v-if="!hideSubjectStatus"
|
||||
:class="{ 'single-line': singleLine }"
|
||||
class="status-content media-body"
|
||||
@click.prevent="linkClicked"
|
||||
v-html="postBodyHtml"
|
||||
/>
|
||||
<button
|
||||
v-if="hideSubjectStatus"
|
||||
class="button-unstyled -link cw-status-hider"
|
||||
@click.prevent="toggleShowMore"
|
||||
>
|
||||
{{ $t("status.show_content") }}
|
||||
<FAIcon
|
||||
v-if="attachmentTypes.includes('image')"
|
||||
icon="image"
|
||||
/>
|
||||
<FAIcon
|
||||
v-if="attachmentTypes.includes('video')"
|
||||
icon="video"
|
||||
/>
|
||||
<FAIcon
|
||||
v-if="attachmentTypes.includes('audio')"
|
||||
icon="music"
|
||||
/>
|
||||
<FAIcon
|
||||
v-if="attachmentTypes.includes('unknown')"
|
||||
icon="file"
|
||||
/>
|
||||
<FAIcon
|
||||
v-if="status.poll && status.poll.options"
|
||||
icon="poll-h"
|
||||
/>
|
||||
<FAIcon
|
||||
v-if="status.card"
|
||||
icon="link"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
v-if="showingMore && !fullContent"
|
||||
class="button-unstyled -link status-unhider"
|
||||
@click.prevent="toggleShowMore"
|
||||
>
|
||||
{{ tallStatus ? $t("general.show_less") : $t("status.hide_content") }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="status.poll && status.poll.options && !hideSubjectStatus">
|
||||
<poll :base-poll="status.poll" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="status.attachments.length !== 0 && (!hideSubjectStatus || showingLongSubject)"
|
||||
v-if="status.attachments.length !== 0"
|
||||
class="attachments media-body"
|
||||
>
|
||||
<attachment
|
||||
|
@ -116,7 +38,7 @@
|
|||
</div>
|
||||
|
||||
<div
|
||||
v-if="status.card && !hideSubjectStatus && !noHeading"
|
||||
v-if="status.card && !noHeading"
|
||||
class="link-preview media-body"
|
||||
>
|
||||
<link-preview
|
||||
|
@ -125,9 +47,9 @@
|
|||
:nsfw="nsfwClickthrough"
|
||||
/>
|
||||
</div>
|
||||
</StatusBody>
|
||||
<slot name="footer" />
|
||||
</div>
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
</template>
|
||||
|
||||
<script src="./status_content.js" ></script>
|
||||
|
@ -139,156 +61,5 @@ $status-margin: 0.75em;
|
|||
.StatusContent {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.status-content-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.tall-status {
|
||||
position: relative;
|
||||
height: 220px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: hidden;
|
||||
z-index: 1;
|
||||
.status-content {
|
||||
min-height: 0;
|
||||
mask: linear-gradient(to top, white, transparent) bottom/100% 70px no-repeat,
|
||||
linear-gradient(to top, white, white);
|
||||
/* Autoprefixed seem to ignore this one, and also syntax is different */
|
||||
-webkit-mask-composite: xor;
|
||||
mask-composite: exclude;
|
||||
}
|
||||
}
|
||||
|
||||
.tall-status-hider {
|
||||
display: inline-block;
|
||||
word-break: break-all;
|
||||
position: absolute;
|
||||
height: 70px;
|
||||
margin-top: 150px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
line-height: 110px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.status-unhider, .cw-status-hider {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
word-break: break-all;
|
||||
|
||||
svg {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
img, video {
|
||||
max-width: 100%;
|
||||
max-height: 400px;
|
||||
vertical-align: middle;
|
||||
object-fit: contain;
|
||||
|
||||
&.emoji {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.summary-wrapper {
|
||||
margin-bottom: 0.5em;
|
||||
border-style: solid;
|
||||
border-width: 0 0 1px 0;
|
||||
border-color: var(--border, $fallback--border);
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.summary {
|
||||
font-style: italic;
|
||||
padding-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.tall-subject {
|
||||
position: relative;
|
||||
.summary {
|
||||
max-height: 2em;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.tall-subject-hider {
|
||||
display: inline-block;
|
||||
word-break: break-all;
|
||||
// position: absolute;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
padding-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.status-content {
|
||||
font-family: var(--postFont, sans-serif);
|
||||
line-height: 1.4em;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
|
||||
blockquote {
|
||||
margin: 0.2em 0 0.2em 2em;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
pre {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
code, samp, kbd, var, pre {
|
||||
font-family: var(--postCodeFont, monospace);
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0 0 1em 0;
|
||||
}
|
||||
|
||||
p:last-child {
|
||||
margin: 0 0 0 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.1em;
|
||||
line-height: 1.2em;
|
||||
margin: 1.4em 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.1em;
|
||||
margin: 1.0em 0;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1em;
|
||||
margin: 1.2em 0;
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 1.1em 0;
|
||||
}
|
||||
|
||||
&.single-line {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
height: 1.4em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.greentext {
|
||||
color: $fallback--cGreen;
|
||||
color: var(--postGreentext, $fallback--cGreen);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
position: relative;
|
||||
line-height: 0;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
canvas {
|
||||
|
@ -47,12 +47,13 @@
|
|||
|
||||
img {
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
&.animated {
|
||||
&::before {
|
||||
zoom: var(--_still_image-label-scale, 1);
|
||||
content: 'gif';
|
||||
position: absolute;
|
||||
line-height: 10px;
|
||||
|
|
|
@ -5,6 +5,7 @@ import FollowButton from '../follow_button/follow_button.vue'
|
|||
import ModerationTools from '../moderation_tools/moderation_tools.vue'
|
||||
import AccountActions from '../account_actions/account_actions.vue'
|
||||
import Select from '../select/select.vue'
|
||||
import RichContent from 'src/components/rich_content/rich_content.jsx'
|
||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||
import { mapGetters } from 'vuex'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
|
@ -120,7 +121,8 @@ export default {
|
|||
AccountActions,
|
||||
ProgressButton,
|
||||
FollowButton,
|
||||
Select
|
||||
Select,
|
||||
RichContent
|
||||
},
|
||||
methods: {
|
||||
muteUser () {
|
||||
|
|
|
@ -38,21 +38,12 @@
|
|||
</router-link>
|
||||
<div class="user-summary">
|
||||
<div class="top-line">
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<div
|
||||
v-if="user.name_html"
|
||||
<RichContent
|
||||
:title="user.name"
|
||||
class="user-name"
|
||||
v-html="user.name_html"
|
||||
:html="user.name"
|
||||
:emoji="user.emoji"
|
||||
/>
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
<div
|
||||
v-else
|
||||
:title="user.name"
|
||||
class="user-name"
|
||||
>
|
||||
{{ user.name }}
|
||||
</div>
|
||||
<button
|
||||
v-if="!isOtherUser && user.is_local"
|
||||
class="button-unstyled edit-profile-button"
|
||||
|
@ -65,7 +56,7 @@
|
|||
:title="$t('user_card.edit_profile')"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
<a
|
||||
v-if="isOtherUser && !user.is_local"
|
||||
:href="user.statusnet_profile_url"
|
||||
target="_blank"
|
||||
|
@ -75,7 +66,7 @@
|
|||
class="icon"
|
||||
icon="external-link-alt"
|
||||
/>
|
||||
</button>
|
||||
</a>
|
||||
<AccountActions
|
||||
v-if="isOtherUser && loggedIn"
|
||||
:user="user"
|
||||
|
@ -267,20 +258,12 @@
|
|||
<span>{{ hideFollowersCount ? $t('user_card.hidden') : user.followers_count }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<p
|
||||
v-if="!hideBio && user.description_html"
|
||||
<RichContent
|
||||
v-if="!hideBio"
|
||||
class="user-card-bio"
|
||||
@click.prevent="linkClicked"
|
||||
v-html="user.description_html"
|
||||
:html="user.description_html"
|
||||
:emoji="user.emoji"
|
||||
/>
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
<p
|
||||
v-else-if="!hideBio"
|
||||
class="user-card-bio"
|
||||
>
|
||||
{{ user.description }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -293,9 +276,10 @@
|
|||
.user-card {
|
||||
position: relative;
|
||||
|
||||
&:hover .Avatar {
|
||||
&:hover {
|
||||
--_still-image-img-visibility: visible;
|
||||
--_still-image-canvas-visibility: hidden;
|
||||
--_still-image-label-visibility: hidden;
|
||||
}
|
||||
|
||||
.panel-heading {
|
||||
|
@ -339,12 +323,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&-bio {
|
||||
text-align: center;
|
||||
display: block;
|
||||
line-height: 18px;
|
||||
padding: 1em;
|
||||
margin: 0;
|
||||
|
||||
a {
|
||||
color: $fallback--link;
|
||||
|
@ -356,11 +340,6 @@
|
|||
vertical-align: middle;
|
||||
max-width: 100%;
|
||||
max-height: 400px;
|
||||
|
||||
&.emoji {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -462,13 +441,6 @@
|
|||
// big one
|
||||
z-index: 1;
|
||||
|
||||
img {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
vertical-align: middle;
|
||||
object-fit: contain
|
||||
}
|
||||
|
||||
.top-line {
|
||||
display: flex;
|
||||
}
|
||||
|
@ -481,12 +453,7 @@
|
|||
margin-right: 1em;
|
||||
font-size: 15px;
|
||||
|
||||
img {
|
||||
object-fit: contain;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
--emoji-size: 14px;
|
||||
}
|
||||
|
||||
.bottom-line {
|
||||
|
|
|
@ -4,6 +4,7 @@ import FollowCard from '../follow_card/follow_card.vue'
|
|||
import Timeline from '../timeline/timeline.vue'
|
||||
import Conversation from '../conversation/conversation.vue'
|
||||
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.js'
|
||||
import RichContent from 'src/components/rich_content/rich_content.jsx'
|
||||
import List from '../list/list.vue'
|
||||
import withLoadMore from '../../hocs/with_load_more/with_load_more'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
|
@ -164,7 +165,8 @@ const UserProfile = {
|
|||
FriendList,
|
||||
FollowCard,
|
||||
TabSwitcher,
|
||||
Conversation
|
||||
Conversation,
|
||||
RichContent
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -20,20 +20,24 @@
|
|||
:key="index"
|
||||
class="user-profile-field"
|
||||
>
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<dt
|
||||
:title="user.fields_text[index].name"
|
||||
class="user-profile-field-name"
|
||||
@click.prevent="linkClicked"
|
||||
v-html="field.name"
|
||||
>
|
||||
<RichContent
|
||||
:html="field.name"
|
||||
:emoji="user.emoji"
|
||||
/>
|
||||
</dt>
|
||||
<dd
|
||||
:title="user.fields_text[index].value"
|
||||
class="user-profile-field-value"
|
||||
@click.prevent="linkClicked"
|
||||
v-html="field.value"
|
||||
>
|
||||
<RichContent
|
||||
:html="field.value"
|
||||
:emoji="user.emoji"
|
||||
/>
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<tab-switcher
|
||||
|
|
545
src/i18n/ca.json
545
src/i18n/ca.json
|
@ -10,11 +10,12 @@
|
|||
"text_limit": "Límit de text",
|
||||
"title": "Funcionalitats",
|
||||
"who_to_follow": "A qui seguir",
|
||||
"pleroma_chat_messages": "Xat de Pleroma"
|
||||
"pleroma_chat_messages": "Xat de Pleroma",
|
||||
"upload_limit": "Límit de càrrega"
|
||||
},
|
||||
"finder": {
|
||||
"error_fetching_user": "No s'ha pogut carregar l'usuari/a",
|
||||
"find_user": "Find user"
|
||||
"find_user": "Trobar usuari"
|
||||
},
|
||||
"general": {
|
||||
"apply": "Aplica",
|
||||
|
@ -32,7 +33,16 @@
|
|||
"error_retry": "Si us plau, prova de nou",
|
||||
"generic_error": "Hi ha hagut un error",
|
||||
"loading": "Carregant…",
|
||||
"more": "Més"
|
||||
"more": "Més",
|
||||
"flash_content": "Fes clic per mostrar el contingut Flash utilitzant Ruffle (experimental, pot no funcionar).",
|
||||
"flash_security": "Tingues en compte que això pot ser potencialment perillós, ja que el contingut Flash encara és un codi arbitrari.",
|
||||
"flash_fail": "No s'ha pogut carregar el contingut del flaix, consulta la consola per als detalls.",
|
||||
"role": {
|
||||
"moderator": "Moderador/a",
|
||||
"admin": "Administrador/a"
|
||||
},
|
||||
"dismiss": "Descartar",
|
||||
"peek": "Donar un cop d'ull"
|
||||
},
|
||||
"login": {
|
||||
"login": "Inicia sessió",
|
||||
|
@ -45,15 +55,20 @@
|
|||
"enter_recovery_code": "Posa un codi de recuperació",
|
||||
"authentication_code": "Codi d'autenticació",
|
||||
"hint": "Entra per participar a la conversa",
|
||||
"description": "Entra amb OAuth"
|
||||
"description": "Entra amb OAuth",
|
||||
"heading": {
|
||||
"totp": "Autenticació de dos factors",
|
||||
"recovery": "Recuperació de dos factors"
|
||||
},
|
||||
"enter_two_factor_code": "Introdueix un codi de dos factors"
|
||||
},
|
||||
"nav": {
|
||||
"chat": "Xat local públic",
|
||||
"friend_requests": "Soŀlicituds de connexió",
|
||||
"friend_requests": "Sol·licituds de seguiment",
|
||||
"mentions": "Mencions",
|
||||
"public_tl": "Flux públic del node",
|
||||
"public_tl": "Línia temporal pública",
|
||||
"timeline": "Flux personal",
|
||||
"twkn": "Flux de la xarxa coneguda",
|
||||
"twkn": "Xarxa coneguda",
|
||||
"chats": "Xats",
|
||||
"timelines": "Línies de temps",
|
||||
"preferences": "Preferències",
|
||||
|
@ -62,19 +77,25 @@
|
|||
"dms": "Missatges directes",
|
||||
"interactions": "Interaccions",
|
||||
"back": "Enrere",
|
||||
"administration": "Administració"
|
||||
"administration": "Administració",
|
||||
"about": "Quant a",
|
||||
"bookmarks": "Marcadors",
|
||||
"user_search": "Cerca d'usuaris",
|
||||
"home_timeline": "Línea temporal personal"
|
||||
},
|
||||
"notifications": {
|
||||
"broken_favorite": "No es coneix aquest estat. S'està cercant.",
|
||||
"broken_favorite": "Publicació desconeguda, s'està cercant…",
|
||||
"favorited_you": "ha marcat un estat teu",
|
||||
"followed_you": "ha començat a seguir-te",
|
||||
"load_older": "Carrega més notificacions",
|
||||
"notifications": "Notificacions",
|
||||
"read": "Read!",
|
||||
"read": "Llegit!",
|
||||
"repeated_you": "ha repetit el teu estat",
|
||||
"migrated_to": "migrat a",
|
||||
"no_more_notifications": "No més notificacions",
|
||||
"follow_request": "et vol seguir"
|
||||
"follow_request": "et vol seguir",
|
||||
"reacted_with": "ha reaccionat amb {0}",
|
||||
"error": "Error obtenint notificacions: {0}"
|
||||
},
|
||||
"post_status": {
|
||||
"account_not_locked_warning": "El teu compte no està {0}. Qualsevol persona pot seguir-te per llegir les teves entrades reservades només a seguidores.",
|
||||
|
@ -83,24 +104,33 @@
|
|||
"content_type": {
|
||||
"text/plain": "Text pla",
|
||||
"text/markdown": "Markdown",
|
||||
"text/html": "HTML"
|
||||
"text/html": "HTML",
|
||||
"text/bbcode": "BBCode"
|
||||
},
|
||||
"content_warning": "Assumpte (opcional)",
|
||||
"default": "Em sento…",
|
||||
"default": "Acabe d'aterrar a L.A.",
|
||||
"direct_warning": "Aquesta entrada només serà visible per les usuràries que etiquetis",
|
||||
"posting": "Publicació",
|
||||
"scope": {
|
||||
"direct": "Directa - Publica només per les usuàries etiquetades",
|
||||
"private": "Només seguidors/es - Publica només per comptes que et segueixin",
|
||||
"public": "Pública - Publica als fluxos públics",
|
||||
"unlisted": "Silenciosa - No la mostris en fluxos públics"
|
||||
"direct": "Directa - publica només per als usuaris etiquetats",
|
||||
"private": "Només seguidors/es - publica només per comptes que et segueixin",
|
||||
"public": "Pública - publica als fluxos públics",
|
||||
"unlisted": "Silenciosa - no la mostris en fluxos públics"
|
||||
},
|
||||
"scope_notice": {
|
||||
"private": "Aquesta entrada serà visible només per a qui et segueixi",
|
||||
"public": "Aquesta entrada serà visible per a tothom"
|
||||
"public": "Aquesta entrada serà visible per a tothom",
|
||||
"unlisted": "Aquesta entrada no es veurà ni a la Línia de temps local ni a la Línia de temps federada"
|
||||
},
|
||||
"preview_empty": "Buida",
|
||||
"preview": "Vista prèvia"
|
||||
"preview": "Vista prèvia",
|
||||
"direct_warning_to_first_only": "Aquesta publicació només serà visible per als usuaris mencionats al principi del missatge.",
|
||||
"empty_status_error": "No es pot publicar un estat buit sense fitxers adjunts",
|
||||
"media_description": "Descripció multimèdia",
|
||||
"direct_warning_to_all": "Aquesta publicació serà visible per a tots els usuaris mencionats.",
|
||||
"new_status": "Publicar un nou estat",
|
||||
"post": "Publicació",
|
||||
"media_description_error": "Ha fallat la pujada del contingut. Prova de nou"
|
||||
},
|
||||
"registration": {
|
||||
"bio": "Presentació",
|
||||
|
@ -118,13 +148,19 @@
|
|||
"username_required": "no es pot deixar en blanc"
|
||||
},
|
||||
"fullname_placeholder": "p. ex. Lain Iwakura",
|
||||
"username_placeholder": "p. ex. lain"
|
||||
"username_placeholder": "p. ex. lain",
|
||||
"captcha": "CAPTCHA",
|
||||
"register": "Registrar-se",
|
||||
"reason": "Raó per a registrar-se",
|
||||
"bio_placeholder": "p.e.\nHola, sóc la Lain.\nSóc una noia anime que viu a un suburbi de Japó. Potser em coneixes per Wired.",
|
||||
"reason_placeholder": "Aquesta instància aprova els registres manualment.\nExplica a l'administració per què vols registrar-te.",
|
||||
"new_captcha": "Clica a la imatge per obtenir un nou captcha"
|
||||
},
|
||||
"settings": {
|
||||
"attachmentRadius": "Adjunts",
|
||||
"attachments": "Adjunts",
|
||||
"avatar": "Avatar",
|
||||
"avatarAltRadius": "Avatars en les notificacions",
|
||||
"avatarAltRadius": "Avatars (notificacions)",
|
||||
"avatarRadius": "Avatars",
|
||||
"background": "Fons de pantalla",
|
||||
"bio": "Presentació",
|
||||
|
@ -134,8 +170,8 @@
|
|||
"cOrange": "Taronja (marca com a preferit)",
|
||||
"cRed": "Vermell (canceŀla)",
|
||||
"change_password": "Canvia la contrasenya",
|
||||
"change_password_error": "No s'ha pogut canviar la contrasenya",
|
||||
"changed_password": "S'ha canviat la contrasenya",
|
||||
"change_password_error": "No s'ha pogut canviar la contrasenya.",
|
||||
"changed_password": "S'ha canviat la contrasenya correctament!",
|
||||
"collapse_subject": "Replega les entrades amb títol",
|
||||
"confirm_new_password": "Confirma la nova contrasenya",
|
||||
"current_avatar": "L'avatar actual",
|
||||
|
@ -176,7 +212,7 @@
|
|||
"new_password": "Contrasenya nova",
|
||||
"notification_visibility": "Notifica'm quan algú",
|
||||
"notification_visibility_follows": "Comença a seguir-me",
|
||||
"notification_visibility_likes": "Marca com a preferida una entrada meva",
|
||||
"notification_visibility_likes": "Favorits",
|
||||
"notification_visibility_mentions": "Em menciona",
|
||||
"notification_visibility_repeats": "Republica una entrada meva",
|
||||
"no_rich_text_description": "Neteja el formatat de text de totes les entrades",
|
||||
|
@ -193,7 +229,7 @@
|
|||
"profile_banner": "Fons de perfil",
|
||||
"profile_tab": "Perfil",
|
||||
"radii_help": "Configura l'arrodoniment de les vores (en píxels)",
|
||||
"replies_in_timeline": "Replies in timeline",
|
||||
"replies_in_timeline": "Respostes al flux",
|
||||
"reply_visibility_all": "Mostra totes les respostes",
|
||||
"reply_visibility_following": "Mostra només les respostes a entrades meves o d'usuàries que jo segueixo",
|
||||
"reply_visibility_self": "Mostra només les respostes a entrades meves",
|
||||
|
@ -216,7 +252,7 @@
|
|||
"true": "sí"
|
||||
},
|
||||
"show_moderator_badge": "Mostra una insígnia de Moderació en el meu perfil",
|
||||
"show_admin_badge": "Mostra una insígnia d'Administració en el meu perfil",
|
||||
"show_admin_badge": "Mostra una insígnia \"d'Administració\" en el meu perfil",
|
||||
"hide_followers_description": "No mostris qui m'està seguint",
|
||||
"hide_follows_description": "No mostris a qui segueixo",
|
||||
"notification_visibility_emoji_reactions": "Reaccions",
|
||||
|
@ -254,25 +290,257 @@
|
|||
"allow_following_move": "Permet el seguiment automàtic quan un compte a qui seguim es mou",
|
||||
"mfa": {
|
||||
"scan": {
|
||||
"secret_code": "Clau"
|
||||
"secret_code": "Clau",
|
||||
"title": "Escanejar",
|
||||
"desc": "S'està usant l'aplicació two-factor, escaneja aquest codi QR o introdueix la clau de text:"
|
||||
},
|
||||
"authentication_methods": "Mètodes d'autenticació",
|
||||
"waiting_a_recovery_codes": "Rebent còpies de seguretat dels codis…",
|
||||
"recovery_codes": "Codis de recuperació.",
|
||||
"warning_of_generate_new_codes": "Quan generes nous codis de recuperació, els antics ja no funcionaran més.",
|
||||
"generate_new_recovery_codes": "Genera nous codis de recuperació"
|
||||
"generate_new_recovery_codes": "Genera nous codis de recuperació",
|
||||
"otp": "OTP",
|
||||
"confirm_and_enable": "Confirmar i habilitar OTP",
|
||||
"recovery_codes_warning": "Anote els codis o guarda'ls en un lloc segur, o no els veuràs una altra volta. Si perds l'accés a la teua aplicació 2FA i els codis de recuperació, no podràs accedir al compte.",
|
||||
"title": "Autenticació de dos factors",
|
||||
"setup_otp": "Configurar OTP",
|
||||
"wait_pre_setup_otp": "preconfiguració OTP",
|
||||
"verify": {
|
||||
"desc": "Per habilitar l'autenticació two-factor, introdueix el codi des de la teva aplicació two-factor:"
|
||||
}
|
||||
},
|
||||
"enter_current_password_to_confirm": "Posar la contrasenya actual per confirmar la teva identitat",
|
||||
"security": "Seguretat",
|
||||
"app_name": "Nom de l'aplicació"
|
||||
"app_name": "Nom de l'aplicació",
|
||||
"subject_line_mastodon": "Com a mastodon: copiar com és",
|
||||
"mute_export_button": "Exportar silenciats a un fitxer csv",
|
||||
"mute_import_error": "Error al importar silenciats",
|
||||
"mutes_imported": "Silenciats importats! Processar-los portarà una estona.",
|
||||
"import_mutes_from_a_csv_file": "Importar silenciats des d'un fitxer csv",
|
||||
"word_filter": "Filtre de paraules",
|
||||
"hide_media_previews": "Ocultar les vistes prèvies multimèdia",
|
||||
"hide_filtered_statuses": "Amagar estats filtrats",
|
||||
"play_videos_in_modal": "Reproduir vídeos en un marc emergent",
|
||||
"file_export_import": {
|
||||
"errors": {
|
||||
"invalid_file": "El fitxer seleccionat no és vàlid com a còpia de seguretat de la configuració. No s'ha realitzat cap canvi."
|
||||
},
|
||||
"backup_settings": "Còpia de seguretat de la configuració a un fitxer",
|
||||
"backup_settings_theme": "Còpia de seguretat de la configuració i tema a un fitxer",
|
||||
"restore_settings": "Restaurar configuració des d'un fitxer",
|
||||
"backup_restore": "Còpia de seguretat de la configuració"
|
||||
},
|
||||
"user_mutes": "Usuaris",
|
||||
"subject_line_email": "Com a l'email: \"re: tema\"",
|
||||
"search_user_to_block": "Busca a qui vols bloquejar",
|
||||
"save": "Guardar els canvis",
|
||||
"use_contain_fit": "No retallar els adjunts en miniatures",
|
||||
"reset_profile_background": "Restablir fons del perfil",
|
||||
"reset_profile_banner": "Restablir banner del perfil",
|
||||
"emoji_reactions_on_timeline": "Mostrar reaccions emoji al flux",
|
||||
"max_thumbnails": "Quantitat màxima de miniatures per publicació",
|
||||
"hide_user_stats": "Amagar les estadístiques de l'usuari (p. ex. el nombre de seguidors)",
|
||||
"reset_banner_confirm": "Realment vols restablir el banner?",
|
||||
"reset_background_confirm": "Realment vols restablir el fons del perfil?",
|
||||
"subject_input_always_show": "Sempre mostrar el camp del tema",
|
||||
"subject_line_noop": "No copiar",
|
||||
"subject_line_behavior": "Copiar el tema a les respostes",
|
||||
"search_user_to_mute": "Busca a qui vols silenciar",
|
||||
"mute_export": "Exportar silenciats",
|
||||
"scope_copy": "Copiar visibilitat quan contestes (En els missatges directes sempre es copia)",
|
||||
"reset_avatar": "Restablir avatar",
|
||||
"right_sidebar": "Mostrar barra lateral a la dreta",
|
||||
"no_blocks": "No hi han bloquejats",
|
||||
"no_mutes": "No hi han silenciats",
|
||||
"hide_follows_count_description": "No mostrar el nombre de comptes que segueixo",
|
||||
"mute_import": "Importar silenciats",
|
||||
"hide_all_muted_posts": "Ocultar publicacions silenciades",
|
||||
"hide_wallpaper": "Amagar el fons de la instància",
|
||||
"notification_visibility_moves": "Usuari Migrat",
|
||||
"reply_visibility_following_short": "Mostrar respostes als meus seguidors",
|
||||
"reply_visibility_self_short": "Mostrar respostes només a un mateix",
|
||||
"autohide_floating_post_button": "Ocultar automàticament el botó 'Nova Publicació' (mòbil)",
|
||||
"minimal_scopes_mode": "Minimitzar les opcions de visibilitat de la publicació",
|
||||
"sensitive_by_default": "Marcar publicacions com a sensibles per defecte",
|
||||
"useStreamingApi": "Rebre publicacions i notificacions en temps real",
|
||||
"hide_isp": "Ocultar el panell especific de la instància",
|
||||
"preload_images": "Precarregar les imatges",
|
||||
"setting_changed": "La configuració és diferent a la predeterminada",
|
||||
"hide_followers_count_description": "No mostrar el nombre de seguidors",
|
||||
"reset_avatar_confirm": "Realment vols restablir l'avatar?",
|
||||
"accent": "Accent",
|
||||
"useStreamingApiWarning": "(No recomanat, experimental, pot ometre publicacions)",
|
||||
"style": {
|
||||
"fonts": {
|
||||
"family": "Nom de la font",
|
||||
"size": "Mida (en píxels)",
|
||||
"custom": "Personalitza",
|
||||
"_tab_label": "Fonts",
|
||||
"help": "Selecciona la font per als elements de la interfície. Per a \"personalitzat\" deus escriure el nom de la font exactament com apareix al sistema.",
|
||||
"components": {
|
||||
"post": "Text de les publicacions",
|
||||
"postCode": "Text monoespai en publicació (text enriquit)",
|
||||
"input": "Camps d'entrada",
|
||||
"interface": "Interfície"
|
||||
}
|
||||
},
|
||||
"preview": {
|
||||
"input": "Acabo d'aterrar a Los Angeles.",
|
||||
"button": "Botó",
|
||||
"mono": "contingut",
|
||||
"content": "Contingut",
|
||||
"header": "Previsualització",
|
||||
"header_faint": "Això està bé",
|
||||
"error": "Exemple d'error",
|
||||
"faint_link": "Manual d'ajuda",
|
||||
"checkbox": "He llegit els termes i condicions",
|
||||
"link": "un bonic enllaç"
|
||||
},
|
||||
"shadows": {
|
||||
"spread": "Difon",
|
||||
"filter_hint": {
|
||||
"drop_shadow_syntax": "{0} no suporta el paràmetre {1} i la paraula clau {2}.",
|
||||
"avatar_inset": "Tingues en compte que combinar ombres interiors i no interiors als avatars podria donar resultats inesperats amb avatars transparents.",
|
||||
"inset_classic": "Les ombres interiors estaran usant {0}",
|
||||
"always_drop_shadow": "Advertència, aquesta ombra sempre utilitza {0} quan el navegador ho suporta.",
|
||||
"spread_zero": "Ombres amb propagació > 0 apareixeran com si estigueren posades a zero"
|
||||
},
|
||||
"components": {
|
||||
"popup": "Texts i finestres emergents (popups & tooltips)",
|
||||
"panel": "Panell",
|
||||
"panelHeader": "Capçalera del panell",
|
||||
"avatar": "Avatar de l'usuari (en vista de perfil)",
|
||||
"input": "Camp d'entrada",
|
||||
"buttonHover": "Botó (surant)",
|
||||
"buttonPressed": "Botó (pressionat)",
|
||||
"topBar": "Barra superior",
|
||||
"buttonPressedHover": "Botó (surant i pressionat)",
|
||||
"avatarStatus": "Avatar de l'usuari (en vista de publicació)",
|
||||
"button": "Botó"
|
||||
},
|
||||
"hintV3": "per a les ombres també pots usar la notació {0} per a utilitzar un altre espai de color.",
|
||||
"blur": "Difuminat",
|
||||
"component": "Component",
|
||||
"override": "Sobreescriure",
|
||||
"shadow_id": "Ombra #{value}",
|
||||
"_tab_label": "Ombra i il·luminació",
|
||||
"inset": "Ombra interior"
|
||||
},
|
||||
"switcher": {
|
||||
"use_snapshot": "Versió antiga",
|
||||
"help": {
|
||||
"future_version_imported": "El fitxer importat es va crear per a una versió del front-end més recent.",
|
||||
"migration_snapshot_ok": "Per a estar segurs, s'ha carregat la instantània del tema. Pots intentar carregar les dades del tema.",
|
||||
"migration_napshot_gone": "Per alguna raó, faltava la instantània, algunes coses podrien veure's diferents del que recordes.",
|
||||
"snapshot_source_mismatch": "Conflicte de versions: probablement el front-end s'ha revertit i actualitzat una altra volta, si has canviat el tema en una versió anterior, segurament vols utilitzar la versió antiga; d'altra banda utilitza la nova versió.",
|
||||
"v2_imported": "El fitxer que has importat va ser creat per a un front-end més antic. Intentem maximitzar la compatibilitat, però podrien haver inconsistències.",
|
||||
"fe_upgraded": "El motor de temes de PleromaFE es va actualitzar després de l'actualització de la versió.",
|
||||
"snapshot_missing": "No hi havia cap instantània del tema al fitxer, per tant podria veure's diferent del previst originalment.",
|
||||
"upgraded_from_v2": "PleromaFE s'ha actualitzat, el tema pot veure's un poc diferent de com recordes.",
|
||||
"fe_downgraded": "Versió de PleromaFE revertida.",
|
||||
"older_version_imported": "El fitxer que has importat va ser creat en una versió del front-end més antiga."
|
||||
},
|
||||
"keep_as_is": "Mantindre com està",
|
||||
"save_load_hint": "Les opcions \"Mantindre\" conserven les opcions configurades actualment al seleccionar o carregar temes, també emmagatzema aquestes opcions quan s'exporta un tema. Quan es desactiven totes les caselles de verificació, el tema exportat ho guardarà tot.",
|
||||
"keep_color": "Mantindre colors",
|
||||
"keep_opacity": "Mantindre opacitat",
|
||||
"keep_shadows": "Mantindre ombres",
|
||||
"keep_fonts": "Mantindre fonts",
|
||||
"keep_roundness": "Mantindre rodoneses",
|
||||
"clear_all": "Netejar tot",
|
||||
"reset": "Reinciar",
|
||||
"load_theme": "Carregar tema",
|
||||
"use_source": "Nova versió",
|
||||
"clear_opacity": "Netejar opacitat"
|
||||
},
|
||||
"common": {
|
||||
"contrast": {
|
||||
"hint": "El ràtio de contrast és {ratio}. {level} {context}",
|
||||
"level": {
|
||||
"bad": "no compleix amb cap pauta d'accecibilitat",
|
||||
"aaa": "Compleix amb el nivell AA (recomanat)",
|
||||
"aa": "Compleix amb el nivell AA (mínim)"
|
||||
},
|
||||
"context": {
|
||||
"18pt": "per a textos grans (+18pt)",
|
||||
"text": "per a textos"
|
||||
}
|
||||
},
|
||||
"opacity": "Opacitat",
|
||||
"color": "Color"
|
||||
},
|
||||
"advanced_colors": {
|
||||
"badge": "Fons de insígnies",
|
||||
"inputs": "Camps d'entrada",
|
||||
"wallpaper": "Fons de pantalla",
|
||||
"pressed": "Pressionat",
|
||||
"chat": {
|
||||
"outgoing": "Eixint",
|
||||
"border": "Borde",
|
||||
"incoming": "Entrants"
|
||||
},
|
||||
"borders": "Bordes",
|
||||
"panel_header": "Capçalera del panell",
|
||||
"buttons": "Botons",
|
||||
"faint_text": "Text esvaït",
|
||||
"poll": "Gràfica de l'enquesta",
|
||||
"toggled": "Commutat",
|
||||
"alert": "Fons d'alertes",
|
||||
"alert_error": "Error",
|
||||
"alert_warning": "Precaució",
|
||||
"post": "Publicacions/Biografies d'usuaris",
|
||||
"badge_notification": "Notificacions",
|
||||
"selectedMenu": "Element del menú seleccionat",
|
||||
"tabs": "Pestanyes",
|
||||
"_tab_label": "Avançat",
|
||||
"alert_neutral": "Neutral",
|
||||
"popover": "Suggeriments, menús, superposicions",
|
||||
"top_bar": "Barra superior",
|
||||
"highlight": "Elements destacats",
|
||||
"disabled": "Deshabilitat",
|
||||
"icons": "Icones",
|
||||
"selectedPost": "Publicació seleccionada",
|
||||
"underlay": "Subratllat"
|
||||
},
|
||||
"common_colors": {
|
||||
"main": "Colors comuns",
|
||||
"rgbo": "Icones, accents, insígnies",
|
||||
"foreground_hint": "mira la pestanya \"Avançat\" per a un control més detallat",
|
||||
"_tab_label": "Comú"
|
||||
},
|
||||
"radii": {
|
||||
"_tab_label": "Rodonesa"
|
||||
}
|
||||
},
|
||||
"version": {
|
||||
"frontend_version": "Versió \"Frontend\"",
|
||||
"backend_version": "Versió \"backend\"",
|
||||
"title": "Versió"
|
||||
},
|
||||
"theme_help_v2_1": "També pots anular alguns components de color i opacitat activant la casella. Usa el botó \"Esborrar tot\" per esborrar totes les anulacions.",
|
||||
"type_domains_to_mute": "Buscar dominis per a silenciar",
|
||||
"greentext": "Text verd (meme arrows)",
|
||||
"fun": "Divertit",
|
||||
"notification_setting_filters": "Filtres",
|
||||
"virtual_scrolling": "Optimitzar la representació del flux",
|
||||
"notification_setting_block_from_strangers": "Bloqueja les notificacions dels usuaris que no segueixes",
|
||||
"enable_web_push_notifications": "Habilitar notificacions del navegador",
|
||||
"notification_blocks": "Bloquejar a un usuari para totes les notificacions i també les cancel·la.",
|
||||
"more_settings": "Més opcions",
|
||||
"notification_setting_privacy": "Privacitat",
|
||||
"upload_a_photo": "Pujar una foto",
|
||||
"notification_setting_hide_notification_contents": "Amagar el remitent i els continguts de les notificacions push",
|
||||
"notifications": "Notificacions",
|
||||
"notification_mutes": "Per a deixar de rebre notificacions d'un usuari en concret, silencia'l-ho.",
|
||||
"theme_help_v2_2": "Les icones per baix d'algunes entrades són indicadors del contrast del fons/text, desplaça el ratolí per a més informació. Tingues en compte que quan s'utilitzen indicadors de contrast de transparència es mostra el pitjor cas possible."
|
||||
},
|
||||
"time": {
|
||||
"day": "{0} dia",
|
||||
"days": "{0} dies",
|
||||
"day_short": "{0} dia",
|
||||
"days_short": "{0} dies",
|
||||
"hour": "{0} hour",
|
||||
"hours": "{0} hours",
|
||||
"hour": "{0} hora",
|
||||
"hours": "{0} hores",
|
||||
"hour_short": "{0}h",
|
||||
"hours_short": "{0}h",
|
||||
"in_future": "in {0}",
|
||||
|
@ -287,12 +555,12 @@
|
|||
"months_short": "{0} mesos",
|
||||
"now": "ara mateix",
|
||||
"now_short": "ara mateix",
|
||||
"second": "{0} second",
|
||||
"seconds": "{0} seconds",
|
||||
"second": "{0} segon",
|
||||
"seconds": "{0} segons",
|
||||
"second_short": "{0}s",
|
||||
"seconds_short": "{0}s",
|
||||
"week": "{0} setm.",
|
||||
"weeks": "{0} setm.",
|
||||
"week": "{0} setmana",
|
||||
"weeks": "{0} setmanes",
|
||||
"week_short": "{0} setm.",
|
||||
"weeks_short": "{0} setm.",
|
||||
"year": "{0} any",
|
||||
|
@ -308,7 +576,13 @@
|
|||
"no_retweet_hint": "L'entrada és només per a seguidores o és \"directa\", i per tant no es pot republicar",
|
||||
"repeated": "republicat",
|
||||
"show_new": "Mostra els nous",
|
||||
"up_to_date": "Actualitzat"
|
||||
"up_to_date": "Actualitzat",
|
||||
"socket_reconnected": "Connexió a temps real establerta",
|
||||
"socket_broke": "Connexió a temps real perduda: codi CloseEvent {0}",
|
||||
"error": "Error de càrrega de la línia de temps: {0}",
|
||||
"no_statuses": "No hi ha entrades",
|
||||
"reload": "Recarrega",
|
||||
"no_more_statuses": "No hi ha més entrades"
|
||||
},
|
||||
"user_card": {
|
||||
"approve": "Aprova",
|
||||
|
@ -324,13 +598,60 @@
|
|||
"muted": "Silenciat",
|
||||
"per_day": "per dia",
|
||||
"remote_follow": "Seguiment remot",
|
||||
"statuses": "Estats"
|
||||
"statuses": "Estats",
|
||||
"unblock_progress": "Desbloquejant…",
|
||||
"unmute": "Deixa de silenciar",
|
||||
"follow_progress": "Sol·licitant…",
|
||||
"admin_menu": {
|
||||
"force_nsfw": "Marca totes les entrades amb \"No segur per a entorns laborals\"",
|
||||
"strip_media": "Esborra els audiovisuals de les entrades",
|
||||
"disable_any_subscription": "Deshabilita completament seguir algú",
|
||||
"quarantine": "Deshabilita la federació a les entrades de les usuàries",
|
||||
"moderation": "Moderació",
|
||||
"delete_user_confirmation": "Estàs completament segur/a? Aquesta acció no es pot desfer.",
|
||||
"revoke_admin": "Revoca l'Admin",
|
||||
"activate_account": "Activa el compte",
|
||||
"deactivate_account": "Desactiva el compte",
|
||||
"revoke_moderator": "Revoca Moderació",
|
||||
"delete_account": "Esborra el compte",
|
||||
"disable_remote_subscription": "Deshabilita seguir algú des d'una instància remota",
|
||||
"delete_user": "Esborra la usuària",
|
||||
"grant_admin": "Concedir permisos d'Administració",
|
||||
"grant_moderator": "Concedir permisos de Moderació"
|
||||
},
|
||||
"edit_profile": "Edita el perfil",
|
||||
"follow_again": "Envia de nou la petició?",
|
||||
"hidden": "Amagat",
|
||||
"follow_sent": "Petició enviada!",
|
||||
"unmute_progress": "Deixant de silenciar…",
|
||||
"bot": "Bot",
|
||||
"mute_progress": "Silenciant…",
|
||||
"favorites": "Favorits",
|
||||
"mention": "Menció",
|
||||
"follow_unfollow": "Deixa de seguir",
|
||||
"subscribe": "Subscriu-te",
|
||||
"show_repeats": "Mostra les repeticions",
|
||||
"report": "Report",
|
||||
"its_you": "Ets tu!",
|
||||
"unblock": "Desbloqueja",
|
||||
"block_progress": "Bloquejant…",
|
||||
"message": "Missatge",
|
||||
"unsubscribe": "Anul·la la subscripció",
|
||||
"hide_repeats": "Amaga les repeticions",
|
||||
"highlight": {
|
||||
"disabled": "Sense ressaltat",
|
||||
"solid": "Fons sòlid",
|
||||
"striped": "Fons a ratlles",
|
||||
"side": "Ratlla lateral"
|
||||
}
|
||||
},
|
||||
"user_profile": {
|
||||
"timeline_title": "Flux personal"
|
||||
"timeline_title": "Flux personal",
|
||||
"profile_loading_error": "Disculpes, hi ha hagut un error carregant aquest perfil.",
|
||||
"profile_does_not_exist": "Disculpes, aquest perfil no existeix."
|
||||
},
|
||||
"who_to_follow": {
|
||||
"more": "More",
|
||||
"more": "Més",
|
||||
"who_to_follow": "A qui seguir"
|
||||
},
|
||||
"selectable_list": {
|
||||
|
@ -342,10 +663,19 @@
|
|||
},
|
||||
"interactions": {
|
||||
"load_older": "Carrega antigues interaccions",
|
||||
"favs_repeats": "Repeticions i favorits"
|
||||
"favs_repeats": "Repeticions i favorits",
|
||||
"follows": "Nous seguidors"
|
||||
},
|
||||
"emoji": {
|
||||
"stickers": "Adhesius"
|
||||
"stickers": "Adhesius",
|
||||
"keep_open": "Mantindre el selector obert",
|
||||
"custom": "Emojis personalitzats",
|
||||
"unicode": "Emojis unicode",
|
||||
"load_all_hint": "Carregat el primer emoji {saneAmount}, carregar tots els emoji pot causar problemes de rendiment.",
|
||||
"emoji": "Emoji",
|
||||
"search_emoji": "Buscar un emoji",
|
||||
"add_emoji": "Inserir un emoji",
|
||||
"load_all": "Carregant tots els {emojiAmount} emoji"
|
||||
},
|
||||
"polls": {
|
||||
"expired": "L'enquesta va acabar fa {0}",
|
||||
|
@ -357,7 +687,11 @@
|
|||
"votes": "vots",
|
||||
"option": "Opció",
|
||||
"add_option": "Afegeix opció",
|
||||
"add_poll": "Afegeix enquesta"
|
||||
"add_poll": "Afegeix enquesta",
|
||||
"expiry": "Temps de vida de l'enquesta",
|
||||
"people_voted_count": "{count} persona ha votat | {count} persones han votat",
|
||||
"votes_count": "{count} vot | {count} vots",
|
||||
"not_enough_options": "L'enquesta no té suficients opcions úniques"
|
||||
},
|
||||
"media_modal": {
|
||||
"next": "Següent",
|
||||
|
@ -365,7 +699,8 @@
|
|||
},
|
||||
"importer": {
|
||||
"error": "Ha succeït un error mentre s'importava aquest arxiu.",
|
||||
"success": "Importat amb èxit."
|
||||
"success": "Importat amb èxit.",
|
||||
"submit": "Enviar"
|
||||
},
|
||||
"image_cropper": {
|
||||
"cancel": "Cancel·la",
|
||||
|
@ -379,7 +714,9 @@
|
|||
},
|
||||
"domain_mute_card": {
|
||||
"mute_progress": "Silenciant…",
|
||||
"mute": "Silencia"
|
||||
"mute": "Silencia",
|
||||
"unmute": "Deixar de silenciar",
|
||||
"unmute_progress": "Deixant de silenciar…"
|
||||
},
|
||||
"about": {
|
||||
"staff": "Equip responsable",
|
||||
|
@ -391,16 +728,132 @@
|
|||
"reject": "Rebutja",
|
||||
"accept_desc": "Aquesta instància només accepta missatges de les següents instàncies:",
|
||||
"accept": "Accepta",
|
||||
"simple_policies": "Polítiques específiques de la instància"
|
||||
"simple_policies": "Polítiques específiques de la instància",
|
||||
"ftl_removal_desc": "Aquesta instància elimina les següents instàncies del flux de la xarxa coneguda:",
|
||||
"ftl_removal": "Eliminació de la línia de temps coneguda",
|
||||
"media_nsfw_desc": "Aquesta instància obliga el contingut multimèdia a establir-se com a sensible dins de les publicacions en les següents instàncies:",
|
||||
"media_removal": "Eliminació de la multimèdia",
|
||||
"media_removal_desc": "Aquesta instància elimina els suports multimèdia de les publicacions en les següents instàncies:",
|
||||
"media_nsfw": "Forçar contingut multimèdia com a sensible"
|
||||
},
|
||||
"mrf_policies_desc": "Les polítiques MRF controlen el comportament federat de la instància. Les següents polítiques estan habilitades:",
|
||||
"mrf_policies": "Polítiques MRF habilitades",
|
||||
"keyword": {
|
||||
"replace": "Reemplaça",
|
||||
"reject": "Rebutja",
|
||||
"keyword_policies": "Polítiques de paraules clau"
|
||||
"keyword_policies": "Filtratge per paraules clau",
|
||||
"is_replaced_by": "→",
|
||||
"ftl_removal": "Eliminació de la línia de temps federada"
|
||||
},
|
||||
"federation": "Federació"
|
||||
}
|
||||
},
|
||||
"shoutbox": {
|
||||
"title": "Gàbia de Grills"
|
||||
},
|
||||
"status": {
|
||||
"delete": "Esborra l'entrada",
|
||||
"delete_confirm": "Segur que vols esborrar aquesta entrada?",
|
||||
"thread_muted_and_words": ", té les paraules:",
|
||||
"show_full_subject": "Mostra tot el tema",
|
||||
"show_content": "Mostra el contingut",
|
||||
"repeats": "Repeticions",
|
||||
"bookmark": "Marcadors",
|
||||
"status_unavailable": "Entrada no disponible",
|
||||
"expand": "Expandeix",
|
||||
"copy_link": "Copia l'enllaç a l'entrada",
|
||||
"hide_full_subject": "Amaga tot el tema",
|
||||
"favorites": "Favorits",
|
||||
"replies_list": "Contestacions:",
|
||||
"mute_conversation": "Silencia la conversa",
|
||||
"thread_muted": "Fil silenciat",
|
||||
"hide_content": "Amaga el contingut",
|
||||
"status_deleted": "S'ha esborrat aquesta entrada",
|
||||
"nsfw": "No segur per a entorns laborals",
|
||||
"unbookmark": "Desmarca",
|
||||
"external_source": "Font externa",
|
||||
"unpin": "Deixa de destacar al perfil",
|
||||
"pinned": "Destacat",
|
||||
"reply_to": "Contesta a",
|
||||
"pin": "Destaca al perfil",
|
||||
"unmute_conversation": "Deixa de silenciar la conversa"
|
||||
},
|
||||
"user_reporting": {
|
||||
"additional_comments": "Comentaris addicionals",
|
||||
"forward_description": "Aquest compte és d'un altre servidor. Vols enviar una còpia del report allà també?",
|
||||
"forward_to": "Endavant a {0}",
|
||||
"generic_error": "Hi ha hagut un error mentre s'estava processant la teva sol·licitud.",
|
||||
"title": "Reportant {0}",
|
||||
"add_comment_description": "Aquest report serà enviat a la moderació a la instància. Pots donar una explicació de per què estàs reportant aquest compte:",
|
||||
"submit": "Envia"
|
||||
},
|
||||
"tool_tip": {
|
||||
"add_reaction": "Afegeix una Reacció",
|
||||
"accept_follow_request": "Accepta la sol·licitud de seguir",
|
||||
"repeat": "Repeteix",
|
||||
"reply": "Respon",
|
||||
"favorite": "Favorit",
|
||||
"user_settings": "Configuració d'usuària",
|
||||
"reject_follow_request": "Rebutja la sol·licitud de seguir",
|
||||
"bookmark": "Marcador",
|
||||
"media_upload": "Pujar multimèdia"
|
||||
},
|
||||
"search": {
|
||||
"no_results": "No hi ha resultats",
|
||||
"people": "Persones",
|
||||
"hashtags": "Etiquetes",
|
||||
"people_talking": "{count} persones parlant"
|
||||
},
|
||||
"upload": {
|
||||
"file_size_units": {
|
||||
"B": "B",
|
||||
"KiB": "KiB",
|
||||
"GiB": "GiB",
|
||||
"TiB": "TiB",
|
||||
"MiB": "MiB"
|
||||
},
|
||||
"error": {
|
||||
"base": "La pujada ha fallat.",
|
||||
"file_too_big": "Fitxer massa gran [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]",
|
||||
"default": "Prova de nou d'aquí una estona",
|
||||
"message": "La pujada ha fallat: {0}"
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"storage_unavailable": "Pleroma no ha pogut accedir a l'emmagatzematge del navegador. El teu inici de sessió o configuració no es desaran i et pots trobar algun altre problema. Prova a habilitar les galetes."
|
||||
},
|
||||
"password_reset": {
|
||||
"password_reset": "Reinicia la contrasenya",
|
||||
"forgot_password": "Has oblidat la contrasenya?",
|
||||
"too_many_requests": "Has arribat al límit d'intents. Prova de nou d'aquí una estona.",
|
||||
"password_reset_required_but_mailer_is_disabled": "Has de reiniciar la teva contrasenya però el reinici de la contrasenya està deshabilitat. Si us plau, contacta l'administració de la teva instància.",
|
||||
"placeholder": "El teu correu electrònic o nom d'usuària",
|
||||
"instruction": "Introdueix la teva adreça de correu electrònic o nom d'usuària. T'enviarem un enllaç per reiniciar la teva contrasenya.",
|
||||
"return_home": "Torna a la pàgina principal",
|
||||
"password_reset_required": "Has de reiniciar la teva contrasenya per iniciar la sessió.",
|
||||
"password_reset_disabled": "El reinici de la contrasenya està deshabilitat. Si us plau, contacta l'administració de la teva instància.",
|
||||
"check_email": "Comprova que has rebut al correu electrònic un enllaç per reiniciar la teva contrasenya."
|
||||
},
|
||||
"file_type": {
|
||||
"image": "Imatge",
|
||||
"file": "Fitxer",
|
||||
"video": "Vídeo",
|
||||
"audio": "Àudio"
|
||||
},
|
||||
"chats": {
|
||||
"chats": "Xats",
|
||||
"new": "Nou xat",
|
||||
"delete_confirm": "Realment vols esborrar aquest missatge?",
|
||||
"error_sending_message": "Alguna cosa ha fallat quan s'enviava el missatge.",
|
||||
"more": "Més",
|
||||
"delete": "Esborra",
|
||||
"empty_message_error": "No es pot publicar un missatge buit",
|
||||
"you": "Tu:",
|
||||
"message_user": "Missatge {nickname}",
|
||||
"error_loading_chat": "Alguna cosa ha fallat quan es carregava el xat.",
|
||||
"empty_chat_list_placeholder": "Encara no tens cap xat. Crea un nou xat!"
|
||||
},
|
||||
"display_date": {
|
||||
"today": "Avui"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
"scope_options": "Reichweitenoptionen",
|
||||
"text_limit": "Zeichenlimit",
|
||||
"title": "Funktionen",
|
||||
"who_to_follow": "Wem folgen?",
|
||||
"who_to_follow": "Vorschläge",
|
||||
"upload_limit": "Maximale Upload Größe",
|
||||
"pleroma_chat_messages": "Pleroma Chat"
|
||||
},
|
||||
|
@ -39,7 +39,10 @@
|
|||
"close": "Schliessen",
|
||||
"retry": "Versuche es erneut",
|
||||
"error_retry": "Bitte versuche es erneut",
|
||||
"loading": "Lade…"
|
||||
"loading": "Lade…",
|
||||
"flash_content": "Klicken, um den Flash-Inhalt mit Ruffle anzuzeigen (Die Funktion ist experimentell und funktioniert daher möglicherweise nicht).",
|
||||
"flash_security": "Diese Funktion stellt möglicherweise eine Risiko dar, weil Flash-Inhalte weiterhin potentiell gefährlich sind.",
|
||||
"flash_fail": "Falsh-Inhalt konnte nicht geladen werden, Details werden in der Konsole angezeigt."
|
||||
},
|
||||
"login": {
|
||||
"login": "Anmelden",
|
||||
|
@ -538,7 +541,9 @@
|
|||
"reset_background_confirm": "Hintergrund wirklich zurücksetzen?",
|
||||
"reset_banner_confirm": "Banner wirklich zurücksetzen?",
|
||||
"reset_avatar_confirm": "Avatar wirklich zurücksetzen?",
|
||||
"reset_profile_banner": "Profilbanner zurücksetzen"
|
||||
"reset_profile_banner": "Profilbanner zurücksetzen",
|
||||
"hide_shoutbox": "Shoutbox der Instanz verbergen",
|
||||
"right_sidebar": "Seitenleiste rechts anzeigen"
|
||||
},
|
||||
"timeline": {
|
||||
"collapse": "Einklappen",
|
||||
|
@ -779,7 +784,7 @@
|
|||
"error_sending_message": "Beim Senden der Nachricht ist ein Fehler aufgetreten.",
|
||||
"error_loading_chat": "Beim Laden des Chats ist ein Fehler aufgetreten.",
|
||||
"delete_confirm": "Soll diese Nachricht wirklich gelöscht werden?",
|
||||
"empty_message_error": "Die Nachricht darf nicht leer sein.",
|
||||
"empty_message_error": "Die Nachricht darf nicht leer sein",
|
||||
"delete": "Löschen",
|
||||
"message_user": "Nachricht an {nickname} senden",
|
||||
"empty_chat_list_placeholder": "Es sind noch keine Chats vorhanden. Jetzt einen Chat starten!",
|
||||
|
|
|
@ -259,6 +259,8 @@
|
|||
"security": "Security",
|
||||
"setting_changed": "Setting is different from default",
|
||||
"enter_current_password_to_confirm": "Enter your current password to confirm your identity",
|
||||
"mentions_new_style": "Fancier mention links",
|
||||
"mentions_new_place": "Put mentions on a separate line",
|
||||
"mfa": {
|
||||
"otp": "OTP",
|
||||
"setup_otp": "Setup OTP",
|
||||
|
@ -350,6 +352,7 @@
|
|||
"hide_isp": "Hide instance-specific panel",
|
||||
"hide_shoutbox": "Hide instance shoutbox",
|
||||
"right_sidebar": "Show sidebar on the right side",
|
||||
"always_show_post_button": "Always show floating New Post button",
|
||||
"hide_wallpaper": "Hide instance wallpaper",
|
||||
"preload_images": "Preload images",
|
||||
"use_one_click_nsfw": "Open NSFW attachments with just one click",
|
||||
|
@ -698,6 +701,7 @@
|
|||
"unbookmark": "Unbookmark",
|
||||
"delete_confirm": "Do you really want to delete this status?",
|
||||
"reply_to": "Reply to",
|
||||
"mentions": "Mentions",
|
||||
"replies_list": "Replies:",
|
||||
"mute_conversation": "Mute conversation",
|
||||
"unmute_conversation": "Unmute conversation",
|
||||
|
@ -712,7 +716,9 @@
|
|||
"hide_content": "Hide content",
|
||||
"status_deleted": "This post was deleted",
|
||||
"nsfw": "NSFW",
|
||||
"expand": "Expand"
|
||||
"expand": "Expand",
|
||||
"you": "(You)",
|
||||
"plus_more": "+{number} more"
|
||||
},
|
||||
"user_card": {
|
||||
"approve": "Approve",
|
||||
|
|
|
@ -39,7 +39,10 @@
|
|||
"role": {
|
||||
"moderator": "Reguligisto",
|
||||
"admin": "Administranto"
|
||||
}
|
||||
},
|
||||
"flash_content": "Klaku por montri enhavon de Flash per Ruffle. (Eksperimente, eble ne funkcios.)",
|
||||
"flash_security": "Sciu, ke tio povas esti danĝera, ĉar la enhavo de Flash ja estas arbitra programo.",
|
||||
"flash_fail": "Malsukcesis enlegi enhavon de Flash; vidu detalojn en konzolo."
|
||||
},
|
||||
"image_cropper": {
|
||||
"crop_picture": "Tondi bildon",
|
||||
|
@ -87,7 +90,8 @@
|
|||
"interactions": "Interagoj",
|
||||
"administration": "Administrado",
|
||||
"bookmarks": "Legosignoj",
|
||||
"timelines": "Historioj"
|
||||
"timelines": "Historioj",
|
||||
"home_timeline": "Hejma historio"
|
||||
},
|
||||
"notifications": {
|
||||
"broken_favorite": "Nekonata stato, serĉante ĝin…",
|
||||
|
@ -119,10 +123,10 @@
|
|||
"direct_warning": "Ĉi tiu afiŝo estos videbla nur por ĉiuj menciitaj uzantoj.",
|
||||
"posting": "Afiŝante",
|
||||
"scope": {
|
||||
"direct": "Rekta – Afiŝi nur al menciitaj uzantoj",
|
||||
"private": "Nur abonantoj – Afiŝi nur al abonantoj",
|
||||
"public": "Publika – Afiŝi al publikaj historioj",
|
||||
"unlisted": "Nelistigita – Ne afiŝi al publikaj historioj"
|
||||
"direct": "Rekta – afiŝi nur al menciitaj uzantoj",
|
||||
"private": "Nur abonantoj – afiŝi nur al abonantoj",
|
||||
"public": "Publika – afiŝi al publikaj historioj",
|
||||
"unlisted": "Nelistigita – ne afiŝi al publikaj historioj"
|
||||
},
|
||||
"scope_notice": {
|
||||
"unlisted": "Ĉi tiu afiŝo ne estos videbla en la Publika historio kaj La tuta konata reto",
|
||||
|
@ -135,7 +139,8 @@
|
|||
"preview": "Antaŭrigardo",
|
||||
"direct_warning_to_first_only": "Ĉi tiu afiŝo estas nur videbla al uzantoj menciitaj je la komenco de la mesaĝo.",
|
||||
"direct_warning_to_all": "Ĉi tiu afiŝo estos videbla al ĉiuj menciitaj uzantoj.",
|
||||
"media_description": "Priskribo de vidaŭdaĵo"
|
||||
"media_description": "Priskribo de vidaŭdaĵo",
|
||||
"post": "Afiŝo"
|
||||
},
|
||||
"registration": {
|
||||
"bio": "Priskribo",
|
||||
|
@ -143,7 +148,7 @@
|
|||
"fullname": "Prezenta nomo",
|
||||
"password_confirm": "Konfirmo de pasvorto",
|
||||
"registration": "Registriĝo",
|
||||
"token": "Invita ĵetono",
|
||||
"token": "Invita peco",
|
||||
"captcha": "TESTO DE HOMECO",
|
||||
"new_captcha": "Klaku la bildon por akiri novan teston",
|
||||
"username_placeholder": "ekz. lain",
|
||||
|
@ -158,7 +163,8 @@
|
|||
"password_confirmation_match": "samu la pasvorton"
|
||||
},
|
||||
"reason_placeholder": "Ĉi-node oni aprobas registriĝojn permane.\nSciigu la administrantojn kial vi volas registriĝi.",
|
||||
"reason": "Kialo registriĝi"
|
||||
"reason": "Kialo registriĝi",
|
||||
"register": "Registriĝi"
|
||||
},
|
||||
"settings": {
|
||||
"app_name": "Nomo de aplikaĵo",
|
||||
|
@ -244,9 +250,9 @@
|
|||
"show_admin_badge": "Montri la insignon de administranto en mia profilo",
|
||||
"show_moderator_badge": "Montri la insignon de reguligisto en mia profilo",
|
||||
"nsfw_clickthrough": "Ŝalti traklakan kaŝadon de kunsendaĵoj kaj antaŭmontroj de ligiloj por konsternaj statoj",
|
||||
"oauth_tokens": "Ĵetonoj de OAuth",
|
||||
"token": "Ĵetono",
|
||||
"refresh_token": "Ĵetono de aktualigo",
|
||||
"oauth_tokens": "Pecoj de OAuth",
|
||||
"token": "Peco",
|
||||
"refresh_token": "Aktualiga peco",
|
||||
"valid_until": "Valida ĝis",
|
||||
"revoke_token": "Senvalidigi",
|
||||
"panelRadius": "Bretoj",
|
||||
|
@ -532,7 +538,22 @@
|
|||
"hide_all_muted_posts": "Kaŝi silentigitajn afiŝojn",
|
||||
"hide_media_previews": "Kaŝi antaŭrigardojn al vidaŭdaĵoj",
|
||||
"word_filter": "Vortofiltro",
|
||||
"reply_visibility_self_short": "Montri nur respondojn por mi"
|
||||
"reply_visibility_self_short": "Montri nur respondojn por mi",
|
||||
"file_export_import": {
|
||||
"errors": {
|
||||
"file_slightly_new": "Etversio de dosiero malsamas, iuj agordoj eble ne funkcios",
|
||||
"file_too_old": "Nekonforma ĉefa versio: {fileMajor}, versio de dosiero estas tro malnova kaj nesubtenata (minimuma estas {feMajor})",
|
||||
"file_too_new": "Nekonforma ĉefa versio: {fileMajor}, ĉi tiu PleromaFE (agordoj je versio {feMajor}) tro malnovas por tio",
|
||||
"invalid_file": "La elektita dosiero ne estas subtenata savkopio de agordoj de Pleroma. Nenio ŝanĝiĝis."
|
||||
},
|
||||
"restore_settings": "Rehavi agordojn el dosiero",
|
||||
"backup_settings_theme": "Savkopii agordojn kaj haŭton al dosiero",
|
||||
"backup_settings": "Savkopii agordojn al dosiero",
|
||||
"backup_restore": "Savkopio de agordoj"
|
||||
},
|
||||
"right_sidebar": "Montri flankan breton dekstre",
|
||||
"save": "Konservi ŝanĝojn",
|
||||
"hide_shoutbox": "Kaŝi kriujon de nodo"
|
||||
},
|
||||
"timeline": {
|
||||
"collapse": "Maletendi",
|
||||
|
@ -546,7 +567,9 @@
|
|||
"no_more_statuses": "Neniuj pliaj statoj",
|
||||
"no_statuses": "Neniuj statoj",
|
||||
"reload": "Enlegi ree",
|
||||
"error": "Eraris akirado de historio: {0}"
|
||||
"error": "Eraris akirado de historio: {0}",
|
||||
"socket_reconnected": "Realtempa konekto fariĝis",
|
||||
"socket_broke": "Realtempa konekto perdiĝis: CloseEvent code {0}"
|
||||
},
|
||||
"user_card": {
|
||||
"approve": "Aprobi",
|
||||
|
@ -696,7 +719,7 @@
|
|||
"media_nsfw": "Devige marki vidaŭdaĵojn konsternaj",
|
||||
"media_removal_desc": "Ĉi tiu nodo forigas vidaŭdaĵojn de afiŝoj el la jenaj nodoj:",
|
||||
"media_removal": "Forigo de vidaŭdaĵoj",
|
||||
"ftl_removal": "Forigo el la historio de «La tuta konata reto»",
|
||||
"ftl_removal": "Forigo el la historio de «Konata reto»",
|
||||
"quarantine_desc": "Ĉi tiu nodo sendos nur publikajn afiŝojn al la jenaj nodoj:",
|
||||
"quarantine": "Kvaranteno",
|
||||
"reject_desc": "Ĉi tiu nodo ne akceptos mesaĝojn de la jenaj nodoj:",
|
||||
|
@ -704,7 +727,7 @@
|
|||
"accept_desc": "Ĉi tiu nodo nur akceptas mesaĝojn de la jenaj nodoj:",
|
||||
"accept": "Akcepti",
|
||||
"simple_policies": "Specialaj politikoj de la nodo",
|
||||
"ftl_removal_desc": "Ĉi tiu nodo forigas la jenajn nodojn el la historio de «La tuta konata reto»:"
|
||||
"ftl_removal_desc": "Ĉi tiu nodo forigas la jenajn nodojn el la historio de «Konata reto»:"
|
||||
},
|
||||
"mrf_policies": "Ŝaltis politikon de Mesaĝa ŝanĝilaro (MRF)",
|
||||
"keyword": {
|
||||
|
|
|
@ -43,7 +43,10 @@
|
|||
"role": {
|
||||
"admin": "Administrador/a",
|
||||
"moderator": "Moderador/a"
|
||||
}
|
||||
},
|
||||
"flash_content": "Haga clic para mostrar contenido Flash usando Ruffle (experimental, puede que no funcione).",
|
||||
"flash_security": "Tenga en cuenta que esto puede ser potencialmente peligroso ya que el contenido Flash sigue siendo código arbitrario.",
|
||||
"flash_fail": "No se pudo cargar el contenido flash, consulte la consola para obtener más detalles."
|
||||
},
|
||||
"image_cropper": {
|
||||
"crop_picture": "Recortar la foto",
|
||||
|
@ -147,7 +150,7 @@
|
|||
"favs_repeats": "Favoritos y repetidos",
|
||||
"follows": "Nuevos seguidores",
|
||||
"load_older": "Cargar interacciones más antiguas",
|
||||
"moves": "Usuario Migrado"
|
||||
"moves": "Usuario migrado"
|
||||
},
|
||||
"post_status": {
|
||||
"new_status": "Publicar un nuevo estado",
|
||||
|
@ -181,7 +184,7 @@
|
|||
"preview_empty": "Vacío",
|
||||
"preview": "Vista previa",
|
||||
"media_description": "Descripción multimedia",
|
||||
"post": "Publicación"
|
||||
"post": "Publicar"
|
||||
},
|
||||
"registration": {
|
||||
"bio": "Biografía",
|
||||
|
@ -585,13 +588,18 @@
|
|||
"save": "Guardar los cambios",
|
||||
"file_export_import": {
|
||||
"errors": {
|
||||
"invalid_file": "El archivo seleccionado no es válido como copia de seguridad de Pleroma. No se han realizado cambios."
|
||||
"invalid_file": "El archivo seleccionado no es válido como copia de seguridad de Pleroma. No se han realizado cambios.",
|
||||
"file_too_new": "Versión principal incompatible: {fileMajor}, este \"FrontEnd\" de Pleroma (versión de configuración {feMajor}) es demasiado antiguo para manejarlo",
|
||||
"file_too_old": "Versión principal incompatible: {fileMajor}, la versión del archivo es demasiado antigua y no es compatible (versión mínima {FeMajor})",
|
||||
"file_slightly_new": "La versión secundaria del archivo es diferente, es posible que algunas configuraciones no se carguen"
|
||||
},
|
||||
"restore_settings": "Restaurar ajustes desde archivo",
|
||||
"backup_settings_theme": "Copia de seguridad de la configuración y tema a archivo",
|
||||
"backup_settings": "Copia de seguridad de la configuración a archivo",
|
||||
"backup_settings_theme": "Descargar la copia de seguridad de la configuración y del tema",
|
||||
"backup_settings": "Descargar la copia de seguridad de la configuración",
|
||||
"backup_restore": "Copia de seguridad de la configuración"
|
||||
}
|
||||
},
|
||||
"hide_shoutbox": "Ocultar cuadro de diálogo de la instancia",
|
||||
"right_sidebar": "Mostrar la barra lateral a la derecha"
|
||||
},
|
||||
"time": {
|
||||
"day": "{0} día",
|
||||
|
@ -735,7 +743,8 @@
|
|||
"solid": "Fondo sólido",
|
||||
"disabled": "Sin resaltado"
|
||||
},
|
||||
"bot": "Bot"
|
||||
"bot": "Bot",
|
||||
"edit_profile": "Edita el perfil"
|
||||
},
|
||||
"user_profile": {
|
||||
"timeline_title": "Línea temporal del usuario",
|
||||
|
|
|
@ -43,7 +43,10 @@
|
|||
"role": {
|
||||
"moderator": "Moderatzailea",
|
||||
"admin": "Administratzailea"
|
||||
}
|
||||
},
|
||||
"flash_content": "Klik egin Flash edukia erakusteko Ruffle erabilita (esperimentala, baliteke ez ibiltzea).",
|
||||
"flash_security": "Kontuan izan arriskutsua izan daitekeela, Flash edukia kode arbitrarioa baita.",
|
||||
"flash_fail": "Ezin izan da Flash edukia kargatu. Ikusi kontsola xehetasunetarako."
|
||||
},
|
||||
"image_cropper": {
|
||||
"crop_picture": "Moztu argazkia",
|
||||
|
@ -96,7 +99,8 @@
|
|||
"preferences": "Hobespenak",
|
||||
"chats": "Txatak",
|
||||
"timelines": "Denbora-lerroak",
|
||||
"bookmarks": "Laster-markak"
|
||||
"bookmarks": "Laster-markak",
|
||||
"home_timeline": "Denbora-lerro pertsonala"
|
||||
},
|
||||
"notifications": {
|
||||
"broken_favorite": "Egoera ezezaguna, bilatzen…",
|
||||
|
@ -136,7 +140,8 @@
|
|||
"add_emoji": "Emoji bat gehitu",
|
||||
"custom": "Ohiko emojiak",
|
||||
"unicode": "Unicode emojiak",
|
||||
"load_all": "{emojiAmount} emoji guztiak kargatzen"
|
||||
"load_all": "{emojiAmount} emoji guztiak kargatzen",
|
||||
"load_all_hint": "Lehenengo {saneAmount} emojia kargatuta, emoji guztiak kargatzeak errendimendu arazoak sor ditzake."
|
||||
},
|
||||
"stickers": {
|
||||
"add_sticker": "Pegatina gehitu"
|
||||
|
@ -144,7 +149,8 @@
|
|||
"interactions": {
|
||||
"favs_repeats": "Errepikapen eta gogokoak",
|
||||
"follows": "Jarraitzaile berriak",
|
||||
"load_older": "Kargatu elkarrekintza zaharragoak"
|
||||
"load_older": "Kargatu elkarrekintza zaharragoak",
|
||||
"moves": "Erabiltzailea migratuta"
|
||||
},
|
||||
"post_status": {
|
||||
"new_status": "Mezu berri bat idatzi",
|
||||
|
@ -172,14 +178,20 @@
|
|||
"private": "Jarraitzaileentzako bakarrik: bidali jarraitzaileentzat bakarrik",
|
||||
"public": "Publikoa: bistaratu denbora-lerro publikoetan",
|
||||
"unlisted": "Zerrendatu gabea: ez bidali denbora-lerro publikoetara"
|
||||
}
|
||||
},
|
||||
"media_description_error": "Ezin izan da artxiboa eguneratu, saiatu berriro",
|
||||
"preview": "Aurrebista",
|
||||
"media_description": "Media deskribapena",
|
||||
"preview_empty": "Hutsik",
|
||||
"post": "Bidali",
|
||||
"empty_status_error": "Ezin da argitaratu ezer idatzi gabe edo eranskinik gabe"
|
||||
},
|
||||
"registration": {
|
||||
"bio": "Biografia",
|
||||
"email": "E-posta",
|
||||
"fullname": "Erakutsi izena",
|
||||
"password_confirm": "Pasahitza berretsi",
|
||||
"registration": "Izena ematea",
|
||||
"registration": "Sortu kontua",
|
||||
"token": "Gonbidapen txartela",
|
||||
"captcha": "CAPTCHA",
|
||||
"new_captcha": "Klikatu irudia captcha berri bat lortzeko",
|
||||
|
@ -193,7 +205,10 @@
|
|||
"password_required": "Ezin da hutsik utzi",
|
||||
"password_confirmation_required": "Ezin da hutsik utzi",
|
||||
"password_confirmation_match": "Pasahitzaren berdina izan behar du"
|
||||
}
|
||||
},
|
||||
"reason": "Kontua sortzeko arrazoia",
|
||||
"reason_placeholder": "Instantzia honek kontu berriak eskuz onartzen ditu.\nJakinarazi administrazioari zergatik erregistratu nahi duzun.",
|
||||
"register": "Erregistratu"
|
||||
},
|
||||
"selectable_list": {
|
||||
"select_all": "Hautatu denak"
|
||||
|
@ -210,7 +225,7 @@
|
|||
"title": "Bi-faktore autentifikazioa",
|
||||
"generate_new_recovery_codes": "Sortu berreskuratze kode berriak",
|
||||
"warning_of_generate_new_codes": "Berreskuratze kode berriak sortzean, zure berreskuratze kode zaharrak ez dute balioko.",
|
||||
"recovery_codes": "Berreskuratze kodea",
|
||||
"recovery_codes": "Berreskuratze kodea.",
|
||||
"waiting_a_recovery_codes": "Babes-kopia kodeak jasotzen…",
|
||||
"recovery_codes_warning": "Idatzi edo gorde kodeak leku seguruan - bestela ez dituzu berriro ikusiko. Zure 2FA aplikaziorako sarbidea eta berreskuratze kodeak galduz gero, zure kontutik blokeatuta egongo zara.",
|
||||
"authentication_methods": "Autentifikazio metodoa",
|
||||
|
@ -468,7 +483,7 @@
|
|||
"button": "Botoia",
|
||||
"text": "Hamaika {0} eta {1}",
|
||||
"mono": "edukia",
|
||||
"input": "Jadanik Los Angeles-en",
|
||||
"input": "Jadanik Los Angeles-en.",
|
||||
"faint_link": "laguntza",
|
||||
"fine_print": "Irakurri gure {0} ezer erabilgarria ikasteko!",
|
||||
"header_faint": "Ondo dago",
|
||||
|
@ -480,7 +495,11 @@
|
|||
"title": "Bertsioa",
|
||||
"backend_version": "Backend bertsioa",
|
||||
"frontend_version": "Frontend bertsioa"
|
||||
}
|
||||
},
|
||||
"save": "Aldaketak gorde",
|
||||
"setting_changed": "Ezarpena lehenetsitakoaren desberdina da",
|
||||
"allow_following_move": "Baimendu jarraipen automatikoa, jarraitzen duzun kontua beste instantzia batera eramaten denean",
|
||||
"new_email": "E-posta berria"
|
||||
},
|
||||
"time": {
|
||||
"day": "{0} egun",
|
||||
|
@ -691,5 +710,12 @@
|
|||
},
|
||||
"shoutbox": {
|
||||
"title": "Oihu-kutxa"
|
||||
},
|
||||
"errors": {
|
||||
"storage_unavailable": "Pleromak ezin izan du nabigatzailearen biltegira sartu. Hasiera-saioa edo tokiko ezarpenak ez dira gordeko eta ustekabeko arazoak sor ditzake. Saiatu cookie-ak gaitzen."
|
||||
},
|
||||
"remote_user_resolver": {
|
||||
"searching_for": "Bilatzen",
|
||||
"error": "Ez da aurkitu."
|
||||
}
|
||||
}
|
||||
|
|
|
@ -579,7 +579,8 @@
|
|||
"hide_full_subject": "Piilota koko otsikko",
|
||||
"show_content": "Näytä sisältö",
|
||||
"hide_content": "Piilota sisältö",
|
||||
"status_deleted": "Poistettu viesti"
|
||||
"status_deleted": "Poistettu viesti",
|
||||
"you": "(sinä)"
|
||||
},
|
||||
"user_card": {
|
||||
"approve": "Hyväksy",
|
||||
|
|
|
@ -43,7 +43,10 @@
|
|||
"role": {
|
||||
"moderator": "Modo'",
|
||||
"admin": "Admin"
|
||||
}
|
||||
},
|
||||
"flash_content": "Clique pour afficher le contenu Flash avec Ruffle (Expérimental, peut ne pas fonctionner).",
|
||||
"flash_security": "Cela reste potentiellement dangereux, Flash restant du code arbitraire.",
|
||||
"flash_fail": "Échec de chargement du contenu Flash, voir la console pour les détails."
|
||||
},
|
||||
"image_cropper": {
|
||||
"crop_picture": "Rogner l'image",
|
||||
|
@ -282,7 +285,7 @@
|
|||
"new_password": "Nouveau mot de passe",
|
||||
"notification_visibility": "Types de notifications à afficher",
|
||||
"notification_visibility_follows": "Suivis",
|
||||
"notification_visibility_likes": "J'aime",
|
||||
"notification_visibility_likes": "Favoris",
|
||||
"notification_visibility_mentions": "Mentionnés",
|
||||
"notification_visibility_repeats": "Partages",
|
||||
"no_rich_text_description": "Ne formatez pas le texte",
|
||||
|
@ -553,7 +556,21 @@
|
|||
"hide_wallpaper": "Cacher le fond d'écran",
|
||||
"hide_all_muted_posts": "Cacher les messages masqués",
|
||||
"word_filter": "Filtrage par mots",
|
||||
"save": "Enregistrer les changements"
|
||||
"save": "Enregistrer les changements",
|
||||
"file_export_import": {
|
||||
"backup_settings_theme": "Sauvegarder les paramètres et le thème dans un fichier",
|
||||
"errors": {
|
||||
"invalid_file": "Le fichier sélectionné n'est pas un format supporté pour les sauvegarde Pleroma. Aucun changement n'a été fait.",
|
||||
"file_too_new": "Version majeure incompatible. {fileMajor}, ce PleromaFE ({feMajor}) est trop ancien",
|
||||
"file_too_old": "Version majeure incompatible : {fileMajor}, la version du fichier est trop vielle et n'est plus supportée (vers. min. {feMajor})",
|
||||
"file_slightly_new": "La version mineure du fichier est différente, quelques paramètres on pût ne pas chargés"
|
||||
},
|
||||
"backup_restore": "Sauvegarde des Paramètres",
|
||||
"backup_settings": "Sauvegarder les paramètres dans un fichier",
|
||||
"restore_settings": "Restaurer les paramètres depuis un fichier"
|
||||
},
|
||||
"hide_shoutbox": "Cacher la shoutbox de l'instance",
|
||||
"right_sidebar": "Afficher le paneau latéral à droite"
|
||||
},
|
||||
"timeline": {
|
||||
"collapse": "Fermer",
|
||||
|
@ -663,7 +680,8 @@
|
|||
"side": "Coté rayé",
|
||||
"striped": "Fond rayé"
|
||||
},
|
||||
"bot": "Robot"
|
||||
"bot": "Robot",
|
||||
"edit_profile": "Éditer le profil"
|
||||
},
|
||||
"user_profile": {
|
||||
"timeline_title": "Flux du compte",
|
||||
|
|
622
src/i18n/id.json
Normal file
622
src/i18n/id.json
Normal file
|
@ -0,0 +1,622 @@
|
|||
{
|
||||
"settings": {
|
||||
"style": {
|
||||
"preview": {
|
||||
"link": "sebuah tautan yang kecil nan bagus",
|
||||
"header": "Pratinjau",
|
||||
"error": "Contoh kesalahan",
|
||||
"button": "Tombol",
|
||||
"input": "Baru saja mendarat di L.A.",
|
||||
"faint_link": "manual berguna",
|
||||
"fine_print": "Baca {0} kami untuk belajar sesuatu yang tak ada gunanya!",
|
||||
"header_faint": "Ini baik-baik saja",
|
||||
"checkbox": "Saya telah membaca sekilas syarat dan ketentuan"
|
||||
},
|
||||
"advanced_colors": {
|
||||
"alert_neutral": "Neutral",
|
||||
"alert_warning": "Peringatan",
|
||||
"alert_error": "Kesalahan",
|
||||
"_tab_label": "Lanjutan",
|
||||
"post": "Postingan/Bio pengguna",
|
||||
"popover": "Tooltip, menu, popover",
|
||||
"badge_notification": "Notifikasi",
|
||||
"top_bar": "Bar atas",
|
||||
"borders": "",
|
||||
"buttons": "Tombol",
|
||||
"wallpaper": "Latar belakang",
|
||||
"panel_header": "Header panel",
|
||||
"icons": "Ikon-ikon",
|
||||
"disabled": "Dinonaktifkan"
|
||||
},
|
||||
"common_colors": {
|
||||
"main": "Warna umum",
|
||||
"_tab_label": "Umum"
|
||||
},
|
||||
"common": {
|
||||
"contrast": {
|
||||
"context": {
|
||||
"text": "untuk teks",
|
||||
"18pt": "Untuk teks besar (18pt+)"
|
||||
}
|
||||
},
|
||||
"color": "Warna"
|
||||
},
|
||||
"switcher": {
|
||||
"help": {
|
||||
"upgraded_from_v2": "PleromaFE telah diperbarui, tema dapat terlihat sedikit berbeda dari apa yang Anda ingat.",
|
||||
"future_version_imported": "Berkas yang Anda impor dibuat pada versi FE yang lebih baru.",
|
||||
"older_version_imported": "Berkas yang Anda impor dibuat pada versi FE yang lebih lama.",
|
||||
"fe_upgraded": "Mesin tema PleromaFE diperbarui setelah pembaruan versi."
|
||||
},
|
||||
"use_source": "Versi baru",
|
||||
"use_snapshot": "Versi lama",
|
||||
"load_theme": "Muat tema"
|
||||
},
|
||||
"fonts": {
|
||||
"_tab_label": "Font",
|
||||
"components": {
|
||||
"interface": "Antarmuka",
|
||||
"post": "Teks postingan"
|
||||
},
|
||||
"family": "Nama font",
|
||||
"size": "Ukuran (dalam px)",
|
||||
"weight": "Berat (ketebalan)"
|
||||
},
|
||||
"shadows": {
|
||||
"components": {
|
||||
"panel": "Panel",
|
||||
"panelHeader": "Header panel"
|
||||
}
|
||||
}
|
||||
},
|
||||
"notification_setting_privacy": "Privasi",
|
||||
"notifications": "Notifikasi",
|
||||
"values": {
|
||||
"true": "ya",
|
||||
"false": "tidak"
|
||||
},
|
||||
"user_settings": "Pengaturan Pengguna",
|
||||
"upload_a_photo": "Unggah foto",
|
||||
"theme": "Tema",
|
||||
"text": "Teks",
|
||||
"settings": "Pengaturan",
|
||||
"security_tab": "Keamanan",
|
||||
"saving_ok": "Pengaturan disimpan",
|
||||
"profile_tab": "Profil",
|
||||
"profile_background": "Latar belakang profil",
|
||||
"token": "Token",
|
||||
"oauth_tokens": "Token OAuth",
|
||||
"show_moderator_badge": "Tampilkan lencana \"Moderator\" di profil saya",
|
||||
"show_admin_badge": "Tampilkan lencana \"Admin\" di profil saya",
|
||||
"new_password": "Kata sandi baru",
|
||||
"new_email": "Surel baru",
|
||||
"name_bio": "Nama & bio",
|
||||
"name": "Nama",
|
||||
"profile_fields": {
|
||||
"value": "Isi",
|
||||
"name": "Label",
|
||||
"label": "Metadata profil"
|
||||
},
|
||||
"limited_availability": "Tidak tersedia di browser Anda",
|
||||
"invalid_theme_imported": "Berkas yang dipilih bukan sebuah tema yang didukung Pleroma. Tidak ada perbuahan yang dibuat pada tema Anda.",
|
||||
"interfaceLanguage": "Bahasa antarmuka",
|
||||
"interface": "Antarmuka",
|
||||
"instance_default_simple": "(bawaan)",
|
||||
"instance_default": "(bawaan: {value})",
|
||||
"general": "Umum",
|
||||
"delete_account_error": "Ada masalah ketika menghapus akun Anda. Jika ini terus terjadi harap hubungi adminstrator instansi Anda.",
|
||||
"delete_account_description": "Hapus data Anda secara permanen dan menonaktifkan akun Anda.",
|
||||
"delete_account": "Hapus akun",
|
||||
"data_import_export_tab": "Impor / ekspor data",
|
||||
"current_password": "Kata sandi saat ini",
|
||||
"confirm_new_password": "Konfirmasi kata sandi baru",
|
||||
"version": {
|
||||
"title": "Versi",
|
||||
"backend_version": "Versi backend",
|
||||
"frontend_version": "Versi frontend"
|
||||
},
|
||||
"security": "Keamanan",
|
||||
"changed_password": "Kata sandi berhasil diubah!",
|
||||
"change_password_error": "Ada masalah ketika mengubah kata sandi Anda.",
|
||||
"change_password": "Ubah kata sandi",
|
||||
"changed_email": "Surel berhasil diubah!",
|
||||
"change_email_error": "Ada masalah ketika mengubah surel Anda.",
|
||||
"change_email": "Ubah surel",
|
||||
"cRed": "Merah (Batal)",
|
||||
"cBlue": "Biru (Balas, ikuti)",
|
||||
"btnRadius": "Tombol",
|
||||
"bot": "Ini adalah akun bot",
|
||||
"block_export": "Ekspor blokiran",
|
||||
"bio": "Bio",
|
||||
"background": "Latar belakang",
|
||||
"avatarRadius": "Avatar",
|
||||
"avatar": "Avatar",
|
||||
"attachments": "Lampiran",
|
||||
"mfa": {
|
||||
"scan": {
|
||||
"title": "Pindai"
|
||||
},
|
||||
"confirm_and_enable": "Konfirmasi & aktifkan OTP",
|
||||
"setup_otp": "Siapkan OTP",
|
||||
"otp": "OTP",
|
||||
"recovery_codes_warning": "Tulis kode-kode nya atau simpan mereka di tempat yang aman - jika tidak Anda tidak akan melihat mereka lagi. Jika Anda tidak dapat mengakses aplikasi 2FA Anda dan kode pemulihan Anda hilang Anda tidak akan bisa mengakses akun Anda.",
|
||||
"authentication_methods": "Metode otentikasi",
|
||||
"recovery_codes": "Kode pemulihan.",
|
||||
"warning_of_generate_new_codes": "Ketika Anda menghasilkan kode pemulihan baru, kode lama Anda berhenti bekerja.",
|
||||
"generate_new_recovery_codes": "Hasilkan kode pemulihan baru",
|
||||
"title": "Otentikasi Dua-faktor",
|
||||
"waiting_a_recovery_codes": "Menerima kode cadangan…",
|
||||
"verify": {
|
||||
"desc": "Untuk mengaktifkan otentikasi dua-faktor, masukkan kode dari aplikasi dua-faktor Anda:"
|
||||
}
|
||||
},
|
||||
"app_name": "Nama aplikasi",
|
||||
"save": "Simpan perubahan",
|
||||
"valid_until": "Valid hingga",
|
||||
"follow_import_error": "Terjadi kesalahan ketika mengimpor pengikut",
|
||||
"emoji_reactions_on_timeline": "Tampilkan reaksi emoji pada linimasa",
|
||||
"chatMessageRadius": "Pesan obrolan",
|
||||
"cOrange": "Jingga (Favorit)",
|
||||
"avatarAltRadius": "Avatar (notifikasi)",
|
||||
"hide_shoutbox": "Sembunyikan kotak suara instansi",
|
||||
"hide_followers_count_description": "Jangan tampilkan jumlah pengikut",
|
||||
"hide_follows_count_description": "Jangan tampilkan jumlah mengikuti",
|
||||
"hide_followers_description": "Jangan tampilkan siapa yang mengikuti saya",
|
||||
"hide_follows_description": "Jangan tampilkan siapa yang saya ikuti",
|
||||
"notification_visibility_emoji_reactions": "Reaksi",
|
||||
"notification_visibility_follows": "Diikuti",
|
||||
"notification_visibility_moves": "Pengguna Bermigrasi",
|
||||
"notification_visibility_repeats": "Ulangan",
|
||||
"notification_visibility_mentions": "Sebutan",
|
||||
"notification_visibility_likes": "Favorit",
|
||||
"notification_visibility": "Jenis notifikasi yang perlu ditampilkan",
|
||||
"links": "Tautan",
|
||||
"hide_user_stats": "Sembunyikan statistik pengguna (contoh. jumlah pengikut)",
|
||||
"hide_post_stats": "Sembunyikan statistik postingan (contoh. jumlah favorit)",
|
||||
"use_one_click_nsfw": "Buka lampiran NSFW hanya dengan satu klik",
|
||||
"hide_wallpaper": "Sembunyikan latar belakang instansi",
|
||||
"blocks_imported": "Blokiran diimpor! Pemrosesannya mungkin memakan sedikit waktu.",
|
||||
"block_import_error": "Terjadi kesalahan ketika mengimpor blokiran",
|
||||
"block_import": "Impor blokiran",
|
||||
"block_export_button": "Ekspor blokiran Anda menjadi berkas csv",
|
||||
"blocks_tab": "Blokiran",
|
||||
"delete_account_instructions": "Ketik kata sandi Anda pada input di bawah untuk mengkonfirmasi penghapusan akun.",
|
||||
"mutes_and_blocks": "Bisuan dan Blokiran",
|
||||
"enter_current_password_to_confirm": "Masukkan kata sandi Anda saat ini untuk mengkonfirmasi identitas Anda",
|
||||
"filtering": "Penyaringan",
|
||||
"word_filter": "Penyaring kata",
|
||||
"avatar_size_instruction": "Ukuran minimum gambar avatar yang disarankan adalah 150x150 piksel.",
|
||||
"attachmentRadius": "Lampiran",
|
||||
"cGreen": "Hijau (Retweet)",
|
||||
"max_thumbnails": "Jumlah thumbnail maksimum per postingan",
|
||||
"loop_video": "Ulang-ulang video",
|
||||
"loop_video_silent_only": "Ulang-ulang video tanpa suara (seperti \"gif\" Mastodon)",
|
||||
"pause_on_unfocused": "Jeda aliran ketika tab di dalam fokus",
|
||||
"reply_visibility_following": "Hanya tampilkan balasan yang ditujukan kepada saya atau orang yang saya ikuti",
|
||||
"reply_visibility_following_short": "Tampilkan balasan ke orang yang saya ikuti",
|
||||
"saving_err": "Terjadi kesalahan ketika menyimpan pengaturan",
|
||||
"search_user_to_block": "Cari siapa yang Anda ingin blokir",
|
||||
"search_user_to_mute": "Cari siapa yang ingin Anda bisukan",
|
||||
"set_new_avatar": "Tetapkan avatar baru",
|
||||
"set_new_profile_background": "Tetapkan latar belakang profil baru",
|
||||
"subject_line_behavior": "Salin subyek ketika membalas",
|
||||
"subject_line_email": "Seperti surel: \"re: subyek\"",
|
||||
"subject_line_mastodon": "Seperti mastodon: salin saja",
|
||||
"subject_line_noop": "Jangan salin",
|
||||
"useStreamingApiWarning": "(Tidak disarankan, eksperimental, diketahui dapat melewati postingan-postingan)",
|
||||
"fun": "Seru",
|
||||
"enable_web_push_notifications": "Aktifkan notifikasi push web",
|
||||
"more_settings": "Lebih banyak pengaturan",
|
||||
"reply_visibility_all": "Tampilkan semua balasan",
|
||||
"reply_visibility_self": "Hanya tampilkan balasan yang ditujukan kepada saya"
|
||||
},
|
||||
"about": {
|
||||
"mrf": {
|
||||
"keyword": {
|
||||
"reject": "Tolak",
|
||||
"is_replaced_by": "→"
|
||||
},
|
||||
"simple": {
|
||||
"quarantine_desc": "Instansi ini hanya akan mengirim postingan publik ke instansi-instansi berikut:",
|
||||
"quarantine": "Karantina",
|
||||
"reject_desc": "Instansi ini tidak akan menerima pesan dari instansi-instansi berikut:",
|
||||
"reject": "Tolak",
|
||||
"accept_desc": "Instansi ini hanya menerima pesan dari instansi-instansi berikut:",
|
||||
"accept": "Terima"
|
||||
},
|
||||
"federation": "Federasi",
|
||||
"mrf_policies": "Kebijakan MRF yang diaktifkan"
|
||||
},
|
||||
"staff": "Staf"
|
||||
},
|
||||
"time": {
|
||||
"day": "{0} hari",
|
||||
"days": "{0} hari",
|
||||
"day_short": "{0}h",
|
||||
"days_short": "{0}h",
|
||||
"hour": "{0} jam",
|
||||
"hours": "{0} jam",
|
||||
"hour_short": "{0}j",
|
||||
"hours_short": "{0}j",
|
||||
"in_future": "dalam {0}",
|
||||
"in_past": "{0} yang lalu",
|
||||
"minute": "{0} menit",
|
||||
"minutes": "{0} menit",
|
||||
"minute_short": "{0}m",
|
||||
"minutes_short": "{0}m",
|
||||
"month": "{0} bulan",
|
||||
"months": "{0} bulan",
|
||||
"month_short": "{0}b",
|
||||
"months_short": "{0}b",
|
||||
"now": "baru saja",
|
||||
"now_short": "sekarang",
|
||||
"second": "{0} detik",
|
||||
"seconds": "{0} detik",
|
||||
"second_short": "{0}d",
|
||||
"seconds_short": "{0}d",
|
||||
"week": "{0} pekan",
|
||||
"weeks": "{0} pekan",
|
||||
"week_short": "{0}p",
|
||||
"weeks_short": "{0}p",
|
||||
"year": "{0} tahun",
|
||||
"years": "{0} tahun",
|
||||
"year_short": "{0}t",
|
||||
"years_short": "{0}t"
|
||||
},
|
||||
"timeline": {
|
||||
"conversation": "Percakapan",
|
||||
"error": "Terjadi kesalahan memuat linimasa: {0}",
|
||||
"no_retweet_hint": "Postingan ditandai sebagai hanya-pengikut atau langsung dan tidak dapat diulang",
|
||||
"repeated": "diulangi",
|
||||
"reload": "Muat ulang",
|
||||
"no_more_statuses": "Tidak ada status lagi",
|
||||
"no_statuses": "Tidak ada status"
|
||||
},
|
||||
"status": {
|
||||
"favorites": "Favorit",
|
||||
"repeats": "Ulangan",
|
||||
"delete": "Hapus status",
|
||||
"pin": "Sematkan di profil",
|
||||
"unpin": "Berhenti menyematkan dari profil",
|
||||
"pinned": "Disematkan",
|
||||
"delete_confirm": "Apakah Anda benar-benar ingin menghapus status ini?",
|
||||
"reply_to": "Balas ke",
|
||||
"replies_list": "Balasan:",
|
||||
"mute_conversation": "Bisukan percakapan",
|
||||
"unmute_conversation": "Berhenti membisikan percakapan",
|
||||
"status_unavailable": "Status tidak tersedia",
|
||||
"thread_muted_and_words": ", memiliki kata:",
|
||||
"hide_content": "",
|
||||
"show_content": "",
|
||||
"status_deleted": "Postingan ini telah dihapus",
|
||||
"nsfw": "NSFW"
|
||||
},
|
||||
"user_card": {
|
||||
"block": "Blokir",
|
||||
"blocked": "Diblokir!",
|
||||
"deny": "Tolak",
|
||||
"edit_profile": "Sunting profil",
|
||||
"favorites": "Favorit",
|
||||
"follow": "Ikuti",
|
||||
"follow_sent": "Permintaan dikirim!",
|
||||
"follow_progress": "Meminta…",
|
||||
"mute": "Bisukan",
|
||||
"muted": "Dibisukan",
|
||||
"per_day": "per hari",
|
||||
"report": "Laporkan",
|
||||
"statuses": "Status",
|
||||
"unblock": "Berhenti memblokir",
|
||||
"block_progress": "Memblokir…",
|
||||
"unmute": "Berhenti membisukan",
|
||||
"mute_progress": "Membisukan…",
|
||||
"hide_repeats": "Sembunyikan ulangan",
|
||||
"show_repeats": "Tampilkan ulangan",
|
||||
"bot": "Bot",
|
||||
"admin_menu": {
|
||||
"moderation": "Moderasi",
|
||||
"activate_account": "Aktifkan akun",
|
||||
"deactivate_account": "Nonaktifkan akun",
|
||||
"delete_account": "Hapus akun",
|
||||
"force_nsfw": "Tandai semua postingan sebagai NSFW",
|
||||
"strip_media": "Hapus media dari postingan-postingan",
|
||||
"delete_user": "Hapus pengguna",
|
||||
"delete_user_confirmation": "Apakah Anda benar-benar yakin? Tindakan ini tidak dapat dibatalkan."
|
||||
},
|
||||
"follow_again": "Kirim permintaan lagi?",
|
||||
"follow_unfollow": "Berhenti mengikuti",
|
||||
"followees": "Mengikuti",
|
||||
"followers": "Pengikut",
|
||||
"following": "Diikuti!",
|
||||
"follows_you": "Mengikuti Anda!",
|
||||
"hidden": "Disembunyikan",
|
||||
"its_you": "Ini Anda!",
|
||||
"media": "Media",
|
||||
"mention": "Sebut",
|
||||
"message": "Kirimkan pesan"
|
||||
},
|
||||
"user_profile": {
|
||||
"timeline_title": "Linimasa pengguna"
|
||||
},
|
||||
"user_reporting": {
|
||||
"title": "Melaporkan {0}",
|
||||
"add_comment_description": "Laporan ini akan dikirim ke moderator instansi Anda. Anda dapat menyediakan penjelasan mengapa Anda melaporkan akun ini di bawah:",
|
||||
"additional_comments": "Komentar tambahan",
|
||||
"forward_description": "Akun ini berada di server lain. Kirim salinan dari laporannya juga?",
|
||||
"submit": "Kirim",
|
||||
"generic_error": "Sebuah kesalahan terjadi ketika memproses permintaan Anda."
|
||||
},
|
||||
"notifications": {
|
||||
"favorited_you": "memfavoritkan status Anda",
|
||||
"reacted_with": "bereaksi dengan {0}",
|
||||
"no_more_notifications": "Tidak ada notifikasi lagi",
|
||||
"repeated_you": "mengulangi status Anda",
|
||||
"read": "Dibaca!",
|
||||
"notifications": "Notifikasi",
|
||||
"follow_request": "ingin mengikuti Anda",
|
||||
"followed_you": "mengikuti Anda",
|
||||
"error": "Terjadi kesalahan ketika memuat notifikasi: {0}",
|
||||
"migrated_to": "bermigrasi ke",
|
||||
"load_older": "Muat notifikasi yang lebih lama",
|
||||
"broken_favorite": "Status tak diketahui, mencarinya…"
|
||||
},
|
||||
"who_to_follow": {
|
||||
"more": "Lebih banyak"
|
||||
},
|
||||
"tool_tip": {
|
||||
"media_upload": "Unggah media",
|
||||
"repeat": "Ulangi",
|
||||
"reply": "Balas",
|
||||
"favorite": "Favorit",
|
||||
"add_reaction": "Tambahkan Reaksi",
|
||||
"user_settings": "Pengaturan Pengguna"
|
||||
},
|
||||
"upload": {
|
||||
"error": {
|
||||
"base": "Pengunggahan gagal.",
|
||||
"message": "Pengunggahan gagal: {0}",
|
||||
"file_too_big": "Berkas terlalu besar [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]",
|
||||
"default": "Coba lagi nanti"
|
||||
},
|
||||
"file_size_units": {
|
||||
"B": "B",
|
||||
"KiB": "KiB",
|
||||
"MiB": "MiB",
|
||||
"GiB": "GiB",
|
||||
"TiB": "TiB"
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"people": "Orang",
|
||||
"hashtags": "Tagar",
|
||||
"person_talking": "{count} orang berbicara",
|
||||
"people_talking": "{count} orang berbicara",
|
||||
"no_results": "Tidak ada hasil"
|
||||
},
|
||||
"password_reset": {
|
||||
"forgot_password": "Lupa kata sandi?",
|
||||
"placeholder": "Surel atau nama pengguna Anda",
|
||||
"return_home": "Kembali ke halaman beranda",
|
||||
"too_many_requests": "Anda telah mencapai batas percobaan, coba lagi nanti.",
|
||||
"instruction": "Masukkan surel atau nama pengguna Anda. Kami akan mengirimkan Anda tautan untuk mengatur ulang kata sandi.",
|
||||
"password_reset": "Pengatur-ulangan kata sandi",
|
||||
"password_reset_disabled": "Pengatur-ulangan kata sandi dinonaktifkan. Hubungi administrator instansi Anda.",
|
||||
"password_reset_required": "Anda harus mengatur ulang kata sandi Anda untuk masuk.",
|
||||
"password_reset_required_but_mailer_is_disabled": "Anda harus mengatur ulang kata sandi, tetapi pengatur-ulangan kata sandi dinonaktifkan. Silakan hubungi administrator instansi Anda."
|
||||
},
|
||||
"chats": {
|
||||
"you": "Anda:",
|
||||
"message_user": "Kirim Pesan ke {nickname}",
|
||||
"delete": "Hapus",
|
||||
"chats": "Obrolan",
|
||||
"new": "Obrolan Baru",
|
||||
"empty_message_error": "Tidak dapat memposting pesan yang kosong",
|
||||
"more": "Lebih banyak",
|
||||
"delete_confirm": "Apakah Anda benar-benar ingin menghapus pesan ini?",
|
||||
"error_loading_chat": "Sesuatu yang salah terjadi ketika memuat obrolan.",
|
||||
"error_sending_message": "Sesuatu yang salah terjadi ketika mengirim pesan.",
|
||||
"empty_chat_list_placeholder": "Anda belum memiliki obrolan. Buat sbeuah obrolan baru!"
|
||||
},
|
||||
"file_type": {
|
||||
"audio": "Audio",
|
||||
"video": "Video",
|
||||
"image": "Gambar",
|
||||
"file": "Berkas"
|
||||
},
|
||||
"registration": {
|
||||
"bio_placeholder": "contoh.\nHai, aku Lain.\nAku seorang putri anime yang tinggal di pinggiran kota Jepang. Kamu mungkin mengenal aku dari Wired.",
|
||||
"validations": {
|
||||
"password_confirmation_required": "tidak boleh kosong",
|
||||
"password_required": "tidak boleh kosong",
|
||||
"email_required": "tidak boleh kosong",
|
||||
"fullname_required": "tidak boleh kosong",
|
||||
"username_required": "tidak boleh kosong"
|
||||
},
|
||||
"register": "Daftar",
|
||||
"fullname_placeholder": "contoh. Lain Iwakura",
|
||||
"username_placeholder": "contoh. lain",
|
||||
"new_captcha": "Klik gambarnya untuk mendapatkan captcha baru",
|
||||
"captcha": "CAPTCHA",
|
||||
"token": "Token undangan",
|
||||
"password_confirm": "Konfirmasi kata sandi",
|
||||
"email": "Surel",
|
||||
"bio": "Bio",
|
||||
"reason_placeholder": "Instansi ini menerima pendaftaran secara manual.\nBeritahu administrasinya mengapa Anda ingin mendaftar.",
|
||||
"reason": "Alasan mendaftar",
|
||||
"registration": "Pendaftaran"
|
||||
},
|
||||
"post_status": {
|
||||
"preview_empty": "Kosong",
|
||||
"default": "Baru saja mendarat di L.A.",
|
||||
"content_warning": "Subyek (opsional)",
|
||||
"content_type": {
|
||||
"text/bbcode": "BBCode",
|
||||
"text/markdown": "Markdown",
|
||||
"text/html": "HTML",
|
||||
"text/plain": "Teks biasa"
|
||||
},
|
||||
"media_description": "Keterangan media",
|
||||
"attachments_sensitive": "Tandai lampiran sebagai sensitif",
|
||||
"scope": {
|
||||
"public": "Publik - posting ke linimasa publik",
|
||||
"private": "Hanya-pengikut - posting hanya kepada pengikut",
|
||||
"direct": "Langsung - posting hanya kepada pengguna yang disebut"
|
||||
},
|
||||
"preview": "Pratinjau",
|
||||
"post": "Posting",
|
||||
"posting": "Memposting",
|
||||
"direct_warning_to_first_only": "Postingan ini akan terlihat oleh pengguna yang disebutkan di awal pesan.",
|
||||
"direct_warning_to_all": "Postingan ini akan terlihat oleh pengguna yang disebutkan.",
|
||||
"scope_notice": {
|
||||
"private": "Postingan ini akan terlihat hanya oleh pengikut Anda",
|
||||
"public": "Postingan ini akan terlihat oleh siapa saja"
|
||||
},
|
||||
"media_description_error": "Gagal memperbarui media, coba lagi",
|
||||
"empty_status_error": "Tidak dapat memposting status kosong tanpa berkas",
|
||||
"account_not_locked_warning_link": "terkunci",
|
||||
"account_not_locked_warning": "Akun Anda tidak {0}. Siapapun dapat mengikuti Anda untuk melihat postingan hanya-pengikut Anda.",
|
||||
"new_status": "Posting status baru"
|
||||
},
|
||||
"general": {
|
||||
"apply": "Terapkan",
|
||||
"flash_fail": "Gagal memuat konten flash, lihat console untuk keterangan.",
|
||||
"flash_security": "Harap ingat ini dapat menjadi berbahaya karena konten Flash masih termasuk arbitrary code.",
|
||||
"flash_content": "Klik untuk menampilkan konten Flash menggunakan Ruffle (Eksperimental, mungkin tidak bekerja).",
|
||||
"role": {
|
||||
"moderator": "Moderator",
|
||||
"admin": "Admin"
|
||||
},
|
||||
"peek": "Intip",
|
||||
"close": "Tutup",
|
||||
"verify": "Verifikasi",
|
||||
"confirm": "Konfirmasi",
|
||||
"enable": "Aktifkan",
|
||||
"disable": "Nonaktifkan",
|
||||
"cancel": "Batal",
|
||||
"show_less": "Tampilkan lebih sedikit",
|
||||
"show_more": "Tampilkan lebih banyak",
|
||||
"optional": "opsional",
|
||||
"retry": "Coba lagi",
|
||||
"error_retry": "Harap coba lagi",
|
||||
"generic_error": "Terjadi kesalahan",
|
||||
"loading": "Memuat…",
|
||||
"more": "Lebih banyak",
|
||||
"submit": "Kirim"
|
||||
},
|
||||
"remote_user_resolver": {
|
||||
"error": "Tidak ditemukan."
|
||||
},
|
||||
"emoji": {
|
||||
"load_all": "Memuat semua {emojiAmount} emoji",
|
||||
"load_all_hint": "Memuat {saneAmount} emoji pertama, memuat semua emoji dapat menyebabkan masalah performa.",
|
||||
"unicode": "Emoji unicode",
|
||||
"add_emoji": "Sisipkan emoji",
|
||||
"search_emoji": "Cari emoji",
|
||||
"emoji": "Emoji",
|
||||
"stickers": "Stiker",
|
||||
"keep_open": "Tetap buka pemilih",
|
||||
"custom": "Emoji kustom"
|
||||
},
|
||||
"polls": {
|
||||
"expired": "Japat berakhir {0} yang lalu",
|
||||
"expires_in": "Japat berakhir dalam {0}",
|
||||
"expiry": "Usia japat",
|
||||
"type": "Jenis japat",
|
||||
"vote": "Pilih",
|
||||
"votes_count": "{count} suara | {count} suara",
|
||||
"people_voted_count": "{count} orang memilih | {count} orang memilih",
|
||||
"votes": "suara",
|
||||
"option": "Opsi",
|
||||
"add_option": "Tambahkan opsi",
|
||||
"add_poll": "Tambahkan japat",
|
||||
"not_enough_options": "Terlalu sedikit opsi yang unik pada japat"
|
||||
},
|
||||
"nav": {
|
||||
"preferences": "Preferensi",
|
||||
"search": "Cari",
|
||||
"user_search": "Pencarian Pengguna",
|
||||
"home_timeline": "Linimasa beranda",
|
||||
"timeline": "Linimasa",
|
||||
"public_tl": "Linimasa publik",
|
||||
"interactions": "Interaksi",
|
||||
"mentions": "Sebutan",
|
||||
"back": "Kembali",
|
||||
"administration": "Administrasi",
|
||||
"about": "Tentang",
|
||||
"timelines": "Linimasa",
|
||||
"chats": "Obrolan",
|
||||
"dms": "Pesan langsung",
|
||||
"friend_requests": "Ingin mengikuti"
|
||||
},
|
||||
"media_modal": {
|
||||
"next": "Selanjutnya",
|
||||
"previous": "Sebelum"
|
||||
},
|
||||
"login": {
|
||||
"recovery_code": "Kode pemulihan",
|
||||
"enter_recovery_code": "Masukkan kode pemulihan",
|
||||
"authentication_code": "Kode otentikasi",
|
||||
"hint": "Masuk untuk ikut berdiskusi",
|
||||
"username": "Nama pengguna",
|
||||
"register": "Daftar",
|
||||
"placeholder": "contoh: lain",
|
||||
"password": "Kata sandi",
|
||||
"logout": "Keluar",
|
||||
"description": "Masuk dengan OAuth",
|
||||
"login": "Masuk",
|
||||
"heading": {
|
||||
"totp": "Otentikasi dua-faktor"
|
||||
},
|
||||
"enter_two_factor_code": "Masukkan kode dua-faktor"
|
||||
},
|
||||
"importer": {
|
||||
"error": "Terjadi kesalahan ketika mnengimpor berkas ini.",
|
||||
"success": "Berhasil mengimpor.",
|
||||
"submit": "Kirim"
|
||||
},
|
||||
"image_cropper": {
|
||||
"cancel": "Batal",
|
||||
"save_without_cropping": "Simpan tanpa memotong",
|
||||
"save": "Simpan",
|
||||
"crop_picture": "Potong gambar"
|
||||
},
|
||||
"finder": {
|
||||
"find_user": "Cari pengguna",
|
||||
"error_fetching_user": "Terjadi kesalahan ketika memuat pengguna"
|
||||
},
|
||||
"features_panel": {
|
||||
"title": "Fitur-fitur",
|
||||
"text_limit": "Batas teks",
|
||||
"gopher": "Gopher",
|
||||
"pleroma_chat_messages": "Pleroma Obrolan",
|
||||
"chat": "Obrolan",
|
||||
"upload_limit": "Batas unggahan"
|
||||
},
|
||||
"exporter": {
|
||||
"processing": "Memproses, Anda akan segera diminta untuk mengunduh berkas Anda",
|
||||
"export": "Ekspor"
|
||||
},
|
||||
"domain_mute_card": {
|
||||
"unmute": "Berhenti membisukan",
|
||||
"mute_progress": "Membisukan…",
|
||||
"mute": "Bisukan",
|
||||
"unmute_progress": "Memberhentikan pembisuan…"
|
||||
},
|
||||
"display_date": {
|
||||
"today": "Hari Ini"
|
||||
},
|
||||
"selectable_list": {
|
||||
"select_all": "Pilih semua"
|
||||
},
|
||||
"interactions": {
|
||||
"moves": "Pengguna yang bermigrasi",
|
||||
"follows": "Pengikut baru",
|
||||
"favs_repeats": "Ulangan dan favorit",
|
||||
"load_older": "Muat interaksi yang lebih tua"
|
||||
},
|
||||
"errors": {
|
||||
"storage_unavailable": "Pleroma tidak dapat mengakses penyimpanan browser. Login Anda atau pengaturan lokal Anda tidak akan tersimpan dan masalah yang tidak terduga dapat terjadi. Coba mengaktifkan kuki."
|
||||
},
|
||||
"shoutbox": {
|
||||
"title": "Kotak Suara"
|
||||
}
|
||||
}
|
|
@ -21,7 +21,10 @@
|
|||
"role": {
|
||||
"moderator": "Moderatore",
|
||||
"admin": "Amministratore"
|
||||
}
|
||||
},
|
||||
"flash_fail": "Contenuto Flash non caricato, vedi console del browser.",
|
||||
"flash_content": "Mostra contenuto Flash tramite Ruffle (funzione in prova).",
|
||||
"flash_security": "Può essere pericoloso perché i contenuti in Flash sono eseguibili."
|
||||
},
|
||||
"nav": {
|
||||
"mentions": "Menzioni",
|
||||
|
@ -65,13 +68,13 @@
|
|||
"current_avatar": "La tua icona attuale",
|
||||
"current_profile_banner": "Il tuo stendardo attuale",
|
||||
"filtering": "Filtri",
|
||||
"filtering_explanation": "Tutti i post contenenti queste parole saranno silenziati, una per riga",
|
||||
"filtering_explanation": "Tutti i messaggi contenenti queste parole saranno silenziati, una per riga",
|
||||
"hide_attachments_in_convo": "Nascondi gli allegati presenti nelle conversazioni",
|
||||
"hide_attachments_in_tl": "Nascondi gli allegati presenti nelle sequenze",
|
||||
"name": "Nome",
|
||||
"name_bio": "Nome ed introduzione",
|
||||
"nsfw_clickthrough": "Fai click per visualizzare gli allegati offuscati",
|
||||
"profile_background": "Sfondo della tua pagina",
|
||||
"profile_background": "Sfondo del tuo profilo",
|
||||
"profile_banner": "Gonfalone del tuo profilo",
|
||||
"set_new_avatar": "Scegli una nuova icona",
|
||||
"set_new_profile_background": "Scegli un nuovo sfondo",
|
||||
|
@ -365,8 +368,8 @@
|
|||
"search_user_to_mute": "Cerca utente da silenziare",
|
||||
"search_user_to_block": "Cerca utente da bloccare",
|
||||
"autohide_floating_post_button": "Nascondi automaticamente il pulsante di composizione (mobile)",
|
||||
"show_moderator_badge": "Mostra l'insegna di moderatore sulla mia pagina",
|
||||
"show_admin_badge": "Mostra l'insegna di amministratore sulla mia pagina",
|
||||
"show_moderator_badge": "Mostra l'insegna di moderatore sul mio profilo",
|
||||
"show_admin_badge": "Mostra l'insegna di amministratore sul mio profilo",
|
||||
"hide_followers_count_description": "Non mostrare quanti seguaci ho",
|
||||
"hide_follows_count_description": "Non mostrare quanti utenti seguo",
|
||||
"hide_followers_description": "Non mostrare i miei seguaci",
|
||||
|
@ -443,7 +446,9 @@
|
|||
"backup_settings_theme": "Archivia impostazioni e tema localmente",
|
||||
"backup_settings": "Archivia impostazioni localmente",
|
||||
"backup_restore": "Archiviazione impostazioni"
|
||||
}
|
||||
},
|
||||
"right_sidebar": "Mostra barra laterale a destra",
|
||||
"hide_shoutbox": "Nascondi muro dei graffiti"
|
||||
},
|
||||
"timeline": {
|
||||
"error_fetching": "Errore nell'aggiornamento",
|
||||
|
@ -522,7 +527,8 @@
|
|||
"striped": "A righe",
|
||||
"solid": "Un colore",
|
||||
"disabled": "Nessun risalto"
|
||||
}
|
||||
},
|
||||
"edit_profile": "Modifica profilo"
|
||||
},
|
||||
"chat": {
|
||||
"title": "Chat"
|
||||
|
@ -660,7 +666,7 @@
|
|||
},
|
||||
"domain_mute_card": {
|
||||
"mute": "Silenzia",
|
||||
"mute_progress": "Silenzio…",
|
||||
"mute_progress": "Procedo…",
|
||||
"unmute": "Ascolta",
|
||||
"unmute_progress": "Procedo…"
|
||||
},
|
||||
|
@ -701,7 +707,7 @@
|
|||
},
|
||||
"interactions": {
|
||||
"favs_repeats": "Condivisi e Graditi",
|
||||
"load_older": "Carica vecchie interazioni",
|
||||
"load_older": "Carica interazioni precedenti",
|
||||
"moves": "Utenti migrati",
|
||||
"follows": "Nuovi seguìti"
|
||||
},
|
||||
|
|
|
@ -19,8 +19,8 @@
|
|||
"reject_desc": "Ta instancja odrzuca posty z wymienionych instancji:",
|
||||
"quarantine": "Kwarantanna",
|
||||
"quarantine_desc": "Ta instancja wysyła tylko publiczne posty do wymienionych instancji:",
|
||||
"ftl_removal": "Usunięcie z \"Całej znanej sieci\"",
|
||||
"ftl_removal_desc": "Ta instancja usuwa wymienionych instancje z \"Całej znanej sieci\":",
|
||||
"ftl_removal": "Usunięcie z „Całej znanej sieci”",
|
||||
"ftl_removal_desc": "Ta instancja usuwa wymienionych instancje z „Całej znanej sieci”:",
|
||||
"media_removal": "Usuwanie multimediów",
|
||||
"media_removal_desc": "Ta instancja usuwa multimedia z postów od wymienionych instancji:",
|
||||
"media_nsfw": "Multimedia ustawione jako wrażliwe",
|
||||
|
@ -75,7 +75,13 @@
|
|||
"loading": "Ładowanie…",
|
||||
"retry": "Spróbuj ponownie",
|
||||
"peek": "Spójrz",
|
||||
"error_retry": "Spróbuj ponownie"
|
||||
"error_retry": "Spróbuj ponownie",
|
||||
"flash_content": "Naciśnij, aby wyświetlić zawartości Flash z użyciem Ruffle (eksperymentalnie, może nie działać).",
|
||||
"flash_fail": "Nie udało się załadować treści flash, zajrzyj do konsoli, aby odnaleźć szczegóły.",
|
||||
"role": {
|
||||
"moderator": "Moderator",
|
||||
"admin": "Administrator"
|
||||
}
|
||||
},
|
||||
"image_cropper": {
|
||||
"crop_picture": "Przytnij obrazek",
|
||||
|
@ -118,7 +124,7 @@
|
|||
"friend_requests": "Prośby o możliwość obserwacji",
|
||||
"mentions": "Wzmianki",
|
||||
"interactions": "Interakcje",
|
||||
"dms": "Wiadomości prywatne",
|
||||
"dms": "Wiadomości bezpośrednie",
|
||||
"public_tl": "Publiczna oś czasu",
|
||||
"timeline": "Oś czasu",
|
||||
"twkn": "Znana sieć",
|
||||
|
@ -128,7 +134,8 @@
|
|||
"preferences": "Preferencje",
|
||||
"bookmarks": "Zakładki",
|
||||
"chats": "Czaty",
|
||||
"timelines": "Osie czasu"
|
||||
"timelines": "Osie czasu",
|
||||
"home_timeline": "Główna oś czasu"
|
||||
},
|
||||
"notifications": {
|
||||
"broken_favorite": "Nieznany status, szukam go…",
|
||||
|
@ -156,7 +163,9 @@
|
|||
"expiry": "Czas trwania ankiety",
|
||||
"expires_in": "Ankieta kończy się za {0}",
|
||||
"expired": "Ankieta skończyła się {0} temu",
|
||||
"not_enough_options": "Zbyt mało unikalnych opcji w ankiecie"
|
||||
"not_enough_options": "Zbyt mało unikalnych opcji w ankiecie",
|
||||
"people_voted_count": "{count} osoba zagłosowała | {count} osoby zagłosowały | {count} osób zagłosowało",
|
||||
"votes_count": "{count} głos | {count} głosy | {count} głosów"
|
||||
},
|
||||
"emoji": {
|
||||
"stickers": "Naklejki",
|
||||
|
@ -197,16 +206,17 @@
|
|||
"unlisted": "Ten post nie będzie widoczny na publicznej osi czasu i całej znanej sieci"
|
||||
},
|
||||
"scope": {
|
||||
"direct": "Bezpośredni – Tylko dla wspomnianych użytkowników",
|
||||
"private": "Tylko dla obserwujących – Umieść dla osób, które cię obserwują",
|
||||
"public": "Publiczny – Umieść na publicznych osiach czasu",
|
||||
"unlisted": "Niewidoczny – Nie umieszczaj na publicznych osiach czasu"
|
||||
"direct": "Bezpośredni – tylko dla wspomnianych użytkowników",
|
||||
"private": "Tylko dla obserwujących – umieść dla osób, które cię obserwują",
|
||||
"public": "Publiczny – umieść na publicznych osiach czasu",
|
||||
"unlisted": "Niewidoczny – nie umieszczaj na publicznych osiach czasu"
|
||||
},
|
||||
"preview_empty": "Pusty",
|
||||
"preview": "Podgląd",
|
||||
"empty_status_error": "Nie można wysłać pustego wpisu bez plików",
|
||||
"media_description_error": "Nie udało się zaktualizować mediów, spróbuj ponownie",
|
||||
"media_description": "Opis mediów"
|
||||
"media_description": "Opis mediów",
|
||||
"post": "Opublikuj"
|
||||
},
|
||||
"registration": {
|
||||
"bio": "Bio",
|
||||
|
@ -227,7 +237,10 @@
|
|||
"password_required": "nie może być puste",
|
||||
"password_confirmation_required": "nie może być puste",
|
||||
"password_confirmation_match": "musi być takie jak hasło"
|
||||
}
|
||||
},
|
||||
"reason": "Powód rejestracji",
|
||||
"reason_placeholder": "Ta instancja ręcznie zatwierdza rejestracje.\nPoinformuj administratora, dlaczego chcesz się zarejestrować.",
|
||||
"register": "Zarejestruj się"
|
||||
},
|
||||
"remote_user_resolver": {
|
||||
"remote_user_resolver": "Wyszukiwarka użytkowników nietutejszych",
|
||||
|
@ -281,7 +294,7 @@
|
|||
"cGreen": "Zielony (powtórzenia)",
|
||||
"cOrange": "Pomarańczowy (ulubione)",
|
||||
"cRed": "Czerwony (anuluj)",
|
||||
"change_email": "Zmień email",
|
||||
"change_email": "Zmień e-mail",
|
||||
"change_email_error": "Wystąpił problem podczas zmiany emaila.",
|
||||
"changed_email": "Pomyślnie zmieniono email!",
|
||||
"change_password": "Zmień hasło",
|
||||
|
@ -345,7 +358,7 @@
|
|||
"use_contain_fit": "Nie przycinaj załączników na miniaturach",
|
||||
"name": "Imię",
|
||||
"name_bio": "Imię i bio",
|
||||
"new_email": "Nowy email",
|
||||
"new_email": "Nowy e-mail",
|
||||
"new_password": "Nowe hasło",
|
||||
"notification_visibility": "Rodzaje powiadomień do wyświetlania",
|
||||
"notification_visibility_follows": "Obserwacje",
|
||||
|
@ -361,8 +374,8 @@
|
|||
"hide_followers_description": "Nie pokazuj kto mnie obserwuje",
|
||||
"hide_follows_count_description": "Nie pokazuj licznika obserwowanych",
|
||||
"hide_followers_count_description": "Nie pokazuj licznika obserwujących",
|
||||
"show_admin_badge": "Pokazuj odznakę Administrator na moim profilu",
|
||||
"show_moderator_badge": "Pokazuj odznakę Moderator na moim profilu",
|
||||
"show_admin_badge": "Pokazuj odznakę „Administrator” na moim profilu",
|
||||
"show_moderator_badge": "Pokazuj odznakę „Moderator” na moim profilu",
|
||||
"nsfw_clickthrough": "Włącz domyślne ukrywanie załączników o treści nieprzyzwoitej (NSFW)",
|
||||
"oauth_tokens": "Tokeny OAuth",
|
||||
"token": "Token",
|
||||
|
@ -600,7 +613,27 @@
|
|||
"mute_import": "Import wyciszeń",
|
||||
"mute_export_button": "Wyeksportuj swoje wyciszenia do pliku .csv",
|
||||
"mute_export": "Eksport wyciszeń",
|
||||
"hide_wallpaper": "Ukryj tło instancji"
|
||||
"hide_wallpaper": "Ukryj tło instancji",
|
||||
"save": "Zapisz zmiany",
|
||||
"setting_changed": "Opcja różni się od domyślnej",
|
||||
"right_sidebar": "Pokaż pasek boczny po prawej",
|
||||
"file_export_import": {
|
||||
"errors": {
|
||||
"invalid_file": "Wybrany plik nie jest obsługiwaną kopią zapasową ustawień Pleromy. Nie dokonano żadnych zmian."
|
||||
},
|
||||
"backup_restore": "Kopia zapasowa ustawień",
|
||||
"backup_settings": "Kopia zapasowa ustawień do pliku",
|
||||
"backup_settings_theme": "Kopia zapasowa ustawień i motywu do pliku",
|
||||
"restore_settings": "Przywróć ustawienia z pliku"
|
||||
},
|
||||
"more_settings": "Więcej ustawień",
|
||||
"word_filter": "Filtr słów",
|
||||
"hide_media_previews": "Ukryj podgląd mediów",
|
||||
"hide_all_muted_posts": "Ukryj wyciszone słowa",
|
||||
"reply_visibility_following_short": "Pokazuj odpowiedzi obserwującym",
|
||||
"reply_visibility_self_short": "Pokazuj odpowiedzi tylko do mnie",
|
||||
"sensitive_by_default": "Domyślnie oznaczaj wpisy jako wrażliwe",
|
||||
"hide_shoutbox": "Ukryj shoutbox instancji"
|
||||
},
|
||||
"time": {
|
||||
"day": "{0} dzień",
|
||||
|
@ -648,7 +681,9 @@
|
|||
"no_more_statuses": "Brak kolejnych statusów",
|
||||
"no_statuses": "Brak statusów",
|
||||
"reload": "Odśwież",
|
||||
"error": "Błąd pobierania osi czasu: {0}"
|
||||
"error": "Błąd pobierania osi czasu: {0}",
|
||||
"socket_broke": "Utracono połączenie w czasie rzeczywistym: kod CloseEvent {0}",
|
||||
"socket_reconnected": "Osiągnięto połączenie w czasie rzeczywistym"
|
||||
},
|
||||
"status": {
|
||||
"favorites": "Ulubione",
|
||||
|
@ -731,7 +766,12 @@
|
|||
"delete_user": "Usuń użytkownika",
|
||||
"delete_user_confirmation": "Czy jesteś absolutnie pewny(-a)? Ta operacja nie może być cofnięta."
|
||||
},
|
||||
"message": "Napisz"
|
||||
"message": "Napisz",
|
||||
"edit_profile": "Edytuj profil",
|
||||
"highlight": {
|
||||
"disabled": "Bez wyróżnienia"
|
||||
},
|
||||
"bot": "Bot"
|
||||
},
|
||||
"user_profile": {
|
||||
"timeline_title": "Oś czasu użytkownika",
|
||||
|
|
|
@ -21,7 +21,10 @@
|
|||
"role": {
|
||||
"moderator": "Модератор",
|
||||
"admin": "Адміністратор"
|
||||
}
|
||||
},
|
||||
"flash_content": "Натисніть для перегляду змісту Flash за допомогою Ruffle (експериментально, може не працювати).",
|
||||
"flash_security": "Ця функція може становити ризик, оскільки Flash-вміст все ще є потенційно небезпечним.",
|
||||
"flash_fail": "Не вдалося завантажити Flash-вміст, докладнішу інформацію дивись у консолі."
|
||||
},
|
||||
"finder": {
|
||||
"error_fetching_user": "Користувача не знайдено",
|
||||
|
@ -633,7 +636,9 @@
|
|||
"backup_settings_theme": "Резервне копіювання налаштувань та теми у файл",
|
||||
"backup_settings": "Резервне копіювання налаштувань у файл",
|
||||
"backup_restore": "Резервне копіювання налаштувань"
|
||||
}
|
||||
},
|
||||
"right_sidebar": "Показувати бокову панель справа",
|
||||
"hide_shoutbox": "Приховати оголошення інстансу"
|
||||
},
|
||||
"selectable_list": {
|
||||
"select_all": "Вибрати все"
|
||||
|
@ -799,7 +804,8 @@
|
|||
"solid": "Суцільний фон",
|
||||
"disabled": "Не виділяти"
|
||||
},
|
||||
"bot": "Бот"
|
||||
"bot": "Бот",
|
||||
"edit_profile": "Редагувати профіль"
|
||||
},
|
||||
"status": {
|
||||
"copy_link": "Скопіювати посилання на допис",
|
||||
|
|
435
src/i18n/vi.json
Normal file
435
src/i18n/vi.json
Normal file
|
@ -0,0 +1,435 @@
|
|||
{
|
||||
"about": {
|
||||
"mrf": {
|
||||
"federation": "Liên hợp",
|
||||
"keyword": {
|
||||
"keyword_policies": "Chính sách quan trọng",
|
||||
"reject": "Từ chối",
|
||||
"replace": "Thay thế",
|
||||
"is_replaced_by": "→",
|
||||
"ftl_removal": "Giới hạn chung"
|
||||
},
|
||||
"mrf_policies": "Kích hoạt chính sách MRF",
|
||||
"simple": {
|
||||
"simple_policies": "Quy tắc máy chủ",
|
||||
"accept": "Đồng ý",
|
||||
"accept_desc": "Máy chủ này chỉ chấp nhận tin nhắn từ những máy chủ:",
|
||||
"reject": "Từ chối",
|
||||
"quarantine": "Bảo hành",
|
||||
"quarantine_desc": "Máy chủ này sẽ gửi tút công khai đến những máy chủ:",
|
||||
"ftl_removal": "Giới hạn chung",
|
||||
"media_removal": "Ẩn Media",
|
||||
"media_removal_desc": "Media từ những máy chủ sau sẽ bị ẩn:",
|
||||
"media_nsfw": "Áp đặt nhạy cảm",
|
||||
"media_nsfw_desc": "Nội dung từ những máy chủ sau sẽ bị tự động gắn nhãn nhạy cảm:",
|
||||
"reject_desc": "Máy chủ này không chấp nhận tin nhắn từ những máy chủ:",
|
||||
"ftl_removal_desc": "Nội dung từ những máy chủ sau sẽ bị ẩn:"
|
||||
},
|
||||
"mrf_policies_desc": "Các chính sách MRF kiểm soát sự liên hợp của máy chủ. Các chính sách sau được bật:"
|
||||
},
|
||||
"staff": "Nhân viên"
|
||||
},
|
||||
"domain_mute_card": {
|
||||
"mute": "Ẩn",
|
||||
"mute_progress": "Đang ẩn…",
|
||||
"unmute": "Ngưng ẩn",
|
||||
"unmute_progress": "Đang ngưng ẩn…"
|
||||
},
|
||||
"exporter": {
|
||||
"export": "Xuất dữ liệu",
|
||||
"processing": "Đang chuẩn bị tập tin cho bạn tải về"
|
||||
},
|
||||
"features_panel": {
|
||||
"chat": "Chat",
|
||||
"pleroma_chat_messages": "Pleroma Chat",
|
||||
"gopher": "Gopher",
|
||||
"media_proxy": "Proxy media",
|
||||
"text_limit": "Giới hạn ký tự",
|
||||
"title": "Tính năng",
|
||||
"who_to_follow": "Đề xuất theo dõi",
|
||||
"upload_limit": "Giới hạn tải lên",
|
||||
"scope_options": "Đa dạng kiểu đăng"
|
||||
},
|
||||
"finder": {
|
||||
"error_fetching_user": "Lỗi người dùng",
|
||||
"find_user": "Tìm người dùng"
|
||||
},
|
||||
"shoutbox": {
|
||||
"title": "Chat cùng nhau"
|
||||
},
|
||||
"general": {
|
||||
"apply": "Áp dụng",
|
||||
"submit": "Gửi tặng",
|
||||
"more": "Nhiều hơn",
|
||||
"loading": "Đang tải…",
|
||||
"generic_error": "Đã có lỗi xảy ra",
|
||||
"error_retry": "Xin hãy thử lại",
|
||||
"retry": "Thử lại",
|
||||
"optional": "tùy chọn",
|
||||
"show_more": "Xem thêm",
|
||||
"show_less": "Thu gọn",
|
||||
"dismiss": "Bỏ qua",
|
||||
"cancel": "Hủy bỏ",
|
||||
"disable": "Tắt",
|
||||
"enable": "Bật",
|
||||
"confirm": "Xác nhận",
|
||||
"verify": "Xác thực",
|
||||
"close": "Đóng",
|
||||
"peek": "Thu gọn",
|
||||
"role": {
|
||||
"admin": "Quản trị viên",
|
||||
"moderator": "Kiểm duyệt viên"
|
||||
},
|
||||
"flash_security": "Lưu ý rằng điều này có thể tiềm ẩn nguy hiểm vì nội dung Flash là mã lập trình tùy ý.",
|
||||
"flash_fail": "Tải nội dung Flash thất bại, tham khảo chi tiết trong console.",
|
||||
"flash_content": "Nhấn để hiện nội dung Flash bằng Ruffle (Thử nghiệm, có thể không dùng được)."
|
||||
},
|
||||
"image_cropper": {
|
||||
"crop_picture": "Cắt hình ảnh",
|
||||
"save": "Lưu",
|
||||
"save_without_cropping": "Bỏ qua cắt",
|
||||
"cancel": "Hủy bỏ"
|
||||
},
|
||||
"importer": {
|
||||
"submit": "Gửi đi",
|
||||
"success": "Đã nhập dữ liệu thành công.",
|
||||
"error": "Có lỗi xảy ra khi nhập dữ liệu từ tập tin này."
|
||||
},
|
||||
"login": {
|
||||
"login": "Đăng nhập",
|
||||
"description": "Đăng nhập bằng OAuth",
|
||||
"logout": "Đăng xuất",
|
||||
"password": "Mật khẩu",
|
||||
"placeholder": "vd: cobetronxinh",
|
||||
"register": "Đăng ký",
|
||||
"username": "Tên người dùng",
|
||||
"hint": "Đăng nhập để cùng trò chuyện",
|
||||
"authentication_code": "Mã truy cập",
|
||||
"enter_recovery_code": "Nhập mã khôi phục",
|
||||
"recovery_code": "Mã khôi phục",
|
||||
"heading": {
|
||||
"totp": "Xác thực hai bước",
|
||||
"recovery": "Khôi phục hai bước"
|
||||
},
|
||||
"enter_two_factor_code": "Nhập mã xác thực hai bước"
|
||||
},
|
||||
"media_modal": {
|
||||
"previous": "Trước đó",
|
||||
"next": "Kế tiếp"
|
||||
},
|
||||
"nav": {
|
||||
"about": "Về máy chủ này",
|
||||
"administration": "Vận hành bởi",
|
||||
"back": "Quay lại",
|
||||
"friend_requests": "Yêu cầu theo dõi",
|
||||
"mentions": "Lượt nhắc đến",
|
||||
"interactions": "Giao tiếp",
|
||||
"dms": "Nhắn tin",
|
||||
"public_tl": "Bảng tin máy chủ",
|
||||
"timeline": "Bảng tin",
|
||||
"home_timeline": "Bảng tin của bạn",
|
||||
"twkn": "Thế giới",
|
||||
"bookmarks": "Đã lưu",
|
||||
"user_search": "Tìm kiếm người dùng",
|
||||
"search": "Tìm kiếm",
|
||||
"who_to_follow": "Đề xuất theo dõi",
|
||||
"preferences": "Thiết lập",
|
||||
"timelines": "Bảng tin",
|
||||
"chats": "Chat"
|
||||
},
|
||||
"notifications": {
|
||||
"broken_favorite": "Trạng thái chưa rõ, đang tìm kiếm…",
|
||||
"favorited_you": "thích tút của bạn",
|
||||
"followed_you": "theo dõi bạn",
|
||||
"follow_request": "yêu cầu theo dõi bạn",
|
||||
"load_older": "Xem những thông báo cũ hơn",
|
||||
"notifications": "Thông báo",
|
||||
"read": "Đọc!",
|
||||
"repeated_you": "chia sẻ tút của bạn",
|
||||
"no_more_notifications": "Không còn thông báo nào",
|
||||
"migrated_to": "chuyển sang",
|
||||
"reacted_with": "chạm tới {0}",
|
||||
"error": "Lỗi xử lý thông báo: {0}"
|
||||
},
|
||||
"polls": {
|
||||
"add_poll": "Tạo bình chọn",
|
||||
"option": "Lựa chọn",
|
||||
"votes": "người bình chọn",
|
||||
"people_voted_count": "{count} người bình chọn | {count} người bình chọn",
|
||||
"vote": "Bình chọn",
|
||||
"type": "Kiểu bình chọn",
|
||||
"single_choice": "Chỉ được chọn một lựa chọn",
|
||||
"multiple_choices": "Cho phép chọn nhiều lựa chọn",
|
||||
"expiry": "Thời hạn bình chọn",
|
||||
"expires_in": "Bình chọn kết thúc sau {0}",
|
||||
"not_enough_options": "Không đủ lựa chọn tối thiểu",
|
||||
"add_option": "Thêm lựa chọn",
|
||||
"votes_count": "{count} bình chọn | {count} bình chọn",
|
||||
"expired": "Bình chọn đã kết thúc {0} trước"
|
||||
},
|
||||
"emoji": {
|
||||
"stickers": "Sticker",
|
||||
"emoji": "Emoji",
|
||||
"keep_open": "Mở khung lựa chọn",
|
||||
"search_emoji": "Tìm emoji",
|
||||
"add_emoji": "Nhập emoji",
|
||||
"custom": "Tùy chỉnh emoji",
|
||||
"unicode": "Unicode emoji",
|
||||
"load_all_hint": "Tải trước {saneAmount} emoji, tải toàn bộ emoji có thể gây xử lí chậm.",
|
||||
"load_all": "Đang tải {emojiAmount} emoji"
|
||||
},
|
||||
"interactions": {
|
||||
"favs_repeats": "Tương tác",
|
||||
"follows": "Lượt theo dõi mới",
|
||||
"moves": "Người dùng chuyển đi",
|
||||
"load_older": "Xem tương tác cũ hơn"
|
||||
},
|
||||
"post_status": {
|
||||
"new_status": "Đăng tút",
|
||||
"account_not_locked_warning": "Tài khoản của bạn chưa {0}. Bất kỳ ai cũng có thể xem những tút dành cho người theo dõi của bạn.",
|
||||
"account_not_locked_warning_link": "đã khóa",
|
||||
"attachments_sensitive": "Đánh dấu media là nhạy cảm",
|
||||
"media_description": "Mô tả media",
|
||||
"content_type": {
|
||||
"text/plain": "Văn bản",
|
||||
"text/html": "HTML",
|
||||
"text/markdown": "Markdown",
|
||||
"text/bbcode": "BBCode"
|
||||
},
|
||||
"content_warning": "Tiêu đề (tùy chọn)",
|
||||
"default": "Just landed in L.A.",
|
||||
"direct_warning_to_first_only": "Người đầu tiên được nhắc đến mới có thể thấy tút này.",
|
||||
"posting": "Đang đăng tút",
|
||||
"post": "Đăng",
|
||||
"preview": "Xem trước",
|
||||
"preview_empty": "Trống",
|
||||
"empty_status_error": "Không thể đăng một tút trống và không có media",
|
||||
"media_description_error": "Cập nhật media thất bại, thử lại sau",
|
||||
"scope_notice": {
|
||||
"private": "Chỉ những người theo dõi bạn mới thấy tút này",
|
||||
"unlisted": "Tút này sẽ không hiện trong bảng tin máy chủ và thế giới",
|
||||
"public": "Mọi người đều có thể thấy tút này"
|
||||
},
|
||||
"scope": {
|
||||
"public": "Công khai - hiện trên bảng tin máy chủ",
|
||||
"private": "Riêng tư - Chỉ dành cho người theo dõi",
|
||||
"unlisted": "Hạn chế - không hiện trên bảng tin",
|
||||
"direct": "Tin nhắn - chỉ người được nhắc đến mới thấy"
|
||||
},
|
||||
"direct_warning_to_all": "Những ai được nhắc đến sẽ đều thấy tút này."
|
||||
},
|
||||
"registration": {
|
||||
"bio": "Tiểu sử",
|
||||
"email": "Email",
|
||||
"fullname": "Tên hiển thị",
|
||||
"password_confirm": "Xác nhận mật khẩu",
|
||||
"registration": "Đăng ký",
|
||||
"token": "Lời mời",
|
||||
"captcha": "CAPTCHA",
|
||||
"new_captcha": "Nhấn vào hình ảnh để đổi captcha mới",
|
||||
"username_placeholder": "vd: cobetronxinh",
|
||||
"fullname_placeholder": "vd: Cô Bé Tròn Xinh",
|
||||
"bio_placeholder": "vd:\nHi, I'm Cô Bé Tròn Xinh.\nI’m an anime girl living in suburban Vietnam. You may know me from the school.",
|
||||
"reason": "Lý do đăng ký",
|
||||
"reason_placeholder": "Máy chủ này phê duyệt đăng ký thủ công.\nHãy cho quản trị viên biết lý do bạn muốn đăng ký.",
|
||||
"register": "Đăng ký",
|
||||
"validations": {
|
||||
"username_required": "không được để trống",
|
||||
"fullname_required": "không được để trống",
|
||||
"email_required": "không được để trống",
|
||||
"password_confirmation_required": "không được để trống",
|
||||
"password_confirmation_match": "phải trùng khớp với mật khẩu",
|
||||
"password_required": "không được để trống"
|
||||
}
|
||||
},
|
||||
"remote_user_resolver": {
|
||||
"remote_user_resolver": "Giải quyết người dùng từ xa",
|
||||
"searching_for": "Tìm kiếm",
|
||||
"error": "Không tìm thấy."
|
||||
},
|
||||
"selectable_list": {
|
||||
"select_all": "Chọn tất cả"
|
||||
},
|
||||
"settings": {
|
||||
"app_name": "Tên app",
|
||||
"save": "Lưu thay đổi",
|
||||
"security": "Bảo mật",
|
||||
"enter_current_password_to_confirm": "Nhập mật khẩu để xác thực",
|
||||
"mfa": {
|
||||
"otp": "OTP",
|
||||
"setup_otp": "Thiết lập OTP",
|
||||
"wait_pre_setup_otp": "hậu thiết lập OTP",
|
||||
"confirm_and_enable": "Xác nhận và kích hoạt OTP",
|
||||
"title": "Xác thực hai bước",
|
||||
"recovery_codes": "Những mã khôi phục.",
|
||||
"waiting_a_recovery_codes": "Đang nhận mã khôi phục…",
|
||||
"authentication_methods": "Phương pháp xác thực",
|
||||
"scan": {
|
||||
"title": "Quét",
|
||||
"desc": "Sử dụng app xác thực hai bước để quét mã QR hoặc nhập mã khôi phục:",
|
||||
"secret_code": "Mã"
|
||||
},
|
||||
"verify": {
|
||||
"desc": "Để bật xác thực hai bước, nhập mã từ app của bạn:"
|
||||
},
|
||||
"generate_new_recovery_codes": "Tạo mã khôi phục mới",
|
||||
"warning_of_generate_new_codes": "Khi tạo mã khôi phục mới, những mã khôi phục cũ sẽ không sử dụng được nữa.",
|
||||
"recovery_codes_warning": "Hãy viết lại mã và cất ở một nơi an toàn - những mã này sẽ không xuất hiện lại nữa. Nếu mất quyền sử dụng app 2FA app và mã khôi phục, tài khoản của bạn sẽ không thể truy cập."
|
||||
},
|
||||
"allow_following_move": "Cho phép tự động theo dõi lại khi tài khoản đang theo dõi chuyển sang máy chủ khác",
|
||||
"attachmentRadius": "Tập tin tải lên",
|
||||
"attachments": "Tập tin tải lên",
|
||||
"avatar": "Ảnh đại diện",
|
||||
"avatarAltRadius": "Ảnh đại diện (thông báo)",
|
||||
"avatarRadius": "Ảnh đại diện",
|
||||
"background": "Ảnh nền",
|
||||
"bio": "Tiểu sử",
|
||||
"block_export": "Xuất danh sách chặn",
|
||||
"block_import": "Nhập danh sách chặn",
|
||||
"block_import_error": "Lỗi khi nhập danh sách chặn",
|
||||
"mute_export": "Xuất danh sách ẩn",
|
||||
"mute_export_button": "Xuất danh sách ẩn ra tập tin CSV",
|
||||
"mute_import": "Nhập danh sách ẩn",
|
||||
"mute_import_error": "Lỗi khi nhập danh sách ẩn",
|
||||
"mutes_imported": "Đã nhập danh sách ẩn! Sẽ mất một lúc nữa để hoàn thành.",
|
||||
"import_mutes_from_a_csv_file": "Nhập danh sách ẩn từ tập tin CSV",
|
||||
"blocks_tab": "Danh sách chặn",
|
||||
"bot": "Đây là tài khoản Bot",
|
||||
"btnRadius": "Nút",
|
||||
"cBlue": "Xanh (Trả lời, theo dõi)",
|
||||
"cOrange": "Cam (Thích)",
|
||||
"cRed": "Đỏ (Hủy bỏ)",
|
||||
"change_email": "Đổi email",
|
||||
"change_email_error": "Có lỗi xảy ra khi đổi email.",
|
||||
"changed_email": "Đã đổi email thành công!",
|
||||
"change_password": "Đổi mật khẩu",
|
||||
"changed_password": "Đổi mật khẩu thành công!",
|
||||
"chatMessageRadius": "Tin nhắn chat",
|
||||
"follows_imported": "Đã nhập danh sách theo dõi! Sẽ mất một lúc nữa để hoàn thành.",
|
||||
"collapse_subject": "Thu gọn những tút có tựa đề",
|
||||
"composing": "Thu gọn",
|
||||
"current_password": "Mật khẩu cũ",
|
||||
"mutes_and_blocks": "Ẩn và Chặn",
|
||||
"data_import_export_tab": "Nhập / Xuất dữ liệu",
|
||||
"default_vis": "Kiểu đăng tút mặc định",
|
||||
"delete_account": "Xóa tài khoản",
|
||||
"delete_account_error": "Có lỗi khi xóa tài khoản. Xin liên hệ quản trị viên máy chủ để tìm hiểu.",
|
||||
"delete_account_instructions": "Nhập mật khẩu bên dưới để xác nhận.",
|
||||
"domain_mutes": "Máy chủ",
|
||||
"avatar_size_instruction": "Kích cỡ tối thiểu 150x150 pixels.",
|
||||
"pad_emoji": "Nhớ chừa khoảng cách khi chèn emoji",
|
||||
"emoji_reactions_on_timeline": "Hiện tương tác emoji trên bảng tin",
|
||||
"export_theme": "Lưu mẫu",
|
||||
"filtering": "Bộ lọc",
|
||||
"filtering_explanation": "Những tút chứa từ sau sẽ bị ẩn, mỗi chữ một hàng",
|
||||
"word_filter": "Bộ lọc từ ngữ",
|
||||
"follow_export": "Xuất danh sách theo dõi",
|
||||
"follow_import": "Nhập danh sách theo dõi",
|
||||
"follow_import_error": "Lỗi khi nhập danh sách theo dõi",
|
||||
"accent": "Màu chủ đạo",
|
||||
"foreground": "Màu phối",
|
||||
"general": "Chung",
|
||||
"hide_attachments_in_convo": "Ẩn tập tin đính kèm trong thảo luận",
|
||||
"hide_media_previews": "Ẩn xem trước media",
|
||||
"hide_all_muted_posts": "Ẩn những tút đã ẩn",
|
||||
"hide_muted_posts": "Ẩn tút từ các người dùng đã ẩn",
|
||||
"max_thumbnails": "Số ảnh xem trước tối đa cho mỗi tút",
|
||||
"hide_isp": "Ẩn thanh bên của máy chủ",
|
||||
"hide_shoutbox": "Ẩn thanh chat máy chủ",
|
||||
"hide_wallpaper": "Ẩn ảnh nền máy chủ",
|
||||
"preload_images": "Tải trước hình ảnh",
|
||||
"use_one_click_nsfw": "Xem nội dung nhạy cảm bằng cách nhấn vào",
|
||||
"hide_user_stats": "Ẩn số liệu người dùng (vd: số người theo dõi)",
|
||||
"hide_filtered_statuses": "Ẩn những tút đã lọc",
|
||||
"import_followers_from_a_csv_file": "Nhập danh sách theo dõi từ tập tin CSV",
|
||||
"import_theme": "Tải mẫu có sẵn",
|
||||
"inputRadius": "Chỗ nhập vào",
|
||||
"checkboxRadius": "Hộp kiểm",
|
||||
"instance_default": "(mặc định: {value})",
|
||||
"instance_default_simple": "(mặc định)",
|
||||
"interface": "Giao diện",
|
||||
"interfaceLanguage": "Ngôn ngữ",
|
||||
"limited_availability": "Trình duyệt không hỗ trợ",
|
||||
"links": "Liên kết",
|
||||
"lock_account_description": "Tự phê duyệt yêu cầu theo dõi",
|
||||
"loop_video": "Lặp lại video",
|
||||
"loop_video_silent_only": "Chỉ lặp lại những video không có âm thanh",
|
||||
"mutes_tab": "Ẩn",
|
||||
"play_videos_in_modal": "Phát video trong khung hình riêng",
|
||||
"file_export_import": {
|
||||
"backup_restore": "Sao lưu",
|
||||
"backup_settings": "Thiết lập sao lưu",
|
||||
"restore_settings": "Khôi phục thiết lập từ tập tin",
|
||||
"errors": {
|
||||
"invalid_file": "Tập tin đã chọn không hỗ trợ bởi Pleroma. Giữ nguyên mọi thay đổi.",
|
||||
"file_too_old": "Phiên bản không tương thích: {fileMajor}, phiên bản tập tin quá cũ và không được hỗ trợ (min. set. ver. {feMajor})",
|
||||
"file_slightly_new": "Phiên bản tập tin khác biệt, không thể áp dụng một vài thay đổi",
|
||||
"file_too_new": "Phiên bản không tương thích: {fileMajor}, phiên bản PleromaFE(settings ver {feMajor}) của máy chủ này quá cũ để sử dụng"
|
||||
},
|
||||
"backup_settings_theme": "Thiết lập sao lưu dữ liệu và giao diện"
|
||||
},
|
||||
"profile_fields": {
|
||||
"label": "Metadata",
|
||||
"add_field": "Thêm mục",
|
||||
"name": "Nhãn",
|
||||
"value": "Nội dung"
|
||||
},
|
||||
"use_contain_fit": "Không cắt ảnh đính kèm trong bản xem trước",
|
||||
"name": "Tên",
|
||||
"name_bio": "Tên & tiểu sử",
|
||||
"new_email": "Email mới",
|
||||
"new_password": "Mật khẩu mới",
|
||||
"notification_visibility_follows": "Theo dõi",
|
||||
"notification_visibility_mentions": "Lượt nhắc",
|
||||
"notification_visibility_repeats": "Chia sẻ",
|
||||
"notification_visibility_moves": "Chuyển máy chủ",
|
||||
"notification_visibility_emoji_reactions": "Tương tác",
|
||||
"no_blocks": "Không có chặn",
|
||||
"no_mutes": "Không có ẩn",
|
||||
"hide_follows_description": "Ẩn danh sách những người tôi theo dõi",
|
||||
"hide_followers_description": "Ẩn danh sách những người theo dõi tôi",
|
||||
"hide_followers_count_description": "Ẩn số lượng người theo dõi tôi",
|
||||
"show_admin_badge": "Hiện huy hiệu \"Quản trị viên\" trên trang của tôi",
|
||||
"show_moderator_badge": "Hiện huy hiệu \"Kiểm duyệt viên\" trên trang của tôi",
|
||||
"oauth_tokens": "OAuth tokens",
|
||||
"token": "Token",
|
||||
"refresh_token": "Làm tươi token",
|
||||
"valid_until": "Có giá trị tới",
|
||||
"revoke_token": "Gỡ",
|
||||
"panelRadius": "Panels",
|
||||
"pause_on_unfocused": "Dừng phát khi đang lướt các tút khác",
|
||||
"presets": "Mẫu có sẵn",
|
||||
"profile_background": "Ảnh nền trang cá nhân",
|
||||
"profile_banner": "Ảnh bìa trang cá nhân",
|
||||
"profile_tab": "Trang cá nhân",
|
||||
"radii_help": "Thiết lập góc bo tròn (bằng pixels)",
|
||||
"replies_in_timeline": "Trả lời trong bảng tin",
|
||||
"reply_visibility_all": "Hiện toàn bộ trả lời",
|
||||
"reply_visibility_self": "Chỉ hiện những trả lời có nhắc tới tôi",
|
||||
"reply_visibility_following_short": "Hiện trả lời có những người tôi theo dõi",
|
||||
"reply_visibility_self_short": "Hiện trả lời của bản thân",
|
||||
"setting_changed": "Thiết lập khác với mặc định",
|
||||
"block_export_button": "Xuất danh sách chặn ra tập tin CSV",
|
||||
"blocks_imported": "Đã nhập danh sách chặn! Sẽ mất một lúc nữa để hoàn thành.",
|
||||
"cGreen": "Green (Chia sẻ)",
|
||||
"change_password_error": "Có lỗi xảy ra khi đổi mật khẩu.",
|
||||
"confirm_new_password": "Xác nhận mật khẩu mới",
|
||||
"delete_account_description": "Xóa vĩnh viễn mọi dữ liệu và vô hiệu hóa tài khoản của bạn.",
|
||||
"discoverable": "Hiện tài khoản trong công cụ tìm kiếm và những tính năng khác",
|
||||
"follow_export_button": "Xuất danh sách theo dõi ra tập tin CSV",
|
||||
"hide_attachments_in_tl": "Ẩn tập tin đính kèm trong bảng tin",
|
||||
"right_sidebar": "Hiện thanh bên bên phải",
|
||||
"hide_post_stats": "Ẩn tương tác của tút (vd: số lượt thích)",
|
||||
"import_blocks_from_a_csv_file": "Nhập danh sách chặn từ tập tin CSV",
|
||||
"invalid_theme_imported": "Tập tin đã chọn không hỗ trợ bởi Pleroma. Giao diện của bạn sẽ giữ nguyên.",
|
||||
"notification_visibility": "Những loại thông báo sẽ hiện",
|
||||
"notification_visibility_likes": "Thích",
|
||||
"no_rich_text_description": "Không hiện rich text trong các tút",
|
||||
"hide_follows_count_description": "Ẩn số lượng người tôi theo dõi",
|
||||
"nsfw_clickthrough": "Cho phép nhấn vào xem các tút nhạy cảm",
|
||||
"reply_visibility_following": "Chỉ hiện những trả lời có nhắc tới tôi hoặc từ những người mà tôi theo dõi"
|
||||
},
|
||||
"errors": {
|
||||
"storage_unavailable": "Pleroma không thể truy cập lưu trữ trình duyệt. Thông tin đăng nhập và những thiết lập tạm thời sẽ bị mất. Hãy cho phép cookies."
|
||||
}
|
||||
}
|
|
@ -43,7 +43,10 @@
|
|||
"role": {
|
||||
"moderator": "监察员",
|
||||
"admin": "管理员"
|
||||
}
|
||||
},
|
||||
"flash_content": "点击以使用 Ruffle 显示 Flash 内容(实验性,可能无效)。",
|
||||
"flash_security": "注意这可能有潜在的危险,因为 Flash 内容仍然是任意的代码。",
|
||||
"flash_fail": "Flash 内容加载失败,请在控制台查看详情。"
|
||||
},
|
||||
"image_cropper": {
|
||||
"crop_picture": "裁剪图片",
|
||||
|
@ -584,7 +587,9 @@
|
|||
"backup_settings_theme": "备份设置和主题到文件",
|
||||
"backup_settings": "备份设置到文件",
|
||||
"backup_restore": "设置备份"
|
||||
}
|
||||
},
|
||||
"right_sidebar": "在右侧显示侧边栏",
|
||||
"hide_shoutbox": "隐藏实例留言板"
|
||||
},
|
||||
"time": {
|
||||
"day": "{0} 天",
|
||||
|
@ -724,7 +729,8 @@
|
|||
"striped": "条纹背景",
|
||||
"solid": "单一颜色背景",
|
||||
"disabled": "不突出显示"
|
||||
}
|
||||
},
|
||||
"edit_profile": "编辑个人资料"
|
||||
},
|
||||
"user_profile": {
|
||||
"timeline_title": "用户时间线",
|
||||
|
|
|
@ -115,7 +115,10 @@
|
|||
"role": {
|
||||
"moderator": "主持人",
|
||||
"admin": "管理員"
|
||||
}
|
||||
},
|
||||
"flash_content": "點擊以使用 Ruffle 顯示 Flash 內容(實驗性,可能無效)。",
|
||||
"flash_security": "請注意,這可能有潜在的危險,因為Flash內容仍然是武斷的程式碼。",
|
||||
"flash_fail": "無法加載flash內容,請參閱控制台瞭解詳細資訊。"
|
||||
},
|
||||
"finder": {
|
||||
"find_user": "尋找用戶",
|
||||
|
@ -556,7 +559,9 @@
|
|||
"backup_settings": "備份設置到文件",
|
||||
"backup_restore": "設定備份"
|
||||
},
|
||||
"sensitive_by_default": "默認標記發文為敏感內容"
|
||||
"sensitive_by_default": "默認標記發文為敏感內容",
|
||||
"right_sidebar": "在右側顯示側邊欄",
|
||||
"hide_shoutbox": "隱藏實例留言框"
|
||||
},
|
||||
"chats": {
|
||||
"more": "更多",
|
||||
|
@ -797,7 +802,8 @@
|
|||
"striped": "條紋背景",
|
||||
"side": "彩條"
|
||||
},
|
||||
"bot": "機器人"
|
||||
"bot": "機器人",
|
||||
"edit_profile": "編輯個人資料"
|
||||
},
|
||||
"user_profile": {
|
||||
"timeline_title": "用戶時間線",
|
||||
|
|
|
@ -35,6 +35,7 @@ export const defaultState = {
|
|||
loopVideoSilentOnly: true,
|
||||
streaming: false,
|
||||
emojiReactionsOnTimeline: true,
|
||||
alwaysShowNewPostButton: false,
|
||||
autohideFloatingPostButton: false,
|
||||
pauseOnUnfocused: true,
|
||||
stopGifs: false,
|
||||
|
|
|
@ -246,6 +246,11 @@ export const getters = {
|
|||
}
|
||||
return result
|
||||
},
|
||||
findUserByUrl: state => query => {
|
||||
return state.users
|
||||
.find(u => u.statusnet_profile_url &&
|
||||
u.statusnet_profile_url.toLowerCase() === query.toLowerCase())
|
||||
},
|
||||
relationship: state => id => {
|
||||
const rel = id && state.relationships[id]
|
||||
return rel || { id, loading: true }
|
||||
|
|
|
@ -54,17 +54,19 @@ export const parseUser = (data) => {
|
|||
return output
|
||||
}
|
||||
|
||||
output.emoji = data.emojis
|
||||
output.name = data.display_name
|
||||
output.name_html = addEmojis(escape(data.display_name), data.emojis)
|
||||
output.name_html = escape(data.display_name)
|
||||
|
||||
output.description = data.note
|
||||
output.description_html = addEmojis(data.note, data.emojis)
|
||||
// TODO cleanup this shit, output.description is overriden with source data
|
||||
output.description_html = data.note
|
||||
|
||||
output.fields = data.fields
|
||||
output.fields_html = data.fields.map(field => {
|
||||
return {
|
||||
name: addEmojis(escape(field.name), data.emojis),
|
||||
value: addEmojis(field.value, data.emojis)
|
||||
name: escape(field.name),
|
||||
value: field.value
|
||||
}
|
||||
})
|
||||
output.fields_text = data.fields.map(field => {
|
||||
|
@ -239,16 +241,6 @@ export const parseAttachment = (data) => {
|
|||
|
||||
return output
|
||||
}
|
||||
export const addEmojis = (string, emojis) => {
|
||||
const matchOperatorsRegex = /[|\\{}()[\]^$+*?.-]/g
|
||||
return emojis.reduce((acc, emoji) => {
|
||||
const regexSafeShortCode = emoji.shortcode.replace(matchOperatorsRegex, '\\$&')
|
||||
return acc.replace(
|
||||
new RegExp(`:${regexSafeShortCode}:`, 'g'),
|
||||
`<img src='${emoji.url}' alt=':${emoji.shortcode}:' title=':${emoji.shortcode}:' class='emoji' />`
|
||||
)
|
||||
}, string)
|
||||
}
|
||||
|
||||
export const parseStatus = (data) => {
|
||||
const output = {}
|
||||
|
@ -266,7 +258,8 @@ export const parseStatus = (data) => {
|
|||
output.type = data.reblog ? 'retweet' : 'status'
|
||||
output.nsfw = data.sensitive
|
||||
|
||||
output.statusnet_html = addEmojis(data.content, data.emojis)
|
||||
output.raw_html = data.content
|
||||
output.emojis = data.emojis
|
||||
|
||||
output.tags = data.tags
|
||||
|
||||
|
@ -293,13 +286,13 @@ export const parseStatus = (data) => {
|
|||
output.retweeted_status = parseStatus(data.reblog)
|
||||
}
|
||||
|
||||
output.summary_html = addEmojis(escape(data.spoiler_text), data.emojis)
|
||||
output.summary_raw_html = escape(data.spoiler_text)
|
||||
output.external_url = data.url
|
||||
output.poll = data.poll
|
||||
if (output.poll) {
|
||||
output.poll.options = (output.poll.options || []).map(field => ({
|
||||
...field,
|
||||
title_html: addEmojis(escape(field.title), data.emojis)
|
||||
title_html: escape(field.title)
|
||||
}))
|
||||
}
|
||||
output.pinned = data.pinned
|
||||
|
@ -325,7 +318,7 @@ export const parseStatus = (data) => {
|
|||
output.nsfw = data.nsfw
|
||||
}
|
||||
|
||||
output.statusnet_html = data.statusnet_html
|
||||
output.raw_html = data.statusnet_html
|
||||
output.text = data.text
|
||||
|
||||
output.in_reply_to_status_id = data.in_reply_to_status_id
|
||||
|
@ -444,11 +437,8 @@ export const parseChatMessage = (message) => {
|
|||
output.id = message.id
|
||||
output.created_at = new Date(message.created_at)
|
||||
output.chat_id = message.chat_id
|
||||
if (message.content) {
|
||||
output.content = addEmojis(message.content, message.emojis)
|
||||
} else {
|
||||
output.content = ''
|
||||
}
|
||||
output.emojis = message.emojis
|
||||
output.content = message.content
|
||||
if (message.attachment) {
|
||||
output.attachments = [parseAttachment(message.attachment)]
|
||||
} else {
|
||||
|
|
|
@ -1,27 +1,30 @@
|
|||
import { find } from 'lodash'
|
||||
|
||||
const createFaviconService = () => {
|
||||
let favimg, favcanvas, favcontext, favicon
|
||||
const favicons = []
|
||||
const faviconWidth = 128
|
||||
const faviconHeight = 128
|
||||
const badgeRadius = 32
|
||||
|
||||
const initFaviconService = () => {
|
||||
const nodes = document.getElementsByTagName('link')
|
||||
favicon = find(nodes, node => node.rel === 'icon')
|
||||
const nodes = document.querySelectorAll('link[rel="icon"]')
|
||||
nodes.forEach(favicon => {
|
||||
if (favicon) {
|
||||
favcanvas = document.createElement('canvas')
|
||||
const favcanvas = document.createElement('canvas')
|
||||
favcanvas.width = faviconWidth
|
||||
favcanvas.height = faviconHeight
|
||||
favimg = new Image()
|
||||
const favimg = new Image()
|
||||
favimg.crossOrigin = 'anonymous'
|
||||
favimg.src = favicon.href
|
||||
favcontext = favcanvas.getContext('2d')
|
||||
const favcontext = favcanvas.getContext('2d')
|
||||
favicons.push({ favcanvas, favimg, favcontext, favicon })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const isImageLoaded = (img) => img.complete && img.naturalHeight !== 0
|
||||
|
||||
const clearFaviconBadge = () => {
|
||||
if (favicons.length === 0) return
|
||||
favicons.forEach(({ favimg, favcanvas, favcontext, favicon }) => {
|
||||
if (!favimg || !favcontext || !favicon) return
|
||||
|
||||
favcontext.clearRect(0, 0, faviconWidth, faviconHeight)
|
||||
|
@ -29,12 +32,14 @@ const createFaviconService = () => {
|
|||
favcontext.drawImage(favimg, 0, 0, favimg.width, favimg.height, 0, 0, faviconWidth, faviconHeight)
|
||||
}
|
||||
favicon.href = favcanvas.toDataURL('image/png')
|
||||
})
|
||||
}
|
||||
|
||||
const drawFaviconBadge = () => {
|
||||
if (!favimg || !favcontext || !favcontext) return
|
||||
|
||||
if (favicons.length === 0) return
|
||||
clearFaviconBadge()
|
||||
favicons.forEach(({ favimg, favcanvas, favcontext, favicon }) => {
|
||||
if (!favimg || !favcontext || !favcontext) return
|
||||
|
||||
const style = getComputedStyle(document.body)
|
||||
const badgeColor = `${style.getPropertyValue('--badgeNotification') || 'rgb(240, 100, 100)'}`
|
||||
|
@ -47,6 +52,7 @@ const createFaviconService = () => {
|
|||
favcontext.arc(faviconWidth - badgeRadius, badgeRadius, badgeRadius, 0, 2 * Math.PI, false)
|
||||
favcontext.fill()
|
||||
favicon.href = favcanvas.toDataURL('image/png')
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
136
src/services/html_converter/html_line_converter.service.js
Normal file
136
src/services/html_converter/html_line_converter.service.js
Normal file
|
@ -0,0 +1,136 @@
|
|||
import { getTagName } from './utility.service.js'
|
||||
|
||||
/**
|
||||
* This is a tiny purpose-built HTML parser/processor. This basically detects
|
||||
* any type of visual newline and converts entire HTML into a array structure.
|
||||
*
|
||||
* Text nodes are represented as object with single property - text - containing
|
||||
* the visual line. Intended usage is to process the array with .map() in which
|
||||
* map function returns a string and resulting array can be converted back to html
|
||||
* with a .join('').
|
||||
*
|
||||
* Generally this isn't very useful except for when you really need to either
|
||||
* modify visual lines (greentext i.e. simple quoting) or do something with
|
||||
* first/last line.
|
||||
*
|
||||
* known issue: doesn't handle CDATA so nested CDATA might not work well
|
||||
*
|
||||
* @param {Object} input - input data
|
||||
* @return {(string|{ text: string })[]} processed html in form of a list.
|
||||
*/
|
||||
export const convertHtmlToLines = (html = '') => {
|
||||
// Elements that are implicitly self-closing
|
||||
// https://developer.mozilla.org/en-US/docs/Glossary/empty_element
|
||||
const emptyElements = new Set([
|
||||
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
|
||||
'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr'
|
||||
])
|
||||
// Block-level element (they make a visual line)
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Block-level_elements
|
||||
const blockElements = new Set([
|
||||
'address', 'article', 'aside', 'blockquote', 'details', 'dialog', 'dd',
|
||||
'div', 'dl', 'dt', 'fieldset', 'figcaption', 'figure', 'footer', 'form',
|
||||
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hgroup', 'hr', 'li', 'main',
|
||||
'nav', 'ol', 'p', 'pre', 'section', 'table', 'ul'
|
||||
])
|
||||
// br is very weird in a way that it's technically not block-level, it's
|
||||
// essentially converted to a \n (or \r\n). There's also wbr but it doesn't
|
||||
// guarantee linebreak, only suggest it.
|
||||
const linebreakElements = new Set(['br'])
|
||||
|
||||
const visualLineElements = new Set([
|
||||
...blockElements.values(),
|
||||
...linebreakElements.values()
|
||||
])
|
||||
|
||||
// All block-level elements that aren't empty elements, i.e. not <hr>
|
||||
const nonEmptyElements = new Set(visualLineElements)
|
||||
// Difference
|
||||
for (let elem of emptyElements) {
|
||||
nonEmptyElements.delete(elem)
|
||||
}
|
||||
|
||||
// All elements that we are recognizing
|
||||
const allElements = new Set([
|
||||
...nonEmptyElements.values(),
|
||||
...emptyElements.values()
|
||||
])
|
||||
|
||||
let buffer = [] // Current output buffer
|
||||
const level = [] // How deep we are in tags and which tags were there
|
||||
let textBuffer = '' // Current line content
|
||||
let tagBuffer = null // Current tag buffer, if null = we are not currently reading a tag
|
||||
|
||||
const flush = () => { // Processes current line buffer, adds it to output buffer and clears line buffer
|
||||
if (textBuffer.trim().length > 0) {
|
||||
buffer.push({ level: [...level], text: textBuffer })
|
||||
} else {
|
||||
buffer.push(textBuffer)
|
||||
}
|
||||
textBuffer = ''
|
||||
}
|
||||
|
||||
const handleBr = (tag) => { // handles single newlines/linebreaks/selfclosing
|
||||
flush()
|
||||
buffer.push(tag)
|
||||
}
|
||||
|
||||
const handleOpen = (tag) => { // handles opening tags
|
||||
flush()
|
||||
buffer.push(tag)
|
||||
level.unshift(getTagName(tag))
|
||||
}
|
||||
|
||||
const handleClose = (tag) => { // handles closing tags
|
||||
if (level[0] === getTagName(tag)) {
|
||||
flush()
|
||||
buffer.push(tag)
|
||||
level.shift()
|
||||
} else { // Broken case
|
||||
textBuffer += tag
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < html.length; i++) {
|
||||
const char = html[i]
|
||||
if (char === '<' && tagBuffer === null) {
|
||||
tagBuffer = char
|
||||
} else if (char !== '>' && tagBuffer !== null) {
|
||||
tagBuffer += char
|
||||
} else if (char === '>' && tagBuffer !== null) {
|
||||
tagBuffer += char
|
||||
const tagFull = tagBuffer
|
||||
tagBuffer = null
|
||||
const tagName = getTagName(tagFull)
|
||||
if (allElements.has(tagName)) {
|
||||
if (linebreakElements.has(tagName)) {
|
||||
handleBr(tagFull)
|
||||
} else if (nonEmptyElements.has(tagName)) {
|
||||
if (tagFull[1] === '/') {
|
||||
handleClose(tagFull)
|
||||
} else if (tagFull[tagFull.length - 2] === '/') {
|
||||
// self-closing
|
||||
handleBr(tagFull)
|
||||
} else {
|
||||
handleOpen(tagFull)
|
||||
}
|
||||
} else {
|
||||
textBuffer += tagFull
|
||||
}
|
||||
} else {
|
||||
textBuffer += tagFull
|
||||
}
|
||||
} else if (char === '\n') {
|
||||
handleBr(char)
|
||||
} else {
|
||||
textBuffer += char
|
||||
}
|
||||
}
|
||||
if (tagBuffer) {
|
||||
textBuffer += tagBuffer
|
||||
}
|
||||
|
||||
flush()
|
||||
|
||||
return buffer
|
||||
}
|
97
src/services/html_converter/html_tree_converter.service.js
Normal file
97
src/services/html_converter/html_tree_converter.service.js
Normal file
|
@ -0,0 +1,97 @@
|
|||
import { getTagName } from './utility.service.js'
|
||||
|
||||
/**
|
||||
* This is a not-so-tiny purpose-built HTML parser/processor. This parses html
|
||||
* and converts it into a tree structure representing tag openers/closers and
|
||||
* children.
|
||||
*
|
||||
* Structure follows this pattern: [opener, [...children], closer] except root
|
||||
* node which is just [...children]. Text nodes can only be within children and
|
||||
* are represented as strings.
|
||||
*
|
||||
* Intended use is to convert HTML structure and then recursively iterate over it
|
||||
* most likely using a map. Very useful for dynamically rendering html replacing
|
||||
* tags with JSX elements in a render function.
|
||||
*
|
||||
* known issue: doesn't handle CDATA so CDATA might not work well
|
||||
* known issue: doesn't handle HTML comments
|
||||
*
|
||||
* @param {Object} input - input data
|
||||
* @return {string} processed html
|
||||
*/
|
||||
export const convertHtmlToTree = (html = '') => {
|
||||
// Elements that are implicitly self-closing
|
||||
// https://developer.mozilla.org/en-US/docs/Glossary/empty_element
|
||||
const emptyElements = new Set([
|
||||
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
|
||||
'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr'
|
||||
])
|
||||
// TODO For future - also parse HTML5 multi-source components?
|
||||
|
||||
const buffer = [] // Current output buffer
|
||||
const levels = [['', buffer]] // How deep we are in tags and which tags were there
|
||||
let textBuffer = '' // Current line content
|
||||
let tagBuffer = null // Current tag buffer, if null = we are not currently reading a tag
|
||||
|
||||
const getCurrentBuffer = () => {
|
||||
return levels[levels.length - 1][1]
|
||||
}
|
||||
|
||||
const flushText = () => { // Processes current line buffer, adds it to output buffer and clears line buffer
|
||||
if (textBuffer === '') return
|
||||
getCurrentBuffer().push(textBuffer)
|
||||
textBuffer = ''
|
||||
}
|
||||
|
||||
const handleSelfClosing = (tag) => {
|
||||
getCurrentBuffer().push([tag])
|
||||
}
|
||||
|
||||
const handleOpen = (tag) => {
|
||||
const curBuf = getCurrentBuffer()
|
||||
const newLevel = [tag, []]
|
||||
levels.push(newLevel)
|
||||
curBuf.push(newLevel)
|
||||
}
|
||||
|
||||
const handleClose = (tag) => {
|
||||
const currentTag = levels[levels.length - 1]
|
||||
if (getTagName(levels[levels.length - 1][0]) === getTagName(tag)) {
|
||||
currentTag.push(tag)
|
||||
levels.pop()
|
||||
} else {
|
||||
getCurrentBuffer().push(tag)
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < html.length; i++) {
|
||||
const char = html[i]
|
||||
if (char === '<' && tagBuffer === null) {
|
||||
flushText()
|
||||
tagBuffer = char
|
||||
} else if (char !== '>' && tagBuffer !== null) {
|
||||
tagBuffer += char
|
||||
} else if (char === '>' && tagBuffer !== null) {
|
||||
tagBuffer += char
|
||||
const tagFull = tagBuffer
|
||||
tagBuffer = null
|
||||
const tagName = getTagName(tagFull)
|
||||
if (tagFull[1] === '/') {
|
||||
handleClose(tagFull)
|
||||
} else if (emptyElements.has(tagName) || tagFull[tagFull.length - 2] === '/') {
|
||||
// self-closing
|
||||
handleSelfClosing(tagFull)
|
||||
} else {
|
||||
handleOpen(tagFull)
|
||||
}
|
||||
} else {
|
||||
textBuffer += char
|
||||
}
|
||||
}
|
||||
if (tagBuffer) {
|
||||
textBuffer += tagBuffer
|
||||
}
|
||||
|
||||
flushText()
|
||||
return buffer
|
||||
}
|
73
src/services/html_converter/utility.service.js
Normal file
73
src/services/html_converter/utility.service.js
Normal file
|
@ -0,0 +1,73 @@
|
|||
/**
|
||||
* Extract tag name from tag opener/closer.
|
||||
*
|
||||
* @param {String} tag - tag string, i.e. '<a href="...">'
|
||||
* @return {String} - tagname, i.e. "div"
|
||||
*/
|
||||
export const getTagName = (tag) => {
|
||||
const result = /(?:<\/(\w+)>|<(\w+)\s?.*?\/?>)/gi.exec(tag)
|
||||
return result && (result[1] || result[2])
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract attributes from tag opener.
|
||||
*
|
||||
* @param {String} tag - tag string, i.e. '<a href="...">'
|
||||
* @return {Object} - map of attributes key = attribute name, value = attribute value
|
||||
* attributes without values represented as boolean true
|
||||
*/
|
||||
export const getAttrs = tag => {
|
||||
const innertag = tag
|
||||
.substring(1, tag.length - 1)
|
||||
.replace(new RegExp('^' + getTagName(tag)), '')
|
||||
.replace(/\/?$/, '')
|
||||
.trim()
|
||||
const attrs = Array.from(innertag.matchAll(/([a-z0-9-]+)(?:=("[^"]+?"|'[^']+?'))?/gi))
|
||||
.map(([trash, key, value]) => [key, value])
|
||||
.map(([k, v]) => {
|
||||
if (!v) return [k, true]
|
||||
return [k, v.substring(1, v.length - 1)]
|
||||
})
|
||||
return Object.fromEntries(attrs)
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds shortcodes in text
|
||||
*
|
||||
* @param {String} text - original text to find emojis in
|
||||
* @param {{ url: String, shortcode: Sring }[]} emoji - list of shortcodes to find
|
||||
* @param {Function} processor - function to call on each encountered emoji,
|
||||
* function is passed single object containing matching emoji ({ url, shortcode })
|
||||
* return value will be inserted into resulting array instead of :shortcode:
|
||||
* @return {Array} resulting array with non-emoji parts of text and whatever {processor}
|
||||
* returned for emoji
|
||||
*/
|
||||
export const processTextForEmoji = (text, emojis, processor) => {
|
||||
const buffer = []
|
||||
let textBuffer = ''
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const char = text[i]
|
||||
if (char === ':') {
|
||||
const next = text.slice(i + 1)
|
||||
let found = false
|
||||
for (let emoji of emojis) {
|
||||
if (next.slice(0, emoji.shortcode.length + 1) === (emoji.shortcode + ':')) {
|
||||
found = emoji
|
||||
break
|
||||
}
|
||||
}
|
||||
if (found) {
|
||||
buffer.push(textBuffer)
|
||||
textBuffer = ''
|
||||
buffer.push(processor(found))
|
||||
i += found.shortcode.length + 1
|
||||
} else {
|
||||
textBuffer += char
|
||||
}
|
||||
} else {
|
||||
textBuffer += char
|
||||
}
|
||||
}
|
||||
if (textBuffer) buffer.push(textBuffer)
|
||||
return buffer
|
||||
}
|
|
@ -369,6 +369,12 @@ export const SLOT_INHERITANCE = {
|
|||
textColor: 'preserve'
|
||||
},
|
||||
|
||||
postCyantext: {
|
||||
depends: ['cBlue'],
|
||||
layer: 'bg',
|
||||
textColor: 'preserve'
|
||||
},
|
||||
|
||||
border: {
|
||||
depends: ['fg'],
|
||||
opacity: 'border',
|
||||
|
|
|
@ -1,94 +0,0 @@
|
|||
/**
|
||||
* This is a tiny purpose-built HTML parser/processor. This basically detects any type of visual newline and
|
||||
* allows it to be processed, useful for greentexting, mostly
|
||||
*
|
||||
* known issue: doesn't handle CDATA so nested CDATA might not work well
|
||||
*
|
||||
* @param {Object} input - input data
|
||||
* @param {(string) => string} processor - function that will be called on every line
|
||||
* @return {string} processed html
|
||||
*/
|
||||
export const processHtml = (html, processor) => {
|
||||
const handledTags = new Set(['p', 'br', 'div'])
|
||||
const openCloseTags = new Set(['p', 'div'])
|
||||
|
||||
let buffer = '' // Current output buffer
|
||||
const level = [] // How deep we are in tags and which tags were there
|
||||
let textBuffer = '' // Current line content
|
||||
let tagBuffer = null // Current tag buffer, if null = we are not currently reading a tag
|
||||
|
||||
// Extracts tag name from tag, i.e. <span a="b"> => span
|
||||
const getTagName = (tag) => {
|
||||
const result = /(?:<\/(\w+)>|<(\w+)\s?[^/]*?\/?>)/gi.exec(tag)
|
||||
return result && (result[1] || result[2])
|
||||
}
|
||||
|
||||
const flush = () => { // Processes current line buffer, adds it to output buffer and clears line buffer
|
||||
if (textBuffer.trim().length > 0) {
|
||||
buffer += processor(textBuffer)
|
||||
} else {
|
||||
buffer += textBuffer
|
||||
}
|
||||
textBuffer = ''
|
||||
}
|
||||
|
||||
const handleBr = (tag) => { // handles single newlines/linebreaks/selfclosing
|
||||
flush()
|
||||
buffer += tag
|
||||
}
|
||||
|
||||
const handleOpen = (tag) => { // handles opening tags
|
||||
flush()
|
||||
buffer += tag
|
||||
level.push(tag)
|
||||
}
|
||||
|
||||
const handleClose = (tag) => { // handles closing tags
|
||||
flush()
|
||||
buffer += tag
|
||||
if (level[level.length - 1] === tag) {
|
||||
level.pop()
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < html.length; i++) {
|
||||
const char = html[i]
|
||||
if (char === '<' && tagBuffer === null) {
|
||||
tagBuffer = char
|
||||
} else if (char !== '>' && tagBuffer !== null) {
|
||||
tagBuffer += char
|
||||
} else if (char === '>' && tagBuffer !== null) {
|
||||
tagBuffer += char
|
||||
const tagFull = tagBuffer
|
||||
tagBuffer = null
|
||||
const tagName = getTagName(tagFull)
|
||||
if (handledTags.has(tagName)) {
|
||||
if (tagName === 'br') {
|
||||
handleBr(tagFull)
|
||||
} else if (openCloseTags.has(tagName)) {
|
||||
if (tagFull[1] === '/') {
|
||||
handleClose(tagFull)
|
||||
} else if (tagFull[tagFull.length - 2] === '/') {
|
||||
// self-closing
|
||||
handleBr(tagFull)
|
||||
} else {
|
||||
handleOpen(tagFull)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
textBuffer += tagFull
|
||||
}
|
||||
} else if (char === '\n') {
|
||||
handleBr(char)
|
||||
} else {
|
||||
textBuffer += char
|
||||
}
|
||||
}
|
||||
if (tagBuffer) {
|
||||
textBuffer += tagBuffer
|
||||
}
|
||||
|
||||
flush()
|
||||
|
||||
return buffer
|
||||
}
|
|
@ -8,6 +8,11 @@ const highlightStyle = (prefs) => {
|
|||
const solidColor = `rgb(${Math.floor(rgb.r)}, ${Math.floor(rgb.g)}, ${Math.floor(rgb.b)})`
|
||||
const tintColor = `rgba(${Math.floor(rgb.r)}, ${Math.floor(rgb.g)}, ${Math.floor(rgb.b)}, .1)`
|
||||
const tintColor2 = `rgba(${Math.floor(rgb.r)}, ${Math.floor(rgb.g)}, ${Math.floor(rgb.b)}, .2)`
|
||||
const customProps = {
|
||||
'--____highlight-solidColor': solidColor,
|
||||
'--____highlight-tintColor': tintColor,
|
||||
'--____highlight-tintColor2': tintColor2
|
||||
}
|
||||
if (type === 'striped') {
|
||||
return {
|
||||
backgroundImage: [
|
||||
|
@ -17,11 +22,13 @@ const highlightStyle = (prefs) => {
|
|||
`${tintColor2} 20px,`,
|
||||
`${tintColor2} 40px`
|
||||
].join(' '),
|
||||
backgroundPosition: '0 0'
|
||||
backgroundPosition: '0 0',
|
||||
...customProps
|
||||
}
|
||||
} else if (type === 'solid') {
|
||||
return {
|
||||
backgroundColor: tintColor2
|
||||
backgroundColor: tintColor2,
|
||||
...customProps
|
||||
}
|
||||
} else if (type === 'side') {
|
||||
return {
|
||||
|
@ -31,7 +38,8 @@ const highlightStyle = (prefs) => {
|
|||
`${solidColor} 2px,`,
|
||||
`transparent 6px`
|
||||
].join(' '),
|
||||
backgroundPosition: '0 0'
|
||||
backgroundPosition: '0 0',
|
||||
...customProps
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
480
test/unit/specs/components/rich_content.spec.js
Normal file
480
test/unit/specs/components/rich_content.spec.js
Normal file
|
@ -0,0 +1,480 @@
|
|||
import { mount, shallowMount, createLocalVue } from '@vue/test-utils'
|
||||
import RichContent from 'src/components/rich_content/rich_content.jsx'
|
||||
|
||||
const localVue = createLocalVue()
|
||||
const attentions = []
|
||||
|
||||
const makeMention = (who) => {
|
||||
attentions.push({ statusnet_profile_url: `https://fake.tld/@${who}` })
|
||||
return `<span class="h-card"><a class="u-url mention" href="https://fake.tld/@${who}">@<span>${who}</span></a></span>`
|
||||
}
|
||||
const p = (...data) => `<p>${data.join('')}</p>`
|
||||
const compwrap = (...data) => `<span class="RichContent">${data.join('')}</span>`
|
||||
const mentionsLine = (times) => [
|
||||
'<mentionsline-stub mentions="',
|
||||
new Array(times).fill('[object Object]').join(','),
|
||||
'"></mentionsline-stub>'
|
||||
].join('')
|
||||
|
||||
describe('RichContent', () => {
|
||||
it('renders simple post without exploding', () => {
|
||||
const html = p('Hello world!')
|
||||
const wrapper = shallowMount(RichContent, {
|
||||
localVue,
|
||||
propsData: {
|
||||
attentions,
|
||||
handleLinks: true,
|
||||
greentext: true,
|
||||
emoji: [],
|
||||
html
|
||||
}
|
||||
})
|
||||
|
||||
expect(wrapper.html()).to.eql(compwrap(html))
|
||||
})
|
||||
|
||||
it('unescapes everything as needed', () => {
|
||||
const html = [
|
||||
p('Testing 'em all'),
|
||||
'Testing 'em all'
|
||||
].join('')
|
||||
const expected = [
|
||||
p('Testing \'em all'),
|
||||
'Testing \'em all'
|
||||
].join('')
|
||||
const wrapper = shallowMount(RichContent, {
|
||||
localVue,
|
||||
propsData: {
|
||||
attentions,
|
||||
handleLinks: true,
|
||||
greentext: true,
|
||||
emoji: [],
|
||||
html
|
||||
}
|
||||
})
|
||||
|
||||
expect(wrapper.html()).to.eql(compwrap(expected))
|
||||
})
|
||||
|
||||
it('replaces mention with mentionsline', () => {
|
||||
const html = p(
|
||||
makeMention('John'),
|
||||
' how are you doing today?'
|
||||
)
|
||||
const wrapper = shallowMount(RichContent, {
|
||||
localVue,
|
||||
propsData: {
|
||||
attentions,
|
||||
handleLinks: true,
|
||||
greentext: true,
|
||||
emoji: [],
|
||||
html
|
||||
}
|
||||
})
|
||||
|
||||
expect(wrapper.html()).to.eql(compwrap(p(
|
||||
mentionsLine(1),
|
||||
' how are you doing today?'
|
||||
)))
|
||||
})
|
||||
|
||||
it('replaces mentions at the end of the hellpost', () => {
|
||||
const html = [
|
||||
p('How are you doing today, fine gentlemen?'),
|
||||
p(
|
||||
makeMention('John'),
|
||||
makeMention('Josh'),
|
||||
makeMention('Jeremy')
|
||||
)
|
||||
].join('')
|
||||
const expected = [
|
||||
p(
|
||||
'How are you doing today, fine gentlemen?'
|
||||
),
|
||||
// TODO fix this extra line somehow?
|
||||
p(
|
||||
'<mentionsline-stub mentions="',
|
||||
'[object Object],',
|
||||
'[object Object],',
|
||||
'[object Object]',
|
||||
'"></mentionsline-stub>'
|
||||
)
|
||||
].join('')
|
||||
|
||||
const wrapper = shallowMount(RichContent, {
|
||||
localVue,
|
||||
propsData: {
|
||||
attentions,
|
||||
handleLinks: true,
|
||||
greentext: true,
|
||||
emoji: [],
|
||||
html
|
||||
}
|
||||
})
|
||||
|
||||
expect(wrapper.html()).to.eql(compwrap(expected))
|
||||
})
|
||||
|
||||
it('Does not touch links if link handling is disabled', () => {
|
||||
const html = [
|
||||
[
|
||||
makeMention('Jack'),
|
||||
'let\'s meet up with ',
|
||||
makeMention('Janet')
|
||||
].join(''),
|
||||
[
|
||||
makeMention('John'),
|
||||
makeMention('Josh'),
|
||||
makeMention('Jeremy')
|
||||
].join('')
|
||||
].join('\n')
|
||||
|
||||
const wrapper = shallowMount(RichContent, {
|
||||
localVue,
|
||||
propsData: {
|
||||
attentions,
|
||||
handleLinks: false,
|
||||
greentext: true,
|
||||
emoji: [],
|
||||
html
|
||||
}
|
||||
})
|
||||
|
||||
expect(wrapper.html()).to.eql(compwrap(html))
|
||||
})
|
||||
|
||||
it('Adds greentext and cyantext to the post', () => {
|
||||
const html = [
|
||||
'>preordering videogames',
|
||||
'>any year'
|
||||
].join('\n')
|
||||
const expected = [
|
||||
'<span class="greentext">>preordering videogames</span>',
|
||||
'<span class="greentext">>any year</span>'
|
||||
].join('\n')
|
||||
|
||||
const wrapper = shallowMount(RichContent, {
|
||||
localVue,
|
||||
propsData: {
|
||||
attentions,
|
||||
handleLinks: false,
|
||||
greentext: true,
|
||||
emoji: [],
|
||||
html
|
||||
}
|
||||
})
|
||||
|
||||
expect(wrapper.html()).to.eql(compwrap(expected))
|
||||
})
|
||||
|
||||
it('Does not add greentext and cyantext if setting is set to false', () => {
|
||||
const html = [
|
||||
'>preordering videogames',
|
||||
'>any year'
|
||||
].join('\n')
|
||||
|
||||
const wrapper = shallowMount(RichContent, {
|
||||
localVue,
|
||||
propsData: {
|
||||
attentions,
|
||||
handleLinks: false,
|
||||
greentext: false,
|
||||
emoji: [],
|
||||
html
|
||||
}
|
||||
})
|
||||
|
||||
expect(wrapper.html()).to.eql(compwrap(html))
|
||||
})
|
||||
|
||||
it('Adds emoji to post', () => {
|
||||
const html = p('Ebin :DDDD :spurdo:')
|
||||
const expected = p(
|
||||
'Ebin :DDDD ',
|
||||
'<anonymous-stub alt=":spurdo:" src="about:blank" title=":spurdo:" class="emoji img"></anonymous-stub>'
|
||||
)
|
||||
|
||||
const wrapper = shallowMount(RichContent, {
|
||||
localVue,
|
||||
propsData: {
|
||||
attentions,
|
||||
handleLinks: false,
|
||||
greentext: false,
|
||||
emoji: [{ url: 'about:blank', shortcode: 'spurdo' }],
|
||||
html
|
||||
}
|
||||
})
|
||||
|
||||
expect(wrapper.html()).to.eql(compwrap(expected))
|
||||
})
|
||||
|
||||
it('Doesn\'t add nonexistent emoji to post', () => {
|
||||
const html = p('Lol :lol:')
|
||||
|
||||
const wrapper = shallowMount(RichContent, {
|
||||
localVue,
|
||||
propsData: {
|
||||
attentions,
|
||||
handleLinks: false,
|
||||
greentext: false,
|
||||
emoji: [],
|
||||
html
|
||||
}
|
||||
})
|
||||
|
||||
expect(wrapper.html()).to.eql(compwrap(html))
|
||||
})
|
||||
|
||||
it('Greentext + last mentions', () => {
|
||||
const html = [
|
||||
'>quote',
|
||||
makeMention('lol'),
|
||||
'>quote',
|
||||
'>quote'
|
||||
].join('\n')
|
||||
const expected = [
|
||||
'<span class="greentext">>quote</span>',
|
||||
mentionsLine(1),
|
||||
'<span class="greentext">>quote</span>',
|
||||
'<span class="greentext">>quote</span>'
|
||||
].join('\n')
|
||||
|
||||
const wrapper = shallowMount(RichContent, {
|
||||
localVue,
|
||||
propsData: {
|
||||
attentions,
|
||||
handleLinks: true,
|
||||
greentext: true,
|
||||
emoji: [],
|
||||
html
|
||||
}
|
||||
})
|
||||
|
||||
expect(wrapper.html()).to.eql(compwrap(expected))
|
||||
})
|
||||
|
||||
it('One buggy example', () => {
|
||||
const html = [
|
||||
'Bruh',
|
||||
'Bruh',
|
||||
[
|
||||
makeMention('foo'),
|
||||
makeMention('bar'),
|
||||
makeMention('baz')
|
||||
].join(''),
|
||||
'Bruh'
|
||||
].join('<br>')
|
||||
const expected = [
|
||||
'Bruh',
|
||||
'Bruh',
|
||||
mentionsLine(3),
|
||||
'Bruh'
|
||||
].join('<br>')
|
||||
|
||||
const wrapper = shallowMount(RichContent, {
|
||||
localVue,
|
||||
propsData: {
|
||||
attentions,
|
||||
handleLinks: true,
|
||||
greentext: true,
|
||||
emoji: [],
|
||||
html
|
||||
}
|
||||
})
|
||||
|
||||
expect(wrapper.html()).to.eql(compwrap(expected))
|
||||
})
|
||||
|
||||
it('buggy example/hashtags', () => {
|
||||
const html = [
|
||||
'<p>',
|
||||
'<a href="http://macrochan.org/images/N/H/NHCMDUXJPPZ6M3Z2CQ6D2EBRSWGE7MZY.jpg">',
|
||||
'NHCMDUXJPPZ6M3Z2CQ6D2EBRSWGE7MZY.jpg</a>',
|
||||
' <a class="hashtag" data-tag="nou" href="https://shitposter.club/tag/nou">',
|
||||
'#nou</a>',
|
||||
' <a class="hashtag" data-tag="screencap" href="https://shitposter.club/tag/screencap">',
|
||||
'#screencap</a>',
|
||||
' </p>'
|
||||
].join('')
|
||||
const expected = [
|
||||
'<p>',
|
||||
'<a href="http://macrochan.org/images/N/H/NHCMDUXJPPZ6M3Z2CQ6D2EBRSWGE7MZY.jpg" target="_blank">',
|
||||
'NHCMDUXJPPZ6M3Z2CQ6D2EBRSWGE7MZY.jpg</a>',
|
||||
' <hashtaglink-stub url="https://shitposter.club/tag/nou" content="#nou" tag="nou">',
|
||||
'</hashtaglink-stub>',
|
||||
' <hashtaglink-stub url="https://shitposter.club/tag/screencap" content="#screencap" tag="screencap">',
|
||||
'</hashtaglink-stub>',
|
||||
' </p>'
|
||||
].join('')
|
||||
|
||||
const wrapper = shallowMount(RichContent, {
|
||||
localVue,
|
||||
propsData: {
|
||||
attentions,
|
||||
handleLinks: true,
|
||||
greentext: true,
|
||||
emoji: [],
|
||||
html
|
||||
}
|
||||
})
|
||||
|
||||
expect(wrapper.html()).to.eql(compwrap(expected))
|
||||
})
|
||||
|
||||
it('rich contents of a mention are handled properly', () => {
|
||||
attentions.push({ statusnet_profile_url: 'lol' })
|
||||
const html = [
|
||||
p(
|
||||
'<a href="lol" class="mention">',
|
||||
'<span>',
|
||||
'https://</span>',
|
||||
'<span>',
|
||||
'lol.tld/</span>',
|
||||
'<span>',
|
||||
'</span>',
|
||||
'</a>'
|
||||
),
|
||||
p(
|
||||
'Testing'
|
||||
)
|
||||
].join('')
|
||||
const expected = [
|
||||
p(
|
||||
'<span class="MentionsLine">',
|
||||
'<span class="MentionLink mention-link">',
|
||||
'<a href="lol" target="_blank" class="original">',
|
||||
'<span>',
|
||||
'https://</span>',
|
||||
'<span>',
|
||||
'lol.tld/</span>',
|
||||
'<span>',
|
||||
'</span>',
|
||||
'</a>',
|
||||
' ',
|
||||
'<!---->', // v-if placeholder, mentionlink's "new" (i.e. rich) display
|
||||
'</span>',
|
||||
'<!---->', // v-if placeholder, mentionsline's extra mentions and stuff
|
||||
'</span>'
|
||||
),
|
||||
p(
|
||||
'Testing'
|
||||
)
|
||||
].join('')
|
||||
|
||||
const wrapper = mount(RichContent, {
|
||||
localVue,
|
||||
propsData: {
|
||||
attentions,
|
||||
handleLinks: true,
|
||||
greentext: true,
|
||||
emoji: [],
|
||||
html
|
||||
}
|
||||
})
|
||||
|
||||
expect(wrapper.html()).to.eql(compwrap(expected))
|
||||
})
|
||||
|
||||
it('rich contents of a link are handled properly', () => {
|
||||
const html = [
|
||||
'<p>',
|
||||
'Freenode is dead.</p>',
|
||||
'<p>',
|
||||
'<a href="https://isfreenodedeadyet.com/">',
|
||||
'<span>',
|
||||
'https://</span>',
|
||||
'<span>',
|
||||
'isfreenodedeadyet.com/</span>',
|
||||
'<span>',
|
||||
'</span>',
|
||||
'</a>',
|
||||
'</p>'
|
||||
].join('')
|
||||
const expected = [
|
||||
'<p>',
|
||||
'Freenode is dead.</p>',
|
||||
'<p>',
|
||||
'<a href="https://isfreenodedeadyet.com/" target="_blank">',
|
||||
'<span>',
|
||||
'https://</span>',
|
||||
'<span>',
|
||||
'isfreenodedeadyet.com/</span>',
|
||||
'<span>',
|
||||
'</span>',
|
||||
'</a>',
|
||||
'</p>'
|
||||
].join('')
|
||||
|
||||
const wrapper = shallowMount(RichContent, {
|
||||
localVue,
|
||||
propsData: {
|
||||
attentions,
|
||||
handleLinks: true,
|
||||
greentext: true,
|
||||
emoji: [],
|
||||
html
|
||||
}
|
||||
})
|
||||
|
||||
expect(wrapper.html()).to.eql(compwrap(expected))
|
||||
})
|
||||
|
||||
it.skip('[INFORMATIVE] Performance testing, 10 000 simple posts', () => {
|
||||
const amount = 20
|
||||
|
||||
const onePost = p(
|
||||
makeMention('Lain'),
|
||||
makeMention('Lain'),
|
||||
makeMention('Lain'),
|
||||
makeMention('Lain'),
|
||||
makeMention('Lain'),
|
||||
makeMention('Lain'),
|
||||
makeMention('Lain'),
|
||||
makeMention('Lain'),
|
||||
makeMention('Lain'),
|
||||
makeMention('Lain'),
|
||||
' i just landed in l a where are you'
|
||||
)
|
||||
|
||||
const TestComponent = {
|
||||
template: `
|
||||
<div v-if="!vhtml">
|
||||
${new Array(amount).fill(`<RichContent html="${onePost}" :greentext="true" :handleLinks="handeLinks" :emoji="[]" :attentions="attentions"/>`)}
|
||||
</div>
|
||||
<div v-else="vhtml">
|
||||
${new Array(amount).fill(`<div v-html="${onePost}"/>`)}
|
||||
</div>
|
||||
`,
|
||||
props: ['handleLinks', 'attentions', 'vhtml']
|
||||
}
|
||||
console.log(1)
|
||||
|
||||
const ptest = (handleLinks, vhtml) => {
|
||||
const t0 = performance.now()
|
||||
|
||||
const wrapper = mount(TestComponent, {
|
||||
localVue,
|
||||
propsData: {
|
||||
attentions,
|
||||
handleLinks,
|
||||
vhtml
|
||||
}
|
||||
})
|
||||
|
||||
const t1 = performance.now()
|
||||
|
||||
wrapper.destroy()
|
||||
|
||||
const t2 = performance.now()
|
||||
|
||||
return `Mount: ${t1 - t0}ms, destroy: ${t2 - t1}ms, avg ${(t1 - t0) / amount}ms - ${(t2 - t1) / amount}ms per item`
|
||||
}
|
||||
|
||||
console.log(`${amount} items with links handling:`)
|
||||
console.log(ptest(true))
|
||||
console.log(`${amount} items without links handling:`)
|
||||
console.log(ptest(false))
|
||||
console.log(`${amount} items plain v-html:`)
|
||||
console.log(ptest(false, true))
|
||||
})
|
||||
})
|
|
@ -1,4 +1,4 @@
|
|||
import { parseStatus, parseUser, parseNotification, addEmojis, parseLinkHeaderPagination } from '../../../../../src/services/entity_normalizer/entity_normalizer.service.js'
|
||||
import { parseStatus, parseUser, parseNotification, parseLinkHeaderPagination } from '../../../../../src/services/entity_normalizer/entity_normalizer.service.js'
|
||||
import mastoapidata from '../../../../fixtures/mastoapi.json'
|
||||
import qvitterapidata from '../../../../fixtures/statuses.json'
|
||||
|
||||
|
@ -23,7 +23,6 @@ const makeMockStatusQvitter = (overrides = {}) => {
|
|||
repeat_num: 0,
|
||||
repeated: false,
|
||||
statusnet_conversation_id: '16300488',
|
||||
statusnet_html: '<p>haha benis</p>',
|
||||
summary: null,
|
||||
tags: [],
|
||||
text: 'haha benis',
|
||||
|
@ -232,22 +231,6 @@ describe('API Entities normalizer', () => {
|
|||
expect(parsedRepeat).to.have.property('retweeted_status')
|
||||
expect(parsedRepeat).to.have.deep.property('retweeted_status.id', 'deadbeef')
|
||||
})
|
||||
|
||||
it('adds emojis to post content', () => {
|
||||
const post = makeMockStatusMasto({ emojis: makeMockEmojiMasto(), content: 'Makes you think :thinking:' })
|
||||
|
||||
const parsedPost = parseStatus(post)
|
||||
|
||||
expect(parsedPost).to.have.property('statusnet_html').that.contains('<img')
|
||||
})
|
||||
|
||||
it('adds emojis to subject line', () => {
|
||||
const post = makeMockStatusMasto({ emojis: makeMockEmojiMasto(), spoiler_text: 'CW: 300 IQ :thinking:' })
|
||||
|
||||
const parsedPost = parseStatus(post)
|
||||
|
||||
expect(parsedPost).to.have.property('summary_html').that.contains('<img')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
@ -261,35 +244,6 @@ describe('API Entities normalizer', () => {
|
|||
expect(parseUser(remote)).to.have.property('is_local', false)
|
||||
})
|
||||
|
||||
it('adds emojis to user name', () => {
|
||||
const user = makeMockUserMasto({ emojis: makeMockEmojiMasto(), display_name: 'The :thinking: thinker' })
|
||||
|
||||
const parsedUser = parseUser(user)
|
||||
|
||||
expect(parsedUser).to.have.property('name_html').that.contains('<img')
|
||||
})
|
||||
|
||||
it('adds emojis to user bio', () => {
|
||||
const user = makeMockUserMasto({ emojis: makeMockEmojiMasto(), note: 'Hello i like to :thinking: a lot' })
|
||||
|
||||
const parsedUser = parseUser(user)
|
||||
|
||||
expect(parsedUser).to.have.property('description_html').that.contains('<img')
|
||||
})
|
||||
|
||||
it('adds emojis to user profile fields', () => {
|
||||
const user = makeMockUserMasto({ emojis: makeMockEmojiMasto(), fields: [{ name: ':thinking:', value: ':image:' }] })
|
||||
|
||||
const parsedUser = parseUser(user)
|
||||
|
||||
expect(parsedUser).to.have.property('fields_html').to.be.an('array')
|
||||
|
||||
const field = parsedUser.fields_html[0]
|
||||
|
||||
expect(field).to.have.property('name').that.contains('<img')
|
||||
expect(field).to.have.property('value').that.contains('<img')
|
||||
})
|
||||
|
||||
it('removes html tags from user profile fields', () => {
|
||||
const user = makeMockUserMasto({ emojis: makeMockEmojiMasto(), fields: [{ name: 'user', value: '<a rel="me" href="https://example.com/@user">@user</a>' }] })
|
||||
|
||||
|
@ -355,41 +309,6 @@ describe('API Entities normalizer', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('MastoAPI emoji adder', () => {
|
||||
const emojis = makeMockEmojiMasto()
|
||||
const imageHtml = '<img src="https://example.com/image.png" alt=":image:" title=":image:" class="emoji" />'
|
||||
.replace(/"/g, '\'')
|
||||
const thinkHtml = '<img src="https://example.com/think.png" alt=":thinking:" title=":thinking:" class="emoji" />'
|
||||
.replace(/"/g, '\'')
|
||||
|
||||
it('correctly replaces shortcodes in supplied string', () => {
|
||||
const result = addEmojis('This post has :image: emoji and :thinking: emoji', emojis)
|
||||
expect(result).to.include(thinkHtml)
|
||||
expect(result).to.include(imageHtml)
|
||||
})
|
||||
|
||||
it('handles consecutive emojis correctly', () => {
|
||||
const result = addEmojis('Lelel emoji spam :thinking::thinking::thinking::thinking:', emojis)
|
||||
expect(result).to.include(thinkHtml + thinkHtml + thinkHtml + thinkHtml)
|
||||
})
|
||||
|
||||
it('Doesn\'t replace nonexistent emojis', () => {
|
||||
const result = addEmojis('Admin add the :tenshi: emoji', emojis)
|
||||
expect(result).to.equal('Admin add the :tenshi: emoji')
|
||||
})
|
||||
|
||||
it('Doesn\'t blow up on regex special characters', () => {
|
||||
const emojis = makeMockEmojiMasto([{
|
||||
shortcode: 'c++'
|
||||
}, {
|
||||
shortcode: '[a-z] {|}*'
|
||||
}])
|
||||
const result = addEmojis('This post has :c++: emoji and :[a-z] {|}*: emoji', emojis)
|
||||
expect(result).to.include('title=\':c++:\'')
|
||||
expect(result).to.include('title=\':[a-z] {|}*:\'')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Link header pagination', () => {
|
||||
it('Parses min and max ids as integers', () => {
|
||||
const linkHeader = '<https://example.com/api/v1/notifications?max_id=861676>; rel="next", <https://example.com/api/v1/notifications?min_id=861741>; rel="prev"'
|
||||
|
|
|
@ -0,0 +1,171 @@
|
|||
import { convertHtmlToLines } from 'src/services/html_converter/html_line_converter.service.js'
|
||||
|
||||
const greentextHandle = new Set(['p', 'div'])
|
||||
const mapOnlyText = (processor) => (input) => {
|
||||
if (input.text && input.level.every(l => greentextHandle.has(l))) {
|
||||
return processor(input.text)
|
||||
} else if (input.text) {
|
||||
return input.text
|
||||
} else {
|
||||
return input
|
||||
}
|
||||
}
|
||||
|
||||
describe('html_line_converter', () => {
|
||||
describe('with processor that keeps original line should not make any changes to HTML when', () => {
|
||||
const processorKeep = (line) => line
|
||||
it('fed with regular HTML with newlines', () => {
|
||||
const inputOutput = '1<br/>2<p class="lol">3 4</p> 5 \n 6 <p > 7 <br> 8 </p> <br>\n<br/>'
|
||||
const result = convertHtmlToLines(inputOutput)
|
||||
const comparableResult = result.map(mapOnlyText(processorKeep)).join('')
|
||||
expect(comparableResult).to.eql(inputOutput)
|
||||
})
|
||||
|
||||
it('fed with possibly broken HTML with invalid tags/composition', () => {
|
||||
const inputOutput = '<feeee dwdwddddddw> <i>ayy<b>lm</i>ao</b> </section>'
|
||||
const result = convertHtmlToLines(inputOutput)
|
||||
const comparableResult = result.map(mapOnlyText(processorKeep)).join('')
|
||||
expect(comparableResult).to.eql(inputOutput)
|
||||
})
|
||||
|
||||
it('fed with very broken HTML with broken composition', () => {
|
||||
const inputOutput = '</p> lmao what </div> whats going on <div> wha <p>'
|
||||
const result = convertHtmlToLines(inputOutput)
|
||||
const comparableResult = result.map(mapOnlyText(processorKeep)).join('')
|
||||
expect(comparableResult).to.eql(inputOutput)
|
||||
})
|
||||
|
||||
it('fed with sorta valid HTML but tags aren\'t closed', () => {
|
||||
const inputOutput = 'just leaving a <div> hanging'
|
||||
const result = convertHtmlToLines(inputOutput)
|
||||
const comparableResult = result.map(mapOnlyText(processorKeep)).join('')
|
||||
expect(comparableResult).to.eql(inputOutput)
|
||||
})
|
||||
|
||||
it('fed with not really HTML at this point... tags that aren\'t finished', () => {
|
||||
const inputOutput = 'do you expect me to finish this <div class='
|
||||
const result = convertHtmlToLines(inputOutput)
|
||||
const comparableResult = result.map(mapOnlyText(processorKeep)).join('')
|
||||
expect(comparableResult).to.eql(inputOutput)
|
||||
})
|
||||
|
||||
it('fed with dubiously valid HTML (p within p and also div inside p)', () => {
|
||||
const inputOutput = 'look ma <p> p \nwithin <p> p! </p> and a <br/><div>div!</div></p>'
|
||||
const result = convertHtmlToLines(inputOutput)
|
||||
const comparableResult = result.map(mapOnlyText(processorKeep)).join('')
|
||||
expect(comparableResult).to.eql(inputOutput)
|
||||
})
|
||||
|
||||
it('fed with maybe valid HTML? self-closing divs and ps', () => {
|
||||
const inputOutput = 'a <div class="what"/> what now <p aria-label="wtf"/> ?'
|
||||
const result = convertHtmlToLines(inputOutput)
|
||||
const comparableResult = result.map(mapOnlyText(processorKeep)).join('')
|
||||
expect(comparableResult).to.eql(inputOutput)
|
||||
})
|
||||
|
||||
it('fed with valid XHTML containing a CDATA', () => {
|
||||
const inputOutput = 'Yes, it is me, <![CDATA[DIO]]>'
|
||||
const result = convertHtmlToLines(inputOutput)
|
||||
const comparableResult = result.map(mapOnlyText(processorKeep)).join('')
|
||||
expect(comparableResult).to.eql(inputOutput)
|
||||
})
|
||||
|
||||
it('fed with some recognized but not handled elements', () => {
|
||||
const inputOutput = 'testing images\n\n<img src="benis.png">'
|
||||
const result = convertHtmlToLines(inputOutput)
|
||||
const comparableResult = result.map(mapOnlyText(processorKeep)).join('')
|
||||
expect(comparableResult).to.eql(inputOutput)
|
||||
})
|
||||
})
|
||||
describe('with processor that replaces lines with word "_" should match expected line when', () => {
|
||||
const processorReplace = (line) => '_'
|
||||
it('fed with regular HTML with newlines', () => {
|
||||
const input = '1<br/>2<p class="lol">3 4</p> 5 \n 6 <p > 7 <br> 8 </p> <br>\n<br/>'
|
||||
const output = '_<br/>_<p class="lol">_</p>_\n_<p >_<br>_</p> <br>\n<br/>'
|
||||
const result = convertHtmlToLines(input)
|
||||
const comparableResult = result.map(mapOnlyText(processorReplace)).join('')
|
||||
expect(comparableResult).to.eql(output)
|
||||
})
|
||||
|
||||
it('fed with possibly broken HTML with invalid tags/composition', () => {
|
||||
const input = '<feeee dwdwddddddw> <i>ayy<b>lm</i>ao</b> </section>'
|
||||
const output = '_'
|
||||
const result = convertHtmlToLines(input)
|
||||
const comparableResult = result.map(mapOnlyText(processorReplace)).join('')
|
||||
expect(comparableResult).to.eql(output)
|
||||
})
|
||||
|
||||
it('fed with very broken HTML with broken composition', () => {
|
||||
const input = '</p> lmao what </div> whats going on <div> wha <p>'
|
||||
const output = '_<div>_<p>'
|
||||
const result = convertHtmlToLines(input)
|
||||
const comparableResult = result.map(mapOnlyText(processorReplace)).join('')
|
||||
expect(comparableResult).to.eql(output)
|
||||
})
|
||||
|
||||
it('fed with sorta valid HTML but tags aren\'t closed', () => {
|
||||
const input = 'just leaving a <div> hanging'
|
||||
const output = '_<div>_'
|
||||
const result = convertHtmlToLines(input)
|
||||
const comparableResult = result.map(mapOnlyText(processorReplace)).join('')
|
||||
expect(comparableResult).to.eql(output)
|
||||
})
|
||||
|
||||
it('fed with not really HTML at this point... tags that aren\'t finished', () => {
|
||||
const input = 'do you expect me to finish this <div class='
|
||||
const output = '_'
|
||||
const result = convertHtmlToLines(input)
|
||||
const comparableResult = result.map(mapOnlyText(processorReplace)).join('')
|
||||
expect(comparableResult).to.eql(output)
|
||||
})
|
||||
|
||||
it('fed with dubiously valid HTML (p within p and also div inside p)', () => {
|
||||
const input = 'look ma <p> p \nwithin <p> p! </p> and a <br/><div>div!</div></p>'
|
||||
const output = '_<p>_\n_<p>_</p>_<br/><div>_</div></p>'
|
||||
const result = convertHtmlToLines(input)
|
||||
const comparableResult = result.map(mapOnlyText(processorReplace)).join('')
|
||||
expect(comparableResult).to.eql(output)
|
||||
})
|
||||
|
||||
it('fed with maybe valid HTML? (XHTML) self-closing divs and ps', () => {
|
||||
const input = 'a <div class="what"/> what now <p aria-label="wtf"/> ?'
|
||||
const output = '_<div class="what"/>_<p aria-label="wtf"/>_'
|
||||
const result = convertHtmlToLines(input)
|
||||
const comparableResult = result.map(mapOnlyText(processorReplace)).join('')
|
||||
expect(comparableResult).to.eql(output)
|
||||
})
|
||||
|
||||
it('fed with valid XHTML containing a CDATA', () => {
|
||||
const input = 'Yes, it is me, <![CDATA[DIO]]>'
|
||||
const output = '_'
|
||||
const result = convertHtmlToLines(input)
|
||||
const comparableResult = result.map(mapOnlyText(processorReplace)).join('')
|
||||
expect(comparableResult).to.eql(output)
|
||||
})
|
||||
|
||||
it('Testing handling ignored blocks', () => {
|
||||
const input = `
|
||||
<pre><code>> rei = "0"
|
||||
'0'
|
||||
> rei == 0
|
||||
true
|
||||
> rei == null
|
||||
false</code></pre><blockquote>That, christian-like JS diagram but it’s evangelion instead.</blockquote>
|
||||
`
|
||||
const result = convertHtmlToLines(input)
|
||||
const comparableResult = result.map(mapOnlyText(processorReplace)).join('')
|
||||
expect(comparableResult).to.eql(input)
|
||||
})
|
||||
it('Testing handling ignored blocks 2', () => {
|
||||
const input = `
|
||||
<blockquote>An SSL error has happened.</blockquote><p>Shakespeare</p>
|
||||
`
|
||||
const output = `
|
||||
<blockquote>An SSL error has happened.</blockquote><p>_</p>
|
||||
`
|
||||
const result = convertHtmlToLines(input)
|
||||
const comparableResult = result.map(mapOnlyText(processorReplace)).join('')
|
||||
expect(comparableResult).to.eql(output)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,132 @@
|
|||
import { convertHtmlToTree } from 'src/services/html_converter/html_tree_converter.service.js'
|
||||
|
||||
describe('html_tree_converter', () => {
|
||||
describe('convertHtmlToTree', () => {
|
||||
it('converts html into a tree structure', () => {
|
||||
const input = '1 <p>2</p> <b>3<img src="a">4</b>5'
|
||||
expect(convertHtmlToTree(input)).to.eql([
|
||||
'1 ',
|
||||
[
|
||||
'<p>',
|
||||
['2'],
|
||||
'</p>'
|
||||
],
|
||||
' ',
|
||||
[
|
||||
'<b>',
|
||||
[
|
||||
'3',
|
||||
['<img src="a">'],
|
||||
'4'
|
||||
],
|
||||
'</b>'
|
||||
],
|
||||
'5'
|
||||
])
|
||||
})
|
||||
it('converts html to tree while preserving tag formatting', () => {
|
||||
const input = '1 <p >2</p><b >3<img src="a">4</b>5'
|
||||
expect(convertHtmlToTree(input)).to.eql([
|
||||
'1 ',
|
||||
[
|
||||
'<p >',
|
||||
['2'],
|
||||
'</p>'
|
||||
],
|
||||
[
|
||||
'<b >',
|
||||
[
|
||||
'3',
|
||||
['<img src="a">'],
|
||||
'4'
|
||||
],
|
||||
'</b>'
|
||||
],
|
||||
'5'
|
||||
])
|
||||
})
|
||||
it('converts semi-broken html', () => {
|
||||
const input = '1 <br> 2 <p> 42'
|
||||
expect(convertHtmlToTree(input)).to.eql([
|
||||
'1 ',
|
||||
['<br>'],
|
||||
' 2 ',
|
||||
[
|
||||
'<p>',
|
||||
[' 42']
|
||||
]
|
||||
])
|
||||
})
|
||||
it('realistic case 1', () => {
|
||||
const input = '<p><span class="h-card"><a class="u-url mention" data-user="9wRC6T2ZZiKWJ0vUi8" href="https://cawfee.club/users/benis" rel="ugc">@<span>benis</span></a></span> <span class="h-card"><a class="u-url mention" data-user="194" href="https://shigusegubu.club/users/hj" rel="ugc">@<span>hj</span></a></span> nice</p>'
|
||||
expect(convertHtmlToTree(input)).to.eql([
|
||||
[
|
||||
'<p>',
|
||||
[
|
||||
[
|
||||
'<span class="h-card">',
|
||||
[
|
||||
[
|
||||
'<a class="u-url mention" data-user="9wRC6T2ZZiKWJ0vUi8" href="https://cawfee.club/users/benis" rel="ugc">',
|
||||
[
|
||||
'@',
|
||||
[
|
||||
'<span>',
|
||||
[
|
||||
'benis'
|
||||
],
|
||||
'</span>'
|
||||
]
|
||||
],
|
||||
'</a>'
|
||||
]
|
||||
],
|
||||
'</span>'
|
||||
],
|
||||
' ',
|
||||
[
|
||||
'<span class="h-card">',
|
||||
[
|
||||
[
|
||||
'<a class="u-url mention" data-user="194" href="https://shigusegubu.club/users/hj" rel="ugc">',
|
||||
[
|
||||
'@',
|
||||
[
|
||||
'<span>',
|
||||
[
|
||||
'hj'
|
||||
],
|
||||
'</span>'
|
||||
]
|
||||
],
|
||||
'</a>'
|
||||
]
|
||||
],
|
||||
'</span>'
|
||||
],
|
||||
' nice'
|
||||
],
|
||||
'</p>'
|
||||
]
|
||||
])
|
||||
})
|
||||
it('realistic case 2', () => {
|
||||
const inputOutput = 'Country improv: give me a city<br/>Audience: Memphis<br/>Improv troupe: come on, a better one<br/>Audience: el paso'
|
||||
expect(convertHtmlToTree(inputOutput)).to.eql([
|
||||
'Country improv: give me a city',
|
||||
[
|
||||
'<br/>'
|
||||
],
|
||||
'Audience: Memphis',
|
||||
[
|
||||
'<br/>'
|
||||
],
|
||||
'Improv troupe: come on, a better one',
|
||||
[
|
||||
'<br/>'
|
||||
],
|
||||
'Audience: el paso'
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
37
test/unit/specs/services/html_converter/utility.spec.js
Normal file
37
test/unit/specs/services/html_converter/utility.spec.js
Normal file
|
@ -0,0 +1,37 @@
|
|||
import { processTextForEmoji, getAttrs } from 'src/services/html_converter/utility.service.js'
|
||||
|
||||
describe('html_converter utility', () => {
|
||||
describe('processTextForEmoji', () => {
|
||||
it('processes all emoji in text', () => {
|
||||
const input = 'Hello from finland! :lol: We have best water! :lmao:'
|
||||
const emojis = [
|
||||
{ shortcode: 'lol', src: 'LOL' },
|
||||
{ shortcode: 'lmao', src: 'LMAO' }
|
||||
]
|
||||
const processor = ({ shortcode, src }) => ({ shortcode, src })
|
||||
expect(processTextForEmoji(input, emojis, processor)).to.eql([
|
||||
'Hello from finland! ',
|
||||
{ shortcode: 'lol', src: 'LOL' },
|
||||
' We have best water! ',
|
||||
{ shortcode: 'lmao', src: 'LMAO' }
|
||||
])
|
||||
})
|
||||
it('leaves text as is', () => {
|
||||
const input = 'Number one: that\'s terror'
|
||||
const emojis = []
|
||||
const processor = ({ shortcode, src }) => ({ shortcode, src })
|
||||
expect(processTextForEmoji(input, emojis, processor)).to.eql([
|
||||
'Number one: that\'s terror'
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('getAttrs', () => {
|
||||
it('extracts arguments from tag', () => {
|
||||
const input = '<img src="boop" cool ebin=\'true\'>'
|
||||
const output = { src: 'boop', cool: true, ebin: 'true' }
|
||||
|
||||
expect(getAttrs(input)).to.eql(output)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,96 +0,0 @@
|
|||
import { processHtml } from 'src/services/tiny_post_html_processor/tiny_post_html_processor.service.js'
|
||||
|
||||
describe('TinyPostHTMLProcessor', () => {
|
||||
describe('with processor that keeps original line should not make any changes to HTML when', () => {
|
||||
const processorKeep = (line) => line
|
||||
it('fed with regular HTML with newlines', () => {
|
||||
const inputOutput = '1<br/>2<p class="lol">3 4</p> 5 \n 6 <p > 7 <br> 8 </p> <br>\n<br/>'
|
||||
expect(processHtml(inputOutput, processorKeep)).to.eql(inputOutput)
|
||||
})
|
||||
|
||||
it('fed with possibly broken HTML with invalid tags/composition', () => {
|
||||
const inputOutput = '<feeee dwdwddddddw> <i>ayy<b>lm</i>ao</b> </section>'
|
||||
expect(processHtml(inputOutput, processorKeep)).to.eql(inputOutput)
|
||||
})
|
||||
|
||||
it('fed with very broken HTML with broken composition', () => {
|
||||
const inputOutput = '</p> lmao what </div> whats going on <div> wha <p>'
|
||||
expect(processHtml(inputOutput, processorKeep)).to.eql(inputOutput)
|
||||
})
|
||||
|
||||
it('fed with sorta valid HTML but tags aren\'t closed', () => {
|
||||
const inputOutput = 'just leaving a <div> hanging'
|
||||
expect(processHtml(inputOutput, processorKeep)).to.eql(inputOutput)
|
||||
})
|
||||
|
||||
it('fed with not really HTML at this point... tags that aren\'t finished', () => {
|
||||
const inputOutput = 'do you expect me to finish this <div class='
|
||||
expect(processHtml(inputOutput, processorKeep)).to.eql(inputOutput)
|
||||
})
|
||||
|
||||
it('fed with dubiously valid HTML (p within p and also div inside p)', () => {
|
||||
const inputOutput = 'look ma <p> p \nwithin <p> p! </p> and a <br/><div>div!</div></p>'
|
||||
expect(processHtml(inputOutput, processorKeep)).to.eql(inputOutput)
|
||||
})
|
||||
|
||||
it('fed with maybe valid HTML? self-closing divs and ps', () => {
|
||||
const inputOutput = 'a <div class="what"/> what now <p aria-label="wtf"/> ?'
|
||||
expect(processHtml(inputOutput, processorKeep)).to.eql(inputOutput)
|
||||
})
|
||||
|
||||
it('fed with valid XHTML containing a CDATA', () => {
|
||||
const inputOutput = 'Yes, it is me, <![CDATA[DIO]]>'
|
||||
expect(processHtml(inputOutput, processorKeep)).to.eql(inputOutput)
|
||||
})
|
||||
})
|
||||
describe('with processor that replaces lines with word "_" should match expected line when', () => {
|
||||
const processorReplace = (line) => '_'
|
||||
it('fed with regular HTML with newlines', () => {
|
||||
const input = '1<br/>2<p class="lol">3 4</p> 5 \n 6 <p > 7 <br> 8 </p> <br>\n<br/>'
|
||||
const output = '_<br/>_<p class="lol">_</p>_\n_<p >_<br>_</p> <br>\n<br/>'
|
||||
expect(processHtml(input, processorReplace)).to.eql(output)
|
||||
})
|
||||
|
||||
it('fed with possibly broken HTML with invalid tags/composition', () => {
|
||||
const input = '<feeee dwdwddddddw> <i>ayy<b>lm</i>ao</b> </section>'
|
||||
const output = '_'
|
||||
expect(processHtml(input, processorReplace)).to.eql(output)
|
||||
})
|
||||
|
||||
it('fed with very broken HTML with broken composition', () => {
|
||||
const input = '</p> lmao what </div> whats going on <div> wha <p>'
|
||||
const output = '</p>_</div>_<div>_<p>'
|
||||
expect(processHtml(input, processorReplace)).to.eql(output)
|
||||
})
|
||||
|
||||
it('fed with sorta valid HTML but tags aren\'t closed', () => {
|
||||
const input = 'just leaving a <div> hanging'
|
||||
const output = '_<div>_'
|
||||
expect(processHtml(input, processorReplace)).to.eql(output)
|
||||
})
|
||||
|
||||
it('fed with not really HTML at this point... tags that aren\'t finished', () => {
|
||||
const input = 'do you expect me to finish this <div class='
|
||||
const output = '_'
|
||||
expect(processHtml(input, processorReplace)).to.eql(output)
|
||||
})
|
||||
|
||||
it('fed with dubiously valid HTML (p within p and also div inside p)', () => {
|
||||
const input = 'look ma <p> p \nwithin <p> p! </p> and a <br/><div>div!</div></p>'
|
||||
const output = '_<p>_\n_<p>_</p>_<br/><div>_</div></p>'
|
||||
expect(processHtml(input, processorReplace)).to.eql(output)
|
||||
})
|
||||
|
||||
it('fed with maybe valid HTML? self-closing divs and ps', () => {
|
||||
const input = 'a <div class="what"/> what now <p aria-label="wtf"/> ?'
|
||||
const output = '_<div class="what"/>_<p aria-label="wtf"/>_'
|
||||
expect(processHtml(input, processorReplace)).to.eql(output)
|
||||
})
|
||||
|
||||
it('fed with valid XHTML containing a CDATA', () => {
|
||||
const input = 'Yes, it is me, <![CDATA[DIO]]>'
|
||||
const output = '_'
|
||||
expect(processHtml(input, processorReplace)).to.eql(output)
|
||||
})
|
||||
})
|
||||
})
|
81
yarn.lock
81
yarn.lock
|
@ -1011,23 +1011,86 @@
|
|||
resolved "https://registry.yarnpkg.com/@ungap/event-target/-/event-target-0.1.0.tgz#88d527d40de86c4b0c99a060ca241d755999915b"
|
||||
integrity sha512-W2oyj0Fe1w/XhPZjkI3oUcDUAmu5P4qsdT2/2S8aMhtAWM/CE/jYWtji0pKNPDfxLI75fa5gWSEmnynKMNP/oA==
|
||||
|
||||
"@vue/babel-helper-vue-jsx-merge-props@^1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.0.0.tgz#048fe579958da408fb7a8b2a3ec050b50a661040"
|
||||
integrity sha512-6tyf5Cqm4m6v7buITuwS+jHzPlIPxbFzEhXR5JGZpbrvOcp1hiQKckd305/3C7C36wFekNTQSxAtgeM0j0yoUw==
|
||||
"@vue/babel-helper-vue-jsx-merge-props@^1.2.1":
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.2.1.tgz#31624a7a505fb14da1d58023725a4c5f270e6a81"
|
||||
integrity sha512-QOi5OW45e2R20VygMSNhyQHvpdUwQZqGPc748JLGCYEy+yp8fNFNdbNIGAgZmi9e+2JHPd6i6idRuqivyicIkA==
|
||||
|
||||
"@vue/babel-plugin-transform-vue-jsx@^1.1.2":
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@vue/babel-plugin-transform-vue-jsx/-/babel-plugin-transform-vue-jsx-1.1.2.tgz#c0a3e6efc022e75e4247b448a8fc6b86f03e91c0"
|
||||
integrity sha512-YfdaoSMvD1nj7+DsrwfTvTnhDXI7bsuh+Y5qWwvQXlD24uLgnsoww3qbiZvWf/EoviZMrvqkqN4CBw0W3BWUTQ==
|
||||
"@vue/babel-plugin-transform-vue-jsx@^1.2.1":
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@vue/babel-plugin-transform-vue-jsx/-/babel-plugin-transform-vue-jsx-1.2.1.tgz#646046c652c2f0242727f34519d917b064041ed7"
|
||||
integrity sha512-HJuqwACYehQwh1fNT8f4kyzqlNMpBuUK4rSiSES5D4QsYncv5fxFsLyrxFPG2ksO7t5WP+Vgix6tt6yKClwPzA==
|
||||
dependencies:
|
||||
"@babel/helper-module-imports" "^7.0.0"
|
||||
"@babel/plugin-syntax-jsx" "^7.2.0"
|
||||
"@vue/babel-helper-vue-jsx-merge-props" "^1.0.0"
|
||||
"@vue/babel-helper-vue-jsx-merge-props" "^1.2.1"
|
||||
html-tags "^2.0.0"
|
||||
lodash.kebabcase "^4.1.1"
|
||||
svg-tags "^1.0.0"
|
||||
|
||||
"@vue/babel-preset-jsx@^1.2.4":
|
||||
version "1.2.4"
|
||||
resolved "https://registry.yarnpkg.com/@vue/babel-preset-jsx/-/babel-preset-jsx-1.2.4.tgz#92fea79db6f13b01e80d3a0099e2924bdcbe4e87"
|
||||
integrity sha512-oRVnmN2a77bYDJzeGSt92AuHXbkIxbf/XXSE3klINnh9AXBmVS1DGa1f0d+dDYpLfsAKElMnqKTQfKn7obcL4w==
|
||||
dependencies:
|
||||
"@vue/babel-helper-vue-jsx-merge-props" "^1.2.1"
|
||||
"@vue/babel-plugin-transform-vue-jsx" "^1.2.1"
|
||||
"@vue/babel-sugar-composition-api-inject-h" "^1.2.1"
|
||||
"@vue/babel-sugar-composition-api-render-instance" "^1.2.4"
|
||||
"@vue/babel-sugar-functional-vue" "^1.2.2"
|
||||
"@vue/babel-sugar-inject-h" "^1.2.2"
|
||||
"@vue/babel-sugar-v-model" "^1.2.3"
|
||||
"@vue/babel-sugar-v-on" "^1.2.3"
|
||||
|
||||
"@vue/babel-sugar-composition-api-inject-h@^1.2.1":
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@vue/babel-sugar-composition-api-inject-h/-/babel-sugar-composition-api-inject-h-1.2.1.tgz#05d6e0c432710e37582b2be9a6049b689b6f03eb"
|
||||
integrity sha512-4B3L5Z2G+7s+9Bwbf+zPIifkFNcKth7fQwekVbnOA3cr3Pq71q71goWr97sk4/yyzH8phfe5ODVzEjX7HU7ItQ==
|
||||
dependencies:
|
||||
"@babel/plugin-syntax-jsx" "^7.2.0"
|
||||
|
||||
"@vue/babel-sugar-composition-api-render-instance@^1.2.4":
|
||||
version "1.2.4"
|
||||
resolved "https://registry.yarnpkg.com/@vue/babel-sugar-composition-api-render-instance/-/babel-sugar-composition-api-render-instance-1.2.4.tgz#e4cbc6997c344fac271785ad7a29325c51d68d19"
|
||||
integrity sha512-joha4PZznQMsxQYXtR3MnTgCASC9u3zt9KfBxIeuI5g2gscpTsSKRDzWQt4aqNIpx6cv8On7/m6zmmovlNsG7Q==
|
||||
dependencies:
|
||||
"@babel/plugin-syntax-jsx" "^7.2.0"
|
||||
|
||||
"@vue/babel-sugar-functional-vue@^1.2.2":
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@vue/babel-sugar-functional-vue/-/babel-sugar-functional-vue-1.2.2.tgz#267a9ac8d787c96edbf03ce3f392c49da9bd2658"
|
||||
integrity sha512-JvbgGn1bjCLByIAU1VOoepHQ1vFsroSA/QkzdiSs657V79q6OwEWLCQtQnEXD/rLTA8rRit4rMOhFpbjRFm82w==
|
||||
dependencies:
|
||||
"@babel/plugin-syntax-jsx" "^7.2.0"
|
||||
|
||||
"@vue/babel-sugar-inject-h@^1.2.2":
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@vue/babel-sugar-inject-h/-/babel-sugar-inject-h-1.2.2.tgz#d738d3c893367ec8491dcbb669b000919293e3aa"
|
||||
integrity sha512-y8vTo00oRkzQTgufeotjCLPAvlhnpSkcHFEp60+LJUwygGcd5Chrpn5480AQp/thrxVm8m2ifAk0LyFel9oCnw==
|
||||
dependencies:
|
||||
"@babel/plugin-syntax-jsx" "^7.2.0"
|
||||
|
||||
"@vue/babel-sugar-v-model@^1.2.3":
|
||||
version "1.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@vue/babel-sugar-v-model/-/babel-sugar-v-model-1.2.3.tgz#fa1f29ba51ebf0aa1a6c35fa66d539bc459a18f2"
|
||||
integrity sha512-A2jxx87mySr/ulAsSSyYE8un6SIH0NWHiLaCWpodPCVOlQVODCaSpiR4+IMsmBr73haG+oeCuSvMOM+ttWUqRQ==
|
||||
dependencies:
|
||||
"@babel/plugin-syntax-jsx" "^7.2.0"
|
||||
"@vue/babel-helper-vue-jsx-merge-props" "^1.2.1"
|
||||
"@vue/babel-plugin-transform-vue-jsx" "^1.2.1"
|
||||
camelcase "^5.0.0"
|
||||
html-tags "^2.0.0"
|
||||
svg-tags "^1.0.0"
|
||||
|
||||
"@vue/babel-sugar-v-on@^1.2.3":
|
||||
version "1.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@vue/babel-sugar-v-on/-/babel-sugar-v-on-1.2.3.tgz#342367178586a69f392f04bfba32021d02913ada"
|
||||
integrity sha512-kt12VJdz/37D3N3eglBywV8GStKNUhNrsxChXIV+o0MwVXORYuhDTHJRKPgLJRb/EY3vM2aRFQdxJBp9CLikjw==
|
||||
dependencies:
|
||||
"@babel/plugin-syntax-jsx" "^7.2.0"
|
||||
"@vue/babel-plugin-transform-vue-jsx" "^1.2.1"
|
||||
camelcase "^5.0.0"
|
||||
|
||||
"@vue/test-utils@^1.0.0-beta.26":
|
||||
version "1.0.0-beta.28"
|
||||
resolved "https://registry.yarnpkg.com/@vue/test-utils/-/test-utils-1.0.0-beta.28.tgz#767c43413df8cde86128735e58923803e444b9a5"
|
||||
|
|
Loading…
Reference in a new issue