Merge branch 'develop' into niko

このコミットが含まれているのは:
Nikotile 2024-01-03 08:36:11 +09:00
コミット 43949d885a
署名者: niko
GPGキーID: 5AE3CC9D7977786A
106個のファイルの変更2992行の追加723行の削除

1
.gitattributes vendored ノーマルファイル
ファイルの表示

@ -0,0 +1 @@
/build/webpack.prod.conf.js export-subst

ファイルの表示

@ -3,6 +3,37 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## 2.6.1
### Fixed
- fix admin dashboard not having any feedback on frontend installation
- Fix frontend admin tab crashing when no primary frontend is set
- Add aria attributes to react and extra buttons
## 2.6.0
### Added
- add the initial i18n translation file for Taiwanese (Hokkien), and modify some related files.
- Implemented a very basic instance administration screen
- Implement quoting
### Fixed
- Keep aspect ratio of custom emoji reaction in notification
- Fix openSettingsModalTab so that it correctly opens Settings modal instead of Admin modal
- Add alt text to emoji picker buttons
- Use export-subst gitattribute to allow tarball builds
- fix reports now showing reason/content
- Fix HTML attribute parsing, discard attributes not strating with a letter
- Make MentionsLine aware of line breaking by non-br elements
- Fix a bug where mentioning a user twice will not fill the mention into the textarea
- Fix parsing non-ascii tags
- Fix OAuth2 token lingering after revocation
- fix regex issue in HTML parser/renderer
- don't display quoted status twice
- fix typo in code that prevented cards from showing at all
- Fix react button not working if reaction accounts are not loaded
- Fix react button misalignment on safari ios
- Fix pinned statuses gone when reloading user timeline
- Fix scrolling emoji selector in modal in safari ios
## 2.5.1
### Fixed
- Checkboxes in settings can now work with screenreaders

ファイルの表示

@ -11,9 +11,16 @@ var env = process.env.NODE_ENV === 'testing'
? require('../config/test.env')
: config.build.env
let commitHash = require('child_process')
.execSync('git rev-parse --short HEAD')
.toString();
let commitHash = (() => {
const subst = "$Format:%h$";
if(!subst.match(/Format:/)) {
return subst;
} else {
return require('child_process')
.execSync('git rev-parse --short HEAD')
.toString();
}
})();
var webpackConfig = merge(baseWebpackConfig, {
mode: 'production',

1
changelog.d/add-apng.add ノーマルファイル
ファイルの表示

@ -0,0 +1 @@
Make Pleroma FE to also view apng (Animated PNG) attachment.

ファイルの表示

@ -1 +0,0 @@
add the initial i18n translation file for Taiwanese (Hokkien), and modify some related files.

ファイルの表示

@ -1 +0,0 @@
Implemented a very basic instance administration screen

ファイルの表示

ファイルの表示

@ -0,0 +1 @@
Create a link to the URL of the scrobble when it's present

ファイルの表示

@ -1 +0,0 @@
Keep aspect ratio of custom emoji reaction in notification

1
changelog.d/double-notifications.fix ノーマルファイル
ファイルの表示

@ -0,0 +1 @@
Fix native notifications appearing as many times as there are open tabs. Clicking on notification will focus last focused tab.

ファイルの表示

@ -1 +0,0 @@
Fix openSettingsModalTab so that it correctly opens Settings modal instead of Admin modal

1
changelog.d/extra-notifications.add ノーマルファイル
ファイルの表示

@ -0,0 +1 @@
Support showing extra notifications in the notifications column

1
changelog.d/focus-clear.add ノーマルファイル
ファイルの表示

@ -0,0 +1 @@
Focusing into a tab clears all current desktop notifications

ファイルの表示

@ -1 +0,0 @@
Fix a bug where mentioning a user twice will not fill the mention into the textarea

ファイルの表示

@ -1 +0,0 @@
Make MentionsLine aware of line breaking by non-br elements

1
changelog.d/mobile-chrome-notifs.fix ノーマルファイル
ファイルの表示

@ -0,0 +1 @@
Fixed error that appeared on mobile Chrome(ium) (and derivatives) when native notifications are allowed

ファイルの表示

@ -0,0 +1 @@
Added option to not mark all notifications when closing notifications drawer on mobile, this creates a new button to mark all as seen.

ファイルの表示

@ -0,0 +1 @@
Fixed being unable to set notification visibility for reports and follow requests

1
changelog.d/native-filtering.add ノーマルファイル
ファイルの表示

@ -0,0 +1 @@
Added option to toggle what notification types appear in native notifications, by default less important ones (likes, repeats, etc) will no longer show up in native notifications.

1
changelog.d/native-notifications.add ノーマルファイル
ファイルの表示

@ -0,0 +1 @@
Native notifications now also have "badge" property that matches instance's favicon (visible in Android Chromium at least)

1
changelog.d/noninteractive-ignore-read.add ノーマルファイル
ファイルの表示

@ -0,0 +1 @@
Added option to treat non-interactive notifications (likes, repeats et all) as seen for visual purposes (no read mark, ignored in counters, still can show in native notifications)

1
changelog.d/notification-read.add ノーマルファイル
ファイルの表示

@ -0,0 +1 @@
Interacting (opening reply box etc) or simply clicking on non-interactive notifications now marks them as read. Clicking on native notifications for non-interactive ones also marks them as seen.

1
changelog.d/notifications-sorting.change ノーマルファイル
ファイルの表示

@ -0,0 +1 @@
Notifications are no longer sorted by "seen" status since interacting with them can change their read status and makes UI jumpy. Old behavior can be restored in settings.

ファイルの表示

@ -1 +0,0 @@
fix regex issue in HTML parser/renderer

ファイルの表示

@ -1 +0,0 @@
Fix react button misalignment on safari ios

ファイルの表示

@ -1 +0,0 @@
Fix react button not working if reaction accounts are not loaded

ファイルの表示

@ -1 +0,0 @@
Fix scrolling emoji selector in modal in safari ios

1
changelog.d/serviceworkers.change ノーマルファイル
ファイルの表示

@ -0,0 +1 @@
Notifications are now shown through a serviceworker (since mobile chrome does not allow them otherwise), it's always enabled, even if previously we only enabled it for WebPush notifications only. If you don't like websites "running" while closed, check how to disable them in your browser. Old way to show notifications will be used as a fallback but might not have all the new features.

1
changelog.d/show-recent-scrobble.skip ノーマルファイル
ファイルの表示

@ -0,0 +1 @@
Shows the most recent scrobble under each post when available

1
changelog.d/unreads-sync.fix ノーマルファイル
ファイルの表示

@ -0,0 +1 @@
unread notifications should now properly catch up (eventually) in polling mode

1
changelog.d/video-poster.fix ノーマルファイル
ファイルの表示

@ -0,0 +1 @@
Video posters on Safari

1
changelog.d/web-push-always.add ノーマルファイル
ファイルの表示

@ -0,0 +1 @@
Added option to always "show" notifications when using web push for better compatibility with some browsers (chrome, edge, safari)

ファイルの表示

@ -3,8 +3,8 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1,user-scalable=no">
<!--server-generated-meta-->
<link rel="icon" type="image/png" href="/favicon.png">
<!--server-generated-meta-->
</head>
<body class="hidden">
<noscript>To use Pleroma, please enable JavaScript.</noscript>

ファイルの表示

@ -1,6 +1,6 @@
{
"name": "pleroma_fe",
"version": "2.5.0",
"version": "2.6.1",
"description": "Pleroma frontend, the default frontend of Pleroma social network server",
"author": "Pleroma contributors <https://git.pleroma.social/pleroma/pleroma-fe/-/blob/develop/CONTRIBUTORS.md>",
"private": false,
@ -25,7 +25,7 @@
"@kazvmoe-infra/pinch-zoom-element": "1.2.0",
"@kazvmoe-infra/unicode-emoji-json": "0.4.0",
"@ruffle-rs/ruffle": "0.1.0-nightly.2022.7.12",
"@vuelidate/core": "2.0.2",
"@vuelidate/core": "2.0.3",
"@vuelidate/validators": "2.0.0",
"body-scroll-lock": "3.1.5",
"chromatism": "3.0.0",
@ -35,9 +35,9 @@
"js-cookie": "3.0.1",
"localforage": "1.10.0",
"parse-link-header": "2.0.0",
"phoenix": "1.6.2",
"phoenix": "1.7.7",
"punycode.js": "2.3.0",
"qrcode": "1.5.1",
"qrcode": "1.5.3",
"querystring-es3": "0.2.1",
"url": "0.11.0",
"utf8": "3.0.0",
@ -97,7 +97,7 @@
"karma-spec-reporter": "0.0.36",
"karma-webpack": "5.0.0",
"lodash": "4.17.21",
"mini-css-extract-plugin": "2.7.5",
"mini-css-extract-plugin": "2.7.6",
"mocha": "10.2.0",
"nightwatch": "2.6.20",
"opn": "5.5.0",

ファイルの表示

@ -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
@ -259,6 +260,7 @@ const getNodeInfo = async ({ store }) => {
store.dispatch('setInstanceOption', { name: 'editingAvailable', value: features.includes('editing') })
store.dispatch('setInstanceOption', { name: 'pollLimits', value: metadata.pollLimits })
store.dispatch('setInstanceOption', { name: 'mailerEnabled', value: metadata.mailerEnabled })
store.dispatch('setInstanceOption', { name: 'quotingAvailable', value: features.includes('quote_posting') })
const uploadLimits = metadata.uploadLimits
store.dispatch('setInstanceOption', { name: 'uploadlimit', value: parseInt(uploadLimits.general) })
@ -343,6 +345,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

ファイルの表示

@ -1,4 +1,5 @@
import Completion from '../../services/completion/completion.js'
import genRandomSeed from '../../services/random_seed/random_seed.service.js'
import EmojiPicker from '../emoji_picker/emoji_picker.vue'
import Popover from 'src/components/popover/popover.vue'
import ScreenReaderNotice from 'src/components/screen_reader_notice/screen_reader_notice.vue'
@ -110,7 +111,7 @@ const EmojiInput = {
},
data () {
return {
randomSeed: `${Math.random()}`.replace('.', '-'),
randomSeed: genRandomSeed(),
input: undefined,
caretEl: undefined,
highlighted: -1,

ファイルの表示

@ -3,7 +3,7 @@
ref="popover"
trigger="click"
popover-class="emoji-picker popover-default"
:trigger-attrs="{ 'aria-hidden': true }"
:trigger-attrs="{ 'aria-hidden': true, tabindex: -1 }"
@show="onPopoverShown"
@close="onPopoverClosed"
>
@ -28,6 +28,7 @@
active: activeGroupView === group.id
}"
:title="group.text"
role="button"
@click.prevent="highlight(group.id)"
>
<span
@ -116,6 +117,7 @@
:key="group.id + emoji.displayText"
:title="maybeLocalizedEmojiName(emoji)"
class="emoji-item"
role="button"
@click.stop.prevent="onEmoji(emoji)"
>
<span
@ -126,6 +128,7 @@
v-else
class="emoji-picker-emoji -custom"
loading="lazy"
:alt="maybeLocalizedEmojiName(emoji)"
:src="emoji.imageUrl"
:data-emoji-name="group.id + emoji.displayText"
/>

ファイルの表示

@ -1,4 +1,5 @@
import Popover from '../popover/popover.vue'
import genRandomSeed from '../../services/random_seed/random_seed.service.js'
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
@ -40,7 +41,8 @@ const ExtraButtons = {
data () {
return {
expanded: false,
showingDeleteDialog: false
showingDeleteDialog: false,
randomSeed: genRandomSeed()
}
},
methods: {
@ -152,6 +154,15 @@ const ExtraButtons = {
editingAvailable () { return this.$store.state.instance.editingAvailable },
shouldConfirmDelete () {
return this.$store.getters.mergedConfig.modalOnDelete
},
triggerAttrs () {
return {
title: this.$t('status.more_actions'),
id: `popup-trigger-${this.randomSeed}`,
'aria-controls': `popup-menu-${this.randomSeed}`,
'aria-expanded': this.expanded,
'aria-haspopup': 'menu'
}
}
}
}

ファイルの表示

@ -2,6 +2,7 @@
<Popover
class="ExtraButtons"
trigger="click"
:trigger-attrs="triggerAttrs"
placement="top"
:offset="{ y: 5 }"
:bound-to="{ x: 'container' }"
@ -10,10 +11,15 @@
@close="onClose"
>
<template #content="{close}">
<div class="dropdown-menu">
<div
class="dropdown-menu"
role="menu"
:id="`popup-menu-${randomSeed}`"
>
<button
v-if="canMute && !status.thread_muted"
class="button-default dropdown-item dropdown-item-icon"
role="menuitem"
@click.prevent="muteConversation"
>
<FAIcon
@ -24,6 +30,7 @@
<button
v-if="canMute && status.thread_muted"
class="button-default dropdown-item dropdown-item-icon"
role="menuitem"
@click.prevent="unmuteConversation"
>
<FAIcon
@ -34,6 +41,7 @@
<button
v-if="!status.pinned && canPin"
class="button-default dropdown-item dropdown-item-icon"
role="menuitem"
@click.prevent="pinStatus"
@click="close"
>
@ -45,6 +53,7 @@
<button
v-if="status.pinned && canPin"
class="button-default dropdown-item dropdown-item-icon"
role="menuitem"
@click.prevent="unpinStatus"
@click="close"
>
@ -57,6 +66,7 @@
<button
v-if="!status.bookmarked"
class="button-default dropdown-item dropdown-item-icon"
role="menuitem"
@click.prevent="bookmarkStatus"
@click="close"
>
@ -68,6 +78,7 @@
<button
v-if="status.bookmarked"
class="button-default dropdown-item dropdown-item-icon"
role="menuitem"
@click.prevent="unbookmarkStatus"
@click="close"
>
@ -80,6 +91,7 @@
<button
v-if="ownStatus && editingAvailable"
class="button-default dropdown-item dropdown-item-icon"
role="menuitem"
@click.prevent="editStatus"
@click="close"
>
@ -91,6 +103,7 @@
<button
v-if="isEdited && editingAvailable"
class="button-default dropdown-item dropdown-item-icon"
role="menuitem"
@click.prevent="showStatusHistory"
@click="close"
>
@ -102,6 +115,7 @@
<button
v-if="canDelete"
class="button-default dropdown-item dropdown-item-icon"
role="menuitem"
@click.prevent="deleteStatus"
@click="close"
>
@ -112,6 +126,7 @@
</button>
<button
class="button-default dropdown-item dropdown-item-icon"
role="menuitem"
@click.prevent="copyLink"
@click="close"
>
@ -123,6 +138,7 @@
<a
v-if="!status.is_local"
class="button-default dropdown-item dropdown-item-icon"
role="menuitem"
title="Source"
:href="status.external_url"
target="_blank"
@ -134,6 +150,7 @@
</a>
<button
class="button-default dropdown-item dropdown-item-icon"
role="menuitem"
@click.prevent="reportStatus"
@click="close"
>

ファイルの表示

@ -0,0 +1,48 @@
import { mapGetters } from 'vuex'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faUserPlus,
faComments,
faBullhorn
} from '@fortawesome/free-solid-svg-icons'
library.add(
faUserPlus,
faComments,
faBullhorn
)
const ExtraNotifications = {
computed: {
shouldShowChats () {
return this.mergedConfig.showExtraNotifications && this.mergedConfig.showChatsInExtraNotifications && this.unreadChatCount
},
shouldShowAnnouncements () {
return this.mergedConfig.showExtraNotifications && this.mergedConfig.showAnnouncementsInExtraNotifications && this.unreadAnnouncementCount
},
shouldShowFollowRequests () {
return this.mergedConfig.showExtraNotifications && this.mergedConfig.showFollowRequestsInExtraNotifications && this.followRequestCount
},
hasAnythingToShow () {
return this.shouldShowChats || this.shouldShowAnnouncements || this.shouldShowFollowRequests
},
shouldShowCustomizationTip () {
return this.mergedConfig.showExtraNotificationsTip && this.hasAnythingToShow
},
currentUser () {
return this.$store.state.users.currentUser
},
...mapGetters(['unreadChatCount', 'unreadAnnouncementCount', 'followRequestCount', 'mergedConfig'])
},
methods: {
openNotificationSettings () {
return this.$store.dispatch('openSettingsModalTab', 'notifications')
},
dismissConfigurationTip () {
return this.$store.dispatch('setOption', { name: 'showExtraNotificationsTip', value: false })
}
}
}
export default ExtraNotifications

ファイルの表示

@ -0,0 +1,113 @@
<template>
<div class="ExtraNotifications">
<div
v-if="shouldShowChats"
class="notification unseen"
>
<div class="notification-overlay" />
<router-link
class="button-unstyled -link extra-notification"
:to="{ name: 'chats', params: { username: currentUser.screen_name } }"
>
<FAIcon
fixed-width
class="fa-scale-110 icon"
icon="comments"
/>
{{ $tc('notifications.unread_chats', unreadChatCount, { num: unreadChatCount }) }}
</router-link>
</div>
<div
v-if="shouldShowAnnouncements"
class="notification unseen"
>
<div class="notification-overlay" />
<router-link
class="button-unstyled -link extra-notification"
:to="{ name: 'announcements' }"
>
<FAIcon
fixed-width
class="fa-scale-110 icon"
icon="bullhorn"
/>
{{ $tc('notifications.unread_announcements', unreadAnnouncementCount, { num: unreadAnnouncementCount }) }}
</router-link>
</div>
<div
v-if="shouldShowFollowRequests"
class="notification unseen"
>
<div class="notification-overlay" />
<router-link
class="button-unstyled -link extra-notification"
:to="{ name: 'friend-requests' }"
>
<FAIcon
fixed-width
class="fa-scale-110 icon"
icon="user-plus"
/>
{{ $tc('notifications.unread_follow_requests', followRequestCount, { num: followRequestCount }) }}
</router-link>
</div>
<i18n-t
v-if="shouldShowCustomizationTip"
tag="span"
class="notification tip extra-notification"
keypath="notifications.configuration_tip"
>
<template #theSettings>
<button
class="button-unstyled -link"
@click="openNotificationSettings"
>
{{ $t('notifications.configuration_tip_settings') }}
</button>
</template>
<template #dismiss>
<button
class="button-unstyled -link"
@click="dismissConfigurationTip"
>
{{ $t('notifications.configuration_tip_dismiss') }}
</button>
</template>
</i18n-t>
</div>
</template>
<script src="./extra_notifications.js" />
<style lang="scss">
@import "../../variables";
.ExtraNotifications {
width: 100%;
display: flex;
flex-direction: column;
align-items: stretch;
.notification {
width: 100%;
border-bottom: 1px solid;
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
display: flex;
flex-direction: column;
align-items: stretch;
}
.extra-notification {
padding: 1em;
}
.icon {
margin-right: 0.5em;
}
.tip {
display: inline;
}
}
</style>

ファイルの表示

@ -32,7 +32,7 @@
top: calc(var(--navbar-height) + 0.5em);
width: 100%;
pointer-events: none;
z-index: var(--ZI_navbar_popovers);
z-index: var(--ZI_modals_popovers);
display: flex;
flex-direction: column;
align-items: center;

ファイルの表示

@ -39,6 +39,7 @@
<Notifications
ref="notifications"
:no-heading="true"
:no-extra="true"
:minimal-mode="true"
:filter-mode="filterMode"
/>

ファイルの表示

@ -1,7 +1,10 @@
import SideDrawer from '../side_drawer/side_drawer.vue'
import Notifications from '../notifications/notifications.vue'
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
import { unseenNotificationsFromStore } from '../../services/notification_utils/notification_utils'
import {
unseenNotificationsFromStore,
countExtraNotifications
} from '../../services/notification_utils/notification_utils'
import GestureService from '../../services/gesture_service/gesture_service'
import NavigationPins from 'src/components/navigation/navigation_pins.vue'
import { mapGetters } from 'vuex'
@ -11,7 +14,8 @@ import {
faBell,
faBars,
faArrowUp,
faMinus
faMinus,
faCheckDouble
} from '@fortawesome/free-solid-svg-icons'
library.add(
@ -19,7 +23,8 @@ library.add(
faBell,
faBars,
faArrowUp,
faMinus
faMinus,
faCheckDouble
)
const MobileNav = {
@ -50,8 +55,14 @@ const MobileNav = {
return unseenNotificationsFromStore(this.$store)
},
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 () {
@ -64,6 +75,9 @@ const MobileNav = {
shouldConfirmLogout () {
return this.$store.getters.mergedConfig.modalOnLogout
},
closingDrawerMarksAsSeen () {
return this.$store.getters.mergedConfig.closingDrawerMarksAsSeen
},
...mapGetters(['unreadChatCount'])
},
methods: {
@ -78,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()
}
}
@ -114,7 +128,6 @@ const MobileNav = {
this.hideConfirmLogout()
},
markNotificationsAsSeen () {
// this.$refs.notifications.markAsSeen()
this.$store.dispatch('markNotificationsAsSeen')
},
onScroll ({ target: { scrollTop, clientHeight, scrollHeight } }) {

ファイルの表示

@ -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"
@ -66,6 +72,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')"

ファイルの表示

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

ファイルの表示

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

ファイルの表示

@ -1,12 +1,15 @@
import { computed } from 'vue'
import { mapGetters } from 'vuex'
import Notification from '../notification/notification.vue'
import ExtraNotifications from '../extra_notifications/extra_notifications.vue'
import NotificationFilters from './notification_filters.vue'
import notificationsFetcher from '../../services/notifications_fetcher/notifications_fetcher.service.js'
import {
notificationsFromStore,
filteredNotificationsFromStore,
unseenNotificationsFromStore
unseenNotificationsFromStore,
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'
@ -23,7 +26,8 @@ const DEFAULT_SEEN_TO_DISPLAY_COUNT = 30
const Notifications = {
components: {
Notification,
NotificationFilters
NotificationFilters,
ExtraNotifications
},
props: {
// Disables panel styles, unread mark, potentially other notification-related actions
@ -31,6 +35,11 @@ const Notifications = {
minimalMode: Boolean,
// Custom filter mode, an array of strings, possible values 'mention', 'repeat', 'like', 'follow', used to override global filter for use in "Interactions" timeline
filterMode: Array,
// Do not show extra notifications
noExtra: {
type: Boolean,
default: false
},
// Disable teleporting (i.e. for /users/user/notifications)
disableTeleport: Boolean
},
@ -57,22 +66,36 @@ 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 ? '*' : ''}`
},
unseenCount () {
return this.unseenNotifications.length
},
ignoreInactionableSeen () { return this.$store.getters.mergedConfig.ignoreInactionableSeen },
extraNotificationsCount () {
return countExtraNotifications(this.$store)
},
unseenCountTitle () {
return this.unseenCount + (this.unreadChatCount) + this.unreadAnnouncementCount
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
@ -94,6 +117,10 @@ 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
},
...mapGetters(['unreadChatCount', 'unreadAnnouncementCount'])
},
mounted () {
@ -137,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

ファイルの表示

@ -17,9 +17,9 @@
<div class="title">
{{ $t('notifications.notifications') }}
<span
v-if="unseenCount"
v-if="unseenCountBadgeText"
class="badge badge-notification unseen-count"
>{{ unseenCount }}</span>
>{{ unseenCountBadgeText }}</span>
</div>
<div
v-if="showScrollTop"
@ -54,15 +54,26 @@
class="panel-body"
role="feed"
>
<div
v-if="showExtraNotifications"
role="listitem"
class="notification"
>
<extra-notifications />
</div>
<div
v-for="notification in notificationsToDisplay"
: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">

ファイルの表示

@ -1,4 +1,5 @@
import Timeago from 'components/timeago/timeago.vue'
import genRandomSeed from '../../services/random_seed/random_seed.service.js'
import RichContent from 'components/rich_content/rich_content.jsx'
import { forEach, map } from 'lodash'
@ -13,7 +14,7 @@ export default {
return {
loading: false,
choices: [],
randomSeed: `${Math.random()}`.replace('.', '-')
randomSeed: genRandomSeed()
}
},
created () {

ファイルの表示

@ -1,4 +1,5 @@
import statusPoster from '../../services/status_poster/status_poster.service.js'
import genRandomSeed from '../../services/random_seed/random_seed.service.js'
import MediaUpload from '../media_upload/media_upload.vue'
import ScopeSelector from '../scope_selector/scope_selector.vue'
import EmojiInput from '../emoji_input/emoji_input.vue'
@ -156,11 +157,13 @@ const PostStatusForm = {
poll: this.statusPoll || {},
mediaDescriptions: this.statusMediaDescriptions || {},
visibility: this.statusScope || scope,
contentType: statusContentType
contentType: statusContentType,
quoting: false
}
}
return {
randomSeed: genRandomSeed(),
dropFiles: [],
uploadingFiles: false,
error: null,
@ -265,6 +268,30 @@ const PostStatusForm = {
isEdit () {
return typeof this.statusId !== 'undefined' && this.statusId.trim() !== ''
},
quotable () {
if (!this.$store.state.instance.quotingAvailable) {
return false
}
if (!this.replyTo) {
return false
}
const repliedStatus = this.$store.state.statuses.allStatusesObject[this.replyTo]
if (!repliedStatus) {
return false
}
if (repliedStatus.visibility === 'public' ||
repliedStatus.visibility === 'unlisted' ||
repliedStatus.visibility === 'local') {
return true
} else if (repliedStatus.visibility === 'private') {
return repliedStatus.user.id === this.$store.state.users.currentUser.id
}
return false
},
...mapGetters(['mergedConfig']),
...mapState({
mobileLayout: state => state.interface.mobileLayout
@ -292,7 +319,8 @@ const PostStatusForm = {
visibility: newStatus.visibility,
contentType: newStatus.contentType,
poll: {},
mediaDescriptions: {}
mediaDescriptions: {},
quoting: false
}
this.pollFormVisible = false
this.$refs.mediaUpload && this.$refs.mediaUpload.clearFile()
@ -340,6 +368,8 @@ const PostStatusForm = {
return
}
const replyOrQuoteAttr = newStatus.quoting ? 'quoteId' : 'inReplyToStatusId'
const postingOptions = {
status: newStatus.status,
spoilerText: newStatus.spoilerText || null,
@ -347,7 +377,7 @@ const PostStatusForm = {
sensitive: newStatus.nsfw,
media: newStatus.files,
store: this.$store,
inReplyToStatusId: this.replyTo,
[replyOrQuoteAttr]: this.replyTo,
contentType: newStatus.contentType,
poll,
idempotencyKey: this.idempotencyKey
@ -373,6 +403,7 @@ const PostStatusForm = {
}
const newStatus = this.newStatus
this.previewLoading = true
const replyOrQuoteAttr = newStatus.quoting ? 'quoteId' : 'inReplyToStatusId'
statusPoster.postStatus({
status: newStatus.status,
spoilerText: newStatus.spoilerText || null,
@ -380,7 +411,7 @@ const PostStatusForm = {
sensitive: newStatus.nsfw,
media: [],
store: this.$store,
inReplyToStatusId: this.replyTo,
[replyOrQuoteAttr]: this.replyTo,
contentType: newStatus.contentType,
poll: {},
preview: true

ファイルの表示

@ -126,6 +126,36 @@
class="preview-status"
/>
</div>
<div
v-if="quotable"
role="radiogroup"
class="btn-group reply-or-quote-selector"
>
<button
:id="`reply-or-quote-option-${randomSeed}-reply`"
class="btn button-default reply-or-quote-option"
:class="{ toggled: !newStatus.quoting }"
tabindex="0"
role="radio"
:aria-labelledby="`reply-or-quote-option-${randomSeed}-reply`"
:aria-checked="!newStatus.quoting"
@click="newStatus.quoting = false"
>
{{ $t('post_status.reply_option') }}
</button>
<button
:id="`reply-or-quote-option-${randomSeed}-quote`"
class="btn button-default reply-or-quote-option"
:class="{ toggled: newStatus.quoting }"
tabindex="0"
role="radio"
:aria-labelledby="`reply-or-quote-option-${randomSeed}-quote`"
:aria-checked="newStatus.quoting"
@click="newStatus.quoting = true"
>
{{ $t('post_status.quote_option') }}
</button>
</div>
<EmojiInput
v-if="!disableSubject && (newStatus.spoilerText || alwaysShowSubject)"
v-model="newStatus.spoilerText"
@ -420,6 +450,10 @@
margin: 0;
}
.reply-or-quote-selector {
margin-bottom: 0.5em;
}
.text-format {
.only-format {
color: $fallback--faint;

ファイルの表示

@ -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 })
}
},

ファイルの表示

@ -11,6 +11,8 @@
/>
<span
class="button-unstyled popover-trigger"
role="button"
:tabindex="0"
:title="$t('tool_tip.add_reaction')"
@click.stop.prevent="show"
>

ファイルの表示

@ -1,6 +1,7 @@
import Select from '../select/select.vue'
import StatusContent from '../status_content/status_content.vue'
import Timeago from '../timeago/timeago.vue'
import RichContent from 'src/components/rich_content/rich_content.jsx'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
const Report = {
@ -10,7 +11,8 @@ const Report = {
components: {
Select,
StatusContent,
Timeago
Timeago,
RichContent
},
computed: {
report () {

ファイルの表示

@ -4,6 +4,7 @@ import IntegerSetting from '../helpers/integer_setting.vue'
import StringSetting from '../helpers/string_setting.vue'
import GroupSetting from '../helpers/group_setting.vue'
import Popover from 'src/components/popover/popover.vue'
import PanelLoading from 'src/components/panel_loading/panel_loading.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js'
import { library } from '@fortawesome/fontawesome-svg-core'
@ -22,12 +23,18 @@ const FrontendsTab = {
defaultSource: 'admin'
}
},
data () {
return {
working: false
}
},
components: {
BooleanSetting,
ChoiceSetting,
IntegerSetting,
StringSetting,
GroupSetting,
PanelLoading,
Popover
},
created () {
@ -42,18 +49,56 @@ const FrontendsTab = {
...SharedComputedObject()
},
methods: {
canInstall (frontend) {
const fe = this.frontends.find(f => f.name === frontend.name)
if (!fe) return false
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
} else {
return frontend.refs[0]
}
},
update (frontend, suggestRef) {
const ref = suggestRef || frontend.refs[0]
const ref = suggestRef || this.getSuggestedRef(frontend)
const { name } = frontend
const payload = { name, ref }
this.working = true
this.$store.state.api.backendInteractor.installFrontend({ payload })
.then((externalUser) => {
.finally(() => {
this.working = false
})
.then(async (response) => {
this.$store.dispatch('loadFrontendsStuff')
if (response.error) {
const reason = await response.error.json()
this.$store.dispatch('pushGlobalNotice', {
level: 'error',
messageKey: 'admin_dash.frontend.failure_installing_frontend',
messageArgs: {
version: name + '/' + ref,
reason: reason.error
},
timeout: 5000
})
} else {
this.$store.dispatch('pushGlobalNotice', {
level: 'success',
messageKey: 'admin_dash.frontend.success_installing_frontend',
messageArgs: {
version: name + '/' + ref
},
timeout: 2000
})
}
})
},
setDefault (frontend, suggestRef) {
const ref = suggestRef || frontend.refs[0]
const ref = suggestRef || this.getSuggestedRef(frontend)
const { name } = frontend
this.$store.commit('updateAdminDraft', { path: [':pleroma', ':frontends', ':primary'], value: { name, ref } })

ファイルの表示

@ -3,6 +3,22 @@
padding: 0;
}
.relative {
position: relative;
}
.overlay {
position: absolute;
background: var(--bg);
// fix buttons showing through
z-index: 2;
opacity: 0.9;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
dd {
text-overflow: ellipsis;
word-wrap: nowrap;

ファイルの表示

@ -10,7 +10,6 @@
<li>
<h3>{{ $t('admin_dash.frontend.default_frontend') }}</h3>
<p>{{ $t('admin_dash.frontend.default_frontend_tip') }}</p>
<p>{{ $t('admin_dash.frontend.default_frontend_tip2') }}</p>
<ul class="setting-list">
<li>
<StringSetting path=":pleroma.:frontends.:primary.name" />
@ -24,7 +23,8 @@
</ul>
</li>
</ul>
<div class="setting-list">
<div class="setting-list relative">
<PanelLoading class="overlay" v-if="working"/>
<h3>{{ $t('admin_dash.frontend.available_frontends') }}</h3>
<ul class="cards-list">
<li
@ -33,9 +33,9 @@
>
<strong>{{ frontend.name }}</strong>
{{ ' ' }}
<span v-if="adminDraft[':pleroma'][':frontends'][':primary'].name === frontend.name">
<span v-if="adminDraft[':pleroma'][':frontends'][':primary']?.name === frontend.name">
<i18n-t
v-if="adminDraft[':pleroma'][':frontends'][':primary'].ref === frontend.refs[0]"
v-if="adminDraft[':pleroma'][':frontends'][':primary']?.ref === frontend.refs[0]"
keypath="admin_dash.frontend.is_default"
/>
<i18n-t
@ -86,6 +86,11 @@
? $t('admin_dash.frontend.reinstall')
: $t('admin_dash.frontend.install')
}}
<code>
{{
getSuggestedRef(frontend)
}}
</code>
</button>
<Popover
v-if="frontend.refs.length > 1"
@ -93,13 +98,14 @@
class="button-dropdown"
placement="bottom"
>
<template #content>
<template #content="{close}">
<div class="dropdown-menu">
<button
v-for="ref in frontend.refs"
:key="ref"
class="button-default dropdown-item"
@click="update(frontend, ref)"
@click.prevent="update(frontend, ref)"
@click="close"
>
<i18n-t keypath="admin_dash.frontend.install_version">
<template #version>
@ -128,14 +134,19 @@
class="button button-default btn"
type="button"
:disabled="
adminDraft[':pleroma'][':frontends'][':primary'].name === frontend.name &&
adminDraft[':pleroma'][':frontends'][':primary'].ref === frontend.refs[0]
adminDraft[':pleroma'][':frontends'][':primary']?.name === frontend.name &&
adminDraft[':pleroma'][':frontends'][':primary']?.ref === frontend.refs[0]
"
@click="setDefault(frontend)"
>
{{
$t('admin_dash.frontend.set_default')
}}
<code>
{{
getSuggestedRef(frontend)
}}
</code>
</button>
{{ ' ' }}
<Popover
@ -144,13 +155,14 @@
class="button-dropdown"
placement="bottom"
>
<template #content>
<template #content="{close}">
<div class="dropdown-menu">
<button
v-for="ref in frontend.refs.slice(1)"
v-for="ref in frontend.installedRefs || frontend.refs"
:key="ref"
class="button-default dropdown-item"
@click="setDefault(frontend, ref)"
@click.prevent="setDefault(frontend, ref)"
@click="close"
>
<i18n-t keypath="admin_dash.frontend.set_default_version">
<template #version>

ファイルの表示

@ -6,6 +6,10 @@
<li>
<StringSetting path=":pleroma.:instance.:name" />
</li>
<!-- See https://git.pleroma.social/pleroma/pleroma/-/merge_requests/3963 -->
<li v-if="adminDraft[':pleroma'][':instance'][':favicon'] !== undefined">
<AttachmentSetting compact path=":pleroma.:instance.:favicon" />
</li>
<li>
<StringSetting path=":pleroma.:instance.:email" />
</li>
@ -16,7 +20,7 @@
<StringSetting path=":pleroma.:instance.:short_description" />
</li>
<li>
<AttachmentSetting path=":pleroma.:instance.:instance_thumbnail" />
<AttachmentSetting compact path=":pleroma.:instance.:instance_thumbnail" />
</li>
<li>
<AttachmentSetting path=":pleroma.:instance.:background_image" />

ファイルの表示

@ -7,6 +7,7 @@ export default {
...Setting,
props: {
...Setting.props,
compact: Boolean,
acceptTypes: {
type: String,
required: false,

ファイルの表示

@ -2,6 +2,7 @@
<span
v-if="matchesExpertLevel"
class="AttachmentSetting"
:class="{ '-compact': compact }"
>
<label
:for="path"
@ -24,8 +25,8 @@
{{ backendDescriptionDescription + ' ' }}
</p>
<div class="attachment-input">
<div>{{ $t('settings.url') }}</div>
<div class="controls">
<div class="controls control-field">
<label for="path">{{ $t('settings.url') }}</label>
<input
:id="path"
class="string-input"
@ -40,7 +41,7 @@
/>
<ProfileSettingIndicator :is-profile="isProfileSetting" />
</div>
<div>{{ $t('settings.preview') }}</div>
<div v-if="!compact">{{ $t('settings.preview') }}</div>
<Attachment
class="attachment"
:compact="compact"
@ -50,7 +51,7 @@
@setMedia="onMedia"
@naturalSizeLoad="onNaturalSizeLoad"
/>
<div class="controls">
<div class="controls control-upload">
<MediaUpload
ref="mediaUpload"
class="media-upload-icon"
@ -84,6 +85,35 @@
width: 20em;
}
&.-compact {
.attachment-input {
flex-direction: row;
align-items: flex-end;
}
.attachment {
flex: 0;
order: 0;
display: block;
min-width: 4em;
height: 4em;
align-self: center;
margin-bottom: 0;
}
.control-field {
order: 1;
min-width: 12em;
margin-left: 0.5em;
}
.control-upload {
order: 2;
min-width: 12em;
padding: 0 0.5em;
}
}
.controls {
margin-bottom: 0.5em;

ファイルの表示

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

ファイルの表示

@ -91,6 +91,11 @@
{{ $t('settings.hide_attachments_in_convo') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="hideScrobbles">
{{ $t('settings.hide_scrobbles') }}
</BooleanSetting>
</li>
</ul>
</div>
<div

ファイルの表示

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

ファイルの表示

@ -1,5 +1,30 @@
<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">
@ -11,42 +36,184 @@
{{ $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') }}
<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>
<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>
<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>
<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>
<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>
<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>
<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>
<li>
<BooleanSetting path="showExtraNotifications">
{{ $t('settings.notification_show_extra') }}
</BooleanSetting>
</li>
<li>
<ul class="setting-list suboptions">
<li>
<BooleanSetting
path="showChatsInExtraNotifications"
:disabled="!mergedConfig.showExtraNotifications"
>
{{ $t('settings.notification_extra_chats') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="notificationVisibility.repeats">
{{ $t('settings.notification_visibility_repeats') }}
<BooleanSetting
path="showAnnouncementsInExtraNotifications"
:disabled="!mergedConfig.showExtraNotifications"
>
{{ $t('settings.notification_extra_announcements') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="notificationVisibility.follows">
{{ $t('settings.notification_visibility_follows') }}
<BooleanSetting
path="showFollowRequestsInExtraNotifications"
:disabled="!mergedConfig.showExtraNotifications"
>
{{ $t('settings.notification_extra_follow_requests') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="notificationVisibility.mentions">
{{ $t('settings.notification_visibility_mentions') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="notificationVisibility.moves">
{{ $t('settings.notification_visibility_moves') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="notificationVisibility.emojiReactions">
{{ $t('settings.notification_visibility_emoji_reactions') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="notificationVisibility.polls">
{{ $t('settings.notification_visibility_polls') }}
<BooleanSetting
path="showExtraNotificationsTip"
:disabled="!mergedConfig.showExtraNotifications"
>
{{ $t('settings.notification_extra_tip') }}
</BooleanSetting>
</li>
</ul>
@ -67,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

ファイルの表示

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

ファイルの表示

@ -39,7 +39,8 @@ import {
faThumbtack,
faChevronUp,
faChevronDown,
faAngleDoubleRight
faAngleDoubleRight,
faPlay
} from '@fortawesome/free-solid-svg-icons'
library.add(
@ -59,7 +60,8 @@ library.add(
faThumbtack,
faChevronUp,
faChevronDown,
faAngleDoubleRight
faAngleDoubleRight,
faPlay
)
const camelCase = name => name.charAt(0).toUpperCase() + name.slice(1)
@ -133,6 +135,7 @@ const Status = {
'showPinned',
'inProfile',
'profileUserId',
'inQuote',
'simpleTree',
'controlledThreadDisplayStatus',
@ -151,6 +154,7 @@ const Status = {
'controlledSetMediaPlaying',
'dive'
],
emits: ['interacted'],
data () {
return {
uncontrolledReplying: false,
@ -159,7 +163,8 @@ const Status = {
uncontrolledMediaPlaying: [],
suspendable: true,
error: null,
headTailLinks: null
headTailLinks: null,
displayQuote: !this.inQuote
}
},
computed: {
@ -401,6 +406,24 @@ const Status = {
},
editingAvailable () {
return this.$store.state.instance.editingAvailable
},
hasVisibleQuote () {
return this.status.quote_url && this.status.quote_visible
},
hasInvisibleQuote () {
return this.status.quote_url && !this.status.quote_visible
},
quotedStatus () {
return this.status.quote_id ? this.$store.state.statuses.allStatusesObject[this.status.quote_id] : undefined
},
shouldDisplayQuote () {
return this.quotedStatus && this.displayQuote
},
scrobblePresent () {
return !this.mergedConfig.hideScrobbles && this.status.user.latestScrobble && this.status.user.latestScrobble.artist
},
scrobble () {
return this.status.user.latestScrobble
}
},
methods: {
@ -420,9 +443,11 @@ const Status = {
this.error = error
},
clearError () {
this.$emit('interacted')
this.error = undefined
},
toggleReplying () {
this.$emit('interacted')
controlledOrUncontrolledToggle(this, 'replying')
},
gotoOriginal (id) {
@ -469,6 +494,18 @@ const Status = {
window.scrollBy(0, rect.bottom - window.innerHeight + 50)
}
}
},
toggleDisplayQuote () {
if (this.shouldDisplayQuote) {
this.displayQuote = false
} else if (!this.quotedStatus) {
this.$store.dispatch('fetchStatus', this.status.quote_id)
.then(() => {
this.displayQuote = true
})
} else {
this.displayQuote = true
}
}
},
watch: {

ファイルの表示

@ -422,4 +422,22 @@
}
}
}
.quoted-status {
margin-top: 0.5em;
border: 1px solid var(--border, $fallback--border);
border-radius: var(--attachmentRadius, $fallback--attachmentRadius);
&.-unavailable-prompt {
padding: 0.5em;
}
}
.display-quoted-status-button {
margin: 0.5em;
&-icon {
color: inherit;
}
}
}

ファイルの表示

@ -249,6 +249,47 @@
</button>
</span>
</div>
<div
v-if="scrobblePresent"
class="status-rich-presence"
>
<a
v-if="scrobble.externalLink"
:href="scrobble.externalLink"
target="_blank"
>
{{ scrobble.artist }} {{ scrobble.title }}
<FAIcon
class="fa-scale-110 fa-old-padding"
icon="play"
/>
<span class="status-rich-presence-time">
<Timeago
template-key="time.in_past"
:time="scrobble.created_at"
:auto-update="60"
/>
</span>
</a>
<span v-if="!scrobble.externalLink">
<FAIcon
class="fa-scale-110 fa-old-padding"
icon="music"
/>
{{ scrobble.artist }} {{ scrobble.title }}
<FAIcon
class="fa-scale-110 fa-old-padding"
icon="play"
/>
<span class="status-rich-presence-time">
<Timeago
template-key="time.in_past"
:time="scrobble.created_at"
:auto-update="60"
/>
</span>
</span>
</div>
<div
v-if="isReply || hasMentionsLine"
class="heading-reply-row"
@ -364,6 +405,45 @@
@parseReady="setHeadTailLinks"
/>
<article
v-if="hasVisibleQuote"
class="quoted-status"
>
<button
class="button-unstyled -link display-quoted-status-button"
:aria-expanded="shouldDisplayQuote"
@click="toggleDisplayQuote"
>
{{ shouldDisplayQuote ? $t('status.hide_quote') : $t('status.display_quote') }}
<FAIcon
class="display-quoted-status-button-icon"
:icon="shouldDisplayQuote ? 'chevron-up' : 'chevron-down'"
/>
</button>
<Status
v-if="shouldDisplayQuote"
:statusoid="quotedStatus"
:in-quote="true"
/>
</article>
<p
v-else-if="hasInvisibleQuote"
class="quoted-status -unavailable-prompt"
>
<i18n-t keypath="status.invisible_quote">
<template #link>
<bdi>
<a
:href="status.quote_url"
target="_blank"
>
{{ status.quote_url }}
</a>
</bdi>
</template>
</i18n-t>
</p>
<div
v-if="inConversation && !isPreview && replies && replies.length"
class="replies"
@ -451,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"

ファイルの表示

@ -73,6 +73,10 @@ const StatusContent = {
},
computed: {
...controlledOrUncontrolledGetters(['showingTall', 'expandingSubject', 'showingLongSubject']),
statusCard () {
if (!this.status.card) return null
return this.status.card.url === this.status.quote_url ? null : this.status.card
},
hideAttachments () {
return (this.mergedConfig.hideAttachments && !this.inConversation) ||
(this.mergedConfig.hideAttachmentsInConv && this.inConversation)

ファイルの表示

@ -43,7 +43,7 @@
/>
<div
v-if="status.card && !noHeading && !compact"
v-if="statusCard && !noHeading && !compact"
class="link-preview media-body"
>
<link-preview

ファイルの表示

@ -160,6 +160,9 @@ const Timeline = {
if (this.timeline.flushMarker !== 0) {
this.$store.commit('clearTimeline', { timeline: this.timelineName, excludeUserId: true })
this.$store.commit('queueFlush', { timeline: this.timelineName, id: 0 })
if (this.timelineName === 'user') {
this.$store.dispatch('fetchPinnedStatuses', this.userId)
}
this.fetchOlderStatuses()
} else {
this.blockClicksTemporarily()

ファイルの表示

@ -2,7 +2,7 @@
<video
class="video"
preload="metadata"
:src="attachment.url"
:src="attachment.url + '#t=0.5'"
:loop="loopVideo"
:controls="controls"
:alt="attachment.description"

ファイルの表示

@ -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": {
@ -205,7 +206,13 @@
"migrated_to": "migrated to",
"reacted_with": "reacted with {0}",
"submitted_report": "submitted a report",
"poll_ended": "poll has ended"
"poll_ended": "poll has ended",
"unread_announcements": "{num} unread announcement | {num} unread announcements",
"unread_chats": "{num} unread chat | {num} unread chats",
"unread_follow_requests": "{num} new follow request | {num} new follow requests",
"configuration_tip": "You can customize what to display here in {theSettings}. {dismiss}",
"configuration_tip_settings": "the settings",
"configuration_tip_dismiss": "Do not show again"
},
"polls": {
"add_poll": "Add poll",
@ -261,6 +268,8 @@
"post_status": {
"edit_status": "Edit status",
"new_status": "Post new status",
"reply_option": "Reply to this status",
"quote_option": "Quote this status",
"account_not_locked_warning": "Your account is not {0}. Anyone can follow you to view your follower-only posts.",
"account_not_locked_warning_link": "locked",
"attachments_sensitive": "Mark attachments as sensitive",
@ -487,6 +496,7 @@
"hide_muted_posts": "Hide posts of muted users",
"mute_bot_posts": "Mute bot posts",
"hide_bot_indication": "Hide bot 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)",
"hide_isp": "Hide instance-specific panel",
@ -552,13 +562,22 @@
"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",
"notification_show_extra": "Show extra notifications in the notifications column",
"notification_extra_chats": "Show unread chats",
"notification_extra_announcements": "Show unread announcements",
"notification_extra_follow_requests": "Show new follow requests",
"notification_extra_tip": "Show the customization tip for extra notifications",
"no_rich_text_description": "Strip rich text formatting from all posts",
"no_blocks": "No blocks",
"no_mutes": "No mutes",
@ -674,13 +693,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": {
@ -900,8 +927,9 @@
"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_tip2": "WIP: Since Pleroma backend doesn't properly list all installed frontends you'll have to enter name and reference manually. List below provides shortcuts to fill the values.",
"available_frontends": "Available for install"
"available_frontends": "Available for install",
"failure_installing_frontend": "Failed to install frontend {version}: {reason}",
"success_installing_frontend": "Frontend {version} successfully installed"
},
"temp_overrides": {
":pleroma": {
@ -1028,7 +1056,11 @@
"show_all_conversation": "Show full conversation ({numStatus} other status) | Show full conversation ({numStatus} other statuses)",
"show_only_conversation_under_this": "Only show replies to this status",
"status_history": "Status history",
"reaction_count_label": "{num} person reacted | {num} people reacted"
"reaction_count_label": "{num} person reacted | {num} people reacted",
"hide_quote": "Hide the quoted status",
"display_quote": "Display the quoted status",
"invisible_quote": "Quoted status unavailable: {link}",
"more_actions": "More actions on this status"
},
"user_card": {
"approve": "Approve",

ファイルの表示

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

ファイル差分が大きすぎるため省略します 差分を読み込み

ファイルの表示

@ -205,7 +205,13 @@
"migrated_to": "移民到",
"reacted_with": "顯出{0} ê 反應",
"submitted_report": "送出檢舉",
"poll_ended": "投票結束"
"poll_ended": "投票結束",
"unread_announcements": "{num} 篇公告iáu bē 讀",
"unread_chats": "{num} ê開講iáu bē讀",
"unread_follow_requests": "{num}ê新ê跟tuè請求",
"configuration_tip": "用{theSettings}lí通自訂siánn物佇tsia顯示。{dismiss}",
"configuration_tip_settings": "設定",
"configuration_tip_dismiss": "Mài koh顯示"
},
"polls": {
"add_poll": "開投票",
@ -248,12 +254,12 @@
"regional_indicator": "地區指引 {letter}"
},
"errors": {
"storage_unavailable": "Pleroma buē-tàng the̍h 著瀏覽器儲存 ê。Lí ê 登入狀態抑是局部設定 buē 儲存mā 凡勢 tú 著意料外 ê 問題。拍開 cookie 看。"
"storage_unavailable": "Pleroma buē-tàng the̍h 著瀏覽器儲存 ê。Lí ê 登入狀態抑是局部設定 buē 儲存mā 凡勢 tú 著意料外 ê 問題。拍開 cookie 看māi。"
},
"interactions": {
"favs_repeats": "轉送 kap kah 意",
"follows": "最近綴 lí ê",
"emoji_reactions": "繪文字 ê 應",
"emoji_reactions": "繪文字 ê 應",
"reports": "檢舉",
"moves": "用者 ê 移民",
"load_older": "載入 koh khah 早 ê 互動"
@ -273,13 +279,13 @@
},
"content_type_selection": "貼 ê 形式",
"content_warning": "主旨(毋是必要)",
"default": "Tú kàu 高雄 ah。",
"default": "Tú正kàu高雄ah。",
"direct_warning_to_all": "Tsit ê PO 文通 hōo 逐 ê 提起 ê 用者看見。",
"direct_warning_to_first_only": "Tsit ê PO 文kan-ta 短信 tú 開始提起 ê 用者tsiah 通看見。",
"direct_warning_to_first_only": "Tsit ê PO 文kan-ta佇短phue tú開始提起ê用者tsiah通看見。",
"edit_remote_warning": "別 ê 站臺可能無支援編輯,無法度收著 PO 文上新 ê 版本。",
"edit_unsupported_warning": "Pleroma 無支持編輯 the̍h 起 hām 投票。",
"posting": "PO 文",
"preview": "Sing 看覓",
"preview": "Sing看māi",
"preview_empty": "空 ê",
"empty_status_error": "無法度 PO 無檔案 koh 空 ê 狀態",
"media_description_error": "更新媒體失敗,請 koh 試一 kái",
@ -295,7 +301,9 @@
"public": "公開 - PO kàu 公開時間線",
"unlisted": "Mài 列出來 - Mài PO tī 公開時間線"
},
"post": "PO 上去"
"post": "PO 上去",
"reply_option": "應tsit ê狀態",
"quote_option": "引用tsit ê狀態"
},
"registration": {
"bio_optional": "介紹(毋是必要)",
@ -360,7 +368,16 @@
"color": "色彩",
"opacity": "無透明度",
"contrast": {
"hint": "色彩ê對比率:{ratio}。{level}、 {context}"
"hint": "色彩ê對比率:{ratio}。{level}、 {context}",
"level": {
"aa": "合AA級ê準則(上kē ê)",
"aaa": "合AAA級ê準則(建議ê)",
"bad": "無合半ê無障礙準則"
},
"context": {
"18pt": "大(18pt 以上)ê文字",
"text": "文字"
}
}
},
"switcher": {
@ -370,7 +387,7 @@
"keep_roundness": "保留邊á角ê khà-buh",
"keep_fonts": "保持字型",
"reset": "重頭設定",
"clear_all": "清掉",
"clear_all": "Lóng清掉",
"clear_opacity": "清掉無透明度",
"load_theme": "載入主題",
"keep_as_is": "Mài振動",
@ -380,8 +397,116 @@
"upgraded_from_v2": "PleromaFE升級ah主題huân-sè kap lí知影ê無kâng。",
"v2_imported": "Lí輸入ê檔案是舊版本ê前端用ê。Guán盡量予版本相通毋過可能有所在buē-tàng。",
"older_version_imported": "Lí輸入ê檔案是予舊ê前端用ê。",
"future_version_imported": "Lí輸入ê檔案是新ê前端所用ê。"
"future_version_imported": "Lí輸入ê檔案是新ê前端所用ê。",
"snapshot_missing": "無主題ê快相佇檔案內所以伊看起來凡勢kap原來預料ê無kâng。",
"snapshot_present": "主題ê快相有載入所以逐ê值lóng khàm過去ah。Lí 通改載入主題實際ê資料。",
"fe_upgraded": "版本更新了後Pleroma前端ê ia̋n-jín 升級ah。",
"fe_downgraded": "Pleroma ê前端滾tńg去ah。",
"migration_snapshot_ok": "為著保險主題快相載入去ah。Lí ē當試載入主題資料。",
"migration_napshot_gone": "快相因故無去ahtsi̍t-kuá所在看起來可能hām lí所想ê無kâng。",
"snapshot_source_mismatch": "版本tshia̋ng-póng上可能因為前端滾轉去koh更新ah若因為用舊版本ê前端主題tsiah改變lí有可能beh用舊ê版本。無著用新ê。"
},
"save_load_hint": "佇揀iah是載入主題ê時「保存」選項保留現tsú時設定ê選項mā佇輸出主題ê時tsūn儲存頭拄á講ê選項。若是逐ê選擇框á無設定逐項設定就ē khǹg佇輸出ê主題。"
},
"common_colors": {
"_tab_label": "一般",
"main": "一般ê色彩",
"foreground_hint": "請看「進階」分頁來調整khah幼ê所在",
"rgbo": "標頭、強調、徽章"
},
"advanced_colors": {
"_tab_label": "進階",
"alert": "警告ê背景",
"alert_error": "錯誤",
"alert_warning": "警告",
"alert_neutral": "其他ê",
"post": "PO文用者紹介",
"badge": "徽章ê背景",
"popover": "提示、目錄、跳出來ê",
"badge_notification": "通知",
"panel_header": "面枋ê標題",
"top_bar": "頂 liâu-á",
"borders": "框á邊",
"buttons": "鈕仔",
"inputs": "輸入框á",
"faint_text": "淺ê文字",
"underlay": "Tshū-á",
"wallpaper": "壁紙",
"poll": "投票數ê圖",
"icons": "標á",
"highlight": "強調ê要素",
"pressed": "Tshi̍h ê 時",
"selectedPost": "選擇ê PO文",
"selectedMenu": "選擇ê目錄項目",
"disabled": "關ê",
"toggled": "切換ê時",
"tabs": "分頁",
"chat": {
"incoming": "收著ê",
"outgoing": "送出ê",
"border": "框á邊"
}
},
"radii": {
"_tab_label": "邊á角ê khà-buh"
},
"shadows": {
"_tab_label": "影kap光",
"override": "Khàm掉",
"shadow_id": "影 #{value}",
"blur": "予n̄g-n̄g",
"spread": "Hōo 闊",
"inset": "內pîng",
"filter_hint": {
"always_drop_shadow": "警告tsit ê 影一直用 {0}若是瀏覽器支援tsē。",
"drop_shadow_syntax": "{0} 無支援參數 {1} kap 關鍵字 {2}。",
"avatar_inset": "請注意結合內pîng kap外pîng ê影佇標頭,可能佇透明ê標頭現無預料ê結果。",
"spread_zero": "若是「hōo 闊」ê值比0較大影ê顯示ē kap hōo 闊設做0 kâng款",
"inset_classic": "內pîng ê影ē用{0}"
},
"component": "部件",
"hintV3": "針對影lí mā ē當用 {0} 標示法,來用其他ê色彩 khang (slot)。",
"components": {
"panelHeader": "面枋ê標題",
"topBar": "頂 liâu-á",
"avatar": "用者ê標頭(佇個人資料欄位)",
"popup": "跳出來ê kap提醒",
"button": "鈕仔",
"buttonHover": "鈕仔(滑鼠ê指標khǹg佇面頂)",
"panel": "面枋",
"avatarStatus": "用者ê標頭(佇PO文ê顯示)",
"buttonPressedHover": "鈕仔(滑鼠指標leh khǹg 佇頂懸koh tshi̍h ê時)",
"buttonPressed": "鈕仔(leh tshi̍h ê時)",
"input": "輸入框á"
}
},
"fonts": {
"_tab_label": "字型",
"components": {
"interface": "界面",
"input": "輸入框á",
"post": "PO文",
"postCode": "RTF ê PO文ê平闊文字"
},
"family": "字型ê名",
"help": "揀界面元件所用ê字型。若是揀「家己指定」lí著輸入系統內ê字型正確ê名。",
"size": "Sài-suh(單位畫素)",
"weight": "字ê重(粗度)",
"custom": "家己指定"
},
"preview": {
"header": "先看māi",
"content": "內容",
"error": "錯誤ê例",
"button": "鈕á",
"text": "Tsē是{0}kap{1} ê例",
"mono": "內容",
"input": "Tú正kàu高雄ah。",
"faint_link": "有幫tsān ê手冊",
"fine_print": "讀guán ê {0},毋過學無有路用ê!",
"header_faint": "Tsē OK",
"checkbox": "我有讀過使用條款",
"link": "好ê細ê連結"
}
},
"upload": {
@ -594,11 +719,11 @@
"hide_all_muted_posts": "Khàm掉消音êPO文",
"max_thumbnails": "PO文ê縮小圖ê khòo-tah(無寫=無限制)",
"hide_isp": "Khàm 站臺特有ê面 pang",
"right_sidebar": "Kā 邊á liâu徙kah正手pîng",
"navbar_column_stretch": "伸導覽liâukah 欄平闊",
"right_sidebar": "Kā 邊á ê欄位徙kah正手pîng",
"navbar_column_stretch": "伸導覽liâukah 欄平闊",
"always_show_post_button": "一直顯示「新ê PO文」ê鈕仔",
"hide_wallpaper": "Khàm站臺ê壁紙",
"use_one_click_nsfw": "Tshi̍h 一ê就會當拍開敏感內容",
"use_one_click_nsfw": "Tshi̍h chi̍t 下就ē當拍開敏感內容",
"hide_post_stats": "Khàm PO文ê統計數據(比如kah 意ê額數)",
"hide_filtered_statuses": "Khàm 逐ê過濾掉êPO文",
"hide_wordfiltered_statuses": "Khàm詞語過濾掉ê狀態",
@ -627,7 +752,7 @@
"mute_bot_posts": "Kā 機器lâng ê PO文消音",
"hide_shoutbox": "Khàm 站臺ê留話pang",
"account_backup_description": "Tse 予 lí ē當 kā lín 口座 ê 資訊 kap PO 文載落來,毋過 in 猶無法度輸入kàu Pleroma口座 ê 內底。",
"theme_help_v2_1": "拍開選擇框á就 ē 當改掉一寡組件ê色彩kap無透明度。Ji̍h「清掉所有ê」,ē 恢復原來ê款。",
"theme_help_v2_1": "拍開選擇框á就 ē 當改掉一寡組件ê色彩kap無透明度。Ji̍h「Lóng清掉」,ē 恢復原來ê款。",
"preload_images": "Kā 圖片先載入",
"hide_user_stats": "Khàm 掉用者ê統計數據(比如:綴ê lâng額)",
"interfaceLanguage": "界面ê語言",
@ -678,7 +803,7 @@
"notification_visibility_mentions": "提起",
"notification_visibility_repeats": "轉送",
"notification_visibility_moves": "用者suá位",
"notification_visibility_emoji_reactions": "應",
"notification_visibility_emoji_reactions": "應",
"notification_visibility_polls": "Lí參與ê選舉辦suah佇",
"no_rich_text_description": "Po文mài用RTF格式",
"no_blocks": "無封鎖",
@ -690,7 +815,7 @@
"show_moderator_badge": "佇我ê個人資料顯示「管理員」證章",
"nsfw_clickthrough": "Khàm掉敏感ê媒體內容",
"oauth_tokens": "OAuth token",
"refresh_token": "頭the̍h token",
"refresh_token": "頭the̍h token",
"valid_until": "到期佇",
"revoke_token": "撤回",
"panelRadius": "面pang",
@ -716,12 +841,12 @@
"set_new_avatar": "設定新ê標頭",
"set_new_profile_background": "設定新ê個人資料ê背景",
"set_new_profile_banner": "設定新ê個人資料ê條á",
"reset_avatar": "Tuì頭設定標頭",
"reset_profile_background": "Tuì頭設個人資料ê背景",
"reset_profile_banner": "Tuì頭設個人資料ê條á",
"reset_avatar_confirm": "Lí敢確實beh tuì頭設定標頭?",
"reset_banner_confirm": "Lí敢確實beh tuì頭設定條á?",
"reset_background_confirm": "Lí敢確實beh tuì頭設定背景?",
"reset_avatar": "頭設定標頭",
"reset_profile_background": "頭設個人資料ê背景",
"reset_profile_banner": "頭設個人資料ê條á",
"reset_avatar_confirm": "Lí敢確實beh 頭設定標頭?",
"reset_banner_confirm": "Lí敢確實beh 頭設定條á?",
"reset_background_confirm": "Lí敢確實beh 頭設定背景?",
"settings": "設定",
"subject_input_always_show": "一直顯示主旨ê格á",
"subject_line_behavior": "回應ê時khóo-pih主旨",
@ -731,11 +856,11 @@
"conversation_display": "顯示對話ê風格",
"conversation_display_tree": "樹á ê形",
"disable_sticky_headers": "Mài 予欄位ê頭牢佇螢幕頂懸",
"show_scrollbars": "展示邊á liâu ê giú-á",
"show_scrollbars": "展示邊á ê欄位 ê giú-á",
"third_column_mode": "空間夠額ê時,展示第三ê欄位",
"third_column_mode_none": "不管時mài顯示第三ê欄位",
"third_column_mode_notifications": "通知ê欄位",
"third_column_mode_postform": "主要êPO文表kah導覽",
"third_column_mode_postform": "主要ê PO文表kah導覽",
"show_admin_badge": "佇我ê個人資料顯示「行政員」證章",
"pause_on_unfocused": "若是 Pleroma ê分頁無點開tiō 暫停更新",
"conversation_display_tree_quick": "樹á形ê展示",
@ -792,15 +917,437 @@
"notification_mutes": "若tsún無愛收tuì指定用者來ê通知著用消音。",
"notification_blocks": "封鎖用者ē停止所有i hia來ê通知mā取消訂伊。",
"enable_web_push_notifications": "拍開網頁sak通知ê功能",
"more_settings": "Koh較tsē ê設定"
"more_settings": "Koh較tsē ê設定",
"version": {
"title": "版本",
"backend_version": "後端ê版本",
"frontend_version": "前端ê版本"
},
"commit_value": "儲存",
"commit_value_tooltip": "值無儲存tshi̍h tsit ê 鈕仔來送出你改變ê",
"hard_reset_value": "硬ê重頭設",
"hard_reset_value_tooltip": "Suá掉儲存內底ê設定強制用預設ê值",
"reset_value": "重頭設",
"reset_value_tooltip": "重頭設草稿",
"hide_scrobbles": "Tshàng scrobble(記錄)",
"notification_show_extra": "顯示koh khah tsē ê通知佇通知ê欄位",
"notification_extra_chats": "顯示bô讀ê開講",
"notification_extra_announcements": "顯示bô讀ê公告",
"notification_extra_follow_requests": "顯示新ê跟tuè請求",
"notification_extra_tip": "顯示自訂其他通知ê撇步"
},
"status": {
"favorites": "收藏"
"favorites": "收藏",
"repeat_confirm_cancel_button": "Mài轉送",
"delete_confirm_title": "Thâi掉ê確認",
"edit": "編輯狀態",
"edited_at": "(頂kái編輯佇{time})",
"pin": "釘佇個人資料",
"unpin": "Tuì個人資料拆掉",
"pinned": "釘入去ê",
"bookmark": "加入冊籤",
"unbookmark": "Tuì冊籤the̍h掉",
"delete_confirm": "Lí kám真ê beh thâi掉tsit ê狀態?",
"delete_confirm_accept_button": "Thâi掉",
"delete_confirm_cancel_button": "保留",
"reply_to": "回應",
"replies_list": "回應:",
"repeats": "轉送",
"repeat_confirm_accept_button": "轉送",
"repeat_confirm_title": "轉送ê確認",
"repeat_confirm": "Lí kám真ê beh轉送tsit ê狀態?",
"delete": "Thâi掉身份",
"delete_error": "Thâi狀態ê時出tshê{0}",
"mentions": "提起",
"move_down": "Kā附件suá kàu正pîng",
"thread_show_full": "展示tsit 條討論線ê所有(lóng總有{numStatus}ê狀態,深度上限:{depth})",
"thread_follow": "看討論線tshun ê部份(lóng總有{numStatus}ê狀態)",
"replies_list_with_others": "回應(+其他{numReplies}ê):",
"mute_conversation": "Kā會話消音",
"unmute_conversation": "Kā會話取消消音",
"status_unavailable": "狀態bē當用",
"copy_link": "Khóo-pih 狀態ê連結",
"external_source": "外口ê來源",
"thread_muted": "討論線消音ah",
"thread_muted_and_words": ",有詞語:",
"hide_full_subject": "Khàm掉主題ê全文",
"show_full_subject": "顯示標題ê全文",
"show_content": "顯示內容",
"hide_content": "Khàm掉內容",
"status_deleted": "Tsit篇PO文thâi掉ah",
"nsfw": "敏感ê內容",
"expand": "Thián開",
"you": "(Lí)",
"plus_more": "Koh有{number}ê",
"many_attachments": "PO文有{number}ê附件",
"collapse_attachments": "Kā附件tshàng起來",
"show_all_attachments": "顯示逐ê附件",
"show_attachment_in_modal": "佇媒體模式顯示",
"show_attachment_description": "Kā敘述先看māi(拍開附件會當看kui ê敘述)",
"hide_attachment": "Khàm掉附件",
"attachment_stop_flash": "停止Flash ê播放器",
"remove_attachment": "Kā附件suá走",
"move_up": "Kā附件suá kàu倒pîng",
"open_gallery": "拍開畫廊",
"thread_hide": "Khàm掉討論線",
"thread_show": "顯示討論線",
"thread_show_full_with_icon": "{icon} {text}",
"thread_follow_with_icon": "{icon} {text}",
"ancestor_follow": "看其他{numReplies}ê佇tsit ê狀態ê回應",
"ancestor_follow_with_icon": "{icon} {text}",
"show_all_conversation_with_icon": "{icon} {text}",
"show_all_conversation": "看kui ê會話(有其他{numStatus}ê狀態)",
"show_only_conversation_under_this": "Kan-ta顯示tsit ê狀態ê回應",
"status_history": "狀態ê歷史",
"reaction_count_label": "{num}ê lâng用表情反應",
"hide_quote": "Khàm條引用ê狀態",
"display_quote": "顯示引用ê狀態",
"invisible_quote": "引用ê狀態bē當用{link}",
"more_actions": "佇tsit ê狀態ê其他動作"
},
"user_card": {
"favorites": "收藏"
"favorites": "收藏",
"show_repeats": "顯示轉送",
"hide_repeats": "Khàm掉轉送",
"remove_follower_confirm": "Lí kám真正想beh kā {user} tuì lí所跟綴ê suá走?",
"statuses": "狀態",
"admin_menu": {
"activate_account": "啟動口座",
"deactivate_account": "予口座失效",
"delete_account": "Thâi掉口座",
"force_nsfw": "Kā逐ê PO文標做敏感內容",
"strip_media": "Tuì PO文thâi掉媒體",
"force_unlisted": "強制PO文mài列佇公共時間線",
"disable_remote_subscription": "Mài允准tuì其他站臺跟tuè用者",
"sandbox": "強制PO文kan-ta予跟tuè ê看",
"disable_any_subscription": "Mài允准跟tuè任何用者",
"quarantine": "Tuì聯邦禁止用者ê PO文",
"delete_user": "Thâi掉用者ê口座",
"delete_user_data_and_deactivate_confirmation": "Án-ne ē永永thâi掉tsit ê口座ê資料兼hōo失效。Lí kám完全確定?",
"grant_admin": "授與行政員ê權",
"revoke_admin": "撤掉行政員ê權",
"moderation": "仲裁",
"grant_moderator": "授與仲裁員ê權",
"revoke_moderator": "撤掉仲裁員ê權"
},
"highlight": {
"disabled": "Mài強調",
"side": "邊á ê花tsuā",
"solid": "孤色ê背景",
"striped": "花tsuā ê背景"
},
"note": "筆記",
"note_blank": "(無)",
"edit_note": "編輯筆記",
"edit_note_apply": "適用",
"approve": "核准",
"approve_confirm_title": "核准ê確認",
"approve_confirm_accept_button": "核准",
"approve_confirm_cancel_button": "Mài核准",
"block": "封鎖",
"blocked": "封鎖ah",
"block_confirm_title": "封鎖ê確認",
"approve_confirm": "Lí kám想beh核准{user}ê跟tuè請求?",
"block_confirm": "Lí kám 真正想beh封鎖{user}?",
"block_confirm_accept_button": "封鎖",
"block_confirm_cancel_button": "Mài封鎖",
"deactivated": "停止使用ah",
"deny": "拒絕",
"deny_confirm_title": "拒絕ê確認",
"deny_confirm_accept_button": "拒絕",
"deny_confirm_cancel_button": "Mài拒絕",
"deny_confirm": "Lí kám想beh拒絕{user}ê跟tuè請求?",
"edit_profile": "編輯個人資料",
"follow": "跟tuè",
"follow_cancel": "取消請求",
"follow_sent": "請求送ah",
"follow_progress": "Teh請求……",
"follow_unfollow": "無愛跟tuè",
"unfollow_confirm_title": "無愛跟tuè ê確認",
"unfollow_confirm": "lí kám真正無beh跟tuè {user}?",
"unfollow_confirm_accept_button": "無愛跟綴",
"unfollow_confirm_cancel_button": "繼續跟tuè",
"followees": "Teh跟綴",
"followers": "跟綴ê",
"following": "Teh跟tuè",
"follows_you": "跟tuè lí",
"hidden": "Tshàng起來ê",
"its_you": "Tse是lí",
"media": "媒體",
"mention": "提起",
"message": "短phue",
"mute": "消音",
"muted": "消音ê",
"mute_confirm_title": "消音ê確認",
"mute_confirm": "Lí確定想beh kā {user}消音?",
"mute_confirm_accept_button": "消音",
"mute_confirm_cancel_button": "Mài消音",
"mute_duration_prompt": "消音tsit ê用戶ê期限(0表示永遠)",
"per_day": "/kang",
"remote_follow": "遠距離ê關注",
"remove_follower": "Suá走跟綴ê",
"remove_follower_confirm_title": "Suá走跟tuè者ê確認",
"remove_follower_confirm_accept_button": "Suá走",
"remove_follower_confirm_cancel_button": "保留",
"report": "檢舉",
"subscribe": "注文",
"unsubscribe": "取消注文",
"unblock": "Mài封鎖",
"unblock_progress": "Teh取消封鎖……",
"block_progress": "Leh封鎖……",
"unmute": "Mài消音",
"mute_progress": "Leh消音……",
"unmute_progress": "Leh取消消音……",
"bot": "機器lâng",
"birthday": "出世佇{birthday}",
"edit_note_cancel": "取消"
},
"tool_tip": {
"favorite": "收藏"
"favorite": "收藏",
"repeat": "轉送",
"media_upload": "Kā媒體傳起去",
"reply": "回應",
"add_reaction": "加反應",
"user_settings": "用者ê設定",
"accept_follow_request": "允准跟tuè ê請求",
"reject_follow_request": "拒絕跟tuè ê請求",
"bookmark": "冊籤",
"toggle_expand": "Thián開á是tshàng通知顯示kui篇PO文",
"toggle_mute": "Thián開á是tshàng通知顯露消音ê內容",
"autocomplete_available": "{number} ê結果通用。用頂kap下ê key來看結果。"
},
"password_reset": {
"instruction": "輸入你ê email地址iah是用者ê名。阮ē寄予lí連結通重頭設你ê密碼。",
"password_reset_disabled": "密碼重頭設ê功能無開放。請聯絡lín站臺ê行政員。",
"password_reset_required_but_mailer_is_disabled": "Lí著重設密碼M̄-koh重頭設密碼ê功能無開放。請聯絡lín站臺ê行政員。",
"forgot_password": "Buē記得密碼?",
"password_reset": "密碼重頭設",
"placeholder": "你ê email iah是用者ê名",
"check_email": "檢查你ê電子phue箱有重頭設密碼ê連結ê phue無。",
"return_home": "Tńg去頭頁",
"too_many_requests": "Lí已經kàu 試ê回數限制 ah小等leh koh試。",
"password_reset_required": "Lí著重設密碼tsiah通登入。"
},
"admin_dash": {
"window_title": "行政員",
"reset_all": "Kui ê重頭設",
"wip_notice": "Tsit ê 管理 la-jí-báng (dashboard) 是試驗êkoh teh 起做,{adminFeLink}.",
"old_ui_link": "舊ê管理界面佇tsia",
"commit_all": "Lóng總儲存",
"tabs": {
"nodb": "無資料庫ê設置",
"instance": "站臺",
"limits": "限制",
"frontends": "前端"
},
"nodb": {
"heading": "資料庫設置無開放",
"text": "Lí需要改後端ê設置檔案tsiah ē當kā{property}設做{value},請佇{documentation}了解詳細。",
"documentation": "文件",
"text2": "大部份ê設定ē無開放。"
},
"limits": {
"user_uploads": "個人資料ê媒體限制",
"arbitrary_limits": "任何限制",
"posts": "PO文ê限制",
"uploads": "附件ê限制",
"users": "用者個人資料ê限制",
"profile_fields": "個人資料欄位ê限制"
},
"captcha": {
"native": "在來ê",
"kocaptcha": "KoCaptcha"
},
"instance": {
"instance": "站臺ê資訊",
"registrations": "用者ê註冊",
"kocaptcha": "KoCaptcha ê設定",
"restrict": {
"header": "管制無落名ê訪客使用",
"timelines": "讀取時間線",
"profiles": "讀取用者ê個人資料",
"activities": "讀取狀態/活動",
"description": "(無)允准一kuá方面ê API the̍h取資源ê詳細設定。預設(無定ê狀態)若是站臺毋是公開êē無允准the̍h取選擇框á若勾就算站臺是公開êiáu是無允准the̍h取若無勾就算站臺是私人êmā是允准the̍h取。請注意若是設一kuá設定無預料ê行為可能產生。比如講若是the̍h取個人資料無開放PO文buē顯示個人資料。"
},
"access": "讀取實體",
"captcha_header": "CAPTCHA"
},
"frontend": {
"repository": "原始碼庫ê連結",
"versions": "通用ê版本",
"build_url": "起做URL",
"reinstall": "重頭安裝",
"is_default": "(預設)",
"is_default_custom": "(預設,版本:{version})",
"install": "安裝",
"install_version": "安裝ê版本:{version}",
"more_install_options": "其他ê安裝選項",
"more_default_options": "其他ê預設設定ê選項",
"set_default": "設做預設ê",
"set_default_version": "Kā版本{version}設做預設ê",
"default_frontend_tip": "預設ê前端ē展示予逐ê用者。現在用者無法度揀個人ê前端。若是lí變換無beh用PleromaFE上有可能ē用舊koh問題tsē ê AdminFE 做站臺ê設置佇阮iáu-bē kā伊取代以前。",
"wip_notice": "請注意tsit ê段落iáu teh起做欠缺一寡特點因為後端tuì前端管理ê實做無齊備。",
"default_frontend": "預設ê前端",
"default_frontend_tip2": "Teh起做因為Pleroma後端無適當列出逐ê安裝ê前端lí著手動輸入名字kap引用。下kha ê列單提供寫tsiah-ê 值ê近路。",
"available_frontends": "Ē當安裝"
},
"temp_overrides": {
":pleroma": {
":instance": {
":public": {
"description": "無開放tseē 控制逐êAPI干焦予登入ê用者用mā ē予公開kap聯邦ê時間線buē當予無落名ê訪客the̍h著。",
"label": "站臺是公開ê"
},
":limit_to_local_content": {
"label": "Kan-ta會當tshuē在地ê內容",
"description": "無開放無認證ê用者、逐儂猶是lóng總開放tshuē全球ê網路"
},
":description_limit": {
"label": "限制",
"description": "附件說明ê字元限制"
},
":background_image": {
"label": "背景ê影像",
"description": "背景ê影像(主要予PleromaFE用)"
}
}
}
}
},
"timeline": {
"up_to_date": "是上新ê",
"collapse": "疊起來",
"conversation": "會話",
"error": "佇the̍h時間線ê時出tshê{0}",
"load_older": "載入舊ê狀態",
"repeated": "轉送ah",
"no_retweet_hint": "PO文hőng標做限定跟綴êá是私人phue無法度轉送",
"show_new": "看新ê",
"reload": "重新載入",
"no_more_statuses": "無其他ê狀態",
"no_statuses": "無狀態",
"socket_reconnected": "實時ê連結成立ah",
"socket_broke": "實時連結拍m̄見ahCloseEvent代碼{0}",
"quick_view_settings": "快速 view ê設定",
"quick_filter_settings": "快速過濾器ê設定"
},
"time": {
"unit": {
"days": "{0}工",
"days_short": "{0}工",
"hours": "{0}點鐘",
"hours_short": "{0}點鐘",
"minutes": "{0}分鐘",
"minutes_short": "{0}分",
"months": "{0}個月",
"months_short": "{0}個月",
"seconds": "{0}秒鐘",
"seconds_short": "{0}秒",
"weeks": "{0}禮拜",
"weeks_short": "{0}週",
"years": "{0}年",
"years_short": "{0}年"
},
"in_future": "koh有{0}",
"in_past": "{0}進前",
"now": "tú正",
"now_short": "tsit-má"
},
"user_reporting": {
"title": "檢舉 {0}",
"forward_description": "Tsit ê口座是別ê站臺ê。Mā kám beh寄報告ê khóo-pih kàu hit ê站?",
"add_comment_description": "本檢舉ē 寄kàu你ê站臺ê仲裁員。Lí會當佇下kha解說檢舉tsit ê口座ê原因:",
"additional_comments": "其他ê意見",
"forward_to": "轉送kàu{0}",
"submit": "送出",
"generic_error": "佇處理lí ê請求ê時出tshê。"
},
"lists": {
"really_delete": "Kám真正 beh thâi列單?",
"search": "Tshiau-tshuē用者",
"create": "建立",
"save": "保存改變",
"delete": "Thâi列單",
"lists": "列單",
"new": "新ê列單",
"title": "列單ê標題",
"following_only": "限制佇跟tuè ê",
"manage_lists": "管理列單",
"manage_members": "管理列單ê成員",
"add_members": "Tshiau-tshuē其他ê用者",
"remove_from_list": "Tuì列單suá走",
"add_to_list": "Ke-thinn kàu列單",
"is_in_list": "已經佇列單內底",
"editing_list": "編輯列單 {listTitle}",
"creating_list": "開新ê列單",
"update_title": "保存標題",
"error": "佇操作列單ê時出tshê{0}"
},
"update": {
"update_bugs": "請報告任何問題kap錯誤佇 {pleromaGitlab}因為已經改變真tsē。雖bóng guán徹底試過ka-kī mā用開發版iáu是有可能有無注意ê所在。Guán歡迎lí tuì所tú tio̍h ê問題提出意見kap建議或者是改進Pleroma kap Pleroma-FE ê方法。",
"big_update_title": "請sió等tsi̍t ê",
"update_bugs_gitlab": "Pleroma GitLab",
"big_update_content": "Guán已經有tsi̍t段時間無推出發行所以外觀kap感覺kap lí所慣勢ê凡勢無kâng。",
"update_changelog": "Beh知影改變ê詳細請看{theFullChangelog}。",
"update_changelog_here": "Changelog全文",
"art_by": "美術製作:{linkToArtist}"
},
"user_profile": {
"timeline_title": "用者ê時間線",
"profile_does_not_exist": "Pháinn勢tsit ê個人資料無佇leh。",
"profile_loading_error": "Pháinn勢佇載入tsit ê個人資料ê時出tshê。"
},
"who_to_follow": {
"more": "詳情",
"who_to_follow": "Siáng通tuè"
},
"upload": {
"error": {
"base": "傳起去ê時失敗。",
"message": "傳起去ê時失敗:{0}",
"file_too_big": "檔案siūnn大[{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]",
"default": "Koh試tsi̍t kái"
},
"file_size_units": {
"B": "B",
"KiB": "KiB",
"MiB": "MiB",
"GiB": "GiB",
"TiB": "TiB"
}
},
"search": {
"people": "Lâng",
"hashtags": "井字ê標籤",
"person_talking": "{count}ê lâng teh開講",
"people_talking": "{count}ê lâng teh開講",
"no_results": "無結果",
"no_more_results": "無其他結果",
"load_more": "載入其他結果"
},
"chats": {
"you": "Lí",
"message_user": "送短phue予{nickname}",
"delete": "Thâi掉",
"chats": "開講",
"new": "新ê開講",
"empty_message_error": "Bē當PO空ê短phue",
"more": "其他",
"delete_confirm": "Lí kám真正beh thâi tsit ê短phue?",
"error_loading_chat": "佇載入開講ê時出問題。",
"error_sending_message": "佇送短phue ê時出問題。",
"empty_chat_list_placeholder": "Lí iáu buē開講過。開始開講"
},
"file_type": {
"audio": "聲音",
"video": "影片",
"image": "影像",
"file": "檔案"
},
"display_date": {
"today": "今á日"
},
"unicode_domain_indicator": {
"tooltip": "Tsit ê域名含m̄是ascii ê字元。"
}
}

ファイルの表示

@ -121,7 +121,8 @@
"placeholder": "напр. stepan",
"logout_confirm": "Ви дійсно хочете вийти?",
"logout_confirm_accept_button": "Вийти",
"logout_confirm_cancel_button": "Ні, хочу назад!"
"logout_confirm_cancel_button": "Ні, хочу назад!",
"logout_confirm_title": "Вихід"
},
"importer": {
"error": "Під час імпортування файлу сталася помилка.",
@ -164,7 +165,13 @@
"broken_favorite": "Невідомий допис, шукаю його…",
"error": "Помилка при оновленні сповіщень: {0}",
"poll_ended": "опитування закінчено",
"submitted_report": "подав скаргу"
"submitted_report": "подав скаргу",
"unread_announcements": "{num} непрочитане оголошення | {num} непрочитаних оголошень",
"unread_chats": "{num} непрочитаний чат | {num} непрочитаних чатів",
"unread_follow_requests": "{num} новий запит на підписку | {num} нових запитів на підписку",
"configuration_tip": "Ви можете налаштувати, що відображати тут у {theSettings}. {dismiss}",
"configuration_tip_settings": "налаштування",
"configuration_tip_dismiss": "Не показувати знову"
},
"nav": {
"chats": "Чати",
@ -267,7 +274,8 @@
"activities": "Активності",
"symbols": "Символи",
"travel-and-places": "Подорожі та Місця"
}
},
"unpacked": "Розпаковані емоджі"
},
"post_status": {
"content_type": {
@ -304,7 +312,11 @@
"post": "Опублікувати",
"edit_unsupported_warning": "Pleroma не підтримує редагування згадувань чи голосувань.",
"edit_status": "Редагувати допис",
"edit_remote_warning": "Інші віддалені інстанси можуть не підтримувати редагування та вони можуть не отримати актуальну версію допису."
"edit_remote_warning": "Інші віддалені інстанси можуть не підтримувати редагування та вони можуть не отримати актуальну версію допису.",
"content_type_selection": "Форматування допису",
"scope_notice_dismiss": "Закрити це сповіщення",
"reply_option": "Відповісти на цей допис",
"quote_option": "Процитувати допис"
},
"settings": {
"blocks_imported": "Блокування імпортовані! Їх обробка триватиме певний час.",
@ -730,7 +742,7 @@
"conversation_display_tree_quick": "Вигляд дерева",
"disable_sticky_headers": "Не закріплювати заголовок колонки зверху на сторінці",
"third_column_mode_none": "Не показувати третю колонку взагалі",
"third_column_mode_notifications": "Колонка сповіщень",
"third_column_mode_notifications": "Колонку сповіщень",
"columns": "Колонки",
"auto_update": "Автоматично показувати нові дописи",
"use_websockets": "Використовувати вебсокети (Оновлення в реальному часі)",
@ -743,7 +755,38 @@
"wordfilter": "Фільтр слів",
"mention_links": "Посилання для згадування",
"user_profiles": "Профілі користувачів",
"notification_visibility_polls": "Закінчення опитувань, в яких ви проголосували"
"notification_visibility_polls": "Закінчення опитувань, в яких ви проголосували",
"remove_language": "Вилучити",
"primary_language": "Основна мова:",
"fallback_language": "Резервна мова {index}:",
"confirm_dialogs_deny_follow": "тим, як відмовити у запиті на підписку",
"confirm_dialogs_remove_follower": "видаленням підписника",
"notification_show_extra": "Показувати додаткові сповіщення в панелі сповіщень",
"notification_extra_chats": "Показувати непрочитані чати",
"notification_extra_announcements": "Показувати непрочитані оголошення",
"notification_extra_follow_requests": "Показувати нові запити на підписку",
"third_column_mode_postform": "Форму відправки повідомлень та панель навігації",
"notification_extra_tip": "Показати пораду з налаштувань для додаткових сповіщень",
"backup_running": "Резервне копіювання триває, оброблено {number} записи. | Резервне копіювання триває, оброблено {number} записів.",
"backup_failed": "Резервне копіювання не вдалося.",
"preview": "Попередній перегляд",
"url": "URL",
"birthday": {
"label": "День народження",
"show_birthday": "Показувати мій день народження"
},
"confirm_dialogs": "Запитувати підтвердження перед",
"confirm_dialogs_repeat": "поширенням допису",
"confirm_dialogs_unfollow": "скасуванням підписки",
"confirm_dialogs_block": "блокуванням користувача",
"confirm_dialogs_mute": "тим, як заглушити користувача",
"show_scrollbars": "Показувати смугу прокрутки на бічних панелях",
"column_sizes": "Розміри панелей",
"column_sizes_sidebar": "Бічна панель",
"add_language": "Додати резервну мову",
"confirm_dialogs_delete": "видаленням допису",
"confirm_dialogs_logout": "виходом із системи",
"confirm_dialogs_approve_follow": "схваленням запиту на підписку"
},
"selectable_list": {
"select_all": "Вибрати все"
@ -760,7 +803,8 @@
"password_required": "не може бути порожнім",
"email_required": "не може бути порожнім",
"fullname_required": "не може бути порожнім",
"username_required": "не може бути порожнім"
"username_required": "не може бути порожнім",
"birthday_required": "не може бути пустим"
},
"bio_placeholder": "напр.\nНаш народ завжди прагне волі для себе і бажає її для інших народів. Він боровся і бореться за правду і справедливість. Ми хочемо жити у згоді і взаємному шануванні з усіми народами доброї волі. Такі самі права визнаємо за іншими народами, за які боремося для себе.",
"fullname_placeholder": "напр. Степан Бандера",
@ -778,7 +822,9 @@
"reason": "Причина реєстрації",
"bio_optional": "Біографія (необов'язково)",
"email_language": "Якою мовою ви бажаєте отримувати електронні листи від сервера?",
"email_optional": "Ел. пошта (необов'язково)"
"email_optional": "Ел. пошта (необов'язково)",
"birthday": "День народження:",
"birthday_optional": "День народження (необов'язково):"
},
"who_to_follow": {
"who_to_follow": "На кого підписатися",
@ -890,7 +936,8 @@
"grant_moderator": "Надати права модератора",
"revoke_admin": "Позбавити прав адміністратора",
"grant_admin": "Надати права адміністратора",
"quarantine": "Не розповсюджувати дописи на інших інстансах"
"quarantine": "Не розповсюджувати дописи на інших інстансах",
"delete_user_data_and_deactivate_confirmation": "Це назовсім видалить дані обліковки й вимкне її. Точно продовжити?"
},
"deny": "Відмовити",
"block": "Заблокувати",
@ -929,7 +976,32 @@
"bot": "Бот",
"edit_profile": "Редагувати профіль",
"deactivated": "Деактивований",
"follow_cancel": "Скасувати запит"
"follow_cancel": "Скасувати запит",
"block_confirm_title": "Блокування",
"block_confirm": "Точно заблокувати {user}?",
"mute_confirm_cancel_button": "Ні, не приглушувати",
"note_blank": "(Пусто)",
"edit_note_apply": "Застосувати",
"edit_note_cancel": "Скасувати",
"block_confirm_accept_button": "Так, заблокувати",
"block_confirm_cancel_button": "Ні, не блокувати",
"deny_confirm_title": "Відхилити запит на підписку",
"mute_confirm_accept_button": "Так, приглушити",
"mute_confirm": "Точно приглушити {user}?",
"edit_note": "Редагувати нотатку",
"mute_confirm_title": "Приглушення",
"mute_duration_prompt": "Приглушити користувача на (0 якщо назавжди):",
"approve_confirm_title": "Дозвіл підписатись",
"approve_confirm_accept_button": "Так, дозволити",
"approve_confirm_cancel_button": "Ні, скасувати",
"deny_confirm_accept_button": "Так, відхилити",
"deny_confirm_cancel_button": "Ні, скасувати",
"deny_confirm": "Ви точно хочете відхилити запит на підписку від {user}?",
"unfollow_confirm_title": "Відписка",
"unfollow_confirm": "Точно відписатись від {user}?",
"unfollow_confirm_accept_button": "Так, відписатись",
"unfollow_confirm_cancel_button": "Ні, не відписуватись",
"note": "Приватна нотатка"
},
"status": {
"copy_link": "Скопіювати посилання на допис",
@ -965,7 +1037,38 @@
"plus_more": "+{number} більше",
"thread_show_full_with_icon": "{icon} {text}",
"show_only_conversation_under_this": "Показати всі відповіді на цей допис",
"status_history": "Історія змін"
"status_history": "Історія змін",
"thread_hide": "Сховати гілку",
"open_gallery": "Відкрити галерею",
"repeat_confirm": "Точно поширити допис?",
"repeat_confirm_title": "Підтвердьте поширення",
"repeat_confirm_accept_button": "Так, поширити",
"repeat_confirm_cancel_button": "Ні, не поширювати",
"delete_error": "Помилка при видаленні допису: {0}",
"delete_confirm_accept_button": "Так, видалити",
"delete_confirm_cancel_button": "Ні, лишити",
"delete_confirm_title": "Підтвердьте видалення",
"you": "(ви)",
"collapse_attachments": "Згорнути вкладення",
"show_all_attachments": "Показати всі вкладення",
"hide_attachment": "Сховати вкладення",
"many_attachments": "Вкладень: {number} | Вкладень: {number}",
"attachment_stop_flash": "Зупинити Flash-плеєр",
"thread_follow": "Ще відповідей: {numStatus} | Ще відповідей: {numStatus}",
"remove_attachment": "Видалити вкладення",
"ancestor_follow": "Переглянути ще {numReplies} під цим дописом | Переглянути ще {numReplies} під цим дописом",
"show_all_conversation": "Показати всю розмову (ще дописів: {numStatus}) | Показати всю розмову (ще дописів: {numStatus})",
"move_up": "Посунути вкладення ліворуч",
"move_down": "Посунути вкладення праворуч",
"thread_show": "Показати гілку",
"mentions": "Згадки",
"thread_show_full": "Показати відповіді: {numStatus} | Показати відповіді: {numStatus}",
"hide_quote": "Сховати процитований допис",
"display_quote": "Показати процитований допис",
"invisible_quote": "Процитований допис недоступний: {link}",
"replies_list_with_others": "Ще відповідей: {numReplies} | Ще відповідей: {numReplies}:",
"show_attachment_in_modal": "Показати вкладення у вікні",
"show_attachment_description": "Переглянути опис (натисніть саме вкладення, якщо опис не вміщається)"
},
"timeline": {
"no_more_statuses": "Більше немає дописів",
@ -980,7 +1083,8 @@
"repeated": "поширив(-ла)",
"no_retweet_hint": "Запис, позначено як \"тільки для підписників\" або \"особисте\" і тому не може бути поширений",
"socket_broke": "Втрачено з'єднання у реальному часі: код {0}",
"socket_reconnected": "Встановлено з'єднання у реальному часі"
"socket_reconnected": "Встановлено з'єднання у реальному часі",
"quick_view_settings": "Налаштування швидкого перегляду"
},
"user_reporting": {
"submit": "Відправити",
@ -1026,5 +1130,35 @@
"submit_edit_action": "Надіслати",
"cancel_edit_action": "Скасувати",
"inactive_message": "Це оголошення неактивне"
},
"lists": {
"really_delete": "Дійсно видалити список?",
"error": "Помилка при роботі зі списками: {0}",
"is_in_list": "Вже є у списку",
"editing_list": "Редагування списку {listTitle}",
"creating_list": "Створення нового списку",
"search": "Знайти користувачів",
"create": "Створити",
"save": "Зберегти зміни",
"manage_members": "Керувати учасниками списку",
"new": "Новий список",
"title": "Назва списку",
"delete": "Видалити список",
"following_only": "Лише за ким ви стежите",
"lists": "Списки",
"manage_lists": "Керувати списками",
"remove_from_list": "Видалити зі списку",
"add_to_list": "Додати до списку"
},
"update": {
"update_changelog": "Щоб дізнатись більше інформації, дивіться {theFullChangelog}.",
"update_bugs": "Будь ласка, повідомляйте про будь-які проблеми та помилки на {pleromaGitlab}, оскільки ми внесли багато змін, і навіть після ретельно проведених перевірок, ми можемо щось пропустити. Ми заздалегідь вдячні за ваші відгуки щодо проблем, з якими ви можете зіткнутися, а також пропозиції щодо вдосконалення Pleroma та Pleroma-FE.",
"update_changelog_here": "повний список змін",
"big_update_title": "Хвилинку уваги",
"update_bugs_gitlab": "Pleroma GitLab",
"big_update_content": "У нас не було оновлень протягом тривалого часу, тому речі можуть мати інакший вигляд, аніж ви звикли."
},
"unicode_domain_indicator": {
"tooltip": "Цей домен містить не-ASCII символи."
}
}

ファイルの表示

@ -146,7 +146,13 @@
"follow_request": "想要关注你",
"error": "取得通知时发生错误:{0}",
"poll_ended": "投票结束了",
"submitted_report": "提交举报"
"submitted_report": "提交举报",
"unread_announcements": "{num} 条未读公告",
"unread_chats": "{num} 条未读聊天讯息",
"unread_follow_requests": "{num} 个新关注请求",
"configuration_tip": "可以在 {theSettings} 里定制什么会显示在这里。{dismiss}",
"configuration_tip_settings": "设置",
"configuration_tip_dismiss": "不再显示"
},
"polls": {
"add_poll": "增加投票",
@ -212,7 +218,9 @@
"edit_unsupported_warning": "Pleroma 不支持对提及或投票进行编辑。",
"edit_status": "编辑状态",
"content_type_selection": "发帖格式",
"scope_notice_dismiss": "关闭此提示"
"scope_notice_dismiss": "关闭此提示",
"reply_option": "回复这条状态",
"quote_option": "引用这条状态"
},
"registration": {
"bio": "简介",
@ -747,7 +755,12 @@
"reset_value_tooltip": "重置草稿",
"hard_reset_value": "硬重置",
"hard_reset_value_tooltip": "从存储中移除设置,强制使用默认值",
"emoji_reactions_scale": "表情回应比例系数"
"emoji_reactions_scale": "表情回应比例系数",
"notification_show_extra": "在通知栏里显示额外通知",
"notification_extra_chats": "显示未读聊天",
"notification_extra_announcements": "显示未读公告",
"notification_extra_follow_requests": "显示新的关注请求",
"notification_extra_tip": "显示额外通知的定制提示"
},
"time": {
"day": "{0} 天",
@ -880,7 +893,10 @@
"show_attachment_in_modal": "在媒体模式中显示",
"status_history": "状态历史",
"delete_error": "删除状态时出错:{0}",
"reaction_count_label": "{num} 人作出了表情回应"
"reaction_count_label": "{num} 人作出了表情回应",
"invisible_quote": "引用的状态不可用:{link}",
"hide_quote": "隐藏引用的状态",
"display_quote": "显示引用的状态"
},
"user_card": {
"approve": "核准",
@ -1184,7 +1200,7 @@
"big_update_title": "请忍耐一下",
"big_update_content": "我们已经有一段时间没有发布发行版,所以事情的外观和感觉可能与你习惯的不一样。",
"update_bugs": "请在 {pleromaGitlab} 上报告任何问题和bug因为我们已经改变了很多虽然我们进行了彻底的测试并且自己使用了开发版本但我们可能错过了一些东西。我们欢迎你对你可能遇到的问题或如何改进Pleroma和Pleroma-FE提出反馈和建议。",
"art_by": "Art by {linkToArtist}"
"art_by": "{linkToArtist} 的作品"
},
"lists": {
"search": "搜索用户",
@ -1266,7 +1282,6 @@
"wip_notice": "请注意此部分是一个WIP缺乏某些功能因为前端管理的后台实现并不完整。",
"default_frontend": "默认前端",
"default_frontend_tip": "默认的前端将显示给所有用户。目前还没有办法让用户选择个人的前端。如果你不使用 PleromaFE你很可能不得不使用旧的和有问题的 AdminFE 来进行实例配置,直到我们替换它。",
"default_frontend_tip2": "WIP: 由于 Pleroma 后端没有正确列出所有已安装的前端,你必须手动输入名称和引用。下面的列表提供了填写这些值的快捷方式。",
"available_frontends": "可供安装"
},
"temp_overrides": {

ファイルの表示

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

ファイルの表示

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

ファイルの表示

@ -26,6 +26,7 @@ const adminSettingsStorage = {
},
setAvailableFrontends (state, { frontends }) {
state.frontends = frontends.map(f => {
f.installedRefs = f.installed_refs
if (f.name === 'pleroma-fe') {
f.refs = ['master', 'develop']
} else {
@ -104,7 +105,6 @@ const adminSettingsStorage = {
}
set(config, path, convert(c.value))
})
console.log(config[':pleroma'])
commit('updateAdminSettings', { config, modifiedPaths })
commit('resetAdminDraft')
},
@ -122,7 +122,6 @@ const adminSettingsStorage = {
const descriptions = {}
backendDescriptions.forEach(d => convert(d, '', descriptions))
console.log(descriptions[':pleroma']['Pleroma.Captcha'])
commit('updateAdminDescriptions', { descriptions })
},

ファイルの表示

@ -40,6 +40,7 @@ export const defaultState = {
padEmoji: true,
hideAttachments: false,
hideAttachmentsInConv: false,
hideScrobbles: false,
maxThumbnails: 16,
hideNsfw: true,
preloadImage: true,
@ -65,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,
@ -117,8 +131,16 @@ export const defaultState = {
conversationTreeAdvanced: undefined, // instance default
conversationOtherRepliesButton: undefined, // instance default
conversationTreeFadeAncestors: undefined, // instance default
showExtraNotifications: undefined, // instance default
showExtraNotificationsTip: undefined, // instance default
showChatsInExtraNotifications: undefined, // instance default
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

ファイルの表示

@ -103,8 +103,16 @@ const defaultState = {
conversationTreeAdvanced: false,
conversationOtherRepliesButton: 'below',
conversationTreeFadeAncestors: false,
showExtraNotifications: true,
showExtraNotificationsTip: true,
showChatsInExtraNotifications: true,
showAnnouncementsInExtraNotifications: true,
showFollowRequestsInExtraNotifications: true,
maxDepthInThread: 6,
autocompleteSelect: false,
closingDrawerMarksAsSeen: true,
unseenAtTop: false,
ignoreInactionableSeen: false,
// Nasty stuff
customEmoji: [],
@ -128,6 +136,7 @@ const defaultState = {
mediaProxyAvailable: false,
suggestionsEnabled: false,
suggestionsWeb: '',
quotingAvailable: false,
// Html stuff
instanceSpecificPanelContent: '',

169
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

ファイルの表示

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

ファイルの表示

@ -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,21 +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(),
@ -120,8 +106,24 @@ const sortTimeline = (timeline) => {
return timeline
}
const getLatestScrobble = (state, user) => {
if (state.scrobblesNextFetch[user.id] && state.scrobblesNextFetch[user.id] > Date.now()) {
return
}
state.scrobblesNextFetch[user.id] = Date.now() + 24 * 60 * 60 * 1000
apiService.fetchScrobbles({ accountId: user.id }).then((scrobbles) => {
if (scrobbles.length > 0) {
user.latestScrobble = scrobbles[0]
state.scrobblesNextFetch[user.id] = Date.now() + 60 * 1000
}
})
}
// Add status to the global storages (arrays and objects maintaining statuses) except timelines
const addStatusToGlobalStorage = (state, data) => {
getLatestScrobble(state, data.user)
const result = mergeOrAdd(state.allStatuses, state.allStatusesObject, data)
if (result.new) {
// Add to conversation
@ -137,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)) {
@ -229,6 +215,10 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
timelineObject.newStatusCount += 1
}
if (status.quote) {
addStatus(status.quote, /* showImmediately = */ false, /* addToTimeline = */ false)
}
return status
}
@ -282,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
},
@ -317,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) {
@ -375,7 +305,6 @@ const removeStatus = (state, { timeline, userId }) => {
export const mutations = {
addNewStatuses,
addNewNotifications,
removeStatus,
showNewStatuses (state, { timeline }) {
const oldTimeline = (state.timelines[timeline])
@ -397,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]
@ -482,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
},
@ -588,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] }))
@ -700,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 }),

ファイルの表示

@ -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) => {
@ -498,7 +498,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)
@ -651,6 +651,12 @@ const users = {
const response = data.error
// Authentication failed
commit('endLogin')
// remove authentication token on client/authentication errors
if ([400, 401, 403, 422].includes(response.status)) {
commit('clearToken')
}
if (response.status === 401) {
reject(new Error('Wrong username or password'))
} else {
@ -661,7 +667,7 @@ const users = {
resolve()
})
.catch((error) => {
console.log(error)
console.error(error)
commit('endLogin')
reject(new Error('Failed to connect to server, try again'))
})

ファイルの表示

@ -107,6 +107,7 @@ const PLEROMA_ANNOUNCEMENTS_URL = '/api/v1/pleroma/admin/announcements'
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_ADMIN_CONFIG_URL = '/api/pleroma/admin/config'
const PLEROMA_ADMIN_DESCRIPTIONS_URL = '/api/pleroma/admin/config/descriptions'
@ -670,6 +671,7 @@ const fetchTimeline = ({
timeline,
credentials,
since = false,
minId = false,
until = false,
userId = false,
listId = false,
@ -704,6 +706,9 @@ const fetchTimeline = ({
url = url(listId)
}
if (minId) {
params.push(['min_id', minId])
}
if (since) {
params.push(['since_id', since])
}
@ -827,6 +832,7 @@ const postStatus = ({
poll,
mediaIds = [],
inReplyToStatusId,
quoteId,
contentType,
preview,
idempotencyKey
@ -859,6 +865,9 @@ const postStatus = ({
if (inReplyToStatusId) {
form.append('in_reply_to_id', inReplyToStatusId)
}
if (quoteId) {
form.append('quote_id', quoteId)
}
if (preview) {
form.append('preview', 'true')
}
@ -1761,6 +1770,23 @@ const installFrontend = ({ credentials, payload }) => {
})
}
const fetchScrobbles = ({ accountId, limit = 1 }) => {
let url = PLEROMA_SCROBBLES_URL(accountId)
const params = [['limit', limit]]
const queryString = map(params, (param) => `${param[0]}=${param[1]}`).join('&')
url += `?${queryString}`
return fetch(url, {})
.then((response) => {
if (response.ok) {
return response.json()
} else {
return {
error: response
}
}
})
}
const apiService = {
verifyCredentials,
fetchTimeline,
@ -1874,6 +1900,7 @@ const apiService = {
postAnnouncement,
editAnnouncement,
deleteAnnouncement,
fetchScrobbles,
adminFetchAnnouncements,
fetchInstanceDBConfig,
fetchInstanceConfigDescriptions,

ファイルの表示

@ -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 }
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()) {
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
if (isSWSupported()) {
swCloseDesktopNotification({})
}
}

ファイルの表示

@ -325,6 +325,10 @@ export const parseStatus = (data) => {
output.thread_muted = pleroma.thread_muted
output.emoji_reactions = pleroma.emoji_reactions
output.parent_visible = pleroma.parent_visible === undefined ? true : pleroma.parent_visible
output.quote = pleroma.quote ? parseStatus(pleroma.quote) : undefined
output.quote_id = pleroma.quote_id ? pleroma.quote_id : (output.quote ? output.quote.id : undefined)
output.quote_url = pleroma.quote_url
output.quote_visible = pleroma.quote_visible
} else {
output.text = data.content
output.summary = data.spoiler_text
@ -435,7 +439,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)

ファイルの表示

@ -55,10 +55,13 @@ const createFaviconService = () => {
})
}
const getOriginalFavicons = () => [...favicons]
return {
initFaviconService,
clearFaviconBadge,
drawFaviconBadge
drawFaviconBadge,
getOriginalFavicons
}
}

ファイルの表示

@ -26,7 +26,7 @@ export const fileType = mimetype => {
}
export const fileTypeExt = url => {
if (url.match(/\.(png|jpe?g|gif|webp|avif)$/)) {
if (url.match(/\.(a?png|jpe?g|gif|webp|avif)$/)) {
return 'image'
}
if (url.match(/\.(ogv|mp4|webm|mov)$/)) {

ファイルの表示

@ -22,7 +22,7 @@ export const getAttrs = (tag, filter) => {
.replace(new RegExp('^' + getTagName(tag)), '')
.replace(/\/?$/, '')
.trim()
const attrs = Array.from(innertag.matchAll(/([a-z0-9-]+)(?:=("[^"]+?"|'[^']+?'))?/gi))
const attrs = Array.from(innertag.matchAll(/([a-z]+[a-z0-9-]*)(?:=("[^"]+?"|'[^']+?'))?/gi))
.map(([trash, key, value]) => [key, value])
.map(([k, v]) => {
if (!v) return [k, true]

ファイルの表示

@ -14,8 +14,11 @@ export const mentionMatchesUrl = (attention, url) => {
* @param {string} url
*/
export const extractTagFromUrl = (url) => {
const regex = /tag[s]*\/(\w+)$/g
const result = regex.exec(url)
const decoded = decodeURI(url)
// https://git.pleroma.social/pleroma/elixir-libraries/linkify/-/blob/master/lib/linkify/parser.ex
// https://www.pcre.org/original/doc/html/pcrepattern.html
const regex = /tag[s]*\/([\p{L}\p{N}_]*[\p{Alphabetic}_·\u{200c}][\p{L}\p{N}_·\p{M}\u{200c}]*)$/ug
const result = regex.exec(decoded)
if (!result) {
return false
}

ファイルの表示

@ -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
@ -124,3 +154,18 @@ export const prepareNotificationObject = (notification, i18n) => {
return notifObj
}
export const countExtraNotifications = (store) => {
const rootGetters = store.rootGetters || store.getters
const mergedConfig = rootGetters.mergedConfig
if (!mergedConfig.showExtraNotifications) {
return 0
}
return [
mergedConfig.showChatsInExtraNotifications ? rootGetters.unreadChatCount : 0,
mergedConfig.showAnnouncementsInExtraNotifications ? rootGetters.unreadAnnouncementCount : 0,
mergedConfig.showFollowRequestsInExtraNotifications ? rootGetters.followRequestCount : 0
].reduce((a, c) => a + c, 0)
}

ファイルの表示

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

ファイルの表示

@ -0,0 +1,3 @@
const genRandomSeed = () => `${Math.random()}`.replace('.', '-')
export default genRandomSeed

ファイルの表示

@ -10,6 +10,7 @@ const postStatus = ({
poll,
media = [],
inReplyToStatusId = undefined,
quoteId = undefined,
contentType = 'text/plain',
preview = false,
idempotencyKey = ''
@ -24,6 +25,7 @@ const postStatus = ({
sensitive,
mediaIds,
inReplyToStatusId,
quoteId,
contentType,
poll,
preview,

ファイルの表示

@ -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}`))
}

変更されたファイルが多すぎるため、一部のファイルは表示されません さらに表示