Merge pull request '2022.09 stable' (#160) from develop into stable
ci/woodpecker/push/woodpecker Pipeline was successful Details

Reviewed-on: AkkomaGang/pleroma-fe#160
This commit is contained in:
floatingghost 2022-09-10 14:39:13 +00:00
commit d7499a1f91
88 changed files with 2411 additions and 240 deletions

View File

@ -10,3 +10,5 @@ Contributors of this project.
- shpuld (shpuld@shitposter.club): CSS and styling - shpuld (shpuld@shitposter.club): CSS and styling
- Vincent Guth (https://unsplash.com/photos/XrwVIFy6rTw): Background images. - Vincent Guth (https://unsplash.com/photos/XrwVIFy6rTw): Background images.
- hj (hj@shigusegubu.club): Code - hj (hj@shigusegubu.club): Code
- Sean King (seanking@freespeechextremist.com): Code
- Tusooa Zhu (tusooa@kazv.moe): Code

View File

@ -1,5 +1,7 @@
# Pleroma-FE # Pleroma-FE
![English OK](https://img.shields.io/badge/English-OK-blueviolet) ![日本語OK](https://img.shields.io/badge/%E6%97%A5%E6%9C%AC%E8%AA%9E-OK-blueviolet)
This is a fork of Pleroma-FE from the Pleroma project, with support for new Akkoma features such as: This is a fork of Pleroma-FE from the Pleroma project, with support for new Akkoma features such as:
- MFM support via [marked-mfm](https://akkoma.dev/sfr/marked-mfm) - MFM support via [marked-mfm](https://akkoma.dev/sfr/marked-mfm)
- Custom emoji reactions - Custom emoji reactions

4
config/ihba.json Normal file
View File

@ -0,0 +1,4 @@
{
"target": "https://ihatebeinga.live",
"staticConfigPreference": false
}

View File

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

View File

@ -19,7 +19,7 @@
"@babel/runtime": "7.17.8", "@babel/runtime": "7.17.8",
"@chenfengyuan/vue-qrcode": "2.0.0", "@chenfengyuan/vue-qrcode": "2.0.0",
"@fortawesome/fontawesome-svg-core": "1.3.0", "@fortawesome/fontawesome-svg-core": "1.3.0",
"@fortawesome/free-regular-svg-icons": "5.15.4", "@fortawesome/free-regular-svg-icons": "^6.1.2",
"@fortawesome/free-solid-svg-icons": "5.15.4", "@fortawesome/free-solid-svg-icons": "5.15.4",
"@fortawesome/vue-fontawesome": "3.0.1", "@fortawesome/vue-fontawesome": "3.0.1",
"@kazvmoe-infra/pinch-zoom-element": "1.2.0", "@kazvmoe-infra/pinch-zoom-element": "1.2.0",

View File

@ -10,7 +10,9 @@ import MobilePostStatusButton from './components/mobile_post_status_button/mobil
import MobileNav from './components/mobile_nav/mobile_nav.vue' import MobileNav from './components/mobile_nav/mobile_nav.vue'
import DesktopNav from './components/desktop_nav/desktop_nav.vue' import DesktopNav from './components/desktop_nav/desktop_nav.vue'
import UserReportingModal from './components/user_reporting_modal/user_reporting_modal.vue' import UserReportingModal from './components/user_reporting_modal/user_reporting_modal.vue'
import EditStatusModal from './components/edit_status_modal/edit_status_modal.vue'
import PostStatusModal from './components/post_status_modal/post_status_modal.vue' import PostStatusModal from './components/post_status_modal/post_status_modal.vue'
import StatusHistoryModal from './components/status_history_modal/status_history_modal.vue'
import GlobalNoticeList from './components/global_notice_list/global_notice_list.vue' import GlobalNoticeList from './components/global_notice_list/global_notice_list.vue'
import { windowWidth, windowHeight } from './services/window_utils/window_utils' import { windowWidth, windowHeight } from './services/window_utils/window_utils'
import { mapGetters } from 'vuex' import { mapGetters } from 'vuex'
@ -33,6 +35,8 @@ export default {
SettingsModal, SettingsModal,
UserReportingModal, UserReportingModal,
PostStatusModal, PostStatusModal,
EditStatusModal,
StatusHistoryModal,
GlobalNoticeList GlobalNoticeList
}, },
data: () => ({ data: () => ({
@ -83,6 +87,7 @@ export default {
return this.$store.getters.mergedConfig.alwaysShowNewPostButton || this.layoutType === 'mobile' return this.$store.getters.mergedConfig.alwaysShowNewPostButton || this.layoutType === 'mobile'
}, },
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel }, showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
editingAvailable () { return this.$store.state.instance.editingAvailable },
layoutType () { return this.$store.state.interface.layoutType }, layoutType () { return this.$store.state.interface.layoutType },
privateMode () { return this.$store.state.instance.private }, privateMode () { return this.$store.state.instance.private },
reverseLayout () { reverseLayout () {

View File

@ -58,8 +58,10 @@
<MobilePostStatusButton /> <MobilePostStatusButton />
<UserReportingModal /> <UserReportingModal />
<PostStatusModal /> <PostStatusModal />
<EditStatusModal v-if="editingAvailable" />
<StatusHistoryModal v-if="editingAvailable" />
<SettingsModal /> <SettingsModal />
<div id="modal" /> <UpdateNotification />
<GlobalNoticeList /> <GlobalNoticeList />
</div> </div>
</template> </template>

View File

@ -1,3 +1,4 @@
import Cookies from 'js-cookie'
import { createApp } from 'vue' import { createApp } from 'vue'
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
import vClickOutside from 'click-outside-vue3' import vClickOutside from 'click-outside-vue3'
@ -48,6 +49,20 @@ const preloadFetch = async (request) => {
} }
} }
const resolveLanguage = (instanceLanguages) => {
// First language in navigator.languages that is listed as an instance language
// falls back to first instance language
const navigatorLanguages = navigator.languages.map((x) => x.split('-')[0])
for (const navLanguage of navigatorLanguages) {
if (instanceLanguages.includes(navLanguage)) {
return navLanguage
}
}
return instanceLanguages[0]
}
const getInstanceConfig = async ({ store }) => { const getInstanceConfig = async ({ store }) => {
try { try {
const res = await preloadFetch('/api/v1/instance') const res = await preloadFetch('/api/v1/instance')
@ -58,6 +73,10 @@ const getInstanceConfig = async ({ store }) => {
store.dispatch('setInstanceOption', { name: 'textlimit', value: textlimit }) store.dispatch('setInstanceOption', { name: 'textlimit', value: textlimit })
store.dispatch('setInstanceOption', { name: 'accountApprovalRequired', value: data.approval_required }) store.dispatch('setInstanceOption', { name: 'accountApprovalRequired', value: data.approval_required })
// don't override cookie if set
if (!Cookies.get('userLanguage')) {
store.dispatch('setOption', { name: 'interfaceLanguage', value: resolveLanguage(data.languages) })
}
if (vapidPublicKey) { if (vapidPublicKey) {
store.dispatch('setInstanceOption', { name: 'vapidPublicKey', value: vapidPublicKey }) store.dispatch('setInstanceOption', { name: 'vapidPublicKey', value: vapidPublicKey })
@ -124,6 +143,11 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
copyInstanceOption('hideBotIndication') copyInstanceOption('hideBotIndication')
copyInstanceOption('hideUserStats') copyInstanceOption('hideUserStats')
copyInstanceOption('hideFilteredStatuses') copyInstanceOption('hideFilteredStatuses')
copyInstanceOption('hideSiteName')
copyInstanceOption('hideSiteFavicon')
copyInstanceOption('showWiderShortcuts')
copyInstanceOption('showNavShortcuts')
copyInstanceOption('showPanelNavShortcuts')
copyInstanceOption('logo') copyInstanceOption('logo')
store.dispatch('setInstanceOption', { store.dispatch('setInstanceOption', {
@ -154,6 +178,7 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
copyInstanceOption('alwaysShowSubjectInput') copyInstanceOption('alwaysShowSubjectInput')
copyInstanceOption('showFeaturesPanel') copyInstanceOption('showFeaturesPanel')
copyInstanceOption('hideSitename') copyInstanceOption('hideSitename')
copyInstanceOption('renderMisskeyMarkdown')
copyInstanceOption('sidebarRight') copyInstanceOption('sidebarRight')
return store.dispatch('setTheme', config['theme']) return store.dispatch('setTheme', config['theme'])
@ -248,8 +273,10 @@ const getNodeInfo = async ({ store }) => {
store.dispatch('setInstanceOption', { name: 'mediaProxyAvailable', value: features.includes('media_proxy') }) store.dispatch('setInstanceOption', { name: 'mediaProxyAvailable', value: features.includes('media_proxy') })
store.dispatch('setInstanceOption', { name: 'safeDM', value: features.includes('safe_dm_mentions') }) store.dispatch('setInstanceOption', { name: 'safeDM', value: features.includes('safe_dm_mentions') })
store.dispatch('setInstanceOption', { name: 'pollsAvailable', value: features.includes('polls') }) store.dispatch('setInstanceOption', { name: 'pollsAvailable', value: features.includes('polls') })
store.dispatch('setInstanceOption', { name: 'editingAvailable', value: features.includes('editing') })
store.dispatch('setInstanceOption', { name: 'pollLimits', value: metadata.pollLimits }) store.dispatch('setInstanceOption', { name: 'pollLimits', value: metadata.pollLimits })
store.dispatch('setInstanceOption', { name: 'mailerEnabled', value: metadata.mailerEnabled }) store.dispatch('setInstanceOption', { name: 'mailerEnabled', value: metadata.mailerEnabled })
store.dispatch('setInstanceOption', { name: 'translationEnabled', value: features.includes('akkoma:machine_translation') })
const uploadLimits = metadata.uploadLimits const uploadLimits = metadata.uploadLimits
store.dispatch('setInstanceOption', { name: 'uploadlimit', value: parseInt(uploadLimits.general) }) store.dispatch('setInstanceOption', { name: 'uploadlimit', value: parseInt(uploadLimits.general) })
@ -371,6 +398,7 @@ const afterStoreSetup = async ({ store, i18n }) => {
store.dispatch('startFetchingAnnouncements') store.dispatch('startFetchingAnnouncements')
getTOS({ store }) getTOS({ store })
getStickers({ store }) getStickers({ store })
store.dispatch('getSupportedTranslationlanguages')
const router = createRouter({ const router = createRouter({
history: createWebHistory(), history: createWebHistory(),

View File

@ -1,6 +1,8 @@
import ProgressButton from '../progress_button/progress_button.vue' import ProgressButton from '../progress_button/progress_button.vue'
import Popover from '../popover/popover.vue' import Popover from '../popover/popover.vue'
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { mapState } from 'vuex'
import { import {
faEllipsisV faEllipsisV
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
@ -14,13 +16,22 @@ const AccountActions = {
'user', 'relationship' 'user', 'relationship'
], ],
data () { data () {
return { } return {
showingConfirmBlock: false
}
}, },
components: { components: {
ProgressButton, ProgressButton,
Popover Popover,
ConfirmModal
}, },
methods: { methods: {
showConfirmBlock () {
this.showingConfirmBlock = true
},
hideConfirmBlock () {
this.showingConfirmBlock = false
},
showRepeats () { showRepeats () {
this.$store.dispatch('showReblogs', this.user.id) this.$store.dispatch('showReblogs', this.user.id)
}, },
@ -28,7 +39,15 @@ const AccountActions = {
this.$store.dispatch('hideReblogs', this.user.id) this.$store.dispatch('hideReblogs', this.user.id)
}, },
blockUser () { blockUser () {
if (!this.shouldConfirmBlock) {
this.doBlockUser()
} else {
this.showConfirmBlock()
}
},
doBlockUser () {
this.$store.dispatch('blockUser', this.user.id) this.$store.dispatch('blockUser', this.user.id)
this.hideConfirmBlock()
}, },
unblockUser () { unblockUser () {
this.$store.dispatch('unblockUser', this.user.id) this.$store.dispatch('unblockUser', this.user.id)
@ -36,6 +55,14 @@ const AccountActions = {
reportUser () { reportUser () {
this.$store.dispatch('openUserReportingModal', { userId: this.user.id }) this.$store.dispatch('openUserReportingModal', { userId: this.user.id })
} }
},
computed: {
shouldConfirmBlock () {
return this.$store.getters.mergedConfig.modalOnBlock
},
...mapState({
pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable
})
} }
} }

View File

@ -59,6 +59,27 @@
</button> </button>
</template> </template>
</Popover> </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 v-slot:user>
<span
v-text="user.screen_name_ui"
/>
</template>
</i18n-t>
</confirm-modal>
</teleport>
</div> </div>
</template> </template>

View File

@ -1,9 +1,9 @@
<template> <template>
<div class="panel panel-default announcements-page"> <div class="panel panel-default announcements-page">
<div class="panel-heading"> <div class="panel-heading">
<span> <div class="title">
{{ $t('announcements.page_header') }} {{ $t('announcements.page_header') }}
</span> </div>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<section <section

View File

@ -132,6 +132,9 @@ const Attachment = {
...mapGetters(['mergedConfig']) ...mapGetters(['mergedConfig'])
}, },
watch: { watch: {
'attachment.description' (newVal) {
this.localDescription = newVal
},
localDescription (newVal) { localDescription (newVal) {
this.onEdit(newVal) this.onEdit(newVal)
} }

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,39 @@
<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="onCancel"
v-text="cancelText"
/>
<button
class="btn button-default button-positive"
@click.prevent="onAccept"
v-text="confirmText"
/>
</template>
</dialog-modal>
</template>
<style lang="scss" scoped>
@import '../../_variables';
.confirm-modal {
.button-positive {
border: 3px solid var(--accent, $fallback--link);
border-radius: var(--btnRadius, $fallback--btnRadius);
}
}
</style>
<script src="./confirm_modal.js"></script>

View File

@ -1,6 +1,8 @@
import { reduce, filter, findIndex, clone, get } from 'lodash' import { reduce, filter, findIndex, clone, get } from 'lodash'
import Status from '../status/status.vue' import Status from '../status/status.vue'
import ThreadTree from '../thread_tree/thread_tree.vue' import ThreadTree from '../thread_tree/thread_tree.vue'
import { WSConnectionStatus } from '../../services/api/api.service.js'
import { mapGetters, mapState } from 'vuex'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
@ -77,6 +79,9 @@ const conversation = {
const maxDepth = this.$store.getters.mergedConfig.maxDepthInThread - 2 const maxDepth = this.$store.getters.mergedConfig.maxDepthInThread - 2
return maxDepth >= 1 ? maxDepth : 1 return maxDepth >= 1 ? maxDepth : 1
}, },
streamingEnabled () {
return this.mergedConfig.useStreamingApi && this.mastoUserSocketStatus === WSConnectionStatus.JOINED
},
displayStyle () { displayStyle () {
return this.$store.getters.mergedConfig.conversationDisplay return this.$store.getters.mergedConfig.conversationDisplay
}, },
@ -339,7 +344,11 @@ const conversation = {
}, },
maybeHighlight () { maybeHighlight () {
return this.isExpanded ? this.highlight : null return this.isExpanded ? this.highlight : null
} },
...mapGetters(['mergedConfig']),
...mapState({
mastoUserSocketStatus: state => state.api.mastoUserSocketStatus
})
}, },
components: { components: {
Status, Status,
@ -395,6 +404,11 @@ const conversation = {
setHighlight (id) { setHighlight (id) {
if (!id) return if (!id) return
this.highlight = id this.highlight = id
if (!this.streamingEnabled) {
this.$store.dispatch('fetchStatus', id)
}
this.$store.dispatch('fetchFavsAndRepeats', id) this.$store.dispatch('fetchFavsAndRepeats', id)
this.$store.dispatch('fetchEmojiReactionsBy', id) this.$store.dispatch('fetchEmojiReactionsBy', id)
}, },

View File

@ -1,16 +1,21 @@
import SearchBar from 'components/search_bar/search_bar.vue' 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 { library } from '@fortawesome/fontawesome-svg-core'
import { import {
faSignInAlt, faSignInAlt,
faSignOutAlt, faSignOutAlt,
faHome, faHome,
faComments, faComments,
faBell,
faUserPlus, faUserPlus,
faBullhorn, faBullhorn,
faSearch, faSearch,
faTachometerAlt, faTachometerAlt,
faCog, faCog,
faGlobe,
faBolt,
faUsers,
faCommentMedical,
faBookmark,
faInfoCircle faInfoCircle
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
@ -19,18 +24,23 @@ library.add(
faSignOutAlt, faSignOutAlt,
faHome, faHome,
faComments, faComments,
faBell,
faUserPlus, faUserPlus,
faBullhorn, faBullhorn,
faSearch, faSearch,
faTachometerAlt, faTachometerAlt,
faCog, faCog,
faGlobe,
faBolt,
faUsers,
faCommentMedical,
faBookmark,
faInfoCircle faInfoCircle
) )
export default { export default {
components: { components: {
SearchBar SearchBar,
ConfirmModal
}, },
data: () => ({ data: () => ({
searchBarHidden: true, searchBarHidden: true,
@ -40,7 +50,8 @@ export default {
window.CSS.supports('-moz-mask-size', 'contain') || window.CSS.supports('-moz-mask-size', 'contain') ||
window.CSS.supports('-ms-mask-size', 'contain') || window.CSS.supports('-ms-mask-size', 'contain') ||
window.CSS.supports('-o-mask-size', 'contain') window.CSS.supports('-o-mask-size', 'contain')
) ),
showingConfirmLogout: false
}), }),
computed: { computed: {
enableMask () { return this.supportsMask && this.$store.state.instance.logoMask }, enableMask () { return this.supportsMask && this.$store.state.instance.logoMask },
@ -65,20 +76,34 @@ export default {
}) })
}, },
logo () { return this.$store.state.instance.logo }, logo () { return this.$store.state.instance.logo },
mergedConfig () {
return this.$store.getters.mergedConfig
},
sitename () { return this.$store.state.instance.name }, 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 }, hideSitename () { return this.$store.state.instance.hideSitename },
logoLeft () { return this.$store.state.instance.logoLeft }, logoLeft () { return this.$store.state.instance.logoLeft },
currentUser () { return this.$store.state.users.currentUser }, 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: { methods: {
scrollToTop () { scrollToTop () {
window.scrollTo(0, 0) window.scrollTo(0, 0)
}, },
logout () {
this.$router.replace('/main/public')
this.$store.dispatch('logout')
},
onSearchBarToggled (hidden) { onSearchBarToggled (hidden) {
this.searchBarHidden = hidden this.searchBarHidden = hidden
}, },

View File

@ -15,16 +15,16 @@
display: grid; display: grid;
grid-template-rows: var(--navbar-height); grid-template-rows: var(--navbar-height);
grid-template-columns: 2fr auto 2fr; grid-template-columns: 2fr auto 2fr;
grid-template-areas: "sitename logo actions"; grid-template-areas: "nav-left logo actions";
box-sizing: border-box; box-sizing: border-box;
padding: 0 1.2em; padding: 0 1.2em;
margin: auto; margin: auto;
max-width: 980px; max-width: 1110px;
} }
&.-logoLeft .inner-nav { &.-logoLeft .inner-nav {
grid-template-columns: auto 2fr 2fr; grid-template-columns: auto 2fr 2fr;
grid-template-areas: "logo sitename actions"; grid-template-areas: "logo nav-left actions";
} }
.button-default { .button-default {
@ -84,14 +84,21 @@
} }
.nav-icon { .nav-icon {
margin-left: 1em; margin-left: 0.2em;
width: 2em; width: 2em;
height: 100%; height: 100%;
font-size: 130%;
text-align: center; text-align: center;
&-logout { &.router-link-active {
margin-left: 2em; 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);
}
} }
.svg-inline--fa { .svg-inline--fa {
@ -100,8 +107,25 @@
} }
} }
.sitename { .-wide {
grid-area: sitename; .nav-icon {
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 { .actions {

View File

@ -5,16 +5,79 @@
:class="{ '-logoLeft': logoLeft }" :class="{ '-logoLeft': logoLeft }"
@click="scrollToTop()" @click="scrollToTop()"
> >
<div class="inner-nav"> <div
<div class="item sitename"> class="inner-nav"
:class="{ '-wide': showWiderShortcuts }"
>
<div class="item nav-left-wrapper">
<router-link <router-link
v-if="!hideSitename" class="site-brand"
class="site-name"
:to="{ name: 'root' }" :to="{ name: 'root' }"
active-class="home" active-class="home"
> >
{{ sitename }} <img
v-if="!hideSiteFavicon"
class="favicon"
src="/favicon.png"
>
<span
v-if="!hideSiteName"
class="site-name"
>
{{ sitename }}
</span>
</router-link> </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
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>
<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>
</div>
</div> </div>
<router-link <router-link
class="logo" class="logo"
@ -36,6 +99,47 @@
@toggled="onSearchBarToggled" @toggled="onSearchBarToggled"
@click.stop @click.stop
/> />
<div
v-if="(currentUser || !privateMode) && showNavShortcuts"
class="nav-items right"
>
<router-link
v-if="currentUser"
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 <button
class="button-unstyled nav-icon" class="button-unstyled nav-icon"
@click.stop="openSettingsModal" @click.stop="openSettingsModal"
@ -61,20 +165,20 @@
:title="$t('nav.administration')" :title="$t('nav.administration')"
/> />
</a> </a>
<button
v-if="currentUser"
class="button-unstyled nav-icon nav-icon-logout"
@click.prevent="logout"
>
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding"
icon="sign-out-alt"
:title="$t('login.logout')"
/>
</button>
</div> </div>
</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> </nav>
</template> </template>
<script src="./desktop_nav.js"></script> <script src="./desktop_nav.js"></script>

View File

@ -39,7 +39,7 @@
right: 0; right: 0;
top: 0; top: 0;
background: rgba(27,31,35,.5); background: rgba(27,31,35,.5);
z-index: 99; z-index: 2000;
} }
} }
@ -51,9 +51,10 @@
margin: 15vh auto; margin: 15vh auto;
position: fixed; position: fixed;
transform: translateX(-50%); transform: translateX(-50%);
z-index: 999; z-index: 2001;
cursor: default; cursor: default;
display: block; display: block;
width: max-content;
background-color: $fallback--bg; background-color: $fallback--bg;
background-color: var(--bg, $fallback--bg); background-color: var(--bg, $fallback--bg);

View File

@ -0,0 +1,75 @@
import PostStatusForm from '../post_status_form/post_status_form.vue'
import Modal from '../modal/modal.vue'
import statusPosterService from '../../services/status_poster/status_poster.service.js'
import get from 'lodash/get'
const EditStatusModal = {
components: {
PostStatusForm,
Modal
},
data () {
return {
resettingForm: false
}
},
computed: {
isLoggedIn () {
return !!this.$store.state.users.currentUser
},
modalActivated () {
return this.$store.state.editStatus.modalActivated
},
isFormVisible () {
return this.isLoggedIn && !this.resettingForm && this.modalActivated
},
params () {
return this.$store.state.editStatus.params || {}
}
},
watch: {
params (newVal, oldVal) {
if (get(newVal, 'statusId') !== get(oldVal, 'statusId')) {
this.resettingForm = true
this.$nextTick(() => {
this.resettingForm = false
})
}
},
isFormVisible (val) {
if (val) {
this.$nextTick(() => this.$el && this.$el.querySelector('textarea').focus())
}
}
},
methods: {
doEditStatus ({ status, spoilerText, sensitive, media, contentType, poll }) {
const params = {
store: this.$store,
statusId: this.$store.state.editStatus.params.statusId,
status,
spoilerText,
sensitive,
poll,
media,
contentType
}
return statusPosterService.editStatus(params)
.then((data) => {
return data
})
.catch((err) => {
console.error('Error editing status', err)
return {
error: err.message
}
})
},
closeModal () {
this.$store.dispatch('closeEditStatusModal')
}
}
}
export default EditStatusModal

View File

@ -0,0 +1,48 @@
<template>
<Modal
v-if="isFormVisible"
class="edit-form-modal-view"
@backdropClicked="closeModal"
>
<div class="edit-form-modal-panel panel">
<div class="panel-heading">
{{ $t('post_status.edit_status') }}
</div>
<PostStatusForm
class="panel-body"
v-bind="params"
@posted="closeModal"
:disablePolls="true"
:disableVisibilitySelector="true"
:post-handler="doEditStatus"
/>
</div>
</Modal>
</template>
<script src="./edit_status_modal.js"></script>
<style lang="scss">
.modal-view.edit-form-modal-view {
align-items: flex-start;
}
.edit-form-modal-panel {
flex-shrink: 0;
margin-top: 25%;
margin-bottom: 2em;
width: 100%;
max-width: 700px;
@media (orientation: landscape) {
margin-top: 8%;
}
.form-bottom-left {
max-width: 6.5em;
.emoji-icon {
justify-content: right;
}
}
}
</style>

View File

@ -46,14 +46,6 @@ const EmojiReactions = {
reactedWith (emoji) { reactedWith (emoji) {
return this.status.emoji_reactions.find(r => r.name === emoji).me return this.status.emoji_reactions.find(r => r.name === emoji).me
}, },
isLocalReaction (emojiUrl) {
if (!emojiUrl) return true
const reacted = this.accountsForEmoji[emojiUrl]
if (reacted.length === 0) {
return true
}
return reacted[0].is_local
},
fetchEmojiReactionsByIfMissing () { fetchEmojiReactionsByIfMissing () {
const hasNoAccounts = this.status.emoji_reactions.find(r => !r.accounts) const hasNoAccounts = this.status.emoji_reactions.find(r => !r.accounts)
if (hasNoAccounts) { if (hasNoAccounts) {

View File

@ -8,7 +8,6 @@
<button <button
class="emoji-reaction btn button-default" class="emoji-reaction btn button-default"
:class="{ 'picked-reaction': reactedWith(reaction.name), 'not-clickable': !loggedIn }" :class="{ 'picked-reaction': reactedWith(reaction.name), 'not-clickable': !loggedIn }"
:disabled="!isLocalReaction(reaction.url)"
@click="emojiOnClick(reaction.name, $event)" @click="emojiOnClick(reaction.name, $event)"
@mouseenter="fetchEmojiReactionsByIfMissing()" @mouseenter="fetchEmojiReactionsByIfMissing()"
> >

View File

@ -1,4 +1,5 @@
import Popover from '../popover/popover.vue' import Popover from '../popover/popover.vue'
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
faEllipsisH, faEllipsisH,
@ -6,7 +7,8 @@ import {
faEyeSlash, faEyeSlash,
faThumbtack, faThumbtack,
faShareAlt, faShareAlt,
faExternalLinkAlt faExternalLinkAlt,
faHistory
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
import { import {
faBookmark as faBookmarkReg, faBookmark as faBookmarkReg,
@ -21,19 +23,49 @@ library.add(
faThumbtack, faThumbtack,
faShareAlt, faShareAlt,
faExternalLinkAlt, faExternalLinkAlt,
faFlag faFlag,
faHistory
) )
const ExtraButtons = { const ExtraButtons = {
props: [ 'status' ], props: ['status'],
components: { Popover }, components: {
Popover,
ConfirmModal
},
data () {
return {
expanded: false,
showingDeleteDialog: false
}
},
methods: { methods: {
deleteStatus () { deleteStatus () {
const confirmed = window.confirm(this.$t('status.delete_confirm')) if (this.shouldConfirmDelete) {
if (confirmed) { this.showDeleteStatusConfirmDialog()
this.$store.dispatch('deleteStatus', { id: this.status.id }) } else {
this.doDeleteStatus()
} }
}, },
doDeleteStatus () {
this.$store.dispatch('deleteStatus', { id: this.status.id })
this.hideDeleteStatusConfirmDialog()
},
showDeleteStatusConfirmDialog () {
this.showingDeleteDialog = true
},
hideDeleteStatusConfirmDialog () {
this.showingDeleteDialog = false
},
translateStatus () {
if (this.noTranslationTargetSet) {
this.$store.dispatch('pushGlobalNotice', { messageKey: 'toast.no_translation_target_set', level: 'info' })
}
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 () { pinStatus () {
this.$store.dispatch('pinStatus', this.status.id) this.$store.dispatch('pinStatus', this.status.id)
.then(() => this.$emit('onSuccess')) .then(() => this.$emit('onSuccess'))
@ -71,6 +103,25 @@ const ExtraButtons = {
}, },
reportStatus () { reportStatus () {
this.$store.dispatch('openUserReportingModal', { userId: this.status.user.id, statusIds: [this.status.id] }) this.$store.dispatch('openUserReportingModal', { userId: this.status.user.id, statusIds: [this.status.id] })
},
editStatus () {
this.$store.dispatch('fetchStatusSource', { id: this.status.id })
.then(data => this.$store.dispatch('openEditStatusModal', {
statusId: this.status.id,
subject: data.spoiler_text,
statusText: data.text,
statusIsSensitive: this.status.nsfw,
statusPoll: this.status.poll,
statusFiles: [...this.status.attachments],
visibility: this.status.visibility,
statusContentType: data.content_type
}))
},
showStatusHistory () {
const originalStatus = { ...this.status }
const stripFieldsList = ['attachments', 'created_at', 'emojis', 'text', 'raw_html', 'nsfw', 'poll', 'summary', 'summary_raw_html']
stripFieldsList.forEach(p => delete originalStatus[p])
this.$store.dispatch('openStatusHistoryModal', originalStatus)
} }
}, },
computed: { computed: {
@ -89,9 +140,26 @@ const ExtraButtons = {
canMute () { canMute () {
return !!this.currentUser return !!this.currentUser
}, },
canTranslate () {
return this.$store.state.instance.translationEnabled === true
},
noTranslationTargetSet () {
return this.$store.getters.mergedConfig.translationLanguage === undefined
},
statusLink () { statusLink () {
return `${this.$store.state.instance.server}${this.$router.resolve({ name: 'conversation', params: { id: this.status.id } }).href}` if (this.status.is_local) {
} return `${this.$store.state.instance.server}${this.$router.resolve({ name: 'conversation', params: { id: this.status.id } }).href}`
} else {
return this.status.external_url
}
},
shouldConfirmDelete () {
return this.$store.getters.mergedConfig.modalOnDelete
},
isEdited () {
return this.status.edited_at !== null
},
editingAvailable () { return this.$store.state.instance.editingAvailable }
} }
} }

View File

@ -73,6 +73,28 @@
icon="bookmark" icon="bookmark"
/><span>{{ $t("status.unbookmark") }}</span> /><span>{{ $t("status.unbookmark") }}</span>
</button> </button>
<button
v-if="ownStatus && editingAvailable"
class="button-default dropdown-item dropdown-item-icon"
@click.prevent="editStatus"
@click="close"
>
<FAIcon
fixed-width
icon="pen"
/><span>{{ $t("status.edit") }}</span>
</button>
<button
v-if="isEdited && editingAvailable"
class="button-default dropdown-item dropdown-item-icon"
@click.prevent="showStatusHistory"
@click="close"
>
<FAIcon
fixed-width
icon="history"
/><span>{{ $t("status.edit_history") }}</span>
</button>
<button <button
v-if="canDelete" v-if="canDelete"
class="button-default dropdown-item dropdown-item-icon" class="button-default dropdown-item dropdown-item-icon"
@ -116,6 +138,27 @@
:icon="['far', 'flag']" :icon="['far', 'flag']"
/><span>{{ $t("user_card.report") }}</span> /><span>{{ $t("user_card.report") }}</span>
</button> </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>
<template v-if="noTranslationTargetSet">
<span class="dropdown-item-icon__badge warning">
<FAIcon
fixed-width
icon="exclamation-triangle"
name="test"
/>
</span>
</template>
</button>
</div> </div>
</template> </template>
<template v-slot:trigger> <template v-slot:trigger>
@ -125,6 +168,18 @@
icon="ellipsis-h" icon="ellipsis-h"
/> />
</button> </button>
<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> </template>
</Popover> </Popover>
</template> </template>

View File

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

View File

@ -7,6 +7,27 @@
@click="onClick" @click="onClick"
> >
{{ label }} {{ 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> </button>
</template> </template>

View File

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

View File

@ -1,5 +1,10 @@
<template> <template>
<div> <div>
<FAIcon
v-if="globeIcon"
icon="globe"
/>
{{ ' ' }}
<label for="interface-language-switcher"> <label for="interface-language-switcher">
{{ promptText }} {{ promptText }}
</label> </label>
@ -39,6 +44,10 @@ export default {
setLanguage: { setLanguage: {
type: Function, type: Function,
required: true required: true
},
globeIcon: {
type: Boolean,
default: true
} }
}, },
computed: { computed: {

View File

@ -1,7 +1,9 @@
import SideDrawer from '../side_drawer/side_drawer.vue' import SideDrawer from '../side_drawer/side_drawer.vue'
import Notifications from '../notifications/notifications.vue' import Notifications from '../notifications/notifications.vue'
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
import { unseenNotificationsFromStore } from '../../services/notification_utils/notification_utils' import { unseenNotificationsFromStore } from '../../services/notification_utils/notification_utils'
import GestureService from '../../services/gesture_service/gesture_service' import GestureService from '../../services/gesture_service/gesture_service'
import { mapGetters } from 'vuex'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
faTimes, faTimes,
@ -18,11 +20,13 @@ library.add(
const MobileNav = { const MobileNav = {
components: { components: {
SideDrawer, SideDrawer,
Notifications Notifications,
ConfirmModal
}, },
data: () => ({ data: () => ({
notificationsCloseGesture: undefined, notificationsCloseGesture: undefined,
notificationsOpen: false notificationsOpen: false,
showingConfirmLogout: false
}), }),
created () { created () {
this.notificationsCloseGesture = GestureService.swipeGesture( this.notificationsCloseGesture = GestureService.swipeGesture(
@ -41,8 +45,17 @@ const MobileNav = {
unseenNotificationsCount () { unseenNotificationsCount () {
return this.unseenNotifications.length return this.unseenNotifications.length
}, },
hideSitename () { return this.$store.state.instance.hideSitename }, mergedConfig () {
sitename () { return this.$store.state.instance.name } return this.$store.getters.mergedConfig
},
hideSiteName () {
return this.mergedConfig.hideSiteName
},
sitename () { return this.$store.state.instance.name },
shouldConfirmLogout () {
return this.$store.getters.mergedConfig.modalOnLogout
},
...mapGetters(['unreadChatCount'])
}, },
methods: { methods: {
toggleMobileSidebar () { toggleMobileSidebar () {
@ -68,9 +81,23 @@ const MobileNav = {
scrollToTop () { scrollToTop () {
window.scrollTo(0, 0) window.scrollTo(0, 0)
}, },
showConfirmLogout () {
this.showingConfirmLogout = true
},
hideConfirmLogout () {
this.showingConfirmLogout = false
},
logout () { logout () {
if (!this.shouldConfirmLogout) {
this.doLogout()
} else {
this.showConfirmLogout()
}
},
doLogout () {
this.$router.replace('/main/public') this.$router.replace('/main/public')
this.$store.dispatch('logout') this.$store.dispatch('logout')
this.hideConfirmLogout()
}, },
markNotificationsAsSeen () { markNotificationsAsSeen () {
// this.$refs.notifications.markAsSeen() // this.$refs.notifications.markAsSeen()

View File

@ -22,7 +22,7 @@
/> />
</button> </button>
<router-link <router-link
v-if="!hideSitename" v-if="!hideSiteName"
class="site-name" class="site-name"
:to="{ name: 'root' }" :to="{ name: 'root' }"
active-class="home" active-class="home"
@ -76,6 +76,18 @@
ref="sideDrawer" ref="sideDrawer"
:logout="logout" :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> </div>
</template> </template>
@ -206,6 +218,14 @@
} }
} }
} }
.confirm-modal.dark-overlay {
&::before {
z-index: 3000;
}
.dialog-modal.panel {
z-index: 3001;
}
}
} }
</style> </style>

View File

@ -13,13 +13,13 @@
<span v-if="user.is_local"> <span v-if="user.is_local">
<button <button
class="button-default dropdown-item" class="button-default dropdown-item"
@click="toggleRight(&quot;admin&quot;)" @click="toggleRight('admin')"
> >
{{ $t(!!user.rights.admin ? 'user_card.admin_menu.revoke_admin' : 'user_card.admin_menu.grant_admin') }} {{ $t(!!user.rights.admin ? 'user_card.admin_menu.revoke_admin' : 'user_card.admin_menu.grant_admin') }}
</button> </button>
<button <button
class="button-default dropdown-item" class="button-default dropdown-item"
@click="toggleRight(&quot;moderator&quot;)" @click="toggleRight('moderator')"
> >
{{ $t(!!user.rights.moderator ? 'user_card.admin_menu.revoke_moderator' : 'user_card.admin_menu.grant_moderator') }} {{ $t(!!user.rights.moderator ? 'user_card.admin_menu.revoke_moderator' : 'user_card.admin_menu.grant_moderator') }}
</button> </button>
@ -167,6 +167,7 @@
.moderation-tools-popover { .moderation-tools-popover {
height: 100%; height: 100%;
z-index: 999;
.trigger { .trigger {
display: flex !important; display: flex !important;
height: 100%; height: 100%;

View File

@ -1,6 +1,6 @@
<template> <template>
<div <div
v-if="federationPolicy" v-if="hasInstanceSpecificPolicies"
class="mrf-transparency-panel" class="mrf-transparency-panel"
> >
<div class="panel panel-default base01-background"> <div class="panel panel-default base01-background">

View File

@ -10,7 +10,7 @@ import {
faChevronDown, faChevronDown,
faChevronUp, faChevronUp,
faComments, faComments,
faBell, faBolt,
faInfoCircle, faInfoCircle,
faStream, faStream,
faList, faList,
@ -25,7 +25,7 @@ library.add(
faChevronDown, faChevronDown,
faChevronUp, faChevronUp,
faComments, faComments,
faBell, faBolt,
faInfoCircle, faInfoCircle,
faStream, faStream,
faList, faList,

View File

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

View File

@ -5,6 +5,7 @@ import UserAvatar from '../user_avatar/user_avatar.vue'
import UserCard from '../user_card/user_card.vue' import UserCard from '../user_card/user_card.vue'
import Timeago from '../timeago/timeago.vue' import Timeago from '../timeago/timeago.vue'
import RichContent from 'src/components/rich_content/rich_content.jsx' import RichContent from 'src/components/rich_content/rich_content.jsx'
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
import { isStatusNotification } from '../../services/notification_utils/notification_utils.js' import { isStatusNotification } from '../../services/notification_utils/notification_utils.js'
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js' import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
@ -36,7 +37,9 @@ const Notification = {
return { return {
userExpanded: false, userExpanded: false,
betterShadow: this.$store.state.interface.browserSupport.cssFilter, betterShadow: this.$store.state.interface.browserSupport.cssFilter,
unmuted: false unmuted: false,
showingApproveConfirmDialog: false,
showingDenyConfirmDialog: false
} }
}, },
props: [ 'notification' ], props: [ 'notification' ],
@ -46,7 +49,8 @@ const Notification = {
UserCard, UserCard,
Timeago, Timeago,
Status, Status,
RichContent RichContent,
ConfirmModal
}, },
methods: { methods: {
toggleUserExpanded () { toggleUserExpanded () {
@ -61,7 +65,26 @@ const Notification = {
toggleMute () { toggleMute () {
this.unmuted = !this.unmuted this.unmuted = !this.unmuted
}, },
showApproveConfirmDialog () {
this.showingApproveConfirmDialog = true
},
hideApproveConfirmDialog () {
this.showingApproveConfirmDialog = false
},
showDenyConfirmDialog () {
this.showingDenyConfirmDialog = true
},
hideDenyConfirmDialog () {
this.showingDenyConfirmDialog = false
},
approveUser () { approveUser () {
if (this.shouldConfirmApprove) {
this.showApproveConfirmDialog()
} else {
this.doApprove()
}
},
doApprove () {
this.$store.state.api.backendInteractor.approveUser({ id: this.user.id }) this.$store.state.api.backendInteractor.approveUser({ id: this.user.id })
this.$store.dispatch('removeFollowRequest', this.user) this.$store.dispatch('removeFollowRequest', this.user)
this.$store.dispatch('markSingleNotificationAsSeen', { id: this.notification.id }) this.$store.dispatch('markSingleNotificationAsSeen', { id: this.notification.id })
@ -71,13 +94,22 @@ const Notification = {
notification.type = 'follow' notification.type = 'follow'
} }
}) })
this.hideApproveConfirmDialog()
}, },
denyUser () { denyUser () {
if (this.shouldConfirmDeny) {
this.showDenyConfirmDialog()
} else {
this.doDeny()
}
},
doDeny () {
this.$store.state.api.backendInteractor.denyUser({ id: this.user.id }) this.$store.state.api.backendInteractor.denyUser({ id: this.user.id })
.then(() => { .then(() => {
this.$store.dispatch('dismissNotificationLocal', { id: this.notification.id }) this.$store.dispatch('dismissNotificationLocal', { id: this.notification.id })
this.$store.dispatch('removeFollowRequest', this.user) this.$store.dispatch('removeFollowRequest', this.user)
}) })
this.hideDenyConfirmDialog()
} }
}, },
computed: { computed: {
@ -107,6 +139,15 @@ const Notification = {
isStatusNotification () { isStatusNotification () {
return isStatusNotification(this.notification.type) return isStatusNotification(this.notification.type)
}, },
mergedConfig () {
return this.$store.getters.mergedConfig
},
shouldConfirmApprove () {
return this.mergedConfig.modalOnApproveFollow
},
shouldConfirmDeny () {
return this.mergedConfig.modalOnDenyFollow
},
...mapState({ ...mapState({
currentUser: state => state.users.currentUser currentUser: state => state.users.currentUser
}) })

View File

@ -231,6 +231,28 @@
</template> </template>
</div> </div>
</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>
</div> </div>
</template> </template>

View File

@ -55,6 +55,14 @@ const pxStringToNumber = (str) => {
const PostStatusForm = { const PostStatusForm = {
props: [ props: [
'statusId',
'statusText',
'statusIsSensitive',
'statusPoll',
'statusFiles',
'statusMediaDescriptions',
'statusScope',
'statusContentType',
'replyTo', 'replyTo',
'quoteId', 'quoteId',
'repliedUser', 'repliedUser',
@ -63,6 +71,7 @@ const PostStatusForm = {
'subject', 'subject',
'disableSubject', 'disableSubject',
'disableScopeSelector', 'disableScopeSelector',
'disableVisibilitySelector',
'disableNotice', 'disableNotice',
'disableLockWarning', 'disableLockWarning',
'disablePolls', 'disablePolls',
@ -120,23 +129,40 @@ const PostStatusForm = {
const { postContentType: contentType, sensitiveByDefault, sensitiveIfSubject } = this.$store.getters.mergedConfig const { postContentType: contentType, sensitiveByDefault, sensitiveIfSubject } = this.$store.getters.mergedConfig
let statusParams = {
spoilerText: this.subject || '',
status: statusText,
sensitiveByDefault,
nsfw: !!sensitiveByDefault,
files: [],
poll: {},
mediaDescriptions: {},
visibility: this.suggestedVisibility(),
contentType
}
if (this.statusId) {
const statusContentType = this.statusContentType || contentType
statusParams = {
spoilerText: this.subject || '',
status: this.statusText || '',
sensitiveIfSubject,
nsfw: this.statusIsSensitive || !!sensitiveByDefault,
files: this.statusFiles || [],
poll: this.statusPoll || {},
mediaDescriptions: this.statusMediaDescriptions || {},
visibility: this.statusScope || this.suggestedVisibility(),
contentType: statusContentType
}
}
return { return {
dropFiles: [], dropFiles: [],
uploadingFiles: false, uploadingFiles: false,
error: null, error: null,
posting: false, posting: false,
highlighted: 0, highlighted: 0,
newStatus: { newStatus: statusParams,
spoilerText: this.subject || '',
status: statusText,
sensitiveIfSubject,
nsfw: !!sensitiveByDefault,
files: [],
poll: {},
mediaDescriptions: {},
visibility: this.suggestedVisibility(),
contentType
},
caret: 0, caret: 0,
pollFormVisible: false, pollFormVisible: false,
showDropIcon: 'hide', showDropIcon: 'hide',
@ -232,6 +258,9 @@ const PostStatusForm = {
uploadFileLimitReached () { uploadFileLimitReached () {
return this.newStatus.files.length >= this.fileLimit return this.newStatus.files.length >= this.fileLimit
}, },
isEdit () {
return typeof this.statusId !== 'undefined' && this.statusId.trim() !== ''
},
...mapGetters(['mergedConfig']), ...mapGetters(['mergedConfig']),
...mapState({ ...mapState({
mobileLayout: state => state.interface.mobileLayout mobileLayout: state => state.interface.mobileLayout

View File

@ -66,6 +66,13 @@
<span v-if="safeDMEnabled">{{ $t('post_status.direct_warning_to_first_only') }}</span> <span v-if="safeDMEnabled">{{ $t('post_status.direct_warning_to_first_only') }}</span>
<span v-else>{{ $t('post_status.direct_warning_to_all') }}</span> <span v-else>{{ $t('post_status.direct_warning_to_all') }}</span>
</p> </p>
<div
v-if="isEdit"
class="visibility-notice edit-warning"
>
<p>{{ $t('post_status.edit_remote_warning') }}</p>
<p>{{ $t('post_status.edit_unsupported_warning') }}</p>
</div>
<div <div
v-if="!disablePreview" v-if="!disablePreview"
class="preview-heading faint" class="preview-heading faint"
@ -180,6 +187,7 @@
class="visibility-tray" class="visibility-tray"
> >
<scope-selector <scope-selector
v-if="!disableVisibilitySelector"
:show-all="showAllScopes" :show-all="showAllScopes"
:user-default="userDefaultScope" :user-default="userDefaultScope"
:original-scope="copyMessageScope" :original-scope="copyMessageScope"
@ -420,6 +428,16 @@
align-items: baseline; align-items: baseline;
} }
.visibility-notice.edit-warning {
> :first-child {
margin-top: 0;
}
> :last-child {
margin-bottom: 0;
}
}
.media-upload-icon, .poll-icon, .emoji-icon { .media-upload-icon, .poll-icon, .emoji-icon {
font-size: 1.85em; font-size: 1.85em;
line-height: 1.1; line-height: 1.1;

View File

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

View File

@ -33,6 +33,18 @@
> >
{{ status.repeat_num }} {{ status.repeat_num }}
</span> </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> </div>
</template> </template>

View File

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

View File

@ -71,5 +71,11 @@
display: flex; display: flex;
flex-grow: 1; flex-grow: 1;
} }
.logout-button {
position: absolute;
right: 20px;
padding-right: 10px;
}
} }
} }

View File

@ -111,6 +111,20 @@
id="unscrolled-content" id="unscrolled-content"
class="extra-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>
</div> </div>
</Modal> </Modal>

View File

@ -43,6 +43,11 @@ const GeneralTab = {
value: mode, value: mode,
label: this.$t(`settings.third_column_mode_${mode}`) label: this.$t(`settings.third_column_mode_${mode}`)
})), })),
userProfileDefaultTabOptions: ['statuses', 'replies'].map(tab => ({
key: tab,
value: tab,
label: this.$t(`user_card.${tab}`)
})),
loopSilentAvailable: loopSilentAvailable:
// Firefox // Firefox
Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') || Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') ||
@ -50,6 +55,7 @@ const GeneralTab = {
Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'webkitAudioDecodedByteCount') || Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'webkitAudioDecodedByteCount') ||
// Future spec, still not supported in Nightly 63 as of 08/2018 // Future spec, still not supported in Nightly 63 as of 08/2018
Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'audioTracks') Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'audioTracks')
} }
}, },
components: { components: {
@ -82,11 +88,23 @@ const GeneralTab = {
this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val }) this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
} }
}, },
translationLanguages () {
return (this.$store.getters.mergedConfig.supportedTranslationLanguages.target || []).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() ...SharedComputedObject()
}, },
methods: { methods: {
changeDefaultScope (value) { changeDefaultScope (value) {
this.$store.dispatch('setServerSideOption', { name: 'defaultScope', value }) this.$store.dispatch('setServerSideOption', { name: 'defaultScope', value })
},
setTranslationLanguage (value) {
this.$store.dispatch('setOption', { name: 'translationLanguage', value })
} }
} }
} }

View File

@ -15,16 +15,51 @@
{{ $t('settings.hide_isp') }} {{ $t('settings.hide_isp') }}
</BooleanSetting> </BooleanSetting>
</li> </li>
<li>
<BooleanSetting path="sidebarRight">
{{ $t('settings.right_sidebar') }}
</BooleanSetting>
</li>
<li v-if="instanceWallpaperUsed"> <li v-if="instanceWallpaperUsed">
<BooleanSetting path="hideInstanceWallpaper"> <BooleanSetting path="hideInstanceWallpaper">
{{ $t('settings.hide_wallpaper') }} {{ $t('settings.hide_wallpaper') }}
</BooleanSetting> </BooleanSetting>
</li> </li>
<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="showPanelNavShortcuts"
expert="1"
>
{{ $t('settings.show_panel_nav_shortcuts') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="showWiderShortcuts"
expert="1"
>
{{ $t('settings.show_wider_shortcuts') }}
</BooleanSetting>
</li>
<li> <li>
<BooleanSetting path="stopGifs"> <BooleanSetting path="stopGifs">
{{ $t('settings.stop_gifs') }} {{ $t('settings.stop_gifs') }}
@ -64,41 +99,7 @@
{{ $t('settings.virtual_scrolling') }} {{ $t('settings.virtual_scrolling') }}
</BooleanSetting> </BooleanSetting>
</li> </li>
<li>
<BooleanSetting path="disableStickyHeaders">
{{ $t('settings.disable_sticky_headers') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="showScrollbars">
{{ $t('settings.show_scrollbars') }}
</BooleanSetting>
</li>
<li>
<ChoiceSetting
v-if="user"
id="thirdColumnMode"
path="thirdColumnMode"
:options="thirdColumnModeOptions"
>
{{ $t('settings.third_column_mode') }}
</ChoiceSetting>
</li>
<li>
<BooleanSetting path="minimalScopesMode">
{{ $t('settings.minimal_scopes_mode') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="sensitiveByDefault">
{{ $t('settings.sensitive_by_default') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="sensitiveIfSubject">
{{ $t('settings.sensitive_if_subject') }}
</BooleanSetting>
</li>
<li> <li>
<BooleanSetting path="renderMisskeyMarkdown"> <BooleanSetting path="renderMisskeyMarkdown">
{{ $t('settings.render_mfm') }} {{ $t('settings.render_mfm') }}
@ -116,6 +117,25 @@
</li> </li>
</ul> </ul>
</li> </li>
<li>
<ChoiceSetting
id="userProfileDefaultTab"
path="userProfileDefaultTab"
:options="userProfileDefaultTabOptions"
>
{{ $t('settings.user_profile_default_tab') }}
</ChoiceSetting>
</li>
<li>
<ChoiceSetting
v-if="user && (translationLanguages.length > 0)"
id="translationLanguage"
path="translationLanguage"
:options="translationLanguages"
>
{{ $t('settings.translation_language') }}
</ChoiceSetting>
</li>
<li> <li>
<BooleanSetting <BooleanSetting
path="alwaysShowNewPostButton" path="alwaysShowNewPostButton"
@ -132,6 +152,77 @@
{{ $t('settings.autohide_floating_post_button') }} {{ $t('settings.autohide_floating_post_button') }}
</BooleanSetting> </BooleanSetting>
</li> </li>
<li>
<h3>{{ $t('settings.columns') }}</h3>
</li>
<li>
<BooleanSetting path="disableStickyHeaders">
{{ $t('settings.disable_sticky_headers') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="showScrollbars">
{{ $t('settings.show_scrollbars') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="sidebarRight">
{{ $t('settings.right_sidebar') }}
</BooleanSetting>
</li>
<li>
<ChoiceSetting
v-if="user"
id="thirdColumnMode"
path="thirdColumnMode"
:options="thirdColumnModeOptions"
>
{{ $t('settings.third_column_mode') }}
</ChoiceSetting>
</li>
<li>
<h3>{{ $t('settings.confirmation_dialogs') }}</h3>
</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="modalOnApproveFollow">
{{ $t('settings.confirm_dialogs_approve_follow') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="modalOnDenyFollow">
{{ $t('settings.confirm_dialogs_deny_follow') }}
</BooleanSetting>
</li>
</ul>
</li>
</ul> </ul>
</div> </div>
<div class="setting-item"> <div class="setting-item">
@ -366,12 +457,22 @@
/> />
</label> </label>
</li> </li>
<li>
<BooleanSetting path="minimalScopesMode">
{{ $t('settings.minimal_scopes_mode') }}
</BooleanSetting>
</li>
<li> <li>
<!-- <BooleanSetting path="serverSide_defaultNSFW"> --> <!-- <BooleanSetting path="serverSide_defaultNSFW"> -->
<BooleanSetting path="sensitiveByDefault"> <BooleanSetting path="sensitiveByDefault">
{{ $t('settings.sensitive_by_default') }} {{ $t('settings.sensitive_by_default') }}
</BooleanSetting> </BooleanSetting>
</li> </li>
<li>
<BooleanSetting path="sensitiveIfSubject">
{{ $t('settings.sensitive_if_subject') }}
</BooleanSetting>
</li>
<li> <li>
<BooleanSetting <BooleanSetting
path="scopeCopy" path="scopeCopy"

View File

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

View File

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

View File

@ -437,6 +437,12 @@ const Status = {
}, },
visibilityLocalized () { visibilityLocalized () {
return this.$i18n.t('general.scope_in_timeline.' + this.status.visibility) return this.$i18n.t('general.scope_in_timeline.' + this.status.visibility)
},
isEdited () {
return this.status.edited_at !== null
},
editingAvailable () {
return this.$store.state.instance.editingAvailable
} }
}, },
methods: { methods: {

View File

@ -160,7 +160,8 @@
margin-right: 0.2em; margin-right: 0.2em;
} }
& .heading-reply-row { & .heading-reply-row,
& .heading-edited-row {
position: relative; position: relative;
align-content: baseline; align-content: baseline;
font-size: 0.85em; font-size: 0.85em;

View File

@ -329,6 +329,24 @@
class="mentions-line" class="mentions-line"
/> />
</div> </div>
<div
v-if="isEdited && editingAvailable && !isPreview"
class="heading-edited-row"
>
<i18n-t
keypath="status.edited_at"
tag="span"
>
<template #time>
<Timeago
:time="status.edited_at"
:auto-update="60"
:long-format="true"
:with-direction="true"
/>
</template>
</i18n-t>
</div>
</div> </div>
<StatusContent <StatusContent

View File

@ -9,6 +9,7 @@ import {
faLink, faLink,
faPollH faPollH
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
import Select from 'src/components/select/select.vue'
library.add( library.add(
faFile, faFile,
@ -39,7 +40,8 @@ const StatusContent = {
return { return {
postLength: this.status.text.length, postLength: this.status.text.length,
parseReadyDone: false, parseReadyDone: false,
renderMisskeyMarkdown renderMisskeyMarkdown,
translateFrom: null
} }
}, },
computed: { computed: {
@ -80,10 +82,14 @@ const StatusContent = {
attachmentTypes () { attachmentTypes () {
return this.status.attachments.map(file => fileType.fileType(file.mimetype)) return this.status.attachments.map(file => fileType.fileType(file.mimetype))
}, },
translationLanguages () {
return (this.$store.getters.mergedConfig.supportedTranslationLanguages.source || []).map(lang => ({ key: lang.code, value: lang.code, label: lang.name }))
},
...mapGetters(['mergedConfig']) ...mapGetters(['mergedConfig'])
}, },
components: { components: {
RichContent RichContent,
Select
}, },
mounted () { mounted () {
this.status.attentions && this.status.attentions.forEach(attn => { this.status.attentions && this.status.attentions.forEach(attn => {
@ -126,6 +132,10 @@ const StatusContent = {
}, },
generateTagLink (tag) { generateTagLink (tag) {
return `/tag/${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,12 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
.translation {
border: 1px solid var(--accent, $fallback--link);
border-radius: var(--panelRadius, $fallback--panelRadius);
margin-top: 1em;
padding: 0.5em;
}
.emoji { .emoji {
--_still_image-label-scale: 0.5; --_still_image-label-scale: 0.5;
--emoji-size: 38px; --emoji-size: 38px;

View File

@ -56,6 +56,44 @@
:attentions="status.attentions" :attentions="status.attentions"
@parseReady="onParseReady" @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>
</div> </div>
<button <button
v-show="hideSubjectStatus" v-show="hideSubjectStatus"

View File

@ -103,6 +103,9 @@ const StatusContent = {
renderMfmOnHover () { renderMfmOnHover () {
return this.mergedConfig.renderMfmOnHover return this.mergedConfig.renderMfmOnHover
}, },
renderMisskeyMarkdown () {
return this.mergedConfig.renderMisskeyMarkdown
},
...mapGetters(['mergedConfig']), ...mapGetters(['mergedConfig']),
...mapState({ ...mapState({
currentUser: state => state.users.currentUser currentUser: state => state.users.currentUser

View File

@ -1,7 +1,7 @@
<template> <template>
<div <div
class="StatusContent" class="StatusContent"
:class="{ '-compact': compact, 'mfm-hover': renderMfmOnHover }" :class="{ '-compact': compact, 'mfm-hover': renderMfmOnHover, 'mfm-disabled': !renderMisskeyMarkdown }"
> >
<slot name="header" /> <slot name="header" />
<StatusBody <StatusBody
@ -78,7 +78,12 @@
&.mfm-hover:not(:hover) { &.mfm-hover:not(:hover) {
.mfm { .mfm {
animation: none; animation: none !important;
}
}
&.mfm-disabled {
.mfm {
animation: none !important;
} }
} }
} }

View File

@ -0,0 +1,60 @@
import { get } from 'lodash'
import Modal from '../modal/modal.vue'
import Status from '../status/status.vue'
const StatusHistoryModal = {
components: {
Modal,
Status
},
data () {
return {
statuses: []
}
},
computed: {
modalActivated () {
return this.$store.state.statusHistory.modalActivated
},
params () {
return this.$store.state.statusHistory.params
},
statusId () {
return this.params.id
},
historyCount () {
return this.statuses.length
},
history () {
return this.statuses
}
},
watch: {
params (newVal, oldVal) {
const newStatusId = get(newVal, 'id') !== get(oldVal, 'id')
if (newStatusId) {
this.resetHistory()
}
if (newStatusId || get(newVal, 'edited_at') !== get(oldVal, 'edited_at')) {
this.fetchStatusHistory()
}
}
},
methods: {
resetHistory () {
this.statuses = []
},
fetchStatusHistory () {
this.$store.dispatch('fetchStatusHistory', this.params)
.then(data => {
this.statuses = data
})
},
closeModal () {
this.$store.dispatch('closeStatusHistoryModal')
}
}
}
export default StatusHistoryModal

View File

@ -0,0 +1,46 @@
<template>
<Modal
v-if="modalActivated"
class="status-history-modal-view"
@backdropClicked="closeModal"
>
<div class="status-history-modal-panel panel">
<div class="panel-heading">
{{ $tc('status.edit_history_modal_title', historyCount - 1, { historyCount: historyCount - 1 }) }}
</div>
<div class="panel-body">
<div
v-if="historyCount > 0"
class="history-body"
>
<status
v-for="status in history"
:key="status.id"
:statusoid="status"
:isPreview="true"
class="conversation-status status-fadein panel-body"
/>
</div>
</div>
</div>
</Modal>
</template>
<script src="./status_history_modal.js"></script>
<style lang="scss">
.modal-view.status-history-modal-view {
align-items: flex-start;
}
.status-history-modal-panel {
flex-shrink: 0;
margin-top: 25%;
margin-bottom: 2em;
width: 100%;
max-width: 700px;
@media (orientation: landscape) {
margin-top: 8%;
}
}
</style>

View File

@ -2,6 +2,7 @@ import Status from '../status/status.vue'
import timelineFetcher from '../../services/timeline_fetcher/timeline_fetcher.service.js' import timelineFetcher from '../../services/timeline_fetcher/timeline_fetcher.service.js'
import Conversation from '../conversation/conversation.vue' import Conversation from '../conversation/conversation.vue'
import TimelineMenu from '../timeline_menu/timeline_menu.vue' import TimelineMenu from '../timeline_menu/timeline_menu.vue'
import TimelineMenuTabs from '../timeline_menu_tabs/timeline_menu_tabs.vue'
import TimelineQuickSettings from './timeline_quick_settings.vue' import TimelineQuickSettings from './timeline_quick_settings.vue'
import { debounce, throttle, keyBy } from 'lodash' import { debounce, throttle, keyBy } from 'lodash'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
@ -39,6 +40,7 @@ const Timeline = {
Status, Status,
Conversation, Conversation,
TimelineMenu, TimelineMenu,
TimelineMenuTabs,
TimelineQuickSettings TimelineQuickSettings
}, },
computed: { computed: {
@ -85,6 +87,9 @@ const Timeline = {
}, },
virtualScrollingEnabled () { virtualScrollingEnabled () {
return this.$store.getters.mergedConfig.virtualScrolling return this.$store.getters.mergedConfig.virtualScrolling
},
showPanelNavShortcuts () {
return this.$store.getters.mergedConfig.showPanelNavShortcuts
} }
}, },
created () { created () {
@ -145,7 +150,9 @@ const Timeline = {
this.$store.commit('showNewStatuses', { timeline: this.timelineName }) this.$store.commit('showNewStatuses', { timeline: this.timelineName })
this.paused = false this.paused = false
} }
window.scrollTo({ top: 0 }) if (!this.inProfile) {
window.scrollTo({ top: 0 })
}
}, },
fetchOlderStatuses: throttle(function () { fetchOlderStatuses: throttle(function () {
const store = this.$store const store = this.$store

View File

@ -1,7 +1,11 @@
<template> <template>
<div :class="['Timeline', classes.root]"> <div :class="['Timeline', classes.root]">
<div :class="classes.header"> <div :class="classes.header">
<TimelineMenu v-if="!embedded" /> <template v-if="!embedded">
<TimelineMenuTabs v-if="showPanelNavShortcuts" />
<TimelineMenu v-else />
</template>
<button <button
v-if="showLoadButton" v-if="showLoadButton"
class="button-default loadmore-button" class="button-default loadmore-button"

View File

@ -0,0 +1,30 @@
import { mapState } from 'vuex'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faUsers,
faGlobe,
faBookmark,
faEnvelope,
faHome
} from '@fortawesome/free-solid-svg-icons'
import { faCircle } from '@fortawesome/free-regular-svg-icons'
library.add(
faUsers,
faGlobe,
faBookmark,
faEnvelope,
faHome,
faCircle
)
const TimelineMenuContent = {
computed: {
...mapState({
currentUser: state => state.users.currentUser,
privateMode: state => state.instance.private,
federating: state => state.instance.federating
})
}
}
export default TimelineMenuContent

View File

@ -0,0 +1,114 @@
<template>
<ul>
<li v-if="currentUser">
<router-link
class="menu-item"
:to="{ name: 'friends' }"
>
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding "
icon="home"
/>
<span
:title="$t('nav.home_timeline_description')"
:aria-label="$t('nav.home_timeline_description')"
>{{ $t("nav.home_timeline") }}</span>
</router-link>
</li>
<li v-if="currentUser">
<router-link
class="menu-item"
:to="{ name: 'bubble-timeline' }"
>
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding "
:icon="['far', 'circle']"
/>
<span
:title="$t('nav.bubble_timeline_description')"
:aria-label="$t('nav.bubble_timeline_description')"
>{{ $t("nav.bubble_timeline") }}</span>
</router-link>
</li>
<li v-if="currentUser || !privateMode">
<router-link
class="menu-item"
:to="{ name: 'public-timeline' }"
>
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding "
icon="users"
/>
<span
:title="$t('nav.public_timeline_description')"
:aria-label="$t('nav.public_timeline_description')"
>{{ $t("nav.public_tl") }}</span>
</router-link>
</li>
<li v-if="federating && (currentUser || !privateMode)">
<router-link
class="menu-item"
:to="{ name: 'public-external-timeline' }"
>
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding "
icon="globe"
/>
<span
:title="$t('nav.twkn_timeline_description')"
:aria-label="$t('nav.twkn_timeline_description')"
>{{ $t("nav.twkn") }}</span>
</router-link>
</li>
<li v-if="currentUser">
<router-link
class="menu-item"
:to="{ name: 'bookmarks'}"
>
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding "
icon="bookmark"
/>
<span
:title="$t('nav.bookmarks')"
:aria-label="$t('nav.bookmarks')"
>{{ $t("nav.bookmarks") }}</span>
</router-link>
</li>
<li v-if="currentUser">
<router-link
class="menu-item"
:to="{ name: 'dms', params: { username: currentUser.screen_name } }"
>
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding "
icon="envelope"
/>
<span
:title="$t('nav.dms')"
:aria-label="$t('nav.dms')"
>{{ $t("nav.dms") }}</span>
</router-link>
</li>
</ul>
</template>
<script src="./timeline_menu_content.js" ></script>
<style lang="scss">
@import "../../_variables.scss";
.timeline-desc {
text-decoration: none;
color: var(--text, $fallback--text);
padding-left: 1em;
display: block;
background-color: scale(var(--bg, $fallback--bg), 0.1);
padding-bottom: 0.4em;
}
</style>

View File

@ -0,0 +1,61 @@
import Popover from '../popover/popover.vue'
import TimelineMenuContent from './timeline_menu_content.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faChevronDown
} from '@fortawesome/free-solid-svg-icons'
library.add(faChevronDown)
// Route -> i18n key mapping, exported and not in the computed
// because nav panel benefits from the same information.
export const timelineNames = () => {
return {
'friends': 'nav.home_timeline',
'bookmarks': 'nav.bookmarks',
'dms': 'nav.dms',
'public-timeline': 'nav.public_tl',
'public-external-timeline': 'nav.twkn',
'bubble-timeline': 'nav.bubble_timeline'
}
}
const TimelineMenuTabs = {
components: {
Popover,
TimelineMenuContent
},
data () {
return {
isOpen: false
}
},
created () {
if (timelineNames()[this.$route.name]) {
this.$store.dispatch('setLastTimeline', this.$route.name)
}
},
computed: {
currentUser () {
return this.$store.state.users.currentUser
},
privateMode () {
return this.$store.state.instance.private
}
},
methods: {
timelineName () {
const route = this.$route.name
if (route === 'tag-timeline') {
return '#' + this.$route.params.tag
}
if (route === 'list-timeline') {
return this.$store.getters.findListTitle(this.$route.params.id)
}
const i18nkey = timelineNames()[this.$route.name]
return i18nkey ? this.$t(i18nkey) : route
}
}
}
export default TimelineMenuTabs

View File

@ -0,0 +1,96 @@
<template>
<div
v-if="currentUser || !privateMode"
class="nav-items timeline-menu-tabs"
>
<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_description')"
/>
</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_timeline_description')"
/>
</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="['far', 'circle']"
:title="$t('nav.bubble_timeline_description')"
/>
</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_timeline_description')"
/>
</router-link>
<span class="timeline-title">{{ timelineName() }}</span>
</div>
</template>
<script src="./timeline_menu_tabs.js" ></script>
<style lang="scss">
@import '../../_variables.scss';
.timeline-menu-tabs {
.nav-icon {
margin-left: 0.7em;
width: 2em;
height: 100%;
text-align: center;
&.router-link-active {
border-bottom: 2px solid var(--selectedMenuText, $fallback--text);
padding-bottom: 0.8em;
.svg-inline--fa {
font-weight: bolder;
color: $fallback--text;
color: var(--selectedMenuText, $fallback--text);
--lightText: var(--selectedMenuLightText, $fallback--lightText);
}
}
}
.timeline-title {
margin-left: 1em;
font-size: 1.2em;
font-weight: bold;
color: var(--selectedMenuText, $fallback--text);
color: var(--selectedMenuText, $fallback--text);
--lightText: var(--selectedMenuLightText, $fallback--lightText);
}
@media all and (max-width: 900px) {
.timeline-title {
display: none;
}
}
}
</style>

View File

@ -6,6 +6,7 @@ import ModerationTools from '../moderation_tools/moderation_tools.vue'
import AccountActions from '../account_actions/account_actions.vue' import AccountActions from '../account_actions/account_actions.vue'
import Select from '../select/select.vue' import Select from '../select/select.vue'
import RichContent from 'src/components/rich_content/rich_content.jsx' 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 generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
import { mapGetters } from 'vuex' import { mapGetters } from 'vuex'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
@ -32,7 +33,8 @@ export default {
data () { data () {
return { return {
followRequestInProgress: false, followRequestInProgress: false,
betterShadow: this.$store.state.interface.browserSupport.cssFilter betterShadow: this.$store.state.interface.browserSupport.cssFilter,
showingConfirmMute: false
} }
}, },
created () { created () {
@ -69,6 +71,7 @@ export default {
return `${serverUrl.protocol}//${serverUrl.host}/main/ostatus` return `${serverUrl.protocol}//${serverUrl.host}/main/ostatus`
}, },
loggedIn () { loggedIn () {
console.log({ ...this.$store.state.users.currentUser })
return this.$store.state.users.currentUser return this.$store.state.users.currentUser
}, },
dailyAvg () { dailyAvg () {
@ -112,6 +115,9 @@ export default {
hideFollowersCount () { hideFollowersCount () {
return this.isOtherUser && this.user.hide_followers_count return this.isOtherUser && this.user.hide_followers_count
}, },
shouldConfirmMute () {
return this.mergedConfig.modalOnMute
},
...mapGetters(['mergedConfig']) ...mapGetters(['mergedConfig'])
}, },
components: { components: {
@ -122,14 +128,29 @@ export default {
ProgressButton, ProgressButton,
FollowButton, FollowButton,
Select, Select,
RichContent RichContent,
ConfirmModal
}, },
methods: { methods: {
refetchRelationship () { refetchRelationship () {
return this.$store.dispatch('fetchUserRelationship', this.user.id) return this.$store.dispatch('fetchUserRelationship', this.user.id)
}, },
showConfirmMute () {
this.showingConfirmMute = true
},
hideConfirmMute () {
this.showingConfirmMute = false
},
muteUser () { muteUser () {
if (!this.shouldConfirmMute) {
this.doMuteUser()
} else {
this.showConfirmMute()
}
},
doMuteUser () {
this.$store.dispatch('muteUser', this.user.id) this.$store.dispatch('muteUser', this.user.id)
this.hideConfirmMute()
}, },
unmuteUser () { unmuteUser () {
this.$store.dispatch('unmuteUser', this.user.id) this.$store.dispatch('unmuteUser', this.user.id)

View File

@ -245,7 +245,7 @@
</button> </button>
</div> </div>
<ModerationTools <ModerationTools
v-if="loggedIn.role === &quot;admin&quot;" v-if="loggedIn.role === 'admin'"
:user="user" :user="user"
/> />
</div> </div>
@ -295,6 +295,27 @@
:handle-links="true" :handle-links="true"
/> />
</div> </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="span"
>
<template #user>
<span
v-text="user.screen_name_ui"
/>
</template>
</i18n-t>
</confirm-modal>
</teleport>
</div> </div>
</template> </template>

View File

@ -33,23 +33,23 @@ const FriendList = withLoadMore({
additionalPropNames: ['userId'] additionalPropNames: ['userId']
})(List) })(List)
const defaultTabKey = 'statuses'
const UserProfile = { const UserProfile = {
data () { data () {
return { return {
error: false, error: false,
userId: null, userId: null,
tab: defaultTabKey, tab: 'statuses',
footerRef: null, footerRef: null,
note: null, note: null,
noteLoading: false noteLoading: false
} }
}, },
created () { created () {
const defaultTabKey = this.defaultTabKey
const routeParams = this.$route.params const routeParams = this.$route.params
const hash = (get(this.$route, 'hash') || defaultTabKey).replace(/^#/, '')
if (hash !== '') this.tab = hash
this.load(routeParams.name || routeParams.id) this.load(routeParams.name || routeParams.id)
this.tab = get(this.$route, 'query.hash', defaultTabKey).replace(/^#/, '')
}, },
unmounted () { unmounted () {
this.stopFetching() this.stopFetching()
@ -58,6 +58,9 @@ const UserProfile = {
timeline () { timeline () {
return this.$store.state.statuses.timelines.user return this.$store.state.statuses.timelines.user
}, },
replies () {
return this.$store.state.statuses.timelines.replies
},
favorites () { favorites () {
return this.$store.state.statuses.timelines.favorites return this.$store.state.statuses.timelines.favorites
}, },
@ -82,28 +85,39 @@ const UserProfile = {
}, },
currentUser () { currentUser () {
return this.$store.state.users.currentUser return this.$store.state.users.currentUser
},
defaultTabKey () {
return this.$store.getters.mergedConfig.userProfileDefaultTab || 'statuses'
} }
}, },
methods: { methods: {
setFooterRef (el) { setFooterRef (el) {
this.footerRef = el this.footerRef = el
}, },
load (userNameOrId) { onRouteChange (previousTab, nextTab) {
const startFetchingTimeline = (timeline, userId) => { const timelineTabMap = {
// Clear timeline only if load another user's profile statuses: 'user',
if (userId !== this.$store.state.statuses.timelines[timeline].userId) { replies: 'replies',
this.$store.commit('clearTimeline', { timeline }) media: 'media'
}
this.$store.dispatch('startFetchingTimeline', { timeline, userId })
} }
// only we can see our own favourites
if (this.isUs) timelineTabMap['favorites'] = 'favorites'
const timeline = timelineTabMap[nextTab]
if (timeline) {
this.stopFetching()
this.$store.dispatch('startFetchingTimeline', { timeline: timeline, userId: this.userId })
}
},
load (userNameOrId) {
const loadById = (userId) => { const loadById = (userId) => {
this.userId = userId this.userId = userId
startFetchingTimeline('user', userId) const timelines = ['user', 'favorites', 'replies', 'media']
startFetchingTimeline('media', userId) timelines.forEach((timeline) => {
if (this.isUs) { this.$store.commit('clearTimeline', { timeline: timeline })
startFetchingTimeline('favorites', userId) })
} this.onRouteChange(null, this.tab)
// Fetch all pinned statuses immediately // Fetch all pinned statuses immediately
this.$store.dispatch('fetchPinnedStatuses', userId) this.$store.dispatch('fetchPinnedStatuses', userId)
} }
@ -137,6 +151,7 @@ const UserProfile = {
}, },
stopFetching () { stopFetching () {
this.$store.dispatch('stopFetchingTimeline', 'user') this.$store.dispatch('stopFetchingTimeline', 'user')
this.$store.dispatch('stopFetchingTimeline', 'replies')
this.$store.dispatch('stopFetchingTimeline', 'favorites') this.$store.dispatch('stopFetchingTimeline', 'favorites')
this.$store.dispatch('stopFetchingTimeline', 'media') this.$store.dispatch('stopFetchingTimeline', 'media')
}, },
@ -177,7 +192,9 @@ const UserProfile = {
} }
}, },
'$route.hash': function (newVal) { '$route.hash': function (newVal) {
this.tab = newVal.replace(/^#/, '') || defaultTabKey const oldTab = this.tab
this.tab = newVal.replace(/^#/, '') || this.defaultTabKey
this.onRouteChange(oldTab, this.tab)
} }
}, },
components: { components: {

View File

@ -79,6 +79,18 @@
:in-profile="true" :in-profile="true"
:footer-slipgate="footerRef" :footer-slipgate="footerRef"
/> />
<Timeline
key="replies"
:label="$t('user_card.replies')"
:count="user.statuses_count"
:embedded="true"
:title="$t('user_card.replies')"
:timeline="replies"
timeline-name="replies"
:user-id="userId"
:in-profile="true"
:footer-slipgate="footerRef"
/>
<div <div
v-if="followsTabVisible" v-if="followsTabVisible"
key="followees" key="followees"
@ -109,7 +121,6 @@
<Timeline <Timeline
key="media" key="media"
:label="$t('user_card.media')" :label="$t('user_card.media')"
:disabled="!media.visibleStatuses.length"
:embedded="true" :embedded="true"
:title="$t('user_card.media')" :title="$t('user_card.media')"
timeline-name="media" timeline-name="media"
@ -122,7 +133,7 @@
v-if="isUs" v-if="isUs"
key="favorites" key="favorites"
:label="$t('user_card.favorites')" :label="$t('user_card.favorites')"
:disabled="!favorites.visibleStatuses.length" :disabled="!isUs"
:embedded="true" :embedded="true"
:title="$t('user_card.favorites')" :title="$t('user_card.favorites')"
timeline-name="favorites" timeline-name="favorites"

View File

@ -98,7 +98,6 @@
"features_panel": { "features_panel": {
"media_proxy": "Proxy per multimèdia", "media_proxy": "Proxy per multimèdia",
"scope_options": "Opcions d'abast", "scope_options": "Opcions d'abast",
"shout": "Altaveu",
"text_limit": "Límit de text", "text_limit": "Límit de text",
"title": "Funcionalitats", "title": "Funcionalitats",
"upload_limit": "Límit de càrrega", "upload_limit": "Límit de càrrega",
@ -400,7 +399,7 @@
"conversation_display_linear": "Estil linear", "conversation_display_linear": "Estil linear",
"conversation_display_tree": "Estil d'arbre", "conversation_display_tree": "Estil d'arbre",
"conversation_other_replies_button": "Mostra el botó \"altres respostes\"", "conversation_other_replies_button": "Mostra el botó \"altres respostes\"",
"conversation_other_replies_button_below": "Apunts a sota", "conversation_other_replies_button_below": "Apunts de sota",
"conversation_other_replies_button_inside": "Apunts interiors", "conversation_other_replies_button_inside": "Apunts interiors",
"current_avatar": "El teu avatar actual", "current_avatar": "El teu avatar actual",
"current_mascot": "La teva mascota actual", "current_mascot": "La teva mascota actual",
@ -461,6 +460,8 @@
"hide_muted_threads": "Amaga fils silenciats", "hide_muted_threads": "Amaga fils silenciats",
"hide_post_stats": "Amaga les estadístiques dels apunts (p. ex. el número de favorits)", "hide_post_stats": "Amaga les estadístiques dels apunts (p. ex. el número de favorits)",
"hide_shoutbox": "Amaga la casella de gàbia de grills", "hide_shoutbox": "Amaga la casella de gàbia de grills",
"hide_site_favicon": "Amaga el favicon de l'instància en el panell superior",
"hide_site_name": "Amaga el nom de l'instància en el panell superior",
"hide_threads_with_blocked_users": "Amaga els fils mencionant usuaris bloquejats", "hide_threads_with_blocked_users": "Amaga els fils mencionant usuaris bloquejats",
"hide_user_stats": "Amaga les estadístiques de l'usuari (p. ex. el número de seguidors)", "hide_user_stats": "Amaga les estadístiques de l'usuari (p. ex. el número de seguidors)",
"hide_wallpaper": "Amagar el fons de l'instància", "hide_wallpaper": "Amagar el fons de l'instància",
@ -546,13 +547,13 @@
"notification_setting_hide_notification_contents": "Amagar el remitent i els continguts de les notificacions push", "notification_setting_hide_notification_contents": "Amagar el remitent i els continguts de les notificacions push",
"notification_setting_privacy": "Privacitat", "notification_setting_privacy": "Privacitat",
"notification_visibility": "Tipus de notificacions a mostrar", "notification_visibility": "Tipus de notificacions a mostrar",
"notification_visibility_emoji_reactions": "reacciona", "notification_visibility_emoji_reactions": "reaccionen",
"notification_visibility_follows": "em segueix", "notification_visibility_follows": "em segueixen",
"notification_visibility_likes": "m'afavoreix", "notification_visibility_likes": "m'afavoreixen",
"notification_visibility_mentions": "em menciona", "notification_visibility_mentions": "em mencionen",
"notification_visibility_moves": "es mou", "notification_visibility_moves": "usuari es mou",
"notification_visibility_polls": "finalitza una enquesta que he votat", "notification_visibility_polls": "finalitza una enquesta que he votat",
"notification_visibility_repeats": "em repeteix", "notification_visibility_repeats": "em repeteixen",
"notifications": "Notificacions", "notifications": "Notificacions",
"nsfw_clickthrough": "Amaga els Mèdia sensibles/NSFW", "nsfw_clickthrough": "Amaga els Mèdia sensibles/NSFW",
"oauth_tokens": "Codis OAuth", "oauth_tokens": "Codis OAuth",
@ -613,7 +614,10 @@
"settings": "Configuració", "settings": "Configuració",
"show_admin_badge": "Mostra l'insígnia \"Administrador\" en el meu perfil", "show_admin_badge": "Mostra l'insígnia \"Administrador\" en el meu perfil",
"show_moderator_badge": "Mostra l'insígnia \"Moderador\" en el meu perfil", "show_moderator_badge": "Mostra l'insígnia \"Moderador\" en el meu perfil",
"show_nav_shortcuts": "Mostra els accessos directes addicionals en el panell superior",
"show_panel_nav_shortcuts": "Mostra els accessos directes de navegació de la línia de temps en el panell superior",
"show_scrollbars": "Mostra les barres de desplaçament de la columna lateral", "show_scrollbars": "Mostra les barres de desplaçament de la columna lateral",
"show_wider_shortcuts": "Mostra més separats els accessos directes del panell superior",
"show_yous": "Mostra (Tu)s", "show_yous": "Mostra (Tu)s",
"stop_gifs": "Pausa les imatges animades fins que hi passis el cursor per sobre", "stop_gifs": "Pausa les imatges animades fins que hi passis el cursor per sobre",
"streaming": "Mostra automàticament els nous apunts quan et desplacis a la part superior", "streaming": "Mostra automàticament els nous apunts quan et desplacis a la part superior",

View File

@ -98,7 +98,6 @@
"features_panel": { "features_panel": {
"media_proxy": "Media proxy", "media_proxy": "Media proxy",
"scope_options": "Scope options", "scope_options": "Scope options",
"shout": "Shoutbox",
"text_limit": "Text limit", "text_limit": "Text limit",
"title": "Features", "title": "Features",
"upload_limit": "Upload limit", "upload_limit": "Upload limit",
@ -280,10 +279,13 @@
"text/plain": "Plain text", "text/plain": "Plain text",
"text/x.misskeymarkdown": "MFM" "text/x.misskeymarkdown": "MFM"
}, },
"content_warning": "Subject (optional)", "content_warning": "Content Warning (optional)",
"default": "Just arrived at Luna Nova Academy", "default": "Just arrived at Luna Nova Academy",
"direct_warning_to_all": "This post will be visible to all the mentioned users.", "direct_warning_to_all": "This post will be visible to all the mentioned users.",
"direct_warning_to_first_only": "This post will only be visible to the mentioned users at the beginning of the message.", "direct_warning_to_first_only": "This post will only be visible to the mentioned users at the beginning of the message.",
"edit_remote_warning": "Changes made to the post may not be visible on some instances!",
"edit_status": "Edit Status",
"edit_unsupported_warning": "Polls and mentions will not be changed by editing.",
"empty_status_error": "Can't send a post with no content and no files", "empty_status_error": "Can't send a post with no content and no files",
"media_description": "Media description", "media_description": "Media description",
"media_description_error": "Failed to update media, try again", "media_description_error": "Failed to update media, try again",
@ -314,7 +316,7 @@
"email": "Email", "email": "Email",
"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?",
"fullname": "Display name", "fullname": "Display name",
"fullname_placeholder": "e.g. Lain Iwakura", "fullname_placeholder": "e.g. Atsuko Kagari",
"new_captcha": "Click the image to get a new captcha", "new_captcha": "Click the image to get a new captcha",
"password_confirm": "Password confirmation", "password_confirm": "Password confirmation",
"reason": "Reason to register", "reason": "Reason to register",
@ -322,7 +324,7 @@
"register": "Register", "register": "Register",
"registration": "Registration", "registration": "Registration",
"token": "Invite token", "token": "Invite token",
"username_placeholder": "e.g. lain", "username_placeholder": "e.g. akko",
"validations": { "validations": {
"email_required": "cannot be left blank", "email_required": "cannot be left blank",
"fullname_required": "cannot be left blank", "fullname_required": "cannot be left blank",
@ -393,9 +395,19 @@
"changed_password": "Password changed successfully!", "changed_password": "Password changed successfully!",
"chatMessageRadius": "Chat message", "chatMessageRadius": "Chat message",
"checkboxRadius": "Checkboxes", "checkboxRadius": "Checkboxes",
"collapse_subject": "Collapse posts with subjects", "collapse_subject": "Collapse posts with content warnings",
"columns": "Columns",
"composing": "Composing", "composing": "Composing",
"confirm_dialogs": "Require confirmation for:",
"confirm_dialogs_approve_follow": "Accepting a follow request",
"confirm_dialogs_block": "Blocking someone",
"confirm_dialogs_delete": "Deleting a post",
"confirm_dialogs_deny_follow": "Rejecting a follow request",
"confirm_dialogs_mute": "Muting someone",
"confirm_dialogs_repeat": "Repeating a post",
"confirm_dialogs_unfollow": "Unfollowing someone",
"confirm_new_password": "Confirm new password", "confirm_new_password": "Confirm new password",
"confirmation_dialogs": "Confirmation options",
"conversation_display": "Conversation display style", "conversation_display": "Conversation display style",
"conversation_display_linear": "Linear-style", "conversation_display_linear": "Linear-style",
"conversation_display_tree": "Tree-style", "conversation_display_tree": "Tree-style",
@ -427,8 +439,8 @@
"backup_settings_theme": "Backup settings and theme to file", "backup_settings_theme": "Backup settings and theme to file",
"errors": { "errors": {
"file_slightly_new": "File minor version is different, some settings might not load", "file_slightly_new": "File minor version is different, some settings might not load",
"file_too_new": "Incompatile major version: {fileMajor}, this PleromaFE (settings ver {feMajor}) is too old to handle it", "file_too_new": "Incompatible major version: {fileMajor}, this PleromaFE (settings ver {feMajor}) is too old to handle it",
"file_too_old": "Incompatile major version: {fileMajor}, file version is too old and not supported (min. set. ver. {feMajor})", "file_too_old": "Incompatible major version: {fileMajor}, file version is too old and not supported (min. set. ver. {feMajor})",
"invalid_file": "The selected file is not a supported Pleroma settings backup. No changes were made." "invalid_file": "The selected file is not a supported Pleroma settings backup. No changes were made."
}, },
"restore_settings": "Restore settings from file" "restore_settings": "Restore settings from file"
@ -461,6 +473,8 @@
"hide_muted_threads": "Hide muted threads", "hide_muted_threads": "Hide muted threads",
"hide_post_stats": "Hide post statistics (e.g. the number of favorites)", "hide_post_stats": "Hide post statistics (e.g. the number of favorites)",
"hide_shoutbox": "Hide instance shoutbox", "hide_shoutbox": "Hide instance shoutbox",
"hide_site_favicon": "Hide instance favicon in top panel",
"hide_site_name": "Hide instance name in top panel",
"hide_threads_with_blocked_users": "Hide threads mentioning blocked users", "hide_threads_with_blocked_users": "Hide threads mentioning blocked users",
"hide_user_stats": "Hide user statistics (e.g. the number of followers)", "hide_user_stats": "Hide user statistics (e.g. the number of followers)",
"hide_wallpaper": "Hide instance wallpaper", "hide_wallpaper": "Hide instance wallpaper",
@ -519,7 +533,7 @@
"more_settings": "More settings", "more_settings": "More settings",
"move_account": "Move account", "move_account": "Move account",
"move_account_error": "Error moving account: {error}", "move_account_error": "Error moving account: {error}",
"move_account_notes": "If you want to move the account somewhere else, you must go to your target account and add an alias pointing here.", "move_account_notes": "If you want to move this account somewhere else, you must go to your target account and add an alias pointing here.",
"move_account_target": "Target account (e.g. {example})", "move_account_target": "Target account (e.g. {example})",
"moved_account": "Account is moved.", "moved_account": "Account is moved.",
"mute_bot_posts": "Mute bot posts", "mute_bot_posts": "Mute bot posts",
@ -603,7 +617,7 @@
"security": "Security", "security": "Security",
"security_tab": "Security", "security_tab": "Security",
"sensitive_by_default": "Mark posts as sensitive by default", "sensitive_by_default": "Mark posts as sensitive by default",
"sensitive_if_subject": "Automatically mark images as sensitive if a subject line is specified", "sensitive_if_subject": "Automatically mark images as sensitive if a content warning is specified",
"set_new_avatar": "Set new avatar", "set_new_avatar": "Set new avatar",
"set_new_mascot": "Set new mascot", "set_new_mascot": "Set new mascot",
"set_new_profile_background": "Set new profile background", "set_new_profile_background": "Set new profile background",
@ -613,7 +627,10 @@
"settings": "Settings", "settings": "Settings",
"show_admin_badge": "Show \"Admin\" badge in my profile", "show_admin_badge": "Show \"Admin\" badge in my profile",
"show_moderator_badge": "Show \"Moderator\" badge in my profile", "show_moderator_badge": "Show \"Moderator\" badge in my profile",
"show_nav_shortcuts": "Show extra navigation shortcuts in top panel",
"show_panel_nav_shortcuts": "Show timeline navigation shortcuts at the top of the panel",
"show_scrollbars": "Show side column's scrollbars", "show_scrollbars": "Show side column's scrollbars",
"show_wider_shortcuts": "Show wider gap between top panel shortcuts",
"show_yous": "Show (You)s", "show_yous": "Show (You)s",
"stop_gifs": "Pause animated images until you hover on them", "stop_gifs": "Pause animated images until you hover on them",
"streaming": "Automatically show new posts when scrolled to the top", "streaming": "Automatically show new posts when scrolled to the top",
@ -763,9 +780,9 @@
"use_source": "New version" "use_source": "New version"
} }
}, },
"subject_input_always_show": "Always show subject field", "subject_input_always_show": "Always show content warning field",
"subject_line_behavior": "Copy subject when replying", "subject_line_behavior": "Copy content warning when replying",
"subject_line_email": "Like email: \"re: subject\"", "subject_line_email": "Like email: \"re: warning\"",
"subject_line_mastodon": "Like mastodon: copy as is", "subject_line_mastodon": "Like mastodon: copy as is",
"subject_line_noop": "Do not copy", "subject_line_noop": "Do not copy",
"text": "Text", "text": "Text",
@ -779,7 +796,8 @@
"third_column_mode_postform": "Main post form and navigation", "third_column_mode_postform": "Main post form and navigation",
"token": "Token", "token": "Token",
"tooltipRadius": "Tooltips/alerts", "tooltipRadius": "Tooltips/alerts",
"tree_advanced": "Allow more flexible navigation in tree view", "translation_language": "Automatic Translation Language",
"tree_advanced": "Display extra buttons to open and close reply chains in threads",
"tree_fade_ancestors": "Display ancestors of the current post in faint text", "tree_fade_ancestors": "Display ancestors of the current post in faint text",
"type_domains_to_mute": "Search domains to mute", "type_domains_to_mute": "Search domains to mute",
"upload_a_photo": "Upload a photo", "upload_a_photo": "Upload a photo",
@ -789,6 +807,7 @@
"use_contain_fit": "Don't crop the attachment in thumbnails", "use_contain_fit": "Don't crop the attachment in thumbnails",
"use_one_click_nsfw": "Open NSFW attachments with just one click", "use_one_click_nsfw": "Open NSFW attachments with just one click",
"user_mutes": "Users", "user_mutes": "Users",
"user_profile_default_tab": "Default Tab on User Profile",
"user_profiles": "User Profiles", "user_profiles": "User Profiles",
"user_settings": "User Settings", "user_settings": "User Settings",
"valid_until": "Valid until", "valid_until": "Valid until",
@ -814,12 +833,19 @@
"copy_link": "Copy link to post", "copy_link": "Copy link to post",
"delete": "Delete post", "delete": "Delete post",
"delete_confirm": "Do you really want to delete this post?", "delete_confirm": "Do you really want to delete this post?",
"delete_confirm_accept_button": "Yes, delete it",
"delete_confirm_cancel_button": "No, keep it",
"delete_confirm_title": "Confirm deletion",
"edit": "Edit",
"edit_history": "Edit History",
"edit_history_modal_title": "Edited {historyCount} time | Edited {historyCount} times",
"edited_at": "Edited {time}",
"expand": "Expand", "expand": "Expand",
"external_source": "External source", "external_source": "External source",
"favorites": "Favorites", "favorites": "Favorites",
"hide_attachment": "Hide attachment", "hide_attachment": "Hide attachment",
"hide_content": "Hide content", "hide_content": "Hide content",
"hide_full_subject": "Hide full subject", "hide_full_subject": "Hide full content warning",
"many_attachments": "Post has {number} attachment | Post has {number} attachments", "many_attachments": "Post has {number} attachment | Post has {number} attachments",
"mentions": "Mentions", "mentions": "Mentions",
"move_down": "Shift attachment right", "move_down": "Shift attachment right",
@ -827,32 +853,39 @@
"mute_conversation": "Mute conversation", "mute_conversation": "Mute conversation",
"nsfw": "NSFW", "nsfw": "NSFW",
"open_gallery": "Open gallery", "open_gallery": "Open gallery",
"override_translation_source_language": "Override source language",
"pin": "Pin on profile", "pin": "Pin on profile",
"pinned": "Pinned", "pinned": "Pinned",
"plus_more": "+{number} more", "plus_more": "+{number} more",
"remove_attachment": "Remove attachment", "remove_attachment": "Remove attachment",
"repeat_confirm": "Do you really want to repeat this post?",
"repeat_confirm_accept_button": "Yes, repeat it",
"repeat_confirm_cancel_button": "No, don't repeat",
"repeat_confirm_title": "Confirm repeat",
"repeats": "Repeats", "repeats": "Repeats",
"replies_list": "Replies:", "replies_list": "Replies:",
"replies_list_with_others": "Replies (+{numReplies} other): | Replies (+{numReplies} others):", "replies_list_with_others": "View {numReplies} more reply | View {numReplies} more replies",
"reply_to": "Reply to", "reply_to": "Reply to",
"show_all_attachments": "Show all attachments", "show_all_attachments": "Show all attachments",
"show_all_conversation": "Show full conversation ({numStatus} other post) | Show full conversation ({numStatus} other posts)", "show_all_conversation": "Show full conversation ({numStatus} other post) | Show full conversation ({numStatus} other posts)",
"show_all_conversation_with_icon": "{icon} {text}", "show_all_conversation_with_icon": "{icon} {text}",
"show_attachment_description": "Preview description (open attachment for full description)", "show_attachment_description": "Preview description (open attachment for full description)",
"show_attachment_in_modal": "Show in media modal", "show_attachment_in_modal": "Show attachment in a window",
"show_content": "Show content", "show_content": "Show content",
"show_full_subject": "Show full subject", "show_full_subject": "Show full content warning",
"show_only_conversation_under_this": "Only show replies to this post", "show_only_conversation_under_this": "Only show replies to this post",
"status_deleted": "This post was deleted", "status_deleted": "This post was deleted",
"status_unavailable": "Post unavailable", "status_unavailable": "Post unavailable",
"thread_follow": "See the remaining part of this thread ({numStatus} post in total) | See the remaining part of this thread ({numStatus} posts in total)", "thread_follow": "View {numStatus} more reply | View {numStatus} more replies",
"thread_follow_with_icon": "{icon} {text}", "thread_follow_with_icon": "{icon} {text}",
"thread_hide": "Hide this thread", "thread_hide": "Hide this thread",
"thread_muted": "Thread muted", "thread_muted": "Thread muted",
"thread_muted_and_words": ", has words:", "thread_muted_and_words": ", has words:",
"thread_show": "Show this thread", "thread_show": "Show this thread",
"thread_show_full": "Show everything under this thread ({numStatus} post in total, max depth {depth}) | Show everything under this thread ({numStatus} posts in total, max depth {depth})", "thread_show_full": "Show {numStatus} reply | Show all {numStatus} replies",
"thread_show_full_with_icon": "{icon} {text}", "thread_show_full_with_icon": "{icon} {text}",
"translate": "Translate",
"translated_from": "Translated from {language}",
"unbookmark": "Unbookmark", "unbookmark": "Unbookmark",
"unmute_conversation": "Unmute conversation", "unmute_conversation": "Unmute conversation",
"unpin": "Unpin from profile", "unpin": "Unpin from profile",
@ -895,6 +928,9 @@
"socket_reconnected": "Realtime connection established", "socket_reconnected": "Realtime connection established",
"up_to_date": "Up-to-date" "up_to_date": "Up-to-date"
}, },
"toast": {
"no_translation_target_set": "No translation target language set - this may fail. Please set a target language in your settings."
},
"tool_tip": { "tool_tip": {
"accept_follow_request": "Accept follow request", "accept_follow_request": "Accept follow request",
"add_reaction": "Add Reaction", "add_reaction": "Add Reaction",
@ -943,12 +979,24 @@
"strip_media": "Remove media from posts" "strip_media": "Remove media from posts"
}, },
"approve": "Approve", "approve": "Approve",
"approve_confirm": "Are you sure you want to let this user follow you?",
"approve_confirm_accept_button": "Yes, accept",
"approve_confirm_cancel_button": "No, cancel",
"approve_confirm_title": "Approve follow request",
"block": "Block", "block": "Block",
"block_confirm": "Are you sure you want to block {user}?",
"block_confirm_accept_button": "Yes, block",
"block_confirm_cancel_button": "No, don't block",
"block_confirm_title": "Block user",
"block_progress": "Blocking…", "block_progress": "Blocking…",
"blocked": "Blocked!", "blocked": "Blocked!",
"bot": "Bot", "bot": "Bot",
"deactivated": "Deactivated", "deactivated": "Deactivated",
"deny": "Deny", "deny": "Deny",
"deny_confirm": "Are you sure you want to deny this user's follow request?",
"deny_confirm_accept_button": "Yes, deny",
"deny_confirm_cancel_button": "No, cancel",
"deny_confirm_title": "Deny follow request",
"domain_muted": "Unblock domain", "domain_muted": "Unblock domain",
"edit_profile": "Edit profile", "edit_profile": "Edit profile",
"favorites": "Favorites", "favorites": "Favorites",
@ -974,18 +1022,27 @@
"mention": "Mention", "mention": "Mention",
"message": "Message", "message": "Message",
"mute": "Mute", "mute": "Mute",
"mute_confirm": "Are you sure you want to mute {user}?",
"mute_confirm_accept_button": "Yes, mute",
"mute_confirm_cancel_button": "No, don't mute",
"mute_confirm_title": "Mute user",
"mute_domain": "Block domain", "mute_domain": "Block domain",
"mute_progress": "Muting…", "mute_progress": "Muting…",
"muted": "Muted", "muted": "Muted",
"note": "Private note", "note": "Private note",
"per_day": "per day", "per_day": "per day",
"remote_follow": "Remote follow", "remote_follow": "Remote follow",
"replies": "With Replies",
"report": "Report", "report": "Report",
"show_repeats": "Show repeats", "show_repeats": "Show repeats",
"statuses": "Posts", "statuses": "Posts",
"subscribe": "Subscribe", "subscribe": "Subscribe",
"unblock": "Unblock", "unblock": "Unblock",
"unblock_progress": "Unblocking…", "unblock_progress": "Unblocking…",
"unfollow_confirm": "Are you sure you want to unfollow {user}?",
"unfollow_confirm_accept_button": "Yes, unfollow",
"unfollow_confirm_cancel_button": "No, don't unfollow",
"unfollow_confirm_title": "Unfollow user",
"unmute": "Unmute", "unmute": "Unmute",
"unmute_progress": "Unmuting…", "unmute_progress": "Unmuting…",
"unsubscribe": "Unsubscribe" "unsubscribe": "Unsubscribe"

View File

@ -1,5 +1,7 @@
{ {
"about": { "about": {
"bubble_instances": "Instancias de burbuja local",
"bubble_instances_description": "Instancias elegidas por los/las adminstradores/as para representar el área local de esta instancia",
"mrf": { "mrf": {
"federation": "Federación", "federation": "Federación",
"keyword": { "keyword": {
@ -137,7 +139,8 @@
"direct": "Directo", "direct": "Directo",
"local": "Local - sólo tu instancia puede ver esta publicación", "local": "Local - sólo tu instancia puede ver esta publicación",
"private": "Sólo para seguidores", "private": "Sólo para seguidores",
"public": "Público" "public": "Público",
"unlisted": "No listado"
}, },
"show_less": "Mostrar menos", "show_less": "Mostrar menos",
"show_more": "Mostrar más", "show_more": "Mostrar más",
@ -164,6 +167,8 @@
"lists": { "lists": {
"create": "Crear", "create": "Crear",
"delete": "Eliminar lista", "delete": "Eliminar lista",
"lists": "Listas",
"new": "Nueva Lista",
"save": "Guardar cambios", "save": "Guardar cambios",
"search": "Buscar cuentas", "search": "Buscar cuentas",
"title": "Título de la lista" "title": "Título de la lista"
@ -193,6 +198,7 @@
"nav": { "nav": {
"about": "Acerca de", "about": "Acerca de",
"administration": "Administración", "administration": "Administración",
"announcements": "Anuncios",
"back": "Volver", "back": "Volver",
"bookmarks": "Marcadores", "bookmarks": "Marcadores",
"chats": "Chats", "chats": "Chats",
@ -200,13 +206,16 @@
"friend_requests": "Solicitudes de seguimiento", "friend_requests": "Solicitudes de seguimiento",
"home_timeline": "Línea temporal personal", "home_timeline": "Línea temporal personal",
"interactions": "Interacciones", "interactions": "Interacciones",
"lists": "Listas",
"mentions": "Menciones", "mentions": "Menciones",
"preferences": "Preferencias", "preferences": "Preferencias",
"public_timeline_description": "Publicaciones públicas de esta instancia",
"public_tl": "Línea temporal pública", "public_tl": "Línea temporal pública",
"search": "Buscar", "search": "Buscar",
"timeline": "Línea Temporal", "timeline": "Línea Temporal",
"timelines": "Líneas de Tiempo", "timelines": "Líneas de Tiempo",
"twkn": "Red Conocida", "twkn": "Red Conocida",
"twkn_timeline_description": "Publicaciones de la red conocida",
"user_search": "Búsqueda de Usuarios", "user_search": "Búsqueda de Usuarios",
"who_to_follow": "A quién seguir" "who_to_follow": "A quién seguir"
}, },

View File

@ -1,5 +1,7 @@
{ {
"about": { "about": {
"bubble_instances": "Les instances de la bulle locale",
"bubble_instances_description": "Des instances choisies par l'administration pour representer la région de cette instance",
"mrf": { "mrf": {
"federation": "Fédération", "federation": "Fédération",
"keyword": { "keyword": {
@ -96,7 +98,6 @@
"features_panel": { "features_panel": {
"media_proxy": "Proxy pièce-jointes", "media_proxy": "Proxy pièce-jointes",
"scope_options": "Options de visibilité", "scope_options": "Options de visibilité",
"shout": "Shoutbox",
"text_limit": "Limite du texte", "text_limit": "Limite du texte",
"title": "Fonctionnalités", "title": "Fonctionnalités",
"upload_limit": "Limite de téléversement", "upload_limit": "Limite de téléversement",
@ -136,6 +137,7 @@
}, },
"scope_in_timeline": { "scope_in_timeline": {
"direct": "Direct", "direct": "Direct",
"local": "Local - visible seulement sur cette instance",
"private": "Abonné·e·s uniquement", "private": "Abonné·e·s uniquement",
"public": "Public", "public": "Public",
"unlisted": "Non-listé" "unlisted": "Non-listé"
@ -277,15 +279,17 @@
"text/plain": "Texte brut", "text/plain": "Texte brut",
"text/x.misskeymarkdown": "Markdown (Misskey)" "text/x.misskeymarkdown": "Markdown (Misskey)"
}, },
"content_warning": "Sujet (optionnel)", "content_warning": "Avertissement (optionnel)",
"default": "Je viens d'arriver au QG de la NERV", "default": "Je viens d'arriver au QG de la NERV",
"direct_warning_to_all": "Ce message sera visible pour toutes les personnes mentionnées.", "direct_warning_to_all": "Ce message sera visible pour toutes les personnes mentionnées.",
"direct_warning_to_first_only": "Ce message sera visible uniquement pour personnes mentionnées au début du message.", "direct_warning_to_first_only": "Ce message sera visible uniquement pour personnes mentionnées au début du message.",
"empty_status_error": "Impossible de poster un statut vide sans pièces-jointes", "edit_remote_warning": "Les modifications apportées au status pourraient ne pas être visible sur certaines instances !",
"edit_status": "Modifier le statut",
"empty_status_error": "Impossible de publier un statut vide sans pièces-jointes",
"media_description": "Description de la pièce-jointe", "media_description": "Description de la pièce-jointe",
"media_description_error": "Échec de téléversement du media, essayez encore", "media_description_error": "Échec de téléversement du media, essayez encore",
"media_not_sensitive_warning": "Il y a un avertissment, mais les pièces jointes ne sont pas marquées comme sensibles !", "media_not_sensitive_warning": "Il y a un avertissment, mais les pièces jointes ne sont pas marquées comme sensibles !",
"new_status": "Poster un nouveau statut", "new_status": "Nouveau statut",
"post": "Post", "post": "Post",
"posting": "Envoi en cours", "posting": "Envoi en cours",
"preview": "Prévisualisation", "preview": "Prévisualisation",
@ -306,7 +310,7 @@
}, },
"registration": { "registration": {
"bio": "Biographie", "bio": "Biographie",
"bio_placeholder": "ex.\nSalut, je suis Lain\nJe suis une héroïne d'animation qui vit dans une banlieue japonaise. Vous me connaissez peut-être du Wired.", "bio_placeholder": "ex :\nSalut, je me présente ici !\nJadore les animés et les jeux vidéos. Jespère qu'on peut être amis⋅ies !",
"captcha": "CAPTCHA", "captcha": "CAPTCHA",
"email": "Courriel", "email": "Courriel",
"email_language": "Dans quelle langue souhaitez-vous recevoir des courriels ?", "email_language": "Dans quelle langue souhaitez-vous recevoir des courriels ?",
@ -458,6 +462,8 @@
"hide_muted_threads": "Masquer les fils de discussion silenciés", "hide_muted_threads": "Masquer les fils de discussion silenciés",
"hide_post_stats": "Masquer les statistiques des messages (ex. le nombre de favoris)", "hide_post_stats": "Masquer les statistiques des messages (ex. le nombre de favoris)",
"hide_shoutbox": "Cacher la shoutbox de l'instance", "hide_shoutbox": "Cacher la shoutbox de l'instance",
"hide_site_favicon": "Ne pas afficher le favicon de linstance dans le panneau supérieur",
"hide_site_name": "Ne pas afficher le nom de l'instance dans le panneau supérieur",
"hide_threads_with_blocked_users": "Masquer les fils qui mentionnent les personnes bloquées", "hide_threads_with_blocked_users": "Masquer les fils qui mentionnent les personnes bloquées",
"hide_user_stats": "Masquer les statistiques de compte (ex. le nombre de suivis)", "hide_user_stats": "Masquer les statistiques de compte (ex. le nombre de suivis)",
"hide_wallpaper": "Cacher le fond d'écran", "hide_wallpaper": "Cacher le fond d'écran",
@ -576,6 +582,7 @@
"remove_alias": "Supprimer ce pseudo", "remove_alias": "Supprimer ce pseudo",
"remove_backup": "Supprimer", "remove_backup": "Supprimer",
"render_mfm": "Afficher le contenu Misskey Markdown", "render_mfm": "Afficher le contenu Misskey Markdown",
"render_mfm_on_hover": "Rester sur du contenu MFM pour déclencher les animations",
"replies_in_timeline": "Réponses dans le flux", "replies_in_timeline": "Réponses dans le flux",
"reply_visibility_all": "Montrer toutes les réponses", "reply_visibility_all": "Montrer toutes les réponses",
"reply_visibility_following": "Afficher uniquement les réponses adressées à moi ou aux personnes que je suis", "reply_visibility_following": "Afficher uniquement les réponses adressées à moi ou aux personnes que je suis",
@ -609,7 +616,10 @@
"settings": "Paramètres", "settings": "Paramètres",
"show_admin_badge": "Afficher le badge d'Admin sur mon profil", "show_admin_badge": "Afficher le badge d'Admin sur mon profil",
"show_moderator_badge": "Afficher le badge de Modo' sur mon profil", "show_moderator_badge": "Afficher le badge de Modo' sur mon profil",
"show_nav_shortcuts": "Afficher plus de raccourcis de navigations dans le panneau supérieur",
"show_panel_nav_shortcuts": "Afficher les raccourcis de navigation du flux dans le panneau supérieur",
"show_scrollbars": "Afficher la barre de défilement", "show_scrollbars": "Afficher la barre de défilement",
"show_wider_shortcuts": "Plus d'espace entre les raccourcis dans le panneau supérieur",
"show_yous": "Afficher « Vous : »", "show_yous": "Afficher « Vous : »",
"stop_gifs": "N'animer les GIFS que lors du survol du curseur de la souris", "stop_gifs": "N'animer les GIFS que lors du survol du curseur de la souris",
"streaming": "Charger automatiquement les nouveaux statuts lorsque vous êtes au haut de la page", "streaming": "Charger automatiquement les nouveaux statuts lorsque vous êtes au haut de la page",
@ -1004,4 +1014,4 @@
"more": "Plus", "more": "Plus",
"who_to_follow": "À qui s'abonner" "who_to_follow": "À qui s'abonner"
} }
} }

View File

@ -23,8 +23,10 @@
"media_nsfw_desc": "このインスタンスでは、以下のインスタンスからの投稿に対して、メディアを閲覧注意に設定します:", "media_nsfw_desc": "このインスタンスでは、以下のインスタンスからの投稿に対して、メディアを閲覧注意に設定します:",
"media_removal": "メディア除去", "media_removal": "メディア除去",
"media_removal_desc": "このインスタンスでは、以下のインスタンスからの投稿に対して、メディアを除去します:", "media_removal_desc": "このインスタンスでは、以下のインスタンスからの投稿に対して、メディアを除去します:",
"not_applicable": "なし",
"quarantine": "検疫", "quarantine": "検疫",
"quarantine_desc": "このインスタンスでは、以下のインスタンスに対して公開投稿のみを送信します:", "quarantine_desc": "このインスタンスでは、以下のインスタンスに投稿を送信しません",
"reason": "理由",
"reject": "拒否", "reject": "拒否",
"reject_desc": "このインスタンスでは、以下のインスタンスからのメッセージを受け付けません:", "reject_desc": "このインスタンスでは、以下のインスタンスからのメッセージを受け付けません:",
"simple_policies": "インスタンス固有のポリシー" "simple_policies": "インスタンス固有のポリシー"
@ -33,7 +35,25 @@
"staff": "スタッフ" "staff": "スタッフ"
}, },
"announcements": { "announcements": {
"page_header": "お知らせ" "all_day_prompt": "一日中",
"cancel_edit_action": "キャンセル",
"close_error": "閉じる",
"delete_action": "削除",
"edit_action": "更新",
"end_time_display": "{time}に終わる",
"end_time_prompt": "終了時間 ",
"inactive_message": "無効",
"mark_as_read_action": "読んだ",
"page_header": "お知らせ",
"post_action": "投稿",
"post_error": "エラーが発生しました: {error}",
"post_form_header": "お知らせ作成",
"post_placeholder": "コンテンツ",
"published_time_display": "{time}に告知されました",
"start_time_display": "{time}で始まる",
"start_time_prompt": "開始時間 ",
"submit_edit_action": "OK",
"title": "お知らせ"
}, },
"chats": { "chats": {
"chats": "チャット一覧", "chats": "チャット一覧",
@ -115,6 +135,13 @@
"admin": "管理者", "admin": "管理者",
"moderator": "モデレーター" "moderator": "モデレーター"
}, },
"scope_in_timeline": {
"direct": "ダイレクト",
"local": "ローカル:このインスタンスのユーザーしか見えません",
"private": "フォロワー限定",
"public": "公開",
"unlisted": "アンリステッド"
},
"show_less": "たたむ", "show_less": "たたむ",
"show_more": "もっと見る", "show_more": "もっと見る",
"submit": "送信", "submit": "送信",
@ -137,6 +164,16 @@
"load_older": "古いインタラクションを見る", "load_older": "古いインタラクションを見る",
"moves": "ユーザーの引っ越し" "moves": "ユーザーの引っ越し"
}, },
"lists": {
"create": "作成",
"delete": "削除",
"following_only": "フォローしているユーザーのみ",
"lists": "リスト",
"new": "リスト作成",
"save": "OK",
"search": "ユーザー探索",
"title": "リスト名"
},
"login": { "login": {
"authentication_code": "認証コード", "authentication_code": "認証コード",
"description": "OAuthでログイン", "description": "OAuthでログイン",
@ -150,12 +187,14 @@
"login": "ログイン", "login": "ログイン",
"logout": "ログアウト", "logout": "ログアウト",
"password": "パスワード", "password": "パスワード",
"placeholder": "例: lain", "placeholder": "例: user",
"recovery_code": "リカバリーコード", "recovery_code": "リカバリーコード",
"register": "登録", "register": "登録",
"username": "ユーザー名" "username": "ユーザー名"
}, },
"media_modal": { "media_modal": {
"counter": "{current} / {total}",
"hide": "閉じる",
"next": "次", "next": "次",
"previous": "前" "previous": "前"
}, },
@ -187,18 +226,19 @@
"who_to_follow": "おすすめユーザー" "who_to_follow": "おすすめユーザー"
}, },
"notifications": { "notifications": {
"broken_favorite": "ステータスが見つかりません。探しています…", "broken_favorite": "投稿が見つかりません。探しています…",
"error": "通知の取得に失敗しました: {0}", "error": "通知の取得に失敗しました: {0}",
"favorited_you": "あなたのステータスがお気に入りされました", "favorited_you": "投稿がお気に入りに登録されました",
"follow_request": "あなたをフォローしたいです", "follow_request": "あなたをフォローしたいです",
"followed_you": "フォローされました", "followed_you": "フォローされました",
"load_older": "古い通知をみる", "load_older": "古い通知をみる",
"migrated_to": "インスタンスを引っ越しました", "migrated_to": "インスタンスを引っ越しました",
"no_more_notifications": "通知はありません", "no_more_notifications": "通知はありません",
"notifications": "通知", "notifications": "通知",
"poll_ended": "投票終了",
"reacted_with": "{0} でリアクションしました", "reacted_with": "{0} でリアクションしました",
"read": "読んだ!", "read": "読んだ!",
"repeated_you": "あなたのステータスがリピートされました" "repeated_you": "投稿がリピートされました"
}, },
"password_reset": { "password_reset": {
"check_email": "パスワードをリセットするためのリンクが記載されたメールが届いているか確認してください。", "check_email": "パスワードをリセットするためのリンクが記載されたメールが届いているか確認してください。",
@ -221,7 +261,7 @@
"multiple_choices": "複数選択式", "multiple_choices": "複数選択式",
"not_enough_options": "相異なる選択肢が不足しています", "not_enough_options": "相異なる選択肢が不足しています",
"option": "選択肢", "option": "選択肢",
"people_voted_count": "{count} 人投票 | {count} 人投票", "people_voted_count": "{count} 人投票",
"single_choice": "択一式", "single_choice": "択一式",
"type": "投票の形式", "type": "投票の形式",
"vote": "投票", "vote": "投票",
@ -229,22 +269,27 @@
"votes_count": "{count} 票 | {count} 票" "votes_count": "{count} 票 | {count} 票"
}, },
"post_status": { "post_status": {
"account_not_locked_warning": "あなたのアカウントは {0} ではありません。あなたをフォローすれば、誰でも、フォロワー限定のステータスを読むことができます。", "account_not_locked_warning": "あなたのアカウントは {0} ではありません。あなたをフォローすれば、誰でも、フォロワー限定の投稿を読むことができます。",
"account_not_locked_warning_link": "ロックされたアカウント", "account_not_locked_warning_link": "ロックされたアカウント",
"attachments_sensitive": "ファイルをNSFWにする", "attachments_sensitive": "ファイルをNSFWにする",
"content_type": { "content_type": {
"text/bbcode": "BBCode", "text/bbcode": "BBCode",
"text/html": "HTML", "text/html": "HTML",
"text/markdown": "Markdown", "text/markdown": "Markdown",
"text/plain": "プレーンテキスト" "text/plain": "プレーンテキスト",
"text/x.misskeymarkdown": "MFM"
}, },
"content_warning": "説明 (省略可)", "content_warning": "警告 (省略可)",
"default": "羽田空港に着きました。", "default": "ただいまルーナノヴァ魔法学校に到着しました",
"direct_warning_to_all": "この投稿は、メンションされたすべてのユーザーが、見ることができます。", "direct_warning_to_all": "この投稿は、メンションされたすべてのユーザーが、見ることができます。",
"direct_warning_to_first_only": "この投稿は、メッセージの冒頭でメンションされたユーザーだけが、見ることができます。", "direct_warning_to_first_only": "この投稿は、メッセージの冒頭でメンションされたユーザーだけが、見ることができます。",
"edit_remote_warning": "注意:リモートインスタンスには、編集した投稿が見えないかもしれません",
"edit_status": "編集",
"edit_unsupported_warning": "編集しても投票やメンションが更新されません",
"empty_status_error": "投稿内容を入力してください", "empty_status_error": "投稿内容を入力してください",
"media_description": "メディアの説明", "media_description": "メディアの説明",
"media_description_error": "メディアのアップロードに失敗しました。もう一度お試しください", "media_description_error": "メディアのアップロードに失敗しました。もう一度お試しください",
"media_not_sensitive_warning": "注意: 説明が付けていますがNSFW警告が付けていません",
"new_status": "投稿する", "new_status": "投稿する",
"post": "投稿", "post": "投稿",
"posting": "投稿", "posting": "投稿",
@ -258,6 +303,7 @@
"unlisted": "アンリステッド: 公開タイムラインに届きません" "unlisted": "アンリステッド: 公開タイムラインに届きません"
}, },
"scope_notice": { "scope_notice": {
"local": "このインスタンスのユーザーしか見えません",
"private": "この投稿は、あなたのフォロワーだけが、見ることができます", "private": "この投稿は、あなたのフォロワーだけが、見ることができます",
"public": "この投稿は、誰でも見ることができます", "public": "この投稿は、誰でも見ることができます",
"unlisted": "この投稿は、パブリックタイムラインと、接続しているすべてのネットワークには、表示されません" "unlisted": "この投稿は、パブリックタイムラインと、接続しているすべてのネットワークには、表示されません"
@ -268,6 +314,7 @@
"bio_placeholder": "例:\nこんにちは。私は玲音。\n私はアニメのキャラクターで、日本の郊外に住んでいます。私をWiredで見たことがあるかもしれません。", "bio_placeholder": "例:\nこんにちは。私は玲音。\n私はアニメのキャラクターで、日本の郊外に住んでいます。私をWiredで見たことがあるかもしれません。",
"captcha": "CAPTCHA", "captcha": "CAPTCHA",
"email": "Eメール", "email": "Eメール",
"email_language": "サーバーからのメールの言語",
"fullname": "スクリーンネーム", "fullname": "スクリーンネーム",
"fullname_placeholder": "例: 岩倉玲音", "fullname_placeholder": "例: 岩倉玲音",
"new_captcha": "文字が読めないときは、画像をクリックすると、新しい画像になります", "new_captcha": "文字が読めないときは、画像をクリックすると、新しい画像になります",
@ -304,6 +351,17 @@
}, },
"settings": { "settings": {
"accent": "アクセント", "accent": "アクセント",
"account_alias": "アカウントのエイリアス名",
"account_alias_table_head": "エイリアス名",
"account_backup": "アカウントのバックアップ",
"account_backup_description": "投稿やアカウント情報をダウンロードできますが、インポートすることがまだできません",
"account_backup_table_head": "バックアップ",
"account_privacy": "プライバシー",
"add_alias_error": "エイリアス名を参加できませんでした:{error}",
"add_backup": "バックアップ作成",
"add_backup_error": "バックアップを作成できませんでした:{error}",
"added_alias": "エイリアス名を参加しました",
"added_backup": "バックアップを作成しました",
"allow_following_move": "フォロー中のアカウントが引っ越したとき、自動フォローを許可する", "allow_following_move": "フォロー中のアカウントが引っ越したとき、自動フォローを許可する",
"always_show_post_button": "投稿ボタンを常に表示", "always_show_post_button": "投稿ボタンを常に表示",
"app_name": "アプリの名称", "app_name": "アプリの名称",
@ -315,6 +373,7 @@
"avatarRadius": "アバター", "avatarRadius": "アバター",
"avatar_size_instruction": "アバターの大きさは、150×150ピクセルか、それよりも大きくするといいです。", "avatar_size_instruction": "アバターの大きさは、150×150ピクセルか、それよりも大きくするといいです。",
"background": "バックグラウンド", "background": "バックグラウンド",
"backup_not_ready": "バックアップはまだ完成していません",
"bio": "プロフィール", "bio": "プロフィール",
"block_export": "ブロックのエクスポート", "block_export": "ブロックのエクスポート",
"block_export_button": "ブロックをCSVファイルにエクスポートする", "block_export_button": "ブロックをCSVファイルにエクスポートする",
@ -336,10 +395,27 @@
"changed_password": "パスワードが、変わりました!", "changed_password": "パスワードが、変わりました!",
"chatMessageRadius": "チャットメッセージ", "chatMessageRadius": "チャットメッセージ",
"checkboxRadius": "チェックボックス", "checkboxRadius": "チェックボックス",
"collapse_subject": "説明のある投稿をたたむ", "collapse_subject": "警告のある投稿をたたむ",
"columns": "カラム",
"composing": "投稿", "composing": "投稿",
"confirm_dialogs": "選択しているアクションは確認必要があります",
"confirm_dialogs_approve_follow": "フォローリクエストを受け入れること",
"confirm_dialogs_block": "ユーザーをブロックすること",
"confirm_dialogs_delete": "投稿を削除すること",
"confirm_dialogs_deny_follow": "フォローリクエストを断ること",
"confirm_dialogs_mute": "ユーザーをミュートすること",
"confirm_dialogs_repeat": "投稿をリピートすること",
"confirm_dialogs_unfollow": "フォローをやめること",
"confirm_new_password": "新しいパスワードの確認", "confirm_new_password": "新しいパスワードの確認",
"confirmation_dialogs": "確認設定",
"conversation_display": "スレッドの表示モード",
"conversation_display_linear": "リニアー",
"conversation_display_tree": "ツリー",
"conversation_other_replies_button": "「別の返信」のボタンは",
"conversation_other_replies_button_below": "投稿の下",
"conversation_other_replies_button_inside": "投稿の中",
"current_avatar": "現在のアバター", "current_avatar": "現在のアバター",
"current_mascot": "現在のマスコット",
"current_password": "現在のパスワード", "current_password": "現在のパスワード",
"data_import_export_tab": "インポートとエクスポート", "data_import_export_tab": "インポートとエクスポート",
"default_vis": "デフォルトの公開範囲", "default_vis": "デフォルトの公開範囲",
@ -350,6 +426,8 @@
"disable_sticky_headers": "ヘッダーを追従させない", "disable_sticky_headers": "ヘッダーを追従させない",
"discoverable": "検索などのサービスでこのアカウントを見つけることを許可する", "discoverable": "検索などのサービスでこのアカウントを見つけることを許可する",
"domain_mutes": "ドメイン", "domain_mutes": "ドメイン",
"download_backup": "ダウンロード",
"email_language": "メールの表示言語",
"emoji_reactions_on_timeline": "絵文字リアクションをタイムラインに表示", "emoji_reactions_on_timeline": "絵文字リアクションをタイムラインに表示",
"enable_web_push_notifications": "ウェブプッシュ通知を許可する", "enable_web_push_notifications": "ウェブプッシュ通知を許可する",
"enter_current_password_to_confirm": "あなたのアイデンティティを証明するため、現在のパスワードを入力してください", "enter_current_password_to_confirm": "あなたのアイデンティティを証明するため、現在のパスワードを入力してください",
@ -361,6 +439,8 @@
"backup_settings_theme": "テーマを含む設定をファイルにバックアップする", "backup_settings_theme": "テーマを含む設定をファイルにバックアップする",
"errors": { "errors": {
"file_slightly_new": "ファイルのマイナーバージョンが異なり、一部の設定が読み込まれないことがあります", "file_slightly_new": "ファイルのマイナーバージョンが異なり、一部の設定が読み込まれないことがあります",
"file_too_new": "非対応Pleroma-FE v{feMajor}は古すぎて、v{fileMajor}の設定を読み込めません",
"file_too_old": "非対応:設定ファイル v{fileMajor}は古すぎて、Pleroma-FE v{feMajor}が読み込めません",
"invalid_file": "これはPleromaの設定をバックアップしたファイルではありません。" "invalid_file": "これはPleromaの設定をバックアップしたファイルではありません。"
}, },
"restore_settings": "設定をファイルから復元する" "restore_settings": "設定をファイルから復元する"
@ -379,17 +459,23 @@
"hide_all_muted_posts": "ミュートした投稿を隠す", "hide_all_muted_posts": "ミュートした投稿を隠す",
"hide_attachments_in_convo": "スレッドのファイルを隠す", "hide_attachments_in_convo": "スレッドのファイルを隠す",
"hide_attachments_in_tl": "タイムラインのファイルを隠す", "hide_attachments_in_tl": "タイムラインのファイルを隠す",
"hide_bot_indication": "botマークを隠す",
"hide_favorites_description": "お気に入りリストを隠す",
"hide_filtered_statuses": "フィルターされた投稿を隠す", "hide_filtered_statuses": "フィルターされた投稿を隠す",
"hide_followers_count_description": "フォロワーの数を見せない", "hide_followers_count_description": "フォロワーの数を見せない",
"hide_followers_description": "フォロワーを見せない", "hide_followers_description": "フォロワーを見せない",
"hide_follows_count_description": "フォローしている人の数を見せない", "hide_follows_count_description": "フォローしている人の数を見せない",
"hide_follows_description": "フォローしている人を見せない", "hide_follows_description": "フォローしている人を見せない",
"hide_isp": "インスタンス固有パネルを隠す", "hide_isp": "インスタンス固有パネルを隠す",
"hide_list_aliases_error_action": "閉じる",
"hide_media_previews": "メディアのプレビューを隠す", "hide_media_previews": "メディアのプレビューを隠す",
"hide_muted_posts": "ミュートしているユーザーの投稿を隠す", "hide_muted_posts": "ミュートしているユーザーの投稿を隠す",
"hide_muted_threads": "ミュートしているスレを隠す", "hide_muted_threads": "ミュートしているスレを隠す",
"hide_post_stats": "投稿の統計を隠す (例: お気に入りの数)", "hide_post_stats": "投稿の統計を隠す (例: お気に入りの数)",
"hide_shoutbox": "Shoutboxを表示しない", "hide_shoutbox": "Shoutboxを表示しない",
"hide_site_favicon": "ページ上にアイコンを隠す",
"hide_site_name": "インスタンス名を隠す",
"hide_threads_with_blocked_users": "ブロックしているユーザーをメンションする投稿を隠す",
"hide_user_stats": "ユーザーの統計を隠す (例: フォロワーの数)", "hide_user_stats": "ユーザーの統計を隠す (例: フォロワーの数)",
"hide_wallpaper": "インスタンスのバックグラウンドを隠す", "hide_wallpaper": "インスタンスのバックグラウンドを隠す",
"hide_wordfiltered_statuses": "単語フィルタに一致する投稿を隠す", "hide_wordfiltered_statuses": "単語フィルタに一致する投稿を隠す",
@ -401,18 +487,26 @@
"instance_default": "(デフォルト: {value})", "instance_default": "(デフォルト: {value})",
"instance_default_simple": "(デフォルト)", "instance_default_simple": "(デフォルト)",
"interface": "インターフェース", "interface": "インターフェース",
"interfaceLanguage": "インターフェースの言語", "interfaceLanguage": "表示言語",
"invalid_theme_imported": "このファイルはPleromaのテーマではありません。テーマは変更されませんでした。", "invalid_theme_imported": "このファイルはPleromaのテーマではありません。テーマは変更されませんでした。",
"limited_availability": "あなたのブラウザではできません", "limited_availability": "あなたのブラウザではできません",
"links": "リンク", "links": "リンク",
"list_aliases_error": "エイリアス を読み込めませんでした:{error}",
"list_backups_error": "バクアップを読み込めませんでした:{error}",
"lock_account_description": "あなたが認めた人だけ、あなたのアカウントをフォローできる", "lock_account_description": "あなたが認めた人だけ、あなたのアカウントをフォローできる",
"loop_video": "ビデオを繰り返す", "loop_video": "ビデオを繰り返す",
"loop_video_silent_only": "音のないビデオだけ繰り返す", "loop_video_silent_only": "音のないビデオだけ繰り返す",
"mascot": "マストドンFEのマスコット",
"max_depth_in_thread": "スレッドの初期の最大レベル数",
"max_thumbnails": "投稿に含まれるサムネイルの最大数", "max_thumbnails": "投稿に含まれるサムネイルの最大数",
"mention_link_bolden_you": "メンションされたら、強調にする",
"mention_link_display": "メンションリンクの表示モード", "mention_link_display": "メンションリンクの表示モード",
"mention_link_display_full": "名前とドメイン、例: {'@'}foo{'@'}example.org", "mention_link_display_full": "名前とドメイン、例: {'@'}foo{'@'}example.org",
"mention_link_display_full_for_remote": "ローカルユーザー: 名前のみ、リモートユーザー: 名前とドメイン", "mention_link_display_full_for_remote": "ローカルユーザー: 名前のみ、リモートユーザー: 名前とドメイン",
"mention_link_display_short": "名前のみ、例:{'@'}foo", "mention_link_display_short": "名前のみ、例:{'@'}foo",
"mention_link_fade_domain": "ドメインを色あせる",
"mention_link_show_avatar": "メンションの側にアバターを表示",
"mention_link_show_tooltip": "リモートユーザに名前とドメインのツールチップを付ける",
"mention_links": "メンションリンク", "mention_links": "メンションリンク",
"mfa": { "mfa": {
"authentication_methods": "認証方法", "authentication_methods": "認証方法",
@ -437,6 +531,12 @@
}, },
"minimal_scopes_mode": "公開範囲選択オプションを最小にする", "minimal_scopes_mode": "公開範囲選択オプションを最小にする",
"more_settings": "その他の設定", "more_settings": "その他の設定",
"move_account": "アカウントの引っ越し",
"move_account_error": "引っ越しできませんでした:{error}",
"move_account_notes": "アカウントを引っ越ししたいなら、先のアカウントでアカウントエイリアスを作成してください",
"move_account_target": "引っ越し先",
"moved_account": "引っ越ししました",
"mute_bot_posts": "botの投稿をミュート",
"mute_export": "ミュートのエクスポート", "mute_export": "ミュートのエクスポート",
"mute_export_button": "ミュートをCSVファイルにエクスポートする", "mute_export_button": "ミュートをCSVファイルにエクスポートする",
"mute_import": "ミュートのインポート", "mute_import": "ミュートのインポート",
@ -446,6 +546,7 @@
"mutes_tab": "ミュート", "mutes_tab": "ミュート",
"name": "名前", "name": "名前",
"name_bio": "名前とプロフィール", "name_bio": "名前とプロフィール",
"new_alias_target": "アカウントエイリアスを作成",
"new_email": "新しいメールアドレス", "new_email": "新しいメールアドレス",
"new_password": "新しいパスワード", "new_password": "新しいパスワード",
"no_blocks": "ブロックはありません", "no_blocks": "ブロックはありません",
@ -455,6 +556,7 @@
"notification_mutes": "特定のユーザーからの通知を止めるには、ミュートしてください。", "notification_mutes": "特定のユーザーからの通知を止めるには、ミュートしてください。",
"notification_setting_block_from_strangers": "フォローしていないユーザーからの通知を拒否する", "notification_setting_block_from_strangers": "フォローしていないユーザーからの通知を拒否する",
"notification_setting_filters": "フィルター", "notification_setting_filters": "フィルター",
"notification_setting_hide_if_cw": "説明がつけたら、プッシュ通知で投稿を隠す",
"notification_setting_hide_notification_contents": "送った人と内容を、プッシュ通知に表示しない", "notification_setting_hide_notification_contents": "送った人と内容を、プッシュ通知に表示しない",
"notification_setting_privacy": "プライバシー", "notification_setting_privacy": "プライバシー",
"notification_visibility": "表示する通知", "notification_visibility": "表示する通知",
@ -463,6 +565,7 @@
"notification_visibility_likes": "お気に入り", "notification_visibility_likes": "お気に入り",
"notification_visibility_mentions": "メンション", "notification_visibility_mentions": "メンション",
"notification_visibility_moves": "ユーザーの引っ越し", "notification_visibility_moves": "ユーザーの引っ越し",
"notification_visibility_polls": "参加した投票の結果",
"notification_visibility_repeats": "リピート", "notification_visibility_repeats": "リピート",
"notifications": "通知", "notifications": "通知",
"nsfw_clickthrough": "NSFWなファイルを隠す", "nsfw_clickthrough": "NSFWなファイルを隠す",
@ -473,6 +576,7 @@
"play_videos_in_modal": "ビデオをメディアビューアーで見る", "play_videos_in_modal": "ビデオをメディアビューアーで見る",
"post_look_feel": "投稿の見た目", "post_look_feel": "投稿の見た目",
"post_status_content_type": "投稿のコンテントタイプ", "post_status_content_type": "投稿のコンテントタイプ",
"posts": "投稿",
"preload_images": "画像を先読みする", "preload_images": "画像を先読みする",
"presets": "プリセット", "presets": "プリセット",
"profile_background": "プロフィールの背景", "profile_background": "プロフィールの背景",
@ -486,7 +590,10 @@
"profile_tab": "プロフィール", "profile_tab": "プロフィール",
"radii_help": "インターフェースの丸さを設定する", "radii_help": "インターフェースの丸さを設定する",
"refresh_token": "トークンを更新", "refresh_token": "トークンを更新",
"remove_alias": "削除",
"remove_backup": "削除",
"render_mfm": "Misskey Markdownを表示", "render_mfm": "Misskey Markdownを表示",
"render_mfm_on_hover": "マウスポインタを置いていないMFMアニメーションを停止",
"replies_in_timeline": "タイムラインのリプライ", "replies_in_timeline": "タイムラインのリプライ",
"reply_visibility_all": "すべてのリプライを見る", "reply_visibility_all": "すべてのリプライを見る",
"reply_visibility_following": "私に宛てられたリプライと、フォローしている人からのリプライを見る", "reply_visibility_following": "私に宛てられたリプライと、フォローしている人からのリプライを見る",
@ -510,15 +617,21 @@
"security": "セキュリティ", "security": "セキュリティ",
"security_tab": "セキュリティ", "security_tab": "セキュリティ",
"sensitive_by_default": "はじめから投稿をセンシティブとして設定", "sensitive_by_default": "はじめから投稿をセンシティブとして設定",
"sensitive_if_subject": "ステータスにサブジェクトをついたらNSFWにする", "sensitive_if_subject": "投稿に警告をついたらNSFWにする",
"set_new_avatar": "新しいアバターを設定する", "set_new_avatar": "新しいアバターを設定する",
"set_new_mascot": "マスコットを更新",
"set_new_profile_background": "新しいプロフィールのバックグラウンドを設定する", "set_new_profile_background": "新しいプロフィールのバックグラウンドを設定する",
"set_new_profile_banner": "新しいプロフィールバナーを設定する", "set_new_profile_banner": "新しいプロフィールバナーを設定する",
"setting_changed": "規定の設定と異なっています", "setting_changed": "規定の設定と異なっています",
"setting_server_side": "この設定は、全クライエントに適用します",
"settings": "設定", "settings": "設定",
"show_admin_badge": "\"管理者\"のバッジを見せる", "show_admin_badge": "\"管理者\"のバッジを見せる",
"show_moderator_badge": "\"モデレーター\"のバッジを見せる", "show_moderator_badge": "\"モデレーター\"のバッジを見せる",
"show_nav_shortcuts": "ページ上にショートカットを追加",
"show_panel_nav_shortcuts": "ページ上にタイムラインショートカットを追加",
"show_scrollbars": "カラムのスクロールバーを表示", "show_scrollbars": "カラムのスクロールバーを表示",
"show_wider_shortcuts": "ページ上にショートカット間のスペースを増大",
"show_yous": "メンション側に「(あなた)」を表示",
"stop_gifs": "カーソルを重ねたとき、GIFを動かす", "stop_gifs": "カーソルを重ねたとき、GIFを動かす",
"streaming": "上までスクロールしたとき、自動的にストリーミングする", "streaming": "上までスクロールしたとき、自動的にストリーミングする",
"style": { "style": {
@ -600,7 +713,7 @@
"fine_print": "私たちの{0}を、読まないでください!", "fine_print": "私たちの{0}を、読まないでください!",
"header": "プレビュー", "header": "プレビュー",
"header_faint": "エラーではありません", "header_faint": "エラーではありません",
"input": "羽田空港に着きました。", "input": "ただいまルーナノヴァ魔法学校に到着しました",
"link": "ハイパーリンク", "link": "ハイパーリンク",
"mono": "monospace", "mono": "monospace",
"text": "これは{0}と{1}の例です" "text": "これは{0}と{1}の例です"
@ -667,9 +780,9 @@
"use_source": "新しいバージョン" "use_source": "新しいバージョン"
} }
}, },
"subject_input_always_show": "サブジェクトフィールドをいつでも表示する", "subject_input_always_show": "警告入力をいつでも表示する",
"subject_line_behavior": "返信するときサブジェクトをコピーする", "subject_line_behavior": "返信するとき警告をコピーする",
"subject_line_email": "メール風: \"re: サブジェクト\"", "subject_line_email": "メール風: \"re: 警告\"",
"subject_line_mastodon": "マストドン風: そのままコピー", "subject_line_mastodon": "マストドン風: そのままコピー",
"subject_line_noop": "コピーしない", "subject_line_noop": "コピーしない",
"text": "文字", "text": "文字",
@ -683,13 +796,19 @@
"third_column_mode_postform": "で投稿フォームを表示", "third_column_mode_postform": "で投稿フォームを表示",
"token": "トークン", "token": "トークン",
"tooltipRadius": "ツールチップとアラート", "tooltipRadius": "ツールチップとアラート",
"translation_language": "翻訳先言語",
"tree_advanced": "スレッドで「返信を見る」、「閉じる」ボタンを表示",
"tree_fade_ancestors": "見ている投稿の親投稿を色あせる",
"type_domains_to_mute": "ミュートしたいドメインを検索", "type_domains_to_mute": "ミュートしたいドメインを検索",
"upload_a_photo": "画像をアップロード", "upload_a_photo": "画像をアップロード",
"useStreamingApi": "投稿と通知を、すぐに受け取る", "useStreamingApi": "投稿と通知を、すぐに受け取る",
"useStreamingApiWarning": "(実験中で、投稿を取りこぼすかもしれないので、おすすめしません)", "useStreamingApiWarning": "(実験中で、投稿を取りこぼすかもしれないので、おすすめしません)",
"use_at_icon": "{'@'}マークをアイコンにする",
"use_contain_fit": "画像のサムネイルを、切り抜かない", "use_contain_fit": "画像のサムネイルを、切り抜かない",
"use_one_click_nsfw": "NSFWなファイルを1クリックで開く", "use_one_click_nsfw": "NSFWなファイルを1クリックで開く",
"user_mutes": "ユーザー", "user_mutes": "ユーザー",
"user_profile_default_tab": "ユーザープロファイルの初期タブ",
"user_profiles": "ユーザーのプロファイル",
"user_settings": "ユーザー設定", "user_settings": "ユーザー設定",
"valid_until": "まで有効", "valid_until": "まで有効",
"values": { "values": {
@ -702,33 +821,71 @@
"title": "バージョン" "title": "バージョン"
}, },
"virtual_scrolling": "タイムラインの描画を最適化する", "virtual_scrolling": "タイムラインの描画を最適化する",
"word_filter": "単語フィルタ" "word_filter": "単語フィルタ",
"wordfilter": "単語フィルター"
}, },
"status": { "status": {
"ancestor_follow": "{numReplies}件の返信を見る",
"ancestor_follow_with_icon": "{icon} {text}",
"attachment_stop_flash": "フラッシュを停止",
"bookmark": "ブックマーク", "bookmark": "ブックマーク",
"collapse_attachments": "ファイルを隠す",
"copy_link": "リンクをコピー", "copy_link": "リンクをコピー",
"delete": "ステータスを削除", "delete": "投稿を削除",
"delete_confirm": "本当にこのステータスを削除してもよろしいですか?", "delete_confirm": "投稿を削除しますか?",
"delete_confirm_accept_button": "削除",
"delete_confirm_cancel_button": "キャンセル",
"delete_confirm_title": "削除を確認してください",
"edit": "編集",
"edit_history": "編集履歴",
"edit_history_modal_title": "{historyCount}回編集",
"edited_at": "{time}に編集",
"expand": "広げる", "expand": "広げる",
"external_source": "外部ソース", "external_source": "外部ソース",
"favorites": "お気に入り", "favorites": "お気に入り",
"hide_attachment": "ファイルを隠す",
"hide_content": "隠す", "hide_content": "隠す",
"hide_full_subject": "隠す", "hide_full_subject": "警告を隠す",
"many_attachments": "ファイルは{number}件があります",
"mentions": "メンション", "mentions": "メンション",
"move_down": "右に移動",
"move_up": "左に移動",
"mute_conversation": "スレッドをミュート", "mute_conversation": "スレッドをミュート",
"nsfw": "閲覧注意", "nsfw": "閲覧注意",
"open_gallery": "ギャラリーを見る",
"override_translation_source_language": "翻訳元言語",
"pin": "プロフィールにピン留め", "pin": "プロフィールにピン留め",
"pinned": "ピン留め", "pinned": "ピン留め",
"plus_more": "ほか{number}件", "plus_more": "ほか{number}件",
"remove_attachment": "削除",
"repeat_confirm": "リピートしますか?",
"repeat_confirm_accept_button": "リピート",
"repeat_confirm_cancel_button": "キャンセル",
"repeat_confirm_title": "リピートを確認してください",
"repeats": "リピート", "repeats": "リピート",
"replies_list": "返信:", "replies_list": "返信:",
"replies_list_with_others": "{numReplies}件の返信を見る",
"reply_to": "返信", "reply_to": "返信",
"show_all_attachments": "全ファイルを表示",
"show_all_conversation": "スレッドの全部を見る",
"show_all_conversation_with_icon": "{icon} {text}",
"show_attachment_description": "説明の略を見る",
"show_attachment_in_modal": "ファイルをウインドウで見る",
"show_content": "見る", "show_content": "見る",
"show_full_subject": "全部見る", "show_full_subject": "警告を見る",
"show_only_conversation_under_this": "この投稿を返信する投稿のみ",
"status_deleted": "この投稿は削除されました", "status_deleted": "この投稿は削除されました",
"status_unavailable": "利用できません", "status_unavailable": "利用できません",
"thread_follow": "{numStatus}件の返信を見る",
"thread_follow_with_icon": "{icon} {text}",
"thread_hide": "スレッドを隠す",
"thread_muted": "ミュートされたスレッド", "thread_muted": "ミュートされたスレッド",
"thread_muted_and_words": "以下の単語を含むため:", "thread_muted_and_words": "以下の単語を含むため:",
"thread_show": "スレッドを表示",
"thread_show_full": "{numStatus}件の返信を見る",
"thread_show_full_with_icon": "{icon} {text}",
"translate": "翻訳",
"translated_from": "{language}から翻訳されました",
"unbookmark": "ブックマーク解除", "unbookmark": "ブックマーク解除",
"unmute_conversation": "スレッドのミュートを解除", "unmute_conversation": "スレッドのミュートを解除",
"unpin": "プロフィールのピン留めを外す", "unpin": "プロフィールのピン留めを外す",
@ -760,10 +917,10 @@
"collapse": "たたむ", "collapse": "たたむ",
"conversation": "スレッド", "conversation": "スレッド",
"error": "タイムラインの読み込みに失敗しました: {0}", "error": "タイムラインの読み込みに失敗しました: {0}",
"load_older": "古いステータス", "load_older": "古い投稿",
"no_more_statuses": "これで終わりです", "no_more_statuses": "これで終わりです",
"no_retweet_hint": "投稿を「フォロワーのみ」または「ダイレクト」にすると、リピートできなくなります", "no_retweet_hint": "投稿を「フォロワーのみ」または「ダイレクト」にすると、リピートできなくなります",
"no_statuses": "ステータスはありません", "no_statuses": "投稿はありません",
"reload": "再読み込み", "reload": "再読み込み",
"repeated": "リピート", "repeated": "リピート",
"show_new": "読み込み", "show_new": "読み込み",
@ -771,12 +928,16 @@
"socket_reconnected": "リアルタイム接続が確立されました", "socket_reconnected": "リアルタイム接続が確立されました",
"up_to_date": "最新" "up_to_date": "最新"
}, },
"toast": {
"no_translation_target_set": "翻訳先言語の設定していません、設定をしてください"
},
"tool_tip": { "tool_tip": {
"accept_follow_request": "フォローリクエストを許可", "accept_follow_request": "フォローリクエストを許可",
"add_reaction": "リアクションを追加", "add_reaction": "リアクションを追加",
"bookmark": "ブックマーク", "bookmark": "ブックマーク",
"favorite": "お気に入り", "favorite": "お気に入り",
"media_upload": "メディアをアップロード", "media_upload": "メディアをアップロード",
"quote": "参照",
"reject_follow_request": "フォローリクエストを拒否", "reject_follow_request": "フォローリクエストを拒否",
"repeat": "リピート", "repeat": "リピート",
"reply": "返信", "reply": "返信",
@ -803,6 +964,7 @@
"deactivate_account": "アカウントをアクティブでなくする", "deactivate_account": "アカウントをアクティブでなくする",
"delete_account": "アカウントを削除", "delete_account": "アカウントを削除",
"delete_user": "ユーザーを削除", "delete_user": "ユーザーを削除",
"delete_user_data_and_deactivate_confirmation": "本当に削除しますか?削除したら、絶対に取り返せません",
"disable_any_subscription": "フォローされないようにする", "disable_any_subscription": "フォローされないようにする",
"disable_remote_subscription": "他のインスタンスからフォローされないようにする", "disable_remote_subscription": "他のインスタンスからフォローされないようにする",
"force_nsfw": "すべての投稿をNSFWにする", "force_nsfw": "すべての投稿をNSFWにする",
@ -817,14 +979,29 @@
"strip_media": "投稿からメディアを除去する" "strip_media": "投稿からメディアを除去する"
}, },
"approve": "受け入れ", "approve": "受け入れ",
"approve_confirm": "フォローリクエストを受け入れますか?",
"approve_confirm_accept_button": "受け入れる",
"approve_confirm_cancel_button": "キャンセル",
"approve_confirm_title": "受け入れを確認してください",
"block": "ブロック", "block": "ブロック",
"block_confirm": "{user}をブロックしますか?",
"block_confirm_accept_button": "ブロック",
"block_confirm_cancel_button": "キャンセル",
"block_confirm_title": "ブロックを確認してください",
"block_progress": "ブロックしています…", "block_progress": "ブロックしています…",
"blocked": "ブロックしています!", "blocked": "ブロックしています!",
"bot": "bot", "bot": "bot",
"deactivated": "無効",
"deny": "お断り", "deny": "お断り",
"deny_confirm": "フォローリクエストを断りますか?",
"deny_confirm_accept_button": "お断り",
"deny_confirm_cancel_button": "キャンセル",
"deny_confirm_title": "お断りを確認してください",
"domain_muted": "ドメインブロックをやめる",
"edit_profile": "プロフィールを編集", "edit_profile": "プロフィールを編集",
"favorites": "お気に入り", "favorites": "お気に入り",
"follow": "フォロー", "follow": "フォロー",
"follow_cancel": "フォローリクエストを取り消す",
"follow_progress": "リクエストしています…", "follow_progress": "リクエストしています…",
"follow_sent": "リクエストを送りました!", "follow_sent": "リクエストを送りました!",
"follow_unfollow": "フォローをやめる", "follow_unfollow": "フォローをやめる",
@ -845,17 +1022,27 @@
"mention": "メンション", "mention": "メンション",
"message": "メッセージ", "message": "メッセージ",
"mute": "ミュート", "mute": "ミュート",
"mute_confirm": "{user}をミュートしますか?",
"mute_confirm_accept_button": "ミュート",
"mute_confirm_cancel_button": "キャンセル",
"mute_confirm_title": "ユーザーをミュート",
"mute_domain": "ドメインをブロック",
"mute_progress": "ミュートしています…", "mute_progress": "ミュートしています…",
"muted": "ミュートしています", "muted": "ミュートしています",
"note": "私的なメモ", "note": "私的なメモ",
"per_day": "/日", "per_day": "/日",
"remote_follow": "リモートフォロー", "remote_follow": "リモートフォロー",
"replies": "投稿と返信",
"report": "通報", "report": "通報",
"show_repeats": "リピートを見る", "show_repeats": "リピートを見る",
"statuses": "ステータス", "statuses": "投稿",
"subscribe": "購読", "subscribe": "購読",
"unblock": "ブロック解除", "unblock": "ブロック解除",
"unblock_progress": "ブロックを解除しています…", "unblock_progress": "ブロックを解除しています…",
"unfollow_confirm": "{user}のフォローをやめますか?",
"unfollow_confirm_accept_button": "やめる",
"unfollow_confirm_cancel_button": "キャンセル",
"unfollow_confirm_title": "フォローをやめる",
"unmute": "ミュート解除", "unmute": "ミュート解除",
"unmute_progress": "ミュートを解除しています…", "unmute_progress": "ミュートを解除しています…",
"unsubscribe": "購読を解除" "unsubscribe": "購読を解除"

View File

@ -98,7 +98,6 @@
"features_panel": { "features_panel": {
"media_proxy": "Mediaproxy", "media_proxy": "Mediaproxy",
"scope_options": "Zichtbaarheidsopties", "scope_options": "Zichtbaarheidsopties",
"shout": "Shoutbox",
"text_limit": "Tekstlimiet", "text_limit": "Tekstlimiet",
"title": "Functies", "title": "Functies",
"upload_limit": "Upload limiet", "upload_limit": "Upload limiet",
@ -394,8 +393,16 @@
"chatMessageRadius": "Chatbericht", "chatMessageRadius": "Chatbericht",
"checkboxRadius": "Checkboxen", "checkboxRadius": "Checkboxen",
"collapse_subject": "Berichten met een onderwerp inklappen", "collapse_subject": "Berichten met een onderwerp inklappen",
"columns": "Kolommen",
"composing": "Opstellen", "composing": "Opstellen",
"confirm_dialogs": "Bevestiging vereisen voor:",
"confirm_dialogs_block": "Iemand blokkeren",
"confirm_dialogs_delete": "Een bericht verwijderen",
"confirm_dialogs_mute": "Iemand negeren",
"confirm_dialogs_repeat": "Herhalen van een bericht",
"confirm_dialogs_unfollow": "Iemand ontvolgen",
"confirm_new_password": "Nieuw wachtwoord bevestigen", "confirm_new_password": "Nieuw wachtwoord bevestigen",
"confirmation_dialogs": "Bevestigingsopties",
"conversation_display": "Gespreksweergave stijl", "conversation_display": "Gespreksweergave stijl",
"conversation_display_linear": "Lineaire weergave", "conversation_display_linear": "Lineaire weergave",
"conversation_display_tree": "Boom weergave", "conversation_display_tree": "Boom weergave",
@ -461,6 +468,8 @@
"hide_muted_threads": "Genegeerde gesprekken verbergen", "hide_muted_threads": "Genegeerde gesprekken verbergen",
"hide_post_stats": "Bericht-statistieken verbergen (bijv. het aantal favorieten)", "hide_post_stats": "Bericht-statistieken verbergen (bijv. het aantal favorieten)",
"hide_shoutbox": "Shoutbox verbergen", "hide_shoutbox": "Shoutbox verbergen",
"hide_site_favicon": "Favicon verbergen in top-paneel",
"hide_site_name": "Instantienaam verbergen in top paneel",
"hide_threads_with_blocked_users": "Gesprekken met geblokkeerde gebruikers verbergen", "hide_threads_with_blocked_users": "Gesprekken met geblokkeerde gebruikers verbergen",
"hide_user_stats": "Gebruikers-statistieken verbergen (bijv. het aantal volgers)", "hide_user_stats": "Gebruikers-statistieken verbergen (bijv. het aantal volgers)",
"hide_wallpaper": "Achtergrond-afbeelding verbergen", "hide_wallpaper": "Achtergrond-afbeelding verbergen",
@ -613,7 +622,10 @@
"settings": "Instellingen", "settings": "Instellingen",
"show_admin_badge": "\"Beheerder\" badge in mijn profiel tonen", "show_admin_badge": "\"Beheerder\" badge in mijn profiel tonen",
"show_moderator_badge": "\"Moderator\" badge in mijn profiel tonen", "show_moderator_badge": "\"Moderator\" badge in mijn profiel tonen",
"show_nav_shortcuts": "Extra navigatie snelkoppelingen tonen in top paneel",
"show_panel_nav_shortcuts": "Tijdlijn navigatie-snelkoppelingen tonen bovenop het paneel",
"show_scrollbars": "Scrollbalk tonen in zijkolommen", "show_scrollbars": "Scrollbalk tonen in zijkolommen",
"show_wider_shortcuts": "Extra ruimte tonen tussen snelkoppelingen in top paneel",
"show_yous": "(Jij)'s tonen", "show_yous": "(Jij)'s tonen",
"stop_gifs": "Geanimeerde afbeeldingen afspelen bij zweven", "stop_gifs": "Geanimeerde afbeeldingen afspelen bij zweven",
"streaming": "Automatisch streamen van nieuwe berichten inschakelen wanneer tot boven gescrold is", "streaming": "Automatisch streamen van nieuwe berichten inschakelen wanneer tot boven gescrold is",

View File

@ -75,7 +75,6 @@
"features_panel": { "features_panel": {
"media_proxy": "Proxy pre médiá", "media_proxy": "Proxy pre médiá",
"scope_options": "Nastavenia rámca", "scope_options": "Nastavenia rámca",
"shout": "Verejné fórum",
"text_limit": "Limit počtu znakov", "text_limit": "Limit počtu znakov",
"title": "Vlastnosti", "title": "Vlastnosti",
"upload_limit": "Limit nahrávania", "upload_limit": "Limit nahrávania",
@ -491,4 +490,4 @@
"more": "Viac", "more": "Viac",
"who_to_follow": "Koho nasledovať" "who_to_follow": "Koho nasledovať"
} }
} }

View File

@ -19,6 +19,8 @@ import reportsModule from './modules/reports.js'
import pollsModule from './modules/polls.js' import pollsModule from './modules/polls.js'
import postStatusModule from './modules/postStatus.js' import postStatusModule from './modules/postStatus.js'
import announcementsModule from './modules/announcements.js' import announcementsModule from './modules/announcements.js'
import editStatusModule from './modules/editStatus.js'
import statusHistoryModule from './modules/statusHistory.js'
import { createI18n } from 'vue-i18n' import { createI18n } from 'vue-i18n'
@ -81,7 +83,9 @@ const persistedStateOptions = {
reports: reportsModule, reports: reportsModule,
polls: pollsModule, polls: pollsModule,
postStatus: postStatusModule, postStatus: postStatusModule,
announcements: announcementsModule announcements: announcementsModule,
editStatus: editStatusModule,
statusHistory: statusHistoryModule
}, },
plugins, plugins,
strict: false // Socket modifies itself, let's ignore this for now. strict: false // Socket modifies itself, let's ignore this for now.

View File

@ -98,6 +98,13 @@ const api = {
showImmediately: timelineData.visibleStatuses.length === 0, showImmediately: timelineData.visibleStatuses.length === 0,
timeline: 'friends' timeline: 'friends'
}) })
} else if (message.event === 'status.update') {
dispatch('addNewStatuses', {
statuses: [message.status],
userId: false,
showImmediately: message.status.id in timelineData.visibleStatusesObject,
timeline: 'friends'
})
} else if (message.event === 'delete') { } else if (message.event === 'delete') {
dispatch('deleteStatusById', message.id) dispatch('deleteStatusById', message.id)
} }
@ -252,6 +259,12 @@ const api = {
if (!fetcher) return if (!fetcher) return
store.commit('removeFetcher', { fetcherName: 'announcements', fetcher }) store.commit('removeFetcher', { fetcherName: 'announcements', fetcher })
}, },
getSupportedTranslationlanguages (store) {
store.state.backendInteractor.getSupportedTranslationlanguages({ store })
.then((data) => {
store.dispatch('setOption', { name: 'supportedTranslationLanguages', value: data })
})
},
// Pleroma websocket // Pleroma websocket
setWsToken (store, token) { setWsToken (store, token) {

View File

@ -34,8 +34,13 @@ export const defaultState = {
hideThreadsWithBlockedUsers: undefined, // instance default hideThreadsWithBlockedUsers: undefined, // instance default
hideWordFilteredPosts: undefined, // instance default hideWordFilteredPosts: undefined, // instance default
muteBotStatuses: undefined, // instance default muteBotStatuses: undefined, // instance default
collapseMessageWithSubject: undefined, // instance default collapseMessageWithSubject: true, // instance default
padEmoji: true, padEmoji: true,
showNavShortcuts: undefined, // instance default
showPanelNavShortcuts: undefined, // instance default
showWiderShortcuts: undefined, // instance default
hideSiteFavicon: undefined, // instance default
hideSiteName: undefined, // instance default
hideAttachments: false, hideAttachments: false,
hideAttachmentsInConv: false, hideAttachmentsInConv: false,
maxThumbnails: 16, maxThumbnails: 16,
@ -76,6 +81,14 @@ export const defaultState = {
minimalScopesMode: undefined, // instance default minimalScopesMode: undefined, // instance default
// This hides statuses filtered via a word filter // This hides statuses filtered via a word filter
hideFilteredStatuses: undefined, // instance default 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
playVideosInModal: false, playVideosInModal: false,
useOneClickNsfw: false, useOneClickNsfw: false,
useContainFit: true, useContainFit: true,
@ -101,7 +114,10 @@ export const defaultState = {
conversationTreeAdvanced: undefined, // instance default conversationTreeAdvanced: undefined, // instance default
conversationOtherRepliesButton: undefined, // instance default conversationOtherRepliesButton: undefined, // instance default
conversationTreeFadeAncestors: undefined, // instance default conversationTreeFadeAncestors: undefined, // instance default
maxDepthInThread: undefined // instance default maxDepthInThread: undefined, // instance default
translationLanguage: undefined, // instance default,
supportedTranslationLanguages: {}, // instance default
userProfileDefaultTab: 'statuses'
} }
// caching the instance default properties // caching the instance default properties
@ -174,6 +190,7 @@ const config = {
case 'interfaceLanguage': case 'interfaceLanguage':
messages.setLanguage(this.getters.i18n, value) messages.setLanguage(this.getters.i18n, value)
Cookies.set(BACKEND_LANGUAGE_COOKIE_NAME, localeService.internalToBackendLocale(value)) Cookies.set(BACKEND_LANGUAGE_COOKIE_NAME, localeService.internalToBackendLocale(value))
dispatch('setInstanceOption', { name: 'interfaceLanguage', value })
break break
case 'thirdColumnMode': case 'thirdColumnMode':
dispatch('setLayoutWidth', undefined) dispatch('setLayoutWidth', undefined)

25
src/modules/editStatus.js Normal file
View File

@ -0,0 +1,25 @@
const editStatus = {
state: {
params: null,
modalActivated: false
},
mutations: {
openEditStatusModal (state, params) {
state.params = params
state.modalActivated = true
},
closeEditStatusModal (state) {
state.modalActivated = false
}
},
actions: {
openEditStatusModal ({ commit }, params) {
commit('openEditStatusModal', params)
},
closeEditStatusModal ({ commit }) {
commit('closeEditStatusModal')
}
}
}
export default editStatus

View File

@ -17,7 +17,7 @@ const defaultState = {
defaultAvatar: '/images/avi.png', defaultAvatar: '/images/avi.png',
defaultBanner: '/images/banner.png', defaultBanner: '/images/banner.png',
background: '/static/aurora_borealis.jpg', background: '/static/aurora_borealis.jpg',
collapseMessageWithSubject: false, collapseMessageWithSubject: true,
greentext: false, greentext: false,
useAtIcon: false, useAtIcon: false,
mentionLinkDisplay: 'short', mentionLinkDisplay: 'short',
@ -34,9 +34,18 @@ const defaultState = {
hideWordFilteredPosts: false, hideWordFilteredPosts: false,
hidePostStats: false, hidePostStats: false,
hideBotIndication: false, hideBotIndication: false,
hideSitename: false, hideSiteFavicon: false,
hideSiteName: false,
hideUserStats: false, hideUserStats: false,
muteBotStatuses: false, muteBotStatuses: false,
modalOnRepeat: false,
modalOnUnfollow: false,
modalOnBlock: true,
modalOnMute: false,
modalOnDelete: true,
modalOnLogout: true,
modalOnApproveFollow: false,
modalOnDenyFollow: false,
loginMethod: 'password', loginMethod: 'password',
logo: '/static/logo.svg', logo: '/static/logo.svg',
logoMargin: '.2em', logoMargin: '.2em',
@ -50,6 +59,8 @@ const defaultState = {
scopeCopy: true, scopeCopy: true,
showFeaturesPanel: true, showFeaturesPanel: true,
showInstanceSpecificPanel: false, showInstanceSpecificPanel: false,
showNavShortcuts: true,
showWiderShortcuts: true,
sidebarRight: false, sidebarRight: false,
subjectLineBehavior: 'email', subjectLineBehavior: 'email',
theme: 'pleroma-dark', theme: 'pleroma-dark',

View File

@ -0,0 +1,25 @@
const statusHistory = {
state: {
params: {},
modalActivated: false
},
mutations: {
openStatusHistoryModal (state, params) {
state.params = params
state.modalActivated = true
},
closeStatusHistoryModal (state) {
state.modalActivated = false
}
},
actions: {
openStatusHistoryModal ({ commit }, params) {
commit('openStatusHistoryModal', params)
},
closeStatusHistoryModal ({ commit }) {
commit('closeStatusHistoryModal')
}
}
}
export default statusHistory

View File

@ -64,7 +64,8 @@ export const defaultState = () => ({
dms: emptyTl(), dms: emptyTl(),
bookmarks: emptyTl(), bookmarks: emptyTl(),
list: emptyTl(), list: emptyTl(),
bubble: emptyTl() bubble: emptyTl(),
replies: emptyTl()
} }
}) })
@ -183,7 +184,7 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
// This makes sure that user timeline won't get data meant for other // This makes sure that user timeline won't get data meant for other
// user. I.e. opening different user profiles makes request which could // user. I.e. opening different user profiles makes request which could
// return data late after user already viewing different user profile // return data late after user already viewing different user profile
if ((timeline === 'user' || timeline === 'media') && timelineObject.userId !== userId) { if ((timeline === 'user' || timeline === 'media' || timeline === 'replies') && timelineObject.userId !== userId) {
return return
} }
@ -250,6 +251,9 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
'status': (status) => { 'status': (status) => {
addStatus(status, showImmediately) addStatus(status, showImmediately)
}, },
'edit': (status) => {
addStatus(status, showImmediately)
},
'retweet': (status) => { 'retweet': (status) => {
// RetweetedStatuses are never shown immediately // RetweetedStatuses are never shown immediately
const retweetedStatus = addStatus(status.retweeted_status, false, false) const retweetedStatus = addStatus(status.retweeted_status, false, false)
@ -425,6 +429,10 @@ export const mutations = {
state.conversationsObject[newStatus.statusnet_conversation_id].forEach(status => { status.thread_muted = newStatus.thread_muted }) 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 }) { setRetweeted (state, { status, value }) {
const newStatus = state.allStatusesObject[status.id] const newStatus = state.allStatusesObject[status.id]
@ -602,6 +610,12 @@ const statuses = {
return rootState.api.backendInteractor.fetchStatus({ id }) return rootState.api.backendInteractor.fetchStatus({ id })
.then((status) => dispatch('addNewStatuses', { statuses: [status] })) .then((status) => dispatch('addNewStatuses', { statuses: [status] }))
}, },
fetchStatusSource ({ rootState, dispatch }, status) {
return apiService.fetchStatusSource({ id: status.id, credentials: rootState.users.currentUser.credentials })
},
fetchStatusHistory ({ rootState, dispatch }, status) {
return apiService.fetchStatusHistory({ status })
},
deleteStatus ({ rootState, commit }, status) { deleteStatus ({ rootState, commit }, status) {
commit('setDeleted', { status }) commit('setDeleted', { status })
apiService.deleteStatus({ id: status.id, credentials: rootState.users.currentUser.credentials }) apiService.deleteStatus({ id: status.id, credentials: rootState.users.currentUser.credentials })
@ -637,6 +651,10 @@ const statuses = {
rootState.api.backendInteractor.unpinOwnStatus({ id: statusId }) rootState.api.backendInteractor.unpinOwnStatus({ id: statusId })
.then((status) => dispatch('addNewStatuses', { statuses: [status] })) .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) { muteConversation ({ rootState, commit }, statusId) {
return rootState.api.backendInteractor.muteConversation({ id: statusId }) return rootState.api.backendInteractor.muteConversation({ id: statusId })
.then((status) => commit('setMutedStatus', status)) .then((status) => commit('setMutedStatus', status))

View File

@ -1,5 +1,5 @@
import { each, map, concat, last, get } from 'lodash' import { each, map, concat, last, get } from 'lodash'
import { parseStatus, parseUser, parseNotification, parseAttachment, parseLinkHeaderPagination } from '../entity_normalizer/entity_normalizer.service.js' import { parseStatus, parseSource, parseUser, parseNotification, parseAttachment, parseLinkHeaderPagination } from '../entity_normalizer/entity_normalizer.service.js'
import { RegistrationError, StatusCodeError } from '../errors/errors' import { RegistrationError, StatusCodeError } from '../errors/errors'
/* eslint-env browser */ /* eslint-env browser */
@ -31,6 +31,8 @@ const MASTODON_LOGIN_URL = '/api/v1/accounts/verify_credentials'
const MASTODON_REGISTRATION_URL = '/api/v1/accounts' const MASTODON_REGISTRATION_URL = '/api/v1/accounts'
const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites' const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites'
const MASTODON_USER_NOTIFICATIONS_URL = '/api/v1/notifications' 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_DISMISS_NOTIFICATION_URL = id => `/api/v1/notifications/${id}/dismiss`
const MASTODON_FAVORITE_URL = id => `/api/v1/statuses/${id}/favourite` const MASTODON_FAVORITE_URL = id => `/api/v1/statuses/${id}/favourite`
const MASTODON_UNFAVORITE_URL = id => `/api/v1/statuses/${id}/unfavourite` const MASTODON_UNFAVORITE_URL = id => `/api/v1/statuses/${id}/unfavourite`
@ -50,6 +52,8 @@ const MASTODON_USER_HOME_TIMELINE_URL = '/api/v1/timelines/home'
const AKKOMA_BUBBLE_TIMELINE_URL = '/api/v1/timelines/bubble' const AKKOMA_BUBBLE_TIMELINE_URL = '/api/v1/timelines/bubble'
const MASTODON_STATUS_URL = id => `/api/v1/statuses/${id}` const MASTODON_STATUS_URL = id => `/api/v1/statuses/${id}`
const MASTODON_STATUS_CONTEXT_URL = id => `/api/v1/statuses/${id}/context` const MASTODON_STATUS_CONTEXT_URL = id => `/api/v1/statuses/${id}/context`
const MASTODON_STATUS_SOURCE_URL = id => `/api/v1/statuses/${id}/source`
const MASTODON_STATUS_HISTORY_URL = id => `/api/v1/statuses/${id}/history`
const MASTODON_USER_URL = id => `/api/v1/accounts/${id}?with_relationships=true` const MASTODON_USER_URL = id => `/api/v1/accounts/${id}?with_relationships=true`
const MASTODON_USER_RELATIONSHIPS_URL = '/api/v1/accounts/relationships' const MASTODON_USER_RELATIONSHIPS_URL = '/api/v1/accounts/relationships'
const MASTODON_USER_TIMELINE_URL = id => `/api/v1/accounts/${id}/statuses` const MASTODON_USER_TIMELINE_URL = id => `/api/v1/accounts/${id}/statuses`
@ -510,6 +514,31 @@ const fetchStatus = ({ id, credentials }) => {
.then((data) => parseStatus(data)) .then((data) => parseStatus(data))
} }
const fetchStatusSource = ({ id, credentials }) => {
let url = MASTODON_STATUS_SOURCE_URL(id)
return fetch(url, { headers: authHeaders(credentials) })
.then((data) => {
if (data.ok) {
return data
}
throw new Error('Error fetching source', data)
})
.then((data) => data.json())
.then((data) => parseSource(data))
}
const fetchStatusHistory = ({ status, credentials }) => {
let url = MASTODON_STATUS_HISTORY_URL(status.id)
return promisedRequest({ url, credentials })
.then((data) => {
data.reverse()
return data.map((item) => {
item.originalStatus = status
return parseStatus(item)
})
})
}
const tagUser = ({ tag, credentials, user }) => { const tagUser = ({ tag, credentials, user }) => {
const screenName = user.screen_name const screenName = user.screen_name
const form = { const form = {
@ -615,6 +644,7 @@ const fetchTimeline = ({
notifications: MASTODON_USER_NOTIFICATIONS_URL, notifications: MASTODON_USER_NOTIFICATIONS_URL,
'publicAndExternal': MASTODON_PUBLIC_TIMELINE, 'publicAndExternal': MASTODON_PUBLIC_TIMELINE,
user: MASTODON_USER_TIMELINE_URL, user: MASTODON_USER_TIMELINE_URL,
replies: MASTODON_USER_TIMELINE_URL,
media: MASTODON_USER_TIMELINE_URL, media: MASTODON_USER_TIMELINE_URL,
list: MASTODON_LIST_TIMELINE_URL, list: MASTODON_LIST_TIMELINE_URL,
favorites: MASTODON_USER_FAVORITES_TIMELINE_URL, favorites: MASTODON_USER_FAVORITES_TIMELINE_URL,
@ -626,7 +656,7 @@ const fetchTimeline = ({
let url = timelineUrls[timeline] let url = timelineUrls[timeline]
if (timeline === 'user' || timeline === 'media') { if (timeline === 'user' || timeline === 'media' || timeline === 'replies') {
url = url(userId) url = url(userId)
} }
@ -658,6 +688,9 @@ const fetchTimeline = ({
if (replyVisibility !== 'all') { if (replyVisibility !== 'all') {
params.push(['reply_visibility', replyVisibility]) params.push(['reply_visibility', replyVisibility])
} }
if (timeline === 'user') {
params.push(['exclude_replies', 1])
}
params.push(['limit', 20]) params.push(['limit', 20])
@ -738,6 +771,18 @@ const unretweet = ({ id, credentials }) => {
.then((data) => parseStatus(data)) .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 }) => { const bookmarkStatus = ({ id, credentials }) => {
return promisedRequest({ return promisedRequest({
url: MASTODON_BOOKMARK_STATUS_URL(id), url: MASTODON_BOOKMARK_STATUS_URL(id),
@ -819,6 +864,54 @@ const postStatus = ({
.then((data) => data.error ? data : parseStatus(data)) .then((data) => data.error ? data : parseStatus(data))
} }
const editStatus = ({
id,
credentials,
status,
spoilerText,
sensitive,
poll,
mediaIds = [],
contentType
}) => {
const form = new FormData()
const pollOptions = poll.options || []
form.append('status', status)
if (spoilerText) form.append('spoiler_text', spoilerText)
if (sensitive) form.append('sensitive', sensitive)
if (contentType) form.append('content_type', contentType)
mediaIds.forEach(val => {
form.append('media_ids[]', val)
})
if (pollOptions.some(option => option !== '')) {
const normalizedPoll = {
expires_in: poll.expiresIn,
multiple: poll.multiple
}
Object.keys(normalizedPoll).forEach(key => {
form.append(`poll[${key}]`, normalizedPoll[key])
})
pollOptions.forEach(option => {
form.append('poll[options][]', option)
})
}
let putHeaders = authHeaders(credentials)
return fetch(MASTODON_STATUS_URL(id), {
body: form,
method: 'PUT',
headers: putHeaders
})
.then((response) => {
return response.json()
})
.then((data) => data.error ? data : parseStatus(data))
}
const deleteStatus = ({ id, credentials }) => { const deleteStatus = ({ id, credentials }) => {
return fetch(MASTODON_DELETE_URL(id), { return fetch(MASTODON_DELETE_URL(id), {
headers: authHeaders(credentials), headers: authHeaders(credentials),
@ -1375,7 +1468,8 @@ const MASTODON_STREAMING_EVENTS = new Set([
'update', 'update',
'notification', 'notification',
'delete', 'delete',
'filters_changed' 'filters_changed',
'status.update'
]) ])
const PLEROMA_STREAMING_EVENTS = new Set([ const PLEROMA_STREAMING_EVENTS = new Set([
@ -1456,6 +1550,8 @@ export const handleMastoWS = (wsEvent) => {
const data = payload ? JSON.parse(payload) : null const data = payload ? JSON.parse(payload) : null
if (event === 'update') { if (event === 'update') {
return { event, status: parseStatus(data) } return { event, status: parseStatus(data) }
} else if (event === 'status.update') {
return { event, status: parseStatus(data) }
} else if (event === 'notification') { } else if (event === 'notification') {
return { event, notification: parseNotification(data) } return { event, notification: parseNotification(data) }
} }
@ -1480,6 +1576,8 @@ const apiService = {
fetchPinnedStatuses, fetchPinnedStatuses,
fetchConversation, fetchConversation,
fetchStatus, fetchStatus,
fetchStatusSource,
fetchStatusHistory,
fetchFriends, fetchFriends,
exportFriends, exportFriends,
fetchFollowers, fetchFollowers,
@ -1500,6 +1598,7 @@ const apiService = {
bookmarkStatus, bookmarkStatus,
unbookmarkStatus, unbookmarkStatus,
postStatus, postStatus,
editStatus,
deleteStatus, deleteStatus,
uploadMedia, uploadMedia,
setMediaDescription, setMediaDescription,
@ -1576,7 +1675,9 @@ const apiService = {
postAnnouncement, postAnnouncement,
editAnnouncement, editAnnouncement,
deleteAnnouncement, deleteAnnouncement,
adminFetchAnnouncements adminFetchAnnouncements,
translateStatus,
getSupportedTranslationlanguages
} }
export default apiService export default apiService

View File

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

View File

@ -242,6 +242,16 @@ export const parseAttachment = (data) => {
return output return output
} }
export const parseSource = (data) => {
const output = {}
output.text = data.text
output.spoiler_text = data.spoiler_text
output.content_type = data.content_type
return output
}
export const parseStatus = (data) => { export const parseStatus = (data) => {
const output = {} const output = {}
const masto = data.hasOwnProperty('account') const masto = data.hasOwnProperty('account')
@ -263,6 +273,8 @@ export const parseStatus = (data) => {
output.tags = data.tags output.tags = data.tags
output.edited_at = data.edited_at
if (data.pleroma) { if (data.pleroma) {
const { pleroma } = data const { pleroma } = data
output.text = pleroma.content ? data.pleroma.content['text/plain'] : data.content output.text = pleroma.content ? data.pleroma.content['text/plain'] : data.content
@ -374,6 +386,10 @@ export const parseStatus = (data) => {
output.favoritedBy = [] output.favoritedBy = []
output.rebloggedBy = [] output.rebloggedBy = []
if (data.hasOwnProperty('originalStatus')) {
Object.assign(output, data.originalStatus)
}
return output return output
} }

View File

@ -49,6 +49,47 @@ const postStatus = ({
}) })
} }
const editStatus = ({
store,
statusId,
status,
spoilerText,
sensitive,
poll,
media = [],
contentType = 'text/plain'
}) => {
const mediaIds = map(media, 'id')
return apiService.editStatus({
id: statusId,
credentials: store.state.users.currentUser.credentials,
status,
spoilerText,
sensitive,
poll,
mediaIds,
contentType
})
.then((data) => {
if (!data.error) {
store.dispatch('addNewStatuses', {
statuses: [data],
timeline: 'friends',
showImmediately: true,
noIdUpdate: true // To prevent missing notices on next pull.
})
}
return data
})
.catch((err) => {
console.error('Error editing status', err)
return {
error: err.message
}
})
}
const uploadMedia = ({ store, formData }) => { const uploadMedia = ({ store, formData }) => {
const credentials = store.state.users.currentUser.credentials const credentials = store.state.users.currentUser.credentials
return apiService.uploadMedia({ credentials, formData }) return apiService.uploadMedia({ credentials, formData })
@ -61,6 +102,7 @@ const setMediaDescription = ({ store, id, description }) => {
const statusPosterService = { const statusPosterService = {
postStatus, postStatus,
editStatus,
uploadMedia, uploadMedia,
setMediaDescription setMediaDescription
} }

View File

@ -1085,6 +1085,11 @@
minimatch "^3.1.2" minimatch "^3.1.2"
strip-json-comments "^3.1.1" strip-json-comments "^3.1.1"
"@fortawesome/fontawesome-common-types@6.1.2":
version "6.1.2"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.1.2.tgz#c1095b1bbabf19f37f9ff0719db38d92a410bcfe"
integrity sha512-wBaAPGz1Awxg05e0PBRkDRuTsy4B3dpBm+zreTTyd9TH4uUM27cAL4xWyWR0rLJCrRwzVsQ4hF3FvM6rqydKPA==
"@fortawesome/fontawesome-common-types@^0.2.36": "@fortawesome/fontawesome-common-types@^0.2.36":
version "0.2.36" version "0.2.36"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.36.tgz#b44e52db3b6b20523e0c57ef8c42d315532cb903" resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.36.tgz#b44e52db3b6b20523e0c57ef8c42d315532cb903"
@ -1102,12 +1107,12 @@
dependencies: dependencies:
"@fortawesome/fontawesome-common-types" "^0.3.0" "@fortawesome/fontawesome-common-types" "^0.3.0"
"@fortawesome/free-regular-svg-icons@5.15.4": "@fortawesome/free-regular-svg-icons@^6.1.2":
version "5.15.4" version "6.1.2"
resolved "https://registry.yarnpkg.com/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-5.15.4.tgz#b97edab436954333bbeac09cfc40c6a951081a02" resolved "https://registry.yarnpkg.com/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.1.2.tgz#9f04009098addcc11d0d185126f058ed042c3099"
integrity sha512-9VNNnU3CXHy9XednJ3wzQp6SwNwT3XaM26oS4Rp391GsxVYA+0oDR2J194YCIWf7jNRCYKjUCOduxdceLrx+xw== integrity sha512-xR4hA+tAwsaTHGfb+25H1gVU/aJ0Rzu+xIUfnyrhaL13yNQ7TWiI2RvzniAaB+VGHDU2a+Pk96Ve+pkN3/+TTQ==
dependencies: dependencies:
"@fortawesome/fontawesome-common-types" "^0.2.36" "@fortawesome/fontawesome-common-types" "6.1.2"
"@fortawesome/free-solid-svg-icons@5.15.4": "@fortawesome/free-solid-svg-icons@5.15.4":
version "5.15.4" version "5.15.4"