aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/boot/after_store.js5
-rw-r--r--src/components/emoji_picker/emoji_picker.js3
-rw-r--r--src/components/emoji_picker/emoji_picker.scss7
-rw-r--r--src/components/emoji_picker/emoji_picker.vue11
-rw-r--r--src/components/mobile_nav/mobile_nav.js18
-rw-r--r--src/components/mobile_nav/mobile_nav.vue19
-rw-r--r--src/components/notification/notification.js6
-rw-r--r--src/components/notification/notification.vue3
-rw-r--r--src/components/notifications/notifications.js37
-rw-r--r--src/components/notifications/notifications.vue8
-rw-r--r--src/components/quick_view_settings/quick_view_settings.js1
-rw-r--r--src/components/registration/registration.js10
-rw-r--r--src/components/registration/registration.vue14
-rw-r--r--src/components/report/report.js1
-rw-r--r--src/components/settings_modal/admin_tabs/emoji_tab.js257
-rw-r--r--src/components/settings_modal/admin_tabs/emoji_tab.scss61
-rw-r--r--src/components/settings_modal/admin_tabs/emoji_tab.vue278
-rw-r--r--src/components/settings_modal/admin_tabs/frontends_tab.js10
-rw-r--r--src/components/settings_modal/admin_tabs/frontends_tab.vue14
-rw-r--r--src/components/settings_modal/helpers/emoji_editing_popover.vue208
-rw-r--r--src/components/settings_modal/helpers/modified_indicator.vue10
-rw-r--r--src/components/settings_modal/helpers/setting.js3
-rw-r--r--src/components/settings_modal/settings_modal.scss12
-rw-r--r--src/components/settings_modal/settings_modal_admin_content.js4
-rw-r--r--src/components/settings_modal/settings_modal_admin_content.vue8
-rw-r--r--src/components/settings_modal/tabs/filtering_tab.vue2
-rw-r--r--src/components/settings_modal/tabs/notifications_tab.js4
-rw-r--r--src/components/settings_modal/tabs/notifications_tab.vue189
-rw-r--r--src/components/settings_modal/tabs/profile_tab.js13
-rw-r--r--src/components/settings_modal/tabs/profile_tab.vue20
-rw-r--r--src/components/settings_modal/tabs/theme_tab/theme_tab.js1
-rw-r--r--src/components/status/status.js15
-rw-r--r--src/components/status/status.vue9
-rw-r--r--src/components/user_avatar/user_avatar.js8
-rw-r--r--src/components/user_avatar/user_avatar.vue11
-rw-r--r--src/components/user_card/user_card.vue8
-rw-r--r--src/components/user_profile/user_profile.js5
-rw-r--r--src/components/user_profile/user_profile.vue3
-rw-r--r--src/components/video_attachment/video_attachment.vue2
-rw-r--r--src/i18n/cs.json590
-rw-r--r--src/i18n/en.json71
-rw-r--r--src/i18n/ja_easy.json30
-rw-r--r--src/i18n/ja_pedantic.json856
-rw-r--r--src/i18n/ko.json15
-rw-r--r--src/i18n/nan-TW.json3
-rw-r--r--src/i18n/zh.json2
-rw-r--r--src/lib/persisted_state.js8
-rw-r--r--src/main.js2
-rw-r--r--src/modules/adminSettings.js2
-rw-r--r--src/modules/config.js18
-rw-r--r--src/modules/instance.js4
-rw-r--r--src/modules/notifications.js169
-rw-r--r--src/modules/serverSideStorage.js1
-rw-r--r--src/modules/statuses.js161
-rw-r--r--src/modules/users.js27
-rw-r--r--src/services/api/api.service.js116
-rw-r--r--src/services/desktop_notification_utils/desktop_notification_utils.js39
-rw-r--r--src/services/entity_normalizer/entity_normalizer.service.js3
-rw-r--r--src/services/favicon_service/favicon_service.js5
-rw-r--r--src/services/notification_utils/notification_utils.js81
-rw-r--r--src/services/notifications_fetcher/notifications_fetcher.service.js14
-rw-r--r--src/services/sw/sw.js (renamed from src/services/push/push.js)55
-rw-r--r--src/sw.js98
63 files changed, 3103 insertions, 565 deletions
diff --git a/src/boot/after_store.js b/src/boot/after_store.js
index 395d4834..7039f85a 100644
--- a/src/boot/after_store.js
+++ b/src/boot/after_store.js
@@ -16,6 +16,7 @@ import backendInteractorService from '../services/backend_interactor_service/bac
import { CURRENT_VERSION } from '../services/theme_data/theme_data.service.js'
import { applyTheme, applyConfig } from '../services/style_setter/style_setter.js'
import FaviconService from '../services/favicon_service/favicon_service.js'
+import { initServiceWorker, updateFocus } from '../services/sw/sw.js'
let staticInitialResults = null
@@ -260,6 +261,7 @@ const getNodeInfo = async ({ store }) => {
store.dispatch('setInstanceOption', { name: 'pollLimits', value: metadata.pollLimits })
store.dispatch('setInstanceOption', { name: 'mailerEnabled', value: metadata.mailerEnabled })
store.dispatch('setInstanceOption', { name: 'quotingAvailable', value: features.includes('quote_posting') })
+ store.dispatch('setInstanceOption', { name: 'groupActorAvailable', value: features.includes('pleroma:group_actors') })
const uploadLimits = metadata.uploadLimits
store.dispatch('setInstanceOption', { name: 'uploadlimit', value: parseInt(uploadLimits.general) })
@@ -344,6 +346,9 @@ const afterStoreSetup = async ({ store, i18n }) => {
store.dispatch('setLayoutHeight', windowHeight())
FaviconService.initFaviconService()
+ initServiceWorker(store)
+
+ window.addEventListener('focus', () => updateFocus())
const overrides = window.___pleromafe_dev_overrides || {}
const server = (typeof overrides.target !== 'undefined') ? overrides.target : window.location.origin
diff --git a/src/components/emoji_picker/emoji_picker.js b/src/components/emoji_picker/emoji_picker.js
index 30c01aa5..eb665c40 100644
--- a/src/components/emoji_picker/emoji_picker.js
+++ b/src/components/emoji_picker/emoji_picker.js
@@ -114,6 +114,7 @@ const EmojiPicker = {
groupsScrolledClass: 'scrolled-top',
keepOpen: false,
customEmojiTimeout: null,
+ hideCustomEmojiInPicker: false,
// Lazy-load only after the first time `showing` becomes true.
contentLoaded: false,
groupRefs: {},
@@ -286,7 +287,7 @@ const EmojiPicker = {
return 0
},
allCustomGroups () {
- if (this.hideCustomEmoji) {
+ if (this.hideCustomEmoji || this.hideCustomEmojiInPicker) {
return {}
}
const emojis = this.$store.getters.groupedCustomEmojis
diff --git a/src/components/emoji_picker/emoji_picker.scss b/src/components/emoji_picker/emoji_picker.scss
index 5bcff33b..aab9251d 100644
--- a/src/components/emoji_picker/emoji_picker.scss
+++ b/src/components/emoji_picker/emoji_picker.scss
@@ -39,11 +39,16 @@ $emoji-picker-emoji-size: 32px;
}
.keep-open,
- .too-many-emoji {
+ .too-many-emoji,
+ .hide-custom-emoji {
padding: 7px;
line-height: normal;
}
+ .hide-custom-emoji {
+ padding-top: 0;
+ }
+
.too-many-emoji {
display: flex;
flex-direction: column;
diff --git a/src/components/emoji_picker/emoji_picker.vue b/src/components/emoji_picker/emoji_picker.vue
index 0788f34c..1231ce2b 100644
--- a/src/components/emoji_picker/emoji_picker.vue
+++ b/src/components/emoji_picker/emoji_picker.vue
@@ -142,6 +142,17 @@
{{ $t('emoji.keep_open') }}
</Checkbox>
</div>
+ <div
+ v-if="!hideCustomEmoji"
+ class="hide-custom-emoji"
+ >
+ <Checkbox
+ v-model="hideCustomEmojiInPicker"
+ @change="onShowing"
+ >
+ {{ $t('emoji.hide_custom_emoji') }}
+ </Checkbox>
+ </div>
</div>
<div
v-if="showingStickers"
diff --git a/src/components/mobile_nav/mobile_nav.js b/src/components/mobile_nav/mobile_nav.js
index b5325116..8c9261b0 100644
--- a/src/components/mobile_nav/mobile_nav.js
+++ b/src/components/mobile_nav/mobile_nav.js
@@ -14,7 +14,8 @@ import {
faBell,
faBars,
faArrowUp,
- faMinus
+ faMinus,
+ faCheckDouble
} from '@fortawesome/free-solid-svg-icons'
library.add(
@@ -22,7 +23,8 @@ library.add(
faBell,
faBars,
faArrowUp,
- faMinus
+ faMinus,
+ faCheckDouble
)
const MobileNav = {
@@ -55,6 +57,12 @@ const MobileNav = {
unseenNotificationsCount () {
return this.unseenNotifications.length + countExtraNotifications(this.$store)
},
+ unseenCount () {
+ return this.unseenNotifications.length
+ },
+ unseenCountBadgeText () {
+ return `${this.unseenCount ? this.unseenCount : ''}`
+ },
hideSitename () { return this.$store.state.instance.hideSitename },
sitename () { return this.$store.state.instance.name },
isChat () {
@@ -67,6 +75,9 @@ const MobileNav = {
shouldConfirmLogout () {
return this.$store.getters.mergedConfig.modalOnLogout
},
+ closingDrawerMarksAsSeen () {
+ return this.$store.getters.mergedConfig.closingDrawerMarksAsSeen
+ },
...mapGetters(['unreadChatCount'])
},
methods: {
@@ -81,7 +92,7 @@ const MobileNav = {
// make sure to mark notifs seen only when the notifs were open and not
// from close-calls.
this.notificationsOpen = false
- if (markRead) {
+ if (markRead && this.closingDrawerMarksAsSeen) {
this.markNotificationsAsSeen()
}
}
@@ -117,7 +128,6 @@ const MobileNav = {
this.hideConfirmLogout()
},
markNotificationsAsSeen () {
- // this.$refs.notifications.markAsSeen()
this.$store.dispatch('markNotificationsAsSeen')
},
onScroll ({ target: { scrollTop, clientHeight, scrollHeight } }) {
diff --git a/src/components/mobile_nav/mobile_nav.vue b/src/components/mobile_nav/mobile_nav.vue
index c2746abe..f20a509d 100644
--- a/src/components/mobile_nav/mobile_nav.vue
+++ b/src/components/mobile_nav/mobile_nav.vue
@@ -50,7 +50,13 @@
@touchmove.stop="notificationsTouchMove"
>
<div class="mobile-notifications-header">
- <span class="title">{{ $t('notifications.notifications') }}</span>
+ <span class="title">
+ {{ $t('notifications.notifications') }}
+ <span
+ v-if="unseenCountBadgeText"
+ class="badge badge-notification unseen-count"
+ >{{ unseenCountBadgeText }}</span>
+ </span>
<span class="spacer" />
<button
v-if="notificationsAtTop"
@@ -67,6 +73,17 @@
</FALayers>
</button>
<button
+ v-if="!closingDrawerMarksAsSeen"
+ class="button-unstyled mobile-nav-button"
+ :title="$t('nav.mobile_notifications_mark_as_seen')"
+ @click.stop.prevent="markNotificationsAsSeen()"
+ >
+ <FAIcon
+ class="fa-scale-110 fa-old-padding"
+ icon="check-double"
+ />
+ </button>
+ <button
class="button-unstyled mobile-nav-button"
:title="$t('nav.mobile_notifications_close')"
@click.stop.prevent="closeMobileNotifications(true)"
diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js
index 420db4f0..0e938c42 100644
--- a/src/components/notification/notification.js
+++ b/src/components/notification/notification.js
@@ -50,6 +50,7 @@ const Notification = {
}
},
props: ['notification'],
+ emits: ['interacted'],
components: {
StatusContent,
UserAvatar,
@@ -72,6 +73,9 @@ const Notification = {
getUser (notification) {
return this.$store.state.users.usersObject[notification.from_profile.id]
},
+ interacted () {
+ this.$emit('interacted')
+ },
toggleMute () {
this.unmuted = !this.unmuted
},
@@ -95,6 +99,7 @@ const Notification = {
}
},
doApprove () {
+ this.$emit('interacted')
this.$store.state.api.backendInteractor.approveUser({ id: this.user.id })
this.$store.dispatch('removeFollowRequest', this.user)
this.$store.dispatch('markSingleNotificationAsSeen', { id: this.notification.id })
@@ -114,6 +119,7 @@ const Notification = {
}
},
doDeny () {
+ this.$emit('interacted')
this.$store.state.api.backendInteractor.denyUser({ id: this.user.id })
.then(() => {
this.$store.dispatch('dismissNotificationLocal', { id: this.notification.id })
diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue
index 6b3315f9..a8eceab0 100644
--- a/src/components/notification/notification.vue
+++ b/src/components/notification/notification.vue
@@ -6,6 +6,7 @@
class="Notification"
:compact="true"
:statusoid="notification.status"
+ @interacted="interacted"
/>
</article>
<article v-else>
@@ -248,7 +249,7 @@
<StatusContent
:class="{ faint: !statusExpanded }"
:compact="!statusExpanded"
- :status="notification.action"
+ :status="notification.status"
/>
</template>
</div>
diff --git a/src/components/notifications/notifications.js b/src/components/notifications/notifications.js
index 571df0f1..a9fa8455 100644
--- a/src/components/notifications/notifications.js
+++ b/src/components/notifications/notifications.js
@@ -8,7 +8,8 @@ import {
notificationsFromStore,
filteredNotificationsFromStore,
unseenNotificationsFromStore,
- countExtraNotifications
+ countExtraNotifications,
+ ACTIONABLE_NOTIFICATION_TYPES
} from '../../services/notification_utils/notification_utils.js'
import FaviconService from '../../services/favicon_service/favicon_service.js'
import { library } from '@fortawesome/fontawesome-svg-core'
@@ -65,13 +66,20 @@ const Notifications = {
return notificationsFromStore(this.$store)
},
error () {
- return this.$store.state.statuses.notifications.error
+ return this.$store.state.notifications.error
},
unseenNotifications () {
return unseenNotificationsFromStore(this.$store)
},
filteredNotifications () {
- return filteredNotificationsFromStore(this.$store, this.filterMode)
+ if (this.unseenAtTop) {
+ return [
+ ...filteredNotificationsFromStore(this.$store).filter(n => this.shouldShowUnseen(n)),
+ ...filteredNotificationsFromStore(this.$store).filter(n => !this.shouldShowUnseen(n))
+ ]
+ } else {
+ return filteredNotificationsFromStore(this.$store, this.filterMode)
+ }
},
unseenCountBadgeText () {
return `${this.unseenCount ? this.unseenCount : ''}${this.extraNotificationsCount ? '*' : ''}`
@@ -79,6 +87,7 @@ const Notifications = {
unseenCount () {
return this.unseenNotifications.length
},
+ ignoreInactionableSeen () { return this.$store.getters.mergedConfig.ignoreInactionableSeen },
extraNotificationsCount () {
return countExtraNotifications(this.$store)
},
@@ -86,7 +95,7 @@ const Notifications = {
return this.unseenNotifications.length + (this.unreadChatCount) + this.unreadAnnouncementCount
},
loading () {
- return this.$store.state.statuses.notifications.loading
+ return this.$store.state.notifications.loading
},
noHeading () {
const { layoutType } = this.$store.state.interface
@@ -108,6 +117,7 @@ const Notifications = {
return this.filteredNotifications.slice(0, this.unseenCount + this.seenToDisplayCount)
},
noSticky () { return this.$store.getters.mergedConfig.disableStickyHeaders },
+ unseenAtTop () { return this.$store.getters.mergedConfig.unseenAtTop },
showExtraNotifications () {
return !this.noExtra
},
@@ -154,11 +164,28 @@ const Notifications = {
scrollToTop () {
const scrollable = this.scrollerRef
scrollable.scrollTo({ top: this.$refs.root.offsetTop })
- // this.$refs.root.scrollIntoView({ behavior: 'smooth', block: 'start' })
},
updateScrollPosition () {
this.showScrollTop = this.$refs.root.offsetTop < this.scrollerRef.scrollTop
},
+ shouldShowUnseen (notification) {
+ if (notification.seen) return false
+
+ const actionable = ACTIONABLE_NOTIFICATION_TYPES.has(notification.type)
+ return this.ignoreInactionableSeen ? actionable : true
+ },
+ /* "Interacted" really refers to "actionable" notifications that require user input,
+ * everything else (likes/repeats/reacts) cannot be acted and therefore we just clear
+ * the "seen" status upon any clicks on them
+ */
+ notificationClicked (notification) {
+ const { id } = notification
+ this.$store.dispatch('notificationClicked', { id })
+ },
+ notificationInteracted (notification) {
+ const { id } = notification
+ this.$store.dispatch('markSingleNotificationAsSeen', { id })
+ },
markAsSeen () {
this.$store.dispatch('markNotificationsAsSeen')
this.seenToDisplayCount = DEFAULT_SEEN_TO_DISPLAY_COUNT
diff --git a/src/components/notifications/notifications.vue b/src/components/notifications/notifications.vue
index 999f8e9c..a0025182 100644
--- a/src/components/notifications/notifications.vue
+++ b/src/components/notifications/notifications.vue
@@ -66,10 +66,14 @@
:key="notification.id"
role="listitem"
class="notification"
- :class="{unseen: !minimalMode && !notification.seen}"
+ :class="{unseen: !minimalMode && shouldShowUnseen(notification)}"
+ @click="e => notificationClicked(notification)"
>
<div class="notification-overlay" />
- <notification :notification="notification" />
+ <notification
+ :notification="notification"
+ @interacted="e => notificationInteracted(notification)"
+ />
</div>
</div>
<div class="panel-footer">
diff --git a/src/components/quick_view_settings/quick_view_settings.js b/src/components/quick_view_settings/quick_view_settings.js
index 2798f37a..67aa6713 100644
--- a/src/components/quick_view_settings/quick_view_settings.js
+++ b/src/components/quick_view_settings/quick_view_settings.js
@@ -52,7 +52,6 @@ const QuickViewSettings = {
get () { return this.mergedConfig.mentionLinkShowAvatar },
set () {
const value = !this.showUserAvatars
- console.log(value)
this.$store.dispatch('setOption', { name: 'mentionLinkShowAvatar', value })
}
},
diff --git a/src/components/registration/registration.js b/src/components/registration/registration.js
index b88bdeec..78d31980 100644
--- a/src/components/registration/registration.js
+++ b/src/components/registration/registration.js
@@ -83,6 +83,8 @@ const registration = {
signedIn: (state) => !!state.users.currentUser,
isPending: (state) => state.users.signUpPending,
serverValidationErrors: (state) => state.users.signUpErrors,
+ signUpNotice: (state) => state.users.signUpNotice,
+ hasSignUpNotice: (state) => !!state.users.signUpNotice.message,
termsOfService: (state) => state.instance.tos,
accountActivationRequired: (state) => state.instance.accountActivationRequired,
accountApprovalRequired: (state) => state.instance.accountApprovalRequired,
@@ -107,8 +109,12 @@ const registration = {
if (!this.v$.$invalid) {
try {
- await this.signUp(this.user)
- this.$router.push({ name: 'friends' })
+ const status = await this.signUp(this.user)
+ if (status === 'ok') {
+ this.$router.push({ name: 'friends' })
+ }
+ // If status is not 'ok' (i.e. it needs further actions to be done
+ // before you can login), display sign up notice, do not switch anywhere
} catch (error) {
console.warn('Registration failed: ', error)
this.setCaptcha()
diff --git a/src/components/registration/registration.vue b/src/components/registration/registration.vue
index 7438a5f4..5c913f94 100644
--- a/src/components/registration/registration.vue
+++ b/src/components/registration/registration.vue
@@ -3,7 +3,10 @@
<div class="panel-heading">
{{ $t('registration.registration') }}
</div>
- <div class="panel-body">
+ <div
+ v-if="!hasSignUpNotice"
+ class="panel-body"
+ >
<form
class="registration-form"
@submit.prevent="submit(user)"
@@ -307,6 +310,11 @@
</div>
</form>
</div>
+ <div v-else>
+ <p class="registration-notice">
+ {{ signUpNotice.message }}
+ </p>
+ </div>
</div>
</template>
@@ -404,6 +412,10 @@ $validations-cRed: #f04124;
}
}
+.registration-notice {
+ margin: 0.6em;
+}
+
@media all and (max-width: 800px) {
.registration-form .container {
flex-direction: column-reverse;
diff --git a/src/components/report/report.js b/src/components/report/report.js
index 5685aa25..f8675c0f 100644
--- a/src/components/report/report.js
+++ b/src/components/report/report.js
@@ -16,7 +16,6 @@ const Report = {
},
computed: {
report () {
- console.log(this.$store.state.reports.reports[this.reportId] || {})
return this.$store.state.reports.reports[this.reportId] || {}
},
state: {
diff --git a/src/components/settings_modal/admin_tabs/emoji_tab.js b/src/components/settings_modal/admin_tabs/emoji_tab.js
new file mode 100644
index 00000000..58e1468f
--- /dev/null
+++ b/src/components/settings_modal/admin_tabs/emoji_tab.js
@@ -0,0 +1,257 @@
+import { clone, assign } from 'lodash'
+import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
+import StringSetting from '../helpers/string_setting.vue'
+import Checkbox from 'components/checkbox/checkbox.vue'
+import StillImage from 'components/still-image/still-image.vue'
+import Select from 'components/select/select.vue'
+import Popover from 'components/popover/popover.vue'
+import ConfirmModal from 'components/confirm_modal/confirm_modal.vue'
+import ModifiedIndicator from '../helpers/modified_indicator.vue'
+import EmojiEditingPopover from '../helpers/emoji_editing_popover.vue'
+
+const EmojiTab = {
+ components: {
+ TabSwitcher,
+ StringSetting,
+ Checkbox,
+ StillImage,
+ Select,
+ Popover,
+ ConfirmModal,
+ ModifiedIndicator,
+ EmojiEditingPopover
+ },
+
+ data () {
+ return {
+ knownLocalPacks: { },
+ knownRemotePacks: { },
+ editedMetadata: { },
+ packName: '',
+ newPackName: '',
+ deleteModalVisible: false,
+ remotePackInstance: '',
+ remotePackDownloadAs: ''
+ }
+ },
+
+ provide () {
+ return { emojiAddr: this.emojiAddr }
+ },
+
+ computed: {
+ pack () {
+ return this.packName !== '' ? this.knownPacks[this.packName] : undefined
+ },
+ packMeta () {
+ if (this.editedMetadata[this.packName] === undefined) {
+ this.editedMetadata[this.packName] = clone(this.pack.pack)
+ }
+
+ return this.editedMetadata[this.packName]
+ },
+ knownPacks () {
+ // Copy the object itself but not the children, so they are still passed by reference and modified
+ const result = clone(this.knownLocalPacks)
+ for (const instName in this.knownRemotePacks) {
+ for (const instPack in this.knownRemotePacks[instName]) {
+ result[`${instPack}@${instName}`] = this.knownRemotePacks[instName][instPack]
+ }
+ }
+
+ return result
+ },
+ downloadWillReplaceLocal () {
+ return (this.remotePackDownloadAs.trim() === '' && this.pack.remote && this.pack.remote.baseName in this.knownLocalPacks) ||
+ (this.remotePackDownloadAs in this.knownLocalPacks)
+ }
+ },
+
+ methods: {
+ reloadEmoji () {
+ this.$store.state.api.backendInteractor.reloadEmoji()
+ },
+ importFromFS () {
+ this.$store.state.api.backendInteractor.importEmojiFromFS()
+ },
+ emojiAddr (name) {
+ if (this.pack.remote !== undefined) {
+ // Remote pack
+ return `${this.pack.remote.instance}/emoji/${encodeURIComponent(this.pack.remote.baseName)}/${name}`
+ } else {
+ return `${this.$store.state.instance.server}/emoji/${encodeURIComponent(this.packName)}/${name}`
+ }
+ },
+
+ createEmojiPack () {
+ this.$store.state.api.backendInteractor.createEmojiPack(
+ { name: this.newPackName }
+ ).then(resp => resp.json()).then(resp => {
+ if (resp === 'ok') {
+ return this.refreshPackList()
+ } else {
+ this.displayError(resp.error)
+ return Promise.reject(resp)
+ }
+ }).then(done => {
+ this.$refs.createPackPopover.hidePopover()
+
+ this.packName = this.newPackName
+ this.newPackName = ''
+ })
+ },
+ deleteEmojiPack () {
+ this.$store.state.api.backendInteractor.deleteEmojiPack(
+ { name: this.packName }
+ ).then(resp => resp.json()).then(resp => {
+ if (resp === 'ok') {
+ return this.refreshPackList()
+ } else {
+ this.displayError(resp.error)
+ return Promise.reject(resp)
+ }
+ }).then(done => {
+ delete this.editedMetadata[this.packName]
+
+ this.deleteModalVisible = false
+ this.packName = ''
+ })
+ },
+
+ metaEdited (prop) {
+ if (!this.pack) return
+
+ const def = this.pack.pack[prop] || ''
+ const edited = this.packMeta[prop] || ''
+ return edited !== def
+ },
+ savePackMetadata () {
+ this.$store.state.api.backendInteractor.saveEmojiPackMetadata({ name: this.packName, newData: this.packMeta }).then(
+ resp => resp.json()
+ ).then(resp => {
+ if (resp.error !== undefined) {
+ this.displayError(resp.error)
+ return
+ }
+
+ // Update actual pack data
+ this.pack.pack = resp
+ // Delete edited pack data, should auto-update itself
+ delete this.editedMetadata[this.packName]
+ })
+ },
+
+ updatePackFiles (newFiles) {
+ this.pack.files = newFiles
+ this.sortPackFiles(this.packName)
+ },
+
+ loadPacksPaginated (listFunction) {
+ const pageSize = 25
+ const allPacks = {}
+
+ return listFunction({ instance: this.remotePackInstance, page: 1, pageSize: 0 })
+ .then(data => data.json())
+ .then(data => {
+ if (data.error !== undefined) { return Promise.reject(data.error) }
+
+ let resultingPromise = Promise.resolve({})
+ for (let i = 0; i < Math.ceil(data.count / pageSize); i++) {
+ resultingPromise = resultingPromise.then(() => listFunction({ instance: this.remotePackInstance, page: i, pageSize })
+ ).then(data => data.json()).then(pageData => {
+ if (pageData.error !== undefined) { return Promise.reject(pageData.error) }
+
+ assign(allPacks, pageData.packs)
+ })
+ }
+
+ return resultingPromise
+ })
+ .then(finished => allPacks)
+ .catch(data => {
+ this.displayError(data)
+ })
+ },
+
+ refreshPackList () {
+ this.loadPacksPaginated(this.$store.state.api.backendInteractor.listEmojiPacks)
+ .then(allPacks => {
+ this.knownLocalPacks = allPacks
+ for (const name of Object.keys(this.knownLocalPacks)) {
+ this.sortPackFiles(name)
+ }
+ })
+ },
+ listRemotePacks () {
+ this.loadPacksPaginated(this.$store.state.api.backendInteractor.listRemoteEmojiPacks)
+ .then(allPacks => {
+ let inst = this.remotePackInstance
+ if (!inst.startsWith('http')) { inst = 'https://' + inst }
+ const instUrl = new URL(inst)
+ inst = instUrl.host
+
+ for (const packName in allPacks) {
+ allPacks[packName].remote = {
+ baseName: packName,
+ instance: instUrl.origin
+ }
+ }
+
+ this.knownRemotePacks[inst] = allPacks
+ for (const pack in this.knownRemotePacks[inst]) {
+ this.sortPackFiles(`${pack}@${inst}`)
+ }
+
+ this.$refs.remotePackPopover.hidePopover()
+ })
+ .catch(data => {
+ this.displayError(data)
+ })
+ },
+ downloadRemotePack () {
+ if (this.remotePackDownloadAs.trim() === '') {
+ this.remotePackDownloadAs = this.pack.remote.baseName
+ }
+
+ this.$store.state.api.backendInteractor.downloadRemoteEmojiPack({
+ instance: this.pack.remote.instance, packName: this.pack.remote.baseName, as: this.remotePackDownloadAs
+ })
+ .then(data => data.json())
+ .then(resp => {
+ if (resp === 'ok') {
+ this.$refs.dlPackPopover.hidePopover()
+
+ return this.refreshPackList()
+ } else {
+ this.displayError(resp.error)
+ return Promise.reject(resp)
+ }
+ }).then(done => {
+ this.packName = this.remotePackDownloadAs
+ this.remotePackDownloadAs = ''
+ })
+ },
+ displayError (msg) {
+ this.$store.dispatch('pushGlobalNotice', {
+ messageKey: 'admin_dash.emoji.error',
+ messageArgs: [msg],
+ level: 'error'
+ })
+ },
+ sortPackFiles (nameOfPack) {
+ // Sort by key
+ const sorted = Object.keys(this.knownPacks[nameOfPack].files).sort().reduce((acc, key) => {
+ if (key.length === 0) return acc
+ acc[key] = this.knownPacks[nameOfPack].files[key]
+ return acc
+ }, {})
+ this.knownPacks[nameOfPack].files = sorted
+ }
+ },
+
+ mounted () {
+ this.refreshPackList()
+ }
+}
+
+export default EmojiTab
diff --git a/src/components/settings_modal/admin_tabs/emoji_tab.scss b/src/components/settings_modal/admin_tabs/emoji_tab.scss
new file mode 100644
index 00000000..cc918870
--- /dev/null
+++ b/src/components/settings_modal/admin_tabs/emoji_tab.scss
@@ -0,0 +1,61 @@
+@import "src/variables";
+
+.emoji-tab {
+ .btn-group .btn:not(:first-child) {
+ margin-left: 0.5em;
+ }
+
+ .pack-info-wrapper {
+ margin-top: 1em;
+ }
+
+ .emoji-info-input {
+ width: 100%;
+ }
+
+ .emoji-data-input {
+ width: 40%;
+ margin-left: 0.5em;
+ margin-right: 0.5em;
+ }
+
+ .emoji {
+ width: 32px;
+ height: 32px;
+ }
+
+ .emoji-unsaved {
+ box-shadow: 0 3px 5px var(--cBlue, $fallback--cBlue);
+ }
+
+ .emoji-list {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 1em 1em;
+ }
+}
+
+.emoji-tab-popover-button:not(:first-child) {
+ margin-left: 0.5em;
+}
+
+.emoji-tab-popover-input {
+ margin-bottom: 0.5em;
+
+ label {
+ display: block;
+ margin-bottom: 0.5em;
+ }
+
+ input {
+ width: 20em;
+ }
+
+ .emoji-tab-popover-file {
+ padding-top: 3px;
+ }
+
+ .warning {
+ color: var(--cOrange, $fallback--cOrange);
+ }
+}
diff --git a/src/components/settings_modal/admin_tabs/emoji_tab.vue b/src/components/settings_modal/admin_tabs/emoji_tab.vue
new file mode 100644
index 00000000..5231f860
--- /dev/null
+++ b/src/components/settings_modal/admin_tabs/emoji_tab.vue
@@ -0,0 +1,278 @@
+<template>
+ <div
+ class="emoji-tab"
+ :label="$t('admin_dash.tabs.emoji')"
+ >
+ <div class="setting-item">
+ <h2>{{ $t('admin_dash.tabs.emoji') }}</h2>
+
+ <ul class="setting-list">
+ <h3>{{ $t('admin_dash.emoji.global_actions') }}</h3>
+
+ <li class="btn-group setting-item">
+ <button
+ class="button button-default btn"
+ type="button"
+ @click="reloadEmoji">
+ {{ $t('admin_dash.emoji.reload') }}
+ </button>
+ <button
+ class="button button-default btn"
+ type="button"
+ @click="importFromFS">
+ {{ $t('admin_dash.emoji.importFS') }}
+ </button>
+ </li>
+
+ <li class="btn-group setting-item">
+ <button
+ class="button button-default btn"
+ type="button"
+ @click="$refs.remotePackPopover.showPopover">
+ {{ $t('admin_dash.emoji.remote_packs') }}
+
+ <Popover
+ ref="remotePackPopover"
+ popover-class="emoji-tab-edit-popover popover-default"
+ trigger="click"
+ placement="bottom"
+ bound-to-selector=".emoji-tab"
+ :bound-to="{ x: 'container' }"
+ :offset="{ y: 5 }"
+ >
+ <template #content>
+ <div class="emoji-tab-popover-input">
+ <h3>{{ $t('admin_dash.emoji.remote_pack_instance') }}</h3>
+ <input v-model="remotePackInstance" :placeholder="$t('admin_dash.emoji.remote_pack_instance')">
+ <button
+ class="button button-default btn emoji-tab-popover-button"
+ type="button"
+ @click="listRemotePacks">
+ {{ $t('admin_dash.emoji.do_list') }}
+ </button>
+ </div>
+ </template>
+ </Popover>
+ </button>
+ </li>
+
+ <h3>{{ $t('admin_dash.emoji.emoji_packs') }}</h3>
+
+ <li>
+ <h4>{{ $t('admin_dash.emoji.edit_pack') }}</h4>
+
+ <Select class="form-control" v-model="packName">
+ <option value="" disabled hidden>{{ $t('admin_dash.emoji.emoji_pack') }}</option>
+ <option v-for="(pack, listPackName) in knownPacks" :label="listPackName" :key="listPackName">
+ {{ listPackName }}
+ </option>
+ </Select>
+
+ <button
+ class="button button-default btn emoji-tab-popover-button"
+ type="button"
+ @click="$refs.createPackPopover.showPopover">
+ {{ $t('admin_dash.emoji.create_pack') }}
+ </button>
+ <Popover
+ ref="createPackPopover"
+ popover-class="emoji-tab-edit-popover popover-default"
+ trigger="click"
+ placement="bottom"
+ bound-to-selector=".emoji-tab"
+ :bound-to="{ x: 'container' }"
+ :offset="{ y: 5 }"
+ >
+ <template #content>
+ <div class="emoji-tab-popover-input">
+ <h3>{{ $t('admin_dash.emoji.new_pack_name') }}</h3>
+ <input v-model="newPackName" :placeholder="$t('admin_dash.emoji.new_pack_name')">
+ <button
+ class="button button-default btn emoji-tab-popover-button"
+ type="button"
+ @click="createEmojiPack">
+ {{ $t('admin_dash.emoji.create') }}
+ </button>
+ </div>
+ </template>
+ </Popover>
+ </li>
+ </ul>
+
+ <div v-if="pack">
+ <div class="pack-info-wrapper">
+ <ul class="setting-list">
+ <li>
+ <label>
+ {{ $t('admin_dash.emoji.description') }}
+ <ModifiedIndicator :changed="metaEdited('description')" message-key="admin_dash.emoji.metadata_changed" />
+
+ <textarea
+ v-model="packMeta.description"
+ :disabled="pack.remote !== undefined"
+ class="bio resize-height" />
+ </label>
+ </li>
+ <li>
+ <label>
+ {{ $t('admin_dash.emoji.homepage') }}
+ <ModifiedIndicator :changed="metaEdited('homepage')" message-key="admin_dash.emoji.metadata_changed" />
+
+ <input
+ class="emoji-info-input" v-model="packMeta.homepage"
+ :disabled="pack.remote !== undefined">
+ </label>
+ </li>
+ <li>
+ <label>
+ {{ $t('admin_dash.emoji.fallback_src') }}
+ <ModifiedIndicator :changed="metaEdited('fallback-src')" message-key="admin_dash.emoji.metadata_changed" />
+
+ <input class="emoji-info-input" v-model="packMeta['fallback-src']" :disabled="pack.remote !== undefined">
+ </label>
+ </li>
+ <li>
+ <label>
+ {{ $t('admin_dash.emoji.fallback_sha256') }}
+
+ <input :disabled="true" class="emoji-info-input" v-model="packMeta['fallback-src-sha256']">
+ </label>
+ </li>
+ <li>
+ <Checkbox :disabled="pack.remote !== undefined" v-model="packMeta['share-files']">
+ {{ $t('admin_dash.emoji.share') }}
+ </Checkbox>
+
+ <ModifiedIndicator :changed="metaEdited('share-files')" message-key="admin_dash.emoji.metadata_changed" />
+ </li>
+ <li class="btn-group">
+ <button
+ class="button button-default btn"
+ type="button"
+ v-if="pack.remote === undefined"
+ @click="savePackMetadata">
+ {{ $t('admin_dash.emoji.save_meta') }}
+ </button>
+ <button
+ class="button button-default btn"
+ type="button"
+ v-if="pack.remote === undefined"
+ @click="savePackMetadata">
+ {{ $t('admin_dash.emoji.revert_meta') }}
+ </button>
+
+ <button
+ class="button button-default btn"
+ v-if="pack.remote === undefined"
+ type="button"
+ @click="deleteModalVisible = true">
+ {{ $t('admin_dash.emoji.delete_pack') }}
+
+ <ConfirmModal
+ v-if="deleteModalVisible"
+ :title="$t('admin_dash.emoji.delete_title')"
+ :cancel-text="$t('status.delete_confirm_cancel_button')"
+ :confirm-text="$t('status.delete_confirm_accept_button')"
+ @cancelled="deleteModalVisible = false"
+ @accepted="deleteEmojiPack" >
+ {{ $t('admin_dash.emoji.delete_confirm', [packName]) }}
+ </ConfirmModal>
+ </button>
+
+ <button
+ class="button button-default btn"
+ type="button"
+ v-if="pack.remote !== undefined"
+ @click="$refs.dlPackPopover.showPopover">
+ {{ $t('admin_dash.emoji.download_pack') }}
+
+ <Popover
+ ref="dlPackPopover"
+ trigger="click"
+ placement="bottom"
+ bound-to-selector=".emoji-tab"
+ popover-class="emoji-tab-edit-popover popover-default"
+ :bound-to="{ x: 'container' }"
+ :offset="{ y: 5 }"
+ >
+ <template #content>
+ <h3>{{ $t('admin_dash.emoji.downloading_pack', [packName]) }}</h3>
+ <div>
+ <div>
+ <div class="emoji-tab-popover-input">
+ <label>
+ {{ $t('admin_dash.emoji.download_as_name') }}
+ <input class="emoji-data-input"
+ v-model="remotePackDownloadAs"
+ :placeholder="$t('admin_dash.emoji.download_as_name_full')">
+ </label>
+
+ <div v-if="downloadWillReplaceLocal" class="warning">
+ <em>{{ $t('admin_dash.emoji.replace_warning') }}</em>
+ </div>
+ </div>
+
+ <button
+ class="button button-default btn"
+ type="button"
+ @click="downloadRemotePack">
+ {{ $t('admin_dash.emoji.download') }}
+ </button>
+ </div>
+ </div>
+ </template>
+ </Popover>
+ </button>
+ </li>
+ </ul>
+ </div>
+
+ <ul class="setting-list">
+ <h4>
+ {{ $t('admin_dash.emoji.files') }}
+
+ <ModifiedIndicator v-if="pack"
+ :changed="$refs.emojiPopovers && $refs.emojiPopovers.some(p => p.isEdited)"
+ message-key="admin_dash.emoji.emoji_changed"/>
+ </h4>
+
+ <div class="emoji-list" v-if="pack">
+ <EmojiEditingPopover
+ v-if="pack.remote === undefined"
+ placement="bottom" new-upload
+ :title="$t('admin_dash.emoji.adding_new')"
+ :packName="packName"
+ @updatePackFiles="updatePackFiles" @displayError="displayError"
+ >
+ <template #trigger>
+ <FAIcon icon="plus" size="2x" :title="$t('admin_dash.emoji.add_file')" />
+ </template>
+ </EmojiEditingPopover>
+
+ <EmojiEditingPopover
+ placement="top" ref="emojiPopovers"
+ v-for="(file, shortcode) in pack.files" :key="shortcode"
+ :title="$t('admin_dash.emoji.editing', [shortcode])"
+ :disabled="pack.remote !== undefined"
+ :shortcode="shortcode" :file="file" :packName="packName"
+ @updatePackFiles="updatePackFiles" @displayError="displayError"
+ >
+ <template #trigger>
+ <StillImage
+ class="emoji"
+ :src="emojiAddr(file)"
+ :title="`:${shortcode}:`"
+ :alt="`:${shortcode}:`"
+ />
+ </template>
+ </EmojiEditingPopover>
+ </div>
+ </ul>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script src="./emoji_tab.js"></script>
+
+<style lang="scss" src="./emoji_tab.scss"></style>
diff --git a/src/components/settings_modal/admin_tabs/frontends_tab.js b/src/components/settings_modal/admin_tabs/frontends_tab.js
index 8163af59..f57310ee 100644
--- a/src/components/settings_modal/admin_tabs/frontends_tab.js
+++ b/src/components/settings_modal/admin_tabs/frontends_tab.js
@@ -55,9 +55,13 @@ const FrontendsTab = {
return fe.refs.includes(frontend.ref)
},
getSuggestedRef (frontend) {
- const defaultFe = this.adminDraft[':pleroma'][':frontends'][':primary']
- if (defaultFe?.name === frontend.name && this.canInstall(defaultFe)) {
- return defaultFe.ref
+ if (this.adminDraft) {
+ const defaultFe = this.adminDraft[':pleroma'][':frontends'][':primary']
+ if (defaultFe?.name === frontend.name && this.canInstall(defaultFe)) {
+ return defaultFe.ref
+ } else {
+ return frontend.refs[0]
+ }
} else {
return frontend.refs[0]
}
diff --git a/src/components/settings_modal/admin_tabs/frontends_tab.vue b/src/components/settings_modal/admin_tabs/frontends_tab.vue
index dd4c9790..097877bc 100644
--- a/src/components/settings_modal/admin_tabs/frontends_tab.vue
+++ b/src/components/settings_modal/admin_tabs/frontends_tab.vue
@@ -6,7 +6,7 @@
<div class="setting-item">
<h2>{{ $t('admin_dash.tabs.frontends') }}</h2>
<p>{{ $t('admin_dash.frontend.wip_notice') }}</p>
- <ul class="setting-list">
+ <ul class="setting-list" v-if="adminDraft">
<li>
<h3>{{ $t('admin_dash.frontend.default_frontend') }}</h3>
<p>{{ $t('admin_dash.frontend.default_frontend_tip') }}</p>
@@ -23,6 +23,10 @@
</ul>
</li>
</ul>
+ <div v-else class="setting-list">
+ {{ $t('admin_dash.frontend.default_frontend_unavail') }}
+ </div>
+
<div class="setting-list relative">
<PanelLoading class="overlay" v-if="working"/>
<h3>{{ $t('admin_dash.frontend.available_frontends') }}</h3>
@@ -33,9 +37,9 @@
>
<strong>{{ frontend.name }}</strong>
{{ ' ' }}
- <span v-if="adminDraft[':pleroma'][':frontends'][':primary']?.name === frontend.name">
+ <span v-if="adminDraft && adminDraft[':pleroma'][':frontends'][':primary']?.name === frontend.name">
<i18n-t
- v-if="adminDraft[':pleroma'][':frontends'][':primary']?.ref === frontend.refs[0]"
+ v-if="adminDraft && adminDraft[':pleroma'][':frontends'][':primary']?.ref === frontend.refs[0]"
keypath="admin_dash.frontend.is_default"
/>
<i18n-t
@@ -43,7 +47,7 @@
keypath="admin_dash.frontend.is_default_custom"
>
<template #version>
- <code>{{ adminDraft[':pleroma'][':frontends'][':primary'].ref }}</code>
+ <code>{{ adminDraft && adminDraft[':pleroma'][':frontends'][':primary'].ref }}</code>
</template>
</i18n-t>
</span>
@@ -134,7 +138,7 @@
class="button button-default btn"
type="button"
:disabled="
- adminDraft[':pleroma'][':frontends'][':primary']?.name === frontend.name &&
+ !adminDraft || adminDraft[':pleroma'][':frontends'][':primary']?.name === frontend.name &&
adminDraft[':pleroma'][':frontends'][':primary']?.ref === frontend.refs[0]
"
@click="setDefault(frontend)"
diff --git a/src/components/settings_modal/helpers/emoji_editing_popover.vue b/src/components/settings_modal/helpers/emoji_editing_popover.vue
new file mode 100644
index 00000000..cdd3e403
--- /dev/null
+++ b/src/components/settings_modal/helpers/emoji_editing_popover.vue
@@ -0,0 +1,208 @@
+<template>
+ <Popover
+ trigger="click"
+ :placement="placement"
+ bound-to-selector=".emoji-list"
+ popover-class="emoji-tab-edit-popover popover-default"
+ ref="emojiPopover"
+ :bound-to="{ x: 'container' }"
+ :offset="{ y: 5 }"
+ :disabled="disabled"
+ :class="{'emoji-unsaved': isEdited}"
+ >
+ <template #trigger>
+ <slot name="trigger" />
+ </template>
+ <template #content>
+ <h3>
+ {{ title }}
+ </h3>
+
+ <StillImage class="emoji" v-if="emojiPreview" :src="emojiPreview" />
+ <div v-else class="emoji"></div>
+
+ <div class="emoji-tab-popover-input" v-if="newUpload">
+ <input
+ type="file"
+ accept="image/*"
+ class="emoji-tab-popover-file"
+ @change="uploadFile = $event.target.files">
+ </div>
+ <div>
+ <div class="emoji-tab-popover-input">
+ <label>
+ {{ $t('admin_dash.emoji.shortcode') }}
+ <input class="emoji-data-input"
+ v-model="editedShortcode"
+ :placeholder="$t('admin_dash.emoji.new_shortcode')">
+ </label>
+ </div>
+
+ <div class="emoji-tab-popover-input">
+ <label>
+ {{ $t('admin_dash.emoji.filename') }}
+
+ <input class="emoji-data-input"
+ v-model="editedFile"
+ :placeholder="$t('admin_dash.emoji.new_filename')">
+ </label>
+ </div>
+
+ <button
+ class="button button-default btn"
+ type="button"
+ :disabled="newUpload ? uploadFile.length == 0 : !isEdited"
+ @click="newUpload ? uploadEmoji() : saveEditedEmoji()">
+ {{ $t('admin_dash.emoji.save') }}
+ </button>
+
+ <template v-if="!newUpload">
+ <button
+ class="button button-default btn emoji-tab-popover-button"
+ type="button"
+ @click="deleteModalVisible = true">
+ {{ $t('admin_dash.emoji.delete') }}
+ </button>
+ <button
+ class="button button-default btn emoji-tab-popover-button"
+ type="button"
+ @click="revertEmoji">
+ {{ $t('admin_dash.emoji.revert') }}
+ </button>
+ <ConfirmModal
+ v-if="deleteModalVisible"
+ :title="$t('admin_dash.emoji.delete_title')"
+ :cancel-text="$t('status.delete_confirm_cancel_button')"
+ :confirm-text="$t('status.delete_confirm_accept_button')"
+ @cancelled="deleteModalVisible = false"
+ @accepted="deleteEmoji" >
+ {{ $t('admin_dash.emoji.delete_confirm', [shortcode]) }}
+ </ConfirmModal>
+ </template>
+ </div>
+ </template>
+ </Popover>
+</template>
+
+<script>
+import Popover from 'components/popover/popover.vue'
+import ConfirmModal from 'components/confirm_modal/confirm_modal.vue'
+import StillImage from 'components/still-image/still-image.vue'
+
+export default {
+ components: { Popover, ConfirmModal, StillImage },
+ data () {
+ return {
+ uploadFile: [],
+ editedShortcode: this.shortcode,
+ editedFile: this.file,
+ deleteModalVisible: false
+ }
+ },
+ computed: {
+ emojiPreview () {
+ if (this.newUpload && this.uploadFile.length > 0) {
+ return URL.createObjectURL(this.uploadFile[0])
+ } else if (!this.newUpload) {
+ return this.emojiAddr(this.file)
+ }
+
+ return null
+ },
+ isEdited () {
+ return !this.newUpload && (this.editedShortcode !== this.shortcode || this.editedFile !== this.file)
+ }
+ },
+ inject: ['emojiAddr'],
+ methods: {
+ saveEditedEmoji () {
+ if (!this.isEdited) return
+
+ this.$store.state.api.backendInteractor.updateEmojiFile(
+ { packName: this.packName, shortcode: this.shortcode, newShortcode: this.editedShortcode, newFilename: this.editedFile, force: false }
+ ).then(resp => {
+ if (resp.error !== undefined) {
+ this.$emit('displayError', resp.error)
+ return Promise.reject(resp.error)
+ }
+
+ return resp.json()
+ }).then(resp => this.$emit('updatePackFiles', resp))
+ },
+ uploadEmoji () {
+ this.$store.state.api.backendInteractor.addNewEmojiFile({
+ packName: this.packName,
+ file: this.uploadFile[0],
+ shortcode: this.editedShortcode,
+ filename: this.editedFile
+ }).then(resp => resp.json()).then(resp => {
+ if (resp.error !== undefined) {
+ this.$emit('displayError', resp.error)
+ return
+ }
+
+ this.$emit('updatePackFiles', resp)
+ this.$refs.emojiPopover.hidePopover()
+
+ this.editedFile = ''
+ this.editedShortcode = ''
+ this.uploadFile = []
+ })
+ },
+ revertEmoji () {
+ this.editedFile = this.file
+ this.editedShortcode = this.shortcode
+ },
+ deleteEmoji () {
+ this.deleteModalVisible = false
+
+ this.$store.state.api.backendInteractor.deleteEmojiFile(
+ { packName: this.packName, shortcode: this.shortcode }
+ ).then(resp => resp.json()).then(resp => {
+ if (resp.error !== undefined) {
+ this.$emit('displayError', resp.error)
+ return
+ }
+
+ this.$emit('updatePackFiles', resp)
+ })
+ }
+ },
+ emits: ['updatePackFiles', 'displayError'],
+ props: {
+ placement: String,
+ disabled: {
+ type: Boolean,
+ default: false
+ },
+
+ newUpload: Boolean,
+
+ title: String,
+ packName: String,
+ shortcode: {
+ type: String,
+ // Only exists when this is not a new upload
+ default: ''
+ },
+ file: {
+ type: String,
+ // Only exists when this is not a new upload
+ default: ''
+ }
+ }
+}
+</script>
+
+<style lang="scss">
+ .emoji-tab-edit-popover {
+ padding-left: 0.5em;
+ padding-right: 0.5em;
+ padding-bottom: 0.5em;
+
+ .emoji {
+ width: 32px;
+ height: 32px;
+ }
+ }
+</style>
diff --git a/src/components/settings_modal/helpers/modified_indicator.vue b/src/components/settings_modal/helpers/modified_indicator.vue
index 45db3fc2..a747cebd 100644
--- a/src/components/settings_modal/helpers/modified_indicator.vue
+++ b/src/components/settings_modal/helpers/modified_indicator.vue
@@ -15,7 +15,7 @@
</template>
<template #content>
<div class="modified-tooltip">
- {{ $t('settings.setting_changed') }}
+ {{ $t(messageKey) }}
</div>
</template>
</Popover>
@@ -33,7 +33,13 @@ library.add(
export default {
components: { Popover },
- props: ['changed']
+ props: {
+ changed: Boolean,
+ messageKey: {
+ type: String,
+ default: 'settings.setting_changed'
+ }
+ }
}
</script>
diff --git a/src/components/settings_modal/helpers/setting.js b/src/components/settings_modal/helpers/setting.js
index b3add346..abf9cfdf 100644
--- a/src/components/settings_modal/helpers/setting.js
+++ b/src/components/settings_modal/helpers/setting.js
@@ -195,7 +195,8 @@ export default {
}
},
canHardReset () {
- return this.realSource === 'admin' && this.$store.state.adminSettings.modifiedPaths.has(this.canonPath.join(' -> '))
+ return this.realSource === 'admin' && this.$store.state.adminSettings.modifiedPaths &&
+ this.$store.state.adminSettings.modifiedPaths.has(this.canonPath.join(' -> '))
},
matchesExpertLevel () {
return (this.expert || 0) <= this.$store.state.config.expertLevel > 0
diff --git a/src/components/settings_modal/settings_modal.scss b/src/components/settings_modal/settings_modal.scss
index 49ef83e0..6bc9459b 100644
--- a/src/components/settings_modal/settings_modal.scss
+++ b/src/components/settings_modal/settings_modal.scss
@@ -3,6 +3,10 @@
.settings-modal {
overflow: hidden;
+ h4 {
+ margin-bottom: 0.5em;
+ }
+
.setting-list,
.option-list {
list-style-type: none;
@@ -15,6 +19,14 @@
.suboptions {
margin-top: 0.3em;
}
+
+ &.two-column {
+ column-count: 2;
+
+ > li {
+ break-inside: avoid;
+ }
+ }
}
.setting-description {
diff --git a/src/components/settings_modal/settings_modal_admin_content.js b/src/components/settings_modal/settings_modal_admin_content.js
index f94721ec..ce835bf2 100644
--- a/src/components/settings_modal/settings_modal_admin_content.js
+++ b/src/components/settings_modal/settings_modal_admin_content.js
@@ -3,6 +3,7 @@ import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
import InstanceTab from './admin_tabs/instance_tab.vue'
import LimitsTab from './admin_tabs/limits_tab.vue'
import FrontendsTab from './admin_tabs/frontends_tab.vue'
+import EmojiTab from './admin_tabs/emoji_tab.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
@@ -33,7 +34,8 @@ const SettingsModalAdminContent = {
InstanceTab,
LimitsTab,
- FrontendsTab
+ FrontendsTab,
+ EmojiTab
},
computed: {
user () {
diff --git a/src/components/settings_modal/settings_modal_admin_content.vue b/src/components/settings_modal/settings_modal_admin_content.vue
index a7a2ac9a..65e23b7e 100644
--- a/src/components/settings_modal/settings_modal_admin_content.vue
+++ b/src/components/settings_modal/settings_modal_admin_content.vue
@@ -60,6 +60,14 @@
>
<FrontendsTab />
</div>
+
+ <div
+ :label="$t('admin_dash.tabs.emoji')"
+ icon="face-smile-beam"
+ data-tab-name="emoji"
+ >
+ <EmojiTab />
+ </div>
</tab-switcher>
</template>
diff --git a/src/components/settings_modal/tabs/filtering_tab.vue b/src/components/settings_modal/tabs/filtering_tab.vue
index 89fdef1a..9e82fcfd 100644
--- a/src/components/settings_modal/tabs/filtering_tab.vue
+++ b/src/components/settings_modal/tabs/filtering_tab.vue
@@ -51,7 +51,7 @@
</li>
<li>
<BooleanSetting path="hideBotIndication">
- {{ $t('settings.hide_bot_indication') }}
+ {{ $t('settings.hide_actor_type_indication') }}
</BooleanSetting>
</li>
<ChoiceSetting
diff --git a/src/components/settings_modal/tabs/notifications_tab.js b/src/components/settings_modal/tabs/notifications_tab.js
index 3c6ab87f..c53b5889 100644
--- a/src/components/settings_modal/tabs/notifications_tab.js
+++ b/src/components/settings_modal/tabs/notifications_tab.js
@@ -16,6 +16,10 @@ const NotificationsTab = {
user () {
return this.$store.state.users.currentUser
},
+ canReceiveReports () {
+ if (!this.user) { return false }
+ return this.user.privileges.includes('reports_manage_reports')
+ },
...SharedComputedObject()
},
methods: {
diff --git a/src/components/settings_modal/tabs/notifications_tab.vue b/src/components/settings_modal/tabs/notifications_tab.vue
index 4dfba444..9ace4c36 100644
--- a/src/components/settings_modal/tabs/notifications_tab.vue
+++ b/src/components/settings_modal/tabs/notifications_tab.vue
@@ -1,6 +1,31 @@
<template>
<div :label="$t('settings.notifications')">
<div class="setting-item">
+ <h2>{{ $t('settings.notification_setting_annoyance') }}</h2>
+ <ul class="setting-list">
+ <li>
+ <BooleanSetting path="closingDrawerMarksAsSeen">
+ {{ $t('settings.notification_setting_drawer_marks_as_seen') }}
+ </BooleanSetting>
+ </li>
+ <li>
+ <BooleanSetting path="ignoreInactionableSeen">
+ {{ $t('settings.notification_setting_ignore_inactionable_seen') }}
+ </BooleanSetting>
+ <div>
+ <small>
+ {{ $t('settings.notification_setting_ignore_inactionable_seen_tip') }}
+ </small>
+ </div>
+ </li>
+ <li>
+ <BooleanSetting path="unseenAtTop" expert="1">
+ {{ $t('settings.notification_setting_unseen_at_top') }}
+ </BooleanSetting>
+ </li>
+ </ul>
+ </div>
+ <div class="setting-item">
<h2>{{ $t('settings.notification_setting_filters') }}</h2>
<ul class="setting-list">
<li>
@@ -11,43 +36,144 @@
{{ $t('settings.notification_setting_block_from_strangers') }}
</BooleanSetting>
</li>
- <li class="select-multiple">
- <span class="label">{{ $t('settings.notification_visibility') }}</span>
- <ul class="option-list">
+ <li>
+ <h3> {{ $t('settings.notification_visibility') }}</h3>
+ <p v-if="expertLevel > 0">{{ $t('settings.notification_setting_filters_chrome_push') }}</p>
+ <ul class="setting-list two-column">
<li>
- <BooleanSetting path="notificationVisibility.likes">
- {{ $t('settings.notification_visibility_likes') }}
- </BooleanSetting>
+ <h4> {{ $t('settings.notification_visibility_mentions') }}</h4>
+ <ul class="setting-list">
+ <li>
+ <BooleanSetting path="notificationVisibility.mentions">
+ {{ $t('settings.notification_visibility_in_column') }}
+ </BooleanSetting>
+ </li>
+ <li>
+ <BooleanSetting path="notificationNative.mentions">
+ {{ $t('settings.notification_visibility_native_notifications') }}
+ </BooleanSetting>
+ </li>
+ </ul>
</li>
<li>
- <BooleanSetting path="notificationVisibility.repeats">
- {{ $t('settings.notification_visibility_repeats') }}
- </BooleanSetting>
+ <h4> {{ $t('settings.notification_visibility_likes') }}</h4>
+ <ul class="setting-list">
+ <li>
+ <BooleanSetting path="notificationVisibility.likes">
+ {{ $t('settings.notification_visibility_in_column') }}
+ </BooleanSetting>
+ </li>
+ <li>
+ <BooleanSetting path="notificationNative.likes">
+ {{ $t('settings.notification_visibility_native_notifications') }}
+ </BooleanSetting>
+ </li>
+ </ul>
</li>
<li>
- <BooleanSetting path="notificationVisibility.follows">
- {{ $t('settings.notification_visibility_follows') }}
- </BooleanSetting>
+ <h4> {{ $t('settings.notification_visibility_repeats') }}</h4>
+ <ul class="setting-list">
+ <li>
+ <BooleanSetting path="notificationVisibility.repeats">
+ {{ $t('settings.notification_visibility_in_column') }}
+ </BooleanSetting>
+ </li>
+ <li>
+ <BooleanSetting path="notificationNative.repeats">
+ {{ $t('settings.notification_visibility_native_notifications') }}
+ </BooleanSetting>
+ </li>
+ </ul>
</li>
<li>
- <BooleanSetting path="notificationVisibility.mentions">
- {{ $t('settings.notification_visibility_mentions') }}
- </BooleanSetting>
+ <h4> {{ $t('settings.notification_visibility_emoji_reactions') }}</h4>
+ <ul class="setting-list">
+ <li>
+ <BooleanSetting path="notificationVisibility.emojiReactions">
+ {{ $t('settings.notification_visibility_in_column') }}
+ </BooleanSetting>
+ </li>
+ <li>
+ <BooleanSetting path="notificationNative.emojiReactions">
+ {{ $t('settings.notification_visibility_native_notifications') }}
+ </BooleanSetting>
+ </li>
+ </ul>
</li>
<li>
- <BooleanSetting path="notificationVisibility.moves">
- {{ $t('settings.notification_visibility_moves') }}
- </BooleanSetting>
+ <h4> {{ $t('settings.notification_visibility_follows') }}</h4>
+ <ul class="setting-list">
+ <li>
+ <BooleanSetting path="notificationVisibility.follows">
+ {{ $t('settings.notification_visibility_in_column') }}
+ </BooleanSetting>
+ </li>
+ <li>
+ <BooleanSetting path="notificationNative.follows">
+ {{ $t('settings.notification_visibility_native_notifications') }}
+ </BooleanSetting>
+ </li>
+ </ul>
</li>
<li>
- <BooleanSetting path="notificationVisibility.emojiReactions">
- {{ $t('settings.notification_visibility_emoji_reactions') }}
- </BooleanSetting>
+ <h4> {{ $t('settings.notification_visibility_follow_requests') }}</h4>
+ <ul class="setting-list">
+ <li>
+ <BooleanSetting path="notificationVisibility.followRequest">
+ {{ $t('settings.notification_visibility_in_column') }}
+ </BooleanSetting>
+ </li>
+ <li>
+ <BooleanSetting path="notificationNative.followRequest">
+ {{ $t('settings.notification_visibility_native_notifications') }}
+ </BooleanSetting>
+ </li>
+ </ul>
</li>
<li>
- <BooleanSetting path="notificationVisibility.polls">
- {{ $t('settings.notification_visibility_polls') }}
- </BooleanSetting>
+ <h4> {{ $t('settings.notification_visibility_moves') }}</h4>
+ <ul class="setting-list">
+ <li>
+ <BooleanSetting path="notificationVisibility.moves">
+ {{ $t('settings.notification_visibility_in_column') }}
+ </BooleanSetting>
+ </li>
+ <li>
+ <BooleanSetting path="notificationNative.moves">
+ {{ $t('settings.notification_visibility_native_notifications') }}
+ </BooleanSetting>
+ </li>
+ </ul>
+ </li>
+ <li>
+ <h4> {{ $t('settings.notification_visibility_polls') }}</h4>
+ <ul class="setting-list">
+ <li>
+ <BooleanSetting path="notificationVisibility.polls">
+ {{ $t('settings.notification_visibility_in_column') }}
+ </BooleanSetting>
+ </li>
+ <li>
+ <BooleanSetting path="notificationNative.polls">
+ {{ $t('settings.notification_visibility_native_notifications') }}
+ </BooleanSetting>
+ </li>
+ </ul>
+ </li>
+ <li v-if="canReceiveReports">
+ <h4> {{ $t('settings.notification_visibility_reports') }}</h4>
+ <ul class="setting-list">
+ <li>
+ <BooleanSetting path="notificationVisibility.reports">
+ {{ $t('settings.notification_visibility_in_column') }}
+ </BooleanSetting>
+ </li>
+ <li>
+ <BooleanSetting path="notificationNative.reports">
+ {{ $t('settings.notification_visibility_native_notifications') }}
+ </BooleanSetting>
+ </li>
+ </ul>
</li>
</ul>
</li>
@@ -108,6 +234,21 @@
>
{{ $t('settings.enable_web_push_notifications') }}
</BooleanSetting>
+ <ul class="setting-list suboptions">
+ <li>
+ <BooleanSetting
+ path="webPushAlwaysShowNotifications"
+ :disabled="!mergedConfig.webPushNotifications"
+ >
+ {{ $t('settings.enable_web_push_always_show') }}
+ </BooleanSetting>
+ <div :class="{ faint: !mergedConfig.webPushNotifications }">
+ <small>
+ {{ $t('settings.enable_web_push_always_show_tip') }}
+ </small>
+ </div>
+ </li>
+ </ul>
</li>
<li>
<BooleanSetting
diff --git a/src/components/settings_modal/tabs/profile_tab.js b/src/components/settings_modal/tabs/profile_tab.js
index eeacad48..dee17450 100644
--- a/src/components/settings_modal/tabs/profile_tab.js
+++ b/src/components/settings_modal/tabs/profile_tab.js
@@ -9,6 +9,7 @@ import suggestor from 'src/components/emoji_input/suggestor.js'
import Autosuggest from 'src/components/autosuggest/autosuggest.vue'
import Checkbox from 'src/components/checkbox/checkbox.vue'
import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue'
+import Select from 'src/components/select/select.vue'
import BooleanSetting from '../helpers/boolean_setting.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js'
import localeService from 'src/services/locale/locale.service.js'
@@ -39,6 +40,7 @@ const ProfileTab = {
showRole: this.$store.state.users.currentUser.show_role,
role: this.$store.state.users.currentUser.role,
bot: this.$store.state.users.currentUser.bot,
+ actorType: this.$store.state.users.currentUser.actor_type,
pickAvatarBtnVisible: true,
bannerUploading: false,
backgroundUploading: false,
@@ -57,7 +59,8 @@ const ProfileTab = {
ProgressButton,
Checkbox,
BooleanSetting,
- InterfaceLanguageSwitcher
+ InterfaceLanguageSwitcher,
+ Select
},
computed: {
user () {
@@ -116,6 +119,12 @@ const ProfileTab = {
bannerImgSrc () {
const src = this.$store.state.users.currentUser.cover_photo
return (!src) ? this.defaultBanner : src
+ },
+ groupActorAvailable () {
+ return this.$store.state.instance.groupActorAvailable
+ },
+ availableActorTypes () {
+ return this.groupActorAvailable ? ['Person', 'Service', 'Group'] : ['Person', 'Service']
}
},
methods: {
@@ -127,7 +136,7 @@ const ProfileTab = {
/* eslint-disable camelcase */
display_name: this.newName,
fields_attributes: this.newFields.filter(el => el != null),
- bot: this.bot,
+ actor_type: this.actorType,
show_role: this.showRole,
birthday: this.newBirthday || '',
show_birthday: this.showBirthday
diff --git a/src/components/settings_modal/tabs/profile_tab.vue b/src/components/settings_modal/tabs/profile_tab.vue
index 1cc850cb..de5219a7 100644
--- a/src/components/settings_modal/tabs/profile_tab.vue
+++ b/src/components/settings_modal/tabs/profile_tab.vue
@@ -109,10 +109,24 @@
</button>
</div>
<p>
- <Checkbox v-model="bot">
- {{ $t('settings.bot') }}
- </Checkbox>
+ <label>
+ {{ $t('settings.actor_type') }}
+ <Select v-model="actorType">
+ <option
+ v-for="option in availableActorTypes"
+ :key="option"
+ :value="option"
+ >
+ {{ $t('settings.actor_type_' + option) }}
+ </option>
+ </Select>
+ </label>
</p>
+ <div v-if="groupActorAvailable">
+ <small>
+ {{ $t('settings.actor_type_description') }}
+ </small>
+ </div>
<p>
<interface-language-switcher
:prompt-text="$t('settings.email_language')"
diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.js b/src/components/settings_modal/tabs/theme_tab/theme_tab.js
index 4a739f73..58f8d44a 100644
--- a/src/components/settings_modal/tabs/theme_tab/theme_tab.js
+++ b/src/components/settings_modal/tabs/theme_tab/theme_tab.js
@@ -755,7 +755,6 @@ export default {
selected () {
this.selectedTheme = Object.entries(this.availableStyles).find(([k, s]) => {
if (Array.isArray(s)) {
- console.log(s[0] === this.selected, this.selected)
return s[0] === this.selected
} else {
return s.name === this.selected
diff --git a/src/components/status/status.js b/src/components/status/status.js
index a339694d..8f22b708 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -154,6 +154,7 @@ const Status = {
'controlledSetMediaPlaying',
'dive'
],
+ emits: ['interacted'],
data () {
return {
uncontrolledReplying: false,
@@ -231,17 +232,11 @@ const Status = {
muteWordHits () {
return muteWordHits(this.status, this.muteWords)
},
- rtBotStatus () {
- return this.statusoid.user.bot
- },
botStatus () {
- return this.status.user.bot
- },
- botIndicator () {
- return this.botStatus && !this.hideBotIndication
+ return this.status.user.actor_type === 'Service'
},
- rtBotIndicator () {
- return this.rtBotStatus && !this.hideBotIndication
+ showActorTypeIndicator () {
+ return !this.hideBotIndication
},
mentionsLine () {
if (!this.headTailLinks) return []
@@ -442,9 +437,11 @@ const Status = {
this.error = error
},
clearError () {
+ this.$emit('interacted')
this.error = undefined
},
toggleReplying () {
+ this.$emit('interacted')
controlledOrUncontrolledToggle(this, 'replying')
},
gotoOriginal (id) {
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index 26fafc91..1c91c36c 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -79,7 +79,7 @@
<UserAvatar
v-if="retweet"
class="left-side repeater-avatar"
- :bot="rtBotIndicator"
+ :show-actor-type-indicator="showActorTypeIndicator"
:better-shadow="betterShadow"
:user="statusoid.user"
/>
@@ -133,7 +133,7 @@
>
<UserAvatar
class="post-avatar"
- :bot="botIndicator"
+ :show-actor-type-indicator="showActorTypeIndicator"
:compact="compact"
:better-shadow="betterShadow"
:user="status.user"
@@ -531,14 +531,17 @@
:visibility="status.visibility"
:logged-in="loggedIn"
:status="status"
+ @click="$emit('interacted')"
/>
<favorite-button
:logged-in="loggedIn"
:status="status"
+ @click="$emit('interacted')"
/>
<ReactButton
v-if="loggedIn"
:status="status"
+ @click="$emit('interacted')"
/>
<extra-buttons
:status="status"
@@ -556,7 +559,7 @@
<UserAvatar
class="post-avatar"
:compact="compact"
- :bot="botIndicator"
+ :show-actor-type-indicator="showActorTypeIndicator"
/>
</div>
<div class="right-side">
diff --git a/src/components/user_avatar/user_avatar.js b/src/components/user_avatar/user_avatar.js
index 33d9a258..ffd81f87 100644
--- a/src/components/user_avatar/user_avatar.js
+++ b/src/components/user_avatar/user_avatar.js
@@ -3,11 +3,13 @@ import StillImage from '../still-image/still-image.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
- faRobot
+ faRobot,
+ faPeopleGroup
} from '@fortawesome/free-solid-svg-icons'
library.add(
- faRobot
+ faRobot,
+ faPeopleGroup
)
const UserAvatar = {
@@ -15,7 +17,7 @@ const UserAvatar = {
'user',
'betterShadow',
'compact',
- 'bot'
+ 'showActorTypeIndicator'
],
data () {
return {
diff --git a/src/components/user_avatar/user_avatar.vue b/src/components/user_avatar/user_avatar.vue
index 91c17611..3cbccec3 100644
--- a/src/components/user_avatar/user_avatar.vue
+++ b/src/components/user_avatar/user_avatar.vue
@@ -18,9 +18,14 @@
:class="{ '-compact': compact }"
/>
<FAIcon
- v-if="bot"
+ v-if="showActorTypeIndicator && user?.actor_type === 'Service'"
icon="robot"
- class="bot-indicator"
+ class="actor-type-indicator"
+ />
+ <FAIcon
+ v-if="showActorTypeIndicator && user?.actor_type === 'Group'"
+ icon="people-group"
+ class="actor-type-indicator"
/>
</span>
</template>
@@ -79,7 +84,7 @@
height: 100%;
}
- .bot-indicator {
+ .actor-type-indicator {
position: absolute;
bottom: 0;
right: 0;
diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue
index 2de14063..2c76a220 100644
--- a/src/components/user_card/user_card.vue
+++ b/src/components/user_card/user_card.vue
@@ -124,11 +124,17 @@
{{ $t(`general.role.${visibleRole}`) }}
</span>
<span
- v-if="user.bot"
+ v-if="user.actor_type === 'Service'"
class="alert user-role"
>
{{ $t('user_card.bot') }}
</span>
+ <span
+ v-if="user.actor_type === 'Group'"
+ class="alert user-role"
+ >
+ {{ $t('user_card.group') }}
+ </span>
</template>
<span v-if="user.locked">
<FAIcon
diff --git a/src/components/user_profile/user_profile.js b/src/components/user_profile/user_profile.js
index acb612ed..751bfd5a 100644
--- a/src/components/user_profile/user_profile.js
+++ b/src/components/user_profile/user_profile.js
@@ -80,6 +80,9 @@ const UserProfile = {
followersTabVisible () {
return this.isUs || !this.user.hide_followers
},
+ favoritesTabVisible () {
+ return this.isUs || !this.user.hide_favorites
+ },
formattedBirthday () {
const browserLocale = localeService.internalToBrowserLocale(this.$i18n.locale)
return this.user.birthday && new Date(Date.parse(this.user.birthday)).toLocaleDateString(browserLocale, { timeZone: 'UTC', day: 'numeric', month: 'long', year: 'numeric' })
@@ -103,6 +106,8 @@ const UserProfile = {
startFetchingTimeline('user', userId)
startFetchingTimeline('media', userId)
if (this.isUs) {
+ startFetchingTimeline('favorites')
+ } else if (!this.user.hide_favorites) {
startFetchingTimeline('favorites', userId)
}
// Fetch all pinned statuses immediately
diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue
index c63a303c..d0618dbb 100644
--- a/src/components/user_profile/user_profile.vue
+++ b/src/components/user_profile/user_profile.vue
@@ -109,7 +109,7 @@
:footer-slipgate="footerRef"
/>
<Timeline
- v-if="isUs"
+ v-if="favoritesTabVisible"
key="favorites"
:label="$t('user_card.favorites')"
:disabled="!favorites.visibleStatuses.length"
@@ -117,6 +117,7 @@
:title="$t('user_card.favorites')"
timeline-name="favorites"
:timeline="favorites"
+ :user-id="userId"
:in-profile="true"
:footer-slipgate="footerRef"
/>
diff --git a/src/components/video_attachment/video_attachment.vue b/src/components/video_attachment/video_attachment.vue
index 8a3ea1e3..df763143 100644
--- a/src/components/video_attachment/video_attachment.vue
+++ b/src/components/video_attachment/video_attachment.vue
@@ -2,7 +2,7 @@
<video
class="video"
preload="metadata"
- :src="attachment.url"
+ :src="attachment.url + '#t=0.00000000000001'"
:loop="loopVideo"
:controls="controls"
:alt="attachment.description"
diff --git a/src/i18n/cs.json b/src/i18n/cs.json
index ca87214e..ac1ff37a 100644
--- a/src/i18n/cs.json
+++ b/src/i18n/cs.json
@@ -9,7 +9,10 @@
"scope_options": "Možnosti rozsahů",
"text_limit": "Textový limit",
"title": "Vlastnosti",
- "who_to_follow": "Koho sledovat"
+ "who_to_follow": "Koho sledovat",
+ "shout": "Shoutbox",
+ "pleroma_chat_messages": "Pleroma Chat",
+ "upload_limit": "Limit pro velikost souborů"
},
"finder": {
"error_fetching_user": "Chyba při načítání uživatele",
@@ -20,12 +23,47 @@
"submit": "Odeslat",
"more": "Více",
"generic_error": "Vyskytla se chyba",
- "optional": "volitelné"
+ "optional": "volitelné",
+ "retry": "Zkuste to znovu",
+ "show_more": "Zobrazit více",
+ "show_less": "Zobrazit méně",
+ "never_show_again": "Znovu již nezobrazovat",
+ "dismiss": "Zahodit",
+ "cancel": "Zrušit",
+ "disable": "Vypnout",
+ "enable": "Zapnout",
+ "close": "Zavřít",
+ "peek": "Nahlédnout",
+ "generic_error_message": "Došlo k chybě: {0}",
+ "error_retry": "Zkuste to prosím znovu",
+ "confirm": "Potvrdit",
+ "verify": "Ověřit",
+ "scope_in_timeline": {
+ "public": "Veřejné",
+ "direct": "Přímá",
+ "unlisted": "Neuvedené",
+ "private": "Pouze pro sledující"
+ },
+ "scroll_to_top": "Přejít na začátek",
+ "role": {
+ "admin": "Správce",
+ "moderator": "Moderátor"
+ },
+ "pin": "Připnout položku",
+ "flash_content": "Klikněte pro zobrazení Flash obsahu pomocí Ruffle (Experimentální, nemusí fungovat).",
+ "flash_security": "Flash obsah může být nebezpečný, protože se jedná o libovolný spustitelný kód.",
+ "flash_fail": "Nepodařilo se načíst Flash obsah. Podrobnosti naleznete v konzoli.",
+ "undo": "Vrátit zpět",
+ "yes": "Ano",
+ "no": "Ne",
+ "unpin": "Odepnout položku",
+ "loading": "Načítání…"
},
"image_cropper": {
"crop_picture": "Oříznout obrázek",
"save": "Uložit",
- "cancel": "Zrušit"
+ "cancel": "Zrušit",
+ "save_without_cropping": "Uložit bez ořezávání"
},
"login": {
"login": "Přihlásit",
@@ -35,17 +73,31 @@
"placeholder": "např. lain",
"register": "Registrovat",
"username": "Uživatelské jméno",
- "hint": "Chcete-li se přidat do diskuze, přihlaste se"
+ "hint": "Chcete-li se přidat do diskuze, přihlaste se",
+ "logout_confirm": "Opravdu se chcete odhlásit?",
+ "logout_confirm_accept_button": "Odhlásit se",
+ "logout_confirm_cancel_button": "Neodhlašovat",
+ "logout_confirm_title": "Potvrzení odhlášení",
+ "authentication_code": "Ověřovací kód",
+ "enter_recovery_code": "Zadejte záložní kód",
+ "enter_two_factor_code": "Zadejte dvoufaktorový ověřovací kód",
+ "recovery_code": "Záložní kód",
+ "heading": {
+ "totp": "Dvoufaktorové ověřování",
+ "recovery": "Dvoufaktorové obnovení"
+ }
},
"media_modal": {
"previous": "Předchozí",
- "next": "Další"
+ "next": "Další",
+ "counter": "{current} / {total}",
+ "hide": "Zavřít prohlížeč médií"
},
"nav": {
"about": "O instanci",
"back": "Zpět",
"chat": "Místní chat",
- "friend_requests": "Požadavky o sledování",
+ "friend_requests": "Požadavky na sledování",
"mentions": "Zmínky",
"dms": "Přímé zprávy",
"public_tl": "Veřejná časová osa",
@@ -53,7 +105,24 @@
"twkn": "Celá známá síť",
"user_search": "Hledání uživatelů",
"who_to_follow": "Koho sledovat",
- "preferences": "Předvolby"
+ "preferences": "Předvolby",
+ "home_timeline": "Domovská časová osa",
+ "timelines": "Časové osy",
+ "search_close": "Zavřít vyhledávací panel",
+ "chats": "Chaty",
+ "lists": "Seznamy",
+ "edit_nav_mobile": "Upravit navigační panel",
+ "mobile_sidebar": "Přepnout mobilní postranní panel",
+ "announcements": "Oznámení",
+ "mobile_notifications_close": "Uzavřít oznámení",
+ "mobile_notifications": "Otevřít oznámení (máte nějaké nepřečtené)",
+ "administration": "Správa",
+ "bookmarks": "Záložky",
+ "search": "Hledat",
+ "edit_pinned": "Upravit připnuté položky",
+ "edit_finish": "Dokončit úpravu",
+ "mobile_notifications_mark_as_seen": "Označit vše jako přečtené",
+ "interactions": "Interakce"
},
"notifications": {
"broken_favorite": "Neznámý příspěvek, hledám jej…",
@@ -61,9 +130,21 @@
"followed_you": "vás nyní sleduje",
"load_older": "Načíst starší oznámení",
"notifications": "Oznámení",
- "read": "Číst!",
+ "read": "Přečíst!",
"repeated_you": "zopakoval/a váš příspěvek",
- "no_more_notifications": "Žádná další oznámení"
+ "no_more_notifications": "Žádná další oznámení",
+ "error": "Nastala chyba při načítání oznámení: {0}",
+ "unread_announcements": "{num} nepřečtené oznámení | {num} nepřečtených oznámení",
+ "unread_chats": "{num} nepřečtených zpráv | {num} nepřečtených zpráv",
+ "unread_follow_requests": "{num} nový požadavek o sledování | {num} nových požadavků o sledování",
+ "configuration_tip": "Může upravit co zde zobrazovat v {theSettings}. {dismiss}",
+ "follow_request": "vás chce sledovat",
+ "migrated_to": "migroval na",
+ "poll_ended": "anketa skončila",
+ "reacted_with": "reagoval/a s {0}",
+ "submitted_report": "Odeslal/a stížnost",
+ "configuration_tip_settings": "nastavení",
+ "configuration_tip_dismiss": "Již nezobrazovat"
},
"post_status": {
"new_status": "Napsat nový příspěvek",
@@ -85,7 +166,27 @@
"private": "Pouze pro sledující - Poslat pouze sledujícím",
"public": "Veřejný - Poslat na veřejné časové osy",
"unlisted": "Neuvedený - Neposlat na veřejné časové osy"
- }
+ },
+ "edit_unsupported_warning": "Pleroma nepodporuje upravování zmínek a anket.",
+ "edit_status": "Upravit příspěvek",
+ "media_description": "Popis médií",
+ "reply_option": "Odpovědět na tento příspěvek",
+ "content_type_selection": "Formát příspěvku",
+ "post": "Odeslat",
+ "empty_status_error": "Nemůžete odeslat prázdný příspěvek bez žádných souborů",
+ "preview_empty": "Prázdné",
+ "media_description_error": "Selhání při aktualizaci médií, zkuste to znovu",
+ "scope_notice": {
+ "public": "Tento příspěvek bude viditelný pro všechny",
+ "private": "Tento příspěvek bude viditelný pouze pro vaše sledující",
+ "unlisted": "Tento příspěvek nebude viditelný ve Veřejné časové ose a časové ose Celá známá síť"
+ },
+ "scope_notice_dismiss": "Zavřít tuto zprávu",
+ "quote_option": "Citovat tento příspěvek",
+ "direct_warning_to_all": "Tento příspěvek budou vidět pouze zmínění uživatelé.",
+ "direct_warning_to_first_only": "Tento příspěvek bude viditelný pouze pro zmíněné uživatele na začátku příspěvku.",
+ "edit_remote_warning": "Jiné vzdálené instance nemusí podporovat úpravy a nemusí přijmout nejnovější verzi vašeho příspěvku.",
+ "preview": "Náhled"
},
"registration": {
"bio": "O vás",
@@ -105,8 +206,18 @@
"email_required": "nemůže být prázdný",
"password_required": "nemůže být prázdné",
"password_confirmation_required": "nemůže být prázdné",
- "password_confirmation_match": "musí být stejné jako heslo"
- }
+ "password_confirmation_match": "musí být stejné jako heslo",
+ "birthday_min_age": "musí být před nebo v {date}",
+ "birthday_required": "nemůže být ponecháno prázdné"
+ },
+ "birthday_optional": "Datum narození (volitelné):",
+ "register": "Registrovat",
+ "reason": "Důvod pro registraci",
+ "reason_placeholder": "Tato instance schvaluje registrace manuálně,\nZdůvodněte administraci důvod registrace.",
+ "birthday": "Datum narození:",
+ "email_language": "V jakém jazyce chcete přijímat emaily z tohoto serveru?",
+ "email_optional": "Email (volitelný)",
+ "bio_optional": "Bio (volitelné)"
},
"settings": {
"app_name": "Název aplikace",
@@ -136,7 +247,7 @@
"default_vis": "Výchozí rozsah viditelnosti",
"delete_account": "Smazat účet",
"delete_account_description": "Trvale smaže váš účet a všechny vaše příspěvky.",
- "delete_account_error": "Při mazání vašeho účtu nastala chyba. Pokud tato chyba bude trvat, kontaktujte prosím admministrátora vaší instance.",
+ "delete_account_error": "Při mazání vašeho účtu nastala chyba. Pokud tato chyba přetrvává, kontaktujte prosím administrátora vaší instance.",
"delete_account_instructions": "Pro potvrzení smazání účtu napište své heslo do pole níže.",
"avatar_size_instruction": "Doporučená minimální velikost pro avatarové obrázky je 150x150 pixelů.",
"export_theme": "Uložit přednastavení",
@@ -152,7 +263,7 @@
"general": "Obecné",
"hide_attachments_in_convo": "Skrývat přílohy v konverzacích",
"hide_attachments_in_tl": "Skrývat přílohy v časové ose",
- "max_thumbnails": "Maximální počet miniatur na příspěvek",
+ "max_thumbnails": "Maximální počet miniatur na příspěvek (prázdné = žádný limit)",
"hide_isp": "Skrýt panel specifický pro instanci",
"preload_images": "Přednačítat obrázky",
"use_one_click_nsfw": "Otevírat citlivé přílohy pouze jedním kliknutím",
@@ -181,7 +292,7 @@
"new_password": "Nové heslo",
"notification_visibility": "Typy oznámení k zobrazení",
"notification_visibility_follows": "Sledující",
- "notification_visibility_likes": "Oblíbení",
+ "notification_visibility_likes": "Oblíbené",
"notification_visibility_mentions": "Zmínky",
"notification_visibility_repeats": "Zopakování",
"no_rich_text_description": "Odstranit ze všech příspěvků formátování textu",
@@ -237,18 +348,34 @@
"true": "ano"
},
"notifications": "Oznámení",
- "enable_web_push_notifications": "Povolit webová push oznámení",
+ "enable_web_push_notifications": "Povolit web push oznámení",
"style": {
"switcher": {
"keep_color": "Ponechat barvy",
"keep_shadows": "Ponechat stíny",
"keep_opacity": "Ponechat průhlednost",
"keep_roundness": "Ponechat kulatost",
- "keep_fonts": "Keep fonts",
+ "keep_fonts": "Ponechat písma",
"save_load_hint": "Možnosti „Ponechat“ dočasně ponechávají aktuálně nastavené možností při volení či nahrávání motivů, také tyto možnosti ukládají při exportování motivu. Pokud není žádné pole zaškrtnuto, uloží export motivu všechno.",
"reset": "Resetovat",
"clear_all": "Vymazat vše",
- "clear_opacity": "Vymazat průhlednost"
+ "clear_opacity": "Vymazat průhlednost",
+ "keep_as_is": "Ponechat jak je",
+ "use_snapshot": "Stará verze",
+ "help": {
+ "migration_napshot_gone": "Z nějakého důvodu chyběl snímek, některé věci můžou vypadat jinak, než si pamatujete.",
+ "fe_upgraded": "Motiv engine PleromaFE byl aktualizován po aktualizaci verze.",
+ "future_version_imported": "Soubor, který jste importoval/a byl vytvořen pro novější verzi FE.",
+ "older_version_imported": "Soubor, který jste importoval/a byl vytvořen pro starší verzi FE.",
+ "v2_imported": "Soubor, který jste importoval/a byl vytvořen pro starší verzi FE. Snažíme se zachovat maximální kompatibilitu, ale může dojít k nekonzistenci.",
+ "snapshot_present": "Snímek motivu byl načten, takže všechny hodnoty byly přepsány. Místo toho můžete načíst skutečná data motivu.",
+ "upgraded_from_v2": "PleromaFE bylo aktualizováno, motiv může vypadat trochu jinak, než si pamatujete.",
+ "snapshot_missing": "V souboru nebyl žádný snímek motivu, takže může vypadat jinak, než bylo původně zamýšleno.",
+ "fe_downgraded": "Verze PleromaFE byla vrácena zpět.",
+ "migration_snapshot_ok": "Pro jistotu byl načten snímek motivu. Můžete zkusit načíst data motivu."
+ },
+ "load_theme": "Načíst motiv",
+ "use_source": "Nová verze"
},
"common": {
"color": "Barva",
@@ -283,7 +410,27 @@
"borders": "Okraje",
"buttons": "Tlačítka",
"inputs": "Vstupní pole",
- "faint_text": "Vybledlý text"
+ "faint_text": "Vybledlý text",
+ "popover": "Popisy, menu, popovery",
+ "underlay": "Podklad",
+ "pressed": "Zmáčknuté",
+ "selectedPost": "Vybraný příspěvek",
+ "selectedMenu": "Vybraná položka menu",
+ "alert_warning": "Varování",
+ "alert_neutral": "Neutrální",
+ "toggled": "Přepnuto",
+ "disabled": "Vypnuto",
+ "tabs": "Karty",
+ "chat": {
+ "incoming": "Příchozí",
+ "border": "Okraj",
+ "outgoing": "Odchozí"
+ },
+ "post": "Příspěvky/Bio uživatelů",
+ "wallpaper": "Tapeta",
+ "poll": "Graf ankety",
+ "icons": "Ikony",
+ "highlight": "Zvýrazněné prvky"
},
"radii": {
"_tab_label": "Kulatost"
@@ -346,7 +493,251 @@
"checkbox": "Pročetl/a jsem podmínky používání",
"link": "hezký malý odkaz"
}
- }
+ },
+ "added_alias": "Přezdívka přidána.",
+ "emoji_reactions_scale": "Měřítko zvětšení reakcí",
+ "file_export_import": {
+ "backup_restore": "Záloha nastavení",
+ "errors": {
+ "file_too_new": "Nekompatibilní hlavní verze: {fileMajor}, tato verze PleromaFE (verze {feMajor}) je příliš stará",
+ "invalid_file": "Vybraný soubor není podporovaná záloha Pleroma nastavení. Žádné změny nebyli provedeny.",
+ "file_too_old": "Nekompatibilní verze: {fileMajor}, verze souboru je příliš stará a nepodporovaná (min. verze {feMajor})",
+ "file_slightly_new": "Menší verze je rozdílná, některé nastavení se nemusí načíst"
+ },
+ "backup_settings": "Zálohovat nastavení do souboru",
+ "backup_settings_theme": "Zálohovat nastavení a motiv do souboru",
+ "restore_settings": "Obnovit nastavení ze souboru"
+ },
+ "backup_failed": "Záloha selhala.",
+ "tree_fade_ancestors": "Zobrazit autory aktuálního příspěvku ve slabém textu",
+ "mention_link_display_full_for_remote": "jako celá jména pouze pro vzdálené uživatele (např. {'@'}foo{'@'}example.org)",
+ "autocomplete_select_first": "Automaticky vybrat prvního kandidáta, když výsledky automatického doplnění jsou dostupné",
+ "import_blocks_from_a_csv_file": "Importovat blokace z csv souboru",
+ "backup_running": "Tato záloha právě probíhá, zpracován záznam {number}. |Tato záloha právě probíhá, zpracováno {number} záznamů.",
+ "changed_email": "Email byl úspěšně změněn!",
+ "chatMessageRadius": "Zpráva chatu",
+ "confirm_dialogs_delete": "mazání příspěvku",
+ "disable_sticky_headers": "Nezanechávat záhlaví sloupců na horní část obrazovky",
+ "third_column_mode_postform": "Editor hlavního příspěvku a navigaci",
+ "columns": "Sloupce",
+ "sensitive_by_default": "Označit příspěvky jako citlivé ve výchozím stavu",
+ "domain_mutes": "Domény",
+ "fallback_language": "Záložní jazyk {index}:",
+ "primary_language": "Hlavní jazyk:",
+ "security": "Zabezpečení",
+ "enter_current_password_to_confirm": "Zadejte vaše současné heslo pro potvrzení vaší identity",
+ "post_look_feel": "Vzhled příspěvků",
+ "mention_links": "Odkazy zmínek",
+ "mfa": {
+ "confirm_and_enable": "Potvrdit a zapnout OTP",
+ "title": "Dvoufázová autentizace",
+ "scan": {
+ "title": "Skenovat",
+ "desc": "Pomocí vaší 2FA aplikace oskenujte QR kód, nebo zadejte klíč:",
+ "secret_code": "Klíč"
+ },
+ "otp": "OTP",
+ "generate_new_recovery_codes": "Vygenerovat nové záložní kódy",
+ "setup_otp": "Nastavit OTP",
+ "wait_pre_setup_otp": "přednastavení OTP",
+ "waiting_a_recovery_codes": "Přijímám záložní kódy…",
+ "recovery_codes_warning": "Zapište nebo uložte si záložní kódy jelikož je znovu již neuvidíte. Pokud ztratíte přístup k vaší 2FA aplikace a záložním kódům nebudete mít možnost se přihlásit k vašemu účtu.",
+ "recovery_codes": "Záložní kódy.",
+ "warning_of_generate_new_codes": "Když vygenerujete nové záložní kódy, tak staré přestanou fungovat.",
+ "authentication_methods": "Autentizační metody",
+ "verify": {
+ "desc": "Pro zapnutí dvoufázové autentizace zadejte kód z vaší 2FA aplikace:"
+ }
+ },
+ "remove_backup": "Odstranit",
+ "email_language": "Jazyk pro přijímání emailů ze serveru",
+ "block_export": "Export blokací",
+ "block_import": "Import blokací",
+ "block_import_error": "Chyba při importování blokací",
+ "mute_export": "Exportovat ztlumení",
+ "mute_export_button": "Exportovat vaše ztlumení jako csv soubor",
+ "wordfilter": "Filtr slov",
+ "user_profiles": "Profily uživatelů",
+ "use_at_icon": "Zobrazovat {'@'} jako ikonu namísto textu",
+ "notification_visibility_moves": "Migrace uživatelů",
+ "hide_followers_count_description": "Nezobrazovat počet sledujících uživatelů",
+ "reply_visibility_self_short": "Zobrazit odpovědi pouze sobě",
+ "third_column_mode_notifications": "Sloupec oznámení",
+ "useStreamingApi": "Přijímat příspěvky a oznámení v reálném čase",
+ "use_websockets": "Používat websockety (Aktualizace v reálném čase)",
+ "user_mutes": "Uživatelé",
+ "mention_link_display": "Zobrazit odkazy na zmínky",
+ "add_language": "Přidat záložní jazyk",
+ "account_backup": "Zálohování účtu",
+ "account_alias": "Přezdívky účtu",
+ "setting_server_side": "Toto nastavení je vázané na váš profil a ovlivňuje všechny vaše sezení a klienty",
+ "block_export_button": "Exportovat vaše blokace jako csv soubor",
+ "blocks_imported": "Blokace importovány! Jejich zpracování může chvíli trvat.",
+ "mute_import": "Importovat ztlumení",
+ "mute_import_error": "Chyba při importování ztlumení",
+ "mutes_imported": "Ztlumení importovány! Jejich zpracování může chvíli trvat.",
+ "account_backup_table_head": "Záloha",
+ "download_backup": "Stáhnout",
+ "import_mutes_from_a_csv_file": "Importovat ztlumení z csv souboru",
+ "account_backup_description": "Toto umožňuje stáhnout archiv vašeho účtu a vašich příspěvků, ale nemůžou být zpětně importovány do Pleroma účtu.",
+ "backup_not_ready": "Tato záloha není zatím připravená.",
+ "list_backups_error": "Chyba při získávání seznamu záloh: {error}",
+ "add_backup": "Vytvořit novou zálohu",
+ "bot": "Toto je účet robota",
+ "change_email": "Změnit email",
+ "change_email_error": "Nastala chyba při změně vašeho emailu.",
+ "confirm_dialogs": "Požádat o potvrzení při",
+ "confirm_dialogs_mute": "ztlumení uživatele",
+ "confirm_dialogs_logout": "odhlašování",
+ "confirm_dialogs_approve_follow": "schvalování sledujícího",
+ "confirm_dialogs_deny_follow": "odmítání sledujícího",
+ "confirm_dialogs_remove_follower": "odstraňování sledujícího",
+ "mutes_and_blocks": "Ztlumení a Blokace",
+ "account_alias_table_head": "Přezdívka",
+ "move_account": "Přesunout účet",
+ "birthday": {
+ "show_birthday": "Zobrazit moje datum narození",
+ "label": "Datum narození"
+ },
+ "account_privacy": "Soukromí",
+ "notification_visibility_in_column": "Zobrazit ve sloupci/zásuvce oznámení",
+ "notification_visibility_reports": "Nahlášení",
+ "notification_visibility_emoji_reactions": "Reakce",
+ "notification_visibility_polls": "Konce anket, ve kterých jste hlasovali",
+ "notification_extra_tip": "Zobrazení tipu přizpůsobení pro další oznámení",
+ "notification_visibility_native_notifications": "Zobrazit nativní oznámení",
+ "notification_visibility_follow_requests": "Požadavky na sledování",
+ "mute_bot_posts": "Skrýt příspěvky od robotů",
+ "hide_bot_indication": "Skrýt indikaci účtů robotů v příspěvcích",
+ "auto_update": "Zobrazovat nové příspěvky automaticky",
+ "url": "URL",
+ "preview": "Náhled",
+ "profile_fields": {
+ "label": "Metadata profilu",
+ "name": "Štítek",
+ "value": "Obsah",
+ "add_field": "Přidat pole"
+ },
+ "hide_favorites_description": "Nezobrazovat seznam oblíbených příspěvků (uživatelé stále budou notifikování)",
+ "right_sidebar": "Prohodit pořadí sloupců",
+ "hide_scrobbles": "Skrýt scrobbles",
+ "hide_shoutbox": "Skrýt shoutbox instance",
+ "new_email": "Nový email",
+ "notification_show_extra": "Zobrazit další oznámení ve sloupci oznámení",
+ "reply_visibility_following_short": "Zobrazit odpovědi mým sledujícím",
+ "search_user_to_block": "Hledat koho chcete zablokovat",
+ "search_user_to_mute": "Hledat koho chcete ztlumit",
+ "reset_avatar_confirm": "Opravdu chcete resetovat avatar?",
+ "tree_advanced": "Umožnit více flexibilní navigaci ve stromovém zobrazení",
+ "conversation_display_linear_quick": "Lineární zobrazení",
+ "max_depth_in_thread": "Maximální počet zobrazených úrovní ve vlákně ve výchozím stavu",
+ "add_backup_error": "Chyba při přidávání nové zálohy: {error}",
+ "added_backup": "Přidána nová záloha.",
+ "word_filter_and_more": "Filtr slov a další...",
+ "posts": "Příspěvky",
+ "reset_banner_confirm": "Opravdu chcete resetovat banner?",
+ "reset_background_confirm": "Opravdu chcete resetovat pozadí?",
+ "reset_avatar": "Resetovat avatar",
+ "reset_profile_background": "Resetovat pozadí profilu",
+ "reset_profile_banner": "Resetovat banner profilu",
+ "type_domains_to_mute": "Hledat domény ke ztlumení",
+ "virtual_scrolling": "Optimalizovat vykreslování časové osy",
+ "remove_language": "Odstranit",
+ "expert_mode": "Zobrazit pokročilé nastavení",
+ "save": "Uložit změny",
+ "setting_changed": "Nastavení je rozdílné od výchozího",
+ "lists_navigation": "Zobrazovat seznamy v navigaci",
+ "allow_following_move": "Povolit automatické sledování pokud se sledovaný účet přesune",
+ "confirm_dialogs_repeat": "opakování příspěvku",
+ "confirm_dialogs_unfollow": "zrušení sledování uživatele",
+ "confirm_dialogs_block": "blokování uživatele",
+ "list_aliases_error": "Chyba při zjišťování přezdívek: {error}",
+ "remove_alias": "Odstranit tuto přezdívku",
+ "new_alias_target": "Přidat novou přezdívku (např. {example})",
+ "add_alias_error": "Chyba při přidávání přezdívky: {error}",
+ "hide_list_aliases_error_action": "Zavřít",
+ "move_account_notes": "Pokud chcete přesunut účet jinam, musíte jít na cílový účet a přidat přezdívku ukazující na tento účet.",
+ "move_account_target": "Cílový účet (např. {example})",
+ "moved_account": "Účet přesunut.",
+ "move_account_error": "Chyba při přesouvání účtu: {error}",
+ "discoverable": "Umožnit objevení tohoto účtu ve výsledcích vyhledávání a v jiných službách",
+ "pad_emoji": "Přidat mezeru okolo emoji při přidávání emoji z výběru",
+ "emoji_reactions_on_timeline": "Zobrazit emoji reakce u příspěvků",
+ "hide_media_previews": "Schovat náhledy médií",
+ "hide_muted_posts": "Skrýt příspěvky od ztlumených uživatelů",
+ "hide_all_muted_posts": "Skrýt ztlumené uživatele",
+ "navbar_column_stretch": "Protáhnout navbar na šířku sloupců",
+ "always_show_post_button": "Vždy zobrazovat plovoucí tlačítko pro nový příspěvek",
+ "hide_wallpaper": "Skrýt pozadí instance",
+ "hide_wordfiltered_statuses": "Skrýt slovně filtrované příspěvky",
+ "hide_muted_threads": "Skrýt ztlumené vlákna",
+ "notification_extra_chats": "Zobrazit nepřečtené chaty",
+ "notification_extra_announcements": "Zobrazit nepřečtené oznámení",
+ "notification_extra_follow_requests": "Zobrazit nové požadavky na sledování",
+ "hide_follows_count_description": "Nezobrazovat počet sledovaných uživatelů",
+ "autohide_floating_post_button": "Automaticky skrýt tlačítko nového příspěvku (mobilní zařízení)",
+ "minimal_scopes_mode": "Minimalizovat možnosti rozsahu příspěvků",
+ "conversation_display": "Styl zobrazení konverzací",
+ "conversation_display_tree": "Stromový styl",
+ "conversation_display_tree_quick": "Stromový styl",
+ "show_scrollbars": "Zobrazit posuvníky bočních sloupců",
+ "third_column_mode": "Pokud je volné místo, zobrazit třetí sloupec obsahující",
+ "third_column_mode_none": "Nikdy nezobrazovat třetí sloupec",
+ "column_sizes": "Velikost sloupců",
+ "column_sizes_sidebar": "Postranní panel",
+ "column_sizes_content": "Obsah",
+ "column_sizes_notifs": "Oznámení",
+ "conversation_display_linear": "Lineární styl",
+ "conversation_other_replies_button": "Zobrazit tlačítko ostatních odpovědí",
+ "conversation_other_replies_button_below": "Pod příspěvky",
+ "conversation_other_replies_button_inside": "Uvnitř příspěvků",
+ "mention_link_display_short": "vždy jako zkrácená jména (např. {'@'}foo)",
+ "mention_link_display_full": "vždy jako celá jména (např. {'@'}foo{'@'}example.org)",
+ "enable_web_push_always_show_tip": "Některé prohlížeče (Chromium, Chrome) vyžadují aby push zprávy vždy vytvořili oznámení, jinak obecné oznámení 'Tento web byl aktualizován na pozadí' je zobrazeno, povolte abyste zabránili tomuto oznámení, protože Chrome nejspíš skrývá push oznámení, pokud je panel zobrazen. Může mít za následek duplicitní oznámení v ostatních prohlížečích.",
+ "actor_type": "Tento účet je:",
+ "actor_type_description": "Když svůj účet označíte jako skupinu, bude automaticky opakovat všechny příspěvky, které ho zmíní.",
+ "actor_type_Person": "normální uživatel",
+ "actor_type_Service": "bot",
+ "actor_type_Group": "skupina",
+ "hide_actor_type_indication": "Skrýt označení typu účtu (bot, skupina atd.) v příspěvcích",
+ "mention_link_show_avatar": "Zobrazit avatar uživatele vedle odkazu",
+ "mention_link_show_avatar_quick": "Zobrazit avatar uživatele vedle zmínky",
+ "mention_link_fade_domain": "Zeslabit doménu (např {'@'}example.org v {'@'}foo{'@'}example.org)",
+ "fun": "Zábava",
+ "notification_mutes": "Pokud nechcete dostávat oznámení od specifický uživatelů, použijte funkci ztlumení.",
+ "more_settings": "Víc nastavení",
+ "user_popover_avatar_action_zoom": "Zvětšit avatar",
+ "user_popover_avatar_action_close": "Zavřít popover",
+ "notification_setting_annoyance": "Nepříjemnost",
+ "user_popover_avatar_action_open": "Otevřit profil",
+ "notification_setting_drawer_marks_as_seen": "Zavření zásuvky (na mobilu) označí všechny oznámení jako přečtené",
+ "notification_setting_ignore_inactionable_seen": "Ignorovat stav přečtení pro oznámení bez akce (oblíbené, opakování atd.)",
+ "notification_setting_unseen_at_top": "Zobrazovat nepřečtené oznámení nad ostatními",
+ "enable_web_push_always_show": "Vždy zobrazovat web push oznámení",
+ "notification_setting_privacy": "Soukromí",
+ "notification_setting_block_from_strangers": "Blokovat oznámení od uživatelů které nesledujete",
+ "notification_setting_hide_notification_contents": "Schovávat odesílatele a obsah push oznámení",
+ "notification_blocks": "Blokování uživatele zastaví všechny notifikace a také je odhlásí.",
+ "mention_link_use_tooltip": "Zobrazit kartu uživatele při kliknutí na zmínku",
+ "user_popover_avatar_overlay": "Zobrazit popover uživatele přes jeho avatar",
+ "greentext": "Vtipné šipky",
+ "mention_link_bolden_you": "Zvýraznit vaši zmínku",
+ "user_popover_avatar_action": "Popover akce při kliknutí na avatar",
+ "show_yous": "Zobrazit (Vy)",
+ "notification_setting_filters": "Filtry",
+ "notification_setting_ignore_inactionable_seen_tip": "Toto ve skutečnosti neoznačí tyto oznámení jako přečtené a stále o nich budete dostávat oznámení na počítači, pokud si tak vyberete",
+ "notification_setting_filters_chrome_push": "V některých prohlížečích (Chrome) nemusí být možné kompletně vyfiltrovat oznámení, pokud přijdou jako push oznámení",
+ "commit_value": "Uložit",
+ "reset_value": "Resetovat",
+ "reset_value_tooltip": "Resetovat koncept",
+ "hard_reset_value": "Tvrdý reset",
+ "version": {
+ "title": "Verze",
+ "backend_version": "Backend verze",
+ "frontend_version": "Frontend verze"
+ },
+ "commit_value_tooltip": "Hodnota není uložena, stiskněte toto tlačítko pro potvrzení změn",
+ "hard_reset_value_tooltip": "Odstranit nastavení z úložiště a vynutit výchozí hodnotu"
},
"time": {
"day": "{0} day",
@@ -358,7 +749,7 @@
"hour_short": "{0}h",
"hours_short": "{0}h",
"in_future": "in {0}",
- "in_past": "{0} ago",
+ "in_past": "před {0}",
"minute": "{0} minute",
"minutes": "{0} minutes",
"minute_short": "{0}min",
@@ -455,5 +846,162 @@
"GiB": "GiB",
"TiB": "TiB"
}
+ },
+ "about": {
+ "mrf": {
+ "federation": "Federace",
+ "keyword": {
+ "ftl_removal": "Odstranění z časové osy \"Celá známá síť\"",
+ "reject": "Odmítnout",
+ "replace": "Nahradit",
+ "is_replaced_by": "→",
+ "keyword_policies": "Zásady klíčových slov"
+ },
+ "mrf_policies": "Povolené MRF zásady",
+ "simple": {
+ "instance": "Instance",
+ "reason": "Důvod",
+ "not_applicable": "N/A",
+ "accept": "Přijmout",
+ "accept_desc": "Tato instance přijímá zprávy pouze z následujících instancí:",
+ "reject": "Odmítnout",
+ "quarantine": "Karanténa",
+ "quarantine_desc": "Tato instance bude posílat pouze veřejné zprávy na tyto instance:",
+ "media_removal": "Odstranění médií",
+ "media_nsfw_desc": "Tato instance vynucuje média nastavené jako citlivé v příspěvcích z následujících instancí:",
+ "simple_policies": "Zásady specifické pro danou instanci",
+ "ftl_removal": "Odstranění z časové osy \"Celá známá síť\"",
+ "media_nsfw": "Vynutit média jako citlivé",
+ "reject_desc": "Tato instance nebude přijímat zprávy z následujících instancí:",
+ "media_removal_desc": "Tato instance odstraňuje média v příspěvcích z následujících instancí:",
+ "ftl_removal_desc": "Tato instance odstraňuje tyto instance z časové osy \"Celá známá síť\":"
+ },
+ "mrf_policies_desc": "Zásady MRF mění chování federace této instance. Následující MRF zásady jsou povoleny:"
+ },
+ "staff": "Personál"
+ },
+ "exporter": {
+ "processing": "Zpracovávám, zanedlouho budete vyzváni ke stažení vašeho souboru",
+ "export": "Exportovat"
+ },
+ "remote_user_resolver": {
+ "searching_for": "Hledám",
+ "error": "Nenalezeno."
+ },
+ "polls": {
+ "multiple_choices": "Výběr více možností",
+ "expiry": "Doba ankety",
+ "add_poll": "Přidat anketu",
+ "add_option": "Přidat možnost",
+ "single_choice": "Výběr jediné možnosti",
+ "option": "Možnost",
+ "votes": "hlasy",
+ "people_voted_count": "{count} hlasoval/a | {count} voličů",
+ "votes_count": "{count} hlasovat | {count} hlasů",
+ "vote": "Hlasovat",
+ "type": "Typ ankety",
+ "expires_in": "Anketa končí za {0}",
+ "expired": "Anketa skončila před {0}",
+ "not_enough_options": "Příliš málo jedinečných možností v anketě"
+ },
+ "interactions": {
+ "follows": "Nových sledujících",
+ "moves": "Uživatel migroval",
+ "load_older": "Načíst starší interakce",
+ "emoji_reactions": "Emoji reakce",
+ "reports": "Stížnosti",
+ "favs_repeats": "Opakované a oblíbené"
+ },
+ "emoji": {
+ "unicode_groups": {
+ "animals-and-nature": "Zvířata a příroda",
+ "flags": "Vlajky",
+ "activities": "Aktivity",
+ "people-and-body": "Lidé a těla",
+ "food-and-drink": "Jídlo a pití",
+ "objects": "Objekty",
+ "smileys-and-emotion": "Smajlíky a emoce",
+ "symbols": "Symboly",
+ "travel-and-places": "Cestování a místa"
+ },
+ "unicode": "Unicode emoji",
+ "load_all": "Načítání všech {emojiAmount} emoji",
+ "stickers": "Nálepky",
+ "emoji": "Emoji",
+ "keep_open": "Ponechat okno výběru otevřené",
+ "search_emoji": "Hledat emoji",
+ "add_emoji": "Vložit emoji",
+ "custom": "Vlastní emoji",
+ "load_all_hint": "Načteno prvních {saneAmount} emoji, načítání všech emoji může způsobit problémy s výkonem.",
+ "unpacked": "Rozbalené emoji",
+ "regional_indicator": "Regionální indikátor {letter}",
+ "hide_custom_emoji": "Skrýt vlastní emoji"
+ },
+ "importer": {
+ "submit": "Odeslat",
+ "success": "Úspěšně importováno.",
+ "error": "Nastala chyba při importování ze souboru."
+ },
+ "report": {
+ "reporter": "Nahlašující:",
+ "reported_user": "Nahlášený uživatel:",
+ "reported_statuses": "Nahlášené příspěvky:",
+ "notes": "Poznámky:",
+ "state": "Stav:",
+ "state_open": "Otevřeno",
+ "state_closed": "Uzavřeno",
+ "state_resolved": "Vyřešeno"
+ },
+ "announcements": {
+ "mark_as_read_action": "Označit jako přečtené",
+ "page_header": "Oznámení",
+ "title": "Oznámení",
+ "post_form_header": "Vyvěsit oznámení",
+ "post_placeholder": "Zde napište obsah vašeho oznámení…",
+ "post_action": "Odeslat",
+ "post_error": "Chyba: {error}",
+ "close_error": "Zavřít",
+ "delete_action": "Smazat",
+ "start_time_prompt": "Čas začátku: ",
+ "end_time_prompt": "Čas ukončení: ",
+ "all_day_prompt": "Toto je celodenní akce",
+ "published_time_display": "Zveřejněno v {time}",
+ "start_time_display": "Začíná v {time}",
+ "end_time_display": "Končí v {time}",
+ "edit_action": "Upravit",
+ "submit_edit_action": "Odeslat",
+ "cancel_edit_action": "Zrušit",
+ "inactive_message": "Toto oznámení není aktivní"
+ },
+ "shoutbox": {
+ "title": "Shoutbox"
+ },
+ "domain_mute_card": {
+ "mute": "Ztlumit",
+ "mute_progress": "Ztlumuji…",
+ "unmute": "Zrušit ztlumení",
+ "unmute_progress": "Ruším ztlumení…"
+ },
+ "errors": {
+ "storage_unavailable": "Pleroma nemohla získat přístup k úložišti prohlížeče. Vaše přihlášení nebo lokální nastavení se neuloží a můžete narazit na neočekávané problémy. Zkuste povolit soubory cookies."
+ },
+ "selectable_list": {
+ "select_all": "Vybrat vše"
+ },
+ "admin_dash": {
+ "window_title": "Administrace",
+ "commit_all": "Uložit vše",
+ "tabs": {
+ "nodb": "Žádné nastavení v databázi",
+ "frontends": "Frontendy",
+ "instance": "Instance",
+ "limits": "Limity"
+ },
+ "nodb": {
+ "heading": "Nastavení v databázi je vypnuto"
+ },
+ "wip_notice": "Tento administrační panel je experimentální a v aktivní vývoji, {adminFeLink}.",
+ "old_ui_link": "staré administrační rozhraní je dostupné zde",
+ "reset_all": "Resetovat vše"
}
}
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 6bb9e4bb..affe4335 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -189,6 +189,7 @@
"mobile_notifications": "Open notifications",
"mobile_notifications": "Open notifications (there are unread ones)",
"mobile_notifications_close": "Close notifications",
+ "mobile_notifications_mark_as_seen": "Mark all as seen",
"announcements": "Announcements"
},
"notifications": {
@@ -236,6 +237,7 @@
"search_emoji": "Search for an emoji",
"add_emoji": "Insert emoji",
"custom": "Custom emoji",
+ "hide_custom_emoji": "Hide custom emojis",
"unpacked": "Unpacked emoji",
"unicode": "Unicode emoji",
"unicode_groups": {
@@ -358,6 +360,11 @@
"remove_language": "Remove",
"primary_language": "Primary language:",
"fallback_language": "Fallback language {index}:",
+ "actor_type": "This account is:",
+ "actor_type_description": "Marking your account as a group will make it automatically repeat statuses that mention it.",
+ "actor_type_Person": "a normal user",
+ "actor_type_Service": "a bot",
+ "actor_type_Group": "a group",
"app_name": "App name",
"expert_mode": "Show advanced",
"save": "Save changes",
@@ -422,7 +429,6 @@
"added_backup": "Added a new backup.",
"add_backup_error": "Error adding a new backup: {error}",
"blocks_tab": "Blocks",
- "bot": "This is a bot account",
"btnRadius": "Buttons",
"cBlue": "Blue (Reply, follow)",
"cGreen": "Green (Retweet)",
@@ -494,7 +500,7 @@
"hide_media_previews": "Hide media previews",
"hide_muted_posts": "Hide posts of muted users",
"mute_bot_posts": "Mute bot posts",
- "hide_bot_indication": "Hide bot indication in posts",
+ "hide_actor_type_indication": "Hide actor type (bots, groups, etc.) indication in posts",
"hide_scrobbles": "Hide scrobbles",
"hide_all_muted_posts": "Hide muted posts",
"max_thumbnails": "Maximum amount of thumbnails per post (empty = no limit)",
@@ -561,10 +567,14 @@
"posts": "Posts",
"user_profiles": "User Profiles",
"notification_visibility": "Types of notifications to show",
+ "notification_visibility_in_column": "Show in notifications column/drawer",
+ "notification_visibility_native_notifications": "Show a native notification",
"notification_visibility_follows": "Follows",
+ "notification_visibility_follow_requests": "Follow requests",
"notification_visibility_likes": "Favorites",
"notification_visibility_mentions": "Mentions",
"notification_visibility_repeats": "Repeats",
+ "notification_visibility_reports": "Reports",
"notification_visibility_moves": "User Migrates",
"notification_visibility_emoji_reactions": "Reactions",
"notification_visibility_polls": "Ends of polls you voted in",
@@ -688,13 +698,21 @@
"greentext": "Meme arrows",
"show_yous": "Show (You)s",
"notifications": "Notifications",
+ "notification_setting_annoyance": "Annoyance",
+ "notification_setting_drawer_marks_as_seen": "Closing drawer (mobile) marks all notifications as read",
+ "notification_setting_ignore_inactionable_seen": "Ignore read state of inactionable notifications (likes, repeats etc)",
+ "notification_setting_ignore_inactionable_seen_tip": "This will not actually mark those notifications as read, and you'll still get desktop notifications about them if you chose so",
+ "notification_setting_unseen_at_top": "Show unread notifications above others",
"notification_setting_filters": "Filters",
+ "notification_setting_filters_chrome_push": "On some browsers (chrome) it might be impossible to completely filter out notifications by type when they arrive by Push",
"notification_setting_block_from_strangers": "Block notifications from users who you do not follow",
"notification_setting_privacy": "Privacy",
"notification_setting_hide_notification_contents": "Hide the sender and contents of push notifications",
"notification_mutes": "To stop receiving notifications from a specific user, use a mute.",
"notification_blocks": "Blocking a user stops all notifications as well as unsubscribes them.",
"enable_web_push_notifications": "Enable web push notifications",
+ "enable_web_push_always_show": "Always show web push notifications",
+ "enable_web_push_always_show_tip": "Some browsers (Chromium, Chrome) require that push messages always result in a notification, otherwise generic 'Website was updated in background' is shown, enable this to prevent this notification from showing, as Chrome seem to hide push notifications if tab is in focus. Can result in showing duplicate notifications on other browsers.",
"more_settings": "More settings",
"style": {
"switcher": {
@@ -864,7 +882,8 @@
"nodb": "No DB Config",
"instance": "Instance",
"limits": "Limits",
- "frontends": "Front-ends"
+ "frontends": "Front-ends",
+ "emoji": "Emoji"
},
"nodb": {
"heading": "Database config is disabled",
@@ -914,10 +933,55 @@
"wip_notice": "Please note that this section is a WIP and lacks certain features as backend implementation of front-end management is incomplete.",
"default_frontend": "Default front-end",
"default_frontend_tip": "Default front-end will be shown to all users. Currently there's no way to for a user to select personal front-end. If you switch away from PleromaFE you'll most likely have to use old and buggy AdminFE to do instance configuration until we replace it.",
+ "default_frontend_unavail": "Default frontend settings are not available, as this requires configuration in the database",
"available_frontends": "Available for install",
"failure_installing_frontend": "Failed to install frontend {version}: {reason}",
"success_installing_frontend": "Frontend {version} successfully installed"
},
+ "emoji": {
+ "global_actions": "Global actions",
+ "reload": "Reload emoji",
+ "importFS": "Import emoji from filesystem",
+ "error": "Error: {0}",
+ "create_pack": "Create pack",
+ "delete_pack": "Delete pack",
+ "new_pack_name": "New pack name",
+ "create": "Create",
+ "emoji_packs": "Emoji packs",
+ "remote_packs": "Remote packs",
+ "do_list": "List",
+ "remote_pack_instance": "Remote pack instance",
+ "emoji_pack": "Emoji pack",
+ "edit_pack": "Edit pack",
+ "description": "Description",
+ "homepage": "Homepage",
+ "fallback_src": "Fallback source",
+ "fallback_sha256": "Fallback SHA256",
+ "share": "Share",
+ "save": "Save",
+ "save_meta": "Save metadata",
+ "revert_meta": "Revert metadata",
+ "delete": "Delete",
+ "revert": "Revert",
+ "add_file": "Add file",
+ "adding_new": "Adding new emoji",
+ "shortcode": "Shortcode",
+ "filename": "Filename",
+ "new_shortcode": "Shortcode, leave blank to infer",
+ "new_filename": "Filename, leave blank to infer",
+ "delete_confirm": "Are you sure you want to delete {0}?",
+ "download_pack": "Download pack",
+ "downloading_pack": "Downloading {0}",
+ "download": "Download",
+ "download_as_name": "New name",
+ "download_as_name_full": "New name, leave blank to reuse",
+ "files": "Files",
+ "editing": "Editing {0}",
+ "delete_title": "Delete?",
+ "metadata_changed": "Metadata different from saved",
+ "emoji_changed": "Unsaved emoji file changes, check highlighted emoji",
+ "replace_warning": "This will REPLACE the local pack of the same name"
+ },
"temp_overrides": {
":pleroma": {
":instance": {
@@ -1114,6 +1178,7 @@
"hide_repeats": "Hide repeats",
"show_repeats": "Show repeats",
"bot": "Bot",
+ "group": "Group",
"birthday": "Born {birthday}",
"admin_menu": {
"moderation": "Moderation",
diff --git a/src/i18n/ja_easy.json b/src/i18n/ja_easy.json
index 21b27d12..c5ed3b6c 100644
--- a/src/i18n/ja_easy.json
+++ b/src/i18n/ja_easy.json
@@ -163,7 +163,8 @@
"search_close": "けんさくバーをとじる",
"edit_nav_mobile": "ナビゲーションバーのせっていをかえる",
"mobile_sidebar": "モバイルのサイドバーをきりかえる",
- "edit_finish": "へんしゅうをおわりにする"
+ "edit_finish": "へんしゅうをおわりにする",
+ "mobile_notifications_mark_as_seen": "ぜんぶ みたことにする"
},
"notifications": {
"broken_favorite": "ステータスがみつかりません。さがしています…",
@@ -179,7 +180,13 @@
"migrated_to": "インスタンスを、ひっこしました",
"reacted_with": "{0} でリアクションしました",
"poll_ended": "とうひょうが、おわりました",
- "submitted_report": "つうほうしました"
+ "submitted_report": "つうほうしました",
+ "unread_announcements": "まだ よんでいない おしらせが {num}こ あります",
+ "configuration_tip_settings": "せってい",
+ "configuration_tip_dismiss": "つぎは ひょうじしない",
+ "unread_chats": "よんでいない チャットが {num}こ あります",
+ "unread_follow_requests": "フォローリクエストが {num}こ あります",
+ "configuration_tip": "ここに ひょうじする ものを {theSettings}で へんこうできます。 {dismiss}"
},
"polls": {
"add_poll": "とうひょうをはじめる",
@@ -218,7 +225,8 @@
"symbols": "きごう",
"travel-and-places": "りょこう・ばしょ"
},
- "regional_indicator": "ばしょをしめすきごう {letter}"
+ "regional_indicator": "ばしょをしめすきごう {letter}",
+ "unpacked": "アンパックされた えもじ"
},
"stickers": {
"add_sticker": "ステッカーをふやす"
@@ -269,7 +277,9 @@
"preview": "プレビュー",
"preview_empty": "なにもありません",
"empty_status_error": "とうこうないようを、にゅうりょくしてください",
- "scope_notice_dismiss": "このつうちをとじる"
+ "scope_notice_dismiss": "このつうちをとじる",
+ "reply_option": "この ステータスに へんしんする",
+ "quote_option": "この ステータスを いんようする"
},
"registration": {
"bio": "プロフィール",
@@ -324,7 +334,7 @@
"warning_of_generate_new_codes": "あたらしいリカバリーコードをつくったら、ふるいコードはつかえなくなります。",
"recovery_codes": "リカバリーコード。",
"waiting_a_recovery_codes": "バックアップコードをうけとっています…",
- "recovery_codes_warning": "コードをかきうつすか、ひとにみられないところにセーブしてください。そうでなければ、あなたはこのコードをふたたびみることはできません。もしあなたが、2FAアプリのアクセスをうしなって、なおかつ、リカバリーコードもおもいだせないならば、あなたはあなたのアカウントから、しめだされます。",
+ "recovery_codes_warning": "コードを かきうつすか、 ほかのひとが みれないところに ほぞんしてください。 そうしないと、 あなたは このコードを にどと みることができません。 もし あなたが 2FAアプリに アクセスできなくなり、 リカバリーコードも おもいだせないなら、 あなたは あなたの アカウントに はいれなくなります。",
"authentication_methods": "にんしょうメソッド",
"scan": {
"title": "スキャン",
@@ -697,9 +707,9 @@
"import_mutes_from_a_csv_file": "CSVファイルからミュートをインポートする",
"reset_avatar": "アバターをリセットする",
"remove_language": "とりのぞく",
- "primary_language": "いちばんわかることば:",
+ "primary_language": "さいしょに つかう ことば:",
"add_language": "よびとしてつかうことばを、ついかする",
- "fallback_language": "よびとしてつかうことば {index}:",
+ "fallback_language": "よびとして つかう ことば {index}:",
"lists_navigation": "ナビゲーションにリストをひょうじする",
"account_alias": "アカウントのエイリアス",
"mention_link_display_full": "いつも、ながいなまえをひょうじする (れい: {'@'}hoge{'@'}example.org)",
@@ -797,7 +807,11 @@
"virtual_scrolling": "タイムラインのレンダリングをよくする",
"use_at_icon": "{'@'} きごうを、もじのかわりに、アイコンでひょうじする",
"mention_link_display_short": "いつも、みじかいなまえにする (れい: {'@'}hoge)",
- "mention_link_display": "メンションのリンクをひょうじするけいしき"
+ "mention_link_display": "メンションのリンクをひょうじするけいしき",
+ "url": "URL",
+ "preview": "プレビュー",
+ "emoji_reactions_scale": "リアクションを なんばいの おおきさで ひょうじするか",
+ "autocomplete_select_first": "じどうほかんが あれば、 さいしょの ものを じどうで えらぶ"
},
"time": {
"day": "{0}日",
diff --git a/src/i18n/ja_pedantic.json b/src/i18n/ja_pedantic.json
index fddf24db..1e9510bd 100644
--- a/src/i18n/ja_pedantic.json
+++ b/src/i18n/ja_pedantic.json
@@ -4,36 +4,37 @@
},
"exporter": {
"export": "エクスポート",
- "processing": "処理中です。処理が完了すると、ファイルをダウンロードするよう指示があります"
+ "processing": "処理中です。処理が完了すると、ファイルのダウンロードが開始します"
},
"features_panel": {
"chat": "チャット",
"gopher": "Gopher",
- "media_proxy": "メディアプロクシ",
+ "media_proxy": "メディアプロキシ",
"scope_options": "公開範囲選択",
- "text_limit": "文字の数",
+ "text_limit": "文字数制限",
"title": "有効な機能",
"who_to_follow": "おすすめユーザー",
"upload_limit": "ファイルサイズの上限",
- "pleroma_chat_messages": "Pleroma チャット"
+ "pleroma_chat_messages": "Pleroma チャット",
+ "shout": "Shoutbox"
},
"finder": {
- "error_fetching_user": "ユーザー検索がエラーになりました",
+ "error_fetching_user": "ユーザーの取得に失敗しました",
"find_user": "ユーザーを探す"
},
"general": {
"apply": "適用",
"submit": "送信",
- "more": "続き",
- "generic_error": "エラーになりました",
- "optional": "省略可",
+ "more": "もっと",
+ "generic_error": "エラーが発生しました",
+ "optional": "任意",
"show_more": "もっと見る",
"show_less": "たたむ",
"cancel": "キャンセル",
"disable": "無効",
"enable": "有効",
"confirm": "確認",
- "verify": "検査",
+ "verify": "検証",
"peek": "隠す",
"close": "閉じる",
"dismiss": "無視",
@@ -46,7 +47,21 @@
},
"flash_security": "Flashコンテンツが任意の命令を実行させることにより、コンピューターが危険にさらされることがあります。",
"flash_fail": "Flashコンテンツの読み込みに失敗しました。コンソールで詳細を確認できます。",
- "flash_content": "(試験的機能)クリックしてFlashコンテンツを再生します。"
+ "flash_content": "(試験的機能) クリックしてFlashコンテンツを再生します。",
+ "yes": "はい",
+ "no": "いいえ",
+ "scroll_to_top": "最上部へスクロール",
+ "unpin": "ピン留めを外す",
+ "pin": "ピン留めする",
+ "scope_in_timeline": {
+ "direct": "ダイレクト",
+ "private": "フォロワー限定",
+ "public": "パブリック",
+ "unlisted": "アンリステッド"
+ },
+ "generic_error_message": "エラーが発生しました: {0}",
+ "never_show_again": "二度と表示しない",
+ "undo": "取り消す"
},
"image_cropper": {
"crop_picture": "画像を切り抜く",
@@ -57,7 +72,7 @@
"importer": {
"submit": "送信",
"success": "正常にインポートされました。",
- "error": "このファイルをインポートするとき、エラーが発生しました。"
+ "error": "ファイルのインポート中にエラーが発生しました。"
},
"login": {
"login": "ログイン",
@@ -69,30 +84,36 @@
"username": "ユーザー名",
"hint": "会話に加わるには、ログインしてください",
"authentication_code": "認証コード",
- "enter_recovery_code": "リカバリーコードを入力してください",
- "enter_two_factor_code": "2段階認証コードを入力してください",
+ "enter_recovery_code": "リカバリーコードを入力",
+ "enter_two_factor_code": "二段階認証コードを入力",
"recovery_code": "リカバリーコード",
"heading": {
- "totp": "2段階認証",
- "recovery": "2段階リカバリー"
- }
+ "totp": "二段階認証",
+ "recovery": "二段階認証リカバリー"
+ },
+ "logout_confirm": "本当にログアウトしますか?",
+ "logout_confirm_accept_button": "ログアウト",
+ "logout_confirm_cancel_button": "ログアウトしない",
+ "logout_confirm_title": "ログアウトの確認"
},
"media_modal": {
- "previous": "前",
- "next": "次"
+ "previous": "前へ",
+ "next": "次へ",
+ "hide": "メディアビューアを閉じる",
+ "counter": "{current} / {total}"
},
"nav": {
"about": "このインスタンスについて",
"back": "戻る",
"chat": "ローカルチャット",
"friend_requests": "フォローリクエスト",
- "mentions": "通知",
- "interactions": "インタラクション",
+ "mentions": "メンション",
+ "interactions": "通知",
"dms": "ダイレクトメッセージ",
"public_tl": "公開タイムライン",
"timeline": "タイムライン",
"twkn": "すべてのネットワーク",
- "user_search": "ユーザーを探す",
+ "user_search": "ユーザー検索",
"search": "検索",
"who_to_follow": "おすすめユーザー",
"preferences": "設定",
@@ -100,21 +121,38 @@
"bookmarks": "ブックマーク",
"timelines": "タイムライン",
"chats": "チャット",
- "home_timeline": "ホームタイムライン"
+ "home_timeline": "ホームタイムライン",
+ "mobile_notifications_mark_as_seen": "すべて既読にする",
+ "search_close": "検索バーを閉じる",
+ "lists": "リスト",
+ "edit_nav_mobile": "ナビゲーションバーを編集",
+ "edit_pinned": "ピン留めを編集",
+ "edit_finish": "完了",
+ "mobile_notifications": "通知を開く (未読あり)",
+ "mobile_notifications_close": "通知を閉じる",
+ "announcements": "お知らせ"
},
"notifications": {
"broken_favorite": "ステータスが見つかりません。探しています…",
- "favorited_you": "あなたのステータスがお気に入りされました",
+ "favorited_you": "ステータスがお気に入りされました",
"followed_you": "フォローされました",
- "load_older": "古い通知をみる",
+ "load_older": "古い通知を読み込む",
"notifications": "通知",
"read": "読んだ!",
- "repeated_you": "あなたのステータスがリピートされました",
+ "repeated_you": "ステータスがリピートされました",
"no_more_notifications": "通知はありません",
"reacted_with": "{0} でリアクションしました",
"migrated_to": "インスタンスを引っ越しました",
- "follow_request": "あなたをフォローしたいです",
- "error": "通知の取得に失敗しました: {0}"
+ "follow_request": "あなたをフォローしたがっています",
+ "error": "通知の取得に失敗しました: {0}",
+ "poll_ended": "投票結果が確定しました",
+ "configuration_tip_dismiss": "二度と表示しない",
+ "unread_announcements": "未読のお知らせが{num}件あります | 未読のお知らせが{num}件あります",
+ "unread_chats": "未読のチャットが{num}件あります | 未読のチャットが{num}件あります",
+ "unread_follow_requests": "フォローリクエストが{num}件来ています | フォローリクエストが{num}件来ています",
+ "configuration_tip": "ここに表示する通知の種類は{theSettings}にて変更することができます。 {dismiss}",
+ "submitted_report": "通報が送信されました",
+ "configuration_tip_settings": "設定"
},
"polls": {
"add_poll": "投票を追加",
@@ -128,20 +166,22 @@
"expiry": "投票期間",
"expires_in": "投票は {0} で終了します",
"expired": "投票は {0} 前に終了しました",
- "not_enough_options": "相異なる選択肢が不足しています",
+ "not_enough_options": "選択肢が少なすぎます",
"votes_count": "{count} 票 | {count} 票",
- "people_voted_count": "{count} 人投票 | {count} 人投票"
+ "people_voted_count": "{count}人が投票しました | {count}人が投票しました"
},
"emoji": {
"stickers": "ステッカー",
"emoji": "絵文字",
- "keep_open": "ピッカーを開いたままにする",
+ "keep_open": "絵文字ピッカーを開いたままにする",
"search_emoji": "絵文字を検索",
"add_emoji": "絵文字を挿入",
"custom": "カスタム絵文字",
"unicode": "Unicode絵文字",
"load_all": "全 {emojiAmount} 絵文字を読み込む",
- "load_all_hint": "最初の {saneAmount} 絵文字を読み込みました、全て読み込むと重くなる可能性があります。"
+ "load_all_hint": "最初の {saneAmount} 件の絵文字を読み込みました。すべて読み込むとパフォーマンスに影響を与える可能性があります。",
+ "unpacked": "パック外の絵文字",
+ "hide_custom_emoji": "カスタム絵文字を表示しない"
},
"stickers": {
"add_sticker": "ステッカーを追加"
@@ -149,30 +189,32 @@
"interactions": {
"favs_repeats": "リピートとお気に入り",
"follows": "新しいフォロワー",
- "load_older": "古いインタラクションを見る",
- "moves": "ユーザーの引っ越し"
+ "load_older": "古い通知を読み込む",
+ "moves": "ユーザーの引っ越し",
+ "emoji_reactions": "絵文字リアクション",
+ "reports": "通報"
},
"post_status": {
"new_status": "投稿する",
- "account_not_locked_warning": "あなたのアカウントは {0} ではありません。あなたをフォローすれば、誰でも、フォロワー限定のステータスを読むことができます。",
- "account_not_locked_warning_link": "ロックされたアカウント",
- "attachments_sensitive": "ファイルをNSFWにする",
+ "account_not_locked_warning": "あなたのアカウントは {0} ではありません。あなたをフォローすれば、誰でもフォロワー限定のステータスを読むことができます。",
+ "account_not_locked_warning_link": "鍵アカウント",
+ "attachments_sensitive": "ファイルを閲覧注意に設定する",
"content_type": {
"text/plain": "プレーンテキスト",
"text/html": "HTML",
"text/markdown": "Markdown",
"text/bbcode": "BBCode"
},
- "content_warning": "説明 (省略可)",
+ "content_warning": "注釈 (任意)",
"default": "羽田空港に着きました。",
- "direct_warning_to_all": "この投稿は、メンションされたすべてのユーザーが、見ることができます。",
- "direct_warning_to_first_only": "この投稿は、メッセージの冒頭でメンションされたユーザーだけが、見ることができます。",
+ "direct_warning_to_all": "この投稿は、メンションされたすべてのユーザーが閲覧できます。",
+ "direct_warning_to_first_only": "この投稿は、メッセージの冒頭でメンションされたユーザーだけが閲覧できます。",
"direct_warning": "このステータスは、メンションされたユーザーだけが、読むことができます。",
"posting": "投稿",
"scope_notice": {
- "public": "この投稿は、誰でも見ることができます",
- "private": "この投稿は、あなたのフォロワーだけが、見ることができます",
- "unlisted": "この投稿は、パブリックタイムラインと、接続しているすべてのネットワークには、表示されません"
+ "public": "この投稿は誰でも閲覧できます",
+ "private": "この投稿はフォロワーのみ閲覧できます",
+ "unlisted": "この投稿は、公開タイムラインとすべてのネットワークには表示されません"
},
"scope": {
"direct": "ダイレクト: メンションされたユーザーのみに届きます",
@@ -180,22 +222,29 @@
"public": "パブリック: 公開タイムラインに届きます",
"unlisted": "アンリステッド: 公開タイムラインに届きません"
},
- "media_description_error": "メディアのアップロードに失敗しました。もう一度お試しください",
+ "media_description_error": "メディアのアップデートに失敗しました。もう一度お試しください",
"empty_status_error": "投稿内容を入力してください",
"preview_empty": "何もありません",
"preview": "プレビュー",
"media_description": "メディアの説明",
- "post": "投稿"
+ "post": "投稿",
+ "edit_status": "ステータスを編集",
+ "reply_option": "このステータスに返信する",
+ "quote_option": "このステータスを引用する",
+ "edit_remote_warning": "他のインスタンスは投稿の編集に対応していないかもしれません。その場合、編集した内容は伝わりません。",
+ "edit_unsupported_warning": "Pleromaは、メンションと投票の編集に対応していません。",
+ "scope_notice_dismiss": "このメッセージを閉じる",
+ "content_type_selection": "投稿形式"
},
"registration": {
"bio": "プロフィール",
- "email": "Eメール",
- "fullname": "スクリーンネーム",
+ "email": "メールアドレス",
+ "fullname": "表示名",
"password_confirm": "パスワードの確認",
"registration": "登録",
- "token": "招待トークン",
+ "token": "招待コード",
"captcha": "CAPTCHA",
- "new_captcha": "文字が読めないときは、画像をクリックすると、新しい画像になります",
+ "new_captcha": "文字が読めない場合、画像をクリックすると新しい画像が表示されます",
"username_placeholder": "例: lain",
"fullname_placeholder": "例: 岩倉玲音",
"bio_placeholder": "例:\nこんにちは。私は玲音。\n私はアニメのキャラクターで、日本の郊外に住んでいます。私をWiredで見たことがあるかもしれません。",
@@ -205,11 +254,18 @@
"email_required": "必須",
"password_required": "必須",
"password_confirmation_required": "必須",
- "password_confirmation_match": "パスワードが違います"
+ "password_confirmation_match": "パスワードが一致しません",
+ "birthday_required": "必須",
+ "birthday_min_age": "{date} 以降のユーザーは登録できません"
},
- "reason_placeholder": "このインスタンスは、新規登録を手動で受け付けています。\n登録したい理由を、インスタンスの管理者に教えてください。",
- "reason": "登録するための目的",
- "register": "登録"
+ "reason_placeholder": "このインスタンスは、新規登録を手動で承認しています。\n登録したい理由をインスタンスの管理者に教えてください。",
+ "reason": "登録を希望する理由",
+ "register": "登録",
+ "email_language": "このサーバーからのメールをどの言語で受け取りますか?",
+ "bio_optional": "プロフィール (任意)",
+ "email_optional": "メールアドレス (任意)",
+ "birthday": "誕生日:",
+ "birthday_optional": "誕生日 (任意):"
},
"selectable_list": {
"select_all": "すべて選択"
@@ -223,44 +279,44 @@
"setup_otp": "OTPのセットアップ",
"wait_pre_setup_otp": "OTPのプリセット",
"confirm_and_enable": "OTPの確認と有効化",
- "title": "2段階認証",
+ "title": "二段階認証",
"generate_new_recovery_codes": "新しいリカバリーコードを生成",
"warning_of_generate_new_codes": "新しいリカバリーコードを生成すると、古いコードは使用できなくなります。",
"recovery_codes": "リカバリーコード。",
"waiting_a_recovery_codes": "バックアップコードを受信しています…",
- "recovery_codes_warning": "コードを紙に書くか、安全な場所に保存してください。そうでなければ、あなたはコードを再び見ることはできません。もし2段階認証アプリのアクセスを喪失し、なおかつ、リカバリーコードもないならば、あなたは自分のアカウントから閉め出されます。",
+ "recovery_codes_warning": "リカバリーコードをどこか安全な場所に書き留めてください。このコードは二度と表示されません。二段階認証アプリへのアクセスを失い、リカバリーコードも紛失した場合、二度とアカウントにログインできなくなります。",
"authentication_methods": "認証方法",
"scan": {
"title": "スキャン",
- "desc": "あなたの2段階認証アプリを使って、このQRコードをスキャンするか、テキストキーを入力してください:",
+ "desc": "二段階認証アプリでQRコードを読み取るか、テキストキーを入力してください:",
"secret_code": "キー"
},
"verify": {
- "desc": "2段階認証を有効にするには、あなたの2段階認証アプリのコードを入力してください:"
+ "desc": "二段階認証を有効にするには、二段階認証アプリに表示されたコードを入力してください:"
}
},
"attachmentRadius": "ファイル",
"attachments": "ファイル",
- "avatar": "アバター",
- "avatarAltRadius": "通知のアバター",
- "avatarRadius": "アバター",
+ "avatar": "アイコン",
+ "avatarAltRadius": "通知内のアイコン",
+ "avatarRadius": "アイコン",
"background": "バックグラウンド",
"bio": "プロフィール",
"block_export": "ブロックのエクスポート",
"block_export_button": "ブロックをCSVファイルにエクスポートする",
"block_import": "ブロックのインポート",
"block_import_error": "ブロックのインポートに失敗しました",
- "blocks_imported": "ブロックをインポートしました! 実際に処理されるまでに、しばらく時間がかかります。",
+ "blocks_imported": "ブロックがインポートされました。処理には時間がかかる場合があります。",
"blocks_tab": "ブロック",
"btnRadius": "ボタン",
"cBlue": "返信とフォロー",
"cGreen": "リピート",
"cOrange": "お気に入り",
"cRed": "キャンセル",
- "change_password": "パスワードを変える",
- "change_password_error": "パスワードを変えることが、できなかったかもしれません。",
- "changed_password": "パスワードが、変わりました!",
- "collapse_subject": "説明のある投稿をたたむ",
+ "change_password": "パスワードを変更",
+ "change_password_error": "パスワードの変更中にエラーが発生しました。",
+ "changed_password": "パスワードが変更されました!",
+ "collapse_subject": "注釈のついた投稿をたたむ",
"composing": "投稿",
"confirm_new_password": "新しいパスワードの確認",
"current_avatar": "現在のアバター",
@@ -268,52 +324,52 @@
"current_profile_banner": "現在のプロフィールバナー",
"data_import_export_tab": "インポートとエクスポート",
"default_vis": "デフォルトの公開範囲",
- "delete_account": "アカウントを消す",
- "delete_account_description": "あなたのデータが消えて、アカウントが使えなくなります。",
- "delete_account_error": "アカウントを消すことが、できなかったかもしれません。インスタンスの管理者に、連絡してください。",
- "delete_account_instructions": "本当にアカウントを消してもいいなら、パスワードを入力してください。",
+ "delete_account": "アカウントの削除",
+ "delete_account_description": "アカウントのデータを永久的に削除し、アカウントを無効化します。",
+ "delete_account_error": "アカウントの削除中にエラーが発生しました。継続して発生する場合、管理者に問い合せてください。",
+ "delete_account_instructions": "アカウント削除の確認のため、パスワードを入力してください。",
"discoverable": "検索などのサービスでこのアカウントを見つけることを許可する",
- "avatar_size_instruction": "アバターの大きさは、150×150ピクセルか、それよりも大きくするといいです。",
- "pad_emoji": "ピッカーから絵文字を挿入するとき、絵文字の両側にスペースを入れる",
- "export_theme": "保存",
+ "avatar_size_instruction": "アイコン画像のサイズは150x150以上を推奨します。",
+ "pad_emoji": "絵文字ピッカーから絵文字を挿入するとき、絵文字の前後に空白を挿入する",
+ "export_theme": "ファイルにテーマを出力",
"filtering": "フィルタリング",
- "filtering_explanation": "これらの言葉を含むすべてのものがミュートされます。1行に1つの言葉を書いてください",
+ "filtering_explanation": "これらの単語を含むステータスはミュートされます。(1行に1単語)",
"follow_export": "フォローのエクスポート",
"follow_export_button": "エクスポート",
"follow_export_processing": "お待ちください。まもなくファイルをダウンロードできます。",
"follow_import": "フォローのインポート",
- "follow_import_error": "フォローのインポートがエラーになりました",
- "follows_imported": "フォローがインポートされました! 少し時間がかかるかもしれません。",
+ "follow_import_error": "フォローのインポートに失敗しました",
+ "follows_imported": "フォローがインポートされました。処理には時間がかかる場合があります。",
"foreground": "フォアグラウンド",
"general": "全般",
- "hide_attachments_in_convo": "スレッドのファイルを隠す",
- "hide_attachments_in_tl": "タイムラインのファイルを隠す",
- "hide_muted_posts": "ミュートしているユーザーの投稿を隠す",
- "max_thumbnails": "投稿に含まれるサムネイルの最大数",
- "hide_isp": "インスタンス固有パネルを隠す",
+ "hide_attachments_in_convo": "スレッド内のファイルを表示しない",
+ "hide_attachments_in_tl": "タイムラインのファイルを表示しない",
+ "hide_muted_posts": "ミュートしているユーザーの投稿を表示しない",
+ "max_thumbnails": "投稿に表示するサムネイルの最大数 (空にすると無制限)",
+ "hide_isp": "インスタンス固有パネルを表示しない",
"preload_images": "画像を先読みする",
- "use_one_click_nsfw": "NSFWなファイルを1クリックで開く",
- "hide_post_stats": "投稿の統計を隠す (例: お気に入りの数)",
- "hide_user_stats": "ユーザーの統計を隠す (例: フォロワーの数)",
- "hide_filtered_statuses": "フィルターされた投稿を隠す",
+ "use_one_click_nsfw": "閲覧注意なファイルを1クリックで開く",
+ "hide_post_stats": "投稿の統計を表示しない (例: お気に入りの数)",
+ "hide_user_stats": "ユーザーの統計を表示しない (例: フォロワーの数)",
+ "hide_filtered_statuses": "フィルタリングされた投稿を表示しない",
"import_blocks_from_a_csv_file": "CSVファイルからブロックをインポートする",
"import_followers_from_a_csv_file": "CSVファイルからフォローをインポートする",
- "import_theme": "ロード",
- "inputRadius": "インプットフィールド",
+ "import_theme": "ファイルからテーマを読み込む",
+ "inputRadius": "入力欄",
"checkboxRadius": "チェックボックス",
"instance_default": "(デフォルト: {value})",
"instance_default_simple": "(デフォルト)",
"interface": "インターフェース",
"interfaceLanguage": "インターフェースの言語",
- "invalid_theme_imported": "このファイルはPleromaのテーマではありません。テーマは変更されませんでした。",
- "limited_availability": "あなたのブラウザではできません",
+ "invalid_theme_imported": "非対応の形式のテーマファイルです。テーマは変更されませんでした。",
+ "limited_availability": "非対応のブラウザです",
"links": "リンク",
- "lock_account_description": "あなたが認めた人だけ、あなたのアカウントをフォローできる",
- "loop_video": "ビデオを繰り返す",
- "loop_video_silent_only": "音のないビデオだけ繰り返す",
+ "lock_account_description": "フォローを承認制にする",
+ "loop_video": "動画をループ再生する",
+ "loop_video_silent_only": "音声のない動画のみループ再生する",
"mutes_tab": "ミュート",
- "play_videos_in_modal": "ビデオをメディアビューアーで見る",
- "use_contain_fit": "画像のサムネイルを、切り抜かない",
+ "play_videos_in_modal": "動画をメディアビューアで再生する",
+ "use_contain_fit": "画像のサムネイルを切り抜かない",
"name": "名前",
"name_bio": "名前とプロフィール",
"new_password": "新しいパスワード",
@@ -322,55 +378,55 @@
"notification_visibility_likes": "お気に入り",
"notification_visibility_mentions": "メンション",
"notification_visibility_repeats": "リピート",
- "no_rich_text_description": "リッチテキストを使わない",
- "no_blocks": "ブロックはありません",
- "no_mutes": "ミュートはありません",
- "hide_follows_description": "フォローしている人を見せない",
- "hide_followers_description": "フォロワーを見せない",
- "hide_follows_count_description": "フォローしている人の数を見せない",
- "hide_followers_count_description": "フォロワーの数を見せない",
- "show_admin_badge": "\"管理者\"のバッジを見せる",
- "show_moderator_badge": "\"モデレーター\"のバッジを見せる",
- "nsfw_clickthrough": "NSFWなファイルを隠す",
+ "no_rich_text_description": "投稿のテキスト装飾を無効化する",
+ "no_blocks": "ブロックしたユーザーはいません",
+ "no_mutes": "ミュートしたユーザーはいません",
+ "hide_follows_description": "フォロー欄を非公開にする",
+ "hide_followers_description": "フォロワー欄を非公開にする",
+ "hide_follows_count_description": "フォロー数を非公開にする",
+ "hide_followers_count_description": "フォロワー数を非公開にする",
+ "show_admin_badge": "プロフィールに「管理者」バッジを表示する",
+ "show_moderator_badge": "プロフィールに「モデレーター」バッジを表示する",
+ "nsfw_clickthrough": "閲覧注意なファイルを隠す",
"oauth_tokens": "OAuthトークン",
"token": "トークン",
"refresh_token": "トークンを更新",
- "valid_until": "まで有効",
+ "valid_until": "有効期限",
"revoke_token": "取り消す",
"panelRadius": "パネル",
- "pause_on_unfocused": "タブにフォーカスがないときストリーミングを止める",
+ "pause_on_unfocused": "タブにフォーカスがないとき、タイムラインの自動更新を止める",
"presets": "プリセット",
"profile_background": "プロフィールの背景",
"profile_banner": "プロフィールのバナー",
"profile_tab": "プロフィール",
- "radii_help": "インターフェースの丸さを設定する",
- "replies_in_timeline": "タイムラインのリプライ",
- "reply_visibility_all": "すべてのリプライを見る",
- "reply_visibility_following": "私に宛てられたリプライと、フォローしている人からのリプライを見る",
- "reply_visibility_self": "私に宛てられたリプライを見る",
- "autohide_floating_post_button": "新しい投稿ボタンを自動的に隠す (モバイル)",
+ "radii_help": "インターフェースの角丸を設定する (ピクセル単位)",
+ "replies_in_timeline": "タイムライン上の返信",
+ "reply_visibility_all": "すべての返信を表示",
+ "reply_visibility_following": "自分、もしくはフォローしているユーザー宛ての返信のみを表示",
+ "reply_visibility_self": "自分に宛てられた返信のみを表示",
+ "autohide_floating_post_button": "投稿ボタンを自動的に隠す (モバイル)",
"saving_err": "設定を保存できませんでした",
"saving_ok": "設定を保存しました",
"search_user_to_block": "ブロックしたいユーザーを検索",
"search_user_to_mute": "ミュートしたいユーザーを検索",
"security_tab": "セキュリティ",
- "scope_copy": "返信するとき、公開範囲をコピーする (DMの公開範囲は、常にコピーされます)",
- "minimal_scopes_mode": "公開範囲選択オプションを最小にする",
- "set_new_avatar": "新しいアバターを設定する",
- "set_new_profile_background": "新しいプロフィールのバックグラウンドを設定する",
- "set_new_profile_banner": "新しいプロフィールバナーを設定する",
+ "scope_copy": "返信の公開範囲を返信先に合わせる",
+ "minimal_scopes_mode": "公開範囲選択オプションを最小化する",
+ "set_new_avatar": "アイコンを設定する",
+ "set_new_profile_background": "プロフィールの背景を設定する",
+ "set_new_profile_banner": "プロフィールのバナーを設定する",
"settings": "設定",
- "subject_input_always_show": "サブジェクトフィールドをいつでも表示する",
- "subject_line_behavior": "返信するときサブジェクトをコピーする",
- "subject_line_email": "メール風: \"re: サブジェクト\"",
- "subject_line_mastodon": "マストドン風: そのままコピー",
+ "subject_input_always_show": "注釈欄をいつでも表示する",
+ "subject_line_behavior": "返信するとき、返信先の注釈をコピーする",
+ "subject_line_email": "メール風: \"re: 注釈\"",
+ "subject_line_mastodon": "Mastodon風: そのままコピー",
"subject_line_noop": "コピーしない",
- "post_status_content_type": "投稿のコンテントタイプ",
- "stop_gifs": "カーソルを重ねたとき、GIFを動かす",
- "streaming": "上までスクロールしたとき、自動的にストリーミングする",
+ "post_status_content_type": "デフォルトの投稿形式",
+ "stop_gifs": "GIFを自動再生しない",
+ "streaming": "上までスクロールしたとき、自動でタイムラインを更新する",
"text": "文字",
"theme": "テーマ",
- "theme_help": "カラーテーマをカスタマイズできます。",
+ "theme_help": "カラーコード(#rrggbb)を使用してカラーテーマをカスタマイズできます。",
"theme_help_v2_1": "チェックボックスをONにすると、コンポーネントごとに、色と透明度をオーバーライドできます。「すべてクリア」ボタンを押すと、すべてのオーバーライドをやめます。",
"theme_help_v2_2": "バックグラウンドとテキストのコントラストを表すアイコンがあります。マウスをホバーすると、詳しい説明が出ます。透明な色を使っているときは、最悪の場合のコントラストが示されます。",
"tooltipRadius": "ツールチップとアラート",
@@ -381,9 +437,9 @@
"true": "はい"
},
"notifications": "通知",
- "notification_mutes": "特定のユーザーからの通知を止めるには、ミュートしてください。",
- "notification_blocks": "ブロックしているユーザーからの通知は、すべて止まります。",
- "enable_web_push_notifications": "ウェブプッシュ通知を許可する",
+ "notification_mutes": "特定のユーザーからの通知を止めるには、ミュートを使用してください。",
+ "notification_blocks": "ユーザーをブロックすると、そのユーザーからの通知はすべて停止されます。",
+ "enable_web_push_notifications": "プッシュ通知を有効にする",
"style": {
"switcher": {
"keep_color": "色を残す",
@@ -398,12 +454,12 @@
"help": {
"snapshot_missing": "テーマのスナップショットがありません。思っていた見た目と違うかもしれません。",
"migration_snapshot_ok": "念のために、テーマのスナップショットが読み込まれました。テーマのデータを読み込むことができます。",
- "fe_downgraded": "フロントエンドが前のバージョンに戻りました。",
- "fe_upgraded": "フロントエンドと一緒に、テーマエンジンが新しくなりました。",
- "older_version_imported": "古いフロントエンドで作られたファイルをインポートしました。",
- "future_version_imported": "新しいフロントエンドで作られたファイルをインポートしました。",
- "v2_imported": "古いフロントエンドのためのファイルをインポートしました。設定した通りにならないかもしれません。",
- "upgraded_from_v2": "フロントエンドが新しくなったので、今までの見た目と少し違うかもしれません。",
+ "fe_downgraded": "PleromaFEが前のバージョンに戻りました。",
+ "fe_upgraded": "PleromaFEのテーマエンジンが更新されました。",
+ "older_version_imported": "古いバージョンで作成されたファイルをインポートしました。",
+ "future_version_imported": "新しいバージョンで作成されたファイルをインポートしました。",
+ "v2_imported": "古いバージョンで作成されたファイルをインポートしました。設定した通りにならないかもしれません。",
+ "upgraded_from_v2": "PleromaFEが更新されました。テーマの表示が以前と異なる場合があります。",
"snapshot_source_mismatch": "フロントエンドがロールバックと更新を繰り返したため、バージョンが競合しています。",
"migration_napshot_gone": "スナップショットがありません、覚えているものと見た目が違うかもしれません。",
"snapshot_present": "テーマのスナップショットが読み込まれました。設定は上書きされました。代わりとして実データを読み込むことができます。"
@@ -432,7 +488,7 @@
"common_colors": {
"_tab_label": "共通",
"main": "共通の色",
- "foreground_hint": "「詳細」タブで、もっと細かく設定できます",
+ "foreground_hint": "「詳細」タブで、より細かく設定できます",
"rgbo": "アイコンとアクセントとバッジ"
},
"advanced_colors": {
@@ -445,7 +501,7 @@
"top_bar": "トップバー",
"borders": "境界",
"buttons": "ボタン",
- "inputs": "インプットフィールド",
+ "inputs": "入力欄",
"faint_text": "薄いテキスト",
"alert_neutral": "それ以外",
"chat": {
@@ -498,7 +554,7 @@
"buttonHover": "ボタン (ホバー)",
"buttonPressed": "ボタン (押されているとき)",
"buttonPressedHover": "ボタン (ホバー、かつ、押されているとき)",
- "input": "インプットフィールド"
+ "input": "入力欄"
},
"hintV3": "影の場合は、 {0} 表記を使って他の色スロットを使うこともできます。"
},
@@ -507,7 +563,7 @@
"help": "「カスタム」を選んだときは、システムにあるフォントの名前を、正しく入力してください。",
"components": {
"interface": "インターフェース",
- "input": "インプットフィールド",
+ "input": "入力欄",
"post": "投稿",
"postCode": "等幅 (投稿がリッチテキストであるとき)"
},
@@ -536,7 +592,7 @@
"backend_version": "バックエンドのバージョン",
"frontend_version": "フロントエンドのバージョン"
},
- "notification_setting_hide_notification_contents": "送った人と内容を、プッシュ通知に表示しない",
+ "notification_setting_hide_notification_contents": "送った人と通知の内容をプッシュ通知に表示しない",
"notification_setting_privacy": "プライバシー",
"notification_setting_block_from_strangers": "フォローしていないユーザーからの通知を拒否する",
"notification_setting_filters": "フィルター",
@@ -544,53 +600,55 @@
"virtual_scrolling": "タイムラインの描画を最適化する",
"type_domains_to_mute": "ミュートしたいドメインを検索",
"useStreamingApiWarning": "(実験中で、投稿を取りこぼすかもしれないので、おすすめしません)",
- "useStreamingApi": "投稿と通知を、すぐに受け取る",
+ "useStreamingApi": "投稿と通知をリアルタイムで受信する",
"user_mutes": "ユーザー",
- "reset_background_confirm": "本当にバックグラウンドを初期化しますか?",
- "reset_banner_confirm": "本当にバナーを初期化しますか?",
- "reset_avatar_confirm": "本当にアバターを初期化しますか?",
- "hide_wallpaper": "インスタンスのバックグラウンドを隠す",
- "reset_profile_background": "プロフィールのバックグラウンドを初期化",
- "reset_profile_banner": "プロフィールのバナーを初期化",
- "reset_avatar": "アバターを初期化",
+ "reset_background_confirm": "本当に背景をリセットしますか?",
+ "reset_banner_confirm": "本当にバナーをリセットしますか?",
+ "reset_avatar_confirm": "本当にアイコンをリセットしますか?",
+ "hide_wallpaper": "インスタンスデフォルトの壁紙を表示しない",
+ "reset_profile_background": "プロフィールの背景をリセット",
+ "reset_profile_banner": "プロフィールのバナーをリセット",
+ "reset_avatar": "アイコンをリセット",
"notification_visibility_emoji_reactions": "リアクション",
"notification_visibility_moves": "ユーザーの引っ越し",
"new_email": "新しいメールアドレス",
"profile_fields": {
"value": "内容",
"name": "ラベル",
- "add_field": "枠を追加",
- "label": "プロフィール補足情報"
+ "add_field": "入力欄を追加",
+ "label": "追加情報"
},
"accent": "アクセント",
- "mutes_imported": "ミュートをインポートしました!少し時間がかかるかもしれません。",
+ "mutes_imported": "ミュートがインポートされました。処理には時間がかかる場合があります。",
"emoji_reactions_on_timeline": "絵文字リアクションをタイムラインに表示",
"domain_mutes": "ドメイン",
"mutes_and_blocks": "ミュートとブロック",
"chatMessageRadius": "チャットメッセージ",
- "change_email_error": "メールアドレスを変えることが、できなかったかもしれません。",
- "changed_email": "メールアドレスが、変わりました!",
- "change_email": "メールアドレスを変える",
+ "change_email_error": "メールアドレスの変更中にエラーが発生しました。",
+ "changed_email": "メールアドレスが変更されました!",
+ "change_email": "メールアドレスを変更",
"bot": "これは bot アカウントです",
"mute_export_button": "ミュートをCSVファイルにエクスポートする",
"import_mutes_from_a_csv_file": "CSVファイルからミュートをインポートする",
"mute_import_error": "ミュートのインポートに失敗しました",
"mute_import": "ミュートのインポート",
"mute_export": "ミュートのエクスポート",
- "allow_following_move": "フォロー中のアカウントが引っ越したとき、自動フォローを許可する",
- "setting_changed": "規定の設定と異なっています",
- "greentext": "引用を緑色で表示",
- "sensitive_by_default": "はじめから投稿をセンシティブとして設定",
+ "allow_following_move": "フォローしているアカウントが引っ越したとき、引っ越し先を自動でフォローする",
+ "setting_changed": "デフォルトから変更された設定",
+ "greentext": ">記号から始まる行を「リピート」の色で表示 (Meme Arrows)",
+ "sensitive_by_default": "デフォルトで投稿を閲覧注意として設定",
"more_settings": "その他の設定",
- "reply_visibility_self_short": "自分宛のリプライを見る",
- "reply_visibility_following_short": "フォローしている人に宛てられたリプライを見る",
- "hide_all_muted_posts": "ミュートした投稿を隠す",
- "hide_media_previews": "メディアのプレビューを隠す",
+ "reply_visibility_self_short": "自分宛ての返信のみ表示",
+ "reply_visibility_following_short": "フォローしているユーザー宛ての返信のみ表示",
+ "hide_all_muted_posts": "ミュートした投稿を表示しない",
+ "hide_media_previews": "メディアのプレビューを表示しない",
"word_filter": "単語フィルタ",
"file_export_import": {
"errors": {
- "invalid_file": "これはPleromaの設定をバックアップしたファイルではありません。",
- "file_slightly_new": "ファイルのマイナーバージョンが異なり、一部の設定が読み込まれないことがあります"
+ "invalid_file": "非対応の形式の設定ファイルです。設定は変更されませんでした。",
+ "file_slightly_new": "設定ファイルのバージョンが異なります。一部の設定は読み込まれないかもしれません",
+ "file_too_new": "互換性エラー: PleromaFEが古すぎます。設定ファイルのバージョン{fileMajor}はこのPleromaFE (バージョン{feMajor})と互換性がありません",
+ "file_too_old": "互換性エラー: 設定ファイルが古すぎます。設定ファイルのバージョン{fileMajor}はこのPleromaFE (バージョン{feMajor})と互換性がありません"
},
"restore_settings": "設定をファイルから復元する",
"backup_settings_theme": "テーマを含む設定をファイルにバックアップする",
@@ -600,7 +658,140 @@
"save": "変更を保存",
"hide_shoutbox": "Shoutboxを表示しない",
"always_show_post_button": "投稿ボタンを常に表示",
- "right_sidebar": "サイドバーを右に表示"
+ "right_sidebar": "サイドバーを右に表示",
+ "email_language": "このサーバーから受け取るメールの言語",
+ "confirm_dialogs": "以下のとき確認ダイアログを表示する:",
+ "confirm_dialogs_repeat": "ステータスをリピートするとき",
+ "confirm_dialogs_unfollow": "ユーザーのフォローを解除するとき",
+ "confirm_dialogs_block": "ユーザーをブロックするとき",
+ "confirm_dialogs_mute": "ユーザーをミュートするとき",
+ "confirm_dialogs_delete": "投稿を削除するとき",
+ "confirm_dialogs_logout": "ログアウトするとき",
+ "confirm_dialogs_deny_follow": "フォローリクエストを却下するとき",
+ "confirm_dialogs_remove_follower": "フォロワーを解除するとき",
+ "move_account_target": "引っ越し先のアカウント (例: {example})",
+ "move_account_error": "引っ越し中にエラーが発生しました: {error}",
+ "autocomplete_select_first": "オートコンプリートで最初の結果を自動的に選択する",
+ "hide_bot_indication": "bot アカウントであることを示すマークを表示しない",
+ "navbar_column_stretch": "ナビゲーションバーの幅を画面幅に合わせる",
+ "notification_visibility_follow_requests": "フォローリクエスト",
+ "notification_visibility_reports": "通報",
+ "notification_extra_chats": "未読のチャットを表示する",
+ "hide_favorites_description": "お気に入り欄を非公開にする (通知は送信されます)",
+ "conversation_display_tree": "ツリー形式",
+ "max_depth_in_thread": "デフォルトで表示するスレッドの深さ",
+ "mention_link_display": "メンションリンクを",
+ "mention_link_display_short": "常に短く表示する (例: {'@'}hoge)",
+ "mention_link_use_tooltip": "メンションリンクをクリックした時ユーザーカードを表示する",
+ "mention_link_show_avatar": "メンションリンクの横にユーザーのアイコンを表示する",
+ "mention_link_display_full_for_remote": "リモートのユーザーのみすべて表示する (例: {'@'}hoge{'@'}example.org)",
+ "mention_link_display_full": "常にすべて表示する (例: {'@'}hoge{'@'}example.org)",
+ "notification_setting_filters_chrome_push": "Chromeなどのブラウザでは、種類に応じた通知の無効化がプッシュ通知に反映されない場合があります",
+ "hard_reset_value_tooltip": "データベースから設定値を削除し、デフォルト値に戻します",
+ "disable_sticky_headers": "カラムヘッダーを画面上部に固定しない",
+ "column_sizes_notifs": "通知カラム",
+ "conversation_other_replies_button": "「その他の返信」ボタンの位置",
+ "use_websockets": "Websocketを利用してリアルタイムで更新を行う",
+ "mention_link_fade_domain": "メンションのドメイン部分を薄く表示する (例: {'@'}foo{'@'}example.org の {'@'}example.org の部分)",
+ "mention_link_show_avatar_quick": "メンションの横にユーザーアイコンを表示する",
+ "mention_link_bolden_you": "自分宛てのメンションを強調表示する",
+ "user_popover_avatar_action": "ユーザーカード内のユーザーアイコンをクリックした際の挙動",
+ "user_popover_avatar_overlay": "ユーザーカードをユーザーアイコンに被せて表示する",
+ "show_yous": "自分宛てのメンションの横に「(あなた)」を表示",
+ "preview": "プレビュー",
+ "url": "URL",
+ "conversation_display": "スレッドの表示形式",
+ "column_sizes": "カラム幅",
+ "third_column_mode_none": "表示しない",
+ "column_sizes_content": "コンテンツ",
+ "third_column_mode_notifications": "通知カラムにする",
+ "third_column_mode_postform": "投稿フォームとナビゲーションにする",
+ "conversation_display_linear_quick": "時系列表示",
+ "conversation_display_linear": "時系列形式",
+ "conversation_display_tree_quick": "ツリー表示",
+ "user_popover_avatar_action_open": "プロフィールを表示する",
+ "account_backup": "アカウントのバックアップ",
+ "wordfilter": "ワードフィルター",
+ "column_sizes_sidebar": "サイドバー",
+ "emoji_reactions_scale": "絵文字リアクションの表示倍率",
+ "hide_wordfiltered_statuses": "ワードフィルターによってフィルタリングされたステータスを表示しない",
+ "hide_muted_threads": "ミュートしたスレッドを表示しない",
+ "notification_visibility_polls": "投票結果の確定",
+ "user_popover_avatar_action_zoom": "アイコンを拡大する",
+ "post_look_feel": "投稿の表示形式",
+ "mention_links": "メンションのリンク",
+ "setting_server_side": "この設定はサーバー側に保存され、すべてのセッションとクライアントに影響します",
+ "word_filter_and_more": "ワードフィルターとその他の設定",
+ "notification_extra_announcements": "未読のお知らせを表示する",
+ "notification_extra_follow_requests": "新着のフォローリクエストを表示する",
+ "show_scrollbars": "サイドカラムにスクロールバーを表示する",
+ "third_column_mode": "十分に幅があるとき、三つ目のカラムを",
+ "columns": "カラム",
+ "commit_value": "保存",
+ "commit_value_tooltip": "値は保存されていません。反映するにはこのボタンを押してください",
+ "remove_backup": "削除",
+ "add_backup": "新規バックアップを作成",
+ "account_backup_description": "アカウント情報と投稿のアーカイブをダウンロードできます。開発段階の機能であり、現状、ダウンロードしたデータをインポートすることはできません。",
+ "mute_bot_posts": "BOTアカウントの投稿をミュートする",
+ "auto_update": "自動でタイムラインを更新する",
+ "enable_web_push_always_show_tip": "この設定は、Chromeなどのブラウザで「このサイトはバックグラウンドで更新されました」という通知が表示されることを防止します。その他のブラウザでこの設定を有効化すると、通知が二重で表示されることがあります。",
+ "backup_failed": "バックアップに失敗しました。",
+ "confirm_dialogs_approve_follow": "フォローリクエストを承認するとき",
+ "moved_account": "アカウントの引っ越しが完了しました。",
+ "reset_value": "リセット",
+ "reset_value_tooltip": "編集中の値を破棄します",
+ "hard_reset_value": "デフォルトに戻す",
+ "conversation_other_replies_button_below": "投稿の下",
+ "conversation_other_replies_button_inside": "投稿の中",
+ "add_language": "代替言語を追加",
+ "remove_language": "削除",
+ "account_alias_table_head": "エイリアス",
+ "account_alias": "アカウントエイリアス",
+ "list_aliases_error": "エイリアスの取得中にエラーが発生しました: {error}",
+ "hide_list_aliases_error_action": "閉じる",
+ "remove_alias": "削除",
+ "new_alias_target": "エイリアスを追加 (例: {example})",
+ "added_alias": "エイリアスが追加されました。",
+ "add_alias_error": "エイリアスの追加中にエラーが発生しました: {error}",
+ "move_account": "アカウントの引っ越し",
+ "move_account_notes": "アカウントを引っ越すためには、まず引っ越し先のアカウントにこのアカウントへのエイリアスを追加する必要があります。",
+ "birthday": {
+ "label": "誕生日",
+ "show_birthday": "誕生日を公開する"
+ },
+ "account_privacy": "プライバシー",
+ "posts": "投稿",
+ "user_profiles": "ユーザープロフィール",
+ "primary_language": "第一言語:",
+ "fallback_language": "代替言語 {index}:",
+ "expert_mode": "高度な設定を表示",
+ "account_backup_table_head": "バックアップ",
+ "download_backup": "ダウンロード",
+ "backup_not_ready": "まだ準備中です。",
+ "backup_running": "処理中…{number}件のデータが処理されました。 | 処理中… {number}件のデータが処理されました。",
+ "list_backups_error": "バックアップ一覧の取得に失敗しました: {error}",
+ "added_backup": "バックアップがキューに追加されました。",
+ "add_backup_error": "バックアップの追加に失敗しました: {error}",
+ "user_popover_avatar_action_close": "ユーザーカードを閉じる",
+ "tree_advanced": "高度なナビゲーションボタンを表示する",
+ "tree_fade_ancestors": "スレッド上で祖先にあたるステータスを薄いテキストで表示する",
+ "actor_type_description": "グループとして設定されたアカウントは、メンションのついたステータスを自動的にリピートします。",
+ "actor_type_Person": "通常アカウント",
+ "actor_type_Service": "BOTアカウント",
+ "actor_type_Group": "グループアカウント",
+ "notification_visibility_in_column": "通知カラム(PC)、通知サイドバー(モバイル)に表示する",
+ "notification_setting_annoyance": "通知のカスタマイズ",
+ "notification_setting_unseen_at_top": "未読の通知を最上部に表示する",
+ "enable_web_push_always_show": "プッシュ通知を常に表示する",
+ "hide_scrobbles": "Scrobbleを表示しない",
+ "actor_type": "アカウントタイプ:",
+ "hide_actor_type_indication": "投稿にアカウントタイプ(BOTアカウント、グループアカウントなど)を示すアイコンを表示しない",
+ "notification_show_extra": "その他の通知を通知カラムに表示する",
+ "notification_setting_drawer_marks_as_seen": "モバイルUIで、通知サイドバーを閉じた時すべての通知を既読にする",
+ "notification_setting_ignore_inactionable_seen_tip": "この設定は通知を自動的に既読にするわけではなく、この設定を有効にしてもプッシュ通知などは届きます",
+ "notification_setting_ignore_inactionable_seen": "お気に入りやリピートの通知など、アクション不可な通知を未読として扱わない",
+ "notification_extra_tip": "通知カラムをカスマイズするためのヒントを表示する",
+ "use_at_icon": "メンションリンク内の{'@'}記号を画像にする"
},
"time": {
"day": "{0}日",
@@ -622,7 +813,7 @@
"month_short": "{0}ヶ月前",
"months_short": "{0}ヶ月前",
"now": "たった今",
- "now_short": "たった今",
+ "now_short": "今",
"second": "{0}秒",
"seconds": "{0}秒",
"second_short": "{0}秒",
@@ -634,23 +825,41 @@
"year": "{0}年",
"years": "{0}年",
"year_short": "{0}年",
- "years_short": "{0}年"
+ "years_short": "{0}年",
+ "unit": {
+ "seconds_short": "{0}秒",
+ "weeks": "{0} 週間 | {0} 週間",
+ "weeks_short": "{0}週",
+ "years": "{0} 年 | {0} 年",
+ "years_short": "{0}年",
+ "days": "{0} 日 | {0} 日",
+ "hours": "{0} 時間 | {0} 時間",
+ "hours_short": "{0}時間",
+ "minutes": "{0} 分 | {0} 分",
+ "minutes_short": "{0}分",
+ "months": "{0} ヶ月 | {0} ヶ月",
+ "months_short": "{0}ヶ月",
+ "seconds": "{0} 秒 | {0} 秒",
+ "days_short": "{0}日"
+ }
},
"timeline": {
"collapse": "たたむ",
"conversation": "スレッド",
"error_fetching": "読み込みがエラーになりました",
- "load_older": "古いステータス",
- "no_retweet_hint": "投稿を「フォロワーのみ」または「ダイレクト」にすると、リピートできなくなります",
+ "load_older": "古いステータスを読み込む",
+ "no_retweet_hint": "公開範囲が「フォロワーのみ」または「ダイレクト」の投稿はリピートできません",
"repeated": "リピート",
"show_new": "読み込み",
"up_to_date": "最新",
"no_more_statuses": "これで終わりです",
"no_statuses": "ステータスはありません",
"reload": "再読み込み",
- "error": "タイムラインの読み込みに失敗しました: {0}",
+ "error": "タイムラインの読み込み中にエラーが発生しました: {0}",
"socket_reconnected": "リアルタイム接続が確立されました",
- "socket_broke": "コード{0}によりリアルタイム接続が切断されました"
+ "socket_broke": "リアルタイム接続が切断されました: コード{0}",
+ "quick_view_settings": "表示の簡易設定",
+ "quick_filter_settings": "フィルターの簡易設定"
},
"status": {
"favorites": "お気に入り",
@@ -659,8 +868,8 @@
"pin": "プロフィールにピン留め",
"unpin": "プロフィールのピン留めを外す",
"pinned": "ピン留め",
- "delete_confirm": "本当にこのステータスを削除してもよろしいですか?",
- "reply_to": "返信",
+ "delete_confirm": "本当にこの削除しますか?",
+ "reply_to": "返信先:",
"replies_list": "返信:",
"mute_conversation": "スレッドをミュート",
"unmute_conversation": "スレッドのミュートを解除",
@@ -679,19 +888,58 @@
"unbookmark": "ブックマーク解除",
"bookmark": "ブックマーク",
"mentions": "メンション",
- "you": "(あなた)",
- "plus_more": "ほか{number}件"
+ "you": "(あなた)",
+ "plus_more": "ほか{number}件",
+ "delete_confirm_title": "削除の確認",
+ "ancestor_follow": "このステータスについた{numReplies}件の返信をすべて表示 | このステータスについた{numReplies}件の返信をすべて表示",
+ "invisible_quote": "引用先のステータスが存在しません: {link}",
+ "ancestor_follow_with_icon": "{icon} {text}",
+ "show_all_conversation_with_icon": "{icon} {text}",
+ "delete_error": "ステータスの削除中にエラーが発生しました: {0}",
+ "delete_confirm_accept_button": "削除する",
+ "delete_confirm_cancel_button": "削除しない",
+ "collapse_attachments": "ファイルをたたむ",
+ "show_all_attachments": "すべてのファイルを表示",
+ "hide_attachment": "ファイルを隠す",
+ "reaction_count_label": "{num}人がリアクションしました | {num}人がリアクションしました",
+ "repeat_confirm_accept_button": "リピートする",
+ "repeat_confirm_cancel_button": "リピートしない",
+ "repeat_confirm": "本当にリピートしますか?",
+ "edit": "ステータスを編集",
+ "edited_at": "(最終編集: {time})",
+ "repeat_confirm_title": "リピートの確認",
+ "many_attachments": "この投稿には{number}件のファイルが添付されています",
+ "remove_attachment": "ファイルを削除",
+ "attachment_stop_flash": "Flashプレーヤーを停止",
+ "move_up": "ファイルを左へ移動",
+ "move_down": "ファイルを右へ移動",
+ "thread_follow": "このスレッドの残りを表示 (全部で{numStatus}件の投稿があります) | このスレッドの残りを表示 (全部で{numStatus}件の投稿があります)",
+ "thread_follow_with_icon": "{icon} {text}",
+ "hide_quote": "引用先を隠す",
+ "display_quote": "引用先を表示",
+ "show_only_conversation_under_this": "このステータスへの返信のみを表示",
+ "show_all_conversation": "スレッドの全体を表示 ({numStatus}件のステータス) | スレッドの全体を表示 ({numStatus}件のステータス)",
+ "replies_list_with_others": "返信 (+{numReplies}人): | 返信 (+{numReplies}人):",
+ "more_actions": "その他のアクション",
+ "thread_show_full": "このスレッドをすべて表示 (全部で{depth}層、{numStatus}件の投稿があります) | このスレッドを全て表示 (全部で{depth}層、{numStatus}件の投稿があります)",
+ "thread_show_full_with_icon": "{icon} {text}",
+ "show_attachment_in_modal": "メディアビューアで開く",
+ "show_attachment_description": "メディアの説明文をポップアップで表示 (全文を読むにはメディアを開いてください)",
+ "thread_hide": "このスレッドをたたむ",
+ "thread_show": "このスレッドを開く",
+ "open_gallery": "メディアビューアで開く",
+ "status_history": "編集履歴"
},
"user_card": {
- "approve": "受け入れ",
+ "approve": "承認",
"block": "ブロック",
"blocked": "ブロックしています!",
- "deny": "お断り",
+ "deny": "拒否",
"favorites": "お気に入り",
"follow": "フォロー",
- "follow_sent": "リクエストを送りました!",
+ "follow_sent": "リクエストを送信しました!",
"follow_progress": "リクエストしています…",
- "follow_unfollow": "フォローをやめる",
+ "follow_unfollow": "フォロー解除",
"followees": "フォロー",
"followers": "フォロワー",
"following": "フォローしています!",
@@ -700,7 +948,7 @@
"media": "メディア",
"mention": "メンション",
"mute": "ミュート",
- "muted": "ミュートしています",
+ "muted": "ミュート済み",
"per_day": "/日",
"remote_follow": "リモートフォロー",
"report": "通報",
@@ -720,16 +968,17 @@
"grant_moderator": "モデレーター権限を付与",
"revoke_moderator": "モデレーター権限を解除",
"activate_account": "アカウントをアクティブにする",
- "deactivate_account": "アカウントをアクティブでなくする",
+ "deactivate_account": "アカウントを無効化する",
"delete_account": "アカウントを削除",
- "force_nsfw": "すべての投稿をNSFWにする",
- "strip_media": "投稿からメディアを除去する",
- "force_unlisted": "投稿を未収載にする",
- "sandbox": "投稿をフォロワーのみにする",
- "disable_remote_subscription": "他のインスタンスからフォローされないようにする",
- "disable_any_subscription": "フォローされないようにする",
- "quarantine": "他のインスタンスからの投稿を止める",
- "delete_user": "ユーザーを削除"
+ "force_nsfw": "すべての投稿を閲覧注意にする",
+ "strip_media": "すべての投稿からメディアを除去する",
+ "force_unlisted": "すべての投稿をアンリステッドにする",
+ "sandbox": "すべての投稿をフォロワー限定にする",
+ "disable_remote_subscription": "他のインスタンスからフォローできないようにする",
+ "disable_any_subscription": "フォローできないようにする",
+ "quarantine": "投稿を連合しないようにする",
+ "delete_user": "ユーザーを削除",
+ "delete_user_data_and_deactivate_confirmation": "このアカウントのデータを永久に削除し、アカウントを無効化します。本当によろしいですね?"
},
"roles": {
"moderator": "モデレーター",
@@ -746,21 +995,55 @@
"side": "端に線を付ける",
"disabled": "強調しない"
},
- "edit_profile": "プロフィールを編集"
+ "edit_profile": "プロフィールを編集",
+ "deny_confirm_accept_button": "拒否する",
+ "note_blank": "(なし)",
+ "edit_note_cancel": "キャンセル",
+ "remove_follower_confirm_cancel_button": "解除しない",
+ "block_confirm_title": "ブロックの確認",
+ "block_confirm": "本当に{user}をブロックしますか?",
+ "birthday": "誕生日: {birthday}",
+ "edit_note": "メモを編集",
+ "edit_note_apply": "適用",
+ "note": "メモ",
+ "remove_follower_confirm": "本当に{user}からのフォローを解除しますか?",
+ "follow_cancel": "リクエストを取り消す",
+ "approve_confirm_title": "承認の確認",
+ "remove_follower": "フォロワーを解除",
+ "remove_follower_confirm_title": "フォロワー解除の確認",
+ "approve_confirm_accept_button": "承認する",
+ "deny_confirm": "本当に{user}からのフォローリクエストを拒否しますか?",
+ "deny_confirm_cancel_button": "拒否しない",
+ "mute_confirm_cancel_button": "ミュートしない",
+ "approve_confirm_cancel_button": "承認しない",
+ "approve_confirm": "本当に{user}からのフォローリクエストを承認しますか?",
+ "unfollow_confirm_title": "フォロー解除の確認",
+ "unfollow_confirm_cancel_button": "解除しない",
+ "mute_confirm_accept_button": "ミュートする",
+ "mute_confirm": "本当に{user}をミュートしますか?",
+ "block_confirm_accept_button": "ブロックする",
+ "block_confirm_cancel_button": "ブロックしない",
+ "deny_confirm_title": "拒否の確認",
+ "unfollow_confirm": "本当に{user}のフォローを解除しますか?",
+ "unfollow_confirm_accept_button": "解除する",
+ "remove_follower_confirm_accept_button": "解除する",
+ "mute_confirm_title": "ミュートの確認",
+ "deactivated": "無効化済み",
+ "group": "グループ"
},
"user_profile": {
"timeline_title": "ユーザータイムライン",
- "profile_does_not_exist": "申し訳ない。このプロフィールは存在しません。",
- "profile_loading_error": "申し訳ない。プロフィールの読み込みがエラーになりました。"
+ "profile_does_not_exist": "このプロフィールは存在しません。",
+ "profile_loading_error": "プロフィールの読み込み中にエラーが発生しました。"
},
"user_reporting": {
- "title": "通報する: {0}",
+ "title": "{0}を通報する",
"add_comment_description": "この通報は、あなたのインスタンスのモデレーターに送られます。このアカウントを通報する理由を説明することができます:",
"additional_comments": "追加のコメント",
- "forward_description": "このアカウントは他のサーバーに置かれています。この通報のコピーをリモートのサーバーに送りますか?",
- "forward_to": "転送する: {0}",
+ "forward_description": "これは他のインスタンスのアカウントです。この通報のコピーをリモートのインスタンスに送りますか?",
+ "forward_to": "{0}に転送する",
"submit": "送信",
- "generic_error": "あなたのリクエストを処理しようとしましたが、エラーになりました。"
+ "generic_error": "リクエストの処理中にエラーが発生しました。"
},
"who_to_follow": {
"more": "詳細",
@@ -774,15 +1057,17 @@
"user_settings": "ユーザー設定",
"bookmark": "ブックマーク",
"reject_follow_request": "フォローリクエストを拒否",
- "accept_follow_request": "フォローリクエストを許可",
- "add_reaction": "リアクションを追加"
+ "accept_follow_request": "フォローリクエストを承認",
+ "add_reaction": "リアクションを追加",
+ "toggle_mute": "ミュートされた通知を開く/閉じる",
+ "toggle_expand": "この投稿を開く/閉じる"
},
"upload": {
"error": {
"base": "アップロードに失敗しました。",
- "file_too_big": "ファイルが大きすぎます [{filesize} {filesizeunit} / {allowedsize} {allowedsizeunit}]",
- "default": "しばらくしてから試してください",
- "message": "アップロードに失敗: {0}"
+ "file_too_big": "ファイルが大きすぎます [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]",
+ "default": "時間を置いて再試行してください",
+ "message": "アップロードに失敗しました: {0}"
},
"file_size_units": {
"B": "B",
@@ -793,11 +1078,13 @@
}
},
"search": {
- "people": "人々",
+ "people": "ユーザー",
"hashtags": "ハッシュタグ",
"person_talking": "{count} 人が話しています",
"people_talking": "{count} 人が話しています",
- "no_results": "見つかりませんでした"
+ "no_results": "見つかりませんでした",
+ "load_more": "さらに読み込む",
+ "no_more_results": "結果は以上です"
},
"password_reset": {
"forgot_password": "パスワードを忘れましたか?",
@@ -817,23 +1104,25 @@
"federation": "連合",
"simple": {
"media_nsfw_desc": "このインスタンスでは、以下のインスタンスからの投稿に対して、メディアを閲覧注意に設定します:",
- "media_nsfw": "メディアを閲覧注意に設定",
+ "media_nsfw": "強制閲覧注意",
"media_removal_desc": "このインスタンスでは、以下のインスタンスからの投稿に対して、メディアを除去します:",
"media_removal": "メディア除去",
- "ftl_removal": "「既知のネットワーク」タイムラインから除外",
- "ftl_removal_desc": "このインスタンスでは、以下のインスタンスを「既知のネットワーク」タイムラインから除外します:",
+ "ftl_removal": "「すべてのネットワーク」タイムラインから除外",
+ "ftl_removal_desc": "このインスタンスでは、以下のインスタンスを「すべてのネットワーク」タイムラインから除外します:",
"quarantine_desc": "このインスタンスでは、以下のインスタンスに対して公開投稿のみを送信します:",
"quarantine": "検疫",
"reject_desc": "このインスタンスでは、以下のインスタンスからのメッセージを受け付けません:",
"accept_desc": "このインスタンスでは、以下のインスタンスからのメッセージのみを受け付けます:",
"accept": "許可",
- "simple_policies": "インスタンス固有のポリシー",
- "reject": "拒否"
+ "simple_policies": "インスタンスに対するポリシー",
+ "reject": "拒否",
+ "instance": "インスタンス",
+ "reason": "理由"
},
"mrf_policies": "有効なMRFポリシー",
"keyword": {
- "replace": "置き換え",
- "ftl_removal": "「接続しているすべてのネットワーク」タイムラインから除外",
+ "replace": "置換",
+ "ftl_removal": "「すべてのネットワーク」タイムラインから除外",
"keyword_policies": "キーワードポリシー",
"is_replaced_by": "→",
"reject": "拒否"
@@ -847,8 +1136,8 @@
"file_type": {
"file": "ファイル",
"image": "画像",
- "video": "ビデオ",
- "audio": "オーディオ"
+ "video": "動画",
+ "audio": "音声"
},
"remote_user_resolver": {
"error": "見つかりませんでした。",
@@ -865,7 +1154,7 @@
"empty_chat_list_placeholder": "チャットはありません。新規チャットのボタンを押して始めましょう!",
"error_sending_message": "メッセージの送信に失敗しました。",
"error_loading_chat": "チャットの読み込みに失敗しました。",
- "delete_confirm": "このメッセージを本当に消してもいいですか?",
+ "delete_confirm": "本当にこのメッセージを削除しますか?",
"more": "もっと見る",
"empty_message_error": "メッセージを入力して下さい",
"new": "新規チャット",
@@ -879,5 +1168,152 @@
"unmute": "ミュート解除",
"mute_progress": "ミュート中…",
"mute": "ミュート"
+ },
+ "admin_dash": {
+ "window_title": "管理者設定",
+ "nodb": {
+ "text": "{property}が{value}に設定されるよう、設定ファイルを編集する必要があります。詳しくは{documentation}を確認してください。",
+ "documentation": "ドキュメント",
+ "text2": "ほとんどの設定項目は利用できません。",
+ "heading": "データベースへの設定の保存は無効化されています"
+ },
+ "captcha": {
+ "native": "ネイティブ",
+ "kocaptcha": "KoCaptcha"
+ },
+ "instance": {
+ "restrict": {
+ "header": "匿名ユーザーへのアクセス制限",
+ "profiles": "ユーザープロフィールへのアクセス",
+ "timelines": "タイムラインへのアクセス",
+ "activities": "ステータスへのアクセス",
+ "description": "この設定は特定のAPIへのアクセスを制御します。デフォルトでは、インスタンスの公開設定を反映します。一部の設定は、変更すると予期しない動作を引き起こすことがあります(例: ユーザープロフィールへのアクセスを禁止すると、投稿にユーザーの情報が表示されなくなります)。"
+ },
+ "instance": "インスタンス情報",
+ "registrations": "ユーザー登録",
+ "access": "インスタンスへのアクセス",
+ "captcha_header": "CAPTCHA",
+ "kocaptcha": "KoCaptchaの設定"
+ },
+ "frontend": {
+ "available_frontends": "インストール可能なフロントエンド",
+ "success_installing_frontend": "{version} は正常にインストールされました",
+ "failure_installing_frontend": "{version} のインストールに失敗しました: {reason}",
+ "repository": "リポジトリのURL",
+ "versions": "利用可能なバージョン",
+ "build_url": "ダウンロードURL",
+ "reinstall": "再インストール",
+ "install": "インストール",
+ "install_version": "バージョン {version} をインストール",
+ "is_default": "(デフォルト)",
+ "is_default_custom": "(デフォルト、バージョン: {version})",
+ "default_frontend": "デフォルトのフロントエンド",
+ "more_install_options": "その他のインストールオプション",
+ "wip_notice": "このセクションは開発段階です。バックエンド側の実装が未完成であるため、一部の機能は欠けています。",
+ "set_default": "デフォルトに設定",
+ "set_default_version": "バージョン {version} をデフォルトに設定",
+ "default_frontend_tip": "デフォルトのフロントエンドはすべてのユーザーに表示されます。現時点で、ユーザーがフロントエンドを選択する方法はありません。デフォルトのフロントエンドをPleromaFE以外に設定した場合、インスタンスの設定を変更するには古いAdminFEを使用する必要があります。"
+ },
+ "temp_overrides": {
+ ":pleroma": {
+ ":instance": {
+ ":public": {
+ "label": "インスタンスを公開する",
+ "description": "この設定を無効化すると、すべてのAPIの使用にログインが必要になります。これにより、匿名ユーザーは公開タイムラインとすべてのネットワークにアクセスできなくなります。"
+ },
+ ":background_image": {
+ "description": "(主にPleromaFEで使用される)背景画像",
+ "label": "背景画像"
+ },
+ ":limit_to_local_content": {
+ "description": "他インスタンスの情報の検索を、未ログインのユーザー(デフォルト)もしくはすべてのユーザーに対して制限します",
+ "label": "検索をローカルのみに制限する"
+ },
+ ":description_limit": {
+ "description": "ファイルの説明欄に対する文字数制限",
+ "label": "制限"
+ }
+ }
+ }
+ },
+ "wip_notice": "この管理者用設定画面は試験段階であり、未完成です。{adminFeLink}。",
+ "reset_all": "すべてリセット",
+ "commit_all": "すべて保存",
+ "old_ui_link": "これまでの管理者画面にはここからアクセスできます",
+ "tabs": {
+ "limits": "制限",
+ "instance": "インスタンス",
+ "frontends": "フロントエンド"
+ },
+ "limits": {
+ "arbitrary_limits": "変更可能な制限",
+ "posts": "投稿の制限",
+ "uploads": "ファイルの制限",
+ "profile_fields": "追加情報欄の制限",
+ "user_uploads": "プロフィール画像の制限",
+ "users": "ユーザープロフィールの設定"
+ }
+ },
+ "lists": {
+ "search": "ユーザーを検索",
+ "update_title": "リスト名を保存",
+ "really_delete": "本当に削除しますか?",
+ "error": "リストの処理中にエラーが発生しました: {0}",
+ "lists": "リスト",
+ "new": "新規リスト",
+ "save": "変更を保存",
+ "delete": "リストを削除",
+ "editing_list": "{listTitle}の編集",
+ "creating_list": "新規リストの作成",
+ "create": "作成",
+ "title": "リスト名",
+ "following_only": "フォローしているユーザーのみ表示",
+ "manage_lists": "リストの管理",
+ "manage_members": "メンバーの管理",
+ "add_members": "メンバーの追加",
+ "remove_from_list": "リストから削除",
+ "add_to_list": "リストに追加",
+ "is_in_list": "追加済み"
+ },
+ "update": {
+ "update_bugs": "何か問題を見つけたら{pleromaGitlab}にて報告してください。開発中のバージョンにて念入りに確認はしましたが、様々なものが変更されているため、我々が見逃したものがあるかもしれません。バグの報告や、Pleroma/PleromaFEを改善するための提案やフィードバックは大歓迎です。",
+ "update_changelog_here": "変更履歴",
+ "update_changelog": "全ての変更点は{theFullChangelog}を参照してください。",
+ "big_update_content": "久しぶりのリリースですので、今までと異なるところがあるかもしれません。",
+ "update_bugs_gitlab": "Pleroma GitLab",
+ "big_update_title": ""
+ },
+ "report": {
+ "reported_statuses": "通報されたステータス:",
+ "notes": "メモ:",
+ "state": "状態:",
+ "state_open": "未解決",
+ "reporter": "通報者:",
+ "state_resolved": "解決済み",
+ "reported_user": "被通報者:",
+ "state_closed": "問題なし"
+ },
+ "unicode_domain_indicator": {
+ "tooltip": "このドメインには非ASCII文字が含まれています。"
+ },
+ "announcements": {
+ "page_header": "お知らせ",
+ "title": "お知らせ",
+ "mark_as_read_action": "既読にする",
+ "post_form_header": "お知らせを投稿",
+ "post_placeholder": "お知らせの内容を入力してください…",
+ "post_action": "投稿",
+ "post_error": "エラー: {error}",
+ "close_error": "閉じる",
+ "delete_action": "削除",
+ "submit_edit_action": "完了",
+ "cancel_edit_action": "キャンセル",
+ "published_time_display": "{time} に公開",
+ "start_time_display": "{time}から開始",
+ "end_time_display": "{time}に終了",
+ "edit_action": "編集",
+ "start_time_prompt": "開始日時: ",
+ "end_time_prompt": "終了日時: ",
+ "all_day_prompt": "終日"
}
}
diff --git a/src/i18n/ko.json b/src/i18n/ko.json
index 69d898ba..003878db 100644
--- a/src/i18n/ko.json
+++ b/src/i18n/ko.json
@@ -109,7 +109,8 @@
"mobile_notifications_close": "알림 닫기",
"mobile_sidebar": "모바일 사이드바 토글",
"announcements": "공지사항",
- "search_close": "검색 바 닫기"
+ "search_close": "검색 바 닫기",
+ "mobile_notifications_mark_as_seen": "모두 읽음으로 표시"
},
"notifications": {
"broken_favorite": "알 수 없는 게시물입니다, 검색합니다…",
@@ -125,7 +126,13 @@
"error": "알림 불러오기 실패: {0}",
"follow_request": "팔로우 요청",
"submitted_report": "신고 내용을 전송함",
- "poll_ended": "투표가 끝남"
+ "poll_ended": "투표가 끝남",
+ "unread_follow_requests": "{num}개의 새 팔로우 요청 | {num}개의 새 팔로우 요청",
+ "configuration_tip": "{theSettings}에서 어떻게 보이는지 바꿀 수 있습니다. {dismiss}",
+ "configuration_tip_settings": "설정",
+ "configuration_tip_dismiss": "다시 보지 않기",
+ "unread_announcements": "{num}개의 읽지 않은 공지사항 | {num}개의 읽지 않은 공지사항",
+ "unread_chats": "{num}개의 읽지 않은 채팅 | {num}개의 읽지 않은 채팅"
},
"post_status": {
"new_status": "새 게시물 게시",
@@ -165,7 +172,9 @@
"post": "게시",
"direct_warning_to_first_only": "맨 앞에 멘션한 사용자들에게만 보여집니다.",
"content_type_selection": "게시물 형태",
- "scope_notice_dismiss": "알림 닫기"
+ "scope_notice_dismiss": "알림 닫기",
+ "reply_option": "이 게시물에 답글",
+ "quote_option": "이 게시물을 인용"
},
"registration": {
"bio": "소개",
diff --git a/src/i18n/nan-TW.json b/src/i18n/nan-TW.json
index 5e0df8ec..782a75f5 100644
--- a/src/i18n/nan-TW.json
+++ b/src/i18n/nan-TW.json
@@ -189,7 +189,8 @@
"mobile_notifications": "拍開通知(有無讀ê)",
"mobile_notifications_close": "關掉通知",
"announcements": "公告",
- "search": "Tshuē"
+ "search": "Tshuē",
+ "mobile_notifications_mark_as_seen": "Lóng 標做有讀"
},
"notifications": {
"broken_favorite": "狀態毋知影,leh tshiau-tshuē……",
diff --git a/src/i18n/zh.json b/src/i18n/zh.json
index 396cb2f2..2ade41f7 100644
--- a/src/i18n/zh.json
+++ b/src/i18n/zh.json
@@ -943,7 +943,7 @@
"sandbox": "强制帖子为只有关注者可看",
"disable_remote_subscription": "禁止从远程实例关注用户",
"disable_any_subscription": "完全禁止关注用户",
- "quarantine": "从联合实例中禁止用户帖子",
+ "quarantine": "不许帖子传入别站",
"delete_user": "删除用户",
"delete_user_data_and_deactivate_confirmation": "这将永久删除该账户的数据并停用该账户。你完全确定吗?"
},
diff --git a/src/lib/persisted_state.js b/src/lib/persisted_state.js
index 6d59c595..22ba1e7f 100644
--- a/src/lib/persisted_state.js
+++ b/src/lib/persisted_state.js
@@ -38,7 +38,7 @@ export default function createPersistedState ({
},
setState = (key, state, storage) => {
if (!loaded) {
- console.log('waiting for old state to be loaded...')
+ console.info('waiting for old state to be loaded...')
return Promise.resolve()
} else {
return storage.setItem(key, state)
@@ -65,7 +65,7 @@ export default function createPersistedState ({
}
loaded = true
} catch (e) {
- console.log("Couldn't load state")
+ console.error("Couldn't load state")
console.error(e)
loaded = true
}
@@ -86,8 +86,8 @@ export default function createPersistedState ({
})
}
} catch (e) {
- console.log("Couldn't persist state:")
- console.log(e)
+ console.error("Couldn't persist state:")
+ console.error(e)
}
})
}
diff --git a/src/main.js b/src/main.js
index 0b7c7674..85eb1f4c 100644
--- a/src/main.js
+++ b/src/main.js
@@ -6,6 +6,7 @@ import './lib/event_target_polyfill.js'
import interfaceModule from './modules/interface.js'
import instanceModule from './modules/instance.js'
import statusesModule from './modules/statuses.js'
+import notificationsModule from './modules/notifications.js'
import listsModule from './modules/lists.js'
import usersModule from './modules/users.js'
import apiModule from './modules/api.js'
@@ -78,6 +79,7 @@ const persistedStateOptions = {
// TODO refactor users/statuses modules, they depend on each other
users: usersModule,
statuses: statusesModule,
+ notifications: notificationsModule,
lists: listsModule,
api: apiModule,
config: configModule,
diff --git a/src/modules/adminSettings.js b/src/modules/adminSettings.js
index 3df3647f..ef28f6e0 100644
--- a/src/modules/adminSettings.js
+++ b/src/modules/adminSettings.js
@@ -105,7 +105,6 @@ const adminSettingsStorage = {
}
set(config, path, convert(c.value))
})
- console.log(config[':pleroma'])
commit('updateAdminSettings', { config, modifiedPaths })
commit('resetAdminDraft')
},
@@ -123,7 +122,6 @@ const adminSettingsStorage = {
const descriptions = {}
backendDescriptions.forEach(d => convert(d, '', descriptions))
- console.log(descriptions[':pleroma']['Pleroma.Captcha'])
commit('updateAdminDescriptions', { descriptions })
},
diff --git a/src/modules/config.js b/src/modules/config.js
index 49e9b2df..abb57272 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -66,7 +66,20 @@ export const defaultState = {
chatMention: true,
polls: true
},
+ notificationNative: {
+ follows: true,
+ mentions: true,
+ likes: false,
+ repeats: false,
+ moves: false,
+ emojiReactions: false,
+ followRequest: true,
+ reports: true,
+ chatMention: true,
+ polls: true
+ },
webPushNotifications: false,
+ webPushAlwaysShowNotifications: false,
muteWords: [],
highlight: {},
interfaceLanguage: browserLocale,
@@ -124,7 +137,10 @@ export const defaultState = {
showAnnouncementsInExtraNotifications: undefined, // instance default
showFollowRequestsInExtraNotifications: undefined, // instance default
maxDepthInThread: undefined, // instance default
- autocompleteSelect: undefined // instance default
+ autocompleteSelect: undefined, // instance default
+ closingDrawerMarksAsSeen: undefined, // instance default
+ unseenAtTop: undefined, // instance default
+ ignoreInactionableSeen: undefined // instance default
}
// caching the instance default properties
diff --git a/src/modules/instance.js b/src/modules/instance.js
index 034348ff..c7a2cad1 100644
--- a/src/modules/instance.js
+++ b/src/modules/instance.js
@@ -110,6 +110,9 @@ const defaultState = {
showFollowRequestsInExtraNotifications: true,
maxDepthInThread: 6,
autocompleteSelect: false,
+ closingDrawerMarksAsSeen: true,
+ unseenAtTop: false,
+ ignoreInactionableSeen: false,
// Nasty stuff
customEmoji: [],
@@ -134,6 +137,7 @@ const defaultState = {
suggestionsEnabled: false,
suggestionsWeb: '',
quotingAvailable: false,
+ groupActorAvailable: false,
// Html stuff
instanceSpecificPanelContent: '',
diff --git a/src/modules/notifications.js b/src/modules/notifications.js
new file mode 100644
index 00000000..c1f5a63e
--- /dev/null
+++ b/src/modules/notifications.js
@@ -0,0 +1,169 @@
+import apiService from '../services/api/api.service.js'
+
+import {
+ isStatusNotification,
+ isValidNotification,
+ maybeShowNotification
+} from '../services/notification_utils/notification_utils.js'
+
+import {
+ closeDesktopNotification,
+ closeAllDesktopNotifications
+} from '../services/desktop_notification_utils/desktop_notification_utils.js'
+
+const emptyNotifications = () => ({
+ desktopNotificationSilence: true,
+ maxId: 0,
+ minId: Number.POSITIVE_INFINITY,
+ data: [],
+ idStore: {},
+ loading: false
+})
+
+export const defaultState = () => ({
+ ...emptyNotifications()
+})
+
+export const notifications = {
+ state: defaultState(),
+ mutations: {
+ addNewNotifications (state, { notifications }) {
+ notifications.forEach(notification => {
+ state.data.push(notification)
+ state.idStore[notification.id] = notification
+ })
+ },
+ clearNotifications (state) {
+ state = emptyNotifications()
+ },
+ updateNotificationsMinMaxId (state, id) {
+ state.maxId = id > state.maxId ? id : state.maxId
+ state.minId = id < state.minId ? id : state.minId
+ },
+ setNotificationsLoading (state, { value }) {
+ state.loading = value
+ },
+ setNotificationsSilence (state, { value }) {
+ state.desktopNotificationSilence = value
+ },
+ markNotificationsAsSeen (state) {
+ state.data.forEach((notification) => {
+ notification.seen = true
+ })
+ },
+ markSingleNotificationAsSeen (state, { id }) {
+ const notification = state.idStore[id]
+ if (notification) notification.seen = true
+ },
+ dismissNotification (state, { id }) {
+ state.data = state.data.filter(n => n.id !== id)
+ delete state.idStore[id]
+ },
+ updateNotification (state, { id, updater }) {
+ const notification = state.idStore[id]
+ notification && updater(notification)
+ }
+ },
+ actions: {
+ addNewNotifications (store, { notifications, older }) {
+ const { commit, dispatch, state, rootState } = store
+ const validNotifications = notifications.filter((notification) => {
+ // If invalid notification, update ids but don't add it to store
+ if (!isValidNotification(notification)) {
+ console.error('Invalid notification:', notification)
+ commit('updateNotificationsMinMaxId', notification.id)
+ return false
+ }
+ return true
+ })
+
+ const statusNotifications = validNotifications.filter(notification => isStatusNotification(notification.type) && notification.status)
+
+ // Synchronous commit to add all the statuses
+ commit('addNewStatuses', { statuses: statusNotifications.map(notification => notification.status) })
+
+ // Update references to statuses in notifications to ones in the store
+ statusNotifications.forEach(notification => {
+ const id = notification.status.id
+ const referenceStatus = rootState.statuses.allStatusesObject[id]
+
+ if (referenceStatus) {
+ notification.status = referenceStatus
+ }
+ })
+
+ validNotifications.forEach(notification => {
+ if (notification.type === 'pleroma:report') {
+ dispatch('addReport', notification.report)
+ }
+
+ if (notification.type === 'pleroma:emoji_reaction') {
+ dispatch('fetchEmojiReactionsBy', notification.status.id)
+ }
+
+ // Only add a new notification if we don't have one for the same action
+ // eslint-disable-next-line no-prototype-builtins
+ if (!state.idStore.hasOwnProperty(notification.id)) {
+ commit('updateNotificationsMinMaxId', notification.id)
+ commit('addNewNotifications', { notifications: [notification] })
+
+ maybeShowNotification(store, notification)
+ } else if (notification.seen) {
+ state.idStore[notification.id].seen = true
+ }
+ })
+ },
+ notificationClicked ({ state, dispatch }, { id }) {
+ const notification = state.idStore[id]
+ const { type, seen } = notification
+
+ if (!seen) {
+ switch (type) {
+ case 'mention':
+ case 'pleroma:report':
+ case 'follow_request':
+ break
+ default:
+ dispatch('markSingleNotificationAsSeen', { id })
+ }
+ }
+ },
+ setNotificationsLoading ({ rootState, commit }, { value }) {
+ commit('setNotificationsLoading', { value })
+ },
+ setNotificationsSilence ({ rootState, commit }, { value }) {
+ commit('setNotificationsSilence', { value })
+ },
+ markNotificationsAsSeen ({ rootState, state, commit }) {
+ commit('markNotificationsAsSeen')
+ apiService.markNotificationsAsSeen({
+ id: state.maxId,
+ credentials: rootState.users.currentUser.credentials
+ }).then(() => {
+ closeAllDesktopNotifications(rootState)
+ })
+ },
+ markSingleNotificationAsSeen ({ rootState, commit }, { id }) {
+ commit('markSingleNotificationAsSeen', { id })
+ apiService.markNotificationsAsSeen({
+ single: true,
+ id,
+ credentials: rootState.users.currentUser.credentials
+ }).then(() => {
+ closeDesktopNotification(rootState, { id })
+ })
+ },
+ dismissNotificationLocal ({ rootState, commit }, { id }) {
+ commit('dismissNotification', { id })
+ },
+ dismissNotification ({ rootState, commit }, { id }) {
+ commit('dismissNotification', { id })
+ rootState.api.backendInteractor.dismissNotification({ id })
+ },
+ updateNotification ({ rootState, commit }, { id, updater }) {
+ commit('updateNotification', { id, updater })
+ }
+ }
+}
+
+export default notifications
diff --git a/src/modules/serverSideStorage.js b/src/modules/serverSideStorage.js
index c933ce8d..c55f54fd 100644
--- a/src/modules/serverSideStorage.js
+++ b/src/modules/serverSideStorage.js
@@ -419,7 +419,6 @@ const serverSideStorage = {
actions: {
pushServerSideStorage ({ state, rootState, commit }, { force = false } = {}) {
const needPush = state.dirty || force
- console.log(needPush)
if (!needPush) return
commit('updateCache', { username: rootState.users.currentUser.fqn })
const params = { pleroma_settings_store: { 'pleroma-fe': state.cache } }
diff --git a/src/modules/statuses.js b/src/modules/statuses.js
index 9b4cd175..462def22 100644
--- a/src/modules/statuses.js
+++ b/src/modules/statuses.js
@@ -12,11 +12,6 @@ import {
isArray,
omitBy
} from 'lodash'
-import {
- isStatusNotification,
- isValidNotification,
- maybeShowNotification
-} from '../services/notification_utils/notification_utils.js'
import apiService from '../services/api/api.service.js'
const emptyTl = (userId = 0) => ({
@@ -36,22 +31,12 @@ const emptyTl = (userId = 0) => ({
flushMarker: 0
})
-const emptyNotifications = () => ({
- desktopNotificationSilence: true,
- maxId: 0,
- minId: Number.POSITIVE_INFINITY,
- data: [],
- idStore: {},
- loading: false
-})
-
export const defaultState = () => ({
allStatuses: [],
scrobblesNextFetch: {},
allStatusesObject: {},
conversationsObject: {},
maxId: 0,
- notifications: emptyNotifications(),
favorites: new Set(),
timelines: {
mentions: emptyTl(),
@@ -154,22 +139,6 @@ const addStatusToGlobalStorage = (state, data) => {
return result
}
-// Remove status from the global storages (arrays and objects maintaining statuses) except timelines
-const removeStatusFromGlobalStorage = (state, status) => {
- remove(state.allStatuses, { id: status.id })
-
- // TODO: Need to remove from allStatusesObject?
-
- // Remove possible notification
- remove(state.notifications.data, ({ action: { id } }) => id === status.id)
-
- // Remove from conversation
- const conversationId = status.statusnet_conversation_id
- if (state.conversationsObject[conversationId]) {
- remove(state.conversationsObject[conversationId], { id: status.id })
- }
-}
-
const addNewStatuses = (state, { statuses, showImmediately = false, timeline, user = {}, noIdUpdate = false, userId, pagination = {} }) => {
// Sanity check
if (!isArray(statuses)) {
@@ -303,20 +272,6 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
favoriteStatus(favorite)
}
},
- deletion: (deletion) => {
- const uri = deletion.uri
- const status = find(allStatuses, { uri })
- if (!status) {
- return
- }
-
- removeStatusFromGlobalStorage(state, status)
-
- if (timeline) {
- remove(timelineObject.statuses, { uri })
- remove(timelineObject.visibleStatuses, { uri })
- }
- },
follow: (follow) => {
// NOOP, it is known status but we don't do anything about it for now
},
@@ -338,52 +293,6 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
}
}
-const updateNotificationsMinMaxId = (state, notification) => {
- state.notifications.maxId = notification.id > state.notifications.maxId
- ? notification.id
- : state.notifications.maxId
- state.notifications.minId = notification.id < state.notifications.minId
- ? notification.id
- : state.notifications.minId
-}
-
-const addNewNotifications = (state, { dispatch, notifications, older, visibleNotificationTypes, rootGetters, newNotificationSideEffects }) => {
- each(notifications, (notification) => {
- // If invalid notification, update ids but don't add it to store
- if (!isValidNotification(notification)) {
- console.error('Invalid notification:', notification)
- updateNotificationsMinMaxId(state, notification)
- return
- }
-
- if (isStatusNotification(notification.type)) {
- notification.action = addStatusToGlobalStorage(state, notification.action).item
- notification.status = notification.status && addStatusToGlobalStorage(state, notification.status).item
- }
-
- if (notification.type === 'pleroma:report') {
- dispatch('addReport', notification.report)
- }
-
- if (notification.type === 'pleroma:emoji_reaction') {
- dispatch('fetchEmojiReactionsBy', notification.status.id)
- }
-
- // Only add a new notification if we don't have one for the same action
- // eslint-disable-next-line no-prototype-builtins
- if (!state.notifications.idStore.hasOwnProperty(notification.id)) {
- updateNotificationsMinMaxId(state, notification)
-
- state.notifications.data.push(notification)
- state.notifications.idStore[notification.id] = notification
-
- newNotificationSideEffects(notification)
- } else if (notification.seen) {
- state.notifications.idStore[notification.id].seen = true
- }
- })
-}
-
const removeStatus = (state, { timeline, userId }) => {
const timelineObject = state.timelines[timeline]
if (userId) {
@@ -396,7 +305,6 @@ const removeStatus = (state, { timeline, userId }) => {
export const mutations = {
addNewStatuses,
- addNewNotifications,
removeStatus,
showNewStatuses (state, { timeline }) {
const oldTimeline = (state.timelines[timeline])
@@ -418,9 +326,6 @@ export const mutations = {
const userId = excludeUserId ? state.timelines[timeline].userId : undefined
state.timelines[timeline] = emptyTl(userId)
},
- clearNotifications (state) {
- state.notifications = emptyNotifications()
- },
setFavorited (state, { status, value }) {
const newStatus = state.allStatusesObject[status.id]
@@ -503,31 +408,6 @@ export const mutations = {
const newStatus = state.allStatusesObject[id]
newStatus.nsfw = nsfw
},
- setNotificationsLoading (state, { value }) {
- state.notifications.loading = value
- },
- setNotificationsSilence (state, { value }) {
- state.notifications.desktopNotificationSilence = value
- },
- markNotificationsAsSeen (state) {
- each(state.notifications.data, (notification) => {
- notification.seen = true
- })
- },
- markSingleNotificationAsSeen (state, { id }) {
- const notification = find(state.notifications.data, n => n.id === id)
- if (notification) notification.seen = true
- },
- dismissNotification (state, { id }) {
- state.notifications.data = state.notifications.data.filter(n => n.id !== id)
- },
- dismissNotifications (state, { finder }) {
- state.notifications.data = state.notifications.data.filter(n => finder)
- },
- updateNotification (state, { id, updater }) {
- const notification = find(state.notifications.data, n => n.id === id)
- notification && updater(notification)
- },
queueFlush (state, { timeline, id }) {
state.timelines[timeline].flushMarker = id
},
@@ -609,23 +489,9 @@ export const mutations = {
const statuses = {
state: defaultState(),
actions: {
- addNewStatuses ({ rootState, commit }, { statuses, showImmediately = false, timeline = false, noIdUpdate = false, userId, pagination }) {
+ addNewStatuses ({ rootState, commit, dispatch, state }, { statuses, showImmediately = false, timeline = false, noIdUpdate = false, userId, pagination }) {
commit('addNewStatuses', { statuses, showImmediately, timeline, noIdUpdate, user: rootState.users.currentUser, userId, pagination })
},
- addNewNotifications (store, { notifications, older }) {
- const { commit, dispatch, rootGetters } = store
-
- const newNotificationSideEffects = (notification) => {
- maybeShowNotification(store, notification)
- }
- commit('addNewNotifications', { dispatch, notifications, older, rootGetters, newNotificationSideEffects })
- },
- setNotificationsLoading ({ rootState, commit }, { value }) {
- commit('setNotificationsLoading', { value })
- },
- setNotificationsSilence ({ rootState, commit }, { value }) {
- commit('setNotificationsSilence', { value })
- },
fetchStatus ({ rootState, dispatch }, id) {
return rootState.api.backendInteractor.fetchStatus({ id })
.then((status) => dispatch('addNewStatuses', { statuses: [status] }))
@@ -721,31 +587,6 @@ const statuses = {
queueFlushAll ({ rootState, commit }) {
commit('queueFlushAll')
},
- markNotificationsAsSeen ({ rootState, commit }) {
- commit('markNotificationsAsSeen')
- apiService.markNotificationsAsSeen({
- id: rootState.statuses.notifications.maxId,
- credentials: rootState.users.currentUser.credentials
- })
- },
- markSingleNotificationAsSeen ({ rootState, commit }, { id }) {
- commit('markSingleNotificationAsSeen', { id })
- apiService.markNotificationsAsSeen({
- single: true,
- id,
- credentials: rootState.users.currentUser.credentials
- })
- },
- dismissNotificationLocal ({ rootState, commit }, { id }) {
- commit('dismissNotification', { id })
- },
- dismissNotification ({ rootState, commit }, { id }) {
- commit('dismissNotification', { id })
- rootState.api.backendInteractor.dismissNotification({ id })
- },
- updateNotification ({ rootState, commit }, { id, updater }) {
- commit('updateNotification', { id, updater })
- },
fetchFavsAndRepeats ({ rootState, commit }, id) {
Promise.all([
rootState.api.backendInteractor.fetchFavoritedByUsers({ id }),
diff --git a/src/modules/users.js b/src/modules/users.js
index 50b4cb84..b8f49f15 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -2,7 +2,7 @@ import backendInteractorService from '../services/backend_interactor_service/bac
import { windowWidth, windowHeight } from '../services/window_utils/window_utils'
import oauthApi from '../services/new_api/oauth.js'
import { compact, map, each, mergeWith, last, concat, uniq, isArray } from 'lodash'
-import { registerPushNotifications, unregisterPushNotifications } from '../services/push/push.js'
+import { registerPushNotifications, unregisterPushNotifications } from '../services/sw/sw.js'
// TODO: Unify with mergeOrAdd in statuses.js
export const mergeOrAdd = (arr, obj, item) => {
@@ -250,6 +250,7 @@ export const mutations = {
signUpPending (state) {
state.signUpPending = true
state.signUpErrors = []
+ state.signUpNotice = {}
},
signUpSuccess (state) {
state.signUpPending = false
@@ -257,6 +258,12 @@ export const mutations = {
signUpFailure (state, errors) {
state.signUpPending = false
state.signUpErrors = errors
+ state.signUpNotice = {}
+ },
+ signUpNotice (state, notice) {
+ state.signUpPending = false
+ state.signUpErrors = []
+ state.signUpNotice = notice
}
}
@@ -287,6 +294,7 @@ export const defaultState = {
usersByNameObject: {},
signUpPending: false,
signUpErrors: [],
+ signUpNotice: {},
relationships: {}
}
@@ -498,7 +506,7 @@ const users = {
store.commit('addNewUsers', users)
store.commit('addNewUsers', targetUsers)
- const notificationsObject = store.rootState.statuses.notifications.idStore
+ const notificationsObject = store.rootState.notifications.idStore
const relevantNotifications = Object.entries(notificationsObject)
.filter(([k, val]) => notificationIds.includes(k))
.map(([k, val]) => val)
@@ -524,9 +532,16 @@ const users = {
const data = await rootState.api.backendInteractor.register(
{ params: { ...userInfo } }
)
- store.commit('signUpSuccess')
- store.commit('setToken', data.access_token)
- store.dispatch('loginUser', data.access_token)
+
+ if (data.access_token) {
+ store.commit('signUpSuccess')
+ store.commit('setToken', data.access_token)
+ store.dispatch('loginUser', data.access_token)
+ return 'ok'
+ } else { // Request succeeded, but user cannot login yet.
+ store.commit('signUpNotice', data)
+ return 'request_sent'
+ }
} catch (e) {
const errors = e.message
store.commit('signUpFailure', errors)
@@ -667,7 +682,7 @@ const users = {
resolve()
})
.catch((error) => {
- console.log(error)
+ console.error(error)
commit('endLogin')
reject(new Error('Failed to connect to server, try again'))
})
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index f45e3958..a47c638c 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -108,12 +108,22 @@ const PLEROMA_POST_ANNOUNCEMENT_URL = '/api/v1/pleroma/admin/announcements'
const PLEROMA_EDIT_ANNOUNCEMENT_URL = id => `/api/v1/pleroma/admin/announcements/${id}`
const PLEROMA_DELETE_ANNOUNCEMENT_URL = id => `/api/v1/pleroma/admin/announcements/${id}`
const PLEROMA_SCROBBLES_URL = id => `/api/v1/pleroma/accounts/${id}/scrobbles`
+const PLEROMA_USER_FAVORITES_TIMELINE_URL = id => `/api/v1/pleroma/accounts/${id}/favourites`
const PLEROMA_ADMIN_CONFIG_URL = '/api/pleroma/admin/config'
const PLEROMA_ADMIN_DESCRIPTIONS_URL = '/api/pleroma/admin/config/descriptions'
const PLEROMA_ADMIN_FRONTENDS_URL = '/api/pleroma/admin/frontends'
const PLEROMA_ADMIN_FRONTENDS_INSTALL_URL = '/api/pleroma/admin/frontends/install'
+const PLEROMA_EMOJI_RELOAD_URL = '/api/pleroma/admin/reload_emoji'
+const PLEROMA_EMOJI_IMPORT_FS_URL = '/api/pleroma/emoji/packs/import'
+const PLEROMA_EMOJI_PACKS_URL = (page, pageSize) => `/api/v1/pleroma/emoji/packs?page=${page}&page_size=${pageSize}`
+const PLEROMA_EMOJI_PACK_URL = (name) => `/api/v1/pleroma/emoji/pack?name=${name}`
+const PLEROMA_EMOJI_PACKS_DL_REMOTE_URL = '/api/v1/pleroma/emoji/packs/download'
+const PLEROMA_EMOJI_PACKS_LS_REMOTE_URL =
+ (url, page, pageSize) => `/api/v1/pleroma/emoji/packs/remote?url=${url}&page=${page}&page_size=${pageSize}`
+const PLEROMA_EMOJI_UPDATE_FILE_URL = (name) => `/api/v1/pleroma/emoji/packs/files?name=${name}`
+
const oldfetch = window.fetch
const fetch = (url, options) => {
@@ -671,6 +681,7 @@ const fetchTimeline = ({
timeline,
credentials,
since = false,
+ minId = false,
until = false,
userId = false,
listId = false,
@@ -689,6 +700,7 @@ const fetchTimeline = ({
media: MASTODON_USER_TIMELINE_URL,
list: MASTODON_LIST_TIMELINE_URL,
favorites: MASTODON_USER_FAVORITES_TIMELINE_URL,
+ publicFavorites: PLEROMA_USER_FAVORITES_TIMELINE_URL,
tag: MASTODON_TAG_TIMELINE_URL,
bookmarks: MASTODON_BOOKMARK_TIMELINE_URL
}
@@ -697,6 +709,10 @@ const fetchTimeline = ({
let url = timelineUrls[timeline]
+ if (timeline === 'favorites' && userId) {
+ url = timelineUrls.publicFavorites(userId)
+ }
+
if (timeline === 'user' || timeline === 'media') {
url = url(userId)
}
@@ -705,6 +721,9 @@ const fetchTimeline = ({
url = url(listId)
}
+ if (minId) {
+ params.push(['min_id', minId])
+ }
if (since) {
params.push(['since_id', since])
}
@@ -1783,6 +1802,90 @@ const fetchScrobbles = ({ accountId, limit = 1 }) => {
})
}
+const deleteEmojiPack = ({ name }) => {
+ return fetch(PLEROMA_EMOJI_PACK_URL(name), { method: 'DELETE' })
+}
+
+const reloadEmoji = () => {
+ return fetch(PLEROMA_EMOJI_RELOAD_URL, { method: 'POST' })
+}
+
+const importEmojiFromFS = () => {
+ return fetch(PLEROMA_EMOJI_IMPORT_FS_URL)
+}
+
+const createEmojiPack = ({ name }) => {
+ return fetch(PLEROMA_EMOJI_PACK_URL(name), { method: 'POST' })
+}
+
+const listEmojiPacks = ({ page, pageSize }) => {
+ return fetch(PLEROMA_EMOJI_PACKS_URL(page, pageSize))
+}
+
+const listRemoteEmojiPacks = ({ instance, page, pageSize }) => {
+ if (!instance.startsWith('http')) {
+ instance = 'https://' + instance
+ }
+
+ return fetch(
+ PLEROMA_EMOJI_PACKS_LS_REMOTE_URL(instance, page, pageSize),
+ {
+ headers: { 'Content-Type': 'application/json' }
+ }
+ )
+}
+
+const downloadRemoteEmojiPack = ({ instance, packName, as }) => {
+ return fetch(
+ PLEROMA_EMOJI_PACKS_DL_REMOTE_URL,
+ {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ url: instance, name: packName, as
+ })
+ }
+ )
+}
+
+const saveEmojiPackMetadata = ({ name, newData }) => {
+ return fetch(
+ PLEROMA_EMOJI_PACK_URL(name),
+ {
+ method: 'PATCH',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ metadata: newData })
+ }
+ )
+}
+
+const addNewEmojiFile = ({ packName, file, shortcode, filename }) => {
+ const data = new FormData()
+ if (filename.trim() !== '') { data.set('filename', filename) }
+ if (shortcode.trim() !== '') { data.set('shortcode', shortcode) }
+ data.set('file', file)
+
+ return fetch(
+ PLEROMA_EMOJI_UPDATE_FILE_URL(packName),
+ { method: 'POST', body: data }
+ )
+}
+
+const updateEmojiFile = ({ packName, shortcode, newShortcode, newFilename, force }) => {
+ return fetch(
+ PLEROMA_EMOJI_UPDATE_FILE_URL(packName),
+ {
+ method: 'PATCH',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ shortcode, new_shortcode: newShortcode, new_filename: newFilename, force })
+ }
+ )
+}
+
+const deleteEmojiFile = ({ packName, shortcode }) => {
+ return fetch(`${PLEROMA_EMOJI_UPDATE_FILE_URL(packName)}&shortcode=${shortcode}`, { method: 'DELETE' })
+}
+
const apiService = {
verifyCredentials,
fetchTimeline,
@@ -1902,7 +2005,18 @@ const apiService = {
fetchInstanceConfigDescriptions,
fetchAvailableFrontends,
pushInstanceDBConfig,
- installFrontend
+ installFrontend,
+ importEmojiFromFS,
+ reloadEmoji,
+ listEmojiPacks,
+ createEmojiPack,
+ deleteEmojiPack,
+ saveEmojiPackMetadata,
+ addNewEmojiFile,
+ updateEmojiFile,
+ deleteEmojiFile,
+ listRemoteEmojiPacks,
+ downloadRemoteEmojiPack
}
export default apiService
diff --git a/src/services/desktop_notification_utils/desktop_notification_utils.js b/src/services/desktop_notification_utils/desktop_notification_utils.js
index b84a1f75..80b8c6e0 100644
--- a/src/services/desktop_notification_utils/desktop_notification_utils.js
+++ b/src/services/desktop_notification_utils/desktop_notification_utils.js
@@ -1,9 +1,38 @@
+import {
+ showDesktopNotification as swDesktopNotification,
+ closeDesktopNotification as swCloseDesktopNotification,
+ isSWSupported
+} from '../sw/sw.js'
+const state = { failCreateNotif: false }
+
export const showDesktopNotification = (rootState, desktopNotificationOpts) => {
if (!('Notification' in window && window.Notification.permission === 'granted')) return
- if (rootState.statuses.notifications.desktopNotificationSilence) { return }
+ if (rootState.notifications.desktopNotificationSilence) { return }
+
+ if (isSWSupported()) {
+ swDesktopNotification(desktopNotificationOpts)
+ } else if (!state.failCreateNotif) {
+ try {
+ const desktopNotification = new window.Notification(desktopNotificationOpts.title, desktopNotificationOpts)
+ setTimeout(desktopNotification.close.bind(desktopNotification), 5000)
+ } catch {
+ state.failCreateNotif = true
+ }
+ }
+}
+
+export const closeDesktopNotification = (rootState, { id }) => {
+ if (!('Notification' in window && window.Notification.permission === 'granted')) return
+
+ if (isSWSupported()) {
+ swCloseDesktopNotification({ id })
+ }
+}
+
+export const closeAllDesktopNotifications = (rootState) => {
+ if (!('Notification' in window && window.Notification.permission === 'granted')) return
- const desktopNotification = new window.Notification(desktopNotificationOpts.title, desktopNotificationOpts)
- // Chrome is known for not closing notifications automatically
- // according to MDN, anyway.
- setTimeout(desktopNotification.close.bind(desktopNotification), 5000)
+ if (isSWSupported()) {
+ swCloseDesktopNotification({})
+ }
}
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index 610ba1ab..21e67b67 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -107,6 +107,7 @@ export const parseUser = (data) => {
output.allow_following_move = data.pleroma.allow_following_move
+ output.hide_favorites = data.pleroma.hide_favorites
output.hide_follows = data.pleroma.hide_follows
output.hide_followers = data.pleroma.hide_followers
output.hide_follows_count = data.pleroma.hide_follows_count
@@ -165,6 +166,7 @@ export const parseUser = (data) => {
output.show_role = data.source.pleroma.show_role
output.discoverable = data.source.pleroma.discoverable
output.show_birthday = data.pleroma.show_birthday
+ output.actor_type = data.source.pleroma.actor_type
}
}
@@ -439,7 +441,6 @@ export const parseNotification = (data) => {
output.type = mastoDict[data.type] || data.type
output.seen = data.pleroma.is_seen
output.status = isStatusNotification(output.type) ? parseStatus(data.status) : null
- output.action = output.status // TODO: Refactor, this is unneeded
output.target = output.type !== 'move'
? null
: parseUser(data.target)
diff --git a/src/services/favicon_service/favicon_service.js b/src/services/favicon_service/favicon_service.js
index 7e19629d..df603bb4 100644
--- a/src/services/favicon_service/favicon_service.js
+++ b/src/services/favicon_service/favicon_service.js
@@ -55,10 +55,13 @@ const createFaviconService = () => {
})
}
+ const getOriginalFavicons = () => [...favicons]
+
return {
initFaviconService,
clearFaviconBadge,
- drawFaviconBadge
+ drawFaviconBadge,
+ getOriginalFavicons
}
}
diff --git a/src/services/notification_utils/notification_utils.js b/src/services/notification_utils/notification_utils.js
index 815e792d..7b705e65 100644
--- a/src/services/notification_utils/notification_utils.js
+++ b/src/services/notification_utils/notification_utils.js
@@ -1,28 +1,36 @@
-import { filter, sortBy, includes } from 'lodash'
import { muteWordHits } from '../status_parser/status_parser.js'
import { showDesktopNotification } from '../desktop_notification_utils/desktop_notification_utils.js'
-export const notificationsFromStore = store => store.state.statuses.notifications.data
+import FaviconService from 'src/services/favicon_service/favicon_service.js'
+
+export const ACTIONABLE_NOTIFICATION_TYPES = new Set(['mention', 'pleroma:report', 'follow_request'])
+
+let cachedBadgeUrl = null
+
+export const notificationsFromStore = store => store.state.notifications.data
export const visibleTypes = store => {
- const rootState = store.rootState || store.state
+ // When called from within a module we need rootGetters to access wider scope
+ // however when called from a component (i.e. this.$store) we already have wider scope
+ const rootGetters = store.rootGetters || store.getters
+ const { notificationVisibility } = rootGetters.mergedConfig
return ([
- rootState.config.notificationVisibility.likes && 'like',
- rootState.config.notificationVisibility.mentions && 'mention',
- rootState.config.notificationVisibility.repeats && 'repeat',
- rootState.config.notificationVisibility.follows && 'follow',
- rootState.config.notificationVisibility.followRequest && 'follow_request',
- rootState.config.notificationVisibility.moves && 'move',
- rootState.config.notificationVisibility.emojiReactions && 'pleroma:emoji_reaction',
- rootState.config.notificationVisibility.reports && 'pleroma:report',
- rootState.config.notificationVisibility.polls && 'poll'
+ notificationVisibility.likes && 'like',
+ notificationVisibility.mentions && 'mention',
+ notificationVisibility.repeats && 'repeat',
+ notificationVisibility.follows && 'follow',
+ notificationVisibility.followRequest && 'follow_request',
+ notificationVisibility.moves && 'move',
+ notificationVisibility.emojiReactions && 'pleroma:emoji_reaction',
+ notificationVisibility.reports && 'pleroma:report',
+ notificationVisibility.polls && 'poll'
].filter(_ => _))
}
-const statusNotifications = ['like', 'mention', 'repeat', 'pleroma:emoji_reaction', 'poll']
+const statusNotifications = new Set(['like', 'mention', 'repeat', 'pleroma:emoji_reaction', 'poll'])
-export const isStatusNotification = (type) => includes(statusNotifications, type)
+export const isStatusNotification = (type) => statusNotifications.has(type)
export const isValidNotification = (notification) => {
if (isStatusNotification(notification.type) && !notification.status) {
@@ -49,35 +57,57 @@ const sortById = (a, b) => {
const isMutedNotification = (store, notification) => {
if (!notification.status) return
- return notification.status.muted || muteWordHits(notification.status, store.rootGetters.mergedConfig.muteWords).length > 0
+ const rootGetters = store.rootGetters || store.getters
+ return notification.status.muted || muteWordHits(notification.status, rootGetters.mergedConfig.muteWords).length > 0
}
export const maybeShowNotification = (store, notification) => {
const rootState = store.rootState || store.state
+ const rootGetters = store.rootGetters || store.getters
if (notification.seen) return
if (!visibleTypes(store).includes(notification.type)) return
if (notification.type === 'mention' && isMutedNotification(store, notification)) return
- const notificationObject = prepareNotificationObject(notification, store.rootGetters.i18n)
+ const notificationObject = prepareNotificationObject(notification, rootGetters.i18n)
showDesktopNotification(rootState, notificationObject)
}
export const filteredNotificationsFromStore = (store, types) => {
// map is just to clone the array since sort mutates it and it causes some issues
- let sortedNotifications = notificationsFromStore(store).map(_ => _).sort(sortById)
- sortedNotifications = sortBy(sortedNotifications, 'seen')
+ const sortedNotifications = notificationsFromStore(store).map(_ => _).sort(sortById)
+ // TODO implement sorting elsewhere and make it optional
return sortedNotifications.filter(
(notification) => (types || visibleTypes(store)).includes(notification.type)
)
}
-export const unseenNotificationsFromStore = store =>
- filter(filteredNotificationsFromStore(store), ({ seen }) => !seen)
+export const unseenNotificationsFromStore = store => {
+ const rootGetters = store.rootGetters || store.getters
+ const ignoreInactionableSeen = rootGetters.mergedConfig.ignoreInactionableSeen
+
+ return filteredNotificationsFromStore(store).filter(({ seen, type }) => {
+ if (!ignoreInactionableSeen) return !seen
+ if (seen) return false
+ return ACTIONABLE_NOTIFICATION_TYPES.has(type)
+ })
+}
export const prepareNotificationObject = (notification, i18n) => {
+ if (cachedBadgeUrl === null) {
+ const favicons = FaviconService.getOriginalFavicons()
+ const favicon = favicons[favicons.length - 1]
+ if (!favicon) {
+ cachedBadgeUrl = 'about:blank'
+ } else {
+ cachedBadgeUrl = favicon.favimg.src
+ }
+ }
+
const notifObj = {
- tag: notification.id
+ tag: notification.id,
+ type: notification.type,
+ badge: cachedBadgeUrl
}
const status = notification.status
const title = notification.from_profile.name
@@ -126,15 +156,16 @@ export const prepareNotificationObject = (notification, i18n) => {
}
export const countExtraNotifications = (store) => {
- const mergedConfig = store.getters.mergedConfig
+ const rootGetters = store.rootGetters || store.getters
+ const mergedConfig = rootGetters.mergedConfig
if (!mergedConfig.showExtraNotifications) {
return 0
}
return [
- mergedConfig.showChatsInExtraNotifications ? store.getters.unreadChatCount : 0,
- mergedConfig.showAnnouncementsInExtraNotifications ? store.getters.unreadAnnouncementCount : 0,
- mergedConfig.showFollowRequestsInExtraNotifications ? store.getters.followRequestCount : 0
+ mergedConfig.showChatsInExtraNotifications ? rootGetters.unreadChatCount : 0,
+ mergedConfig.showAnnouncementsInExtraNotifications ? rootGetters.unreadAnnouncementCount : 0,
+ mergedConfig.showFollowRequestsInExtraNotifications ? rootGetters.followRequestCount : 0
].reduce((a, c) => a + c, 0)
}
diff --git a/src/services/notifications_fetcher/notifications_fetcher.service.js b/src/services/notifications_fetcher/notifications_fetcher.service.js
index 6c247210..c91a86c8 100644
--- a/src/services/notifications_fetcher/notifications_fetcher.service.js
+++ b/src/services/notifications_fetcher/notifications_fetcher.service.js
@@ -21,7 +21,7 @@ const fetchAndUpdate = ({ store, credentials, older = false, since }) => {
const args = { credentials }
const { getters } = store
const rootState = store.rootState || store.state
- const timelineData = rootState.statuses.notifications
+ const timelineData = rootState.notifications
const hideMutedPosts = getters.mergedConfig.hideMutedPosts
args.includeTypes = mastoApiNotificationTypes
@@ -49,10 +49,14 @@ const fetchAndUpdate = ({ store, credentials, older = false, since }) => {
// The normal maxId-check does not tell if older notifications have changed
const notifications = timelineData.data
const readNotifsIds = notifications.filter(n => n.seen).map(n => n.id)
- const numUnseenNotifs = notifications.length - readNotifsIds.length
- if (numUnseenNotifs > 0 && readNotifsIds.length > 0) {
- args.since = Math.max(...readNotifsIds)
- fetchNotifications({ store, args, older })
+ const unreadNotifsIds = notifications.filter(n => !n.seen).map(n => n.id)
+ if (readNotifsIds.length > 0 && readNotifsIds.length > 0) {
+ const minId = Math.min(...unreadNotifsIds) // Oldest known unread notification
+ if (minId !== Infinity) {
+ args.since = false // Don't use since_id since it sorta conflicts with min_id
+ args.minId = minId - 1 // go beyond
+ fetchNotifications({ store, args, older })
+ }
}
return result
diff --git a/src/services/push/push.js b/src/services/sw/sw.js
index 1787ac36..554cc7b8 100644
--- a/src/services/push/push.js
+++ b/src/services/sw/sw.js
@@ -10,8 +10,12 @@ function urlBase64ToUint8Array (base64String) {
return Uint8Array.from([...rawData].map((char) => char.charCodeAt(0)))
}
+export function isSWSupported () {
+ return 'serviceWorker' in navigator
+}
+
function isPushSupported () {
- return 'serviceWorker' in navigator && 'PushManager' in window
+ return 'PushManager' in window
}
function getOrCreateServiceWorker () {
@@ -24,7 +28,7 @@ function subscribePush (registration, isEnabled, vapidPublicKey) {
if (!vapidPublicKey) return Promise.reject(new Error('VAPID public key is not found'))
const subscribeOptions = {
- userVisibleOnly: true,
+ userVisibleOnly: false,
applicationServerKey: urlBase64ToUint8Array(vapidPublicKey)
}
return registration.pushManager.subscribe(subscribeOptions)
@@ -39,7 +43,7 @@ function unsubscribePush (registration) {
}
function deleteSubscriptionFromBackEnd (token) {
- return window.fetch('/api/v1/push/subscription/', {
+ return fetch('/api/v1/push/subscription/', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
@@ -78,6 +82,44 @@ function sendSubscriptionToBackEnd (subscription, token, notificationVisibility)
return responseData
})
}
+export async function initServiceWorker (store) {
+ if (!isSWSupported()) return
+ await getOrCreateServiceWorker()
+ navigator.serviceWorker.addEventListener('message', (event) => {
+ const { dispatch } = store
+ const { type, ...rest } = event.data
+
+ switch (type) {
+ case 'notificationClicked':
+ dispatch('notificationClicked', { id: rest.id })
+ }
+ })
+}
+
+export async function showDesktopNotification (content) {
+ if (!isSWSupported) return
+ const { active: sw } = await window.navigator.serviceWorker.getRegistration()
+ if (!sw) return console.error('No serviceworker found!')
+ sw.postMessage({ type: 'desktopNotification', content })
+}
+
+export async function closeDesktopNotification ({ id }) {
+ if (!isSWSupported) return
+ const { active: sw } = await window.navigator.serviceWorker.getRegistration()
+ if (!sw) return console.error('No serviceworker found!')
+ if (id >= 0) {
+ sw.postMessage({ type: 'desktopNotificationClose', content: { id } })
+ } else {
+ sw.postMessage({ type: 'desktopNotificationClose', content: { all: true } })
+ }
+}
+
+export async function updateFocus () {
+ if (!isSWSupported) return
+ const { active: sw } = await window.navigator.serviceWorker.getRegistration()
+ if (!sw) return console.error('No serviceworker found!')
+ sw.postMessage({ type: 'updateFocus' })
+}
export function registerPushNotifications (isEnabled, vapidPublicKey, token, notificationVisibility) {
if (isPushSupported()) {
@@ -98,13 +140,8 @@ export function unregisterPushNotifications (token) {
})
.then(([registration, unsubResult]) => {
if (!unsubResult) {
- console.warn('Push subscription cancellation wasn\'t successful, killing SW anyway...')
+ console.warn('Push subscription cancellation wasn\'t successful')
}
- return registration.unregister().then((result) => {
- if (!result) {
- console.warn('Failed to kill SW')
- }
- })
})
]).catch((e) => console.warn(`Failed to disable Web Push Notifications: ${e.message}`))
}
diff --git a/src/sw.js b/src/sw.js
index 70fed44b..4ee7b9ff 100644
--- a/src/sw.js
+++ b/src/sw.js
@@ -13,9 +13,10 @@ const i18n = createI18n({
messages
})
-function isEnabled () {
- return localForage.getItem('vuex-lz')
- .then(data => data.config.webPushNotifications)
+const state = {
+ lastFocused: null,
+ notificationIds: new Set(),
+ allowedNotificationTypes: null
}
function getWindowClients () {
@@ -23,17 +24,46 @@ function getWindowClients () {
.then((clientList) => clientList.filter(({ type }) => type === 'window'))
}
-const setLocale = async () => {
- const state = await localForage.getItem('vuex-lz')
- const locale = state.config.interfaceLanguage || 'en'
+const setSettings = async () => {
+ const vuexState = await localForage.getItem('vuex-lz')
+ const locale = vuexState.config.interfaceLanguage || 'en'
i18n.locale = locale
+ const notificationsNativeArray = Object.entries(vuexState.config.notificationNative)
+ state.webPushAlwaysShowNotifications = vuexState.config.webPushAlwaysShowNotifications
+
+ state.allowedNotificationTypes = new Set(
+ notificationsNativeArray
+ .filter(([k, v]) => v)
+ .map(([k]) => {
+ switch (k) {
+ case 'mentions':
+ return 'mention'
+ case 'likes':
+ return 'like'
+ case 'repeats':
+ return 'repeat'
+ case 'emojiReactions':
+ return 'pleroma:emoji_reaction'
+ case 'reports':
+ return 'pleroma:report'
+ case 'followRequest':
+ return 'follow_request'
+ case 'follows':
+ return 'follow'
+ case 'polls':
+ return 'poll'
+ default:
+ return k
+ }
+ })
+ )
}
-const maybeShowNotification = async (event) => {
- const enabled = await isEnabled()
+const showPushNotification = async (event) => {
const activeClients = await getWindowClients()
- await setLocale()
- if (enabled && (activeClients.length === 0)) {
+ await setSettings()
+ // Only show push notifications if all tabs/windows are closed
+ if (state.webPushAlwaysShowNotifications || activeClients.length === 0) {
const data = event.data.json()
const url = `${self.registration.scope}api/v1/notifications/${data.notification_id}`
@@ -43,13 +73,48 @@ const maybeShowNotification = async (event) => {
const res = prepareNotificationObject(parsedNotification, i18n)
- self.registration.showNotification(res.title, res)
+ if (state.webPushAlwaysShowNotifications || state.allowedNotificationTypes.has(parsedNotification.type)) {
+ return self.registration.showNotification(res.title, res)
+ }
}
+ return Promise.resolve()
}
self.addEventListener('push', async (event) => {
if (event.data) {
- event.waitUntil(maybeShowNotification(event))
+ // Supposedly, we HAVE to return a promise inside waitUntil otherwise it will
+ // show (extra) notification that website is updated in background
+ event.waitUntil(showPushNotification(event))
+ }
+})
+
+self.addEventListener('message', async (event) => {
+ await setSettings()
+ const { type, content } = event.data
+
+ if (type === 'desktopNotification') {
+ const { title, ...rest } = content
+ const { tag, type } = rest
+ if (state.notificationIds.has(tag)) return
+ state.notificationIds.add(tag)
+ setTimeout(() => state.notificationIds.delete(tag), 10000)
+ if (state.allowedNotificationTypes.has(type)) {
+ self.registration.showNotification(title, rest)
+ }
+ }
+
+ if (type === 'desktopNotificationClose') {
+ const { id, all } = content
+ const search = all ? null : { tag: id }
+ const notifications = await self.registration.getNotifications(search)
+ notifications.forEach(n => n.close())
+ }
+
+ if (type === 'updateFocus') {
+ state.lastFocused = event.source.id
+
+ const notifications = await self.registration.getNotifications()
+ notifications.forEach(n => n.close())
}
})
@@ -59,7 +124,14 @@ self.addEventListener('notificationclick', (event) => {
event.waitUntil(getWindowClients().then((list) => {
for (let i = 0; i < list.length; i++) {
const client = list[i]
- if (client.url === '/' && 'focus' in client) { return client.focus() }
+ client.postMessage({ type: 'notificationClicked', id: event.notification.tag })
+ }
+
+ for (let i = 0; i < list.length; i++) {
+ const client = list[i]
+ if (state.lastFocused === null || client.id === state.lastFocused) {
+ if ('focus' in client) return client.focus()
+ }
}
if (clients.openWindow) return clients.openWindow('/')