forked from AkkomaGang/akkoma-fe
merge develop & resolve conflict
This commit is contained in:
commit
783cc00dd0
28 changed files with 695 additions and 236 deletions
51
src/App.scss
51
src/App.scss
|
@ -767,3 +767,54 @@ nav {
|
||||||
.btn.btn-default {
|
.btn.btn-default {
|
||||||
min-height: 28px;
|
min-height: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.autocomplete {
|
||||||
|
&-panel {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&-body {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-item {
|
||||||
|
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;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -241,7 +241,7 @@ const afterStoreSetup = async ({ store, i18n }) => {
|
||||||
|
|
||||||
// Now we have the server settings and can try logging in
|
// Now we have the server settings and can try logging in
|
||||||
if (store.state.oauth.token) {
|
if (store.state.oauth.token) {
|
||||||
store.dispatch('loginUser', store.state.oauth.token)
|
await store.dispatch('loginUser', store.state.oauth.token)
|
||||||
}
|
}
|
||||||
|
|
||||||
const router = new VueRouter({
|
const router = new VueRouter({
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<conversation :collapsable="false" :statusoid="statusoid"></conversation>
|
<conversation
|
||||||
|
:collapsable="false"
|
||||||
|
isPage="true"
|
||||||
|
:statusoid="statusoid"
|
||||||
|
></conversation>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script src="./conversation-page.js"></script>
|
<script src="./conversation-page.js"></script>
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import { reduce, filter } from 'lodash'
|
import { reduce, filter, findIndex } from 'lodash'
|
||||||
import { set } from 'vue'
|
import { set } from 'vue'
|
||||||
import Status from '../status/status.vue'
|
import Status from '../status/status.vue'
|
||||||
|
|
||||||
const sortById = (a, b) => {
|
const sortById = (a, b) => {
|
||||||
const seqA = Number(a.id)
|
const idA = a.type === 'retweet' ? a.retweeted_status.id : a.id
|
||||||
const seqB = Number(b.id)
|
const idB = b.type === 'retweet' ? b.retweeted_status.id : b.id
|
||||||
|
const seqA = Number(idA)
|
||||||
|
const seqB = Number(idB)
|
||||||
const isSeqA = !Number.isNaN(seqA)
|
const isSeqA = !Number.isNaN(seqA)
|
||||||
const isSeqB = !Number.isNaN(seqB)
|
const isSeqB = !Number.isNaN(seqB)
|
||||||
if (isSeqA && isSeqB) {
|
if (isSeqA && isSeqB) {
|
||||||
|
@ -14,12 +16,19 @@ const sortById = (a, b) => {
|
||||||
} else if (!isSeqA && isSeqB) {
|
} else if (!isSeqA && isSeqB) {
|
||||||
return 1
|
return 1
|
||||||
} else {
|
} else {
|
||||||
return a.id < b.id ? -1 : 1
|
return idA < idB ? -1 : 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const sortAndFilterConversation = (conversation) => {
|
const sortAndFilterConversation = (conversation, statusoid) => {
|
||||||
|
if (statusoid.type === 'retweet') {
|
||||||
|
conversation = filter(
|
||||||
|
conversation,
|
||||||
|
(status) => (status.type === 'retweet' || status.id !== statusoid.retweeted_status.id)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
conversation = filter(conversation, (status) => status.type !== 'retweet')
|
conversation = filter(conversation, (status) => status.type !== 'retweet')
|
||||||
|
}
|
||||||
return conversation.filter(_ => _).sort(sortById)
|
return conversation.filter(_ => _).sort(sortById)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,13 +36,20 @@ const conversation = {
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
highlight: null,
|
highlight: null,
|
||||||
|
expanded: false,
|
||||||
converationStatusIds: []
|
converationStatusIds: []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
props: [
|
props: [
|
||||||
'statusoid',
|
'statusoid',
|
||||||
'collapsable'
|
'collapsable',
|
||||||
|
'isPage'
|
||||||
],
|
],
|
||||||
|
created () {
|
||||||
|
if (this.isPage) {
|
||||||
|
this.fetchConversation()
|
||||||
|
}
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
status () {
|
status () {
|
||||||
return this.statusoid
|
return this.statusoid
|
||||||
|
@ -59,12 +75,22 @@ const conversation = {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this.isExpanded) {
|
||||||
|
return [this.status]
|
||||||
|
}
|
||||||
|
|
||||||
const statusesObject = this.$store.state.statuses.allStatusesObject
|
const statusesObject = this.$store.state.statuses.allStatusesObject
|
||||||
const conversation = this.idsToShow.reduce((acc, id) => {
|
const conversation = this.idsToShow.reduce((acc, id) => {
|
||||||
acc.push(statusesObject[id])
|
acc.push(statusesObject[id])
|
||||||
return acc
|
return acc
|
||||||
}, [])
|
}, [])
|
||||||
return sortAndFilterConversation(conversation)
|
|
||||||
|
const statusIndex = findIndex(conversation, { id: this.statusId })
|
||||||
|
if (statusIndex !== -1) {
|
||||||
|
conversation[statusIndex] = this.status
|
||||||
|
}
|
||||||
|
|
||||||
|
return sortAndFilterConversation(conversation, this.status)
|
||||||
},
|
},
|
||||||
replies () {
|
replies () {
|
||||||
let i = 1
|
let i = 1
|
||||||
|
@ -82,16 +108,21 @@ const conversation = {
|
||||||
i++
|
i++
|
||||||
return result
|
return result
|
||||||
}, {})
|
}, {})
|
||||||
|
},
|
||||||
|
isExpanded () {
|
||||||
|
return this.expanded || this.isPage
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
Status
|
Status
|
||||||
},
|
},
|
||||||
created () {
|
|
||||||
this.fetchConversation()
|
|
||||||
},
|
|
||||||
watch: {
|
watch: {
|
||||||
'$route': 'fetchConversation'
|
'$route': 'fetchConversation',
|
||||||
|
expanded (value) {
|
||||||
|
if (value) {
|
||||||
|
this.fetchConversation()
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
fetchConversation () {
|
fetchConversation () {
|
||||||
|
@ -101,9 +132,9 @@ const conversation = {
|
||||||
this.$store.dispatch('addNewStatuses', { statuses: ancestors })
|
this.$store.dispatch('addNewStatuses', { statuses: ancestors })
|
||||||
this.$store.dispatch('addNewStatuses', { statuses: descendants })
|
this.$store.dispatch('addNewStatuses', { statuses: descendants })
|
||||||
set(this, 'converationStatusIds', [].concat(
|
set(this, 'converationStatusIds', [].concat(
|
||||||
ancestors.map(_ => _.id),
|
ancestors.map(_ => _.id).filter(_ => _ !== this.statusId),
|
||||||
this.statusId,
|
this.statusId,
|
||||||
descendants.map(_ => _.id)))
|
descendants.map(_ => _.id).filter(_ => _ !== this.statusId)))
|
||||||
})
|
})
|
||||||
.then(() => this.setHighlight(this.statusId))
|
.then(() => this.setHighlight(this.statusId))
|
||||||
} else {
|
} else {
|
||||||
|
@ -117,10 +148,19 @@ const conversation = {
|
||||||
return this.replies[id] || []
|
return this.replies[id] || []
|
||||||
},
|
},
|
||||||
focused (id) {
|
focused (id) {
|
||||||
return id === this.statusId
|
return (this.isExpanded) && id === this.status.id
|
||||||
},
|
},
|
||||||
setHighlight (id) {
|
setHighlight (id) {
|
||||||
this.highlight = id
|
this.highlight = id
|
||||||
|
},
|
||||||
|
getHighlight () {
|
||||||
|
return this.isExpanded ? this.highlight : null
|
||||||
|
},
|
||||||
|
toggleExpanded () {
|
||||||
|
this.expanded = !this.expanded
|
||||||
|
if (!this.expanded) {
|
||||||
|
this.setHighlight(null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,26 +1,42 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="timeline panel panel-default">
|
<div class="timeline panel-default" :class="[isExpanded ? 'panel' : 'panel-disabled']">
|
||||||
<div class="panel-heading conversation-heading">
|
<div v-if="isExpanded" class="panel-heading conversation-heading">
|
||||||
<span class="title"> {{ $t('timeline.conversation') }} </span>
|
<span class="title"> {{ $t('timeline.conversation') }} </span>
|
||||||
<span v-if="collapsable">
|
<span v-if="collapsable">
|
||||||
<a href="#" @click.prevent="$emit('toggleExpanded')">{{ $t('timeline.collapse') }}</a>
|
<a href="#" @click.prevent="toggleExpanded">{{ $t('timeline.collapse') }}</a>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
|
||||||
<div class="timeline">
|
|
||||||
<status
|
<status
|
||||||
v-for="status in conversation"
|
v-for="status in conversation"
|
||||||
@goto="setHighlight" :key="status.id"
|
@goto="setHighlight"
|
||||||
:inlineExpanded="collapsable" :statusoid="status"
|
@toggleExpanded="toggleExpanded"
|
||||||
:expandable='false' :focused="focused(status.id)"
|
:key="status.id"
|
||||||
:inConversation='true'
|
:inlineExpanded="collapsable"
|
||||||
:highlight="highlight"
|
:statusoid="status"
|
||||||
|
:expandable='!expanded'
|
||||||
|
:focused="focused(status.id)"
|
||||||
|
:inConversation="isExpanded"
|
||||||
|
:highlight="getHighlight()"
|
||||||
:replies="getReplies(status.id)"
|
:replies="getReplies(status.id)"
|
||||||
class="status-fadein">
|
class="status-fadein panel-body"
|
||||||
</status>
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script src="./conversation.js"></script>
|
<script src="./conversation.js"></script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
|
.timeline {
|
||||||
|
.panel-disabled {
|
||||||
|
.status-el {
|
||||||
|
border-left: none;
|
||||||
|
border-bottom-width: 1px;
|
||||||
|
border-bottom-style: solid;
|
||||||
|
border-color: var(--border, $fallback--border);
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
107
src/components/emoji-input/emoji-input.js
Normal file
107
src/components/emoji-input/emoji-input.js
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
import Completion from '../../services/completion/completion.js'
|
||||||
|
import { take, filter, map } from 'lodash'
|
||||||
|
|
||||||
|
const EmojiInput = {
|
||||||
|
props: [
|
||||||
|
'value',
|
||||||
|
'placeholder',
|
||||||
|
'type',
|
||||||
|
'classname'
|
||||||
|
],
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
highlighted: 0,
|
||||||
|
caret: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
suggestions () {
|
||||||
|
const firstchar = this.textAtCaret.charAt(0)
|
||||||
|
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) => ({
|
||||||
|
shortcode: `:${shortcode}:`,
|
||||||
|
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.value, this.caret - 1) || {}
|
||||||
|
return word
|
||||||
|
},
|
||||||
|
emoji () {
|
||||||
|
return this.$store.state.instance.emoji || []
|
||||||
|
},
|
||||||
|
customEmoji () {
|
||||||
|
return this.$store.state.instance.customEmoji || []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
replace (replacement) {
|
||||||
|
const newValue = Completion.replaceWord(this.value, this.wordAtCaret, replacement)
|
||||||
|
this.$emit('input', newValue)
|
||||||
|
this.caret = 0
|
||||||
|
},
|
||||||
|
replaceEmoji (e) {
|
||||||
|
const len = this.suggestions.length || 0
|
||||||
|
if (this.textAtCaret === ':' || e.ctrlKey) { return }
|
||||||
|
if (len > 0) {
|
||||||
|
e.preventDefault()
|
||||||
|
const emoji = this.suggestions[this.highlighted]
|
||||||
|
const replacement = emoji.utf || (emoji.shortcode + ' ')
|
||||||
|
const newValue = Completion.replaceWord(this.value, this.wordAtCaret, replacement)
|
||||||
|
this.$emit('input', newValue)
|
||||||
|
this.caret = 0
|
||||||
|
this.highlighted = 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cycleBackward (e) {
|
||||||
|
const len = this.suggestions.length || 0
|
||||||
|
if (len > 0) {
|
||||||
|
e.preventDefault()
|
||||||
|
this.highlighted -= 1
|
||||||
|
if (this.highlighted < 0) {
|
||||||
|
this.highlighted = this.suggestions.length - 1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.highlighted = 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cycleForward (e) {
|
||||||
|
const len = this.suggestions.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
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onKeydown (e) {
|
||||||
|
e.stopPropagation()
|
||||||
|
},
|
||||||
|
onInput (e) {
|
||||||
|
this.$emit('input', e.target.value)
|
||||||
|
},
|
||||||
|
setCaret ({target: {selectionStart}}) {
|
||||||
|
this.caret = selectionStart
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EmojiInput
|
64
src/components/emoji-input/emoji-input.vue
Normal file
64
src/components/emoji-input/emoji-input.vue
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
<template>
|
||||||
|
<div class="emoji-input">
|
||||||
|
<input
|
||||||
|
v-if="type !== 'textarea'"
|
||||||
|
:class="classname"
|
||||||
|
:type="type"
|
||||||
|
:value="value"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
@input="onInput"
|
||||||
|
@click="setCaret"
|
||||||
|
@keyup="setCaret"
|
||||||
|
@keydown="onKeydown"
|
||||||
|
@keydown.down="cycleForward"
|
||||||
|
@keydown.up="cycleBackward"
|
||||||
|
@keydown.shift.tab="cycleBackward"
|
||||||
|
@keydown.tab="cycleForward"
|
||||||
|
@keydown.enter="replaceEmoji"
|
||||||
|
/>
|
||||||
|
<textarea
|
||||||
|
v-else
|
||||||
|
:class="classname"
|
||||||
|
:value="value"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
@input="onInput"
|
||||||
|
@click="setCaret"
|
||||||
|
@keyup="setCaret"
|
||||||
|
@keydown="onKeydown"
|
||||||
|
@keydown.down="cycleForward"
|
||||||
|
@keydown.up="cycleBackward"
|
||||||
|
@keydown.shift.tab="cycleBackward"
|
||||||
|
@keydown.tab="cycleForward"
|
||||||
|
@keydown.enter="replaceEmoji"
|
||||||
|
></textarea>
|
||||||
|
<div class="autocomplete-panel" v-if="suggestions">
|
||||||
|
<div class="autocomplete-panel-body">
|
||||||
|
<div
|
||||||
|
v-for="(emoji, index) in suggestions"
|
||||||
|
:key="index"
|
||||||
|
@click="replace(emoji.utf || (emoji.shortcode + ' '))"
|
||||||
|
class="autocomplete-item"
|
||||||
|
:class="{ highlighted: emoji.highlighted }"
|
||||||
|
>
|
||||||
|
<span v-if="emoji.img">
|
||||||
|
<img :src="emoji.img" />
|
||||||
|
</span>
|
||||||
|
<span v-else>{{emoji.utf}}</span>
|
||||||
|
<span>{{emoji.shortcode}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script src="./emoji-input.js"></script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import '../../_variables.scss';
|
||||||
|
|
||||||
|
.emoji-input {
|
||||||
|
.form-control {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,5 +1,6 @@
|
||||||
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 EmojiInput from '../emoji-input/emoji-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 Completion from '../../services/completion/completion.js'
|
||||||
import { take, filter, reject, map, uniqBy } from 'lodash'
|
import { take, filter, reject, map, uniqBy } from 'lodash'
|
||||||
|
@ -28,7 +29,8 @@ const PostStatusForm = {
|
||||||
'subject'
|
'subject'
|
||||||
],
|
],
|
||||||
components: {
|
components: {
|
||||||
MediaUpload
|
MediaUpload,
|
||||||
|
EmojiInput
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
this.resize(this.$refs.textarea)
|
this.resize(this.$refs.textarea)
|
||||||
|
|
|
@ -10,12 +10,13 @@
|
||||||
<router-link :to="{ name: 'user-settings' }">{{ $t('post_status.account_not_locked_warning_link') }}</router-link>
|
<router-link :to="{ name: 'user-settings' }">{{ $t('post_status.account_not_locked_warning_link') }}</router-link>
|
||||||
</i18n>
|
</i18n>
|
||||||
<p v-if="this.newStatus.visibility == 'direct'" class="visibility-notice">{{ $t('post_status.direct_warning') }}</p>
|
<p v-if="this.newStatus.visibility == 'direct'" class="visibility-notice">{{ $t('post_status.direct_warning') }}</p>
|
||||||
<input
|
<EmojiInput
|
||||||
v-if="newStatus.spoilerText || alwaysShowSubject"
|
v-if="newStatus.spoilerText || alwaysShowSubject"
|
||||||
type="text"
|
type="text"
|
||||||
:placeholder="$t('post_status.content_warning')"
|
:placeholder="$t('post_status.content_warning')"
|
||||||
v-model="newStatus.spoilerText"
|
v-model="newStatus.spoilerText"
|
||||||
class="form-cw">
|
classname="form-control"
|
||||||
|
/>
|
||||||
<textarea
|
<textarea
|
||||||
ref="textarea"
|
ref="textarea"
|
||||||
@click="setCaret"
|
@click="setCaret"
|
||||||
|
@ -55,17 +56,21 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="position:relative;" v-if="candidates">
|
<div class="autocomplete-panel" v-if="candidates">
|
||||||
<div class="autocomplete-panel">
|
<div class="autocomplete-panel-body">
|
||||||
<div v-for="candidate in candidates" @click="replace(candidate.utf || (candidate.screen_name + ' '))">
|
<div
|
||||||
<div class="autocomplete" :class="{ highlighted: candidate.highlighted }">
|
v-for="(candidate, index) in candidates"
|
||||||
<span v-if="candidate.img"><img :src="candidate.img"></img></span>
|
:key="index"
|
||||||
|
@click="replace(candidate.utf || (candidate.screen_name + ' '))"
|
||||||
|
class="autocomplete-item"
|
||||||
|
:class="{ highlighted: candidate.highlighted }"
|
||||||
|
>
|
||||||
|
<span v-if="candidate.img"><img :src="candidate.img" /></span>
|
||||||
<span v-else>{{candidate.utf}}</span>
|
<span v-else>{{candidate.utf}}</span>
|
||||||
<span>{{candidate.screen_name}}<small>{{candidate.name}}</small></span>
|
<span>{{candidate.screen_name}}<small>{{candidate.name}}</small></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
|
@ -261,50 +266,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;
|
|
||||||
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>
|
||||||
|
|
|
@ -1,17 +1,16 @@
|
||||||
import UserCard from '../user_card/user_card.vue'
|
import UserCard from '../user_card/user_card.vue'
|
||||||
import { unseenNotificationsFromStore } from '../../services/notification_utils/notification_utils'
|
import { unseenNotificationsFromStore } from '../../services/notification_utils/notification_utils'
|
||||||
|
import GestureService from '../../services/gesture_service/gesture_service'
|
||||||
// TODO: separate touch gesture stuff into their own utils if more components want them
|
|
||||||
const deltaCoord = (oldCoord, newCoord) => [newCoord[0] - oldCoord[0], newCoord[1] - oldCoord[1]]
|
|
||||||
|
|
||||||
const touchEventCoord = e => ([e.touches[0].screenX, e.touches[0].screenY])
|
|
||||||
|
|
||||||
const SideDrawer = {
|
const SideDrawer = {
|
||||||
props: [ 'logout' ],
|
props: [ 'logout' ],
|
||||||
data: () => ({
|
data: () => ({
|
||||||
closed: true,
|
closed: true,
|
||||||
touchCoord: [0, 0]
|
closeGesture: undefined
|
||||||
}),
|
}),
|
||||||
|
created () {
|
||||||
|
this.closeGesture = GestureService.swipeGesture(GestureService.DIRECTION_LEFT, this.toggleDrawer)
|
||||||
|
},
|
||||||
components: { UserCard },
|
components: { UserCard },
|
||||||
computed: {
|
computed: {
|
||||||
currentUser () {
|
currentUser () {
|
||||||
|
@ -46,13 +45,10 @@ const SideDrawer = {
|
||||||
this.toggleDrawer()
|
this.toggleDrawer()
|
||||||
},
|
},
|
||||||
touchStart (e) {
|
touchStart (e) {
|
||||||
this.touchCoord = touchEventCoord(e)
|
GestureService.beginSwipe(e, this.closeGesture)
|
||||||
},
|
},
|
||||||
touchMove (e) {
|
touchMove (e) {
|
||||||
const delta = deltaCoord(this.touchCoord, touchEventCoord(e))
|
GestureService.updateSwipe(e, this.closeGesture)
|
||||||
if (delta[0] < -30 && Math.abs(delta[1]) < Math.abs(delta[0]) && !this.closed) {
|
|
||||||
this.toggleDrawer()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
<div class="side-drawer-container"
|
<div class="side-drawer-container"
|
||||||
:class="{ 'side-drawer-container-closed': closed, 'side-drawer-container-open': !closed }"
|
:class="{ 'side-drawer-container-closed': closed, 'side-drawer-container-open': !closed }"
|
||||||
>
|
>
|
||||||
|
<div class="side-drawer-darken" :class="{ 'side-drawer-darken-closed': closed}" />
|
||||||
<div class="side-drawer"
|
<div class="side-drawer"
|
||||||
:class="{'side-drawer-closed': closed}"
|
:class="{'side-drawer-closed': closed}"
|
||||||
@touchstart="touchStart"
|
@touchstart="touchStart"
|
||||||
|
@ -111,16 +112,32 @@
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
|
transition-duration: 0s;
|
||||||
|
transition-property: transform;
|
||||||
}
|
}
|
||||||
|
|
||||||
.side-drawer-container-open {
|
.side-drawer-container-open {
|
||||||
|
transform: translate(0%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-drawer-container-closed {
|
||||||
|
transition-delay: 0.35s;
|
||||||
|
transform: translate(-100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-drawer-darken {
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
position: fixed;
|
||||||
|
z-index: -1;
|
||||||
transition: 0.35s;
|
transition: 0.35s;
|
||||||
transition-property: background-color;
|
transition-property: background-color;
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.side-drawer-container-closed {
|
.side-drawer-darken-closed {
|
||||||
left: -100%;
|
|
||||||
background-color: rgba(0, 0, 0, 0);
|
background-color: rgba(0, 0, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -130,8 +147,9 @@
|
||||||
|
|
||||||
.side-drawer {
|
.side-drawer {
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
transition: 0.35s;
|
|
||||||
transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
|
transition-timing-function: cubic-bezier(0, 1, 0.5, 1);
|
||||||
|
transition: 0.35s;
|
||||||
|
transition-property: transform;
|
||||||
margin: 0 0 0 -100px;
|
margin: 0 0 0 -100px;
|
||||||
padding: 0 0 1em 100px;
|
padding: 0 0 1em 100px;
|
||||||
width: 80%;
|
width: 80%;
|
||||||
|
|
|
@ -310,7 +310,6 @@ const Status = {
|
||||||
this.replying = !this.replying
|
this.replying = !this.replying
|
||||||
},
|
},
|
||||||
gotoOriginal (id) {
|
gotoOriginal (id) {
|
||||||
// only handled by conversation, not status_or_conversation
|
|
||||||
if (this.inConversation) {
|
if (this.inConversation) {
|
||||||
this.$emit('goto', id)
|
this.$emit('goto', id)
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div v-if="retweet && !noHeading" :class="[repeaterClass, { highlighted: repeaterStyle }]" :style="[repeaterStyle]" class="media container retweet-info">
|
<div v-if="retweet && !noHeading && !inConversation" :class="[repeaterClass, { highlighted: repeaterStyle }]" :style="[repeaterStyle]" class="media container retweet-info">
|
||||||
<UserAvatar class="media-left" v-if="retweet" :betterShadow="betterShadow" :src="statusoid.user.profile_image_url_original"/>
|
<UserAvatar class="media-left" v-if="retweet" :betterShadow="betterShadow" :src="statusoid.user.profile_image_url_original"/>
|
||||||
<div class="media-body faint">
|
<div class="media-body faint">
|
||||||
<span class="user-name">
|
<span class="user-name">
|
||||||
|
@ -24,7 +24,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div :class="[userClass, { highlighted: userStyle, 'is-retweet': retweet }]" :style="[ userStyle ]" class="media status">
|
<div :class="[userClass, { highlighted: userStyle, 'is-retweet': retweet && !inConversation }]" :style="[ userStyle ]" class="media status">
|
||||||
<div v-if="!noHeading" class="media-left">
|
<div v-if="!noHeading" class="media-left">
|
||||||
<router-link :to="userProfileLink" @click.stop.prevent.capture.native="toggleUserExpanded">
|
<router-link :to="userProfileLink" @click.stop.prevent.capture.native="toggleUserExpanded">
|
||||||
<UserAvatar :compact="compact" :betterShadow="betterShadow" :src="status.user.profile_image_url_original"/>
|
<UserAvatar :compact="compact" :betterShadow="betterShadow" :src="status.user.profile_image_url_original"/>
|
||||||
|
@ -135,9 +135,8 @@
|
||||||
|
|
||||||
<div v-if="!noHeading && !isPreview" class='status-actions media-body'>
|
<div v-if="!noHeading && !isPreview" class='status-actions media-body'>
|
||||||
<div v-if="loggedIn">
|
<div v-if="loggedIn">
|
||||||
<a href="#" v-on:click.prevent="toggleReplying" :title="$t('tool_tip.reply')">
|
<i class="button-icon icon-reply" v-on:click.prevent="toggleReplying" :title="$t('tool_tip.reply')" :class="{'icon-reply-active': replying}"></i>
|
||||||
<i class="button-icon icon-reply" :class="{'icon-reply-active': replying}"></i>
|
<span v-if="status.replies_count > 0">{{status.replies_count}}</span>
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
<retweet-button :visibility='status.visibility' :loggedIn='loggedIn' :status='status'></retweet-button>
|
<retweet-button :visibility='status.visibility' :loggedIn='loggedIn' :status='status'></retweet-button>
|
||||||
<favorite-button :loggedIn='loggedIn' :status='status'></favorite-button>
|
<favorite-button :loggedIn='loggedIn' :status='status'></favorite-button>
|
||||||
|
@ -551,6 +550,7 @@ $status-margin: 0.75em;
|
||||||
.icon-reply:hover {
|
.icon-reply:hover {
|
||||||
color: $fallback--cBlue;
|
color: $fallback--cBlue;
|
||||||
color: var(--cBlue, $fallback--cBlue);
|
color: var(--cBlue, $fallback--cBlue);
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-reply.icon-reply-active {
|
.icon-reply.icon-reply-active {
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
import Status from '../status/status.vue'
|
|
||||||
import Conversation from '../conversation/conversation.vue'
|
|
||||||
|
|
||||||
const statusOrConversation = {
|
|
||||||
props: ['statusoid'],
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
expanded: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
components: {
|
|
||||||
Status,
|
|
||||||
Conversation
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
toggleExpanded () {
|
|
||||||
this.expanded = !this.expanded
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default statusOrConversation
|
|
|
@ -1,14 +0,0 @@
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<conversation v-if="expanded" @toggleExpanded="toggleExpanded" :collapsable="true" :statusoid="statusoid"></conversation>
|
|
||||||
<status v-if="!expanded" @toggleExpanded="toggleExpanded" :expandable="true" :inConversation="false" :focused="false" :statusoid="statusoid"></status>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script src="./status_or_conversation.js"></script>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.spacer {
|
|
||||||
height: 1em
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,6 +1,6 @@
|
||||||
import Status from '../status/status.vue'
|
import Status from '../status/status.vue'
|
||||||
import timelineFetcher from '../../services/timeline_fetcher/timeline_fetcher.service.js'
|
import timelineFetcher from '../../services/timeline_fetcher/timeline_fetcher.service.js'
|
||||||
import StatusOrConversation from '../status_or_conversation/status_or_conversation.vue'
|
import Conversation from '../conversation/conversation.vue'
|
||||||
import { throttle } from 'lodash'
|
import { throttle } from 'lodash'
|
||||||
|
|
||||||
const Timeline = {
|
const Timeline = {
|
||||||
|
@ -43,7 +43,7 @@ const Timeline = {
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
Status,
|
Status,
|
||||||
StatusOrConversation
|
Conversation
|
||||||
},
|
},
|
||||||
created () {
|
created () {
|
||||||
const store = this.$store
|
const store = this.$store
|
||||||
|
|
|
@ -16,7 +16,13 @@
|
||||||
</div>
|
</div>
|
||||||
<div :class="classes.body">
|
<div :class="classes.body">
|
||||||
<div class="timeline">
|
<div class="timeline">
|
||||||
<status-or-conversation v-for="status in timeline.visibleStatuses" :key="status.id" v-bind:statusoid="status" class="status-fadein"></status-or-conversation>
|
<conversation
|
||||||
|
v-for="status in timeline.visibleStatuses"
|
||||||
|
class="status-fadein"
|
||||||
|
:key="status.id"
|
||||||
|
:statusoid="status"
|
||||||
|
:collapsable="true"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div :class="classes.footer">
|
<div :class="classes.footer">
|
||||||
|
|
|
@ -7,6 +7,7 @@ import StyleSwitcher from '../style_switcher/style_switcher.vue'
|
||||||
import fileSizeFormatService from '../../services/file_size_format/file_size_format.js'
|
import fileSizeFormatService from '../../services/file_size_format/file_size_format.js'
|
||||||
import BlockCard from '../block_card/block_card.vue'
|
import BlockCard from '../block_card/block_card.vue'
|
||||||
import MuteCard from '../mute_card/mute_card.vue'
|
import MuteCard from '../mute_card/mute_card.vue'
|
||||||
|
import EmojiInput from '../emoji-input/emoji-input.vue'
|
||||||
import withSubscription from '../../hocs/with_subscription/with_subscription'
|
import withSubscription from '../../hocs/with_subscription/with_subscription'
|
||||||
import withList from '../../hocs/with_list/with_list'
|
import withList from '../../hocs/with_list/with_list'
|
||||||
|
|
||||||
|
@ -69,7 +70,8 @@ const UserSettings = {
|
||||||
TabSwitcher,
|
TabSwitcher,
|
||||||
ImageCropper,
|
ImageCropper,
|
||||||
BlockList,
|
BlockList,
|
||||||
MuteList
|
MuteList,
|
||||||
|
EmojiInput
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
user () {
|
user () {
|
||||||
|
|
|
@ -22,9 +22,18 @@
|
||||||
<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>
|
<EmojiInput
|
||||||
|
type="text"
|
||||||
|
v-model="newName"
|
||||||
|
id="username"
|
||||||
|
classname="name-changer"
|
||||||
|
/>
|
||||||
<p>{{$t('settings.bio')}}</p>
|
<p>{{$t('settings.bio')}}</p>
|
||||||
<textarea class="bio" v-model="newBio"></textarea>
|
<EmojiInput
|
||||||
|
type="textarea"
|
||||||
|
v-model="newBio"
|
||||||
|
classname="bio"
|
||||||
|
/>
|
||||||
<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>
|
||||||
|
@ -61,7 +70,7 @@
|
||||||
<h2>{{$t('settings.avatar')}}</h2>
|
<h2>{{$t('settings.avatar')}}</h2>
|
||||||
<p class="visibility-notice">{{$t('settings.avatar_size_instruction')}}</p>
|
<p class="visibility-notice">{{$t('settings.avatar_size_instruction')}}</p>
|
||||||
<p>{{$t('settings.current_avatar')}}</p>
|
<p>{{$t('settings.current_avatar')}}</p>
|
||||||
<img :src="user.profile_image_url_original" class="current-avatar"></img>
|
<img :src="user.profile_image_url_original" class="current-avatar" />
|
||||||
<p>{{$t('settings.set_new_avatar')}}</p>
|
<p>{{$t('settings.set_new_avatar')}}</p>
|
||||||
<button class="btn" type="button" id="pick-avatar" v-show="pickAvatarBtnVisible">{{$t('settings.upload_a_photo')}}</button>
|
<button class="btn" type="button" id="pick-avatar" v-show="pickAvatarBtnVisible">{{$t('settings.upload_a_photo')}}</button>
|
||||||
<image-cropper trigger="#pick-avatar" :submitHandler="submitAvatar" @open="pickAvatarBtnVisible=false" @close="pickAvatarBtnVisible=true" />
|
<image-cropper trigger="#pick-avatar" :submitHandler="submitAvatar" @open="pickAvatarBtnVisible=false" @close="pickAvatarBtnVisible=true" />
|
||||||
|
@ -69,12 +78,11 @@
|
||||||
<div class="setting-item">
|
<div class="setting-item">
|
||||||
<h2>{{$t('settings.profile_banner')}}</h2>
|
<h2>{{$t('settings.profile_banner')}}</h2>
|
||||||
<p>{{$t('settings.current_profile_banner')}}</p>
|
<p>{{$t('settings.current_profile_banner')}}</p>
|
||||||
<img :src="user.cover_photo" class="banner"></img>
|
<img :src="user.cover_photo" class="banner" />
|
||||||
<p>{{$t('settings.set_new_profile_banner')}}</p>
|
<p>{{$t('settings.set_new_profile_banner')}}</p>
|
||||||
<img class="banner" v-bind:src="bannerPreview" v-if="bannerPreview">
|
<img class="banner" v-bind:src="bannerPreview" v-if="bannerPreview" />
|
||||||
</img>
|
|
||||||
<div>
|
<div>
|
||||||
<input type="file" @change="uploadFile('banner', $event)" ></input>
|
<input type="file" @change="uploadFile('banner', $event)" />
|
||||||
</div>
|
</div>
|
||||||
<i class=" icon-spin4 animate-spin uploading" v-if="bannerUploading"></i>
|
<i class=" icon-spin4 animate-spin uploading" v-if="bannerUploading"></i>
|
||||||
<button class="btn btn-default" v-else-if="bannerPreview" @click="submitBanner">{{$t('general.submit')}}</button>
|
<button class="btn btn-default" v-else-if="bannerPreview" @click="submitBanner">{{$t('general.submit')}}</button>
|
||||||
|
@ -86,10 +94,9 @@
|
||||||
<div class="setting-item">
|
<div class="setting-item">
|
||||||
<h2>{{$t('settings.profile_background')}}</h2>
|
<h2>{{$t('settings.profile_background')}}</h2>
|
||||||
<p>{{$t('settings.set_new_profile_background')}}</p>
|
<p>{{$t('settings.set_new_profile_background')}}</p>
|
||||||
<img class="bg" v-bind:src="backgroundPreview" v-if="backgroundPreview">
|
<img class="bg" v-bind:src="backgroundPreview" v-if="backgroundPreview" />
|
||||||
</img>
|
|
||||||
<div>
|
<div>
|
||||||
<input type="file" @change="uploadFile('background', $event)" ></input>
|
<input type="file" @change="uploadFile('background', $event)" />
|
||||||
</div>
|
</div>
|
||||||
<i class=" icon-spin4 animate-spin uploading" v-if="backgroundUploading"></i>
|
<i class=" icon-spin4 animate-spin uploading" v-if="backgroundUploading"></i>
|
||||||
<button class="btn btn-default" v-else-if="backgroundPreview" @click="submitBg">{{$t('general.submit')}}</button>
|
<button class="btn btn-default" v-else-if="backgroundPreview" @click="submitBg">{{$t('general.submit')}}</button>
|
||||||
|
@ -165,7 +172,7 @@
|
||||||
<h2>{{$t('settings.follow_import')}}</h2>
|
<h2>{{$t('settings.follow_import')}}</h2>
|
||||||
<p>{{$t('settings.import_followers_from_a_csv_file')}}</p>
|
<p>{{$t('settings.import_followers_from_a_csv_file')}}</p>
|
||||||
<form>
|
<form>
|
||||||
<input type="file" ref="followlist" v-on:change="followListChange"></input>
|
<input type="file" ref="followlist" v-on:change="followListChange" />
|
||||||
</form>
|
</form>
|
||||||
<i class=" icon-spin4 animate-spin uploading" v-if="followListUploading"></i>
|
<i class=" icon-spin4 animate-spin uploading" v-if="followListUploading"></i>
|
||||||
<button class="btn btn-default" v-else @click="importFollows">{{$t('general.submit')}}</button>
|
<button class="btn btn-default" v-else @click="importFollows">{{$t('general.submit')}}</button>
|
||||||
|
|
|
@ -60,9 +60,6 @@ export default function createPersistedState ({
|
||||||
merge({}, store.state, savedState)
|
merge({}, store.state, savedState)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (store.state.oauth.token) {
|
|
||||||
store.dispatch('loginUser', store.state.oauth.token)
|
|
||||||
}
|
|
||||||
loaded = true
|
loaded = true
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("Couldn't load state")
|
console.log("Couldn't load state")
|
||||||
|
|
|
@ -433,13 +433,6 @@ const statuses = {
|
||||||
// Optimistic favoriting...
|
// Optimistic favoriting...
|
||||||
commit('setFavorited', { status, value: true })
|
commit('setFavorited', { status, value: true })
|
||||||
apiService.favorite({ id: status.id, credentials: rootState.users.currentUser.credentials })
|
apiService.favorite({ id: status.id, credentials: rootState.users.currentUser.credentials })
|
||||||
.then(response => {
|
|
||||||
if (response.ok) {
|
|
||||||
return response.json()
|
|
||||||
} else {
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(status => {
|
.then(status => {
|
||||||
commit('setFavoritedConfirm', { status })
|
commit('setFavoritedConfirm', { status })
|
||||||
})
|
})
|
||||||
|
@ -448,13 +441,6 @@ const statuses = {
|
||||||
// Optimistic favoriting...
|
// Optimistic favoriting...
|
||||||
commit('setFavorited', { status, value: false })
|
commit('setFavorited', { status, value: false })
|
||||||
apiService.unfavorite({ id: status.id, credentials: rootState.users.currentUser.credentials })
|
apiService.unfavorite({ id: status.id, credentials: rootState.users.currentUser.credentials })
|
||||||
.then(response => {
|
|
||||||
if (response.ok) {
|
|
||||||
return response.json()
|
|
||||||
} else {
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then(status => {
|
.then(status => {
|
||||||
commit('setFavoritedConfirm', { status })
|
commit('setFavoritedConfirm', { status })
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
|
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
|
||||||
import { compact, map, each, merge, find } from 'lodash'
|
import { compact, map, each, merge, find, last } from 'lodash'
|
||||||
import { set } from 'vue'
|
import { set } from 'vue'
|
||||||
import { registerPushNotifications, unregisterPushNotifications } from '../services/push/push.js'
|
import { registerPushNotifications, unregisterPushNotifications } from '../services/push/push.js'
|
||||||
import oauthApi from '../services/new_api/oauth'
|
import oauthApi from '../services/new_api/oauth'
|
||||||
|
@ -52,23 +52,23 @@ export const mutations = {
|
||||||
state.loggingIn = false
|
state.loggingIn = false
|
||||||
},
|
},
|
||||||
// TODO Clean after ourselves?
|
// TODO Clean after ourselves?
|
||||||
addFriends (state, { id, friends, page }) {
|
addFriends (state, { id, friends }) {
|
||||||
const user = state.usersObject[id]
|
const user = state.usersObject[id]
|
||||||
each(friends, friend => {
|
each(friends, friend => {
|
||||||
if (!find(user.friends, { id: friend.id })) {
|
if (!find(user.friends, { id: friend.id })) {
|
||||||
user.friends.push(friend)
|
user.friends.push(friend)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
user.friendsPage = page + 1
|
user.lastFriendId = last(friends).id
|
||||||
},
|
},
|
||||||
addFollowers (state, { id, followers, page }) {
|
addFollowers (state, { id, followers }) {
|
||||||
const user = state.usersObject[id]
|
const user = state.usersObject[id]
|
||||||
each(followers, follower => {
|
each(followers, follower => {
|
||||||
if (!find(user.followers, { id: follower.id })) {
|
if (!find(user.followers, { id: follower.id })) {
|
||||||
user.followers.push(follower)
|
user.followers.push(follower)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
user.followersPage = page + 1
|
user.lastFollowerId = last(followers).id
|
||||||
},
|
},
|
||||||
// Because frontend doesn't have a reason to keep these stuff in memory
|
// Because frontend doesn't have a reason to keep these stuff in memory
|
||||||
// outside of viewing someones user profile.
|
// outside of viewing someones user profile.
|
||||||
|
@ -78,7 +78,7 @@ export const mutations = {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
user.friends = []
|
user.friends = []
|
||||||
user.friendsPage = 0
|
user.lastFriendId = null
|
||||||
},
|
},
|
||||||
clearFollowers (state, userId) {
|
clearFollowers (state, userId) {
|
||||||
const user = state.usersObject[userId]
|
const user = state.usersObject[userId]
|
||||||
|
@ -86,7 +86,7 @@ export const mutations = {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
user.followers = []
|
user.followers = []
|
||||||
user.followersPage = 0
|
user.lastFollowerId = null
|
||||||
},
|
},
|
||||||
addNewUsers (state, users) {
|
addNewUsers (state, users) {
|
||||||
each(users, (user) => mergeOrAdd(state.users, state.usersObject, user))
|
each(users, (user) => mergeOrAdd(state.users, state.usersObject, user))
|
||||||
|
@ -219,10 +219,10 @@ const users = {
|
||||||
addFriends ({ rootState, commit }, fetchBy) {
|
addFriends ({ rootState, commit }, fetchBy) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const user = rootState.users.usersObject[fetchBy]
|
const user = rootState.users.usersObject[fetchBy]
|
||||||
const page = user.friendsPage || 1
|
const maxId = user.lastFriendId
|
||||||
rootState.api.backendInteractor.fetchFriends({ id: user.id, page })
|
rootState.api.backendInteractor.fetchFriends({ id: user.id, maxId })
|
||||||
.then((friends) => {
|
.then((friends) => {
|
||||||
commit('addFriends', { id: user.id, friends, page })
|
commit('addFriends', { id: user.id, friends })
|
||||||
resolve(friends)
|
resolve(friends)
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
reject()
|
reject()
|
||||||
|
@ -231,10 +231,10 @@ const users = {
|
||||||
},
|
},
|
||||||
addFollowers ({ rootState, commit }, fetchBy) {
|
addFollowers ({ rootState, commit }, fetchBy) {
|
||||||
const user = rootState.users.usersObject[fetchBy]
|
const user = rootState.users.usersObject[fetchBy]
|
||||||
const page = user.followersPage || 1
|
const maxId = user.lastFollowerId
|
||||||
return rootState.api.backendInteractor.fetchFollowers({ id: user.id, page })
|
return rootState.api.backendInteractor.fetchFollowers({ id: user.id, maxId })
|
||||||
.then((followers) => {
|
.then((followers) => {
|
||||||
commit('addFollowers', { id: user.id, followers, page })
|
commit('addFollowers', { id: user.id, followers })
|
||||||
return followers
|
return followers
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,21 +1,7 @@
|
||||||
/* eslint-env browser */
|
/* eslint-env browser */
|
||||||
const LOGIN_URL = '/api/account/verify_credentials.json'
|
const LOGIN_URL = '/api/account/verify_credentials.json'
|
||||||
const FRIENDS_TIMELINE_URL = '/api/statuses/friends_timeline.json'
|
|
||||||
const ALL_FOLLOWING_URL = '/api/qvitter/allfollowing'
|
const ALL_FOLLOWING_URL = '/api/qvitter/allfollowing'
|
||||||
const PUBLIC_TIMELINE_URL = '/api/statuses/public_timeline.json'
|
|
||||||
const PUBLIC_AND_EXTERNAL_TIMELINE_URL = '/api/statuses/public_and_external_timeline.json'
|
|
||||||
const TAG_TIMELINE_URL = '/api/statusnet/tags/timeline'
|
|
||||||
const FAVORITE_URL = '/api/favorites/create'
|
|
||||||
const UNFAVORITE_URL = '/api/favorites/destroy'
|
|
||||||
const RETWEET_URL = '/api/statuses/retweet'
|
|
||||||
const UNRETWEET_URL = '/api/statuses/unretweet'
|
|
||||||
const STATUS_DELETE_URL = '/api/statuses/destroy'
|
|
||||||
const MENTIONS_URL = '/api/statuses/mentions.json'
|
const MENTIONS_URL = '/api/statuses/mentions.json'
|
||||||
const DM_TIMELINE_URL = '/api/statuses/dm_timeline.json'
|
|
||||||
const FOLLOWERS_URL = '/api/statuses/followers.json'
|
|
||||||
const FRIENDS_URL = '/api/statuses/friends.json'
|
|
||||||
const FOLLOWING_URL = '/api/friendships/create.json'
|
|
||||||
const UNFOLLOWING_URL = '/api/friendships/destroy.json'
|
|
||||||
const REGISTRATION_URL = '/api/account/register.json'
|
const REGISTRATION_URL = '/api/account/register.json'
|
||||||
const AVATAR_UPDATE_URL = '/api/qvitter/update_avatar.json'
|
const AVATAR_UPDATE_URL = '/api/qvitter/update_avatar.json'
|
||||||
const BG_UPDATE_URL = '/api/qvitter/update_background_image.json'
|
const BG_UPDATE_URL = '/api/qvitter/update_background_image.json'
|
||||||
|
@ -33,11 +19,24 @@ const SUGGESTIONS_URL = '/api/v1/suggestions'
|
||||||
|
|
||||||
const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites'
|
const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites'
|
||||||
const MASTODON_USER_NOTIFICATIONS_URL = '/api/v1/notifications'
|
const MASTODON_USER_NOTIFICATIONS_URL = '/api/v1/notifications'
|
||||||
|
const MASTODON_FAVORITE_URL = id => `/api/v1/statuses/${id}/favourite`
|
||||||
|
const MASTODON_UNFAVORITE_URL = id => `/api/v1/statuses/${id}/unfavourite`
|
||||||
|
const MASTODON_RETWEET_URL = id => `/api/v1/statuses/${id}/reblog`
|
||||||
|
const MASTODON_UNRETWEET_URL = id => `/api/v1/statuses/${id}/unreblog`
|
||||||
|
const MASTODON_DELETE_URL = id => `/api/v1/statuses/${id}`
|
||||||
|
const MASTODON_FOLLOW_URL = id => `/api/v1/accounts/${id}/follow`
|
||||||
|
const MASTODON_UNFOLLOW_URL = id => `/api/v1/accounts/${id}/unfollow`
|
||||||
|
const MASTODON_FOLLOWING_URL = id => `/api/v1/accounts/${id}/following`
|
||||||
|
const MASTODON_FOLLOWERS_URL = id => `/api/v1/accounts/${id}/followers`
|
||||||
|
const MASTODON_DIRECT_MESSAGES_TIMELINE_URL = '/api/v1/timelines/direct'
|
||||||
|
const MASTODON_PUBLIC_TIMELINE = '/api/v1/timelines/public'
|
||||||
|
const MASTODON_USER_HOME_TIMELINE_URL = '/api/v1/timelines/home'
|
||||||
const MASTODON_STATUS_URL = id => `/api/v1/statuses/${id}`
|
const MASTODON_STATUS_URL = id => `/api/v1/statuses/${id}`
|
||||||
const MASTODON_STATUS_CONTEXT_URL = id => `/api/v1/statuses/${id}/context`
|
const MASTODON_STATUS_CONTEXT_URL = id => `/api/v1/statuses/${id}/context`
|
||||||
const MASTODON_USER_URL = '/api/v1/accounts'
|
const MASTODON_USER_URL = '/api/v1/accounts'
|
||||||
const MASTODON_USER_RELATIONSHIPS_URL = '/api/v1/accounts/relationships'
|
const MASTODON_USER_RELATIONSHIPS_URL = '/api/v1/accounts/relationships'
|
||||||
const MASTODON_USER_TIMELINE_URL = id => `/api/v1/accounts/${id}/statuses`
|
const MASTODON_USER_TIMELINE_URL = id => `/api/v1/accounts/${id}/statuses`
|
||||||
|
const MASTODON_TAG_TIMELINE_URL = tag => `/api/v1/timelines/tag/${tag}`
|
||||||
const MASTODON_USER_BLOCKS_URL = '/api/v1/blocks/'
|
const MASTODON_USER_BLOCKS_URL = '/api/v1/blocks/'
|
||||||
const MASTODON_USER_MUTES_URL = '/api/v1/mutes/'
|
const MASTODON_USER_MUTES_URL = '/api/v1/mutes/'
|
||||||
const MASTODON_BLOCK_USER_URL = id => `/api/v1/accounts/${id}/block`
|
const MASTODON_BLOCK_USER_URL = id => `/api/v1/accounts/${id}/block`
|
||||||
|
@ -211,7 +210,7 @@ const externalProfile = ({profileUrl, credentials}) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const followUser = ({id, credentials}) => {
|
const followUser = ({id, credentials}) => {
|
||||||
let url = `${FOLLOWING_URL}?user_id=${id}`
|
let url = MASTODON_FOLLOW_URL(id)
|
||||||
return fetch(url, {
|
return fetch(url, {
|
||||||
headers: authHeaders(credentials),
|
headers: authHeaders(credentials),
|
||||||
method: 'POST'
|
method: 'POST'
|
||||||
|
@ -219,7 +218,7 @@ const followUser = ({id, credentials}) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const unfollowUser = ({id, credentials}) => {
|
const unfollowUser = ({id, credentials}) => {
|
||||||
let url = `${UNFOLLOWING_URL}?user_id=${id}`
|
let url = MASTODON_UNFOLLOW_URL(id)
|
||||||
return fetch(url, {
|
return fetch(url, {
|
||||||
headers: authHeaders(credentials),
|
headers: authHeaders(credentials),
|
||||||
method: 'POST'
|
method: 'POST'
|
||||||
|
@ -276,28 +275,36 @@ const fetchUserRelationship = ({id, credentials}) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchFriends = ({id, page, credentials}) => {
|
const fetchFriends = ({id, maxId, sinceId, limit = 20, credentials}) => {
|
||||||
let url = `${FRIENDS_URL}?user_id=${id}`
|
let url = MASTODON_FOLLOWING_URL(id)
|
||||||
if (page) {
|
const args = [
|
||||||
url = url + `&page=${page}`
|
maxId && `max_id=${maxId}`,
|
||||||
}
|
sinceId && `since_id=${sinceId}`,
|
||||||
|
limit && `limit=${limit}`
|
||||||
|
].filter(_ => _).join('&')
|
||||||
|
|
||||||
|
url = url + (args ? '?' + args : '')
|
||||||
return fetch(url, { headers: authHeaders(credentials) })
|
return fetch(url, { headers: authHeaders(credentials) })
|
||||||
.then((data) => data.json())
|
.then((data) => data.json())
|
||||||
.then((data) => data.map(parseUser))
|
.then((data) => data.map(parseUser))
|
||||||
}
|
}
|
||||||
|
|
||||||
const exportFriends = ({id, credentials}) => {
|
const exportFriends = ({id, credentials}) => {
|
||||||
let url = `${FRIENDS_URL}?user_id=${id}&all=true`
|
let url = MASTODON_FOLLOWING_URL(id) + `?all=true`
|
||||||
return fetch(url, { headers: authHeaders(credentials) })
|
return fetch(url, { headers: authHeaders(credentials) })
|
||||||
.then((data) => data.json())
|
.then((data) => data.json())
|
||||||
.then((data) => data.map(parseUser))
|
.then((data) => data.map(parseUser))
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchFollowers = ({id, page, credentials}) => {
|
const fetchFollowers = ({id, maxId, sinceId, limit = 20, credentials}) => {
|
||||||
let url = `${FOLLOWERS_URL}?user_id=${id}`
|
let url = MASTODON_FOLLOWERS_URL(id)
|
||||||
if (page) {
|
const args = [
|
||||||
url = url + `&page=${page}`
|
maxId && `max_id=${maxId}`,
|
||||||
}
|
sinceId && `since_id=${sinceId}`,
|
||||||
|
limit && `limit=${limit}`
|
||||||
|
].filter(_ => _).join('&')
|
||||||
|
|
||||||
|
url += args ? '?' + args : ''
|
||||||
return fetch(url, { headers: authHeaders(credentials) })
|
return fetch(url, { headers: authHeaders(credentials) })
|
||||||
.then((data) => data.json())
|
.then((data) => data.json())
|
||||||
.then((data) => data.map(parseUser))
|
.then((data) => data.map(parseUser))
|
||||||
|
@ -347,16 +354,16 @@ const fetchStatus = ({id, credentials}) => {
|
||||||
|
|
||||||
const fetchTimeline = ({timeline, credentials, since = false, until = false, userId = false, tag = false, withMuted = false}) => {
|
const fetchTimeline = ({timeline, credentials, since = false, until = false, userId = false, tag = false, withMuted = false}) => {
|
||||||
const timelineUrls = {
|
const timelineUrls = {
|
||||||
public: PUBLIC_TIMELINE_URL,
|
public: MASTODON_PUBLIC_TIMELINE,
|
||||||
friends: FRIENDS_TIMELINE_URL,
|
friends: MASTODON_USER_HOME_TIMELINE_URL,
|
||||||
mentions: MENTIONS_URL,
|
mentions: MENTIONS_URL,
|
||||||
dms: DM_TIMELINE_URL,
|
dms: MASTODON_DIRECT_MESSAGES_TIMELINE_URL,
|
||||||
notifications: MASTODON_USER_NOTIFICATIONS_URL,
|
notifications: MASTODON_USER_NOTIFICATIONS_URL,
|
||||||
'publicAndExternal': PUBLIC_AND_EXTERNAL_TIMELINE_URL,
|
'publicAndExternal': MASTODON_PUBLIC_TIMELINE,
|
||||||
user: MASTODON_USER_TIMELINE_URL,
|
user: MASTODON_USER_TIMELINE_URL,
|
||||||
media: MASTODON_USER_TIMELINE_URL,
|
media: MASTODON_USER_TIMELINE_URL,
|
||||||
favorites: MASTODON_USER_FAVORITES_TIMELINE_URL,
|
favorites: MASTODON_USER_FAVORITES_TIMELINE_URL,
|
||||||
tag: TAG_TIMELINE_URL
|
tag: MASTODON_TAG_TIMELINE_URL
|
||||||
}
|
}
|
||||||
const isNotifications = timeline === 'notifications'
|
const isNotifications = timeline === 'notifications'
|
||||||
const params = []
|
const params = []
|
||||||
|
@ -374,11 +381,17 @@ const fetchTimeline = ({timeline, credentials, since = false, until = false, use
|
||||||
params.push(['max_id', until])
|
params.push(['max_id', until])
|
||||||
}
|
}
|
||||||
if (tag) {
|
if (tag) {
|
||||||
url += `/${tag}.json`
|
url = url(tag)
|
||||||
}
|
}
|
||||||
if (timeline === 'media') {
|
if (timeline === 'media') {
|
||||||
params.push(['only_media', 1])
|
params.push(['only_media', 1])
|
||||||
}
|
}
|
||||||
|
if (timeline === 'public') {
|
||||||
|
params.push(['local', true])
|
||||||
|
}
|
||||||
|
if (timeline === 'public' || timeline === 'publicAndExternal') {
|
||||||
|
params.push(['only_media', false])
|
||||||
|
}
|
||||||
|
|
||||||
params.push(['count', 20])
|
params.push(['count', 20])
|
||||||
params.push(['with_muted', withMuted])
|
params.push(['with_muted', withMuted])
|
||||||
|
@ -415,31 +428,63 @@ const verifyCredentials = (user) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const favorite = ({ id, credentials }) => {
|
const favorite = ({ id, credentials }) => {
|
||||||
return fetch(`${FAVORITE_URL}/${id}.json`, {
|
return fetch(MASTODON_FAVORITE_URL(id), {
|
||||||
headers: authHeaders(credentials),
|
headers: authHeaders(credentials),
|
||||||
method: 'POST'
|
method: 'POST'
|
||||||
})
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (response.ok) {
|
||||||
|
return response.json()
|
||||||
|
} else {
|
||||||
|
throw new Error('Error favoriting post')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((data) => parseStatus(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
const unfavorite = ({ id, credentials }) => {
|
const unfavorite = ({ id, credentials }) => {
|
||||||
return fetch(`${UNFAVORITE_URL}/${id}.json`, {
|
return fetch(MASTODON_UNFAVORITE_URL(id), {
|
||||||
headers: authHeaders(credentials),
|
headers: authHeaders(credentials),
|
||||||
method: 'POST'
|
method: 'POST'
|
||||||
})
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (response.ok) {
|
||||||
|
return response.json()
|
||||||
|
} else {
|
||||||
|
throw new Error('Error removing favorite')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((data) => parseStatus(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
const retweet = ({ id, credentials }) => {
|
const retweet = ({ id, credentials }) => {
|
||||||
return fetch(`${RETWEET_URL}/${id}.json`, {
|
return fetch(MASTODON_RETWEET_URL(id), {
|
||||||
headers: authHeaders(credentials),
|
headers: authHeaders(credentials),
|
||||||
method: 'POST'
|
method: 'POST'
|
||||||
})
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (response.ok) {
|
||||||
|
return response.json()
|
||||||
|
} else {
|
||||||
|
throw new Error('Error repeating post')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((data) => parseStatus(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
const unretweet = ({ id, credentials }) => {
|
const unretweet = ({ id, credentials }) => {
|
||||||
return fetch(`${UNRETWEET_URL}/${id}.json`, {
|
return fetch(MASTODON_UNRETWEET_URL(id), {
|
||||||
headers: authHeaders(credentials),
|
headers: authHeaders(credentials),
|
||||||
method: 'POST'
|
method: 'POST'
|
||||||
})
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (response.ok) {
|
||||||
|
return response.json()
|
||||||
|
} else {
|
||||||
|
throw new Error('Error removing repeat')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((data) => parseStatus(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
const postStatus = ({credentials, status, spoilerText, visibility, sensitive, mediaIds = [], inReplyToStatusId, contentType}) => {
|
const postStatus = ({credentials, status, spoilerText, visibility, sensitive, mediaIds = [], inReplyToStatusId, contentType}) => {
|
||||||
|
@ -476,9 +521,9 @@ const postStatus = ({credentials, status, spoilerText, visibility, sensitive, me
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteStatus = ({ id, credentials }) => {
|
const deleteStatus = ({ id, credentials }) => {
|
||||||
return fetch(`${STATUS_DELETE_URL}/${id}.json`, {
|
return fetch(MASTODON_DELETE_URL(id), {
|
||||||
headers: authHeaders(credentials),
|
headers: authHeaders(credentials),
|
||||||
method: 'POST'
|
method: 'DELETE'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,16 +10,16 @@ const backendInteractorService = (credentials) => {
|
||||||
return apiService.fetchConversation({id, credentials})
|
return apiService.fetchConversation({id, credentials})
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchFriends = ({id, page}) => {
|
const fetchFriends = ({id, maxId, sinceId, limit}) => {
|
||||||
return apiService.fetchFriends({id, page, credentials})
|
return apiService.fetchFriends({id, maxId, sinceId, limit, credentials})
|
||||||
}
|
}
|
||||||
|
|
||||||
const exportFriends = ({id}) => {
|
const exportFriends = ({id}) => {
|
||||||
return apiService.exportFriends({id, credentials})
|
return apiService.exportFriends({id, credentials})
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchFollowers = ({id, page}) => {
|
const fetchFollowers = ({id, maxId, sinceId, limit}) => {
|
||||||
return apiService.fetchFollowers({id, page, credentials})
|
return apiService.fetchFollowers({id, maxId, sinceId, limit, credentials})
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchAllFollowing = ({username}) => {
|
const fetchAllFollowing = ({username}) => {
|
||||||
|
|
|
@ -177,6 +177,7 @@ export const parseStatus = (data) => {
|
||||||
|
|
||||||
output.in_reply_to_status_id = data.in_reply_to_id
|
output.in_reply_to_status_id = data.in_reply_to_id
|
||||||
output.in_reply_to_user_id = data.in_reply_to_account_id
|
output.in_reply_to_user_id = data.in_reply_to_account_id
|
||||||
|
output.replies_count = data.replies_count
|
||||||
|
|
||||||
// Missing!! fix in UI?
|
// Missing!! fix in UI?
|
||||||
// output.in_reply_to_screen_name = ???
|
// output.in_reply_to_screen_name = ???
|
||||||
|
|
|
@ -19,7 +19,7 @@ const fetchUser = (attempt, user, store) => new Promise((resolve, reject) => {
|
||||||
export const requestFollow = (user, store) => new Promise((resolve, reject) => {
|
export const requestFollow = (user, store) => new Promise((resolve, reject) => {
|
||||||
store.state.api.backendInteractor.followUser(user.id)
|
store.state.api.backendInteractor.followUser(user.id)
|
||||||
.then((updated) => {
|
.then((updated) => {
|
||||||
store.commit('addNewUsers', [updated])
|
store.commit('updateUserRelationship', [updated])
|
||||||
|
|
||||||
// For locked users we just mark it that we sent the follow request
|
// For locked users we just mark it that we sent the follow request
|
||||||
if (updated.locked) {
|
if (updated.locked) {
|
||||||
|
@ -66,7 +66,7 @@ export const requestFollow = (user, store) => new Promise((resolve, reject) => {
|
||||||
export const requestUnfollow = (user, store) => new Promise((resolve, reject) => {
|
export const requestUnfollow = (user, store) => new Promise((resolve, reject) => {
|
||||||
store.state.api.backendInteractor.unfollowUser(user.id)
|
store.state.api.backendInteractor.unfollowUser(user.id)
|
||||||
.then((updated) => {
|
.then((updated) => {
|
||||||
store.commit('addNewUsers', [updated])
|
store.commit('updateUserRelationship', [updated])
|
||||||
resolve({
|
resolve({
|
||||||
updated
|
updated
|
||||||
})
|
})
|
||||||
|
|
74
src/services/gesture_service/gesture_service.js
Normal file
74
src/services/gesture_service/gesture_service.js
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
|
||||||
|
const DIRECTION_LEFT = [-1, 0]
|
||||||
|
const DIRECTION_RIGHT = [1, 0]
|
||||||
|
const DIRECTION_UP = [0, -1]
|
||||||
|
const DIRECTION_DOWN = [0, 1]
|
||||||
|
|
||||||
|
const deltaCoord = (oldCoord, newCoord) => [newCoord[0] - oldCoord[0], newCoord[1] - oldCoord[1]]
|
||||||
|
|
||||||
|
const touchEventCoord = e => ([e.touches[0].screenX, e.touches[0].screenY])
|
||||||
|
|
||||||
|
const vectorLength = v => Math.sqrt(v[0] * v[0] + v[1] * v[1])
|
||||||
|
|
||||||
|
const perpendicular = v => [v[1], -v[0]]
|
||||||
|
|
||||||
|
const dotProduct = (v1, v2) => v1[0] * v2[0] + v1[1] * v2[1]
|
||||||
|
|
||||||
|
const project = (v1, v2) => {
|
||||||
|
const scalar = (dotProduct(v1, v2) / dotProduct(v2, v2))
|
||||||
|
return [scalar * v2[0], scalar * v2[1]]
|
||||||
|
}
|
||||||
|
|
||||||
|
// direction: either use the constants above or an arbitrary 2d vector.
|
||||||
|
// threshold: how many Px to move from touch origin before checking if the
|
||||||
|
// callback should be called.
|
||||||
|
// divergentTolerance: a scalar for much of divergent direction we tolerate when
|
||||||
|
// above threshold. for example, with 1.0 we only call the callback if
|
||||||
|
// divergent component of delta is < 1.0 * direction component of delta.
|
||||||
|
const swipeGesture = (direction, onSwipe, threshold = 30, perpendicularTolerance = 1.0) => {
|
||||||
|
return {
|
||||||
|
direction,
|
||||||
|
onSwipe,
|
||||||
|
threshold,
|
||||||
|
perpendicularTolerance,
|
||||||
|
_startPos: [0, 0],
|
||||||
|
_swiping: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const beginSwipe = (event, gesture) => {
|
||||||
|
gesture._startPos = touchEventCoord(event)
|
||||||
|
gesture._swiping = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateSwipe = (event, gesture) => {
|
||||||
|
if (!gesture._swiping) return
|
||||||
|
// movement too small
|
||||||
|
const delta = deltaCoord(gesture._startPos, touchEventCoord(event))
|
||||||
|
if (vectorLength(delta) < gesture.threshold) return
|
||||||
|
// movement is opposite from direction
|
||||||
|
if (dotProduct(delta, gesture.direction) < 0) return
|
||||||
|
// movement perpendicular to direction is too much
|
||||||
|
const towardsDir = project(delta, gesture.direction)
|
||||||
|
const perpendicularDir = perpendicular(gesture.direction)
|
||||||
|
const towardsPerpendicular = project(delta, perpendicularDir)
|
||||||
|
if (
|
||||||
|
vectorLength(towardsDir) * gesture.perpendicularTolerance <
|
||||||
|
vectorLength(towardsPerpendicular)
|
||||||
|
) return
|
||||||
|
|
||||||
|
gesture.onSwipe()
|
||||||
|
gesture._swiping = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const GestureService = {
|
||||||
|
DIRECTION_LEFT,
|
||||||
|
DIRECTION_RIGHT,
|
||||||
|
DIRECTION_UP,
|
||||||
|
DIRECTION_DOWN,
|
||||||
|
swipeGesture,
|
||||||
|
beginSwipe,
|
||||||
|
updateSwipe
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GestureService
|
120
test/unit/specs/services/gesture_service/gesture_service.spec.js
Normal file
120
test/unit/specs/services/gesture_service/gesture_service.spec.js
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
import GestureService from 'src/services/gesture_service/gesture_service.js'
|
||||||
|
|
||||||
|
const mockTouchEvent = (x, y) => ({
|
||||||
|
touches: [
|
||||||
|
{
|
||||||
|
screenX: x,
|
||||||
|
screenY: y
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
describe.only('GestureService', () => {
|
||||||
|
describe('swipeGesture', () => {
|
||||||
|
it('calls the callback on a successful swipe', () => {
|
||||||
|
let swiped = false
|
||||||
|
const callback = () => { swiped = true }
|
||||||
|
const gesture = GestureService.swipeGesture(
|
||||||
|
GestureService.DIRECTION_RIGHT,
|
||||||
|
callback
|
||||||
|
)
|
||||||
|
|
||||||
|
GestureService.beginSwipe(mockTouchEvent(100, 100), gesture)
|
||||||
|
GestureService.updateSwipe(mockTouchEvent(200, 100), gesture)
|
||||||
|
|
||||||
|
expect(swiped).to.eql(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('calls the callback only once per begin', () => {
|
||||||
|
let hits = 0
|
||||||
|
const callback = () => { hits += 1 }
|
||||||
|
const gesture = GestureService.swipeGesture(
|
||||||
|
GestureService.DIRECTION_RIGHT,
|
||||||
|
callback
|
||||||
|
)
|
||||||
|
|
||||||
|
GestureService.beginSwipe(mockTouchEvent(100, 100), gesture)
|
||||||
|
GestureService.updateSwipe(mockTouchEvent(150, 100), gesture)
|
||||||
|
GestureService.updateSwipe(mockTouchEvent(200, 100), gesture)
|
||||||
|
|
||||||
|
expect(hits).to.eql(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('doesn\'t call the callback on an opposite swipe', () => {
|
||||||
|
let swiped = false
|
||||||
|
const callback = () => { swiped = true }
|
||||||
|
const gesture = GestureService.swipeGesture(
|
||||||
|
GestureService.DIRECTION_RIGHT,
|
||||||
|
callback
|
||||||
|
)
|
||||||
|
|
||||||
|
GestureService.beginSwipe(mockTouchEvent(100, 100), gesture)
|
||||||
|
GestureService.updateSwipe(mockTouchEvent(0, 100), gesture)
|
||||||
|
|
||||||
|
expect(swiped).to.eql(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('doesn\'t call the callback on a swipe below threshold', () => {
|
||||||
|
let swiped = false
|
||||||
|
const callback = () => { swiped = true }
|
||||||
|
const gesture = GestureService.swipeGesture(
|
||||||
|
GestureService.DIRECTION_RIGHT,
|
||||||
|
callback,
|
||||||
|
100
|
||||||
|
)
|
||||||
|
|
||||||
|
GestureService.beginSwipe(mockTouchEvent(100, 100), gesture)
|
||||||
|
GestureService.updateSwipe(mockTouchEvent(150, 100), gesture)
|
||||||
|
|
||||||
|
expect(swiped).to.eql(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('doesn\'t call the callback on a perpendicular swipe', () => {
|
||||||
|
let swiped = false
|
||||||
|
const callback = () => { swiped = true }
|
||||||
|
const gesture = GestureService.swipeGesture(
|
||||||
|
GestureService.DIRECTION_RIGHT,
|
||||||
|
callback,
|
||||||
|
30,
|
||||||
|
0.5
|
||||||
|
)
|
||||||
|
|
||||||
|
GestureService.beginSwipe(mockTouchEvent(100, 100), gesture)
|
||||||
|
GestureService.updateSwipe(mockTouchEvent(150, 200), gesture)
|
||||||
|
|
||||||
|
expect(swiped).to.eql(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('calls the callback on perpendicular swipe if within tolerance', () => {
|
||||||
|
let swiped = false
|
||||||
|
const callback = () => { swiped = true }
|
||||||
|
const gesture = GestureService.swipeGesture(
|
||||||
|
GestureService.DIRECTION_RIGHT,
|
||||||
|
callback,
|
||||||
|
30,
|
||||||
|
2.0
|
||||||
|
)
|
||||||
|
|
||||||
|
GestureService.beginSwipe(mockTouchEvent(100, 100), gesture)
|
||||||
|
GestureService.updateSwipe(mockTouchEvent(150, 150), gesture)
|
||||||
|
|
||||||
|
expect(swiped).to.eql(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('works with any arbitrary 2d directions', () => {
|
||||||
|
let swiped = false
|
||||||
|
const callback = () => { swiped = true }
|
||||||
|
const gesture = GestureService.swipeGesture(
|
||||||
|
[-1, -1],
|
||||||
|
callback,
|
||||||
|
30,
|
||||||
|
0.1
|
||||||
|
)
|
||||||
|
|
||||||
|
GestureService.beginSwipe(mockTouchEvent(100, 100), gesture)
|
||||||
|
GestureService.updateSwipe(mockTouchEvent(60, 60), gesture)
|
||||||
|
|
||||||
|
expect(swiped).to.eql(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
Loading…
Reference in a new issue