Pull upstream
This commit is contained in:
commit
a184a562f7
65 changed files with 10542 additions and 11780 deletions
|
@ -9,6 +9,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 -->
|
||||
<script type="text/javascript" src="/instance/pleroma-mod-loader.js"></script>
|
||||
<div id="popovers" />
|
||||
|
|
|
@ -34,7 +34,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",
|
||||
|
@ -57,9 +57,9 @@
|
|||
"@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",
|
||||
|
|
|
@ -71,7 +71,6 @@
|
|||
<StatusHistoryModal v-if="editingAvailable" />
|
||||
<SettingsModal />
|
||||
<UpdateNotification />
|
||||
<div id="modal" />
|
||||
<GlobalNoticeList />
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -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 })
|
||||
|
|
|
@ -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
|
||||
})
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
37
src/components/confirm_modal/confirm_modal.js
Normal file
37
src/components/confirm_modal/confirm_modal.js
Normal 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
|
29
src/components/confirm_modal/confirm_modal.vue
Normal file
29
src/components/confirm_modal/confirm_modal.vue
Normal 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>
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,7 +127,8 @@ const EmojiInput = {
|
|||
components: {
|
||||
Popover,
|
||||
EmojiPicker,
|
||||
UnicodeDomainIndicator
|
||||
UnicodeDomainIndicator,
|
||||
ScreenReaderNotice
|
||||
},
|
||||
computed: {
|
||||
padEmoji () {
|
||||
|
@ -203,6 +206,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 +287,11 @@ const EmojiInput = {
|
|||
...rest,
|
||||
img: imageUrl || ''
|
||||
}))
|
||||
this.highlighted = -1
|
||||
this.$refs.screenReaderNotice.announce(
|
||||
this.$tc('tool_tip.autocomplete_available',
|
||||
this.suggestions.length,
|
||||
{ number: this.suggestions.length }))
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
@ -374,26 +388,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 +555,13 @@ const EmojiInput = {
|
|||
})
|
||||
},
|
||||
resize () {
|
||||
},
|
||||
autoCompleteItemLabel (suggestion) {
|
||||
if (suggestion.user) {
|
||||
return suggestion.displayText + ' ' + suggestion.detailText
|
||||
} else {
|
||||
return this.maybeLocalizedEmojiName(suggestion)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
ref="popover"
|
||||
trigger="click"
|
||||
popover-class="emoji-picker popover-default"
|
||||
:trigger-attrs="{ 'aria-hidden': true }"
|
||||
@show="onPopoverShown"
|
||||
@close="onPopoverClosed"
|
||||
>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
/>
|
||||
<RemoveFollowerButton
|
||||
v-if="noFollowsYou && relationship.followed_by"
|
||||
:user="user"
|
||||
:relationship="relationship"
|
||||
class="follow-card-button"
|
||||
/>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
})
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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'
|
||||
|
@ -633,6 +634,9 @@ const PostStatusForm = {
|
|||
},
|
||||
openProfileTab () {
|
||||
this.$store.dispatch('openSettingsModalTab', 'profile')
|
||||
},
|
||||
propsToNative (props) {
|
||||
return propsToNative(props)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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') }}
|
||||
|
|
|
@ -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') }}
|
||||
|
|
|
@ -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,6 +14,7 @@ const registration = {
|
|||
username: '',
|
||||
password: '',
|
||||
confirm: '',
|
||||
birthday: '',
|
||||
reason: '',
|
||||
language: ''
|
||||
},
|
||||
|
@ -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: {
|
||||
|
|
|
@ -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 }"
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
21
src/components/screen_reader_notice/screen_reader_notice.js
Normal file
21
src/components/screen_reader_notice/screen_reader_notice.js
Normal 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
|
21
src/components/screen_reader_notice/screen_reader_notice.vue
Normal file
21
src/components/screen_reader_notice/screen_reader_notice.vue
Normal file
|
@ -0,0 +1,21 @@
|
|||
<template>
|
||||
<div
|
||||
class="screen-reader-text"
|
||||
:aria-live="ariaLive"
|
||||
>
|
||||
{{ currentText }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./screen_reader_notice.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
.screen-reader-text {
|
||||
display: block;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
visibility: visible;
|
||||
}
|
||||
</style>
|
|
@ -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
|
||||
|
|
|
@ -13,6 +13,7 @@ export default {
|
|||
'modelValue',
|
||||
'disabled',
|
||||
'unstyled',
|
||||
'kind'
|
||||
'kind',
|
||||
'attrs'
|
||||
]
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
<select
|
||||
:disabled="disabled"
|
||||
:value="modelValue"
|
||||
v-bind="attrs"
|
||||
@change="$emit('update:modelValue', $event.target.value)"
|
||||
>
|
||||
<slot />
|
||||
|
|
|
@ -153,6 +153,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">
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -129,4 +129,9 @@
|
|||
padding: 0 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.birthday-input {
|
||||
display: block;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -357,3 +357,8 @@
|
|||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.mute-expiry {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
|
|
@ -321,6 +321,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>
|
||||
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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": "Garbage Inserted",
|
||||
"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",
|
||||
|
@ -515,6 +540,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",
|
||||
|
@ -852,6 +881,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})",
|
||||
|
@ -861,6 +894,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:",
|
||||
|
@ -907,11 +943,23 @@
|
|||
},
|
||||
"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",
|
||||
"blocks_you": "Blocks you!",
|
||||
"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",
|
||||
|
@ -919,6 +967,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!",
|
||||
|
@ -930,9 +982,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",
|
||||
|
@ -946,6 +1007,7 @@
|
|||
"hide_repeats": "Hide repeats",
|
||||
"show_repeats": "Show repeats",
|
||||
"bot": "Bot",
|
||||
"birthday": "Born {birthday}",
|
||||
"admin_menu": {
|
||||
"moderation": "Moderation",
|
||||
"grant_admin": "Grant Admin",
|
||||
|
@ -1007,7 +1069,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": {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -82,6 +82,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,
|
||||
|
@ -188,7 +197,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)
|
||||
|
|
|
@ -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',
|
||||
|
@ -107,6 +116,8 @@ const defaultState = {
|
|||
restrictedNicknames: [],
|
||||
safeDM: true,
|
||||
knownDomains: [],
|
||||
birthdayRequired: false,
|
||||
birthdayMinAge: 0,
|
||||
|
||||
// Feature-set, apparently, not everything here is reported...
|
||||
shoutAvailable: false,
|
||||
|
@ -286,8 +297,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
|
||||
}
|
||||
}
|
||||
}))
|
||||
},
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -1122,8 +1122,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 }) => {
|
||||
|
|
|
@ -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 }
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -14,7 +14,8 @@ const generateInput = (value, padEmoji = true) => {
|
|||
padEmoji
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
$t: (msg) => msg
|
||||
},
|
||||
stubs: {
|
||||
FAIcon: true
|
||||
|
|
Loading…
Reference in a new issue