Compare commits

..

18 commits

68 changed files with 600 additions and 1633 deletions

3
.gitignore vendored
View file

@ -9,4 +9,5 @@ selenium-debug.log
config/local.json
config/local.*.json
docs/site/
.vscode/
.vscode/
.yarn/

View file

@ -1 +0,0 @@
nodejs 20.12.2

View file

@ -1,12 +1,9 @@
labels:
platform: linux/amd64
steps:
pipeline:
lint:
when:
event:
- pull_request
image: node:20
image: node:18
commands:
- yarn
- yarn lint
@ -16,11 +13,11 @@ steps:
when:
event:
- pull_request
image: node:20
image: node:18
commands:
- apt update
- apt install firefox-esr -y --no-install-recommends
- yarn
- yarn
- yarn unit
build:
@ -30,7 +27,7 @@ steps:
branch:
- develop
- stable
image: node:20
image: node:18
commands:
- yarn
- yarn build
@ -42,15 +39,15 @@ steps:
branch:
- develop
- stable
image: node:20
image: node:18
secrets:
- SCW_ACCESS_KEY
- SCW_SECRET_KEY
- SCW_DEFAULT_ORGANIZATION_ID
commands:
- apt-get update && apt-get install -y rclone wget zip
- wget https://github.com/scaleway/scaleway-cli/releases/download/v2.30.0/scaleway-cli_2.30.0_linux_amd64
- mv scaleway-cli_2.30.0_linux_amd64 scaleway-cli
- wget https://github.com/scaleway/scaleway-cli/releases/download/v2.5.1/scaleway-cli_2.5.1_linux_amd64
- mv scaleway-cli_2.5.1_linux_amd64 scaleway-cli
- chmod +x scaleway-cli
- ./scaleway-cli object config install type=rclone
- zip akkoma-fe.zip -r dist
@ -72,8 +69,8 @@ steps:
- SCW_DEFAULT_ORGANIZATION_ID
commands:
- apt-get update && apt-get install -y rclone wget git zip
- wget https://github.com/scaleway/scaleway-cli/releases/download/v2.30.0/scaleway-cli_2.30.0_linux_amd64
- mv scaleway-cli_2.30.0_linux_amd64 scaleway-cli
- wget https://github.com/scaleway/scaleway-cli/releases/download/v2.5.1/scaleway-cli_2.5.1_linux_amd64
- mv scaleway-cli_2.5.1_linux_amd64 scaleway-cli
- chmod +x scaleway-cli
- ./scaleway-cli object config install type=rclone
- cd docs
@ -81,4 +78,4 @@ steps:
- mkdocs build
- zip -r docs.zip site/*
- cd site
- rclone copy . scaleway:akkoma-docs/frontend/$CI_COMMIT_BRANCH/
- rclone copy . scaleway:akkoma-docs/frontend/$CI_COMMIT_BRANCH/

View file

@ -1,4 +1,21 @@
# Akkoma-FE
A soft fork of Akkoma-FE fixing a few minor but longstanding annoyances.
Hopefully most of these will be merged upstream when I learn how to do PRs.
## Comprehensive List of Fixes and Changes
### Added
- Support for 'Fedi Links' web+ap addresses on user profiles and posts
- Approve and deny follow request buttons to the user profile page
### Fixed
- Styling adjustments for the user profile to avoid some ellipsing in the notifications panel
- Settings backup when the word filters contain non-ASCII characters
- The hover color of the checkmark icon on follow request notifications
# Akkoma-FE
![English OK](https://img.shields.io/badge/English-OK-blueviolet) ![日本語OK](https://img.shields.io/badge/%E6%97%A5%E6%9C%AC%E8%AA%9E-OK-blueviolet)
@ -8,7 +25,7 @@ This is a fork of Akkoma-FE from the Pleroma project, with support for new Akkom
# For Translators
The [Weblate UI](https://translate.akkoma.dev/projects/akkoma/pleroma-fe/) is recommended for adding or modifying translations for Akkoma-FE.
The [Weblate UI](https://translate.akkoma.dev/projects/akkoma/pleroma-fe/) is recommended for adding or modifying translations for Akkoma-FE.
Alternatively, edit/create `src/i18n/$LANGUAGE_CODE.json` (where `$LANGUAGE_CODE` is the [ISO 639-1 code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) for your language), then add your language to [src/i18n/messages.js](https://akkoma.dev/AkkomaGang/pleroma-fe/src/branch/develop/src/i18n/messages.js) if it doesn't already exist there.
@ -22,7 +39,7 @@ To use Akkoma-FE in Akkoma, use the [frontend](https://docs.akkoma.dev/stable/ad
``` bash
# install dependencies
corepack enable
npm install -g yarn
yarn
# serve with hot reload at localhost:8080
@ -37,7 +54,7 @@ npm run unit
# For Contributors:
You can create file `/config/local.json` (see [example](https://akkoma.dev/AkkomaGang/akkoma-fe/src/branch/develop/config/local.example.json)) to enable some convenience dev options:
You can create file `/config/local.json` (see [example](https://git.pleroma.social/pleroma/pleroma-fe/blob/develop/config/local.example.json)) to enable some convenience dev options:
* `target`: makes local dev server redirect to some existing instance's BE instead of local BE, useful for testing things in near-production environment and searching for real-life use-cases.
* `staticConfigPreference`: makes FE's `/static/config.json` take preference of BE-served `/api/statusnet/config.json`. Only works in dev mode.
@ -52,4 +69,4 @@ Edit config.json for configuration.
### Login methods
```loginMethod``` can be set to either ```password``` (the default) or ```token```, which will use the full oauth redirection flow, which is useful for SSO situations.
```loginMethod``` can be set to either ```password``` (the default) or ```token```, which will use the full oauth redirection flow, which is useful for SSO situations.

View file

@ -1,4 +1,4 @@
{
"target": "https://otp.akkoma.dev/",
"target": "https://pleroma.soykaf.com/",
"staticConfigPreference": false
}

View file

@ -1,6 +1,6 @@
{
"name": "pleroma_fe",
"version": "3.10.0",
"version": "3.5.0",
"description": "A frontend for Akkoma instances",
"author": "Roger Braun <roger@rogerbraun.net>",
"private": true,

View file

@ -64,11 +64,6 @@ export default {
'-' + this.layoutType
]
},
pageBackground () {
return this.mergedConfig.displayPageBackgrounds
? this.$store.state.users.displayBackground
: null
},
currentUser () { return this.$store.state.users.currentUser },
userBackground () { return this.currentUser.background_image },
instanceBackground () {
@ -76,7 +71,7 @@ export default {
? null
: this.$store.state.instance.background
},
background () { return this.pageBackground || this.userBackground || this.instanceBackground },
background () { return this.userBackground || this.instanceBackground },
bgStyle () {
if (this.background) {
return {

View file

@ -8,7 +8,7 @@
}
html {
font-size: 0.875rem;
font-size: 14px;
// overflow-x: clip causes my browser's tab to crash with SIGILL lul
}

View file

@ -0,0 +1,13 @@
const facActivityPub = {
prefix: "fac",
iconName: "activity-pub",
icon: [
448,
512,
[],
"",
"M 253.31024,102.2775 -10,254.04683 v 60.95152 L 200.28249,193.7048 v 243.19662 l 53.02775,29.86625 z m 52.41823,0 v 60.95153 L 516.62048,284.52259 305.72847,406.42564 v 60.34203 L 569.03871,314.99835 v -60.95152 z m 0,121.90308 V 345.47412 L 411.17449,284.52259 Z M 147.86424,284.52259 42.418248,345.47412 147.86424,406.42564 Z"
]
}
export default facActivityPub

View file

@ -37,7 +37,7 @@
white-space: pre-line;
word-break: break-word;
text-overflow: ellipsis;
overflow: auto;
overflow: scroll;
}
&.-static {

View file

@ -7,7 +7,7 @@ import {
faStickyNote,
faSmileBeam
} from '@fortawesome/free-solid-svg-icons'
import { trim, escapeRegExp, startCase, debounce } from 'lodash'
import { trim, escapeRegExp, startCase } from 'lodash'
library.add(
faBoxOpen,
@ -42,9 +42,6 @@ const EmojiPicker = {
EmojiGrid
},
methods: {
debouncedSearch: debounce(function (e) {
this.keyword = e.target.value
}, 500),
onStickerUploaded (e) {
this.$emit('sticker-uploaded', e)
},
@ -88,6 +85,17 @@ const EmojiPicker = {
activeGroupView () {
return this.showingStickers ? '' : this.activeGroup
},
stickersAvailable () {
if (this.$store.state.instance.stickers) {
return this.$store.state.instance.stickers.length > 0
}
return 0
},
filteredEmoji () {
return this.filterByKeyword(
this.$store.state.instance.customEmoji || []
)
},
emojis () {
const recentEmojis = this.$store.getters.recentEmojis
const standardEmojis = this.$store.state.instance.emoji || []

View file

@ -44,10 +44,11 @@
>
<div class="emoji-search">
<input
v-model="keyword"
type="text"
class="form-control"
:placeholder="$t('emoji.search_emoji')"
@input="debouncedSearch"
@input="$event.target.composing = false"
>
</div>
<EmojiGrid

View file

@ -1,5 +1,6 @@
import Popover from '../popover/popover.vue'
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
import facActivityPub from '../../assets/icons/activity-pub'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faEllipsisH,
@ -9,7 +10,7 @@ import {
faShareAlt,
faExternalLinkAlt,
faHistory,
faFilePen
faFilePen,
} from '@fortawesome/free-solid-svg-icons'
import {
faBookmark as faBookmarkReg,
@ -25,6 +26,7 @@ library.add(
faThumbtack,
faShareAlt,
faExternalLinkAlt,
facActivityPub,
faFlag,
faHistory,
faFilePen
@ -95,6 +97,11 @@ const ExtraButtons = {
.then(() => this.$emit('onSuccess'))
.catch(err => this.$emit('onError', err.error.error))
},
copyFediLink () {
navigator.clipboard.writeText(this.fediLinkURL)
.then(() => this.$emit('onSuccess'))
.catch(err => this.$emit('onError', err.error.error))
},
bookmarkStatus () {
this.$store.dispatch('bookmark', { id: this.status.id })
.then(() => this.$emit('onSuccess'))
@ -136,26 +143,18 @@ const ExtraButtons = {
},
doRedraftStatus () {
this.$store.dispatch('fetchStatusSource', { id: this.status.id })
.then(data => {
let repliedUserId = this.status.in_reply_to_user_id;
let repliedUser = this.status.attentions.filter(user =>
user.id === repliedUserId);
this.$store.dispatch('openPostStatusModal', {
isRedraft: true,
attentions: this.status.attentions,
statusId: this.status.id,
subject: data.spoiler_text,
statusText: data.text,
statusIsSensitive: this.status.nsfw,
statusPoll: this.status.poll,
statusFiles: [...this.status.attachments],
statusScope: this.status.visibility,
statusLanguage: this.status.language,
statusContentType: data.content_type,
replyTo: this.status.in_reply_to_status_id,
repliedUser: repliedUser
})
})
.then(data => this.$store.dispatch('openPostStatusModal', {
isRedraft: true,
statusId: this.status.id,
subject: data.spoiler_text,
statusText: data.text,
statusIsSensitive: this.status.nsfw,
statusPoll: this.status.poll,
statusFiles: [...this.status.attachments],
statusScope: this.status.visibility,
statusLanguage: this.status.language,
statusContentType: data.content_type
}))
this.doDeleteStatus()
},
showRedraftStatusConfirmDialog () {
@ -178,7 +177,7 @@ const ExtraButtons = {
canPin () {
return this.ownStatus && (this.status.visibility === 'public' || this.status.visibility === 'unlisted')
},
canMute () {
loggedIn () {
return !!this.currentUser
},
canTranslate () {
@ -194,6 +193,16 @@ const ExtraButtons = {
return this.status.external_url
}
},
showFediLinks () {
return this.$store.getters.mergedConfig.showFediLinks === true
},
fediLinkURL () {
try {
return this.status.external_uri.replace(/^https?/, 'web+ap')
} catch (e) {
return null
}
},
shouldConfirmDelete () {
return this.$store.getters.mergedConfig.modalOnDelete
},

View file

@ -10,7 +10,7 @@
<template v-slot:content="{close}">
<div class="dropdown-menu">
<button
v-if="canMute && !status.thread_muted"
v-if="loggedIn && !status.thread_muted"
class="button-default dropdown-item dropdown-item-icon"
@click.prevent="muteConversation"
>
@ -20,7 +20,7 @@
/><span>{{ $t("status.mute_conversation") }}</span>
</button>
<button
v-if="canMute && status.thread_muted"
v-if="loggedIn && status.thread_muted"
class="button-default dropdown-item dropdown-item-icon"
@click.prevent="unmuteConversation"
>
@ -52,7 +52,7 @@
/><span>{{ $t("status.unpin") }}</span>
</button>
<button
v-if="!status.bookmarked"
v-if="loggedIn && !status.bookmarked"
class="button-default dropdown-item dropdown-item-icon"
@click.prevent="bookmarkStatus"
@click="close"
@ -63,7 +63,7 @@
/><span>{{ $t("status.bookmark") }}</span>
</button>
<button
v-if="status.bookmarked"
v-if="loggedIn && status.bookmarked"
class="button-default dropdown-item dropdown-item-icon"
@click.prevent="unbookmarkStatus"
@click="close"
@ -127,6 +127,17 @@
icon="share-alt"
/><span>{{ $t("status.copy_link") }}</span>
</button>
<button
v-if="fediLinkURL && showFediLinks && loggedIn"
class="button-default dropdown-item dropdown-item-icon"
@click.prevent="copyFediLink"
@click="close"
>
<FAIcon
fixed-width
:icon="['fac', 'activity-pub']"
/><span>{{ $t("status.copy_fedi_link") }}</span>
</button>
<a
v-if="!status.is_local"
class="button-default dropdown-item dropdown-item-icon"
@ -139,6 +150,18 @@
icon="external-link-alt"
/><span>{{ $t("status.external_source") }}</span>
</a>
<a
v-if="fediLinkURL && showFediLinks && !loggedIn"
class="button-default dropdown-item dropdown-item-icon"
title="Source"
:href="fediLinkURL"
target="_blank"
>
<FAIcon
fixed-width
:icon="['fac', 'activity-pub']"
/><span>{{ $t("status.open_fedi_link") }}</span>
</a>
<button
class="button-default dropdown-item dropdown-item-icon"
@click.prevent="reportStatus"

View file

@ -55,9 +55,6 @@
.interactive {
.svg-inline--fa {
@media (prefers-reduced-motion: reduce) {
animation: unset;
}
animation-duration: 0.6s;
}

View file

@ -1,13 +1,9 @@
<template>
<div
class="list"
role="list"
>
<div class="list">
<div
v-for="item in items"
:key="getKey(item)"
class="list-item"
role="listitem"
>
<slot
name="item"

View file

@ -93,6 +93,9 @@ const MentionLink = {
this.highlightType
]
},
useAtIcon () {
return this.mergedConfig.useAtIcon
},
isRemote () {
return this.userName !== this.userNameFull
},

View file

@ -157,9 +157,6 @@
box-shadow: var(--panelShadow);
transition-property: transform;
transition-duration: 0.25s;
@media (prefers-reduced-motion: reduce) {
transition: unset;
}
transform: translateX(0);
z-index: 1001;
-webkit-overflow-scrolling: touch;

View file

@ -6,7 +6,6 @@ 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 ConfirmModal from '../confirm_modal/confirm_modal.vue'
import StillImage from '../still-image/still-image.vue'
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'
@ -51,8 +50,7 @@ const Notification = {
Timeago,
Status,
RichContent,
ConfirmModal,
StillImage
ConfirmModal
},
methods: {
toggleUserExpanded () {

View file

@ -116,13 +116,12 @@
scope="global"
keypath="notifications.reacted_with"
>
<still-image
<img
v-if="notification.emoji_url !== null"
class="notification-reaction-emoji"
:src="notification.emoji_url"
:title="notification.emoji"
:alt="notification.emoji"
/>
:name="notification.emoji"
>
<span
v-else
class="emoji-reaction-emoji"
@ -152,6 +151,7 @@
>
<Timeago
:time="notification.created_at"
:with-direction="true"
:auto-update="240"
/>
</router-link>

View file

@ -65,8 +65,8 @@
.follow-request-accept {
&:hover {
color: $fallback--text;
color: var(--text, $fallback--text);
color: $fallback--cGreen;
color: var(--cGreen, $fallback--text);
}
}
@ -105,12 +105,9 @@
flex: 1;
padding-left: 0.8em;
min-width: 0;
}
.heading-right, .notification-right {
.timeago {
display: inline-block;
min-width: 6em;
min-width: 3em;
text-align: right;
}
}

View file

@ -9,12 +9,11 @@ import StatusContent from '../status_content/status_content.vue'
import fileTypeService from '../../services/file_type/file_type.service.js'
import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
import { reject, map, uniqBy, debounce } from 'lodash'
import { usePostLanguageOptions } from 'src/lib/post_language'
import suggestor from '../emoji_input/suggestor.js'
import { mapGetters, mapState } from 'vuex'
import Checkbox from '../checkbox/checkbox.vue'
import Select from '../select/select.vue'
import iso6391 from 'iso-639-1'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
@ -63,13 +62,6 @@ const deleteDraft = (draftKey) => {
localStorage.setItem('drafts', JSON.stringify(draftData));
}
const interfaceToISOLanguage = (ilang) => {
const sep = ilang.indexOf("_");
return sep < 0 ?
ilang :
ilang.substr(0, sep);
}
const PostStatusForm = {
props: [
'statusId',
@ -137,13 +129,6 @@ const PostStatusForm = {
this.$refs.textarea.focus()
}
},
setup() {
const {postLanguageOptions} = usePostLanguageOptions()
return {
postLanguageOptions,
}
},
data () {
const preset = this.$route.query.message
let statusText = preset || ''
@ -153,8 +138,7 @@ const PostStatusForm = {
statusText = buildMentionsString({ user: this.repliedUser, attentions: this.attentions }, currentUser)
}
const { postContentType: contentType, postLanguage: defaultPostLanguage, sensitiveByDefault, sensitiveIfSubject, interfaceLanguage, alwaysShowSubjectInput } = this.$store.getters.mergedConfig
const postLanguage = defaultPostLanguage || interfaceToISOLanguage(interfaceLanguage)
const { postContentType: contentType, sensitiveByDefault, sensitiveIfSubject, interfaceLanguage } = this.$store.getters.mergedConfig
let statusParams = {
spoilerText: this.subject || '',
@ -165,7 +149,7 @@ const PostStatusForm = {
poll: {},
mediaDescriptions: {},
visibility: this.suggestedVisibility(),
language: postLanguage,
language: interfaceLanguage,
contentType
}
@ -180,7 +164,7 @@ const PostStatusForm = {
poll: this.statusPoll || {},
mediaDescriptions: this.statusMediaDescriptions || {},
visibility: this.statusScope || this.suggestedVisibility(),
language: this.statusLanguage || postLanguage,
language: this.statusLanguage || interfaceLanguage,
contentType: statusContentType
}
}
@ -215,10 +199,6 @@ const PostStatusForm = {
}
}
// When first loading the form, hide the subject (CW) field if it's disabled or doesn't have a starting value.
// "disableSubject" seems to take priority over "alwaysShowSubjectInput".
const showSubject = !this.disableSubject && (statusParams.spoilerText || alwaysShowSubjectInput)
return {
dropFiles: [],
uploadingFiles: false,
@ -233,10 +213,7 @@ const PostStatusForm = {
preview: null,
previewLoading: false,
emojiInputShown: false,
idempotencyKey: '',
activeEmojiInput: undefined,
activeTextInput: undefined,
subjectVisible: showSubject
idempotencyKey: ''
}
},
computed: {
@ -325,6 +302,9 @@ const PostStatusForm = {
...mapState({
mobileLayout: state => state.interface.mobileLayout
}),
isoLanguages () {
return iso6391.getAllCodes();
}
},
watch: {
'newStatus': {
@ -694,33 +674,8 @@ const PostStatusForm = {
this.$refs['emoji-input'].resize()
},
showEmojiPicker () {
if (!this.activeEmojiInput || !this.activeTextInput)
this.focusStatusInput()
this.$refs[this.activeTextInput].focus()
this.$refs[this.activeEmojiInput].triggerShowPicker()
},
focusStatusInput() {
this.activeEmojiInput = 'emoji-input'
this.activeTextInput = 'textarea'
},
focusSubjectInput() {
this.activeEmojiInput = 'subject-emoji-input'
this.activeTextInput = 'subject-input'
},
toggleSubjectVisible() {
// If hiding CW, then we need to clear the subject and reset focus
if (this.subjectVisible)
{
this.focusStatusInput()
// "nsfw" property is normally set by the @change listener, but this bypasses it.
// We need to clear it manually instead.
this.newStatus.spoilerText = ''
this.newStatus.nsfw = false
}
this.subjectVisible = !this.subjectVisible
this.$refs['textarea'].focus()
this.$refs['emoji-input'].triggerShowPicker()
},
clearError () {
this.error = null

View file

@ -118,16 +118,13 @@
/>
</div>
<EmojiInput
ref="subject-emoji-input"
v-if="subjectVisible"
v-if="!disableSubject && (newStatus.spoilerText || alwaysShowSubject)"
v-model="newStatus.spoilerText"
enable-emoji-picker
hide-emoji-button
:suggest="emojiSuggestor"
class="form-control"
>
<input
ref="subject-input"
v-model="newStatus.spoilerText"
type="text"
:placeholder="$t('post_status.content_warning')"
@ -135,7 +132,6 @@
size="1"
class="form-post-subject"
@input="onSubjectInput"
@focus="focusSubjectInput()"
>
</EmojiInput>
<i18n-t
@ -177,7 +173,6 @@
@input="resize"
@compositionupdate="resize"
@paste="paste"
@focus="focusStatusInput()"
/>
<p
v-if="hasStatusLengthLimit"
@ -208,11 +203,11 @@
class="form-control"
>
<option
v-for="language in postLanguageOptions"
:key="language.key"
:value="language.value"
v-for="language in isoLanguages"
:key="language"
:value="language"
>
{{ language.label }}
{{ language }}
</option>
</Select>
</div>
@ -281,15 +276,6 @@
>
<FAIcon icon="poll-h" />
</button>
<button
v-if="!disableSubject"
class="spoiler-icon button-unstyled"
:class="{ selected: subjectVisible }"
:title="$t('post_status.toggle_content_warning')"
@click="toggleSubjectVisible"
>
<FAIcon icon="eye-slash" />
</button>
</div>
<button
v-if="posting"
@ -470,7 +456,7 @@
}
}
.media-upload-icon, .poll-icon, .emoji-icon, .spoiler-icon {
.media-upload-icon, .poll-icon, .emoji-icon {
font-size: 1.85em;
line-height: 1.1;
flex: 1;
@ -513,11 +499,6 @@
.poll-icon {
order: 3;
justify-content: center;
}
.spoiler-icon {
order: 4;
justify-content: right;
}

View file

@ -74,9 +74,6 @@
.interactive {
.svg-inline--fa {
@media (prefers-reduced-motion: reduce) {
animation: unset;
}
animation-duration: 0.6s;
}

View file

@ -4,7 +4,6 @@ import ScopeSelector from 'src/components/scope_selector/scope_selector.vue'
import IntegerSetting from '../helpers/integer_setting.vue'
import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue'
import { usePostLanguageOptions } from 'src/lib/post_language'
import SharedComputedObject from '../helpers/shared_computed_object.js'
import ServerSideIndicator from '../helpers/server_side_indicator.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
@ -18,11 +17,6 @@ library.add(
)
const GeneralTab = {
setup() {
const {postLanguageOptions} = usePostLanguageOptions()
return {postLanguageOptions}
},
data () {
return {
subjectLineOptions: ['email', 'noop', 'masto'].map(mode => ({
@ -124,12 +118,6 @@ const GeneralTab = {
this.$store.dispatch('setOption', { name: 'translationLanguage', value: val })
}
},
postLanguage: {
get: function () { return this.$store.getters.mergedConfig.postLanguage },
set: function (val) {
this.$store.dispatch('setOption', { name: 'postLanguage', value: val })
}
},
...SharedComputedObject()
},
methods: {

View file

@ -147,8 +147,11 @@
</BooleanSetting>
</li>
<li>
<BooleanSetting path="displayPageBackgrounds">
{{ $t('settings.show_page_backgrounds') }}
<BooleanSetting
path="showFediLinks"
expert="1"
>
{{ $t('settings.show_fedi_links') }}
</BooleanSetting>
</li>
<li>
@ -488,6 +491,14 @@
</BooleanSetting>
</li>
</ul>
<li>
<BooleanSetting
path="useAtIcon"
expert="1"
>
{{ $t('settings.use_at_icon') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="mentionLinkShowAvatar">
{{ $t('settings.mention_link_show_avatar') }}
@ -585,15 +596,6 @@
{{ $t('settings.post_status_content_type') }}
</ChoiceSetting>
</li>
<li>
<ChoiceSetting
id="postLanguage"
path="postLanguage"
:options="postLanguageOptions"
>
{{ $t('settings.post_language') }}
</ChoiceSetting>
</li>
<li>
<BooleanSetting
path="alwaysShowNewPostButton"

View file

@ -10,20 +10,17 @@ import SelectableList from 'src/components/selectable_list/selectable_list.vue'
import ProgressButton from 'src/components/progress_button/progress_button.vue'
import withSubscription from 'src/components/../hocs/with_subscription/with_subscription'
import Checkbox from 'src/components/checkbox/checkbox.vue'
import withLoadMore from 'src/components/../hocs/with_load_more/with_load_more'
const BlockList = withLoadMore({
const BlockList = withSubscription({
fetch: (props, $store) => $store.dispatch('fetchBlocks'),
select: (props, $store) => get($store.state.users.currentUser, 'blockIds', []),
childPropName: 'items',
destroy: () => {}
childPropName: 'items'
})(SelectableList)
const MuteList = withLoadMore({
const MuteList = withSubscription({
fetch: (props, $store) => $store.dispatch('fetchMutes'),
select: (props, $store) => get($store.state.users.currentUser, 'muteIds', []),
childPropName: 'items',
destroy: () => {}
childPropName: 'items'
})(SelectableList)
const DomainMuteList = withSubscription({

View file

@ -33,7 +33,6 @@ const ProfileTab = {
newName: this.$store.state.users.currentUser.name_unescaped,
newBio: unescape(this.$store.state.users.currentUser.description),
newLocked: this.$store.state.users.currentUser.locked,
newPermitFollowback: this.$store.state.users.currentUser.permit_followback,
newFields: this.$store.state.users.currentUser.fields.map(field => ({ name: field.name, value: field.value })),
showRole: this.$store.state.users.currentUser.show_role,
role: this.$store.state.users.currentUser.role,
@ -136,7 +135,6 @@ const ProfileTab = {
bot: this.bot,
show_role: this.showRole,
status_ttl_days: this.expirePosts ? this.newPostTTLDays : -1,
permit_followback: this.permit_followback,
accepts_direct_messages_from: this.userAcceptsDirectMessagesFrom
/* eslint-enable camelcase */
}

View file

@ -259,19 +259,6 @@
<BooleanSetting path="serverSide_locked">
{{ $t('settings.lock_account_description') }}
</BooleanSetting>
<ul
class="setting-list suboptions"
:class="[{disabled: !serverSide_locked}]"
>
<li>
<BooleanSetting
path="serverSide_permitFollowback"
:disabled="!serverSide_locked"
>
{{ $t('settings.permit_followback_description') }}
</BooleanSetting>
</li>
</ul>
</li>
<li>
<BooleanSetting path="serverSide_discoverable">

View file

@ -268,10 +268,6 @@
.side-drawer {
overflow-x: hidden;
transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
@media (prefers-reduced-motion: reduce) {
transition-timing-function: unset;
transition: unset;
}
transition: 0.35s;
transition-property: transform;
margin: 0 0 0 -100px;

View file

@ -20,7 +20,6 @@ import generateProfileLink from 'src/services/user_profile_link_generator/user_p
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
import { muteWordHits } from '../../services/status_parser/status_parser.js'
import { unescape, uniqBy } from 'lodash'
import StillImage from '../still-image/still-image.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
@ -118,8 +117,7 @@ const Status = {
RichContent,
MentionLink,
MentionsLine,
QuoteButton,
StillImage
QuoteButton
},
props: [
'statusoid',

View file

@ -174,12 +174,12 @@
>
@{{ status.user.screen_name_ui }}
</router-link>
<StillImage
<img
v-if="!!(status.user && status.user.favicon)"
class="status-favicon"
:src="status.user.favicon"
:title="faviconAlt(status)"
/>
>
</span>
</div>
@ -190,7 +190,7 @@
>
<Timeago
:time="status.created_at"
:with-direction="!compact"
:with-direction="true"
:auto-update="60"
/>
</router-link>

View file

@ -17,9 +17,6 @@
.emoji:hover {
transform: scale(1.4);
@media (prefers-reduced-motion: reduce) {
transition: unset;
}
transition: 0.05s;
}

View file

@ -13,7 +13,6 @@ const StillImage = {
return {
stopGifs: this.$store.getters.mergedConfig.stopGifs || window.matchMedia('(prefers-reduced-motion: reduce)').matches,
isAnimated: false,
imageTypeLabel: ''
}
},
computed: {
@ -40,24 +39,14 @@ const StillImage = {
this.imageLoadError && this.imageLoadError()
},
detectAnimation (image) {
const mediaProxyAvailable = this.$store.state.instance.mediaProxyAvailable
if (!mediaProxyAvailable) {
// It's a bit aggressive to assume all images we can't find the mimetype of is animated, but necessary for
// people in need of reduced motion accessibility. As such, we'll consider those images animated if the user
// agent is set to prefer reduced motion. Otherwise, it'll just be used as an early exit.
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
// Since the canvas and images are not pixel-perfect matching (due to scaling),
// It makes the images jiggle on hover, which is not ideal for accessibility, methinks
this.isAnimated = true
return
}
this.detectWithoutMediaProxy(image)
} else {
this.detectWithMediaProxy(image)
if (this.mimetype === 'image/gif' || this.src.endsWith('.gif')) {
this.isAnimated = true
return
}
},
detectAnimationWithFetch (image) {
// harmless CORS errors without-- clean console with
if (!this.$store.state.instance.mediaProxyAvailable) return
// Animated JPEGs?
if (!(this.src.endsWith('.webp') || this.src.endsWith('.png'))) return
// Browser Cache should ensure image doesn't get loaded twice if cache exists
fetch(image.src, {
referrerPolicy: 'same-origin'
@ -66,20 +55,12 @@ const StillImage = {
// We don't need to read the whole file so only call it once
data.body.getReader().read()
.then(reader => {
// Ordered from least to most intensive
if (this.isGIF(reader.value)) {
if (this.src.endsWith('.webp') && this.isAnimatedWEBP(reader.value)) {
this.isAnimated = true
this.setLabel('GIF')
return
}
if (this.isAnimatedWEBP(reader.value)) {
if (this.src.endsWith('.png') && this.isAnimatedPNG(reader.value)) {
this.isAnimated = true
this.setLabel('WEBP')
return
}
if (this.isAnimatedPNG(reader.value)) {
this.isAnimated = true
this.setLabel('APNG')
}
})
})
@ -87,53 +68,6 @@ const StillImage = {
// this.imageLoadError && this.imageLoadError()
})
},
detectWithMediaProxy (image) {
this.detectAnimationWithFetch(image)
},
detectWithoutMediaProxy (image) {
// We'll just assume that gifs and webp are animated
const extension = image.src.split('.').pop().toLowerCase()
if (extension === 'gif') {
this.isAnimated = true
this.setLabel('GIF')
return
}
if (extension === 'webp') {
this.isAnimated = true
this.setLabel('WEBP')
return
}
// Beware the apng! use this if ye dare
// if (extension === 'png') {
// this.isAnimated = true
// this.setLabel('PNG')
// return
// }
// Hail mary for extensionless
if (extension.includes('/')) {
// Don't mind the CORS error barrage
this.detectAnimationWithFetch(image)
}
},
setLabel (name) {
this.imageTypeLabel = name;
},
isGIF (data) {
// I am a perfectly sane individual
//
// GIF HEADER CHUNK
// === START HEADER ===
// 47 49 46 38 ("GIF8")
const gifHeader = [0x47, 0x49, 0x46];
for (let i = 0; i < 3; i++) {
if (data[i] !== gifHeader[i]) {
return false;
}
}
return true
},
isAnimatedWEBP (data) {
/**
* WEBP HEADER CHUNK
@ -167,55 +101,16 @@ const StillImage = {
const idatPos = str.indexOf('IDAT')
return (str.substring(0, idatPos > 0 ? idatPos : 0).indexOf('acTL') > 0)
},
drawThumbnail() {
const canvas = this.$refs.canvas;
if (!canvas) return;
const context = canvas.getContext('2d');
const image = this.$refs.src;
const parentElement = canvas.parentElement;
// Draw the quick, unscaled version first
context.drawImage(image, 0, 0, parentElement.clientWidth, parentElement.clientHeight);
// Use requestAnimationFrame to schedule the scaling to the next frame
requestAnimationFrame(() => {
// Compute scaling ratio between the natural dimensions of the image and its display dimensions
const scalingRatioWidth = parentElement.clientWidth / image.naturalWidth;
const scalingRatioHeight = parentElement.clientHeight / image.naturalHeight;
// Adjust for high-DPI displays
const ratio = window.devicePixelRatio || 1;
canvas.width = image.naturalWidth * scalingRatioWidth * ratio;
canvas.height = image.naturalHeight * scalingRatioHeight * ratio;
canvas.style.width = `${parentElement.clientWidth}px`;
canvas.style.height = `${parentElement.clientHeight}px`;
context.scale(ratio, ratio);
// Maintain the aspect ratio of the image
const imgAspectRatio = image.naturalWidth / image.naturalHeight;
const canvasAspectRatio = parentElement.clientWidth / parentElement.clientHeight;
let drawWidth, drawHeight;
if (imgAspectRatio > canvasAspectRatio) {
drawWidth = parentElement.clientWidth;
drawHeight = parentElement.clientWidth / imgAspectRatio;
} else {
drawHeight = parentElement.clientHeight;
drawWidth = parentElement.clientHeight * imgAspectRatio;
}
context.clearRect(0, 0, canvas.width, canvas.height); // Clear the previous unscaled image
context.imageSmoothingEnabled = true;
context.imageSmoothingQuality = 'high';
// Draw the good one for realsies
const dx = (parentElement.clientWidth - drawWidth) / 2;
const dy = (parentElement.clientHeight - drawHeight) / 2;
context.drawImage(image, dx, dy, drawWidth, drawHeight);
});
}
drawThumbnail () {
const canvas = this.$refs.canvas
if (!this.$refs.canvas) return
const image = this.$refs.src
const width = image.naturalWidth
const height = image.naturalHeight
canvas.width = width
canvas.height = height
canvas.getContext('2d').drawImage(image, 0, 0, width, height)
}
},
updated () {
// On computed animated change

View file

@ -1,15 +1,9 @@
<template>
<div
ref="still-image"
class="still-image"
:class="{ animated: animated }"
:style="style"
>
<div
v-if="animated && imageTypeLabel"
class="image-type-label">
{{ imageTypeLabel }}
</div>
<canvas
v-if="animated"
ref="canvas"
@ -63,26 +57,30 @@
}
}
.image-type-label {
position: absolute;
top: 0.25em;
left: 0.25em;
line-height: 1;
font-size: 0.6em;
background: rgba(127, 127, 127, 0.5);
color: #fff;
padding: 2px 4px;
border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
z-index: 2;
visibility: var(--_still-image-label-visibility, visible);
}
&.animated {
&::before {
zoom: var(--_still_image-label-scale, 1);
content: 'gif';
position: absolute;
line-height: 1;
font-size: 0.7em;
top: 0.5em;
left: 0.5em;
background: rgba(127, 127, 127, 0.5);
color: #fff;
display: block;
padding: 2px 4px;
border-radius: $fallback--tooltipRadius;
border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
z-index: 2;
visibility: var(--_still-image-label-visibility, visible);
}
&:hover canvas {
display: none;
}
&:hover .image-type-label {
&:hover::before {
visibility: var(--_still-image-label-visibility, hidden);
}

View file

@ -62,9 +62,6 @@
border-top-right-radius: 0;
border-top-left-radius: 0;
transform: translateY(-100%);
@media (prefers-reduced-motion: reduce) {
transition: unset;
}
transition: transform 100ms;
}
@ -92,9 +89,6 @@
svg {
margin-left: 0.6em;
@media (prefers-reduced-motion: reduce) {
transition: unset;
}
transition: transform 100ms;
}

View file

@ -33,7 +33,7 @@
--_avatarShadowBox: var(--avatarStatusShadow);
--_avatarShadowFilter: var(--avatarStatusShadowFilter);
--_avatarShadowInset: var(--avatarStatusShadowInset);
// --_still-image-label-visibility: hidden;
--_still-image-label-visibility: hidden;
display: inline-block;
position: relative;

View file

@ -8,6 +8,8 @@ import Select from '../select/select.vue'
import RichContent from 'src/components/rich_content/rich_content.jsx'
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
import facActivityPub from '../../assets/icons/activity-pub'
import { notificationsFromStore } from 'src/services/notification_utils/notification_utils.js'
import { mapGetters } from 'vuex'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
@ -15,7 +17,7 @@ import {
faRss,
faSearchPlus,
faExternalLinkAlt,
faEdit
faEdit,
} from '@fortawesome/free-solid-svg-icons'
library.add(
@ -23,7 +25,8 @@ library.add(
faBell,
faSearchPlus,
faExternalLinkAlt,
faEdit
faEdit,
facActivityPub
)
export default {
@ -34,7 +37,9 @@ export default {
return {
followRequestInProgress: false,
betterShadow: this.$store.state.interface.browserSupport.cssFilter,
showingConfirmMute: false
showingConfirmMute: false,
showingApproveConfirmDialog: false,
showingDenyConfirmDialog: false
}
},
created () {
@ -44,6 +49,16 @@ export default {
user () {
return this.$store.getters.findUser(this.userId)
},
showFediLinks () {
return this.$store.getters.mergedConfig.showFediLinks === true
},
fediLinkURL () {
try {
return this.user._original.pleroma.ap_id.replace(/^https?/, 'web+ap')
} catch (e) {
return null
}
},
relationship () {
return this.$store.getters.relationship(this.userId)
},
@ -117,6 +132,12 @@ export default {
shouldConfirmMute () {
return this.mergedConfig.modalOnMute
},
shouldConfirmApprove () {
return this.mergedConfig.modalOnApproveFollow
},
shouldConfirmDeny () {
return this.mergedConfig.modalOnDenyFollow
},
...mapGetters(['mergedConfig'])
},
components: {
@ -193,6 +214,58 @@ export default {
},
mentionUser () {
this.$store.dispatch('openPostStatusModal', { replyTo: true, repliedUser: this.user })
},
showApproveConfirmDialog () {
this.showingApproveConfirmDialog = true
},
hideApproveConfirmDialog () {
this.showingApproveConfirmDialog = false
},
showDenyConfirmDialog () {
this.showingDenyConfirmDialog = true
},
hideDenyConfirmDialog () {
this.showingDenyConfirmDialog = false
},
approveUser () {
if (this.shouldConfirmApprove) {
this.showApproveConfirmDialog()
} else {
this.doApprove()
}
},
doApprove () {
const notifId = this.findFollowRequestNotificationId()
this.$store.dispatch('approveUser', this.user.id)
this.$store.dispatch('markSingleNotificationAsSeen', { id: notifId })
this.$store.dispatch('updateNotification', {
id: notifId,
updater: notification => {
notification.type = 'follow'
}
})
this.hideApproveConfirmDialog()
},
denyUser () {
if (this.shouldConfirmDeny) {
this.showDenyConfirmDialog()
} else {
this.doDeny()
}
},
doDeny () {
const notifId = this.findFollowRequestNotificationId()
this.$store.dispatch('denyUser', this.user.id)
.then(() => {
this.$store.dispatch('dismissNotificationLocal', { id: notifId })
})
this.hideDenyConfirmDialog()
},
findFollowRequestNotificationId () {
const notif = notificationsFromStore(this.$store).find(
(notif) => notif.from_profile.id === this.user.id && notif.type === 'follow_request'
)
return notif && notif.id
}
}
}

View file

@ -97,14 +97,13 @@
.user-info {
color: $fallback--lightText;
color: var(--lightText, $fallback--lightText);
padding: 0 26px;
padding: 0 1.5em;
.container {
min-width: 0;
padding: 16px 0 6px;
display: flex;
align-items: flex-start;
max-height: 56px;
> * {
min-width: 0;
@ -165,7 +164,8 @@
}
.user-summary {
display: block;
display: flex;
flex-wrap: wrap;
margin-left: 0.6em;
text-align: left;
text-overflow: ellipsis;
@ -184,6 +184,11 @@
}
}
.user-names {
flex-grow: 1;
max-width: 100%;
}
.user-name {
text-overflow: ellipsis;
overflow: hidden;
@ -228,6 +233,13 @@
}
}
.button-line {
display: flex;
min-width: 10em;
align-items: flex-start;
justify-content: flex-end;
}
.user-meta {
margin-bottom: .15em;
display: flex;
@ -242,6 +254,12 @@
text-align: left;
}
.requested_by {
.btn {
margin-right: .25em;
}
}
.highlighter {
flex: 0 1 auto;
display: flex;

View file

@ -1,175 +1,108 @@
<template>
<div
class="user-card"
:class="classes"
>
<div
:class="{ 'hide-bio': hideBio }"
:style="style"
class="background-image"
/>
<div class="user-card" :class="classes">
<div :class="{ 'hide-bio': hideBio }" :style="style" class="background-image" />
<div class="panel-heading -flexible-height">
<div class="user-info">
<div class="container">
<a
v-if="allowZoomingAvatar"
class="user-info-avatar-link"
@click="zoomAvatar"
>
<UserAvatar
:better-shadow="betterShadow"
:user="user"
/>
<a v-if="allowZoomingAvatar" class="user-info-avatar-link" @click="zoomAvatar">
<UserAvatar :better-shadow="betterShadow" :user="user" />
<div class="user-info-avatar-link-overlay">
<FAIcon
class="fa-scale-110 fa-old-padding"
icon="search-plus"
/>
<FAIcon class="fa-scale-110 fa-old-padding" icon="search-plus" />
</div>
</a>
<router-link
v-else
:to="userProfileLink(user)"
>
<UserAvatar
:better-shadow="betterShadow"
:user="user"
/>
<router-link v-else :to="userProfileLink(user)">
<UserAvatar :better-shadow="betterShadow" :user="user" />
</router-link>
<div class="user-summary">
<div class="top-line">
<RichContent
:title="user.name"
class="user-name"
:html="user.name"
:emoji="user.emoji"
/>
<button
v-if="!isOtherUser && user.is_local"
class="button-unstyled edit-profile-button"
@click.stop="openProfileTab"
>
<FAIcon
fixed-width
class="icon"
icon="edit"
:title="$t('user_card.edit_profile')"
/>
</button>
<a
v-if="isOtherUser && !user.is_local"
:href="user.statusnet_profile_url"
target="_blank"
class="button-unstyled external-link-button"
>
<FAIcon
class="icon"
icon="external-link-alt"
/>
</a>
<a
v-if="isOtherUser"
:href="user.statusnet_profile_url + '.rss'"
target="_blank"
class="button-unstyled external-link-button"
>
<FAIcon
class="icon"
icon="rss"
/>
</a>
<AccountActions
v-if="isOtherUser && loggedIn"
:user="user"
:relationship="relationship"
/>
<div class="user-names">
<div class="top-line">
<RichContent :title="user.name" class="user-name" :html="user.name" :emoji="user.emoji" />
</div>
<div class="bottom-line">
<router-link class="user-screen-name" :title="user.screen_name_ui" :to="userProfileLink(user)">
@{{ user.screen_name_ui }}
</router-link>
<template v-if="!hideBio">
<span v-if="user.deactivated" class="alert user-role">
{{ $t('user_card.deactivated') }}
</span>
<span v-if="!!visibleRole" class="alert user-role">
{{ $t(`general.role.${visibleRole}`) }}
</span>
<span v-if="user.bot" class="alert user-role">
{{ $t('user_card.bot') }}
</span>
</template>
<span v-if="user.locked">
<FAIcon class="lock-icon" icon="lock" size="sm" />
</span>
<span v-if="!mergedConfig.hideUserStats && !hideBio" class="dailyAvg">{{ dailyAvg }} {{
$t('user_card.per_day') }}</span>
</div>
</div>
<div class="bottom-line">
<router-link
class="user-screen-name"
:title="user.screen_name_ui"
:to="userProfileLink(user)"
>
@{{ user.screen_name_ui }}
</router-link>
<template v-if="!hideBio">
<span
v-if="user.deactivated"
class="alert user-role"
>
{{ $t('user_card.deactivated') }}
</span>
<span
v-if="!!visibleRole"
class="alert user-role"
>
{{ $t(`general.role.${visibleRole}`) }}
</span>
<span
v-if="user.bot"
class="alert user-role"
>
{{ $t('user_card.bot') }}
</span>
</template>
<span v-if="user.locked">
<FAIcon
class="lock-icon"
icon="lock"
size="sm"
/>
</span>
<span
v-if="!mergedConfig.hideUserStats && !hideBio"
class="dailyAvg"
>{{ dailyAvg }} {{ $t('user_card.per_day') }}</span>
<div class="button-line">
<button v-if="!isOtherUser && user.is_local" class="button-unstyled edit-profile-button"
@click.stop="openProfileTab">
<FAIcon fixed-width class="icon" icon="edit" :title="$t('user_card.edit_profile')" />
</button>
<a v-if="isOtherUser && !user.is_local" :href="user.statusnet_profile_url" target="_blank"
class="button-unstyled external-link-button" :title="$t('user_card.external_source')">
<FAIcon class="icon" icon="external-link-alt" />
</a>
<a v-if="fediLinkURL && showFediLinks && !hideBio" :href="fediLinkURL" target="_blank"
class="button-unstyled external-link-button" :title="$t('user_card.open_fedi_link')">
<FAIcon class="icon" :icon="['fac', 'activity-pub']" />
</a>
<a v-if="isOtherUser" :href="user.statusnet_profile_url + '.rss'" target="_blank"
class="button-unstyled external-link-button">
<FAIcon class="icon" icon="rss" />
</a>
<AccountActions v-if="isOtherUser && loggedIn" :user="user" :relationship="relationship" />
</div>
</div>
</div>
<div class="user-meta">
<div
v-if="relationship.blocked_by && loggedIn && isOtherUser"
class="blocking"
>
<div v-if="relationship.blocked_by && loggedIn && isOtherUser" class="blocking">
{{ $t('user_card.blocks_you') }}
</div>
<div
v-if="relationship.followed_by && loggedIn && isOtherUser"
class="following"
>
<div v-if="relationship.followed_by && loggedIn && isOtherUser" class="following">
{{ $t('user_card.follows_you') }}
</div>
<div
v-if="relationship.requested_by && loggedIn && isOtherUser"
class="requested_by"
style="white-space: nowrap;"
>
{{ $t('user_card.requested_by') }}
<button
class="btn button-default"
:title="$t('tool_tip.accept_follow_request')"
@click="approveUser()"
>
<FAIcon
icon="check"
class="fa-scale-110 fa-old-padding follow-request-accept"
/>
</button>
<button
class="btn button-default"
:title="$t('tool_tip.reject_follow_request')"
@click="denyUser()"
>
<FAIcon
icon="times"
class="fa-scale-110 fa-old-padding follow-request-reject"
/>
</button>
</div>
<div
v-if="isOtherUser && (loggedIn || !switcher)"
class="highlighter"
>
<div v-if="isOtherUser && (loggedIn || !switcher)" class="highlighter">
<!-- id's need to be unique, otherwise vue confuses which user-card checkbox belongs to -->
<input
v-if="userHighlightType !== 'disabled'"
:id="'userHighlightColorTx'+user.id"
v-model="userHighlightColor"
class="userHighlightText"
type="text"
>
<input
v-if="userHighlightType !== 'disabled'"
:id="'userHighlightColor'+user.id"
v-model="userHighlightColor"
class="userHighlightCl"
type="color"
>
<input v-if="userHighlightType !== 'disabled'" :id="'userHighlightColorTx' + user.id"
v-model="userHighlightColor" class="userHighlightText" type="text">
<input v-if="userHighlightType !== 'disabled'" :id="'userHighlightColor' + user.id" v-model="userHighlightColor"
class="userHighlightCl" type="color">
{{ ' ' }}
<Select
:id="'userHighlightSel'+user.id"
v-model="userHighlightType"
class="userHighlightSel"
>
<Select :id="'userHighlightSel' + user.id" v-model="userHighlightType" class="userHighlightSel">
<option value="disabled">
{{ $t('user_card.highlight.disabled') }}
</option>
@ -185,144 +118,93 @@
</Select>
</div>
</div>
<div
v-if="loggedIn && isOtherUser"
class="user-interactions"
>
<div v-if="loggedIn && isOtherUser" class="user-interactions">
<div class="btn-group">
<FollowButton
:relationship="relationship"
:user="user"
:disabled="relationship.blocked_by"
/>
<FollowButton :relationship="relationship" :user="user" :disabled="relationship.blocked_by" />
<template v-if="relationship.following">
<ProgressButton
v-if="!relationship.subscribing"
class="btn button-default"
:click="subscribeUser"
:title="$t('user_card.subscribe')"
>
<ProgressButton v-if="!relationship.subscribing" class="btn button-default" :click="subscribeUser"
:title="$t('user_card.subscribe')">
<FAIcon icon="bell" />
</ProgressButton>
<ProgressButton
v-else
class="btn button-default toggled"
:click="unsubscribeUser"
:title="$t('user_card.unsubscribe')"
>
<ProgressButton v-else class="btn button-default toggled" :click="unsubscribeUser"
:title="$t('user_card.unsubscribe')">
<FALayers>
<FAIcon
icon="rss"
transform="left-5 shrink-6 up-3 rotate-20"
flip="horizontal"
/>
<FAIcon
icon="rss"
transform="right-5 shrink-6 up-3 rotate-20"
/>
<FAIcon icon="rss" transform="left-5 shrink-6 up-3 rotate-20" flip="horizontal" />
<FAIcon icon="rss" transform="right-5 shrink-6 up-3 rotate-20" />
<FAIcon icon="bell" />
</FALayers>
</ProgressButton>
</template>
</div>
<div>
<button
v-if="relationship.muting"
class="btn button-default btn-block toggled"
:disabled="user.deactivated"
@click="unmuteUser"
>
<button v-if="relationship.muting" class="btn button-default btn-block toggled" :disabled="user.deactivated"
@click="unmuteUser">
{{ $t('user_card.muted') }}
</button>
<button
v-else
class="btn button-default btn-block"
:disabled="user.deactivated"
@click="muteUser"
>
<button v-else class="btn button-default btn-block" :disabled="user.deactivated" @click="muteUser">
{{ $t('user_card.mute') }}
</button>
</div>
<div>
<button
class="btn button-default btn-block"
:disabled="user.deactivated"
@click="mentionUser"
>
<button class="btn button-default btn-block" :disabled="user.deactivated" @click="mentionUser">
{{ $t('user_card.mention') }}
</button>
</div>
<ModerationTools
v-if="loggedIn.role === 'admin'"
:user="user"
/>
<ModerationTools v-if="loggedIn.role === 'admin'" :user="user" />
</div>
<div
v-if="!loggedIn && user.is_local"
class="user-interactions"
>
<div v-if="!loggedIn && user.is_local" class="user-interactions">
<RemoteFollow :user="user" />
</div>
</div>
</div>
<div
v-if="!hideBio"
class="panel-body"
>
<div
v-if="!mergedConfig.hideUserStats && switcher"
class="user-counts"
>
<div
class="user-count"
@click.prevent="setProfileView('statuses')"
>
<div v-if="!hideBio" class="panel-body">
<div v-if="!mergedConfig.hideUserStats && switcher" class="user-counts">
<div class="user-count" @click.prevent="setProfileView('statuses')">
<h5>{{ $t('user_card.statuses') }}</h5>
<span>{{ user.statuses_count }} <br></span>
</div>
<div
class="user-count"
@click.prevent="setProfileView('friends')"
>
<div class="user-count" @click.prevent="setProfileView('friends')">
<h5>{{ $t('user_card.followees') }}</h5>
<span>{{ hideFollowsCount ? $t('user_card.hidden') : user.friends_count }}</span>
</div>
<div
class="user-count"
@click.prevent="setProfileView('followers')"
>
<div class="user-count" @click.prevent="setProfileView('followers')">
<h5>{{ $t('user_card.followers') }}</h5>
<span>{{ hideFollowersCount ? $t('user_card.hidden') : user.followers_count }}</span>
</div>
</div>
<RichContent
v-if="!hideBio"
class="user-card-bio"
:html="user.description_html"
:emoji="user.emoji"
:handle-links="true"
/>
<RichContent v-if="!hideBio" class="user-card-bio" :html="user.description_html" :emoji="user.emoji"
:handle-links="true" />
</div>
<teleport to="#modal">
<confirm-modal
v-if="showingConfirmMute"
:title="$t('user_card.mute_confirm_title')"
<confirm-modal v-if="showingConfirmMute" :title="$t('user_card.mute_confirm_title')"
:confirm-text="$t('user_card.mute_confirm_accept_button')"
:cancel-text="$t('user_card.mute_confirm_cancel_button')"
@accepted="doMuteUser"
@cancelled="hideConfirmMute"
>
<i18n-t
keypath="user_card.mute_confirm"
tag="span"
>
:cancel-text="$t('user_card.mute_confirm_cancel_button')" @accepted="doMuteUser" @cancelled="hideConfirmMute">
<i18n-t keypath="user_card.mute_confirm" tag="span">
<template #user>
<span
v-text="user.screen_name_ui"
/>
<span v-text="user.screen_name_ui" />
</template>
</i18n-t>
</confirm-modal>
<confirm-modal
v-if="showingApproveConfirmDialog"
:title="$t('user_card.approve_confirm_title')"
:confirm-text="$t('user_card.approve_confirm_accept_button')"
:cancel-text="$t('user_card.approve_confirm_cancel_button')"
@accepted="doApprove"
@cancelled="hideApproveConfirmDialog"
>
{{ $t('user_card.approve_confirm', { user: user.screen_name_ui }) }}
</confirm-modal>
<confirm-modal
v-if="showingDenyConfirmDialog"
:title="$t('user_card.deny_confirm_title')"
:confirm-text="$t('user_card.deny_confirm_accept_button')"
:cancel-text="$t('user_card.deny_confirm_cancel_button')"
@accepted="doDeny"
@cancelled="hideDenyConfirmDialog"
>
{{ $t('user_card.deny_confirm', { user: user.screen_name_ui }) }}
</confirm-modal>
</teleport>
</div>
</template>

View file

@ -145,12 +145,10 @@ const UserProfile = {
if (user) {
loadById(user.id)
this.note = user.relationship.note
this.$store.dispatch('setDisplayBackground', user.background_image)
} else {
this.$store.dispatch('fetchUser', userNameOrId)
.then(({ id, relationship, background_image }) => {
.then(({ id, relationship }) => {
this.note = relationship.note
this.$store.dispatch('setDisplayBackground', background_image)
return loadById(id)
})
.catch((reason) => {
@ -227,9 +225,6 @@ const UserProfile = {
Conversation,
RichContent,
FollowedTagList
},
beforeRouteLeave(to, from) {
this.$store.dispatch('setDisplayBackground', null)
}
}

View file

@ -884,6 +884,7 @@
"upload_a_photo": "Pujar una foto",
"useStreamingApi": "Rebre apunts i notificacions en temps real",
"useStreamingApiWarning": "És genial emprar-lo. Si es trenca, refresca, suposo?",
"use_at_icon": "Mostra el símbol {'@'} com a icona enlloc de text",
"use_contain_fit": "No retallar els adjunts en miniatures",
"use_one_click_nsfw": "Obre els adjunts NSFW amb només un clic",
"user_mutes": "Usuaris",

View file

@ -916,6 +916,7 @@
"upload_a_photo": "Lade ein Foto hoch",
"useStreamingApi": "Empfange Posts und Benachrichtigungen in Echtzeit",
"useStreamingApiWarning": "(Nicht empfohlen, experimentell, bekannt dafür, Posts zu überspringen)",
"use_at_icon": "{'@'}-Symbol als Icon und nicht als Text anzeigen",
"use_blurhash": "Blurhash für NSFW-Vorschauen verwenden",
"use_contain_fit": "Vorschaubilder nicht zuschneiden",
"use_one_click_nsfw": "Heikle Anhänge mit nur einem Klick öffnen",

View file

@ -1,215 +0,0 @@
{
"about": {
"mrf": {
"keyword": {
"keyword_policies": "Πολιτικές λέξεων-κλειδιών",
"reject": "Απόρριψη",
"replace": "Αντικατάσταση"
},
"mrf_policies": "Ενεργοποιημένες πολιτικές MRF",
"mrf_policies_desc": "",
"simple": {
"accept": "Αποδοχή",
"accept_desc": "Αυτό το instance αποδέχεται μηνύματα μόνο από τα ακόλουθα instances:",
"ftl_removal": "Αφαίρεση από το χρονολόγιο \"Γνωστού Δίκτυου\"",
"ftl_removal_desc": "Αυτό το instance αφαιρεί αυτά τα instances από το χρονολόγιο \"Γνωστού Δικτύου\":",
"quarantine": "Καραντίνα",
"quarantine_desc": "Αυτό το instance δε θα στέλνει αναρτήσεις στα ακόλουθα instances:",
"reason": "Λόγος",
"simple_policies": "Πολιτικές του instance"
}
}
},
"announcements": {
"all_day_prompt": "Αυτό είναι ένα ολοήμερο συμβάν",
"cancel_edit_action": "Ακύρωση",
"close_error": "Κλείσιμο",
"delete_action": "Διαγραφή",
"edit_action": "Επεξεργασία",
"end_time_display": "Λήγει στις {time}",
"page_header": "Ανακοινώσεις",
"title": "Ανακοίνωση"
},
"chats": {
"empty_message_error": "Δε μπορεί να σταλεί κενό μήνυμα",
"error_sending_message": "Κάτι πήγε λάθος κατά την αποστολή του μηνύματος.",
"message_user": "Στείλε μήνυμα στον/στην {nickname}",
"more": "Περισσότερα",
"new": "Νέο Chat",
"you": "Εσείς:"
},
"display_date": {
"today": "Σήμερα"
},
"domain_mute_card": {
"mute": "Σίγαση"
},
"emoji": {
"add_emoji": "Εισαγωγή emoji",
"load_all": "Φόρτωση όλων των {emojiAmount} emoji",
"recent": "Χρησιμοποιήθηκαν πρόσφατα",
"search_emoji": "Αναζήτηση για ένα emoji",
"stickers": "Αυτοκόλλητα"
},
"errors": {
"storage_unavailable": "Το Pleroma δε μπόρεσε να προσπελάσει τον αποθηκευτικό χώρο του browser. Η σύνδεσή σας ή οι τοπικές ρυθμίσεις σας δε θα αποθηκευτούν και μπορεί να αντιμετωπίσετε απρόοπτα θέματα. Προσπαθήστε να ενεργοποιήσετε τα cookies."
},
"exporter": {
"export": "Εξαγωγή"
},
"features_panel": {
"text_limit": "Όριο κειμένου"
},
"file_type": {
"audio": "Ήχος",
"file": "Αρχείο",
"image": "Εικόνα",
"video": "Βίντεο"
},
"general": {
"apply": "Εφαρμογή",
"cancel": "Ακύρωση",
"close": "Κλείσιμο",
"disable": "Απενεργοποίηση",
"enable": "Ενεργοποίηση",
"error_retry": "Παρακαλώ δοκιμάστε ξανά",
"flash_content": "Κάντε κλικ για την εμφάνιση Flash περιεχομένου με τη χρήση του Ruffle (Πειραματικό, μπορεί να μη λειτουργεί).",
"loading": "Φόρτωση…",
"more": "Περισσότερα",
"optional": "προαιρετικό",
"retry": "Δοκιμάστε ξανά",
"role": {
"admin": "Διαχειριστής",
"moderator": "Συντονιστής"
},
"scope_in_timeline": {
"direct": "Άμεσο",
"local": "Τοπικό",
"private": "Μόνο για ακόλουθους",
"public": "Δημόσιο",
"unlisted": "Εκτός Λίστας"
},
"show_less": "Δείξε λιγότερα",
"show_more": "Δείξε περισσότερα"
},
"image_cropper": {
"cancel": "Ακύρωση",
"crop_picture": "Περικοπή εικόνας",
"save": "Αποθήκευση",
"save_without_cropping": "Αποθήκευση χωρίς περικοπή"
},
"importer": {
"success": "Εισήχθη επιτυχώς."
},
"languages": {
"ar": "Αραβικά",
"az": "Αζερικά",
"bg": "Βουλγαρικά",
"cs": "Τσεχικά",
"da": "Δανικά",
"de": "Γερμανικά",
"el": "Ελληνικά",
"en": "Αγγλικά",
"eo": "Εσπεράντο",
"es": "Ισπανικά",
"fa": "Περσικά",
"fi": "Φινλανδικά",
"fr": "Γαλλικά",
"ga": "Ιρλανδικά",
"he": "Εβραϊκά",
"hi": "Χίντι",
"hu": "Ουγγρικά",
"id": "Ινδονησιακά",
"it": "Ιταλικά",
"ja": "Ιαπωνικά",
"ko": "Κορεατικά",
"lt": "Λιθουανικά",
"lv": "Λετονικά",
"nl": "Ολλανδικά",
"pl": "Πολωνικά",
"pt": "Πορτογαλικά",
"ru": "Ρωσικά",
"sk": "Σλοβακικά",
"sv": "Σουηδικά",
"tr": "Τουρκικά",
"translated_from": {
"ar": "Μεταφράστηκε από τα @:languages.ar",
"az": "Μεταφράστηκε από τα @:languages.az",
"bg": "Μεταφράστηκε από τα @:languages.bg",
"cs": "Μεταφράστηκε από τα @:languages.cs",
"da": "Μεταφράστηκε από τα @:languages.da",
"de": "Μεταφράστηκε από τα @:languages.de",
"el": "Μεταφράστηκε από τα @:languages.el",
"en": "Μεταφράστηκε από τα @:languages.en",
"eo": "Μεταφράστηκε από τα @:languages.eo",
"es": "Μεταφράστηκε από τα @:languages.es",
"fa": "Μεταφράστηκε από τα @:languages.fa",
"fi": "Μεταφράστηκε από τα @:languages.fi",
"fr": "Μεταφράστηκε από τα @:languages.fr",
"ga": "Μεταφράστηκε από τα @:languages.ga",
"he": "Μεταφράστηκε από τα @:languages.he",
"hi": "Μεταφράστηκε από τα @:languages.hi",
"hu": "Μεταφράστηκε από τα @:languages.hu",
"id": "Μεταφράστηκε από τα @:languages.id",
"it": "Μεταφράστηκε από τα @:languages.it",
"ja": "Μεταφράστηκε από τα @:languages.ja",
"ko": "Μεταφράστηκε από τα @:languages.ko",
"lt": "Μεταφράστηκε από τα @:languages.lt",
"lv": "Μεταφράστηκε από τα @:languages.lv",
"nl": "Μεταφράστηκε από τα @:languages.nl",
"pl": "Μεταφράστηκε από τα @:languages.pl",
"pt": "Μεταφράστηκε από τα @:languages.pt",
"ru": "Μεταφράστηκε από τα @:languages.ru",
"sk": "Μεταφράστηκε από τα @:languages.sk",
"sv": "Μεταφράστηκε από τα @:languages.sv",
"tr": "tr",
"uk": "Μεταφράστηκε από τα @:languages.uk",
"zh": "Μεταφράστηκε από τα @:languages.zh"
},
"uk": "Ουκρανικά",
"zh": "Κινεζικά"
},
"lists": {
"create": "Δημιουργία",
"delete": "Διαγραφή λίστας",
"lists": "Λίστες",
"new": "Νέα Λίστα",
"save": "Αποθήκευση αλλαγών",
"search": "Αναζήτηση χρηστών",
"title": "Τίτλος λίστας"
},
"login": {
"authentication_code": "Κωδικός επαλήθευσης",
"description": "Σύνδεση με OAuth",
"enter_recovery_code": "Εισάγετε τον κωδικό ανάκτησης",
"hint": "Συνδεθείτε για να μπείτε στη συζήτηση",
"login": "Σύνδεση",
"logout": "Αποσύνδεση",
"logout_confirm": "Θέλετε σίγουρα να αποσυνδεθείτε;",
"logout_confirm_accept_button": "Αποσύνδεση",
"logout_confirm_cancel_button": "Ακύρωση",
"logout_confirm_title": "Αποσύνδεση",
"password": "Κωδικός πρόσβασης",
"placeholder": "τοονομαχρηστημου",
"recovery_code": "Κωδικός ανάκτησης",
"register": "Εγγραφή",
"username": "Όνομα χρήστη"
},
"media_modal": {
"next": "Επόμενο",
"previous": "Προηγούμενο"
},
"moderation": {
"reports": {
"add_note": "Προσθήκη σημείωσης",
"close": "Κλείσιμο",
"delete_note": "Διαγραφή",
"delete_note_accept": "Ναι, διάγραψέ το",
"delete_note_cancel": "Όχι, κράτα το",
"delete_note_confirm": "Θέλετε σίγουρα να διαγράψετε αυτήν τη σημείωση;",
"note_placeholder": "Αφήστε μια σημείωση",
"notes": "{ count } σημείωση | { count } σημειώσεις",
"statuses": "{ count } ανάρτηση| { count } αναρτήσεις"
}
}
}

View file

@ -380,7 +380,6 @@
"text/x.misskeymarkdown": "MFM"
},
"content_warning": "Content Warning (optional)",
"toggle_content_warning": "Toggle content warning",
"default": "Just arrived at Luna Nova Academy",
"direct_warning_to_all": "This post will be visible to all the mentioned users.",
"direct_warning_to_first_only": "This post will only be visible to the mentioned users at the beginning of the message.",
@ -601,7 +600,6 @@
"list_aliases_error": "Error fetching aliases: {error}",
"list_backups_error": "Error fetching backup list: {error}",
"lock_account_description": "Restrict your account to approved followers only",
"permit_followback_description": "Automatically approve requests from already followed users",
"loop_video": "Loop videos",
"loop_video_silent_only": "Loop only videos without sound (i.e. Mastodon's \"gifs\")",
"mascot": "Mastodon FE Mascot",
@ -684,7 +682,6 @@
"play_videos_in_modal": "Play videos in a popup frame",
"post_look_feel": "Posts Look & Feel",
"post_status_content_type": "Default post content type",
"post_language": "Default post language",
"posts": "Posts",
"preload_images": "Preload images",
"presets": "Presets",
@ -752,8 +749,8 @@
"show_nav_shortcuts": "Show extra navigation shortcuts in top panel",
"show_panel_nav_shortcuts": "Show timeline navigation shortcuts at the top of the panel",
"show_scrollbars": "Show side column's scrollbars",
"show_page_backgrounds": "Show page-specific backgrounds, e.g. for user profiles",
"show_wider_shortcuts": "Show wider gap between top panel shortcuts",
"show_fedi_links": "Show Fedi Links (web+ap URLs)",
"show_yous": "Show (You)s",
"stop_gifs": "Pause animated images until you hover on them",
"streaming": "Automatically show new posts when scrolled to the top",
@ -926,6 +923,7 @@
"upload_a_photo": "Upload a photo",
"useStreamingApi": "Receive posts and notifications real-time",
"useStreamingApiWarning": "It's cool use it. If it breaks refresh I guess?",
"use_at_icon": "Display {'@'} symbol as an icon instead of text",
"use_contain_fit": "Don't crop the attachment in thumbnails",
"use_one_click_nsfw": "Open NSFW attachments with just one click",
"user_mutes": "Users",
@ -964,6 +962,7 @@
"bookmark": "Bookmark",
"collapse_attachments": "Collapse attachments",
"copy_link": "Copy link to post",
"copy_fedi_link": "Copy fedi link",
"delete": "Delete post",
"delete_confirm": "Do you really want to delete this post?",
"delete_confirm_accept_button": "Yes, delete it",
@ -975,6 +974,7 @@
"edited_at": "Edited {time}",
"expand": "Expand",
"external_source": "External source",
"open_fedi_link": "Open fedi link",
"favorites": "Favorites",
"hide_attachment": "Hide attachment",
"hide_content": "Hide content",
@ -1141,6 +1141,7 @@
"deny_confirm_title": "Deny follow request",
"domain_muted": "Unblock domain",
"edit_profile": "Edit profile",
"external_source": "External source",
"favorites": "Favorites",
"follow": "Follow",
"follow_cancel": "Cancel request",
@ -1174,6 +1175,7 @@
"mute_progress": "Muting…",
"muted": "Muted",
"note": "Private note",
"open_fedi_link": "Open fedi link",
"per_day": "per day",
"remote_follow": "Remote follow",
"remove_follower": "Remove follower",

View file

@ -425,8 +425,6 @@
"reason_placeholder": "Cette instance modère les inscriptions manuellement.\nExpliquer ce qui motive votre inscription à l'administration.",
"register": "Enregistrer",
"registration": "Inscription",
"request_sent": "Votre demande d'enregistrement est en attente d'approbation. Vous recevrez un courriel lorsque votre compte sera approuvé.",
"request_sent_title": "Demande d'abonnement envoyée",
"token": "Jeton d'invitation",
"username_placeholder": "ex : misato",
"validations": {
@ -920,6 +918,7 @@
"upload_a_photo": "Envoyer une photo",
"useStreamingApi": "Recevoir les messages et notifications en temps réel",
"useStreamingApiWarning": "(Non recommandé, expérimental, connu pour rater des messages)",
"use_at_icon": "Afficher le symbol {'@'} comme une image",
"use_contain_fit": "Ne pas rogner les miniatures des pièces-jointes",
"use_one_click_nsfw": "Ouvrir les pièces-jointes sensibles avec un seul clic",
"user_mutes": "Comptes",

View file

@ -1,15 +1,11 @@
{
"about": {
"bubble_instances": "Instance Bubble Lokal",
"bubble_instances_description": "Instansi yang dipilih oleh admin untuk mewakili instance ini",
"mrf": {
"federation": "Federasi",
"keyword": {
"ftl_removal": "Penghapusan dari Linimasa \"Jaringan Yang Dikenal\"",
"is_replaced_by": "→",
"keyword_policies": "Kebijakan Kata Kunci",
"reject": "Tolak",
"replace": "Ganti"
"reject": "Tolak"
},
"mrf_policies": "Kebijakan MRF yang diaktifkan",
"mrf_policies_desc": "Kebijakan MRF memanipulasi federasi yang terjadi pada instansi ini. Kebijakan berikut adalah yang aktif:",
@ -84,7 +80,6 @@
"keep_open": "Tetap buka pemilih",
"load_all": "Memuat semua {emojiAmount} emoji",
"load_all_hint": "Memuat {saneAmount} emoji pertama, memuat semua emoji dapat menyebabkan masalah performa.",
"recent": "Baru-baru ini digunakan",
"search_emoji": "Cari emoji",
"stickers": "Stiker",
"unicode": "Emoji Unicode"
@ -98,11 +93,9 @@
},
"features_panel": {
"media_proxy": "Proxy media",
"scope_options": "Opsi cakupan",
"text_limit": "Batas teks",
"title": "Fitur-fitur",
"upload_limit": "Batas unggahan",
"who_to_follow": "Siapa untuk diikuti"
"upload_limit": "Batas unggahan"
},
"file_type": {
"audio": "Audio",
@ -120,7 +113,6 @@
"close": "Tutup",
"confirm": "Konfirmasi",
"disable": "Nonaktifkan",
"dismiss": "Tutup",
"enable": "Aktifkan",
"error_retry": "Harap coba lagi",
"flash_content": "Klik untuk menampilkan konten Flash menggunakan Ruffle (Eksperimental, mungkin tidak bekerja).",
@ -140,8 +132,7 @@
"direct": "Langsung",
"local": "Lokal - hanya instansi kamu yang dapat melihat postingan ini",
"private": "Hanya pengikut",
"public": "Publik",
"unlisted": "Tidak Tercantum"
"public": "Publik"
},
"show_less": "Tampilkan lebih sedikit",
"show_more": "Tampilkan lebih banyak",
@ -236,7 +227,6 @@
"lists": {
"create": "Buat",
"delete": "Hapus daftar",
"following_only": "Batas mengikuti",
"lists": "Daftar",
"new": "Buat Daftar",
"save": "Simpan perubahan",
@ -286,7 +276,6 @@
"note_placeholder": "Tinggalkan catatan",
"notes": "{ count } catatan",
"reopen": "Buka kembali",
"report": "Laporan di",
"reports": "Laporan",
"resolve": "Selesaikan",
"show_closed": "Tampilkan yang telah ditutup",
@ -302,8 +291,6 @@
"administration": "Administrasi",
"announcements": "Pengumuman",
"back": "Kembali",
"bookmarks": "Bookmark",
"bubble_timeline": "Timeline Bubble",
"bubble_timeline_description": "Postingan dari instansi yang dekat dengan instansimu, yang direkomendasikan oleh admin kamu",
"chats": "Obrolan",
"dms": "Pesan langsung",
@ -322,8 +309,7 @@
"timelines": "Linimasa",
"twkn": "Jaringan Yang Dikenal",
"twkn_timeline_description": "Postingan dari seluruh jaringan",
"user_search": "Penelusuran Pengguna",
"who_to_follow": "Siapa untuk diikuti"
"user_search": "Penelusuran Pengguna"
},
"notifications": {
"broken_favorite": "Postingan tak dikenal, mencarinya…",
@ -399,8 +385,7 @@
"direct": "Langsung - posting hanya kepada pengguna yang disebut",
"local": "Lokal - postingan tidak akan difederasi",
"private": "Hanya-pengikut - posting hanya kepada pengikut",
"public": "Publik - posting ke linimasa publik",
"unlisted": "Tidak Tercantum - Tidak memposting ke timeline publik"
"public": "Publik - posting ke linimasa publik"
},
"scope_notice": {
"local": "Postingan ini tidak akan terlihat di instansi lain",
@ -417,7 +402,6 @@
"captcha": "CAPTCHA",
"email": "Surel",
"email_language": "Dalam bahasa apa kamu ingin menerima surel dari server ini?",
"fullname": "Tampilan Nama",
"fullname_placeholder": "cth. Atsuko Kagari",
"new_captcha": "Klik gambarnya untuk mendapatkan captcha baru",
"password_confirm": "Konfirmasi kata sandi",
@ -440,7 +424,6 @@
},
"remote_user_resolver": {
"error": "Tidak ditemukan.",
"remote_user_resolver": "Pencarian pengguna jarak jauh",
"searching_for": "Mencari"
},
"search": {
@ -454,22 +437,16 @@
"select_all": "Pilih semua"
},
"settings": {
"accent": "Aksen",
"account_alias": "Akun alias",
"account_backup": "Pencadangan akun",
"account_backup_description": "Ini memungkinkan kamu untuk mengunduh arsip yang berisi informasi tentang akun dan postingan kamu, namun belum bisa diimpor ke akun Pleroma.",
"account_privacy": "Privasi",
"add_alias_error": "Gagal menambahkan alias: {error}",
"add_backup": "Buat cadangan baru",
"add_backup_error": "Gagal menambahkan cadangan baru: {error}",
"added_alias": "Alias telah ditambahkan.",
"added_backup": "Cadangan baru ditambahkan.",
"allow_following_move": "Ikuti otomatis apabila akun yang diikuti pindah",
"always_show_post_button": "Selalu tampilkan tombol posting baru yang mengambang",
"app_name": "Nama aplikasi",
"attachmentRadius": "Lampiran",
"attachments": "Lampiran",
"autohide_floating_post_button": "Sembunyikan tombol buat posting secara otomatis (Ponsel)",
"avatar": "Avatar",
"avatarAltRadius": "Avatar (notifikasi)",
"avatarRadius": "Avatar",
@ -497,8 +474,6 @@
"changed_password": "Kata sandi berhasil diubah!",
"chatMessageRadius": "Pesan obrolan",
"checkboxRadius": "Kotak centang",
"collapse_subject": "Tampilkan post dengan peringatan",
"columns": "Kolum",
"composing": "Menulis",
"confirm_dialogs": "Perlukan konfirmasi sebelum:",
"confirm_dialogs_approve_follow": "Menerima permintaan mengikuti",
@ -509,23 +484,17 @@
"confirm_dialogs_repeat": "Mengulangi postingan",
"confirm_dialogs_unfollow": "Berhenti mengikuti seseorang",
"confirm_new_password": "Konfirmasi kata sandi baru",
"confirmation_dialogs": "Opsi Konfirmasi",
"conversation_display": "Gaya tampilan percakapan",
"conversation_display_linear": "Gaya Linier",
"conversation_display_tree": "Bercabang",
"conversation_other_replies_button": "Tampilkan tombol \"Balasan lainnya\"",
"conversation_other_replies_button_below": "Di bawah postingan",
"conversation_other_replies_button_inside": "Di postingan",
"current_avatar": "Avatarmu saat ini",
"current_mascot": "Maskot utamamu",
"current_password": "Kata sandi saat ini",
"data_import_export_tab": "Impor / ekspor data",
"default_vis": "Cakupan visibilitas default",
"delete_account": "Hapus akun",
"delete_account_description": "Hapus data kamu secara permanen dan nonaktifkan akunmu.",
"delete_account_error": "Ada masalah ketika menghapus akun kamu. Jika ini terus terjadi harap hubungi adminstrator instansi kamu.",
"delete_account_instructions": "Ketik kata sandi kamu pada input di bawah untuk mengonfirmasi penghapusan akun.",
"disable_sticky_headers": "Jangan tempel tajuk kolom ke bagian atas layar",
"discoverable": "Izinkan penelusuran akun ini pada hasil pencarian dan layanan lainnya",
"domain_mutes": "Domain",
"download_backup": "Unduh",
@ -536,7 +505,6 @@
"expert_mode": "Tampilkan pengaturan lanjutan",
"expire_posts_enabled": "Hapus postingan setelah jumlah hari yang ditentukan",
"expire_posts_input_placeholder": "Jumlah hari",
"export_theme": "Simpan preset",
"file_export_import": {
"backup_restore": "Pencadangan pengaturan",
"backup_settings": "Cadangkan pengaturan ke berkas",
@ -551,11 +519,7 @@
},
"filtering": "Penyaringan",
"filtering_explanation": "Semua postingan yang mengandung kata-kata ini akan dibisukan, satu kata per baris",
"follow_export": "Ekspor Follow",
"follow_export_button": "Export yang kamu ikuti ke dalam file csv",
"follow_import": "Import pengikut",
"follow_import_error": "Terjadi kesalahan ketika mengimpor pengikut",
"follows_imported": "Pengguna yang diikuti telak diimpor! Proses mungkin membutuhkan beberapa saat.",
"fun": "Seru",
"general": "Umum",
"greentext": "Panah meme",

View file

@ -4,7 +4,6 @@
"mrf": {
"federation": "フェデレーション",
"keyword": {
"ftl_removal": "「せつぞくしているすべてのネットワーク」タイムラインからのぞく",
"is_replaced_by": "→",
"keyword_policies": "キーワードポリシー",
"reject": "おことわり",
@ -121,7 +120,7 @@
"error_retry": "もういちど、ためしてください",
"flash_content": "クリックすると、Ruffle をつかって、フラッシュさくひんをひょうじします。(うまくうごかないかもしれません)",
"flash_fail": "フラッシュさくひんのロードに、しっぱいしました。コンソールに、くわしいことがかかれています。",
"flash_security": "フラッシュさくひんは、あなたのコンピューターに、あぶないことをしてくるかもしれないので、ちゅういしてください。",
"flash_security": "フラッシュさくひんは、あぶないことをしてくるかもしれないので、ちゅういしてください。",
"generic_error": "エラーになりました",
"loading": "ロードしています…",
"more": "つづき",
@ -534,7 +533,7 @@
"expert_mode": "こまかいせっていをひょうじ",
"export_theme": "セーブ",
"filtering": "フィルタリング",
"filtering_explanation": "これらのことばをふくむすべてのとうこうがミュートされます。1ぎょうに1つのことばをかいてください",
"filtering_explanation": "これらのことばをふくむすべてのものがミュートされます。1ぎょうに1つのことばをかいてください",
"follow_export": "フォローのエクスポート",
"follow_export_button": "エクスポート",
"follow_import": "フォローインポート",
@ -888,7 +887,6 @@
"media": "メディア",
"mention": "メンション",
"mute": "ミュート",
"mute_domain": "ドメインをブロック",
"mute_progress": "ミュートしています…",
"muted": "ミュートしています!",
"per_day": "/日",
@ -899,7 +897,6 @@
"subscribe": "サブスクライブ",
"unblock": "ブロックをやめる",
"unblock_progress": "ブロックをとりけしています…",
"unfollow_confirm": "{user}のフォローをやめますか?",
"unmute": "ミュートをやめる",
"unmute_progress": "ミュートをとりけしています…",
"unsubscribe": "サブスクライブをやめる"

View file

@ -921,6 +921,7 @@
"upload_a_photo": "画像をアップロード",
"useStreamingApi": "投稿と通知を、すぐに受け取る",
"useStreamingApiWarning": "(実験中で、投稿を取りこぼすかもしれないので、おすすめしません)",
"use_at_icon": "{'@'}マークをアイコンにする",
"use_contain_fit": "画像のサムネイルを、切り抜かない",
"use_one_click_nsfw": "NSFWなファイルを1クリックで開く",
"user_mutes": "ユーザー",

View file

@ -21,7 +21,6 @@ const loaders = {
ga: () => import('./ga.json'),
he: () => import('./he.json'),
hu: () => import('./hu.json'),
id: () => import('./id.json'),
it: () => import('./it.json'),
ja: () => import('./ja_pedantic.json'),
ja_easy: () => import('./ja_easy.json'),
@ -36,7 +35,6 @@ const loaders = {
sk: () => import('./sk.json'),
te: () => import('./te.json'),
uk: () => import('./uk.json'),
vi: () => import('./vi.json'),
zh: () => import('./zh.json'),
zh_Hant: () => import('./zh_Hant.json')
}

View file

@ -918,6 +918,7 @@
"upload_a_photo": "Foto uploaden",
"useStreamingApi": "Berichten en meldingen in real-time ontvangen",
"useStreamingApiWarning": "Iets experimenteels met berichten streamen uwu miss kun je beter uit laten ofzo?",
"use_at_icon": "{'@'} symbool als icoon tonen in plaats van tekst",
"use_blurhash": "Waas tonen over NSFW-miniaturen",
"use_contain_fit": "Bijlage in miniaturen niet bijsnijden",
"use_one_click_nsfw": "Gevoelige bijlagen met slechts één klik openen",

View file

@ -30,7 +30,7 @@
"simple_policies": "Políticas especificas do domínio"
}
},
"staff": "Equipe"
"staff": "Staff"
},
"announcements": {
"all_day_prompt": "Este evento dura o dia inteiro",
@ -167,7 +167,7 @@
"password": "Palavra-passe",
"placeholder": "ex. lain",
"recovery_code": "Código de recuperação",
"register": "Registrar",
"register": "Registar",
"username": "Nome de Utilizador"
},
"media_modal": {
@ -244,7 +244,7 @@
"text/plain": "Texto puro"
},
"content_warning": "Assunto (opcional)",
"default": "Acabei de chegar em",
"default": "Acabei de chegar a Lisboa.",
"direct_warning_to_all": "Esta publicação será visível para todos os utilizadores mencionados.",
"direct_warning_to_first_only": "Esta publicação só será visível para os utilizadores mencionados no início da mensagem.",
"empty_status_error": "Não consegues publicar um post vazio e sem ficheiros",
@ -268,7 +268,7 @@
},
"registration": {
"bio": "Biografia",
"bio_placeholder": "Ex.:\nBem-vindo a minha bio.\nEu amo assistir anime e jogar. Eu espero que possamos ser amigos!",
"bio_placeholder": "ex.\nOlá, sou a Lain\nSou uma menina de anime que vive no Japão suburbano. Devem conhecer-me do \"the Wired\".",
"captcha": "CAPTCHA",
"email": "Endereço de e-mail",
"fullname": "Nome para exibição",
@ -401,7 +401,7 @@
"scan": {
"desc": "Utilizando a sua aplicação de dois fatores, faça scan deste código QR ou insira a chave de texto:",
"secret_code": "Chave",
"title": "Escanear"
"title": "Scan"
},
"setup_otp": "Configurar palavra-passe de utilização única",
"title": "Autenticação de Dois Fatores",
@ -566,7 +566,7 @@
"fine_print": "Leia nosso {0} para não aprender nada!",
"header": "Pré-visualizar",
"header_faint": "Isto está bem",
"input": "Acabei de chegar na Lua",
"input": "Acabei de chegar a Lisboa.",
"link": "um belo link",
"mono": "conteúdo",
"text": "Vários {0} e {1}"
@ -648,7 +648,7 @@
"type_domains_to_mute": "Pesquisar domínios para silenciar",
"upload_a_photo": "Enviar uma foto",
"useStreamingApi": "Receber publicações e notificações em tempo real",
"useStreamingApiWarning": "É legal usar isso. Se der algum problema, atualize, eu acho?",
"useStreamingApiWarning": "(não recomendado, experimental, pode omitir publicações)",
"use_contain_fit": "Não cortar o anexo na miniatura",
"use_one_click_nsfw": "Abrir anexos sensíveis com um clique",
"user_mutes": "Utilizadores",

View file

@ -1,34 +0,0 @@
{
"about": {
"bubble_instances": "เซิร์ฟเวอร์เพื่อนบ้าน",
"bubble_instances_description": "เซิร์ฟเวอร์ที่แอดมินตั้งค่าไว้เพื่อแสดงเป็นเพื่อนบ้าน",
"mrf": {
"keyword": {
"keyword_policies": "นโยบายของคีย์เวิร์ดค้นหา",
"reject": "ไม่อนุมัติ",
"replace": "แทนที่"
},
"simple": {
"accept": "ยอมรับ",
"accept_desc": "เซิร์ฟเวอร์นี้จะรับข้อความเฉพาะเซิฟร์เวอร์ต่อไปนี้:",
"instance": "เซิฟเวอร์",
"media_nsfw": "มีเดียถูกบังคับตั้งให้เป็น NSFW",
"not_applicable": "ไม่สามารถใช้ได้",
"quarantine": "คัดกรอง",
"reason": "เหตุผล",
"reject": "ปฎิเสธ"
}
},
"staff": "ผู้ดูแล"
},
"announcements": {
"cancel_edit_action": "ยกเลิก",
"close_error": "ปิด",
"delete_action": "ลบประกาศ",
"edit_action": "แก้ไขประกาศ",
"end_time_prompt": "เวลาสิ้นสุด: ",
"mark_as_read_action": "ตั้งเป็นอ่านแล้ว",
"page_header": "ประกาศ",
"post_action": "โพสต์"
}
}

View file

@ -49,86 +49,6 @@
"post_error": "Hata: {error}",
"post_form_header": "Post sonrası",
"post_placeholder": "Duyuru içeriği",
"published_time_display": "{time} tarihinde yayınlandı",
"start_time_display": "{time} saatinde başlar",
"start_time_prompt": "Başlangıç saati: ",
"submit_edit_action": "Gönder",
"title": "Duyuru"
},
"chats": {
"chats": "Sohbetler",
"delete": "Sil",
"delete_confirm": "Bu mesajı gerçekten silmek istiyor musun?",
"empty_chat_list_placeholder": "Henüz hiç sohbetiniz yok. Yeni bir sohbet başlatın!",
"empty_message_error": "Boş mesaj gönderilemiyor",
"error_loading_chat": "Sohbet yüklenirken bir şeyler ters gitti.",
"error_sending_message": "Mesajı gönderirken bir şeyler ters gitti.",
"message_user": "{nickname} mesajı",
"more": "Daha",
"new": "Yeni Sohbet",
"you": "Sen:"
},
"display_date": {
"today": "Bugün"
},
"domain_mute_card": {
"mute": "Sesini kapat",
"mute_progress": "Ses kapatılıyor…",
"unmute": "Sesini aç",
"unmute_progress": "Sessize alınıyor…"
},
"emoji": {
"add_emoji": "Emoji ekle",
"custom": "Özel emoji",
"emoji": "Emoji",
"keep_open": "Seçiciyi açık tut",
"load_all": "Tüm {emojiAmount} emoji yükleniyor",
"load_all_hint": "İlk {saneAmount} emoji yüklendi, tüm emojilerin yüklenmesi performans sorunlarına neden olabilir.",
"recent": "Son zamanlarda kullanılmış",
"search_emoji": "Emoji ara",
"stickers": ıkartmalar",
"unicode": "Unicode emojisi"
},
"errors": {
"storage_unavailable": "Pleroma, tarayıcı depolama alanına erişemedi. Oturum açma bilgileriniz veya yerel ayarlarınız kaydedilmeyecek ve beklenmeyen sorunlarla karşılaşabilirsiniz. Çerezleri etkinleştirmeyi deneyin."
},
"exporter": {
"export": "Dışa aktar",
"processing": "İşleniyor, yakında dosyanızı indirmeniz istenecek"
},
"features_panel": {
"media_proxy": "Medya proxy'si",
"scope_options": "Kapsam seçenekleri",
"text_limit": "Metin sınırı",
"title": "Özellikler",
"upload_limit": "Yükleme sınırı",
"who_to_follow": "Kimi takip etmeli"
},
"file_type": {
"audio": "Ses",
"file": "Dosya",
"image": "Resim",
"video": "Video"
},
"finder": {
"error_fetching_user": "Kullanıcı getirilirken hata oluştu",
"find_user": "Kullanıcı bul"
},
"general": {
"apply": "Uygula",
"cancel": "İptal",
"close": "Kapalı",
"confirm": "Onayla",
"disable": "Devre dışı bırak",
"dismiss": "Reddetmek",
"enable": "Etkinleştir",
"error_retry": "Lütfen tekrar deneyin",
"flash_content": "Ruffle kullanarak Flash içeriğini göstermek için tıklayın (Deneysel, çalışmayabilir).",
"flash_fail": "Flash içeriği yüklenemedi, ayrıntılar için konsola bakın.",
"flash_security": "Flash içeriği hala isteğe bağlı kod olduğundan, bunun potansiyel olarak tehlikeli olabileceğini unutmayın.",
"generic_error": "Bir hata oluştu",
"loading": "Yükleniyor…",
"more": "Daha",
"optional": "Seçenek"
"published_time_display": "{time} tarihinde yayınlandı"
}
}

View file

@ -23,7 +23,6 @@
"media_nsfw_desc": "Сервер примусово позначає дражливими медіа з таких серверів:",
"media_removal": "Видалення медіа",
"media_removal_desc": "Сервер вилучає медіа в дописах із таких серверів:",
"not_applicable": "Не застосовується",
"quarantine": "Карантин",
"quarantine_desc": "Сервер не надсилатиме дописів таким серверам:",
"reason": "Підстава",
@ -32,7 +31,7 @@
"simple_policies": "Відносно цілих серверів"
}
},
"staff": "Команда"
"staff": "Адміністрація"
},
"announcements": {
"all_day_prompt": "Триває весь день",
@ -72,10 +71,10 @@
"today": "Сьогодні"
},
"domain_mute_card": {
"mute": "Приглушити",
"mute_progress": "Глушіння…",
"unmute": "Відновити",
"unmute_progress": "Відновлення…"
"mute": "Ігнорувати",
"mute_progress": "Вимикаю…",
"unmute": "Вимкнути заглушення",
"unmute_progress": "Вмикаю…"
},
"emoji": {
"add_emoji": "Додати емоджі",
@ -84,7 +83,6 @@
"keep_open": "Тримати панель відкритою",
"load_all": "Всі {emojiAmount} емоджі завантажуються",
"load_all_hint": "Завантажено {saneAmount} емоджі, більша кількість може спричинити гальмування.",
"recent": "Нещодавно вжиті",
"search_emoji": "Пошук емоджі",
"stickers": "Наліпки",
"unicode": "Стандартні емоджі"
@ -133,19 +131,19 @@
"peek": "Глянути",
"retry": "Спробуйте ще раз",
"role": {
"admin": "Адміністрація",
"moderator": "Модерування"
"admin": "Адмініструє",
"moderator": "Модерує"
},
"scope_in_timeline": {
"direct": "Особисто",
"local": "Місцево — допис видно тільки на вашому сервері",
"private": "Приватно",
"public": "Загальнодоступно",
"unlisted": "Приховано"
"unlisted": "Непублічно"
},
"show_less": "Показати менше",
"show_more": "Показати більше",
"submit": "Надіслати",
"submit": "Відправити",
"verify": "Перевірити"
},
"image_cropper": {
@ -156,7 +154,7 @@
},
"importer": {
"error": "Під час імпортування файлу сталася помилка.",
"submit": "Надіслати",
"submit": "Відправити",
"success": "Імпортовано успішно."
},
"interactions": {
@ -166,72 +164,8 @@
"moves": "Переїзди користувач_ок"
},
"languages": {
"ar": "арабська",
"az": "азербайджанська",
"bg": "болгарська",
"cs": "чеська",
"da": "данська",
"de": "німецька",
"el": "грецька",
"en": "англійська",
"eo": "есперанто",
"es": "іспанська",
"fa": "перська",
"fi": "фінська",
"fr": "французька",
"ga": "ірландська",
"he": "іврит",
"hi": "гінді",
"hu": "угорська",
"id": "індонезійська",
"it": "італійська",
"ja": "японська",
"ko": "корейська",
"lt": "литовська",
"lv": "латвійська",
"nl": "нідерландська",
"pl": "польська",
"pt": "португальська",
"ru": "російська",
"sk": "словацька",
"sv": "шведська",
"tr": "турецька",
"translated_from": {
"ar": "Перекладено з арабської",
"az": "Перекладено з азербайджанської",
"bg": "Перекладено з болгарської",
"cs": "Перекладено з чеської",
"da": "Перекладено з данської",
"de": "Перекладено з німецької",
"el": "Перекладено з грецької",
"en": "Перекладено з англійської",
"eo": "Перекладено з есперанто",
"es": "Перекладено з іспанської",
"fa": "Перекладено з перської",
"fi": "Перекладено з фінської",
"fr": "Перекладено з французької",
"ga": "Перекладено з ірландської",
"he": "Перекладено з івриту",
"hi": "Перекладено з гінді",
"hu": "Перекладено з угорської",
"id": "Перекладено з індонезійської",
"it": "Перекладено з італійської",
"ja": "Перекладено з японської",
"ko": "Перекладено з корейської",
"lt": "Перекладено з литовської",
"lv": "Перекладено з латвійської",
"nl": "Перекладено з нідерландської",
"pl": "Перекладено з польської",
"pt": "Перекладено з португальської",
"ru": "Перекладено з російської",
"sk": "Перекладено зі словацької",
"sv": "Перекладено зі словенської",
"tr": "Перекладено з турецької",
"uk": "Перекладено з української",
"zh": "Перекладено з китайської"
},
"uk": "українська",
"zh": "китайська"
"uk": "українська"
},
"lists": {
"create": "Створити",
@ -239,7 +173,6 @@
"following_only": "Лише за ким ви стежите",
"lists": "Списки",
"new": "Новий список",
"save": "Зберегти зміни",
"search": "Знайти користувач_ок",
"title": "Назва списку"
},
@ -266,7 +199,6 @@
"username": "Логін"
},
"media_modal": {
"counter": "{current} / {total}",
"hide": "Закрити медіаперегляд",
"next": "Наступна",
"previous": "Попередня"
@ -290,9 +222,7 @@
"reports": "Скарги",
"resolve": "Вирішено",
"show_closed": "Показати закриті",
"statuses": "Дописів: { count } | Дописів: { count }",
"tag_policy_notice": "Увімкніть MRF TagPolicy, щоб обмежувати окремі обліковки",
"tags": "Обмежити обліковку"
"statuses": "Дописів: { count } | Дописів: { count }"
},
"statuses": "Дописи",
"users": "Користувач_ки"
@ -312,21 +242,21 @@
"home_timeline_description": "Дописи від тих, на кого ви підписані",
"interactions": "Взаємодії",
"lists": "Списки",
"mentions": "Згадки",
"mentions": "Згадування",
"moderation": "Модерування",
"preferences": "Налаштування",
"public_timeline_description": "Загальнодоступні дописи всіх на цьому сервері",
"public_tl": "Спільна стрічка",
"search": "Пошук",
"timeline": "Стрічка",
"timeline": "Домашня стрічка",
"timelines": "Стрічки",
"twkn": "Уся відома мережа",
"twkn_timeline_description": "Дописи з усієї мережі",
"user_search": "Пошук користувач_ок",
"who_to_follow": "На кого підписатись"
"who_to_follow": "Кого відстежувати"
},
"notifications": {
"broken_favorite": "Допис невідомий, пошук триває…",
"broken_favorite": "Невідомий допис, шукаю його…",
"error": "Помилка при оновленні сповіщень: {0}",
"favorited_you": "вподобує",
"follow_request": "хоче підписатись",
@ -355,12 +285,12 @@
"polls": {
"add_option": "Додати варіант",
"add_poll": "Додати опитування",
"expired": "Завершено {0} тому",
"expires_in": "До кінця {0}",
"expired": "Опитування закінчилось {0} тому",
"expires_in": "Опитування закінчується через {0}",
"expiry": "Термін опитування",
"multiple_choices": "Декілька варіантів",
"not_enough_options": "Замало унікальних варіантів в опитуванні",
"option": "Варіант",
"option": "Відповідь",
"people_voted_count": "Осіб: {count} | Осіб: {count}",
"single_choice": "Один варіант",
"type": "Тип опитування",
@ -392,7 +322,7 @@
"media_not_sensitive_warning": "Ви вказали засторогу, але не позначили вкладення дражливими!",
"new_status": "Новий допис",
"post": "Розмістити",
"posting": "Розміщення",
"posting": "Відправляється",
"preview": "Попередній перегляд",
"preview_empty": "Порожньо",
"scope": {
@ -410,13 +340,11 @@
}
},
"registration": {
"awaiting_email_confirmation": "Вам надіслано лист підтвердження створення обліковки. Перевірте е-пошту, щоб завершити реєстрацію.",
"awaiting_email_confirmation_title": "Очікування підтвердження поштою",
"bio": "Опис",
"bio_placeholder": "як-от:\nВолинянка. Подорожую.\nДемократична соціалістка. Пишу драматичні поеми. Жити хочу! Геть, думи сумні!",
"captcha": "CAPTCHA",
"email": "Е-пошта",
"email_language": "Мова листів сервера",
"email_language": "Якою мовою отримувати листи з сервера?",
"fullname": "Ім'я",
"fullname_placeholder": "як-от Лариса Косач",
"new_captcha": "Натисніть на зображення, щоб оновити код, якщо він нерозбірливий",
@ -425,8 +353,6 @@
"reason_placeholder": "Запити на реєстрацію підтверджуються вручну.\nРозкажіть адміністрації, чому хочете зареєструватися.",
"register": "Зареєструватися",
"registration": "Реєстрація",
"request_sent": "Ваш запит на реєстрацію надіслано на розгляд. Ви отримаєте лист, коли вашу реєстрацію схвалять.",
"request_sent_title": "Запит на реєстрацію надіслано",
"token": "Ключ запрошення",
"username_placeholder": "як-от lesia",
"validations": {
@ -444,7 +370,7 @@
"searching_for": "Шукаю"
},
"search": {
"hashtags": "Теги",
"hashtags": "Хештеги",
"no_results": "Немає результатів",
"people": "Люди",
"people_talking": "{count} людей говорять про це",
@ -456,34 +382,34 @@
"settings": {
"accent": "Акцент",
"account_alias": "Інші обліковки",
"account_alias_table_head": "Попередня обліковка",
"account_alias_table_head": "Нова обліковка",
"account_backup": "Резервне копіювання обліковки",
"account_backup_description": "Можете завантажити архів даних і дописів своєї обліковки. Але імпортувати їх на іншу обліковку поки неможливо.",
"account_backup_table_head": "Резервна копія",
"account_privacy": "Приватність",
"add_alias_error": "Помилка додання обліковки: {error}",
"add_alias_error": "Помилка додання нової обліковки: {error}",
"add_backup": "Створити резервну копію",
"add_backup_error": "Помилка резервного копіювання: {error}",
"added_alias": "Обліковку додано.",
"added_alias": "Нову обліковку додано.",
"added_backup": "Резервну копію створено.",
"allow_following_move": "Дозволити автостеження при переміщенні на інший інстанс",
"always_show_post_button": "Завжди показувати кнопку «Новий допис»",
"app_name": "Назва програми",
"attachmentRadius": "Вкладення",
"attachments": "Вкладення",
"autohide_floating_post_button": "Автоматично ховати кнопку «Новий допис» (на мобільному)",
"autohide_floating_post_button": "Автоматично ховати кнопку \"Новий допис\" (в мобільній версії)",
"avatar": "Аватар",
"avatarAltRadius": "Аватари у сповіщеннях",
"avatarRadius": "Аватари",
"avatar_size_instruction": "Рекомендований мінімальний розмір для зображень аватара становить 150x150 пікселів.",
"background": "Тло",
"background": "Обкладинка",
"backup_not_ready": "Резервна копія ще не готова.",
"bio": "Опис",
"block_export": "Експорт блокувань",
"block_export_button": "Експорт блокувань у CSV-файл",
"block_export_button": "Експорт блокувань у файл CSV",
"block_import": "Імпорт блокувань",
"block_import_error": "Помилка імпорту блокувань",
"blocks_imported": "Блокування імпортовано! Їх буде застосовано поступово.",
"block_import_error": "Помилка імпортування блокувань",
"blocks_imported": "Блокування імпортовані! Їх обробка триватиме певний час.",
"blocks_tab": "Блокування",
"bot": "Це обліковка бота",
"btnRadius": "Кнопки",
@ -515,11 +441,6 @@
"conversation_display": "Стиль показу розмови",
"conversation_display_linear": "Колонка",
"conversation_display_tree": "Дерево",
"conversation_other_replies_button": "Кнопка «Переглянути ще»",
"conversation_other_replies_button_below": "Під дописами",
"conversation_other_replies_button_inside": "Всередині дописів",
"current_avatar": "Ваш поточний аватар",
"current_mascot": "Ваш поточний маскот",
"current_password": "Поточний пароль",
"data_import_export_tab": "Імпорт/експорт даних",
"default_vis": "Типовий рівень приватності",
@ -540,24 +461,24 @@
"expire_posts_input_placeholder": "Скільки днів",
"export_theme": "Завантажити",
"file_export_import": {
"backup_restore": "Копія налаштувань",
"backup_restore": "Резервне копіювання налаштувань",
"backup_settings": "Завантажити файл налаштувань",
"backup_settings_theme": "Завантажити налаштування й тему",
"errors": {
"file_slightly_new": "Файл призначено для трохи новішої версії й може спрацювати не повністю",
"file_too_new": "Версія {fileMajor} новіша, ніж ця AkkomaFE ({feMajor}) може обробити",
"file_too_old": "Версія {fileMajor} більше не підтримується (треба {feMajor} чи новіше)",
"file_slightly_new": "Другорядна версія файлу відрізняється, деякі налаштування можуть бути не прийняті",
"file_too_new": "Несумісна основна версія: {fileMajor}, ця версія PleromaFE ({feMajor}) занадто стара для його обробки",
"file_too_old": "Несумісна основна версія: {fileMajor}, версія файлу занадто стара і не підтримується (мінімальна версія налаштувань {feMajor})",
"invalid_file": "Вибраний файл не є резервною копією налаштувань Pleroma. Ніяких змін не було зроблено."
},
"restore_settings": "Відновити налаштування з файлу"
},
"filtering": "Фільтр",
"filtering_explanation": "Усі дописи з цими словами буде сховано. Пишіть кожне слово з нового рядка",
"follow_export": "Експорт підписок",
"follow_export_button": "Експорт підписок до CSV-файлу",
"follow_import": "Імпорт підписок",
"follow_import_error": "Помилка імпорту підписок",
"follows_imported": "Підписки імпортовано! Їх буде застосовано поступово.",
"follow_export": "Експортувати відстежуваних",
"follow_export_button": "Експортувати відстежуваних до csv файлу",
"follow_import": "Імпортувати відстежуваних",
"follow_import_error": "Помилка імпортування відстежуваних",
"follows_imported": "Відстежуваних імпортовано! Їхня обробка потребує часу.",
"foreground": "Передній план",
"fun": "Іграшки",
"general": "Загальні",
@ -576,7 +497,7 @@
"hide_list_aliases_error_action": "Закрити",
"hide_media_previews": "Приховати попередній перегляд медіа",
"hide_muted_posts": "Ховати повідомлення приглушених користувач_ок",
"hide_muted_threads": "Ховати приглушені гілки",
"hide_muted_threads": "Ховати заглушені гілки",
"hide_post_stats": "Ховати статистику дописів (скільки вподобань тощо)",
"hide_shoutbox": "Приховати оголошення сервера",
"hide_site_favicon": "Вилучити значок сервера з шапки",
@ -585,9 +506,9 @@
"hide_user_stats": "Ховати користувацьку статистику (скільки підписок тощо)",
"hide_wallpaper": "Сховати шпалери сервера",
"hide_wordfiltered_statuses": "Ховати дописи зі словами з фільтру",
"import_blocks_from_a_csv_file": "Імпортуйте CSV-файл блокувань",
"import_followers_from_a_csv_file": "Імпортуйте CSV-файл підписок",
"import_mutes_from_a_csv_file": "Імпортуйте CSV-файл приглушень",
"import_blocks_from_a_csv_file": "Імпортувати заблокованих з csv файлу",
"import_followers_from_a_csv_file": "Імпортувати відстежуваних з csv файлу",
"import_mutes_from_a_csv_file": "Імпорт заглушених з csv файлу",
"import_theme": "Вивантажити",
"inputRadius": "Поля вводу",
"instance_default": "(усталено: {value})",
@ -597,16 +518,13 @@
"invalid_theme_imported": "Вибраний файл не є темою Pleroma. У вашу тему не внесено жодних змін.",
"limited_availability": "Недоступно у вашому браузері",
"links": "Посилання",
"list_aliases_error": "Помилка завантаження обліковок: {error}",
"list_aliases_error": "Помилка завантаження нових обліковок: {error}",
"list_backups_error": "Помилка завантаження переліку копій: {error}",
"lock_account_description": "Показувати лише схваленим підписни_цям",
"loop_video": "Зациклити відео",
"loop_video_silent_only": "Зациклити відео без звуку (напр. Mastodon \"gifs\")",
"mascot": "Маскот Mastodon FE",
"max_depth_in_thread": "Максимальна кількість типово показаних рівнів гілки обговорення",
"max_thumbnails": "Максимальна кількість мініатюр на повідомлення",
"mention_link_bolden_you": "Підсвічувати посилання, коли вас згадують",
"mention_link_display": "Формат посилань згадок",
"mention_link_display": "Показувати згадування",
"mention_link_display_full": "завжди повністю (як-от {'@'}lesia{'@'}example.org)",
"mention_link_display_full_for_remote": "повністю, якщо це інший сервер (як-от {'@'}lesia{'@'}example.org)",
"mention_link_display_short": "завжди коротко (як-от {'@'}lesia)",
@ -639,37 +557,34 @@
"more_settings": "Більше налаштувань",
"move_account": "Переїхати",
"move_account_error": "Помилка переїзду обліковки: {error}",
"move_account_notes": "Щоб переїхати з цієї обліковки на іншу, додайте в її налаштуваннях цю обліковку як попередню.",
"move_account_target": "Нова обліковка (як-от {example})",
"moved_account": "Переїзд обліковки виконано.",
"move_account_notes": "Щоб переїхати з цієї обліковки на іншу, вкажіть у її налаштуваннях цю облікову як нову.",
"mute_bot_posts": "Сховати дописи ботів",
"mute_export": "Експорт приглушень",
"mute_export_button": "Експорт приглушених у csv файл",
"mute_import": "Імпорт приглушень",
"mute_import_error": "Під час імпорту приглушених сталася помилка",
"mutes_and_blocks": "Приглушення та блокування",
"mutes_imported": "Приглушення імпортовано! Їх буде застосовано поступово.",
"mutes_tab": "Приглушення",
"mute_export": "Експорт ігнорувань",
"mute_export_button": "Експорт заглушених у csv файл",
"mute_import": "Імпорт ігнорувань",
"mute_import_error": "Під час імпорту заглушених сталася помилка",
"mutes_and_blocks": "Заглушення та блокування",
"mutes_imported": "Заглушені імпортовані! Їх обробка триватиме певний час.",
"mutes_tab": "Заглушені",
"name": "Ім'я",
"name_bio": "Особисті дані",
"new_alias_target": "Додайте стару обліковку ({example})",
"new_alias_target": "Вкажіть нову обліковку (як-от {example})",
"new_email": "Нова е-пошта",
"new_password": "Новий пароль",
"no_blocks": "Нікого не заблоковано",
"no_mutes": "Нікого не приглушено",
"no_blocks": "Блокування відсутні",
"no_mutes": "Заглушені відсутні",
"no_rich_text_description": "Видалення всього форматування тексту з усіх дописів",
"notification_blocks": "Щоб не отримувати сповіщень від когось і вилучити їх із підписни_ць, заблокуйте їх.",
"notification_mutes": "Щоб просто не отримувати сповіщень від когось, приглушіть їх.",
"notification_blocks": "Блокування користувача зупиняє всі сповіщення від нього, а також скасовує його відстеження.",
"notification_mutes": "Щоб перестати отримувати сповіщення від певного користувача, заглушіть його.",
"notification_setting_block_from_strangers": "Блокувати сповіщення від тих, на кого ви не підписані",
"notification_setting_filters": "Фільтри",
"notification_setting_hide_if_cw": "Ховати вміст push-сповіщення за наявності застороги",
"notification_setting_hide_notification_contents": "Ховати відправника та вміст push-сповіщень",
"notification_setting_privacy": "Приватність",
"notification_visibility": "Сповіщати про такі події",
"notification_visibility_emoji_reactions": "Реакції",
"notification_visibility_follows": ідписни_ці",
"notification_visibility_likes": "Вподобане",
"notification_visibility_mentions": "Згадки",
"notification_visibility_mentions": "Згадування",
"notification_visibility_moves": "Переїзди користувач_ок",
"notification_visibility_polls": "Результати голосувань",
"notification_visibility_repeats": "Поширення",
@ -678,7 +593,7 @@
"oauth_tokens": "OAuth-ключі",
"pad_emoji": "Автоматично додавати простір з обидвох сторін емоджі",
"panelRadius": "Панелі",
"pause_on_unfocused": "Лише коли вкладка активна",
"pause_on_unfocused": "Призупинити трансляцію, коли вкладка неактивна",
"play_videos_in_modal": "Відтворювати відео у спливаючій рамці",
"post_look_feel": "Вигляд дописів",
"post_status_content_type": "Типова розмітка тексту",
@ -696,7 +611,7 @@
"profile_tab": "Профіль",
"radii_help": "Радіус заокруглення кутів інтерфейсу (в пікселях)",
"refresh_token": "Оновити ключ",
"remove_alias": "Вилучити обліковку",
"remove_alias": "Вилучити нову обліковку",
"remove_backup": "Вилучити",
"render_mfm": "Показувати розмітку Misskey",
"render_mfm_on_hover": "Анімувати MFM лише при наведенні",
@ -718,14 +633,13 @@
"saving_err": "Помилка при збереженні налаштувань",
"saving_ok": "Налаштування збережені",
"scope_copy": "Відповідати на тому ж рівні приватності (завжди ввімкнено для особистих повідомлень)",
"search_user_to_block": "Шукайте, кого слід заблокувати",
"search_user_to_mute": "Шукайте, кого слід приглушити",
"search_user_to_block": "Шукайте кого ви хочете заблокувати",
"search_user_to_mute": "Шукайте кого ви хочете заглушити",
"security": "Безпека",
"security_tab": "Безпека",
"sensitive_by_default": "Одразу позначати дописи дражливими",
"sensitive_if_subject": "Позначати зображення дражливими, якщо вказано засторогу",
"sensitive_if_subject": "Позначати зображення дражливими, якщо вказано застереження",
"set_new_avatar": "Встановити новий аватар",
"set_new_mascot": "Встановити нового маскота",
"set_new_profile_background": "Встановити нову обкладинку профілю",
"set_new_profile_banner": "Встановити новий банер",
"setting_changed": "Конфігурація відрізняється від типової",
@ -750,7 +664,6 @@
"show_panel_nav_shortcuts": "Ярлики стрічок у заголовку панелі",
"show_scrollbars": "Смуги прокрутки в бокових панелях",
"show_wider_shortcuts": "Більша відстань між ярликами в заголовку",
"show_yous": "Показувати «(ви)»",
"stop_gifs": "Анімувати GIF лише при наведенні",
"streaming": "Автоматично показувати нові дописи вгорі стрічки",
"style": {
@ -831,10 +744,10 @@
"faint_link": "корисний підручник",
"fine_print": "Прочитайте наш {0} аби нічого нового не дізнатись!",
"header": "Попередній перегляд",
"header_faint": "Усе гаразд",
"header_faint": "Все гаразд",
"input": "Що нового?",
"link": "невеличке посилання",
"mono": "вмісту",
"mono": "змісту",
"text": "Трохи більше {0} та {1}"
},
"radii": {
@ -906,7 +819,7 @@
"subject_line_noop": "не копіювати",
"text": "Текст",
"theme": "Тема",
"theme_help": "Замініть кольори теми своїми в форматі #rrggbb.",
"theme_help": "Замініть кольори теми своїми в форматі (#rrggbb).",
"theme_help_v2_1": "Деякі кольори й прозорості розраховуються автоматично. Позначайте їх пташками, щоб замінити їх вручну. Кнопка «Очистити все» видаляє всі такі заміни.",
"theme_help_v2_2": "Значки під кольорами показують, наскільки добре видно текст на тлі. При наведенні побачите деталі. Якщо використовуєте прозорість, буде оцінено найгірший варіант.",
"third_column_mode": "Третя панель, якщо достатньо місця",
@ -915,20 +828,13 @@
"third_column_mode_postform": "Новий допис і меню",
"token": "Ключ",
"tooltipRadius": "Підказки/попередження",
"translation_language": "Мова автоматичного перекладу",
"tree_advanced": "Показувати додаткові кнопки відкриття й закриття ланцюгів відповідей у гілках",
"tree_fade_ancestors": "Показувати менш виразно дописи, що передують поточному",
"type_domains_to_mute": "Шукайте домени, які слід приглушити",
"type_domains_to_mute": "Пошук доменів для заглушення",
"upload_a_photo": "Вивантажити фото",
"useStreamingApi": "Отримувати дописи та сповіщення наживо",
"useStreamingApiWarning": "Загалом працює. Якщо не зовсім, спробуєте оновити сторінку?",
"use_blurhash": "Показувати дражливі мініатюри (як розмиті кольори)",
"useStreamingApiWarning": "(Не рекомендується, експериментально, повідомлення можуть зникати)",
"use_at_icon": "Значок {'@'} замість символа",
"use_contain_fit": "Не обрізати краї мініатюр",
"use_one_click_nsfw": "Відкривати NSFW вкладення одним кліком миші",
"user_accepts_direct_messages_from": "Приймати особисті повідомлення",
"user_accepts_direct_messages_from_everybody": "Будь-чиї",
"user_accepts_direct_messages_from_nobody": "Нічиї",
"user_accepts_direct_messages_from_people_i_follow": "Від тих, на кого я підписуюсь",
"user_mutes": "Користувач_ки",
"user_profile_default_tab": "Типова вкладка профілів",
"user_profiles": "Користувацькі профілі",
@ -947,18 +853,8 @@
"word_filter": "Фільтр слів",
"wordfilter": "Фільтр слів"
},
"settings_profile": {
"creating": "Створення профілю налаштувань «{profile}»…",
"synchronization_error": "Не вдалося синхронізувати налаштування: {err}",
"synchronized": "Налаштування синхронізовано!",
"synchronizing": "Синхронізація профілю налаштувань «{profile}»…"
},
"status": {
"ancestor_follow": "Переглянути ще {numReplies} під цим дописом | Переглянути ще {numReplies} під цим дописом",
"ancestor_follow_with_icon": "{icon} {text}",
"attachment_stop_flash": "Зупинити Flash-плеєр",
"bookmark": "Додати до закладок",
"collapse_attachments": "Згорнути вкладення",
"copy_link": "Скопіювати посилання на допис",
"delete": "Видалити допис",
"delete_confirm": "Точно видалити допис?",
@ -972,17 +868,11 @@
"expand": "Розгорнути",
"external_source": "Зовнішнє джерело",
"favorites": "Вподобане",
"hide_attachment": "Сховати вкладення",
"hide_content": "Сховати вміст",
"hide_full_subject": "Згорнути засторогу",
"many_attachments": "Вкладень: {number} | Вкладень: {number}",
"mentions": "Згадки",
"move_down": "Посунути вкладення праворуч",
"move_up": "Посунути вкладення ліворуч",
"mute_conversation": "Приглушити розмову",
"hide_full_subject": "Сховати всю тему",
"mute_conversation": "Заглушити розмову",
"nsfw": "Дражливий вміст",
"open_gallery": "Відкрити галерею",
"override_translation_source_language": "Замістити початкову мову",
"pin": "Закріпити в профілі",
"pinned": "Закріплено",
"plus_more": "ще {number}",
@ -991,33 +881,21 @@
"redraft_confirm_accept_button": "Так, видалити й переписати",
"redraft_confirm_cancel_button": "Ні, лишити оригінал",
"redraft_confirm_title": "Підтвердьте видалення й переписання",
"remove_attachment": "Видалити вкладення",
"repeat_confirm": "Точно поширити допис?",
"repeat_confirm_accept_button": "Так, поширити",
"repeat_confirm_cancel_button": "Ні, не поширювати",
"repeat_confirm_title": "Підтвердьте поширення",
"remove_attachment": "Видалити долучення",
"repeats": "Поширення",
"replies_list": "Відповіді:",
"replies_list_with_others": "Ще відповідей: {numReplies} | Ще відповідей: {numReplies}",
"reply_to": "Відповідь",
"show_all_attachments": "Показати всі вкладення",
"show_all_conversation": "Показати всю розмову (ще дописів: {numStatus}) | Показати всю розмову (ще дописів: {numStatus})",
"show_all_conversation_with_icon": "{icon} {text}",
"show_attachment_description": "Переглянути опис (натисніть саме вкладення, якщо опис не вміщається)",
"show_attachment_in_modal": "Показати вкладення у вікні",
"show_content": "Показати вміст",
"show_full_subject": "Показати всю засторогу",
"show_only_conversation_under_this": "Показати відповіді лише до цього допису",
"status_deleted": "Цей допис був видалений",
"status_unavailable": "Допис недоступний",
"thread_follow": "Ще відповідей: {numStatus} | Ще відповідей: {numStatus}",
"thread_follow_with_icon": "{icon} {text}",
"thread_hide": "Сховати гілку",
"thread_muted": "Гілку приглушено",
"thread_muted_and_words": ", має слова:",
"thread_show": "Показати гілку",
"thread_show_full": "Показати відповіді: {numStatus} | Показати відповіді: {numStatus}",
"thread_show_full_with_icon": "{icon} {text}",
"translate": "Перекласти",
"translated_from": "В оригіналі: {language}",
"unbookmark": "Видалити із закладок",
@ -1051,7 +929,6 @@
"collapse": "Згорнути",
"conversation": "Розмова",
"error": "Помилка завантаження стрічки: {0}",
"follow_tag": "Підписатись на тег",
"load_older": "Завантажити давніші дописи",
"no_more_statuses": "Більше немає дописів",
"no_retweet_hint": "Допис приватний чи особистий, тож його не можна поширити чи цитувати",
@ -1061,12 +938,8 @@
"show_new": "Показати нові",
"socket_broke": "Втрачено з'єднання у реальному часі: код {0}",
"socket_reconnected": "Встановлено з'єднання у реальному часі",
"unfollow_tag": "Відписатись від тегу",
"up_to_date": "Оновлено"
},
"toast": {
"no_translation_target_set": "Не обрано цільової мови перекладу — може не вдатися. Оберіть у налаштуваннях, на яку мову перекладати."
},
"tool_tip": {
"accept_follow_request": "Дозволити підписатись",
"add_reaction": "Додати реакцію",
@ -1105,14 +978,14 @@
"disable_remote_subscription": "Заборонити підписуватись користувач_кам з інших серверів",
"force_nsfw": "Позначити всі дописи дражливими",
"force_unlisted": "Не показувати дописи в стрічці",
"grant_admin": "Дозволити адміністрування",
"grant_moderator": "Дозволити модерування",
"grant_admin": "Надати права адміністратора",
"grant_moderator": "Надати права модератора",
"moderation": "Модерування",
"quarantine": "Не розповсюджувати дописи на інших інстансах",
"revoke_admin": "Позбавити прав адміністрування",
"revoke_moderator": "Позбавити прав модерування",
"revoke_admin": "Позбавити прав адміністратора",
"revoke_moderator": "Позбавити прав модератора",
"sandbox": "Показувати дописи лише підписни_цям",
"strip_media": "Вилучити медіа з дописів"
"strip_media": "Вилучити медіа з дописів користувача"
},
"approve": "Дозволити",
"approve_confirm": "Точно дозволити користувач_ці на вас підписатись?",
@ -1133,18 +1006,13 @@
"deny_confirm": "Точно відхилити запит на підписку?",
"deny_confirm_accept_button": "Так, відхилити",
"deny_confirm_cancel_button": "Ні, скасувати",
"deny_confirm_title": "Відхилити запит на підписку",
"domain_muted": "Розблокувати домен",
"edit_profile": "Редагувати профіль",
"favorites": "Вподобання",
"follow": "Підписатись",
"follow_cancel": "Скасувати запит",
"follow_progress": "Надсилання…",
"follow_sent": "Запит надіслано!",
"follow_tag": "Підписатись на тег",
"follow_progress": "Запитую…",
"follow_sent": "Запит відправлено!",
"follow_unfollow": "Відписатись",
"followed_tags": "Підписки на теги",
"followed_users": "Підписки на користувач_ок",
"followees": "Підписки",
"followers": ідписни_ці",
"following": "Ви підписані!",
@ -1168,8 +1036,7 @@
"mute_confirm_title": "Приглушення",
"mute_domain": "Заблокувати домен",
"mute_progress": "Глушимо…",
"muted": "Приглушено",
"not_following_any_hashtags": "У вас нема підписок на теги",
"muted": "Заглушено",
"note": "Приватна нотатка",
"per_day": "на день",
"remote_follow": "Підписатись",
@ -1186,9 +1053,8 @@
"unfollow_confirm_accept_button": "Так, відписатись",
"unfollow_confirm_cancel_button": "Ні, не відписуватись",
"unfollow_confirm_title": "Відписка",
"unfollow_tag": "Відписатись від тегу",
"unmute": "Відновити",
"unmute_progress": "Відновлення…",
"unmute": "Зняти глушення",
"unmute_progress": "Знімаємо глушення…",
"unsubscribe": "Відписатись"
},
"user_profile": {
@ -1203,7 +1069,7 @@
"forward_description": "Це обліковка з іншого сервера. Надіслати його адміністрації копію скарги?",
"forward_to": "Переслати до {0}",
"generic_error": "Виникла помилка під час обробки вашого запиту.",
"submit": "Надіслати",
"submit": "Відправити",
"title": "Поскаржитись на {0}"
},
"who_to_follow": {

View file

@ -922,13 +922,10 @@
"upload_a_photo": "上传照片",
"useStreamingApi": "实时接收帖文和通知",
"useStreamingApiWarning": "十分炫酷推荐使用。要是崩了试试刷新?",
"use_at_icon": "将 {'@'} 符号显示为图标而不是文本",
"use_blurhash": "对NSFW的缩略图使用模糊处理",
"use_contain_fit": "生成缩略图时不要裁剪附件",
"use_one_click_nsfw": "点击一次以打开工作场所不适宜NSFW的附件",
"user_accepts_direct_messages_from": "允许私信自",
"user_accepts_direct_messages_from_everybody": "所有人",
"user_accepts_direct_messages_from_nobody": "没有人",
"user_accepts_direct_messages_from_people_i_follow": "我关注的人",
"user_mutes": "用户",
"user_profile_default_tab": "用户资料中的默认标签页",
"user_profiles": "用户资料",

View file

@ -1,16 +0,0 @@
import iso6391 from 'iso-639-1'
import { computed } from 'vue'
export const usePostLanguageOptions = () => {
const postLanguageOptions = computed(() => {
return iso6391.getAllCodes().map(lang => ({
key: lang,
value: lang,
label: lang,
}));
})
return {
postLanguageOptions,
}
}

View file

@ -41,6 +41,7 @@ export const defaultState = {
showNavShortcuts: undefined, // instance default
showPanelNavShortcuts: undefined, // instance default
showWiderShortcuts: undefined, // instance default
showFediLinks: true,
hideSiteFavicon: undefined, // instance default
hideSiteName: undefined, // instance default
hideAttachments: false,
@ -55,7 +56,6 @@ export const defaultState = {
alwaysShowNewPostButton: false,
autohideFloatingPostButton: false,
pauseOnUnfocused: true,
displayPageBackgrounds: true,
stopGifs: undefined,
replyVisibility: 'all',
thirdColumnMode: 'notifications',
@ -96,6 +96,7 @@ export const defaultState = {
disableStickyHeaders: false,
showScrollbars: false,
greentext: undefined, // instance default
useAtIcon: undefined, // instance default
mentionLinkDisplay: undefined, // instance default
mentionLinkShowTooltip: undefined, // instance default
mentionLinkShowAvatar: undefined, // instance default
@ -116,7 +117,6 @@ export const defaultState = {
conversationTreeFadeAncestors: undefined, // instance default
maxDepthInThread: undefined, // instance default
translationLanguage: undefined, // instance default,
postLanguage: undefined, // instance default,
supportedTranslationLanguages: {}, // instance default
userProfileDefaultTab: 'statuses',
useBlurhash: true,
@ -230,7 +230,7 @@ const config = {
break
case 'interfaceLanguage':
messages.setLanguage(this.getters.i18n, value)
Cookies.set(BACKEND_LANGUAGE_COOKIE_NAME, localeService.internalToBackendLocale(value), {sameSite: 'Lax'})
Cookies.set(BACKEND_LANGUAGE_COOKIE_NAME, localeService.internalToBackendLocale(value))
dispatch('setInstanceOption', { name: 'interfaceLanguage', value })
break
case 'thirdColumnMode':

View file

@ -21,6 +21,7 @@ const defaultState = {
background: '/static/aurora_borealis.jpg',
collapseMessageWithSubject: true,
greentext: false,
useAtIcon: false,
mentionLinkDisplay: 'short',
mentionLinkShowTooltip: true,
mentionLinkShowAvatar: false,

View file

@ -37,18 +37,11 @@ const recentEmojis = {
getters: {
recentEmojis: (state, getters, rootState) => state.emojis.reduce((objects, displayText) => {
let comparator = emoji => emoji.displayText === displayText
let emojiObject = rootState.instance.emoji.find(comparator)
const allEmojis = rootState.instance.emoji.concat(rootState.instance.customEmoji)
let emojiObject = allEmojis.find(emoji => emoji.displayText === displayText)
if (emojiObject !== undefined) {
objects.push(emojiObject)
} else {
emojiObject = rootState.instance.customEmoji.find(comparator)
if (emojiObject !== undefined) {
objects.push(emojiObject)
}
}
return objects
}, []),
},

View file

@ -47,10 +47,6 @@ export const settingsMap = {
},
// Privacy
'locked': 'locked',
'permitFollowback': {
get: 'akkoma.permit_followback',
set: 'permit_followback'
},
'allowFollowingMove': {
get: 'pleroma.allow_following_move',
set: 'allow_following_move'

View file

@ -314,7 +314,7 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
})
// Keep the visible statuses sorted
if (timeline && !(['bookmarks', 'favorites'].includes(timeline))) {
if (timeline && !(timeline === 'bookmarks')) {
sortTimeline(timelineObject)
}
}

View file

@ -54,6 +54,26 @@ const unblockUser = (store, id) => {
.then((relationship) => store.commit('updateUserRelationship', [relationship]))
}
const approveUser = (store, id) => {
const predictedRelationship = store.state.relationships[id] || { id }
predictedRelationship.requested_by = false
predictedRelationship.followed_by = true
store.commit('updateUserRelationship', [predictedRelationship])
return store.rootState.api.backendInteractor.approveUser({ id })
.then((relationship) => store.commit('updateUserRelationship', [relationship]))
}
const denyUser = (store, id) => {
const predictedRelationship = store.state.relationships[id] || { id }
predictedRelationship.requested_by = false
predictedRelationship.followed_by = false
store.commit('updateUserRelationship', [predictedRelationship])
return store.rootState.api.backendInteractor.denyUser({ id })
.then((relationship) => store.commit('updateUserRelationship', [relationship]))
}
const removeUserFromFollowers = (store, id) => {
return store.rootState.api.backendInteractor.removeUserFromFollowers({ id })
.then((relationship) => store.commit('updateUserRelationship', [relationship]))
@ -135,10 +155,6 @@ export const mutations = {
const user = state.usersObject[id]
user['deactivated'] = deactivated
},
setDisplayBackground(state, url) {
console.log("Commiting user profile bg mutation")
state.displayBackground = url
},
setCurrentUser (state, user) {
state.lastLoginName = user.screen_name
state.currentUser = mergeWith(state.currentUser || {}, user, mergeArrayLength)
@ -203,28 +219,21 @@ export const mutations = {
})
},
saveBlockIds (state, blockIds) {
console.log("ADDING BLOCK IDS", blockIds);
state.currentUser.blockIds = uniq(concat(state.currentUser.blockIds || [], blockIds))
state.currentUser.blockIds = blockIds
},
addBlockId (state, blockId) {
if (state.currentUser.blockIds.indexOf(blockId) === -1) {
state.currentUser.blockIds.push(blockId)
}
},
setBlockIdsMaxId (state, blockIdsMaxId) {
state.currentUser.blockIdsMaxId = blockIdsMaxId
},
saveMuteIds (state, muteIds) {
state.currentUser.muteIds = uniq(concat(state.currentUser.muteIds || [], muteIds))
state.currentUser.muteIds = muteIds
},
addMuteId (state, muteId) {
if (state.currentUser.muteIds.indexOf(muteId) === -1) {
state.currentUser.muteIds.push(muteId)
}
},
setMuteIdsMaxId (state, muteIdsMaxId) {
state.currentUser.muteIdsMaxId = muteIdsMaxId
},
updateMascot (state, mascotUrl) {
state.currentUser.mascot = mascotUrl
},
@ -311,7 +320,6 @@ export const defaultState = {
currentUser: false,
users: [],
usersObject: {},
displayBackground: null,
signUpPending: false,
signUpErrors: [],
relationships: {},
@ -324,10 +332,6 @@ const users = {
mutations,
getters,
actions: {
setDisplayBackground (store, url) {
console.log("Performing user profile bg action...")
store.commit('setDisplayBackground', url)
},
fetchUserIfMissing (store, id) {
if (!store.getters.findUser(id)) {
store.dispatch('fetchUser', id)
@ -346,21 +350,10 @@ const users = {
.then((relationships) => store.commit('updateUserRelationship', relationships))
}
},
fetchBlocks (store, args) {
const { reset } = args || {}
const maxId = store.state.currentUser.blockIdsMaxId
return store.rootState.api.backendInteractor.fetchBlocks({ maxId })
fetchBlocks (store) {
return store.rootState.api.backendInteractor.fetchBlocks()
.then((blocks) => {
store.commit('saveBlockIds', map(blocks, 'id'))
if (reset) {
store.commit('saveBlockIds', map(blocks, 'id'))
} else {
map(blocks, 'id').map(id => store.commit('addBlockId', id))
}
if (blocks.length) {
store.commit('setBlockIdsMaxId', last(blocks).id)
}
store.commit('addNewUsers', blocks)
return blocks
})
@ -371,6 +364,12 @@ const users = {
unblockUser (store, id) {
return unblockUser(store, id)
},
approveUser (store, id) {
return approveUser(store, id)
},
denyUser (store, id) {
return denyUser(store, id)
},
removeUserFromFollowers (store, id) {
return removeUserFromFollowers(store, id)
},
@ -380,22 +379,10 @@ const users = {
unblockUsers (store, ids = []) {
return Promise.all(ids.map(id => unblockUser(store, id)))
},
fetchMutes (store, args) {
const { reset } = args || {}
const maxId = store.state.currentUser.muteIdsMaxId
return store.rootState.api.backendInteractor.fetchMutes({ maxId })
fetchMutes (store) {
return store.rootState.api.backendInteractor.fetchMutes()
.then((mutes) => {
store.commit('saveMuteIds', map(mutes, 'id'))
if (reset) {
store.commit('saveMuteIds', map(mutes, 'id'))
} else {
map(mutes, 'id').map(id => store.commit('addMuteId', id))
}
if (mutes.length) {
store.commit('setMuteIdsMaxId', last(mutes).id)
}
store.commit('addNewUsers', mutes)
return mutes
})

View file

@ -1166,13 +1166,8 @@ const generateMfaBackupCodes = ({ credentials }) => {
}).then((data) => data.json())
}
const fetchMutes = ({ maxId, credentials }) => {
const query = new URLSearchParams({ with_relationships: true })
if (maxId) {
query.append('max_id', maxId)
}
return promisedRequest({ url: `${MASTODON_USER_MUTES_URL}?${query.toString()}`, credentials })
const fetchMutes = ({ credentials }) => {
return promisedRequest({ url: MASTODON_USER_MUTES_URL, credentials })
.then((users) => users.map(parseUser))
}
@ -1218,12 +1213,8 @@ const unsubscribeUser = ({ id, credentials }) => {
return promisedRequest({ url: MASTODON_UNSUBSCRIBE_USER(id), credentials, method: 'POST' })
}
const fetchBlocks = ({ maxId, credentials }) => {
const query = new URLSearchParams({ with_relationships: true })
if (maxId) {
query.append('max_id', maxId)
}
return promisedRequest({ url: `${MASTODON_USER_BLOCKS_URL}?${query.toString()}`, credentials })
const fetchBlocks = ({ credentials }) => {
return promisedRequest({ url: MASTODON_USER_BLOCKS_URL, credentials })
.then((users) => users.map(parseUser))
}

View file

@ -95,7 +95,6 @@ export const parseUser = (data) => {
if (data.akkoma) {
output.instance = data.akkoma.instance
output.status_ttl_days = data.akkoma.status_ttl_days
output.permit_followback = data.akkoma.permit_followback
}
if (data.pleroma) {
@ -317,6 +316,7 @@ export const parseStatus = (data) => {
output.summary_raw_html = escape(data.spoiler_text)
output.external_url = data.url
output.external_uri = data.uri
output.poll = data.poll
if (output.poll) {
output.poll.options = (output.poll.options || []).map(field => ({

View file

@ -4,11 +4,14 @@ export const newExporter = ({
}) => ({
exportData () {
const stringified = JSON.stringify(getExportedObject(), null, 2) // Pretty-print and indent with 2 spaces
const bytes = new TextEncoder().encode(stringified)
const ascii = Array.from(bytes, (x) => String.fromCodePoint(x)).join("")
const data = window.btoa(ascii)
// Create an invisible link with a data url and simulate a click
const e = document.createElement('a')
e.setAttribute('download', `${filename}.json`)
e.setAttribute('href', 'data:application/json;base64,' + window.btoa(stringified))
e.setAttribute('href', 'data:application/json;base64,' + data)
e.style.display = 'none'
document.body.appendChild(e)
@ -33,7 +36,9 @@ export const newImporter = ({
const reader = new FileReader()
reader.onload = ({ target }) => {
try {
const parsed = JSON.parse(target.result)
const bytes = Uint8Array.from(target.result, (x) => x.codePointAt(0))
const data = new TextDecoder().decode(bytes)
const parsed = JSON.parse(data)
const validationResult = validator(parsed)
if (validationResult === true) {
onImport(parsed)

View file

@ -8,30 +8,7 @@ const specialLanguageCodes = {
'zh': 'zh-Hans'
}
// Find a browser language that matches the configured UI language.
// Browser language should match the configured generic short code prefix:
// eg 'en-GB' browser language matches 'en' UI language.
const findBrowserRegionMatch = genericLang => {
for (const blang of window.navigator.languages) {
if (genericLang === blang.split('-')[0])
return blang;
}
return null;
}
const internalToBrowserLocale = (() => {
const resolvedBrowserLocales = {}
return i18nLocale => {
if (resolvedBrowserLocales[i18nLocale]) {
return resolvedBrowserLocales[i18nLocale]
}
const lang = specialLanguageCodes[i18nLocale] || i18nLocale;
const resolved = findBrowserRegionMatch(lang) || lang;
resolvedBrowserLocales[i18nLocale] = resolved
return resolved
}
})()
const internalToBrowserLocale = code => specialLanguageCodes[code] || code
const internalToBackendLocale = code => internalToBrowserLocale(code).replace('_', '-')

File diff suppressed because it is too large Load diff