Merge branch 'develop' of git.pleroma.social:pleroma/pleroma-fe into develop

This commit is contained in:
sadposter 2021-03-03 13:51:57 +00:00
commit d1bffd7659
59 changed files with 609 additions and 143 deletions

View file

@ -4,12 +4,29 @@ 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/). The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased] ## [Unreleased]
### Added
- Added a quick settings to timeline header for easier access
- Added option to mark posts as sensitive by default
## [2.3.0] - 2021-03-01
### Fixed ### Fixed
- Button to remove uploaded media in post status form is now properly placed and sized. - Button to remove uploaded media in post status form is now properly placed and sized.
- Fixed shoutbox not working in mobile layout - Fixed shoutbox not working in mobile layout
- Fixed missing highlighted border in expanded conversations again
- Fixed some UI jumpiness when opening images particularly in chat view
- Fixed chat unread badge looking weird
- Fixed punycode names not working properly
- Fixed notifications crashing on an invalid notification
### Changed ### Changed
- Display 'people voted' instead of 'votes' for multi-choice polls - Display 'people voted' instead of 'votes' for multi-choice polls
- Optimized chat to not get horrible performance after keeping the same chat open for a long time
- When opening emoji picker or react picker, it automatically focuses the search field
- Language picker now uses native language names
### Added
- Added reason field for registration when approval is required
- Group staff members by role in the About page
## [2.2.3] - 2021-01-18 ## [2.2.3] - 2021-01-18
### Added ### Added
@ -21,7 +38,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Changed ### Changed
- Don't filter own posts when they hit your wordfilter - Don't filter own posts when they hit your wordfilter
- Language picker now uses native language names
## [2.2.2] - 2020-12-22 ## [2.2.2] - 2020-12-22
@ -31,7 +47,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Added some missing unicode emoji - Added some missing unicode emoji
- Added the upload limit to the Features panel in the About page - Added the upload limit to the Features panel in the About page
- Support for solid color wallpaper, instance doesn't have to define a wallpaper anymore - Support for solid color wallpaper, instance doesn't have to define a wallpaper anymore
- Group staff members by role in the About page
### Fixed ### Fixed
- Fixed the occasional bug where screen would scroll 1px when typing into a reply form - Fixed the occasional bug where screen would scroll 1px when typing into a reply form

View file

@ -103,7 +103,7 @@
"selenium-server": "2.53.1", "selenium-server": "2.53.1",
"semver": "^5.3.0", "semver": "^5.3.0",
"serviceworker-webpack-plugin": "^1.0.0", "serviceworker-webpack-plugin": "^1.0.0",
"shelljs": "^0.7.4", "shelljs": "^0.8.4",
"sinon": "^2.1.0", "sinon": "^2.1.0",
"sinon-chai": "^2.8.0", "sinon-chai": "^2.8.0",
"stylelint": "^13.6.1", "stylelint": "^13.6.1",

View file

@ -586,6 +586,7 @@ nav {
color: var(--faint, $fallback--faint); color: var(--faint, $fallback--faint);
box-shadow: 0px 0px 4px rgba(0,0,0,.6); box-shadow: 0px 0px 4px rgba(0,0,0,.6);
box-shadow: var(--topBarShadow); box-shadow: var(--topBarShadow);
box-sizing: border-box;
} }
.fade-enter-active, .fade-leave-active { .fade-enter-active, .fade-leave-active {
@ -878,6 +879,11 @@ nav {
overflow: hidden; overflow: hidden;
height: 100%; height: 100%;
// Get rid of scrollbar on body as scrolling happens on different element
body {
overflow: hidden;
}
// Ensures the fixed position of the mobile browser bars on scroll up / down events. // Ensures the fixed position of the mobile browser bars on scroll up / down events.
// Prevents the mobile browser bars from overlapping or hiding the message posting form. // Prevents the mobile browser bars from overlapping or hiding the message posting form.
@media all and (max-width: 800px) { @media all and (max-width: 800px) {

View file

@ -51,6 +51,7 @@ const getInstanceConfig = async ({ store }) => {
const vapidPublicKey = data.pleroma.vapid_public_key const vapidPublicKey = data.pleroma.vapid_public_key
store.dispatch('setInstanceOption', { name: 'textlimit', value: textlimit }) store.dispatch('setInstanceOption', { name: 'textlimit', value: textlimit })
store.dispatch('setInstanceOption', { name: 'accountApprovalRequired', value: data.approval_required })
if (vapidPublicKey) { if (vapidPublicKey) {
store.dispatch('setInstanceOption', { name: 'vapidPublicKey', value: vapidPublicKey }) store.dispatch('setInstanceOption', { name: 'vapidPublicKey', value: vapidPublicKey })

View file

@ -42,7 +42,7 @@
class="basic-user-card-screen-name" class="basic-user-card-screen-name"
:to="userProfileLink(user)" :to="userProfileLink(user)"
> >
@{{ user.screen_name }} @{{ user.screen_name_ui }}
</router-link> </router-link>
</div> </div>
<slot /> <slot />

View file

@ -73,7 +73,7 @@ const Chat = {
}, },
formPlaceholder () { formPlaceholder () {
if (this.recipient) { if (this.recipient) {
return this.$t('chats.message_user', { nickname: this.recipient.screen_name }) return this.$t('chats.message_user', { nickname: this.recipient.screen_name_ui })
} else { } else {
return '' return ''
} }
@ -234,6 +234,13 @@ const Chat = {
const scrollable = this.$refs.scrollable const scrollable = this.$refs.scrollable
return scrollable && scrollable.scrollTop <= 0 return scrollable && scrollable.scrollTop <= 0
}, },
cullOlderCheck () {
window.setTimeout(() => {
if (this.bottomedOut(JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET)) {
this.$store.dispatch('cullOlderMessages', this.currentChatMessageService.chatId)
}
}, 5000)
},
handleScroll: _.throttle(function () { handleScroll: _.throttle(function () {
if (!this.currentChat) { return } if (!this.currentChat) { return }
@ -241,6 +248,7 @@ const Chat = {
this.fetchChat({ maxId: this.currentChatMessageService.minId }) this.fetchChat({ maxId: this.currentChatMessageService.minId })
} else if (this.bottomedOut(JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET)) { } else if (this.bottomedOut(JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET)) {
this.jumpToBottomButtonVisible = false this.jumpToBottomButtonVisible = false
this.cullOlderCheck()
if (this.newMessageCount > 0) { if (this.newMessageCount > 0) {
// Use a delay before marking as read to prevent situation where new messages // Use a delay before marking as read to prevent situation where new messages
// arrive just as you're leaving the view and messages that you didn't actually // arrive just as you're leaving the view and messages that you didn't actually

View file

@ -98,10 +98,10 @@
.unread-message-count { .unread-message-count {
font-size: 0.8em; font-size: 0.8em;
left: 50%; left: 50%;
transform: translate(-50%, 0);
border-radius: 100%;
margin-top: -1rem; margin-top: -1rem;
padding: 0; padding: 0.1em;
border-radius: 50px;
position: absolute;
} }
.chat-loading-error { .chat-loading-error {

View file

@ -12,7 +12,7 @@ export default Vue.component('chat-title', {
], ],
computed: { computed: {
title () { title () {
return this.user ? this.user.screen_name : '' return this.user ? this.user.screen_name_ui : ''
}, },
htmlTitle () { htmlTitle () {
return this.user ? this.user.name_html : '' return this.user ? this.user.name_html : ''

View file

@ -50,7 +50,6 @@
.Conversation { .Conversation {
.conversation-status { .conversation-status {
border-left: none;
border-bottom-width: 1px; border-bottom-width: 1px;
border-bottom-style: solid; border-bottom-style: solid;
border-bottom-color: var(--border, $fallback--border); border-bottom-color: var(--border, $fallback--border);

View file

@ -194,11 +194,18 @@ const EmojiInput = {
} }
}, },
methods: { methods: {
focusPickerInput () {
const pickerEl = this.$refs.picker.$el
if (!pickerEl) return
const pickerInput = pickerEl.querySelector('input')
if (pickerInput) pickerInput.focus()
},
triggerShowPicker () { triggerShowPicker () {
this.showPicker = true this.showPicker = true
this.$refs.picker.startEmojiLoad() this.$refs.picker.startEmojiLoad()
this.$nextTick(() => { this.$nextTick(() => {
this.scrollIntoView() this.scrollIntoView()
this.focusPickerInput()
}) })
// This temporarily disables "click outside" handler // This temporarily disables "click outside" handler
// since external trigger also means click originates // since external trigger also means click originates
@ -214,6 +221,7 @@ const EmojiInput = {
if (this.showPicker) { if (this.showPicker) {
this.scrollIntoView() this.scrollIntoView()
this.$refs.picker.startEmojiLoad() this.$refs.picker.startEmojiLoad()
this.$nextTick(this.focusPickerInput)
} }
}, },
replace (replacement) { replace (replacement) {

View file

@ -9,8 +9,8 @@
<button <button
v-if="!hideEmojiButton" v-if="!hideEmojiButton"
class="button-unstyled emoji-picker-icon" class="button-unstyled emoji-picker-icon"
@click.prevent="togglePicker"
type="button" type="button"
@click.prevent="togglePicker"
> >
<FAIcon :icon="['far', 'smile-beam']" /> <FAIcon :icon="['far', 'smile-beam']" />
</button> </button>

View file

@ -116,8 +116,8 @@ export const suggestUsers = ({ dispatch, state }) => {
return diff + nameAlphabetically + screenNameAlphabetically return diff + nameAlphabetically + screenNameAlphabetically
/* eslint-disable camelcase */ /* eslint-disable camelcase */
}).map(({ screen_name, name, profile_image_url_original }) => ({ }).map(({ screen_name, screen_name_ui, name, profile_image_url_original }) => ({
displayText: screen_name, displayText: screen_name_ui,
detailText: name, detailText: name,
imageUrl: profile_image_url_original, imageUrl: profile_image_url_original,
replacement: '@' + screen_name + ' ' replacement: '@' + screen_name + ' '

View file

@ -73,11 +73,21 @@
} }
} }
@keyframes media-fadein {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.modal-image { .modal-image {
max-width: 90%; max-width: 90%;
max-height: 90%; max-height: 90%;
box-shadow: 0px 5px 15px 0 rgba(0, 0, 0, 0.5); box-shadow: 0px 5px 15px 0 rgba(0, 0, 0, 0.5);
image-orientation: from-image; // NOTE: only FF supports this image-orientation: from-image; // NOTE: only FF supports this
animation: 0.1s cubic-bezier(0.7, 0, 1, 0.6) media-fadein;
} }
.modal-view-button-arrow { .modal-view-button-arrow {

View file

@ -25,16 +25,16 @@
<div> <div>
<button <button
class="button-unstyled -link" class="button-unstyled -link"
@click.prevent="requireTOTP"
type="button" type="button"
@click.prevent="requireTOTP"
> >
{{ $t('login.enter_two_factor_code') }} {{ $t('login.enter_two_factor_code') }}
</button> </button>
<br> <br>
<button <button
class="button-unstyled -link" class="button-unstyled -link"
@click.prevent="abortMFA"
type="button" type="button"
@click.prevent="abortMFA"
> >
{{ $t('general.cancel') }} {{ $t('general.cancel') }}
</button> </button>

View file

@ -27,16 +27,16 @@
<div> <div>
<button <button
class="button-unstyled -link" class="button-unstyled -link"
@click.prevent="requireRecovery"
type="button" type="button"
@click.prevent="requireRecovery"
> >
{{ $t('login.enter_recovery_code') }} {{ $t('login.enter_recovery_code') }}
</button> </button>
<br> <br>
<button <button
class="button-unstyled -link" class="button-unstyled -link"
@click.prevent="abortMFA"
type="button" type="button"
@click.prevent="abortMFA"
> >
{{ $t('general.cancel') }} {{ $t('general.cancel') }}
</button> </button>

View file

@ -50,74 +50,74 @@
class="button-default dropdown-item" class="button-default dropdown-item"
@click="toggleTag(tags.FORCE_NSFW)" @click="toggleTag(tags.FORCE_NSFW)"
> >
{{ $t('user_card.admin_menu.force_nsfw') }}
<span <span
class="menu-checkbox" class="menu-checkbox"
:class="{ 'menu-checkbox-checked': hasTag(tags.FORCE_NSFW) }" :class="{ 'menu-checkbox-checked': hasTag(tags.FORCE_NSFW) }"
/> />
{{ $t('user_card.admin_menu.force_nsfw') }}
</button> </button>
<button <button
class="button-default dropdown-item" class="button-default dropdown-item"
@click="toggleTag(tags.STRIP_MEDIA)" @click="toggleTag(tags.STRIP_MEDIA)"
> >
{{ $t('user_card.admin_menu.strip_media') }}
<span <span
class="menu-checkbox" class="menu-checkbox"
:class="{ 'menu-checkbox-checked': hasTag(tags.STRIP_MEDIA) }" :class="{ 'menu-checkbox-checked': hasTag(tags.STRIP_MEDIA) }"
/> />
{{ $t('user_card.admin_menu.strip_media') }}
</button> </button>
<button <button
class="button-default dropdown-item" class="button-default dropdown-item"
@click="toggleTag(tags.FORCE_UNLISTED)" @click="toggleTag(tags.FORCE_UNLISTED)"
> >
{{ $t('user_card.admin_menu.force_unlisted') }}
<span <span
class="menu-checkbox" class="menu-checkbox"
:class="{ 'menu-checkbox-checked': hasTag(tags.FORCE_UNLISTED) }" :class="{ 'menu-checkbox-checked': hasTag(tags.FORCE_UNLISTED) }"
/> />
{{ $t('user_card.admin_menu.force_unlisted') }}
</button> </button>
<button <button
class="button-default dropdown-item" class="button-default dropdown-item"
@click="toggleTag(tags.SANDBOX)" @click="toggleTag(tags.SANDBOX)"
> >
{{ $t('user_card.admin_menu.sandbox') }}
<span <span
class="menu-checkbox" class="menu-checkbox"
:class="{ 'menu-checkbox-checked': hasTag(tags.SANDBOX) }" :class="{ 'menu-checkbox-checked': hasTag(tags.SANDBOX) }"
/> />
{{ $t('user_card.admin_menu.sandbox') }}
</button> </button>
<button <button
v-if="user.is_local" v-if="user.is_local"
class="button-default dropdown-item" class="button-default dropdown-item"
@click="toggleTag(tags.DISABLE_REMOTE_SUBSCRIPTION)" @click="toggleTag(tags.DISABLE_REMOTE_SUBSCRIPTION)"
> >
{{ $t('user_card.admin_menu.disable_remote_subscription') }}
<span <span
class="menu-checkbox" class="menu-checkbox"
:class="{ 'menu-checkbox-checked': hasTag(tags.DISABLE_REMOTE_SUBSCRIPTION) }" :class="{ 'menu-checkbox-checked': hasTag(tags.DISABLE_REMOTE_SUBSCRIPTION) }"
/> />
{{ $t('user_card.admin_menu.disable_remote_subscription') }}
</button> </button>
<button <button
v-if="user.is_local" v-if="user.is_local"
class="button-default dropdown-item" class="button-default dropdown-item"
@click="toggleTag(tags.DISABLE_ANY_SUBSCRIPTION)" @click="toggleTag(tags.DISABLE_ANY_SUBSCRIPTION)"
> >
{{ $t('user_card.admin_menu.disable_any_subscription') }}
<span <span
class="menu-checkbox" class="menu-checkbox"
:class="{ 'menu-checkbox-checked': hasTag(tags.DISABLE_ANY_SUBSCRIPTION) }" :class="{ 'menu-checkbox-checked': hasTag(tags.DISABLE_ANY_SUBSCRIPTION) }"
/> />
{{ $t('user_card.admin_menu.disable_any_subscription') }}
</button> </button>
<button <button
v-if="user.is_local" v-if="user.is_local"
class="button-default dropdown-item" class="button-default dropdown-item"
@click="toggleTag(tags.QUARANTINE)" @click="toggleTag(tags.QUARANTINE)"
> >
{{ $t('user_card.admin_menu.quarantine') }}
<span <span
class="menu-checkbox" class="menu-checkbox"
:class="{ 'menu-checkbox-checked': hasTag(tags.QUARANTINE) }" :class="{ 'menu-checkbox-checked': hasTag(tags.QUARANTINE) }"
/> />
{{ $t('user_card.admin_menu.quarantine') }}
</button> </button>
</span> </span>
</div> </div>
@ -163,25 +163,6 @@
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; @import '../../_variables.scss';
.menu-checkbox {
float: right;
min-width: 22px;
max-width: 22px;
min-height: 22px;
max-height: 22px;
line-height: 22px;
text-align: center;
border-radius: 0px;
background-color: $fallback--fg;
background-color: var(--input, $fallback--fg);
box-shadow: 0px 0px 2px black inset;
box-shadow: var(--inputShadow);
&.menu-checkbox-checked::after {
content: '✓';
}
}
.moderation-tools-popover { .moderation-tools-popover {
height: 100%; height: 100%;
.trigger { .trigger {

View file

@ -11,7 +11,7 @@
> >
<small> <small>
<router-link :to="userProfileLink"> <router-link :to="userProfileLink">
{{ notification.from_profile.screen_name }} {{ notification.from_profile.screen_name_ui }}
</router-link> </router-link>
</small> </small>
<button <button
@ -54,14 +54,14 @@
<bdi <bdi
v-if="!!notification.from_profile.name_html" v-if="!!notification.from_profile.name_html"
class="username" class="username"
:title="'@'+notification.from_profile.screen_name" :title="'@'+notification.from_profile.screen_name_ui"
v-html="notification.from_profile.name_html" v-html="notification.from_profile.name_html"
/> />
<!-- eslint-enable vue/no-v-html --> <!-- eslint-enable vue/no-v-html -->
<span <span
v-else v-else
class="username" class="username"
:title="'@'+notification.from_profile.screen_name" :title="'@'+notification.from_profile.screen_name_ui"
>{{ notification.from_profile.name }}</span> >{{ notification.from_profile.name }}</span>
<span v-if="notification.type === 'like'"> <span v-if="notification.type === 'like'">
<FAIcon <FAIcon
@ -152,7 +152,7 @@
:to="userProfileLink" :to="userProfileLink"
class="follow-name" class="follow-name"
> >
@{{ notification.from_profile.screen_name }} @{{ notification.from_profile.screen_name_ui }}
</router-link> </router-link>
<div <div
v-if="notification.type === 'follow_request'" v-if="notification.type === 'follow_request'"
@ -177,7 +177,7 @@
class="move-text" class="move-text"
> >
<router-link :to="targetUserProfileLink"> <router-link :to="targetUserProfileLink">
@{{ notification.target.screen_name }} @{{ notification.target.screen_name_ui }}
</router-link> </router-link>
</div> </div>
<template v-else> <template v-else>

View file

@ -151,6 +151,7 @@
border: none; border: none;
box-shadow: none; box-shadow: none;
background-color: transparent; background-color: transparent;
padding-right: 0.75em;
} }
} }

View file

@ -3,25 +3,32 @@ const Popover = {
props: { props: {
// Action to trigger popover: either 'hover' or 'click' // Action to trigger popover: either 'hover' or 'click'
trigger: String, trigger: String,
// Either 'top' or 'bottom' // Either 'top' or 'bottom'
placement: String, placement: String,
// Takes object with properties 'x' and 'y', values of these can be // Takes object with properties 'x' and 'y', values of these can be
// 'container' for using offsetParent as boundaries for either axis // 'container' for using offsetParent as boundaries for either axis
// or 'viewport' // or 'viewport'
boundTo: Object, boundTo: Object,
// Takes a selector to use as a replacement for the parent container // Takes a selector to use as a replacement for the parent container
// for getting boundaries for x an y axis // for getting boundaries for x an y axis
boundToSelector: String, boundToSelector: String,
// Takes a top/bottom/left/right object, how much space to leave // Takes a top/bottom/left/right object, how much space to leave
// between boundary and popover element // between boundary and popover element
margin: Object, margin: Object,
// Takes a x/y object and tells how many pixels to offset from // Takes a x/y object and tells how many pixels to offset from
// anchor point on either axis // anchor point on either axis
offset: Object, offset: Object,
// Replaces the classes you may want for the popover container. // Replaces the classes you may want for the popover container.
// Use 'popover-default' in addition to get the default popover // Use 'popover-default' in addition to get the default popover
// styles with your custom class. // styles with your custom class.
popoverClass: String, popoverClass: String,
// If true, subtract padding when calculating position for the popover, // If true, subtract padding when calculating position for the popover,
// use it when popover offset looks to be different on top vs bottom. // use it when popover offset looks to be different on top vs bottom.
removePadding: Boolean removePadding: Boolean
@ -121,9 +128,12 @@ const Popover = {
} }
}, },
showPopover () { showPopover () {
if (this.hidden) this.$emit('show') const wasHidden = this.hidden
this.hidden = false this.hidden = false
this.$nextTick(this.updateStyles) this.$nextTick(() => {
if (wasHidden) this.$emit('show')
this.updateStyles()
})
}, },
hidePopover () { hidePopover () {
if (!this.hidden) this.$emit('close') if (!this.hidden) this.$emit('close')

View file

@ -6,8 +6,8 @@
<button <button
ref="trigger" ref="trigger"
class="button-unstyled -fullwidth popover-trigger-button" class="button-unstyled -fullwidth popover-trigger-button"
@click="onClick"
type="button" type="button"
@click="onClick"
> >
<slot name="trigger" /> <slot name="trigger" />
</button> </button>
@ -82,10 +82,9 @@
.dropdown-item { .dropdown-item {
line-height: 21px; line-height: 21px;
margin-right: 5px;
overflow: auto; overflow: auto;
display: block; display: block;
padding: .25rem 1.0rem .25rem 1.5rem; padding: .5em 0.75em;
clear: both; clear: both;
font-weight: 400; font-weight: 400;
text-align: inherit; text-align: inherit;
@ -101,10 +100,9 @@
--btnText: var(--popoverText, $fallback--text); --btnText: var(--popoverText, $fallback--text);
&-icon { &-icon {
padding-left: 0.5rem;
svg { svg {
margin-right: 0.25rem; width: 22px;
margin-right: 0.75rem;
color: var(--menuPopoverIcon, $fallback--icon) color: var(--menuPopoverIcon, $fallback--icon)
} }
} }
@ -123,6 +121,33 @@
} }
} }
.menu-checkbox {
display: inline-block;
vertical-align: middle;
min-width: 22px;
max-width: 22px;
min-height: 22px;
max-height: 22px;
line-height: 22px;
text-align: center;
border-radius: 0px;
background-color: $fallback--fg;
background-color: var(--input, $fallback--fg);
box-shadow: 0px 0px 2px black inset;
box-shadow: var(--inputShadow);
margin-right: 0.75em;
&.menu-checkbox-checked::after {
font-size: 1.25em;
content: '✓';
}
&.menu-checkbox-radio::after {
font-size: 2em;
content: '•';
}
}
} }
} }
</style> </style>

View file

@ -115,7 +115,7 @@ const PostStatusForm = {
? this.copyMessageScope ? this.copyMessageScope
: this.$store.state.users.currentUser.default_scope : this.$store.state.users.currentUser.default_scope
const { postContentType: contentType } = this.$store.getters.mergedConfig const { postContentType: contentType, sensitiveByDefault } = this.$store.getters.mergedConfig
return { return {
dropFiles: [], dropFiles: [],
@ -126,7 +126,7 @@ const PostStatusForm = {
newStatus: { newStatus: {
spoilerText: this.subject || '', spoilerText: this.subject || '',
status: statusText, status: statusText,
nsfw: false, nsfw: !!sensitiveByDefault,
files: [], files: [],
poll: {}, poll: {},
mediaDescriptions: {}, mediaDescriptions: {},

View file

@ -23,6 +23,12 @@ const ReactButton = {
this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji }) this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji })
} }
close() close()
},
focusInput () {
this.$nextTick(() => {
const input = this.$el.querySelector('input')
if (input) input.focus()
})
} }
}, },
computed: { computed: {

View file

@ -6,6 +6,7 @@
:offset="{ y: 5 }" :offset="{ y: 5 }"
:bound-to="{ x: 'container' }" :bound-to="{ x: 'container' }"
remove-padding remove-padding
@show="focusInput"
> >
<div <div
slot="content" slot="content"

View file

@ -10,7 +10,8 @@ const registration = {
fullname: '', fullname: '',
username: '', username: '',
password: '', password: '',
confirm: '' confirm: '',
reason: ''
}, },
captcha: {} captcha: {}
}), }),
@ -24,7 +25,8 @@ const registration = {
confirm: { confirm: {
required, required,
sameAsPassword: sameAs('password') sameAsPassword: sameAs('password')
} },
reason: { required: requiredIf(() => this.accountApprovalRequired) }
} }
} }
}, },
@ -38,7 +40,10 @@ const registration = {
computed: { computed: {
token () { return this.$route.params.token }, token () { return this.$route.params.token },
bioPlaceholder () { bioPlaceholder () {
return this.$t('registration.bio_placeholder').replace(/\s*\n\s*/g, ' \n') return this.replaceNewlines(this.$t('registration.bio_placeholder'))
},
reasonPlaceholder () {
return this.replaceNewlines(this.$t('registration.reason_placeholder'))
}, },
...mapState({ ...mapState({
registrationOpen: (state) => state.instance.registrationOpen, registrationOpen: (state) => state.instance.registrationOpen,
@ -46,7 +51,8 @@ const registration = {
isPending: (state) => state.users.signUpPending, isPending: (state) => state.users.signUpPending,
serverValidationErrors: (state) => state.users.signUpErrors, serverValidationErrors: (state) => state.users.signUpErrors,
termsOfService: (state) => state.instance.tos, termsOfService: (state) => state.instance.tos,
accountActivationRequired: (state) => state.instance.accountActivationRequired accountActivationRequired: (state) => state.instance.accountActivationRequired,
accountApprovalRequired: (state) => state.instance.accountApprovalRequired
}) })
}, },
methods: { methods: {
@ -73,6 +79,9 @@ const registration = {
}, },
setCaptcha () { setCaptcha () {
this.getCaptcha().then(cpt => { this.captcha = cpt }) this.getCaptcha().then(cpt => { this.captcha = cpt })
},
replaceNewlines (str) {
return str.replace(/\s*\n\s*/g, ' \n')
} }
} }
} }

View file

@ -162,6 +162,23 @@
</ul> </ul>
</div> </div>
<div
v-if="accountApprovalRequired"
class="form-group"
>
<label
class="form--label"
for="reason"
>{{ $t('registration.reason') }}</label>
<textarea
id="reason"
v-model="user.reason"
:disabled="isPending"
class="form-control"
:placeholder="reasonPlaceholder"
/>
</div>
<div <div
v-if="captcha.type != 'none'" v-if="captcha.type != 'none'"
id="captcha-group" id="captcha-group"

View file

@ -8,8 +8,8 @@
class="button-unstyled scope" class="button-unstyled scope"
:class="css.direct" :class="css.direct"
:title="$t('post_status.scope.direct')" :title="$t('post_status.scope.direct')"
@click="changeVis('direct')"
type="button" type="button"
@click="changeVis('direct')"
> >
<FAIcon <FAIcon
icon="envelope" icon="envelope"
@ -21,8 +21,8 @@
class="button-unstyled scope" class="button-unstyled scope"
:class="css.private" :class="css.private"
:title="$t('post_status.scope.private')" :title="$t('post_status.scope.private')"
@click="changeVis('private')"
type="button" type="button"
@click="changeVis('private')"
> >
<FAIcon <FAIcon
icon="lock" icon="lock"
@ -34,8 +34,8 @@
class="button-unstyled scope" class="button-unstyled scope"
:class="css.unlisted" :class="css.unlisted"
:title="$t('post_status.scope.unlisted')" :title="$t('post_status.scope.unlisted')"
@click="changeVis('unlisted')"
type="button" type="button"
@click="changeVis('unlisted')"
> >
<FAIcon <FAIcon
icon="lock-open" icon="lock-open"
@ -47,8 +47,8 @@
class="button-unstyled scope" class="button-unstyled scope"
:class="css.public" :class="css.public"
:title="$t('post_status.scope.public')" :title="$t('post_status.scope.public')"
@click="changeVis('public')"
type="button" type="button"
@click="changeVis('public')"
> >
<FAIcon <FAIcon
icon="globe" icon="globe"

View file

@ -15,8 +15,8 @@
> >
<button <button
class="btn button-default search-button" class="btn button-default search-button"
@click="newQuery(searchTerm)"
type="submit" type="submit"
@click="newQuery(searchTerm)"
> >
<FAIcon icon="search" /> <FAIcon icon="search" />
</button> </button>

View file

@ -7,8 +7,8 @@
v-if="hidden" v-if="hidden"
class="button-unstyled nav-icon" class="button-unstyled nav-icon"
:title="$t('nav.search')" :title="$t('nav.search')"
@click.prevent.stop="toggleHidden"
type="button" type="button"
@click.prevent.stop="toggleHidden"
> >
<FAIcon <FAIcon
fixed-width fixed-width
@ -28,8 +28,8 @@
> >
<button <button
class="button-default search-button" class="button-default search-button"
@click="find(searchTerm)"
type="submit" type="submit"
@click="find(searchTerm)"
> >
<FAIcon <FAIcon
fixed-width fixed-width
@ -38,8 +38,8 @@
</button> </button>
<button <button
class="button-unstyled cancel-search" class="button-unstyled cancel-search"
@click.prevent.stop="toggleHidden"
type="button" type="button"
@click.prevent.stop="toggleHidden"
> >
<FAIcon <FAIcon
fixed-width fixed-width

View file

@ -4,8 +4,8 @@
> >
<Checkbox <Checkbox
:checked="state" :checked="state"
@change="update"
:disabled="disabled" :disabled="disabled"
@change="update"
> >
<span <span
v-if="!!$slots.default" v-if="!!$slots.default"
@ -23,14 +23,14 @@ import { get, set } from 'lodash'
import Checkbox from 'src/components/checkbox/checkbox.vue' import Checkbox from 'src/components/checkbox/checkbox.vue'
import ModifiedIndicator from './modified_indicator.vue' import ModifiedIndicator from './modified_indicator.vue'
export default { export default {
props: [
'path',
'disabled'
],
components: { components: {
Checkbox, Checkbox,
ModifiedIndicator ModifiedIndicator
}, },
props: [
'path',
'disabled'
],
computed: { computed: {
pathDefault () { pathDefault () {
const [firstSegment, ...rest] = this.path.split('.') const [firstSegment, ...rest] = this.path.split('.')

View file

@ -13,8 +13,8 @@
/> />
</span> </span>
<div <div
class="modified-tooltip"
slot="content" slot="content"
class="modified-tooltip"
> >
{{ $t('settings.setting_changed') }} {{ $t('settings.setting_changed') }}
</div> </div>
@ -32,8 +32,8 @@ library.add(
) )
export default { export default {
props: ['changed'], components: { Popover },
components: { Popover } props: ['changed']
} }
</script> </script>

View file

@ -75,8 +75,8 @@
<p>{{ $t('settings.filtering_explanation') }}</p> <p>{{ $t('settings.filtering_explanation') }}</p>
<textarea <textarea
id="muteWords" id="muteWords"
class="resize-height"
v-model="muteWordsString" v-model="muteWordsString"
class="resize-height"
/> />
</div> </div>
<div> <div>

View file

@ -144,7 +144,12 @@
</li> </li>
<li> <li>
<BooleanSetting path="minimalScopesMode"> <BooleanSetting path="minimalScopesMode">
{{ $t('settings.minimal_scopes_mode') }} {{ minimalScopesModeDefaultValue }} {{ $t('settings.minimal_scopes_mode') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="sensitiveByDefault">
{{ $t('settings.sensitive_by_default') }}
</BooleanSetting> </BooleanSetting>
</li> </li>
<li> <li>

View file

@ -136,7 +136,7 @@ const Status = {
} }
}, },
retweet () { return !!this.statusoid.retweeted_status }, retweet () { return !!this.statusoid.retweeted_status },
retweeter () { return this.statusoid.user.name || this.statusoid.user.screen_name }, retweeter () { return this.statusoid.user.name || this.statusoid.user.screen_name_ui },
retweeterHtml () { return this.statusoid.user.name_html }, retweeterHtml () { return this.statusoid.user.name_html },
retweeterProfileLink () { return this.generateUserProfileLink(this.statusoid.user.id, this.statusoid.user.screen_name) }, retweeterProfileLink () { return this.generateUserProfileLink(this.statusoid.user.id, this.statusoid.user.screen_name) },
status () { status () {
@ -216,7 +216,7 @@ const Status = {
return this.status.in_reply_to_screen_name return this.status.in_reply_to_screen_name
} else { } else {
const user = this.$store.getters.findUser(this.status.in_reply_to_user_id) const user = this.$store.getters.findUser(this.status.in_reply_to_user_id)
return user && user.screen_name return user && user.screen_name_ui
} }
}, },
replySubject () { replySubject () {

View file

@ -26,7 +26,7 @@
icon="retweet" icon="retweet"
/> />
<router-link :to="userProfileLink"> <router-link :to="userProfileLink">
{{ status.user.screen_name }} {{ status.user.screen_name_ui }}
</router-link> </router-link>
</small> </small>
<small <small
@ -156,10 +156,10 @@
</h4> </h4>
<router-link <router-link
class="account-name" class="account-name"
:title="status.user.screen_name" :title="status.user.screen_name_ui"
:to="userProfileLink" :to="userProfileLink"
> >
{{ status.user.screen_name }} {{ status.user.screen_name_ui }}
</router-link> </router-link>
<img <img
v-if="!!(status.user && status.user.favicon)" v-if="!!(status.user && status.user.favicon)"

View file

@ -2,12 +2,14 @@ import Status from '../status/status.vue'
import timelineFetcher from '../../services/timeline_fetcher/timeline_fetcher.service.js' import timelineFetcher from '../../services/timeline_fetcher/timeline_fetcher.service.js'
import Conversation from '../conversation/conversation.vue' import Conversation from '../conversation/conversation.vue'
import TimelineMenu from '../timeline_menu/timeline_menu.vue' import TimelineMenu from '../timeline_menu/timeline_menu.vue'
import TimelineQuickSettings from './timeline_quick_settings.vue'
import { debounce, throttle, keyBy } from 'lodash' import { debounce, throttle, keyBy } from 'lodash'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { faCircleNotch } from '@fortawesome/free-solid-svg-icons' import { faCircleNotch, faCog } from '@fortawesome/free-solid-svg-icons'
library.add( library.add(
faCircleNotch faCircleNotch,
faCog
) )
export const getExcludedStatusIdsByPinning = (statuses, pinnedStatusIds) => { export const getExcludedStatusIdsByPinning = (statuses, pinnedStatusIds) => {
@ -47,7 +49,8 @@ const Timeline = {
components: { components: {
Status, Status,
Conversation, Conversation,
TimelineMenu TimelineMenu,
TimelineQuickSettings
}, },
computed: { computed: {
newStatusCount () { newStatusCount () {

View file

@ -16,6 +16,7 @@
> >
{{ $t('timeline.up_to_date') }} {{ $t('timeline.up_to_date') }}
</div> </div>
<TimelineQuickSettings v-if="!embedded" />
</div> </div>
<div :class="classes.body"> <div :class="classes.body">
<div <div
@ -103,9 +104,12 @@
max-width: 100%; max-width: 100%;
flex-wrap: nowrap; flex-wrap: nowrap;
align-items: center; align-items: center;
position: relative;
.loadmore-button { .loadmore-button {
flex-shrink: 0; flex-shrink: 0;
} }
.loadmore-text { .loadmore-text {
flex-shrink: 0; flex-shrink: 0;
line-height: 1em; line-height: 1em;

View file

@ -0,0 +1,63 @@
import Popover from '../popover/popover.vue'
import BooleanSetting from '../settings_modal/helpers/boolean_setting.vue'
import { mapGetters } from 'vuex'
import { library } from '@fortawesome/fontawesome-svg-core'
import { faFilter, faFont, faWrench } from '@fortawesome/free-solid-svg-icons'
library.add(
faFilter,
faFont,
faWrench
)
const TimelineQuickSettings = {
components: {
Popover,
BooleanSetting
},
methods: {
setReplyVisibility (visibility) {
this.$store.dispatch('setOption', { name: 'replyVisibility', value: visibility })
this.$store.dispatch('queueFlushAll')
},
openTab (tab) {
this.$store.dispatch('openSettingsModalTab', tab)
}
},
computed: {
...mapGetters(['mergedConfig']),
loggedIn () {
return !!this.$store.state.users.currentUser
},
replyVisibilitySelf: {
get () { return this.mergedConfig.replyVisibility === 'self' },
set () { this.setReplyVisibility('self') }
},
replyVisibilityFollowing: {
get () { return this.mergedConfig.replyVisibility === 'following' },
set () { this.setReplyVisibility('following') }
},
replyVisibilityAll: {
get () { return this.mergedConfig.replyVisibility === 'all' },
set () { this.setReplyVisibility('all') }
},
hideMedia: {
get () { return this.mergedConfig.hideAttachments || this.mergedConfig.hideAttachmentsInConv },
set () {
const value = !this.hideMedia
this.$store.dispatch('setOption', { name: 'hideAttachments', value })
this.$store.dispatch('setOption', { name: 'hideAttachmentsInConv', value })
}
},
hideMutedPosts: {
get () { return this.mergedConfig.hideMutedPosts || this.mergedConfig.hideFilteredStatuses },
set () {
const value = !this.hideMutedPosts
this.$store.dispatch('setOption', { name: 'hideMutedPosts', value })
this.$store.dispatch('setOption', { name: 'hideFilteredStatuses', value })
}
}
}
}
export default TimelineQuickSettings

View file

@ -0,0 +1,107 @@
<template>
<Popover
trigger="click"
class="TimelineQuickSettings"
:bound-to="{ x: 'container' }"
>
<div
slot="content"
class="timeline-settings-menu dropdown-menu"
>
<div v-if="loggedIn">
<button
class="button-default dropdown-item"
@click="replyVisibilityAll = true"
>
<span
class="menu-checkbox"
:class="{ 'menu-checkbox-radio': replyVisibilityAll }"
/>{{ $t('settings.reply_visibility_all') }}
</button>
<button
class="button-default dropdown-item"
@click="replyVisibilityFollowing = true"
>
<span
class="menu-checkbox"
:class="{ 'menu-checkbox-radio': replyVisibilityFollowing }"
/>{{ $t('settings.reply_visibility_following_short') }}
</button>
<button
class="button-default dropdown-item"
@click="replyVisibilitySelf = true"
>
<span
class="menu-checkbox"
:class="{ 'menu-checkbox-radio': replyVisibilitySelf }"
/>{{ $t('settings.reply_visibility_self_short') }}
</button>
<div
role="separator"
class="dropdown-divider"
/>
</div>
<button
class="button-default dropdown-item"
@click="hideMedia = !hideMedia"
>
<span
class="menu-checkbox"
:class="{ 'menu-checkbox-checked': hideMedia }"
/>{{ $t('settings.hide_media_previews') }}
</button>
<button
class="button-default dropdown-item"
@click="hideMutedPosts = !hideMutedPosts"
>
<span
class="menu-checkbox"
:class="{ 'menu-checkbox-checked': hideMutedPosts }"
/>{{ $t('settings.hide_all_muted_posts') }}
</button>
<button
class="button-default dropdown-item dropdown-item-icon"
@click="openTab('filtering')"
>
<FAIcon icon="font" />{{ $t('settings.word_filter') }}
</button>
<button
class="button-default dropdown-item dropdown-item-icon"
@click="openTab('general')"
>
<FAIcon icon="wrench" />{{ $t('settings.more_settings') }}
</button>
</div>
<div slot="trigger">
<FAIcon icon="filter" />
</div>
</Popover>
</template>
<script src="./timeline_quick_settings.js"></script>
<style lang="scss">
.TimelineQuickSettings {
align-self: stretch;
> button {
font-size: 1.2em;
padding-left: 0.7em;
padding-right: 0.2em;
line-height: 100%;
height: 100%;
}
.dropdown-item {
margin: 0;
}
.timeline-settings-menu {
display: flex;
min-width: 12em;
flex-direction: column;
}
}
</style>

View file

@ -2,8 +2,8 @@
<StillImage <StillImage
v-if="user" v-if="user"
class="Avatar" class="Avatar"
:alt="user.screen_name" :alt="user.screen_name_ui"
:title="user.screen_name" :title="user.screen_name_ui"
:src="imgSrc(user.profile_image_url_original)" :src="imgSrc(user.profile_image_url_original)"
:class="{ 'avatar-compact': compact, 'better-shadow': betterShadow }" :class="{ 'avatar-compact': compact, 'better-shadow': betterShadow }"
:image-load-error="imageLoadError" :image-load-error="imageLoadError"

View file

@ -73,10 +73,10 @@
<div class="bottom-line"> <div class="bottom-line">
<router-link <router-link
class="user-screen-name" class="user-screen-name"
:title="user.screen_name" :title="user.screen_name_ui"
:to="userProfileLink(user)" :to="userProfileLink(user)"
> >
@{{ user.screen_name }} @{{ user.screen_name_ui }}
</router-link> </router-link>
<template v-if="!hideBio"> <template v-if="!hideBio">
<span <span

View file

@ -26,7 +26,7 @@
<!-- eslint-disable vue/no-v-html --> <!-- eslint-disable vue/no-v-html -->
<span v-html="user.name_html" /> <span v-html="user.name_html" />
<!-- eslint-enable vue/no-v-html --> <!-- eslint-enable vue/no-v-html -->
<span class="user-list-screen-name">{{ user.screen_name }}</span> <span class="user-list-screen-name">{{ user.screen_name_ui }}</span>
</div> </div>
</div> </div>
</div> </div>

View file

@ -6,7 +6,7 @@
<div class="user-reporting-panel panel"> <div class="user-reporting-panel panel">
<div class="panel-heading"> <div class="panel-heading">
<div class="title"> <div class="title">
{{ $t('user_reporting.title', [user.screen_name]) }} {{ $t('user_reporting.title', [user.screen_name_ui]) }}
</div> </div>
</div> </div>
<div class="panel-body"> <div class="panel-body">

View file

@ -228,6 +228,8 @@
"username_placeholder": "e.g. lain", "username_placeholder": "e.g. lain",
"fullname_placeholder": "e.g. Lain Iwakura", "fullname_placeholder": "e.g. Lain Iwakura",
"bio_placeholder": "e.g.\nHi, I'm Lain.\nIm an anime girl living in suburban Japan. You may know me from the Wired.", "bio_placeholder": "e.g.\nHi, I'm Lain.\nIm an anime girl living in suburban Japan. You may know me from the Wired.",
"reason": "Reason to register",
"reason_placeholder": "This instance approves registrations manually.\nLet the administration know why you want to register.",
"validations": { "validations": {
"username_required": "cannot be left blank", "username_required": "cannot be left blank",
"fullname_required": "cannot be left blank", "fullname_required": "cannot be left blank",
@ -325,6 +327,7 @@
"export_theme": "Save preset", "export_theme": "Save preset",
"filtering": "Filtering", "filtering": "Filtering",
"filtering_explanation": "All statuses containing these words will be muted, one per line", "filtering_explanation": "All statuses containing these words will be muted, one per line",
"word_filter": "Word filter",
"follow_export": "Follow export", "follow_export": "Follow export",
"follow_export_button": "Export your follows to a csv file", "follow_export_button": "Export your follows to a csv file",
"follow_import": "Follow import", "follow_import": "Follow import",
@ -335,7 +338,9 @@
"general": "General", "general": "General",
"hide_attachments_in_convo": "Hide attachments in conversations", "hide_attachments_in_convo": "Hide attachments in conversations",
"hide_attachments_in_tl": "Hide attachments in timeline", "hide_attachments_in_tl": "Hide attachments in timeline",
"hide_media_previews": "Hide media previews",
"hide_muted_posts": "Hide posts of muted users", "hide_muted_posts": "Hide posts of muted users",
"hide_all_muted_posts": "Hide muted posts",
"max_thumbnails": "Maximum amount of thumbnails per post", "max_thumbnails": "Maximum amount of thumbnails per post",
"hide_isp": "Hide instance-specific panel", "hide_isp": "Hide instance-specific panel",
"hide_wallpaper": "Hide instance wallpaper", "hide_wallpaper": "Hide instance wallpaper",
@ -405,6 +410,8 @@
"reply_visibility_all": "Show all replies", "reply_visibility_all": "Show all replies",
"reply_visibility_following": "Only show replies directed at me or users I'm following", "reply_visibility_following": "Only show replies directed at me or users I'm following",
"reply_visibility_self": "Only show replies directed at me", "reply_visibility_self": "Only show replies directed at me",
"reply_visibility_following_short": "Show replies to my follows",
"reply_visibility_self_short": "Show replies to self only",
"autohide_floating_post_button": "Automatically hide New Post button (mobile)", "autohide_floating_post_button": "Automatically hide New Post button (mobile)",
"saving_err": "Error saving settings", "saving_err": "Error saving settings",
"saving_ok": "Settings saved", "saving_ok": "Settings saved",
@ -431,6 +438,7 @@
"subject_line_mastodon": "Like mastodon: copy as is", "subject_line_mastodon": "Like mastodon: copy as is",
"subject_line_noop": "Do not copy", "subject_line_noop": "Do not copy",
"post_status_content_type": "Post status content type", "post_status_content_type": "Post status content type",
"sensitive_by_default": "Mark posts as sensitive by default",
"stop_gifs": "Play-on-hover GIFs", "stop_gifs": "Play-on-hover GIFs",
"streaming": "Enable automatic streaming of new posts when scrolled to the top", "streaming": "Enable automatic streaming of new posts when scrolled to the top",
"user_mutes": "Users", "user_mutes": "Users",
@ -460,6 +468,7 @@
"notification_mutes": "To stop receiving notifications from a specific user, use a mute.", "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.", "notification_blocks": "Blocking a user stops all notifications as well as unsubscribes them.",
"enable_web_push_notifications": "Enable web push notifications", "enable_web_push_notifications": "Enable web push notifications",
"more_settings": "More settings",
"style": { "style": {
"switcher": { "switcher": {
"keep_color": "Keep colors", "keep_color": "Keep colors",

View file

@ -35,7 +35,11 @@
"retry": "Reprovi", "retry": "Reprovi",
"error_retry": "Bonvolu reprovi", "error_retry": "Bonvolu reprovi",
"loading": "Enlegante…", "loading": "Enlegante…",
"peek": "Antaŭmontri" "peek": "Antaŭmontri",
"role": {
"moderator": "Reguligisto",
"admin": "Administranto"
}
}, },
"image_cropper": { "image_cropper": {
"crop_picture": "Tondi bildon", "crop_picture": "Tondi bildon",
@ -365,7 +369,8 @@
"post": "Afiŝoj/Priskriboj de uzantoj", "post": "Afiŝoj/Priskriboj de uzantoj",
"alert_neutral": "Neŭtrala", "alert_neutral": "Neŭtrala",
"alert_warning": "Averto", "alert_warning": "Averto",
"toggled": "Ŝaltita" "toggled": "Ŝaltita",
"wallpaper": "Fonbildo"
}, },
"radii": { "radii": {
"_tab_label": "Rondeco" "_tab_label": "Rondeco"
@ -516,7 +521,9 @@
"mute_import_error": "Eraris enporto de silentigoj", "mute_import_error": "Eraris enporto de silentigoj",
"mute_import": "Enporto de silentigoj", "mute_import": "Enporto de silentigoj",
"mute_export_button": "Elportu viajn silentigojn al CSV-dosiero", "mute_export_button": "Elportu viajn silentigojn al CSV-dosiero",
"mute_export": "Elporto de silentigoj" "mute_export": "Elporto de silentigoj",
"hide_wallpaper": "Kaŝi fonbildon de nodo",
"setting_changed": "Agordo malsamas de la implicita"
}, },
"timeline": { "timeline": {
"collapse": "Maletendi", "collapse": "Maletendi",
@ -586,7 +593,8 @@
"show_repeats": "Montri ripetojn", "show_repeats": "Montri ripetojn",
"hide_repeats": "Kaŝi ripetojn", "hide_repeats": "Kaŝi ripetojn",
"unsubscribe": "Ne ricevi sciigojn", "unsubscribe": "Ne ricevi sciigojn",
"subscribe": "Ricevi sciigojn" "subscribe": "Ricevi sciigojn",
"bot": "Roboto"
}, },
"user_profile": { "user_profile": {
"timeline_title": "Historio de uzanto", "timeline_title": "Historio de uzanto",
@ -612,7 +620,8 @@
"error": { "error": {
"base": "Alŝuto malsukcesis.", "base": "Alŝuto malsukcesis.",
"file_too_big": "Dosiero estas tro granda [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]", "file_too_big": "Dosiero estas tro granda [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]",
"default": "Reprovu pli poste" "default": "Reprovu pli poste",
"message": "Malsukcesis alŝuto: {0}"
}, },
"file_size_units": { "file_size_units": {
"B": "B", "B": "B",
@ -645,7 +654,9 @@
"votes": "voĉoj", "votes": "voĉoj",
"option": "Elekteblo", "option": "Elekteblo",
"add_option": "Aldoni elekteblon", "add_option": "Aldoni elekteblon",
"add_poll": "Aldoni enketon" "add_poll": "Aldoni enketon",
"votes_count": "{count} voĉdono | {count} voĉdonoj",
"people_voted_count": "{count} persono voĉdonis | {count} personoj voĉdonis"
}, },
"importer": { "importer": {
"error": "Eraris enporto de ĉi tiu dosiero.", "error": "Eraris enporto de ĉi tiu dosiero.",
@ -732,7 +743,9 @@
"repeats": "Ripetoj", "repeats": "Ripetoj",
"favorites": "Ŝatoj", "favorites": "Ŝatoj",
"status_deleted": "Ĉi tiu afiŝo foriĝis", "status_deleted": "Ĉi tiu afiŝo foriĝis",
"nsfw": "Konsterna" "nsfw": "Konsterna",
"expand": "Etendi",
"external_source": "Ekstera fonto"
}, },
"time": { "time": {
"years_short": "{0}j", "years_short": "{0}j",

View file

@ -584,7 +584,9 @@
"fullname_placeholder": "es. Lupo Lucio", "fullname_placeholder": "es. Lupo Lucio",
"username_placeholder": "es. mister_wolf", "username_placeholder": "es. mister_wolf",
"new_captcha": "Clicca l'immagine per avere un altro captcha", "new_captcha": "Clicca l'immagine per avere un altro captcha",
"captcha": "CAPTCHA" "captcha": "CAPTCHA",
"reason_placeholder": "L'amministratore esamina ciascuna richiesta.\nFornisci il motivo della tua iscrizione.",
"reason": "Motivo dell'iscrizione"
}, },
"user_profile": { "user_profile": {
"timeline_title": "Sequenza dell'Utente", "timeline_title": "Sequenza dell'Utente",

View file

@ -201,7 +201,9 @@
"password_required": "必須", "password_required": "必須",
"password_confirmation_required": "必須", "password_confirmation_required": "必須",
"password_confirmation_match": "パスワードが違います" "password_confirmation_match": "パスワードが違います"
} },
"reason_placeholder": "このインスタンスは、新規登録を手動で受け付けています。\n登録したい理由を、インスタンスの管理者に教えてください。",
"reason": "登録するための目的"
}, },
"selectable_list": { "selectable_list": {
"select_all": "すべて選択" "select_all": "すべて選択"
@ -411,8 +413,8 @@
"contrast": { "contrast": {
"hint": "コントラストは {ratio} です。{level}。({context})", "hint": "コントラストは {ratio} です。{level}。({context})",
"level": { "level": {
"aa": "AAレベルガイドライン (ミニマル) を満たします", "aa": "AAレベルガイドライン (最低限) を満たします",
"aaa": "AAAレベルガイドライン (レコメンデッド) を満たします", "aaa": "AAAレベルガイドライン (推奨) を満たします",
"bad": "ガイドラインを満たしません" "bad": "ガイドラインを満たしません"
}, },
"context": { "context": {
@ -571,7 +573,8 @@
"mute_export": "ミュートのエクスポート", "mute_export": "ミュートのエクスポート",
"allow_following_move": "フォロー中のアカウントが引っ越したとき、自動フォローを許可する", "allow_following_move": "フォロー中のアカウントが引っ越したとき、自動フォローを許可する",
"setting_changed": "規定の設定と異なっています", "setting_changed": "規定の設定と異なっています",
"greentext": "引用を緑色で表示" "greentext": "引用を緑色で表示",
"sensitive_by_default": "はじめから投稿をセンシティブとして設定"
}, },
"time": { "time": {
"day": "{0}日", "day": "{0}日",

View file

@ -35,7 +35,11 @@
"retry": "다시 시도하십시오", "retry": "다시 시도하십시오",
"error_retry": "다시 시도하십시오", "error_retry": "다시 시도하십시오",
"generic_error": "잘못되었습니다", "generic_error": "잘못되었습니다",
"more": "더 보기" "more": "더 보기",
"role": {
"moderator": "중재자",
"admin": "관리자"
}
}, },
"login": { "login": {
"login": "로그인", "login": "로그인",
@ -85,7 +89,8 @@
"repeated_you": "당신의 게시물을 리핏", "repeated_you": "당신의 게시물을 리핏",
"no_more_notifications": "알림이 없습니다", "no_more_notifications": "알림이 없습니다",
"migrated_to": "이사했습니다", "migrated_to": "이사했습니다",
"reacted_with": "{0} 로 반응했습니다" "reacted_with": "{0} 로 반응했습니다",
"error": "알림 불러오기 실패: {0}"
}, },
"post_status": { "post_status": {
"new_status": "새 게시물 게시", "new_status": "새 게시물 게시",
@ -93,7 +98,10 @@
"account_not_locked_warning_link": "잠김", "account_not_locked_warning_link": "잠김",
"attachments_sensitive": "첨부물을 민감함으로 설정", "attachments_sensitive": "첨부물을 민감함으로 설정",
"content_type": { "content_type": {
"text/plain": "평문" "text/plain": "평문",
"text/bbcode": "BBCode",
"text/markdown": "Markdown",
"text/html": "HTML"
}, },
"content_warning": "주제 (필수 아님)", "content_warning": "주제 (필수 아님)",
"default": "인천공항에 도착했습니다.", "default": "인천공항에 도착했습니다.",
@ -106,7 +114,13 @@
"unlisted": "비공개 - 공개 타임라인에 게시 안 함" "unlisted": "비공개 - 공개 타임라인에 게시 안 함"
}, },
"preview_empty": "아무것도 없습니다", "preview_empty": "아무것도 없습니다",
"preview": "미리보기" "preview": "미리보기",
"scope_notice": {
"public": "이 글은 누구나 볼 수 있습니다"
},
"media_description_error": "파일을 올리지 못하였습니다. 다시한번 시도하여 주십시오",
"empty_status_error": "글을 입력하십시오",
"media_description": "첨부파일 설명"
}, },
"registration": { "registration": {
"bio": "소개", "bio": "소개",
@ -288,7 +302,16 @@
"borders": "테두리", "borders": "테두리",
"buttons": "버튼", "buttons": "버튼",
"inputs": "입력칸", "inputs": "입력칸",
"faint_text": "흐려진 텍스트" "faint_text": "흐려진 텍스트",
"chat": {
"border": "경계선",
"outgoing": "송신",
"incoming": "수신"
},
"selectedMenu": "선택된 메뉴 요소",
"selectedPost": "선택된 글",
"icons": "아이콘",
"alert_warning": "경고"
}, },
"radii": { "radii": {
"_tab_label": "둥글기" "_tab_label": "둥글기"
@ -364,9 +387,25 @@
"generate_new_recovery_codes": "새로운 복구 코드를 작성", "generate_new_recovery_codes": "새로운 복구 코드를 작성",
"title": "2단계인증", "title": "2단계인증",
"confirm_and_enable": "OTP 확인과 활성화", "confirm_and_enable": "OTP 확인과 활성화",
"setup_otp": "OTP 설치" "setup_otp": "OTP 설치",
"otp": "OTP"
}, },
"security": "보안" "security": "보안",
"emoji_reactions_on_timeline": "이모지 반응을 타임라인으로 표시",
"avatar_size_instruction": "크기를 150x150 이상으로 설정할 것을 추장합니다.",
"blocks_tab": "차단",
"notification_setting_privacy": "보안",
"user_mutes": "사용자",
"notification_visibility_emoji_reactions": "반응",
"profile_fields": {
"value": "내용"
},
"mutes_and_blocks": "침묵과 차단",
"chatMessageRadius": "챗 메시지",
"change_email": "전자메일 주소 바꾸기",
"changed_email": "메일주소가 갱신되었습니다!",
"bot": "이 계정은 bot입니다",
"mutes_tab": "침묵"
}, },
"timeline": { "timeline": {
"collapse": "접기", "collapse": "접기",
@ -445,7 +484,11 @@
"votes": "표", "votes": "표",
"vote": "투표", "vote": "투표",
"type": "투표 형식", "type": "투표 형식",
"expiry": "투표 기간" "expiry": "투표 기간",
"votes_count": "{count} 표 | {count} 표",
"people_voted_count": "{count} 명 투표 | {count} 명 투표",
"option": "선택지",
"add_option": "선택지 추가"
}, },
"media_modal": { "media_modal": {
"next": "다음", "next": "다음",
@ -500,5 +543,44 @@
}, },
"federation": "연합" "federation": "연합"
} }
},
"shoutbox": {
"title": "Shoutbox"
},
"time": {
"years_short": "{0} 년",
"year_short": "{0} 년",
"years": "{0} 년",
"year": "{0} 년",
"weeks_short": "{0} 주일",
"week_short": "{0} 주일",
"weeks": "{0} 주일",
"week": "{0} 주일",
"seconds_short": "{0} 초",
"second_short": "{0} 초",
"seconds": "{0} 초",
"second": "{0} 초",
"now_short": "방금",
"now": "방끔",
"months_short": "{0} 달 전",
"month_short": "{0} 달 전",
"months": "{0} 달 전",
"month": "{0} 달 전",
"minutes_short": "{0} 분",
"minute_short": "{0} 분",
"minutes": "{0} 분",
"minute": "{0} 분",
"in_past": "{0} 전",
"hours_short": "{0} 시간",
"hour_short": "{0} 시간",
"hours": "{0} 시간",
"hour": "{0} 시간",
"days_short": "{0} 일",
"day_short": "{0} 일",
"days": "{0} 일",
"day": "{0} 일"
},
"remote_user_resolver": {
"error": "찾을 수 없습니다."
} }
} }

View file

@ -39,7 +39,11 @@
"close": "关闭", "close": "关闭",
"retry": "重试", "retry": "重试",
"error_retry": "请重试", "error_retry": "请重试",
"loading": "载入中…" "loading": "载入中…",
"role": {
"moderator": "监察员",
"admin": "管理员"
}
}, },
"image_cropper": { "image_cropper": {
"crop_picture": "裁剪图片", "crop_picture": "裁剪图片",
@ -120,7 +124,9 @@
"expiry": "投票期限", "expiry": "投票期限",
"expires_in": "投票于 {0} 后结束", "expires_in": "投票于 {0} 后结束",
"expired": "投票 {0} 前已结束", "expired": "投票 {0} 前已结束",
"not_enough_options": "投票的选项太少" "not_enough_options": "投票的选项太少",
"votes_count": "{count} 票 | {count} 票",
"people_voted_count": "{count} 人已投票 | {count} 人已投票"
}, },
"stickers": { "stickers": {
"add_sticker": "添加贴纸" "add_sticker": "添加贴纸"
@ -183,7 +189,9 @@
"password_required": "不能留空", "password_required": "不能留空",
"password_confirmation_required": "不能留空", "password_confirmation_required": "不能留空",
"password_confirmation_match": "密码不一致" "password_confirmation_match": "密码不一致"
} },
"reason_placeholder": "此实例的注册需要手动批准。\n请让管理员知道您为什么想要注册。",
"reason": "注册理由"
}, },
"selectable_list": { "selectable_list": {
"select_all": "选择全部" "select_all": "选择全部"
@ -552,7 +560,8 @@
"mute_import": "隐藏名单导入", "mute_import": "隐藏名单导入",
"mute_export_button": "导出你的隐藏名单到一个 csv 文件", "mute_export_button": "导出你的隐藏名单到一个 csv 文件",
"mute_export": "隐藏名单导出", "mute_export": "隐藏名单导出",
"hide_wallpaper": "隐藏实例壁纸" "hide_wallpaper": "隐藏实例壁纸",
"setting_changed": "与默认设置不同"
}, },
"time": { "time": {
"day": "{0} 天", "day": "{0} 天",
@ -683,7 +692,8 @@
"show_repeats": "显示转发", "show_repeats": "显示转发",
"hide_repeats": "隐藏转发", "hide_repeats": "隐藏转发",
"message": "消息", "message": "消息",
"mention": "提及" "mention": "提及",
"bot": "机器人"
}, },
"user_profile": { "user_profile": {
"timeline_title": "用户时间线", "timeline_title": "用户时间线",

View file

@ -115,6 +115,9 @@ const chats = {
}, },
handleMessageError ({ commit }, value) { handleMessageError ({ commit }, value) {
commit('handleMessageError', { commit, ...value }) commit('handleMessageError', { commit, ...value })
},
cullOlderMessages ({ commit }, chatId) {
commit('cullOlderMessages', chatId)
} }
}, },
mutations: { mutations: {
@ -227,6 +230,9 @@ const chats = {
handleMessageError (state, { chatId, fakeId, isRetry }) { handleMessageError (state, { chatId, fakeId, isRetry }) {
const chatMessageService = state.openedChatMessageServices[chatId] const chatMessageService = state.openedChatMessageServices[chatId]
chatService.handleMessageError(chatMessageService, fakeId, isRetry) chatService.handleMessageError(chatMessageService, fakeId, isRetry)
},
cullOlderMessages (state, chatId) {
chatService.cullOlderMessages(state.openedChatMessageServices[chatId])
} }
} }
} }

View file

@ -67,7 +67,8 @@ export const defaultState = {
greentext: undefined, // instance default greentext: undefined, // instance default
hidePostStats: undefined, // instance default hidePostStats: undefined, // instance default
hideUserStats: undefined, // instance default hideUserStats: undefined, // instance default
virtualScrolling: undefined // instance default virtualScrolling: undefined, // instance default
sensitiveByDefault: undefined // instance default
} }
// caching the instance default properties // caching the instance default properties

View file

@ -43,6 +43,7 @@ const defaultState = {
subjectLineBehavior: 'email', subjectLineBehavior: 'email',
theme: 'pleroma-dark', theme: 'pleroma-dark',
virtualScrolling: true, virtualScrolling: true,
sensitiveByDefault: false,
// Nasty stuff // Nasty stuff
customEmoji: [], customEmoji: [],

View file

@ -13,7 +13,11 @@ import {
omitBy omitBy
} from 'lodash' } from 'lodash'
import { set } from 'vue' import { set } from 'vue'
import { isStatusNotification, maybeShowNotification } from '../services/notification_utils/notification_utils.js' import {
isStatusNotification,
isValidNotification,
maybeShowNotification
} from '../services/notification_utils/notification_utils.js'
import apiService from '../services/api/api.service.js' import apiService from '../services/api/api.service.js'
const emptyTl = (userId = 0) => ({ const emptyTl = (userId = 0) => ({
@ -310,8 +314,24 @@ 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 }) => { const addNewNotifications = (state, { dispatch, notifications, older, visibleNotificationTypes, rootGetters, newNotificationSideEffects }) => {
each(notifications, (notification) => { 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)) { if (isStatusNotification(notification.type)) {
notification.action = addStatusToGlobalStorage(state, notification.action).item notification.action = addStatusToGlobalStorage(state, notification.action).item
notification.status = notification.status && addStatusToGlobalStorage(state, notification.status).item notification.status = notification.status && addStatusToGlobalStorage(state, notification.status).item
@ -323,12 +343,7 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot
// Only add a new notification if we don't have one for the same action // Only add a new notification if we don't have one for the same action
if (!state.notifications.idStore.hasOwnProperty(notification.id)) { if (!state.notifications.idStore.hasOwnProperty(notification.id)) {
state.notifications.maxId = notification.id > state.notifications.maxId updateNotificationsMinMaxId(state, notification)
? notification.id
: state.notifications.maxId
state.notifications.minId = notification.id < state.notifications.minId
? notification.id
: state.notifications.minId
state.notifications.data.push(notification) state.notifications.data.push(notification)
state.notifications.idStore[notification.id] = notification state.notifications.idStore[notification.id] = notification

View file

@ -48,6 +48,22 @@ const deleteMessage = (storage, messageId) => {
} }
} }
const cullOlderMessages = (storage) => {
const maxIndex = storage.messages.length
const minIndex = maxIndex - 50
if (maxIndex <= 50) return
storage.messages = _.sortBy(storage.messages, ['id'])
storage.minId = storage.messages[minIndex].id
for (const message of storage.messages) {
if (message.id < storage.minId) {
delete storage.idIndex[message.id]
delete storage.idempotencyKeyIndex[message.idempotency_key]
}
}
storage.messages = storage.messages.slice(minIndex, maxIndex)
}
const handleMessageError = (storage, fakeId, isRetry) => { const handleMessageError = (storage, fakeId, isRetry) => {
if (!storage) { return } if (!storage) { return }
const fakeMessage = storage.idIndex[fakeId] const fakeMessage = storage.idIndex[fakeId]
@ -201,6 +217,7 @@ const ChatService = {
empty, empty,
getView, getView,
deleteMessage, deleteMessage,
cullOlderMessages,
resetNewMessageCount, resetNewMessageCount,
clear, clear,
handleMessageError handleMessageError

View file

@ -203,7 +203,8 @@ export const parseUser = (data) => {
output.rights = output.rights || {} output.rights = output.rights || {}
output.notification_settings = output.notification_settings || {} output.notification_settings = output.notification_settings || {}
// Convert punycode to unicode // Convert punycode to unicode for UI
output.screen_name_ui = output.screen_name
if (output.screen_name.includes('@')) { if (output.screen_name.includes('@')) {
const parts = output.screen_name.split('@') const parts = output.screen_name.split('@')
let unicodeDomain = punycode.toUnicode(parts[1]) let unicodeDomain = punycode.toUnicode(parts[1])
@ -211,7 +212,7 @@ export const parseUser = (data) => {
// Add some identifier so users can potentially spot spoofing attempts: // Add some identifier so users can potentially spot spoofing attempts:
// lain.com and xn--lin-6cd.com would appear identical otherwise. // lain.com and xn--lin-6cd.com would appear identical otherwise.
unicodeDomain = '🌏' + unicodeDomain unicodeDomain = '🌏' + unicodeDomain
output.screen_name = [parts[0], unicodeDomain].join('@') output.screen_name_ui = [parts[0], unicodeDomain].join('@')
} }
} }

View file

@ -22,6 +22,13 @@ const statusNotifications = ['like', 'mention', 'repeat', 'pleroma:emoji_reactio
export const isStatusNotification = (type) => includes(statusNotifications, type) export const isStatusNotification = (type) => includes(statusNotifications, type)
export const isValidNotification = (notification) => {
if (isStatusNotification(notification.type) && !notification.status) {
return false
}
return true
}
const sortById = (a, b) => { const sortById = (a, b) => {
const seqA = Number(a.id) const seqA = Number(a.id)
const seqB = Number(b.id) const seqB = Number(b.id)

View file

@ -31,13 +31,15 @@ const testGetters = {
const localUser = { const localUser = {
id: 100, id: 100,
is_local: true, is_local: true,
screen_name: 'testUser' screen_name: 'testUser',
screen_name_ui: 'testUser'
} }
const extUser = { const extUser = {
id: 100, id: 100,
is_local: false, is_local: false,
screen_name: 'testUser@test.instance' screen_name: 'testUser@test.instance',
screen_name_ui: 'testUser@test.instance'
} }
const externalProfileStore = new Vuex.Store({ const externalProfileStore = new Vuex.Store({

View file

@ -88,4 +88,21 @@ describe('chatService', () => {
expect(view.map(i => i.type)).to.eql(['date', 'message', 'message', 'date', 'message']) expect(view.map(i => i.type)).to.eql(['date', 'message', 'message', 'date', 'message'])
}) })
}) })
describe('.cullOlderMessages', () => {
it('keeps 50 newest messages and idIndex matches', () => {
const chat = chatService.empty()
for (let i = 100; i > 0; i--) {
// Use decimal values with toFixed to hack together constant length predictable strings
chatService.add(chat, { messages: [{ ...message1, id: 'a' + (i / 1000).toFixed(3), idempotency_key: i }] })
}
chatService.cullOlderMessages(chat)
expect(chat.messages.length).to.eql(50)
expect(chat.messages[0].id).to.eql('a0.051')
expect(chat.minId).to.eql('a0.051')
expect(chat.messages[49].id).to.eql('a0.100')
expect(Object.keys(chat.idIndex).length).to.eql(50)
})
})
}) })

View file

@ -315,7 +315,7 @@ describe('API Entities normalizer', () => {
it('converts IDN to unicode and marks it as internatonal', () => { it('converts IDN to unicode and marks it as internatonal', () => {
const user = makeMockUserMasto({ acct: 'lain@xn--lin-6cd.com' }) const user = makeMockUserMasto({ acct: 'lain@xn--lin-6cd.com' })
expect(parseUser(user)).to.have.property('screen_name').that.equal('lain@🌏lаin.com') expect(parseUser(user)).to.have.property('screen_name_ui').that.equal('lain@🌏lаin.com')
}) })
}) })

View file

@ -7842,9 +7842,10 @@ shebang-regex@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
shelljs@^0.7.4: shelljs@^0.8.4:
version "0.7.8" version "0.8.4"
resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.7.8.tgz#decbcf874b0d1e5fb72e14b164a9683048e9acb3" resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.4.tgz#de7684feeb767f8716b326078a8a00875890e3c2"
integrity sha512-7gk3UZ9kOfPLIAbslLzyWeGiEqx9e3rxwZM0KE6EL8GlGwjym9Mrlx5/p33bWTu9YG6vcS4MBxYZDHYr5lr8BQ==
dependencies: dependencies:
glob "^7.0.0" glob "^7.0.0"
interpret "^1.0.0" interpret "^1.0.0"