Merge develop and fix conflict
This commit is contained in:
commit
0ab828bb30
25 changed files with 2489 additions and 312 deletions
|
@ -24,10 +24,6 @@ const afterStoreSetup = ({ store, i18n }) => {
|
||||||
store.dispatch('setInstanceOption', { name: 'bannerlimit', value: parseInt(uploadlimit.bannerlimit) })
|
store.dispatch('setInstanceOption', { name: 'bannerlimit', value: parseInt(uploadlimit.bannerlimit) })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.nsfwCensorImage) {
|
|
||||||
store.dispatch('setInstanceOption', { name: 'nsfwCensorImage', value: data.nsfwCensorImage })
|
|
||||||
}
|
|
||||||
|
|
||||||
if (vapidPublicKey) {
|
if (vapidPublicKey) {
|
||||||
store.dispatch('setInstanceOption', { name: 'vapidPublicKey', value: vapidPublicKey })
|
store.dispatch('setInstanceOption', { name: 'vapidPublicKey', value: vapidPublicKey })
|
||||||
}
|
}
|
||||||
|
@ -54,46 +50,44 @@ const afterStoreSetup = ({ store, i18n }) => {
|
||||||
config = Object.assign({}, staticConfig, apiConfig)
|
config = Object.assign({}, staticConfig, apiConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
var theme = (config.theme)
|
const copyInstanceOption = (name) => {
|
||||||
var background = (config.background)
|
store.dispatch('setInstanceOption', {name, value: config[name]})
|
||||||
var hidePostStats = (config.hidePostStats)
|
}
|
||||||
var hideUserStats = (config.hideUserStats)
|
|
||||||
var logo = (config.logo)
|
|
||||||
var logoMask = (typeof config.logoMask === 'undefined' ? true : config.logoMask)
|
|
||||||
var logoMargin = (typeof config.logoMargin === 'undefined' ? 0 : config.logoMargin)
|
|
||||||
var redirectRootNoLogin = (config.redirectRootNoLogin)
|
|
||||||
var redirectRootLogin = (config.redirectRootLogin)
|
|
||||||
var chatDisabled = (config.chatDisabled)
|
|
||||||
var showInstanceSpecificPanel = (config.showInstanceSpecificPanel)
|
|
||||||
var scopeOptionsEnabled = (config.scopeOptionsEnabled)
|
|
||||||
var formattingOptionsEnabled = (config.formattingOptionsEnabled)
|
|
||||||
var collapseMessageWithSubject = (config.collapseMessageWithSubject)
|
|
||||||
var loginMethod = (config.loginMethod)
|
|
||||||
var scopeCopy = (config.scopeCopy)
|
|
||||||
var subjectLineBehavior = (config.subjectLineBehavior)
|
|
||||||
var alwaysShowSubjectInput = (config.alwaysShowSubjectInput)
|
|
||||||
var noAttachmentLinks = (config.noAttachmentLinks)
|
|
||||||
|
|
||||||
store.dispatch('setInstanceOption', { name: 'theme', value: theme })
|
copyInstanceOption('nsfwCensorImage')
|
||||||
store.dispatch('setInstanceOption', { name: 'background', value: background })
|
copyInstanceOption('theme')
|
||||||
store.dispatch('setInstanceOption', { name: 'hidePostStats', value: hidePostStats })
|
copyInstanceOption('background')
|
||||||
store.dispatch('setInstanceOption', { name: 'hideUserStats', value: hideUserStats })
|
copyInstanceOption('hidePostStats')
|
||||||
store.dispatch('setInstanceOption', { name: 'logo', value: logo })
|
copyInstanceOption('hideUserStats')
|
||||||
store.dispatch('setInstanceOption', { name: 'logoMask', value: logoMask })
|
copyInstanceOption('logo')
|
||||||
store.dispatch('setInstanceOption', { name: 'logoMargin', value: logoMargin })
|
|
||||||
store.dispatch('setInstanceOption', { name: 'redirectRootNoLogin', value: redirectRootNoLogin })
|
|
||||||
store.dispatch('setInstanceOption', { name: 'redirectRootLogin', value: redirectRootLogin })
|
|
||||||
store.dispatch('setInstanceOption', { name: 'showInstanceSpecificPanel', value: showInstanceSpecificPanel })
|
|
||||||
store.dispatch('setInstanceOption', { name: 'scopeOptionsEnabled', value: scopeOptionsEnabled })
|
|
||||||
store.dispatch('setInstanceOption', { name: 'formattingOptionsEnabled', value: formattingOptionsEnabled })
|
|
||||||
store.dispatch('setInstanceOption', { name: 'collapseMessageWithSubject', value: collapseMessageWithSubject })
|
|
||||||
store.dispatch('setInstanceOption', { name: 'loginMethod', value: loginMethod })
|
|
||||||
store.dispatch('setInstanceOption', { name: 'scopeCopy', value: scopeCopy })
|
|
||||||
store.dispatch('setInstanceOption', { name: 'subjectLineBehavior', value: subjectLineBehavior })
|
|
||||||
store.dispatch('setInstanceOption', { name: 'alwaysShowSubjectInput', value: alwaysShowSubjectInput })
|
|
||||||
store.dispatch('setInstanceOption', { name: 'noAttachmentLinks', value: noAttachmentLinks })
|
|
||||||
|
|
||||||
if (chatDisabled) {
|
store.dispatch('setInstanceOption', {
|
||||||
|
name: 'logoMask',
|
||||||
|
value: typeof config.logoMask === 'undefined'
|
||||||
|
? true
|
||||||
|
: config.logoMask
|
||||||
|
})
|
||||||
|
|
||||||
|
store.dispatch('setInstanceOption', {
|
||||||
|
name: 'logoMargin',
|
||||||
|
value: typeof config.logoMargin === 'undefined'
|
||||||
|
? 0
|
||||||
|
: config.logoMargin
|
||||||
|
})
|
||||||
|
|
||||||
|
copyInstanceOption('redirectRootNoLogin')
|
||||||
|
copyInstanceOption('redirectRootLogin')
|
||||||
|
copyInstanceOption('showInstanceSpecificPanel')
|
||||||
|
copyInstanceOption('scopeOptionsEnabled')
|
||||||
|
copyInstanceOption('formattingOptionsEnabled')
|
||||||
|
copyInstanceOption('collapseMessageWithSubject')
|
||||||
|
copyInstanceOption('loginMethod')
|
||||||
|
copyInstanceOption('scopeCopy')
|
||||||
|
copyInstanceOption('subjectLineBehavior')
|
||||||
|
copyInstanceOption('alwaysShowSubjectInput')
|
||||||
|
copyInstanceOption('noAttachmentLinks')
|
||||||
|
|
||||||
|
if ((config.chatDisabled)) {
|
||||||
store.dispatch('disableChat')
|
store.dispatch('disableChat')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ const Attachment = {
|
||||||
],
|
],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
nsfwImage: this.$store.state.config.nsfwCensorImage || nsfwImage,
|
nsfwImage: this.$store.state.instance.nsfwCensorImage || nsfwImage,
|
||||||
hideNsfwLocal: this.$store.state.config.hideNsfw,
|
hideNsfwLocal: this.$store.state.config.hideNsfw,
|
||||||
preloadImage: this.$store.state.config.preloadImage,
|
preloadImage: this.$store.state.config.preloadImage,
|
||||||
loading: false,
|
loading: false,
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
:href="attachment.url" target="_blank"
|
:href="attachment.url" target="_blank"
|
||||||
:title="attachment.description"
|
:title="attachment.description"
|
||||||
>
|
>
|
||||||
<StillImage referrerpolicy="referrerPolicy" :mimetype="attachment.mimetype" :src="attachment.large_thumb_url || attachment.url"/>
|
<StillImage :referrerpolicy="referrerpolicy" :mimetype="attachment.mimetype" :src="attachment.large_thumb_url || attachment.url"/>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a class="video-container"
|
<a class="video-container"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import Conversation from '../conversation/conversation.vue'
|
import Conversation from '../conversation/conversation.vue'
|
||||||
import { find, toInteger } from 'lodash'
|
import { find } from 'lodash'
|
||||||
|
|
||||||
const conversationPage = {
|
const conversationPage = {
|
||||||
components: {
|
components: {
|
||||||
|
@ -7,7 +7,7 @@ const conversationPage = {
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
statusoid () {
|
statusoid () {
|
||||||
const id = toInteger(this.$route.params.id)
|
const id = this.$route.params.id
|
||||||
const statuses = this.$store.state.statuses.allStatuses
|
const statuses = this.$store.state.statuses.allStatuses
|
||||||
const status = find(statuses, {id})
|
const status = find(statuses, {id})
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,25 @@
|
||||||
import { reduce, filter, sortBy } from 'lodash'
|
import { reduce, filter } from 'lodash'
|
||||||
import { statusType } from '../../modules/statuses.js'
|
|
||||||
import Status from '../status/status.vue'
|
import Status from '../status/status.vue'
|
||||||
|
|
||||||
|
const sortById = (a, b) => {
|
||||||
|
const seqA = Number(a.id)
|
||||||
|
const seqB = Number(b.id)
|
||||||
|
const isSeqA = !Number.isNaN(seqA)
|
||||||
|
const isSeqB = !Number.isNaN(seqB)
|
||||||
|
if (isSeqA && isSeqB) {
|
||||||
|
return seqA < seqB ? -1 : 1
|
||||||
|
} else if (isSeqA && !isSeqB) {
|
||||||
|
return 1
|
||||||
|
} else if (!isSeqA && isSeqB) {
|
||||||
|
return -1
|
||||||
|
} else {
|
||||||
|
return a.id < b.id ? -1 : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const sortAndFilterConversation = (conversation) => {
|
const sortAndFilterConversation = (conversation) => {
|
||||||
conversation = filter(conversation, (status) => statusType(status) !== 'retweet')
|
conversation = filter(conversation, (status) => status.type !== 'retweet')
|
||||||
return sortBy(conversation, 'id')
|
return conversation.filter(_ => _).sort(sortById)
|
||||||
}
|
}
|
||||||
|
|
||||||
const conversation = {
|
const conversation = {
|
||||||
|
@ -18,10 +33,12 @@ const conversation = {
|
||||||
'collapsable'
|
'collapsable'
|
||||||
],
|
],
|
||||||
computed: {
|
computed: {
|
||||||
status () { return this.statusoid },
|
status () {
|
||||||
|
return this.statusoid
|
||||||
|
},
|
||||||
conversation () {
|
conversation () {
|
||||||
if (!this.status) {
|
if (!this.status) {
|
||||||
return false
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
const conversationId = this.status.statusnet_conversation_id
|
const conversationId = this.status.statusnet_conversation_id
|
||||||
|
@ -32,7 +49,9 @@ const conversation = {
|
||||||
replies () {
|
replies () {
|
||||||
let i = 1
|
let i = 1
|
||||||
return reduce(this.conversation, (result, {id, in_reply_to_status_id}) => {
|
return reduce(this.conversation, (result, {id, in_reply_to_status_id}) => {
|
||||||
const irid = Number(in_reply_to_status_id)
|
/* eslint-disable camelcase */
|
||||||
|
const irid = in_reply_to_status_id
|
||||||
|
/* eslint-enable camelcase */
|
||||||
if (irid) {
|
if (irid) {
|
||||||
result[irid] = result[irid] || []
|
result[irid] = result[irid] || []
|
||||||
result[irid].push({
|
result[irid].push({
|
||||||
|
@ -69,7 +88,6 @@ const conversation = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getReplies (id) {
|
getReplies (id) {
|
||||||
id = Number(id)
|
|
||||||
return this.replies[id] || []
|
return this.replies[id] || []
|
||||||
},
|
},
|
||||||
focused (id) {
|
focused (id) {
|
||||||
|
@ -80,7 +98,7 @@ const conversation = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setHighlight (id) {
|
setHighlight (id) {
|
||||||
this.highlight = Number(id)
|
this.highlight = id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -76,6 +76,14 @@ const Status = {
|
||||||
return (this.$store.state.config.hideAttachments && !this.inConversation) ||
|
return (this.$store.state.config.hideAttachments && !this.inConversation) ||
|
||||||
(this.$store.state.config.hideAttachmentsInConv && this.inConversation)
|
(this.$store.state.config.hideAttachmentsInConv && this.inConversation)
|
||||||
},
|
},
|
||||||
|
userProfileLink () {
|
||||||
|
return this.generateUserProfileLink(this.status.user.id, this.status.user.screen_name)
|
||||||
|
},
|
||||||
|
replyProfileLink () {
|
||||||
|
if (this.isReply) {
|
||||||
|
return this.generateUserProfileLink(this.status.in_reply_to_status_id, this.replyToName)
|
||||||
|
}
|
||||||
|
},
|
||||||
retweet () { return !!this.statusoid.retweeted_status },
|
retweet () { return !!this.statusoid.retweeted_status },
|
||||||
retweeter () { return this.statusoid.user.name },
|
retweeter () { return this.statusoid.user.name },
|
||||||
retweeterHtml () { return this.statusoid.user.name_html },
|
retweeterHtml () { return this.statusoid.user.name_html },
|
||||||
|
@ -122,6 +130,14 @@ const Status = {
|
||||||
isReply () {
|
isReply () {
|
||||||
return !!this.status.in_reply_to_status_id
|
return !!this.status.in_reply_to_status_id
|
||||||
},
|
},
|
||||||
|
replyToName () {
|
||||||
|
const user = this.$store.state.users.usersObject[this.status.in_reply_to_user_id]
|
||||||
|
if (user) {
|
||||||
|
return user.screen_name
|
||||||
|
} else {
|
||||||
|
return this.status.in_reply_to_screen_name
|
||||||
|
}
|
||||||
|
},
|
||||||
hideReply () {
|
hideReply () {
|
||||||
if (this.$store.state.config.replyVisibility === 'all') {
|
if (this.$store.state.config.replyVisibility === 'all') {
|
||||||
return false
|
return false
|
||||||
|
@ -132,7 +148,7 @@ const Status = {
|
||||||
if (this.status.user.id === this.$store.state.users.currentUser.id) {
|
if (this.status.user.id === this.$store.state.users.currentUser.id) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if (this.status.activity_type === 'repeat') {
|
if (this.status.type === 'retweet') {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
var checkFollowing = this.$store.state.config.replyVisibility === 'following'
|
var checkFollowing = this.$store.state.config.replyVisibility === 'following'
|
||||||
|
@ -281,7 +297,7 @@ const Status = {
|
||||||
},
|
},
|
||||||
replyEnter (id, event) {
|
replyEnter (id, event) {
|
||||||
this.showPreview = true
|
this.showPreview = true
|
||||||
const targetId = Number(id)
|
const targetId = id
|
||||||
const statuses = this.$store.state.statuses.allStatuses
|
const statuses = this.$store.state.statuses.allStatuses
|
||||||
|
|
||||||
if (!this.preview) {
|
if (!this.preview) {
|
||||||
|
@ -300,7 +316,7 @@ const Status = {
|
||||||
replyLeave () {
|
replyLeave () {
|
||||||
this.showPreview = false
|
this.showPreview = false
|
||||||
},
|
},
|
||||||
userProfileLink (id, name) {
|
generateUserProfileLink (id, name) {
|
||||||
return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames)
|
return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames)
|
||||||
},
|
},
|
||||||
setMedia () {
|
setMedia () {
|
||||||
|
@ -310,7 +326,6 @@ const Status = {
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
'highlight': function (id) {
|
'highlight': function (id) {
|
||||||
id = Number(id)
|
|
||||||
if (this.status.id === id) {
|
if (this.status.id === id) {
|
||||||
let rect = this.$el.getBoundingClientRect()
|
let rect = this.$el.getBoundingClientRect()
|
||||||
if (rect.top < 100) {
|
if (rect.top < 100) {
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<template v-if="muted && !noReplyLinks">
|
<template v-if="muted && !noReplyLinks">
|
||||||
<div class="media status container muted">
|
<div class="media status container muted">
|
||||||
<small>
|
<small>
|
||||||
<router-link :to="userProfileLink(status.user.id, status.user.screen_name)">
|
<router-link :to="userProfileLink">
|
||||||
{{status.user.screen_name}}
|
{{status.user.screen_name}}
|
||||||
</router-link>
|
</router-link>
|
||||||
</small>
|
</small>
|
||||||
|
@ -38,16 +38,16 @@
|
||||||
<h4 class="user-name" v-if="status.user.name_html" v-html="status.user.name_html"></h4>
|
<h4 class="user-name" v-if="status.user.name_html" v-html="status.user.name_html"></h4>
|
||||||
<h4 class="user-name" v-else>{{status.user.name}}</h4>
|
<h4 class="user-name" v-else>{{status.user.name}}</h4>
|
||||||
<span class="links">
|
<span class="links">
|
||||||
<router-link :to="userProfileLink(status.user.id, status.user.screen_name)">
|
<router-link :to="userProfileLink">
|
||||||
{{status.user.screen_name}}
|
{{status.user.screen_name}}
|
||||||
</router-link>
|
</router-link>
|
||||||
<span v-if="status.in_reply_to_screen_name" class="faint reply-info">
|
<span v-if="isReply" class="faint reply-info">
|
||||||
<i class="icon-right-open"></i>
|
<i class="icon-right-open"></i>
|
||||||
<router-link :to="userProfileLink(status.in_reply_to_user_id, status.in_reply_to_screen_name)">
|
<router-link :to="replyProfileLink">
|
||||||
{{status.in_reply_to_screen_name}}
|
{{replyToName}}
|
||||||
</router-link>
|
</router-link>
|
||||||
</span>
|
</span>
|
||||||
<a v-if="isReply && !noReplyLinks" href="#" @click.prevent="gotoOriginal(status.in_reply_to_status_id)" :title="$t('tool_tip.reply')">
|
<a v-if="isReply && !noReplyLinks" href="#" @click.prevent="gotoOriginal(status.in_reply_to_status_id)" :aria-label="$t('tool_tip.reply')">
|
||||||
<i class="button-icon icon-reply" @mouseenter="replyEnter(status.in_reply_to_status_id, $event)" @mouseout="replyLeave()"></i>
|
<i class="button-icon icon-reply" @mouseenter="replyEnter(status.in_reply_to_status_id, $event)" @mouseout="replyLeave()"></i>
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -6,18 +6,26 @@ export default Vue.component('tab-switcher', {
|
||||||
name: 'TabSwitcher',
|
name: 'TabSwitcher',
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
active: 0
|
active: this.$slots.default.findIndex(_ => _.tag)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
activateTab(index) {
|
activateTab (index) {
|
||||||
return () => this.active = index;
|
return () => {
|
||||||
|
this.active = index
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
render(h) {
|
beforeUpdate () {
|
||||||
|
const currentSlot = this.$slots.default[this.active]
|
||||||
|
if (!currentSlot.tag) {
|
||||||
|
this.active = this.$slots.default.findIndex(_ => _.tag)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
render (h) {
|
||||||
const tabs = this.$slots.default
|
const tabs = this.$slots.default
|
||||||
.filter(slot => slot.data)
|
|
||||||
.map((slot, index) => {
|
.map((slot, index) => {
|
||||||
|
if (!slot.tag) return
|
||||||
const classesTab = ['tab']
|
const classesTab = ['tab']
|
||||||
const classesWrapper = ['tab-wrapper']
|
const classesWrapper = ['tab-wrapper']
|
||||||
|
|
||||||
|
@ -25,20 +33,24 @@ export default Vue.component('tab-switcher', {
|
||||||
classesTab.push('active')
|
classesTab.push('active')
|
||||||
classesWrapper.push('active')
|
classesWrapper.push('active')
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={ classesWrapper.join(' ')}>
|
<div class={ classesWrapper.join(' ')}>
|
||||||
<button onClick={this.activateTab(index)} class={ classesTab.join(' ') }>{slot.data.attrs.label}</button>
|
<button onClick={this.activateTab(index)} class={ classesTab.join(' ') }>{slot.data.attrs.label}</button>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
});
|
})
|
||||||
const contents = this.$slots.default.filter(_=>_.data).map(( slot, index ) => {
|
|
||||||
|
const contents = this.$slots.default.map((slot, index) => {
|
||||||
|
if (!slot.tag) return
|
||||||
const active = index === this.active
|
const active = index === this.active
|
||||||
return (
|
return (
|
||||||
<div class={active ? 'active' : 'hidden'}>
|
<div class={active ? 'active' : 'hidden'}>
|
||||||
{slot}
|
{slot}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
});
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="tab-switcher">
|
<div class="tab-switcher">
|
||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
|
|
|
@ -5,24 +5,33 @@ import Timeline from '../timeline/timeline.vue'
|
||||||
const UserProfile = {
|
const UserProfile = {
|
||||||
created () {
|
created () {
|
||||||
this.$store.commit('clearTimeline', { timeline: 'user' })
|
this.$store.commit('clearTimeline', { timeline: 'user' })
|
||||||
|
this.$store.commit('clearTimeline', { timeline: 'favorites' })
|
||||||
this.$store.dispatch('startFetching', ['user', this.fetchBy])
|
this.$store.dispatch('startFetching', ['user', this.fetchBy])
|
||||||
|
this.$store.dispatch('startFetching', ['favorites', this.fetchBy])
|
||||||
if (!this.user.id) {
|
if (!this.user.id) {
|
||||||
this.$store.dispatch('fetchUser', this.fetchBy)
|
this.$store.dispatch('fetchUser', this.fetchBy)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
destroyed () {
|
destroyed () {
|
||||||
this.$store.dispatch('stopFetching', 'user')
|
this.$store.dispatch('stopFetching', 'user')
|
||||||
|
this.$store.dispatch('stopFetching', 'favorites')
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
timeline () {
|
timeline () {
|
||||||
return this.$store.state.statuses.timelines.user
|
return this.$store.state.statuses.timelines.user
|
||||||
},
|
},
|
||||||
|
favorites () {
|
||||||
|
return this.$store.state.statuses.timelines.favorites
|
||||||
|
},
|
||||||
userId () {
|
userId () {
|
||||||
return this.$route.params.id || this.user.id
|
return this.$route.params.id || this.user.id
|
||||||
},
|
},
|
||||||
userName () {
|
userName () {
|
||||||
return this.$route.params.name || this.user.screen_name
|
return this.$route.params.name || this.user.screen_name
|
||||||
},
|
},
|
||||||
|
isUs () {
|
||||||
|
return this.userId === this.$store.state.users.currentUser.id
|
||||||
|
},
|
||||||
friends () {
|
friends () {
|
||||||
return this.user.friends
|
return this.user.friends
|
||||||
},
|
},
|
||||||
|
@ -62,21 +71,28 @@ const UserProfile = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
// TODO get rid of this copypasta
|
||||||
userName () {
|
userName () {
|
||||||
if (this.isExternal) {
|
if (this.isExternal) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.$store.dispatch('stopFetching', 'user')
|
this.$store.dispatch('stopFetching', 'user')
|
||||||
|
this.$store.dispatch('stopFetching', 'favorites')
|
||||||
this.$store.commit('clearTimeline', { timeline: 'user' })
|
this.$store.commit('clearTimeline', { timeline: 'user' })
|
||||||
|
this.$store.commit('clearTimeline', { timeline: 'favorites' })
|
||||||
this.$store.dispatch('startFetching', ['user', this.fetchBy])
|
this.$store.dispatch('startFetching', ['user', this.fetchBy])
|
||||||
|
this.$store.dispatch('startFetching', ['favorites', this.fetchBy])
|
||||||
},
|
},
|
||||||
userId () {
|
userId () {
|
||||||
if (!this.isExternal) {
|
if (!this.isExternal) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.$store.dispatch('stopFetching', 'user')
|
this.$store.dispatch('stopFetching', 'user')
|
||||||
|
this.$store.dispatch('stopFetching', 'favorites')
|
||||||
this.$store.commit('clearTimeline', { timeline: 'user' })
|
this.$store.commit('clearTimeline', { timeline: 'user' })
|
||||||
|
this.$store.commit('clearTimeline', { timeline: 'favorites' })
|
||||||
this.$store.dispatch('startFetching', ['user', this.fetchBy])
|
this.$store.dispatch('startFetching', ['user', this.fetchBy])
|
||||||
|
this.$store.dispatch('startFetching', ['favorites', this.fetchBy])
|
||||||
},
|
},
|
||||||
user () {
|
user () {
|
||||||
if (this.user.id && !this.user.followers) {
|
if (this.user.id && !this.user.followers) {
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
<i class="icon-spin3 animate-spin"></i>
|
<i class="icon-spin3 animate-spin"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<Timeline v-if="isUs" :label="$t('user_card.favorites')" :embedded="true" :title="$t('user_profile.favorites_title')" timeline-name="favorites" :timeline="favorites"/>
|
||||||
</tab-switcher>
|
</tab-switcher>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="panel user-profile-placeholder">
|
<div v-else class="panel user-profile-placeholder">
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import apiService from '../../services/api/api.service.js'
|
import apiService from '../../services/api/api.service.js'
|
||||||
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
|
||||||
import _ from 'lodash'
|
import { shuffle } from 'lodash'
|
||||||
|
|
||||||
function showWhoToFollow (panel, reply) {
|
function showWhoToFollow (panel, reply) {
|
||||||
_.shuffle(reply)
|
const shuffled = shuffle(reply)
|
||||||
|
|
||||||
panel.usersToFollow.forEach((toFollow, index) => {
|
panel.usersToFollow.forEach((toFollow, index) => {
|
||||||
let user = reply[index]
|
let user = shuffled[index]
|
||||||
let img = user.avatar || '/images/avi.png'
|
let img = user.avatar || '/images/avi.png'
|
||||||
let name = user.acct
|
let name = user.acct
|
||||||
|
|
||||||
|
|
|
@ -327,6 +327,7 @@
|
||||||
"block": "Block",
|
"block": "Block",
|
||||||
"blocked": "Blocked!",
|
"blocked": "Blocked!",
|
||||||
"deny": "Deny",
|
"deny": "Deny",
|
||||||
|
"favorites": "Favorites",
|
||||||
"follow": "Follow",
|
"follow": "Follow",
|
||||||
"follow_sent": "Request sent!",
|
"follow_sent": "Request sent!",
|
||||||
"follow_progress": "Requesting…",
|
"follow_progress": "Requesting…",
|
||||||
|
|
|
@ -279,6 +279,7 @@
|
||||||
"user_card": {
|
"user_card": {
|
||||||
"block": "Заблокировать",
|
"block": "Заблокировать",
|
||||||
"blocked": "Заблокирован",
|
"blocked": "Заблокирован",
|
||||||
|
"favorites": "Понравившиеся",
|
||||||
"follow": "Читать",
|
"follow": "Читать",
|
||||||
"follow_sent": "Запрос отправлен!",
|
"follow_sent": "Запрос отправлен!",
|
||||||
"follow_progress": "Запрашиваем…",
|
"follow_progress": "Запрашиваем…",
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { includes, remove, slice, sortBy, toInteger, each, find, flatten, maxBy, minBy, merge, last, isArray } from 'lodash'
|
import { remove, slice, each, find, maxBy, minBy, merge, last, isArray } from 'lodash'
|
||||||
import apiService from '../services/api/api.service.js'
|
import apiService from '../services/api/api.service.js'
|
||||||
// import parse from '../services/status_parser/status_parser.js'
|
// import parse from '../services/status_parser/status_parser.js'
|
||||||
|
|
||||||
const emptyTl = (userId = 0) => ({
|
const emptyTl = () => ({
|
||||||
statuses: [],
|
statuses: [],
|
||||||
statusesObject: {},
|
statusesObject: {},
|
||||||
faves: [],
|
faves: [],
|
||||||
|
@ -14,8 +14,8 @@ const emptyTl = (userId = 0) => ({
|
||||||
loading: false,
|
loading: false,
|
||||||
followers: [],
|
followers: [],
|
||||||
friends: [],
|
friends: [],
|
||||||
flushMarker: 0,
|
userId: 0,
|
||||||
userId
|
flushMarker: 0
|
||||||
})
|
})
|
||||||
|
|
||||||
export const defaultState = {
|
export const defaultState = {
|
||||||
|
@ -36,6 +36,7 @@ export const defaultState = {
|
||||||
mentions: emptyTl(),
|
mentions: emptyTl(),
|
||||||
public: emptyTl(),
|
public: emptyTl(),
|
||||||
user: emptyTl(),
|
user: emptyTl(),
|
||||||
|
favorites: emptyTl(),
|
||||||
publicAndExternal: emptyTl(),
|
publicAndExternal: emptyTl(),
|
||||||
friends: emptyTl(),
|
friends: emptyTl(),
|
||||||
tag: emptyTl(),
|
tag: emptyTl(),
|
||||||
|
@ -43,20 +44,7 @@ export const defaultState = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const isNsfw = (status) => {
|
|
||||||
const nsfwRegex = /#nsfw/i
|
|
||||||
return includes(status.tags, 'nsfw') || !!status.text.match(nsfwRegex)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const prepareStatus = (status) => {
|
export const prepareStatus = (status) => {
|
||||||
// Parse nsfw tags
|
|
||||||
if (status.nsfw === undefined) {
|
|
||||||
status.nsfw = isNsfw(status)
|
|
||||||
if (status.retweeted_status) {
|
|
||||||
status.nsfw = status.retweeted_status.nsfw
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set deleted flag
|
// Set deleted flag
|
||||||
status.deleted = false
|
status.deleted = false
|
||||||
|
|
||||||
|
@ -75,35 +63,6 @@ const visibleNotificationTypes = (rootState) => {
|
||||||
].filter(_ => _)
|
].filter(_ => _)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const statusType = (status) => {
|
|
||||||
if (status.is_post_verb) {
|
|
||||||
return 'status'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status.retweeted_status) {
|
|
||||||
return 'retweet'
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((typeof status.uri === 'string' && status.uri.match(/(fave|objectType=Favourite)/)) ||
|
|
||||||
(typeof status.text === 'string' && status.text.match(/favorited/))) {
|
|
||||||
return 'favorite'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status.text.match(/deleted notice {{tag/) || status.qvitter_delete_notice) {
|
|
||||||
return 'deletion'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status.text.match(/started following/) || status.activity_type === 'follow') {
|
|
||||||
return 'follow'
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'unknown'
|
|
||||||
}
|
|
||||||
|
|
||||||
export const findMaxId = (...args) => {
|
|
||||||
return (maxBy(flatten(args), 'id') || {}).id
|
|
||||||
}
|
|
||||||
|
|
||||||
const mergeOrAdd = (arr, obj, item) => {
|
const mergeOrAdd = (arr, obj, item) => {
|
||||||
const oldItem = obj[item.id]
|
const oldItem = obj[item.id]
|
||||||
|
|
||||||
|
@ -122,9 +81,25 @@ const mergeOrAdd = (arr, obj, item) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sortById = (a, b) => {
|
||||||
|
const seqA = Number(a.id)
|
||||||
|
const seqB = Number(b.id)
|
||||||
|
const isSeqA = !Number.isNaN(seqA)
|
||||||
|
const isSeqB = !Number.isNaN(seqB)
|
||||||
|
if (isSeqA && isSeqB) {
|
||||||
|
return seqA > seqB ? -1 : 1
|
||||||
|
} else if (isSeqA && !isSeqB) {
|
||||||
|
return 1
|
||||||
|
} else if (!isSeqA && isSeqB) {
|
||||||
|
return -1
|
||||||
|
} else {
|
||||||
|
return a.id > b.id ? -1 : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const sortTimeline = (timeline) => {
|
const sortTimeline = (timeline) => {
|
||||||
timeline.visibleStatuses = sortBy(timeline.visibleStatuses, ({id}) => -id)
|
timeline.visibleStatuses = timeline.visibleStatuses.sort(sortById)
|
||||||
timeline.statuses = sortBy(timeline.statuses, ({id}) => -id)
|
timeline.statuses = timeline.statuses.sort(sortById)
|
||||||
timeline.minVisibleId = (last(timeline.visibleStatuses) || {}).id
|
timeline.minVisibleId = (last(timeline.visibleStatuses) || {}).id
|
||||||
return timeline
|
return timeline
|
||||||
}
|
}
|
||||||
|
@ -153,13 +128,13 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const addStatus = (status, showImmediately, addToTimeline = true) => {
|
const addStatus = (data, showImmediately, addToTimeline = true) => {
|
||||||
const result = mergeOrAdd(allStatuses, allStatusesObject, status)
|
const result = mergeOrAdd(allStatuses, allStatusesObject, data)
|
||||||
status = result.item
|
const status = result.item
|
||||||
|
|
||||||
if (result.new) {
|
if (result.new) {
|
||||||
// We are mentioned in a post
|
// We are mentioned in a post
|
||||||
if (statusType(status) === 'status' && find(status.attentions, { id: user.id })) {
|
if (status.type === 'status' && find(status.attentions, { id: user.id })) {
|
||||||
const mentions = state.timelines.mentions
|
const mentions = state.timelines.mentions
|
||||||
|
|
||||||
// Add the mention to the mentions timeline
|
// Add the mention to the mentions timeline
|
||||||
|
@ -200,7 +175,7 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
|
||||||
}
|
}
|
||||||
|
|
||||||
const favoriteStatus = (favorite, counter) => {
|
const favoriteStatus = (favorite, counter) => {
|
||||||
const status = find(allStatuses, { id: toInteger(favorite.in_reply_to_status_id) })
|
const status = find(allStatuses, { id: favorite.in_reply_to_status_id })
|
||||||
if (status) {
|
if (status) {
|
||||||
// This is our favorite, so the relevant bit.
|
// This is our favorite, so the relevant bit.
|
||||||
if (favorite.user.id === user.id) {
|
if (favorite.user.id === user.id) {
|
||||||
|
@ -263,6 +238,9 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
|
||||||
remove(timelineObject.visibleStatuses, { uri })
|
remove(timelineObject.visibleStatuses, { uri })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
'follow': (follow) => {
|
||||||
|
// NOOP, it is known status but we don't do anything about it for now
|
||||||
|
},
|
||||||
'default': (unknown) => {
|
'default': (unknown) => {
|
||||||
console.log('unknown status type')
|
console.log('unknown status type')
|
||||||
console.log(unknown)
|
console.log(unknown)
|
||||||
|
@ -270,12 +248,12 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
|
||||||
}
|
}
|
||||||
|
|
||||||
each(statuses, (status) => {
|
each(statuses, (status) => {
|
||||||
const type = statusType(status)
|
const type = status.type
|
||||||
const processor = processors[type] || processors['default']
|
const processor = processors[type] || processors['default']
|
||||||
processor(status)
|
processor(status)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Keep the visible statuses sorted
|
// Keep the visible statuses sorted
|
||||||
if (timeline) {
|
if (timeline) {
|
||||||
sortTimeline(timelineObject)
|
sortTimeline(timelineObject)
|
||||||
if ((older || timelineObject.minVisibleId <= 0) && statuses.length > 0) {
|
if ((older || timelineObject.minVisibleId <= 0) && statuses.length > 0) {
|
||||||
|
@ -288,42 +266,36 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot
|
||||||
const allStatuses = state.allStatuses
|
const allStatuses = state.allStatuses
|
||||||
const allStatusesObject = state.allStatusesObject
|
const allStatusesObject = state.allStatusesObject
|
||||||
each(notifications, (notification) => {
|
each(notifications, (notification) => {
|
||||||
const result = mergeOrAdd(allStatuses, allStatusesObject, notification.notice)
|
notification.action = mergeOrAdd(allStatuses, allStatusesObject, notification.action).item
|
||||||
const action = result.item
|
notification.status = notification.status && mergeOrAdd(allStatuses, allStatusesObject, notification.status).item
|
||||||
|
|
||||||
// Only add a new notification if we don't have one for the same action
|
// Only add a new notification if we don't have one for the same action
|
||||||
if (!find(state.notifications.data, (oldNotification) => oldNotification.action.id === action.id)) {
|
if (!state.notifications.idStore.hasOwnProperty(notification.id)) {
|
||||||
state.notifications.maxId = Math.max(notification.id, state.notifications.maxId)
|
state.notifications.maxId = notification.id > state.notifications.maxId
|
||||||
state.notifications.minId = Math.min(notification.id, state.notifications.minId)
|
? notification.id
|
||||||
|
: state.notifications.maxId
|
||||||
|
state.notifications.minId = notification.id < state.notifications.minId
|
||||||
|
? notification.id
|
||||||
|
: state.notifications.minId
|
||||||
|
|
||||||
const fresh = !notification.is_seen
|
state.notifications.data.push(notification)
|
||||||
const status = notification.ntype === 'like'
|
state.notifications.idStore[notification.id] = notification
|
||||||
? action.favorited_status
|
|
||||||
: action
|
|
||||||
|
|
||||||
const result = {
|
|
||||||
type: notification.ntype,
|
|
||||||
status,
|
|
||||||
action,
|
|
||||||
seen: !fresh
|
|
||||||
}
|
|
||||||
|
|
||||||
state.notifications.data.push(result)
|
|
||||||
state.notifications.idStore[notification.id] = result
|
|
||||||
|
|
||||||
if ('Notification' in window && window.Notification.permission === 'granted') {
|
if ('Notification' in window && window.Notification.permission === 'granted') {
|
||||||
|
const notifObj = {}
|
||||||
|
const action = notification.action
|
||||||
const title = action.user.name
|
const title = action.user.name
|
||||||
const result = {}
|
notifObj.icon = action.user.profile_image_url
|
||||||
result.icon = action.user.profile_image_url
|
notifObj.body = action.text // there's a problem that it doesn't put a space before links tho
|
||||||
result.body = action.text // there's a problem that it doesn't put a space before links tho
|
|
||||||
|
|
||||||
// Shows first attached non-nsfw image, if any. Should add configuration for this somehow...
|
// Shows first attached non-nsfw image, if any. Should add configuration for this somehow...
|
||||||
if (action.attachments && action.attachments.length > 0 && !action.nsfw &&
|
if (action.attachments && action.attachments.length > 0 && !action.nsfw &&
|
||||||
action.attachments[0].mimetype.startsWith('image/')) {
|
action.attachments[0].mimetype.startsWith('image/')) {
|
||||||
result.image = action.attachments[0].url
|
notifObj.image = action.attachments[0].url
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fresh && !state.notifications.desktopNotificationSilence && visibleNotificationTypes.includes(notification.ntype)) {
|
if (notification.fresh && !state.notifications.desktopNotificationSilence && visibleNotificationTypes.includes(notification.ntype)) {
|
||||||
let notification = new window.Notification(title, result)
|
let notification = new window.Notification(title, notifObj)
|
||||||
// Chrome is known for not closing notifications automatically
|
// Chrome is known for not closing notifications automatically
|
||||||
// according to MDN, anyway.
|
// according to MDN, anyway.
|
||||||
setTimeout(notification.close.bind(notification), 5000)
|
setTimeout(notification.close.bind(notification), 5000)
|
||||||
|
@ -346,7 +318,7 @@ export const mutations = {
|
||||||
each(oldTimeline.visibleStatuses, (status) => { oldTimeline.visibleStatusesObject[status.id] = status })
|
each(oldTimeline.visibleStatuses, (status) => { oldTimeline.visibleStatusesObject[status.id] = status })
|
||||||
},
|
},
|
||||||
clearTimeline (state, { timeline }) {
|
clearTimeline (state, { timeline }) {
|
||||||
state.timelines[timeline] = emptyTl(state.timelines[timeline].userId)
|
state.timelines[timeline] = emptyTl()
|
||||||
},
|
},
|
||||||
setFavorited (state, { status, value }) {
|
setFavorited (state, { status, value }) {
|
||||||
const newStatus = state.allStatusesObject[status.id]
|
const newStatus = state.allStatusesObject[status.id]
|
||||||
|
|
|
@ -68,6 +68,7 @@ export const mutations = {
|
||||||
},
|
},
|
||||||
setUserForNotification (state, notification) {
|
setUserForNotification (state, notification) {
|
||||||
notification.action.user = state.usersObject[notification.action.user.id]
|
notification.action.user = state.usersObject[notification.action.user.id]
|
||||||
|
notification.from_profile = state.usersObject[notification.action.user.id]
|
||||||
},
|
},
|
||||||
setColor (state, { user: { id }, highlighted }) {
|
setColor (state, { user: { id }, highlighted }) {
|
||||||
const user = state.usersObject[id]
|
const user = state.usersObject[id]
|
||||||
|
@ -149,8 +150,8 @@ const users = {
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
addNewNotifications (store, { notifications }) {
|
addNewNotifications (store, { notifications }) {
|
||||||
const users = compact(map(notifications, 'from_profile'))
|
const users = map(notifications, 'from_profile')
|
||||||
const notificationIds = compact(notifications.map(_ => String(_.id)))
|
const notificationIds = notifications.map(_ => _.id)
|
||||||
store.commit('addNewUsers', users)
|
store.commit('addNewUsers', users)
|
||||||
|
|
||||||
const notificationsObject = store.rootState.statuses.notifications.idStore
|
const notificationsObject = store.rootState.statuses.notifications.idStore
|
||||||
|
@ -206,39 +207,38 @@ const users = {
|
||||||
const commit = store.commit
|
const commit = store.commit
|
||||||
commit('beginLogin')
|
commit('beginLogin')
|
||||||
store.rootState.api.backendInteractor.verifyCredentials(accessToken)
|
store.rootState.api.backendInteractor.verifyCredentials(accessToken)
|
||||||
.then((response) => {
|
.then((data) => {
|
||||||
if (response.ok) {
|
if (!data.error) {
|
||||||
response.json()
|
const user = data
|
||||||
.then((user) => {
|
// user.credentials = userCredentials
|
||||||
// user.credentials = userCredentials
|
user.credentials = accessToken
|
||||||
user.credentials = accessToken
|
commit('setCurrentUser', user)
|
||||||
commit('setCurrentUser', user)
|
commit('addNewUsers', [user])
|
||||||
commit('addNewUsers', [user])
|
|
||||||
|
|
||||||
getNotificationPermission()
|
getNotificationPermission()
|
||||||
.then(permission => commit('setNotificationPermission', permission))
|
.then(permission => commit('setNotificationPermission', permission))
|
||||||
|
|
||||||
// Set our new backend interactor
|
// Set our new backend interactor
|
||||||
commit('setBackendInteractor', backendInteractorService(accessToken))
|
commit('setBackendInteractor', backendInteractorService(accessToken))
|
||||||
|
|
||||||
if (user.token) {
|
if (user.token) {
|
||||||
store.dispatch('initializeSocket', user.token)
|
store.dispatch('initializeSocket', user.token)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start getting fresh tweets.
|
// Start getting fresh tweets.
|
||||||
store.dispatch('startFetching', 'friends')
|
store.dispatch('startFetching', 'friends')
|
||||||
|
|
||||||
// Get user mutes and follower info
|
// Get user mutes and follower info
|
||||||
store.rootState.api.backendInteractor.fetchMutes().then((mutedUsers) => {
|
store.rootState.api.backendInteractor.fetchMutes().then((mutedUsers) => {
|
||||||
each(mutedUsers, (user) => { user.muted = true })
|
each(mutedUsers, (user) => { user.muted = true })
|
||||||
store.commit('addNewUsers', mutedUsers)
|
store.commit('addNewUsers', mutedUsers)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Fetch our friends
|
// Fetch our friends
|
||||||
store.rootState.api.backendInteractor.fetchFriends({ id: user.id })
|
store.rootState.api.backendInteractor.fetchFriends({ id: user.id })
|
||||||
.then((friends) => commit('addNewUsers', friends))
|
.then((friends) => commit('addNewUsers', friends))
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
|
const response = data.error
|
||||||
// Authentication failed
|
// Authentication failed
|
||||||
commit('endLogin')
|
commit('endLogin')
|
||||||
if (response.status === 401) {
|
if (response.status === 401) {
|
||||||
|
@ -250,11 +250,11 @@ const users = {
|
||||||
commit('endLogin')
|
commit('endLogin')
|
||||||
resolve()
|
resolve()
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
commit('endLogin')
|
commit('endLogin')
|
||||||
reject('Failed to connect to server, try again')
|
reject('Failed to connect to server, try again')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,7 +41,10 @@ const APPROVE_USER_URL = '/api/pleroma/friendships/approve'
|
||||||
const DENY_USER_URL = '/api/pleroma/friendships/deny'
|
const DENY_USER_URL = '/api/pleroma/friendships/deny'
|
||||||
const SUGGESTIONS_URL = '/api/v1/suggestions'
|
const SUGGESTIONS_URL = '/api/v1/suggestions'
|
||||||
|
|
||||||
|
const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites'
|
||||||
|
|
||||||
import { each, map } from 'lodash'
|
import { each, map } from 'lodash'
|
||||||
|
import { parseStatus, parseUser, parseNotification } from '../entity_normalizer/entity_normalizer.service.js'
|
||||||
import 'whatwg-fetch'
|
import 'whatwg-fetch'
|
||||||
|
|
||||||
const oldfetch = window.fetch
|
const oldfetch = window.fetch
|
||||||
|
@ -70,6 +73,7 @@ const updateAvatar = ({credentials, params}) => {
|
||||||
form.append(key, value)
|
form.append(key, value)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return fetch(url, {
|
return fetch(url, {
|
||||||
headers: authHeaders(credentials),
|
headers: authHeaders(credentials),
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
@ -87,6 +91,7 @@ const updateBg = ({credentials, params}) => {
|
||||||
form.append(key, value)
|
form.append(key, value)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return fetch(url, {
|
return fetch(url, {
|
||||||
headers: authHeaders(credentials),
|
headers: authHeaders(credentials),
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
@ -110,6 +115,7 @@ const updateBanner = ({credentials, params}) => {
|
||||||
form.append(key, value)
|
form.append(key, value)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return fetch(url, {
|
return fetch(url, {
|
||||||
headers: authHeaders(credentials),
|
headers: authHeaders(credentials),
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
@ -123,13 +129,14 @@ const updateBanner = ({credentials, params}) => {
|
||||||
// location
|
// location
|
||||||
// description
|
// description
|
||||||
const updateProfile = ({credentials, params}) => {
|
const updateProfile = ({credentials, params}) => {
|
||||||
|
// Always include these fields, because they might be empty or false
|
||||||
|
const fields = ['description', 'locked', 'no_rich_text', 'hide_network']
|
||||||
let url = PROFILE_UPDATE_URL
|
let url = PROFILE_UPDATE_URL
|
||||||
|
|
||||||
const form = new FormData()
|
const form = new FormData()
|
||||||
|
|
||||||
each(params, (value, key) => {
|
each(params, (value, key) => {
|
||||||
/* Always include description, no_rich_text and locked, because it might be empty or false */
|
if (fields.includes(key) || value) {
|
||||||
if (key === 'description' || key === 'locked' || key === 'no_rich_text' || value) {
|
|
||||||
form.append(key, value)
|
form.append(key, value)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -237,24 +244,28 @@ const fetchUser = ({id, credentials}) => {
|
||||||
let url = `${USER_URL}?user_id=${id}`
|
let url = `${USER_URL}?user_id=${id}`
|
||||||
return fetch(url, { headers: authHeaders(credentials) })
|
return fetch(url, { headers: authHeaders(credentials) })
|
||||||
.then((data) => data.json())
|
.then((data) => data.json())
|
||||||
|
.then((data) => parseUser(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchFriends = ({id, credentials}) => {
|
const fetchFriends = ({id, credentials}) => {
|
||||||
let url = `${FRIENDS_URL}?user_id=${id}`
|
let url = `${FRIENDS_URL}?user_id=${id}`
|
||||||
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))
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchFollowers = ({id, credentials}) => {
|
const fetchFollowers = ({id, credentials}) => {
|
||||||
let url = `${FOLLOWERS_URL}?user_id=${id}`
|
let url = `${FOLLOWERS_URL}?user_id=${id}`
|
||||||
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))
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchAllFollowing = ({username, credentials}) => {
|
const fetchAllFollowing = ({username, credentials}) => {
|
||||||
const url = `${ALL_FOLLOWING_URL}/${username}.json`
|
const url = `${ALL_FOLLOWING_URL}/${username}.json`
|
||||||
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))
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchFollowRequests = ({credentials}) => {
|
const fetchFollowRequests = ({credentials}) => {
|
||||||
|
@ -266,13 +277,27 @@ const fetchFollowRequests = ({credentials}) => {
|
||||||
const fetchConversation = ({id, credentials}) => {
|
const fetchConversation = ({id, credentials}) => {
|
||||||
let url = `${CONVERSATION_URL}/${id}.json?count=100`
|
let url = `${CONVERSATION_URL}/${id}.json?count=100`
|
||||||
return fetch(url, { headers: authHeaders(credentials) })
|
return fetch(url, { headers: authHeaders(credentials) })
|
||||||
|
.then((data) => {
|
||||||
|
if (data.ok) {
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
throw new Error('Error fetching timeline', data)
|
||||||
|
})
|
||||||
.then((data) => data.json())
|
.then((data) => data.json())
|
||||||
|
.then((data) => data.map(parseStatus))
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchStatus = ({id, credentials}) => {
|
const fetchStatus = ({id, credentials}) => {
|
||||||
let url = `${STATUS_URL}/${id}.json`
|
let url = `${STATUS_URL}/${id}.json`
|
||||||
return fetch(url, { headers: authHeaders(credentials) })
|
return fetch(url, { headers: authHeaders(credentials) })
|
||||||
|
.then((data) => {
|
||||||
|
if (data.ok) {
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
throw new Error('Error fetching timeline', data)
|
||||||
|
})
|
||||||
.then((data) => data.json())
|
.then((data) => data.json())
|
||||||
|
.then((data) => parseStatus(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
const setUserMute = ({id, credentials, muted = true}) => {
|
const setUserMute = ({id, credentials, muted = true}) => {
|
||||||
|
@ -300,13 +325,14 @@ const fetchTimeline = ({timeline, credentials, since = false, until = false, use
|
||||||
notifications: QVITTER_USER_NOTIFICATIONS_URL,
|
notifications: QVITTER_USER_NOTIFICATIONS_URL,
|
||||||
'publicAndExternal': PUBLIC_AND_EXTERNAL_TIMELINE_URL,
|
'publicAndExternal': PUBLIC_AND_EXTERNAL_TIMELINE_URL,
|
||||||
user: QVITTER_USER_TIMELINE_URL,
|
user: QVITTER_USER_TIMELINE_URL,
|
||||||
|
favorites: MASTODON_USER_FAVORITES_TIMELINE_URL,
|
||||||
tag: TAG_TIMELINE_URL
|
tag: TAG_TIMELINE_URL
|
||||||
}
|
}
|
||||||
|
const isNotifications = timeline === 'notifications'
|
||||||
|
const params = []
|
||||||
|
|
||||||
let url = timelineUrls[timeline]
|
let url = timelineUrls[timeline]
|
||||||
|
|
||||||
let params = []
|
|
||||||
|
|
||||||
if (since) {
|
if (since) {
|
||||||
params.push(['since_id', since])
|
params.push(['since_id', since])
|
||||||
}
|
}
|
||||||
|
@ -330,9 +356,10 @@ const fetchTimeline = ({timeline, credentials, since = false, until = false, use
|
||||||
if (data.ok) {
|
if (data.ok) {
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
throw new Error('Error fetching timeline')
|
throw new Error('Error fetching timeline', data)
|
||||||
})
|
})
|
||||||
.then((data) => data.json())
|
.then((data) => data.json())
|
||||||
|
.then((data) => data.map(isNotifications ? parseNotification : parseStatus))
|
||||||
}
|
}
|
||||||
|
|
||||||
const verifyCredentials = (user) => {
|
const verifyCredentials = (user) => {
|
||||||
|
@ -340,6 +367,16 @@ const verifyCredentials = (user) => {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: authHeaders(user)
|
headers: authHeaders(user)
|
||||||
})
|
})
|
||||||
|
.then((response) => {
|
||||||
|
if (response.ok) {
|
||||||
|
return response.json()
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
error: response
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((data) => data.error ? data : parseUser(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
const favorite = ({ id, credentials }) => {
|
const favorite = ({ id, credentials }) => {
|
||||||
|
@ -391,6 +428,16 @@ const postStatus = ({credentials, status, spoilerText, visibility, sensitive, me
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: authHeaders(credentials)
|
headers: authHeaders(credentials)
|
||||||
})
|
})
|
||||||
|
.then((response) => {
|
||||||
|
if (response.ok) {
|
||||||
|
return response.json()
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
error: response
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((data) => data.error ? data : parseStatus(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteStatus = ({ id, credentials }) => {
|
const deleteStatus = ({ id, credentials }) => {
|
||||||
|
|
273
src/services/entity_normalizer/entity_normalizer.service.js
Normal file
273
src/services/entity_normalizer/entity_normalizer.service.js
Normal file
|
@ -0,0 +1,273 @@
|
||||||
|
const qvitterStatusType = (status) => {
|
||||||
|
if (status.is_post_verb) {
|
||||||
|
return 'status'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.retweeted_status) {
|
||||||
|
return 'retweet'
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((typeof status.uri === 'string' && status.uri.match(/(fave|objectType=Favourite)/)) ||
|
||||||
|
(typeof status.text === 'string' && status.text.match(/favorited/))) {
|
||||||
|
return 'favorite'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.text.match(/deleted notice {{tag/) || status.qvitter_delete_notice) {
|
||||||
|
return 'deletion'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.text.match(/started following/) || status.activity_type === 'follow') {
|
||||||
|
return 'follow'
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const parseUser = (data) => {
|
||||||
|
const output = {}
|
||||||
|
const masto = data.hasOwnProperty('acct')
|
||||||
|
// case for users in "mentions" property for statuses in MastoAPI
|
||||||
|
const mastoShort = masto && !data.hasOwnProperty('avatar')
|
||||||
|
|
||||||
|
output.id = String(data.id)
|
||||||
|
|
||||||
|
if (masto) {
|
||||||
|
output.screen_name = data.acct
|
||||||
|
|
||||||
|
// There's nothing else to get
|
||||||
|
if (mastoShort) {
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
output.name = null // missing
|
||||||
|
output.name_html = data.display_name
|
||||||
|
|
||||||
|
output.description = null // missing
|
||||||
|
output.description_html = data.note
|
||||||
|
|
||||||
|
// Utilize avatar_static for gif avatars?
|
||||||
|
output.profile_image_url = data.avatar
|
||||||
|
output.profile_image_url_original = data.avatar
|
||||||
|
|
||||||
|
// Same, utilize header_static?
|
||||||
|
output.cover_photo = data.header
|
||||||
|
|
||||||
|
output.friends_count = data.following_count
|
||||||
|
|
||||||
|
output.bot = data.bot
|
||||||
|
|
||||||
|
output.statusnet_profile_url = data.url
|
||||||
|
|
||||||
|
if (data.pleroma) {
|
||||||
|
const pleroma = data.pleroma
|
||||||
|
output.follows_you = pleroma.follows_you
|
||||||
|
output.statusnet_blocking = pleroma.statusnet_blocking
|
||||||
|
output.muted = pleroma.muted
|
||||||
|
}
|
||||||
|
|
||||||
|
// Missing, trying to recover
|
||||||
|
output.is_local = !output.screen_name.includes('@')
|
||||||
|
} else {
|
||||||
|
output.screen_name = data.screen_name
|
||||||
|
|
||||||
|
output.name = data.name
|
||||||
|
output.name_html = data.name_html
|
||||||
|
|
||||||
|
output.description = data.description
|
||||||
|
output.description_html = data.description_html
|
||||||
|
|
||||||
|
output.profile_image_url = data.profile_image_url
|
||||||
|
output.profile_image_url_original = data.profile_image_url_original
|
||||||
|
|
||||||
|
output.cover_photo = data.cover_photo
|
||||||
|
|
||||||
|
output.friends_count = data.friends_count
|
||||||
|
|
||||||
|
output.bot = null // missing
|
||||||
|
|
||||||
|
output.statusnet_profile_url = data.statusnet_profile_url
|
||||||
|
|
||||||
|
output.statusnet_blocking = data.statusnet_blocking
|
||||||
|
|
||||||
|
output.is_local = data.is_local
|
||||||
|
|
||||||
|
output.follows_you = data.follows_you
|
||||||
|
|
||||||
|
output.muted = data.muted
|
||||||
|
|
||||||
|
// QVITTER ONLY FOR NOW
|
||||||
|
// Really only applies to logged in user, really.. I THINK
|
||||||
|
output.rights = data.rights
|
||||||
|
output.no_rich_text = data.no_rich_text
|
||||||
|
output.default_scope = data.default_scope
|
||||||
|
output.hide_network = data.hide_network
|
||||||
|
output.background_image = data.background_image
|
||||||
|
}
|
||||||
|
|
||||||
|
output.created_at = new Date(data.created_at)
|
||||||
|
output.locked = data.locked
|
||||||
|
output.followers_count = data.followers_count
|
||||||
|
output.statuses_count = data.statuses_count
|
||||||
|
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseAttachment = (data) => {
|
||||||
|
const output = {}
|
||||||
|
const masto = !data.hasOwnProperty('oembed')
|
||||||
|
|
||||||
|
if (masto) {
|
||||||
|
// Not exactly same...
|
||||||
|
output.mimetype = data.type
|
||||||
|
output.meta = data.meta // not present in BE yet
|
||||||
|
} else {
|
||||||
|
output.mimetype = data.mimetype
|
||||||
|
output.meta = null // missing
|
||||||
|
}
|
||||||
|
|
||||||
|
output.url = data.url
|
||||||
|
output.description = data.description
|
||||||
|
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
export const parseStatus = (data) => {
|
||||||
|
const output = {}
|
||||||
|
const masto = data.hasOwnProperty('account')
|
||||||
|
|
||||||
|
if (masto) {
|
||||||
|
output.favorited = data.favourited
|
||||||
|
output.fave_num = data.favourites_count
|
||||||
|
|
||||||
|
output.repeated = data.reblogged
|
||||||
|
output.repeat_num = data.reblogs_count
|
||||||
|
|
||||||
|
output.type = data.reblog ? 'retweet' : 'status'
|
||||||
|
output.nsfw = data.sensitive
|
||||||
|
|
||||||
|
output.statusnet_html = data.content
|
||||||
|
|
||||||
|
// Not exactly the same but works?
|
||||||
|
output.text = data.content
|
||||||
|
|
||||||
|
output.in_reply_to_status_id = data.in_reply_to_id
|
||||||
|
output.in_reply_to_user_id = data.in_reply_to_account_id
|
||||||
|
|
||||||
|
// Missing!! fix in UI?
|
||||||
|
output.in_reply_to_screen_name = null
|
||||||
|
|
||||||
|
// Not exactly the same but works
|
||||||
|
output.statusnet_conversation_id = data.id
|
||||||
|
|
||||||
|
if (output.type === 'retweet') {
|
||||||
|
output.retweeted_status = parseStatus(data.reblog)
|
||||||
|
}
|
||||||
|
|
||||||
|
output.summary = data.spoiler_text
|
||||||
|
output.summary_html = data.spoiler_text
|
||||||
|
output.external_url = data.url
|
||||||
|
|
||||||
|
// FIXME missing!!
|
||||||
|
output.is_local = false
|
||||||
|
} else {
|
||||||
|
output.favorited = data.favorited
|
||||||
|
output.fave_num = data.fave_num
|
||||||
|
|
||||||
|
output.repeated = data.repeated
|
||||||
|
output.repeat_num = data.repeat_num
|
||||||
|
|
||||||
|
// catchall, temporary
|
||||||
|
// Object.assign(output, data)
|
||||||
|
|
||||||
|
output.type = qvitterStatusType(data)
|
||||||
|
|
||||||
|
if (data.nsfw === undefined) {
|
||||||
|
output.nsfw = isNsfw(data)
|
||||||
|
if (data.retweeted_status) {
|
||||||
|
output.nsfw = data.retweeted_status.nsfw
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
output.nsfw = data.nsfw
|
||||||
|
}
|
||||||
|
|
||||||
|
output.statusnet_html = data.statusnet_html
|
||||||
|
output.text = data.text
|
||||||
|
|
||||||
|
output.in_reply_to_status_id = data.in_reply_to_status_id
|
||||||
|
output.in_reply_to_user_id = data.in_reply_to_user_id
|
||||||
|
output.in_reply_to_screen_name = data.in_reply_to_screen_name
|
||||||
|
|
||||||
|
output.statusnet_conversation_id = data.statusnet_conversation_id
|
||||||
|
|
||||||
|
if (output.type === 'retweet') {
|
||||||
|
output.retweeted_status = parseStatus(data.retweeted_status)
|
||||||
|
}
|
||||||
|
|
||||||
|
output.summary = data.summary
|
||||||
|
output.summary_html = data.summary_html
|
||||||
|
output.external_url = data.external_url
|
||||||
|
output.is_local = data.is_local
|
||||||
|
}
|
||||||
|
|
||||||
|
output.id = String(data.id)
|
||||||
|
output.visibility = data.visibility
|
||||||
|
output.created_at = new Date(data.created_at)
|
||||||
|
|
||||||
|
// Converting to string, the right way.
|
||||||
|
output.in_reply_to_status_id = output.in_reply_to_status_id
|
||||||
|
? String(output.in_reply_to_status_id)
|
||||||
|
: null
|
||||||
|
output.in_reply_to_user_id = output.in_reply_to_user_id
|
||||||
|
? String(output.in_reply_to_user_id)
|
||||||
|
: null
|
||||||
|
|
||||||
|
output.user = parseUser(masto ? data.account : data.user)
|
||||||
|
|
||||||
|
output.attentions = ((masto ? data.mentions : data.attentions) || []).map(parseUser)
|
||||||
|
|
||||||
|
output.attachments = ((masto ? data.media_attachments : data.attachments) || [])
|
||||||
|
.map(parseAttachment)
|
||||||
|
|
||||||
|
const retweetedStatus = masto ? data.reblog : data.retweeted_status
|
||||||
|
if (retweetedStatus) {
|
||||||
|
output.retweeted_status = parseStatus(retweetedStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
export const parseNotification = (data) => {
|
||||||
|
const mastoDict = {
|
||||||
|
'favourite': 'like',
|
||||||
|
'reblog': 'repeat'
|
||||||
|
}
|
||||||
|
const masto = !data.hasOwnProperty('ntype')
|
||||||
|
const output = {}
|
||||||
|
|
||||||
|
if (masto) {
|
||||||
|
output.type = mastoDict[data.type] || data.type
|
||||||
|
output.seen = null // missing
|
||||||
|
output.status = parseStatus(data.status)
|
||||||
|
output.action = output.status // not sure
|
||||||
|
output.from_profile = parseUser(data.account)
|
||||||
|
} else {
|
||||||
|
const parsedNotice = parseStatus(data.notice)
|
||||||
|
output.type = data.ntype
|
||||||
|
output.seen = Boolean(data.is_seen)
|
||||||
|
output.status = output.type === 'like'
|
||||||
|
? parseStatus(data.notice.favorited_status)
|
||||||
|
: parsedNotice
|
||||||
|
output.action = parsedNotice
|
||||||
|
output.from_profile = parseUser(data.from_profile)
|
||||||
|
}
|
||||||
|
|
||||||
|
output.created_at = new Date(data.created_at)
|
||||||
|
output.id = String(data.id)
|
||||||
|
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
const isNsfw = (status) => {
|
||||||
|
const nsfwRegex = /#nsfw/i
|
||||||
|
return (status.tags || []).includes('nsfw') || !!status.text.match(nsfwRegex)
|
||||||
|
}
|
|
@ -9,9 +9,25 @@ export const visibleTypes = store => ([
|
||||||
store.state.config.notificationVisibility.follows && 'follow'
|
store.state.config.notificationVisibility.follows && 'follow'
|
||||||
].filter(_ => _))
|
].filter(_ => _))
|
||||||
|
|
||||||
|
const sortById = (a, b) => {
|
||||||
|
const seqA = Number(a.action.id)
|
||||||
|
const seqB = Number(b.action.id)
|
||||||
|
const isSeqA = !Number.isNaN(seqA)
|
||||||
|
const isSeqB = !Number.isNaN(seqB)
|
||||||
|
if (isSeqA && isSeqB) {
|
||||||
|
return seqA > seqB ? -1 : 1
|
||||||
|
} else if (isSeqA && !isSeqB) {
|
||||||
|
return 1
|
||||||
|
} else if (!isSeqA && isSeqB) {
|
||||||
|
return -1
|
||||||
|
} else {
|
||||||
|
return a.action.id > b.action.id ? -1 : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const visibleNotificationsFromStore = store => {
|
export const visibleNotificationsFromStore = store => {
|
||||||
// Don't know why, but sortBy([seen, -action.id]) doesn't work.
|
// map is just to clone the array since sort mutates it and it causes some issues
|
||||||
let sortedNotifications = sortBy(notificationsFromStore(store), ({action}) => -action.id)
|
let sortedNotifications = notificationsFromStore(store).map(_ => _).sort(sortById)
|
||||||
sortedNotifications = sortBy(sortedNotifications, 'seen')
|
sortedNotifications = sortBy(sortedNotifications, 'seen')
|
||||||
return sortedNotifications.filter((notification) => visibleTypes(store).includes(notification.type))
|
return sortedNotifications.filter((notification) => visibleTypes(store).includes(notification.type))
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,6 @@ const postStatus = ({ store, status, spoilerText, visibility, sensitive, media =
|
||||||
const mediaIds = map(media, 'id')
|
const mediaIds = map(media, 'id')
|
||||||
|
|
||||||
return apiService.postStatus({credentials: store.state.users.currentUser.credentials, status, spoilerText, visibility, sensitive, mediaIds, inReplyToStatusId, contentType, noAttachmentLinks: store.state.instance.noAttachmentLinks})
|
return apiService.postStatus({credentials: store.state.users.currentUser.credentials, status, spoilerText, visibility, sensitive, mediaIds, inReplyToStatusId, contentType, noAttachmentLinks: store.state.instance.noAttachmentLinks})
|
||||||
.then((data) => data.json())
|
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (!data.error) {
|
if (!data.error) {
|
||||||
store.dispatch('addNewStatuses', {
|
store.dispatch('addNewStatuses', {
|
||||||
|
|
|
@ -18,5 +18,6 @@
|
||||||
"hideUserStats": false,
|
"hideUserStats": false,
|
||||||
"loginMethod": "password",
|
"loginMethod": "password",
|
||||||
"webPushNotifications": false,
|
"webPushNotifications": false,
|
||||||
"noAttachmentLinks": false
|
"noAttachmentLinks": false,
|
||||||
|
"nsfwCensorImage": ""
|
||||||
}
|
}
|
||||||
|
|
1582
test/fixtures/mastoapi.json
vendored
Normal file
1582
test/fixtures/mastoapi.json
vendored
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,76 +1,31 @@
|
||||||
import { cloneDeep } from 'lodash'
|
import { cloneDeep } from 'lodash'
|
||||||
import { defaultState, mutations, findMaxId, prepareStatus, statusType } from '../../../../src/modules/statuses.js'
|
import { defaultState, mutations, prepareStatus } from '../../../../src/modules/statuses.js'
|
||||||
|
|
||||||
// eslint-disable-next-line camelcase
|
// eslint-disable-next-line camelcase
|
||||||
const makeMockStatus = ({id, text, is_post_verb = true}) => {
|
const makeMockStatus = ({id, text, type = 'status'}) => {
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
user: {id: 0},
|
user: {id: '0'},
|
||||||
name: 'status',
|
name: 'status',
|
||||||
text: text || `Text number ${id}`,
|
text: text || `Text number ${id}`,
|
||||||
fave_num: 0,
|
fave_num: 0,
|
||||||
uri: '',
|
uri: '',
|
||||||
is_post_verb,
|
type,
|
||||||
attentions: []
|
attentions: []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('Statuses.statusType', () => {
|
|
||||||
it('identifies favorites', () => {
|
|
||||||
const fav = {
|
|
||||||
uri: 'tag:soykaf.com,2016-08-21:fave:2558:note:339495:2016-08-21T16:54:04+00:00'
|
|
||||||
}
|
|
||||||
|
|
||||||
const mastoFav = {
|
|
||||||
uri: 'tag:mastodon.social,2016-11-27:objectId=73903:objectType=Favourite'
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(statusType(fav)).to.eql('favorite')
|
|
||||||
expect(statusType(mastoFav)).to.eql('favorite')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('Statuses.prepareStatus', () => {
|
describe('Statuses.prepareStatus', () => {
|
||||||
it('sets nsfw for statuses with the #nsfw tag', () => {
|
|
||||||
const safe = makeMockStatus({id: 1, text: 'Hello oniichan'})
|
|
||||||
const nsfw = makeMockStatus({id: 1, text: 'Hello oniichan #nsfw'})
|
|
||||||
|
|
||||||
expect(prepareStatus(safe).nsfw).to.eq(false)
|
|
||||||
expect(prepareStatus(nsfw).nsfw).to.eq(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('leaves existing nsfw settings alone', () => {
|
|
||||||
const nsfw = makeMockStatus({id: 1, text: 'Hello oniichan #nsfw'})
|
|
||||||
nsfw.nsfw = false
|
|
||||||
|
|
||||||
expect(prepareStatus(nsfw).nsfw).to.eq(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('sets deleted flag to false', () => {
|
it('sets deleted flag to false', () => {
|
||||||
const aStatus = makeMockStatus({id: 1, text: 'Hello oniichan'})
|
const aStatus = makeMockStatus({id: '1', text: 'Hello oniichan'})
|
||||||
expect(prepareStatus(aStatus).deleted).to.eq(false)
|
expect(prepareStatus(aStatus).deleted).to.eq(false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Statuses.findMaxId', () => {
|
|
||||||
it('returns the largest id in any of the given arrays', () => {
|
|
||||||
const statusesOne = [{ id: 100 }, { id: 2 }]
|
|
||||||
const statusesTwo = [{ id: 3 }]
|
|
||||||
|
|
||||||
const maxId = findMaxId(statusesOne, statusesTwo)
|
|
||||||
expect(maxId).to.eq(100)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('returns undefined for empty arrays', () => {
|
|
||||||
const maxId = findMaxId([], [])
|
|
||||||
expect(maxId).to.eq(undefined)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('The Statuses module', () => {
|
describe('The Statuses module', () => {
|
||||||
it('adds the status to allStatuses and to the given timeline', () => {
|
it('adds the status to allStatuses and to the given timeline', () => {
|
||||||
const state = cloneDeep(defaultState)
|
const state = cloneDeep(defaultState)
|
||||||
const status = makeMockStatus({id: 1})
|
const status = makeMockStatus({id: '1'})
|
||||||
|
|
||||||
mutations.addNewStatuses(state, { statuses: [status], timeline: 'public' })
|
mutations.addNewStatuses(state, { statuses: [status], timeline: 'public' })
|
||||||
|
|
||||||
|
@ -82,7 +37,7 @@ describe('The Statuses module', () => {
|
||||||
|
|
||||||
it('counts the status as new if it has not been seen on this timeline', () => {
|
it('counts the status as new if it has not been seen on this timeline', () => {
|
||||||
const state = cloneDeep(defaultState)
|
const state = cloneDeep(defaultState)
|
||||||
const status = makeMockStatus({id: 1})
|
const status = makeMockStatus({id: '1'})
|
||||||
|
|
||||||
mutations.addNewStatuses(state, { statuses: [status], timeline: 'public' })
|
mutations.addNewStatuses(state, { statuses: [status], timeline: 'public' })
|
||||||
mutations.addNewStatuses(state, { statuses: [status], timeline: 'friends' })
|
mutations.addNewStatuses(state, { statuses: [status], timeline: 'friends' })
|
||||||
|
@ -100,7 +55,7 @@ describe('The Statuses module', () => {
|
||||||
|
|
||||||
it('add the statuses to allStatuses if no timeline is given', () => {
|
it('add the statuses to allStatuses if no timeline is given', () => {
|
||||||
const state = cloneDeep(defaultState)
|
const state = cloneDeep(defaultState)
|
||||||
const status = makeMockStatus({id: 1})
|
const status = makeMockStatus({id: '1'})
|
||||||
|
|
||||||
mutations.addNewStatuses(state, { statuses: [status] })
|
mutations.addNewStatuses(state, { statuses: [status] })
|
||||||
|
|
||||||
|
@ -112,7 +67,7 @@ describe('The Statuses module', () => {
|
||||||
|
|
||||||
it('adds the status to allStatuses and to the given timeline, directly visible', () => {
|
it('adds the status to allStatuses and to the given timeline, directly visible', () => {
|
||||||
const state = cloneDeep(defaultState)
|
const state = cloneDeep(defaultState)
|
||||||
const status = makeMockStatus({id: 1})
|
const status = makeMockStatus({id: '1'})
|
||||||
|
|
||||||
mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' })
|
mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' })
|
||||||
|
|
||||||
|
@ -124,10 +79,10 @@ describe('The Statuses module', () => {
|
||||||
|
|
||||||
it('removes statuses by tag on deletion', () => {
|
it('removes statuses by tag on deletion', () => {
|
||||||
const state = cloneDeep(defaultState)
|
const state = cloneDeep(defaultState)
|
||||||
const status = makeMockStatus({id: 1})
|
const status = makeMockStatus({id: '1'})
|
||||||
const otherStatus = makeMockStatus({id: 3})
|
const otherStatus = makeMockStatus({id: '3'})
|
||||||
status.uri = 'xxx'
|
status.uri = 'xxx'
|
||||||
const deletion = makeMockStatus({id: 2, is_post_verb: false})
|
const deletion = makeMockStatus({id: '2', type: 'deletion'})
|
||||||
deletion.text = 'Dolus deleted notice {{tag:gs.smuglo.li,2016-11-18:noticeId=1038007:objectType=note}}.'
|
deletion.text = 'Dolus deleted notice {{tag:gs.smuglo.li,2016-11-18:noticeId=1038007:objectType=note}}.'
|
||||||
deletion.uri = 'xxx'
|
deletion.uri = 'xxx'
|
||||||
|
|
||||||
|
@ -137,36 +92,36 @@ describe('The Statuses module', () => {
|
||||||
expect(state.allStatuses).to.eql([otherStatus])
|
expect(state.allStatuses).to.eql([otherStatus])
|
||||||
expect(state.timelines.public.statuses).to.eql([otherStatus])
|
expect(state.timelines.public.statuses).to.eql([otherStatus])
|
||||||
expect(state.timelines.public.visibleStatuses).to.eql([otherStatus])
|
expect(state.timelines.public.visibleStatuses).to.eql([otherStatus])
|
||||||
expect(state.timelines.public.maxId).to.eql(3)
|
expect(state.timelines.public.maxId).to.eql('3')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('does not update the maxId when the noIdUpdate flag is set', () => {
|
it('does not update the maxId when the noIdUpdate flag is set', () => {
|
||||||
const state = cloneDeep(defaultState)
|
const state = cloneDeep(defaultState)
|
||||||
const status = makeMockStatus({id: 1})
|
const status = makeMockStatus({id: '1'})
|
||||||
const secondStatus = makeMockStatus({id: 2})
|
const secondStatus = makeMockStatus({id: '2'})
|
||||||
|
|
||||||
mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' })
|
mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' })
|
||||||
expect(state.timelines.public.maxId).to.eql(1)
|
expect(state.timelines.public.maxId).to.eql('1')
|
||||||
|
|
||||||
mutations.addNewStatuses(state, { statuses: [secondStatus], showImmediately: true, timeline: 'public', noIdUpdate: true })
|
mutations.addNewStatuses(state, { statuses: [secondStatus], showImmediately: true, timeline: 'public', noIdUpdate: true })
|
||||||
expect(state.timelines.public.statuses).to.eql([secondStatus, status])
|
expect(state.timelines.public.statuses).to.eql([secondStatus, status])
|
||||||
expect(state.timelines.public.visibleStatuses).to.eql([secondStatus, status])
|
expect(state.timelines.public.visibleStatuses).to.eql([secondStatus, status])
|
||||||
expect(state.timelines.public.maxId).to.eql(1)
|
expect(state.timelines.public.maxId).to.eql('1')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('keeps a descending by id order in timeline.visibleStatuses and timeline.statuses', () => {
|
it('keeps a descending by id order in timeline.visibleStatuses and timeline.statuses', () => {
|
||||||
const state = cloneDeep(defaultState)
|
const state = cloneDeep(defaultState)
|
||||||
const nonVisibleStatus = makeMockStatus({id: 1})
|
const nonVisibleStatus = makeMockStatus({id: '1'})
|
||||||
const status = makeMockStatus({id: 3})
|
const status = makeMockStatus({id: '3'})
|
||||||
const statusTwo = makeMockStatus({id: 2})
|
const statusTwo = makeMockStatus({id: '2'})
|
||||||
const statusThree = makeMockStatus({id: 4})
|
const statusThree = makeMockStatus({id: '4'})
|
||||||
|
|
||||||
mutations.addNewStatuses(state, { statuses: [nonVisibleStatus], showImmediately: false, timeline: 'public' })
|
mutations.addNewStatuses(state, { statuses: [nonVisibleStatus], showImmediately: false, timeline: 'public' })
|
||||||
|
|
||||||
mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' })
|
mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' })
|
||||||
mutations.addNewStatuses(state, { statuses: [statusTwo], showImmediately: true, timeline: 'public' })
|
mutations.addNewStatuses(state, { statuses: [statusTwo], showImmediately: true, timeline: 'public' })
|
||||||
|
|
||||||
expect(state.timelines.public.minVisibleId).to.equal(2)
|
expect(state.timelines.public.minVisibleId).to.equal('2')
|
||||||
|
|
||||||
mutations.addNewStatuses(state, { statuses: [statusThree], showImmediately: true, timeline: 'public' })
|
mutations.addNewStatuses(state, { statuses: [statusThree], showImmediately: true, timeline: 'public' })
|
||||||
|
|
||||||
|
@ -176,9 +131,9 @@ describe('The Statuses module', () => {
|
||||||
|
|
||||||
it('splits retweets from their status and links them', () => {
|
it('splits retweets from their status and links them', () => {
|
||||||
const state = cloneDeep(defaultState)
|
const state = cloneDeep(defaultState)
|
||||||
const status = makeMockStatus({id: 1})
|
const status = makeMockStatus({id: '1'})
|
||||||
const retweet = makeMockStatus({id: 2, is_post_verb: false})
|
const retweet = makeMockStatus({id: '2', type: 'retweet'})
|
||||||
const modStatus = makeMockStatus({id: 1, text: 'something else'})
|
const modStatus = makeMockStatus({id: '1', text: 'something else'})
|
||||||
|
|
||||||
retweet.retweeted_status = status
|
retweet.retweeted_status = status
|
||||||
|
|
||||||
|
@ -187,22 +142,22 @@ describe('The Statuses module', () => {
|
||||||
expect(state.timelines.public.visibleStatuses).to.have.length(1)
|
expect(state.timelines.public.visibleStatuses).to.have.length(1)
|
||||||
expect(state.timelines.public.statuses).to.have.length(1)
|
expect(state.timelines.public.statuses).to.have.length(1)
|
||||||
expect(state.allStatuses).to.have.length(2)
|
expect(state.allStatuses).to.have.length(2)
|
||||||
expect(state.allStatuses[0].id).to.equal(1)
|
expect(state.allStatuses[0].id).to.equal('1')
|
||||||
expect(state.allStatuses[1].id).to.equal(2)
|
expect(state.allStatuses[1].id).to.equal('2')
|
||||||
|
|
||||||
// It refers to the modified status.
|
// It refers to the modified status.
|
||||||
mutations.addNewStatuses(state, { statuses: [modStatus], timeline: 'public' })
|
mutations.addNewStatuses(state, { statuses: [modStatus], timeline: 'public' })
|
||||||
expect(state.allStatuses).to.have.length(2)
|
expect(state.allStatuses).to.have.length(2)
|
||||||
expect(state.allStatuses[0].id).to.equal(1)
|
expect(state.allStatuses[0].id).to.equal('1')
|
||||||
expect(state.allStatuses[0].text).to.equal(modStatus.text)
|
expect(state.allStatuses[0].text).to.equal(modStatus.text)
|
||||||
expect(state.allStatuses[1].id).to.equal(2)
|
expect(state.allStatuses[1].id).to.equal('2')
|
||||||
expect(retweet.retweeted_status.text).to.eql(modStatus.text)
|
expect(retweet.retweeted_status.text).to.eql(modStatus.text)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('replaces existing statuses with the same id', () => {
|
it('replaces existing statuses with the same id', () => {
|
||||||
const state = cloneDeep(defaultState)
|
const state = cloneDeep(defaultState)
|
||||||
const status = makeMockStatus({id: 1})
|
const status = makeMockStatus({id: '1'})
|
||||||
const modStatus = makeMockStatus({id: 1, text: 'something else'})
|
const modStatus = makeMockStatus({id: '1', text: 'something else'})
|
||||||
|
|
||||||
// Add original status
|
// Add original status
|
||||||
mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' })
|
mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' })
|
||||||
|
@ -218,9 +173,9 @@ describe('The Statuses module', () => {
|
||||||
|
|
||||||
it('replaces existing statuses with the same id, coming from a retweet', () => {
|
it('replaces existing statuses with the same id, coming from a retweet', () => {
|
||||||
const state = cloneDeep(defaultState)
|
const state = cloneDeep(defaultState)
|
||||||
const status = makeMockStatus({id: 1})
|
const status = makeMockStatus({id: '1'})
|
||||||
const modStatus = makeMockStatus({id: 1, text: 'something else'})
|
const modStatus = makeMockStatus({id: '1', text: 'something else'})
|
||||||
const retweet = makeMockStatus({id: 2, is_post_verb: false})
|
const retweet = makeMockStatus({id: '2', type: 'retweet'})
|
||||||
retweet.retweeted_status = modStatus
|
retweet.retweeted_status = modStatus
|
||||||
|
|
||||||
// Add original status
|
// Add original status
|
||||||
|
@ -239,15 +194,15 @@ describe('The Statuses module', () => {
|
||||||
|
|
||||||
it('handles favorite actions', () => {
|
it('handles favorite actions', () => {
|
||||||
const state = cloneDeep(defaultState)
|
const state = cloneDeep(defaultState)
|
||||||
const status = makeMockStatus({id: 1})
|
const status = makeMockStatus({id: '1'})
|
||||||
|
|
||||||
const favorite = {
|
const favorite = {
|
||||||
id: 2,
|
id: '2',
|
||||||
is_post_verb: false,
|
type: 'favorite',
|
||||||
in_reply_to_status_id: '1', // The API uses strings here...
|
in_reply_to_status_id: '1', // The API uses strings here...
|
||||||
uri: 'tag:shitposter.club,2016-08-21:fave:3895:note:773501:2016-08-21T16:52:15+00:00',
|
uri: 'tag:shitposter.club,2016-08-21:fave:3895:note:773501:2016-08-21T16:52:15+00:00',
|
||||||
text: 'a favorited something by b',
|
text: 'a favorited something by b',
|
||||||
user: { id: 99 }
|
user: { id: '99' }
|
||||||
}
|
}
|
||||||
|
|
||||||
mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' })
|
mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' })
|
||||||
|
@ -266,12 +221,12 @@ describe('The Statuses module', () => {
|
||||||
|
|
||||||
// If something is favorited by the current user, it also sets the 'favorited' property but does not increment counter to avoid over-counting. Counter is incremented (updated, really) via response to the favorite request.
|
// If something is favorited by the current user, it also sets the 'favorited' property but does not increment counter to avoid over-counting. Counter is incremented (updated, really) via response to the favorite request.
|
||||||
const user = {
|
const user = {
|
||||||
id: 1
|
id: '1'
|
||||||
}
|
}
|
||||||
|
|
||||||
const ownFavorite = {
|
const ownFavorite = {
|
||||||
id: 3,
|
id: '3',
|
||||||
is_post_verb: false,
|
type: 'favorite',
|
||||||
in_reply_to_status_id: '1', // The API uses strings here...
|
in_reply_to_status_id: '1', // The API uses strings here...
|
||||||
uri: 'tag:shitposter.club,2016-08-21:fave:3895:note:773501:2016-08-21T16:52:15+00:00',
|
uri: 'tag:shitposter.club,2016-08-21:fave:3895:note:773501:2016-08-21T16:52:15+00:00',
|
||||||
text: 'a favorited something by b',
|
text: 'a favorited something by b',
|
||||||
|
@ -287,16 +242,16 @@ describe('The Statuses module', () => {
|
||||||
|
|
||||||
describe('notifications', () => {
|
describe('notifications', () => {
|
||||||
it('removes a notification when the notice gets removed', () => {
|
it('removes a notification when the notice gets removed', () => {
|
||||||
const user = { id: 1 }
|
const user = { id: '1' }
|
||||||
const state = cloneDeep(defaultState)
|
const state = cloneDeep(defaultState)
|
||||||
const status = makeMockStatus({id: 1})
|
const status = makeMockStatus({id: '1'})
|
||||||
const otherStatus = makeMockStatus({id: 3})
|
const otherStatus = makeMockStatus({id: '3'})
|
||||||
const mentionedStatus = makeMockStatus({id: 2})
|
const mentionedStatus = makeMockStatus({id: '2'})
|
||||||
mentionedStatus.attentions = [user]
|
mentionedStatus.attentions = [user]
|
||||||
mentionedStatus.uri = 'xxx'
|
mentionedStatus.uri = 'xxx'
|
||||||
otherStatus.attentions = [user]
|
otherStatus.attentions = [user]
|
||||||
|
|
||||||
const deletion = makeMockStatus({id: 4, is_post_verb: false})
|
const deletion = makeMockStatus({id: '4', type: 'deletion'})
|
||||||
deletion.text = 'Dolus deleted notice {{tag:gs.smuglo.li,2016-11-18:noticeId=1038007:objectType=note}}.'
|
deletion.text = 'Dolus deleted notice {{tag:gs.smuglo.li,2016-11-18:noticeId=1038007:objectType=note}}.'
|
||||||
deletion.uri = 'xxx'
|
deletion.uri = 'xxx'
|
||||||
|
|
||||||
|
@ -305,10 +260,12 @@ describe('The Statuses module', () => {
|
||||||
state,
|
state,
|
||||||
{
|
{
|
||||||
notifications: [{
|
notifications: [{
|
||||||
ntype: 'mention',
|
from_profile: { id: '2' },
|
||||||
|
id: '998',
|
||||||
|
type: 'mention',
|
||||||
status: otherStatus,
|
status: otherStatus,
|
||||||
notice: otherStatus,
|
action: otherStatus,
|
||||||
is_seen: false
|
seen: false
|
||||||
}]
|
}]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -317,10 +274,12 @@ describe('The Statuses module', () => {
|
||||||
state,
|
state,
|
||||||
{
|
{
|
||||||
notifications: [{
|
notifications: [{
|
||||||
ntype: 'mention',
|
from_profile: { id: '2' },
|
||||||
|
id: '999',
|
||||||
|
type: 'mention',
|
||||||
status: mentionedStatus,
|
status: mentionedStatus,
|
||||||
notice: mentionedStatus,
|
action: mentionedStatus,
|
||||||
is_seen: false
|
seen: false
|
||||||
}]
|
}]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -6,8 +6,8 @@ describe('The users module', () => {
|
||||||
describe('mutations', () => {
|
describe('mutations', () => {
|
||||||
it('adds new users to the set, merging in new information for old users', () => {
|
it('adds new users to the set, merging in new information for old users', () => {
|
||||||
const state = cloneDeep(defaultState)
|
const state = cloneDeep(defaultState)
|
||||||
const user = { id: 1, name: 'Guy' }
|
const user = { id: '1', name: 'Guy' }
|
||||||
const modUser = { id: 1, name: 'Dude' }
|
const modUser = { id: '1', name: 'Dude' }
|
||||||
|
|
||||||
mutations.addNewUsers(state, [user])
|
mutations.addNewUsers(state, [user])
|
||||||
expect(state.users).to.have.length(1)
|
expect(state.users).to.have.length(1)
|
||||||
|
@ -21,7 +21,7 @@ describe('The users module', () => {
|
||||||
|
|
||||||
it('sets a mute bit on users', () => {
|
it('sets a mute bit on users', () => {
|
||||||
const state = cloneDeep(defaultState)
|
const state = cloneDeep(defaultState)
|
||||||
const user = { id: 1, name: 'Guy' }
|
const user = { id: '1', name: 'Guy' }
|
||||||
|
|
||||||
mutations.addNewUsers(state, [user])
|
mutations.addNewUsers(state, [user])
|
||||||
mutations.setMuted(state, {user, muted: true})
|
mutations.setMuted(state, {user, muted: true})
|
||||||
|
@ -38,11 +38,11 @@ describe('The users module', () => {
|
||||||
it('returns user with matching screen_name', () => {
|
it('returns user with matching screen_name', () => {
|
||||||
const state = {
|
const state = {
|
||||||
users: [
|
users: [
|
||||||
{ screen_name: 'Guy', id: 1 }
|
{ screen_name: 'Guy', id: '1' }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
const name = 'Guy'
|
const name = 'Guy'
|
||||||
const expected = { screen_name: 'Guy', id: 1 }
|
const expected = { screen_name: 'Guy', id: '1' }
|
||||||
expect(getters.userByName(state)(name)).to.eql(expected)
|
expect(getters.userByName(state)(name)).to.eql(expected)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -51,11 +51,11 @@ describe('The users module', () => {
|
||||||
it('returns user with matching id', () => {
|
it('returns user with matching id', () => {
|
||||||
const state = {
|
const state = {
|
||||||
users: [
|
users: [
|
||||||
{ screen_name: 'Guy', id: 1 }
|
{ screen_name: 'Guy', id: '1' }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
const id = 1
|
const id = '1'
|
||||||
const expected = { screen_name: 'Guy', id: 1 }
|
const expected = { screen_name: 'Guy', id: '1' }
|
||||||
expect(getters.userById(state)(id)).to.eql(expected)
|
expect(getters.userById(state)(id)).to.eql(expected)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -0,0 +1,270 @@
|
||||||
|
import { parseStatus, parseUser, parseNotification } from '../../../../../src/services/entity_normalizer/entity_normalizer.service.js'
|
||||||
|
import mastoapidata from '../../../../fixtures/mastoapi.json'
|
||||||
|
import qvitterapidata from '../../../../fixtures/statuses.json'
|
||||||
|
|
||||||
|
const makeMockStatusQvitter = (overrides = {}) => {
|
||||||
|
return Object.assign({
|
||||||
|
activity_type: 'post',
|
||||||
|
attachments: [],
|
||||||
|
attentions: [],
|
||||||
|
created_at: 'Tue Jan 15 13:57:56 +0000 2019',
|
||||||
|
external_url: 'https://ap.example/whatever',
|
||||||
|
fave_num: 1,
|
||||||
|
favorited: false,
|
||||||
|
id: '10335970',
|
||||||
|
in_reply_to_ostatus_uri: null,
|
||||||
|
in_reply_to_profileurl: null,
|
||||||
|
in_reply_to_screen_name: null,
|
||||||
|
in_reply_to_status_id: null,
|
||||||
|
in_reply_to_user_id: null,
|
||||||
|
is_local: false,
|
||||||
|
is_post_verb: true,
|
||||||
|
possibly_sensitive: false,
|
||||||
|
repeat_num: 0,
|
||||||
|
repeated: false,
|
||||||
|
statusnet_conversation_id: '16300488',
|
||||||
|
statusnet_html: '<p>haha benis</p>',
|
||||||
|
summary: null,
|
||||||
|
tags: [],
|
||||||
|
text: 'haha benis',
|
||||||
|
uri: 'https://ap.example/whatever',
|
||||||
|
user: makeMockUserQvitter(),
|
||||||
|
visibility: 'public'
|
||||||
|
}, overrides)
|
||||||
|
}
|
||||||
|
|
||||||
|
const makeMockUserQvitter = (overrides = {}) => {
|
||||||
|
return Object.assign({
|
||||||
|
background_image: null,
|
||||||
|
cover_photo: '',
|
||||||
|
created_at: 'Mon Jan 14 13:56:51 +0000 2019',
|
||||||
|
default_scope: 'public',
|
||||||
|
description: 'ebin',
|
||||||
|
description_html: '<p>ebin</p>',
|
||||||
|
favourites_count: 0,
|
||||||
|
fields: [],
|
||||||
|
followers_count: 1,
|
||||||
|
following: true,
|
||||||
|
follows_you: true,
|
||||||
|
friends_count: 1,
|
||||||
|
id: '60717',
|
||||||
|
is_local: false,
|
||||||
|
locked: false,
|
||||||
|
name: 'Spurdo :ebin:',
|
||||||
|
name_html: 'Spurdo <img src="whatever">',
|
||||||
|
no_rich_text: false,
|
||||||
|
pleroma: { confirmation_pending: false, tags: [] },
|
||||||
|
profile_image_url: 'https://ap.example/whatever',
|
||||||
|
profile_image_url_https: 'https://ap.example/whatever',
|
||||||
|
profile_image_url_original: 'https://ap.example/whatever',
|
||||||
|
profile_image_url_profile_size: 'https://ap.example/whatever',
|
||||||
|
rights: { delete_others_notice: false },
|
||||||
|
screen_name: 'spurdo@ap.example',
|
||||||
|
statuses_count: 46,
|
||||||
|
statusnet_blocking: false,
|
||||||
|
statusnet_profile_url: ''
|
||||||
|
}, overrides)
|
||||||
|
}
|
||||||
|
|
||||||
|
const makeMockUserMasto = (overrides = {}) => {
|
||||||
|
return Object.assign({
|
||||||
|
acct: 'hj',
|
||||||
|
avatar:
|
||||||
|
'https://shigusegubu.club/media/1657b945-8d5b-4ce6-aafb-4c3fc5772120/8ce851029af84d55de9164e30cc7f46d60cbf12eee7e96c5c0d35d9038ddade1.png',
|
||||||
|
avatar_static:
|
||||||
|
'https://shigusegubu.club/media/1657b945-8d5b-4ce6-aafb-4c3fc5772120/8ce851029af84d55de9164e30cc7f46d60cbf12eee7e96c5c0d35d9038ddade1.png',
|
||||||
|
bot: false,
|
||||||
|
created_at: '2017-12-17T21:54:14.000Z',
|
||||||
|
display_name: 'whatever whatever whatever witch',
|
||||||
|
emojis: [],
|
||||||
|
fields: [],
|
||||||
|
followers_count: 705,
|
||||||
|
following_count: 326,
|
||||||
|
header:
|
||||||
|
'https://shigusegubu.club/media/7ab024d9-2a8a-4fbc-9ce8-da06756ae2db/6aadefe4e264133bc377ab450e6b045b6f5458542a5c59e6c741f86107f0388b.png',
|
||||||
|
header_static:
|
||||||
|
'https://shigusegubu.club/media/7ab024d9-2a8a-4fbc-9ce8-da06756ae2db/6aadefe4e264133bc377ab450e6b045b6f5458542a5c59e6c741f86107f0388b.png',
|
||||||
|
id: '1',
|
||||||
|
locked: false,
|
||||||
|
note:
|
||||||
|
'Volatile Internet Weirdo. Name pronounced as Hee Jay. JS and Java dark arts mage, Elixir trainee. I love sampo and lain. Matrix is <span><a data-user="1" href="https://shigusegubu.club/users/hj">@<span>hj</span></a></span>:matrix.heldscal.la Pronouns are whatever. Do not DM me unless it\'s truly private matter and you\'re instance\'s admin or you risk your DM to be reposted publicly.Wish i was Finnish girl.',
|
||||||
|
pleroma: { confirmation_pending: false, tags: null },
|
||||||
|
source: { note: '', privacy: 'public', sensitive: false },
|
||||||
|
statuses_count: 41775,
|
||||||
|
url: 'https://shigusegubu.club/users/hj',
|
||||||
|
username: 'hj'
|
||||||
|
}, overrides)
|
||||||
|
}
|
||||||
|
|
||||||
|
const makeMockStatusMasto = (overrides = {}) => {
|
||||||
|
return Object.assign({
|
||||||
|
account: makeMockUserMasto(),
|
||||||
|
application: { name: 'Web', website: null },
|
||||||
|
content:
|
||||||
|
'<span><a data-user="14660" href="https://pleroma.soykaf.com/users/sampo">@<span>sampo</span></a></span> god i wish i was there',
|
||||||
|
created_at: '2019-01-17T16:29:23.000Z',
|
||||||
|
emojis: [],
|
||||||
|
favourited: false,
|
||||||
|
favourites_count: 1,
|
||||||
|
id: '10423476',
|
||||||
|
in_reply_to_account_id: '14660',
|
||||||
|
in_reply_to_id: '10423197',
|
||||||
|
language: null,
|
||||||
|
media_attachments: [],
|
||||||
|
mentions: [
|
||||||
|
{
|
||||||
|
acct: 'sampo@pleroma.soykaf.com',
|
||||||
|
id: '14660',
|
||||||
|
url: 'https://pleroma.soykaf.com/users/sampo',
|
||||||
|
username: 'sampo'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
muted: false,
|
||||||
|
reblog: null,
|
||||||
|
reblogged: false,
|
||||||
|
reblogs_count: 0,
|
||||||
|
replies_count: 0,
|
||||||
|
sensitive: false,
|
||||||
|
spoiler_text: '',
|
||||||
|
tags: [],
|
||||||
|
uri: 'https://shigusegubu.club/objects/16033fbb-97c0-4f0e-b834-7abb92fb8639',
|
||||||
|
url: 'https://shigusegubu.club/objects/16033fbb-97c0-4f0e-b834-7abb92fb8639',
|
||||||
|
visibility: 'public'
|
||||||
|
}, overrides)
|
||||||
|
}
|
||||||
|
|
||||||
|
const makeMockNotificationQvitter = (overrides = {}) => {
|
||||||
|
return Object.assign({
|
||||||
|
notice: makeMockStatusQvitter(),
|
||||||
|
ntype: 'follow',
|
||||||
|
from_profile: makeMockUserQvitter(),
|
||||||
|
is_seen: 0,
|
||||||
|
id: 123
|
||||||
|
}, overrides)
|
||||||
|
}
|
||||||
|
|
||||||
|
parseNotification
|
||||||
|
parseUser
|
||||||
|
parseStatus
|
||||||
|
makeMockStatusQvitter
|
||||||
|
makeMockUserQvitter
|
||||||
|
|
||||||
|
describe('API Entities normalizer', () => {
|
||||||
|
describe('parseStatus', () => {
|
||||||
|
describe('QVitter preprocessing', () => {
|
||||||
|
it('doesn\'t blow up', () => {
|
||||||
|
const parsed = qvitterapidata.map(parseStatus)
|
||||||
|
expect(parsed.length).to.eq(qvitterapidata.length)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('identifies favorites', () => {
|
||||||
|
const fav = {
|
||||||
|
uri: 'tag:soykaf.com,2016-08-21:fave:2558:note:339495:2016-08-21T16:54:04+00:00',
|
||||||
|
is_post_verb: false
|
||||||
|
}
|
||||||
|
|
||||||
|
const mastoFav = {
|
||||||
|
uri: 'tag:mastodon.social,2016-11-27:objectId=73903:objectType=Favourite',
|
||||||
|
is_post_verb: false
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(parseStatus(makeMockStatusQvitter(fav))).to.have.property('type', 'favorite')
|
||||||
|
expect(parseStatus(makeMockStatusQvitter(mastoFav))).to.have.property('type', 'favorite')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('processes repeats correctly', () => {
|
||||||
|
const post = makeMockStatusQvitter({ retweeted_status: null, id: 'deadbeef' })
|
||||||
|
const repeat = makeMockStatusQvitter({ retweeted_status: post, is_post_verb: false, id: 'foobar' })
|
||||||
|
|
||||||
|
const parsedPost = parseStatus(post)
|
||||||
|
const parsedRepeat = parseStatus(repeat)
|
||||||
|
|
||||||
|
expect(parsedPost).to.have.property('type', 'status')
|
||||||
|
expect(parsedRepeat).to.have.property('type', 'retweet')
|
||||||
|
expect(parsedRepeat).to.have.property('retweeted_status')
|
||||||
|
expect(parsedRepeat).to.have.deep.property('retweeted_status.id', 'deadbeef')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sets nsfw for statuses with the #nsfw tag', () => {
|
||||||
|
const safe = makeMockStatusQvitter({id: '1', text: 'Hello oniichan'})
|
||||||
|
const nsfw = makeMockStatusQvitter({id: '1', text: 'Hello oniichan #nsfw'})
|
||||||
|
|
||||||
|
expect(parseStatus(safe).nsfw).to.eq(false)
|
||||||
|
expect(parseStatus(nsfw).nsfw).to.eq(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('leaves existing nsfw settings alone', () => {
|
||||||
|
const nsfw = makeMockStatusQvitter({id: '1', text: 'Hello oniichan #nsfw', nsfw: false})
|
||||||
|
|
||||||
|
expect(parseStatus(nsfw).nsfw).to.eq(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Mastoapi preprocessing and converting', () => {
|
||||||
|
it('doesn\'t blow up', () => {
|
||||||
|
const parsed = mastoapidata.map(parseStatus)
|
||||||
|
expect(parsed.length).to.eq(mastoapidata.length)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('processes repeats correctly', () => {
|
||||||
|
const post = makeMockStatusMasto({ reblog: null, id: 'deadbeef' })
|
||||||
|
const repeat = makeMockStatusMasto({ reblog: post, id: 'foobar' })
|
||||||
|
|
||||||
|
const parsedPost = parseStatus(post)
|
||||||
|
const parsedRepeat = parseStatus(repeat)
|
||||||
|
|
||||||
|
expect(parsedPost).to.have.property('type', 'status')
|
||||||
|
expect(parsedRepeat).to.have.property('type', 'retweet')
|
||||||
|
expect(parsedRepeat).to.have.property('retweeted_status')
|
||||||
|
expect(parsedRepeat).to.have.deep.property('retweeted_status.id', 'deadbeef')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// Statuses generally already contain some info regarding users and there's nearly 1:1 mapping, so very little to test
|
||||||
|
describe('parseUsers (MastoAPI)', () => {
|
||||||
|
it('sets correct is_local for users depending on their screen_name', () => {
|
||||||
|
const local = makeMockUserMasto({ acct: 'foo' })
|
||||||
|
const remote = makeMockUserMasto({ acct: 'foo@bar.baz' })
|
||||||
|
|
||||||
|
expect(parseUser(local)).to.have.property('is_local', true)
|
||||||
|
expect(parseUser(remote)).to.have.property('is_local', false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// We currently use QvitterAPI notifications only, and especially due to MastoAPI lacking is_seen, support for MastoAPI
|
||||||
|
// is more of an afterthought
|
||||||
|
describe('parseNotifications (QvitterAPI)', () => {
|
||||||
|
it('correctly normalizes data to FE\'s format', () => {
|
||||||
|
const notif = makeMockNotificationQvitter({
|
||||||
|
id: 123,
|
||||||
|
notice: makeMockStatusQvitter({ id: 444 }),
|
||||||
|
from_profile: makeMockUserQvitter({ id: 'spurdo' })
|
||||||
|
})
|
||||||
|
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')
|
||||||
|
expect(parseNotification(notif)).to.have.deep.property('from_profile.id', 'spurdo')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('correctly normalizes favorite notifications', () => {
|
||||||
|
const notif = makeMockNotificationQvitter({
|
||||||
|
id: 123,
|
||||||
|
ntype: 'like',
|
||||||
|
notice: makeMockStatusQvitter({
|
||||||
|
id: 444,
|
||||||
|
favorited_status: makeMockStatusQvitter({ id: 4412 })
|
||||||
|
}),
|
||||||
|
is_seen: 1,
|
||||||
|
from_profile: makeMockUserQvitter({ id: 'spurdo' })
|
||||||
|
})
|
||||||
|
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')
|
||||||
|
expect(parseNotification(notif)).to.have.deep.property('action.id', '444')
|
||||||
|
expect(parseNotification(notif)).to.have.deep.property('from_profile.id', 'spurdo')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -9,15 +9,15 @@ describe('NotificationUtils', () => {
|
||||||
notifications: {
|
notifications: {
|
||||||
data: [
|
data: [
|
||||||
{
|
{
|
||||||
action: { id: 1 },
|
action: { id: '1' },
|
||||||
type: 'like'
|
type: 'like'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: { id: 2 },
|
action: { id: '2' },
|
||||||
type: 'mention'
|
type: 'mention'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: { id: 3 },
|
action: { id: '3' },
|
||||||
type: 'repeat'
|
type: 'repeat'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -34,11 +34,11 @@ describe('NotificationUtils', () => {
|
||||||
}
|
}
|
||||||
const expected = [
|
const expected = [
|
||||||
{
|
{
|
||||||
action: { id: 3 },
|
action: { id: '3' },
|
||||||
type: 'repeat'
|
type: 'repeat'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: { id: 1 },
|
action: { id: '1' },
|
||||||
type: 'like'
|
type: 'like'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -54,12 +54,12 @@ describe('NotificationUtils', () => {
|
||||||
notifications: {
|
notifications: {
|
||||||
data: [
|
data: [
|
||||||
{
|
{
|
||||||
action: { id: 1 },
|
action: { id: '1' },
|
||||||
type: 'like',
|
type: 'like',
|
||||||
seen: false
|
seen: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
action: { id: 2 },
|
action: { id: '2' },
|
||||||
type: 'mention',
|
type: 'mention',
|
||||||
seen: true
|
seen: true
|
||||||
}
|
}
|
||||||
|
@ -77,7 +77,7 @@ describe('NotificationUtils', () => {
|
||||||
}
|
}
|
||||||
const expected = [
|
const expected = [
|
||||||
{
|
{
|
||||||
action: { id: 1 },
|
action: { id: '1' },
|
||||||
type: 'like',
|
type: 'like',
|
||||||
seen: false
|
seen: false
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue