Compare commits

...

97 Commits

Author SHA1 Message Date
FloatingGhost 44c1f43bd8
allow translating to any other language
continuous-integration/drone/push Build is passing Details
2023-01-30 17:57:56 +01:00
FloatingGhost 5ee4f76de3
use supported languages from service 2023-01-30 17:54:13 +01:00
FloatingGhost 11f24727c7
allow modal to expand with content 2023-01-30 17:50:12 +01:00
FloatingGhost 4126a1d95e
allow selecting languages for translation 2023-01-30 17:49:30 +01:00
Sam Therapy 089b80f00f
Lint
Signed-off-by: Sam Therapy <sam@samtherapy.net>
2023-01-30 17:41:50 +01:00
FloatingGhost 91703a2eee
add translation options 2023-01-30 17:21:24 +01:00
Sam Therapy c48c23f406
Some API v1 garbage that hopefully does not break regular Pleroma
Signed-off-by: Sam Therapy <sam@samtherapy.net>
2023-01-30 17:13:09 +01:00
eris 73362ef2be
Make all new nav options expert/advanced options 2023-01-30 16:56:47 +01:00
eris d30d684602
Better padding for logout button 2023-01-30 16:55:38 +01:00
eris 06b306473b
Move logout button to settings modal 2023-01-30 16:55:38 +01:00
eris 058b6b6182
Make nav panel icon margin optional 2023-01-30 16:52:52 +01:00
eris 20064ee6f7
Make new top nav links optional 2023-01-30 16:51:21 +01:00
eris 78382a27ea
Add user options to hide instance favicon and name 2023-01-30 16:41:40 +01:00
eris 79e3dc099b
Update nav icon spacing to prevent misclicks 2023-01-30 16:35:56 +01:00
eris 28c6412f03
Redo desktop nav and change bell to bolt for interactions 2023-01-30 16:35:54 +01:00
FloatingGhost c452695a01
show local bubble instances in about 2023-01-30 16:19:37 +01:00
FloatingGhost 0613a4d285
add bubble timeline
Signed-off-by: Sam Therapy <sam@samtherapy.net>
2023-01-30 16:04:37 +01:00
Sam Therapy 430fc8607a
Lint
Signed-off-by: Sam Therapy <sam@samtherapy.net>
2023-01-30 14:59:49 +01:00
eris 7bb303726f
Don't render inline quote policy links 2023-01-30 14:59:20 +01:00
Sol Fisher Romanoff 2936e2efee
Align quote timestamp to right 2023-01-30 14:58:30 +01:00
Sol Fisher Romanoff 0468aad584
Merge disabled boost and quote buttons on private posts 2023-01-30 14:58:15 +01:00
Sam Therapy a184a562f7
Pull upstream 2023-01-30 14:56:55 +01:00
Sol Fisher Romanoff 0888b9462f
Change quoted status link to router-link 2023-01-30 14:55:24 +01:00
Sol Fisher Romanoff c372d05b25
i18n: add quote tooltip 2023-01-30 14:54:42 +01:00
FloatingGhost f3712e1152
fix scss lint 2023-01-30 14:49:41 +01:00
FloatingGhost d2f20b3323
lint fix 2023-01-30 14:49:41 +01:00
FloatingGhost b4846be5fc
fix emoji sizing in quotes 2023-01-30 14:49:41 +01:00
FloatingGhost 473faa6b57
don't show quotes on compact display 2023-01-30 14:43:20 +01:00
FloatingGhost 1ac46ce7ac
display quotes first 2023-01-30 14:42:51 +01:00
FloatingGhost ce479c5e12
link to quote on click 2023-01-30 14:42:51 +01:00
FloatingGhost 169dec5048
add quote box 2023-01-30 14:42:51 +01:00
FloatingGhost a2212b4396
add quote form 2023-01-30 14:42:48 +01:00
tusooa 04f5a448e4 Merge branch 'renovate/vue-test-utils-2.x' into 'develop'
Update dependency @vue/test-utils to v2.2.8

See merge request pleroma/pleroma-fe!1781
2023-01-29 13:58:26 +00:00
Pleroma Renovate Bot d2716341cb Update dependency @vue/test-utils to v2.2.8 2023-01-29 09:07:16 +00:00
HJ f229c4a106 Merge branch 'from/develop/tusooa/autocomplete-accessibility' into 'develop'
Autocomplete accessibility

Closes #1219

See merge request pleroma/pleroma-fe!1771
2023-01-28 23:04:59 +00:00
HJ af22092472 Merge branch 'tusooa/anon-xact-acc' into 'develop'
Make interact buttons accessible for anonymous users

See merge request pleroma/pleroma-fe!1773
2023-01-28 23:00:07 +00:00
HJ a08378253f Merge branch 'tusooa/topbar-alttext' into 'develop'
Accessibility fixes for panel headers/top bar

See merge request pleroma/pleroma-fe!1772
2023-01-28 22:59:40 +00:00
tusooa 2635e24679 Merge branch 'renovate/babel-loader-9.x' into 'develop'
Update dependency babel-loader to v9.1.2

See merge request pleroma/pleroma-fe!1745
2023-01-28 15:01:26 +00:00
tusooa 93e01aefad Merge branch 'renovate/punycode.js-2.x' into 'develop'
Update dependency punycode.js to v2.3.0

See merge request pleroma/pleroma-fe!1775
2023-01-28 14:57:10 +00:00
Sam Therapy 63d8c83e3d
fuck off install-state
Signed-off-by: Sam Therapy <sam@samtherapy.net>
2023-01-27 19:14:08 +01:00
Pleroma Renovate Bot ec0b239496 Update dependency punycode.js to v2.3.0 2023-01-27 09:09:34 +00:00
Pleroma Renovate Bot 31c9b6885b Update dependency babel-loader to v9.1.2 2023-01-27 09:08:38 +00:00
tusooa bb1c60fa93 Merge branch 'tusooa/fix-stylelint' into 'develop'
Fix stylelint

See merge request pleroma/pleroma-fe!1777
2023-01-26 00:13:06 +00:00
tusooa 817432bd62
Fix stylelint 2023-01-25 19:01:29 -05:00
HJ 22c3012e1c Merge branch 'birthdays' into 'develop'
Birthdays

See merge request pleroma/pleroma-fe!1432
2023-01-25 23:50:54 +00:00
HJ 65e10f07de Merge branch 'from/develop/tusooa/confirm-dialogs' into 'develop'
Confirmation dialogs

See merge request pleroma/pleroma-fe!1431
2023-01-25 23:49:16 +00:00
HJ a9716701be Merge branch 'from/develop/tusooa/multi-iface-lang' into 'develop'
Multiple interface languages support

See merge request pleroma/pleroma-fe!1568
2023-01-25 23:47:29 +00:00
tusooa 03d5c2e140
Make date picker aware of the birthday max value 2023-01-22 11:22:24 -05:00
tusooa dd97a23ce7
Add birthday to registration form 2023-01-22 11:15:52 -05:00
tusooa b1e75c25bd
Merge remote-tracking branch 'upstream/develop' into birthdays 2023-01-22 09:34:01 -05:00
tusooa c7c68340f1
Fix mobile nav stylelint 2023-01-22 09:25:55 -05:00
Tusooa Zhu ce8101e60a
Add remove follower confirmation 2023-01-22 09:25:24 -05:00
tusooa 5f12c3ae76
Fix unit tests 2023-01-21 22:42:53 -05:00
tusooa d159031121
Add some aria to post status form 2023-01-21 16:28:33 -05:00
Tusooa Zhu 68110ce825
Make interact buttons accessible for anonymous users 2023-01-21 15:41:12 -05:00
tusooa 1ab958ba6b
Make quick settings menus more accessible 2023-01-21 15:30:23 -05:00
tusooa 5243632678
Label buttons in top bar 2023-01-21 15:08:17 -05:00
tusooa 5478192e20
Make keys work as intended when there is no suggestions 2023-01-21 14:50:57 -05:00
tusooa 246593970b
Make autocomplete prompt more user-friendly 2023-01-21 01:30:44 -05:00
tusooa 72cb9e8bdb
Make all emoji inputs screen-reader-friendly 2023-01-21 01:28:43 -05:00
tusooa 6235af4592
Make screenreaders read out autocomplete results 2023-01-21 01:07:07 -05:00
tusooa 4db7f07421
Make autocomplete items buttons 2023-01-20 23:43:09 -05:00
Tusooa Zhu 5359633c74
Fix timed mute lint 2023-01-20 23:40:12 -05:00
Tusooa Zhu 8a99d129dc
Fix confirm modal lint 2023-01-20 23:40:12 -05:00
Tusooa Zhu 1856eeda40
Auto close confirm dialog after approve/deny 2023-01-20 23:40:12 -05:00
Tusooa Zhu 041bbb1622
Add English translation for accept & deny follow requests 2023-01-20 23:40:12 -05:00
Tusooa Zhu 547e85c7c6
Add confirm dialogs for accept & deny follow requests 2023-01-20 23:40:12 -05:00
Tusooa Zhu b7af37fce8
Add English translation for mute duration 2023-01-20 23:40:12 -05:00
Tusooa Zhu 228a9afdf5
Add timed-mute functionality 2023-01-20 23:40:11 -05:00
Tusooa Zhu 95c15fca22
Use correct html syntax for modal <div> 2023-01-20 23:40:11 -05:00
Tusooa Zhu ec957d4162
Make confirm dialogs work with vue-i18n 9 2023-01-20 23:40:11 -05:00
Tusooa Zhu 91c4a57fe5
Make page unscrollable when confirm modal is shown
Or we could scroll until the component is ... hidden
due to virtual-scrolling, and the modal disappears!
2023-01-20 23:40:11 -05:00
Tusooa Zhu 0b914d7815
Make modal display over the top bar 2023-01-20 23:40:09 -05:00
Tusooa Zhu 76d99c08d6
Move modal out of vue tree 2023-01-20 23:39:45 -05:00
Tusooa Zhu dc04c8cbd4
Use vue3 teleport instead of portal 2023-01-20 23:39:45 -05:00
Tusooa Zhu 39e4746f61
Use portal for modals 2023-01-20 23:39:42 -05:00
Tusooa Zhu 51ade26066
Fix logout confirm dialog title 2023-01-20 23:39:09 -05:00
Tusooa Zhu 8c8a8232c8
Lint 2023-01-20 23:39:09 -05:00
Tusooa Zhu 5c048321e7
Add English translation for logout confirmation 2023-01-20 23:39:09 -05:00
Tusooa Zhu c202c89ca0
Add confirmation for logout 2023-01-20 23:39:08 -05:00
Tusooa Zhu 0bfe100ef7
Add English translation for block confirmation 2023-01-20 23:39:08 -05:00
Tusooa Zhu 3b7aaae2b3
Add confirmation for blocking 2023-01-20 23:39:08 -05:00
Tusooa Zhu c032b48219
Add English translation for mute confirmation 2023-01-20 23:39:08 -05:00
Tusooa Zhu e7e35ead09
Add confirmation for muting 2023-01-20 23:39:08 -05:00
Tusooa Zhu a0c6d642af
Add English translations for repeat and unfollow confirmation 2023-01-20 23:39:08 -05:00
Tusooa Zhu 0684f19d1b
Add ConfirmModal comp 2023-01-20 23:39:08 -05:00
Tusooa Zhu a0b886459b
Add confirmation for following 2023-01-20 23:39:07 -05:00
Tusooa Zhu 4d175235f1
Add confirmation for repeating 2023-01-20 23:39:07 -05:00
Tusooa Zhu f8b522e36d
Add English translations for setting entries 2023-01-20 23:39:07 -05:00
Tusooa Zhu 1ff2948aeb
Add setting entries for whether to show confirmation dialogs 2023-01-20 23:39:07 -05:00
Tusooa Zhu 1e352fbfac
Add English translations for delete status confirm modal 2023-01-20 23:39:07 -05:00
Tusooa Zhu 3ad5df805e
Add delete status confirm modal 2023-01-20 23:39:01 -05:00
tusooa 7e2ae2ba95
Optimize UI 2022-12-23 23:02:21 -05:00
Tusooa Zhu 52eef2eed1
Add English translations for multiple interface languages 2022-12-23 22:46:17 -05:00
Tusooa Zhu b7e9373965
Add support for multiple interface languages 2022-12-23 22:45:55 -05:00
marcin mikołajczak 6649baaac9 Merge remote-tracking branch 'pleroma/develop' into birthdays
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2022-08-05 11:53:44 +02:00
marcin mikołajczak 79d02bddbe Birthdays
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
2022-02-13 19:11:14 +01:00
104 changed files with 4841 additions and 1205 deletions

Binary file not shown.

View File

@ -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" />

View File

@ -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",

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 })
@ -256,6 +258,7 @@ const getNodeInfo = async ({ store }) => {
store.dispatch('setInstanceOption', { name: 'editingAvailable', value: features.includes('editing') })
store.dispatch('setInstanceOption', { name: 'pollLimits', value: metadata.pollLimits })
store.dispatch('setInstanceOption', { name: 'mailerEnabled', value: metadata.mailerEnabled })
store.dispatch('setInstanceOption', { name: 'translationEnabled', value: features.includes('akkoma:machine_translation') })
const uploadLimits = metadata.uploadLimits
store.dispatch('setInstanceOption', { name: 'uploadlimit', value: parseInt(uploadLimits.general) })
@ -291,6 +294,7 @@ const getNodeInfo = async ({ store }) => {
})
store.dispatch('setInstanceOption', { name: 'federationPolicy', value: federation })
store.dispatch('setInstanceOption', { name: 'localBubbleInstances', value: metadata.localBubbleInstances })
store.dispatch('setInstanceOption', {
name: 'federating',
value: typeof federation.enabled === 'undefined'
@ -379,6 +383,7 @@ const afterStoreSetup = async ({ store, i18n }) => {
store.dispatch('startFetchingAnnouncements')
getTOS({ store })
getStickers({ store })
store.dispatch('getSupportedTranslationlanguages')
const router = createRouter({
history: createWebHistory(),

View File

@ -2,6 +2,7 @@ import PublicTimeline from 'components/public_timeline/public_timeline.vue'
import PublicAndExternalTimeline from 'components/public_and_external_timeline/public_and_external_timeline.vue'
import FriendsTimeline from 'components/friends_timeline/friends_timeline.vue'
import TagTimeline from 'components/tag_timeline/tag_timeline.vue'
import BubbleTimeline from 'components/bubble_timeline/bubble_timeline.vue'
import BookmarkTimeline from 'components/bookmark_timeline/bookmark_timeline.vue'
import ConversationPage from 'components/conversation-page/conversation-page.vue'
import Interactions from 'components/interactions/interactions.vue'
@ -47,6 +48,7 @@ export default (store) => {
},
{ name: 'public-external-timeline', path: '/main/all', component: PublicAndExternalTimeline },
{ name: 'public-timeline', path: '/main/public', component: PublicTimeline },
{ name: 'bubble-timeline', path: '/main/bubble', component: BubbleTimeline },
{ name: 'friends', path: '/main/friends', component: FriendsTimeline, beforeEnter: validateAuthenticatedRoute },
{ name: 'tag-timeline', path: '/tag/:tag', component: TagTimeline },
{ name: 'bookmarks', path: '/bookmarks', component: BookmarkTimeline },

View File

@ -3,6 +3,7 @@ import FeaturesPanel from '../features_panel/features_panel.vue'
import TermsOfServicePanel from '../terms_of_service_panel/terms_of_service_panel.vue'
import StaffPanel from '../staff_panel/staff_panel.vue'
import MRFTransparencyPanel from '../mrf_transparency_panel/mrf_transparency_panel.vue'
import LocalBubblePanel from '../local_bubble_panel/local_bubble_panel.vue'
const About = {
components: {
@ -10,7 +11,8 @@ const About = {
FeaturesPanel,
TermsOfServicePanel,
StaffPanel,
MRFTransparencyPanel
MRFTransparencyPanel,
LocalBubblePanel
},
computed: {
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },

View File

@ -3,6 +3,7 @@
<instance-specific-panel v-if="showInstanceSpecificPanel" />
<staff-panel />
<terms-of-service-panel />
<LocalBubblePanel />
<MRFTransparencyPanel />
<features-panel v-if="showFeaturesPanel" />
</div>

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

@ -0,0 +1,18 @@
import Timeline from '../timeline/timeline.vue'
const PublicTimeline = {
components: {
Timeline
},
computed: {
timeline () { return this.$store.state.statuses.timelines.bubble }
},
created () {
this.$store.dispatch('startFetchingTimeline', { timeline: 'bubble' })
},
unmounted () {
this.$store.dispatch('stopFetchingTimeline', 'bubble')
}
}
export default PublicTimeline

View File

@ -0,0 +1,9 @@
<template>
<Timeline
:title="$t('nav.bubble_timeline')"
:timeline="timeline"
:timeline-name="'bubble'"
/>
</template>
<script src="./bubble_timeline.js"></script>

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,16 +1,21 @@
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,
faSignOutAlt,
faHome,
faComments,
faBell,
faUserPlus,
faBullhorn,
faSearch,
faTachometerAlt,
faCog,
faGlobe,
faBolt,
faUsers,
faCommentMedical,
faBookmark,
faInfoCircle
} from '@fortawesome/free-solid-svg-icons'
@ -19,18 +24,23 @@ library.add(
faSignOutAlt,
faHome,
faComments,
faBell,
faUserPlus,
faBullhorn,
faSearch,
faTachometerAlt,
faCog,
faGlobe,
faBolt,
faUsers,
faCommentMedical,
faBookmark,
faInfoCircle
)
export default {
components: {
SearchBar
SearchBar,
ConfirmModal
},
data: () => ({
searchBarHidden: true,
@ -40,7 +50,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 },
@ -69,19 +80,51 @@ export default {
})
},
logo () { return this.$store.state.instance.logo },
mergedConfig () {
return this.$store.getters.mergedConfig
},
sitename () { return this.$store.state.instance.name },
showNavShortcuts () {
return this.mergedConfig.showNavShortcuts
},
showWiderShortcuts () {
return this.mergedConfig.showWiderShortcuts
},
hideSiteFavicon () {
return this.mergedConfig.hideSiteFavicon
},
hideSiteName () {
return this.mergedConfig.hideSiteName
},
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

@ -16,11 +16,11 @@
display: grid;
grid-template-rows: var(--navbar-height);
grid-template-columns: 2fr auto 2fr;
grid-template-areas: "sitename logo actions";
grid-template-areas: "nav-left logo actions";
box-sizing: border-box;
padding: 0 1.2em;
margin: auto;
max-width: 980px;
max-width: 1110px;
}
&.-column-stretch .inner-nav {
@ -38,7 +38,7 @@
&.-logoLeft .inner-nav {
grid-template-columns: auto 2fr 2fr;
grid-template-areas: "logo sitename actions";
grid-template-areas: "logo nav-left actions";
}
&.-column-stretch.-wide .inner-nav {
@ -109,20 +109,54 @@
}
}
.svg-inline--fa {
color: $fallback--link;
color: var(--topBarLink, $fallback--link);
}
.nav-icon {
margin-left: 0.2em;
width: 2em;
height: 100%;
text-align: center;
.svg-inline--fa {
color: $fallback--link;
color: var(--topBarLink, $fallback--link);
&.router-link-active {
font-size: 1.2em;
margin-top: 0.05em;
.svg-inline--fa {
font-weight: bolder;
color: $fallback--text;
color: var(--selectedMenuText, $fallback--text);
--lightText: var(--selectedMenuLightText, $fallback--lightText);
}
}
&-logout {
margin-left: 2em;
}
}
.sitename {
grid-area: sitename;
.-wide {
.nav-icon:not(.nav-icon-logout) {
margin-left: 0.7em;
}
}
.left {
padding-left: 5px;
display: flex;
}
.nav-left-wrapper {
grid-area: nav-left;
.favicon {
height: 28px;
vertical-align: middle;
padding-right: 5px;
}
}
.actions {

View File

@ -5,21 +5,85 @@
:class="{ '-logoLeft': logoLeft }"
@click="scrollToTop()"
>
<div class="inner-nav">
<div class="item sitename">
<div
class="inner-nav"
:class="{ '-wide': showWiderShortcuts }"
>
<div class="item nav-left-wrapper">
<router-link
v-if="!hideSitename"
class="site-name"
class="site-brand"
:to="{ name: 'root' }"
active-class="home"
>
{{ sitename }}
<img
v-if="!hideSiteFavicon"
class="favicon"
src="/favicon.png"
>
<span
v-if="!hideSiteName"
class="site-name"
>
{{ sitename }}
</span>
</router-link>
<div
v-if="(currentUser || !privateMode) && showNavShortcuts"
class="nav-items left"
>
<router-link
v-if="currentUser"
:to="{ name: 'friends' }"
class="nav-icon"
>
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding"
icon="home"
:title="$t('nav.home_timeline')"
/>
</router-link>
<router-link
:to="{ name: 'public-timeline' }"
class="nav-icon"
>
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding"
icon="users"
:title="$t('nav.public_tl')"
/>
</router-link>
<router-link
:to="{ name: 'public-external-timeline' }"
class="nav-icon"
>
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding"
icon="globe"
:title="$t('nav.twkn')"
/>
</router-link>
<router-link
v-if="currentUser"
:to="{ name: 'bubble-timeline' }"
class="nav-icon"
>
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding"
icon="circle"
:title="$t('nav.bubble_timeline')"
/>
</router-link>
</div>
</div>
<router-link
class="logo"
:to="{ name: 'root' }"
:style="logoBgStyle"
:title="sitename"
>
<div
class="mask"
@ -36,15 +100,55 @@
@toggled="onSearchBarToggled"
@click.stop
/>
<div
v-if="(currentUser || !privateMode) && showNavShortcuts"
class="nav-items right"
>
<router-link
class="nav-icon"
:to="{ name: 'interactions', params: { username: currentUser.screen_name } }"
>
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding"
icon="bolt"
:title="$t('nav.interactions')"
/>
</router-link>
<router-link
v-if="currentUser"
:to="{ name: 'lists' }"
class="nav-icon"
>
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding"
icon="list"
:title="$t('nav.lists')"
/>
</router-link>
<router-link
v-if="currentUser"
:to="{ name: 'bookmarks' }"
class="nav-icon"
>
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding"
icon="bookmark"
:title="$t('nav.bookmarks')"
/>
</router-link>
</div>
<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 +156,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,9 +51,10 @@
margin: 15vh auto;
position: fixed;
transform: translateX(-50%);
z-index: 999;
z-index: 2001;
cursor: default;
display: block;
width: max-content;
background-color: $fallback--bg;
background-color: var(--bg, $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,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)
}
}
}
}

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

@ -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,29 @@ 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
},
translateStatus () {
const translateTo = this.$store.getters.mergedConfig.translationLanguage || this.$store.state.instance.interfaceLanguage
this.$store.dispatch('translateStatus', { id: this.status.id, language: translateTo })
.then(() => this.$emit('onSuccess'))
.catch(err => this.$emit('onError', err.error.error))
},
pinStatus () {
this.$store.dispatch('pinStatus', this.status.id)
.then(() => this.$emit('onSuccess'))
@ -124,6 +147,9 @@ const ExtraButtons = {
canMute () {
return !!this.currentUser
},
canTranslate () {
return this.$store.state.instance.translationEnabled === true
},
canBookmark () {
return !!this.currentUser
},
@ -133,7 +159,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

@ -142,6 +142,17 @@
:icon="['far', 'flag']"
/><span>{{ $t("user_card.report") }}</span>
</button>
<button
v-if="canTranslate"
class="button-default dropdown-item dropdown-item-icon"
@click.prevent="translateStatus"
@click="close"
>
<FAIcon
fixed-width
icon="globe"
/><span>{{ $t("status.translate") }}</span>
</button>
</div>
</template>
<template #trigger>
@ -165,6 +176,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

@ -1,21 +1,51 @@
<template>
<div>
<FAIcon
v-if="globeIcon"
icon="globe"
/>
{{ ' ' }}
<label for="interface-language-switcher">
{{ 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,12 +64,16 @@ export default {
required: true
},
language: {
type: String,
type: [Array, String],
required: true
},
setLanguage: {
type: Function,
required: true
},
globeIcon: {
type: Boolean,
default: true
}
},
computed: {
@ -48,7 +82,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 +94,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

@ -0,0 +1,12 @@
import { mapState } from 'vuex'
import { get } from 'lodash'
const LocalBubblePanel = {
computed: {
...mapState({
bubbleInstances: state => get(state, 'instance.localBubbleInstances')
})
}
}
export default LocalBubblePanel

View File

@ -0,0 +1,23 @@
.mrf-section {
margin: 1em;
table {
width: 100%;
text-align: left;
padding-left: 10px;
padding-bottom: 20px;
th,
td {
width: 180px;
max-width: 360px;
overflow: hidden;
vertical-align: text-top;
}
th + th,
td + td {
width: auto;
}
}
}

View File

@ -0,0 +1,31 @@
<template>
<div
v-if="bubbleInstances"
class="bubble-panel"
>
<div class="panel panel-default base01-background">
<div class="panel-heading timeline-heading base02-background">
<div class="title">
{{ $t("about.bubble_instances") }}
</div>
</div>
<div class="panel-body">
<p>{{ $t("about.bubble_instances_description") }}:</p>
<ul>
<li
v-for="instance in bubbleInstances"
:key="instance"
v-text="instance"
/>
</ul>
</div>
</div>
</div>
</template>
<script src="./local_bubble_panel.js"></script>
<style lang="scss">
@import '../../_variables.scss';
@import './local_bubble_panel.scss';
</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(
@ -49,7 +52,12 @@ const MobileNav = {
unseenNotificationsCount () {
return this.unseenNotifications.length
},
hideSitename () { return this.$store.state.instance.hideSitename },
mergedConfig () {
return this.$store.getters.mergedConfig
},
hideSiteName () {
return this.mergedConfig.hideSiteName
},
sitename () { return this.$store.state.instance.name },
isChat () {
return this.$route.name === 'chat'
@ -57,7 +65,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 +100,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

@ -23,8 +23,16 @@
class="alert-dot"
/>
</button>
<NavigationPins class="pins" />
</div> <div class="item right">
<router-link
v-if="!hideSiteName"
class="site-name"
:to="{ name: 'root' }"
active-class="home"
>
{{ sitename }}
</router-link>
</div>
<div class="item right">
<button
v-if="currentUser"
class="button-unstyled mobile-nav-button"
@ -88,6 +96,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 +255,16 @@
}
}
}
.confirm-modal.dark-overlay {
&::before {
z-index: 3000;
}
.dialog-modal.panel {
z-index: 3001;
}
}
}
</style>

View File

@ -15,11 +15,12 @@ import {
faChevronDown,
faChevronUp,
faComments,
faBell,
faBolt,
faInfoCircle,
faStream,
faList,
faBullhorn
faBullhorn,
faCircle
} from '@fortawesome/free-solid-svg-icons'
library.add(
@ -30,11 +31,12 @@ library.add(
faChevronDown,
faChevronUp,
faComments,
faBell,
faBolt,
faInfoCircle,
faStream,
faList,
faBullhorn
faBullhorn,
faCircle
)
const NavPanel = {
props: ['forceExpand', 'forceEditMode'],

View File

@ -22,6 +22,13 @@ export const TIMELINES = {
label: 'nav.public_tl',
criteria: ['!private']
},
bubble: {
route: 'bubble-timeline',
anon: true,
icon: 'circle',
label: 'nav.bubble_timeline',
criteria: ['!private', 'federating']
},
twkn: {
route: 'public-external-timeline',
anon: true,

View File

@ -58,6 +58,7 @@ const NavPanel = {
if (!this.currentUser) {
return filterNavigation([
{ ...TIMELINES.public, name: 'public' },
{ ...TIMELINES.bubble, name: 'bubble' },
{ ...TIMELINES.twkn, name: 'twkn' },
{ ...ROOT_ITEMS.about, name: 'about' }
],

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

@ -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'
@ -64,6 +65,7 @@ const PostStatusForm = {
'statusScope',
'statusContentType',
'replyTo',
'quoteId',
'repliedUser',
'attentions',
'copyMessageScope',
@ -108,12 +110,12 @@ const PostStatusForm = {
this.updateIdempotencyKey()
this.resize(this.$refs.textarea)
if (this.replyTo) {
if (this.replyTo || this.quoteId) {
const textLength = this.$refs.textarea.value.length
this.$refs.textarea.setSelectionRange(textLength, textLength)
}
if (this.replyTo || this.autoFocus) {
if (this.replyTo || this.quoteId || this.autoFocus) {
this.$refs.textarea.focus()
}
},
@ -123,7 +125,7 @@ const PostStatusForm = {
const { scopeCopy } = this.$store.getters.mergedConfig
if (this.replyTo) {
if (this.replyTo || this.quoteId) {
const currentUser = this.$store.state.users.currentUser
statusText = buildMentionsString({ user: this.repliedUser, attentions: this.attentions }, currentUser)
}
@ -347,6 +349,7 @@ const PostStatusForm = {
media: newStatus.files,
store: this.$store,
inReplyToStatusId: this.replyTo,
quoteId: this.quoteId,
contentType: newStatus.contentType,
poll,
idempotencyKey: this.idempotencyKey
@ -380,6 +383,7 @@ const PostStatusForm = {
media: [],
store: this.$store,
inReplyToStatusId: this.replyTo,
quoteId: this.quoteId,
contentType: newStatus.contentType,
poll: {},
preview: true
@ -630,6 +634,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"

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

@ -0,0 +1,16 @@
import { library } from '@fortawesome/fontawesome-svg-core'
import { faQuoteLeft } from '@fortawesome/free-solid-svg-icons'
library.add(faQuoteLeft)
const QuoteButton = {
name: 'QuoteButton',
props: ['status', 'quoting', 'visibility'],
computed: {
loggedIn () {
return !!this.$store.state.users.currentUser
}
}
}
export default QuoteButton

View File

@ -0,0 +1,47 @@
<template>
<div
v-if="(visibility === 'public' || visibility === 'unlisted') && loggedIn"
class="QuoteButton"
>
<button
class="button-unstyled interactive"
:class="{'-active': quoting}"
:title="$t('tool_tip.quote')"
@click.prevent="$emit('toggle')"
>
<FAIcon
class="fa-scale-110 fa-old-padding"
icon="quote-left"
/>
</button>
</div>
</template>
<script src="./quote_button.js"></script>
<style lang="scss">
@import '../../_variables.scss';
.QuoteButton {
display: flex;
> :first-child {
padding: 10px;
margin: -10px -8px -10px -10px;
}
.action-counter {
pointer-events: none;
user-select: none;
}
.interactive {
&:hover .svg-inline--fa,
&.-active .svg-inline--fa {
color: $fallback--cBlue;
color: var(--cBlue, $fallback--cBlue);
}
}
}
</style>

View File

@ -0,0 +1,24 @@
import { mapGetters } from 'vuex'
import QuoteCardContent from '../quote_card_content/quote_card_content.vue'
const QuoteCard = {
name: 'QuoteCard',
props: [
'status'
],
data () {
return {
imageLoaded: false
}
},
computed: {
...mapGetters([
'mergedConfig'
])
},
components: {
QuoteCardContent
}
}
export default QuoteCard

View File

@ -0,0 +1,74 @@
<template>
<div>
<router-link
class="quote-card"
:to="{ name: 'conversation', params: { id: status.id } }"
>
<QuoteCardContent
:status="status"
/>
</router-link>
</div>
</template>
<script src="./quote_card"></script>
<style lang="scss">
@import '../../_variables.scss';
.quote-card {
display: flex;
flex-direction: column;
cursor: pointer;
overflow: hidden;
margin-top: 0.5em;
.card-image {
flex-shrink: 0;
width: 120px;
max-width: 25%;
img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: $fallback--attachmentRadius;
border-radius: var(--attachmentRadius, $fallback--attachmentRadius);
}
}
.card-content {
max-height: 100%;
margin: 0.5em;
display: flex;
flex-direction: column;
}
.card-host {
font-size: 0.85em;
}
.card-description {
margin: 0.5em 0 0 0;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-word;
line-height: 1.2em;
// cap description at 3 lines, the 1px is to clean up some stray pixels
// TODO: fancier fade-out at the bottom to show off that it's too long?
max-height: calc(1.2em * 3 - 1px);
}
.nsfw-alert {
margin: 2em 0;
}
color: $fallback--text;
color: var(--text, $fallback--text);
border-style: solid;
border-width: 1px;
border-radius: $fallback--attachmentRadius;
border-radius: var(--attachmentRadius, $fallback--attachmentRadius);
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
}
</style>

View File

@ -0,0 +1,22 @@
<template>
<Status
v-if="status"
:is-preview="true"
:statusoid="status"
:compact="true"
/>
</template>
<script>
import { defineAsyncComponent } from 'vue'
export default {
name: 'QuoteCardContent',
components: {
Status: defineAsyncComponent(() => import('../status/status.vue'))
},
props: [
'status'
]
}
</script>

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,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: {

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 }"

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,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>

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

@ -14,6 +14,7 @@ import {
faTimes,
faFileUpload,
faFileDownload,
faSignOutAlt,
faChevronDown
} from '@fortawesome/free-solid-svg-icons'
import {
@ -28,6 +29,7 @@ library.add(
faWindowMinimize,
faFileUpload,
faFileDownload,
faSignOutAlt,
faChevronDown
)
@ -66,6 +68,11 @@ const SettingsModal = {
closeModal () {
this.$store.dispatch('closeSettingsModal')
},
logout () {
this.$router.replace('/main/public')
this.$store.dispatch('closeSettingsModal')
this.$store.dispatch('logout')
},
peekModal () {
this.$store.dispatch('togglePeekSettingsModal')
},
@ -150,6 +157,7 @@ const SettingsModal = {
}
},
computed: {
currentUser () { return this.$store.state.users.currentUser },
currentSaveStateNotice () {
return this.$store.state.interface.settings.currentSaveStateNotice
},

View File

@ -76,5 +76,11 @@
transform: translateY(calc(100% - 50px));
}
}
.logout-button {
position: absolute;
right: 20px;
padding-right: 10px;
}
}
}

View File

@ -111,6 +111,20 @@
id="unscrolled-content"
class="extra-content"
/>
<button
v-if="currentUser"
class="button-default logout-button"
:title="$t('login.logout')"
:aria-label="$t('login.logout')"
@click.prevent="logout"
>
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding"
icon="sign-out-alt"
/>
<span>{{ $t('login.logout') }}</span>
</button>
</div>
</div>
</Modal>

View File

@ -56,6 +56,7 @@ const GeneralTab = {
Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'webkitAudioDecodedByteCount') ||
// Future spec, still not supported in Nightly 63 as of 08/2018
Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'audioTracks')
}
},
components: {
@ -104,11 +105,23 @@ const GeneralTab = {
this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
}
},
translationLanguages () {
return (this.$store.getters.mergedConfig.supportedTranslationLanguages || []).map(lang => ({ key: lang.code, value: lang.code, label: lang.name }))
},
translationLanguage: {
get: function () { return this.$store.getters.mergedConfig.translationLanguage },
set: function (val) {
this.$store.dispatch('setOption', { name: 'translationLanguage', value: val })
}
},
...SharedComputedObject()
},
methods: {
changeDefaultScope (value) {
this.$store.dispatch('setServerSideOption', { name: 'defaultScope', value })
},
setTranslationLanguage (value) {
this.$store.dispatch('setOption', { name: 'translationLanguage', value })
}
}
}

View File

@ -25,6 +25,38 @@
<div class="setting-item">
<h2>{{ $t('nav.timeline') }}</h2>
<ul class="setting-list">
<li>
<BooleanSetting
path="hideSiteFavicon"
expert="1"
>
{{ $t('settings.hide_site_favicon') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="hideSiteName"
expert="1"
>
{{ $t('settings.hide_site_name') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="showNavShortcuts"
expert="1"
>
{{ $t('settings.show_nav_shortcuts') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="showWiderShortcuts"
expert="1"
>
{{ $t('settings.show_wider_shortcuts') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="stopGifs">
{{ $t('settings.stop_gifs') }}
@ -82,6 +114,16 @@
{{ $t('settings.user_popover_avatar_overlay') }}
</BooleanSetting>
</li>
<li>
<ChoiceSetting
v-if="user && (translationLanguages.length > 0)"
id="translationLanguage"
path="translationLanguage"
:options="translationLanguages"
>
{{ $t('settings.translation_language') }}
</ChoiceSetting>
</li>
<li>
<BooleanSetting
path="alwaysShowNewPostButton"
@ -153,6 +195,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">

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

@ -9,7 +9,7 @@ import {
faSignOutAlt,
faHome,
faComments,
faBell,
faBolt,
faUserPlus,
faBullhorn,
faSearch,
@ -25,7 +25,7 @@ library.add(
faSignOutAlt,
faHome,
faComments,
faBell,
faBolt,
faUserPlus,
faBullhorn,
faSearch,

View File

@ -95,7 +95,7 @@
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding"
icon="bell"
icon="bolt"
/> {{ $t("nav.interactions") }}
</router-link>
</li>

View File

@ -1,4 +1,5 @@
import ReplyButton from '../reply_button/reply_button.vue'
import QuoteButton from '../quote_button/quote_button.vue'
import FavoriteButton from '../favorite_button/favorite_button.vue'
import ReactButton from '../react_button/react_button.vue'
import RetweetButton from '../retweet_button/retweet_button.vue'
@ -117,7 +118,8 @@ const Status = {
MentionLink,
MentionsLine,
UserPopover,
UserLink
UserLink,
QuoteButton
},
props: [
'statusoid',
@ -147,6 +149,8 @@ const Status = {
'controlledToggleShowingLongSubject',
'controlledReplying',
'controlledToggleReplying',
'controlledQuoting',
'controlledToggleQuoting',
'controlledMediaPlaying',
'controlledSetMediaPlaying',
'dive'
@ -154,6 +158,7 @@ const Status = {
data () {
return {
uncontrolledReplying: false,
uncontrolledQuoting: false,
unmuted: false,
userExpanded: false,
uncontrolledMediaPlaying: [],
@ -166,7 +171,7 @@ const Status = {
swapReacts () {
return this.mergedConfig.swapReacts
},
...controlledOrUncontrolledGetters(['replying', 'mediaPlaying']),
...controlledOrUncontrolledGetters(['replying', 'quoting', 'mediaPlaying']),
muteWords () {
return this.mergedConfig.muteWords
},
@ -428,6 +433,9 @@ const Status = {
toggleReplying () {
controlledOrUncontrolledToggle(this, 'replying')
},
toggleQuoting () {
controlledOrUncontrolledToggle(this, 'quoting')
},
gotoOriginal (id) {
if (this.inConversation) {
this.$emit('goto', id)

View File

@ -101,6 +101,10 @@
.status-heading {
margin-bottom: 0.5em;
.emoji {
--emoji-size: 16px;
}
}
.heading-name-row {
@ -357,6 +361,15 @@
flex: 1;
}
.quote-form {
padding-top: 0;
padding-bottom: 0;
}
.quote-body {
flex: 1;
}
.favs-repeated-users {
margin-top: var(--status-margin, $status-margin);
}

View File

@ -447,6 +447,12 @@
:status="status"
@toggle="toggleReplying"
/>
<quote-button
:visibility="status.visibility"
:quoting="quoting"
:status="status"
@toggle="toggleQuoting"
/>
<retweet-button
:visibility="status.visibility"
:logged-in="loggedIn"
@ -509,6 +515,20 @@
@posted="toggleReplying"
/>
</div>
<div
v-if="quoting"
class="status-container quote-form"
>
<PostStatusForm
class="quote-body"
:quote-id="status.id"
:attentions="[status.user]"
:replied-user="status.user"
:copy-message-scope="status.visibility"
:subject="replySubject"
@posted="toggleQuoting"
/>
</div>
</template>
</div>
</template>

View File

@ -9,6 +9,7 @@ import {
faLink,
faPollH
} from '@fortawesome/free-solid-svg-icons'
import Select from 'src/components/select/select.vue'
library.add(
faFile,
@ -37,7 +38,8 @@ const StatusContent = {
data () {
return {
postLength: this.status.text.length,
parseReadyDone: false
parseReadyDone: false,
translateFrom: null
}
},
computed: {
@ -78,10 +80,14 @@ const StatusContent = {
attachmentTypes () {
return this.status.attachments.map(file => fileType.fileType(file.mimetype))
},
translationLanguages () {
return (this.$store.getters.mergedConfig.supportedTranslationLanguages || []).map(lang => ({ key: lang.code, value: lang.code, label: lang.name }))
},
...mapGetters(['mergedConfig'])
},
components: {
RichContent
RichContent,
Select
},
mounted () {
this.status.attentions && this.status.attentions.forEach(attn => {
@ -124,6 +130,10 @@ const StatusContent = {
},
generateTagLink (tag) {
return `/tag/${tag}`
},
translateStatus () {
const translateTo = this.$store.getters.mergedConfig.translationLanguage || this.$store.state.instance.interfaceLanguage
this.$store.dispatch('translateStatus', { id: this.status.id, language: translateTo, from: this.translateFrom })
}
}
}

View File

@ -4,6 +4,13 @@
display: flex;
flex-direction: column;
.translation {
border: 1px solid var(--accent, $fallback--link);
border-radius: var(--panelRadius, $fallback--panelRadius);
margin-top: 1em;
padding: 0.5em;
}
.emoji {
--_still_image-label-scale: 0.5;
}

View File

@ -52,6 +52,44 @@
:attentions="status.attentions"
@parseReady="onParseReady"
/>
<div
v-if="status.translation"
class="translation"
>
<h4>{{ $t('status.translated_from', { language: status.translation.detected_language }) }}</h4>
<RichContent
:class="{ '-single-line': singleLine }"
class="text media-body"
:html="status.translation.text"
:emoji="status.emojis"
:handle-links="true"
:mfm="renderMisskeyMarkdown && (status.media_type === 'text/x.misskeymarkdown')"
:greentext="mergedConfig.greentext"
:attentions="status.attentions"
@parseReady="onParseReady"
/>
<div>
<label class="label">{{ $t('status.override_translation_source_language') }}</label>
{{ ' ' }}
<Select
id="source-language-switcher"
v-model="translateFrom"
class="preset-switcher"
>
<option
v-for="language in translationLanguages"
:key="language.key"
:value="language.value"
>
{{ language.label }}
</option>
</Select>
{{ ' ' }}
<button @click="translateStatus" class="btn button-default">
{{ $t('status.translate') }}
</button>
</div>
</div>
<button
v-show="hideSubjectStatus"

View File

@ -3,6 +3,7 @@ import Poll from '../poll/poll.vue'
import Gallery from '../gallery/gallery.vue'
import StatusBody from 'src/components/status_body/status_body.vue'
import LinkPreview from '../link-preview/link-preview.vue'
import QuoteCard from '../quote_card/quote_card.vue'
import { mapGetters, mapState } from 'vuex'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
@ -109,7 +110,8 @@ const StatusContent = {
Poll,
Gallery,
LinkPreview,
StatusBody
StatusBody,
QuoteCard
},
methods: {
toggleShowingTall () {

View File

@ -41,7 +41,14 @@
@play="$emit('mediaplay', attachment.id)"
@pause="$emit('mediapause', attachment.id)"
/>
<div
v-if="status.quote && !compact"
class="quote"
>
<QuoteCard
:status="status.quote"
/>
</div>
<div
v-if="status.card && !noHeading && !compact"
class="link-preview media-body"
@ -63,4 +70,10 @@
flex: 1;
min-width: 0;
}
.quote-inline,
.quote + .link-preview {
display: none;
}
</style>

View File

@ -19,7 +19,8 @@ export const timelineNames = () => {
bookmarks: 'nav.bookmarks',
dms: 'nav.dms',
'public-timeline': 'nav.public_tl',
'public-external-timeline': 'nav.twkn'
'public-external-timeline': 'nav.twkn',
'bubble-timeline': 'nav.bubble_timeline'
}
}

View File

@ -66,6 +66,7 @@
padding: 0 0.65em;
height: 3.5em;
line-height: 3.5em;
padding-bottom: 0;
&:hover {
background-color: $fallback--lightBg;

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

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

View File

@ -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>

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

@ -1,5 +1,7 @@
{
"about": {
"bubble_instances": "Local Bubble Instances",
"bubble_instances_description": "Instances chosen by the admins to represent the local area of this instance",
"mrf": {
"federation": "Federation",
"keyword": {
@ -137,6 +139,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",
@ -166,12 +172,18 @@
"interactions": "Interactions",
"dms": "Direct messages",
"public_tl": "Public timeline",
"public_timeline_description": "Public posts from this instance",
"timeline": "Timeline",
"home_timeline": "Home timeline",
"home_timeline_description": "Posts from people you follow",
"bubble_timeline": "Bubble timeline",
"bubble_timeline_description": "Posts from instances close to yours, as recommended by the admin(s)",
"twkn": "Known Network",
"twkn_timeline_description": "Posts from the entire network",
"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 +278,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 +296,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 +326,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 +353,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 +438,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",
@ -464,7 +496,6 @@
"hide_all_muted_posts": "Hide muted posts",
"max_thumbnails": "Maximum amount of thumbnails per post (empty = no limit)",
"hide_isp": "Hide instance-specific panel",
"show_third_column": "Move Notifications to a seperate column",
"hide_shoutbox": "Hide instance frothbox",
"compact_nav_panel": "Compact navigation panel",
"compact_user_panel": "Compact user panel",
@ -475,6 +506,8 @@
"preload_images": "Preload images",
"use_one_click_nsfw": "Open NSFW attachments with just one click",
"hide_post_stats": "Hide post statistics (e.g. the number of favorites)",
"hide_site_favicon": "Hide instance favicon in top panel",
"hide_site_name": "Hide instance name in top panel",
"hide_user_stats": "Hide user statistics (e.g. the number of followers)",
"hide_filtered_statuses": "Hide all filtered posts",
"hide_wordfiltered_statuses": "Hide word-filtered statuses",
@ -515,6 +548,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",
@ -541,6 +578,8 @@
"hide_followers_count_description": "Don't show follower count",
"show_admin_badge": "Show \"Admin\" badge in my profile",
"show_moderator_badge": "Show \"Moderator\" badge in my profile",
"show_nav_shortcuts": "Show extra navigation shortcuts in top panel",
"show_wider_shortcuts": "Show wider gap between top panel shortcuts",
"nsfw_clickthrough": "Hide sensitive/NSFW media",
"oauth_tokens": "OAuth tokens",
"token": "Token",
@ -619,6 +658,7 @@
"theme_help_v2_1": "You can also override certain component's colors and opacity by toggling the checkbox, use \"Clear all\" button to clear all overrides.",
"theme_help_v2_2": "Icons underneath some entries are background/text contrast indicators, hover over for detailed info. Please keep in mind that when using transparency contrast indicators show the worst possible case.",
"tooltipRadius": "Tooltips/alerts",
"translation_language": "Automatic Translation Language",
"type_domains_to_mute": "Search domains to mute",
"upload_a_photo": "Upload a photo",
"user_settings": "User Settings",
@ -836,7 +876,7 @@
"conversation": "Conversation",
"error": "Error fetching timeline: {0}",
"load_older": "Load older statuses",
"no_retweet_hint": "Post is marked as followers-only or direct and cannot be repeated",
"no_retweet_hint": "Post is marked as followers-only or direct and cannot be repeated or quoted",
"repeated": "repeated",
"show_new": "Show new",
"reload": "Reload",
@ -851,6 +891,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})",
@ -858,8 +902,13 @@
"unpin": "Unpin from profile",
"pinned": "Pinned",
"bookmark": "Bookmark",
"translate": "Translate",
"translated_from": "Translated from {language}",
"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:",
@ -906,11 +955,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",
@ -918,6 +979,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!",
@ -929,9 +994,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",
@ -945,6 +1019,7 @@
"hide_repeats": "Hide repeats",
"show_repeats": "Show repeats",
"bot": "Bot",
"birthday": "Born {birthday}",
"admin_menu": {
"moderation": "Moderation",
"grant_admin": "Grant Admin",
@ -996,6 +1071,7 @@
},
"tool_tip": {
"media_upload": "Upload media",
"quote": "Quote",
"repeat": "Repeat",
"reply": "Reply",
"favorite": "Favorite",
@ -1005,7 +1081,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

@ -90,8 +90,12 @@
"interactions": "インタラクション",
"dms": "ダイレクトメッセージ",
"public_tl": "公開タイムライン",
"public_timeline_description": "このインスタンスからの公開投稿",
"timeline": "タイムライン",
"twkn": "すべてのネットワーク",
"twkn_timeline_description": "全連合からの投稿",
"bubble_timeline": "バブルタイムライン",
"bubble_timeline_description": "管理者がおすすめしているインスタンスからの投稿",
"user_search": "ユーザーを探す",
"search": "検索",
"who_to_follow": "おすすめユーザー",
@ -100,7 +104,9 @@
"bookmarks": "ブックマーク",
"timelines": "タイムライン",
"chats": "チャット",
"home_timeline": "ホームタイムライン"
"home_timeline": "ホームタイムライン",
"home_timeline_description": "フォローしているユーザーからの投稿",
"announcements": "お知らせ"
},
"notifications": {
"broken_favorite": "ステータスが見つかりません。探しています…",
@ -597,199 +603,179 @@
"backup_settings": "設定をファイルにバックアップする",
"backup_restore": "設定をバックアップ"
},
"save": "変更を保存",
"hide_shoutbox": "Shoutboxを表示しない",
"always_show_post_button": "投稿ボタンを常に表示",
"right_sidebar": "サイドバーを右に表示"
},
"time": {
"day": "{0}日",
"days": "{0}日",
"day_short": "{0}日",
"days_short": "{0}日",
"hour": "{0}時間",
"hours": "{0}時間",
"hour_short": "{0}時間",
"hours_short": "{0}時間",
"in_future": "{0}で",
"in_past": "{0}前",
"minute": "{0}分",
"minutes": "{0}分",
"minute_short": "{0}分",
"minutes_short": "{0}分",
"month": "{0}ヶ月前",
"months": "{0}ヶ月前",
"month_short": "{0}ヶ月前",
"months_short": "{0}ヶ月前",
"now": "たった今",
"now_short": "たった今",
"second": "{0}秒",
"seconds": "{0}秒",
"second_short": "{0}秒",
"seconds_short": "{0}秒",
"week": "{0}週間",
"weeks": "{0}週間",
"week_short": "{0}週間",
"weeks_short": "{0}週間",
"year": "{0}年",
"years": "{0}年",
"year_short": "{0}年",
"years_short": "{0}年"
},
"timeline": {
"collapse": "たたむ",
"conversation": "スレッド",
"error_fetching": "読み込みがエラーになりました",
"load_older": "古いステータス",
"no_retweet_hint": "投稿を「フォロワーのみ」または「ダイレクト」にすると、リピートできなくなります",
"repeated": "リピート",
"show_new": "読み込み",
"up_to_date": "最新",
"no_more_statuses": "これで終わりです",
"no_statuses": "ステータスはありません",
"reload": "再読み込み",
"error": "タイムラインの読み込みに失敗しました: {0}",
"socket_reconnected": "リアルタイム接続が確立されました",
"socket_broke": "コード{0}によりリアルタイム接続が切断されました"
},
"status": {
"favorites": "お気に入り",
"repeats": "リピート",
"delete": "ステータスを削除",
"pin": "プロフィールにピン留め",
"unpin": "プロフィールのピン留めを外す",
"pinned": "ピン留め",
"delete_confirm": "本当にこのステータスを削除してもよろしいですか?",
"reply_to": "返信",
"replies_list": "返信:",
"mute_conversation": "スレッドをミュート",
"unmute_conversation": "スレッドのミュートを解除",
"nsfw": "閲覧注意",
"expand": "広げる",
"status_deleted": "この投稿は削除されました",
"hide_content": "隠す",
"show_content": "見る",
"hide_full_subject": "隠す",
"show_full_subject": "全部見る",
"thread_muted_and_words": "以下の単語を含むため:",
"thread_muted": "ミュートされたスレッド",
"external_source": "外部ソース",
"copy_link": "リンクをコピー",
"status_unavailable": "利用できません",
"unbookmark": "ブックマーク解除",
"bookmark": "ブックマーク",
"mentions": "メンション",
"you": "(あなた)",
"plus_more": "ほか{number}件"
},
"user_card": {
"approve": "受け入れ",
"block": "ブロック",
"blocked": "ブロックしています!",
"deny": "お断り",
"favorites": "お気に入り",
"follow": "フォロー",
"follow_sent": "リクエストを送りました!",
"follow_progress": "リクエストしています…",
"follow_unfollow": "フォローをやめる",
"followees": "フォロー",
"followers": "フォロワー",
"following": "フォローしています!",
"follows_you": "フォローされました!",
"its_you": "これはあなたです!",
"media": "メディア",
"mention": "メンション",
"mute": "ミュート",
"muted": "ミュートしています",
"per_day": "/日",
"remote_follow": "リモートフォロー",
"report": "通報",
"statuses": "ステータス",
"subscribe": "購読",
"unsubscribe": "購読を解除",
"unblock": "ブロック解除",
"unblock_progress": "ブロックを解除しています…",
"block_progress": "ブロックしています…",
"unmute": "ミュート解除",
"unmute_progress": "ミュートを解除しています…",
"mute_progress": "ミュートしています…",
"admin_menu": {
"moderation": "モデレーション",
"grant_admin": "管理者権限を付与",
"revoke_admin": "管理者権限を解除",
"grant_moderator": "モデレーター権限を付与",
"revoke_moderator": "モデレーター権限を解除",
"activate_account": "アカウントをアクティブにする",
"deactivate_account": "アカウントをアクティブでなくする",
"delete_account": "アカウントを削除",
"force_nsfw": "すべての投稿をNSFWにする",
"strip_media": "投稿からメディアを除去する",
"force_unlisted": "投稿を未収載にする",
"sandbox": "投稿をフォロワーのみにする",
"disable_remote_subscription": "他のインスタンスからフォローされないようにする",
"disable_any_subscription": "フォローされないようにする",
"quarantine": "他のインスタンスからの投稿を止める",
"delete_user": "ユーザーを削除"
"status": {
"bookmark": "ブックマーク",
"copy_link": "リンクをコピー",
"delete": "ステータスを削除",
"delete_confirm": "本当にこのステータスを削除してもよろしいですか?",
"expand": "広げる",
"external_source": "外部ソース",
"favorites": "お気に入り",
"hide_content": "隠す",
"hide_full_subject": "隠す",
"mentions": "メンション",
"mute_conversation": "スレッドをミュート",
"nsfw": "閲覧注意",
"pin": "プロフィールにピン留め",
"pinned": "ピン留め",
"plus_more": "ほか{number}件",
"repeats": "リピート",
"replies_list": "返信:",
"reply_to": "返信",
"show_content": "見る",
"show_full_subject": "全部見る",
"status_deleted": "この投稿は削除されました",
"status_unavailable": "利用できません",
"thread_muted": "ミュートされたスレッド",
"thread_muted_and_words": "以下の単語を含むため:",
"unbookmark": "ブックマーク解除",
"unmute_conversation": "スレッドのミュートを解除",
"unpin": "プロフィールのピン留めを外す",
"you": "(あなた)"
},
"roles": {
"moderator": "モデレーター",
"admin": "管理者"
"time": {
"in_future": "{0}で",
"in_past": "{0}前",
"now": "たった今",
"now_short": "たった今",
"unit": {
"days": "{0}日",
"days_short": "{0}日",
"hours": "{0}時間",
"hours_short": "{0}時間",
"minutes": "{0}分",
"minutes_short": "{0}分",
"months": "{0}ヶ月",
"months_short": "{0}ヶ月",
"seconds": "{0}秒",
"seconds_short": "{0}秒",
"weeks": "{0}週間",
"weeks_short": "{0}週間",
"years": "{0}年",
"years_short": "{0}年"
}
},
"show_repeats": "リピートを見る",
"hide_repeats": "リピートを隠す",
"message": "メッセージ",
"hidden": "隠す",
"bot": "bot",
"highlight": {
"solid": "背景を単色にする",
"striped": "背景を縞模様にする",
"side": "端に線を付ける",
"disabled": "強調しない"
"timeline": {
"collapse": "たたむ",
"conversation": "スレッド",
"error": "タイムラインの読み込みに失敗しました: {0}",
"load_older": "古いステータス",
"no_more_statuses": "これで終わりです",
"no_retweet_hint": "投稿を「フォロワーのみ」または「ダイレクト」にすると、リピートできなくなります",
"no_statuses": "ステータスはありません",
"reload": "再読み込み",
"repeated": "リピート",
"show_new": "読み込み",
"socket_broke": "コード{0}によりリアルタイム接続が切断されました",
"socket_reconnected": "リアルタイム接続が確立されました",
"up_to_date": "最新"
},
"edit_profile": "プロフィールを編集"
},
"user_profile": {
"timeline_title": "ユーザータイムライン",
"profile_does_not_exist": "申し訳ない。このプロフィールは存在しません。",
"profile_loading_error": "申し訳ない。プロフィールの読み込みがエラーになりました。"
},
"user_reporting": {
"title": "通報する: {0}",
"add_comment_description": "この通報は、あなたのインスタンスのモデレーターに送られます。このアカウントを通報する理由を説明することができます:",
"additional_comments": "追加のコメント",
"forward_description": "このアカウントは他のサーバーに置かれています。この通報のコピーをリモートのサーバーに送りますか?",
"forward_to": "転送する: {0}",
"submit": "送信",
"generic_error": "あなたのリクエストを処理しようとしましたが、エラーになりました。"
},
"who_to_follow": {
"more": "詳細",
"who_to_follow": "おすすめユーザー"
},
"tool_tip": {
"media_upload": "メディアをアップロード",
"repeat": "リピート",
"reply": "返信",
"favorite": "お気に入り",
"user_settings": "ユーザー設定",
"bookmark": "ブックマーク",
"reject_follow_request": "フォローリクエストを拒否",
"accept_follow_request": "フォローリクエストを許可",
"add_reaction": "リアクションを追加"
},
"upload": {
"error": {
"base": "アップロードに失敗しました。",
"file_too_big": "ファイルが大きすぎます [{filesize} {filesizeunit} / {allowedsize} {allowedsizeunit}]",
"default": "しばらくしてから試してください",
"message": "アップロードに失敗: {0}"
"tool_tip": {
"accept_follow_request": "フォローリクエストを許可",
"add_reaction": "リアクションを追加",
"bookmark": "ブックマーク",
"favorite": "お気に入り",
"media_upload": "メディアをアップロード",
"reject_follow_request": "フォローリクエストを拒否",
"repeat": "リピート",
"reply": "返信",
"user_settings": "ユーザー設定"
},
"file_size_units": {
"B": "B",
"KiB": "KiB",
"MiB": "MiB",
"GiB": "GiB",
"TiB": "TiB"
"upload": {
"error": {
"base": "アップロードに失敗しました。",
"default": "しばらくしてから試してください",
"file_too_big": "ファイルが大きすぎます [{filesize} {filesizeunit} / {allowedsize} {allowedsizeunit}]",
"message": "アップロードに失敗: {0}"
},
"file_size_units": {
"B": "B",
"GiB": "GiB",
"KiB": "KiB",
"MiB": "MiB",
"TiB": "TiB"
}
},
"user_card": {
"admin_menu": {
"activate_account": "アカウントをアクティブにする",
"deactivate_account": "アカウントをアクティブでなくする",
"delete_account": "アカウントを削除",
"delete_user": "ユーザーを削除",
"disable_any_subscription": "フォローされないようにする",
"disable_remote_subscription": "他のインスタンスからフォローされないようにする",
"force_nsfw": "すべての投稿をNSFWにする",
"force_unlisted": "投稿を未収載にする",
"grant_admin": "管理者権限を付与",
"grant_moderator": "モデレーター権限を付与",
"moderation": "モデレーション",
"quarantine": "他のインスタンスからの投稿を止める",
"revoke_admin": "管理者権限を解除",
"revoke_moderator": "モデレーター権限を解除",
"sandbox": "投稿をフォロワーのみにする",
"strip_media": "投稿からメディアを除去する"
},
"approve": "受け入れ",
"block": "ブロック",
"block_progress": "ブロックしています…",
"blocked": "ブロックしています!",
"bot": "bot",
"deny": "お断り",
"edit_profile": "プロフィールを編集",
"favorites": "お気に入り",
"follow": "フォロー",
"follow_progress": "リクエストしています…",
"follow_sent": "リクエストを送りました!",
"follow_unfollow": "フォローをやめる",
"followees": "フォロー",
"followers": "フォロワー",
"following": "フォローしています!",
"follows_you": "フォローされました!",
"hidden": "隠す",
"hide_repeats": "リピートを隠す",
"highlight": {
"disabled": "強調しない",
"side": "端に線を付ける",
"solid": "背景を単色にする",
"striped": "背景を縞模様にする"
},
"its_you": "これはあなたです!",
"media": "メディア",
"mention": "メンション",
"message": "メッセージ",
"mute": "ミュート",
"mute_progress": "ミュートしています…",
"muted": "ミュートしています",
"note": "私的なメモ",
"per_day": "/日",
"remote_follow": "リモートフォロー",
"report": "通報",
"show_repeats": "リピートを見る",
"statuses": "ステータス",
"subscribe": "購読",
"unblock": "ブロック解除",
"unblock_progress": "ブロックを解除しています…",
"unmute": "ミュート解除",
"unmute_progress": "ミュートを解除しています…",
"unsubscribe": "購読を解除"
},
"user_profile": {
"profile_does_not_exist": "申し訳ない。このプロフィールは存在しません。",
"profile_loading_error": "申し訳ない。プロフィールの読み込みがエラーになりました。",
"timeline_title": "ユーザータイムライン"
},
"user_reporting": {
"add_comment_description": "この通報は、あなたのインスタンスのモデレーターに送られます。このアカウントを通報する理由を説明することができます:",
"additional_comments": "追加のコメント",
"forward_description": "このアカウントは他のサーバーに置かれています。この通報のコピーをリモートのサーバーに送りますか?",
"forward_to": "転送する: {0}",
"generic_error": "あなたのリクエストを処理しようとしましたが、エラーになりました。",
"submit": "送信",
"title": "通報する: {0}"
},
"who_to_follow": {
"more": "詳細",
"who_to_follow": "おすすめユーザー"
}
},
"search": {

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

@ -270,6 +270,12 @@ const api = {
if (!fetcher) return
store.commit('removeFetcher', { fetcherName: 'lists', fetcher })
},
getSupportedTranslationlanguages (store) {
store.state.backendInteractor.getSupportedTranslationlanguages({ store })
.then((data) => {
store.dispatch('setOption', { name: 'supportedTranslationLanguages', value: data })
})
},
// Pleroma websocket
setWsToken (store, token) {

View File

@ -40,6 +40,10 @@ export const defaultState = {
muteBotStatuses: undefined, // instance default
collapseMessageWithSubject: undefined, // instance default
padEmoji: true,
showNavShortcuts: undefined, // instance default
showWiderShortcuts: undefined, // instance default
hideSiteFavicon: undefined, // instance default
hideSiteName: undefined, // instance default
swapReacts: true,
hideAttachments: false,
hideAttachmentsInConv: false,
@ -82,6 +86,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,
@ -110,7 +123,9 @@ export const defaultState = {
conversationTreeAdvanced: undefined, // instance default
conversationOtherRepliesButton: undefined, // instance default
conversationTreeFadeAncestors: undefined, // instance default
maxDepthInThread: undefined // instance default
maxDepthInThread: undefined, // instance default
translationLanguage: undefined, // instance default,
supportedTranslationLanguages: [] // instance default
}
// caching the instance default properties
@ -188,7 +203,11 @@ 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)
)
dispatch('setInstanceOption', { name: 'interfaceLanguage', value })
break
case 'thirdColumnMode':
dispatch('setLayoutWidth', undefined)

View File

@ -68,9 +68,19 @@ const defaultState = {
hideWordFilteredPosts: false,
hidePostStats: false,
hideBotIndication: false,
hideSitename: false,
hideSiteFavicon: false,
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',
@ -85,6 +95,8 @@ const defaultState = {
scopeCopy: true,
showFeaturesPanel: true,
showInstanceSpecificPanel: false,
showNavShortcuts: true,
showWiderShortcuts: true,
sidebarRight: false,
subjectLineBehavior: 'email',
theme: 'pleroma-dark',
@ -107,6 +119,8 @@ const defaultState = {
restrictedNicknames: [],
safeDM: true,
knownDomains: [],
birthdayRequired: false,
birthdayMinAge: 0,
// Feature-set, apparently, not everything here is reported...
shoutAvailable: false,
@ -286,8 +300,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

@ -63,7 +63,8 @@ export const defaultState = () => ({
tag: emptyTl(),
dms: emptyTl(),
bookmarks: emptyTl(),
list: emptyTl()
list: emptyTl(),
bubble: emptyTl()
}
})
@ -432,6 +433,10 @@ export const mutations = {
state.conversationsObject[newStatus.statusnet_conversation_id].forEach(status => { status.thread_muted = newStatus.thread_muted })
}
},
setTranslatedStatus (state, { id, translation }) {
const newStatus = state.allStatusesObject[id]
newStatus.translation = translation
},
setRetweeted (state, { status, value }) {
const newStatus = state.allStatusesObject[status.id]
@ -650,6 +655,10 @@ const statuses = {
rootState.api.backendInteractor.unpinOwnStatus({ id: statusId })
.then((status) => dispatch('addNewStatuses', { statuses: [status] }))
},
translateStatus ({ rootState, commit }, { id, translation, language, from }) {
return rootState.api.backendInteractor.translateStatus({ id: id, translation, language, from })
.then((translation) => commit('setTranslatedStatus', { id, translation }))
},
muteConversation ({ rootState, commit }, statusId) {
return rootState.api.backendInteractor.muteConversation({ id: statusId })
.then((status) => commit('setMutedStatus', status))

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

@ -11,11 +11,11 @@ const CHANGE_EMAIL_URL = '/api/pleroma/change_email'
const CHANGE_PASSWORD_URL = '/api/pleroma/change_password'
const MOVE_ACCOUNT_URL = '/api/pleroma/move_account'
const ALIASES_URL = '/api/pleroma/aliases'
const TAG_USER_URL = '/api/pleroma/admin/users/tag'
const PERMISSION_GROUP_URL = (screenName, right) => `/api/pleroma/admin/users/${screenName}/permission_group/${right}`
const ACTIVATE_USER_URL = '/api/pleroma/admin/users/activate'
const DEACTIVATE_USER_URL = '/api/pleroma/admin/users/deactivate'
const ADMIN_USERS_URL = '/api/pleroma/admin/users'
const TAG_USER_URL = '/api/v1/pleroma/admin/users/tag'
const PERMISSION_GROUP_URL = (screenName, right) => `/api/v1/pleroma/admin/users/${screenName}/permission_group/${right}`
const ACTIVATE_USER_URL = '/api/v1/pleroma/admin/users/activate'
const DEACTIVATE_USER_URL = '/api/v1/pleroma/admin/users/deactivate'
const ADMIN_USERS_URL = '/api/v1/pleroma/admin/users'
const SUGGESTIONS_URL = '/api/v1/suggestions'
const NOTIFICATION_SETTINGS_URL = '/api/pleroma/notification_settings'
const NOTIFICATION_READ_URL = '/api/v1/pleroma/notifications/read'
@ -31,6 +31,8 @@ const MASTODON_LOGIN_URL = '/api/v1/accounts/verify_credentials'
const MASTODON_REGISTRATION_URL = '/api/v1/accounts'
const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites'
const MASTODON_USER_NOTIFICATIONS_URL = '/api/v1/notifications'
const AKKOMA_LANGUAGES_URL = '/api/v1/akkoma/translation/languages'
const AKKOMA_TRANSLATE_URL = (id, lang) => `/api/v1/statuses/${id}/translations/${lang}`
const MASTODON_DISMISS_NOTIFICATION_URL = id => `/api/v1/notifications/${id}/dismiss`
const MASTODON_FAVORITE_URL = id => `/api/v1/statuses/${id}/favourite`
const MASTODON_UNFAVORITE_URL = id => `/api/v1/statuses/${id}/unfavourite`
@ -47,6 +49,7 @@ const MASTODON_DENY_USER_URL = id => `/api/v1/follow_requests/${id}/reject`
const MASTODON_DIRECT_MESSAGES_TIMELINE_URL = '/api/v1/timelines/direct'
const MASTODON_PUBLIC_TIMELINE = '/api/v1/timelines/public'
const MASTODON_USER_HOME_TIMELINE_URL = '/api/v1/timelines/home'
const AKKOMA_BUBBLE_TIMELINE_URL = '/api/v1/timelines/bubble'
const MASTODON_STATUS_URL = id => `/api/v1/statuses/${id}`
const MASTODON_STATUS_CONTEXT_URL = id => `/api/v1/statuses/${id}/context`
const MASTODON_STATUS_SOURCE_URL = id => `/api/v1/statuses/${id}/source`
@ -101,7 +104,7 @@ const PLEROMA_CHAT_URL = id => `/api/v1/pleroma/chats/by-account-id/${id}`
const PLEROMA_CHAT_MESSAGES_URL = id => `/api/v1/pleroma/chats/${id}/messages`
const PLEROMA_CHAT_READ_URL = id => `/api/v1/pleroma/chats/${id}/read`
const PLEROMA_DELETE_CHAT_MESSAGE_URL = (chatId, messageId) => `/api/v1/pleroma/chats/${chatId}/messages/${messageId}`
const PLEROMA_ADMIN_REPORTS = '/api/pleroma/admin/reports'
const PLEROMA_ADMIN_REPORTS = '/api/v1/pleroma/admin/reports'
const PLEROMA_BACKUP_URL = '/api/v1/pleroma/backups'
const PLEROMA_ANNOUNCEMENTS_URL = '/api/v1/pleroma/admin/announcements'
const PLEROMA_POST_ANNOUNCEMENT_URL = '/api/v1/pleroma/admin/announcements'
@ -675,6 +678,7 @@ const fetchTimeline = ({
}) => {
const timelineUrls = {
public: MASTODON_PUBLIC_TIMELINE,
bubble: AKKOMA_BUBBLE_TIMELINE_URL,
friends: MASTODON_USER_HOME_TIMELINE_URL,
dms: MASTODON_DIRECT_MESSAGES_TIMELINE_URL,
notifications: MASTODON_USER_NOTIFICATIONS_URL,
@ -797,6 +801,18 @@ const unretweet = ({ id, credentials }) => {
.then((data) => parseStatus(data))
}
const getSupportedTranslationlanguages = ({ credentials }) => {
return promisedRequest({ url: AKKOMA_LANGUAGES_URL, credentials })
}
const translateStatus = ({ id, credentials, language, from }) => {
const queryString = from ? `?from=${from}` : ''
return promisedRequest({ url: AKKOMA_TRANSLATE_URL(id, language) + queryString, method: 'GET', credentials })
.then((data) => {
return data
})
}
const bookmarkStatus = ({ id, credentials }) => {
return promisedRequest({
url: MASTODON_BOOKMARK_STATUS_URL(id),
@ -822,6 +838,7 @@ const postStatus = ({
poll,
mediaIds = [],
inReplyToStatusId,
quoteId,
contentType,
preview,
idempotencyKey
@ -854,6 +871,9 @@ const postStatus = ({
if (inReplyToStatusId) {
form.append('in_reply_to_id', inReplyToStatusId)
}
if (quoteId) {
form.append('quote_id', quoteId)
}
if (preview) {
form.append('preview', 'true')
}
@ -1118,8 +1138,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 }) => {
@ -1480,7 +1504,15 @@ export const ProcessedWS = ({
}
socket.addEventListener('open', (wsEvent) => {
console.debug(`[WS][${id}] Socket connected`, wsEvent)
setInterval(() => {
try {
socket.send('ping')
} catch (e) {
clearInterval(this)
}
}, 30000)
})
socket.addEventListener('error', (wsEvent) => {
console.debug(`[WS][${id}] Socket errored`, wsEvent)
})
@ -1768,7 +1800,9 @@ const apiService = {
postAnnouncement,
editAnnouncement,
deleteAnnouncement,
adminFetchAnnouncements
adminFetchAnnouncements,
translateStatus,
getSupportedTranslationlanguages
}
export default apiService

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

@ -35,6 +35,10 @@ const backendInteractorService = credentials => ({
return ProcessedWS({ url, id: 'User' })
},
getSupportedTranslationlanguages ({ store }) {
return apiService.getSupportedTranslationlanguages({ store, credentials })
},
...Object.entries(apiService).reduce((acc, [key, func]) => {
return {
...acc,

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
}
}
@ -389,6 +392,9 @@ export const parseStatus = (data) => {
output.visibility = data.visibility
output.card = data.card
output.created_at = new Date(data.created_at)
if (data.quote) {
output.quote = parseStatus(data.quote)
}
// Converting to string, the right way.
output.in_reply_to_status_id = output.in_reply_to_status_id

Some files were not shown because too many files have changed in this diff Show More