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 },
|
||||
style () {
|
||||
bgStyle () {
|
||||
return {
|
||||
'--body-background-image': `url(${this.background})`,
|
||||
'background-image': `url(${this.background})`
|
||||
|
@ -82,7 +82,7 @@ export default {
|
|||
unseenNotificationsCount () {
|
||||
return this.unseenNotifications.length
|
||||
},
|
||||
showFeaturesPanel () { return this.$store.state.config.showFeaturesPanel }
|
||||
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel }
|
||||
},
|
||||
methods: {
|
||||
scrollToTop () {
|
||||
|
|
14
src/App.scss
14
src/App.scss
|
@ -1,15 +1,21 @@
|
|||
@import './_variables.scss';
|
||||
|
||||
#app {
|
||||
background-size: cover;
|
||||
background-attachment: fixed;
|
||||
background-repeat: no-repeat;
|
||||
background-position: 0 50px;
|
||||
min-height: 100vh;
|
||||
max-width: 100%;
|
||||
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 {
|
||||
user-select: none;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<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">
|
||||
<div class='logo' :style='logoBgStyle'>
|
||||
<div class='mask' :style='logoMaskStyle'></div>
|
||||
|
|
|
@ -55,10 +55,10 @@ const afterStoreSetup = ({ store, i18n }) => {
|
|||
}
|
||||
|
||||
copyInstanceOption('nsfwCensorImage')
|
||||
copyInstanceOption('theme')
|
||||
copyInstanceOption('background')
|
||||
copyInstanceOption('hidePostStats')
|
||||
copyInstanceOption('hideUserStats')
|
||||
copyInstanceOption('hideFilteredStatuses')
|
||||
copyInstanceOption('logo')
|
||||
|
||||
store.dispatch('setInstanceOption', {
|
||||
|
@ -87,6 +87,7 @@ const afterStoreSetup = ({ store, i18n }) => {
|
|||
copyInstanceOption('postContentType')
|
||||
copyInstanceOption('alwaysShowSubjectInput')
|
||||
copyInstanceOption('noAttachmentLinks')
|
||||
copyInstanceOption('showFeaturesPanel')
|
||||
|
||||
if ((config.chatDisabled)) {
|
||||
store.dispatch('disableChat')
|
||||
|
@ -94,6 +95,9 @@ const afterStoreSetup = ({ store, i18n }) => {
|
|||
store.dispatch('initializeSocket')
|
||||
}
|
||||
|
||||
return store.dispatch('setTheme', config['theme'])
|
||||
})
|
||||
.then(() => {
|
||||
const router = new VueRouter({
|
||||
mode: 'history',
|
||||
routes: routes(store),
|
||||
|
|
|
@ -9,7 +9,7 @@ const About = {
|
|||
TermsOfServicePanel
|
||||
},
|
||||
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) {
|
||||
return seqA < seqB ? -1 : 1
|
||||
} else if (isSeqA && !isSeqB) {
|
||||
return 1
|
||||
} else if (!isSeqA && isSeqB) {
|
||||
return -1
|
||||
} else if (!isSeqA && isSeqB) {
|
||||
return 1
|
||||
} else {
|
||||
return a.id < b.id ? -1 : 1
|
||||
}
|
||||
|
@ -36,6 +36,13 @@ const conversation = {
|
|||
status () {
|
||||
return this.statusoid
|
||||
},
|
||||
statusId () {
|
||||
if (this.statusoid.retweeted_status) {
|
||||
return this.statusoid.retweeted_status.id
|
||||
} else {
|
||||
return this.statusoid.id
|
||||
}
|
||||
},
|
||||
conversation () {
|
||||
if (!this.status) {
|
||||
return []
|
||||
|
@ -79,7 +86,7 @@ const conversation = {
|
|||
const conversationId = this.status.statusnet_conversation_id
|
||||
this.$store.state.api.backendInteractor.fetchConversation({id: conversationId})
|
||||
.then((statuses) => this.$store.dispatch('addNewStatuses', { statuses }))
|
||||
.then(() => this.setHighlight(this.statusoid.id))
|
||||
.then(() => this.setHighlight(this.statusId))
|
||||
} else {
|
||||
const id = this.$route.params.id
|
||||
this.$store.state.api.backendInteractor.fetchStatus({id})
|
||||
|
@ -91,11 +98,7 @@ const conversation = {
|
|||
return this.replies[id] || []
|
||||
},
|
||||
focused (id) {
|
||||
if (this.statusoid.retweeted_status) {
|
||||
return (id === this.statusoid.retweeted_status.id)
|
||||
} else {
|
||||
return (id === this.statusoid.id)
|
||||
}
|
||||
return id === this.statusId
|
||||
},
|
||||
setHighlight (id) {
|
||||
this.highlight = id
|
||||
|
|
|
@ -25,7 +25,8 @@ const FollowList = {
|
|||
},
|
||||
entries () {
|
||||
return this.showFollowers ? this.user.followers : this.user.friends
|
||||
}
|
||||
},
|
||||
showActions () { return this.$store.state.users.currentUser.id === this.userId }
|
||||
},
|
||||
methods: {
|
||||
fetchEntries () {
|
||||
|
|
|
@ -3,7 +3,8 @@
|
|||
<user-card
|
||||
v-for="entry in entries"
|
||||
:key="entry.id" :user="entry"
|
||||
:showFollows="true"
|
||||
:showFollows="!showFollowers"
|
||||
:showActions="showActions"
|
||||
/>
|
||||
<div class="text-center panel-footer">
|
||||
<a v-if="error" @click="fetchEntries" class="alert error">
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import statusPoster from '../../services/status_poster/status_poster.service.js'
|
||||
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 Completion from '../../services/completion/completion.js'
|
||||
import { take, filter, reject, map, uniqBy } from 'lodash'
|
||||
import { reject, map, uniqBy } from 'lodash'
|
||||
|
||||
const buildMentionsString = ({user, attentions}, currentUser) => {
|
||||
let allAttentions = [...attentions]
|
||||
|
@ -28,13 +28,10 @@ const PostStatusForm = {
|
|||
'subject'
|
||||
],
|
||||
components: {
|
||||
MediaUpload
|
||||
MediaUpload,
|
||||
AutoCompleteInput
|
||||
},
|
||||
mounted () {
|
||||
this.resize(this.$refs.textarea)
|
||||
const textLength = this.$refs.textarea.value.length
|
||||
this.$refs.textarea.setSelectionRange(textLength, textLength)
|
||||
|
||||
if (this.replyTo) {
|
||||
this.$refs.textarea.focus()
|
||||
}
|
||||
|
@ -61,15 +58,13 @@ const PostStatusForm = {
|
|||
submitDisabled: false,
|
||||
error: null,
|
||||
posting: false,
|
||||
highlighted: 0,
|
||||
newStatus: {
|
||||
spoilerText: this.subject || '',
|
||||
status: statusText,
|
||||
nsfw: false,
|
||||
files: [],
|
||||
visibility: scope
|
||||
},
|
||||
caret: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -81,59 +76,6 @@ const PostStatusForm = {
|
|||
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 () {
|
||||
return this.newStatus.status.length
|
||||
},
|
||||
|
@ -174,53 +116,8 @@ const PostStatusForm = {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
replace (replacement) {
|
||||
this.newStatus.status = Completion.replaceWord(this.newStatus.status, this.wordAtCaret, replacement)
|
||||
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
|
||||
postStatusCopy () {
|
||||
this.postStatus(this.newStatus)
|
||||
},
|
||||
postStatus (newStatus) {
|
||||
if (this.posting) { return }
|
||||
|
@ -303,18 +200,6 @@ const PostStatusForm = {
|
|||
fileDrag (e) {
|
||||
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 () {
|
||||
this.error = null
|
||||
},
|
||||
|
|
|
@ -16,22 +16,16 @@
|
|||
:placeholder="$t('post_status.content_warning')"
|
||||
v-model="newStatus.spoilerText"
|
||||
class="form-cw">
|
||||
<textarea
|
||||
ref="textarea"
|
||||
@click="setCaret"
|
||||
@keyup="setCaret" v-model="newStatus.status" :placeholder="$t('post_status.default')" rows="1" class="form-control"
|
||||
@keydown.down="cycleForward"
|
||||
@keydown.up="cycleBackward"
|
||||
@keydown.shift.tab="cycleBackward"
|
||||
@keydown.tab="cycleForward"
|
||||
@keydown.enter="replaceCandidate"
|
||||
@keydown.meta.enter="postStatus(newStatus)"
|
||||
@keyup.ctrl.enter="postStatus(newStatus)"
|
||||
@drop="fileDrop"
|
||||
@dragover.prevent="fileDrag"
|
||||
@input="resize"
|
||||
@paste="paste">
|
||||
</textarea>
|
||||
<auto-complete-input v-model="newStatus.status"
|
||||
:classObj="{ 'form-control': true }"
|
||||
:placeholder="$t('post_status.default')"
|
||||
:autoResize="true"
|
||||
:multiline="true"
|
||||
:drop="fileDrop"
|
||||
:dragoverPrevent="fileDrag"
|
||||
:paste="paste"
|
||||
:keydownMetaEnter="postStatusCopy"
|
||||
:keyupCtrlEnter="postStatusCopy"/>
|
||||
<div class="visibility-tray">
|
||||
<span class="text-format" v-if="formattingOptionsEnabled">
|
||||
<label for="post-content-type" class="select">
|
||||
|
@ -52,17 +46,6 @@
|
|||
</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'>
|
||||
<media-upload ref="mediaUpload" @uploading="disableSubmit" @uploaded="addMediaFile" @upload-failed="uploadFailed" :drop-files="dropFiles"></media-upload>
|
||||
|
||||
|
@ -250,52 +233,5 @@
|
|||
cursor: pointer;
|
||||
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>
|
||||
|
|
|
@ -7,7 +7,7 @@ const PublicAndExternalTimeline = {
|
|||
timeline () { return this.$store.state.statuses.timelines.publicAndExternal }
|
||||
},
|
||||
created () {
|
||||
this.$store.dispatch('startFetching', 'publicAndExternal')
|
||||
this.$store.dispatch('startFetching', { timeline: 'publicAndExternal' })
|
||||
},
|
||||
destroyed () {
|
||||
this.$store.dispatch('stopFetching', 'publicAndExternal')
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<Timeline :title="$t('nav.twkn')"v-bind:timeline="timeline" v-bind:timeline-name="'publicAndExternal'"/>
|
||||
<Timeline :title="$t('nav.twkn')" v-bind:timeline="timeline" v-bind:timeline-name="'publicAndExternal'"/>
|
||||
</template>
|
||||
|
||||
<script src="./public_and_external_timeline.js"></script>
|
||||
|
|
|
@ -7,7 +7,7 @@ const PublicTimeline = {
|
|||
timeline () { return this.$store.state.statuses.timelines.public }
|
||||
},
|
||||
created () {
|
||||
this.$store.dispatch('startFetching', 'public')
|
||||
this.$store.dispatch('startFetching', { timeline: 'public' })
|
||||
},
|
||||
destroyed () {
|
||||
this.$store.dispatch('stopFetching', 'public')
|
||||
|
|
|
@ -27,6 +27,11 @@ const settings = {
|
|||
: user.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,
|
||||
replyVisibilityLocal: user.replyVisibility,
|
||||
loopVideoLocal: user.loopVideo,
|
||||
|
@ -101,6 +106,9 @@ const settings = {
|
|||
hideUserStatsLocal (value) {
|
||||
this.$store.dispatch('setOption', { name: 'hideUserStats', value })
|
||||
},
|
||||
hideFilteredStatusesLocal (value) {
|
||||
this.$store.dispatch('setOption', { name: 'hideFilteredStatuses', value })
|
||||
},
|
||||
hideNsfwLocal (value) {
|
||||
this.$store.dispatch('setOption', { name: 'hideNsfw', value })
|
||||
},
|
||||
|
|
|
@ -227,7 +227,6 @@
|
|||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
{{$t('settings.replies_in_timeline')}}
|
||||
|
@ -254,11 +253,18 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<p>{{$t('settings.filtering_explanation')}}</p>
|
||||
<textarea id="muteWords" v-model="muteWordsString"></textarea>
|
||||
<div>
|
||||
<p>{{$t('settings.filtering_explanation')}}</p>
|
||||
<textarea id="muteWords" v-model="muteWordsString"></textarea>
|
||||
</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>
|
||||
|
||||
</tab-switcher>
|
||||
</keep-alive>
|
||||
</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 fileType from 'src/services/file_type/file_type.service'
|
||||
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
|
||||
import { mentionMatchesUrl } from 'src/services/mention_matcher/mention_matcher.js'
|
||||
import { filter, find } from 'lodash'
|
||||
import { mentionMatchesUrl, extractTagFromUrl } from 'src/services/matcher/matcher.service.js'
|
||||
import { filter, find, unescape } from 'lodash'
|
||||
|
||||
const Status = {
|
||||
name: 'Status',
|
||||
|
@ -110,6 +110,14 @@ const Status = {
|
|||
return hits
|
||||
},
|
||||
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 () {
|
||||
// retweet or root of an expanded conversation
|
||||
if (this.focused) {
|
||||
|
@ -201,14 +209,15 @@ const Status = {
|
|||
},
|
||||
replySubject () {
|
||||
if (!this.status.summary) return ''
|
||||
const decodedSummary = unescape(this.status.summary)
|
||||
const behavior = typeof this.$store.state.config.subjectLineBehavior === 'undefined'
|
||||
? this.$store.state.instance.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') {
|
||||
return this.status.summary
|
||||
return decodedSummary
|
||||
} else if (behavior === 'email') {
|
||||
return 're: '.concat(this.status.summary)
|
||||
return 're: '.concat(decodedSummary)
|
||||
} else if (behavior === 'noop') {
|
||||
return ''
|
||||
}
|
||||
|
@ -273,7 +282,7 @@ const Status = {
|
|||
}
|
||||
if (target.tagName === 'A') {
|
||||
if (target.className.match(/mention/)) {
|
||||
const href = target.getAttribute('href')
|
||||
const href = target.href
|
||||
const attn = this.status.attentions.find(attn => mentionMatchesUrl(attn, href))
|
||||
if (attn) {
|
||||
event.stopPropagation()
|
||||
|
@ -283,6 +292,15 @@ const Status = {
|
|||
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')
|
||||
}
|
||||
},
|
||||
|
@ -339,6 +357,9 @@ const Status = {
|
|||
generateUserProfileLink (id, name) {
|
||||
return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames)
|
||||
},
|
||||
generateTagLink (tag) {
|
||||
return `/tag/${tag}`
|
||||
},
|
||||
setMedia () {
|
||||
const attachments = this.attachmentSize === 'hide' ? this.status.attachments : this.galleryAttachments
|
||||
return () => this.$store.dispatch('setMedia', attachments)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<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">
|
||||
<div class="media status container muted">
|
||||
<small>
|
||||
|
@ -56,7 +56,7 @@
|
|||
</div>
|
||||
<h4 class="replies" v-if="inConversation && !noReplyLinks">
|
||||
<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>
|
||||
</small>
|
||||
</h4>
|
||||
|
@ -438,6 +438,8 @@
|
|||
|
||||
.user-name {
|
||||
font-weight: bold;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
img {
|
||||
width: 14px;
|
||||
|
|
|
@ -37,7 +37,7 @@ export default Vue.component('tab-switcher', {
|
|||
|
||||
return (
|
||||
<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>
|
||||
)
|
||||
})
|
||||
|
|
|
@ -3,7 +3,7 @@ import Timeline from '../timeline/timeline.vue'
|
|||
const TagTimeline = {
|
||||
created () {
|
||||
this.$store.commit('clearTimeline', { timeline: 'tag' })
|
||||
this.$store.dispatch('startFetching', { 'tag': this.tag })
|
||||
this.$store.dispatch('startFetching', { timeline: 'tag', tag: this.tag })
|
||||
},
|
||||
components: {
|
||||
Timeline
|
||||
|
@ -15,7 +15,7 @@ const TagTimeline = {
|
|||
watch: {
|
||||
tag () {
|
||||
this.$store.commit('clearTimeline', { timeline: 'tag' })
|
||||
this.$store.dispatch('startFetching', { 'tag': this.tag })
|
||||
this.$store.dispatch('startFetching', { timeline: 'tag', tag: this.tag })
|
||||
}
|
||||
},
|
||||
destroyed () {
|
||||
|
|
|
@ -1,16 +1,21 @@
|
|||
import UserCardContent from '../user_card_content/user_card_content.vue'
|
||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||
import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate'
|
||||
|
||||
const UserCard = {
|
||||
props: [
|
||||
'user',
|
||||
'showFollows',
|
||||
'showApproval'
|
||||
'showApproval',
|
||||
'showActions'
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
userExpanded: false
|
||||
userExpanded: false,
|
||||
followRequestInProgress: false,
|
||||
followRequestSent: false,
|
||||
updated: false
|
||||
}
|
||||
},
|
||||
components: {
|
||||
|
@ -18,7 +23,11 @@ const UserCard = {
|
|||
UserAvatar
|
||||
},
|
||||
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: {
|
||||
toggleUserExpanded () {
|
||||
|
@ -34,6 +43,21 @@ const UserCard = {
|
|||
},
|
||||
userProfileLink (user) {
|
||||
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>
|
||||
</div>
|
||||
<div class="name-and-screen-name" v-else>
|
||||
<div :title="user.name" v-if="user.name_html" class="user-name">
|
||||
<span v-html="user.name_html"></span>
|
||||
<div :title="user.name" class="user-name">
|
||||
<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">
|
||||
{{ currentUser.id == user.id ? $t('user_card.its_you') : $t('user_card.follows_you') }}
|
||||
</span>
|
||||
</div>
|
||||
<div :title="user.name" v-else class="user-name">
|
||||
{{ 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 class="user-link-action">
|
||||
<router-link class='user-screen-name' :to="userProfileLink(user)">
|
||||
@{{user.screen_name}}
|
||||
</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>
|
||||
|
||||
<router-link class='user-screen-name' :to="userProfileLink(user)">
|
||||
@{{user.screen_name}}
|
||||
</router-link>
|
||||
</div>
|
||||
<div class="approval" v-if="showApproval">
|
||||
<button class="btn btn-default" @click="approveUser">{{ $t('user_card.approve') }}</button>
|
||||
|
@ -42,6 +63,9 @@
|
|||
text-align: left;
|
||||
width: 100%;
|
||||
.user-name {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
img {
|
||||
object-fit: contain;
|
||||
height: 16px;
|
||||
|
@ -49,11 +73,20 @@
|
|||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
.user-link-action {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
|
||||
button {
|
||||
margin-top: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.follows-you {
|
||||
margin-left: 2em;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.card {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
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'
|
||||
|
||||
export default {
|
||||
|
@ -92,69 +93,17 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
followUser () {
|
||||
const store = this.$store
|
||||
this.followRequestInProgress = true
|
||||
store.state.api.backendInteractor.followUser(this.user.id)
|
||||
.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.followRequestSent = true
|
||||
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
|
||||
}
|
||||
})
|
||||
})
|
||||
requestFollow(this.user, this.$store).then(({sent}) => {
|
||||
this.followRequestInProgress = false
|
||||
this.followRequestSent = sent
|
||||
})
|
||||
},
|
||||
unfollowUser () {
|
||||
const store = this.$store
|
||||
this.followRequestInProgress = true
|
||||
store.state.api.backendInteractor.unfollowUser(this.user.id)
|
||||
.then((unfollowedUser) => store.commit('addNewUsers', [unfollowedUser]))
|
||||
.then(() => {
|
||||
this.followRequestInProgress = false
|
||||
})
|
||||
requestUnfollow(this.user, this.$store).then(() => {
|
||||
this.followRequestInProgress = false
|
||||
})
|
||||
},
|
||||
blockUser () {
|
||||
const store = this.$store
|
||||
|
|
|
@ -8,8 +8,8 @@ const UserProfile = {
|
|||
this.$store.commit('clearTimeline', { timeline: 'user' })
|
||||
this.$store.commit('clearTimeline', { timeline: 'favorites' })
|
||||
this.$store.commit('clearTimeline', { timeline: 'media' })
|
||||
this.$store.dispatch('startFetching', ['user', this.fetchBy])
|
||||
this.$store.dispatch('startFetching', ['media', this.fetchBy])
|
||||
this.$store.dispatch('startFetching', { timeline: 'user', userId: this.fetchBy })
|
||||
this.$store.dispatch('startFetching', { timeline: 'media', userId: this.fetchBy })
|
||||
this.startFetchFavorites()
|
||||
if (!this.user.id) {
|
||||
this.$store.dispatch('fetchUser', this.fetchBy)
|
||||
|
@ -69,12 +69,12 @@ const UserProfile = {
|
|||
methods: {
|
||||
startFetchFavorites () {
|
||||
if (this.isUs) {
|
||||
this.$store.dispatch('startFetching', ['favorites', this.fetchBy])
|
||||
this.$store.dispatch('startFetching', { timeline: 'favorites', userId: this.fetchBy })
|
||||
}
|
||||
},
|
||||
startUp () {
|
||||
this.$store.dispatch('startFetching', ['user', this.fetchBy])
|
||||
this.$store.dispatch('startFetching', ['media', this.fetchBy])
|
||||
this.$store.dispatch('startFetching', { timeline: 'user', userId: this.fetchBy })
|
||||
this.$store.dispatch('startFetching', { timeline: 'media', userId: this.fetchBy })
|
||||
|
||||
this.startFetchFavorites()
|
||||
},
|
||||
|
|
|
@ -9,19 +9,20 @@
|
|||
<tab-switcher :renderOnlyFocused="true">
|
||||
<Timeline
|
||||
:label="$t('user_card.statuses')"
|
||||
:disabled="!user.statuses_count"
|
||||
:embedded="true"
|
||||
:title="$t('user_profile.timeline_title')"
|
||||
:timeline="timeline"
|
||||
:timeline-name="'user'"
|
||||
: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" />
|
||||
<div class="userlist-placeholder" v-else>
|
||||
<i class="icon-spin3 animate-spin"></i>
|
||||
</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" />
|
||||
<div class="userlist-placeholder" v-else>
|
||||
<i class="icon-spin3 animate-spin"></i>
|
||||
|
@ -29,6 +30,7 @@
|
|||
</div>
|
||||
<Timeline
|
||||
:label="$t('user_card.media')"
|
||||
:disabled="!media.visibleStatuses.length"
|
||||
:embedded="true" :title="$t('user_card.media')"
|
||||
timeline-name="media"
|
||||
:timeline="media"
|
||||
|
@ -37,6 +39,7 @@
|
|||
<Timeline
|
||||
v-if="isUs"
|
||||
:label="$t('user_card.favorites')"
|
||||
:disabled="!favorites.visibleStatuses.length"
|
||||
:embedded="true"
|
||||
:title="$t('user_card.favorites')"
|
||||
timeline-name="favorites"
|
||||
|
|
|
@ -2,6 +2,7 @@ import { unescape } from 'lodash'
|
|||
|
||||
import TabSwitcher from '../tab_switcher/tab_switcher.js'
|
||||
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'
|
||||
|
||||
const UserSettings = {
|
||||
|
@ -41,7 +42,8 @@ const UserSettings = {
|
|||
},
|
||||
components: {
|
||||
StyleSwitcher,
|
||||
TabSwitcher
|
||||
TabSwitcher,
|
||||
AutoCompleteInput
|
||||
},
|
||||
computed: {
|
||||
user () {
|
||||
|
|
|
@ -9,9 +9,9 @@
|
|||
<div class="setting-item" >
|
||||
<h2>{{$t('settings.name_bio')}}</h2>
|
||||
<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>
|
||||
<textarea class="bio" v-model="newBio"></textarea>
|
||||
<auto-complete-input :classObj="{ bio: true }" v-model="newBio" :multiline="true"/>
|
||||
<p>
|
||||
<input type="checkbox" v-model="newLocked" id="account-locked">
|
||||
<label for="account-locked">{{$t('settings.lock_account_description')}}</label>
|
||||
|
@ -178,7 +178,7 @@
|
|||
}
|
||||
|
||||
.banner {
|
||||
max-width: 400px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.uploading {
|
||||
|
@ -189,5 +189,9 @@
|
|||
.name-changer {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.bg {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -132,6 +132,7 @@
|
|||
"preload_images": "Bilder vorausladen",
|
||||
"hide_post_stats": "Beitragsstatistiken verbergen (z.B. die Anzahl der Favoriten)",
|
||||
"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_theme": "Farbschema laden",
|
||||
"inputRadius": "Eingabefelder",
|
||||
|
|
|
@ -140,6 +140,7 @@
|
|||
"use_one_click_nsfw": "Open NSFW attachments with just one click",
|
||||
"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_filtered_statuses": "Hide filtered statuses",
|
||||
"import_followers_from_a_csv_file": "Import follows from a csv file",
|
||||
"import_theme": "Load preset",
|
||||
"inputRadius": "Input fields",
|
||||
|
|
|
@ -28,7 +28,8 @@
|
|||
"password": "Contraseña",
|
||||
"placeholder": "p.ej. lain",
|
||||
"register": "Registrar",
|
||||
"username": "Usuario"
|
||||
"username": "Usuario",
|
||||
"hint": "Inicia sesión para unirte a la discusión"
|
||||
},
|
||||
"nav": {
|
||||
"about": "Sobre",
|
||||
|
@ -55,7 +56,7 @@
|
|||
"no_more_notifications": "No hay más notificaciones"
|
||||
},
|
||||
"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_link": "bloqueada",
|
||||
"attachments_sensitive": "Contenido sensible",
|
||||
|
@ -139,7 +140,8 @@
|
|||
"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_user_stats": "Ocultar las estadísticas del usuario (p.ej. el número de seguidores)",
|
||||
"import_followers_from_a_csv_file": "Importar personas que tú sigues apartir de un archivo csv",
|
||||
"hide_filtered_statuses": "Ocultar estados filtrados",
|
||||
"import_followers_from_a_csv_file": "Importar personas que tú sigues a partir de un archivo csv",
|
||||
"import_theme": "Importar tema",
|
||||
"inputRadius": "Campos de entrada",
|
||||
"checkboxRadius": "Casillas de verificación",
|
||||
|
@ -164,7 +166,10 @@
|
|||
"notification_visibility_mentions": "Menciones",
|
||||
"notification_visibility_repeats": "Repeticiones (Repeats)",
|
||||
"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",
|
||||
"panelRadius": "Paneles",
|
||||
"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_mastodon": "Tipo mastodon: copiar como es",
|
||||
"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",
|
||||
"streaming": "Habilite la transmisión automática de nuevas publicaciones cuando se desplaza hacia la parte superior",
|
||||
"text": "Texto",
|
||||
|
|
|
@ -48,7 +48,7 @@ export default function createPersistedState ({
|
|||
return getState(key, storage).then((savedState) => {
|
||||
return store => {
|
||||
try {
|
||||
if (typeof savedState === 'object') {
|
||||
if (savedState !== null && typeof savedState === 'object') {
|
||||
// build user cache
|
||||
const usersState = savedState.users || {}
|
||||
usersState.usersObject = {}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
|
||||
import {isArray} from 'lodash'
|
||||
import { Socket } from 'phoenix'
|
||||
|
||||
const api = {
|
||||
|
@ -34,20 +33,12 @@ const api = {
|
|||
}
|
||||
},
|
||||
actions: {
|
||||
startFetching (store, timeline) {
|
||||
let userId = false
|
||||
|
||||
// This is for user timelines
|
||||
if (isArray(timeline)) {
|
||||
userId = timeline[1]
|
||||
timeline = timeline[0]
|
||||
}
|
||||
|
||||
startFetching (store, {timeline = 'friends', tag = false, userId = false}) {
|
||||
// Don't start fetching if we already are.
|
||||
if (!store.state.fetchers[timeline]) {
|
||||
const fetcher = store.state.backendInteractor.startFetching({timeline, store, userId})
|
||||
store.commit('addFetcher', {timeline, fetcher})
|
||||
}
|
||||
if (store.state.fetchers[timeline]) return
|
||||
|
||||
const fetcher = store.state.backendInteractor.startFetching({ timeline, store, userId, tag })
|
||||
store.commit('addFetcher', { timeline, fetcher })
|
||||
},
|
||||
stopFetching (store, timeline) {
|
||||
const fetcher = store.state.fetchers[timeline]
|
||||
|
|
|
@ -31,7 +31,6 @@ const defaultState = {
|
|||
scopeCopy: undefined, // instance default
|
||||
subjectLineBehavior: undefined, // instance default
|
||||
alwaysShowSubjectInput: undefined, // instance default
|
||||
showFeaturesPanel: true,
|
||||
postContentType: undefined // instance default
|
||||
}
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@ const defaultState = {
|
|||
collapseMessageWithSubject: false,
|
||||
hidePostStats: false,
|
||||
hideUserStats: false,
|
||||
hideFilteredStatuses: true,
|
||||
disableChat: false,
|
||||
scopeCopy: true,
|
||||
subjectLineBehavior: 'email',
|
||||
|
@ -29,6 +30,7 @@ const defaultState = {
|
|||
nsfwCensorImage: undefined,
|
||||
vapidPublicKey: undefined,
|
||||
noAttachmentLinks: false,
|
||||
showFeaturesPanel: true,
|
||||
|
||||
// Nasty stuff
|
||||
pleromaBackend: true,
|
||||
|
@ -64,9 +66,11 @@ const instance = {
|
|||
case 'name':
|
||||
dispatch('setPageTitle')
|
||||
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.
|
||||
store.dispatch('startFetching', 'friends')
|
||||
store.dispatch('startFetching', { timeline: 'friends' })
|
||||
|
||||
// Get user mutes and follower info
|
||||
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) => {
|
||||
getThemes().then((themes) => {
|
||||
return getThemes().then((themes) => {
|
||||
const theme = themes[val] ? themes[val] : themes['pleroma-dark']
|
||||
const isV1 = Array.isArray(theme)
|
||||
const data = isV1 ? {} : theme.theme
|
||||
|
|
|
@ -21,5 +21,6 @@
|
|||
"loginMethod": "password",
|
||||
"webPushNotifications": false,
|
||||
"noAttachmentLinks": false,
|
||||
"nsfwCensorImage": ""
|
||||
"nsfwCensorImage": "",
|
||||
"showFeaturesPanel": true
|
||||
}
|
||||
|
|
|
@ -241,7 +241,7 @@ describe('API Entities normalizer', () => {
|
|||
notice: makeMockStatusQvitter({ id: 444 }),
|
||||
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.deep.property('status.id', '444')
|
||||
expect(parseNotification(notif)).to.have.deep.property('action.id', '444')
|
||||
|
@ -259,7 +259,7 @@ describe('API Entities normalizer', () => {
|
|||
is_seen: 1,
|
||||
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('seen', true)
|
||||
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 = () => ({
|
||||
id: 123,
|
||||
|
@ -16,48 +16,67 @@ const externalAttn = () => ({
|
|||
statusnet_profile_url: 'https://instance.com/users/person'
|
||||
})
|
||||
|
||||
describe('MentionMatcher', () => {
|
||||
describe.only('mentionMatchesUrl', () => {
|
||||
describe('MatcherService', () => {
|
||||
describe('mentionMatchesUrl', () => {
|
||||
it('should match local mention', () => {
|
||||
const attention = localAttn()
|
||||
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', () => {
|
||||
const attention = localAttn()
|
||||
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', () => {
|
||||
const attention = externalAttn()
|
||||
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', () => {
|
||||
const attention = externalAttn()
|
||||
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', () => {
|
||||
const attention = externalAttn()
|
||||
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', () => {
|
||||
const attention = externalAttn()
|
||||
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