Merge remote-tracking branch 'origin/develop' into websocket-fixes

* origin/develop: (119 commits)
  Apply 1 suggestion(s) to 1 file(s)
  Make it possible to localize user highlight options
  remove shoutbox test hacks
  fix shoutbox header, use custom scroll-to-bottom system, remove vue-chat-scroll, temporarily add chat test hack
  update changelog with 2.3.0
  change icons around
  Translated using Weblate (Japanese)
  Update timeline_quick_settings.js
  add screen_name_ui to tests
  separate screen_name and screen_name_ui with decoded punycode
  Update CHANGELOG.md
  add basic validation for statusless status notifications
  changelog mention
  fix chat unread badge
  update shelljs to get rid of warnings on build
  save a few characters
  focus input in emoji picker and react picker
  fix vue warnings
  add only to wording
  basic loggedin check for reply filtering
  ...
This commit is contained in:
Henry Jameson 2021-03-08 22:01:28 +02:00
commit 2e7bd99444
89 changed files with 2193 additions and 628 deletions

View file

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

View file

@ -73,7 +73,7 @@ const Chat = {
},
formPlaceholder () {
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 {
return ''
}
@ -234,6 +234,13 @@ const Chat = {
const scrollable = this.$refs.scrollable
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 () {
if (!this.currentChat) { return }
@ -241,6 +248,7 @@ const Chat = {
this.fetchChat({ maxId: this.currentChatMessageService.minId })
} else if (this.bottomedOut(JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET)) {
this.jumpToBottomButtonVisible = false
this.cullOlderCheck()
if (this.newMessageCount > 0) {
// 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

View file

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

View file

@ -5,6 +5,8 @@
</template>
<script>
import localeService from 'src/services/locale/locale.service.js'
export default {
name: 'Timeago',
props: ['date'],
@ -16,7 +18,7 @@ export default {
if (this.date.getTime() === today.getTime()) {
return this.$t('display_date.today')
} else {
return this.date.toLocaleDateString('en', { day: 'numeric', month: 'long' })
return this.date.toLocaleDateString(localeService.internalToBrowserLocale(this.$i18n.locale), { day: 'numeric', month: 'long' })
}
}
}

View file

@ -35,6 +35,18 @@ const chatPanel = {
userProfileLink (user) {
return generateProfileLink(user.id, user.username, this.$store.state.instance.restrictedNicknames)
}
},
watch: {
messages (newVal) {
const scrollEl = this.$el.querySelector('.chat-window')
if (!scrollEl) return
if (scrollEl.scrollTop + scrollEl.offsetHeight + 20 > scrollEl.scrollHeight) {
this.$nextTick(() => {
if (!scrollEl) return
scrollEl.scrollTop = scrollEl.scrollHeight - scrollEl.offsetHeight
})
}
}
}
}

View file

@ -10,17 +10,15 @@
@click.stop.prevent="togglePanel"
>
<div class="title">
<span>{{ $t('shoutbox.title') }}</span>
{{ $t('shoutbox.title') }}
<FAIcon
v-if="floating"
icon="times"
class="close-icon"
/>
</div>
</div>
<div
v-chat-scroll
class="chat-window"
>
<div class="chat-window">
<div
v-for="message in messages"
:key="message.id"
@ -94,6 +92,13 @@
.icon {
color: $fallback--text;
color: var(--text, $fallback--text);
margin-right: 0.5em;
}
.title {
display: flex;
justify-content: space-between;
align-items: center;
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -139,6 +139,11 @@
@import '../../_variables.scss';
.ExtraButtons {
/* override of popover internal stuff */
.popover-trigger-button {
width: auto;
}
.popover-trigger {
position: static;
padding: 10px;

View file

@ -12,11 +12,11 @@
v-model="language"
>
<option
v-for="(langCode, i) in languageCodes"
:key="langCode"
:value="langCode"
v-for="lang in languages"
:key="lang.code"
:value="lang.code"
>
{{ languageNames[i] }}
{{ lang.name }}
</option>
</select>
<FAIcon
@ -29,6 +29,7 @@
<script>
import languagesObject from '../../i18n/messages'
import localeService from '../../services/locale/locale.service.js'
import ISO6391 from 'iso-639-1'
import _ from 'lodash'
import { library } from '@fortawesome/fontawesome-svg-core'
@ -42,12 +43,8 @@ library.add(
export default {
computed: {
languageCodes () {
return languagesObject.languages
},
languageNames () {
return _.map(this.languageCodes, this.getLanguageName)
languages () {
return _.map(languagesObject.languages, (code) => ({ code: code, name: this.getLanguageName(code) })).sort((a, b) => a.name.localeCompare(b.name))
},
language: {
@ -61,12 +58,13 @@ export default {
methods: {
getLanguageName (code) {
const specialLanguageNames = {
'ja': 'Japanese (日本語)',
'ja_easy': 'Japanese (やさしいにほんご)',
'zh': 'Simplified Chinese (简体中文)',
'zh_Hant': 'Traditional Chinese (繁體中文)'
'ja_easy': 'やさしいにほんご',
'zh': '简体中文',
'zh_Hant': '繁體中文'
}
return specialLanguageNames[code] || ISO6391.getName(code)
const languageName = specialLanguageNames[code] || ISO6391.getNativeName(code)
const browserLocale = localeService.internalToBrowserLocale(code)
return languageName.charAt(0).toLocaleUpperCase(browserLocale) + languageName.slice(1)
}
}
}

View file

@ -73,11 +73,21 @@
}
}
@keyframes media-fadein {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.modal-image {
max-width: 90%;
max-height: 90%;
box-shadow: 0px 5px 15px 0 rgba(0, 0, 0, 0.5);
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 {

View file

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

View file

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

View file

@ -50,74 +50,74 @@
class="button-default dropdown-item"
@click="toggleTag(tags.FORCE_NSFW)"
>
{{ $t('user_card.admin_menu.force_nsfw') }}
<span
class="menu-checkbox"
:class="{ 'menu-checkbox-checked': hasTag(tags.FORCE_NSFW) }"
/>
{{ $t('user_card.admin_menu.force_nsfw') }}
</button>
<button
class="button-default dropdown-item"
@click="toggleTag(tags.STRIP_MEDIA)"
>
{{ $t('user_card.admin_menu.strip_media') }}
<span
class="menu-checkbox"
:class="{ 'menu-checkbox-checked': hasTag(tags.STRIP_MEDIA) }"
/>
{{ $t('user_card.admin_menu.strip_media') }}
</button>
<button
class="button-default dropdown-item"
@click="toggleTag(tags.FORCE_UNLISTED)"
>
{{ $t('user_card.admin_menu.force_unlisted') }}
<span
class="menu-checkbox"
:class="{ 'menu-checkbox-checked': hasTag(tags.FORCE_UNLISTED) }"
/>
{{ $t('user_card.admin_menu.force_unlisted') }}
</button>
<button
class="button-default dropdown-item"
@click="toggleTag(tags.SANDBOX)"
>
{{ $t('user_card.admin_menu.sandbox') }}
<span
class="menu-checkbox"
:class="{ 'menu-checkbox-checked': hasTag(tags.SANDBOX) }"
/>
{{ $t('user_card.admin_menu.sandbox') }}
</button>
<button
v-if="user.is_local"
class="button-default dropdown-item"
@click="toggleTag(tags.DISABLE_REMOTE_SUBSCRIPTION)"
>
{{ $t('user_card.admin_menu.disable_remote_subscription') }}
<span
class="menu-checkbox"
:class="{ 'menu-checkbox-checked': hasTag(tags.DISABLE_REMOTE_SUBSCRIPTION) }"
/>
{{ $t('user_card.admin_menu.disable_remote_subscription') }}
</button>
<button
v-if="user.is_local"
class="button-default dropdown-item"
@click="toggleTag(tags.DISABLE_ANY_SUBSCRIPTION)"
>
{{ $t('user_card.admin_menu.disable_any_subscription') }}
<span
class="menu-checkbox"
:class="{ 'menu-checkbox-checked': hasTag(tags.DISABLE_ANY_SUBSCRIPTION) }"
/>
{{ $t('user_card.admin_menu.disable_any_subscription') }}
</button>
<button
v-if="user.is_local"
class="button-default dropdown-item"
@click="toggleTag(tags.QUARANTINE)"
>
{{ $t('user_card.admin_menu.quarantine') }}
<span
class="menu-checkbox"
:class="{ 'menu-checkbox-checked': hasTag(tags.QUARANTINE) }"
/>
{{ $t('user_card.admin_menu.quarantine') }}
</button>
</span>
</div>
@ -163,25 +163,6 @@
<style lang="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 {
height: 100%;
.trigger {

View file

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

View file

@ -58,7 +58,12 @@
{{ $t('polls.vote') }}
</button>
<div class="total">
{{ totalVotesCount }} {{ $t("polls.votes") }}&nbsp;·&nbsp;
<template v-if="typeof poll.voters_count === 'number'">
{{ $tc("polls.people_voted_count", poll.voters_count, { count: poll.voters_count }) }}&nbsp;·&nbsp;
</template>
<template v-else>
{{ $tc("polls.votes_count", poll.votes_count, { count: poll.votes_count }) }}&nbsp;·&nbsp;
</template>
</div>
<i18n :path="expired ? 'polls.expired' : 'polls.expires_in'">
<Timeago

View file

@ -21,20 +21,17 @@
@keydown.enter.stop.prevent="nextOption(index)"
>
</div>
<div
<button
v-if="options.length > 2"
class="icon-container"
class="delete-option button-unstyled -hover-highlight"
@click="deleteOption(index)"
>
<FAIcon
icon="times"
class="delete"
@click="deleteOption(index)"
/>
</div>
<FAIcon icon="times" />
</button>
</div>
<a
<button
v-if="options.length < maxOptions"
class="add-option faint"
class="add-option faint button-unstyled -hover-highlight"
@click="addOption"
>
<FAIcon
@ -43,7 +40,7 @@
/>
{{ $t("polls.add_option") }}
</a>
</button>
<div class="poll-type-expiry">
<div
class="poll-type"
@ -116,7 +113,6 @@
align-self: flex-start;
padding-top: 0.25em;
padding-left: 0.1em;
cursor: pointer;
}
.poll-option {
@ -135,19 +131,11 @@
}
}
.icon-container {
.delete-option {
// Hack: Move the icon over the input box
width: 1.5em;
margin-left: -1.5em;
z-index: 1;
.delete {
cursor: pointer;
&:hover {
color: inherit;
}
}
}
.poll-type-expiry {
@ -163,6 +151,7 @@
border: none;
box-shadow: none;
background-color: transparent;
padding-right: 0.75em;
}
}

View file

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

View file

@ -6,6 +6,7 @@
<button
ref="trigger"
class="button-unstyled -fullwidth popover-trigger-button"
type="button"
@click="onClick"
>
<slot name="trigger" />
@ -81,10 +82,9 @@
.dropdown-item {
line-height: 21px;
margin-right: 5px;
overflow: auto;
display: block;
padding: .25rem 1.0rem .25rem 1.5rem;
padding: .5em 0.75em;
clear: both;
font-weight: 400;
text-align: inherit;
@ -100,10 +100,9 @@
--btnText: var(--popoverText, $fallback--text);
&-icon {
padding-left: 0.5rem;
svg {
margin-right: 0.25rem;
width: 22px;
margin-right: 0.75rem;
color: var(--menuPopoverIcon, $fallback--icon)
}
}
@ -122,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>

View file

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

View file

@ -302,11 +302,12 @@
:key="file.url"
class="media-upload-wrapper"
>
<FAIcon
class="fa-scale-110 fa-old-padding"
icon="times"
<button
class="button-unstyled hider"
@click="removeMediaFile(file)"
/>
>
<FAIcon icon="times" />
</button>
<attachment
:attachment="file"
:set-media="() => $store.dispatch('setMedia', newStatus.files)"
@ -516,26 +517,11 @@
}
.attachments .media-upload-wrapper {
padding: 0 0.5em;
position: relative;
.attachment {
margin: 0;
padding: 0;
position: relative;
}
.fa-scale-110 fa-old-padding {
position: absolute;
margin: 10px;
margin: .75em;
padding: .5em;
background: rgba(230,230,230,0.6);
z-index: 2;
color: black;
border-radius: $fallback--attachmentRadius;
border-radius: var(--attachmentRadius, $fallback--attachmentRadius);
font-weight: bold;
cursor: pointer;
}
}

View file

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

View file

@ -1,10 +1,12 @@
<template>
<Popover
trigger="click"
class="ReactButton"
placement="top"
:offset="{ y: 5 }"
:bound-to="{ x: 'container' }"
remove-padding
@show="focusInput"
>
<div
slot="content"
@ -42,7 +44,7 @@
</div>
<span
slot="trigger"
class="ReactButton"
class="popover-trigger"
:title="$t('tool_tip.add_reaction')"
>
<FAIcon
@ -58,63 +60,72 @@
<style lang="scss">
@import '../../_variables.scss';
.reaction-picker-filter {
padding: 0.5em;
display: flex;
input {
flex: 1;
.ReactButton {
.reaction-picker-filter {
padding: 0.5em;
display: flex;
input {
flex: 1;
}
}
}
.reaction-picker-divider {
height: 1px;
width: 100%;
margin: 0.5em;
background-color: var(--border, $fallback--border);
}
.reaction-picker-divider {
height: 1px;
width: 100%;
margin: 0.5em;
background-color: var(--border, $fallback--border);
}
.reaction-picker {
width: 10em;
height: 9em;
font-size: 1.5em;
overflow-y: scroll;
display: flex;
flex-wrap: wrap;
padding: 0.5em;
text-align: center;
align-content: flex-start;
user-select: none;
.reaction-picker {
width: 10em;
height: 9em;
font-size: 1.5em;
overflow-y: scroll;
display: flex;
flex-wrap: wrap;
padding: 0.5em;
text-align: center;
align-content: flex-start;
user-select: none;
mask: linear-gradient(to top, white 0, transparent 100%) bottom no-repeat,
linear-gradient(to bottom, white 0, transparent 100%) top no-repeat,
linear-gradient(to top, white, white);
transition: mask-size 150ms;
mask-size: 100% 20px, 100% 20px, auto;
// Autoprefixed seem to ignore this one, and also syntax is different
-webkit-mask-composite: xor;
mask-composite: exclude;
mask: linear-gradient(to top, white 0, transparent 100%) bottom no-repeat,
linear-gradient(to bottom, white 0, transparent 100%) top no-repeat,
linear-gradient(to top, white, white);
transition: mask-size 150ms;
mask-size: 100% 20px, 100% 20px, auto;
.emoji-button {
cursor: pointer;
/* Autoprefixed seem to ignore this one, and also syntax is different */
-webkit-mask-composite: xor;
mask-composite: exclude;
flex-basis: 20%;
line-height: 1.5em;
align-content: center;
.emoji-button {
cursor: pointer;
&:hover {
transform: scale(1.25);
flex-basis: 20%;
line-height: 1.5em;
align-content: center;
&:hover {
transform: scale(1.25);
}
}
}
/* override of popover internal stuff */
.popover-trigger-button {
width: auto;
}
.popover-trigger {
padding: 10px;
margin: -10px;
&:hover .svg-inline--fa {
color: $fallback--text;
color: var(--text, $fallback--text);
}
}
}
.ReactButton {
padding: 10px;
margin: -10px;
&:hover .svg-inline--fa {
color: $fallback--text;
color: var(--text, $fallback--text);
}
}
</style>

View file

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

View file

@ -162,6 +162,23 @@
</ul>
</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
v-if="captcha.type != 'none'"
id="captcha-group"

View file

@ -8,6 +8,7 @@
class="button-unstyled scope"
:class="css.direct"
:title="$t('post_status.scope.direct')"
type="button"
@click="changeVis('direct')"
>
<FAIcon
@ -20,6 +21,7 @@
class="button-unstyled scope"
:class="css.private"
:title="$t('post_status.scope.private')"
type="button"
@click="changeVis('private')"
>
<FAIcon
@ -32,6 +34,7 @@
class="button-unstyled scope"
:class="css.unlisted"
:title="$t('post_status.scope.unlisted')"
type="button"
@click="changeVis('unlisted')"
>
<FAIcon
@ -44,6 +47,7 @@
class="button-unstyled scope"
:class="css.public"
:title="$t('post_status.scope.public')"
type="button"
@click="changeVis('public')"
>
<FAIcon

View file

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

View file

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

View file

@ -0,0 +1,57 @@
<template>
<label
class="BooleanSetting"
>
<Checkbox
:checked="state"
:disabled="disabled"
@change="update"
>
<span
v-if="!!$slots.default"
class="label"
>
<slot />
</span>
<ModifiedIndicator :changed="isChanged" />
</Checkbox>
</label>
</template>
<script>
import { get, set } from 'lodash'
import Checkbox from 'src/components/checkbox/checkbox.vue'
import ModifiedIndicator from './modified_indicator.vue'
export default {
components: {
Checkbox,
ModifiedIndicator
},
props: [
'path',
'disabled'
],
computed: {
pathDefault () {
const [firstSegment, ...rest] = this.path.split('.')
return [firstSegment + 'DefaultValue', ...rest].join('.')
},
state () {
return get(this.$parent, this.path)
},
isChanged () {
return get(this.$parent, this.path) !== get(this.$parent, this.pathDefault)
}
},
methods: {
update (e) {
set(this.$parent, this.path, e)
}
}
}
</script>
<style lang="scss">
.BooleanSetting {
}
</style>

View file

@ -0,0 +1,51 @@
<template>
<span
v-if="changed"
class="ModifiedIndicator"
>
<Popover
trigger="hover"
>
<span slot="trigger">
&nbsp;
<FAIcon
icon="wrench"
/>
</span>
<div
slot="content"
class="modified-tooltip"
>
{{ $t('settings.setting_changed') }}
</div>
</Popover>
</span>
</template>
<script>
import Popover from 'src/components/popover/popover.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import { faWrench } from '@fortawesome/free-solid-svg-icons'
library.add(
faWrench
)
export default {
components: { Popover },
props: ['changed']
}
</script>
<style lang="scss">
.ModifiedIndicator {
display: inline-block;
position: relative;
.modified-tooltip {
margin: 0.5em 1em;
min-width: 10em;
text-align: center;
}
}
</style>

View file

@ -1,29 +1,15 @@
import {
instanceDefaultProperties,
multiChoiceProperties,
defaultState as configDefaultState
} from 'src/modules/config.js'
import { defaultState as configDefaultState } from 'src/modules/config.js'
const SharedComputedObject = () => ({
user () {
return this.$store.state.users.currentUser
},
// Getting localized values for instance-default properties
...instanceDefaultProperties
.filter(key => multiChoiceProperties.includes(key))
// Getting values for default properties
...Object.keys(configDefaultState)
.map(key => [
key + 'DefaultValue',
function () {
return this.$store.getters.instanceDefaultConfig[key]
}
])
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
...instanceDefaultProperties
.filter(key => !multiChoiceProperties.includes(key))
.map(key => [
key + 'LocalizedValue',
function () {
return this.$t('settings.values.' + this.$store.getters.instanceDefaultConfig[key])
return this.$store.getters.defaultConfig[key]
}
])
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),

View file

@ -1,5 +1,5 @@
import { filter, trim } from 'lodash'
import Checkbox from 'src/components/checkbox/checkbox.vue'
import BooleanSetting from '../helpers/boolean_setting.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js'
import { library } from '@fortawesome/fontawesome-svg-core'
@ -18,7 +18,7 @@ const FilteringTab = {
}
},
components: {
Checkbox
BooleanSetting
},
computed: {
...SharedComputedObject(),

View file

@ -5,34 +5,34 @@
<span class="label">{{ $t('settings.notification_visibility') }}</span>
<ul class="option-list">
<li>
<Checkbox v-model="notificationVisibility.likes">
<BooleanSetting path="notificationVisibility.likes">
{{ $t('settings.notification_visibility_likes') }}
</Checkbox>
</BooleanSetting>
</li>
<li>
<Checkbox v-model="notificationVisibility.repeats">
<BooleanSetting path="notificationVisibility.repeats">
{{ $t('settings.notification_visibility_repeats') }}
</Checkbox>
</BooleanSetting>
</li>
<li>
<Checkbox v-model="notificationVisibility.follows">
<BooleanSetting path="notificationVisibility.follows">
{{ $t('settings.notification_visibility_follows') }}
</Checkbox>
</BooleanSetting>
</li>
<li>
<Checkbox v-model="notificationVisibility.mentions">
<BooleanSetting path="notificationVisibility.mentions">
{{ $t('settings.notification_visibility_mentions') }}
</Checkbox>
</BooleanSetting>
</li>
<li>
<Checkbox v-model="notificationVisibility.moves">
<BooleanSetting path="notificationVisibility.moves">
{{ $t('settings.notification_visibility_moves') }}
</Checkbox>
</BooleanSetting>
</li>
<li>
<Checkbox v-model="notificationVisibility.emojiReactions">
<BooleanSetting path="notificationVisibility.emojiReactions">
{{ $t('settings.notification_visibility_emoji_reactions') }}
</Checkbox>
</BooleanSetting>
</li>
</ul>
</div>
@ -60,14 +60,14 @@
</label>
</div>
<div>
<Checkbox v-model="hidePostStats">
{{ $t('settings.hide_post_stats') }} {{ $t('settings.instance_default', { value: hidePostStatsLocalizedValue }) }}
</Checkbox>
<BooleanSetting path="hidePostStats">
{{ $t('settings.hide_post_stats') }}
</BooleanSetting>
</div>
<div>
<Checkbox v-model="hideUserStats">
{{ $t('settings.hide_user_stats') }} {{ $t('settings.instance_default', { value: hideUserStatsLocalizedValue }) }}
</Checkbox>
<BooleanSetting path="hideUserStats">
{{ $t('settings.hide_user_stats') }}
</BooleanSetting>
</div>
</div>
<div class="setting-item">
@ -75,14 +75,14 @@
<p>{{ $t('settings.filtering_explanation') }}</p>
<textarea
id="muteWords"
class="resize-height"
v-model="muteWordsString"
class="resize-height"
/>
</div>
<div>
<Checkbox v-model="hideFilteredStatuses">
{{ $t('settings.hide_filtered_statuses') }} {{ $t('settings.instance_default', { value: hideFilteredStatusesLocalizedValue }) }}
</Checkbox>
<BooleanSetting path="hideFilteredStatuses">
{{ $t('settings.hide_filtered_statuses') }}
</BooleanSetting>
</div>
</div>
</div>

View file

@ -1,4 +1,4 @@
import Checkbox from 'src/components/checkbox/checkbox.vue'
import BooleanSetting from '../helpers/boolean_setting.vue'
import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js'
@ -26,7 +26,7 @@ const GeneralTab = {
}
},
components: {
Checkbox,
BooleanSetting,
InterfaceLanguageSwitcher
},
computed: {

View file

@ -7,14 +7,14 @@
<interface-language-switcher />
</li>
<li v-if="instanceSpecificPanelPresent">
<Checkbox v-model="hideISP">
<BooleanSetting path="hideISP">
{{ $t('settings.hide_isp') }}
</Checkbox>
</BooleanSetting>
</li>
<li v-if="instanceWallpaperUsed">
<Checkbox v-model="hideInstanceWallpaper">
<BooleanSetting path="hideInstanceWallpaper">
{{ $t('settings.hide_wallpaper') }}
</Checkbox>
</BooleanSetting>
</li>
</ul>
</div>
@ -22,51 +22,51 @@
<h2>{{ $t('nav.timeline') }}</h2>
<ul class="setting-list">
<li>
<Checkbox v-model="hideMutedPosts">
{{ $t('settings.hide_muted_posts') }} {{ $t('settings.instance_default', { value: hideMutedPostsLocalizedValue }) }}
</Checkbox>
<BooleanSetting path="hideMutedPosts">
{{ $t('settings.hide_muted_posts') }}
</BooleanSetting>
</li>
<li>
<Checkbox v-model="collapseMessageWithSubject">
{{ $t('settings.collapse_subject') }} {{ $t('settings.instance_default', { value: collapseMessageWithSubjectLocalizedValue }) }}
</Checkbox>
<BooleanSetting path="collapseMessageWithSubject">
{{ $t('settings.collapse_subject') }}
</BooleanSetting>
</li>
<li>
<Checkbox v-model="streaming">
<BooleanSetting path="streaming">
{{ $t('settings.streaming') }}
</Checkbox>
</BooleanSetting>
<ul
class="setting-list suboptions"
:class="[{disabled: !streaming}]"
>
<li>
<Checkbox
v-model="pauseOnUnfocused"
<BooleanSetting
path="pauseOnUnfocused"
:disabled="!streaming"
>
{{ $t('settings.pause_on_unfocused') }}
</Checkbox>
</BooleanSetting>
</li>
</ul>
</li>
<li>
<Checkbox v-model="useStreamingApi">
<BooleanSetting path="useStreamingApi">
{{ $t('settings.useStreamingApi') }}
<br>
<small>
{{ $t('settings.useStreamingApiWarning') }}
</small>
</Checkbox>
</BooleanSetting>
</li>
<li>
<Checkbox v-model="emojiReactionsOnTimeline">
<BooleanSetting path="emojiReactionsOnTimeline">
{{ $t('settings.emoji_reactions_on_timeline') }}
</Checkbox>
</BooleanSetting>
</li>
<li>
<Checkbox v-model="virtualScrolling">
<BooleanSetting path="virtualScrolling">
{{ $t('settings.virtual_scrolling') }}
</Checkbox>
</BooleanSetting>
</li>
</ul>
</div>
@ -75,14 +75,14 @@
<h2>{{ $t('settings.composing') }}</h2>
<ul class="setting-list">
<li>
<Checkbox v-model="scopeCopy">
{{ $t('settings.scope_copy') }} {{ $t('settings.instance_default', { value: scopeCopyLocalizedValue }) }}
</Checkbox>
<BooleanSetting path="scopeCopy">
{{ $t('settings.scope_copy') }}
</BooleanSetting>
</li>
<li>
<Checkbox v-model="alwaysShowSubjectInput">
{{ $t('settings.subject_input_always_show') }} {{ $t('settings.instance_default', { value: alwaysShowSubjectInputLocalizedValue }) }}
</Checkbox>
<BooleanSetting path="alwaysShowSubjectInput">
{{ $t('settings.subject_input_always_show') }}
</BooleanSetting>
</li>
<li>
<div>
@ -143,19 +143,24 @@
</div>
</li>
<li>
<Checkbox v-model="minimalScopesMode">
{{ $t('settings.minimal_scopes_mode') }} {{ $t('settings.instance_default', { value: minimalScopesModeLocalizedValue }) }}
</Checkbox>
<BooleanSetting path="minimalScopesMode">
{{ $t('settings.minimal_scopes_mode') }}
</BooleanSetting>
</li>
<li>
<Checkbox v-model="autohideFloatingPostButton">
<BooleanSetting path="sensitiveByDefault">
{{ $t('settings.sensitive_by_default') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="autohideFloatingPostButton">
{{ $t('settings.autohide_floating_post_button') }}
</Checkbox>
</BooleanSetting>
</li>
<li>
<Checkbox v-model="padEmoji">
<BooleanSetting path="padEmoji">
{{ $t('settings.pad_emoji') }}
</Checkbox>
</BooleanSetting>
</li>
</ul>
</div>
@ -164,14 +169,14 @@
<h2>{{ $t('settings.attachments') }}</h2>
<ul class="setting-list">
<li>
<Checkbox v-model="hideAttachments">
<BooleanSetting path="hideAttachments">
{{ $t('settings.hide_attachments_in_tl') }}
</Checkbox>
</BooleanSetting>
</li>
<li>
<Checkbox v-model="hideAttachmentsInConv">
<BooleanSetting path="hideAttachmentsInConv">
{{ $t('settings.hide_attachments_in_convo') }}
</Checkbox>
</BooleanSetting>
</li>
<li>
<label for="maxThumbnails">
@ -179,7 +184,7 @@
</label>
<input
id="maxThumbnails"
v-model.number="maxThumbnails"
path.number="maxThumbnails"
class="number-input"
type="number"
min="0"
@ -187,48 +192,48 @@
>
</li>
<li>
<Checkbox v-model="hideNsfw">
<BooleanSetting path="hideNsfw">
{{ $t('settings.nsfw_clickthrough') }}
</Checkbox>
</BooleanSetting>
</li>
<ul class="setting-list suboptions">
<li>
<Checkbox
v-model="preloadImage"
<BooleanSetting
path="preloadImage"
:disabled="!hideNsfw"
>
{{ $t('settings.preload_images') }}
</Checkbox>
</BooleanSetting>
</li>
<li>
<Checkbox
v-model="useOneClickNsfw"
<BooleanSetting
path="useOneClickNsfw"
:disabled="!hideNsfw"
>
{{ $t('settings.use_one_click_nsfw') }}
</Checkbox>
</BooleanSetting>
</li>
</ul>
<li>
<Checkbox v-model="stopGifs">
<BooleanSetting path="stopGifs">
{{ $t('settings.stop_gifs') }}
</Checkbox>
</BooleanSetting>
</li>
<li>
<Checkbox v-model="loopVideo">
<BooleanSetting path="loopVideo">
{{ $t('settings.loop_video') }}
</Checkbox>
</BooleanSetting>
<ul
class="setting-list suboptions"
:class="[{disabled: !streaming}]"
>
<li>
<Checkbox
v-model="loopVideoSilentOnly"
<BooleanSetting
path="loopVideoSilentOnly"
:disabled="!loopVideo || !loopSilentAvailable"
>
{{ $t('settings.loop_video_silent_only') }}
</Checkbox>
</BooleanSetting>
<div
v-if="!loopSilentAvailable"
class="unavailable"
@ -239,14 +244,14 @@
</ul>
</li>
<li>
<Checkbox v-model="playVideosInModal">
<BooleanSetting path="playVideosInModal">
{{ $t('settings.play_videos_in_modal') }}
</Checkbox>
</BooleanSetting>
</li>
<li>
<Checkbox v-model="useContainFit">
<BooleanSetting path="useContainFit">
{{ $t('settings.use_contain_fit') }}
</Checkbox>
</BooleanSetting>
</li>
</ul>
</div>
@ -255,9 +260,9 @@
<h2>{{ $t('settings.notifications') }}</h2>
<ul class="setting-list">
<li>
<Checkbox v-model="webPushNotifications">
<BooleanSetting path="webPushNotifications">
{{ $t('settings.enable_web_push_notifications') }}
</Checkbox>
</BooleanSetting>
</li>
</ul>
</div>
@ -266,9 +271,9 @@
<h2>{{ $t('settings.fun') }}</h2>
<ul class="setting-list">
<li>
<Checkbox v-model="greentext">
{{ $t('settings.greentext') }} {{ $t('settings.instance_default', { value: greentextLocalizedValue }) }}
</Checkbox>
<BooleanSetting path="greentext">
{{ $t('settings.greentext') }}
</BooleanSetting>
</li>
</ul>
</div>

View file

@ -111,16 +111,17 @@
.profile-fields {
display: flex;
&>.emoji-input {
& > .emoji-input {
flex: 1 1 auto;
margin: 0 .2em .5em;
margin: 0 0.2em 0.5em;
min-width: 0;
}
&>.icon-container {
.delete-field {
width: 20px;
align-self: center;
margin: 0 .2em .5em;
margin: 0 0.2em 0.5em;
padding: 0 0.5em;
}
}
}

View file

@ -124,24 +124,24 @@
:placeholder="$t('settings.profile_fields.value')"
>
</EmojiInput>
<div
class="icon-container"
<button
class="delete-field button-unstyled -hover-highlight"
@click="deleteField(i)"
>
<FAIcon
v-show="newFields.length > 1"
icon="times"
@click="deleteField(i)"
/>
</div>
</button>
</div>
<a
<button
v-if="newFields.length < maxFields"
class="add-field faint"
class="add-field faint button-unstyled -hover-highlight"
@click="addField"
>
<FAIcon icon="plus" />
{{ $t("settings.profile_fields.add_field") }}
</a>
</button>
</div>
<p>
<Checkbox v-model="bot">

View file

@ -1,6 +1,7 @@
import ProgressButton from 'src/components/progress_button/progress_button.vue'
import Checkbox from 'src/components/checkbox/checkbox.vue'
import Mfa from './mfa.vue'
import localeService from 'src/services/locale/locale.service.js'
const SecurityTab = {
data () {
@ -37,7 +38,7 @@ const SecurityTab = {
return {
id: oauthToken.id,
appName: oauthToken.app_name,
validUntil: new Date(oauthToken.valid_until).toLocaleDateString()
validUntil: new Date(oauthToken.valid_until).toLocaleDateString(localeService.internalToBrowserLocale(this.$i18n.locale))
}
})
}

View file

@ -109,7 +109,7 @@
v-if="chat"
@click="toggleDrawer"
>
<router-link :to="{ name: 'chat' }">
<router-link :to="{ name: 'chat-panel' }">
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding"

View file

@ -1,4 +1,6 @@
import map from 'lodash/map'
import groupBy from 'lodash/groupBy'
import { mapGetters, mapState } from 'vuex'
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
const StaffPanel = {
@ -10,9 +12,21 @@ const StaffPanel = {
BasicUserCard
},
computed: {
staffAccounts () {
return map(this.$store.state.instance.staffAccounts, nickname => this.$store.getters.findUser(nickname)).filter(_ => _)
}
groupedStaffAccounts () {
const staffAccounts = map(this.staffAccounts, this.findUser).filter(_ => _)
const groupedStaffAccounts = groupBy(staffAccounts, 'role')
return [
{ role: 'admin', users: groupedStaffAccounts['admin'] },
{ role: 'moderator', users: groupedStaffAccounts['moderator'] }
].filter(group => group.users)
},
...mapGetters([
'findUser'
]),
...mapState({
staffAccounts: state => state.instance.staffAccounts
})
}
}

View file

@ -7,11 +7,18 @@
</div>
</div>
<div class="panel-body">
<basic-user-card
v-for="user in staffAccounts"
:key="user.screen_name"
:user="user"
/>
<div
v-for="group in groupedStaffAccounts"
:key="group.role"
class="staff-group"
>
<h4>{{ $t('general.role.' + group.role) }}</h4>
<basic-user-card
v-for="user in group.users"
:key="user.screen_name"
:user="user"
/>
</div>
</div>
</div>
</div>
@ -20,4 +27,14 @@
<script src="./staff_panel.js" ></script>
<style lang="scss">
.staff-group {
padding-left: 1em;
padding-top: 1em;
.basic-user-card {
padding-left: 0;
}
}
</style>

View file

@ -136,7 +136,7 @@ const 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 },
retweeterProfileLink () { return this.generateUserProfileLink(this.statusoid.user.id, this.statusoid.user.screen_name) },
status () {
@ -157,6 +157,7 @@ const Status = {
return muteWordHits(this.status, this.muteWords)
},
muted () {
if (this.statusoid.user.id === this.currentUser.id) return false
const { status } = this
const { reblog } = status
const relationship = this.$store.getters.relationship(status.user.id)
@ -215,7 +216,7 @@ const Status = {
return this.status.in_reply_to_screen_name
} else {
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 () {

View file

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

View file

@ -93,7 +93,9 @@ export default Vue.component('tab-switcher', {
<button
disabled={slot.data.attrs.disabled}
onClick={this.clickTab(index)}
class={classesTab.join(' ')}>
class={classesTab.join(' ')}
type="button"
>
<img src={slot.data.attrs.image} title={slot.data.attrs['image-tooltip']}/>
{slot.data.attrs.label ? '' : slot.data.attrs.label}
</button>

View file

@ -9,6 +9,7 @@
<script>
import * as DateUtils from 'src/services/date_utils/date_utils.js'
import localeService from 'src/services/locale/locale.service.js'
export default {
name: 'Timeago',
@ -21,9 +22,10 @@ export default {
},
computed: {
localeDateString () {
const browserLocale = localeService.internalToBrowserLocale(this.$i18n.locale)
return typeof this.time === 'string'
? new Date(Date.parse(this.time)).toLocaleString()
: this.time.toLocaleString()
? new Date(Date.parse(this.time)).toLocaleString(browserLocale)
: this.time.toLocaleString(browserLocale)
}
},
created () {

View file

@ -2,12 +2,14 @@ import Status from '../status/status.vue'
import timelineFetcher from '../../services/timeline_fetcher/timeline_fetcher.service.js'
import Conversation from '../conversation/conversation.vue'
import TimelineMenu from '../timeline_menu/timeline_menu.vue'
import TimelineQuickSettings from './timeline_quick_settings.vue'
import { debounce, throttle, keyBy } from 'lodash'
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(
faCircleNotch
faCircleNotch,
faCog
)
export const getExcludedStatusIdsByPinning = (statuses, pinnedStatusIds) => {
@ -47,7 +49,8 @@ const Timeline = {
components: {
Status,
Conversation,
TimelineMenu
TimelineMenu,
TimelineQuickSettings
},
computed: {
newStatusCount () {

View file

@ -16,6 +16,7 @@
>
{{ $t('timeline.up_to_date') }}
</div>
<TimelineQuickSettings v-if="!embedded" />
</div>
<div :class="classes.body">
<div
@ -103,9 +104,12 @@
max-width: 100%;
flex-wrap: nowrap;
align-items: center;
position: relative;
.loadmore-button {
flex-shrink: 0;
}
.loadmore-text {
flex-shrink: 0;
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
v-if="user"
class="Avatar"
:alt="user.screen_name"
:title="user.screen_name"
:alt="user.screen_name_ui"
:title="user.screen_name_ui"
:src="imgSrc(user.profile_image_url_original)"
:class="{ 'avatar-compact': compact, 'better-shadow': betterShadow }"
:image-load-error="imageLoadError"

View file

@ -73,23 +73,23 @@
<div class="bottom-line">
<router-link
class="user-screen-name"
:title="user.screen_name"
:title="user.screen_name_ui"
:to="userProfileLink(user)"
>
@{{ user.screen_name }}
@{{ user.screen_name_ui }}
</router-link>
<template v-if="!hideBio">
<span
v-if="!!visibleRole"
class="alert user-role"
>
{{ visibleRole }}
{{ $t(`general.role.${visibleRole}`) }}
</span>
<span
v-if="user.bot"
class="alert user-role"
>
bot
{{ $t('user_card.bot') }}
</span>
</template>
<span v-if="user.locked">
@ -141,10 +141,10 @@
v-model="userHighlightType"
class="userHighlightSel"
>
<option value="disabled">No highlight</option>
<option value="solid">Solid bg</option>
<option value="striped">Striped bg</option>
<option value="side">Side stripe</option>
<option value="disabled">{{ $t('user_card.highlight.disabled') }}</option>
<option value="solid">{{ $t('user_card.highlight.solid') }}</option>
<option value="striped">{{ $t('user_card.highlight.striped') }}</option>
<option value="side">{{ $t('user_card.highlight.side') }}</option>
</select>
<FAIcon
class="select-down-icon"
@ -507,7 +507,6 @@
.user-role {
flex: none;
text-transform: capitalize;
color: $fallback--text;
color: var(--alertNeutralText, $fallback--text);
background-color: $fallback--fg;

View file

@ -26,7 +26,7 @@
<!-- eslint-disable vue/no-v-html -->
<span v-html="user.name_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>

View file

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