aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/boot/after_store.js3
-rw-r--r--src/components/emoji_input/suggestor.js24
-rw-r--r--src/components/extra_buttons/extra_buttons.js8
-rw-r--r--src/components/extra_buttons/extra_buttons.vue19
-rw-r--r--src/components/follow_request_card/follow_request_card.js22
-rw-r--r--src/components/notification/notification.js22
-rw-r--r--src/components/notification/notification.vue46
-rw-r--r--src/components/notifications/notifications.scss34
-rw-r--r--src/components/react_button/react_button.js2
-rw-r--r--src/components/react_button/react_button.vue1
-rw-r--r--src/components/registration/registration.js25
-rw-r--r--src/components/status/status.js193
-rw-r--r--src/components/status/status.vue232
-rw-r--r--src/components/status_content/status_content.js210
-rw-r--r--src/components/status_content/status_content.vue240
-rw-r--r--src/components/user_settings/user_settings.vue12
-rw-r--r--src/i18n/en.json11
-rw-r--r--src/i18n/pl.json320
-rw-r--r--src/modules/config.js3
-rw-r--r--src/modules/statuses.js37
-rw-r--r--src/services/api/api.service.js31
-rw-r--r--src/services/entity_normalizer/entity_normalizer.service.js5
-rw-r--r--src/services/notification_utils/notification_utils.js7
-rw-r--r--src/services/notifications_fetcher/notifications_fetcher.service.js3
24 files changed, 1027 insertions, 483 deletions
diff --git a/src/boot/after_store.js b/src/boot/after_store.js
index 1522d4f0..34f6d6e7 100644
--- a/src/boot/after_store.js
+++ b/src/boot/after_store.js
@@ -241,6 +241,9 @@ const getNodeInfo = async ({ store }) => {
: federation.enabled
})
+ const accountActivationRequired = metadata.accountActivationRequired
+ store.dispatch('setInstanceOption', { name: 'accountActivationRequired', value: accountActivationRequired })
+
const accounts = metadata.staffAccounts
resolveStaffAccounts({ store, accounts })
} else {
diff --git a/src/components/emoji_input/suggestor.js b/src/components/emoji_input/suggestor.js
index aec5c39d..15a71eff 100644
--- a/src/components/emoji_input/suggestor.js
+++ b/src/components/emoji_input/suggestor.js
@@ -29,17 +29,29 @@ export default data => input => {
export const suggestEmoji = emojis => input => {
const noPrefix = input.toLowerCase().substr(1)
return emojis
- .filter(({ displayText }) => displayText.toLowerCase().startsWith(noPrefix))
+ .filter(({ displayText }) => displayText.toLowerCase().match(noPrefix))
.sort((a, b) => {
let aScore = 0
let bScore = 0
- // Make custom emojis a priority
- aScore += a.imageUrl ? 10 : 0
- bScore += b.imageUrl ? 10 : 0
+ // An exact match always wins
+ aScore += a.displayText.toLowerCase() === noPrefix ? 200 : 0
+ bScore += b.displayText.toLowerCase() === noPrefix ? 200 : 0
- // Sort alphabetically
- const alphabetically = a.displayText > b.displayText ? 1 : -1
+ // Prioritize custom emoji a lot
+ aScore += a.imageUrl ? 100 : 0
+ bScore += b.imageUrl ? 100 : 0
+
+ // Prioritize prefix matches somewhat
+ aScore += a.displayText.toLowerCase().startsWith(noPrefix) ? 10 : 0
+ bScore += b.displayText.toLowerCase().startsWith(noPrefix) ? 10 : 0
+
+ // Sort by length
+ aScore -= a.displayText.length
+ bScore -= b.displayText.length
+
+ // Break ties alphabetically
+ const alphabetically = a.displayText > b.displayText ? 0.5 : -0.5
return bScore - aScore + alphabetically
})
diff --git a/src/components/extra_buttons/extra_buttons.js b/src/components/extra_buttons/extra_buttons.js
index 37485383..e4b19d01 100644
--- a/src/components/extra_buttons/extra_buttons.js
+++ b/src/components/extra_buttons/extra_buttons.js
@@ -29,6 +29,11 @@ const ExtraButtons = {
this.$store.dispatch('unmuteConversation', this.status.id)
.then(() => this.$emit('onSuccess'))
.catch(err => this.$emit('onError', err.error.error))
+ },
+ copyLink () {
+ navigator.clipboard.writeText(this.statusLink)
+ .then(() => this.$emit('onSuccess'))
+ .catch(err => this.$emit('onError', err.error.error))
}
},
computed: {
@@ -46,6 +51,9 @@ const ExtraButtons = {
},
canMute () {
return !!this.currentUser
+ },
+ statusLink () {
+ return `${this.$store.state.instance.server}${this.$router.resolve({ name: 'conversation', params: { id: this.status.id } }).href}`
}
}
}
diff --git a/src/components/extra_buttons/extra_buttons.vue b/src/components/extra_buttons/extra_buttons.vue
index 3a7f1283..bca93ea7 100644
--- a/src/components/extra_buttons/extra_buttons.vue
+++ b/src/components/extra_buttons/extra_buttons.vue
@@ -1,11 +1,13 @@
<template>
<Popover
- v-if="canDelete || canMute || canPin"
trigger="click"
placement="top"
class="extra-button-popover"
>
- <div slot="content">
+ <div
+ slot="content"
+ slot-scope="{close}"
+ >
<div class="dropdown-menu">
<button
v-if="canMute && !status.thread_muted"
@@ -23,28 +25,35 @@
</button>
<button
v-if="!status.pinned && canPin"
- v-close-popover
class="dropdown-item dropdown-item-icon"
@click.prevent="pinStatus"
+ @click="close"
>
<i class="icon-pin" /><span>{{ $t("status.pin") }}</span>
</button>
<button
v-if="status.pinned && canPin"
- v-close-popover
class="dropdown-item dropdown-item-icon"
@click.prevent="unpinStatus"
+ @click="close"
>
<i class="icon-pin" /><span>{{ $t("status.unpin") }}</span>
</button>
<button
v-if="canDelete"
- v-close-popover
class="dropdown-item dropdown-item-icon"
@click.prevent="deleteStatus"
+ @click="close"
>
<i class="icon-cancel" /><span>{{ $t("status.delete") }}</span>
</button>
+ <button
+ class="dropdown-item dropdown-item-icon"
+ @click.prevent="copyLink"
+ @click="close"
+ >
+ <i class="icon-share" /><span>{{ $t("status.copy_link") }}</span>
+ </button>
</div>
</div>
<i
diff --git a/src/components/follow_request_card/follow_request_card.js b/src/components/follow_request_card/follow_request_card.js
index a8931787..cbd75311 100644
--- a/src/components/follow_request_card/follow_request_card.js
+++ b/src/components/follow_request_card/follow_request_card.js
@@ -1,4 +1,5 @@
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
+import { notificationsFromStore } from '../../services/notification_utils/notification_utils.js'
const FollowRequestCard = {
props: ['user'],
@@ -6,13 +7,32 @@ const FollowRequestCard = {
BasicUserCard
},
methods: {
+ findFollowRequestNotificationId () {
+ const notif = notificationsFromStore(this.$store).find(
+ (notif) => notif.from_profile.id === this.user.id && notif.type === 'follow_request'
+ )
+ return notif && notif.id
+ },
approveUser () {
this.$store.state.api.backendInteractor.approveUser({ id: this.user.id })
this.$store.dispatch('removeFollowRequest', this.user)
+
+ const notifId = this.findFollowRequestNotificationId()
+ this.$store.dispatch('markSingleNotificationAsSeen', { id: notifId })
+ this.$store.dispatch('updateNotification', {
+ id: notifId,
+ updater: notification => {
+ notification.type = 'follow'
+ }
+ })
},
denyUser () {
+ const notifId = this.findFollowRequestNotificationId()
this.$store.state.api.backendInteractor.denyUser({ id: this.user.id })
- this.$store.dispatch('removeFollowRequest', this.user)
+ .then(() => {
+ this.$store.dispatch('dismissNotificationLocal', { id: notifId })
+ this.$store.dispatch('removeFollowRequest', this.user)
+ })
}
}
}
diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js
index ff1c2817..1cf4c9bc 100644
--- a/src/components/notification/notification.js
+++ b/src/components/notification/notification.js
@@ -2,6 +2,7 @@ import Status from '../status/status.vue'
import UserAvatar from '../user_avatar/user_avatar.vue'
import UserCard from '../user_card/user_card.vue'
import Timeago from '../timeago/timeago.vue'
+import { isStatusNotification } from '../../services/notification_utils/notification_utils.js'
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
@@ -32,6 +33,24 @@ const Notification = {
},
toggleMute () {
this.unmuted = !this.unmuted
+ },
+ approveUser () {
+ this.$store.state.api.backendInteractor.approveUser({ id: this.user.id })
+ this.$store.dispatch('removeFollowRequest', this.user)
+ this.$store.dispatch('markSingleNotificationAsSeen', { id: this.notification.id })
+ this.$store.dispatch('updateNotification', {
+ id: this.notification.id,
+ updater: notification => {
+ notification.type = 'follow'
+ }
+ })
+ },
+ denyUser () {
+ this.$store.state.api.backendInteractor.denyUser({ id: this.user.id })
+ .then(() => {
+ this.$store.dispatch('dismissNotificationLocal', { id: this.notification.id })
+ this.$store.dispatch('removeFollowRequest', this.user)
+ })
}
},
computed: {
@@ -57,6 +76,9 @@ const Notification = {
},
needMute () {
return this.$store.getters.relationship(this.user.id).muting
+ },
+ isStatusNotification () {
+ return isStatusNotification(this.notification.type)
}
}
}
diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue
index 24d9fe90..0e46a2a7 100644
--- a/src/components/notification/notification.vue
+++ b/src/components/notification/notification.vue
@@ -47,7 +47,7 @@
<span class="notification-details">
<div class="name-and-action">
<!-- eslint-disable vue/no-v-html -->
- <span
+ <bdi
v-if="!!notification.from_profile.name_html"
class="username"
:title="'@'+notification.from_profile.screen_name"
@@ -74,6 +74,10 @@
<i class="fa icon-user-plus lit" />
<small>{{ $t('notifications.followed_you') }}</small>
</span>
+ <span v-if="notification.type === 'follow_request'">
+ <i class="fa icon-user lit" />
+ <small>{{ $t('notifications.follow_request') }}</small>
+ </span>
<span v-if="notification.type === 'move'">
<i class="fa icon-arrow-curved lit" />
<small>{{ $t('notifications.migrated_to') }}</small>
@@ -87,30 +91,30 @@
</span>
</div>
<div
- v-if="notification.type === 'follow' || notification.type === 'move'"
+ v-if="isStatusNotification"
class="timeago"
>
- <span class="faint">
+ <router-link
+ v-if="notification.status"
+ :to="{ name: 'conversation', params: { id: notification.status.id } }"
+ class="faint-link"
+ >
<Timeago
:time="notification.created_at"
:auto-update="240"
/>
- </span>
+ </router-link>
</div>
<div
v-else
class="timeago"
>
- <router-link
- v-if="notification.status"
- :to="{ name: 'conversation', params: { id: notification.status.id } }"
- class="faint-link"
- >
+ <span class="faint">
<Timeago
:time="notification.created_at"
:auto-update="240"
/>
- </router-link>
+ </span>
</div>
<a
v-if="needMute"
@@ -119,12 +123,30 @@
><i class="button-icon icon-eye-off" /></a>
</span>
<div
- v-if="notification.type === 'follow'"
+ v-if="notification.type === 'follow' || notification.type === 'follow_request'"
class="follow-text"
>
- <router-link :to="userProfileLink">
+ <router-link
+ :to="userProfileLink"
+ class="follow-name"
+ >
@{{ notification.from_profile.screen_name }}
</router-link>
+ <div
+ v-if="notification.type === 'follow_request'"
+ style="white-space: nowrap;"
+ >
+ <i
+ class="icon-ok button-icon follow-request-accept"
+ :title="$t('tool_tip.accept_follow_request')"
+ @click="approveUser()"
+ />
+ <i
+ class="icon-cancel button-icon follow-request-reject"
+ :title="$t('tool_tip.reject_follow_request')"
+ @click="denyUser()"
+ />
+ </div>
</div>
<div
v-else-if="notification.type === 'move'"
diff --git a/src/components/notifications/notifications.scss b/src/components/notifications/notifications.scss
index a8f4430f..9efcfcf8 100644
--- a/src/components/notifications/notifications.scss
+++ b/src/components/notifications/notifications.scss
@@ -79,9 +79,38 @@
}
}
+ .follow-request-accept {
+ cursor: pointer;
+
+ &:hover {
+ color: $fallback--text;
+ color: var(--text, $fallback--text);
+ }
+ }
+
+ .follow-request-reject {
+ cursor: pointer;
+
+ &:hover {
+ color: $fallback--cRed;
+ color: var(--cRed, $fallback--cRed);
+ }
+ }
+
+
.follow-text, .move-text {
padding: 0.5em 0;
overflow-wrap: break-word;
+ display: flex;
+ justify-content: space-between;
+
+ .follow-name {
+ display: block;
+ max-width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
}
.status-el {
@@ -143,6 +172,11 @@
color: var(--cGreen, $fallback--cGreen);
}
+ .icon-user.lit {
+ color: $fallback--cBlue;
+ color: var(--cBlue, $fallback--cBlue);
+ }
+
.icon-user-plus.lit {
color: $fallback--cBlue;
color: var(--cBlue, $fallback--cBlue);
diff --git a/src/components/react_button/react_button.js b/src/components/react_button/react_button.js
index 19949563..abc3bf07 100644
--- a/src/components/react_button/react_button.js
+++ b/src/components/react_button/react_button.js
@@ -2,7 +2,7 @@ import Popover from '../popover/popover.vue'
import { mapGetters } from 'vuex'
const ReactButton = {
- props: ['status', 'loggedIn'],
+ props: ['status'],
data () {
return {
filterWord: ''
diff --git a/src/components/react_button/react_button.vue b/src/components/react_button/react_button.vue
index ab4b4fcd..0b34add1 100644
--- a/src/components/react_button/react_button.vue
+++ b/src/components/react_button/react_button.vue
@@ -37,7 +37,6 @@
</div>
</div>
<i
- v-if="loggedIn"
slot="trigger"
class="icon-smile button-icon add-reaction-button"
:title="$t('tool_tip.add_reaction')"
diff --git a/src/components/registration/registration.js b/src/components/registration/registration.js
index ace8cc7c..dab06e1e 100644
--- a/src/components/registration/registration.js
+++ b/src/components/registration/registration.js
@@ -1,5 +1,5 @@
import { validationMixin } from 'vuelidate'
-import { required, sameAs } from 'vuelidate/lib/validators'
+import { required, requiredIf, sameAs } from 'vuelidate/lib/validators'
import { mapActions, mapState } from 'vuex'
const registration = {
@@ -14,15 +14,17 @@ const registration = {
},
captcha: {}
}),
- validations: {
- user: {
- email: { required },
- username: { required },
- fullname: { required },
- password: { required },
- confirm: {
- required,
- sameAsPassword: sameAs('password')
+ validations () {
+ return {
+ user: {
+ email: { required: requiredIf(() => this.accountActivationRequired) },
+ username: { required },
+ fullname: { required },
+ password: { required },
+ confirm: {
+ required,
+ sameAsPassword: sameAs('password')
+ }
}
}
},
@@ -43,7 +45,8 @@ const registration = {
signedIn: (state) => !!state.users.currentUser,
isPending: (state) => state.users.signUpPending,
serverValidationErrors: (state) => state.users.signUpErrors,
- termsOfService: (state) => state.instance.tos
+ termsOfService: (state) => state.instance.tos,
+ accountActivationRequired: (state) => state.instance.accountActivationRequired
})
},
methods: {
diff --git a/src/components/status/status.js b/src/components/status/status.js
index 890f4b91..9cd9d61c 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -1,23 +1,17 @@
-import Attachment from '../attachment/attachment.vue'
import FavoriteButton from '../favorite_button/favorite_button.vue'
import ReactButton from '../react_button/react_button.vue'
import RetweetButton from '../retweet_button/retweet_button.vue'
-import Poll from '../poll/poll.vue'
import ExtraButtons from '../extra_buttons/extra_buttons.vue'
import PostStatusForm from '../post_status_form/post_status_form.vue'
import UserCard from '../user_card/user_card.vue'
import UserAvatar from '../user_avatar/user_avatar.vue'
-import Gallery from '../gallery/gallery.vue'
-import LinkPreview from '../link-preview/link-preview.vue'
import AvatarList from '../avatar_list/avatar_list.vue'
import Timeago from '../timeago/timeago.vue'
+import StatusContent from '../status_content/status_content.vue'
import StatusPopover from '../status_popover/status_popover.vue'
import EmojiReactions from '../emoji_reactions/emoji_reactions.vue'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
-import fileType from 'src/services/file_type/file_type.service'
-import { processHtml } from 'src/services/tiny_post_html_processor/tiny_post_html_processor.service.js'
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
-import { mentionMatchesUrl, extractTagFromUrl } from 'src/services/matcher/matcher.service.js'
import { filter, unescape, uniqBy } from 'lodash'
import { mapGetters, mapState } from 'vuex'
@@ -43,17 +37,10 @@ const Status = {
replying: false,
unmuted: false,
userExpanded: false,
- showingTall: this.inConversation && this.focused,
- showingLongSubject: false,
- error: null,
- // not as computed because it sets the initial state which will be changed later
- expandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject
+ error: null
}
},
computed: {
- localCollapseSubjectDefault () {
- return this.mergedConfig.collapseMessageWithSubject
- },
muteWords () {
return this.mergedConfig.muteWords
},
@@ -79,10 +66,6 @@ const Status = {
const highlight = this.mergedConfig.highlight
return highlightStyle(highlight[user.screen_name])
},
- hideAttachments () {
- return (this.mergedConfig.hideAttachments && !this.inConversation) ||
- (this.mergedConfig.hideAttachmentsInConv && this.inConversation)
- },
userProfileLink () {
return this.generateUserProfileLink(this.status.user.id, this.status.user.screen_name)
},
@@ -141,20 +124,6 @@ const Status = {
// use conversation highlight only when in conversation
return this.status.id === this.highlight
},
- // This is a bit hacky, but we want to approximate post height before rendering
- // so we count newlines (masto uses <p> for paragraphs, GS uses <br> between them)
- // as well as approximate line count by counting characters and approximating ~80
- // per line.
- //
- // Using max-height + overflow: auto for status components resulted in false positives
- // very often with japanese characters, and it was very annoying.
- tallStatus () {
- const lengthScore = this.status.statusnet_html.split(/<p|<br/).length + this.status.text.length / 80
- return lengthScore > 20
- },
- longSubject () {
- return this.status.summary.length > 900
- },
isReply () {
return !!(this.status.in_reply_to_status_id && this.status.in_reply_to_user_id)
},
@@ -197,33 +166,6 @@ const Status = {
}
return this.status.attentions.length > 0
},
- hideSubjectStatus () {
- if (this.tallStatus && !this.localCollapseSubjectDefault) {
- return false
- }
- return !this.expandingSubject && this.status.summary
- },
- hideTallStatus () {
- if (this.status.summary && this.localCollapseSubjectDefault) {
- return false
- }
- if (this.showingTall) {
- return false
- }
- return this.tallStatus
- },
- showingMore () {
- return (this.tallStatus && this.showingTall) || (this.status.summary && this.expandingSubject)
- },
- nsfwClickthrough () {
- if (!this.status.nsfw) {
- return false
- }
- if (this.status.summary && this.localCollapseSubjectDefault) {
- return false
- }
- return true
- },
replySubject () {
if (!this.status.summary) return ''
const decodedSummary = unescape(this.status.summary)
@@ -237,83 +179,6 @@ const Status = {
return ''
}
},
- attachmentSize () {
- if ((this.mergedConfig.hideAttachments && !this.inConversation) ||
- (this.mergedConfig.hideAttachmentsInConv && this.inConversation) ||
- (this.status.attachments.length > this.maxThumbnails)) {
- return 'hide'
- } else if (this.compact) {
- return 'small'
- }
- return 'normal'
- },
- galleryTypes () {
- if (this.attachmentSize === 'hide') {
- return []
- }
- return this.mergedConfig.playVideosInModal
- ? ['image', 'video']
- : ['image']
- },
- galleryAttachments () {
- return this.status.attachments.filter(
- file => fileType.fileMatchesSomeType(this.galleryTypes, file)
- )
- },
- nonGalleryAttachments () {
- return this.status.attachments.filter(
- file => !fileType.fileMatchesSomeType(this.galleryTypes, file)
- )
- },
- hasImageAttachments () {
- return this.status.attachments.some(
- file => fileType.fileType(file.mimetype) === 'image'
- )
- },
- hasVideoAttachments () {
- return this.status.attachments.some(
- file => fileType.fileType(file.mimetype) === 'video'
- )
- },
- maxThumbnails () {
- return this.mergedConfig.maxThumbnails
- },
- postBodyHtml () {
- const html = this.status.statusnet_html
-
- if (this.mergedConfig.greentext) {
- try {
- if (html.includes('&gt;')) {
- // This checks if post has '>' at the beginning, excluding mentions so that @mention >impying works
- return processHtml(html, (string) => {
- if (string.includes('&gt;') &&
- string
- .replace(/<[^>]+?>/gi, '') // remove all tags
- .replace(/@\w+/gi, '') // remove mentions (even failed ones)
- .trim()
- .startsWith('&gt;')) {
- return `<span class='greentext'>${string}</span>`
- } else {
- return string
- }
- })
- } else {
- return html
- }
- } catch (e) {
- console.err('Failed to process status html', e)
- return html
- }
- } else {
- return html
- }
- },
- contentHtml () {
- if (!this.status.summary_html) {
- return this.postBodyHtml
- }
- return this.status.summary_html + '<br />' + this.postBodyHtml
- },
combinedFavsAndRepeatsUsers () {
// Use the status from the global status repository since favs and repeats are saved in it
const combinedUsers = [].concat(
@@ -322,9 +187,6 @@ const Status = {
)
return uniqBy(combinedUsers, 'id')
},
- ownStatus () {
- return this.status.user.id === this.currentUser.id
- },
tags () {
return this.status.tags.filter(tagObj => tagObj.hasOwnProperty('name')).map(tagObj => tagObj.name).join(' ')
},
@@ -338,21 +200,18 @@ const Status = {
})
},
components: {
- Attachment,
FavoriteButton,
ReactButton,
RetweetButton,
ExtraButtons,
PostStatusForm,
- Poll,
UserCard,
UserAvatar,
- Gallery,
- LinkPreview,
AvatarList,
Timeago,
StatusPopover,
- EmojiReactions
+ EmojiReactions,
+ StatusContent
},
methods: {
visibilityIcon (visibility) {
@@ -373,32 +232,6 @@ const Status = {
clearError () {
this.error = undefined
},
- linkClicked (event) {
- const target = event.target.closest('.status-content a')
- if (target) {
- if (target.className.match(/mention/)) {
- const href = target.href
- const attn = this.status.attentions.find(attn => mentionMatchesUrl(attn, href))
- if (attn) {
- event.stopPropagation()
- event.preventDefault()
- const link = this.generateUserProfileLink(attn.id, attn.screen_name)
- this.$router.push(link)
- return
- }
- }
- if (target.rel.match(/(?:^|\s)tag(?:$|\s)/) || target.className.match(/hashtag/)) {
- // Extract tag name from link url
- const tag = extractTagFromUrl(target.href)
- if (tag) {
- const link = this.generateTagLink(tag)
- this.$router.push(link)
- return
- }
- }
- window.open(target.href, '_blank')
- }
- },
toggleReplying () {
this.replying = !this.replying
},
@@ -416,26 +249,8 @@ const Status = {
toggleUserExpanded () {
this.userExpanded = !this.userExpanded
},
- toggleShowMore () {
- if (this.showingTall) {
- this.showingTall = false
- } else if (this.expandingSubject && this.status.summary) {
- this.expandingSubject = false
- } else if (this.hideTallStatus) {
- this.showingTall = true
- } else if (this.hideSubjectStatus && this.status.summary) {
- this.expandingSubject = true
- }
- },
generateUserProfileLink (id, name) {
return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames)
- },
- generateTagLink (tag) {
- return `/tag/${tag}`
- },
- setMedia () {
- const attachments = this.attachmentSize === 'hide' ? this.status.attachments : this.galleryAttachments
- return () => this.$store.dispatch('setMedia', attachments)
}
},
watch: {
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index dd7cd579..e4c7545b 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -226,118 +226,12 @@
</div>
</div>
- <div
- v-if="longSubject"
- class="status-content-wrapper"
- :class="{ 'tall-status': !showingLongSubject }"
- >
- <a
- v-if="!showingLongSubject"
- class="tall-status-hider"
- :class="{ 'tall-status-hider_focused': isFocused }"
- href="#"
- @click.prevent="showingLongSubject=true"
- >{{ $t("general.show_more") }}</a>
- <div
- class="status-content media-body"
- @click.prevent="linkClicked"
- v-html="contentHtml"
- />
- <a
- v-if="showingLongSubject"
- href="#"
- class="status-unhider"
- @click.prevent="showingLongSubject=false"
- >{{ $t("general.show_less") }}</a>
- </div>
- <div
- v-else
- :class="{'tall-status': hideTallStatus}"
- class="status-content-wrapper"
- >
- <a
- v-if="hideTallStatus"
- class="tall-status-hider"
- :class="{ 'tall-status-hider_focused': isFocused }"
- href="#"
- @click.prevent="toggleShowMore"
- >{{ $t("general.show_more") }}</a>
- <div
- v-if="!hideSubjectStatus"
- class="status-content media-body"
- @click.prevent="linkClicked"
- v-html="contentHtml"
- />
- <div
- v-else
- class="status-content media-body"
- @click.prevent="linkClicked"
- v-html="status.summary_html"
- />
- <a
- v-if="hideSubjectStatus"
- href="#"
- class="cw-status-hider"
- @click.prevent="toggleShowMore"
- >
- {{ $t("general.show_more") }}
- <span
- v-if="hasImageAttachments"
- class="icon-picture"
- />
- <span
- v-if="hasVideoAttachments"
- class="icon-video"
- />
- <span
- v-if="status.card"
- class="icon-link"
- />
- </a>
- <a
- v-if="showingMore"
- href="#"
- class="status-unhider"
- @click.prevent="toggleShowMore"
- >{{ $t("general.show_less") }}</a>
- </div>
-
- <div v-if="status.poll && status.poll.options">
- <poll :base-poll="status.poll" />
- </div>
-
- <div
- v-if="status.attachments && (!hideSubjectStatus || showingLongSubject)"
- class="attachments media-body"
- >
- <attachment
- v-for="attachment in nonGalleryAttachments"
- :key="attachment.id"
- class="non-gallery"
- :size="attachmentSize"
- :nsfw="nsfwClickthrough"
- :attachment="attachment"
- :allow-play="true"
- :set-media="setMedia()"
- />
- <gallery
- v-if="galleryAttachments.length > 0"
- :nsfw="nsfwClickthrough"
- :attachments="galleryAttachments"
- :set-media="setMedia()"
- />
- </div>
-
- <div
- v-if="status.card && !hideSubjectStatus && !noHeading"
- class="link-preview media-body"
- >
- <link-preview
- :card="status.card"
- :size="attachmentSize"
- :nsfw="nsfwClickthrough"
- />
- </div>
+ <StatusContent
+ :status="status"
+ :no-heading="noHeading"
+ :highlight="highlight"
+ :focused="isFocused"
+ />
<transition name="fade">
<div
@@ -404,7 +298,7 @@
:status="status"
/>
<ReactButton
- :logged-in="loggedIn"
+ v-if="loggedIn"
:status="status"
/>
<extra-buttons
@@ -630,105 +524,6 @@ $status-margin: 0.75em;
}
}
- .tall-status {
- position: relative;
- height: 220px;
- overflow-x: hidden;
- overflow-y: hidden;
- z-index: 1;
- .status-content {
- height: 100%;
- mask: linear-gradient(to top, white, transparent) bottom/100% 70px no-repeat,
- linear-gradient(to top, white, white);
- /* Autoprefixed seem to ignore this one, and also syntax is different */
- -webkit-mask-composite: xor;
- mask-composite: exclude;
- }
- }
-
- .tall-status-hider {
- display: inline-block;
- word-break: break-all;
- position: absolute;
- height: 70px;
- margin-top: 150px;
- width: 100%;
- text-align: center;
- line-height: 110px;
- z-index: 2;
- }
-
- .status-unhider, .cw-status-hider {
- width: 100%;
- text-align: center;
- display: inline-block;
- word-break: break-all;
- }
-
- .status-content {
- font-family: var(--postFont, sans-serif);
- line-height: 1.4em;
- white-space: pre-wrap;
-
- a {
- color: $fallback--link;
- color: var(--postLink, $fallback--link);
- }
-
- img, video {
- max-width: 100%;
- max-height: 400px;
- vertical-align: middle;
- object-fit: contain;
-
- &.emoji {
- width: 32px;
- height: 32px;
- }
- }
-
- blockquote {
- margin: 0.2em 0 0.2em 2em;
- font-style: italic;
- }
-
- pre {
- overflow: auto;
- }
-
- code, samp, kbd, var, pre {
- font-family: var(--postCodeFont, monospace);
- }
-
- p {
- margin: 0 0 1em 0;
- }
-
- p:last-child {
- margin: 0 0 0 0;
- }
-
- h1 {
- font-size: 1.1em;
- line-height: 1.2em;
- margin: 1.4em 0;
- }
-
- h2 {
- font-size: 1.1em;
- margin: 1.0em 0;
- }
-
- h3 {
- font-size: 1em;
- margin: 1.2em 0;
- }
-
- h4 {
- margin: 1.1em 0;
- }
- }
-
.retweet-info {
padding: 0.4em $status-margin;
margin: 0;
@@ -790,11 +585,6 @@ $status-margin: 0.75em;
}
}
-.greentext {
- color: $fallback--cGreen;
- color: var(--cGreen, $fallback--cGreen);
-}
-
.status-conversation {
border-left-style: solid;
}
@@ -866,14 +656,6 @@ a.unmute {
flex: 1;
}
-.timeline :not(.panel-disabled) > {
- .status-el:last-child {
- border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
- border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
- border-bottom: none;
- }
-}
-
.favs-repeated-users {
margin-top: $status-margin;
diff --git a/src/components/status_content/status_content.js b/src/components/status_content/status_content.js
new file mode 100644
index 00000000..ccc01b6f
--- /dev/null
+++ b/src/components/status_content/status_content.js
@@ -0,0 +1,210 @@
+import Attachment from '../attachment/attachment.vue'
+import Poll from '../poll/poll.vue'
+import Gallery from '../gallery/gallery.vue'
+import LinkPreview from '../link-preview/link-preview.vue'
+import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
+import fileType from 'src/services/file_type/file_type.service'
+import { processHtml } from 'src/services/tiny_post_html_processor/tiny_post_html_processor.service.js'
+import { mentionMatchesUrl, extractTagFromUrl } from 'src/services/matcher/matcher.service.js'
+import { mapGetters, mapState } from 'vuex'
+
+const StatusContent = {
+ name: 'StatusContent',
+ props: [
+ 'status',
+ 'focused',
+ 'noHeading',
+ 'fullContent'
+ ],
+ data () {
+ return {
+ showingTall: this.inConversation && this.focused,
+ showingLongSubject: false,
+ // not as computed because it sets the initial state which will be changed later
+ expandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject
+ }
+ },
+ computed: {
+ localCollapseSubjectDefault () {
+ return this.mergedConfig.collapseMessageWithSubject
+ },
+ hideAttachments () {
+ return (this.mergedConfig.hideAttachments && !this.inConversation) ||
+ (this.mergedConfig.hideAttachmentsInConv && this.inConversation)
+ },
+ // This is a bit hacky, but we want to approximate post height before rendering
+ // so we count newlines (masto uses <p> for paragraphs, GS uses <br> between them)
+ // as well as approximate line count by counting characters and approximating ~80
+ // per line.
+ //
+ // Using max-height + overflow: auto for status components resulted in false positives
+ // very often with japanese characters, and it was very annoying.
+ tallStatus () {
+ const lengthScore = this.status.statusnet_html.split(/<p|<br/).length + this.status.text.length / 80
+ return lengthScore > 20
+ },
+ longSubject () {
+ return this.status.summary.length > 900
+ },
+ // When a status has a subject and is also tall, we should only have one show more/less button. If the default is to collapse statuses with subjects, we just treat it like a status with a subject; otherwise, we just treat it like a tall status.
+ mightHideBecauseSubject () {
+ return this.status.summary && (!this.tallStatus || this.localCollapseSubjectDefault)
+ },
+ mightHideBecauseTall () {
+ return this.tallStatus && (!this.status.summary || !this.localCollapseSubjectDefault)
+ },
+ hideSubjectStatus () {
+ return this.mightHideBecauseSubject && !this.expandingSubject
+ },
+ hideTallStatus () {
+ return this.mightHideBecauseTall && !this.showingTall
+ },
+ showingMore () {
+ return (this.mightHideBecauseTall && this.showingTall) || (this.mightHideBecauseSubject && this.expandingSubject)
+ },
+ nsfwClickthrough () {
+ if (!this.status.nsfw) {
+ return false
+ }
+ if (this.status.summary && this.localCollapseSubjectDefault) {
+ return false
+ }
+ return true
+ },
+ attachmentSize () {
+ if ((this.mergedConfig.hideAttachments && !this.inConversation) ||
+ (this.mergedConfig.hideAttachmentsInConv && this.inConversation) ||
+ (this.status.attachments.length > this.maxThumbnails)) {
+ return 'hide'
+ } else if (this.compact) {
+ return 'small'
+ }
+ return 'normal'
+ },
+ galleryTypes () {
+ if (this.attachmentSize === 'hide') {
+ return []
+ }
+ return this.mergedConfig.playVideosInModal
+ ? ['image', 'video']
+ : ['image']
+ },
+ galleryAttachments () {
+ return this.status.attachments.filter(
+ file => fileType.fileMatchesSomeType(this.galleryTypes, file)
+ )
+ },
+ nonGalleryAttachments () {
+ return this.status.attachments.filter(
+ file => !fileType.fileMatchesSomeType(this.galleryTypes, file)
+ )
+ },
+ hasImageAttachments () {
+ return this.status.attachments.some(
+ file => fileType.fileType(file.mimetype) === 'image'
+ )
+ },
+ hasVideoAttachments () {
+ return this.status.attachments.some(
+ file => fileType.fileType(file.mimetype) === 'video'
+ )
+ },
+ maxThumbnails () {
+ return this.mergedConfig.maxThumbnails
+ },
+ postBodyHtml () {
+ const html = this.status.statusnet_html
+
+ if (this.mergedConfig.greentext) {
+ try {
+ if (html.includes('&gt;')) {
+ // This checks if post has '>' at the beginning, excluding mentions so that @mention >impying works
+ return processHtml(html, (string) => {
+ if (string.includes('&gt;') &&
+ string
+ .replace(/<[^>]+?>/gi, '') // remove all tags
+ .replace(/@\w+/gi, '') // remove mentions (even failed ones)
+ .trim()
+ .startsWith('&gt;')) {
+ return `<span class='greentext'>${string}</span>`
+ } else {
+ return string
+ }
+ })
+ } else {
+ return html
+ }
+ } catch (e) {
+ console.err('Failed to process status html', e)
+ return html
+ }
+ } else {
+ return html
+ }
+ },
+ contentHtml () {
+ if (!this.status.summary_html) {
+ return this.postBodyHtml
+ }
+ return this.status.summary_html + '<br />' + this.postBodyHtml
+ },
+ ...mapGetters(['mergedConfig']),
+ ...mapState({
+ betterShadow: state => state.interface.browserSupport.cssFilter,
+ currentUser: state => state.users.currentUser
+ })
+ },
+ components: {
+ Attachment,
+ Poll,
+ Gallery,
+ LinkPreview
+ },
+ methods: {
+ linkClicked (event) {
+ const target = event.target.closest('.status-content a')
+ if (target) {
+ if (target.className.match(/mention/)) {
+ const href = target.href
+ const attn = this.status.attentions.find(attn => mentionMatchesUrl(attn, href))
+ if (attn) {
+ event.stopPropagation()
+ event.preventDefault()
+ const link = this.generateUserProfileLink(attn.id, attn.screen_name)
+ this.$router.push(link)
+ return
+ }
+ }
+ if (target.rel.match(/(?:^|\s)tag(?:$|\s)/) || target.className.match(/hashtag/)) {
+ // Extract tag name from link url
+ const tag = extractTagFromUrl(target.href)
+ if (tag) {
+ const link = this.generateTagLink(tag)
+ this.$router.push(link)
+ return
+ }
+ }
+ window.open(target.href, '_blank')
+ }
+ },
+ toggleShowMore () {
+ if (this.mightHideBecauseTall) {
+ this.showingTall = !this.showingTall
+ } else if (this.mightHideBecauseSubject) {
+ this.expandingSubject = !this.expandingSubject
+ }
+ },
+ generateUserProfileLink (id, name) {
+ return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames)
+ },
+ generateTagLink (tag) {
+ return `/tag/${tag}`
+ },
+ setMedia () {
+ const attachments = this.attachmentSize === 'hide' ? this.status.attachments : this.galleryAttachments
+ return () => this.$store.dispatch('setMedia', attachments)
+ }
+ }
+}
+
+export default StatusContent
diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue
new file mode 100644
index 00000000..8c2e8749
--- /dev/null
+++ b/src/components/status_content/status_content.vue
@@ -0,0 +1,240 @@
+<template>
+ <!-- eslint-disable vue/no-v-html -->
+ <div class="status-body">
+ <slot name="header" />
+ <div
+ v-if="longSubject"
+ class="status-content-wrapper"
+ :class="{ 'tall-status': !showingLongSubject }"
+ >
+ <a
+ v-if="!showingLongSubject"
+ class="tall-status-hider"
+ :class="{ 'tall-status-hider_focused': focused }"
+ href="#"
+ @click.prevent="showingLongSubject=true"
+ >
+ {{ $t("general.show_more") }}
+ <span
+ v-if="hasImageAttachments"
+ class="icon-picture"
+ />
+ <span
+ v-if="hasVideoAttachments"
+ class="icon-video"
+ />
+ <span
+ v-if="status.card"
+ class="icon-link"
+ />
+ </a>
+ <div
+ class="status-content media-body"
+ @click.prevent="linkClicked"
+ v-html="contentHtml"
+ />
+ <a
+ v-if="showingLongSubject"
+ href="#"
+ class="status-unhider"
+ @click.prevent="showingLongSubject=false"
+ >{{ $t("general.show_less") }}</a>
+ </div>
+ <div
+ v-else
+ :class="{'tall-status': hideTallStatus}"
+ class="status-content-wrapper"
+ >
+ <a
+ v-if="hideTallStatus"
+ class="tall-status-hider"
+ :class="{ 'tall-status-hider_focused': focused }"
+ href="#"
+ @click.prevent="toggleShowMore"
+ >{{ $t("general.show_more") }}</a>
+ <div
+ v-if="!hideSubjectStatus"
+ class="status-content media-body"
+ @click.prevent="linkClicked"
+ v-html="contentHtml"
+ />
+ <div
+ v-else
+ class="status-content media-body"
+ @click.prevent="linkClicked"
+ v-html="status.summary_html"
+ />
+ <a
+ v-if="hideSubjectStatus"
+ href="#"
+ class="cw-status-hider"
+ @click.prevent="toggleShowMore"
+ >{{ $t("general.show_more") }}</a>
+ <a
+ v-if="showingMore"
+ href="#"
+ class="status-unhider"
+ @click.prevent="toggleShowMore"
+ >{{ $t("general.show_less") }}</a>
+ </div>
+
+ <div v-if="status.poll && status.poll.options">
+ <poll :base-poll="status.poll" />
+ </div>
+
+ <div
+ v-if="status.attachments.length !== 0 && (!hideSubjectStatus || showingLongSubject)"
+ class="attachments media-body"
+ >
+ <attachment
+ v-for="attachment in nonGalleryAttachments"
+ :key="attachment.id"
+ class="non-gallery"
+ :size="attachmentSize"
+ :nsfw="nsfwClickthrough"
+ :attachment="attachment"
+ :allow-play="true"
+ :set-media="setMedia()"
+ />
+ <gallery
+ v-if="galleryAttachments.length > 0"
+ :nsfw="nsfwClickthrough"
+ :attachments="galleryAttachments"
+ :set-media="setMedia()"
+ />
+ </div>
+
+ <div
+ v-if="status.card && !hideSubjectStatus && !noHeading"
+ class="link-preview media-body"
+ >
+ <link-preview
+ :card="status.card"
+ :size="attachmentSize"
+ :nsfw="nsfwClickthrough"
+ />
+ </div>
+ <slot name="footer" />
+ </div>
+ <!-- eslint-enable vue/no-v-html -->
+</template>
+
+<script src="./status_content.js" ></script>
+<style lang="scss">
+@import '../../_variables.scss';
+
+$status-margin: 0.75em;
+
+.status-body {
+ flex: 1;
+ min-width: 0;
+
+ .tall-status {
+ position: relative;
+ height: 220px;
+ overflow-x: hidden;
+ overflow-y: hidden;
+ z-index: 1;
+ .status-content {
+ height: 100%;
+ mask: linear-gradient(to top, white, transparent) bottom/100% 70px no-repeat,
+ linear-gradient(to top, white, white);
+ /* Autoprefixed seem to ignore this one, and also syntax is different */
+ -webkit-mask-composite: xor;
+ mask-composite: exclude;
+ }
+ }
+
+ .tall-status-hider {
+ display: inline-block;
+ word-break: break-all;
+ position: absolute;
+ height: 70px;
+ margin-top: 150px;
+ width: 100%;
+ text-align: center;
+ line-height: 110px;
+ z-index: 2;
+ }
+
+ .status-unhider, .cw-status-hider {
+ width: 100%;
+ text-align: center;
+ display: inline-block;
+ word-break: break-all;
+ }
+
+ .status-content {
+ font-family: var(--postFont, sans-serif);
+ line-height: 1.4em;
+ white-space: pre-wrap;
+
+ img, video {
+ max-width: 100%;
+ max-height: 400px;
+ vertical-align: middle;
+ object-fit: contain;
+
+ &.emoji {
+ width: 32px;
+ height: 32px;
+ }
+ }
+
+ blockquote {
+ margin: 0.2em 0 0.2em 2em;
+ font-style: italic;
+ }
+
+ pre {
+ overflow: auto;
+ }
+
+ code, samp, kbd, var, pre {
+ font-family: var(--postCodeFont, monospace);
+ }
+
+ p {
+ margin: 0 0 1em 0;
+ }
+
+ p:last-child {
+ margin: 0 0 0 0;
+ }
+
+ h1 {
+ font-size: 1.1em;
+ line-height: 1.2em;
+ margin: 1.4em 0;
+ }
+
+ h2 {
+ font-size: 1.1em;
+ margin: 1.0em 0;
+ }
+
+ h3 {
+ font-size: 1em;
+ margin: 1.2em 0;
+ }
+
+ h4 {
+ margin: 1.1em 0;
+ }
+ }
+}
+
+.greentext {
+ color: $fallback--cGreen;
+ color: var(--cGreen, $fallback--cGreen);
+}
+
+.timeline :not(.panel-disabled) > {
+ .status-el:last-child {
+ border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
+ border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
+ border-bottom: none;
+ }
+}
+
+</style>
diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue
index 8b2336b4..ad184520 100644
--- a/src/components/user_settings/user_settings.vue
+++ b/src/components/user_settings/user_settings.vue
@@ -379,6 +379,7 @@
:label="$t('settings.notifications')"
>
<div class="setting-item">
+ <h2>{{ $t('settings.notification_setting_filters') }}</h2>
<div class="select-multiple">
<span class="label">{{ $t('settings.notification_setting') }}</span>
<ul class="option-list">
@@ -404,6 +405,17 @@
</li>
</ul>
</div>
+ </div>
+
+ <div class="setting-item">
+ <h2>{{ $t('settings.notification_setting_privacy') }}</h2>
+ <p>
+ <Checkbox v-model="notificationSettings.privacy_option">
+ {{ $t('settings.notification_setting_privacy_option') }}
+ </Checkbox>
+ </p>
+ </div>
+ <div class="setting-item">
<p>{{ $t('settings.notification_mutes') }}</p>
<p>{{ $t('settings.notification_blocks') }}</p>
<button
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 54d0608e..d5748719 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -124,6 +124,7 @@
"broken_favorite": "Unknown status, searching for it...",
"favorited_you": "favorited your status",
"followed_you": "followed you",
+ "follow_request": "wants to follow you",
"load_older": "Load older notifications",
"notifications": "Notifications",
"read": "Read!",
@@ -404,11 +405,14 @@
"fun": "Fun",
"greentext": "Meme arrows",
"notifications": "Notifications",
+ "notification_setting_filters": "Filters",
"notification_setting": "Receive notifications from:",
"notification_setting_follows": "Users you follow",
"notification_setting_non_follows": "Users you do not follow",
"notification_setting_followers": "Users who follow you",
"notification_setting_non_followers": "Users who do not follow you",
+ "notification_setting_privacy": "Privacy",
+ "notification_setting_privacy_option": "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",
@@ -616,7 +620,8 @@
"replies_list": "Replies:",
"mute_conversation": "Mute conversation",
"unmute_conversation": "Unmute conversation",
- "status_unavailable": "Status unavailable"
+ "status_unavailable": "Status unavailable",
+ "copy_link": "Copy link to status"
},
"user_card": {
"approve": "Approve",
@@ -697,7 +702,9 @@
"reply": "Reply",
"favorite": "Favorite",
"add_reaction": "Add Reaction",
- "user_settings": "User Settings"
+ "user_settings": "User Settings",
+ "accept_follow_request": "Accept follow request",
+ "reject_follow_request": "Reject follow request"
},
"upload":{
"error": {
diff --git a/src/i18n/pl.json b/src/i18n/pl.json
index 51cadfb6..4a4b1e31 100644
--- a/src/i18n/pl.json
+++ b/src/i18n/pl.json
@@ -1,7 +1,47 @@
{
+ "about": {
+ "mrf": {
+ "federation": "Federacja",
+ "keyword": {
+ "keyword_policies": "Zasady słów kluczowych",
+ "ftl_removal": "Usunięcie z \"Całej znanej sieci\"",
+ "reject": "Odrzucanie",
+ "replace": "Zastąpienie",
+ "is_replaced_by": "→"
+ },
+ "mrf_policies": "Włączone zasady MRF",
+ "mrf_policies_desc": "Zasady MRF zmieniają zachowanie federowania instancji. Następujące zasady są włączone:",
+ "simple": {
+ "simple_policies": "Zasady specyficzne dla instancji",
+ "accept": "Akceptowanie",
+ "accept_desc": "Ta instancja akceptuje tylko posty z wymienionych instancji:",
+ "reject": "Odrzucanie",
+ "reject_desc": "Ta instancja odrzuca posty z wymienionych instancji:",
+ "quarantine": "Kwarantanna",
+ "quarantine_desc": "Ta instancja wysyła tylko publiczne posty do wymienionych instancji:",
+ "ftl_removal": "Usunięcie z \"Całej znanej sieci\"",
+ "ftl_removal_desc": "Ta instancja usuwa te instancje z \"Całej znanej sieci\"",
+ "media_removal": "Usuwanie multimediów",
+ "media_removal_desc": "Ta instancja usuwa multimedia z postów od wymienionych instancji:",
+ "media_nsfw": "Multimedia ustawione jako wrażliwe",
+ "media_nsfw_desc": "Ta instancja wymusza, by multimedia z wymienionych instancji były ustawione jako wrażliwe:"
+ }
+ },
+ "staff": "Obsługa"
+ },
"chat": {
"title": "Czat"
},
+ "domain_mute_card": {
+ "mute": "Wycisz",
+ "mute_progress": "Wyciszam...",
+ "unmute": "Odcisz",
+ "unmute_progress": "Odciszam..."
+ },
+ "exporter": {
+ "export": "Eksportuj",
+ "processing": "Przetwarzam, za chwilę zostaniesz zapytany o ściągnięcie pliku"
+ },
"features_panel": {
"chat": "Czat",
"gopher": "Gopher",
@@ -20,7 +60,15 @@
"submit": "Wyślij",
"more": "Więcej",
"generic_error": "Wystąpił błąd",
- "optional": "nieobowiązkowe"
+ "optional": "nieobowiązkowe",
+ "show_more": "Pokaż więcej",
+ "show_less": "Pokaż mniej",
+ "dismiss": "Odrzuć",
+ "cancel": "Anuluj",
+ "disable": "Wyłącz",
+ "enable": "Włącz",
+ "confirm": "Potwierdź",
+ "verify": "Zweryfikuj"
},
"image_cropper": {
"crop_picture": "Przytnij obrazek",
@@ -28,6 +76,11 @@
"save_without_cropping": "Zapisz bez przycinania",
"cancel": "Anuluj"
},
+ "importer": {
+ "submit": "Wyślij",
+ "success": "Zaimportowano pomyślnie",
+ "error": "Wystąpił błąd podczas importowania pliku."
+ },
"login": {
"login": "Zaloguj",
"description": "Zaloguj używając OAuth",
@@ -36,7 +89,15 @@
"placeholder": "n.p. lain",
"register": "Zarejestruj",
"username": "Użytkownik",
- "hint": "Zaloguj się, aby dołączyć do dyskusji"
+ "hint": "Zaloguj się, aby dołączyć do dyskusji",
+ "authentication_code": "Kod weryfikacyjny",
+ "enter_recovery_code": "Wprowadź kod zapasowy",
+ "enter_two_factor_code": "Wprowadź kod weryfikacyjny",
+ "recovery_code": "Kod zapasowy",
+ "heading" : {
+ "totp" : "Weryfikacja dwuetapowa",
+ "recovery" : "Zapasowa weryfikacja dwuetapowa"
+ }
},
"media_modal": {
"previous": "Poprzednie",
@@ -44,15 +105,18 @@
},
"nav": {
"about": "O nas",
+ "administration": "Administracja",
"back": "Wróć",
"chat": "Lokalny czat",
"friend_requests": "Prośby o możliwość obserwacji",
"mentions": "Wzmianki",
+ "interactions": "Interakcje",
"dms": "Wiadomości prywatne",
"public_tl": "Publiczna oś czasu",
"timeline": "Oś czasu",
"twkn": "Cała znana sieć",
"user_search": "Wyszukiwanie użytkowników",
+ "search": "Wyszukiwanie",
"who_to_follow": "Sugestie obserwacji",
"preferences": "Preferencje"
},
@@ -64,7 +128,40 @@
"notifications": "Powiadomienia",
"read": "Przeczytane!",
"repeated_you": "powtórzył(-a) twój status",
- "no_more_notifications": "Nie masz więcej powiadomień"
+ "no_more_notifications": "Nie masz więcej powiadomień",
+ "migrated_to": "wyemigrował do",
+ "reacted_with": "zareagował z {0}"
+ },
+ "polls": {
+ "add_poll": "Dodaj ankietę",
+ "add_option": "Dodaj opcję",
+ "option": "Opcja",
+ "votes": "głosów",
+ "vote": "Głosuj",
+ "type": "Typ ankiety",
+ "single_choice": "jednokrotnego wyboru",
+ "multiple_choices": "wielokrotnego wyboru",
+ "expiry": "Czas trwania ankiety",
+ "expires_in": "Ankieta kończy się za{0}",
+ "expired": "Ankieta skończyła się {0} temu",
+ "not_enough_options": "Zbyt mało unikalnych opcji w ankiecie"
+ },
+ "emoji": {
+ "stickers": "Naklejki",
+ "emoji": "Emoji",
+ "keep_open": "Zostaw selektor otwarty",
+ "search_emoji": "Wyszukaj emoji",
+ "add_emoji": "Wstaw emoji",
+ "custom": "Niestandardowe emoji",
+ "unicode": "Emoji unicode",
+ "load_all_hint": "Załadowano pierwsze {saneAmount} emoji, Załadowanie wszystkich emoji może spowodować problemy z wydajnością.",
+ "load_all": "Ładuję wszystkie {emojiAmount} emoji"
+ },
+ "interactions": {
+ "favs_repeats": "Powtórzenia i ulubione",
+ "follows": "Nowi obserwujący",
+ "moves": "Użytkownik migruje",
+ "load_older": "Załaduj starsze interakcje"
},
"post_status": {
"new_status": "Dodaj nowy status",
@@ -79,8 +176,14 @@
},
"content_warning": "Temat (nieobowiązkowy)",
"default": "Właśnie wróciłem z kościoła",
- "direct_warning": "Ten wpis zobaczą tylko osoby, o których wspomniałeś(-aś).",
+ "direct_warning_to_all": "Ten wpis zobaczą wszystkie osoby, o których wspomniałeś(-aś).",
+ "direct_warning_to_first_only": "Ten wpis zobaczą tylko te osoby, o których wspomniałeś(-aś) na początku wiadomości.",
"posting": "Wysyłanie",
+ "scope_notice": {
+ "public": "Ten post będzie widoczny dla każdego",
+ "private": "Ten post będzie widoczny tylko dla twoich obserwujących",
+ "unlisted": "Ten post nie będzie widoczny na publicznej osi czasu i całej znanej sieci"
+ },
"scope": {
"direct": "Bezpośredni – Tylko dla wspomnianych użytkowników",
"private": "Tylko dla obserwujących – Umieść dla osób, które cię obserwują",
@@ -109,8 +212,40 @@
"password_confirmation_match": "musi być takie jak hasło"
}
},
+ "remote_user_resolver": {
+ "remote_user_resolver": "Wyszukiwarka użytkowników nietutejszych",
+ "searching_for": "Szukam",
+ "error": "Nie znaleziono."
+ },
+ "selectable_list": {
+ "select_all": "Zaznacz wszystko"
+ },
"settings": {
"app_name": "Nazwa aplikacji",
+ "security": "Bezpieczeństwo",
+ "enter_current_password_to_confirm": "Wprowadź obecne hasło, by potwierdzić twoją tożsamość",
+ "mfa": {
+ "otp" : "OTP",
+ "setup_otp" : "Ustaw OTP",
+ "wait_pre_setup_otp" : "początkowe ustawianie OTP",
+ "confirm_and_enable" : "Potwierdź i włącz OTP",
+ "title": "Weryfikacja dwuetapowa",
+ "generate_new_recovery_codes" : "Wygeneruj nowe kody zapasowe",
+ "warning_of_generate_new_codes" : "Po tym gdy generujesz nowe kody zapasowe, stare przestaną działać.",
+ "recovery_codes" : "Kody zapasowe.",
+ "waiting_a_recovery_codes": "Otrzymuję kody zapasowe...",
+ "recovery_codes_warning" : "Spisz kody na kartce papieru, albo zapisz je w bezpiecznym miejscu - inaczej nie zobaczysz ich już nigdy. Jeśli stracisz dostęp do twojej aplikacji 2FA i kodów zapasowych, nie będziesz miał dostępu do swojego konta.",
+ "authentication_methods" : "Metody weryfikacji",
+ "scan": {
+ "title": "Skanuj",
+ "desc": "Zeskanuj ten kod QR używając twojej aplikacji 2FA albo wpisz ten klucz:",
+ "secret_code": "Klucz"
+ },
+ "verify": {
+ "desc": "By włączyć weryfikację dwuetapową, wpisz kod z twojej aplikacji 2FA:"
+ }
+ },
+ "allow_following_move": "Zezwalaj na automatyczną obserwację gdy obserwowane konto migruje",
"attachmentRadius": "Załączniki",
"attachments": "Załączniki",
"autoload": "Włącz automatyczne ładowanie po przewinięciu do końca strony",
@@ -119,12 +254,20 @@
"avatarRadius": "Awatary",
"background": "Tło",
"bio": "Bio",
+ "block_export": "Eksport blokad",
+ "block_export_button": "Eksportuj twoje blokady do pliku .csv",
+ "block_import": "Import blokad",
+ "block_import_error": "Wystąpił błąd podczas importowania blokad",
+ "blocks_imported": "Zaimportowano blokady, przetwarzanie może zająć trochę czasu.",
"blocks_tab": "Bloki",
"btnRadius": "Przyciski",
"cBlue": "Niebieski (odpowiedz, obserwuj)",
"cGreen": "Zielony (powtórzenia)",
"cOrange": "Pomarańczowy (ulubione)",
"cRed": "Czerwony (anuluj)",
+ "change_email": "Zmień email",
+ "change_email_error": "Wystąpił problem podczas zmiany emaila.",
+ "changed_email": "Pomyślnie zmieniono email!",
"change_password": "Zmień hasło",
"change_password_error": "Podczas zmiany hasła wystąpił problem.",
"changed_password": "Pomyślnie zmieniono hasło!",
@@ -140,16 +283,20 @@
"delete_account_description": "Trwale usuń konto i wszystkie posty.",
"delete_account_error": "Wystąpił problem z usuwaniem twojego konta. Jeżeli problem powtarza się, poinformuj administratora swojej instancji.",
"delete_account_instructions": "Wprowadź swoje hasło w poniższe pole aby potwierdzić usunięcie konta.",
+ "discoverable": "Zezwól na odkrywanie tego konta w wynikach wyszukiwania i innych usługa.",
+ "domain_mutes": "Domeny",
"avatar_size_instruction": "Zalecany minimalny rozmiar awatarów to 150x150 pikseli.",
+ "pad_emoji": "Dodaj odstęp z obu stron emoji podczas dodawania selektorem",
+ "emoji_reactions_on_timeline": "Pokaż reakcje emoji na osi czasu",
"export_theme": "Zapisz motyw",
"filtering": "Filtrowanie",
"filtering_explanation": "Wszystkie statusy zawierające te słowa będą wyciszone. Jedno słowo na linijkę.",
"follow_export": "Eksport obserwowanych",
"follow_export_button": "Eksportuj swoją listę obserwowanych do pliku CSV",
- "follow_export_processing": "Przetwarzanie, wkrótce twój plik zacznie się ściągać.",
"follow_import": "Import obserwowanych",
"follow_import_error": "Błąd przy importowaniu obserwowanych",
"follows_imported": "Obserwowani zaimportowani! Przetwarzanie może trochę potrwać.",
+ "accent": "Akcent",
"foreground": "Pierwszy plan",
"general": "Ogólne",
"hide_attachments_in_convo": "Ukrywaj załączniki w rozmowach",
@@ -162,6 +309,7 @@
"hide_post_stats": "Ukrywaj statysyki postów (np. liczbę polubień)",
"hide_user_stats": "Ukrywaj statysyki użytkowników (np. liczbę obserwujących)",
"hide_filtered_statuses": "Ukrywaj filtrowane statusy",
+ "import_blocks_from_a_csv_file": "Importuj blokady z pliku CSV",
"import_followers_from_a_csv_file": "Importuj obserwowanych z pliku CSV",
"import_theme": "Załaduj motyw",
"inputRadius": "Pola tekstowe",
@@ -181,17 +329,22 @@
"use_contain_fit": "Nie przycinaj załączników na miniaturach",
"name": "Imię",
"name_bio": "Imię i bio",
+ "new_email": "Nowy email",
"new_password": "Nowe hasło",
"notification_visibility": "Rodzaje powiadomień do wyświetlania",
"notification_visibility_follows": "Obserwacje",
"notification_visibility_likes": "Ulubione",
"notification_visibility_mentions": "Wzmianki",
"notification_visibility_repeats": "Powtórzenia",
+ "notification_visibility_moves": "Użytkownik migruje",
+ "notification_visibility_emoji_reactions": "Reakcje",
"no_rich_text_description": "Usuwaj formatowanie ze wszystkich postów",
"no_blocks": "Bez blokad",
"no_mutes": "Bez wyciszeń",
"hide_follows_description": "Nie pokazuj kogo obserwuję",
"hide_followers_description": "Nie pokazuj kto mnie obserwuje",
+ "hide_follows_count_description": "Nie pokazuj licznika obserwowanych",
+ "hide_followers_count_description": "Nie pokazuj licznika obserwujących",
"show_admin_badge": "Pokazuj odznakę Administrator na moim profilu",
"show_moderator_badge": "Pokazuj odznakę Moderator na moim profilu",
"nsfw_clickthrough": "Włącz domyślne ukrywanie załączników o treści nieprzyzwoitej (NSFW)",
@@ -212,10 +365,14 @@
"reply_visibility_all": "Pokazuj wszystkie odpowiedzi",
"reply_visibility_following": "Pokazuj tylko odpowiedzi skierowane do mnie i osób które obserwuję",
"reply_visibility_self": "Pokazuj tylko odpowiedzi skierowane do mnie",
+ "autohide_floating_post_button": "Ukryj automatycznie przycisk \"Nowy post\" (mobile)",
"saving_err": "Nie udało się zapisać ustawień",
"saving_ok": "Zapisano ustawienia",
+ "search_user_to_block": "Wyszukaj kogo chcesz zablokować",
+ "search_user_to_mute": "Wyszukaj kogo chcesz wyciszyć",
"security_tab": "Bezpieczeństwo",
"scope_copy": "Kopiuj zakres podczas odpowiadania (DM-y zawsze są kopiowane)",
+ "minimal_scopes_mode": "Zminimalizuj opcje wyboru zakresu postów",
"set_new_avatar": "Ustaw nowy awatar",
"set_new_profile_background": "Ustaw nowe tło profilu",
"set_new_profile_banner": "Ustaw nowy banner profilu",
@@ -228,19 +385,32 @@
"post_status_content_type": "Post status content type",
"stop_gifs": "Odtwarzaj GIFy po najechaniu kursorem",
"streaming": "Włącz automatycznie strumieniowanie nowych postów gdy jesteś na początku strony",
+ "user_mutes": "Users",
+ "useStreamingApi": "Otrzymuj posty i powiadomienia w czasie rzeczywistym",
+ "useStreamingApiWarning": "(Niezalecane, eksperymentalne, pomija posty)",
"text": "Tekst",
"theme": "Motyw",
"theme_help": "Użyj kolorów w notacji szesnastkowej (#rrggbb), by stworzyć swój motyw.",
"theme_help_v2_1": "Możesz też zastąpić kolory i widoczność poszczególnych komponentów przełączając pola wyboru, użyj „Wyczyść wszystko” aby usunąć wszystkie zastąpienia.",
"theme_help_v2_2": "Ikony pod niektórych wpisami są wskaźnikami kontrastu pomiędzy tłem a tekstem, po najechaniu na nie otrzymasz szczegółowe informacje. Zapamiętaj, że jeżeli używasz przezroczystości, wskaźniki pokazują najgorszy możliwy przypadek.",
"tooltipRadius": "Etykiety/alerty",
+ "type_domains_to_mute": "Wpisz domeny, które chcesz wyciszyć",
"upload_a_photo": "Wyślij zdjęcie",
"user_settings": "Ustawienia użytkownika",
"values": {
"false": "nie",
"true": "tak"
},
+ "fun": "Zabawa",
+ "greentext": "Memiczne strzałki",
"notifications": "Powiadomienia",
+ "notification_setting": "Otrzymuj powiadomienia od:",
+ "notification_setting_follows": "Ludzi których obserwujesz",
+ "notification_setting_non_follows": "Ludzi których nie obserwujesz",
+ "notification_setting_followers": "Ludzi którzy obserwują ciebie",
+ "notification_setting_non_followers": "Ludzi którzy nie obserwują ciebie",
+ "notification_mutes": "By przestać otrzymywać powiadomienia od jednego użytkownika, wycisz go",
+ "notification_blocks": "Blokowanie uzytkownika zatrzymuje wszystkie powiadomienia i odsubskrybowuje go.",
"enable_web_push_notifications": "Włącz powiadomienia push",
"style": {
"switcher": {
@@ -252,7 +422,24 @@
"save_load_hint": "Opcje „zachowaj” pozwalają na pozostanie przy obecnych opcjach po wybraniu lub załadowaniu motywu, jak i przechowywanie ich podczas eksportowania motywu. Jeżeli wszystkie są odznaczone, eksportowanie motywu spowoduje zapisanie wszystkiego.",
"reset": "Wyzeruj",
"clear_all": "Wyczyść wszystko",
- "clear_opacity": "Wyczyść widoczność"
+ "clear_opacity": "Wyczyść widoczność",
+ "load_theme": "Załaduj motyw",
+ "keep_as_is": "Zostaw po staremu",
+ "use_snapshot": "Stara wersja",
+ "use_source": "Nowa wersja",
+ "help": {
+ "upgraded_from_v2": "PleromaFE zostało zaaktualizowane, motyw może wyglądać nieco inaczej niż sobie zapamiętałeś.",
+ "v2_imported": "Plik który zaimportowałeś został stworzony dla starszego FE. Próbujemy zwiększyć kompatybiliność, lecz wciąż mogą występować rozbieżności.",
+ "future_version_imported": "Plik który zaimportowałeś został stworzony w nowszej wersji FE.",
+ "older_version_imported": "Plik który zaimportowałeś został stworzony w starszej wersji FE.",
+ "snapshot_present": "Migawka motywu jest załadowana, więc wszystkie wartości zostały nadpisane. Zamiast tego, możesz załadować właściwe dane motywu",
+ "snapshot_missing": "Nie znaleziono migawki motywu w pliku, więc motyw może wyglądać inaczej niż pierwotnie zaplanowano.",
+ "fe_upgraded": "Silnik motywów PleromaFE został zaaktualizowany.",
+ "fe_downgraded": "Wersja PleromaFE została cofnięta.",
+ "migration_snapshot_ok": "Żeby być bezpiecznym, migawka motywu została załadowana. Możesz spróbować załadować dane motywu.",
+ "migration_napshot_gone": "Z jakiegoś powodu migawka zniknęła, niektóre rzeczy mogą wyglądać inaczej niż sobie zapamiętałeś.",
+ "snapshot_source_mismatch": "Konflikt wersji: najprawdopodobniej FE zostało cofnięte do poprzedniej wersji i zaaktualizowane ponownie, jeśli zmieniłeś motyw używając starszej wersji FE, najprawdopodobniej chcesz używać starszej wersji, w przeciwnym razie użyj nowej wersji."
+ }
},
"common": {
"color": "Kolor",
@@ -280,14 +467,28 @@
"_tab_label": "Zaawansowane",
"alert": "Tło alertu",
"alert_error": "Błąd",
+ "alert_warning": "Ostrzeżenie",
+ "alert_neutral": "Neutralne",
+ "post": "Posty/Bio użytkowników",
"badge": "Tło odznaki",
+ "popover": "Etykiety, menu, popovery",
"badge_notification": "Powiadomienie",
"panel_header": "Nagłówek panelu",
"top_bar": "Górny pasek",
"borders": "Granice",
"buttons": "Przyciski",
"inputs": "Pola wejścia",
- "faint_text": "Zanikający tekst"
+ "faint_text": "Zanikający tekst",
+ "underlay": "Podkład",
+ "poll": "Wykres ankiety",
+ "icons": "Ikony",
+ "highlight": "Podświetlone elementy",
+ "pressed": "Naciśnięte",
+ "selectedPost": "Wybrany post",
+ "selectedMenu": "Wybrany element menu",
+ "disabled": "Wyłączone",
+ "toggled": "Przełączone",
+ "tabs": "Karty"
},
"radii": {
"_tab_label": "Zaokrąglenie"
@@ -300,7 +501,7 @@
"blur": "Rozmycie",
"spread": "Szerokość",
"inset": "Inset",
- "hint": "Możesz też używać --zmiennych jako kolorów, aby wykorzystać zmienne CSS3. Pamiętaj, że ustawienie widoczności nie będzie wtedy działać.",
+ "hintV3": "Dla cieni możesz również użyć notacji {0} by użyć inny slot koloru.",
"filter_hint": {
"always_drop_shadow": "Ostrzeżenie, ten cień zawsze używa {0} jeżeli to obsługiwane przez przeglądarkę.",
"drop_shadow_syntax": "{0} nie obsługuje parametru {1} i słowa kluczowego {2}.",
@@ -357,6 +558,40 @@
"frontend_version": "Wersja front-endu"
}
},
+ "time": {
+ "day": "{0} dzień",
+ "days": "{0} dni",
+ "day_short": "{0}d",
+ "days_short": "{0}d",
+ "hour": "{0} godzina",
+ "hours": "{0} godzin",
+ "hour_short": "{0} godz.",
+ "hours_short": "{0} godz.",
+ "in_future": "za {0}",
+ "in_past": "{0} temu",
+ "minute": "{0} minuta",
+ "minutes": "{0} minut",
+ "minute_short": "{0}min",
+ "minutes_short": "{0}min",
+ "month": "{0} miesiąc",
+ "months": "{0} miesięcy",
+ "month_short": "{0} mies.",
+ "months_short": "{0} mies.",
+ "now": "teraz",
+ "now_short": "teraz",
+ "second": "{0} sekunda",
+ "seconds": "{0} sekund",
+ "second_short": "{0}s",
+ "seconds_short": "{0}s",
+ "week": "{0} tydzień",
+ "weeks": "{0} tygodni",
+ "week_short": "{0} tydz.",
+ "weeks_short": "{0} tyg.",
+ "year": "{0} rok",
+ "years": "{0} lata",
+ "year_short": "{0} r.",
+ "years_short": "{0} lata"
+ },
"timeline": {
"collapse": "Zwiń",
"conversation": "Rozmowa",
@@ -370,8 +605,17 @@
"no_statuses": "Brak statusów"
},
"status": {
+ "favorites": "Ulubione",
+ "repeats": "Powtórzenia",
+ "delete": "Usuń status",
+ "pin": "Przypnij na profilu",
+ "unpin": "Odepnij z profilu",
+ "pinned": "Przypnięte",
+ "delete_confirm": "Czy naprawdę chcesz usunąć ten status?",
"reply_to": "Odpowiedź dla",
- "replies_list": "Odpowiedzi:"
+ "replies_list": "Odpowiedzi:",
+ "mute_conversation": "Wycisz konwersację",
+ "unmute_conversation": "Odcisz konwersację"
},
"user_card": {
"approve": "Przyjmij",
@@ -388,25 +632,60 @@
"followers": "Obserwujący",
"following": "Obserwowany!",
"follows_you": "Obserwuje cię!",
+ "hidden": "Ukryte",
"its_you": "To ty!",
"media": "Media",
+ "mention": "Wspomnienie",
"mute": "Wycisz",
"muted": "Wyciszony(-a)",
"per_day": "dziennie",
"remote_follow": "Zdalna obserwacja",
+ "report": "Raportuj",
"statuses": "Statusy",
+ "subscribe": "Subskrybuj",
+ "unsubscribe": "Odsubskrybuj",
"unblock": "Odblokuj",
"unblock_progress": "Odblokowuję…",
"block_progress": "Blokuję…",
"unmute": "Cofnij wyciszenie",
"unmute_progress": "Cofam wyciszenie…",
- "mute_progress": "Wyciszam…"
+ "mute_progress": "Wyciszam…",
+ "hide_repeats": "Ukryj powtórzenia",
+ "show_repeats": "Pokaż powtórzenia",
+ "admin_menu": {
+ "moderation": "Moderacja",
+ "grant_admin": "Przyznaj admina",
+ "revoke_admin": "Odwołaj admina",
+ "grant_moderator": "Przyznaj moderatora",
+ "revoke_moderator": "Odwołaj moderatora",
+ "activate_account": "Aktywuj konto",
+ "deactivate_account": "Dezaktywuj konto",
+ "delete_account": "Usuń konto",
+ "force_nsfw": "Oznacz wszystkie posty jako NSFW",
+ "strip_media": "Usuń multimedia z postów",
+ "force_unlisted": "Wymuś posty na niepubliczne",
+ "sandbox": "Wymuś by posty były tylko dla obserwujących",
+ "disable_remote_subscription": "Zakaż obserwowania użytkownika ze zdalnych instancji",
+ "disable_any_subscription": "Zakaż całkowicie obserwowania użytkownika",
+ "quarantine": "Zakaż federowania postów od tego użytkownika",
+ "delete_user": "Usuń użytkownika",
+ "delete_user_confirmation": "Czy jesteś absolutnie pewny? Ta operacja nie może być cofnięta."
+ }
},
"user_profile": {
"timeline_title": "Oś czasu użytkownika",
"profile_does_not_exist": "Przepraszamy, ten profil nie istnieje.",
"profile_loading_error": "Przepraszamy, wystąpił błąd podczas ładowania tego profilu."
},
+ "user_reporting": {
+ "title": "Raportowanie {0}",
+ "add_comment_description": "Raport zostanie wysłany do moderatorów instancji. Możesz dodać powód dlaczego raportujesz to konto poniżej:",
+ "additional_comments": "Dodatkowe komentarze",
+ "forward_description": "To konto jest z innego serwera. Wysłać również tam kopię raportu?",
+ "forward_to": "Przekaż do{0}",
+ "submit": "Wyślij",
+ "generic_error": "Wystąpił błąd podczas przetwarzania twojej prośby."
+ },
"who_to_follow": {
"more": "Więcej",
"who_to_follow": "Propozycje obserwacji"
@@ -416,6 +695,7 @@
"repeat": "Powtórz",
"reply": "Odpowiedz",
"favorite": "Dodaj do ulubionych",
+ "add_reaction": "Dodaj reakcję",
"user_settings": "Ustawienia użytkownika"
},
"upload":{
@@ -431,5 +711,25 @@
"GiB": "GiB",
"TiB": "TiB"
}
+ },
+ "search": {
+ "people": "Ludzie",
+ "hashtags": "Hasztagi",
+ "person_talking": "{count} osoba rozmawia o tym",
+ "people_talking": "{count} osób rozmawia o tym",
+ "no_results": "Brak wyników"
+ },
+ "password_reset": {
+ "forgot_password": "Zapomniałeś hasła?",
+ "password_reset": "Reset hasła",
+ "instruction": "Wprowadź swój adres email lub nazwę użytkownika. Wyślemy ci link z którym możesz zresetować hasło.",
+ "placeholder": "Twój email lub nazwa użytkownika",
+ "check_email": "Sprawdź pocztę, aby uzyskać link do zresetowania hasła.",
+ "return_home": "Wróć do strony głównej",
+ "not_found": "Nie mogliśmy znaleźć tego emaila lub nazwy użytkownika.",
+ "too_many_requests": "Przekroczyłeś limit prób, spróbuj ponownie później.",
+ "password_reset_disabled": "Resetowanie hasła jest wyłączone. Proszę skontaktuj się z administratorem tej instancji.",
+ "password_reset_required": "Musisz zresetować hasło, by się zalogować.",
+ "password_reset_required_but_mailer_is_disabled": "Musisz zresetować hasło, ale resetowanie hasła jest wyłączone. Proszę skontaktuj się z administratorem tej instancji."
}
}
diff --git a/src/modules/config.js b/src/modules/config.js
index 7997521d..8f4638f5 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -34,7 +34,8 @@ export const defaultState = {
likes: true,
repeats: true,
moves: true,
- emojiReactions: false
+ emojiReactions: false,
+ followRequest: true
},
webPushNotifications: false,
muteWords: [],
diff --git a/src/modules/statuses.js b/src/modules/statuses.js
index f1b7dcbd..cd8c1dba 100644
--- a/src/modules/statuses.js
+++ b/src/modules/statuses.js
@@ -13,6 +13,7 @@ import {
omitBy
} from 'lodash'
import { set } from 'vue'
+import { isStatusNotification } from '../services/notification_utils/notification_utils.js'
import apiService from '../services/api/api.service.js'
// import parse from '../services/status_parser/status_parser.js'
@@ -321,7 +322,7 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
const addNewNotifications = (state, { dispatch, notifications, older, visibleNotificationTypes, rootGetters }) => {
each(notifications, (notification) => {
- if (notification.type !== 'follow' && notification.type !== 'move') {
+ if (isStatusNotification(notification.type)) {
notification.action = addStatusToGlobalStorage(state, notification.action).item
notification.status = notification.status && addStatusToGlobalStorage(state, notification.status).item
}
@@ -361,13 +362,16 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot
case 'move':
i18nString = 'migrated_to'
break
+ case 'follow_request':
+ i18nString = 'follow_request'
+ break
}
if (notification.type === 'pleroma:emoji_reaction') {
notifObj.body = rootGetters.i18n.t('notifications.reacted_with', [notification.emoji])
} else if (i18nString) {
notifObj.body = rootGetters.i18n.t('notifications.' + i18nString)
- } else {
+ } else if (isStatusNotification(notification.type)) {
notifObj.body = notification.status.text
}
@@ -521,6 +525,17 @@ export const mutations = {
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)
+ },
+ 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
},
@@ -680,6 +695,24 @@ const statuses = {
credentials: rootState.users.currentUser.credentials
})
},
+ markSingleNotificationAsSeen ({ rootState, commit }, { id }) {
+ commit('markSingleNotificationAsSeen', { id })
+ apiService.markNotificationsAsSeen({
+ single: true,
+ id,
+ credentials: rootState.users.currentUser.credentials
+ })
+ },
+ dismissNotificationLocal ({ rootState, commit }, { id }) {
+ commit('dismissNotification', { id })
+ },
+ dismissNotification ({ rootState, commit }, { id }) {
+ commit('dismissNotification', { id })
+ rootState.api.backendInteractor.dismissNotification({ id })
+ },
+ updateNotification ({ rootState, commit }, { id, updater }) {
+ commit('updateNotification', { id, updater })
+ },
fetchFavsAndRepeats ({ rootState, commit }, id) {
Promise.all([
rootState.api.backendInteractor.fetchFavoritedByUsers({ id }),
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index 7db1d094..72c8874f 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -4,7 +4,6 @@ import 'whatwg-fetch'
import { RegistrationError, StatusCodeError } from '../errors/errors'
/* eslint-env browser */
-const QVITTER_USER_NOTIFICATIONS_READ_URL = '/api/qvitter/statuses/notifications/read.json'
const BLOCKS_IMPORT_URL = '/api/pleroma/blocks_import'
const FOLLOW_IMPORT_URL = '/api/pleroma/follow_import'
const DELETE_ACCOUNT_URL = '/api/pleroma/delete_account'
@@ -17,6 +16,7 @@ const DEACTIVATE_USER_URL = '/api/pleroma/admin/users/deactivate'
const ADMIN_USERS_URL = '/api/pleroma/admin/users'
const SUGGESTIONS_URL = '/api/v1/suggestions'
const NOTIFICATION_SETTINGS_URL = '/api/pleroma/notification_settings'
+const NOTIFICATION_READ_URL = '/api/v1/pleroma/notifications/read'
const MFA_SETTINGS_URL = '/api/pleroma/accounts/mfa'
const MFA_BACKUP_CODES_URL = '/api/pleroma/accounts/mfa/backup_codes'
@@ -29,6 +29,7 @@ const MASTODON_LOGIN_URL = '/api/v1/accounts/verify_credentials'
const MASTODON_REGISTRATION_URL = '/api/v1/accounts'
const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites'
const MASTODON_USER_NOTIFICATIONS_URL = '/api/v1/notifications'
+const MASTODON_DISMISS_NOTIFICATION_URL = id => `/api/v1/notifications/${id}/dismiss`
const MASTODON_FAVORITE_URL = id => `/api/v1/statuses/${id}/favourite`
const MASTODON_UNFAVORITE_URL = id => `/api/v1/statuses/${id}/unfavourite`
const MASTODON_RETWEET_URL = id => `/api/v1/statuses/${id}/reblog`
@@ -495,8 +496,7 @@ const fetchTimeline = ({
until = false,
userId = false,
tag = false,
- withMuted = false,
- withMove = false
+ withMuted = false
}) => {
const timelineUrls = {
public: MASTODON_PUBLIC_TIMELINE,
@@ -536,11 +536,8 @@ const fetchTimeline = ({
if (timeline === 'public' || timeline === 'publicAndExternal') {
params.push(['only_media', false])
}
- if (timeline === 'notifications') {
- params.push(['with_move', withMove])
- }
- params.push(['count', 20])
+ params.push(['limit', 20])
params.push(['with_muted', withMuted])
const queryString = map(params, (param) => `${param[0]}=${param[1]}`).join('&')
@@ -844,12 +841,16 @@ const suggestions = ({ credentials }) => {
}).then((data) => data.json())
}
-const markNotificationsAsSeen = ({ id, credentials }) => {
+const markNotificationsAsSeen = ({ id, credentials, single = false }) => {
const body = new FormData()
- body.append('latest_id', id)
+ if (single) {
+ body.append('id', id)
+ } else {
+ body.append('max_id', id)
+ }
- return fetch(QVITTER_USER_NOTIFICATIONS_READ_URL, {
+ return fetch(NOTIFICATION_READ_URL, {
body,
headers: authHeaders(credentials),
method: 'POST'
@@ -1010,6 +1011,15 @@ const unmuteDomain = ({ domain, credentials }) => {
})
}
+const dismissNotification = ({ credentials, id }) => {
+ return promisedRequest({
+ url: MASTODON_DISMISS_NOTIFICATION_URL(id),
+ method: 'POST',
+ payload: { id },
+ credentials
+ })
+}
+
export const getMastodonSocketURI = ({ credentials, stream, args = {} }) => {
return Object.entries({
...(credentials
@@ -1165,6 +1175,7 @@ const apiService = {
denyUser,
suggestions,
markNotificationsAsSeen,
+ dismissNotification,
vote,
fetchPoll,
fetchFavoritedByUsers,
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index 724dcc4b..6dac7c15 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -1,4 +1,5 @@
import escape from 'escape-html'
+import { isStatusNotification } from '../notification_utils/notification_utils.js'
const qvitterStatusType = (status) => {
if (status.is_post_verb) {
@@ -340,9 +341,7 @@ export const parseNotification = (data) => {
if (masto) {
output.type = mastoDict[data.type] || data.type
output.seen = data.pleroma.is_seen
- output.status = output.type === 'follow' || output.type === 'move'
- ? null
- : parseStatus(data.status)
+ output.status = isStatusNotification(output.type) ? parseStatus(data.status) : null
output.action = output.status // TODO: Refactor, this is unneeded
output.target = output.type !== 'move'
? null
diff --git a/src/services/notification_utils/notification_utils.js b/src/services/notification_utils/notification_utils.js
index b17bd7bf..eb479227 100644
--- a/src/services/notification_utils/notification_utils.js
+++ b/src/services/notification_utils/notification_utils.js
@@ -1,4 +1,4 @@
-import { filter, sortBy } from 'lodash'
+import { filter, sortBy, includes } from 'lodash'
export const notificationsFromStore = store => store.state.statuses.notifications.data
@@ -7,10 +7,15 @@ export const visibleTypes = store => ([
store.state.config.notificationVisibility.mentions && 'mention',
store.state.config.notificationVisibility.repeats && 'repeat',
store.state.config.notificationVisibility.follows && 'follow',
+ store.state.config.notificationVisibility.followRequest && 'follow_request',
store.state.config.notificationVisibility.moves && 'move',
store.state.config.notificationVisibility.emojiReactions && 'pleroma:emoji_reaction'
].filter(_ => _))
+const statusNotifications = ['like', 'mention', 'repeat', 'pleroma:emoji_reaction']
+
+export const isStatusNotification = (type) => includes(statusNotifications, type)
+
const sortById = (a, b) => {
const seqA = Number(a.id)
const seqB = Number(b.id)
diff --git a/src/services/notifications_fetcher/notifications_fetcher.service.js b/src/services/notifications_fetcher/notifications_fetcher.service.js
index 864e32f8..64499a1b 100644
--- a/src/services/notifications_fetcher/notifications_fetcher.service.js
+++ b/src/services/notifications_fetcher/notifications_fetcher.service.js
@@ -11,12 +11,9 @@ const fetchAndUpdate = ({ store, credentials, older = false }) => {
const rootState = store.rootState || store.state
const timelineData = rootState.statuses.notifications
const hideMutedPosts = getters.mergedConfig.hideMutedPosts
- const allowFollowingMove = rootState.users.currentUser.allow_following_move
args['withMuted'] = !hideMutedPosts
- args['withMove'] = !allowFollowingMove
-
args['timeline'] = 'notifications'
if (older) {
if (timelineData.minId !== Number.POSITIVE_INFINITY) {