Merge remote-tracking branch 'origin/develop' into shigusegubu-vue3

* origin/develop: (79 commits)
  Fix display of theme checkboxes
  Make suggestor ignore users without valid names
  Give tab switcher a role
  Remove @touchstart in post status button
  Update babel monorepo to v7.21.0
  Make it possible to auto-select the first candidate in autocomplete
  Fix poll interaction
  Fix registration error with email language selected
  Add changelog for 2.5.1
  Translated using Weblate (Chinese (Traditional))
  Translated using Weblate (Ukrainian)
  Translated using Weblate (Japanese (ja_EASY))
  Translated using Weblate (Chinese (Simplified))
  Translated using Weblate (Chinese (Simplified))
  Translated using Weblate (Chinese (Simplified))
  Use class to style screenreader-only text
  Update Font Awesome
  Update dependency nightwatch to v2.6.11
  Update dependency eslint to v8.33.0
  Update dependency @vue/test-utils to v2.2.8
  ...
This commit is contained in:
Henry Jameson 2023-03-09 23:18:07 +02:00
commit 085cbc62e7
82 changed files with 2920 additions and 481 deletions

View file

@ -3,6 +3,34 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## 2.5.1
### Fixed
- Checkboxes in settings can now work with screenreaders
- Autocomplete in edit boxes can now work with screenreaders
- Status interact buttons now have focus indicator for anonymous users
- Top bar buttons now correctly have text labels
- It is now possible to register if the site admin requires birthday to register
- User cards from search results will correctly popup
- Fix notification attachment icon overflow
- Editing mute words is less laggy
- Repeater's name will no longer mess up with the directionality of the text sitting on the same line
- Unauthenticated access will give better error messages
- It is now easier to close the media viewer with a mouse when there is only one image
- Deleting profile fields can work properly
- Clicking the react button will correctly focus the search box
- Clicking buttons on the top-bar will no longer bring you to the top of the page
- Emoji picker is much faster to load
- `blockquote`s have a better display style
- Announcements posting and editing are now available to everyone with such a privilege, not just admins
- Adding or removing list members will actually work
- Emojis without a pack are now correctly displayed in emoji picker
- Changing notification settings will actually work
### Added
- You can now set and see birthdays
- Optional confirmation dialogs when performing various actions
- You can now set fallback languages
## 2.5.0 - 23.12.2022
### Fixed
- UI no longer lags when switching between mobile and desktop mode

View file

@ -25,7 +25,17 @@ This could be a bit trickier, you basically need steps 1-4 from *develop build*
### Replacing your instance's frontend with custom FE build
This is the most easiest way to use and test FE build: you just need to copy or symlink contents of `dist` folder into backend's [static directory](../backend/configuration/static_dir.md), by default it is located in `instance/static`, or in `/var/lib/pleroma/static` for OTP release installations, create it if it doesn't exist already. Be aware that running `yarn build` wipes the contents of `dist` folder.
#### New way (via AdminFE, a bit janky but works)
In backend's [static directory](../backend/configuration/static_dir.md) there should be a folder called `frontends` if you installed any frontends from AdminFE before, otherwise you can create it yourself (ensuring correct permissions). Backend will serve given frontend from path `frontends/{frontend}/{reference}`, where `{frontend}` is name of frontend (`pleroma-fe`) and `{reference}` is version. You could make a production build, move `dist` folder into `frontends/pleroma-fe` and rename it into something like `myCustomVersion`. To actually make backend serve this frontend by default, in AdminFE you'll need to set name/reference in Settings -> Frontend -> Frontends -> Primary.
You could also install from a zip file (i.e. CI build) but AdminFE UI is a bit buggy and lacking, so this approach is not recommended.
Take note that frontend management is in early development and currently there's no way for user to change frontend or version for themselves, primary frontend becomes default frontend for all users and visitors.
#### Old way (replaces everything, hard to maintain, not recommended)
Copy or symlink contents of `dist` folder into backend's [static directory](../backend/configuration/static_dir.md), by default it is located in `instance/static`, or in `/var/lib/pleroma/static` for OTP release installations, create it if it doesn't exist already. Be aware that running `yarn build` wipes the contents of `dist` folder, and this could remove emojis, other frontends etc. and therefore this approach is not recommended.
### Running production build locally or on a separate server

View file

@ -14,6 +14,7 @@
<body class="hidden">
<noscript>To use Pleroma, please enable JavaScript.</noscript>
<div id="app"></div>
<div id="modal"></div>
<!-- built files will be auto injected -->
<div id="popovers" />
</body>

View file

@ -16,12 +16,12 @@
"lint-fix": "eslint --fix --ext .js,.vue src test/unit/specs test/e2e/specs"
},
"dependencies": {
"@babel/runtime": "7.20.7",
"@babel/runtime": "7.21.0",
"@chenfengyuan/vue-qrcode": "2.0.0",
"@fortawesome/fontawesome-svg-core": "6.2.1",
"@fortawesome/free-regular-svg-icons": "6.2.1",
"@fortawesome/free-solid-svg-icons": "6.2.1",
"@fortawesome/vue-fontawesome": "3.0.2",
"@fortawesome/fontawesome-svg-core": "6.3.0",
"@fortawesome/free-regular-svg-icons": "6.3.0",
"@fortawesome/free-solid-svg-icons": "6.3.0",
"@fortawesome/vue-fontawesome": "3.0.3",
"@kazvmoe-infra/pinch-zoom-element": "1.2.0",
"@kazvmoe-infra/unicode-emoji-json": "0.4.0",
"@ruffle-rs/ruffle": "0.1.0-nightly.2022.7.12",
@ -36,7 +36,7 @@
"localforage": "1.10.0",
"parse-link-header": "2.0.0",
"phoenix": "1.6.2",
"punycode.js": "2.1.0",
"punycode.js": "2.3.0",
"qrcode": "1.5.0",
"querystring-es3": "0.2.1",
"url": "0.11.0",
@ -49,19 +49,19 @@
"vuex": "4.1.0"
},
"devDependencies": {
"@babel/core": "7.20.7",
"@babel/core": "7.21.0",
"@babel/eslint-parser": "7.19.1",
"@babel/plugin-transform-runtime": "7.19.6",
"@babel/plugin-transform-runtime": "7.21.0",
"@babel/preset-env": "7.20.2",
"@babel/register": "7.18.9",
"@babel/register": "7.21.0",
"@intlify/vue-i18n-loader": "5.0.0",
"@ungap/event-target": "0.2.3",
"@vue/babel-helper-vue-jsx-merge-props": "1.4.0",
"@vue/babel-plugin-jsx": "1.1.1",
"@vue/compiler-sfc": "3.2.45",
"@vue/test-utils": "2.2.7",
"@vue/test-utils": "2.2.8",
"autoprefixer": "10.4.13",
"babel-loader": "9.1.0",
"babel-loader": "9.1.2",
"babel-plugin-lodash": "3.3.4",
"chai": "4.3.7",
"chalk": "1.1.3",
@ -72,13 +72,13 @@
"css-loader": "6.7.3",
"css-minimizer-webpack-plugin": "4.2.2",
"custom-event-polyfill": "1.0.7",
"eslint": "8.31.0",
"eslint": "8.33.0",
"eslint-config-standard": "17.0.0",
"eslint-formatter-friendly": "7.0.0",
"eslint-plugin-import": "2.26.0",
"eslint-plugin-import": "2.27.5",
"eslint-plugin-n": "15.6.1",
"eslint-plugin-promise": "6.1.1",
"eslint-plugin-vue": "9.8.0",
"eslint-plugin-vue": "9.9.0",
"eslint-webpack-plugin": "3.2.0",
"eventsource-polyfill": "0.9.6",
"express": "4.18.2",
@ -99,7 +99,7 @@
"lodash": "4.17.21",
"mini-css-extract-plugin": "2.7.2",
"mocha": "10.2.0",
"nightwatch": "2.6.6",
"nightwatch": "2.6.11",
"opn": "5.5.0",
"ora": "0.4.1",
"postcss": "8.4.20",

View file

@ -580,8 +580,6 @@ textarea,
}
&[type="checkbox"] {
display: none;
&:checked + label::before {
color: $fallback--text;
color: var(--inputText, $fallback--text);
@ -887,3 +885,15 @@ option {
opacity: 0;
}
/* stylelint-enable no-descending-specificity */
.visible-for-screenreader-only {
display: block;
width: 1px;
height: 1px;
margin: -1px;
overflow: hidden;
visibility: visible;
clip: rect(0 0 0 0);
padding: 0;
position: absolute;
}

View file

@ -71,7 +71,6 @@
<StatusHistoryModal v-if="editingAvailable" />
<SettingsModal />
<UpdateNotification />
<div id="modal" />
<GlobalNoticeList />
</div>
</template>

View file

@ -60,6 +60,8 @@ const getInstanceConfig = async ({ store }) => {
store.dispatch('setInstanceOption', { name: 'textlimit', value: textlimit })
store.dispatch('setInstanceOption', { name: 'accountApprovalRequired', value: data.approval_required })
store.dispatch('setInstanceOption', { name: 'birthdayRequired', value: !!data.pleroma.metadata.birthday_required })
store.dispatch('setInstanceOption', { name: 'birthdayMinAge', value: data.pleroma.metadata.birthday_min_age || 0 })
if (vapidPublicKey) {
store.dispatch('setInstanceOption', { name: 'vapidPublicKey', value: vapidPublicKey })

View file

@ -2,6 +2,7 @@ import { mapState } from 'vuex'
import ProgressButton from '../progress_button/progress_button.vue'
import Popover from '../popover/popover.vue'
import UserListMenu from 'src/components/user_list_menu/user_list_menu.vue'
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faEllipsisV
@ -16,14 +17,30 @@ const AccountActions = {
'user', 'relationship'
],
data () {
return { }
return {
showingConfirmBlock: false,
showingConfirmRemoveFollower: false
}
},
components: {
ProgressButton,
Popover,
UserListMenu
UserListMenu,
ConfirmModal
},
methods: {
showConfirmBlock () {
this.showingConfirmBlock = true
},
hideConfirmBlock () {
this.showingConfirmBlock = false
},
showConfirmRemoveUserFromFollowers () {
this.showingConfirmRemoveFollower = true
},
hideConfirmRemoveUserFromFollowers () {
this.showingConfirmRemoveFollower = false
},
showRepeats () {
this.$store.dispatch('showReblogs', this.user.id)
},
@ -31,13 +48,29 @@ const AccountActions = {
this.$store.dispatch('hideReblogs', this.user.id)
},
blockUser () {
if (!this.shouldConfirmBlock) {
this.doBlockUser()
} else {
this.showConfirmBlock()
}
},
doBlockUser () {
this.$store.dispatch('blockUser', this.user.id)
this.hideConfirmBlock()
},
unblockUser () {
this.$store.dispatch('unblockUser', this.user.id)
},
removeUserFromFollowers () {
if (!this.shouldConfirmRemoveUserFromFollowers) {
this.doRemoveUserFromFollowers()
} else {
this.showConfirmRemoveUserFromFollowers()
}
},
doRemoveUserFromFollowers () {
this.$store.dispatch('removeUserFromFollowers', this.user.id)
this.hideConfirmRemoveUserFromFollowers()
},
reportUser () {
this.$store.dispatch('openUserReportingModal', { userId: this.user.id })
@ -50,6 +83,12 @@ const AccountActions = {
}
},
computed: {
shouldConfirmBlock () {
return this.$store.getters.mergedConfig.modalOnBlock
},
shouldConfirmRemoveUserFromFollowers () {
return this.$store.getters.mergedConfig.modalOnRemoveUserFromFollowers
},
...mapState({
pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable
})

View file

@ -74,6 +74,48 @@
</button>
</template>
</Popover>
<teleport to="#modal">
<confirm-modal
v-if="showingConfirmBlock"
:title="$t('user_card.block_confirm_title')"
:confirm-text="$t('user_card.block_confirm_accept_button')"
:cancel-text="$t('user_card.block_confirm_cancel_button')"
@accepted="doBlockUser"
@cancelled="hideConfirmBlock"
>
<i18n-t
keypath="user_card.block_confirm"
tag="span"
>
<template #user>
<span
v-text="user.screen_name_ui"
/>
</template>
</i18n-t>
</confirm-modal>
</teleport>
<teleport to="#modal">
<confirm-modal
v-if="showingConfirmRemoveFollower"
:title="$t('user_card.remove_follower_confirm_title')"
:confirm-text="$t('user_card.remove_follower_confirm_accept_button')"
:cancel-text="$t('user_card.remove_follower_confirm_cancel_button')"
@accepted="doRemoveUserFromFollowers"
@cancelled="hideConfirmRemoveUserFromFollowers"
>
<i18n-t
keypath="user_card.remove_follower_confirm"
tag="span"
>
<template #user>
<span
v-text="user.screen_name_ui"
/>
</template>
</i18n-t>
</confirm-modal>
</teleport>
</div>
</template>

View file

@ -5,12 +5,16 @@
>
<input
type="checkbox"
class="visible-for-screenreader-only"
:disabled="disabled"
:checked="modelValue"
:indeterminate="indeterminate"
@change="$emit('update:modelValue', $event.target.checked)"
>
<i class="checkbox-indicator" />
<i
class="checkbox-indicator"
:aria-hidden="true"
/>
<span
v-if="!!$slots.default"
class="label"
@ -33,6 +37,7 @@ export default {
<style lang="scss">
@import "../../variables";
@import "../../mixins";
.checkbox {
position: relative;
@ -81,8 +86,6 @@ export default {
}
input[type="checkbox"] {
display: none;
&:checked + .checkbox-indicator::before {
color: $fallback--text;
color: var(--inputText, $fallback--text);

View file

@ -0,0 +1,37 @@
import DialogModal from '../dialog_modal/dialog_modal.vue'
/**
* This component emits the following events:
* cancelled, emitted when the action should not be performed;
* accepted, emitted when the action should be performed;
*
* The caller should close this dialog after receiving any of the two events.
*/
const ConfirmModal = {
components: {
DialogModal
},
props: {
title: {
type: String
},
cancelText: {
type: String
},
confirmText: {
type: String
}
},
computed: {
},
methods: {
onCancel () {
this.$emit('cancelled')
},
onAccept () {
this.$emit('accepted')
}
}
}
export default ConfirmModal

View file

@ -0,0 +1,29 @@
<template>
<dialog-modal
v-body-scroll-lock="true"
class="confirm-modal"
:on-cancel="onCancel"
>
<template #header>
<span v-text="title" />
</template>
<slot />
<template #footer>
<button
class="btn button-default"
@click.prevent="onAccept"
v-text="confirmText"
/>
<button
class="btn button-default"
@click.prevent="onCancel"
v-text="cancelText"
/>
</template>
</dialog-modal>
</template>
<script src="./confirm_modal.js"></script>

View file

@ -1,4 +1,5 @@
import SearchBar from 'components/search_bar/search_bar.vue'
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faSignInAlt,
@ -30,7 +31,8 @@ library.add(
export default {
components: {
SearchBar
SearchBar,
ConfirmModal
},
data: () => ({
searchBarHidden: true,
@ -40,7 +42,8 @@ export default {
window.CSS.supports('-moz-mask-size', 'contain') ||
window.CSS.supports('-ms-mask-size', 'contain') ||
window.CSS.supports('-o-mask-size', 'contain')
)
),
showingConfirmLogout: false
}),
computed: {
enableMask () { return this.supportsMask && this.$store.state.instance.logoMask },
@ -73,15 +76,32 @@ export default {
hideSitename () { return this.$store.state.instance.hideSitename },
logoLeft () { return this.$store.state.instance.logoLeft },
currentUser () { return this.$store.state.users.currentUser },
privateMode () { return this.$store.state.instance.private }
privateMode () { return this.$store.state.instance.private },
shouldConfirmLogout () {
return this.$store.getters.mergedConfig.modalOnLogout
}
},
methods: {
scrollToTop () {
window.scrollTo(0, 0)
},
showConfirmLogout () {
this.showingConfirmLogout = true
},
hideConfirmLogout () {
this.showingConfirmLogout = false
},
logout () {
if (!this.shouldConfirmLogout) {
this.doLogout()
} else {
this.showConfirmLogout()
}
},
doLogout () {
this.$router.replace('/main/public')
this.$store.dispatch('logout')
this.hideConfirmLogout()
},
onSearchBarToggled (hidden) {
this.searchBarHidden = hidden

View file

@ -20,6 +20,7 @@
class="logo"
:to="{ name: 'root' }"
:style="logoBgStyle"
:title="sitename"
>
<div
class="mask"
@ -38,13 +39,13 @@
/>
<button
class="button-unstyled nav-icon"
:title="$t('nav.preferences')"
@click.stop="openSettingsModal"
>
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding"
icon="cog"
:title="$t('nav.preferences')"
/>
</button>
<a
@ -52,30 +53,42 @@
href="/pleroma/admin/#/login-pleroma"
class="nav-icon"
target="_blank"
:title="$t('nav.administration')"
@click.stop
>
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding"
icon="tachometer-alt"
:title="$t('nav.administration')"
/>
</a>
<span class="spacer" />
<button
v-if="currentUser"
class="button-unstyled nav-icon"
:title="$t('login.logout')"
@click.stop.prevent="logout"
>
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding"
icon="sign-out-alt"
:title="$t('login.logout')"
/>
</button>
</div>
</div>
<teleport to="#modal">
<confirm-modal
v-if="showingConfirmLogout"
:title="$t('login.logout_confirm_title')"
:confirm-text="$t('login.logout_confirm_accept_button')"
:cancel-text="$t('login.logout_confirm_cancel_button')"
@accepted="doLogout"
@cancelled="hideConfirmLogout"
>
{{ $t('login.logout_confirm') }}
</confirm-modal>
</teleport>
</nav>
</template>
<script src="./desktop_nav.js"></script>

View file

@ -39,7 +39,7 @@
right: 0;
top: 0;
background: rgb(27 31 35 / 50%);
z-index: 99;
z-index: 2000;
}
}
@ -51,7 +51,7 @@
margin: 15vh auto;
position: fixed;
transform: translateX(-50%);
z-index: 999;
z-index: 2001;
cursor: default;
display: block;
background-color: $fallback--bg;

View file

@ -1,6 +1,7 @@
import Completion from '../../services/completion/completion.js'
import EmojiPicker from '../emoji_picker/emoji_picker.vue'
import Popover from 'src/components/popover/popover.vue'
import ScreenReaderNotice from 'src/components/screen_reader_notice/screen_reader_notice.vue'
import UnicodeDomainIndicator from '../unicode_domain_indicator/unicode_domain_indicator.vue'
import { take } from 'lodash'
import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
@ -109,9 +110,10 @@ const EmojiInput = {
},
data () {
return {
randomSeed: `${Math.random()}`.replace('.', '-'),
input: undefined,
caretEl: undefined,
highlighted: 0,
highlighted: -1,
caret: 0,
focused: false,
blurTimeout: null,
@ -125,12 +127,16 @@ const EmojiInput = {
components: {
Popover,
EmojiPicker,
UnicodeDomainIndicator
UnicodeDomainIndicator,
ScreenReaderNotice
},
computed: {
padEmoji () {
return this.$store.getters.mergedConfig.padEmoji
},
defaultCandidateIndex () {
return this.$store.getters.mergedConfig.autocompleteSelect ? 0 : -1
},
preText () {
return this.modelValue.slice(0, this.caret)
},
@ -203,6 +209,12 @@ const EmojiInput = {
top: this.input.scrollTop,
left: this.input.scrollLeft
})
},
suggestionListId () {
return `suggestions-${this.randomSeed}`
},
suggestionItemId () {
return (index) => `suggestion-item-${index}-${this.randomSeed}`
}
},
mounted () {
@ -278,6 +290,11 @@ const EmojiInput = {
...rest,
img: imageUrl || ''
}))
this.highlighted = this.defaultCandidateIndex
this.$refs.screenReaderNotice.announce(
this.$tc('tool_tip.autocomplete_available',
this.suggestions.length,
{ number: this.suggestions.length }))
}
},
methods: {
@ -374,26 +391,27 @@ const EmojiInput = {
},
cycleBackward (e) {
const len = this.suggestions.length || 0
if (len > 1) {
this.highlighted -= 1
if (this.highlighted < 0) {
this.highlighted = this.suggestions.length - 1
}
this.highlighted -= 1
if (this.highlighted === -1) {
this.input.focus()
} else if (this.highlighted < -1) {
this.highlighted = len - 1
}
if (len > 0) {
e.preventDefault()
} else {
this.highlighted = 0
}
},
cycleForward (e) {
const len = this.suggestions.length || 0
if (len > 1) {
this.highlighted += 1
if (this.highlighted >= len) {
this.highlighted = 0
}
this.highlighted += 1
if (this.highlighted >= len) {
this.highlighted = -1
this.input.focus()
}
if (len > 0) {
e.preventDefault()
} else {
this.highlighted = 0
}
},
scrollIntoView () {
@ -540,6 +558,13 @@ const EmojiInput = {
})
},
resize () {
},
autoCompleteItemLabel (suggestion) {
if (suggestion.user) {
return suggestion.displayText + ' ' + suggestion.detailText
} else {
return this.maybeLocalizedEmojiName(suggestion)
}
}
}
}

View file

@ -4,12 +4,19 @@
class="emoji-input"
:class="{ 'with-picker': !hideEmojiButton }"
>
<slot />
<slot
:id="'textbox-' + randomSeed"
:aria-owns="suggestionListId"
aria-autocomplete="both"
:aria-expanded="showSuggestions"
:aria-activedescendant="(!showSuggestions || highlighted === -1) ? '' : suggestionItemId(highlighted)"
/>
<!-- TODO: make the 'x' disappear if at the end maybe? -->
<div
ref="hiddenOverlay"
class="hidden-overlay"
:style="overlayStyle"
:aria-hidden="true"
>
<span>{{ preText }}</span>
<span
@ -18,11 +25,16 @@
>x</span>
<span>{{ postText }}</span>
</div>
<screen-reader-notice
ref="screenReaderNotice"
aria-live="assertive"
/>
<template v-if="enableEmojiPicker">
<button
v-if="!hideEmojiButton"
class="button-unstyled emoji-picker-icon"
type="button"
:title="$t('emoji.add_emoji')"
@click.prevent="togglePicker"
>
<FAIcon :icon="['far', 'smile-beam']" />
@ -43,17 +55,24 @@
ref="suggestorPopover"
class="autocomplete-panel"
placement="bottom"
:trigger-attrs="{ 'aria-hidden': true }"
>
<template #content>
<div
:id="suggestionListId"
ref="panel-body"
class="autocomplete-panel-body"
role="listbox"
>
<div
v-for="(suggestion, index) in suggestions"
:id="suggestionItemId(index)"
:key="index"
class="autocomplete-item"
role="option"
:class="{ highlighted: index === highlighted }"
:aria-label="autoCompleteItemLabel(suggestion)"
:aria-selected="index === highlighted"
@click.stop.prevent="onClick($event, suggestion)"
>
<span class="image">

View file

@ -94,8 +94,9 @@ export const suggestUsers = ({ dispatch, state }) => {
const newSuggestions = state.users.users.filter(
user =>
user.screen_name.toLowerCase().startsWith(noPrefix) ||
user.name.toLowerCase().startsWith(noPrefix)
user.screen_name && user.name && (
user.screen_name.toLowerCase().startsWith(noPrefix) ||
user.name.toLowerCase().startsWith(noPrefix))
).slice(0, 20).sort((a, b) => {
let aScore = 0
let bScore = 0

View file

@ -3,6 +3,7 @@
ref="popover"
trigger="click"
popover-class="emoji-picker popover-default"
:trigger-attrs="{ 'aria-hidden': true }"
@show="onPopoverShown"
@close="onPopoverClosed"
>

View file

@ -1,4 +1,5 @@
import Popover from '../popover/popover.vue'
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faEllipsisH,
@ -32,10 +33,14 @@ library.add(
const ExtraButtons = {
props: ['status'],
components: { Popover },
components: {
Popover,
ConfirmModal
},
data () {
return {
expanded: false
expanded: false,
showingDeleteDialog: false
}
},
methods: {
@ -46,11 +51,22 @@ const ExtraButtons = {
this.expanded = false
},
deleteStatus () {
const confirmed = window.confirm(this.$t('status.delete_confirm'))
if (confirmed) {
this.$store.dispatch('deleteStatus', { id: this.status.id })
if (this.shouldConfirmDelete) {
this.showDeleteStatusConfirmDialog()
} else {
this.doDeleteStatus()
}
},
doDeleteStatus () {
this.$store.dispatch('deleteStatus', { id: this.status.id })
this.hideDeleteStatusConfirmDialog()
},
showDeleteStatusConfirmDialog () {
this.showingDeleteDialog = true
},
hideDeleteStatusConfirmDialog () {
this.showingDeleteDialog = false
},
pinStatus () {
this.$store.dispatch('pinStatus', this.status.id)
.then(() => this.$emit('onSuccess'))
@ -133,7 +149,10 @@ const ExtraButtons = {
isEdited () {
return this.status.edited_at !== null
},
editingAvailable () { return this.$store.state.instance.editingAvailable }
editingAvailable () { return this.$store.state.instance.editingAvailable },
shouldConfirmDelete () {
return this.$store.getters.mergedConfig.modalOnDelete
}
}
}

View file

@ -165,6 +165,18 @@
/>
</FALayers>
</span>
<teleport to="#modal">
<ConfirmModal
v-if="showingDeleteDialog"
:title="$t('status.delete_confirm_title')"
:cancel-text="$t('status.delete_confirm_cancel_button')"
:confirm-text="$t('status.delete_confirm_accept_button')"
@cancelled="hideDeleteStatusConfirmDialog"
@accepted="doDeleteStatus"
>
{{ $t('status.delete_confirm') }}
</ConfirmModal>
</teleport>
</template>
</Popover>
</template>

View file

@ -38,13 +38,20 @@
class="button-unstyled interactive"
target="_blank"
role="button"
:title="$t('tool_tip.favorite')"
:href="remoteInteractionLink"
>
<FAIcon
class="fa-scale-110 fa-old-padding"
:title="$t('tool_tip.favorite')"
:icon="['far', 'star']"
/>
<FALayers class="fa-scale-110 fa-old-padding-layer">
<FAIcon
class="fa-scale-110"
:icon="['far', 'star']"
/>
<FAIcon
class="focus-marker"
transform="shrink-6 up-9 right-12"
icon="plus"
/>
</FALayers>
</a>
<span
v-if="!mergedConfig.hidePostStats && status.fave_num > 0"

View file

@ -1,12 +1,20 @@
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate'
export default {
props: ['relationship', 'user', 'labelFollowing', 'buttonClass'],
components: {
ConfirmModal
},
data () {
return {
inProgress: false
inProgress: false,
showingConfirmUnfollow: false
}
},
computed: {
shouldConfirmUnfollow () {
return this.$store.getters.mergedConfig.modalOnUnfollow
},
isPressed () {
return this.inProgress || this.relationship.following
},
@ -35,6 +43,12 @@ export default {
}
},
methods: {
showConfirmUnfollow () {
this.showingConfirmUnfollow = true
},
hideConfirmUnfollow () {
this.showingConfirmUnfollow = false
},
onClick () {
this.relationship.following || this.relationship.requested ? this.unfollow() : this.follow()
},
@ -45,12 +59,21 @@ export default {
})
},
unfollow () {
if (this.shouldConfirmUnfollow) {
this.showConfirmUnfollow()
} else {
this.doUnfollow()
}
},
doUnfollow () {
const store = this.$store
this.inProgress = true
requestUnfollow(this.relationship.id, store).then(() => {
this.inProgress = false
store.commit('removeStatus', { timeline: 'friends', userId: this.relationship.id })
})
this.hideConfirmUnfollow()
}
}
}

View file

@ -7,6 +7,27 @@
@click="onClick"
>
{{ label }}
<teleport to="#modal">
<confirm-modal
v-if="showingConfirmUnfollow"
:title="$t('user_card.unfollow_confirm_title')"
:confirm-text="$t('user_card.unfollow_confirm_accept_button')"
:cancel-text="$t('user_card.unfollow_confirm_cancel_button')"
@accepted="doUnfollow"
@cancelled="hideConfirmUnfollow"
>
<i18n-t
keypath="user_card.unfollow_confirm"
tag="span"
>
<template #user>
<span
v-text="user.screen_name_ui"
/>
</template>
</i18n-t>
</confirm-modal>
</teleport>
</button>
</template>

View file

@ -24,6 +24,7 @@
/>
<RemoveFollowerButton
v-if="noFollowsYou && relationship.followed_by"
:user="user"
:relationship="relationship"
class="follow-card-button"
/>

View file

@ -1,10 +1,18 @@
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
import { notificationsFromStore } from '../../services/notification_utils/notification_utils.js'
const FollowRequestCard = {
props: ['user'],
components: {
BasicUserCard
BasicUserCard,
ConfirmModal
},
data () {
return {
showingApproveConfirmDialog: false,
showingDenyConfirmDialog: false
}
},
methods: {
findFollowRequestNotificationId () {
@ -13,7 +21,26 @@ const FollowRequestCard = {
)
return notif && notif.id
},
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 () {
this.$store.state.api.backendInteractor.approveUser({ id: this.user.id })
this.$store.dispatch('removeFollowRequest', this.user)
@ -25,14 +52,34 @@ const FollowRequestCard = {
notification.type = 'follow'
}
})
this.hideApproveConfirmDialog()
},
denyUser () {
if (this.shouldConfirmDeny) {
this.showDenyConfirmDialog()
} else {
this.doDeny()
}
},
doDeny () {
const notifId = this.findFollowRequestNotificationId()
this.$store.state.api.backendInteractor.denyUser({ id: this.user.id })
.then(() => {
this.$store.dispatch('dismissNotificationLocal', { id: notifId })
this.$store.dispatch('removeFollowRequest', this.user)
})
this.hideDenyConfirmDialog()
}
},
computed: {
mergedConfig () {
return this.$store.getters.mergedConfig
},
shouldConfirmApprove () {
return this.mergedConfig.modalOnApproveFollow
},
shouldConfirmDeny () {
return this.mergedConfig.modalOnDenyFollow
}
}
}

View file

@ -14,6 +14,28 @@
{{ $t('user_card.deny') }}
</button>
</div>
<teleport to="#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>
</basic-user-card>
</template>

View file

@ -4,6 +4,7 @@
:class="{ custom: isCustom }"
>
<label
:id="name + '-label'"
:for="preset === 'custom' ? name : name + '-font-switcher'"
class="label"
>
@ -12,7 +13,8 @@
<input
v-if="typeof fallback !== 'undefined'"
:id="name + '-o'"
class="opt exlcude-disabled"
:aria-labelledby="name + '-label'"
class="opt exlcude-disabled visible-for-screenreader-only"
type="checkbox"
:checked="present"
@change="$emit('update:modelValue', typeof modelValue === 'undefined' ? fallback : undefined)"
@ -21,6 +23,7 @@
v-if="typeof fallback !== 'undefined'"
class="opt-l"
:for="name + '-o'"
:aria-hidden="true"
/>
{{ ' ' }}
<Select

View file

@ -1,21 +1,44 @@
<template>
<div>
<label for="interface-language-switcher">
<div class="interface-language-switcher">
<label>
{{ promptText }}
</label>
{{ ' ' }}
<Select
id="interface-language-switcher"
v-model="controlledLanguage"
>
<option
v-for="lang in languages"
:key="lang.code"
:value="lang.code"
<ul class="setting-list">
<li
v-for="index of controlledLanguage.keys()"
:key="index"
>
{{ lang.name }}
</option>
</Select>
<label>
{{ index === 0 ? $t('settings.primary_language') : $tc('settings.fallback_language', index, { index }) }}
<Select
class="language-select"
:model-value="controlledLanguage[index]"
@update:modelValue="val => setLanguageAt(index, val)"
>
<option
v-for="lang in languages"
:key="lang.code"
:value="lang.code"
>
{{ lang.name }}
</option>
</Select>
</label>
<button
v-if="controlledLanguage.length > 1 && index !== 0"
class="button-default btn"
@click="() => removeLanguageAt(index)"
>
{{ $t('settings.remove_language') }}
</button>
</li>
<li>
<button
class="button-default btn"
@click="addLanguage"
>{{ $t('settings.add_language') }}</button>
</li>
</ul>
</div>
</template>
@ -34,7 +57,7 @@ export default {
required: true
},
language: {
type: String,
type: [Array, String],
required: true
},
setLanguage: {
@ -48,7 +71,9 @@ export default {
},
controlledLanguage: {
get: function () { return this.language },
get: function () {
return Array.isArray(this.language) ? this.language : [this.language]
},
set: function (val) {
this.setLanguage(val)
}
@ -58,7 +83,30 @@ export default {
methods: {
getLanguageName (code) {
return localeService.getLanguageName(code)
},
addLanguage () {
this.controlledLanguage = [...this.controlledLanguage, '']
},
setLanguageAt (index, val) {
const lang = [...this.controlledLanguage]
lang[index] = val
this.controlledLanguage = lang
},
removeLanguageAt (index) {
const lang = [...this.controlledLanguage]
lang.splice(index, 1)
this.controlledLanguage = lang
}
}
}
</script>
<style lang="scss">
@import "../../variables";
.interface-language-switcher {
.language-select {
margin-right: 1em;
}
}
</style>

View file

@ -1,5 +1,6 @@
import SideDrawer from '../side_drawer/side_drawer.vue'
import Notifications from '../notifications/notifications.vue'
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
import { unseenNotificationsFromStore } from '../../services/notification_utils/notification_utils'
import GestureService from '../../services/gesture_service/gesture_service'
import NavigationPins from 'src/components/navigation/navigation_pins.vue'
@ -25,12 +26,14 @@ const MobileNav = {
components: {
SideDrawer,
Notifications,
NavigationPins
NavigationPins,
ConfirmModal
},
data: () => ({
notificationsCloseGesture: undefined,
notificationsOpen: false,
notificationsAtTop: true
notificationsAtTop: true,
showingConfirmLogout: false
}),
created () {
this.notificationsCloseGesture = GestureService.swipeGesture(
@ -57,7 +60,11 @@ const MobileNav = {
...mapGetters(['unreadChatCount', 'unreadAnnouncementCount']),
chatsPinned () {
return new Set(this.$store.state.serverSideStorage.prefsStorage.collections.pinnedNavItems).has('chats')
}
},
shouldConfirmLogout () {
return this.$store.getters.mergedConfig.modalOnLogout
},
...mapGetters(['unreadChatCount'])
},
methods: {
toggleMobileSidebar () {
@ -88,9 +95,23 @@ const MobileNav = {
scrollMobileNotificationsToTop () {
this.$refs.mobileNotifications.scrollTo(0, 0)
},
showConfirmLogout () {
this.showingConfirmLogout = true
},
hideConfirmLogout () {
this.showingConfirmLogout = false
},
logout () {
if (!this.shouldConfirmLogout) {
this.doLogout()
} else {
this.showConfirmLogout()
}
},
doLogout () {
this.$router.replace('/main/public')
this.$store.dispatch('logout')
this.hideConfirmLogout()
},
markNotificationsAsSeen () {
// this.$refs.notifications.markAsSeen()

View file

@ -88,6 +88,18 @@
ref="sideDrawer"
:logout="logout"
/>
<teleport to="#modal">
<confirm-modal
v-if="showingConfirmLogout"
:title="$t('login.logout_confirm_title')"
:confirm-text="$t('login.logout_confirm_accept_button')"
:cancel-text="$t('login.logout_confirm_cancel_button')"
@accepted="doLogout"
@cancelled="hideConfirmLogout"
>
{{ $t('login.logout_confirm') }}
</confirm-modal>
</teleport>
</div>
</template>
@ -235,6 +247,16 @@
}
}
}
.confirm-modal.dark-overlay {
&::before {
z-index: 3000;
}
.dialog-modal.panel {
z-index: 3001;
}
}
}
</style>

View file

@ -8,6 +8,7 @@ import Report from '../report/report.vue'
import UserLink from '../user_link/user_link.vue'
import RichContent from 'src/components/rich_content/rich_content.jsx'
import UserPopover from '../user_popover/user_popover.vue'
import ConfirmModal from '../confirm_modal/confirm_modal.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'
@ -43,7 +44,9 @@ const Notification = {
return {
statusExpanded: false,
betterShadow: this.$store.state.interface.browserSupport.cssFilter,
unmuted: false
unmuted: false,
showingApproveConfirmDialog: false,
showingDenyConfirmDialog: false
}
},
props: ['notification'],
@ -56,7 +59,8 @@ const Notification = {
Report,
RichContent,
UserPopover,
UserLink
UserLink,
ConfirmModal
},
methods: {
toggleStatusExpanded () {
@ -71,7 +75,26 @@ const Notification = {
toggleMute () {
this.unmuted = !this.unmuted
},
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 () {
this.$store.state.api.backendInteractor.approveUser({ id: this.user.id })
this.$store.dispatch('removeFollowRequest', this.user)
this.$store.dispatch('markSingleNotificationAsSeen', { id: this.notification.id })
@ -81,13 +104,22 @@ const Notification = {
notification.type = 'follow'
}
})
this.hideApproveConfirmDialog()
},
denyUser () {
if (this.shouldConfirmDeny) {
this.showDenyConfirmDialog()
} else {
this.doDeny()
}
},
doDeny () {
this.$store.state.api.backendInteractor.denyUser({ id: this.user.id })
.then(() => {
this.$store.dispatch('dismissNotificationLocal', { id: this.notification.id })
this.$store.dispatch('removeFollowRequest', this.user)
})
this.hideDenyConfirmDialog()
}
},
computed: {
@ -117,6 +149,15 @@ const Notification = {
isStatusNotification () {
return isStatusNotification(this.notification.type)
},
mergedConfig () {
return this.$store.getters.mergedConfig
},
shouldConfirmApprove () {
return this.mergedConfig.modalOnApproveFollow
},
shouldConfirmDeny () {
return this.mergedConfig.modalOnDenyFollow
},
...mapState({
currentUser: state => state.users.currentUser
})

View file

@ -243,6 +243,28 @@
</template>
</div>
</div>
<teleport to="#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>
</article>
</template>

View file

@ -12,7 +12,8 @@ export default {
data () {
return {
loading: false,
choices: []
choices: [],
randomSeed: `${Math.random()}`.replace('.', '-')
}
},
created () {

View file

@ -4,53 +4,63 @@
:class="containerClass"
>
<div
v-for="(option, index) in options"
:key="index"
class="poll-option"
:role="showResults ? 'section' : (poll.multiple ? 'group' : 'radiogroup')"
>
<div
v-if="showResults"
:title="resultTitle(option)"
class="option-result"
v-for="(option, index) in options"
:key="index"
class="poll-option"
>
<div class="option-result-label">
<span class="result-percentage">
{{ percentageForOption(option.votes_count) }}%
</span>
<RichContent
:html="option.title_html"
:handle-links="false"
:emoji="emoji"
<div
v-if="showResults"
:title="resultTitle(option)"
class="option-result"
>
<div class="option-result-label">
<span class="result-percentage">
{{ percentageForOption(option.votes_count) }}%
</span>
<RichContent
:html="option.title_html"
:handle-links="false"
:emoji="emoji"
/>
</div>
<div
class="result-fill"
:style="{ 'width': `${percentageForOption(option.votes_count)}%` }"
/>
</div>
<div
class="result-fill"
:style="{ 'width': `${percentageForOption(option.votes_count)}%` }"
/>
</div>
<div
v-else
@click="activateOption(index)"
>
<input
v-if="poll.multiple"
type="checkbox"
:disabled="loading"
:value="index"
>
<input
v-else
type="radio"
:disabled="loading"
:value="index"
tabindex="0"
:role="poll.multiple ? 'checkbox' : 'radio'"
:aria-labelledby="`option-vote-${randomSeed}-${index}`"
:aria-checked="choices[index]"
@click="activateOption(index)"
>
<label class="option-vote">
<RichContent
:html="option.title_html"
:handle-links="false"
:emoji="emoji"
/>
</label>
<input
v-if="poll.multiple"
type="checkbox"
class="poll-checkbox"
:disabled="loading"
:value="index"
>
<input
v-else
type="radio"
:disabled="loading"
:value="index"
>
<label class="option-vote">
<RichContent
:id="`option-vote-${randomSeed}-${index}`"
:html="option.title_html"
:handle-links="false"
:emoji="emoji"
/>
</label>
</div>
</div>
</div>
<div class="footer faint">
@ -161,5 +171,9 @@
padding: 0 0.5em;
margin-right: 0.5em;
}
.poll-checkbox {
display: none;
}
}
</style>

View file

@ -94,19 +94,10 @@ export default {
},
convertExpiryToUnit (unit, amount) {
// Note: we want seconds and not milliseconds
switch (unit) {
case 'minutes': return (1000 * amount) / DateUtils.MINUTE
case 'hours': return (1000 * amount) / DateUtils.HOUR
case 'days': return (1000 * amount) / DateUtils.DAY
}
return DateUtils.secondsToUnit(unit, amount)
},
convertExpiryFromUnit (unit, amount) {
// Note: we want seconds and not milliseconds
switch (unit) {
case 'minutes': return 0.001 * amount * DateUtils.MINUTE
case 'hours': return 0.001 * amount * DateUtils.HOUR
case 'days': return 0.001 * amount * DateUtils.DAY
}
return DateUtils.unitToSeconds(unit, amount)
},
expiryAmountChange () {
this.expiryAmount =

View file

@ -8,6 +8,7 @@ import Gallery from 'src/components/gallery/gallery.vue'
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 { propsToNative } from '../../services/attributes_helper/attributes_helper.service.js'
import { reject, map, uniqBy, debounce } from 'lodash'
import suggestor from '../emoji_input/suggestor.js'
import { mapGetters, mapState } from 'vuex'
@ -629,6 +630,9 @@ const PostStatusForm = {
},
openProfileTab () {
this.$store.dispatch('openSettingsModalTab', 'profile')
},
propsToNative (props) {
return propsToNative(props)
}
}
}

View file

@ -30,6 +30,9 @@
<span>{{ $t('post_status.scope_notice.public') }}</span>
<a
class="fa-scale-110 fa-old-padding dismiss"
:title="$t('post_status.scope_notice_dismiss')"
role="button"
tabindex="0"
@click.prevent="dismissScopeNotice()"
>
<FAIcon icon="times" />
@ -42,6 +45,9 @@
<span>{{ $t('post_status.scope_notice.unlisted') }}</span>
<a
class="fa-scale-110 fa-old-padding dismiss"
:title="$t('post_status.scope_notice_dismiss')"
role="button"
tabindex="0"
@click.prevent="dismissScopeNotice()"
>
<FAIcon icon="times" />
@ -54,6 +60,9 @@
<span>{{ $t('post_status.scope_notice.private') }}</span>
<a
class="fa-scale-110 fa-old-padding dismiss"
:title="$t('post_status.scope_notice_dismiss')"
role="button"
tabindex="0"
@click.prevent="dismissScopeNotice()"
>
<FAIcon icon="times" />
@ -124,14 +133,17 @@
:suggest="emojiSuggestor"
class="form-control"
>
<input
v-model="newStatus.spoilerText"
type="text"
:placeholder="$t('post_status.content_warning')"
:disabled="posting && !optimisticPosting"
size="1"
class="form-post-subject"
>
<template #default="inputProps">
<input
v-model="newStatus.spoilerText"
type="text"
:placeholder="$t('post_status.content_warning')"
:disabled="posting && !optimisticPosting"
v-bind="propsToNative(inputProps)"
size="1"
class="form-post-subject"
>
</template>
</EmojiInput>
<EmojiInput
ref="emoji-input"
@ -148,29 +160,32 @@
@sticker-upload-failed="uploadFailed"
@shown="handleEmojiInputShow"
>
<textarea
ref="textarea"
v-model="newStatus.status"
:placeholder="placeholder || $t('post_status.default')"
rows="1"
cols="1"
:disabled="posting && !optimisticPosting"
class="form-post-body"
:class="{ 'scrollable-form': !!maxHeight }"
@keydown.exact.enter="submitOnEnter && postStatus($event, newStatus)"
@keydown.meta.enter="postStatus($event, newStatus)"
@keydown.ctrl.enter="!submitOnEnter && postStatus($event, newStatus)"
@input="resize"
@compositionupdate="resize"
@paste="paste"
/>
<p
v-if="hasStatusLengthLimit"
class="character-counter faint"
:class="{ error: isOverLengthLimit }"
>
{{ charactersLeft }}
</p>
<template #default="inputProps">
<textarea
ref="textarea"
v-model="newStatus.status"
:placeholder="placeholder || $t('post_status.default')"
rows="1"
cols="1"
:disabled="posting && !optimisticPosting"
class="form-post-body"
:class="{ 'scrollable-form': !!maxHeight }"
v-bind="propsToNative(inputProps)"
@keydown.exact.enter="submitOnEnter && postStatus($event, newStatus)"
@keydown.meta.enter="postStatus($event, newStatus)"
@keydown.ctrl.enter="!submitOnEnter && postStatus($event, newStatus)"
@input="resize"
@compositionupdate="resize"
@paste="paste"
/>
<p
v-if="hasStatusLengthLimit"
class="character-counter faint"
:class="{ error: isOverLengthLimit }"
>
{{ charactersLeft }}
</p>
</template>
</EmojiInput>
<div
v-if="!disableScopeSelector"
@ -193,6 +208,7 @@
id="post-content-type"
v-model="newStatus.contentType"
class="form-control"
:attrs="{ 'aria-label': $t('post_status.content_type_selection') }"
>
<option
v-for="postFormat in postFormats"
@ -265,12 +281,10 @@
>
{{ $t('post_status.post') }}
</button>
<!-- touchstart is used to keep the OSK at the same position after a message send -->
<button
v-else
:disabled="uploadingFiles || disableSubmit"
class="btn button-default"
@touchstart.stop.prevent="postStatus($event, newStatus)"
@click.stop.prevent="postStatus($event, newStatus)"
>
{{ $t('post_status.post') }}

View file

@ -6,36 +6,51 @@
:trigger-attrs="{ title: $t('timeline.quick_filter_settings') }"
>
<template #content>
<div class="dropdown-menu">
<div v-if="loggedIn">
<div
class="dropdown-menu"
role="menu"
>
<div
v-if="loggedIn"
role="group"
>
<button
v-if="!conversation"
class="button-default dropdown-item"
:aria-checked="replyVisibilityAll"
role="menuitemradio"
@click="replyVisibilityAll = true"
>
<span
class="menu-checkbox -radio"
:class="{ 'menu-checkbox-checked': replyVisibilityAll }"
:aria-hidden="true"
/>{{ $t('settings.reply_visibility_all') }}
</button>
<button
v-if="!conversation"
class="button-default dropdown-item"
:aria-checked="replyVisibilityFollowing"
role="menuitemradio"
@click="replyVisibilityFollowing = true"
>
<span
class="menu-checkbox -radio"
:class="{ 'menu-checkbox-checked': replyVisibilityFollowing }"
:aria-hidden="true"
/>{{ $t('settings.reply_visibility_following_short') }}
</button>
<button
v-if="!conversation"
class="button-default dropdown-item"
:aria-checked="replyVisibilitySelf"
role="menuitemradio"
@click="replyVisibilitySelf = true"
>
<span
class="menu-checkbox -radio"
:class="{ 'menu-checkbox-checked': replyVisibilitySelf }"
:aria-hidden="true"
/>{{ $t('settings.reply_visibility_self_short') }}
</button>
<div
@ -46,33 +61,43 @@
</div>
<button
class="button-default dropdown-item"
role="menuitemcheckbox"
:aria-checked="muteBotStatuses"
@click="muteBotStatuses = !muteBotStatuses"
>
<span
class="menu-checkbox"
:class="{ 'menu-checkbox-checked': muteBotStatuses }"
:aria-hidden="true"
/>{{ $t('settings.mute_bot_posts') }}
</button>
<button
class="button-default dropdown-item"
role="menuitemcheckbox"
:aria-checked="hideMedia"
@click="hideMedia = !hideMedia"
>
<span
class="menu-checkbox"
:class="{ 'menu-checkbox-checked': hideMedia }"
:aria-hidden="true"
/>{{ $t('settings.hide_media_previews') }}
</button>
<button
class="button-default dropdown-item"
role="menuitemcheckbox"
:aria-checked="hideMutedPosts"
@click="hideMutedPosts = !hideMutedPosts"
>
<span
class="menu-checkbox"
:class="{ 'menu-checkbox-checked': hideMutedPosts }"
:aria-hidden="true"
/>{{ $t('settings.hide_all_muted_posts') }}
</button>
<button
class="button-default dropdown-item dropdown-item-icon"
role="menuitem"
@click="openTab('filtering')"
>
<FAIcon icon="font" />{{ $t('settings.word_filter_and_more') }}

View file

@ -6,60 +6,87 @@
:trigger-attrs="{ title: $t('timeline.quick_view_settings') }"
>
<template #content>
<div class="dropdown-menu">
<button
class="button-default dropdown-item"
@click="conversationDisplay = 'tree'"
>
<span
class="menu-checkbox -radio"
:class="{ 'menu-checkbox-checked': conversationDisplay === 'tree' }"
/><FAIcon icon="folder-tree" /> {{ $t('settings.conversation_display_tree_quick') }}
</button>
<button
class="button-default dropdown-item"
@click="conversationDisplay = 'linear'"
>
<span
class="menu-checkbox -radio"
:class="{ 'menu-checkbox-checked': conversationDisplay === 'linear' }"
/><FAIcon icon="list" /> {{ $t('settings.conversation_display_linear_quick') }}
</button>
<div
class="dropdown-menu"
role="menu"
>
<div role="group">
<button
class="button-default dropdown-item"
:aria-checked="conversationDisplay === 'tree'"
role="menuitemradio"
@click="conversationDisplay = 'tree'"
>
<span
class="menu-checkbox -radio"
:aria-hidden="true"
:class="{ 'menu-checkbox-checked': conversationDisplay === 'tree' }"
/><FAIcon
icon="folder-tree"
:aria-hidden="true"
/> {{ $t('settings.conversation_display_tree_quick') }}
</button>
<button
class="button-default dropdown-item"
:aria-checked="conversationDisplay === 'linear'"
role="menuitemradio"
@click="conversationDisplay = 'linear'"
>
<span
class="menu-checkbox -radio"
:class="{ 'menu-checkbox-checked': conversationDisplay === 'linear' }"
:aria-hidden="true"
/><FAIcon
icon="list"
:aria-hidden="true"
/> {{ $t('settings.conversation_display_linear_quick') }}
</button>
</div>
<div
role="separator"
class="dropdown-divider"
/>
<button
class="button-default dropdown-item"
role="menuitemcheckbox"
:aria-checked="showUserAvatars"
@click="showUserAvatars = !showUserAvatars"
>
<span
class="menu-checkbox"
:class="{ 'menu-checkbox-checked': showUserAvatars }"
:aria-hidden="true"
/>{{ $t('settings.mention_link_show_avatar_quick') }}
</button>
<button
v-if="!conversation"
class="button-default dropdown-item"
role="menuitemcheckbox"
:aria-checked="autoUpdate"
@click="autoUpdate = !autoUpdate"
>
<span
class="menu-checkbox"
:class="{ 'menu-checkbox-checked': autoUpdate }"
:aria-hidden="true"
/>{{ $t('settings.auto_update') }}
</button>
<button
v-if="!conversation"
class="button-default dropdown-item"
role="menuitemcheckbox"
:aria-checked="collapseWithSubjects"
@click="collapseWithSubjects = !collapseWithSubjects"
>
<span
class="menu-checkbox"
:class="{ 'menu-checkbox-checked': collapseWithSubjects }"
:aria-hidden="true"
/>{{ $t('settings.collapse_subject') }}
</button>
<button
class="button-default dropdown-item dropdown-item-icon"
role="menuitem"
@click="openTab('general')"
>
<FAIcon icon="wrench" />{{ $t('settings.more_settings') }}

View file

@ -4,6 +4,7 @@
:class="{ disabled: !present || disabled }"
>
<label
:id="name + '-label'"
:for="name"
class="label"
>
@ -12,7 +13,8 @@
<input
v-if="typeof fallback !== 'undefined'"
:id="name + '-o'"
class="opt"
:aria-labelledby="name + '-label'"
class="opt visible-for-screenreader-only"
type="checkbox"
:checked="present"
@change="$emit('update:modelValue', !present ? fallback : undefined)"
@ -21,6 +23,7 @@
v-if="typeof fallback !== 'undefined'"
class="opt-l"
:for="name + '-o'"
:aria-hidden="true"
/>
<input
:id="name"
@ -34,9 +37,10 @@
@input="$emit('update:modelValue', $event.target.value)"
>
<input
:id="name"
:id="name + '-numeric'"
class="input-number"
type="number"
:aria-labelledby="name + '-label'"
:value="modelValue || fallback"
:disabled="!present || disabled"
:max="hardMax"

View file

@ -3,6 +3,7 @@ import { required, requiredIf, sameAs } from '@vuelidate/validators'
import { mapActions, mapState } from 'vuex'
import InterfaceLanguageSwitcher from '../interface_language_switcher/interface_language_switcher.vue'
import localeService from '../../services/locale/locale.service.js'
import { DAY } from 'src/services/date_utils/date_utils.js'
const registration = {
setup () { return { v$: useVuelidate() } },
@ -13,8 +14,9 @@ const registration = {
username: '',
password: '',
confirm: '',
birthday: '',
reason: '',
language: ''
language: ['']
},
captcha: {}
}),
@ -32,6 +34,12 @@ const registration = {
required,
sameAs: sameAs(this.user.password)
},
birthday: {
required: requiredIf(() => this.birthdayRequired),
maxValue: value => {
return !this.birthdayRequired || new Date(value).getTime() <= this.birthdayMin.getTime()
}
},
reason: { required: requiredIf(() => this.accountApprovalRequired) },
language: {}
}
@ -52,6 +60,24 @@ const registration = {
reasonPlaceholder () {
return this.replaceNewlines(this.$t('registration.reason_placeholder'))
},
birthdayMin () {
const minAge = this.birthdayMinAge
const today = new Date()
today.setUTCMilliseconds(0)
today.setUTCSeconds(0)
today.setUTCMinutes(0)
today.setUTCHours(0)
const minDate = new Date()
minDate.setTime(today.getTime() - minAge * DAY)
return minDate
},
birthdayMinAttr () {
return this.birthdayMin.toJSON().replace(/T.+$/, '')
},
birthdayMinFormatted () {
const browserLocale = localeService.internalToBrowserLocale(this.$i18n.locale)
return this.user.birthday && new Date(Date.parse(this.birthdayMin)).toLocaleDateString(browserLocale, { timeZone: 'UTC', day: 'numeric', month: 'long', year: 'numeric' })
},
...mapState({
registrationOpen: (state) => state.instance.registrationOpen,
signedIn: (state) => !!state.users.currentUser,
@ -59,7 +85,9 @@ const registration = {
serverValidationErrors: (state) => state.users.signUpErrors,
termsOfService: (state) => state.instance.tos,
accountActivationRequired: (state) => state.instance.accountActivationRequired,
accountApprovalRequired: (state) => state.instance.accountApprovalRequired
accountApprovalRequired: (state) => state.instance.accountApprovalRequired,
birthdayRequired: (state) => state.instance.birthdayRequired,
birthdayMinAge: (state) => state.instance.birthdayMinAge
})
},
methods: {
@ -72,7 +100,7 @@ const registration = {
this.user.captcha_token = this.captcha.token
this.user.captcha_answer_data = this.captcha.answer_data
if (this.user.language) {
this.user.language = localeService.internalToBackendLocale(this.user.language)
this.user.language = localeService.internalToBackendLocaleMulti(this.user.language.filter(k => k))
}
this.v$.$touch()

View file

@ -167,6 +167,40 @@
</ul>
</div>
<div
class="form-group"
:class="{ 'form-group--error': v$.user.birthday.$error }"
>
<label
class="form--label"
for="sign-up-birthday"
>
{{ birthdayRequired ? $t('registration.birthday') : $t('registration.birthday_optional') }}
</label>
<input
id="sign-up-birthday"
v-model="user.birthday"
:disabled="isPending"
class="form-control"
type="date"
:max="birthdayRequired ? birthdayMinAttr : undefined"
:aria-required="birthdayRequired"
>
</div>
<div
v-if="v$.user.birthday.$dirty"
class="form-error"
>
<ul>
<li v-if="v$.user.birthday.required.$invalid">
<span>{{ $t('registration.validations.birthday_required') }}</span>
</li>
<li v-if="v$.user.birthday.maxValue.$invalid">
<span>{{ $tc('registration.validations.birthday_min_age', { date: birthdayMinFormatted }) }}</span>
</li>
</ul>
</div>
<div
class="form-group"
:class="{ 'form-group--error': v$.user.language.$error }"
@ -176,6 +210,7 @@
:prompt-text="$t('registration.email_language')"
:language="v$.user.language.$model"
:set-language="val => v$.user.language.$model = val"
@click.stop.prevent
/>
</div>

View file

@ -1,10 +1,16 @@
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
export default {
props: ['relationship'],
props: ['user', 'relationship'],
data () {
return {
inProgress: false
inProgress: false,
showingConfirmRemoveFollower: false
}
},
components: {
ConfirmModal
},
computed: {
label () {
if (this.inProgress) {
@ -12,14 +18,31 @@ export default {
} else {
return this.$t('user_card.remove_follower')
}
},
shouldConfirmRemoveUserFromFollowers () {
return this.$store.getters.mergedConfig.modalOnRemoveUserFromFollowers
}
},
methods: {
showConfirmRemoveUserFromFollowers () {
this.showingConfirmRemoveFollower = true
},
hideConfirmRemoveUserFromFollowers () {
this.showingConfirmRemoveFollower = false
},
onClick () {
if (!this.shouldConfirmRemoveUserFromFollowers) {
this.doRemoveUserFromFollowers()
} else {
this.showConfirmRemoveUserFromFollowers()
}
},
doRemoveUserFromFollowers () {
this.inProgress = true
this.$store.dispatch('removeUserFromFollowers', this.relationship.id).then(() => {
this.inProgress = false
})
this.hideConfirmRemoveUserFromFollowers()
}
}
}

View file

@ -7,6 +7,27 @@
@click="onClick"
>
{{ label }}
<teleport to="#modal">
<confirm-modal
v-if="showingConfirmRemoveFollower"
:title="$t('user_card.remove_follower_confirm_title')"
:confirm-text="$t('user_card.remove_follower_confirm_accept_button')"
:cancel-text="$t('user_card.remove_follower_confirm_cancel_button')"
@accepted="doRemoveUserFromFollowers"
@cancelled="hideConfirmRemoveUserFromFollowers"
>
<i18n-t
keypath="user_card.remove_follower_confirm"
tag="span"
>
<template #user>
<span
v-text="user.screen_name_ui"
/>
</template>
</i18n-t>
</confirm-modal>
</teleport>
</button>
</template>

View file

@ -32,12 +32,20 @@
target="_blank"
role="button"
:href="remoteInteractionLink"
:title="$t('tool_tip.reply')"
>
<FAIcon
icon="reply"
class="fa-scale-110 fa-old-padding"
:title="$t('tool_tip.reply')"
/>
<FALayers class="fa-old-padding-layer">
<FAIcon
class="fa-scale-110"
icon="reply"
/>
<FAIcon
v-if="!replying"
class="focus-marker"
transform="shrink-6 up-8 right-16"
icon="plus"
/>
</FALayers>
</a>
<span
v-if="status.replies_count > 0"

View file

@ -1,3 +1,4 @@
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faRetweet,
@ -15,13 +16,24 @@ library.add(
const RetweetButton = {
props: ['status', 'loggedIn', 'visibility'],
components: {
ConfirmModal
},
data () {
return {
animated: false
animated: false,
showingConfirmDialog: false
}
},
methods: {
retweet () {
if (!this.status.repeated && this.shouldConfirmRepeat) {
this.showConfirmDialog()
} else {
this.doRetweet()
}
},
doRetweet () {
if (!this.status.repeated) {
this.$store.dispatch('retweet', { id: this.status.id })
} else {
@ -31,6 +43,13 @@ const RetweetButton = {
setTimeout(() => {
this.animated = false
}, 500)
this.hideConfirmDialog()
},
showConfirmDialog () {
this.showingConfirmDialog = true
},
hideConfirmDialog () {
this.showingConfirmDialog = false
}
},
computed: {
@ -39,6 +58,9 @@ const RetweetButton = {
},
remoteInteractionLink () {
return this.$store.getters.remoteInteractionLink({ statusId: this.status.id })
},
shouldConfirmRepeat () {
return this.mergedConfig.modalOnRepeat
}
}
}

View file

@ -45,13 +45,20 @@
class="button-unstyled interactive"
target="_blank"
role="button"
:title="$t('tool_tip.repeat')"
:href="remoteInteractionLink"
>
<FAIcon
class="fa-scale-110 fa-old-padding"
icon="retweet"
:title="$t('tool_tip.repeat')"
/>
<FALayers class="fa-old-padding-layer">
<FAIcon
class="fa-scale-110"
icon="retweet"
/>
<FAIcon
class="focus-marker"
transform="shrink-6 up-9 right-12"
icon="plus"
/>
</FALayers>
</a>
<span
v-if="!mergedConfig.hidePostStats && status.repeat_num > 0"
@ -59,6 +66,18 @@
>
{{ status.repeat_num }}
</span>
<teleport to="#modal">
<confirm-modal
v-if="showingConfirmDialog"
:title="$t('status.repeat_confirm_title')"
:confirm-text="$t('status.repeat_confirm_accept_button')"
:cancel-text="$t('status.repeat_confirm_cancel_button')"
@accepted="doRetweet"
@cancelled="hideConfirmDialog"
>
{{ $t('status.repeat_confirm') }}
</confirm-modal>
</teleport>
</div>
</template>

View file

@ -0,0 +1,21 @@
const ScreenReaderNotice = {
props: {
ariaLive: {
type: String,
defualt: 'assertive'
}
},
data () {
return {
currentText: ''
}
},
methods: {
announce (text) {
this.currentText = text
setTimeout(() => { this.currentText = '' }, 1000)
}
}
}
export default ScreenReaderNotice

View file

@ -0,0 +1,10 @@
<template>
<div
class="visible-for-screenreader-only"
:aria-live="ariaLive"
>
{{ currentText }}
</div>
</template>
<script src="./screen_reader_notice.js"></script>

View file

@ -8,6 +8,7 @@
class="button-unstyled nav-icon"
:title="$t('nav.search')"
type="button"
:aria-expanded="!hidden"
@click.prevent.stop="toggleHidden"
>
<FAIcon
@ -29,6 +30,7 @@
<button
class="button-default search-button"
type="submit"
:title="$t('nav.search')"
@click="find(searchTerm)"
>
<FAIcon
@ -39,6 +41,8 @@
<button
class="button-unstyled cancel-search"
type="button"
:title="$t('nav.search_close')"
:aria-expanded="!hidden"
@click.prevent.stop="toggleHidden"
>
<FAIcon

View file

@ -13,6 +13,7 @@ export default {
'modelValue',
'disabled',
'unstyled',
'kind'
'kind',
'attrs'
]
}

View file

@ -6,6 +6,7 @@
<select
:disabled="disabled"
:value="modelValue"
v-bind="attrs"
@change="$emit('update:modelValue', $event.target.value)"
>
<slot />

View file

@ -5,12 +5,12 @@
>
<Popover
trigger="hover"
:trigger-attrs="{ 'aria-label': $t('settings.setting_changed') }"
>
<template #trigger>
&nbsp;
<FAIcon
icon="wrench"
:aria-label="$t('settings.setting_changed')"
/>
</template>
<template #content>

View file

@ -148,6 +148,56 @@
</SizeSetting>
</div>
</li>
<li class="select-multiple">
<span class="label">{{ $t('settings.confirm_dialogs') }}</span>
<ul class="option-list">
<li>
<BooleanSetting path="modalOnRepeat">
{{ $t('settings.confirm_dialogs_repeat') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="modalOnUnfollow">
{{ $t('settings.confirm_dialogs_unfollow') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="modalOnBlock">
{{ $t('settings.confirm_dialogs_block') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="modalOnMute">
{{ $t('settings.confirm_dialogs_mute') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="modalOnDelete">
{{ $t('settings.confirm_dialogs_delete') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="modalOnLogout">
{{ $t('settings.confirm_dialogs_logout') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="modalOnApproveFollow">
{{ $t('settings.confirm_dialogs_approve_follow') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="modalOnDenyFollow">
{{ $t('settings.confirm_dialogs_deny_follow') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="modalOnRemoveUserFromFollowers">
{{ $t('settings.confirm_dialogs_remove_follower') }}
</BooleanSetting>
</li>
</ul>
</li>
</ul>
</div>
<div class="setting-item">
@ -451,6 +501,14 @@
{{ $t('settings.pad_emoji') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="autocompleteSelect"
expert="1"
>
{{ $t('settings.autocomplete_select_first') }}
</BooleanSetting>
</li>
</ul>
</div>
</div>

View file

@ -12,6 +12,7 @@ import InterfaceLanguageSwitcher from 'src/components/interface_language_switche
import BooleanSetting from '../helpers/boolean_setting.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js'
import localeService from 'src/services/locale/locale.service.js'
import { propsToNative } from 'src/services/attributes_helper/attributes_helper.service.js'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
@ -32,6 +33,8 @@ 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,
newBirthday: this.$store.state.users.currentUser.birthday,
showBirthday: this.$store.state.users.currentUser.show_birthday,
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,
@ -43,7 +46,7 @@ const ProfileTab = {
bannerPreview: null,
background: null,
backgroundPreview: null,
emailLanguage: this.$store.state.users.currentUser.language || ''
emailLanguage: this.$store.state.users.currentUser.language || ['']
}
},
components: {
@ -125,12 +128,14 @@ const ProfileTab = {
display_name: this.newName,
fields_attributes: this.newFields.filter(el => el != null),
bot: this.bot,
show_role: this.showRole
show_role: this.showRole,
birthday: this.newBirthday || '',
show_birthday: this.showBirthday
/* eslint-enable camelcase */
}
if (this.emailLanguage) {
params.language = localeService.internalToBackendLocale(this.emailLanguage)
params.language = localeService.internalToBackendLocaleMulti(this.emailLanguage)
}
this.$store.state.api.backendInteractor
@ -257,6 +262,9 @@ const ProfileTab = {
messageArgs: [error.message],
level: 'error'
})
},
propsToNative (props) {
return propsToNative(props)
}
}
}

View file

@ -129,4 +129,9 @@
padding: 0 0.5em;
}
}
.birthday-input {
display: block;
margin-bottom: 1em;
}
}

View file

@ -8,11 +8,14 @@
enable-emoji-picker
:suggest="emojiSuggestor"
>
<input
id="username"
v-model="newName"
class="name-changer"
>
<template #default="inputProps">
<input
id="username"
v-model="newName"
class="name-changer"
v-bind="propsToNative(inputProps)"
>
</template>
</EmojiInput>
<p>{{ $t('settings.bio') }}</p>
<EmojiInput
@ -20,10 +23,13 @@
enable-emoji-picker
:suggest="emojiUserSuggestor"
>
<textarea
v-model="newBio"
class="bio resize-height"
/>
<template #default="inputProps">
<textarea
v-model="newBio"
class="bio resize-height"
v-bind="propsToNative(inputProps)"
/>
</template>
</EmojiInput>
<p v-if="role === 'admin' || role === 'moderator'">
<Checkbox v-model="showRole">
@ -35,6 +41,18 @@
</template>
</Checkbox>
</p>
<div>
<p>{{ $t('settings.birthday.label') }}</p>
<input
id="birthday"
v-model="newBirthday"
type="date"
class="birthday-input"
>
<Checkbox v-model="showBirthday">
{{ $t('settings.birthday.show_birthday') }}
</Checkbox>
</div>
<div v-if="maxFields > 0">
<p>{{ $t('settings.profile_fields.label') }}</p>
<div
@ -48,10 +66,13 @@
hide-emoji-button
:suggest="userSuggestor"
>
<input
v-model="newFields[i].name"
:placeholder="$t('settings.profile_fields.name')"
>
<template #default="inputProps">
<input
v-model="newFields[i].name"
:placeholder="$t('settings.profile_fields.name')"
v-bind="propsToNative(inputProps)"
>
</template>
</EmojiInput>
<EmojiInput
v-model="newFields[i].value"
@ -59,10 +80,13 @@
hide-emoji-button
:suggest="userSuggestor"
>
<input
v-model="newFields[i].value"
:placeholder="$t('settings.profile_fields.value')"
>
<template #default="inputProps">
<input
v-model="newFields[i].value"
:placeholder="$t('settings.profile_fields.value')"
v-bind="propsToNative(inputProps)"
>
</template>
</EmojiInput>
<button
class="delete-field button-unstyled -hover-highlight"

View file

@ -129,12 +129,13 @@
v-model="selected.inset"
:disabled="!present"
name="inset"
class="input-inset"
class="input-inset visible-for-screenreader-only"
type="checkbox"
>
<label
class="checkbox-label"
for="inset"
:aria-hidden="true"
/>
</div>
<div

View file

@ -117,6 +117,7 @@ export default {
onClick={this.clickTab(index)}
class={classesTab.join(' ')}
type="button"
role="tab"
>
<img src={props.image} title={props['image-tooltip']}/>
{props.label ? '' : props.label}
@ -131,6 +132,7 @@ export default {
onClick={this.clickTab(index)}
class={classesTab.join(' ')}
type="button"
role="tab"
>
{!props.icon ? '' : (<FAIcon class="tab-icon" size="2x" fixed-width icon={props.icon}/>)}
<span class="text">
@ -167,11 +169,15 @@ export default {
return (
<div class={'tab-switcher ' + (this.sideTabBar ? 'side-tabs' : 'top-tabs')}>
<div class="tabs">
<div
class="tabs"
role="tablist"
>
{tabs}
</div>
<div
ref="contents"
role="tabpanel"
class={'contents' + (this.scrollableTabs ? ' scrollable-tabs' : '')}
v-body-scroll-lock={this.bodyScrollLock}
>

View file

@ -1,3 +1,4 @@
import { unitToSeconds } from 'src/services/date_utils/date_utils.js'
import UserAvatar from '../user_avatar/user_avatar.vue'
import RemoteFollow from '../remote_follow/remote_follow.vue'
import ProgressButton from '../progress_button/progress_button.vue'
@ -8,6 +9,7 @@ import UserNote from '../user_note/user_note.vue'
import Select from '../select/select.vue'
import UserLink from '../user_link/user_link.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 { mapGetters } from 'vuex'
import { library } from '@fortawesome/fontawesome-svg-core'
@ -46,7 +48,10 @@ export default {
data () {
return {
followRequestInProgress: false,
betterShadow: this.$store.state.interface.browserSupport.cssFilter
betterShadow: this.$store.state.interface.browserSupport.cssFilter,
showingConfirmMute: false,
muteExpiryAmount: 0,
muteExpiryUnit: 'minutes'
}
},
created () {
@ -137,6 +142,12 @@ export default {
supportsNote () {
return 'note' in this.relationship
},
shouldConfirmMute () {
return this.mergedConfig.modalOnMute
},
muteExpiryUnits () {
return ['minutes', 'hours', 'days']
},
...mapGetters(['mergedConfig'])
},
components: {
@ -149,11 +160,29 @@ export default {
Select,
RichContent,
UserLink,
UserNote
UserNote,
ConfirmModal
},
methods: {
showConfirmMute () {
this.showingConfirmMute = true
},
hideConfirmMute () {
this.showingConfirmMute = false
},
muteUser () {
this.$store.dispatch('muteUser', this.user.id)
if (!this.shouldConfirmMute) {
this.doMuteUser()
} else {
this.showConfirmMute()
}
},
doMuteUser () {
this.$store.dispatch('muteUser', {
id: this.user.id,
expiresIn: this.shouldConfirmMute ? unitToSeconds(this.muteExpiryUnit, this.muteExpiryAmount) : 0
})
this.hideConfirmMute()
},
unmuteUser () {
this.$store.dispatch('unmuteUser', this.user.id)

View file

@ -355,3 +355,8 @@
text-decoration: none;
}
}
.mute-expiry {
display: flex;
flex-direction: row;
}

View file

@ -314,6 +314,53 @@
:handle-links="true"
/>
</div>
<teleport to="#modal">
<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="div"
>
<template #user>
<span
v-text="user.screen_name_ui"
/>
</template>
</i18n-t>
<div
class="mute-expiry"
>
<label>
{{ $t('user_card.mute_duration_prompt') }}
</label>
<input
v-model="muteExpiryAmount"
type="number"
class="expiry-amount hide-number-spinner"
:min="0"
>
<Select
v-model="muteExpiryUnit"
unstyled="true"
class="expiry-unit"
>
<option
v-for="unit in muteExpiryUnits"
:key="unit"
:value="unit"
>
{{ $t(`time.${unit}_short`, ['']) }}
</option>
</Select>
</div>
</confirm-modal>
</teleport>
</div>
</template>

View file

@ -7,13 +7,16 @@ import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
import RichContent from 'src/components/rich_content/rich_content.jsx'
import List from '../list/list.vue'
import withLoadMore from '../../hocs/with_load_more/with_load_more'
import localeService from 'src/services/locale/locale.service.js'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faCircleNotch
faCircleNotch,
faBirthdayCake
} from '@fortawesome/free-solid-svg-icons'
library.add(
faCircleNotch
faCircleNotch,
faBirthdayCake
)
const FollowerList = withLoadMore({
@ -76,6 +79,10 @@ const UserProfile = {
},
followersTabVisible () {
return this.isUs || !this.user.hide_followers
},
formattedBirthday () {
const browserLocale = localeService.internalToBrowserLocale(this.$i18n.locale)
return this.user.birthday && new Date(Date.parse(this.user.birthday)).toLocaleDateString(browserLocale, { timeZone: 'UTC', day: 'numeric', month: 'long', year: 'numeric' })
}
},
methods: {

View file

@ -12,6 +12,16 @@
rounded="top"
:has-note-editor="true"
/>
<span
v-if="!!user.birthday"
class="user-birthday"
>
<FAIcon
class="fa-old-padding"
icon="birthday-cake"
/>
{{ $t('user_card.birthday', { birthday: formattedBirthday }) }}
</span>
<div
v-if="user.fields_html && user.fields_html.length > 0"
class="user-profile-fields"
@ -149,6 +159,10 @@
// No sticky header on user profile
--currentPanelStack: 1;
.user-birthday {
margin: 0 0.75em 0.5em;
}
.user-profile-fields {
margin: 0 0.5em;

View file

@ -137,6 +137,10 @@
"login": "Log in",
"description": "Log in with OAuth",
"logout": "Log out",
"logout_confirm_title": "Logout confirmation",
"logout_confirm": "Do you really want to logout?",
"logout_confirm_accept_button": "Logout",
"logout_confirm_cancel_button": "Do not logout",
"password": "Password",
"placeholder": "e.g. lain",
"register": "Register",
@ -172,6 +176,7 @@
"bookmarks": "Bookmarks",
"user_search": "User Search",
"search": "Search",
"search_close": "Close search bar",
"who_to_follow": "Who to follow",
"preferences": "Preferences",
"timelines": "Timelines",
@ -266,6 +271,7 @@
"text/markdown": "Markdown",
"text/bbcode": "BBCode"
},
"content_type_selection": "Post format",
"content_warning": "Subject (optional)",
"default": "Just landed in L.A.",
"direct_warning_to_all": "This post will be visible to all the mentioned users.",
@ -283,6 +289,7 @@
"private": "This post will be visible to your followers only",
"unlisted": "This post will not be visible in Public Timeline and The Whole Known Network"
},
"scope_notice_dismiss": "Close this notice",
"scope": {
"direct": "Direct - post to mentioned users only",
"private": "Followers-only - post to followers only",
@ -312,9 +319,13 @@
"email_required": "cannot be left blank",
"password_required": "cannot be left blank",
"password_confirmation_required": "cannot be left blank",
"password_confirmation_match": "should be the same as password"
"password_confirmation_match": "should be the same as password",
"birthday_required": "cannot be left blank",
"birthday_min_age": "must be on or before {date}"
},
"email_language": "In which language do you want to receive emails from the server?"
"email_language": "In which language do you want to receive emails from the server?",
"birthday": "Birthday:",
"birthday_optional": "Birthday (optional):"
},
"remote_user_resolver": {
"remote_user_resolver": "Remote user resolver",
@ -335,6 +346,10 @@
"select_all": "Select all"
},
"settings": {
"add_language": "Add fallback language",
"remove_language": "Remove",
"primary_language": "Primary language:",
"fallback_language": "Fallback language {index}:",
"app_name": "App name",
"expert_mode": "Show advanced",
"save": "Save changes",
@ -416,6 +431,16 @@
"composing": "Composing",
"confirm_new_password": "Confirm new password",
"current_password": "Current password",
"confirm_dialogs": "Ask for confirmation when",
"confirm_dialogs_repeat": "repeating a status",
"confirm_dialogs_unfollow": "unfollowing a user",
"confirm_dialogs_block": "blocking a user",
"confirm_dialogs_mute": "muting a user",
"confirm_dialogs_delete": "deleting a status",
"confirm_dialogs_logout": "logging out",
"confirm_dialogs_approve_follow": "approving a follower",
"confirm_dialogs_deny_follow": "denying a follower",
"confirm_dialogs_remove_follower": "removing a follower",
"mutes_and_blocks": "Mutes and Blocks",
"data_import_export_tab": "Data import / export",
"default_vis": "Default visibility scope",
@ -440,6 +465,7 @@
"domain_mutes": "Domains",
"avatar_size_instruction": "The recommended minimum size for avatar images is 150x150 pixels.",
"pad_emoji": "Pad emoji with spaces when adding from picker",
"autocomplete_select_first": "Automatically select the first candidate when autocomplete results are available",
"emoji_reactions_on_timeline": "Show emoji reactions on timeline",
"export_theme": "Save preset",
"filtering": "Filtering",
@ -510,6 +536,10 @@
"name": "Label",
"value": "Content"
},
"birthday": {
"label": "Birthday",
"show_birthday": "Show my birthday"
},
"account_privacy": "Privacy",
"use_contain_fit": "Don't crop the attachment in thumbnails",
"name": "Name",
@ -843,6 +873,10 @@
"status": {
"favorites": "Favorites",
"repeats": "Repeats",
"repeat_confirm": "Do you really want to repeat this status?",
"repeat_confirm_title": "Repeat confirmation",
"repeat_confirm_accept_button": "Repeat",
"repeat_confirm_cancel_button": "Do not repeat",
"delete": "Delete status",
"edit": "Edit status",
"edited_at": "(last edited {time})",
@ -852,6 +886,9 @@
"bookmark": "Bookmark",
"unbookmark": "Unbookmark",
"delete_confirm": "Do you really want to delete this status?",
"delete_confirm_title": "Delete confirmation",
"delete_confirm_accept_button": "Delete",
"delete_confirm_cancel_button": "Keep",
"reply_to": "Reply to",
"mentions": "Mentions",
"replies_list": "Replies:",
@ -898,10 +935,22 @@
},
"user_card": {
"approve": "Approve",
"approve_confirm_title": "Approve confirmation",
"approve_confirm_accept_button": "Approve",
"approve_confirm_cancel_button": "Do not approve",
"approve_confirm": "Do you want to approve {user}'s follow request?",
"block": "Block",
"blocked": "Blocked!",
"block_confirm_title": "Block confirmation",
"block_confirm": "Do you really want to block {user}?",
"block_confirm_accept_button": "Block",
"block_confirm_cancel_button": "Do not block",
"deactivated": "Deactivated",
"deny": "Deny",
"deny_confirm_title": "Deny confirmation",
"deny_confirm_accept_button": "Deny",
"deny_confirm_cancel_button": "Do not deny",
"deny_confirm": "Do you want to deny {user}'s follow request?",
"edit_profile": "Edit profile",
"favorites": "Favorites",
"follow": "Follow",
@ -909,6 +958,10 @@
"follow_sent": "Request sent!",
"follow_progress": "Requesting…",
"follow_unfollow": "Unfollow",
"unfollow_confirm_title": "Unfollow confirmation",
"unfollow_confirm": "Do you really want to unfollow {user}?",
"unfollow_confirm_accept_button": "Unfollow",
"unfollow_confirm_cancel_button": "Do not unfollow",
"followees": "Following",
"followers": "Followers",
"following": "Following!",
@ -920,9 +973,18 @@
"message": "Message",
"mute": "Mute",
"muted": "Muted",
"mute_confirm_title": "Mute confirmation",
"mute_confirm": "Do you really want to mute {user}?",
"mute_confirm_accept_button": "Mute",
"mute_confirm_cancel_button": "Do not mute",
"mute_duration_prompt": "Mute this user for (0 for indefinite time):",
"per_day": "per day",
"remote_follow": "Remote follow",
"remove_follower": "Remove follower",
"remove_follower_confirm_title": "Remove follower confirmation",
"remove_follower_confirm_accept_button": "Remove",
"remove_follower_confirm_cancel_button": "Keep",
"remove_follower_confirm": "Do you really want to remove {user} from your followers?",
"report": "Report",
"statuses": "Statuses",
"subscribe": "Subscribe",
@ -936,6 +998,7 @@
"hide_repeats": "Hide repeats",
"show_repeats": "Show repeats",
"bot": "Bot",
"birthday": "Born {birthday}",
"admin_menu": {
"moderation": "Moderation",
"grant_admin": "Grant Admin",
@ -996,7 +1059,8 @@
"reject_follow_request": "Reject follow request",
"bookmark": "Bookmark",
"toggle_expand": "Expand or collapse notification to show post in full",
"toggle_mute": "Expand or collapse notification to reveal muted content"
"toggle_mute": "Expand or collapse notification to reveal muted content",
"autocomplete_available": "{number} result is available. Use up and down keys to navigate through them. | {number} results are available. Use up and down keys to navigate through them."
},
"upload": {
"error": {

View file

@ -603,7 +603,7 @@
"use_websockets": "Uzi teĥnikaron «websockets» (tuja ĝisdatigo)",
"mention_link_display_full_for_remote": "plene nur je uzantoj foraj (ekz. {'@'}zozo{'@'}ekzemplo.org)",
"expert_mode": "Montri altnivelajn",
"setting_server_side": "Ĉi tiu agordo estas ligita al via profilo, kaj efektiviĝon en ĉiuj viaj salutoj kaj klientoj",
"setting_server_side": "Ĉi tiu agordo estas ligita al via profilo, kaj efektiviĝos en ĉiuj viaj salutoj kaj klientoj",
"post_look_feel": "Aspekto de afiŝoj",
"mention_links": "Menciaj ligiloj",
"email_language": "Lingvo de leteroj ricevotaj de la servilo",

View file

@ -17,7 +17,17 @@
"media_removal": "メディアをのぞく",
"media_removal_desc": "このインスタンスは、これらのインスタンスからおくられてきたメディアを、とりのぞきます:",
"media_nsfw": "メディアをすべてセンシティブにする",
"media_nsfw_desc": "このインスタンスは、これらのインスタンスからおくられてきたメディアを、すべて、センシティブにマークします:"
"media_nsfw_desc": "このインスタンスは、これらのインスタンスからおくられてきたメディアを、すべて、センシティブにマークします:",
"reason": "りゆう",
"instance": "インスタンス",
"not_applicable": "なし"
},
"keyword": {
"keyword_policies": "キーワードポリシー",
"reject": "おことわり",
"replace": "おきかえ",
"ftl_removal": "「つながっているすべてのネットワーク」タイムラインからのぞく",
"is_replaced_by": "→"
}
},
"staff": "スタッフ"
@ -36,7 +46,10 @@
"scope_options": "こうかいはんいせんたく",
"text_limit": "もじのかず",
"title": "ゆうこうなきのう",
"who_to_follow": "おすすめユーザー"
"who_to_follow": "おすすめユーザー",
"pleroma_chat_messages": "Pleroma チャット",
"upload_limit": "アップロードできるファイルのおおきさ",
"shout": "Shoutbox"
},
"finder": {
"error_fetching_user": "ユーザーけんさくがエラーになりました",
@ -54,7 +67,34 @@
"disable": "なし",
"enable": "あり",
"confirm": "たしかめる",
"verify": "たしかめる"
"verify": "たしかめる",
"retry": "もういちど、ためしてください",
"loading": "よみこんでいます…",
"undo": "もとにもどす",
"yes": "はい",
"no": "いいえ",
"unpin": "ピンどめするのをやめる",
"scroll_to_top": "いちばんうえにもどる",
"role": {
"moderator": "モデレーター",
"admin": "かんりするひと"
},
"flash_security": "Flash コンテンツはどんなコードでもじっこうできるので、あぶないかもしれません。",
"flash_fail": "Flash コンテンツをよみこむことに、しっぱいしました。コンソールで、くわしいないようを、よむことができます。",
"scope_in_timeline": {
"private": "フォロワーげんてい",
"public": "パブリック",
"unlisted": "アンリステッド",
"direct": "ダイレクト"
},
"pin": "ピンどめする",
"flash_content": "Flash コンテンツを、 Ruffle をつかってひょうじする (うごかないかもしれません)。",
"generic_error_message": "エラーになりました: {0}",
"error_retry": "もういちど、ためしてください",
"never_show_again": "にどとひょうじしない",
"close": "とじる",
"dismiss": "むしする",
"peek": "かくす"
},
"image_cropper": {
"crop_picture": "がぞうをきりぬく",
@ -83,11 +123,17 @@
"heading": {
"totp": "2-ファクターにんしょう",
"recovery": "2-ファクターリカバリー"
}
},
"logout_confirm_title": "ログアウトのかくにん",
"logout_confirm": "ほんとうに、ログアウトしますか?",
"logout_confirm_accept_button": "ログアウトする",
"logout_confirm_cancel_button": "ログアウトしない"
},
"media_modal": {
"previous": "まえ",
"next": "つぎ"
"next": "つぎ",
"counter": "{current} / {total}",
"hide": "メディアビューアーをとじる"
},
"nav": {
"about": "これはなに?",
@ -104,7 +150,20 @@
"user_search": "ユーザーをさがす",
"search": "さがす",
"who_to_follow": "おすすめユーザー",
"preferences": "せってい"
"preferences": "せってい",
"home_timeline": "ホームタイムライン",
"bookmarks": "ブックマーク",
"timelines": "タイムライン",
"chats": "チャット",
"lists": "リスト",
"mobile_notifications": "つうちをひらく (よんでないものがあります)",
"mobile_notifications_close": "つうちをとじる",
"announcements": "おしらせ",
"edit_pinned": "ピンどめをへんしゅう",
"search_close": "けんさくバーをとじる",
"edit_nav_mobile": "ナビゲーションバーのせっていをかえる",
"mobile_sidebar": "モバイルのサイドバーをきりかえる",
"edit_finish": "へんしゅうをおわりにする"
},
"notifications": {
"broken_favorite": "ステータスがみつかりません。さがしています…",
@ -114,21 +173,29 @@
"notifications": "つうち",
"read": "よんだ!",
"repeated_you": "あなたのステータスがリピートされました",
"no_more_notifications": "つうちはありません"
"no_more_notifications": "つうちはありません",
"error": "つうちをとりにいくことに、しっぱいしました: {0}",
"follow_request": "あなたをフォローしたいです",
"migrated_to": "インスタンスを、ひっこしました",
"reacted_with": "{0} でリアクションしました",
"poll_ended": "とうひょうが、おわりました",
"submitted_report": "つうほうしました"
},
"polls": {
"add_poll": "いれふだをはじめる",
"add_poll": "とうひょうをはじめる",
"add_option": "オプションをふやす",
"option": "オプション",
"votes": "いれふだ",
"vote": "ふだをいれる",
"type": "いれふだのかた",
"votes": "ひょう",
"vote": "とうひょうする",
"type": "とうひょうのけいしき",
"single_choice": "ひとつえらぶ",
"multiple_choices": "いくつでもえらべる",
"expiry": "いれふだのながさ",
"expires_in": "いれふだは {0} で、おわります",
"expired": "いれふだは {0} まえに、おわりました",
"not_enough_options": "ユニークなオプションが、たりません"
"expiry": "とうひょうのながさ",
"expires_in": "とうひょうは {0} で、おわります",
"expired": "とうひょうは {0} まえに、おわりました",
"not_enough_options": "ユニークなオプションが、たりません",
"people_voted_count": "{count} にんが、とうひょうしました",
"votes_count": "{count} ひょう"
},
"emoji": {
"stickers": "ステッカー",
@ -139,7 +206,19 @@
"custom": "カスタムえもじ",
"unicode": "ユニコードえもじ",
"load_all_hint": "はじめの {saneAmount} このえもじだけがロードされています。すべてのえもじをロードすると、パフォーマンスがわるくなるかもしれません。",
"load_all": "すべてのえもじをロード ({emojiAmount} こあります)"
"load_all": "すべてのえもじをロード ({emojiAmount} こあります)",
"unicode_groups": {
"flags": "はた",
"activities": "かつどう",
"animals-and-nature": "どうぶつ・しぜん",
"food-and-drink": "たべもの・のみもの",
"objects": "もの",
"people-and-body": "ひと・からだ",
"smileys-and-emotion": "えがお・きもち",
"symbols": "きごう",
"travel-and-places": "りょこう・ばしょ"
},
"regional_indicator": "ばしょをしめすきごう {letter}"
},
"stickers": {
"add_sticker": "ステッカーをふやす"
@ -147,7 +226,10 @@
"interactions": {
"favs_repeats": "リピートとおきにいり",
"follows": "あたらしいフォロー",
"load_older": "ふるいやりとりをみる"
"load_older": "ふるいやりとりをみる",
"emoji_reactions": "えもじリアクション",
"moves": "ユーザーのひっこし",
"reports": "つうほう"
},
"post_status": {
"new_status": "とうこうする",
@ -176,7 +258,18 @@
"private": "フォロワーげんてい: フォロワーのみにとどきます",
"public": "パブリック: パブリックタイムラインにとどきます",
"unlisted": "アンリステッド: パブリックタイムラインにとどきません"
}
},
"media_description_error": "メディアのアップロードにしっぱいしました。もういちどためしてください",
"edit_status": "ステータスをへんしゅうする",
"media_description": "メディアのせつめい",
"content_type_selection": "とうこうのけいしき",
"edit_remote_warning": "ほかのリモートインスタンスは、へんしゅうをサポートしていないかもしれません。そして、へんしゅうされたとうこうをうけとることができないかもしれません。",
"post": "とうこう",
"edit_unsupported_warning": "Pleroma は、メンションやとうひょうのへんしゅうを、サポートしていません。",
"preview": "プレビュー",
"preview_empty": "なにもありません",
"empty_status_error": "とうこうないようを、にゅうりょくしてください",
"scope_notice_dismiss": "このつうちをとじる"
},
"registration": {
"bio": "プロフィール",
@ -196,8 +289,18 @@
"email_required": "なにかかいてください",
"password_required": "なにかかいてください",
"password_confirmation_required": "なにかかいてください",
"password_confirmation_match": "パスワードがちがいます"
}
"password_confirmation_match": "パスワードがちがいます",
"birthday_required": "なにかかいてください",
"birthday_min_age": "{date} か、それよりまえにしてください"
},
"reason_placeholder": "このインスタンスでは、ひとがかくにんして、とうろくをうけいれています。\nなぜあなたがとうろくしたいのかを、かんりしているひとに、おしえてください。",
"bio_optional": "プロフィール (かかなくてもよい)",
"reason": "とうろくするりゆう",
"email_optional": "Eメール (かかなくてもよい)",
"register": "とうろくする",
"email_language": "サーバーからのメールは、なにご(どのことば)がいいですか?",
"birthday": "たんじょうび:",
"birthday_optional": "たんじょうび (かかなくてもよい):"
},
"remote_user_resolver": {
"remote_user_resolver": "リモートユーザーリゾルバー",
@ -393,7 +496,24 @@
"save_load_hint": "「のこす」オプションをONにすると、テーマをえらんだときとロードしたとき、いまのせっていをのこします。また、テーマをエクスポートするとき、これらのオプションをストアします。すべてのチェックボックスをOFFにすると、テーマをエクスポートしたとき、すべてのせっていをセーブします。",
"reset": "リセット",
"clear_all": "すべてクリア",
"clear_opacity": "とうめいどをクリア"
"clear_opacity": "とうめいどをクリア",
"help": {
"older_version_imported": "ふるいバージョンのフロントエンドでつくられたファイルをインポートしました。",
"snapshot_missing": "ファイルにはテーマのスナップショットがありません。おもっていたみためと、ちがうかもしれません。",
"migration_snapshot_ok": "あんぜんのため、テーマのスナップショットがよみこまれました。テーマのデータをよみこむことができます。",
"snapshot_source_mismatch": "バージョンがただしくないです。フロントエンドのバージョンをもとにもどしたあと、あたらしくしたことが、りゆうかもしれません。ふるいフロントエンドでテーマをへんこうしていたばあい、ふるいバージョンをつかうのがいいです。そうでないばあい、あたらしいバージョンをつかってください。",
"snapshot_present": "テーマのスナップショットをよみこみました。せっていはうわがきされました。かわりに、テーマのじっさいのデータをよみこむことができます。",
"fe_upgraded": "フロントエンドといっしょに、テーマエンジンもあたらしくなりました。",
"fe_downgraded": "フロントエンドが、まえのバージョンにもどりました。",
"migration_napshot_gone": "スナップショットがありません。おぼえているみためと、ちがうかもしれません。",
"upgraded_from_v2": "PleromaFEがあたらしくなったので、いままでのみためとすこしちがうかもしれません。",
"v2_imported": "ふるいフロントエンドのためのファイルをインポートしました。せっていしたのとは、すこしちがうかもしれません。",
"future_version_imported": "あたらしいフロントエンドでつくられたファイルをインポートしました。"
},
"load_theme": "テーマをよみこむ",
"keep_as_is": "そのままにする",
"use_snapshot": "ふるいバージョン",
"use_source": "あたらしいバージョン"
},
"common": {
"color": "いろ",
@ -429,7 +549,26 @@
"borders": "さかいめ",
"buttons": "ボタン",
"inputs": "インプットフィールド",
"faint_text": "うすいテキスト"
"faint_text": "うすいテキスト",
"post": "とうこう / プロフィール",
"wallpaper": "かべがみ",
"icons": "アイコン",
"highlight": "よくみえるようにした、ようそ",
"pressed": "おしたとき",
"chat": {
"border": "さかいめ",
"incoming": "うけとったもの",
"outgoing": "おくったもの"
},
"underlay": "アンダーレイ",
"alert_neutral": "それいがい",
"popover": "ツールチップ、メニュー、ポップオーバー",
"poll": "とうひょうのグラフ",
"selectedPost": "えらんだとうこう",
"selectedMenu": "えらんだメニューアイテム",
"disabled": "つかえないとき",
"toggled": "きりかえたとき",
"tabs": "タブ"
},
"radii": {
"_tab_label": "まるさ"
@ -462,7 +601,8 @@
"buttonPressed": "ボタン (おされているとき)",
"buttonPressedHover": "ボタン (ホバー、かつ、おされているとき)",
"input": "インプットフィールド"
}
},
"hintV3": "かげのばあいは、 {0} というかきかたをつかうことができます。そうすると、ほかのいろのスロットをつかうことができます。"
},
"fonts": {
"_tab_label": "フォント",
@ -497,7 +637,167 @@
"title": "バージョン",
"backend_version": "バックエンドのバージョン",
"frontend_version": "フロントエンドのバージョン"
}
},
"notification_visibility_polls": "あなたがさんかしたとうひょうが、おわりました",
"setting_server_side": "このせっていは、あなたのプロフィールについてのものです。へんこうすると、すべてのセッションとクライアントにえいきょうします",
"mute_import_error": "ミュートのインポートが、エラーになりました",
"account_backup_description": "あなたのアカウントじょうほうや、とうこうのアーカイブを、ダウンロードすることができます。しかし、 Pleroma アカウントにインポートすることはまだできません。",
"list_backups_error": "バックアップリストをとりにいくことが、エラーになりました: {error}",
"list_aliases_error": "エイリアスをとりにいくときに、エラーになりました: {error}",
"added_alias": "エイリアスをつくりました。",
"move_account_notes": "もしあなたがアカウントをほかのインスタンスにひっこしたいのなら、ひっこすさきのアカウントからここへのエイリアスをつくってください。",
"file_export_import": {
"backup_settings_theme": "せっていとテーマをファイルにバックアップする",
"restore_settings": "ファイルからせっていをもとにもどす",
"errors": {
"file_too_new": "メジャーバージョン({fileMajor})がちがいます。この PleromaFE (せっていのバージョン {feMajor}) はふるいので、つかうことができません",
"file_slightly_new": "ファイルのマイナーバージョンがちがっています。いくつかのせっていは、よみこまれないかもしれません",
"invalid_file": "これは Pleroma のせっていをバックアップしたファイルではありません。",
"file_too_old": "メジャーバージョン({fileMajor})がちがいます。ファイルのバージョンが古いので、使うことができません(バージョン {feMajor} いじょうのせっていバージョンをつかってください)"
},
"backup_settings": "せっていをファイルにバックアップする",
"backup_restore": "せっていのバックアップ"
},
"hide_wallpaper": "このインスタンスのバックグラウンドをかくす",
"reply_visibility_following_short": "わたしのフォローしているひとにあてられたリプライをみる",
"reply_visibility_self_short": "じぶんにあてられたリプライだけをみる",
"save": "へんこうをほぞんする",
"reset_banner_confirm": "ほんとうに、バナーをリセットしますか?",
"tree_advanced": "ツリービューで、ナビゲーションをもっとじゅうなんにする",
"third_column_mode": "じゅうぶんなくうかんがあれば、3ばんめのれつをひょうじする",
"conversation_other_replies_button": "「ほかのリプライ」ボタンをひょうじするばしょ",
"user_popover_avatar_action_open": "プロフィールをひらく",
"notification_setting_filters": "フィルター",
"notification_setting_hide_notification_contents": "おくったひとと、ないようを、プッシュつうちにひょうじしない",
"backup_running": "バックアップしています。{number}このデータをしょりしました。",
"word_filter_and_more": "ことばのフィルターと、そのほか…",
"account_privacy": "プライバシー",
"posts": "とうこう",
"move_account": "アカウントをひっこす",
"move_account_target": "ひっこしさきのアカウント (れい: {example})",
"mute_bot_posts": "Bot のとうこうをミュートする",
"hide_bot_indication": "Bot によるとうこうであることを、とうこうにひょうじしない",
"hide_all_muted_posts": "ミュートしたとうこうをかくす",
"hide_shoutbox": "Shoutbox をかくす",
"conversation_display_tree": "ツリーけいしき",
"mention_link_display_full_for_remote": "リモートユーザーだけ、ながいなまえでひょうじする (れい: {'@'}hoge{'@'}example.org)",
"mention_link_bolden_you": "あなたがメンションされたとき、あなたへのメンションを、よくみえるようにする",
"user_popover_avatar_action": "ポップオーバーのアバターをクリックしたとき",
"user_popover_avatar_action_zoom": "アバターをおおきくする",
"user_popover_avatar_action_close": "ポップオーバーをとじる",
"always_show_post_button": "とうこうボタンをいつもひょうじする",
"auto_update": "あたらしいとうこうを、じどうてきにみせる",
"user_mutes": "ユーザー",
"useStreamingApi": "とうこうとつうちを、リアルタイムにうけとる",
"use_websockets": "Websockets をつかう (リアルタイムアップデート)",
"mutes_and_blocks": "ミュートとブロック",
"emoji_reactions_on_timeline": "えもじリアクションをタイムラインにひょうじする",
"accent": "アクセント",
"domain_mutes": "ドメイン",
"import_mutes_from_a_csv_file": "CSVファイルからミュートをインポートする",
"reset_avatar": "アバターをリセットする",
"remove_language": "とりのぞく",
"primary_language": "いちばんわかることば:",
"add_language": "よびとしてつかうことばを、ついかする",
"fallback_language": "よびとしてつかうことば {index}:",
"lists_navigation": "ナビゲーションにリストをひょうじする",
"account_alias": "アカウントのエイリアス",
"mention_link_display_full": "いつも、ながいなまえをひょうじする (れい: {'@'}hoge{'@'}example.org)",
"setting_changed": "せっていは、デフォルトとちがっています",
"email_language": "サーバーからうけとるEメールのことば",
"mute_export": "ミュートのエクスポート",
"mute_export_button": "あなたのミュートを、 CSV ファイルにエクスポートします",
"mute_import": "ミュートのインポート",
"mutes_imported": "ミュートをインポートしました!すこしじかんがかかるかもしれません。",
"account_backup": "アカウントのバックアップ",
"account_backup_table_head": "バックアップ",
"download_backup": "ダウンロード",
"backup_not_ready": "バックアップのじゅんびが、まだできていません。",
"backup_failed": "バックアップにしっぱいしました。",
"remove_backup": "とりのぞく",
"add_backup": "あたらしいバックアップをつくる",
"added_backup": "あたらしいバックアップをつくりました。",
"add_backup_error": "あたらしいバックアップをつくるときに、エラーになりました: {error}",
"bot": "これは bot アカウントです",
"account_alias_table_head": "エイリアス",
"hide_list_aliases_error_action": "とじる",
"remove_alias": "このエイリアスをけす",
"add_alias_error": "エイリアスをつくるときに、エラーになりました: {error}",
"new_alias_target": "あたらしいエイリアスをつくる (れい: {example})",
"moved_account": "アカウントをひっこしました。",
"move_account_error": "アカウントをひっこしているときに、エラーになりました: {error}",
"wordfilter": "ことばのフィルター",
"hide_media_previews": "メディアのプレビューをかくす",
"right_sidebar": "サイドバーをみぎにひょうじする",
"hide_wordfiltered_statuses": "ことばのフィルターでフィルターされたステータスをかくす",
"hide_muted_threads": "ミュートされたスレッドをかくす",
"navbar_column_stretch": "ナビゲーションバーをれつのはばまでのばす",
"birthday": {
"label": "たんじょうび",
"show_birthday": "たんじょうびを、ひょうじする"
},
"profile_fields": {
"label": "プロフィールのメタデータ",
"add_field": "フィールドをふやす",
"name": "ラベル",
"value": "ないよう"
},
"user_profiles": "ユーザープロフィール",
"notification_visibility_moves": "ユーザーのひっこし",
"notification_visibility_emoji_reactions": "リアクション",
"hide_favorites_description": "おきにいりのリストをみせない (つうちはおくられます)",
"reset_profile_background": "プロフィールバックグラウンドをリセットする",
"reset_profile_banner": "プロフィールバナーをリセットする",
"reset_avatar_confirm": "ほんとうに、アバターをリセットしますか?",
"reset_background_confirm": "ほんとうに、バックグラウンドをリセットしますか?",
"column_sizes_sidebar": "サイドバー",
"column_sizes_notifs": "つうち",
"columns": "れつ",
"column_sizes": "れつのおおきさ",
"column_sizes_content": "コンテンツ",
"conversation_display": "スレッドのひょうじけいしき",
"conversation_display_linear": "リニアけいしき",
"conversation_display_linear_quick": "リニアビュー",
"show_scrollbars": "よこのれつにスクロールバーをひょうじする",
"third_column_mode_none": "3ばんめのれつをひょうじしない",
"third_column_mode_postform": "とうこうフォームとナビゲーション",
"third_column_mode_notifications": "つうちのれつをひょうじする",
"tree_fade_ancestors": "げんざいのステータスのおやを、うすいいろのもじでひょうじする",
"conversation_other_replies_button_below": "ステータスのした",
"conversation_other_replies_button_inside": "ステータスのなか",
"max_depth_in_thread": "デフォルトでひょうじするスレッドのふかさ",
"sensitive_by_default": "デフォルトで、とうこうをNSFWにする",
"type_domains_to_mute": "ミュートしたいドメインを、ここでけんさくできます",
"mention_link_use_tooltip": "メンションのリンクをクリックしたとき、ユーザーカードをみせる",
"mention_link_show_avatar": "ユーザーのアバターをリンクのよこにひょうじする",
"mention_link_show_avatar_quick": "ユーザーのアバターをメンションのとなりにひょうじする",
"mention_link_fade_domain": "ドメイン(れい: {'@'}hoge{'@'}example.org のなかの {'@'}example.org)を、うすいいろにする",
"user_popover_avatar_overlay": "ユーザーのポップオーバーを、ユーザーのアバターのうえにひょうじする",
"show_yous": "(あなた)をひょうじする",
"notification_setting_block_from_strangers": "フォローしていないユーザーからのつうちをブロックする",
"notification_setting_privacy": "プライバシー",
"more_settings": "そのたのせってい",
"expert_mode": "くわしいせっていを、ひょうじする",
"mention_links": "メンションのリンク",
"post_look_feel": "とうこうのみためとかんかく",
"allow_following_move": "フォローしているアカウントがインスタンスをひっこしたばあい、じどうでフォローしてもよい",
"chatMessageRadius": "チャットメッセージ",
"confirm_dialogs": "つぎのばあいに、かくにんをする",
"confirm_dialogs_repeat": "ステータスをリピートするとき",
"confirm_dialogs_unfollow": "ユーザーのフォローをはずすとき",
"confirm_dialogs_block": "ユーザーをブロックするとき",
"confirm_dialogs_mute": "ユーザーをミュートするとき",
"confirm_dialogs_delete": "ステータスをけすとき",
"confirm_dialogs_logout": "ログアウトするとき",
"confirm_dialogs_approve_follow": "フォローをうけいれるとき",
"confirm_dialogs_deny_follow": "フォローをことわるとき",
"confirm_dialogs_remove_follower": "フォロワーをとりのぞくとき",
"conversation_display_tree_quick": "ツリービュー",
"disable_sticky_headers": "れつのヘッダーを、がめんのいちばんうえにこていしない",
"virtual_scrolling": "タイムラインのレンダリングをよくする",
"use_at_icon": "{'@'} きごうを、もじのかわりに、アイコンでひょうじする",
"mention_link_display_short": "いつも、みじかいなまえにする (れい: {'@'}hoge)",
"mention_link_display": "メンションのリンクをひょうじするけいしき"
},
"time": {
"day": "{0}日",
@ -531,7 +831,23 @@
"year": "{0}年",
"years": "{0}年",
"year_short": "{0}年",
"years_short": "{0}年"
"years_short": "{0}年",
"unit": {
"minutes": "{0}ふん",
"seconds_short": "{0}びょう",
"weeks": "{0}しゅうかん",
"weeks_short": "{0}しゅう",
"years": "{0}ねん",
"years_short": "{0}ねん",
"days": "{0}にち",
"days_short": "{0}にち",
"hours": "{0}じかん",
"hours_short": "{0}じかん",
"minutes_short": "{0}ふん",
"months": "{0}かげつ",
"months_short": "{0}かげつ",
"seconds": "{0}びょう"
}
},
"timeline": {
"collapse": "たたむ",
@ -543,7 +859,11 @@
"show_new": "よみこみ",
"up_to_date": "さいしん",
"no_more_statuses": "これでおわりです",
"no_statuses": "ありません"
"no_statuses": "ありません",
"socket_broke": "コード{0}により、リアルタイムでつながることがなくなりました",
"socket_reconnected": "リアルタイムでつながることを、つくりました",
"reload": "もういちど、よみこむ",
"error": "タイムラインをとりにいくときに、エラーになりました: {0}"
},
"status": {
"favorites": "おきにいり",
@ -556,7 +876,57 @@
"reply_to": "へんしん:",
"replies_list": "へんしん:",
"mute_conversation": "スレッドをミュートする",
"unmute_conversation": "スレッドをミュートするのをやめる"
"unmute_conversation": "スレッドをミュートするのをやめる",
"repeat_confirm_title": "リピートのかくにん",
"mentions": "メンション",
"thread_muted": "ミュートされたスレッド",
"collapse_attachments": "ファイルをかくす",
"remove_attachment": "ファイルをとりのぞく",
"thread_show_full": "このスレッドのすべてのとうこうをみる (ぜんぶで{numStatus}このステータス、ふかさ{depth})",
"show_all_attachments": "すべてのファイルをみる",
"hide_full_subject": "かくす",
"nsfw": "NSFW",
"hide_content": "かくす",
"status_deleted": "このとうこうは、けされました",
"you": "(あなた)",
"expand": "ひろげる",
"repeat_confirm_accept_button": "リピートする",
"repeat_confirm_cancel_button": "リピートしない",
"edited_at": "({time} まえにへんしゅう)",
"delete_confirm_title": "けすことのかくにん",
"delete_confirm_accept_button": "けす",
"delete_confirm_cancel_button": "のこす",
"edit": "ステータスをへんしゅうする",
"bookmark": "ブックマークする",
"unbookmark": "ブックマークをはずす",
"replies_list_with_others": "へんしん (ほかに +{numReplies}こ):",
"status_unavailable": "ステータスがありません",
"copy_link": "リンクをコピー",
"external_source": "そとにあるソース",
"thread_muted_and_words": "つぎのことばをふくむので:",
"show_content": "みる",
"plus_more": "あと {number}こ",
"many_attachments": "とうこうには、{number}このファイルがついています",
"show_attachment_in_modal": "メディアモーダルでみる",
"show_attachment_description": "せつめいのプレビュー (ぜんぶみるには、ファイルをひらいてください)",
"hide_attachment": "ファイルをかくす",
"attachment_stop_flash": "Flash プレーヤーをとめる",
"move_up": "ファイルをひだりにうごかす",
"move_down": "ファイルをみぎにうごかす",
"open_gallery": "ギャラリーをひらく",
"thread_hide": "スレッドをかくす",
"thread_show": "スレッドをみる",
"show_full_subject": "すべてをみる",
"repeat_confirm": "ほんとうに、このステータスをリピートしますか?",
"show_all_conversation": "このスレッドをぜんぶみる (あと {numStatus}このステータス)",
"show_only_conversation_under_this": "このステータスへのへんしんだけをみる",
"status_history": "ステータスのれきし",
"thread_show_full_with_icon": "{icon} {text}",
"thread_follow": "のこりのとうこうをみる (ぜんぶで {numStatus}このステータス)",
"thread_follow_with_icon": "{icon} {text}",
"ancestor_follow": "このステータスよりしたの、{numReplies}このへんしんをみる",
"ancestor_follow_with_icon": "{icon} {text}",
"show_all_conversation_with_icon": "{icon} {text}"
},
"user_card": {
"approve": "うけいれ",
@ -577,7 +947,7 @@
"media": "メディア",
"mention": "メンション",
"mute": "ミュート",
"muted": "ミュートしています",
"muted": "ミュートしています",
"per_day": "/日",
"remote_follow": "リモートフォロー",
"report": "つうほう",
@ -608,8 +978,52 @@
"disable_remote_subscription": "ほかのインスタンスからフォローされないようにする",
"disable_any_subscription": "フォローされないようにする",
"quarantine": "ほかのインスタンスのユーザーのとうこうをとめる",
"delete_user": "ユーザーをけす"
}
"delete_user": "ユーザーをけす",
"delete_user_data_and_deactivate_confirmation": "これをすると、このアカウントのデータがきえて、にどとつかえなくなります。ほんとうに、していいですか?"
},
"mute_confirm_accept_button": "ミュートする",
"unfollow_confirm_title": "フォローをやめることのかくにん",
"mute_confirm": "ほんとうに、 {user} をミュートしますか?",
"mute_duration_prompt": "このユーザーをつぎのじかんだけミュートする (0にすると、おわりがありません):",
"edit_note_apply": "てきよう",
"block_confirm": "ほんとうに、 {user} をブロックしますか?",
"deactivated": "つかえない",
"remove_follower": "フォロワーをとりのぞく",
"highlight": {
"solid": "バッググラウンドをひとつのいろにする",
"striped": "しまもようのバックグラウンドにする",
"side": "はじにせんをつける",
"disabled": "めだたせない"
},
"mute_confirm_cancel_button": "ミュートしない",
"unfollow_confirm_accept_button": "フォローをやめる",
"unfollow_confirm": "ほんとうに、 {user} のフォローをやめますか?",
"unfollow_confirm_cancel_button": "フォローしたままにする",
"mute_confirm_title": "ミュートのかくにん",
"block_confirm_accept_button": "ブロックする",
"block_confirm_cancel_button": "ブロックしない",
"deny_confirm_title": "おことわりのかくにん",
"deny_confirm_accept_button": "ことわる",
"deny_confirm_cancel_button": "ことわらない",
"deny_confirm": "{user} のフォローリクエストをことわりますか?",
"follow_cancel": "リクエストをキャンセル",
"birthday": "{birthday} に、うまれました",
"remove_follower_confirm_title": "フォロワーをとりのぞくことのかくにん",
"remove_follower_confirm_accept_button": "とりのぞく",
"remove_follower_confirm_cancel_button": "のこす",
"remove_follower_confirm": "ほんとうに、 {user} をあなたのフォロワーからとりのぞきますか?",
"edit_note": "メモをへんしゅうする",
"edit_note_cancel": "キャンセル",
"message": "メッセージ",
"bot": "bot",
"approve_confirm_title": "うけいれのかくにん",
"approve_confirm_accept_button": "うけいれる",
"approve_confirm_cancel_button": "うけいれない",
"approve_confirm": "{user} のフォローリクエストをうけいれますか?",
"edit_profile": "プロフィールをへんしゅう",
"block_confirm_title": "ブロックのかくにん",
"note_blank": "(なし)",
"note": "メモ"
},
"user_profile": {
"timeline_title": "ユーザータイムライン",
@ -634,13 +1048,21 @@
"repeat": "リピート",
"reply": "リプライ",
"favorite": "おきにいり",
"user_settings": "ユーザーせってい"
"user_settings": "ユーザーせってい",
"accept_follow_request": "フォローのおねがいを、うけいれる",
"toggle_mute": "ミュートされたないようをみるために、つうちをひらくか、とじる",
"autocomplete_available": "{number}このけっかが、あります。うえとしたのキーをつかって、けっかをみることができます。",
"add_reaction": "リアクションをつける",
"reject_follow_request": "フォローのおねがいを、ことわる",
"bookmark": "ブックマーク",
"toggle_expand": "とうこうをすべてみるために、つうちをひらくか、とじる"
},
"upload": {
"error": {
"base": "アップロードにしっぱいしました。",
"file_too_big": "ファイルがおおきすぎます [{filesize} {filesizeunit} / {allowedsize} {allowedsizeunit}]",
"default": "しばらくしてから、ためしてください"
"default": "しばらくしてから、ためしてください",
"message": "アップロードにしっぱいしました: {0}"
},
"file_size_units": {
"B": "B",
@ -655,7 +1077,9 @@
"hashtags": "ハッシュタグ",
"person_talking": "{count} にんが、はなしています",
"people_talking": "{count} にんが、はなしています",
"no_results": "みつかりませんでした"
"no_results": "みつかりませんでした",
"no_more_results": "これでおわりです",
"load_more": "もっとみる"
},
"password_reset": {
"forgot_password": "パスワードを、わすれましたか?",
@ -668,5 +1092,103 @@
"password_reset_disabled": "このインスタンスでは、パスワードリセットは、できません。インスタンスのアドミニストレーターに、おといあわせください。",
"password_reset_required": "ログインするには、パスワードをリセットしてください。",
"password_reset_required_but_mailer_is_disabled": "あなたはパスワードのリセットがひつようです。しかし、まずいことに、このインスタンスでは、パスワードのリセットができなくなっています。このインスタンスのアドミニストレーターに、おといあわせください。"
},
"announcements": {
"post_placeholder": "おしらせのないようを、にゅうりょくしてください。",
"end_time_prompt": "おわるじかん: ",
"inactive_message": "このおしらせは、つかわれていません",
"page_header": "おしらせ",
"title": "おしらせ",
"post_action": "とうこう",
"post_form_header": "おしらせをとうこう",
"mark_as_read_action": "よんだことにする",
"post_error": "エラー: {error}",
"close_error": "とじる",
"delete_action": "けす",
"start_time_display": "{time}にはじまります",
"end_time_display": "{time}におわります",
"edit_action": "へんしゅう",
"start_time_prompt": "はじまるじかん: ",
"all_day_prompt": "このイベントはいちにちじゅうやります",
"published_time_display": "{time}にこうかいされました",
"submit_edit_action": "そうしん",
"cancel_edit_action": "キャンセル"
},
"report": {
"reported_statuses": "つうほうされたステータス:",
"reporter": "つうほうしたひと:",
"state_closed": "クローズ",
"state_resolved": "かいけつしました",
"reported_user": "つうほうされたユーザー:",
"notes": "メモ:",
"state": "じょうたい:",
"state_open": "オープン"
},
"update": {
"update_bugs": "もんだいや、バグがあれば、 {pleromaGitlab} でおしえてください。ちゃんとテストはしているのですが、たくさんのことをかえているので、そしてかいはつバージョンをつかっているので、もんだいやバグに、きづかないことがあります。あなたがきづいたもんだいについての、フィードバックやていあんを、まっています。 Pleroma や Pleroma-FE をよくするやりかたについても、おしえてください。",
"update_changelog_here": "すべてのかわったことのきろく",
"art_by": "{linkToArtist}によるさくひん",
"big_update_title": "すこし、まってください",
"big_update_content": "しばらくリリースがありませんでした。おもっていたみためと、ちがうかもしれません。",
"update_bugs_gitlab": "Pleroma GitLab",
"update_changelog": "かわったことをすべてみるには、{theFullChangelog}をみてください。"
},
"chats": {
"new": "あたらしいチャット",
"chats": "チャット",
"you": "あなた:",
"message_user": "{nickname} にメッセージ",
"delete": "けす",
"empty_message_error": "なにかかいてください",
"more": "もっとみる",
"delete_confirm": "ほんとうに、このメッセージをけしますか?",
"error_loading_chat": "チャットをよみこむことに、しっぱいしました。",
"error_sending_message": "メッセージをおくることに、しっぱいしました。",
"empty_chat_list_placeholder": "チャットがありません。あたらしいチャットボタンをおして、はじめてください!"
},
"shoutbox": {
"title": "Shoutbox"
},
"errors": {
"storage_unavailable": "Pleroma はブラウザーのストレージにアクセスすることができません。あなたがログインしたことと、あなたのローカルのせっていは、ほぞんされません。ほかにももんだいがおきるかもしれません。 Cookie をゆうこうにしてください。"
},
"lists": {
"lists": "リスト",
"new": "あたらしいリスト",
"search": "ユーザーをさがす",
"title": "リストのなまえ",
"create": "つくる",
"save": "へんこうをほぞんする",
"delete": "リストをけす",
"following_only": "フォローしているひとげんていにする",
"manage_lists": "リストをかんりする",
"manage_members": "リストにふくまれるひとを、かんりする",
"add_members": "もっとユーザーをさがす",
"remove_from_list": "リストからとりのぞく",
"add_to_list": "リストにいれる",
"editing_list": "リスト {listTitle} をへんしゅうしています",
"creating_list": "あたらしいリストをつくっています",
"update_title": "なまえをほぞんする",
"really_delete": "ほんとうに、リストをけしますか?",
"is_in_list": "すでにリストのなかにあります",
"error": "リストをへんしゅうするときに、エラーになりました: {0}"
},
"file_type": {
"audio": "オーディオ",
"video": "ビデオ",
"image": "がぞう",
"file": "ファイル"
},
"display_date": {
"today": "きょう"
},
"unicode_domain_indicator": {
"tooltip": "このドメインは、ASCIIいがいのもじをふくんでいます。"
},
"domain_mute_card": {
"mute": "ミュート",
"mute_progress": "ミュートしています…",
"unmute": "ミュートをやめる",
"unmute_progress": "ミュートをやめています…"
}
}

View file

@ -7,8 +7,11 @@
// sed -i -e "s/'//gm" -e 's/"/\\"/gm' -re 's/^( +)(.+?): ((.+?))?(,?)(\{?)$/\1"\2": "\4"/gm' -e 's/\"\{\"/{/g' -e 's/,"$/",/g' file.json
// There's only problem that apostrophe character ' gets replaced by \\ so you have to fix it manually, sorry.
import { isEqual } from 'lodash'
import { languages, langCodeToJsonName } from './languages.js'
const ULTIMATE_FALLBACK_LOCALE = 'en'
const hasLanguageFile = (code) => languages.includes(code)
const loadLanguageFile = (code) => {
@ -25,11 +28,26 @@ const messages = {
en: require('./en.json').default
},
setLanguage: async (i18n, language) => {
if (hasLanguageFile(language)) {
const messages = await loadLanguageFile(language)
i18n.setLocaleMessage(language, messages.default)
const languages = (Array.isArray(language) ? language : [language]).filter(k => k)
if (!languages.includes(ULTIMATE_FALLBACK_LOCALE)) {
languages.push(ULTIMATE_FALLBACK_LOCALE)
}
i18n.locale = language
const [first, ...rest] = languages
if (first === i18n.locale && isEqual(rest, i18n.fallbackLocale)) {
return
}
for (const lang of languages) {
if (hasLanguageFile(lang)) {
const messages = await loadLanguageFile(lang)
i18n.setLocaleMessage(lang, messages.default)
}
}
i18n.fallbackLocale = rest
i18n.locale = first
}
}

View file

@ -118,7 +118,10 @@
"totp": "Двофакторна автентифікація"
},
"enter_two_factor_code": "Введіть двофакторний код автентифікації",
"placeholder": "напр. stepan"
"placeholder": "напр. stepan",
"logout_confirm": "Ви дійсно хочете вийти?",
"logout_confirm_accept_button": "Вийти",
"logout_confirm_cancel_button": "Ні, хочу назад!"
},
"importer": {
"error": "Під час імпортування файлу сталася помилка.",
@ -189,7 +192,8 @@
"mobile_notifications": "Відкрити сповіщення (є непрочитані)",
"mobile_notifications_close": "Закрити сповіщення",
"edit_nav_mobile": "Редагувати панель навігації",
"announcements": "Анонси"
"announcements": "Анонси",
"search_close": "Закрити панель пошуку"
},
"media_modal": {
"next": "Наступна",

View file

@ -53,7 +53,15 @@
"direct": "私讯",
"private": "仅关注者",
"unlisted": "列外"
}
},
"scroll_to_top": "滚动至顶",
"generic_error_message": "发生一个错误:{0}",
"never_show_again": "不再显示",
"undo": "撤销",
"yes": "是",
"no": "否",
"unpin": "取消固定该项",
"pin": "固定该项"
},
"image_cropper": {
"crop_picture": "裁剪图片",
@ -82,7 +90,11 @@
"heading": {
"totp": "双重因素验证",
"recovery": "双重因素恢复"
}
},
"logout_confirm_cancel_button": "不要登出",
"logout_confirm_title": "确认登出",
"logout_confirm_accept_button": "登出",
"logout_confirm": "您确定要登出吗?"
},
"media_modal": {
"previous": "往前",
@ -109,7 +121,16 @@
"chats": "聊天",
"timelines": "时间线",
"bookmarks": "书签",
"home_timeline": "主页时间线"
"home_timeline": "主页时间线",
"lists": "列表",
"edit_finish": "完成编辑",
"mobile_notifications": "打开通知(有未读的)",
"mobile_notifications_close": "关闭通知",
"announcements": "公告",
"edit_nav_mobile": "自定义导航栏",
"edit_pinned": "编辑固定的项目",
"mobile_sidebar": "切换移动设备侧栏",
"search_close": "关闭搜索栏"
},
"notifications": {
"broken_favorite": "未知的状态,正在搜索中…",
@ -124,7 +145,8 @@
"migrated_to": "迁移到了",
"follow_request": "想要关注你",
"error": "取得通知时发生错误:{0}",
"poll_ended": "投票结束了"
"poll_ended": "投票结束了",
"submitted_report": "提交举报"
},
"polls": {
"add_poll": "增加投票",
@ -149,7 +171,9 @@
"favs_repeats": "转发和喜欢",
"follows": "新的关注者",
"load_older": "加载更早的互动",
"moves": "用户迁移"
"moves": "用户迁移",
"reports": "举报",
"emoji_reactions": "表情回应"
},
"post_status": {
"new_status": "发布新状态",
@ -183,7 +207,12 @@
"media_description": "媒体描述",
"media_description_error": "更新媒体失败,请重试",
"empty_status_error": "不能发布没有内容、没有附件的发文",
"post": "发送"
"post": "发送",
"edit_remote_warning": "其它远程实例可能不支持编辑并且无法接收您的帖子的最新版本。",
"edit_unsupported_warning": "Pleroma 不支持对提及或投票进行编辑。",
"edit_status": "编辑状态",
"content_type_selection": "发帖格式",
"scope_notice_dismiss": "关闭此提示"
},
"registration": {
"bio": "简介",
@ -203,12 +232,18 @@
"email_required": "不能留空",
"password_required": "不能留空",
"password_confirmation_required": "不能留空",
"password_confirmation_match": "密码不一致"
"password_confirmation_match": "密码不一致",
"birthday_required": "不能为空",
"birthday_min_age": "必须在 {date} 或之前"
},
"reason_placeholder": "此实例的注册需要手动批准。\n请让管理员知道您为什么想要注册。",
"reason": "注册理由",
"register": "注册",
"email_language": "你想从服务器收到什么语言的邮件?"
"email_language": "你想从服务器收到什么语言的邮件?",
"bio_optional": "介绍(可选)",
"email_optional": "电子邮件(可选)",
"birthday": "生日:",
"birthday_optional": "生日(可选):"
},
"selectable_list": {
"select_all": "选择全部"
@ -599,7 +634,7 @@
"backup_settings": "备份设置到文件",
"backup_restore": "设置备份"
},
"right_sidebar": "在右侧显示侧边栏",
"right_sidebar": "反转分栏的顺序",
"hide_shoutbox": "隐藏实例留言板",
"expert_mode": "显示高级",
"download_backup": "下载",
@ -631,7 +666,78 @@
"move_account_notes": "如果你想把账号移动到别的地方,你必须去目标账号,然后加一个指向这里的别名。",
"wordfilter": "词语过滤器",
"user_profiles": "用户资料",
"third_column_mode_notifications": "消息栏"
"third_column_mode_notifications": "通知栏",
"backup_running": "此备份正在进行,已处理 {number} 条记录。 |此备份正在进行,已处理 {number} 条记录。",
"lists_navigation": "在导航中显示列表",
"word_filter_and_more": "词过滤器及其它...",
"backup_failed": "此备份已失败。",
"birthday": {
"label": "生日",
"show_birthday": "展示我的生日"
},
"hide_favorites_description": "不显示我的喜欢列表(人们仍然会收到通知)",
"third_column_mode": "当有足够的空间时,显示第三栏包含",
"third_column_mode_postform": "主要的发文形式和导航",
"columns": "分栏",
"user_popover_avatar_overlay": "在用户头像上显示用户弹出窗口",
"navbar_column_stretch": "延伸导航栏至分栏宽度",
"posts": "帖子",
"conversation_display_linear_quick": "线性视图",
"conversation_other_replies_button": "显示 “其它回复” 按钮",
"confirm_dialogs_delete": "删除状态",
"confirm_dialogs_mute": "隐藏用户",
"column_sizes": "分栏大小",
"column_sizes_sidebar": "侧栏",
"column_sizes_content": "內容",
"column_sizes_notifs": "通知",
"conversation_other_replies_button_below": "在状态下方",
"conversation_other_replies_button_inside": "在状态中",
"auto_update": "自动显示新的帖子",
"use_websockets": "使用 websockets实时更新",
"max_depth_in_thread": "默认显示同主题帖子中的最大层数",
"hide_wordfiltered_statuses": "隐藏经过词语过滤的状态",
"hide_muted_threads": "不显示已隐藏的同主题帖子",
"notification_visibility_polls": "你所投的投票的结束于",
"tree_advanced": "允许在树状视图中进行更灵活的导航",
"tree_fade_ancestors": "以模糊的文字显示当前状态的原型",
"conversation_display_linear": "线性样式",
"mention_link_fade_domain": "淡化域名(例如:{'@'}example.org 中的 {'@'}foo{'@'}example.org",
"mention_link_bolden_you": "当你被提及时突出显示提及你",
"user_popover_avatar_action": "弹出式头像点击动作",
"user_popover_avatar_action_zoom": "缩放头像",
"user_popover_avatar_action_close": "关闭弹出窗口",
"show_yous": "显示 (You)s",
"add_language": "添加备用语言",
"remove_language": "移除",
"primary_language": "主要语言:",
"fallback_language": "备用语言 {index}",
"account_privacy": "隐私",
"conversation_display": "对话显示样式",
"conversation_display_tree": "树状样式",
"conversation_display_tree_quick": "树状视图",
"disable_sticky_headers": "不要把分栏的顶栏固定在屏幕的顶部",
"confirm_dialogs": "请求确认于",
"confirm_dialogs_logout": "登出",
"confirm_dialogs_deny_follow": "拒绝关注请求",
"confirm_dialogs_approve_follow": "批准关注请求",
"confirm_dialogs_block": "屏蔽用户",
"confirm_dialogs_unfollow": "取消关注用户",
"confirm_dialogs_repeat": "转发状态",
"confirm_dialogs_remove_follower": "移除关注者",
"mute_bot_posts": "隐藏机器人的帖子",
"hide_bot_indication": "隐藏帖子中的机器人提示",
"always_show_post_button": "始终显示浮动的新帖子按钮",
"show_scrollbars": "显示侧栏的滚动条",
"third_column_mode_none": "完全不显示第三栏",
"use_at_icon": "将 {'@'} 符号显示为图标而不是文本",
"mention_link_display": "显示提及链接",
"mention_link_display_short": "始终以简称的形式出现(例如:{'@'}foo",
"mention_link_display_full_for_remote": "仅远程实例用户以全名的形式出现(例如:{'@'}foo{'@'}example.org",
"mention_link_display_full": "始终以全名的形式出现(例如:{'@'}foo{'@'}example.org",
"mention_link_use_tooltip": "点击提及链接时显示用户卡片",
"mention_link_show_avatar": "在链接旁边显示用户头像",
"mention_link_show_avatar_quick": "在提及内容旁边显示用户头像",
"user_popover_avatar_action_open": "打开个人资料"
},
"time": {
"day": "{0} 天",
@ -697,7 +803,9 @@
"reload": "重新载入",
"error": "取得时间轴时发生错误:{0}",
"socket_broke": "丢失实时连接CloseEvent code {0}",
"socket_reconnected": "已建立实时连接"
"socket_reconnected": "已建立实时连接",
"quick_view_settings": "快速视图设置",
"quick_filter_settings": "快速过滤设置"
},
"status": {
"favorites": "喜欢",
@ -706,7 +814,7 @@
"pin": "在个人资料置顶",
"unpin": "取消在个人资料置顶",
"pinned": "置顶",
"delete_confirm": "你真的想要删除这条状态吗?",
"delete_confirm": "您确定要删除这条状态吗?",
"reply_to": "回复",
"replies_list": "回复:",
"mute_conversation": "隐藏对话",
@ -715,7 +823,7 @@
"show_content": "显示内容",
"hide_full_subject": "隐藏此部分标题",
"show_full_subject": "显示全部标题",
"thread_muted": "此系列消息已被隐藏",
"thread_muted": "同主题帖子已被隐藏",
"copy_link": "复制状态链接",
"status_unavailable": "状态不可取得",
"unbookmark": "取消书签",
@ -736,10 +844,10 @@
"attachment_stop_flash": "停止 Flash 播放器",
"move_up": "把附件左移",
"open_gallery": "打开图库",
"thread_hide": "隐藏这个线索",
"thread_show": "显示这个线索",
"thread_hide": "隐藏这个同主题帖子",
"thread_show": "显示这个同主题帖子",
"thread_show_full_with_icon": "{icon} {text}",
"thread_follow": "查看这个线索的剩余部分(一共有 {numStatus} 个状态)",
"thread_follow": "查看这个同主题帖子的剩余部分(一共有 {numStatus} 个状态)",
"thread_follow_with_icon": "{icon} {text}",
"ancestor_follow": "查看这个状态下的别的 {numReplies} 个回复",
"ancestor_follow_with_icon": "{icon} {text}",
@ -748,8 +856,19 @@
"mentions": "提及",
"replies_list_with_others": "回复(另外 +{numReplies} 个):",
"move_down": "把附件右移",
"thread_show_full": "显示这个线索下的所有东西(一共有 {numStatus} 个状态,最大深度 {depth}",
"show_only_conversation_under_this": "只显示这个状态的回复"
"thread_show_full": "显示这个同主题帖子下的所有东西(一共有 {numStatus} 个状态,最大深度 {depth}",
"show_only_conversation_under_this": "只显示这个状态的回复",
"repeat_confirm": "您确定要转发这条状态吗?",
"repeat_confirm_title": "确认转发",
"repeat_confirm_accept_button": "转发",
"repeat_confirm_cancel_button": "不要转发",
"edit": "编辑状态",
"edited_at": "(最后编辑于 {time}",
"delete_confirm_title": "确认删除",
"delete_confirm_accept_button": "删除",
"delete_confirm_cancel_button": "保留",
"show_attachment_in_modal": "在媒体模式中显示",
"status_history": "状态历史"
},
"user_card": {
"approve": "核准",
@ -797,7 +916,8 @@
"disable_remote_subscription": "禁止从远程实例关注用户",
"disable_any_subscription": "完全禁止关注用户",
"quarantine": "从联合实例中禁止用户帖子",
"delete_user": "删除用户"
"delete_user": "删除用户",
"delete_user_data_and_deactivate_confirmation": "这将永久删除该账户的数据并停用该账户。你完全确定吗?"
},
"hidden": "已隐藏",
"show_repeats": "显示转发",
@ -811,7 +931,41 @@
"solid": "单一颜色背景",
"disabled": "不突出显示"
},
"edit_profile": "编辑个人资料"
"edit_profile": "编辑个人资料",
"approve_confirm_title": "确认批准",
"approve_confirm_accept_button": "批准",
"block_confirm_accept_button": "屏蔽",
"block_confirm_cancel_button": "不要屏蔽",
"deactivated": "已停用",
"deny_confirm_title": "确认拒绝",
"deny_confirm_accept_button": "拒绝",
"deny_confirm_cancel_button": "不要拒绝",
"deny_confirm": "您是否要拒绝 {user} 的关注请求?",
"follow_cancel": "取消请求",
"unfollow_confirm_title": "确认取消关注",
"unfollow_confirm": "您确定要取消关注 {user} 吗?",
"unfollow_confirm_accept_button": "取消关注",
"unfollow_confirm_cancel_button": "不要取消关注",
"mute_confirm_title": "确认隐藏",
"mute_confirm_accept_button": "隐藏",
"mute_confirm_cancel_button": "不要隐藏",
"mute_duration_prompt": "让这个用户隐藏0表示无限期",
"remove_follower": "移除关注者",
"remove_follower_confirm_title": "确认移除关注者",
"remove_follower_confirm_cancel_button": "保留",
"remove_follower_confirm": "您确定要将 {user} 从您的关注者里移除吗?",
"birthday": "生于 {birthday}",
"note": "备注",
"approve_confirm_cancel_button": "不要批准",
"approve_confirm": "您是否要批准 {user} 的关注请求?",
"block_confirm_title": "确认屏蔽",
"block_confirm": "您确定要屏蔽 {user} 吗?",
"mute_confirm": "您确定要隐藏 {user} 吗?",
"remove_follower_confirm_accept_button": "移除",
"note_blank": "(空)",
"edit_note": "编辑备注",
"edit_note_apply": "应用",
"edit_note_cancel": "取消"
},
"user_profile": {
"timeline_title": "用户时间线",
@ -840,7 +994,10 @@
"reject_follow_request": "拒绝关注请求",
"add_reaction": "添加互动",
"bookmark": "书签",
"accept_follow_request": "接受关注请求"
"accept_follow_request": "接受关注请求",
"toggle_expand": "展开或折叠通知以显示帖子全文",
"toggle_mute": "展开或折叠通知以显示已隐藏的内容",
"autocomplete_available": "共有 {number} 个结果可用。使用向上和向下键浏览它们。"
},
"upload": {
"error": {
@ -862,7 +1019,9 @@
"hashtags": "话题标签",
"person_talking": "{count} 人正在讨论",
"people_talking": "{count} 人正在讨论",
"no_results": "没有搜索结果"
"no_results": "没有搜索结果",
"no_more_results": "没有更多结果",
"load_more": "加载更多结果"
},
"password_reset": {
"forgot_password": "忘记密码了?",
@ -890,7 +1049,20 @@
"search_emoji": "搜索表情符号",
"emoji": "表情符号",
"load_all": "加载所有表情符号(共 {emojiAmount} 个)",
"load_all_hint": "最先加载的 {saneAmount} 表情符号,加载全部表情符号可能会带来性能问题。"
"load_all_hint": "最先加载的 {saneAmount} 表情符号,加载全部表情符号可能会带来性能问题。",
"unicode_groups": {
"flags": "旗帜",
"food-and-drink": "饮食",
"objects": "物件",
"people-and-body": "人和身体",
"symbols": "符号",
"travel-and-places": "旅行和地点",
"activities": "活动",
"animals-and-nature": "动物和自然",
"smileys-and-emotion": "表情与情感"
},
"regional_indicator": "地区指示符 {letter}",
"unpacked": "拆分的表情符号"
},
"about": {
"mrf": {
@ -950,7 +1122,7 @@
"empty_chat_list_placeholder": "您还没有任何聊天记录。开始聊天吧!",
"error_sending_message": "发送消息时出了点问题。",
"error_loading_chat": "加载聊天时出了点问题。",
"delete_confirm": "您确要删除此消息吗?",
"delete_confirm": "您确要删除此消息吗?",
"more": "更多",
"empty_message_error": "无法发布空消息",
"new": "新聊天",
@ -958,5 +1130,69 @@
"delete": "删除",
"message_user": "发消息给 {nickname}",
"you": "你:"
},
"announcements": {
"page_header": "公告",
"title": "公告",
"mark_as_read_action": "标为已读",
"post_form_header": "发布公告",
"post_placeholder": "在这里输入公告内容...",
"post_action": "发布",
"post_error": "错误:{error}",
"close_error": "关闭",
"delete_action": "删除",
"start_time_prompt": "起始时间: ",
"end_time_prompt": "终止时间: ",
"all_day_prompt": "这是全天的事件",
"published_time_display": "发表于 {time}",
"start_time_display": "开始于 {time}",
"end_time_display": "结束于 {time}",
"edit_action": "编辑",
"submit_edit_action": "提交",
"cancel_edit_action": "取消",
"inactive_message": "这个公告不活跃"
},
"report": {
"reported_user": "被举报者:",
"state_closed": "已关闭",
"state_resolved": "已解决",
"reporter": "举报者:",
"state_open": "开启",
"reported_statuses": "已举报的状态:",
"notes": "备注:",
"state": "状态:"
},
"unicode_domain_indicator": {
"tooltip": "此域名包含非 ascii 字符。"
},
"update": {
"update_bugs_gitlab": "Pleroma GitLab",
"update_changelog": "关于变化的更多细节,请参见 {theFullChangelog} 。",
"update_changelog_here": "完整的更新日志",
"big_update_title": "请忍耐一下",
"big_update_content": "我们已经有一段时间没有发布发行版,所以事情的外观和感觉可能与你习惯的不一样。",
"update_bugs": "请在 {pleromaGitlab} 上报告任何问题和bug因为我们已经改变了很多虽然我们进行了彻底的测试并且自己使用了开发版本但我们可能错过了一些东西。我们欢迎你对你可能遇到的问题或如何改进Pleroma和Pleroma-FE提出反馈和建议。",
"art_by": "Art by {linkToArtist}"
},
"lists": {
"search": "搜索用户",
"create": "创建",
"save": "保存更改",
"delete": "删除列表",
"following_only": "限制于正在关注",
"manage_lists": "管理列表",
"manage_members": "管理列表成员",
"add_members": "搜索更多用户",
"remove_from_list": "从列表中移除",
"add_to_list": "添加到列表",
"is_in_list": "已在列表中",
"editing_list": "正在编辑列表 {listTitle}",
"creating_list": "正在创建新的列表",
"update_title": "保存标题",
"really_delete": "真的要删除列表吗?",
"error": "操作列表时出错:{0}",
"lists": "列表",
"new": "新的列表",
"title": "列表标题"
}
}

View file

@ -113,12 +113,23 @@
"submit": "提交",
"apply": "應用",
"role": {
"moderator": "主持人",
"moderator": "審查者",
"admin": "管理員"
},
"flash_content": "點擊以使用 Ruffle 顯示 Flash 內容(實驗性,可能無效)。",
"flash_security": "請注意這可能有潜在的危險因為Flash內容仍然是武斷的程式碼。",
"flash_fail": "無法加載flash內容請參閱控制台瞭解詳細資訊。"
"flash_fail": "無法加載flash內容請參閱控制台瞭解詳細資訊。",
"no": "否",
"generic_error_message": "發生了一個錯誤: {0}",
"never_show_again": "不再顯示",
"yes": "是",
"undo": "復原",
"scroll_to_top": "滾動至頂部",
"pin": "置頂",
"scope_in_timeline": {
"private": "僅關注者"
},
"unpin": "停止置頂"
},
"finder": {
"find_user": "尋找用戶",
@ -133,7 +144,8 @@
"pleroma_chat_messages": "Pleroma 聊天",
"chat": "聊天",
"gopher": "Gopher",
"upload_limit": "上傳限制"
"upload_limit": "上傳限制",
"shout": "留言板"
},
"exporter": {
"processing": "正在處理,稍後會提示您下載文件",
@ -164,11 +176,14 @@
"reject": "拒絕",
"accept_desc": "本實例只接收來自下列實例的消息:",
"simple_policies": "站規",
"accept": "接受"
"accept": "接受",
"instance": "實例",
"reason": "原因",
"not_applicable": "N/A"
},
"mrf_policies_desc": "MRF 策略會影響本實例的互通行為。以下策略已啟用:",
"keyword": {
"ftl_removal": "從“全部已知網絡”時間線上移除",
"ftl_removal": "從「全部已知網絡」時間線上移除",
"replace": "取代",
"reject": "拒絕",
"is_replaced_by": "→",
@ -865,5 +880,26 @@
"password_reset_disabled": "密碼重置已經被禁用。請聯繫你的實例管理員。",
"password_reset_required": "您必須重置密碼才能登陸。",
"password_reset_required_but_mailer_is_disabled": "您必須重置密碼,但是密碼重置被禁用了。請聯繫您所在實例的管理員。"
},
"announcements": {
"post_error": "錯誤: {error}",
"close_error": "關閉",
"delete_action": "刪除",
"start_time_prompt": "開始時間: ",
"end_time_prompt": "結束時間: ",
"all_day_prompt": "這是全日活動",
"start_time_display": "{time} 開始",
"end_time_display": "{time} 結束",
"published_time_display": "{time} 發布",
"edit_action": "編輯",
"submit_edit_action": "送出",
"cancel_edit_action": "取消",
"inactive_message": "此公告無效",
"page_header": "公告",
"title": "公告",
"mark_as_read_action": "標示為以閱讀",
"post_placeholder": "在此輸入您的公告內容……",
"post_form_header": "發布公告",
"post_action": "發布"
}
}

View file

@ -78,6 +78,15 @@ export const defaultState = {
minimalScopesMode: undefined, // instance default
// This hides statuses filtered via a word filter
hideFilteredStatuses: undefined, // instance default
modalOnRepeat: undefined, // instance default
modalOnUnfollow: undefined, // instance default
modalOnBlock: undefined, // instance default
modalOnMute: undefined, // instance default
modalOnDelete: undefined, // instance default
modalOnLogout: undefined, // instance default
modalOnApproveFollow: undefined, // instance default
modalOnDenyFollow: undefined, // instance default
modalOnRemoveUserFromFollowers: undefined, // instance default
playVideosInModal: false,
useOneClickNsfw: false,
useContainFit: true,
@ -106,7 +115,8 @@ export const defaultState = {
conversationTreeAdvanced: undefined, // instance default
conversationOtherRepliesButton: undefined, // instance default
conversationTreeFadeAncestors: undefined, // instance default
maxDepthInThread: undefined // instance default
maxDepthInThread: undefined, // instance default
autocompleteSelect: undefined // instance default
}
// caching the instance default properties
@ -184,7 +194,10 @@ const config = {
case 'interfaceLanguage':
messages.setLanguage(this.getters.i18n, value)
dispatch('loadUnicodeEmojiData', value)
Cookies.set(BACKEND_LANGUAGE_COOKIE_NAME, localeService.internalToBackendLocale(value))
Cookies.set(
BACKEND_LANGUAGE_COOKIE_NAME,
localeService.internalToBackendLocaleMulti(value)
)
break
case 'thirdColumnMode':
dispatch('setLayoutWidth', undefined)

View file

@ -71,6 +71,15 @@ const defaultState = {
hideSitename: false,
hideUserStats: false,
muteBotStatuses: false,
modalOnRepeat: false,
modalOnUnfollow: false,
modalOnBlock: true,
modalOnMute: false,
modalOnDelete: true,
modalOnLogout: true,
modalOnApproveFollow: false,
modalOnDenyFollow: false,
modalOnRemoveUserFromFollowers: false,
loginMethod: 'password',
logo: '/static/logo.svg',
logoMargin: '.2em',
@ -95,6 +104,7 @@ const defaultState = {
conversationOtherRepliesButton: 'below',
conversationTreeFadeAncestors: false,
maxDepthInThread: 6,
autocompleteSelect: false,
// Nasty stuff
customEmoji: [],
@ -107,6 +117,8 @@ const defaultState = {
restrictedNicknames: [],
safeDM: true,
knownDomains: [],
birthdayRequired: false,
birthdayMinAge: 0,
// Feature-set, apparently, not everything here is reported...
shoutAvailable: false,
@ -286,8 +298,13 @@ const instance = {
langList
.map(async lang => {
if (!state.unicodeEmojiAnnotations[lang]) {
const annotations = await loadAnnotations(lang)
commit('setUnicodeEmojiAnnotations', { lang, annotations })
try {
const annotations = await loadAnnotations(lang)
commit('setUnicodeEmojiAnnotations', { lang, annotations })
} catch (e) {
console.warn(`Error loading unicode emoji annotations for ${lang}: `, e)
// ignore
}
}
}))
},

View file

@ -61,13 +61,16 @@ const editUserNote = (store, { id, comment }) => {
.then((relationship) => store.commit('updateUserRelationship', [relationship]))
}
const muteUser = (store, id) => {
const muteUser = (store, args) => {
const id = typeof args === 'object' ? args.id : args
const expiresIn = typeof args === 'object' ? args.expiresIn : 0
const predictedRelationship = store.state.relationships[id] || { id }
predictedRelationship.muting = true
store.commit('updateUserRelationship', [predictedRelationship])
store.commit('addMuteId', id)
return store.rootState.api.backendInteractor.muteUser({ id })
return store.rootState.api.backendInteractor.muteUser({ id, expiresIn })
.then((relationship) => {
store.commit('updateUserRelationship', [relationship])
store.commit('addMuteId', id)

View file

@ -1118,8 +1118,12 @@ const fetchMutes = ({ credentials }) => {
.then((users) => users.map(parseUser))
}
const muteUser = ({ id, credentials }) => {
return promisedRequest({ url: MASTODON_MUTE_USER_URL(id), credentials, method: 'POST' })
const muteUser = ({ id, expiresIn, credentials }) => {
const payload = {}
if (expiresIn) {
payload.expires_in = expiresIn
}
return promisedRequest({ url: MASTODON_MUTE_USER_URL(id), credentials, method: 'POST', payload })
}
const unmuteUser = ({ id, credentials }) => {

View file

@ -0,0 +1,8 @@
import { kebabCase } from 'lodash'
const propsToNative = props => Object.keys(props).reduce((acc, cur) => {
acc[kebabCase(cur)] = props[cur]
return acc
}, {})
export { propsToNative }

View file

@ -41,3 +41,19 @@ export const relativeTimeShort = (date, nowThreshold = 1) => {
r.key += '_short'
return r
}
export const unitToSeconds = (unit, amount) => {
switch (unit) {
case 'minutes': return 0.001 * amount * MINUTE
case 'hours': return 0.001 * amount * HOUR
case 'days': return 0.001 * amount * DAY
}
}
export const secondsToUnit = (unit, amount) => {
switch (unit) {
case 'minutes': return (1000 * amount) / MINUTE
case 'hours': return (1000 * amount) / HOUR
case 'days': return (1000 * amount) / DAY
}
}

View file

@ -125,6 +125,8 @@ export const parseUser = (data) => {
output.role = 'member'
}
output.birthday = data.pleroma.birthday
if (data.pleroma.privileges) {
output.privileges = data.pleroma.privileges
} else if (data.pleroma.is_admin) {
@ -162,6 +164,7 @@ export const parseUser = (data) => {
output.no_rich_text = data.source.pleroma.no_rich_text
output.show_role = data.source.pleroma.show_role
output.discoverable = data.source.pleroma.discoverable
output.show_birthday = data.pleroma.show_birthday
}
}

View file

@ -11,6 +11,10 @@ const specialLanguageCodes = {
const internalToBrowserLocale = code => specialLanguageCodes[code] || code
const internalToBackendLocale = code => internalToBrowserLocale(code).replace('_', '-')
const internalToBackendLocaleMulti = codes => {
const langs = Array.isArray(codes) ? codes : [codes]
return langs.map(internalToBackendLocale).join(',')
}
const getLanguageName = (code) => {
const specialLanguageNames = {
@ -28,6 +32,7 @@ const languages = _.map(languagesObject.languages, (code) => ({ code, name: getL
const localeService = {
internalToBrowserLocale,
internalToBackendLocale,
internalToBackendLocaleMulti,
languages,
getLanguageName
}

View file

@ -14,7 +14,8 @@ const generateInput = (value, padEmoji = true) => {
padEmoji
}
}
}
},
$t: (msg) => msg
},
stubs: {
FAIcon: true

748
yarn.lock

File diff suppressed because it is too large Load diff