Merge remote-tracking branch 'upstream/develop' into shigusegubu
* upstream/develop: (51 commits) fix convos resolve focus issue in the auto-complete for name field resolve issue related to event handler resolve issue in prop binding this fixes #350 - v-model binding issue #341 - fix naming #341 - automatic scroll with repeats/reports 345 - fix long username issue in repeat status Update spanish translate - es.json Fix return Fix clicking link - open new tab unless tag or mention Fix max-width of profile banner in setting Fix unit test Typo #332 - add follow/not follow button to follow list Add quick css fix for user profile bg img preview Switch into Lodash Switch into Lodash Change the async stuff to not render app before theme is loaded fixing conflicts ...
This commit is contained in:
commit
f4e8b781a9
43 changed files with 616 additions and 363 deletions
|
@ -66,7 +66,7 @@ export default {
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
logo () { return this.$store.state.instance.logo },
|
logo () { return this.$store.state.instance.logo },
|
||||||
style () {
|
bgStyle () {
|
||||||
return {
|
return {
|
||||||
'--body-background-image': `url(${this.background})`,
|
'--body-background-image': `url(${this.background})`,
|
||||||
'background-image': `url(${this.background})`
|
'background-image': `url(${this.background})`
|
||||||
|
@ -82,7 +82,7 @@ export default {
|
||||||
unseenNotificationsCount () {
|
unseenNotificationsCount () {
|
||||||
return this.unseenNotifications.length
|
return this.unseenNotifications.length
|
||||||
},
|
},
|
||||||
showFeaturesPanel () { return this.$store.state.config.showFeaturesPanel }
|
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel }
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
scrollToTop () {
|
scrollToTop () {
|
||||||
|
|
14
src/App.scss
14
src/App.scss
|
@ -1,15 +1,21 @@
|
||||||
@import './_variables.scss';
|
@import './_variables.scss';
|
||||||
|
|
||||||
#app {
|
#app {
|
||||||
background-size: cover;
|
|
||||||
background-attachment: fixed;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-position: 0 50px;
|
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app-bg-wrapper {
|
||||||
|
position: fixed;
|
||||||
|
z-index: -1;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
background-size: cover;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: 0 50%;
|
||||||
|
}
|
||||||
|
|
||||||
i {
|
i {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div id="app" v-bind:style="style">
|
<div id="app">
|
||||||
|
<div class="app-bg-wrapper" v-bind:style="bgStyle"></div>
|
||||||
<nav class='nav-bar container' @click="scrollToTop()" id="nav">
|
<nav class='nav-bar container' @click="scrollToTop()" id="nav">
|
||||||
<div class='logo' :style='logoBgStyle'>
|
<div class='logo' :style='logoBgStyle'>
|
||||||
<div class='mask' :style='logoMaskStyle'></div>
|
<div class='mask' :style='logoMaskStyle'></div>
|
||||||
|
|
|
@ -55,10 +55,10 @@ const afterStoreSetup = ({ store, i18n }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
copyInstanceOption('nsfwCensorImage')
|
copyInstanceOption('nsfwCensorImage')
|
||||||
copyInstanceOption('theme')
|
|
||||||
copyInstanceOption('background')
|
copyInstanceOption('background')
|
||||||
copyInstanceOption('hidePostStats')
|
copyInstanceOption('hidePostStats')
|
||||||
copyInstanceOption('hideUserStats')
|
copyInstanceOption('hideUserStats')
|
||||||
|
copyInstanceOption('hideFilteredStatuses')
|
||||||
copyInstanceOption('logo')
|
copyInstanceOption('logo')
|
||||||
|
|
||||||
store.dispatch('setInstanceOption', {
|
store.dispatch('setInstanceOption', {
|
||||||
|
@ -87,6 +87,7 @@ const afterStoreSetup = ({ store, i18n }) => {
|
||||||
copyInstanceOption('postContentType')
|
copyInstanceOption('postContentType')
|
||||||
copyInstanceOption('alwaysShowSubjectInput')
|
copyInstanceOption('alwaysShowSubjectInput')
|
||||||
copyInstanceOption('noAttachmentLinks')
|
copyInstanceOption('noAttachmentLinks')
|
||||||
|
copyInstanceOption('showFeaturesPanel')
|
||||||
|
|
||||||
if ((config.chatDisabled)) {
|
if ((config.chatDisabled)) {
|
||||||
store.dispatch('disableChat')
|
store.dispatch('disableChat')
|
||||||
|
@ -94,6 +95,9 @@ const afterStoreSetup = ({ store, i18n }) => {
|
||||||
store.dispatch('initializeSocket')
|
store.dispatch('initializeSocket')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return store.dispatch('setTheme', config['theme'])
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
const router = new VueRouter({
|
const router = new VueRouter({
|
||||||
mode: 'history',
|
mode: 'history',
|
||||||
routes: routes(store),
|
routes: routes(store),
|
||||||
|
|
|
@ -9,7 +9,7 @@ const About = {
|
||||||
TermsOfServicePanel
|
TermsOfServicePanel
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
showFeaturesPanel () { return this.$store.state.config.showFeaturesPanel }
|
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
149
src/components/autocomplete_input/autocomplete_input.js
Normal file
149
src/components/autocomplete_input/autocomplete_input.js
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
import Completion from '../../services/completion/completion.js'
|
||||||
|
import { take, filter, map } from 'lodash'
|
||||||
|
|
||||||
|
const AutoCompleteInput = {
|
||||||
|
props: [
|
||||||
|
'id',
|
||||||
|
'classObj',
|
||||||
|
'value',
|
||||||
|
'placeholder',
|
||||||
|
'autoResize',
|
||||||
|
'multiline',
|
||||||
|
'drop',
|
||||||
|
'dragoverPrevent',
|
||||||
|
'paste',
|
||||||
|
'keydownMetaEnter',
|
||||||
|
'keyupCtrlEnter'
|
||||||
|
],
|
||||||
|
components: {},
|
||||||
|
mounted () {
|
||||||
|
this.autoResize && this.resize(this.$refs.textarea)
|
||||||
|
const textLength = this.$refs.textarea.value.length
|
||||||
|
this.$refs.textarea.setSelectionRange(textLength, textLength)
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
caret: 0,
|
||||||
|
highlighted: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
users () {
|
||||||
|
return this.$store.state.users.users
|
||||||
|
},
|
||||||
|
emoji () {
|
||||||
|
return this.$store.state.instance.emoji || []
|
||||||
|
},
|
||||||
|
customEmoji () {
|
||||||
|
return this.$store.state.instance.customEmoji || []
|
||||||
|
},
|
||||||
|
textAtCaret () {
|
||||||
|
return (this.wordAtCaret || {}).word || ''
|
||||||
|
},
|
||||||
|
wordAtCaret () {
|
||||||
|
const word = Completion.wordAtPosition(this.value, this.caret - 1) || {}
|
||||||
|
return word
|
||||||
|
},
|
||||||
|
candidates () {
|
||||||
|
const firstchar = this.textAtCaret.charAt(0)
|
||||||
|
if (firstchar === '@') {
|
||||||
|
const query = this.textAtCaret.slice(1).toUpperCase()
|
||||||
|
const matchedUsers = filter(this.users, (user) => {
|
||||||
|
return user.screen_name.toUpperCase().startsWith(query) ||
|
||||||
|
user.name && user.name.toUpperCase().startsWith(query)
|
||||||
|
})
|
||||||
|
if (matchedUsers.length <= 0) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line camelcase
|
||||||
|
return map(take(matchedUsers, 5), ({screen_name, name, profile_image_url_original}, index) => ({
|
||||||
|
// eslint-disable-next-line camelcase
|
||||||
|
screen_name: `@${screen_name}`,
|
||||||
|
name: name,
|
||||||
|
img: profile_image_url_original,
|
||||||
|
highlighted: index === this.highlighted
|
||||||
|
}))
|
||||||
|
} else if (firstchar === ':') {
|
||||||
|
if (this.textAtCaret === ':') { return }
|
||||||
|
const matchedEmoji = filter(this.emoji.concat(this.customEmoji), (emoji) => emoji.shortcode.startsWith(this.textAtCaret.slice(1)))
|
||||||
|
if (matchedEmoji.length <= 0) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return map(take(matchedEmoji, 5), ({shortcode, image_url, utf}, index) => ({
|
||||||
|
screen_name: `:${shortcode}:`,
|
||||||
|
name: '',
|
||||||
|
utf: utf || '',
|
||||||
|
// eslint-disable-next-line camelcase
|
||||||
|
img: utf ? '' : this.$store.state.instance.server + image_url,
|
||||||
|
highlighted: index === this.highlighted
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setCaret ({target: {selectionStart}}) {
|
||||||
|
this.caret = selectionStart
|
||||||
|
},
|
||||||
|
cycleBackward (e) {
|
||||||
|
const len = this.candidates.length || 0
|
||||||
|
if (len > 0) {
|
||||||
|
e.preventDefault()
|
||||||
|
this.highlighted -= 1
|
||||||
|
if (this.highlighted < 0) {
|
||||||
|
this.highlighted = this.candidates.length - 1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.highlighted = 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cycleForward (e) {
|
||||||
|
const len = this.candidates.length || 0
|
||||||
|
if (len > 0) {
|
||||||
|
if (e.shiftKey) { return }
|
||||||
|
e.preventDefault()
|
||||||
|
this.highlighted += 1
|
||||||
|
if (this.highlighted >= len) {
|
||||||
|
this.highlighted = 0
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.highlighted = 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
replace (replacement) {
|
||||||
|
this.$emit('input', Completion.replaceWord(this.value, this.wordAtCaret, replacement))
|
||||||
|
const el = this.$el.querySelector('textarea') || this.$el.querySelector('input')
|
||||||
|
el.focus()
|
||||||
|
this.caret = 0
|
||||||
|
},
|
||||||
|
replaceCandidate (e) {
|
||||||
|
const len = this.candidates.length || 0
|
||||||
|
if (this.textAtCaret === ':' || e.ctrlKey) { return }
|
||||||
|
if (len > 0) {
|
||||||
|
e.preventDefault()
|
||||||
|
const candidate = this.candidates[this.highlighted]
|
||||||
|
const replacement = candidate.utf || (candidate.screen_name + ' ')
|
||||||
|
this.$emit('input', Completion.replaceWord(this.value, this.wordAtCaret, replacement))
|
||||||
|
const el = this.$el.querySelector('textarea') || this.$el.querySelector('input')
|
||||||
|
el.focus()
|
||||||
|
this.caret = 0
|
||||||
|
this.highlighted = 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
resize (e) {
|
||||||
|
const target = e.target || e
|
||||||
|
if (!(target instanceof window.Element)) { return }
|
||||||
|
const vertPadding = Number(window.getComputedStyle(target)['padding-top'].substr(0, 1)) +
|
||||||
|
Number(window.getComputedStyle(target)['padding-bottom'].substr(0, 1))
|
||||||
|
// Auto is needed to make textbox shrink when removing lines
|
||||||
|
target.style.height = 'auto'
|
||||||
|
target.style.height = `${target.scrollHeight - vertPadding}px`
|
||||||
|
if (target.value === '') {
|
||||||
|
target.style.height = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AutoCompleteInput
|
104
src/components/autocomplete_input/autocomplete_input.vue
Normal file
104
src/components/autocomplete_input/autocomplete_input.vue
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
<template>
|
||||||
|
<div style="display: flex; flex-direction: column;">
|
||||||
|
<textarea
|
||||||
|
v-if="multiline"
|
||||||
|
ref="textarea"
|
||||||
|
rows="1"
|
||||||
|
:value="value" :class="classObj" :id="id" :placeholder="placeholder"
|
||||||
|
@input="$emit('input', $event.target.value), autoResize && resize($event)"
|
||||||
|
@click="setCaret"
|
||||||
|
@keyup="setCaret"
|
||||||
|
@keydown.down="cycleForward"
|
||||||
|
@keydown.up="cycleBackward"
|
||||||
|
@keydown.shift.tab="cycleBackward"
|
||||||
|
@keydown.tab="cycleForward"
|
||||||
|
@keydown.enter="replaceCandidate"
|
||||||
|
@drop="drop && drop($event)"
|
||||||
|
@dragover.prevent="dragoverPrevent && dragoverPrevent($event)"
|
||||||
|
@paste="paste && paste($event)"
|
||||||
|
@keydown.meta.enter="keydownMetaEnter && keydownMetaEnter($event)"
|
||||||
|
@keyup.ctrl.enter="keyupCtrlEnter && keyupCtrlEnter($event)">
|
||||||
|
</textarea>
|
||||||
|
<input
|
||||||
|
v-else
|
||||||
|
ref="textarea"
|
||||||
|
:value="value" :class="classObj" :id="id" :placeholder="placeholder"
|
||||||
|
@input="$emit('input', $event.target.value), autoResize && resize($event)"
|
||||||
|
@click="setCaret"
|
||||||
|
@keyup="setCaret"
|
||||||
|
@keydown.down="cycleForward"
|
||||||
|
@keydown.up="cycleBackward"
|
||||||
|
@keydown.shift.tab="cycleBackward"
|
||||||
|
@keydown.tab="cycleForward"
|
||||||
|
@keydown.enter="replaceCandidate"
|
||||||
|
@drop="drop && drop($event)"
|
||||||
|
@dragover.prevent="dragoverPrevent && dragoverPrevent($event)"
|
||||||
|
@paste="paste && paste($event)"
|
||||||
|
@keydown.meta.enter="keydownMetaEnter && keydownMetaEnter($event)"
|
||||||
|
@keyup.ctrl.enter="keyupCtrlEnter && keyupCtrlEnter($event)"/>
|
||||||
|
<div style="position:relative;" v-if="candidates">
|
||||||
|
<div class="autocomplete-panel">
|
||||||
|
<div v-for="candidate in candidates" @click="replace(candidate.utf || (candidate.screen_name + ' '))">
|
||||||
|
<div class="autocomplete" :class="{ highlighted: candidate.highlighted }">
|
||||||
|
<span v-if="candidate.img"><img :src="candidate.img"></img></span>
|
||||||
|
<span v-else>{{candidate.utf}}</span>
|
||||||
|
<span>{{candidate.screen_name}}<small>{{candidate.name}}</small></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./autocomplete_input.js"></script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
|
.autocomplete-panel {
|
||||||
|
margin: 0 0.5em 0 0.5em;
|
||||||
|
border-radius: $fallback--tooltipRadius;
|
||||||
|
border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
box-shadow: 1px 2px 4px rgba(0, 0, 0, 0.5);
|
||||||
|
// this doesn't match original but i don't care, making it uniform.
|
||||||
|
box-shadow: var(--popupShadow);
|
||||||
|
min-width: 75%;
|
||||||
|
background: $fallback--bg;
|
||||||
|
background: var(--bg, $fallback--bg);
|
||||||
|
color: $fallback--lightText;
|
||||||
|
color: var(--lightText, $fallback--lightText);
|
||||||
|
}
|
||||||
|
|
||||||
|
.autocomplete {
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.2em 0.4em 0.2em 0.4em;
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.4);
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: $fallback--avatarRadius;
|
||||||
|
border-radius: var(--avatarRadius, $fallback--avatarRadius);
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
line-height: 24px;
|
||||||
|
margin: 0 0.1em 0 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
small {
|
||||||
|
margin-left: .5em;
|
||||||
|
color: $fallback--faint;
|
||||||
|
color: var(--faint, $fallback--faint);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.highlighted {
|
||||||
|
background-color: $fallback--fg;
|
||||||
|
background-color: var(--lightBg, $fallback--fg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -9,9 +9,9 @@ const sortById = (a, b) => {
|
||||||
if (isSeqA && isSeqB) {
|
if (isSeqA && isSeqB) {
|
||||||
return seqA < seqB ? -1 : 1
|
return seqA < seqB ? -1 : 1
|
||||||
} else if (isSeqA && !isSeqB) {
|
} else if (isSeqA && !isSeqB) {
|
||||||
return 1
|
|
||||||
} else if (!isSeqA && isSeqB) {
|
|
||||||
return -1
|
return -1
|
||||||
|
} else if (!isSeqA && isSeqB) {
|
||||||
|
return 1
|
||||||
} else {
|
} else {
|
||||||
return a.id < b.id ? -1 : 1
|
return a.id < b.id ? -1 : 1
|
||||||
}
|
}
|
||||||
|
@ -36,6 +36,13 @@ const conversation = {
|
||||||
status () {
|
status () {
|
||||||
return this.statusoid
|
return this.statusoid
|
||||||
},
|
},
|
||||||
|
statusId () {
|
||||||
|
if (this.statusoid.retweeted_status) {
|
||||||
|
return this.statusoid.retweeted_status.id
|
||||||
|
} else {
|
||||||
|
return this.statusoid.id
|
||||||
|
}
|
||||||
|
},
|
||||||
conversation () {
|
conversation () {
|
||||||
if (!this.status) {
|
if (!this.status) {
|
||||||
return []
|
return []
|
||||||
|
@ -79,7 +86,7 @@ const conversation = {
|
||||||
const conversationId = this.status.statusnet_conversation_id
|
const conversationId = this.status.statusnet_conversation_id
|
||||||
this.$store.state.api.backendInteractor.fetchConversation({id: conversationId})
|
this.$store.state.api.backendInteractor.fetchConversation({id: conversationId})
|
||||||
.then((statuses) => this.$store.dispatch('addNewStatuses', { statuses }))
|
.then((statuses) => this.$store.dispatch('addNewStatuses', { statuses }))
|
||||||
.then(() => this.setHighlight(this.statusoid.id))
|
.then(() => this.setHighlight(this.statusId))
|
||||||
} else {
|
} else {
|
||||||
const id = this.$route.params.id
|
const id = this.$route.params.id
|
||||||
this.$store.state.api.backendInteractor.fetchStatus({id})
|
this.$store.state.api.backendInteractor.fetchStatus({id})
|
||||||
|
@ -91,11 +98,7 @@ const conversation = {
|
||||||
return this.replies[id] || []
|
return this.replies[id] || []
|
||||||
},
|
},
|
||||||
focused (id) {
|
focused (id) {
|
||||||
if (this.statusoid.retweeted_status) {
|
return id === this.statusId
|
||||||
return (id === this.statusoid.retweeted_status.id)
|
|
||||||
} else {
|
|
||||||
return (id === this.statusoid.id)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
setHighlight (id) {
|
setHighlight (id) {
|
||||||
this.highlight = id
|
this.highlight = id
|
||||||
|
|
|
@ -25,7 +25,8 @@ const FollowList = {
|
||||||
},
|
},
|
||||||
entries () {
|
entries () {
|
||||||
return this.showFollowers ? this.user.followers : this.user.friends
|
return this.showFollowers ? this.user.followers : this.user.friends
|
||||||
}
|
},
|
||||||
|
showActions () { return this.$store.state.users.currentUser.id === this.userId }
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
fetchEntries () {
|
fetchEntries () {
|
||||||
|
|
|
@ -3,7 +3,8 @@
|
||||||
<user-card
|
<user-card
|
||||||
v-for="entry in entries"
|
v-for="entry in entries"
|
||||||
:key="entry.id" :user="entry"
|
:key="entry.id" :user="entry"
|
||||||
:showFollows="true"
|
:showFollows="!showFollowers"
|
||||||
|
:showActions="showActions"
|
||||||
/>
|
/>
|
||||||
<div class="text-center panel-footer">
|
<div class="text-center panel-footer">
|
||||||
<a v-if="error" @click="fetchEntries" class="alert error">
|
<a v-if="error" @click="fetchEntries" class="alert error">
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import statusPoster from '../../services/status_poster/status_poster.service.js'
|
import statusPoster from '../../services/status_poster/status_poster.service.js'
|
||||||
import MediaUpload from '../media_upload/media_upload.vue'
|
import MediaUpload from '../media_upload/media_upload.vue'
|
||||||
|
import AutoCompleteInput from '../autocomplete_input/autocomplete_input.vue'
|
||||||
import fileTypeService from '../../services/file_type/file_type.service.js'
|
import fileTypeService from '../../services/file_type/file_type.service.js'
|
||||||
import Completion from '../../services/completion/completion.js'
|
import { reject, map, uniqBy } from 'lodash'
|
||||||
import { take, filter, reject, map, uniqBy } from 'lodash'
|
|
||||||
|
|
||||||
const buildMentionsString = ({user, attentions}, currentUser) => {
|
const buildMentionsString = ({user, attentions}, currentUser) => {
|
||||||
let allAttentions = [...attentions]
|
let allAttentions = [...attentions]
|
||||||
|
@ -28,13 +28,10 @@ const PostStatusForm = {
|
||||||
'subject'
|
'subject'
|
||||||
],
|
],
|
||||||
components: {
|
components: {
|
||||||
MediaUpload
|
MediaUpload,
|
||||||
|
AutoCompleteInput
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
this.resize(this.$refs.textarea)
|
|
||||||
const textLength = this.$refs.textarea.value.length
|
|
||||||
this.$refs.textarea.setSelectionRange(textLength, textLength)
|
|
||||||
|
|
||||||
if (this.replyTo) {
|
if (this.replyTo) {
|
||||||
this.$refs.textarea.focus()
|
this.$refs.textarea.focus()
|
||||||
}
|
}
|
||||||
|
@ -61,15 +58,13 @@ const PostStatusForm = {
|
||||||
submitDisabled: false,
|
submitDisabled: false,
|
||||||
error: null,
|
error: null,
|
||||||
posting: false,
|
posting: false,
|
||||||
highlighted: 0,
|
|
||||||
newStatus: {
|
newStatus: {
|
||||||
spoilerText: this.subject || '',
|
spoilerText: this.subject || '',
|
||||||
status: statusText,
|
status: statusText,
|
||||||
nsfw: false,
|
nsfw: false,
|
||||||
files: [],
|
files: [],
|
||||||
visibility: scope
|
visibility: scope
|
||||||
},
|
}
|
||||||
caret: 0
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -81,59 +76,6 @@ const PostStatusForm = {
|
||||||
direct: { selected: this.newStatus.visibility === 'direct' }
|
direct: { selected: this.newStatus.visibility === 'direct' }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
candidates () {
|
|
||||||
const firstchar = this.textAtCaret.charAt(0)
|
|
||||||
if (firstchar === '@') {
|
|
||||||
const query = this.textAtCaret.slice(1).toUpperCase()
|
|
||||||
const matchedUsers = filter(this.users, (user) => {
|
|
||||||
return user.screen_name.toUpperCase().startsWith(query) ||
|
|
||||||
user.name && user.name.toUpperCase().startsWith(query)
|
|
||||||
})
|
|
||||||
if (matchedUsers.length <= 0) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line camelcase
|
|
||||||
return map(take(matchedUsers, 5), ({screen_name, name, profile_image_url_original}, index) => ({
|
|
||||||
// eslint-disable-next-line camelcase
|
|
||||||
screen_name: `@${screen_name}`,
|
|
||||||
name: name,
|
|
||||||
img: profile_image_url_original,
|
|
||||||
highlighted: index === this.highlighted
|
|
||||||
}))
|
|
||||||
} else if (firstchar === ':') {
|
|
||||||
if (this.textAtCaret === ':') { return }
|
|
||||||
const matchedEmoji = filter(this.emoji.concat(this.customEmoji), (emoji) => emoji.shortcode.startsWith(this.textAtCaret.slice(1)))
|
|
||||||
if (matchedEmoji.length <= 0) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return map(take(matchedEmoji, 5), ({shortcode, image_url, utf}, index) => ({
|
|
||||||
screen_name: `:${shortcode}:`,
|
|
||||||
name: '',
|
|
||||||
utf: utf || '',
|
|
||||||
// eslint-disable-next-line camelcase
|
|
||||||
img: utf ? '' : this.$store.state.instance.server + image_url,
|
|
||||||
highlighted: index === this.highlighted
|
|
||||||
}))
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
textAtCaret () {
|
|
||||||
return (this.wordAtCaret || {}).word || ''
|
|
||||||
},
|
|
||||||
wordAtCaret () {
|
|
||||||
const word = Completion.wordAtPosition(this.newStatus.status, this.caret - 1) || {}
|
|
||||||
return word
|
|
||||||
},
|
|
||||||
users () {
|
|
||||||
return this.$store.state.users.users
|
|
||||||
},
|
|
||||||
emoji () {
|
|
||||||
return this.$store.state.instance.emoji || []
|
|
||||||
},
|
|
||||||
customEmoji () {
|
|
||||||
return this.$store.state.instance.customEmoji || []
|
|
||||||
},
|
|
||||||
statusLength () {
|
statusLength () {
|
||||||
return this.newStatus.status.length
|
return this.newStatus.status.length
|
||||||
},
|
},
|
||||||
|
@ -174,53 +116,8 @@ const PostStatusForm = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
replace (replacement) {
|
postStatusCopy () {
|
||||||
this.newStatus.status = Completion.replaceWord(this.newStatus.status, this.wordAtCaret, replacement)
|
this.postStatus(this.newStatus)
|
||||||
const el = this.$el.querySelector('textarea')
|
|
||||||
el.focus()
|
|
||||||
this.caret = 0
|
|
||||||
},
|
|
||||||
replaceCandidate (e) {
|
|
||||||
const len = this.candidates.length || 0
|
|
||||||
if (this.textAtCaret === ':' || e.ctrlKey) { return }
|
|
||||||
if (len > 0) {
|
|
||||||
e.preventDefault()
|
|
||||||
const candidate = this.candidates[this.highlighted]
|
|
||||||
const replacement = candidate.utf || (candidate.screen_name + ' ')
|
|
||||||
this.newStatus.status = Completion.replaceWord(this.newStatus.status, this.wordAtCaret, replacement)
|
|
||||||
const el = this.$el.querySelector('textarea')
|
|
||||||
el.focus()
|
|
||||||
this.caret = 0
|
|
||||||
this.highlighted = 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
cycleBackward (e) {
|
|
||||||
const len = this.candidates.length || 0
|
|
||||||
if (len > 0) {
|
|
||||||
e.preventDefault()
|
|
||||||
this.highlighted -= 1
|
|
||||||
if (this.highlighted < 0) {
|
|
||||||
this.highlighted = this.candidates.length - 1
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.highlighted = 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
cycleForward (e) {
|
|
||||||
const len = this.candidates.length || 0
|
|
||||||
if (len > 0) {
|
|
||||||
if (e.shiftKey) { return }
|
|
||||||
e.preventDefault()
|
|
||||||
this.highlighted += 1
|
|
||||||
if (this.highlighted >= len) {
|
|
||||||
this.highlighted = 0
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.highlighted = 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setCaret ({target: {selectionStart}}) {
|
|
||||||
this.caret = selectionStart
|
|
||||||
},
|
},
|
||||||
postStatus (newStatus) {
|
postStatus (newStatus) {
|
||||||
if (this.posting) { return }
|
if (this.posting) { return }
|
||||||
|
@ -303,18 +200,6 @@ const PostStatusForm = {
|
||||||
fileDrag (e) {
|
fileDrag (e) {
|
||||||
e.dataTransfer.dropEffect = 'copy'
|
e.dataTransfer.dropEffect = 'copy'
|
||||||
},
|
},
|
||||||
resize (e) {
|
|
||||||
const target = e.target || e
|
|
||||||
if (!(target instanceof window.Element)) { return }
|
|
||||||
const vertPadding = Number(window.getComputedStyle(target)['padding-top'].substr(0, 1)) +
|
|
||||||
Number(window.getComputedStyle(target)['padding-bottom'].substr(0, 1))
|
|
||||||
// Auto is needed to make textbox shrink when removing lines
|
|
||||||
target.style.height = 'auto'
|
|
||||||
target.style.height = `${target.scrollHeight - vertPadding}px`
|
|
||||||
if (target.value === '') {
|
|
||||||
target.style.height = null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
clearError () {
|
clearError () {
|
||||||
this.error = null
|
this.error = null
|
||||||
},
|
},
|
||||||
|
|
|
@ -16,22 +16,16 @@
|
||||||
:placeholder="$t('post_status.content_warning')"
|
:placeholder="$t('post_status.content_warning')"
|
||||||
v-model="newStatus.spoilerText"
|
v-model="newStatus.spoilerText"
|
||||||
class="form-cw">
|
class="form-cw">
|
||||||
<textarea
|
<auto-complete-input v-model="newStatus.status"
|
||||||
ref="textarea"
|
:classObj="{ 'form-control': true }"
|
||||||
@click="setCaret"
|
:placeholder="$t('post_status.default')"
|
||||||
@keyup="setCaret" v-model="newStatus.status" :placeholder="$t('post_status.default')" rows="1" class="form-control"
|
:autoResize="true"
|
||||||
@keydown.down="cycleForward"
|
:multiline="true"
|
||||||
@keydown.up="cycleBackward"
|
:drop="fileDrop"
|
||||||
@keydown.shift.tab="cycleBackward"
|
:dragoverPrevent="fileDrag"
|
||||||
@keydown.tab="cycleForward"
|
:paste="paste"
|
||||||
@keydown.enter="replaceCandidate"
|
:keydownMetaEnter="postStatusCopy"
|
||||||
@keydown.meta.enter="postStatus(newStatus)"
|
:keyupCtrlEnter="postStatusCopy"/>
|
||||||
@keyup.ctrl.enter="postStatus(newStatus)"
|
|
||||||
@drop="fileDrop"
|
|
||||||
@dragover.prevent="fileDrag"
|
|
||||||
@input="resize"
|
|
||||||
@paste="paste">
|
|
||||||
</textarea>
|
|
||||||
<div class="visibility-tray">
|
<div class="visibility-tray">
|
||||||
<span class="text-format" v-if="formattingOptionsEnabled">
|
<span class="text-format" v-if="formattingOptionsEnabled">
|
||||||
<label for="post-content-type" class="select">
|
<label for="post-content-type" class="select">
|
||||||
|
@ -52,17 +46,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="position:relative;" v-if="candidates">
|
|
||||||
<div class="autocomplete-panel">
|
|
||||||
<div v-for="candidate in candidates" @click="replace(candidate.utf || (candidate.screen_name + ' '))">
|
|
||||||
<div class="autocomplete" :class="{ highlighted: candidate.highlighted }">
|
|
||||||
<span v-if="candidate.img"><img :src="candidate.img"></img></span>
|
|
||||||
<span v-else>{{candidate.utf}}</span>
|
|
||||||
<span>{{candidate.screen_name}}<small>{{candidate.name}}</small></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class='form-bottom'>
|
<div class='form-bottom'>
|
||||||
<media-upload ref="mediaUpload" @uploading="disableSubmit" @uploaded="addMediaFile" @upload-failed="uploadFailed" :drop-files="dropFiles"></media-upload>
|
<media-upload ref="mediaUpload" @uploading="disableSubmit" @uploaded="addMediaFile" @upload-failed="uploadFailed" :drop-files="dropFiles"></media-upload>
|
||||||
|
|
||||||
|
@ -250,52 +233,5 @@
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
z-index: 4;
|
z-index: 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.autocomplete-panel {
|
|
||||||
margin: 0 0.5em 0 0.5em;
|
|
||||||
border-radius: $fallback--tooltipRadius;
|
|
||||||
border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
|
|
||||||
position: absolute;
|
|
||||||
z-index: 1;
|
|
||||||
box-shadow: 1px 2px 4px rgba(0, 0, 0, 0.5);
|
|
||||||
// this doesn't match original but i don't care, making it uniform.
|
|
||||||
box-shadow: var(--popupShadow);
|
|
||||||
min-width: 75%;
|
|
||||||
background: $fallback--bg;
|
|
||||||
background: var(--bg, $fallback--bg);
|
|
||||||
color: $fallback--lightText;
|
|
||||||
color: var(--lightText, $fallback--lightText);
|
|
||||||
}
|
|
||||||
|
|
||||||
.autocomplete {
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 0.2em 0.4em 0.2em 0.4em;
|
|
||||||
border-bottom: 1px solid rgba(0, 0, 0, 0.4);
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
border-radius: $fallback--avatarRadius;
|
|
||||||
border-radius: var(--avatarRadius, $fallback--avatarRadius);
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
|
||||||
line-height: 24px;
|
|
||||||
margin: 0 0.1em 0 0.2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
small {
|
|
||||||
margin-left: .5em;
|
|
||||||
color: $fallback--faint;
|
|
||||||
color: var(--faint, $fallback--faint);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.highlighted {
|
|
||||||
background-color: $fallback--fg;
|
|
||||||
background-color: var(--lightBg, $fallback--fg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -7,7 +7,7 @@ const PublicAndExternalTimeline = {
|
||||||
timeline () { return this.$store.state.statuses.timelines.publicAndExternal }
|
timeline () { return this.$store.state.statuses.timelines.publicAndExternal }
|
||||||
},
|
},
|
||||||
created () {
|
created () {
|
||||||
this.$store.dispatch('startFetching', 'publicAndExternal')
|
this.$store.dispatch('startFetching', { timeline: 'publicAndExternal' })
|
||||||
},
|
},
|
||||||
destroyed () {
|
destroyed () {
|
||||||
this.$store.dispatch('stopFetching', 'publicAndExternal')
|
this.$store.dispatch('stopFetching', 'publicAndExternal')
|
||||||
|
|
|
@ -7,7 +7,7 @@ const PublicTimeline = {
|
||||||
timeline () { return this.$store.state.statuses.timelines.public }
|
timeline () { return this.$store.state.statuses.timelines.public }
|
||||||
},
|
},
|
||||||
created () {
|
created () {
|
||||||
this.$store.dispatch('startFetching', 'public')
|
this.$store.dispatch('startFetching', { timeline: 'public' })
|
||||||
},
|
},
|
||||||
destroyed () {
|
destroyed () {
|
||||||
this.$store.dispatch('stopFetching', 'public')
|
this.$store.dispatch('stopFetching', 'public')
|
||||||
|
|
|
@ -27,6 +27,11 @@ const settings = {
|
||||||
: user.hideUserStats,
|
: user.hideUserStats,
|
||||||
hideUserStatsDefault: this.$t('settings.values.' + instance.hideUserStats),
|
hideUserStatsDefault: this.$t('settings.values.' + instance.hideUserStats),
|
||||||
|
|
||||||
|
hideFilteredStatusesLocal: typeof user.hideFilteredStatuses === 'undefined'
|
||||||
|
? instance.hideFilteredStatuses
|
||||||
|
: user.hideFilteredStatuses,
|
||||||
|
hideFilteredStatusesDefault: this.$t('settings.values.' + instance.hideFilteredStatuses),
|
||||||
|
|
||||||
notificationVisibilityLocal: user.notificationVisibility,
|
notificationVisibilityLocal: user.notificationVisibility,
|
||||||
replyVisibilityLocal: user.replyVisibility,
|
replyVisibilityLocal: user.replyVisibility,
|
||||||
loopVideoLocal: user.loopVideo,
|
loopVideoLocal: user.loopVideo,
|
||||||
|
@ -101,6 +106,9 @@ const settings = {
|
||||||
hideUserStatsLocal (value) {
|
hideUserStatsLocal (value) {
|
||||||
this.$store.dispatch('setOption', { name: 'hideUserStats', value })
|
this.$store.dispatch('setOption', { name: 'hideUserStats', value })
|
||||||
},
|
},
|
||||||
|
hideFilteredStatusesLocal (value) {
|
||||||
|
this.$store.dispatch('setOption', { name: 'hideFilteredStatuses', value })
|
||||||
|
},
|
||||||
hideNsfwLocal (value) {
|
hideNsfwLocal (value) {
|
||||||
this.$store.dispatch('setOption', { name: 'hideNsfw', value })
|
this.$store.dispatch('setOption', { name: 'hideNsfw', value })
|
||||||
},
|
},
|
||||||
|
|
|
@ -227,7 +227,6 @@
|
||||||
</label>
|
</label>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{{$t('settings.replies_in_timeline')}}
|
{{$t('settings.replies_in_timeline')}}
|
||||||
|
@ -254,11 +253,18 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="setting-item">
|
<div class="setting-item">
|
||||||
|
<div>
|
||||||
<p>{{$t('settings.filtering_explanation')}}</p>
|
<p>{{$t('settings.filtering_explanation')}}</p>
|
||||||
<textarea id="muteWords" v-model="muteWordsString"></textarea>
|
<textarea id="muteWords" v-model="muteWordsString"></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<input type="checkbox" id="hideFilteredStatuses" v-model="hideFilteredStatusesLocal">
|
||||||
|
<label for="hideFilteredStatuses">
|
||||||
|
{{$t('settings.hide_filtered_statuses')}} {{$t('settings.instance_default', { value: hideFilteredStatusesDefault })}}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</tab-switcher>
|
</tab-switcher>
|
||||||
</keep-alive>
|
</keep-alive>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -10,8 +10,8 @@ import LinkPreview from '../link-preview/link-preview.vue'
|
||||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||||
import fileType from 'src/services/file_type/file_type.service'
|
import fileType from 'src/services/file_type/file_type.service'
|
||||||
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
|
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
|
||||||
import { mentionMatchesUrl } from 'src/services/mention_matcher/mention_matcher.js'
|
import { mentionMatchesUrl, extractTagFromUrl } from 'src/services/matcher/matcher.service.js'
|
||||||
import { filter, find } from 'lodash'
|
import { filter, find, unescape } from 'lodash'
|
||||||
|
|
||||||
const Status = {
|
const Status = {
|
||||||
name: 'Status',
|
name: 'Status',
|
||||||
|
@ -110,6 +110,14 @@ const Status = {
|
||||||
return hits
|
return hits
|
||||||
},
|
},
|
||||||
muted () { return !this.unmuted && (this.status.user.muted || this.muteWordHits.length > 0) },
|
muted () { return !this.unmuted && (this.status.user.muted || this.muteWordHits.length > 0) },
|
||||||
|
hideFilteredStatuses () {
|
||||||
|
return typeof this.$store.state.config.hideFilteredStatuses === 'undefined'
|
||||||
|
? this.$store.state.instance.hideFilteredStatuses
|
||||||
|
: this.$store.state.config.hideFilteredStatuses
|
||||||
|
},
|
||||||
|
hideStatus () {
|
||||||
|
return (this.hideReply || this.deleted) || (this.muted && this.hideFilteredStatuses)
|
||||||
|
},
|
||||||
isFocused () {
|
isFocused () {
|
||||||
// retweet or root of an expanded conversation
|
// retweet or root of an expanded conversation
|
||||||
if (this.focused) {
|
if (this.focused) {
|
||||||
|
@ -201,14 +209,15 @@ const Status = {
|
||||||
},
|
},
|
||||||
replySubject () {
|
replySubject () {
|
||||||
if (!this.status.summary) return ''
|
if (!this.status.summary) return ''
|
||||||
|
const decodedSummary = unescape(this.status.summary)
|
||||||
const behavior = typeof this.$store.state.config.subjectLineBehavior === 'undefined'
|
const behavior = typeof this.$store.state.config.subjectLineBehavior === 'undefined'
|
||||||
? this.$store.state.instance.subjectLineBehavior
|
? this.$store.state.instance.subjectLineBehavior
|
||||||
: this.$store.state.config.subjectLineBehavior
|
: this.$store.state.config.subjectLineBehavior
|
||||||
const startsWithRe = this.status.summary.match(/^re[: ]/i)
|
const startsWithRe = decodedSummary.match(/^re[: ]/i)
|
||||||
if (behavior !== 'noop' && startsWithRe || behavior === 'masto') {
|
if (behavior !== 'noop' && startsWithRe || behavior === 'masto') {
|
||||||
return this.status.summary
|
return decodedSummary
|
||||||
} else if (behavior === 'email') {
|
} else if (behavior === 'email') {
|
||||||
return 're: '.concat(this.status.summary)
|
return 're: '.concat(decodedSummary)
|
||||||
} else if (behavior === 'noop') {
|
} else if (behavior === 'noop') {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
@ -273,7 +282,7 @@ const Status = {
|
||||||
}
|
}
|
||||||
if (target.tagName === 'A') {
|
if (target.tagName === 'A') {
|
||||||
if (target.className.match(/mention/)) {
|
if (target.className.match(/mention/)) {
|
||||||
const href = target.getAttribute('href')
|
const href = target.href
|
||||||
const attn = this.status.attentions.find(attn => mentionMatchesUrl(attn, href))
|
const attn = this.status.attentions.find(attn => mentionMatchesUrl(attn, href))
|
||||||
if (attn) {
|
if (attn) {
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
|
@ -283,6 +292,15 @@ const Status = {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (target.className.match(/hashtag/)) {
|
||||||
|
// Extract tag name from link url
|
||||||
|
const tag = extractTagFromUrl(target.href)
|
||||||
|
if (tag) {
|
||||||
|
const link = this.generateTagLink(tag)
|
||||||
|
this.$router.push(link)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
window.open(target.href, '_blank')
|
window.open(target.href, '_blank')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -339,6 +357,9 @@ const Status = {
|
||||||
generateUserProfileLink (id, name) {
|
generateUserProfileLink (id, name) {
|
||||||
return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames)
|
return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames)
|
||||||
},
|
},
|
||||||
|
generateTagLink (tag) {
|
||||||
|
return `/tag/${tag}`
|
||||||
|
},
|
||||||
setMedia () {
|
setMedia () {
|
||||||
const attachments = this.attachmentSize === 'hide' ? this.status.attachments : this.galleryAttachments
|
const attachments = this.attachmentSize === 'hide' ? this.status.attachments : this.galleryAttachments
|
||||||
return () => this.$store.dispatch('setMedia', attachments)
|
return () => this.$store.dispatch('setMedia', attachments)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="status-el" v-if="!hideReply && !deleted" :class="[{ 'status-el_focused': isFocused }, { 'status-conversation': inlineExpanded }]">
|
<div class="status-el" v-if="!hideStatus" :class="[{ 'status-el_focused': isFocused }, { 'status-conversation': inlineExpanded }]">
|
||||||
<template v-if="muted && !noReplyLinks">
|
<template v-if="muted && !noReplyLinks">
|
||||||
<div class="media status container muted">
|
<div class="media status container muted">
|
||||||
<small>
|
<small>
|
||||||
|
@ -56,7 +56,7 @@
|
||||||
</div>
|
</div>
|
||||||
<h4 class="replies" v-if="inConversation && !noReplyLinks">
|
<h4 class="replies" v-if="inConversation && !noReplyLinks">
|
||||||
<small v-if="replies.length">Replies:</small>
|
<small v-if="replies.length">Replies:</small>
|
||||||
<small class="reply-link" v-for="reply in replies">
|
<small class="reply-link" v-bind:key="reply.id" v-for="reply in replies">
|
||||||
<a href="#" @click.prevent="gotoOriginal(reply.id)" @mouseenter="replyEnter(reply.id, $event)" @mouseout="replyLeave()">{{reply.name}} </a>
|
<a href="#" @click.prevent="gotoOriginal(reply.id)" @mouseenter="replyEnter(reply.id, $event)" @mouseout="replyLeave()">{{reply.name}} </a>
|
||||||
</small>
|
</small>
|
||||||
</h4>
|
</h4>
|
||||||
|
@ -438,6 +438,8 @@
|
||||||
|
|
||||||
.user-name {
|
.user-name {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
width: 14px;
|
width: 14px;
|
||||||
|
|
|
@ -37,7 +37,7 @@ export default Vue.component('tab-switcher', {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={ classesWrapper.join(' ')}>
|
<div class={ classesWrapper.join(' ')}>
|
||||||
<button onClick={this.activateTab(index)} class={ classesTab.join(' ') }>{slot.data.attrs.label}</button>
|
<button disabled={slot.data.attrs.disabled} onClick={this.activateTab(index)} class={ classesTab.join(' ') }>{slot.data.attrs.label}</button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
@ -3,7 +3,7 @@ import Timeline from '../timeline/timeline.vue'
|
||||||
const TagTimeline = {
|
const TagTimeline = {
|
||||||
created () {
|
created () {
|
||||||
this.$store.commit('clearTimeline', { timeline: 'tag' })
|
this.$store.commit('clearTimeline', { timeline: 'tag' })
|
||||||
this.$store.dispatch('startFetching', { 'tag': this.tag })
|
this.$store.dispatch('startFetching', { timeline: 'tag', tag: this.tag })
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
Timeline
|
Timeline
|
||||||
|
@ -15,7 +15,7 @@ const TagTimeline = {
|
||||||
watch: {
|
watch: {
|
||||||
tag () {
|
tag () {
|
||||||
this.$store.commit('clearTimeline', { timeline: 'tag' })
|
this.$store.commit('clearTimeline', { timeline: 'tag' })
|
||||||
this.$store.dispatch('startFetching', { 'tag': this.tag })
|
this.$store.dispatch('startFetching', { timeline: 'tag', tag: this.tag })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
destroyed () {
|
destroyed () {
|
||||||
|
|
|
@ -1,16 +1,21 @@
|
||||||
import UserCardContent from '../user_card_content/user_card_content.vue'
|
import UserCardContent from '../user_card_content/user_card_content.vue'
|
||||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||||
|
import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate'
|
||||||
|
|
||||||
const UserCard = {
|
const UserCard = {
|
||||||
props: [
|
props: [
|
||||||
'user',
|
'user',
|
||||||
'showFollows',
|
'showFollows',
|
||||||
'showApproval'
|
'showApproval',
|
||||||
|
'showActions'
|
||||||
],
|
],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
userExpanded: false
|
userExpanded: false,
|
||||||
|
followRequestInProgress: false,
|
||||||
|
followRequestSent: false,
|
||||||
|
updated: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
@ -18,7 +23,11 @@ const UserCard = {
|
||||||
UserAvatar
|
UserAvatar
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
currentUser () { return this.$store.state.users.currentUser }
|
currentUser () { return this.$store.state.users.currentUser },
|
||||||
|
following () { return this.updated ? this.updated.following : this.user.following },
|
||||||
|
showFollow () {
|
||||||
|
return this.showActions && (!this.showFollows && !this.following || this.updated && !this.updated.following)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
toggleUserExpanded () {
|
toggleUserExpanded () {
|
||||||
|
@ -34,6 +43,21 @@ const UserCard = {
|
||||||
},
|
},
|
||||||
userProfileLink (user) {
|
userProfileLink (user) {
|
||||||
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
|
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
|
||||||
|
},
|
||||||
|
followUser () {
|
||||||
|
this.followRequestInProgress = true
|
||||||
|
requestFollow(this.user, this.$store).then(({ sent, updated }) => {
|
||||||
|
this.followRequestInProgress = false
|
||||||
|
this.followRequestSent = sent
|
||||||
|
this.updated = updated
|
||||||
|
})
|
||||||
|
},
|
||||||
|
unfollowUser () {
|
||||||
|
this.followRequestInProgress = true
|
||||||
|
requestUnfollow(this.user, this.$store).then(({ updated }) => {
|
||||||
|
this.followRequestInProgress = false
|
||||||
|
this.updated = updated
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,22 +7,43 @@
|
||||||
<user-card-content :user="user" :switcher="false"></user-card-content>
|
<user-card-content :user="user" :switcher="false"></user-card-content>
|
||||||
</div>
|
</div>
|
||||||
<div class="name-and-screen-name" v-else>
|
<div class="name-and-screen-name" v-else>
|
||||||
<div :title="user.name" v-if="user.name_html" class="user-name">
|
<div :title="user.name" class="user-name">
|
||||||
<span v-html="user.name_html"></span>
|
<span v-if="user.name_html" v-html="user.name_html"></span>
|
||||||
|
<span v-else>{{ user.name }}</span>
|
||||||
<span class="follows-you" v-if="!userExpanded && showFollows && user.follows_you">
|
<span class="follows-you" v-if="!userExpanded && showFollows && user.follows_you">
|
||||||
{{ currentUser.id == user.id ? $t('user_card.its_you') : $t('user_card.follows_you') }}
|
{{ currentUser.id == user.id ? $t('user_card.its_you') : $t('user_card.follows_you') }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div :title="user.name" v-else class="user-name">
|
<div class="user-link-action">
|
||||||
{{ user.name }}
|
|
||||||
<span class="follows-you" v-if="!userExpanded && showFollows && user.follows_you">
|
|
||||||
{{ currentUser.id == user.id ? $t('user_card.its_you') : $t('user_card.follows_you') }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<router-link class='user-screen-name' :to="userProfileLink(user)">
|
<router-link class='user-screen-name' :to="userProfileLink(user)">
|
||||||
@{{user.screen_name}}
|
@{{user.screen_name}}
|
||||||
</router-link>
|
</router-link>
|
||||||
|
<button
|
||||||
|
v-if="showFollow"
|
||||||
|
class="btn btn-default"
|
||||||
|
@click="followUser"
|
||||||
|
:disabled="followRequestInProgress"
|
||||||
|
:title="followRequestSent ? $t('user_card.follow_again') : ''"
|
||||||
|
>
|
||||||
|
<template v-if="followRequestInProgress">
|
||||||
|
{{ $t('user_card.follow_progress') }}
|
||||||
|
</template>
|
||||||
|
<template v-else-if="followRequestSent">
|
||||||
|
{{ $t('user_card.follow_sent') }}
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
{{ $t('user_card.follow') }}
|
||||||
|
</template>
|
||||||
|
</button>
|
||||||
|
<button v-if="showActions && showFollows && following" class="btn btn-default" @click="unfollowUser" :disabled="followRequestInProgress">
|
||||||
|
<template v-if="followRequestInProgress">
|
||||||
|
{{ $t('user_card.follow_progress') }}
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
{{ $t('user_card.follow_unfollow') }}
|
||||||
|
</template>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="approval" v-if="showApproval">
|
<div class="approval" v-if="showApproval">
|
||||||
<button class="btn btn-default" @click="approveUser">{{ $t('user_card.approve') }}</button>
|
<button class="btn btn-default" @click="approveUser">{{ $t('user_card.approve') }}</button>
|
||||||
|
@ -42,6 +63,9 @@
|
||||||
text-align: left;
|
text-align: left;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
.user-name {
|
.user-name {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
|
@ -49,11 +73,20 @@
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.user-link-action {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin-top: 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.follows-you {
|
.follows-you {
|
||||||
margin-left: 2em;
|
margin-left: 2em;
|
||||||
float: right;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||||
import { hex2rgb } from '../../services/color_convert/color_convert.js'
|
import { hex2rgb } from '../../services/color_convert/color_convert.js'
|
||||||
|
import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate'
|
||||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -92,67 +93,15 @@ export default {
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
followUser () {
|
followUser () {
|
||||||
const store = this.$store
|
|
||||||
this.followRequestInProgress = true
|
this.followRequestInProgress = true
|
||||||
store.state.api.backendInteractor.followUser(this.user.id)
|
requestFollow(this.user, this.$store).then(({sent}) => {
|
||||||
.then((followedUser) => store.commit('addNewUsers', [followedUser]))
|
|
||||||
.then(() => {
|
|
||||||
// For locked users we just mark it that we sent the follow request
|
|
||||||
if (this.user.locked) {
|
|
||||||
this.followRequestInProgress = false
|
this.followRequestInProgress = false
|
||||||
this.followRequestSent = true
|
this.followRequestSent = sent
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.user.following) {
|
|
||||||
// If we get result immediately, just stop.
|
|
||||||
this.followRequestInProgress = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// But usually we don't get result immediately, so we ask server
|
|
||||||
// for updated user profile to confirm if we are following them
|
|
||||||
// Sometimes it takes several tries. Sometimes we end up not following
|
|
||||||
// user anyway, probably because they locked themselves and we
|
|
||||||
// don't know that yet.
|
|
||||||
// Recursive Promise, it will call itself up to 3 times.
|
|
||||||
const fetchUser = (attempt) => new Promise((resolve, reject) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
store.state.api.backendInteractor.fetchUser({ id: this.user.id })
|
|
||||||
.then((user) => store.commit('addNewUsers', [user]))
|
|
||||||
.then(() => resolve([this.user.following, attempt]))
|
|
||||||
.catch((e) => reject(e))
|
|
||||||
}, 500)
|
|
||||||
}).then(([following, attempt]) => {
|
|
||||||
if (!following && attempt <= 3) {
|
|
||||||
// If we BE reports that we still not following that user - retry,
|
|
||||||
// increment attempts by one
|
|
||||||
return fetchUser(++attempt)
|
|
||||||
} else {
|
|
||||||
// If we run out of attempts, just return whatever status is.
|
|
||||||
return following
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return fetchUser(1)
|
|
||||||
.then((following) => {
|
|
||||||
if (following) {
|
|
||||||
// We confirmed and everything its good.
|
|
||||||
this.followRequestInProgress = false
|
|
||||||
} else {
|
|
||||||
// If after all the tries, just treat it as if user is locked
|
|
||||||
this.followRequestInProgress = false
|
|
||||||
this.followRequestSent = true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
unfollowUser () {
|
unfollowUser () {
|
||||||
const store = this.$store
|
|
||||||
this.followRequestInProgress = true
|
this.followRequestInProgress = true
|
||||||
store.state.api.backendInteractor.unfollowUser(this.user.id)
|
requestUnfollow(this.user, this.$store).then(() => {
|
||||||
.then((unfollowedUser) => store.commit('addNewUsers', [unfollowedUser]))
|
|
||||||
.then(() => {
|
|
||||||
this.followRequestInProgress = false
|
this.followRequestInProgress = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
|
@ -8,8 +8,8 @@ const UserProfile = {
|
||||||
this.$store.commit('clearTimeline', { timeline: 'user' })
|
this.$store.commit('clearTimeline', { timeline: 'user' })
|
||||||
this.$store.commit('clearTimeline', { timeline: 'favorites' })
|
this.$store.commit('clearTimeline', { timeline: 'favorites' })
|
||||||
this.$store.commit('clearTimeline', { timeline: 'media' })
|
this.$store.commit('clearTimeline', { timeline: 'media' })
|
||||||
this.$store.dispatch('startFetching', ['user', this.fetchBy])
|
this.$store.dispatch('startFetching', { timeline: 'user', userId: this.fetchBy })
|
||||||
this.$store.dispatch('startFetching', ['media', this.fetchBy])
|
this.$store.dispatch('startFetching', { timeline: 'media', userId: this.fetchBy })
|
||||||
this.startFetchFavorites()
|
this.startFetchFavorites()
|
||||||
if (!this.user.id) {
|
if (!this.user.id) {
|
||||||
this.$store.dispatch('fetchUser', this.fetchBy)
|
this.$store.dispatch('fetchUser', this.fetchBy)
|
||||||
|
@ -69,12 +69,12 @@ const UserProfile = {
|
||||||
methods: {
|
methods: {
|
||||||
startFetchFavorites () {
|
startFetchFavorites () {
|
||||||
if (this.isUs) {
|
if (this.isUs) {
|
||||||
this.$store.dispatch('startFetching', ['favorites', this.fetchBy])
|
this.$store.dispatch('startFetching', { timeline: 'favorites', userId: this.fetchBy })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
startUp () {
|
startUp () {
|
||||||
this.$store.dispatch('startFetching', ['user', this.fetchBy])
|
this.$store.dispatch('startFetching', { timeline: 'user', userId: this.fetchBy })
|
||||||
this.$store.dispatch('startFetching', ['media', this.fetchBy])
|
this.$store.dispatch('startFetching', { timeline: 'media', userId: this.fetchBy })
|
||||||
|
|
||||||
this.startFetchFavorites()
|
this.startFetchFavorites()
|
||||||
},
|
},
|
||||||
|
|
|
@ -9,19 +9,20 @@
|
||||||
<tab-switcher :renderOnlyFocused="true">
|
<tab-switcher :renderOnlyFocused="true">
|
||||||
<Timeline
|
<Timeline
|
||||||
:label="$t('user_card.statuses')"
|
:label="$t('user_card.statuses')"
|
||||||
|
:disabled="!user.statuses_count"
|
||||||
:embedded="true"
|
:embedded="true"
|
||||||
:title="$t('user_profile.timeline_title')"
|
:title="$t('user_profile.timeline_title')"
|
||||||
:timeline="timeline"
|
:timeline="timeline"
|
||||||
:timeline-name="'user'"
|
:timeline-name="'user'"
|
||||||
:user-id="fetchBy"
|
:user-id="fetchBy"
|
||||||
/>
|
/>
|
||||||
<div :label="$t('user_card.followees')" v-if="followsTabVisible">
|
<div :label="$t('user_card.followees')" v-if="followsTabVisible" :disabled="!user.friends_count">
|
||||||
<FollowList v-if="user.friends_count > 0" :userId="userId" :showFollowers="false" />
|
<FollowList v-if="user.friends_count > 0" :userId="userId" :showFollowers="false" />
|
||||||
<div class="userlist-placeholder" v-else>
|
<div class="userlist-placeholder" v-else>
|
||||||
<i class="icon-spin3 animate-spin"></i>
|
<i class="icon-spin3 animate-spin"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div :label="$t('user_card.followers')" v-if="followersTabVisible">
|
<div :label="$t('user_card.followers')" v-if="followersTabVisible" :disabled="!user.followers_count">
|
||||||
<FollowList v-if="user.followers_count > 0" :userId="userId" :showFollowers="true" />
|
<FollowList v-if="user.followers_count > 0" :userId="userId" :showFollowers="true" />
|
||||||
<div class="userlist-placeholder" v-else>
|
<div class="userlist-placeholder" v-else>
|
||||||
<i class="icon-spin3 animate-spin"></i>
|
<i class="icon-spin3 animate-spin"></i>
|
||||||
|
@ -29,6 +30,7 @@
|
||||||
</div>
|
</div>
|
||||||
<Timeline
|
<Timeline
|
||||||
:label="$t('user_card.media')"
|
:label="$t('user_card.media')"
|
||||||
|
:disabled="!media.visibleStatuses.length"
|
||||||
:embedded="true" :title="$t('user_card.media')"
|
:embedded="true" :title="$t('user_card.media')"
|
||||||
timeline-name="media"
|
timeline-name="media"
|
||||||
:timeline="media"
|
:timeline="media"
|
||||||
|
@ -37,6 +39,7 @@
|
||||||
<Timeline
|
<Timeline
|
||||||
v-if="isUs"
|
v-if="isUs"
|
||||||
:label="$t('user_card.favorites')"
|
:label="$t('user_card.favorites')"
|
||||||
|
:disabled="!favorites.visibleStatuses.length"
|
||||||
:embedded="true"
|
:embedded="true"
|
||||||
:title="$t('user_card.favorites')"
|
:title="$t('user_card.favorites')"
|
||||||
timeline-name="favorites"
|
timeline-name="favorites"
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { unescape } from 'lodash'
|
||||||
|
|
||||||
import TabSwitcher from '../tab_switcher/tab_switcher.js'
|
import TabSwitcher from '../tab_switcher/tab_switcher.js'
|
||||||
import StyleSwitcher from '../style_switcher/style_switcher.vue'
|
import StyleSwitcher from '../style_switcher/style_switcher.vue'
|
||||||
|
import AutoCompleteInput from '../autocomplete_input/autocomplete_input.vue'
|
||||||
import fileSizeFormatService from '../../services/file_size_format/file_size_format.js'
|
import fileSizeFormatService from '../../services/file_size_format/file_size_format.js'
|
||||||
|
|
||||||
const UserSettings = {
|
const UserSettings = {
|
||||||
|
@ -41,7 +42,8 @@ const UserSettings = {
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
StyleSwitcher,
|
StyleSwitcher,
|
||||||
TabSwitcher
|
TabSwitcher,
|
||||||
|
AutoCompleteInput
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
user () {
|
user () {
|
||||||
|
|
|
@ -9,9 +9,9 @@
|
||||||
<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>
|
||||||
<input class='name-changer' id='username' v-model="newName"></input>
|
<auto-complete-input :classObj="{ 'name-changer': true }" :id="'username'" v-model="newName"/>
|
||||||
<p>{{$t('settings.bio')}}</p>
|
<p>{{$t('settings.bio')}}</p>
|
||||||
<textarea class="bio" v-model="newBio"></textarea>
|
<auto-complete-input :classObj="{ bio: true }" v-model="newBio" :multiline="true"/>
|
||||||
<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>
|
||||||
|
@ -178,7 +178,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.banner {
|
.banner {
|
||||||
max-width: 400px;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.uploading {
|
.uploading {
|
||||||
|
@ -189,5 +189,9 @@
|
||||||
.name-changer {
|
.name-changer {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bg {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -132,6 +132,7 @@
|
||||||
"preload_images": "Bilder vorausladen",
|
"preload_images": "Bilder vorausladen",
|
||||||
"hide_post_stats": "Beitragsstatistiken verbergen (z.B. die Anzahl der Favoriten)",
|
"hide_post_stats": "Beitragsstatistiken verbergen (z.B. die Anzahl der Favoriten)",
|
||||||
"hide_user_stats": "Benutzerstatistiken verbergen (z.B. die Anzahl der Follower)",
|
"hide_user_stats": "Benutzerstatistiken verbergen (z.B. die Anzahl der Follower)",
|
||||||
|
"hide_filtered_statuses": "Gefilterte Beiträge verbergen",
|
||||||
"import_followers_from_a_csv_file": "Importiere Follower, denen du folgen möchtest, aus einer CSV-Datei",
|
"import_followers_from_a_csv_file": "Importiere Follower, denen du folgen möchtest, aus einer CSV-Datei",
|
||||||
"import_theme": "Farbschema laden",
|
"import_theme": "Farbschema laden",
|
||||||
"inputRadius": "Eingabefelder",
|
"inputRadius": "Eingabefelder",
|
||||||
|
|
|
@ -140,6 +140,7 @@
|
||||||
"use_one_click_nsfw": "Open NSFW attachments with just one click",
|
"use_one_click_nsfw": "Open NSFW attachments with just one click",
|
||||||
"hide_post_stats": "Hide post statistics (e.g. the number of favorites)",
|
"hide_post_stats": "Hide post statistics (e.g. the number of favorites)",
|
||||||
"hide_user_stats": "Hide user statistics (e.g. the number of followers)",
|
"hide_user_stats": "Hide user statistics (e.g. the number of followers)",
|
||||||
|
"hide_filtered_statuses": "Hide filtered statuses",
|
||||||
"import_followers_from_a_csv_file": "Import follows from a csv file",
|
"import_followers_from_a_csv_file": "Import follows from a csv file",
|
||||||
"import_theme": "Load preset",
|
"import_theme": "Load preset",
|
||||||
"inputRadius": "Input fields",
|
"inputRadius": "Input fields",
|
||||||
|
|
|
@ -28,7 +28,8 @@
|
||||||
"password": "Contraseña",
|
"password": "Contraseña",
|
||||||
"placeholder": "p.ej. lain",
|
"placeholder": "p.ej. lain",
|
||||||
"register": "Registrar",
|
"register": "Registrar",
|
||||||
"username": "Usuario"
|
"username": "Usuario",
|
||||||
|
"hint": "Inicia sesión para unirte a la discusión"
|
||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"about": "Sobre",
|
"about": "Sobre",
|
||||||
|
@ -55,7 +56,7 @@
|
||||||
"no_more_notifications": "No hay más notificaciones"
|
"no_more_notifications": "No hay más notificaciones"
|
||||||
},
|
},
|
||||||
"post_status": {
|
"post_status": {
|
||||||
"new_status": "Post new status",
|
"new_status": "Publicar un nuevo estado",
|
||||||
"account_not_locked_warning": "Tu cuenta no está {0}. Cualquiera puede seguirte y leer las entradas para Solo-Seguidores.",
|
"account_not_locked_warning": "Tu cuenta no está {0}. Cualquiera puede seguirte y leer las entradas para Solo-Seguidores.",
|
||||||
"account_not_locked_warning_link": "bloqueada",
|
"account_not_locked_warning_link": "bloqueada",
|
||||||
"attachments_sensitive": "Contenido sensible",
|
"attachments_sensitive": "Contenido sensible",
|
||||||
|
@ -139,6 +140,7 @@
|
||||||
"use_one_click_nsfw": "Abrir los adjuntos NSFW con un solo click.",
|
"use_one_click_nsfw": "Abrir los adjuntos NSFW con un solo click.",
|
||||||
"hide_post_stats": "Ocultar las estadísticas de las entradas (p.ej. el número de favoritos)",
|
"hide_post_stats": "Ocultar las estadísticas de las entradas (p.ej. el número de favoritos)",
|
||||||
"hide_user_stats": "Ocultar las estadísticas del usuario (p.ej. el número de seguidores)",
|
"hide_user_stats": "Ocultar las estadísticas del usuario (p.ej. el número de seguidores)",
|
||||||
|
"hide_filtered_statuses": "Ocultar estados filtrados",
|
||||||
"import_followers_from_a_csv_file": "Importar personas que tú sigues a partir de un archivo csv",
|
"import_followers_from_a_csv_file": "Importar personas que tú sigues a partir de un archivo csv",
|
||||||
"import_theme": "Importar tema",
|
"import_theme": "Importar tema",
|
||||||
"inputRadius": "Campos de entrada",
|
"inputRadius": "Campos de entrada",
|
||||||
|
@ -164,7 +166,10 @@
|
||||||
"notification_visibility_mentions": "Menciones",
|
"notification_visibility_mentions": "Menciones",
|
||||||
"notification_visibility_repeats": "Repeticiones (Repeats)",
|
"notification_visibility_repeats": "Repeticiones (Repeats)",
|
||||||
"no_rich_text_description": "Eliminar el formato de texto enriquecido de todas las entradas",
|
"no_rich_text_description": "Eliminar el formato de texto enriquecido de todas las entradas",
|
||||||
"hide_network_description": "No mostrar a quién sigo, ni quién me sigue",
|
"hide_follows_description": "No mostrar a quién sigo",
|
||||||
|
"hide_followers_description": "No mostrar quién me sigue",
|
||||||
|
"show_admin_badge": "Mostrar la placa de administrador en mi perfil",
|
||||||
|
"show_moderator_badge": "Mostrar la placa de moderador en mi perfil",
|
||||||
"nsfw_clickthrough": "Activar el clic para ocultar los adjuntos NSFW",
|
"nsfw_clickthrough": "Activar el clic para ocultar los adjuntos NSFW",
|
||||||
"panelRadius": "Paneles",
|
"panelRadius": "Paneles",
|
||||||
"pause_on_unfocused": "Parar la transmisión cuando no estés en foco.",
|
"pause_on_unfocused": "Parar la transmisión cuando no estés en foco.",
|
||||||
|
@ -191,6 +196,8 @@
|
||||||
"subject_line_email": "Tipo email: \"re: tema\"",
|
"subject_line_email": "Tipo email: \"re: tema\"",
|
||||||
"subject_line_mastodon": "Tipo mastodon: copiar como es",
|
"subject_line_mastodon": "Tipo mastodon: copiar como es",
|
||||||
"subject_line_noop": "No copiar",
|
"subject_line_noop": "No copiar",
|
||||||
|
"post_status_content_type": "Formato de publicación",
|
||||||
|
"status_content_type_plain": "Texto plano",
|
||||||
"stop_gifs": "Iniciar GIFs al pasar el ratón",
|
"stop_gifs": "Iniciar GIFs al pasar el ratón",
|
||||||
"streaming": "Habilite la transmisión automática de nuevas publicaciones cuando se desplaza hacia la parte superior",
|
"streaming": "Habilite la transmisión automática de nuevas publicaciones cuando se desplaza hacia la parte superior",
|
||||||
"text": "Texto",
|
"text": "Texto",
|
||||||
|
|
|
@ -48,7 +48,7 @@ export default function createPersistedState ({
|
||||||
return getState(key, storage).then((savedState) => {
|
return getState(key, storage).then((savedState) => {
|
||||||
return store => {
|
return store => {
|
||||||
try {
|
try {
|
||||||
if (typeof savedState === 'object') {
|
if (savedState !== null && typeof savedState === 'object') {
|
||||||
// build user cache
|
// build user cache
|
||||||
const usersState = savedState.users || {}
|
const usersState = savedState.users || {}
|
||||||
usersState.usersObject = {}
|
usersState.usersObject = {}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
|
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
|
||||||
import {isArray} from 'lodash'
|
|
||||||
import { Socket } from 'phoenix'
|
import { Socket } from 'phoenix'
|
||||||
|
|
||||||
const api = {
|
const api = {
|
||||||
|
@ -34,20 +33,12 @@ const api = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
startFetching (store, timeline) {
|
startFetching (store, {timeline = 'friends', tag = false, userId = false}) {
|
||||||
let userId = false
|
|
||||||
|
|
||||||
// This is for user timelines
|
|
||||||
if (isArray(timeline)) {
|
|
||||||
userId = timeline[1]
|
|
||||||
timeline = timeline[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't start fetching if we already are.
|
// Don't start fetching if we already are.
|
||||||
if (!store.state.fetchers[timeline]) {
|
if (store.state.fetchers[timeline]) return
|
||||||
const fetcher = store.state.backendInteractor.startFetching({timeline, store, userId})
|
|
||||||
|
const fetcher = store.state.backendInteractor.startFetching({ timeline, store, userId, tag })
|
||||||
store.commit('addFetcher', { timeline, fetcher })
|
store.commit('addFetcher', { timeline, fetcher })
|
||||||
}
|
|
||||||
},
|
},
|
||||||
stopFetching (store, timeline) {
|
stopFetching (store, timeline) {
|
||||||
const fetcher = store.state.fetchers[timeline]
|
const fetcher = store.state.fetchers[timeline]
|
||||||
|
|
|
@ -31,7 +31,6 @@ const defaultState = {
|
||||||
scopeCopy: undefined, // instance default
|
scopeCopy: undefined, // instance default
|
||||||
subjectLineBehavior: undefined, // instance default
|
subjectLineBehavior: undefined, // instance default
|
||||||
alwaysShowSubjectInput: undefined, // instance default
|
alwaysShowSubjectInput: undefined, // instance default
|
||||||
showFeaturesPanel: true,
|
|
||||||
postContentType: undefined // instance default
|
postContentType: undefined // instance default
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,7 @@ const defaultState = {
|
||||||
collapseMessageWithSubject: false,
|
collapseMessageWithSubject: false,
|
||||||
hidePostStats: false,
|
hidePostStats: false,
|
||||||
hideUserStats: false,
|
hideUserStats: false,
|
||||||
|
hideFilteredStatuses: true,
|
||||||
disableChat: false,
|
disableChat: false,
|
||||||
scopeCopy: true,
|
scopeCopy: true,
|
||||||
subjectLineBehavior: 'email',
|
subjectLineBehavior: 'email',
|
||||||
|
@ -29,6 +30,7 @@ const defaultState = {
|
||||||
nsfwCensorImage: undefined,
|
nsfwCensorImage: undefined,
|
||||||
vapidPublicKey: undefined,
|
vapidPublicKey: undefined,
|
||||||
noAttachmentLinks: false,
|
noAttachmentLinks: false,
|
||||||
|
showFeaturesPanel: true,
|
||||||
|
|
||||||
// Nasty stuff
|
// Nasty stuff
|
||||||
pleromaBackend: true,
|
pleromaBackend: true,
|
||||||
|
@ -64,9 +66,11 @@ const instance = {
|
||||||
case 'name':
|
case 'name':
|
||||||
dispatch('setPageTitle')
|
dispatch('setPageTitle')
|
||||||
break
|
break
|
||||||
case 'theme':
|
|
||||||
setPreset(value, commit)
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
setTheme ({ commit }, themeName) {
|
||||||
|
commit('setInstanceOption', { name: 'theme', value: themeName })
|
||||||
|
return setPreset(themeName, commit)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -271,7 +271,7 @@ const users = {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start getting fresh posts.
|
// Start getting fresh posts.
|
||||||
store.dispatch('startFetching', 'friends')
|
store.dispatch('startFetching', { timeline: 'friends' })
|
||||||
|
|
||||||
// Get user mutes and follower info
|
// Get user mutes and follower info
|
||||||
store.rootState.api.backendInteractor.fetchMutes().then((mutedUsers) => {
|
store.rootState.api.backendInteractor.fetchMutes().then((mutedUsers) => {
|
||||||
|
|
74
src/services/follow_manipulate/follow_manipulate.js
Normal file
74
src/services/follow_manipulate/follow_manipulate.js
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
const fetchUser = (attempt, user, store) => new Promise((resolve, reject) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
store.state.api.backendInteractor.fetchUser({ id: user.id })
|
||||||
|
.then((user) => store.commit('addNewUsers', [user]))
|
||||||
|
.then(() => resolve([user.following, attempt]))
|
||||||
|
.catch((e) => reject(e))
|
||||||
|
}, 500)
|
||||||
|
}).then(([following, attempt]) => {
|
||||||
|
if (!following && attempt <= 3) {
|
||||||
|
// If we BE reports that we still not following that user - retry,
|
||||||
|
// increment attempts by one
|
||||||
|
return fetchUser(++attempt, user, store)
|
||||||
|
} else {
|
||||||
|
// If we run out of attempts, just return whatever status is.
|
||||||
|
return following
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export const requestFollow = (user, store) => new Promise((resolve, reject) => {
|
||||||
|
store.state.api.backendInteractor.followUser(user.id)
|
||||||
|
.then((updated) => {
|
||||||
|
store.commit('addNewUsers', [updated])
|
||||||
|
|
||||||
|
// For locked users we just mark it that we sent the follow request
|
||||||
|
if (updated.locked) {
|
||||||
|
resolve({
|
||||||
|
sent: true,
|
||||||
|
updated
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updated.following) {
|
||||||
|
// If we get result immediately, just stop.
|
||||||
|
resolve({
|
||||||
|
sent: false,
|
||||||
|
updated
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// But usually we don't get result immediately, so we ask server
|
||||||
|
// for updated user profile to confirm if we are following them
|
||||||
|
// Sometimes it takes several tries. Sometimes we end up not following
|
||||||
|
// user anyway, probably because they locked themselves and we
|
||||||
|
// don't know that yet.
|
||||||
|
// Recursive Promise, it will call itself up to 3 times.
|
||||||
|
|
||||||
|
return fetchUser(1, user, store)
|
||||||
|
.then((following) => {
|
||||||
|
if (following) {
|
||||||
|
// We confirmed and everything's good.
|
||||||
|
resolve({
|
||||||
|
sent: false,
|
||||||
|
updated
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// If after all the tries, just treat it as if user is locked
|
||||||
|
resolve({
|
||||||
|
sent: false,
|
||||||
|
updated
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
export const requestUnfollow = (user, store) => new Promise((resolve, reject) => {
|
||||||
|
store.state.api.backendInteractor.unfollowUser(user.id)
|
||||||
|
.then((updated) => {
|
||||||
|
store.commit('addNewUsers', [updated])
|
||||||
|
resolve({
|
||||||
|
updated
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
23
src/services/matcher/matcher.service.js
Normal file
23
src/services/matcher/matcher.service.js
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
export const mentionMatchesUrl = (attention, url) => {
|
||||||
|
if (url === attention.statusnet_profile_url) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
const [namepart, instancepart] = attention.screen_name.split('@')
|
||||||
|
const matchstring = new RegExp('://' + instancepart + '/.*' + namepart + '$', 'g')
|
||||||
|
|
||||||
|
return !!url.match(matchstring)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract tag name from pleroma or mastodon url.
|
||||||
|
* i.e https://bikeshed.party/tag/photo or https://quey.org/tags/sky
|
||||||
|
* @param {string} url
|
||||||
|
*/
|
||||||
|
export const extractTagFromUrl = (url) => {
|
||||||
|
const regex = /tag[s]*\/(\w+)$/g
|
||||||
|
const result = regex.exec(url)
|
||||||
|
if (!result) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return result[1]
|
||||||
|
}
|
|
@ -1,9 +0,0 @@
|
||||||
|
|
||||||
export const mentionMatchesUrl = (attention, url) => {
|
|
||||||
if (url === attention.statusnet_profile_url) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
const [namepart, instancepart] = attention.screen_name.split('@')
|
|
||||||
const matchstring = new RegExp('://' + instancepart + '/.*' + namepart + '$', 'g')
|
|
||||||
return !!url.match(matchstring)
|
|
||||||
}
|
|
|
@ -480,7 +480,7 @@ const getThemes = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const setPreset = (val, commit) => {
|
const setPreset = (val, commit) => {
|
||||||
getThemes().then((themes) => {
|
return getThemes().then((themes) => {
|
||||||
const theme = themes[val] ? themes[val] : themes['pleroma-dark']
|
const theme = themes[val] ? themes[val] : themes['pleroma-dark']
|
||||||
const isV1 = Array.isArray(theme)
|
const isV1 = Array.isArray(theme)
|
||||||
const data = isV1 ? {} : theme.theme
|
const data = isV1 ? {} : theme.theme
|
||||||
|
|
|
@ -21,5 +21,6 @@
|
||||||
"loginMethod": "password",
|
"loginMethod": "password",
|
||||||
"webPushNotifications": false,
|
"webPushNotifications": false,
|
||||||
"noAttachmentLinks": false,
|
"noAttachmentLinks": false,
|
||||||
"nsfwCensorImage": ""
|
"nsfwCensorImage": "",
|
||||||
|
"showFeaturesPanel": true
|
||||||
}
|
}
|
||||||
|
|
|
@ -241,7 +241,7 @@ describe('API Entities normalizer', () => {
|
||||||
notice: makeMockStatusQvitter({ id: 444 }),
|
notice: makeMockStatusQvitter({ id: 444 }),
|
||||||
from_profile: makeMockUserQvitter({ id: 'spurdo' })
|
from_profile: makeMockUserQvitter({ id: 'spurdo' })
|
||||||
})
|
})
|
||||||
expect(parseNotification(notif)).to.have.property('id', '123')
|
expect(parseNotification(notif)).to.have.property('id', 123)
|
||||||
expect(parseNotification(notif)).to.have.property('seen', false)
|
expect(parseNotification(notif)).to.have.property('seen', false)
|
||||||
expect(parseNotification(notif)).to.have.deep.property('status.id', '444')
|
expect(parseNotification(notif)).to.have.deep.property('status.id', '444')
|
||||||
expect(parseNotification(notif)).to.have.deep.property('action.id', '444')
|
expect(parseNotification(notif)).to.have.deep.property('action.id', '444')
|
||||||
|
@ -259,7 +259,7 @@ describe('API Entities normalizer', () => {
|
||||||
is_seen: 1,
|
is_seen: 1,
|
||||||
from_profile: makeMockUserQvitter({ id: 'spurdo' })
|
from_profile: makeMockUserQvitter({ id: 'spurdo' })
|
||||||
})
|
})
|
||||||
expect(parseNotification(notif)).to.have.property('id', '123')
|
expect(parseNotification(notif)).to.have.property('id', 123)
|
||||||
expect(parseNotification(notif)).to.have.property('type', 'like')
|
expect(parseNotification(notif)).to.have.property('type', 'like')
|
||||||
expect(parseNotification(notif)).to.have.property('seen', true)
|
expect(parseNotification(notif)).to.have.property('seen', true)
|
||||||
expect(parseNotification(notif)).to.have.deep.property('status.id', '4412')
|
expect(parseNotification(notif)).to.have.deep.property('status.id', '4412')
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import * as MentionMatcher from 'src/services/mention_matcher/mention_matcher.js'
|
import * as MatcherService from 'src/services/matcher/matcher.service.js'
|
||||||
|
|
||||||
const localAttn = () => ({
|
const localAttn = () => ({
|
||||||
id: 123,
|
id: 123,
|
||||||
|
@ -16,48 +16,67 @@ const externalAttn = () => ({
|
||||||
statusnet_profile_url: 'https://instance.com/users/person'
|
statusnet_profile_url: 'https://instance.com/users/person'
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('MentionMatcher', () => {
|
describe('MatcherService', () => {
|
||||||
describe.only('mentionMatchesUrl', () => {
|
describe('mentionMatchesUrl', () => {
|
||||||
it('should match local mention', () => {
|
it('should match local mention', () => {
|
||||||
const attention = localAttn()
|
const attention = localAttn()
|
||||||
const url = 'https://instance.com/users/person'
|
const url = 'https://instance.com/users/person'
|
||||||
|
|
||||||
expect(MentionMatcher.mentionMatchesUrl(attention, url)).to.eql(true)
|
expect(MatcherService.mentionMatchesUrl(attention, url)).to.eql(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should not match a local mention with same name but different instance', () => {
|
it('should not match a local mention with same name but different instance', () => {
|
||||||
const attention = localAttn()
|
const attention = localAttn()
|
||||||
const url = 'https://website.com/users/person'
|
const url = 'https://website.com/users/person'
|
||||||
|
|
||||||
expect(MentionMatcher.mentionMatchesUrl(attention, url)).to.eql(false)
|
expect(MatcherService.mentionMatchesUrl(attention, url)).to.eql(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should match external pleroma mention', () => {
|
it('should match external pleroma mention', () => {
|
||||||
const attention = externalAttn()
|
const attention = externalAttn()
|
||||||
const url = 'https://instance.com/users/person'
|
const url = 'https://instance.com/users/person'
|
||||||
|
|
||||||
expect(MentionMatcher.mentionMatchesUrl(attention, url)).to.eql(true)
|
expect(MatcherService.mentionMatchesUrl(attention, url)).to.eql(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should not match external pleroma mention with same name but different instance', () => {
|
it('should not match external pleroma mention with same name but different instance', () => {
|
||||||
const attention = externalAttn()
|
const attention = externalAttn()
|
||||||
const url = 'https://website.com/users/person'
|
const url = 'https://website.com/users/person'
|
||||||
|
|
||||||
expect(MentionMatcher.mentionMatchesUrl(attention, url)).to.eql(false)
|
expect(MatcherService.mentionMatchesUrl(attention, url)).to.eql(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should match external mastodon mention', () => {
|
it('should match external mastodon mention', () => {
|
||||||
const attention = externalAttn()
|
const attention = externalAttn()
|
||||||
const url = 'https://instance.com/@person'
|
const url = 'https://instance.com/@person'
|
||||||
|
|
||||||
expect(MentionMatcher.mentionMatchesUrl(attention, url)).to.eql(true)
|
expect(MatcherService.mentionMatchesUrl(attention, url)).to.eql(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should not match external mastodon mention with same name but different instance', () => {
|
it('should not match external mastodon mention with same name but different instance', () => {
|
||||||
const attention = externalAttn()
|
const attention = externalAttn()
|
||||||
const url = 'https://website.com/@person'
|
const url = 'https://website.com/@person'
|
||||||
|
|
||||||
expect(MentionMatcher.mentionMatchesUrl(attention, url)).to.eql(false)
|
expect(MatcherService.mentionMatchesUrl(attention, url)).to.eql(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
describe('extractTagFromUrl', () => {
|
||||||
|
it('should return tag name from valid pleroma url', () => {
|
||||||
|
const url = 'https://website.com/tag/photo'
|
||||||
|
|
||||||
|
expect(MatcherService.extractTagFromUrl(url)).to.eql('photo')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return tag name from valid mastodon url', () => {
|
||||||
|
const url = 'https://website.com/tags/sky'
|
||||||
|
|
||||||
|
expect(MatcherService.extractTagFromUrl(url)).to.eql('sky')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not return string but false if invalid url', () => {
|
||||||
|
const url = 'https://website.com/users/sky'
|
||||||
|
|
||||||
|
expect(MatcherService.extractTagFromUrl(url)).to.eql(false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
Loading…
Add table
Reference in a new issue