Merge remote-tracking branch 'upstream/develop' into shigusegubu
* upstream/develop: review review Update emoji-input.js Misc fixes: Fix uploads stretching on chrome, fix warnings in console apply font smoothing in webkit and firefox fix user search Apply suggestion to src/services/backend_interactor_service/backend_interactor_service.js properly position the caret after replacement Apply suggestion to src/services/api/api.service.js fix MFA crashing on user-settings page fixup! Removed formattingOptionsEnabled in favor of relying on BE-provided list of accepted formatting options getting and setting user background via MastoAPI Removed formattingOptionsEnabled in favor of relying on BE-provided list of accepted formatting options fix small annoyance fixed some bugs i found, also cleaned up some stuff + documentation self-review Linting
This commit is contained in:
commit
cda2d6cb20
23 changed files with 214 additions and 162 deletions
|
@ -1,5 +1,8 @@
|
||||||
# v1.0
|
# v1.0
|
||||||
## Removed features/radically changed behavior
|
## Removed features/radically changed behavior
|
||||||
|
### formattingOptionsEnabled
|
||||||
|
as of !833 `formattingOptionsEnabled` is no longer available and instead FE check for available post formatting options and enables formatting control if there's more than one option.
|
||||||
|
|
||||||
### minimalScopesMode
|
### minimalScopesMode
|
||||||
As of !633, `scopeOptions` is no longer available and instead is changed for `minimalScopesMode` (default: `false`)
|
As of !633, `scopeOptions` is no longer available and instead is changed for `minimalScopesMode` (default: `false`)
|
||||||
|
|
||||||
|
|
|
@ -47,6 +47,8 @@ body {
|
||||||
color: var(--text, $fallback--text);
|
color: var(--text, $fallback--text);
|
||||||
max-width: 100vw;
|
max-width: 100vw;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
|
|
|
@ -100,7 +100,6 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
|
||||||
copyInstanceOption('redirectRootLogin')
|
copyInstanceOption('redirectRootLogin')
|
||||||
copyInstanceOption('showInstanceSpecificPanel')
|
copyInstanceOption('showInstanceSpecificPanel')
|
||||||
copyInstanceOption('minimalScopesMode')
|
copyInstanceOption('minimalScopesMode')
|
||||||
copyInstanceOption('formattingOptionsEnabled')
|
|
||||||
copyInstanceOption('hideMutedPosts')
|
copyInstanceOption('hideMutedPosts')
|
||||||
copyInstanceOption('collapseMessageWithSubject')
|
copyInstanceOption('collapseMessageWithSubject')
|
||||||
copyInstanceOption('scopeCopy')
|
copyInstanceOption('scopeCopy')
|
||||||
|
|
|
@ -68,6 +68,7 @@
|
||||||
max-height: 200px;
|
max-height: 200px;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
video {
|
video {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="avatars">
|
<div class="avatars">
|
||||||
<router-link :to="userProfileLink(user)" class="avatars-item" v-for="user in slicedUsers">
|
<router-link
|
||||||
|
:to="userProfileLink(user)"
|
||||||
|
class="avatars-item"
|
||||||
|
v-for="user in slicedUsers"
|
||||||
|
:key="user.id"
|
||||||
|
>
|
||||||
<UserAvatar :user="user" class="avatar-small" />
|
<UserAvatar :user="user" class="avatar-small" />
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -139,6 +139,7 @@ const conversation = {
|
||||||
return (this.isExpanded) && id === this.status.id
|
return (this.isExpanded) && id === this.status.id
|
||||||
},
|
},
|
||||||
setHighlight (id) {
|
setHighlight (id) {
|
||||||
|
if (!id) return
|
||||||
this.highlight = id
|
this.highlight = id
|
||||||
this.$store.dispatch('fetchFavsAndRepeats', id)
|
this.$store.dispatch('fetchFavsAndRepeats', id)
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,31 +1,65 @@
|
||||||
import Completion from '../../services/completion/completion.js'
|
import Completion from '../../services/completion/completion.js'
|
||||||
import { take } from 'lodash'
|
import { take } from 'lodash'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* EmojiInput - augmented inputs for emoji and autocomplete support in inputs
|
||||||
|
* without having to give up the comfort of <input/> and <textarea/> elements
|
||||||
|
*
|
||||||
|
* Intended usage is:
|
||||||
|
* <EmojiInput v-model="something">
|
||||||
|
* <input v-model="something"/>
|
||||||
|
* </EmojiInput>
|
||||||
|
*
|
||||||
|
* Works only with <input> and <textarea>. Intended to use with only one nested
|
||||||
|
* input. It will find first input or textarea and work with that, multiple
|
||||||
|
* nested children not tested. You HAVE TO duplicate v-model for both
|
||||||
|
* <emoji-input> and <input>/<textarea> otherwise it will not work.
|
||||||
|
*
|
||||||
|
* Be prepared for CSS troubles though because it still wraps component in a div
|
||||||
|
* while TRYING to make it look like nothing happened, but it could break stuff.
|
||||||
|
*/
|
||||||
|
|
||||||
const EmojiInput = {
|
const EmojiInput = {
|
||||||
props: [
|
props: {
|
||||||
'placeholder',
|
suggest: {
|
||||||
'suggest',
|
/**
|
||||||
'value',
|
* suggest: function (input: String) => Suggestion[]
|
||||||
'type',
|
*
|
||||||
'classname'
|
* Function that takes input string which takes string (textAtCaret)
|
||||||
],
|
* and returns an array of Suggestions
|
||||||
|
*
|
||||||
|
* Suggestion is an object containing following properties:
|
||||||
|
* displayText: string. Main display text, what actual suggestion
|
||||||
|
* represents (user's screen name/emoji shortcode)
|
||||||
|
* replacement: string. Text that should replace the textAtCaret
|
||||||
|
* detailText: string, optional. Subtitle text, providing additional info
|
||||||
|
* if present (user's nickname)
|
||||||
|
* imageUrl: string, optional. Image to display alongside with suggestion,
|
||||||
|
* currently if no image is provided, replacement will be used (for
|
||||||
|
* unicode emojis)
|
||||||
|
*
|
||||||
|
* TODO: make it asynchronous when adding proper server-provided user
|
||||||
|
* suggestions
|
||||||
|
*
|
||||||
|
* For commonly used suggestors (emoji, users, both) use suggestor.js
|
||||||
|
*/
|
||||||
|
required: true,
|
||||||
|
type: Function
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
/**
|
||||||
|
* Used for v-model
|
||||||
|
*/
|
||||||
|
required: true,
|
||||||
|
type: String
|
||||||
|
}
|
||||||
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
input: undefined,
|
input: undefined,
|
||||||
highlighted: 0,
|
highlighted: 0,
|
||||||
caret: 0,
|
caret: 0,
|
||||||
focused: false,
|
focused: false
|
||||||
popupOptions: {
|
|
||||||
placement: 'bottom-start',
|
|
||||||
trigger: 'hover',
|
|
||||||
// See: https://github.com/RobinCK/vue-popper/issues/63
|
|
||||||
'delay-on-mouse-over': 9999999,
|
|
||||||
'delay-on-mouse-out': 9999999,
|
|
||||||
modifiers: {
|
|
||||||
arrow: { enabled: true },
|
|
||||||
offset: { offset: '0, 5px' }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -64,22 +98,22 @@ const EmojiInput = {
|
||||||
if (!input) return
|
if (!input) return
|
||||||
this.input = input
|
this.input = input
|
||||||
this.resize()
|
this.resize()
|
||||||
input.elm.addEventListener('transitionend', this.onTransition)
|
|
||||||
input.elm.addEventListener('blur', this.onBlur)
|
input.elm.addEventListener('blur', this.onBlur)
|
||||||
input.elm.addEventListener('focus', this.onFocus)
|
input.elm.addEventListener('focus', this.onFocus)
|
||||||
input.elm.addEventListener('paste', this.onPaste)
|
input.elm.addEventListener('paste', this.onPaste)
|
||||||
input.elm.addEventListener('keyup', this.onKeyUp)
|
input.elm.addEventListener('keyup', this.onKeyUp)
|
||||||
input.elm.addEventListener('keydown', this.onKeyDown)
|
input.elm.addEventListener('keydown', this.onKeyDown)
|
||||||
|
input.elm.addEventListener('transitionend', this.onTransition)
|
||||||
},
|
},
|
||||||
unmounted () {
|
unmounted () {
|
||||||
const { input } = this
|
const { input } = this
|
||||||
if (input) {
|
if (input) {
|
||||||
input.elm.removeEventListener('transitionend', this.onTransition)
|
|
||||||
input.elm.removeEventListener('blur', this.onBlur)
|
input.elm.removeEventListener('blur', this.onBlur)
|
||||||
input.elm.removeEventListener('focus', this.onFocus)
|
input.elm.removeEventListener('focus', this.onFocus)
|
||||||
input.elm.removeEventListener('paste', this.onPaste)
|
input.elm.removeEventListener('paste', this.onPaste)
|
||||||
input.elm.removeEventListener('keyup', this.onKeyUp)
|
input.elm.removeEventListener('keyup', this.onKeyUp)
|
||||||
input.elm.removeEventListener('keydown', this.onKeyDown)
|
input.elm.removeEventListener('keydown', this.onKeyDown)
|
||||||
|
input.elm.removeEventListener('transitionend', this.onTransition)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -96,8 +130,16 @@ const EmojiInput = {
|
||||||
const replacement = suggestion.replacement
|
const replacement = suggestion.replacement
|
||||||
const newValue = Completion.replaceWord(this.value, this.wordAtCaret, replacement)
|
const newValue = Completion.replaceWord(this.value, this.wordAtCaret, replacement)
|
||||||
this.$emit('input', newValue)
|
this.$emit('input', newValue)
|
||||||
this.caret = 0
|
|
||||||
this.highlighted = 0
|
this.highlighted = 0
|
||||||
|
const position = this.wordAtCaret.start + replacement.length
|
||||||
|
|
||||||
|
this.$nextTick(function () {
|
||||||
|
// Re-focus inputbox after clicking suggestion
|
||||||
|
this.input.elm.focus()
|
||||||
|
// Set selection right after the replacement instead of the very end
|
||||||
|
this.input.elm.setSelectionRange(position, position)
|
||||||
|
this.caret = position
|
||||||
|
})
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -126,29 +168,33 @@ const EmojiInput = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onTransition (e) {
|
onTransition (e) {
|
||||||
this.resize(e)
|
this.resize()
|
||||||
},
|
},
|
||||||
onBlur (e) {
|
onBlur (e) {
|
||||||
this.focused = false
|
// Clicking on any suggestion removes focus from autocomplete,
|
||||||
this.setCaret(e)
|
// preventing click handler ever executing.
|
||||||
this.resize(e)
|
setTimeout(() => {
|
||||||
|
this.focused = false
|
||||||
|
this.setCaret(e)
|
||||||
|
this.resize()
|
||||||
|
}, 200)
|
||||||
},
|
},
|
||||||
onFocus (e) {
|
onFocus (e) {
|
||||||
this.focused = true
|
this.focused = true
|
||||||
this.setCaret(e)
|
this.setCaret(e)
|
||||||
this.resize(e)
|
this.resize()
|
||||||
},
|
},
|
||||||
onKeyUp (e) {
|
onKeyUp (e) {
|
||||||
this.setCaret(e)
|
this.setCaret(e)
|
||||||
this.resize(e)
|
this.resize()
|
||||||
},
|
},
|
||||||
onPaste (e) {
|
onPaste (e) {
|
||||||
this.setCaret(e)
|
this.setCaret(e)
|
||||||
this.resize(e)
|
this.resize()
|
||||||
},
|
},
|
||||||
onKeyDown (e) {
|
onKeyDown (e) {
|
||||||
this.setCaret(e)
|
this.setCaret(e)
|
||||||
this.resize(e)
|
this.resize()
|
||||||
|
|
||||||
const { ctrlKey, shiftKey, key } = e
|
const { ctrlKey, shiftKey, key } = e
|
||||||
if (key === 'Tab') {
|
if (key === 'Tab') {
|
||||||
|
@ -172,7 +218,7 @@ const EmojiInput = {
|
||||||
onInput (e) {
|
onInput (e) {
|
||||||
this.$emit('input', e.target.value)
|
this.$emit('input', e.target.value)
|
||||||
},
|
},
|
||||||
setCaret ({ target: { selectionStart, value } }) {
|
setCaret ({ target: { selectionStart } }) {
|
||||||
this.caret = selectionStart
|
this.caret = selectionStart
|
||||||
},
|
},
|
||||||
resize () {
|
resize () {
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
<div
|
<div
|
||||||
v-for="(suggestion, index) in suggestions"
|
v-for="(suggestion, index) in suggestions"
|
||||||
:key="index"
|
:key="index"
|
||||||
@click.stop.prevent="replace(suggestion.replacement)"
|
@click.stop.prevent="replaceText"
|
||||||
class="autocomplete-item"
|
class="autocomplete-item"
|
||||||
:class="{ highlighted: suggestion.highlighted }"
|
:class="{ highlighted: suggestion.highlighted }"
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,70 +1,79 @@
|
||||||
export default function suggest (data) {
|
/**
|
||||||
return input => {
|
* suggest - generates a suggestor function to be used by emoji-input
|
||||||
const firstChar = input[0]
|
* data: object providing source information for specific types of suggestions:
|
||||||
if (firstChar === ':' && data.emoji) {
|
* data.emoji - optional, an array of all emoji available i.e.
|
||||||
return suggestEmoji(data.emoji)(input)
|
* (state.instance.emoji + state.instance.customEmoji)
|
||||||
}
|
* data.users - optional, an array of all known users
|
||||||
if (firstChar === '@' && data.users) {
|
*
|
||||||
return suggestUsers(data.users)(input)
|
* Depending on data present one or both (or none) can be present, so if field
|
||||||
}
|
* doesn't support user linking you can just provide only emoji.
|
||||||
return []
|
*/
|
||||||
|
export default data => input => {
|
||||||
|
const firstChar = input[0]
|
||||||
|
if (firstChar === ':' && data.emoji) {
|
||||||
|
return suggestEmoji(data.emoji)(input)
|
||||||
}
|
}
|
||||||
|
if (firstChar === '@' && data.users) {
|
||||||
|
return suggestUsers(data.users)(input)
|
||||||
|
}
|
||||||
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
function suggestEmoji (emojis) {
|
export const suggestEmoji = emojis => input => {
|
||||||
return input => {
|
const noPrefix = input.toLowerCase().substr(1)
|
||||||
const noPrefix = input.toLowerCase().substr(1)
|
return emojis
|
||||||
return emojis
|
.filter(({ displayText }) => displayText.toLowerCase().startsWith(noPrefix))
|
||||||
.filter(({ displayText }) => displayText.toLowerCase().startsWith(noPrefix))
|
.sort((a, b) => {
|
||||||
.sort((a, b) => {
|
|
||||||
let aScore = 0
|
|
||||||
let bScore = 0
|
|
||||||
|
|
||||||
// Make custom emojis a priority
|
|
||||||
aScore += Number(!!a.imageUrl) * 10
|
|
||||||
bScore += Number(!!b.imageUrl) * 10
|
|
||||||
|
|
||||||
// Sort alphabetically
|
|
||||||
const alphabetically = a.displayText > b.displayText ? 1 : -1
|
|
||||||
|
|
||||||
return bScore - aScore + alphabetically
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function suggestUsers (users) {
|
|
||||||
return input => {
|
|
||||||
const noPrefix = input.toLowerCase().substr(1)
|
|
||||||
return users.filter(
|
|
||||||
user =>
|
|
||||||
user.screen_name.toLowerCase().startsWith(noPrefix) ||
|
|
||||||
user.name.toLowerCase().startsWith(noPrefix)
|
|
||||||
/* eslint-disable camelcase */
|
|
||||||
).slice(0, 20).sort((a, b) => {
|
|
||||||
let aScore = 0
|
let aScore = 0
|
||||||
let bScore = 0
|
let bScore = 0
|
||||||
|
|
||||||
// Matches on screen name (i.e. user@instance) makes a priority
|
// Make custom emojis a priority
|
||||||
aScore += a.screen_name.toLowerCase().startsWith(noPrefix) * 2
|
aScore += a.imageUrl ? 10 : 0
|
||||||
bScore += b.screen_name.toLowerCase().startsWith(noPrefix) * 2
|
bScore += b.imageUrl ? 10 : 0
|
||||||
|
|
||||||
// Matches on name takes second priority
|
// Sort alphabetically
|
||||||
aScore += a.name.toLowerCase().startsWith(noPrefix)
|
const alphabetically = a.displayText > b.displayText ? 1 : -1
|
||||||
bScore += b.name.toLowerCase().startsWith(noPrefix)
|
|
||||||
|
|
||||||
const diff = bScore * 10 - aScore * 10
|
return bScore - aScore + alphabetically
|
||||||
|
})
|
||||||
// Then sort alphabetically
|
}
|
||||||
const nameAlphabetically = a.name > b.name ? 1 : -1
|
|
||||||
const screenNameAlphabetically = a.screen_name > b.screen_name ? 1 : -1
|
export const suggestUsers = users => input => {
|
||||||
|
const noPrefix = input.toLowerCase().substr(1)
|
||||||
return diff + nameAlphabetically + screenNameAlphabetically
|
return users.filter(
|
||||||
}).map(({ screen_name, name, profile_image_url_original }) => ({
|
user =>
|
||||||
displayText: screen_name,
|
user.screen_name.toLowerCase().startsWith(noPrefix) ||
|
||||||
detailText: name,
|
user.name.toLowerCase().startsWith(noPrefix)
|
||||||
imageUrl: profile_image_url_original,
|
|
||||||
replacement: '@' + screen_name
|
/* taking only 20 results so that sorting is a bit cheaper, we display
|
||||||
}))
|
* only 5 anyway. could be inaccurate, but we ideally we should query
|
||||||
/* eslint-enable camelcase */
|
* backend anyway
|
||||||
}
|
*/
|
||||||
|
).slice(0, 20).sort((a, b) => {
|
||||||
|
let aScore = 0
|
||||||
|
let bScore = 0
|
||||||
|
|
||||||
|
// Matches on screen name (i.e. user@instance) makes a priority
|
||||||
|
aScore += a.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0
|
||||||
|
bScore += b.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0
|
||||||
|
|
||||||
|
// Matches on name takes second priority
|
||||||
|
aScore += a.name.toLowerCase().startsWith(noPrefix) ? 1 : 0
|
||||||
|
bScore += b.name.toLowerCase().startsWith(noPrefix) ? 1 : 0
|
||||||
|
|
||||||
|
const diff = (bScore - aScore) * 10
|
||||||
|
|
||||||
|
// Then sort alphabetically
|
||||||
|
const nameAlphabetically = a.name > b.name ? 1 : -1
|
||||||
|
const screenNameAlphabetically = a.screen_name > b.screen_name ? 1 : -1
|
||||||
|
|
||||||
|
return diff + nameAlphabetically + screenNameAlphabetically
|
||||||
|
/* eslint-disable camelcase */
|
||||||
|
}).map(({ screen_name, name, profile_image_url_original }) => ({
|
||||||
|
displayText: screen_name,
|
||||||
|
detailText: name,
|
||||||
|
imageUrl: profile_image_url_original,
|
||||||
|
replacement: '@' + screen_name + ' '
|
||||||
|
}))
|
||||||
|
/* eslint-enable camelcase */
|
||||||
}
|
}
|
||||||
|
|
|
@ -147,9 +147,6 @@ const PostStatusForm = {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
formattingOptionsEnabled () {
|
|
||||||
return this.$store.state.instance.formattingOptionsEnabled
|
|
||||||
},
|
|
||||||
postFormats () {
|
postFormats () {
|
||||||
return this.$store.state.instance.postFormats || []
|
return this.$store.state.instance.postFormats || []
|
||||||
},
|
},
|
||||||
|
|
|
@ -31,7 +31,7 @@
|
||||||
<span v-if="safeDMEnabled">{{ $t('post_status.direct_warning_to_first_only') }}</span>
|
<span v-if="safeDMEnabled">{{ $t('post_status.direct_warning_to_first_only') }}</span>
|
||||||
<span v-else>{{ $t('post_status.direct_warning_to_all') }}</span>
|
<span v-else>{{ $t('post_status.direct_warning_to_all') }}</span>
|
||||||
</p>
|
</p>
|
||||||
<emoji-input
|
<EmojiInput
|
||||||
v-if="newStatus.spoilerText || alwaysShowSubject"
|
v-if="newStatus.spoilerText || alwaysShowSubject"
|
||||||
:suggest="emojiSuggestor"
|
:suggest="emojiSuggestor"
|
||||||
v-model="newStatus.spoilerText"
|
v-model="newStatus.spoilerText"
|
||||||
|
@ -44,8 +44,8 @@
|
||||||
v-model="newStatus.spoilerText"
|
v-model="newStatus.spoilerText"
|
||||||
class="form-post-subject"
|
class="form-post-subject"
|
||||||
/>
|
/>
|
||||||
</emoji-input>
|
</EmojiInput>
|
||||||
<emoji-input
|
<EmojiInput
|
||||||
:suggest="emojiUserSuggestor"
|
:suggest="emojiUserSuggestor"
|
||||||
v-model="newStatus.status"
|
v-model="newStatus.status"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
|
@ -65,9 +65,9 @@
|
||||||
class="form-post-body"
|
class="form-post-body"
|
||||||
>
|
>
|
||||||
</textarea>
|
</textarea>
|
||||||
</emoji-input>
|
</EmojiInput>
|
||||||
<div class="visibility-tray">
|
<div class="visibility-tray">
|
||||||
<div class="text-format" v-if="formattingOptionsEnabled">
|
<div class="text-format" v-if="postFormats.length > 1">
|
||||||
<label for="post-content-type" class="select">
|
<label for="post-content-type" class="select">
|
||||||
<select id="post-content-type" v-model="newStatus.contentType" class="form-control">
|
<select id="post-content-type" v-model="newStatus.contentType" class="form-control">
|
||||||
<option v-for="postFormat in postFormats" :key="postFormat" :value="postFormat">
|
<option v-for="postFormat in postFormats" :key="postFormat" :value="postFormat">
|
||||||
|
@ -77,6 +77,11 @@
|
||||||
<i class="icon-down-open"></i>
|
<i class="icon-down-open"></i>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="text-format" v-if="postFormats.length === 1">
|
||||||
|
<span class="only-format">
|
||||||
|
{{$t(`post_status.content_type["${postFormats[0]}"]`)}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<scope-selector
|
<scope-selector
|
||||||
:showAll="showAllScopes"
|
:showAll="showAllScopes"
|
||||||
|
@ -167,6 +172,14 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-format {
|
||||||
|
.only-format {
|
||||||
|
color: $fallback--faint;
|
||||||
|
color: var(--faint, $fallback--faint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.error {
|
.error {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
|
@ -102,7 +102,7 @@
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li v-if="postFormats.length > 0">
|
||||||
<div>
|
<div>
|
||||||
{{$t('settings.post_status_content_type')}}
|
{{$t('settings.post_status_content_type')}}
|
||||||
<label for="postContentType" class="select">
|
<label for="postContentType" class="select">
|
||||||
|
|
|
@ -78,6 +78,8 @@ const Timeline = {
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
handleShortKey (e) {
|
handleShortKey (e) {
|
||||||
|
// Ignore when input fields are focused
|
||||||
|
if (['textarea', 'input'].includes(e.target.tagName.toLowerCase())) return
|
||||||
if (e.key === '.') this.showNewStatuses()
|
if (e.key === '.') this.showNewStatuses()
|
||||||
},
|
},
|
||||||
showNewStatuses () {
|
showNewStatuses () {
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { mapState } from 'vuex'
|
||||||
const Mfa = {
|
const Mfa = {
|
||||||
data: () => ({
|
data: () => ({
|
||||||
settings: { // current settings of MFA
|
settings: { // current settings of MFA
|
||||||
|
available: false,
|
||||||
enabled: false,
|
enabled: false,
|
||||||
totp: false
|
totp: false
|
||||||
},
|
},
|
||||||
|
@ -139,7 +140,9 @@ const Mfa = {
|
||||||
// fetch settings from server
|
// fetch settings from server
|
||||||
async fetchSettings () {
|
async fetchSettings () {
|
||||||
let result = await this.backendInteractor.fetchSettingsMFA()
|
let result = await this.backendInteractor.fetchSettingsMFA()
|
||||||
|
if (result.error) return
|
||||||
this.settings = result.settings
|
this.settings = result.settings
|
||||||
|
this.settings.available = true
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="setting-item mfa-settings" v-if="readyInit">
|
<div class="setting-item mfa-settings" v-if="readyInit && settings.available">
|
||||||
|
|
||||||
<div class="mfa-heading">
|
<div class="mfa-heading">
|
||||||
<h2>{{$t('settings.mfa.title')}}</h2>
|
<h2>{{$t('settings.mfa.title')}}</h2>
|
||||||
|
|
|
@ -47,7 +47,9 @@ const UserSettings = {
|
||||||
pickAvatarBtnVisible: true,
|
pickAvatarBtnVisible: true,
|
||||||
bannerUploading: false,
|
bannerUploading: false,
|
||||||
backgroundUploading: false,
|
backgroundUploading: false,
|
||||||
|
banner: null,
|
||||||
bannerPreview: null,
|
bannerPreview: null,
|
||||||
|
background: null,
|
||||||
backgroundPreview: null,
|
backgroundPreview: null,
|
||||||
bannerUploadError: null,
|
bannerUploadError: null,
|
||||||
backgroundUploadError: null,
|
backgroundUploadError: null,
|
||||||
|
@ -214,22 +216,12 @@ const UserSettings = {
|
||||||
},
|
},
|
||||||
submitBg () {
|
submitBg () {
|
||||||
if (!this.backgroundPreview) { return }
|
if (!this.backgroundPreview) { return }
|
||||||
let img = this.backgroundPreview
|
let background = this.background
|
||||||
// eslint-disable-next-line no-undef
|
|
||||||
let imginfo = new Image()
|
|
||||||
let cropX, cropY, cropW, cropH
|
|
||||||
imginfo.src = img
|
|
||||||
cropX = 0
|
|
||||||
cropY = 0
|
|
||||||
cropW = imginfo.width
|
|
||||||
cropH = imginfo.width
|
|
||||||
this.backgroundUploading = true
|
this.backgroundUploading = true
|
||||||
this.$store.state.api.backendInteractor.updateBg({params: {img, cropX, cropY, cropW, cropH}}).then((data) => {
|
this.$store.state.api.backendInteractor.updateBg({ background }).then((data) => {
|
||||||
if (!data.error) {
|
if (!data.error) {
|
||||||
let clone = JSON.parse(JSON.stringify(this.$store.state.users.currentUser))
|
this.$store.commit('addNewUsers', [data])
|
||||||
clone.background_image = data.url
|
this.$store.commit('setCurrentUser', data)
|
||||||
this.$store.commit('addNewUsers', [clone])
|
|
||||||
this.$store.commit('setCurrentUser', clone)
|
|
||||||
this.backgroundPreview = null
|
this.backgroundPreview = null
|
||||||
} else {
|
} else {
|
||||||
this.backgroundUploadError = this.$t('upload.error.base') + data.error
|
this.backgroundUploadError = this.$t('upload.error.base') + data.error
|
||||||
|
|
|
@ -22,20 +22,20 @@
|
||||||
<div class="setting-item" >
|
<div class="setting-item" >
|
||||||
<h2>{{$t('settings.name_bio')}}</h2>
|
<h2>{{$t('settings.name_bio')}}</h2>
|
||||||
<p>{{$t('settings.name')}}</p>
|
<p>{{$t('settings.name')}}</p>
|
||||||
<emoji-input :suggest="emojiSuggestor" v-model="newName">
|
<EmojiInput :suggest="emojiSuggestor" v-model="newName">
|
||||||
<input
|
<input
|
||||||
v-model="newName"
|
v-model="newName"
|
||||||
id="username"
|
id="username"
|
||||||
classname="name-changer"
|
classname="name-changer"
|
||||||
/>
|
/>
|
||||||
</emoji-input>
|
</EmojiInput>
|
||||||
<p>{{$t('settings.bio')}}</p>
|
<p>{{$t('settings.bio')}}</p>
|
||||||
<emoji-input :suggest="emojiUserSuggestor" v-model="newBio">
|
<EmojiInput :suggest="emojiUserSuggestor" v-model="newBio">
|
||||||
<textarea
|
<textarea
|
||||||
v-model="newBio"
|
v-model="newBio"
|
||||||
classname="bio"
|
classname="bio"
|
||||||
/>
|
/>
|
||||||
</emoji-input>
|
</EmojiInput>
|
||||||
<p>
|
<p>
|
||||||
<input type="checkbox" v-model="newLocked" id="account-locked">
|
<input type="checkbox" v-model="newLocked" id="account-locked">
|
||||||
<label for="account-locked">{{$t('settings.lock_account_description')}}</label>
|
<label for="account-locked">{{$t('settings.lock_account_description')}}</label>
|
||||||
|
|
|
@ -16,7 +16,6 @@ const defaultState = {
|
||||||
redirectRootNoLogin: '/main/all',
|
redirectRootNoLogin: '/main/all',
|
||||||
redirectRootLogin: '/main/friends',
|
redirectRootLogin: '/main/friends',
|
||||||
showInstanceSpecificPanel: false,
|
showInstanceSpecificPanel: false,
|
||||||
formattingOptionsEnabled: false,
|
|
||||||
alwaysShowSubjectInput: true,
|
alwaysShowSubjectInput: true,
|
||||||
hideMutedPosts: false,
|
hideMutedPosts: false,
|
||||||
collapseMessageWithSubject: false,
|
collapseMessageWithSubject: false,
|
||||||
|
|
|
@ -356,7 +356,13 @@ const users = {
|
||||||
},
|
},
|
||||||
searchUsers (store, query) {
|
searchUsers (store, query) {
|
||||||
// TODO: Move userSearch api into api.service
|
// TODO: Move userSearch api into api.service
|
||||||
return userSearchApi.search({query, store: { state: store.rootState }})
|
return userSearchApi.search({
|
||||||
|
query,
|
||||||
|
store: {
|
||||||
|
state: store.rootState,
|
||||||
|
getters: store.rootGetters
|
||||||
|
}
|
||||||
|
})
|
||||||
.then((users) => {
|
.then((users) => {
|
||||||
store.commit('addNewUsers', users)
|
store.commit('addNewUsers', users)
|
||||||
return users
|
return users
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
/* eslint-env browser */
|
/* eslint-env browser */
|
||||||
const BG_UPDATE_URL = '/api/qvitter/update_background_image.json'
|
|
||||||
const EXTERNAL_PROFILE_URL = '/api/externalprofile/show.json'
|
const EXTERNAL_PROFILE_URL = '/api/externalprofile/show.json'
|
||||||
const QVITTER_USER_NOTIFICATIONS_READ_URL = '/api/qvitter/statuses/notifications/read.json'
|
const QVITTER_USER_NOTIFICATIONS_READ_URL = '/api/qvitter/statuses/notifications/read.json'
|
||||||
const BLOCKS_IMPORT_URL = '/api/pleroma/blocks_import'
|
const BLOCKS_IMPORT_URL = '/api/pleroma/blocks_import'
|
||||||
|
@ -25,7 +24,6 @@ const MFA_DISABLE_OTP_URL = '/api/pleroma/profile/mfa/totp'
|
||||||
|
|
||||||
const MASTODON_LOGIN_URL = '/api/v1/accounts/verify_credentials'
|
const MASTODON_LOGIN_URL = '/api/v1/accounts/verify_credentials'
|
||||||
const MASTODON_REGISTRATION_URL = '/api/v1/accounts'
|
const MASTODON_REGISTRATION_URL = '/api/v1/accounts'
|
||||||
const GET_BACKGROUND_HACK = '/api/account/verify_credentials.json'
|
|
||||||
const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites'
|
const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites'
|
||||||
const MASTODON_USER_NOTIFICATIONS_URL = '/api/v1/notifications'
|
const MASTODON_USER_NOTIFICATIONS_URL = '/api/v1/notifications'
|
||||||
const MASTODON_FAVORITE_URL = id => `/api/v1/statuses/${id}/favourite`
|
const MASTODON_FAVORITE_URL = id => `/api/v1/statuses/${id}/favourite`
|
||||||
|
@ -133,22 +131,16 @@ const updateAvatar = ({credentials, avatar}) => {
|
||||||
.then((data) => parseUser(data))
|
.then((data) => parseUser(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateBg = ({credentials, params}) => {
|
const updateBg = ({ credentials, background }) => {
|
||||||
let url = BG_UPDATE_URL
|
|
||||||
|
|
||||||
const form = new FormData()
|
const form = new FormData()
|
||||||
|
form.append('pleroma_background_image', background)
|
||||||
each(params, (value, key) => {
|
return fetch(MASTODON_PROFILE_UPDATE_URL, {
|
||||||
if (value) {
|
|
||||||
form.append(key, value)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return fetch(url, {
|
|
||||||
headers: authHeaders(credentials),
|
headers: authHeaders(credentials),
|
||||||
method: 'POST',
|
method: 'PATCH',
|
||||||
body: form
|
body: form
|
||||||
}).then((data) => data.json())
|
})
|
||||||
|
.then((data) => data.json())
|
||||||
|
.then((data) => parseUser(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateBanner = ({credentials, banner}) => {
|
const updateBanner = ({credentials, banner}) => {
|
||||||
|
@ -544,26 +536,6 @@ const verifyCredentials = (user) => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then((data) => data.error ? data : parseUser(data))
|
.then((data) => data.error ? data : parseUser(data))
|
||||||
.then((mastoUser) => {
|
|
||||||
// REMOVE WHEN BE SUPPORTS background_image
|
|
||||||
return fetch(GET_BACKGROUND_HACK, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: authHeaders(user)
|
|
||||||
})
|
|
||||||
.then((response) => {
|
|
||||||
if (response.ok) {
|
|
||||||
return response.json()
|
|
||||||
} else {
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
/* eslint-disable camelcase */
|
|
||||||
.then(({ background_image }) => ({
|
|
||||||
...mastoUser,
|
|
||||||
background_image
|
|
||||||
}))
|
|
||||||
/* eslint-enable camelcase */
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const favorite = ({ id, credentials }) => {
|
const favorite = ({ id, credentials }) => {
|
||||||
|
|
|
@ -105,7 +105,7 @@ const backendInteractorService = (credentials) => {
|
||||||
const getCaptcha = () => apiService.getCaptcha()
|
const getCaptcha = () => apiService.getCaptcha()
|
||||||
const register = (params) => apiService.register({ credentials, params })
|
const register = (params) => apiService.register({ credentials, params })
|
||||||
const updateAvatar = ({avatar}) => apiService.updateAvatar({credentials, avatar})
|
const updateAvatar = ({avatar}) => apiService.updateAvatar({credentials, avatar})
|
||||||
const updateBg = ({params}) => apiService.updateBg({credentials, params})
|
const updateBg = ({ background }) => apiService.updateBg({ credentials, background })
|
||||||
const updateBanner = ({banner}) => apiService.updateBanner({credentials, banner})
|
const updateBanner = ({banner}) => apiService.updateBanner({credentials, banner})
|
||||||
const updateProfile = ({params}) => apiService.updateProfile({credentials, params})
|
const updateProfile = ({params}) => apiService.updateProfile({credentials, params})
|
||||||
|
|
||||||
|
|
|
@ -60,6 +60,9 @@ export const parseUser = (data) => {
|
||||||
if (data.pleroma) {
|
if (data.pleroma) {
|
||||||
const relationship = data.pleroma.relationship
|
const relationship = data.pleroma.relationship
|
||||||
|
|
||||||
|
output.background_image = data.pleroma.background_image
|
||||||
|
output.token = data.pleroma.chat_token
|
||||||
|
|
||||||
if (relationship) {
|
if (relationship) {
|
||||||
output.follows_you = relationship.followed_by
|
output.follows_you = relationship.followed_by
|
||||||
output.following = relationship.following
|
output.following = relationship.following
|
||||||
|
|
|
@ -9,7 +9,6 @@
|
||||||
"redirectRootLogin": "/main/friends",
|
"redirectRootLogin": "/main/friends",
|
||||||
"chatDisabled": false,
|
"chatDisabled": false,
|
||||||
"showInstanceSpecificPanel": false,
|
"showInstanceSpecificPanel": false,
|
||||||
"formattingOptionsEnabled": false,
|
|
||||||
"collapseMessageWithSubject": false,
|
"collapseMessageWithSubject": false,
|
||||||
"scopeCopy": true,
|
"scopeCopy": true,
|
||||||
"subjectLineBehavior": "noop",
|
"subjectLineBehavior": "noop",
|
||||||
|
|
Loading…
Add table
Reference in a new issue