Page MenuHomePhorge

No OneTemporary

Authored By
Unknown
Size
257 KB
Referenced Files
None
Subscribers
None
This file is larger than 256 KB, so syntax highlighting was skipped.
diff --git a/src/boot/after_store.js b/src/boot/after_store.js
index 08c00c641f..505005825d 100644
--- a/src/boot/after_store.js
+++ b/src/boot/after_store.js
@@ -1,179 +1,173 @@
import Vue from 'vue'
import VueRouter from 'vue-router'
import routes from './routes'
import App from '../App.vue'
const afterStoreSetup = ({ store, i18n }) => {
window.fetch('/api/statusnet/config.json')
.then((res) => res.json())
.then((data) => {
const { name, closed: registrationClosed, textlimit, uploadlimit, server, vapidPublicKey } = data.site
store.dispatch('setInstanceOption', { name: 'name', value: name })
store.dispatch('setInstanceOption', { name: 'registrationOpen', value: (registrationClosed === '0') })
store.dispatch('setInstanceOption', { name: 'textlimit', value: parseInt(textlimit) })
store.dispatch('setInstanceOption', { name: 'server', value: server })
// TODO: default values for this stuff, added if to not make it break on
// my dev config out of the box.
if (uploadlimit) {
store.dispatch('setInstanceOption', { name: 'uploadlimit', value: parseInt(uploadlimit.uploadlimit) })
store.dispatch('setInstanceOption', { name: 'avatarlimit', value: parseInt(uploadlimit.avatarlimit) })
store.dispatch('setInstanceOption', { name: 'backgroundlimit', value: parseInt(uploadlimit.backgroundlimit) })
store.dispatch('setInstanceOption', { name: 'bannerlimit', value: parseInt(uploadlimit.bannerlimit) })
}
- if (data.nsfwCensorImage) {
- store.dispatch('setInstanceOption', { name: 'nsfwCensorImage', value: data.nsfwCensorImage })
- }
-
if (vapidPublicKey) {
store.dispatch('setInstanceOption', { name: 'vapidPublicKey', value: vapidPublicKey })
}
var apiConfig = data.site.pleromafe
window.fetch('/static/config.json')
.then((res) => res.json())
.catch((err) => {
console.warn('Failed to load static/config.json, continuing without it.')
console.warn(err)
return {}
})
.then((staticConfig) => {
const overrides = window.___pleromafe_dev_overrides || {}
const env = window.___pleromafe_mode.NODE_ENV
// This takes static config and overrides properties that are present in apiConfig
let config = {}
if (overrides.staticConfigPreference && env === 'development') {
console.warn('OVERRIDING API CONFIG WITH STATIC CONFIG')
config = Object.assign({}, apiConfig, staticConfig)
} else {
config = Object.assign({}, staticConfig, apiConfig)
}
- var theme = (config.theme)
- var background = (config.background)
- 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 })
- store.dispatch('setInstanceOption', { name: 'background', value: background })
- store.dispatch('setInstanceOption', { name: 'hidePostStats', value: hidePostStats })
- store.dispatch('setInstanceOption', { name: 'hideUserStats', value: hideUserStats })
- store.dispatch('setInstanceOption', { name: 'logo', value: logo })
- store.dispatch('setInstanceOption', { name: 'logoMask', value: logoMask })
- 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) {
+ const copyInstanceOption = (name) => {
+ store.dispatch('setInstanceOption', {name, value: config[name]})
+ }
+
+ copyInstanceOption('nsfwCensorImage')
+ copyInstanceOption('theme')
+ copyInstanceOption('background')
+ copyInstanceOption('hidePostStats')
+ copyInstanceOption('hideUserStats')
+ copyInstanceOption('logo')
+
+ 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')
}
const router = new VueRouter({
mode: 'history',
routes: routes(store),
scrollBehavior: (to, _from, savedPosition) => {
if (to.matched.some(m => m.meta.dontScroll)) {
return false
}
return savedPosition || { x: 0, y: 0 }
}
})
/* eslint-disable no-new */
new Vue({
router,
store,
i18n,
el: '#app',
render: h => h(App)
})
})
})
window.fetch('/static/terms-of-service.html')
.then((res) => res.text())
.then((html) => {
store.dispatch('setInstanceOption', { name: 'tos', value: html })
})
window.fetch('/api/pleroma/emoji.json')
.then(
(res) => res.json()
.then(
(values) => {
const emoji = Object.keys(values).map((key) => {
return { shortcode: key, image_url: values[key] }
})
store.dispatch('setInstanceOption', { name: 'customEmoji', value: emoji })
store.dispatch('setInstanceOption', { name: 'pleromaBackend', value: true })
},
(failure) => {
store.dispatch('setInstanceOption', { name: 'pleromaBackend', value: false })
}
),
(error) => console.log(error)
)
window.fetch('/static/emoji.json')
.then((res) => res.json())
.then((values) => {
const emoji = Object.keys(values).map((key) => {
return { shortcode: key, image_url: false, 'utf': values[key] }
})
store.dispatch('setInstanceOption', { name: 'emoji', value: emoji })
})
window.fetch('/instance/panel.html')
.then((res) => res.text())
.then((html) => {
store.dispatch('setInstanceOption', { name: 'instanceSpecificPanelContent', value: html })
})
window.fetch('/nodeinfo/2.0.json')
.then((res) => res.json())
.then((data) => {
const metadata = data.metadata
const features = metadata.features
store.dispatch('setInstanceOption', { name: 'mediaProxyAvailable', value: features.includes('media_proxy') })
store.dispatch('setInstanceOption', { name: 'chatAvailable', value: features.includes('chat') })
store.dispatch('setInstanceOption', { name: 'gopherAvailable', value: features.includes('gopher') })
store.dispatch('setInstanceOption', { name: 'restrictedNicknames', value: metadata.restrictedNicknames })
const suggestions = metadata.suggestions
store.dispatch('setInstanceOption', { name: 'suggestionsEnabled', value: suggestions.enabled })
store.dispatch('setInstanceOption', { name: 'suggestionsWeb', value: suggestions.web })
})
}
export default afterStoreSetup
diff --git a/src/components/attachment/attachment.js b/src/components/attachment/attachment.js
index 0ae8f71a41..4793985249 100644
--- a/src/components/attachment/attachment.js
+++ b/src/components/attachment/attachment.js
@@ -1,95 +1,95 @@
import StillImage from '../still-image/still-image.vue'
import VideoAttachment from '../video_attachment/video_attachment.vue'
import nsfwImage from '../../assets/nsfw.png'
import fileTypeService from '../../services/file_type/file_type.service.js'
const Attachment = {
props: [
'attachment',
'nsfw',
'statusId',
'size',
'allowPlay',
'setMedia'
],
data () {
return {
- nsfwImage: this.$store.state.config.nsfwCensorImage || nsfwImage,
+ nsfwImage: this.$store.state.instance.nsfwCensorImage || nsfwImage,
hideNsfwLocal: this.$store.state.config.hideNsfw,
preloadImage: this.$store.state.config.preloadImage,
loading: false,
img: fileTypeService.fileType(this.attachment.mimetype) === 'image' && document.createElement('img'),
modalOpen: false,
showHidden: false
}
},
components: {
StillImage,
VideoAttachment
},
computed: {
usePlaceHolder () {
return this.size === 'hide' || this.type === 'unknown'
},
referrerpolicy () {
return this.$store.state.instance.mediaProxyAvailable ? '' : 'no-referrer'
},
type () {
return fileTypeService.fileType(this.attachment.mimetype)
},
hidden () {
return this.nsfw && this.hideNsfwLocal && !this.showHidden
},
isEmpty () {
return (this.type === 'html' && !this.attachment.oembed) || this.type === 'unknown'
},
isSmall () {
return this.size === 'small'
},
fullwidth () {
return this.type === 'html' || this.type === 'audio'
}
},
methods: {
linkClicked ({target}) {
if (target.tagName === 'A') {
window.open(target.href, '_blank')
}
},
openModal (event) {
const modalTypes = this.$store.state.config.playVideosInline
? ['image']
: ['image', 'video']
if (fileTypeService.fileMatchesSomeType(modalTypes, this.attachment) ||
this.usePlaceHolder
) {
event.stopPropagation()
event.preventDefault()
this.setMedia()
this.$store.dispatch('setCurrent', this.attachment)
}
},
toggleHidden (event) {
if (this.$store.state.config.useOneClickNsfw && !this.showHidden) {
this.openModal(event)
return
}
if (this.img && !this.preloadImage) {
if (this.img.onload) {
this.img.onload()
} else {
this.loading = true
this.img.src = this.attachment.url
this.img.onload = () => {
this.loading = false
this.showHidden = !this.showHidden
}
}
} else {
this.showHidden = !this.showHidden
}
}
}
}
export default Attachment
diff --git a/src/components/attachment/attachment.vue b/src/components/attachment/attachment.vue
index 56a49d2d37..5a80db8ad7 100644
--- a/src/components/attachment/attachment.vue
+++ b/src/components/attachment/attachment.vue
@@ -1,239 +1,239 @@
<template>
<div v-if="usePlaceHolder" @click="openModal">
<a class="placeholder"
v-if="type !== 'html'"
target="_blank" :href="attachment.url"
>
[{{nsfw ? "NSFW/" : ""}}{{type.toUpperCase()}}]
</a>
</div>
<div
v-else class="attachment"
:class="{[type]: true, loading, 'fullwidth': fullwidth, 'nsfw-placeholder': hidden}"
v-show="!isEmpty"
>
<a class="image-attachment" v-if="hidden" :href="attachment.url" @click.prevent="toggleHidden">
<img class="nsfw" :key="nsfwImage" :src="nsfwImage" :class="{'small': isSmall}"/>
<i v-if="type === 'video'" class="play-icon icon-play-circled"></i>
</a>
<div class="hider" v-if="nsfw && hideNsfwLocal && !hidden">
<a href="#" @click.prevent="toggleHidden">Hide</a>
</div>
<a v-if="type === 'image' && (!hidden || preloadImage)"
@click="openModal"
class="image-attachment"
:class="{'hidden': hidden && preloadImage }"
:href="attachment.url" target="_blank"
: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 class="video-container"
@click="openModal"
v-if="type === 'video' && !hidden"
:class="{'small': isSmall}"
:href="attachment.url"
>
<VideoAttachment class="video" :attachment="attachment" :controls="allowPlay" />
<i v-if="!allowPlay" class="play-icon icon-play-circled"></i>
</a>
<audio v-if="type === 'audio'" :src="attachment.url" controls></audio>
<div @click.prevent="linkClicked" v-if="type === 'html' && attachment.oembed" class="oembed">
<div v-if="attachment.thumb_url" class="image">
<img :src="attachment.thumb_url"/>
</div>
<div class="text">
<h1><a :href="attachment.url">{{attachment.oembed.title}}</a></h1>
<div v-html="attachment.oembed.oembedHTML"></div>
</div>
</div>
</div>
</template>
<script src="./attachment.js"></script>
<style lang="scss">
@import '../../_variables.scss';
.attachments {
display: flex;
flex-wrap: wrap;
.attachment.media-upload-container {
flex: 0 0 auto;
max-height: 200px;
max-width: 100%;
display: flex;
video {
max-width: 100%;
}
}
.placeholder {
margin-right: 8px;
margin-bottom: 4px;
}
.nsfw-placeholder {
cursor: pointer;
&.loading {
cursor: progress;
}
}
.attachment {
position: relative;
margin: 0.5em 0.5em 0em 0em;
align-self: flex-start;
line-height: 0;
border-style: solid;
border-width: 1px;
border-radius: $fallback--attachmentRadius;
border-radius: var(--attachmentRadius, $fallback--attachmentRadius);
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
overflow: hidden;
}
.non-gallery.attachment {
&.video {
flex: 1 0 40%;
}
.nsfw {
height: 260px;
}
.small {
height: 120px;
flex-grow: 0;
}
.video {
height: 260px;
display: flex;
}
video {
max-height: 100%;
object-fit: contain;
}
}
.fullwidth {
flex-basis: 100%;
}
// fixes small gap below video
&.video {
line-height: 0;
}
.video-container {
display: flex;
max-height: 100%;
}
.video {
width: 100%;
}
.play-icon {
position: absolute;
font-size: 64px;
top: calc(50% - 32px);
left: calc(50% - 32px);
color: rgba(255, 255, 255, 0.75);
text-shadow: 0 0 2px rgba(0, 0, 0, 0.4);
}
.play-icon::before {
margin: 0;
}
&.html {
flex-basis: 90%;
width: 100%;
display: flex;
}
.hider {
position: absolute;
white-space: nowrap;
margin: 10px;
padding: 5px;
background: rgba(230,230,230,0.6);
font-weight: bold;
z-index: 4;
line-height: 1;
border-radius: $fallback--tooltipRadius;
border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
}
video {
z-index: 0;
}
audio {
width: 100%;
}
img.media-upload {
line-height: 0;
max-height: 200px;
max-width: 100%;
}
.oembed {
line-height: 1.2em;
flex: 1 0 100%;
width: 100%;
margin-right: 15px;
display: flex;
img {
width: 100%;
}
.image {
flex: 1;
img {
border: 0px;
border-radius: 5px;
height: 100%;
object-fit: cover;
}
}
.text {
flex: 2;
margin: 8px;
word-break: break-all;
h1 {
font-size: 14px;
margin: 0px;
}
}
}
.image-attachment {
width: 100%;
height: 100%;
&.hidden {
display: none;
}
.nsfw {
object-fit: cover;
width: 100%;
height: 100%;
}
img {
image-orientation: from-image;
}
}
}
</style>
diff --git a/src/components/conversation-page/conversation-page.js b/src/components/conversation-page/conversation-page.js
index beffa5bbc1..8f1ac3d90d 100644
--- a/src/components/conversation-page/conversation-page.js
+++ b/src/components/conversation-page/conversation-page.js
@@ -1,19 +1,19 @@
import Conversation from '../conversation/conversation.vue'
-import { find, toInteger } from 'lodash'
+import { find } from 'lodash'
const conversationPage = {
components: {
Conversation
},
computed: {
statusoid () {
- const id = toInteger(this.$route.params.id)
+ const id = this.$route.params.id
const statuses = this.$store.state.statuses.allStatuses
const status = find(statuses, {id})
return status
}
}
}
export default conversationPage
diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js
index 9d9f7bbe16..c18781de54 100644
--- a/src/components/conversation/conversation.js
+++ b/src/components/conversation/conversation.js
@@ -1,88 +1,106 @@
-import { reduce, filter, sortBy } from 'lodash'
-import { statusType } from '../../modules/statuses.js'
+import { reduce, filter } from 'lodash'
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) => {
- conversation = filter(conversation, (status) => statusType(status) !== 'retweet')
- return sortBy(conversation, 'id')
+ conversation = filter(conversation, (status) => status.type !== 'retweet')
+ return conversation.filter(_ => _).sort(sortById)
}
const conversation = {
data () {
return {
highlight: null
}
},
props: [
'statusoid',
'collapsable'
],
computed: {
- status () { return this.statusoid },
+ status () {
+ return this.statusoid
+ },
conversation () {
if (!this.status) {
- return false
+ return []
}
const conversationId = this.status.statusnet_conversation_id
const statuses = this.$store.state.statuses.allStatuses
const conversation = filter(statuses, { statusnet_conversation_id: conversationId })
return sortAndFilterConversation(conversation)
},
replies () {
let i = 1
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) {
result[irid] = result[irid] || []
result[irid].push({
name: `#${i}`,
id: id
})
}
i++
return result
}, {})
}
},
components: {
Status
},
created () {
this.fetchConversation()
},
watch: {
'$route': 'fetchConversation'
},
methods: {
fetchConversation () {
if (this.status) {
const conversationId = this.status.statusnet_conversation_id
this.$store.state.api.backendInteractor.fetchConversation({id: conversationId})
.then((statuses) => this.$store.dispatch('addNewStatuses', { statuses }))
.then(() => this.setHighlight(this.statusoid.id))
} else {
const id = this.$route.params.id
this.$store.state.api.backendInteractor.fetchStatus({id})
.then((status) => this.$store.dispatch('addNewStatuses', { statuses: [status] }))
.then(() => this.fetchConversation())
}
},
getReplies (id) {
- id = Number(id)
return this.replies[id] || []
},
focused (id) {
if (this.statusoid.retweeted_status) {
return (id === this.statusoid.retweeted_status.id)
} else {
return (id === this.statusoid.id)
}
},
setHighlight (id) {
- this.highlight = Number(id)
+ this.highlight = id
}
}
}
export default conversation
diff --git a/src/components/status/status.js b/src/components/status/status.js
index 1db074e21a..2d48561601 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -1,331 +1,346 @@
import Attachment from '../attachment/attachment.vue'
import FavoriteButton from '../favorite_button/favorite_button.vue'
import RetweetButton from '../retweet_button/retweet_button.vue'
import DeleteButton from '../delete_button/delete_button.vue'
import PostStatusForm from '../post_status_form/post_status_form.vue'
import UserCardContent from '../user_card_content/user_card_content.vue'
import StillImage from '../still-image/still-image.vue'
import Gallery from '../gallery/gallery.vue'
import { filter, find } from 'lodash'
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
import fileType from 'src/services/file_type/file_type.service'
const Status = {
name: 'Status',
props: [
'statusoid',
'expandable',
'inConversation',
'focused',
'highlight',
'compact',
'replies',
'noReplyLinks',
'noHeading',
'inlineExpanded'
],
data () {
return {
replying: false,
expanded: false,
unmuted: false,
userExpanded: false,
preview: null,
showPreview: false,
showingTall: false,
expandingSubject: typeof this.$store.state.config.collapseMessageWithSubject === 'undefined'
? !this.$store.state.instance.collapseMessageWithSubject
: !this.$store.state.config.collapseMessageWithSubject,
betterShadow: this.$store.state.interface.browserSupport.cssFilter,
maxAttachments: 9
}
},
computed: {
localCollapseSubjectDefault () {
return typeof this.$store.state.config.collapseMessageWithSubject === 'undefined'
? this.$store.state.instance.collapseMessageWithSubject
: this.$store.state.config.collapseMessageWithSubject
},
muteWords () {
return this.$store.state.config.muteWords
},
repeaterClass () {
const user = this.statusoid.user
return highlightClass(user)
},
userClass () {
const user = this.retweet ? (this.statusoid.retweeted_status.user) : this.statusoid.user
return highlightClass(user)
},
deleted () {
return this.statusoid.deleted
},
repeaterStyle () {
const user = this.statusoid.user
const highlight = this.$store.state.config.highlight
return highlightStyle(highlight[user.screen_name])
},
userStyle () {
if (this.noHeading) return
const user = this.retweet ? (this.statusoid.retweeted_status.user) : this.statusoid.user
const highlight = this.$store.state.config.highlight
return highlightStyle(highlight[user.screen_name])
},
hideAttachments () {
return (this.$store.state.config.hideAttachments && !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 },
retweeter () { return this.statusoid.user.name },
retweeterHtml () { return this.statusoid.user.name_html },
status () {
if (this.retweet) {
return this.statusoid.retweeted_status
} else {
return this.statusoid
}
},
loggedIn () {
return !!this.$store.state.users.currentUser
},
muteWordHits () {
const statusText = this.status.text.toLowerCase()
const hits = filter(this.muteWords, (muteWord) => {
return statusText.includes(muteWord.toLowerCase())
})
return hits
},
muted () { return !this.unmuted && (this.status.user.muted || this.muteWordHits.length > 0) },
isFocused () {
// retweet or root of an expanded conversation
if (this.focused) {
return true
} else if (!this.inConversation) {
return false
}
// use conversation highlight only when in conversation
return this.status.id === this.highlight
},
// This is a bit hacky, but we want to approximate post height before rendering
// so we count newlines (masto uses <p> for paragraphs, GS uses <br> between them)
// as well as approximate line count by counting characters and approximating ~80
// per line.
//
// Using max-height + overflow: auto for status components resulted in false positives
// very often with japanese characters, and it was very annoying.
tallStatus () {
const lengthScore = this.status.statusnet_html.split(/<p|<br/).length + this.status.text.length / 80
return lengthScore > 20
},
isReply () {
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 () {
if (this.$store.state.config.replyVisibility === 'all') {
return false
}
if (this.inlineExpanded || this.expanded || this.inConversation || !this.isReply) {
return false
}
if (this.status.user.id === this.$store.state.users.currentUser.id) {
return false
}
- if (this.status.activity_type === 'repeat') {
+ if (this.status.type === 'retweet') {
return false
}
var checkFollowing = this.$store.state.config.replyVisibility === 'following'
for (var i = 0; i < this.status.attentions.length; ++i) {
if (this.status.user.id === this.status.attentions[i].id) {
continue
}
if (checkFollowing && this.status.attentions[i].following) {
return false
}
if (this.status.attentions[i].id === this.$store.state.users.currentUser.id) {
return false
}
}
return this.status.attentions.length > 0
},
hideSubjectStatus () {
if (this.tallStatus && !this.localCollapseSubjectDefault) {
return false
}
return !this.expandingSubject && this.status.summary
},
hideTallStatus () {
if (this.status.summary && this.localCollapseSubjectDefault) {
return false
}
if (this.showingTall) {
return false
}
return this.tallStatus
},
showingMore () {
return this.showingTall || (this.status.summary && this.expandingSubject)
},
nsfwClickthrough () {
if (!this.status.nsfw) {
return false
}
if (this.status.summary && this.localCollapseSubjectDefault) {
return false
}
return true
},
replySubject () {
if (!this.status.summary) return ''
const behavior = typeof this.$store.state.config.subjectLineBehavior === 'undefined'
? this.$store.state.instance.subjectLineBehavior
: this.$store.state.config.subjectLineBehavior
const startsWithRe = this.status.summary.match(/^re[: ]/i)
if (behavior !== 'noop' && startsWithRe || behavior === 'masto') {
return this.status.summary
} else if (behavior === 'email') {
return 're: '.concat(this.status.summary)
} else if (behavior === 'noop') {
return ''
}
},
attachmentSize () {
if ((this.$store.state.config.hideAttachments && !this.inConversation) ||
(this.$store.state.config.hideAttachmentsInConv && this.inConversation) ||
(this.status.attachments.length > this.maxAttachments)) {
return 'hide'
} else if (this.compact) {
return 'small'
}
return 'normal'
},
galleryTypes () {
if (this.attachmentSize === 'hide') {
return []
}
return this.$store.state.config.playVideosInline
? ['image']
: ['image', 'video']
},
galleryAttachments () {
return this.status.attachments.filter(
file => fileType.fileMatchesSomeType(this.galleryTypes, file)
)
},
nonGalleryAttachments () {
return this.status.attachments.filter(
file => !fileType.fileMatchesSomeType(this.galleryTypes, file)
)
}
},
components: {
Attachment,
FavoriteButton,
RetweetButton,
DeleteButton,
PostStatusForm,
UserCardContent,
StillImage,
Gallery
},
methods: {
visibilityIcon (visibility) {
switch (visibility) {
case 'private':
return 'icon-lock'
case 'unlisted':
return 'icon-lock-open-alt'
case 'direct':
return 'icon-mail-alt'
default:
return 'icon-globe'
}
},
linkClicked ({target}) {
if (target.tagName === 'SPAN') {
target = target.parentNode
}
if (target.tagName === 'A') {
window.open(target.href, '_blank')
}
},
toggleReplying () {
this.replying = !this.replying
},
gotoOriginal (id) {
// only handled by conversation, not status_or_conversation
if (this.inConversation) {
this.$emit('goto', id)
}
},
toggleExpanded () {
this.$emit('toggleExpanded')
},
toggleMute () {
this.unmuted = !this.unmuted
},
toggleUserExpanded () {
this.userExpanded = !this.userExpanded
},
toggleShowMore () {
if (this.showingTall) {
this.showingTall = false
} else if (this.expandingSubject) {
this.expandingSubject = false
} else if (this.hideTallStatus) {
this.showingTall = true
} else if (this.hideSubjectStatus) {
this.expandingSubject = true
}
},
replyEnter (id, event) {
this.showPreview = true
- const targetId = Number(id)
+ const targetId = id
const statuses = this.$store.state.statuses.allStatuses
if (!this.preview) {
// if we have the status somewhere already
this.preview = find(statuses, { 'id': targetId })
// or if we have to fetch it
if (!this.preview) {
this.$store.state.api.backendInteractor.fetchStatus({id}).then((status) => {
this.preview = status
})
}
} else if (this.preview.id !== targetId) {
this.preview = find(statuses, { 'id': targetId })
}
},
replyLeave () {
this.showPreview = false
},
- userProfileLink (id, name) {
+ generateUserProfileLink (id, name) {
return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames)
},
setMedia () {
const attachments = this.attachmentSize === 'hide' ? this.status.attachments : this.galleryAttachments
return () => this.$store.dispatch('setMedia', attachments)
}
},
watch: {
'highlight': function (id) {
- id = Number(id)
if (this.status.id === id) {
let rect = this.$el.getBoundingClientRect()
if (rect.top < 100) {
window.scrollBy(0, rect.top - 200)
} else if (rect.bottom > window.innerHeight - 50) {
window.scrollBy(0, rect.bottom - window.innerHeight + 50)
}
}
}
},
filters: {
capitalize: function (str) {
return str.charAt(0).toUpperCase() + str.slice(1)
}
}
}
export default Status
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index ba3b9a4a38..c1800d6466 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -1,601 +1,601 @@
<template>
<div class="status-el" v-if="!hideReply && !deleted" :class="[{ 'status-el_focused': isFocused }, { 'status-conversation': inlineExpanded }]">
<template v-if="muted && !noReplyLinks">
<div class="media status container muted">
<small>
- <router-link :to="userProfileLink(status.user.id, status.user.screen_name)">
+ <router-link :to="userProfileLink">
{{status.user.screen_name}}
</router-link>
</small>
<small class="muteWords">{{muteWordHits.join(', ')}}</small>
<a href="#" class="unmute" @click.prevent="toggleMute"><i class="button-icon icon-eye-off"></i></a>
</div>
</template>
<template v-else>
<div v-if="retweet && !noHeading" :class="[repeaterClass, { highlighted: repeaterStyle }]" :style="[repeaterStyle]" class="media container retweet-info">
<StillImage v-if="retweet" class='avatar' :class='{ "better-shadow": betterShadow }' :src="statusoid.user.profile_image_url_original"/>
<div class="media-body faint">
<a v-if="retweeterHtml" :href="statusoid.user.statusnet_profile_url" class="user-name" :title="'@'+statusoid.user.screen_name" v-html="retweeterHtml"></a>
<a v-else :href="statusoid.user.statusnet_profile_url" class="user-name" :title="'@'+statusoid.user.screen_name">{{retweeter}}</a>
<i class='fa icon-retweet retweeted' :title="$t('tool_tip.repeat')"></i>
{{$t('timeline.repeated')}}
</div>
</div>
<div :class="[userClass, { highlighted: userStyle, 'is-retweet': retweet }]" :style="[ userStyle ]" class="media status">
<div v-if="!noHeading" class="media-left">
<a :href="status.user.statusnet_profile_url" @click.stop.prevent.capture="toggleUserExpanded">
<StillImage class='avatar' :class="{'avatar-compact': compact, 'better-shadow': betterShadow}" :src="status.user.profile_image_url_original"/>
</a>
</div>
<div class="status-body">
<div class="usercard media-body" v-if="userExpanded">
<user-card-content :user="status.user" :switcher="false"></user-card-content>
</div>
<div v-if="!noHeading" class="media-body container media-heading">
<div class="media-heading-left">
<div class="name-and-links">
<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>
<span class="links">
- <router-link :to="userProfileLink(status.user.id, status.user.screen_name)">
+ <router-link :to="userProfileLink">
{{status.user.screen_name}}
</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>
- <router-link :to="userProfileLink(status.in_reply_to_user_id, status.in_reply_to_screen_name)">
- {{status.in_reply_to_screen_name}}
+ <router-link :to="replyProfileLink">
+ {{replyToName}}
</router-link>
</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>
</a>
</span>
</div>
<h4 class="replies" v-if="inConversation && !noReplyLinks">
<small v-if="replies.length">Replies:</small>
<small class="reply-link" v-for="reply in replies">
<a href="#" @click.prevent="gotoOriginal(reply.id)" @mouseenter="replyEnter(reply.id, $event)" @mouseout="replyLeave()">{{reply.name}}&nbsp;</a>
</small>
</h4>
</div>
<div class="media-heading-right">
<router-link class="timeago" :to="{ name: 'conversation', params: { id: status.id } }">
<timeago :since="status.created_at" :auto-update="60"></timeago>
</router-link>
<div class="button-icon visibility-icon" v-if="status.visibility">
<i :class="visibilityIcon(status.visibility)" :title="status.visibility | capitalize"></i>
</div>
<a :href="status.external_url" target="_blank" v-if="!status.is_local" class="source_url" title="Source">
<i class="button-icon icon-link-ext-alt"></i>
</a>
<template v-if="expandable">
<a href="#" @click.prevent="toggleExpanded" title="Expand">
<i class="button-icon icon-plus-squared"></i>
</a>
</template>
<a href="#" @click.prevent="toggleMute" v-if="unmuted"><i class="button-icon icon-eye-off"></i></a>
</div>
</div>
<div v-if="showPreview" class="status-preview-container">
<status class="status-preview" v-if="preview" :noReplyLinks="true" :statusoid="preview" :compact=true></status>
<div class="status-preview status-preview-loading" v-else>
<i class="icon-spin4 animate-spin"></i>
</div>
</div>
<div :class="{'tall-status': hideTallStatus}" class="status-content-wrapper">
<a class="tall-status-hider" :class="{ 'tall-status-hider_focused': isFocused }" v-if="hideTallStatus" href="#" @click.prevent="toggleShowMore">Show more</a>
<div @click.prevent="linkClicked" class="status-content media-body" v-html="status.statusnet_html" v-if="!hideSubjectStatus"></div>
<div @click.prevent="linkClicked" class="status-content media-body" v-html="status.summary_html" v-else></div>
<a v-if="hideSubjectStatus" href="#" class="cw-status-hider" @click.prevent="toggleShowMore">Show more</a>
<a v-if="showingMore" href="#" class="status-unhider" @click.prevent="toggleShowMore">Show less</a>
</div>
<div v-if="status.attachments && !hideSubjectStatus" class="attachments media-body">
<attachment
class="non-gallery"
v-for="attachment in nonGalleryAttachments"
:size="attachmentSize"
:nsfw="nsfwClickthrough"
:attachment="attachment"
:allowPlay="true"
:setMedia="setMedia()"
:key="attachment.id"
/>
<gallery
v-if="galleryAttachments.length > 0"
:nsfw="nsfwClickthrough"
:attachments="galleryAttachments"
:setMedia="setMedia()"
/>
</div>
<div v-if="!noHeading && !noReplyLinks" class='status-actions media-body'>
<div v-if="loggedIn">
<a href="#" v-on:click.prevent="toggleReplying" :title="$t('tool_tip.reply')">
<i class="button-icon icon-reply" :class="{'icon-reply-active': replying}"></i>
</a>
</div>
<retweet-button :visibility='status.visibility' :loggedIn='loggedIn' :status='status'></retweet-button>
<favorite-button :loggedIn='loggedIn' :status='status'></favorite-button>
<delete-button :status='status'></delete-button>
</div>
</div>
</div>
<div class="container" v-if="replying">
<div class="reply-left"/>
<post-status-form class="reply-body" :reply-to="status.id" :attentions="status.attentions" :repliedUser="status.user" :copy-message-scope="status.visibility" :subject="replySubject" v-on:posted="toggleReplying"/>
</div>
</template>
</div>
</template>
<script src="./status.js" ></script>
<style lang="scss">
@import '../../_variables.scss';
.status-body {
flex: 1;
min-width: 0;
}
.status-preview.status-el {
border-style: solid;
border-width: 1px;
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
}
.status-preview-container {
position: relative;
max-width: 100%;
}
.status-preview {
position: absolute;
max-width: 95%;
display: flex;
background-color: $fallback--bg;
background-color: var(--bg, $fallback--bg);
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
border-style: solid;
border-width: 1px;
border-radius: $fallback--tooltipRadius;
border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
box-shadow: 2px 2px 3px rgba(0, 0, 0, 0.5);
box-shadow: var(--popupShadow);
margin-top: 0.25em;
margin-left: 0.5em;
z-index: 50;
.status {
flex: 1;
border: 0;
min-width: 15em;
}
}
.status-preview-loading {
display: block;
min-width: 15em;
padding: 1em;
text-align: center;
border-width: 1px;
border-style: solid;
i {
font-size: 2em;
}
}
.status-el {
hyphens: auto;
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-word;
border-left-width: 0px;
line-height: 18px;
min-width: 0;
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
border-left: 4px $fallback--cRed;
border-left: 4px var(--cRed, $fallback--cRed);
&_focused {
background-color: $fallback--lightBg;
background-color: var(--lightBg, $fallback--lightBg);
}
.timeline & {
border-bottom-width: 1px;
border-bottom-style: solid;
}
.media-body {
flex: 1;
padding: 0;
margin: 0 0 0.25em 0.8em;
}
.usercard {
margin-bottom: .7em
}
.media-heading {
flex-wrap: nowrap;
line-height: 18px;
}
.media-heading-left {
padding: 0;
vertical-align: bottom;
flex-basis: 100%;
small {
font-weight: lighter;
}
h4 {
white-space: nowrap;
font-size: 14px;
margin-right: 0.25em;
overflow: hidden;
text-overflow: ellipsis;
}
.name-and-links {
padding: 0;
flex: 1 0;
display: flex;
flex-wrap: wrap;
align-items: baseline;
.user-name {
margin-right: .45em;
img {
width: 14px;
height: 14px;
vertical-align: middle;
object-fit: contain
}
}
}
.links {
display: flex;
font-size: 12px;
color: $fallback--link;
color: var(--link, $fallback--link);
max-width: 100%;
a {
max-width: 100%;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
}
.reply-info {
display: flex;
}
.replies {
line-height: 16px;
}
.reply-link {
margin-right: 0.2em;
}
}
.media-heading-right {
display: inline-flex;
flex-shrink: 0;
flex-wrap: nowrap;
margin-left: .25em;
align-self: baseline;
.timeago {
margin-right: 0.2em;
font-size: 12px;
align-self: last baseline;
}
> * {
margin-left: 0.2em;
}
a:hover i {
color: $fallback--text;
color: var(--text, $fallback--text);
}
}
a {
display: inline-block;
word-break: break-all;
}
.tall-status {
position: relative;
height: 220px;
overflow-x: hidden;
overflow-y: hidden;
}
.tall-status-hider {
position: absolute;
height: 70px;
margin-top: 150px;
width: 100%;
text-align: center;
line-height: 110px;
background: linear-gradient(to bottom, rgba(0, 0, 0, 0), $fallback--bg 80%);
background: linear-gradient(to bottom, rgba(0, 0, 0, 0), var(--bg, $fallback--bg) 80%);
&_focused {
background: linear-gradient(to bottom, rgba(0, 0, 0, 0), $fallback--lightBg 80%);
background: linear-gradient(to bottom, rgba(0, 0, 0, 0), var(--lightBg, $fallback--lightBg) 80%);
}
}
.status-unhider, .cw-status-hider {
width: 100%;
text-align: center;
}
.status-content {
margin-right: 0.5em;
font-family: var(--postFont, sans-serif);
img, video {
max-width: 100%;
max-height: 400px;
vertical-align: middle;
object-fit: contain;
}
blockquote {
margin: 0.2em 0 0.2em 2em;
font-style: italic;
}
pre {
overflow: auto;
}
code, samp, kbd, var, pre {
font-family: var(--postCodeFont, monospace);
}
p {
margin: 0;
margin-top: 0.2em;
margin-bottom: 0.5em;
}
h1 {
font-size: 1.1em;
line-height: 1.2em;
margin: 1.4em 0;
}
h2 {
font-size: 1.1em;
margin: 1.0em 0;
}
h3 {
font-size: 1em;
margin: 1.2em 0;
}
h4 {
margin: 1.1em 0;
}
}
.retweet-info {
padding: 0.4em 0.6em 0 0.6em;
margin: 0;
.avatar {
border-radius: $fallback--avatarAltRadius;
border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
margin-left: 28px;
width: 20px;
height: 20px;
}
.media-body {
font-size: 1em;
line-height: 22px;
display: flex;
align-content: center;
flex-wrap: wrap;
.user-name {
font-weight: bold;
img {
width: 14px;
height: 14px;
vertical-align: middle;
object-fit: contain
}
}
i {
padding: 0 0.2em;
}
a {
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
}
.status-fadein {
animation-duration: 0.4s;
animation-name: fadein;
}
@keyframes fadein {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.greentext {
color: green;
}
.status-conversation {
border-left-style: solid;
}
.status-actions {
width: 100%;
display: flex;
div, favorite-button {
padding-top: 0.25em;
max-width: 6em;
flex: 1;
}
}
.icon-reply:hover {
color: $fallback--cBlue;
color: var(--cBlue, $fallback--cBlue);
}
.icon-reply.icon-reply-active {
color: $fallback--cBlue;
color: var(--cBlue, $fallback--cBlue);
}
.status .avatar-compact {
width: 32px;
height: 32px;
box-shadow: var(--avatarStatusShadow);
border-radius: $fallback--avatarAltRadius;
border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
&.better-shadow {
box-shadow: var(--avatarStatusShadowInset);
filter: var(--avatarStatusShadowFilter)
}
}
.avatar.still-image {
width: 48px;
height: 48px;
box-shadow: var(--avatarStatusShadow);
border-radius: $fallback--avatarRadius;
border-radius: var(--avatarRadius, $fallback--avatarRadius);
overflow: hidden;
position: relative;
&.better-shadow {
box-shadow: var(--avatarStatusShadowInset);
filter: var(--avatarStatusShadowFilter)
}
img {
width: 100%;
height: 100%;
}
&.animated::before {
display: none;
}
&.retweeted {
}
}
.status:hover .animated.avatar {
canvas {
display: none;
}
img {
visibility: visible;
}
}
.status {
display: flex;
padding: 0.6em;
&.is-retweet {
padding-top: 0.1em;
}
}
.status-conversation:last-child {
border-bottom: none;
}
.muted {
padding: 0.25em 0.5em;
button {
margin-left: auto;
}
.muteWords {
margin-left: 10px;
}
}
a.unmute {
display: block;
margin-left: auto;
}
.reply-left {
flex: 0;
min-width: 48px;
}
.reply-body {
flex: 1;
}
.timeline > {
.status-el:last-child {
border-bottom-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;;
border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
border-bottom: none;
}
}
@media all and (max-width: 800px) {
.status-el {
.retweet-info {
.avatar {
margin-left: 20px;
}
}
}
.status {
max-width: 100%;
}
.status .avatar {
width: 40px;
height: 40px;
}
.status .avatar-compact {
width: 32px;
height: 32px;
}
}
</style>
diff --git a/src/components/tab_switcher/tab_switcher.jsx b/src/components/tab_switcher/tab_switcher.jsx
index 2f362c4d0b..9038733cd2 100644
--- a/src/components/tab_switcher/tab_switcher.jsx
+++ b/src/components/tab_switcher/tab_switcher.jsx
@@ -1,53 +1,65 @@
import Vue from 'vue'
import './tab_switcher.scss'
export default Vue.component('tab-switcher', {
name: 'TabSwitcher',
data () {
return {
- active: 0
+ active: this.$slots.default.findIndex(_ => _.tag)
}
},
methods: {
- activateTab(index) {
- return () => this.active = index;
+ activateTab (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
- .filter(slot => slot.data)
.map((slot, index) => {
+ if (!slot.tag) return
const classesTab = ['tab']
const classesWrapper = ['tab-wrapper']
if (index === this.active) {
classesTab.push('active')
classesWrapper.push('active')
}
+
return (
<div class={ classesWrapper.join(' ')}>
<button onClick={this.activateTab(index)} class={ classesTab.join(' ') }>{slot.data.attrs.label}</button>
</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
return (
<div class={active ? 'active' : 'hidden'}>
{slot}
</div>
)
- });
+ })
+
return (
<div class="tab-switcher">
<div class="tabs">
{tabs}
</div>
<div class="contents">
{contents}
</div>
</div>
)
}
})
diff --git a/src/components/user_profile/user_profile.js b/src/components/user_profile/user_profile.js
index bde207079a..c9197a1cf5 100644
--- a/src/components/user_profile/user_profile.js
+++ b/src/components/user_profile/user_profile.js
@@ -1,95 +1,111 @@
import UserCardContent from '../user_card_content/user_card_content.vue'
import UserCard from '../user_card/user_card.vue'
import Timeline from '../timeline/timeline.vue'
const UserProfile = {
created () {
this.$store.commit('clearTimeline', { timeline: 'user' })
+ this.$store.commit('clearTimeline', { timeline: 'favorites' })
this.$store.dispatch('startFetching', ['user', this.fetchBy])
+ this.$store.dispatch('startFetching', ['favorites', this.fetchBy])
if (!this.user.id) {
this.$store.dispatch('fetchUser', this.fetchBy)
}
},
destroyed () {
this.$store.dispatch('stopFetching', 'user')
+ this.$store.dispatch('stopFetching', 'favorites')
},
computed: {
timeline () {
return this.$store.state.statuses.timelines.user
},
+ favorites () {
+ return this.$store.state.statuses.timelines.favorites
+ },
userId () {
return this.$route.params.id || this.user.id
},
userName () {
return this.$route.params.name || this.user.screen_name
},
+ isUs () {
+ return this.userId === this.$store.state.users.currentUser.id
+ },
friends () {
return this.user.friends
},
followers () {
return this.user.followers
},
userInStore () {
if (this.isExternal) {
return this.$store.getters.userById(this.userId)
}
return this.$store.getters.userByName(this.userName)
},
user () {
if (this.timeline.statuses[0]) {
return this.timeline.statuses[0].user
}
if (this.userInStore) {
return this.userInStore
}
return {}
},
fetchBy () {
return this.isExternal ? this.userId : this.userName
},
isExternal () {
return this.$route.name === 'external-user-profile'
}
},
methods: {
fetchFollowers () {
const id = this.userId
this.$store.dispatch('addFollowers', { id })
},
fetchFriends () {
const id = this.userId
this.$store.dispatch('addFriends', { id })
}
},
watch: {
+ // TODO get rid of this copypasta
userName () {
if (this.isExternal) {
return
}
this.$store.dispatch('stopFetching', 'user')
+ this.$store.dispatch('stopFetching', 'favorites')
this.$store.commit('clearTimeline', { timeline: 'user' })
+ this.$store.commit('clearTimeline', { timeline: 'favorites' })
this.$store.dispatch('startFetching', ['user', this.fetchBy])
+ this.$store.dispatch('startFetching', ['favorites', this.fetchBy])
},
userId () {
if (!this.isExternal) {
return
}
this.$store.dispatch('stopFetching', 'user')
+ this.$store.dispatch('stopFetching', 'favorites')
this.$store.commit('clearTimeline', { timeline: 'user' })
+ this.$store.commit('clearTimeline', { timeline: 'favorites' })
this.$store.dispatch('startFetching', ['user', this.fetchBy])
+ this.$store.dispatch('startFetching', ['favorites', this.fetchBy])
},
user () {
if (this.user.id && !this.user.followers) {
this.fetchFollowers()
this.fetchFriends()
}
}
},
components: {
UserCardContent,
UserCard,
Timeline
}
}
export default UserProfile
diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue
index 506190260d..d64ce27703 100644
--- a/src/components/user_profile/user_profile.vue
+++ b/src/components/user_profile/user_profile.vue
@@ -1,84 +1,85 @@
<template>
<div>
<div v-if="user.id" class="user-profile panel panel-default">
<user-card-content :user="user" :switcher="true" :selected="timeline.viewing"></user-card-content>
<tab-switcher>
<Timeline :label="$t('user_card.statuses')" :embedded="true" :title="$t('user_profile.timeline_title')" :timeline="timeline" :timeline-name="'user'" :user-id="fetchBy"/>
<div :label="$t('user_card.followees')">
<div v-if="friends">
<user-card v-for="friend in friends" :key="friend.id" :user="friend" :showFollows="true"></user-card>
</div>
<div class="userlist-placeholder" v-else>
<i class="icon-spin3 animate-spin"></i>
</div>
</div>
<div :label="$t('user_card.followers')">
<div v-if="followers">
<user-card v-for="follower in followers" :key="follower.id" :user="follower" :showFollows="false"></user-card>
</div>
<div class="userlist-placeholder" v-else>
<i class="icon-spin3 animate-spin"></i>
</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>
</div>
<div v-else class="panel user-profile-placeholder">
<div class="panel-heading">
<div class="title">
{{ $t('settings.profile_tab') }}
</div>
</div>
<div class="panel-body">
<i class="icon-spin3 animate-spin"></i>
</div>
</div>
</div>
</template>
<script src="./user_profile.js"></script>
<style lang="scss">
.user-profile {
flex: 2;
flex-basis: 500px;
.profile-panel-background .panel-heading {
background: transparent;
flex-direction: column;
align-items: stretch;
}
.userlist-placeholder {
display: flex;
justify-content: center;
align-items: middle;
padding: 2em;
}
.timeline-heading {
display: flex;
justify-content: center;
.loadmore-button, .alert {
flex: 1;
}
.loadmore-button {
height: 28px;
margin: 10px .6em;
}
.title, .loadmore-text {
display: none
}
}
}
.user-profile-placeholder {
.panel-body {
display: flex;
justify-content: center;
align-items: middle;
padding: 7em;
}
}
</style>
diff --git a/src/components/who_to_follow_panel/who_to_follow_panel.js b/src/components/who_to_follow_panel/who_to_follow_panel.js
index 5e204001ae..a56a27eafa 100644
--- a/src/components/who_to_follow_panel/who_to_follow_panel.js
+++ b/src/components/who_to_follow_panel/who_to_follow_panel.js
@@ -1,77 +1,77 @@
import apiService from '../../services/api/api.service.js'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
-import _ from 'lodash'
+import { shuffle } from 'lodash'
function showWhoToFollow (panel, reply) {
- _.shuffle(reply)
+ const shuffled = shuffle(reply)
panel.usersToFollow.forEach((toFollow, index) => {
- let user = reply[index]
+ let user = shuffled[index]
let img = user.avatar || '/images/avi.png'
let name = user.acct
toFollow.img = img
toFollow.name = name
panel.$store.state.api.backendInteractor.externalProfile(name)
.then((externalUser) => {
if (!externalUser.error) {
panel.$store.commit('addNewUsers', [externalUser])
toFollow.id = externalUser.id
}
})
})
}
function getWhoToFollow (panel) {
var credentials = panel.$store.state.users.currentUser.credentials
if (credentials) {
panel.usersToFollow.forEach(toFollow => {
toFollow.name = 'Loading...'
})
apiService.suggestions({credentials: credentials})
.then((reply) => {
showWhoToFollow(panel, reply)
})
}
}
const WhoToFollowPanel = {
data: () => ({
usersToFollow: new Array(3).fill().map(x => (
{
img: '/images/avi.png',
name: '',
id: 0
}
))
}),
computed: {
user: function () {
return this.$store.state.users.currentUser.screen_name
},
suggestionsEnabled () {
return this.$store.state.instance.suggestionsEnabled
}
},
methods: {
userProfileLink (id, name) {
return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames)
}
},
watch: {
user: function (user, oldUser) {
if (this.suggestionsEnabled) {
getWhoToFollow(this)
}
}
},
mounted:
function () {
if (this.suggestionsEnabled) {
getWhoToFollow(this)
}
}
}
export default WhoToFollowPanel
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 6008287f5e..2c6887bc56 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -1,374 +1,375 @@
{
"chat": {
"title": "Chat"
},
"features_panel": {
"chat": "Chat",
"gopher": "Gopher",
"media_proxy": "Media proxy",
"scope_options": "Scope options",
"text_limit": "Text limit",
"title": "Features",
"who_to_follow": "Who to follow"
},
"finder": {
"error_fetching_user": "Error fetching user",
"find_user": "Find user"
},
"general": {
"apply": "Apply",
"submit": "Submit"
},
"login": {
"login": "Log in",
"description": "Log in with OAuth",
"logout": "Log out",
"password": "Password",
"placeholder": "e.g. lain",
"register": "Register",
"username": "Username"
},
"nav": {
"about": "About",
"back": "Back",
"chat": "Local Chat",
"friend_requests": "Follow Requests",
"mentions": "Mentions",
"dms": "Direct Messages",
"public_tl": "Public Timeline",
"timeline": "Timeline",
"twkn": "The Whole Known Network",
"user_search": "User Search",
"who_to_follow": "Who to follow",
"preferences": "Preferences"
},
"notifications": {
"broken_favorite": "Unknown status, searching for it...",
"favorited_you": "favorited your status",
"followed_you": "followed you",
"load_older": "Load older notifications",
"notifications": "Notifications",
"read": "Read!",
"repeated_you": "repeated your status"
},
"post_status": {
"new_status": "Post new status",
"account_not_locked_warning": "Your account is not {0}. Anyone can follow you to view your follower-only posts.",
"account_not_locked_warning_link": "locked",
"attachments_sensitive": "Mark attachments as sensitive",
"content_type": {
"plain_text": "Plain text"
},
"content_warning": "Subject (optional)",
"default": "Just landed in L.A.",
"direct_warning": "This post will only be visible to all the mentioned users.",
"posting": "Posting",
"scope": {
"direct": "Direct - Post to mentioned users only",
"private": "Followers-only - Post to followers only",
"public": "Public - Post to public timelines",
"unlisted": "Unlisted - Do not post to public timelines"
}
},
"registration": {
"bio": "Bio",
"email": "Email",
"fullname": "Display name",
"password_confirm": "Password confirmation",
"registration": "Registration",
"token": "Invite token",
"captcha": "CAPTCHA",
"new_captcha": "Click the image to get a new captcha",
"validations": {
"username_required": "cannot be left blank",
"fullname_required": "cannot be left blank",
"email_required": "cannot be left blank",
"password_required": "cannot be left blank",
"password_confirmation_required": "cannot be left blank",
"password_confirmation_match": "should be the same as password"
}
},
"settings": {
"attachmentRadius": "Attachments",
"attachments": "Attachments",
"autoload": "Enable automatic loading when scrolled to the bottom",
"avatar": "Avatar",
"avatarAltRadius": "Avatars (Notifications)",
"avatarRadius": "Avatars",
"background": "Background",
"bio": "Bio",
"btnRadius": "Buttons",
"cBlue": "Blue (Reply, follow)",
"cGreen": "Green (Retweet)",
"cOrange": "Orange (Favorite)",
"cRed": "Red (Cancel)",
"change_password": "Change Password",
"change_password_error": "There was an issue changing your password.",
"changed_password": "Password changed successfully!",
"collapse_subject": "Collapse posts with subjects",
"composing": "Composing",
"confirm_new_password": "Confirm new password",
"current_avatar": "Your current avatar",
"current_password": "Current password",
"current_profile_banner": "Your current profile banner",
"data_import_export_tab": "Data Import / Export",
"default_vis": "Default visibility scope",
"delete_account": "Delete Account",
"delete_account_description": "Permanently delete your account and all your messages.",
"delete_account_error": "There was an issue deleting your account. If this persists please contact your instance administrator.",
"delete_account_instructions": "Type your password in the input below to confirm account deletion.",
"export_theme": "Save preset",
"filtering": "Filtering",
"filtering_explanation": "All statuses containing these words will be muted, one per line",
"follow_export": "Follow export",
"follow_export_button": "Export your follows to a csv file",
"follow_export_processing": "Processing, you'll soon be asked to download your file",
"follow_import": "Follow import",
"follow_import_error": "Error importing followers",
"follows_imported": "Follows imported! Processing them will take a while.",
"foreground": "Foreground",
"general": "General",
"hide_attachments_in_convo": "Hide attachments in conversations",
"hide_attachments_in_tl": "Hide attachments in timeline",
"hide_isp": "Hide instance-specific panel",
"preload_images": "Preload images",
"use_one_click_nsfw": "Open NSFW attachments with just one click",
"hide_post_stats": "Hide post statistics (e.g. the number of favorites)",
"hide_user_stats": "Hide user statistics (e.g. the number of followers)",
"import_followers_from_a_csv_file": "Import follows from a csv file",
"import_theme": "Load preset",
"inputRadius": "Input fields",
"checkboxRadius": "Checkboxes",
"instance_default": "(default: {value})",
"instance_default_simple": "(default)",
"interface": "Interface",
"interfaceLanguage": "Interface language",
"invalid_theme_imported": "The selected file is not a supported Pleroma theme. No changes to your theme were made.",
"limited_availability": "Unavailable in your browser",
"links": "Links",
"lock_account_description": "Restrict your account to approved followers only",
"loop_video": "Loop videos",
"loop_video_silent_only": "Loop only videos without sound (i.e. Mastodon's \"gifs\")",
"play_videos_inline": "Play videos directly on timeline",
"use_contain_fit": "Don't crop the attachment in thumbnails",
"name": "Name",
"name_bio": "Name & Bio",
"new_password": "New password",
"notification_visibility": "Types of notifications to show",
"notification_visibility_follows": "Follows",
"notification_visibility_likes": "Likes",
"notification_visibility_mentions": "Mentions",
"notification_visibility_repeats": "Repeats",
"no_rich_text_description": "Strip rich text formatting from all posts",
"hide_network_description": "Don't show who I'm following and who's following me",
"nsfw_clickthrough": "Enable clickthrough NSFW attachment hiding",
"panelRadius": "Panels",
"pause_on_unfocused": "Pause streaming when tab is not focused",
"presets": "Presets",
"profile_background": "Profile Background",
"profile_banner": "Profile Banner",
"profile_tab": "Profile",
"radii_help": "Set up interface edge rounding (in pixels)",
"replies_in_timeline": "Replies in timeline",
"reply_link_preview": "Enable reply-link preview on mouse hover",
"reply_visibility_all": "Show all replies",
"reply_visibility_following": "Only show replies directed at me or users I'm following",
"reply_visibility_self": "Only show replies directed at me",
"saving_err": "Error saving settings",
"saving_ok": "Settings saved",
"security_tab": "Security",
"scope_copy": "Copy scope when replying (DMs are always copied)",
"set_new_avatar": "Set new avatar",
"set_new_profile_background": "Set new profile background",
"set_new_profile_banner": "Set new profile banner",
"settings": "Settings",
"subject_input_always_show": "Always show subject field",
"subject_line_behavior": "Copy subject when replying",
"subject_line_email": "Like email: \"re: subject\"",
"subject_line_mastodon": "Like mastodon: copy as is",
"subject_line_noop": "Do not copy",
"stop_gifs": "Play-on-hover GIFs",
"streaming": "Enable automatic streaming of new posts when scrolled to the top",
"text": "Text",
"theme": "Theme",
"theme_help": "Use hex color codes (#rrggbb) to customize your color theme.",
"theme_help_v2_1": "You can also override certain component's colors and opacity by toggling the checkbox, use \"Clear all\" button to clear all overrides.",
"theme_help_v2_2": "Icons underneath some entries are background/text contrast indicators, hover over for detailed info. Please keep in mind that when using transparency contrast indicators show the worst possible case.",
"tooltipRadius": "Tooltips/alerts",
"user_settings": "User Settings",
"values": {
"false": "no",
"true": "yes"
},
"notifications": "Notifications",
"enable_web_push_notifications": "Enable web push notifications",
"style": {
"switcher": {
"keep_color": "Keep colors",
"keep_shadows": "Keep shadows",
"keep_opacity": "Keep opacity",
"keep_roundness": "Keep roundness",
"keep_fonts": "Keep fonts",
"save_load_hint": "\"Keep\" options preserve currently set options when selecting or loading themes, it also stores said options when exporting a theme. When all checkboxes unset, exporting theme will save everything.",
"reset": "Reset",
"clear_all": "Clear all",
"clear_opacity": "Clear opacity"
},
"common": {
"color": "Color",
"opacity": "Opacity",
"contrast": {
"hint": "Contrast ratio is {ratio}, it {level} {context}",
"level": {
"aa": "meets Level AA guideline (minimal)",
"aaa": "meets Level AAA guideline (recommended)",
"bad": "doesn't meet any accessibility guidelines"
},
"context": {
"18pt": "for large (18pt+) text",
"text": "for text"
}
}
},
"common_colors": {
"_tab_label": "Common",
"main": "Common colors",
"foreground_hint": "See \"Advanced\" tab for more detailed control",
"rgbo": "Icons, accents, badges"
},
"advanced_colors": {
"_tab_label": "Advanced",
"alert": "Alert background",
"alert_error": "Error",
"badge": "Badge background",
"badge_notification": "Notification",
"panel_header": "Panel header",
"top_bar": "Top bar",
"borders": "Borders",
"buttons": "Buttons",
"inputs": "Input fields",
"faint_text": "Faded text"
},
"radii": {
"_tab_label": "Roundness"
},
"shadows": {
"_tab_label": "Shadow and lighting",
"component": "Component",
"override": "Override",
"shadow_id": "Shadow #{value}",
"blur": "Blur",
"spread": "Spread",
"inset": "Inset",
"hint": "For shadows you can also use --variable as a color value to use CSS3 variables. Please note that setting opacity won't work in this case.",
"filter_hint": {
"always_drop_shadow": "Warning, this shadow always uses {0} when browser supports it.",
"drop_shadow_syntax": "{0} does not support {1} parameter and {2} keyword.",
"avatar_inset": "Please note that combining both inset and non-inset shadows on avatars might give unexpected results with transparent avatars.",
"spread_zero": "Shadows with spread > 0 will appear as if it was set to zero",
"inset_classic": "Inset shadows will be using {0}"
},
"components": {
"panel": "Panel",
"panelHeader": "Panel header",
"topBar": "Top bar",
"avatar": "User avatar (in profile view)",
"avatarStatus": "User avatar (in post display)",
"popup": "Popups and tooltips",
"button": "Button",
"buttonHover": "Button (hover)",
"buttonPressed": "Button (pressed)",
"buttonPressedHover": "Button (pressed+hover)",
"input": "Input field"
}
},
"fonts": {
"_tab_label": "Fonts",
"help": "Select font to use for elements of UI. For \"custom\" you have to enter exact font name as it appears in system.",
"components": {
"interface": "Interface",
"input": "Input fields",
"post": "Post text",
"postCode": "Monospaced text in a post (rich text)"
},
"family": "Font name",
"size": "Size (in px)",
"weight": "Weight (boldness)",
"custom": "Custom"
},
"preview": {
"header": "Preview",
"content": "Content",
"error": "Example error",
"button": "Button",
"text": "A bunch of more {0} and {1}",
"mono": "content",
"input": "Just landed in L.A.",
"faint_link": "helpful manual",
"fine_print": "Read our {0} to learn nothing useful!",
"header_faint": "This is fine",
"checkbox": "I have skimmed over terms and conditions",
"link": "a nice lil' link"
}
}
},
"timeline": {
"collapse": "Collapse",
"conversation": "Conversation",
"error_fetching": "Error fetching updates",
"load_older": "Load older statuses",
"no_retweet_hint": "Post is marked as followers-only or direct and cannot be repeated",
"repeated": "repeated",
"show_new": "Show new",
"up_to_date": "Up-to-date"
},
"user_card": {
"approve": "Approve",
"block": "Block",
"blocked": "Blocked!",
"deny": "Deny",
+ "favorites": "Favorites",
"follow": "Follow",
"follow_sent": "Request sent!",
"follow_progress": "Requesting…",
"follow_again": "Send request again?",
"follow_unfollow": "Stop following",
"followees": "Following",
"followers": "Followers",
"following": "Following!",
"follows_you": "Follows you!",
"its_you": "It's you!",
"mute": "Mute",
"muted": "Muted",
"per_day": "per day",
"remote_follow": "Remote follow",
"statuses": "Statuses"
},
"user_profile": {
"timeline_title": "User Timeline"
},
"who_to_follow": {
"more": "More",
"who_to_follow": "Who to follow"
},
"tool_tip": {
"media_upload": "Upload Media",
"repeat": "Repeat",
"reply": "Reply",
"favorite": "Favorite",
"user_settings": "User Settings"
},
"upload":{
"error": {
"base": "Upload failed.",
"file_too_big": "File too big [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]",
"default": "Try again later"
},
"file_size_units": {
"B": "B",
"KiB": "KiB",
"MiB": "MiB",
"GiB": "GiB",
"TiB": "TiB"
}
}
}
diff --git a/src/i18n/ru.json b/src/i18n/ru.json
index 249eab6965..0887bb5908 100644
--- a/src/i18n/ru.json
+++ b/src/i18n/ru.json
@@ -1,300 +1,301 @@
{
"chat": {
"title": "Чат"
},
"finder": {
"error_fetching_user": "Пользователь не найден",
"find_user": "Найти пользователя"
},
"general": {
"apply": "Применить",
"submit": "Отправить"
},
"login": {
"login": "Войти",
"logout": "Выйти",
"password": "Пароль",
"placeholder": "e.c. lain",
"register": "Зарегистрироваться",
"username": "Имя пользователя"
},
"nav": {
"back": "Назад",
"chat": "Локальный чат",
"mentions": "Упоминания",
"public_tl": "Публичная лента",
"timeline": "Лента",
"twkn": "Федеративная лента"
},
"notifications": {
"broken_favorite": "Неизвестный статус, ищем...",
"favorited_you": "нравится ваш статус",
"followed_you": "начал(а) читать вас",
"load_older": "Загрузить старые уведомления",
"notifications": "Уведомления",
"read": "Прочесть",
"repeated_you": "повторил(а) ваш статус"
},
"post_status": {
"account_not_locked_warning": "Ваш аккаунт не {0}. Кто угодно может зафоловить вас чтобы прочитать посты только для подписчиков",
"account_not_locked_warning_link": "залочен",
"attachments_sensitive": "Вложения содержат чувствительный контент",
"content_warning": "Тема (не обязательно)",
"default": "Что нового?",
"direct_warning": "Этот пост будет видет только упомянутым пользователям",
"posting": "Отправляется",
"scope": {
"direct": "Личное - этот пост видят только те кто в нём упомянут",
"private": "Для подписчиков - этот пост видят только подписчики",
"public": "Публичный - этот пост виден всем",
"unlisted": "Непубличный - этот пост не виден на публичных лентах"
}
},
"registration": {
"bio": "Описание",
"email": "Email",
"fullname": "Отображаемое имя",
"password_confirm": "Подтверждение пароля",
"registration": "Регистрация",
"token": "Код приглашения",
"validations": {
"username_required": "не должно быть пустым",
"fullname_required": "не должно быть пустым",
"email_required": "не должен быть пустым",
"password_required": "не должен быть пустым",
"password_confirmation_required": "не должно быть пустым",
"password_confirmation_match": "должно совпадать с паролем"
}
},
"settings": {
"attachmentRadius": "Прикреплённые файлы",
"attachments": "Вложения",
"autoload": "Включить автоматическую загрузку при прокрутке вниз",
"avatar": "Аватар",
"avatarAltRadius": "Аватары в уведомлениях",
"avatarRadius": "Аватары",
"background": "Фон",
"bio": "Описание",
"btnRadius": "Кнопки",
"cBlue": "Ответить, читать",
"cGreen": "Повторить",
"cOrange": "Нравится",
"cRed": "Отменить",
"change_password": "Сменить пароль",
"change_password_error": "Произошла ошибка при попытке изменить пароль.",
"changed_password": "Пароль изменён успешно.",
"collapse_subject": "Сворачивать посты с темой",
"confirm_new_password": "Подтверждение нового пароля",
"current_avatar": "Текущий аватар",
"current_password": "Текущий пароль",
"current_profile_banner": "Текущий баннер профиля",
"data_import_export_tab": "Импорт / Экспорт данных",
"delete_account": "Удалить аккаунт",
"delete_account_description": "Удалить ваш аккаунт и все ваши сообщения.",
"delete_account_error": "Возникла ошибка в процессе удаления вашего аккаунта. Если это повторяется, свяжитесь с администратором вашего сервера.",
"delete_account_instructions": "Введите ваш пароль в поле ниже для подтверждения удаления.",
"export_theme": "Сохранить Тему",
"filtering": "Фильтрация",
"filtering_explanation": "Все статусы, содержащие данные слова, будут игнорироваться, по одному в строке",
"follow_export": "Экспортировать читаемых",
"follow_export_button": "Экспортировать читаемых в файл .csv",
"follow_export_processing": "Ведётся обработка, скоро вам будет предложено загрузить файл",
"follow_import": "Импортировать читаемых",
"follow_import_error": "Ошибка при импортировании читаемых.",
"follows_imported": "Список читаемых импортирован. Обработка займёт некоторое время..",
"foreground": "Передний план",
"general": "Общие",
"hide_attachments_in_convo": "Прятать вложения в разговорах",
"hide_attachments_in_tl": "Прятать вложения в ленте",
"hide_isp": "Скрыть серверную панель",
"import_followers_from_a_csv_file": "Импортировать читаемых из файла .csv",
"import_theme": "Загрузить Тему",
"inputRadius": "Поля ввода",
"checkboxRadius": "Чекбоксы",
"interface": "Интерфейс",
"interfaceLanguage": "Язык интерфейса",
"limited_availability": "Не доступно в вашем браузере",
"links": "Ссылки",
"lock_account_description": "Аккаунт доступен только подтверждённым подписчикам",
"loop_video": "Зациливать видео",
"loop_video_silent_only": "Зацикливать только беззвучные видео (т.е. \"гифки\" с Mastodon)",
"name": "Имя",
"name_bio": "Имя и описание",
"new_password": "Новый пароль",
"notification_visibility": "Показывать уведомления",
"notification_visibility_follows": "Подписки",
"notification_visibility_likes": "Лайки",
"notification_visibility_mentions": "Упоминания",
"notification_visibility_repeats": "Повторы",
"no_rich_text_description": "Убрать форматирование из всех постов",
"hide_network_description": "Не показывать кого я читаю и кто меня читает",
"nsfw_clickthrough": "Включить скрытие NSFW вложений",
"panelRadius": "Панели",
"pause_on_unfocused": "Приостановить загрузку когда вкладка не в фокусе",
"presets": "Пресеты",
"profile_background": "Фон профиля",
"profile_banner": "Баннер профиля",
"profile_tab": "Профиль",
"radii_help": "Скругление углов элементов интерфейса (в пикселях)",
"replies_in_timeline": "Ответы в ленте",
"reply_link_preview": "Включить предварительный просмотр ответа при наведении мыши",
"reply_visibility_all": "Показывать все ответы",
"reply_visibility_following": "Показывать только ответы мне и тех на кого я подписан",
"reply_visibility_self": "Показывать только ответы мне",
"security_tab": "Безопасность",
"set_new_avatar": "Загрузить новый аватар",
"set_new_profile_background": "Загрузить новый фон профиля",
"set_new_profile_banner": "Загрузить новый баннер профиля",
"settings": "Настройки",
"subject_input_always_show": "Всегда показывать поле ввода темы",
"stop_gifs": "Проигрывать GIF анимации только при наведении",
"streaming": "Включить автоматическую загрузку новых сообщений при прокрутке вверх",
"text": "Текст",
"theme": "Тема",
"theme_help": "Используйте шестнадцатеричные коды цветов (#rrggbb) для настройки темы.",
"theme_help_v2_1": "Вы так же можете перепоределить цвета определенных компонентов нажав соотв. галочку. Используйте кнопку \"Очистить всё\" чтобы снять все переопределения",
"theme_help_v2_2": "Под некоторыми полями ввода это идикаторы контрастности, наведите на них мышью чтобы узнать больше. Приспользовании прозрачности контраст расчитывается для наихудшего варианта.",
"tooltipRadius": "Всплывающие подсказки/уведомления",
"user_settings": "Настройки пользователя",
"style": {
"switcher": {
"keep_color": "Оставить цвета",
"keep_shadows": "Оставить тени",
"keep_opacity": "Оставить прозрачность",
"keep_roundness": "Оставить скругление",
"keep_fonts": "Оставить шрифты",
"save_load_hint": "Опции \"оставить...\" позволяют сохранить текущие настройки при выборе другой темы или импорта её из файла. Так же они влияют на то какие компоненты будут сохранены при экспорте темы. Когда все галочки сняты все компоненты будут экспортированы.",
"reset": "Сбросить",
"clear_all": "Очистить всё",
"clear_opacity": "Очистить прозрачность"
},
"common": {
"color": "Цвет",
"opacity": "Прозрачность",
"contrast": {
"hint": "Уровень контраста: {ratio}, что {level} {context}",
"level": {
"aa": "соответствует гайдлайну Level AA (минимальный)",
"aaa": "соответствует гайдлайну Level AAA (рекомендуемый)",
"bad": "не соответствует каким либо гайдлайнам"
},
"context": {
"18pt": "для крупного (18pt+) текста",
"text": "для текста"
}
}
},
"common_colors": {
"_tab_label": "Общие",
"main": "Общие цвета",
"foreground_hint": "См. вкладку \"Дополнительно\" для более детального контроля",
"rgbo": "Иконки, акценты, ярылки"
},
"advanced_colors": {
"_tab_label": "Дополнительно",
"alert": "Фон уведомлений",
"alert_error": "Ошибки",
"badge": "Фон значков",
"badge_notification": "Уведомления",
"panel_header": "Заголовок панели",
"top_bar": "Верняя полоска",
"borders": "Границы",
"buttons": "Кнопки",
"inputs": "Поля ввода",
"faint_text": "Маловажный текст"
},
"radii": {
"_tab_label": "Скругление"
},
"shadows": {
"_tab_label": "Светотень",
"component": "Компонент",
"override": "Переопределить",
"shadow_id": "Тень №{value}",
"blur": "Размытие",
"spread": "Разброс",
"inset": "Внутренняя",
"hint": "Для теней вы так же можете использовать --variable в качестве цвета чтобы использовать CSS3-переменные. В таком случае прозрачность работать не будет.",
"filter_hint": {
"always_drop_shadow": "Внимание, эта тень всегда использует {0} когда браузер поддерживает это",
"drop_shadow_syntax": "{0} не поддерживает параметр {1} и ключевое слово {2}",
"avatar_inset": "Одновременное использование внутренних и внешних теней на (прозрачных) аватарках может дать не те результаты что вы ожидаете",
"spread_zero": "Тени с разбросом > 0 будут выглядеть как если бы разброс установлен в 0",
"inset_classic": "Внутренние тени будут использовать {0}"
},
"components": {
"panel": "Панель",
"panelHeader": "Заголовок панели",
"topBar": "Верхняя полоска",
"avatar": "Аватарка (профиль)",
"avatarStatus": "Аватарка (в ленте)",
"popup": "Всплывающие подсказки",
"button": "Кнопки",
"buttonHover": "Кнопки (наведен курсор)",
"buttonPressed": "Кнопки (нажата)",
"buttonPressedHover": "Кнопки (нажата+наведен курсор)",
"input": "Поля ввода"
}
},
"fonts": {
"_tab_label": "Шрифты",
"help": "Выберите тип шрифта для использования в интерфейсе. При выборе варианта \"другой\" надо ввести название шрифта в точности как он называется в системе.",
"components": {
"interface": "Интерфейс",
"input": "Поля ввода",
"post": "Текст постов",
"postCode": "Моноширинный текст в посте (форматирование)"
},
"family": "Шрифт",
"size": "Размер (в пикселях)",
"weight": "Ширина",
"custom": "Другой"
},
"preview": {
"header": "Пример",
"content": "Контент",
"error": "Ошибка стоп 000",
"button": "Кнопка",
"text": "Еще немного {0} и масенькая {1}",
"mono": "контента",
"input": "Что нового?",
"faint_link": "Его придется убрать",
"fine_print": "Если проблемы остались — ваш гуртовщик мыши плохо стоит. {0}.",
"header_faint": "Все идет по плану",
"checkbox": "Я подтверждаю что не было ни единого разрыва",
"link": "ссылка"
}
}
},
"timeline": {
"collapse": "Свернуть",
"conversation": "Разговор",
"error_fetching": "Ошибка при обновлении",
"load_older": "Загрузить старые статусы",
"no_retweet_hint": "Пост помечен как \"только для подписчиков\" или \"личное\" и поэтому не может быть повторён",
"repeated": "повторил(а)",
"show_new": "Показать новые",
"up_to_date": "Обновлено"
},
"user_card": {
"block": "Заблокировать",
"blocked": "Заблокирован",
+ "favorites": "Понравившиеся",
"follow": "Читать",
"follow_sent": "Запрос отправлен!",
"follow_progress": "Запрашиваем…",
"follow_again": "Запросить еще заново?",
"follow_unfollow": "Перестать читать",
"followees": "Читаемые",
"followers": "Читатели",
"following": "Читаю",
"follows_you": "Читает вас",
"mute": "Игнорировать",
"muted": "Игнорирую",
"per_day": "в день",
"remote_follow": "Читать удалённо",
"statuses": "Статусы"
},
"user_profile": {
"timeline_title": "Лента пользователя"
}
}
diff --git a/src/modules/statuses.js b/src/modules/statuses.js
index f92239a968..20a8d2eb43 100644
--- a/src/modules/statuses.js
+++ b/src/modules/statuses.js
@@ -1,469 +1,441 @@
-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 parse from '../services/status_parser/status_parser.js'
-const emptyTl = (userId = 0) => ({
+const emptyTl = () => ({
statuses: [],
statusesObject: {},
faves: [],
visibleStatuses: [],
visibleStatusesObject: {},
newStatusCount: 0,
maxId: 0,
minVisibleId: 0,
loading: false,
followers: [],
friends: [],
- flushMarker: 0,
- userId
+ userId: 0,
+ flushMarker: 0
})
export const defaultState = {
allStatuses: [],
allStatusesObject: {},
maxId: 0,
notifications: {
desktopNotificationSilence: true,
maxId: 0,
minId: Number.POSITIVE_INFINITY,
data: [],
idStore: {},
error: false
},
favorites: new Set(),
error: false,
timelines: {
mentions: emptyTl(),
public: emptyTl(),
user: emptyTl(),
+ favorites: emptyTl(),
publicAndExternal: emptyTl(),
friends: emptyTl(),
tag: emptyTl(),
dms: emptyTl()
}
}
-const isNsfw = (status) => {
- const nsfwRegex = /#nsfw/i
- return includes(status.tags, 'nsfw') || !!status.text.match(nsfwRegex)
-}
-
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
status.deleted = false
// To make the array reactive
status.attachments = status.attachments || []
return status
}
const visibleNotificationTypes = (rootState) => {
return [
rootState.config.notificationVisibility.likes && 'like',
rootState.config.notificationVisibility.mentions && 'mention',
rootState.config.notificationVisibility.repeats && 'repeat',
rootState.config.notificationVisibility.follows && 'follow'
].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 oldItem = obj[item.id]
if (oldItem) {
// We already have this, so only merge the new info.
merge(oldItem, item)
// Reactivity fix.
oldItem.attachments.splice(oldItem.attachments.length)
return {item: oldItem, new: false}
} else {
// This is a new item, prepare it
prepareStatus(item)
arr.push(item)
obj[item.id] = item
return {item, new: true}
}
}
+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) => {
- timeline.visibleStatuses = sortBy(timeline.visibleStatuses, ({id}) => -id)
- timeline.statuses = sortBy(timeline.statuses, ({id}) => -id)
+ timeline.visibleStatuses = timeline.visibleStatuses.sort(sortById)
+ timeline.statuses = timeline.statuses.sort(sortById)
timeline.minVisibleId = (last(timeline.visibleStatuses) || {}).id
return timeline
}
const addNewStatuses = (state, { statuses, showImmediately = false, timeline, user = {}, noIdUpdate = false, userId }) => {
// Sanity check
if (!isArray(statuses)) {
return false
}
const allStatuses = state.allStatuses
const allStatusesObject = state.allStatusesObject
const timelineObject = state.timelines[timeline]
const maxNew = statuses.length > 0 ? maxBy(statuses, 'id').id : 0
const older = timeline && maxNew < timelineObject.maxId
if (timeline && !noIdUpdate && statuses.length > 0 && !older) {
timelineObject.maxId = maxNew
}
// This makes sure that user timeline won't get data meant for other
// user. I.e. opening different user profiles makes request which could
// return data late after user already viewing different user profile
if (timeline === 'user' && timelineObject.userId !== userId) {
return
}
- const addStatus = (status, showImmediately, addToTimeline = true) => {
- const result = mergeOrAdd(allStatuses, allStatusesObject, status)
- status = result.item
+ const addStatus = (data, showImmediately, addToTimeline = true) => {
+ const result = mergeOrAdd(allStatuses, allStatusesObject, data)
+ const status = result.item
if (result.new) {
// 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
// Add the mention to the mentions timeline
if (timelineObject !== mentions) {
mergeOrAdd(mentions.statuses, mentions.statusesObject, status)
mentions.newStatusCount += 1
sortTimeline(mentions)
}
}
if (status.visibility === 'direct') {
const dms = state.timelines.dms
mergeOrAdd(dms.statuses, dms.statusesObject, status)
dms.newStatusCount += 1
sortTimeline(dms)
}
}
// Decide if we should treat the status as new for this timeline.
let resultForCurrentTimeline
// Some statuses should only be added to the global status repository.
if (timeline && addToTimeline) {
resultForCurrentTimeline = mergeOrAdd(timelineObject.statuses, timelineObject.statusesObject, status)
}
if (timeline && showImmediately) {
// Add it directly to the visibleStatuses, don't change
// newStatusCount
mergeOrAdd(timelineObject.visibleStatuses, timelineObject.visibleStatusesObject, status)
} else if (timeline && addToTimeline && resultForCurrentTimeline.new) {
// Just change newStatuscount
timelineObject.newStatusCount += 1
}
return status
}
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) {
// This is our favorite, so the relevant bit.
if (favorite.user.id === user.id) {
status.favorited = true
} else {
status.fave_num += 1
}
}
return status
}
const processors = {
'status': (status) => {
addStatus(status, showImmediately)
},
'retweet': (status) => {
// RetweetedStatuses are never shown immediately
const retweetedStatus = addStatus(status.retweeted_status, false, false)
let retweet
// If the retweeted status is already there, don't add the retweet
// to the timeline.
if (timeline && find(timelineObject.statuses, (s) => {
if (s.retweeted_status) {
return s.id === retweetedStatus.id || s.retweeted_status.id === retweetedStatus.id
} else {
return s.id === retweetedStatus.id
}
})) {
// Already have it visible (either as the original or another RT), don't add to timeline, don't show.
retweet = addStatus(status, false, false)
} else {
retweet = addStatus(status, showImmediately)
}
retweet.retweeted_status = retweetedStatus
},
'favorite': (favorite) => {
// Only update if this is a new favorite.
// Ignore our own favorites because we get info about likes as response to like request
if (!state.favorites.has(favorite.id)) {
state.favorites.add(favorite.id)
favoriteStatus(favorite)
}
},
'deletion': (deletion) => {
const uri = deletion.uri
// Remove possible notification
const status = find(allStatuses, {uri})
if (!status) {
return
}
remove(state.notifications.data, ({action: {id}}) => id === status.id)
remove(allStatuses, { uri })
if (timeline) {
remove(timelineObject.statuses, { uri })
remove(timelineObject.visibleStatuses, { uri })
}
},
+ 'follow': (follow) => {
+ // NOOP, it is known status but we don't do anything about it for now
+ },
'default': (unknown) => {
console.log('unknown status type')
console.log(unknown)
}
}
each(statuses, (status) => {
- const type = statusType(status)
+ const type = status.type
const processor = processors[type] || processors['default']
processor(status)
})
- // Keep the visible statuses sorted
+ // Keep the visible statuses sorted
if (timeline) {
sortTimeline(timelineObject)
if ((older || timelineObject.minVisibleId <= 0) && statuses.length > 0) {
timelineObject.minVisibleId = minBy(statuses, 'id').id
}
}
}
const addNewNotifications = (state, { dispatch, notifications, older, visibleNotificationTypes }) => {
const allStatuses = state.allStatuses
const allStatusesObject = state.allStatusesObject
each(notifications, (notification) => {
- const result = mergeOrAdd(allStatuses, allStatusesObject, notification.notice)
- const action = result.item
+ notification.action = mergeOrAdd(allStatuses, allStatusesObject, notification.action).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
- if (!find(state.notifications.data, (oldNotification) => oldNotification.action.id === action.id)) {
- state.notifications.maxId = Math.max(notification.id, state.notifications.maxId)
- state.notifications.minId = Math.min(notification.id, state.notifications.minId)
-
- const fresh = !notification.is_seen
- const status = notification.ntype === 'like'
- ? action.favorited_status
- : action
-
- const result = {
- type: notification.ntype,
- status,
- action,
- seen: !fresh
- }
+ if (!state.notifications.idStore.hasOwnProperty(notification.id)) {
+ state.notifications.maxId = notification.id > state.notifications.maxId
+ ? notification.id
+ : state.notifications.maxId
+ state.notifications.minId = notification.id < state.notifications.minId
+ ? notification.id
+ : state.notifications.minId
- state.notifications.data.push(result)
- state.notifications.idStore[notification.id] = result
+ state.notifications.data.push(notification)
+ state.notifications.idStore[notification.id] = notification
if ('Notification' in window && window.Notification.permission === 'granted') {
+ const notifObj = {}
+ const action = notification.action
const title = action.user.name
- const result = {}
- result.icon = action.user.profile_image_url
- result.body = action.text // there's a problem that it doesn't put a space before links tho
+ notifObj.icon = action.user.profile_image_url
+ notifObj.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...
if (action.attachments && action.attachments.length > 0 && !action.nsfw &&
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)) {
- let notification = new window.Notification(title, result)
+ if (notification.fresh && !state.notifications.desktopNotificationSilence && visibleNotificationTypes.includes(notification.ntype)) {
+ let notification = new window.Notification(title, notifObj)
// Chrome is known for not closing notifications automatically
// according to MDN, anyway.
setTimeout(notification.close.bind(notification), 5000)
}
}
}
})
}
export const mutations = {
addNewStatuses,
addNewNotifications,
showNewStatuses (state, { timeline }) {
const oldTimeline = (state.timelines[timeline])
oldTimeline.newStatusCount = 0
oldTimeline.visibleStatuses = slice(oldTimeline.statuses, 0, 50)
oldTimeline.minVisibleId = last(oldTimeline.visibleStatuses).id
oldTimeline.visibleStatusesObject = {}
each(oldTimeline.visibleStatuses, (status) => { oldTimeline.visibleStatusesObject[status.id] = status })
},
clearTimeline (state, { timeline }) {
- state.timelines[timeline] = emptyTl(state.timelines[timeline].userId)
+ state.timelines[timeline] = emptyTl()
},
setFavorited (state, { status, value }) {
const newStatus = state.allStatusesObject[status.id]
newStatus.favorited = value
},
setFavoritedConfirm (state, { status }) {
const newStatus = state.allStatusesObject[status.id]
newStatus.favorited = status.favorited
newStatus.fave_num = status.fave_num
},
setRetweeted (state, { status, value }) {
const newStatus = state.allStatusesObject[status.id]
newStatus.repeated = value
},
setDeleted (state, { status }) {
const newStatus = state.allStatusesObject[status.id]
newStatus.deleted = true
},
setLoading (state, { timeline, value }) {
state.timelines[timeline].loading = value
},
setNsfw (state, { id, nsfw }) {
const newStatus = state.allStatusesObject[id]
newStatus.nsfw = nsfw
},
setError (state, { value }) {
state.error = value
},
setNotificationsError (state, { value }) {
state.notifications.error = value
},
setNotificationsSilence (state, { value }) {
state.notifications.desktopNotificationSilence = value
},
markNotificationsAsSeen (state) {
each(state.notifications.data, (notification) => {
notification.seen = true
})
},
queueFlush (state, { timeline, id }) {
state.timelines[timeline].flushMarker = id
}
}
const statuses = {
state: defaultState,
actions: {
addNewStatuses ({ rootState, commit }, { statuses, showImmediately = false, timeline = false, noIdUpdate = false, userId }) {
commit('addNewStatuses', { statuses, showImmediately, timeline, noIdUpdate, user: rootState.users.currentUser, userId })
},
addNewNotifications ({ rootState, commit, dispatch }, { notifications, older }) {
commit('addNewNotifications', { visibleNotificationTypes: visibleNotificationTypes(rootState), dispatch, notifications, older })
},
setError ({ rootState, commit }, { value }) {
commit('setError', { value })
},
setNotificationsError ({ rootState, commit }, { value }) {
commit('setNotificationsError', { value })
},
setNotificationsSilence ({ rootState, commit }, { value }) {
commit('setNotificationsSilence', { value })
},
deleteStatus ({ rootState, commit }, status) {
commit('setDeleted', { status })
apiService.deleteStatus({ id: status.id, credentials: rootState.users.currentUser.credentials })
},
favorite ({ rootState, commit }, status) {
// Optimistic favoriting...
commit('setFavorited', { status, value: true })
apiService.favorite({ id: status.id, credentials: rootState.users.currentUser.credentials })
.then(response => {
if (response.ok) {
return response.json()
} else {
return {}
}
})
.then(status => {
commit('setFavoritedConfirm', { status })
})
},
unfavorite ({ rootState, commit }, status) {
// Optimistic favoriting...
commit('setFavorited', { status, value: false })
apiService.unfavorite({ id: status.id, credentials: rootState.users.currentUser.credentials })
.then(response => {
if (response.ok) {
return response.json()
} else {
return {}
}
})
.then(status => {
commit('setFavoritedConfirm', { status })
})
},
retweet ({ rootState, commit }, status) {
// Optimistic retweeting...
commit('setRetweeted', { status, value: true })
apiService.retweet({ id: status.id, credentials: rootState.users.currentUser.credentials })
},
unretweet ({ rootState, commit }, status) {
commit('setRetweeted', { status, value: false })
apiService.unretweet({ id: status.id, credentials: rootState.users.currentUser.credentials })
},
queueFlush ({ rootState, commit }, { timeline, id }) {
commit('queueFlush', { timeline, id })
},
markNotificationsAsSeen ({ rootState, commit }) {
commit('markNotificationsAsSeen')
apiService.markNotificationsAsSeen({
id: rootState.statuses.notifications.maxId,
credentials: rootState.users.currentUser.credentials
})
}
},
mutations
}
export default statuses
diff --git a/src/modules/users.js b/src/modules/users.js
index adbd37dd4b..d83f0dd898 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -1,263 +1,263 @@
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
import { compact, map, each, merge } from 'lodash'
import { set } from 'vue'
import { registerPushNotifications, unregisterPushNotifications } from '../services/push/push.js'
import oauthApi from '../services/new_api/oauth'
import { humanizeErrors } from './errors'
// TODO: Unify with mergeOrAdd in statuses.js
export const mergeOrAdd = (arr, obj, item) => {
if (!item) { return false }
const oldItem = obj[item.id]
if (oldItem) {
// We already have this, so only merge the new info.
merge(oldItem, item)
return { item: oldItem, new: false }
} else {
// This is a new item, prepare it
arr.push(item)
obj[item.id] = item
if (item.screen_name && !item.screen_name.includes('@')) {
obj[item.screen_name] = item
}
return { item, new: true }
}
}
const getNotificationPermission = () => {
const Notification = window.Notification
if (!Notification) return Promise.resolve(null)
if (Notification.permission === 'default') return Notification.requestPermission()
return Promise.resolve(Notification.permission)
}
export const mutations = {
setMuted (state, { user: { id }, muted }) {
const user = state.usersObject[id]
set(user, 'muted', muted)
},
setCurrentUser (state, user) {
state.lastLoginName = user.screen_name
state.currentUser = merge(state.currentUser || {}, user)
},
clearCurrentUser (state) {
state.currentUser = false
state.lastLoginName = false
},
beginLogin (state) {
state.loggingIn = true
},
endLogin (state) {
state.loggingIn = false
},
// TODO Clean after ourselves?
addFriends (state, { id, friends }) {
const user = state.usersObject[id]
user.friends = friends
},
addFollowers (state, { id, followers }) {
const user = state.usersObject[id]
user.followers = followers
},
addNewUsers (state, users) {
each(users, (user) => mergeOrAdd(state.users, state.usersObject, user))
},
setUserForStatus (state, status) {
status.user = state.usersObject[status.user.id]
},
setUserForNotification (state, notification) {
notification.action.user = state.usersObject[notification.action.user.id]
+ notification.from_profile = state.usersObject[notification.action.user.id]
},
setColor (state, { user: { id }, highlighted }) {
const user = state.usersObject[id]
set(user, 'highlight', highlighted)
},
signUpPending (state) {
state.signUpPending = true
state.signUpErrors = []
},
signUpSuccess (state) {
state.signUpPending = false
},
signUpFailure (state, errors) {
state.signUpPending = false
state.signUpErrors = errors
}
}
export const getters = {
userById: state => id =>
state.users.find(user => user.id === id),
userByName: state => name =>
state.users.find(user => user.screen_name === name)
}
export const defaultState = {
loggingIn: false,
lastLoginName: false,
currentUser: false,
users: [],
usersObject: {},
signUpPending: false,
signUpErrors: []
}
const users = {
state: defaultState,
mutations,
getters,
actions: {
fetchUser (store, id) {
store.rootState.api.backendInteractor.fetchUser({ id })
.then((user) => store.commit('addNewUsers', [user]))
},
addFriends ({ rootState, commit }, { id }) {
rootState.api.backendInteractor.fetchFriends({ id })
.then((friends) => commit('addFriends', { id, friends }))
},
addFollowers ({ rootState, commit }, { id }) {
rootState.api.backendInteractor.fetchFollowers({ id })
.then((followers) => commit('addFollowers', { id, followers }))
},
registerPushNotifications (store) {
const token = store.state.currentUser.credentials
const vapidPublicKey = store.rootState.instance.vapidPublicKey
const isEnabled = store.rootState.config.webPushNotifications
const notificationVisibility = store.rootState.config.notificationVisibility
registerPushNotifications(isEnabled, vapidPublicKey, token, notificationVisibility)
},
unregisterPushNotifications (store) {
const token = store.state.currentUser.credentials
unregisterPushNotifications(token)
},
addNewStatuses (store, { statuses }) {
const users = map(statuses, 'user')
const retweetedUsers = compact(map(statuses, 'retweeted_status.user'))
store.commit('addNewUsers', users)
store.commit('addNewUsers', retweetedUsers)
// Reconnect users to statuses
each(statuses, (status) => {
store.commit('setUserForStatus', status)
})
// Reconnect users to retweets
each(compact(map(statuses, 'retweeted_status')), (status) => {
store.commit('setUserForStatus', status)
})
},
addNewNotifications (store, { notifications }) {
- const users = compact(map(notifications, 'from_profile'))
- const notificationIds = compact(notifications.map(_ => String(_.id)))
+ const users = map(notifications, 'from_profile')
+ const notificationIds = notifications.map(_ => _.id)
store.commit('addNewUsers', users)
const notificationsObject = store.rootState.statuses.notifications.idStore
const relevantNotifications = Object.entries(notificationsObject)
.filter(([k, val]) => notificationIds.includes(k))
.map(([k, val]) => val)
// Reconnect users to notifications
each(relevantNotifications, (notification) => {
store.commit('setUserForNotification', notification)
})
},
async signUp (store, userInfo) {
store.commit('signUpPending')
let rootState = store.rootState
let response = await rootState.api.backendInteractor.register(userInfo)
if (response.ok) {
const data = {
oauth: rootState.oauth,
instance: rootState.instance.server
}
let app = await oauthApi.getOrCreateApp(data)
let result = await oauthApi.getTokenWithCredentials({
app,
instance: data.instance,
username: userInfo.username,
password: userInfo.password
})
store.commit('signUpSuccess')
store.commit('setToken', result.access_token)
store.dispatch('loginUser', result.access_token)
} else {
let data = await response.json()
let errors = humanizeErrors(JSON.parse(data.error))
store.commit('signUpFailure', errors)
throw Error(errors)
}
},
async getCaptcha (store) {
return await store.rootState.api.backendInteractor.getCaptcha()
},
logout (store) {
store.commit('clearCurrentUser')
store.commit('setToken', false)
store.dispatch('stopFetching', 'friends')
store.commit('setBackendInteractor', backendInteractorService())
},
loginUser (store, accessToken) {
return new Promise((resolve, reject) => {
const commit = store.commit
commit('beginLogin')
store.rootState.api.backendInteractor.verifyCredentials(accessToken)
- .then((response) => {
- if (response.ok) {
- response.json()
- .then((user) => {
- // user.credentials = userCredentials
- user.credentials = accessToken
- commit('setCurrentUser', user)
- commit('addNewUsers', [user])
+ .then((data) => {
+ if (!data.error) {
+ const user = data
+ // user.credentials = userCredentials
+ user.credentials = accessToken
+ commit('setCurrentUser', user)
+ commit('addNewUsers', [user])
- getNotificationPermission()
- .then(permission => commit('setNotificationPermission', permission))
+ getNotificationPermission()
+ .then(permission => commit('setNotificationPermission', permission))
- // Set our new backend interactor
- commit('setBackendInteractor', backendInteractorService(accessToken))
+ // Set our new backend interactor
+ commit('setBackendInteractor', backendInteractorService(accessToken))
- if (user.token) {
- store.dispatch('initializeSocket', user.token)
- }
+ if (user.token) {
+ store.dispatch('initializeSocket', user.token)
+ }
- // Start getting fresh tweets.
- store.dispatch('startFetching', 'friends')
+ // Start getting fresh tweets.
+ store.dispatch('startFetching', 'friends')
- // Get user mutes and follower info
- store.rootState.api.backendInteractor.fetchMutes().then((mutedUsers) => {
- each(mutedUsers, (user) => { user.muted = true })
- store.commit('addNewUsers', mutedUsers)
- })
+ // Get user mutes and follower info
+ store.rootState.api.backendInteractor.fetchMutes().then((mutedUsers) => {
+ each(mutedUsers, (user) => { user.muted = true })
+ store.commit('addNewUsers', mutedUsers)
+ })
- // Fetch our friends
- store.rootState.api.backendInteractor.fetchFriends({ id: user.id })
- .then((friends) => commit('addNewUsers', friends))
- })
+ // Fetch our friends
+ store.rootState.api.backendInteractor.fetchFriends({ id: user.id })
+ .then((friends) => commit('addNewUsers', friends))
} else {
+ const response = data.error
// Authentication failed
commit('endLogin')
if (response.status === 401) {
reject('Wrong username or password')
} else {
reject('An error occurred, please try again')
}
}
commit('endLogin')
resolve()
})
- .catch((error) => {
- console.log(error)
- commit('endLogin')
- reject('Failed to connect to server, try again')
- })
+ .catch((error) => {
+ console.log(error)
+ commit('endLogin')
+ reject('Failed to connect to server, try again')
+ })
})
}
}
}
export default users
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index 4ee95bd10e..776d8daef1 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -1,515 +1,562 @@
/* eslint-env browser */
const LOGIN_URL = '/api/account/verify_credentials.json'
const FRIENDS_TIMELINE_URL = '/api/statuses/friends_timeline.json'
const ALL_FOLLOWING_URL = '/api/qvitter/allfollowing'
const PUBLIC_TIMELINE_URL = '/api/statuses/public_timeline.json'
const PUBLIC_AND_EXTERNAL_TIMELINE_URL = '/api/statuses/public_and_external_timeline.json'
const TAG_TIMELINE_URL = '/api/statusnet/tags/timeline'
const FAVORITE_URL = '/api/favorites/create'
const UNFAVORITE_URL = '/api/favorites/destroy'
const RETWEET_URL = '/api/statuses/retweet'
const UNRETWEET_URL = '/api/statuses/unretweet'
const STATUS_UPDATE_URL = '/api/statuses/update.json'
const STATUS_DELETE_URL = '/api/statuses/destroy'
const STATUS_URL = '/api/statuses/show'
const MEDIA_UPLOAD_URL = '/api/statusnet/media/upload'
const CONVERSATION_URL = '/api/statusnet/conversation'
const MENTIONS_URL = '/api/statuses/mentions.json'
const DM_TIMELINE_URL = '/api/statuses/dm_timeline.json'
const FOLLOWERS_URL = '/api/statuses/followers.json'
const FRIENDS_URL = '/api/statuses/friends.json'
const FOLLOWING_URL = '/api/friendships/create.json'
const UNFOLLOWING_URL = '/api/friendships/destroy.json'
const QVITTER_USER_PREF_URL = '/api/qvitter/set_profile_pref.json'
const REGISTRATION_URL = '/api/account/register.json'
const AVATAR_UPDATE_URL = '/api/qvitter/update_avatar.json'
const BG_UPDATE_URL = '/api/qvitter/update_background_image.json'
const BANNER_UPDATE_URL = '/api/account/update_profile_banner.json'
const PROFILE_UPDATE_URL = '/api/account/update_profile.json'
const EXTERNAL_PROFILE_URL = '/api/externalprofile/show.json'
const QVITTER_USER_TIMELINE_URL = '/api/qvitter/statuses/user_timeline.json'
const QVITTER_USER_NOTIFICATIONS_URL = '/api/qvitter/statuses/notifications.json'
const QVITTER_USER_NOTIFICATIONS_READ_URL = '/api/qvitter/statuses/notifications/read.json'
const BLOCKING_URL = '/api/blocks/create.json'
const UNBLOCKING_URL = '/api/blocks/destroy.json'
const USER_URL = '/api/users/show.json'
const FOLLOW_IMPORT_URL = '/api/pleroma/follow_import'
const DELETE_ACCOUNT_URL = '/api/pleroma/delete_account'
const CHANGE_PASSWORD_URL = '/api/pleroma/change_password'
const FOLLOW_REQUESTS_URL = '/api/pleroma/friend_requests'
const APPROVE_USER_URL = '/api/pleroma/friendships/approve'
const DENY_USER_URL = '/api/pleroma/friendships/deny'
const SUGGESTIONS_URL = '/api/v1/suggestions'
+const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites'
+
import { each, map } from 'lodash'
+import { parseStatus, parseUser, parseNotification } from '../entity_normalizer/entity_normalizer.service.js'
import 'whatwg-fetch'
const oldfetch = window.fetch
let fetch = (url, options) => {
options = options || {}
const baseUrl = ''
const fullUrl = baseUrl + url
options.credentials = 'same-origin'
return oldfetch(fullUrl, options)
}
// Params
// cropH
// cropW
// cropX
// cropY
// img (base 64 encodend data url)
const updateAvatar = ({credentials, params}) => {
let url = AVATAR_UPDATE_URL
const form = new FormData()
each(params, (value, key) => {
if (value) {
form.append(key, value)
}
})
+
return fetch(url, {
headers: authHeaders(credentials),
method: 'POST',
body: form
}).then((data) => data.json())
}
const updateBg = ({credentials, params}) => {
let url = BG_UPDATE_URL
const form = new FormData()
each(params, (value, key) => {
if (value) {
form.append(key, value)
}
})
+
return fetch(url, {
headers: authHeaders(credentials),
method: 'POST',
body: form
}).then((data) => data.json())
}
// Params
// height
// width
// offset_left
// offset_top
// banner (base 64 encodend data url)
const updateBanner = ({credentials, params}) => {
let url = BANNER_UPDATE_URL
const form = new FormData()
each(params, (value, key) => {
if (value) {
form.append(key, value)
}
})
+
return fetch(url, {
headers: authHeaders(credentials),
method: 'POST',
body: form
}).then((data) => data.json())
}
// Params
// name
// url
// location
// description
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
const form = new FormData()
each(params, (value, key) => {
- /* Always include description, no_rich_text and locked, because it might be empty or false */
- if (key === 'description' || key === 'locked' || key === 'no_rich_text' || value) {
+ if (fields.includes(key) || value) {
form.append(key, value)
}
})
return fetch(url, {
headers: authHeaders(credentials),
method: 'POST',
body: form
}).then((data) => data.json())
}
// Params needed:
// nickname
// email
// fullname
// password
// password_confirm
//
// Optional
// bio
// homepage
// location
// token
const register = (params) => {
const form = new FormData()
each(params, (value, key) => {
if (value) {
form.append(key, value)
}
})
return fetch(REGISTRATION_URL, {
method: 'POST',
body: form
})
}
const getCaptcha = () => fetch('/api/pleroma/captcha').then(resp => resp.json())
const authHeaders = (accessToken) => {
if (accessToken) {
return { 'Authorization': `Bearer ${accessToken}` }
} else {
return { }
}
}
const externalProfile = ({profileUrl, credentials}) => {
let url = `${EXTERNAL_PROFILE_URL}?profileurl=${profileUrl}`
return fetch(url, {
headers: authHeaders(credentials),
method: 'GET'
}).then((data) => data.json())
}
const followUser = ({id, credentials}) => {
let url = `${FOLLOWING_URL}?user_id=${id}`
return fetch(url, {
headers: authHeaders(credentials),
method: 'POST'
}).then((data) => data.json())
}
const unfollowUser = ({id, credentials}) => {
let url = `${UNFOLLOWING_URL}?user_id=${id}`
return fetch(url, {
headers: authHeaders(credentials),
method: 'POST'
}).then((data) => data.json())
}
const blockUser = ({id, credentials}) => {
let url = `${BLOCKING_URL}?user_id=${id}`
return fetch(url, {
headers: authHeaders(credentials),
method: 'POST'
}).then((data) => data.json())
}
const unblockUser = ({id, credentials}) => {
let url = `${UNBLOCKING_URL}?user_id=${id}`
return fetch(url, {
headers: authHeaders(credentials),
method: 'POST'
}).then((data) => data.json())
}
const approveUser = ({id, credentials}) => {
let url = `${APPROVE_USER_URL}?user_id=${id}`
return fetch(url, {
headers: authHeaders(credentials),
method: 'POST'
}).then((data) => data.json())
}
const denyUser = ({id, credentials}) => {
let url = `${DENY_USER_URL}?user_id=${id}`
return fetch(url, {
headers: authHeaders(credentials),
method: 'POST'
}).then((data) => data.json())
}
const fetchUser = ({id, credentials}) => {
let url = `${USER_URL}?user_id=${id}`
return fetch(url, { headers: authHeaders(credentials) })
.then((data) => data.json())
+ .then((data) => parseUser(data))
}
const fetchFriends = ({id, credentials}) => {
let url = `${FRIENDS_URL}?user_id=${id}`
return fetch(url, { headers: authHeaders(credentials) })
.then((data) => data.json())
+ .then((data) => data.map(parseUser))
}
const fetchFollowers = ({id, credentials}) => {
let url = `${FOLLOWERS_URL}?user_id=${id}`
return fetch(url, { headers: authHeaders(credentials) })
.then((data) => data.json())
+ .then((data) => data.map(parseUser))
}
const fetchAllFollowing = ({username, credentials}) => {
const url = `${ALL_FOLLOWING_URL}/${username}.json`
return fetch(url, { headers: authHeaders(credentials) })
.then((data) => data.json())
+ .then((data) => data.map(parseUser))
}
const fetchFollowRequests = ({credentials}) => {
const url = FOLLOW_REQUESTS_URL
return fetch(url, { headers: authHeaders(credentials) })
.then((data) => data.json())
}
const fetchConversation = ({id, credentials}) => {
let url = `${CONVERSATION_URL}/${id}.json?count=100`
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.map(parseStatus))
}
const fetchStatus = ({id, credentials}) => {
let url = `${STATUS_URL}/${id}.json`
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) => parseStatus(data))
}
const setUserMute = ({id, credentials, muted = true}) => {
const form = new FormData()
const muteInteger = muted ? 1 : 0
form.append('namespace', 'qvitter')
form.append('data', muteInteger)
form.append('topic', `mute:${id}`)
return fetch(QVITTER_USER_PREF_URL, {
method: 'POST',
headers: authHeaders(credentials),
body: form
})
}
const fetchTimeline = ({timeline, credentials, since = false, until = false, userId = false, tag = false}) => {
const timelineUrls = {
public: PUBLIC_TIMELINE_URL,
friends: FRIENDS_TIMELINE_URL,
mentions: MENTIONS_URL,
dms: DM_TIMELINE_URL,
notifications: QVITTER_USER_NOTIFICATIONS_URL,
'publicAndExternal': PUBLIC_AND_EXTERNAL_TIMELINE_URL,
user: QVITTER_USER_TIMELINE_URL,
+ favorites: MASTODON_USER_FAVORITES_TIMELINE_URL,
tag: TAG_TIMELINE_URL
}
+ const isNotifications = timeline === 'notifications'
+ const params = []
let url = timelineUrls[timeline]
- let params = []
-
if (since) {
params.push(['since_id', since])
}
if (until) {
params.push(['max_id', until])
}
if (userId) {
params.push(['user_id', userId])
}
if (tag) {
url += `/${tag}.json`
}
params.push(['count', 20])
const queryString = map(params, (param) => `${param[0]}=${param[1]}`).join('&')
url += `?${queryString}`
return fetch(url, { headers: authHeaders(credentials) })
.then((data) => {
if (data.ok) {
return data
}
- throw new Error('Error fetching timeline')
+ throw new Error('Error fetching timeline', data)
})
.then((data) => data.json())
+ .then((data) => data.map(isNotifications ? parseNotification : parseStatus))
}
const verifyCredentials = (user) => {
return fetch(LOGIN_URL, {
method: 'POST',
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 }) => {
return fetch(`${FAVORITE_URL}/${id}.json`, {
headers: authHeaders(credentials),
method: 'POST'
})
}
const unfavorite = ({ id, credentials }) => {
return fetch(`${UNFAVORITE_URL}/${id}.json`, {
headers: authHeaders(credentials),
method: 'POST'
})
}
const retweet = ({ id, credentials }) => {
return fetch(`${RETWEET_URL}/${id}.json`, {
headers: authHeaders(credentials),
method: 'POST'
})
}
const unretweet = ({ id, credentials }) => {
return fetch(`${UNRETWEET_URL}/${id}.json`, {
headers: authHeaders(credentials),
method: 'POST'
})
}
const postStatus = ({credentials, status, spoilerText, visibility, sensitive, mediaIds, inReplyToStatusId, contentType, noAttachmentLinks}) => {
const idsText = mediaIds.join(',')
const form = new FormData()
form.append('status', status)
form.append('source', 'Pleroma FE')
if (noAttachmentLinks) form.append('no_attachment_links', noAttachmentLinks)
if (spoilerText) form.append('spoiler_text', spoilerText)
if (visibility) form.append('visibility', visibility)
if (sensitive) form.append('sensitive', sensitive)
if (contentType) form.append('content_type', contentType)
form.append('media_ids', idsText)
if (inReplyToStatusId) {
form.append('in_reply_to_status_id', inReplyToStatusId)
}
return fetch(STATUS_UPDATE_URL, {
body: form,
method: 'POST',
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 }) => {
return fetch(`${STATUS_DELETE_URL}/${id}.json`, {
headers: authHeaders(credentials),
method: 'POST'
})
}
const uploadMedia = ({formData, credentials}) => {
return fetch(MEDIA_UPLOAD_URL, {
body: formData,
method: 'POST',
headers: authHeaders(credentials)
})
.then((response) => response.text())
.then((text) => (new DOMParser()).parseFromString(text, 'application/xml'))
}
const followImport = ({params, credentials}) => {
return fetch(FOLLOW_IMPORT_URL, {
body: params,
method: 'POST',
headers: authHeaders(credentials)
})
.then((response) => response.ok)
}
const deleteAccount = ({credentials, password}) => {
const form = new FormData()
form.append('password', password)
return fetch(DELETE_ACCOUNT_URL, {
body: form,
method: 'POST',
headers: authHeaders(credentials)
})
.then((response) => response.json())
}
const changePassword = ({credentials, password, newPassword, newPasswordConfirmation}) => {
const form = new FormData()
form.append('password', password)
form.append('new_password', newPassword)
form.append('new_password_confirmation', newPasswordConfirmation)
return fetch(CHANGE_PASSWORD_URL, {
body: form,
method: 'POST',
headers: authHeaders(credentials)
})
.then((response) => response.json())
}
const fetchMutes = ({credentials}) => {
const url = '/api/qvitter/mutes.json'
return fetch(url, {
headers: authHeaders(credentials)
}).then((data) => data.json())
}
const suggestions = ({credentials}) => {
return fetch(SUGGESTIONS_URL, {
headers: authHeaders(credentials)
}).then((data) => data.json())
}
const markNotificationsAsSeen = ({id, credentials}) => {
const body = new FormData()
body.append('latest_id', id)
return fetch(QVITTER_USER_NOTIFICATIONS_READ_URL, {
body,
headers: authHeaders(credentials),
method: 'POST'
}).then((data) => data.json())
}
const apiService = {
verifyCredentials,
fetchTimeline,
fetchConversation,
fetchStatus,
fetchFriends,
fetchFollowers,
followUser,
unfollowUser,
blockUser,
unblockUser,
fetchUser,
favorite,
unfavorite,
retweet,
unretweet,
postStatus,
deleteStatus,
uploadMedia,
fetchAllFollowing,
setUserMute,
fetchMutes,
register,
getCaptcha,
updateAvatar,
updateBg,
updateProfile,
updateBanner,
externalProfile,
followImport,
deleteAccount,
changePassword,
fetchFollowRequests,
approveUser,
denyUser,
suggestions,
markNotificationsAsSeen
}
export default apiService
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
new file mode 100644
index 0000000000..becbab154d
--- /dev/null
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -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)
+}
diff --git a/src/services/notification_utils/notification_utils.js b/src/services/notification_utils/notification_utils.js
index f5ac0d4766..cd8f3f9efd 100644
--- a/src/services/notification_utils/notification_utils.js
+++ b/src/services/notification_utils/notification_utils.js
@@ -1,20 +1,36 @@
import { filter, sortBy } from 'lodash'
export const notificationsFromStore = store => store.state.statuses.notifications.data
export const visibleTypes = store => ([
store.state.config.notificationVisibility.likes && 'like',
store.state.config.notificationVisibility.mentions && 'mention',
store.state.config.notificationVisibility.repeats && 'repeat',
store.state.config.notificationVisibility.follows && 'follow'
].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 => {
- // Don't know why, but sortBy([seen, -action.id]) doesn't work.
- let sortedNotifications = sortBy(notificationsFromStore(store), ({action}) => -action.id)
+ // map is just to clone the array since sort mutates it and it causes some issues
+ let sortedNotifications = notificationsFromStore(store).map(_ => _).sort(sortById)
sortedNotifications = sortBy(sortedNotifications, 'seen')
return sortedNotifications.filter((notification) => visibleTypes(store).includes(notification.type))
}
export const unseenNotificationsFromStore = store =>
filter(visibleNotificationsFromStore(store), ({seen}) => !seen)
diff --git a/src/services/status_poster/status_poster.service.js b/src/services/status_poster/status_poster.service.js
index 1e20d336e3..f1932bb613 100644
--- a/src/services/status_poster/status_poster.service.js
+++ b/src/services/status_poster/status_poster.service.js
@@ -1,56 +1,55 @@
import { map } from 'lodash'
import apiService from '../api/api.service.js'
const postStatus = ({ store, status, spoilerText, visibility, sensitive, media = [], inReplyToStatusId = undefined, contentType = 'text/plain' }) => {
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})
- .then((data) => data.json())
.then((data) => {
if (!data.error) {
store.dispatch('addNewStatuses', {
statuses: [data],
timeline: 'friends',
showImmediately: true,
noIdUpdate: true // To prevent missing notices on next pull.
})
}
return data
})
.catch((err) => {
return {
error: err.message
}
})
}
const uploadMedia = ({ store, formData }) => {
const credentials = store.state.users.currentUser.credentials
return apiService.uploadMedia({ credentials, formData }).then((xml) => {
// Firefox and Chrome treat method differently...
let link = xml.getElementsByTagName('link')
if (link.length === 0) {
link = xml.getElementsByTagName('atom:link')
}
link = link[0]
const mediaData = {
id: xml.getElementsByTagName('media_id')[0].textContent,
url: xml.getElementsByTagName('media_url')[0].textContent,
image: link.getAttribute('href'),
mimetype: link.getAttribute('type')
}
return mediaData
})
}
const statusPosterService = {
postStatus,
uploadMedia
}
export default statusPosterService
diff --git a/static/config.json b/static/config.json
index cc72900a8f..24e26696fa 100644
--- a/static/config.json
+++ b/static/config.json
@@ -1,22 +1,23 @@
{
"theme": "pleroma-dark",
"background": "/static/aurora_borealis.jpg",
"logo": "/static/logo.png",
"logoMask": true,
"logoMargin": ".1em",
"redirectRootNoLogin": "/main/all",
"redirectRootLogin": "/main/friends",
"chatDisabled": false,
"showInstanceSpecificPanel": false,
"scopeOptionsEnabled": false,
"formattingOptionsEnabled": false,
"collapseMessageWithSubject": false,
"scopeCopy": true,
"subjectLineBehavior": "email",
"alwaysShowSubjectInput": true,
"hidePostStats": false,
"hideUserStats": false,
"loginMethod": "password",
"webPushNotifications": false,
- "noAttachmentLinks": false
+ "noAttachmentLinks": false,
+ "nsfwCensorImage": ""
}
diff --git a/test/fixtures/mastoapi.json b/test/fixtures/mastoapi.json
new file mode 100644
index 0000000000..858d7a0d32
--- /dev/null
+++ b/test/fixtures/mastoapi.json
@@ -0,0 +1,1582 @@
+[{
+ "account": {
+ "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"
+ },
+ "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"
+}, {
+ "account": {
+ "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"
+ },
+ "application": {
+ "name": "Web",
+ "website": null
+ },
+ "content": "<span><a data-user=\"982\" href=\"https://bikeshed.party/users/feld\">@<span>feld</span></a></span><br /><a href=\"https://shigusegubu.club/media/4596ab2f-28e6-4f26-83db-0f26f29848cd/Jotaro - YES YES YES YES YES-sq_Fm7qfRQk.webm\">Jotaro - YES YES YES YES YES-sq…</a>",
+ "created_at": "2019-01-17T16:29:12.000Z",
+ "emojis": [],
+ "favourited": false,
+ "favourites_count": 1,
+ "id": "10423465",
+ "in_reply_to_account_id": "982",
+ "in_reply_to_id": "10423446",
+ "language": null,
+ "media_attachments": [{
+ "description": "Jotaro - YES YES YES YES YES-sq_Fm7qfRQk.webm",
+ "id": "1104859167",
+ "preview_url": "https://shigusegubu.club/media/4596ab2f-28e6-4f26-83db-0f26f29848cd/Jotaro - YES YES YES YES YES-sq_Fm7qfRQk.webm",
+ "remote_url": "https://shigusegubu.club/media/4596ab2f-28e6-4f26-83db-0f26f29848cd/Jotaro - YES YES YES YES YES-sq_Fm7qfRQk.webm",
+ "text_url": "https://shigusegubu.club/media/4596ab2f-28e6-4f26-83db-0f26f29848cd/Jotaro - YES YES YES YES YES-sq_Fm7qfRQk.webm",
+ "type": "video",
+ "url": "https://shigusegubu.club/media/4596ab2f-28e6-4f26-83db-0f26f29848cd/Jotaro - YES YES YES YES YES-sq_Fm7qfRQk.webm"
+ }],
+ "mentions": [{
+ "acct": "feld@bikeshed.party",
+ "id": "982",
+ "url": "https://bikeshed.party/users/feld",
+ "username": "feld"
+ }],
+ "muted": false,
+ "reblog": null,
+ "reblogged": false,
+ "reblogs_count": 1,
+ "replies_count": 0,
+ "sensitive": false,
+ "spoiler_text": "",
+ "tags": [],
+ "uri": "https://shigusegubu.club/objects/6634d32b-96a8-4852-a3db-ac8730715779",
+ "url": "https://shigusegubu.club/objects/6634d32b-96a8-4852-a3db-ac8730715779",
+ "visibility": "public"
+}, {
+ "account": {
+ "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"
+ },
+ "application": {
+ "name": "Web",
+ "website": null
+ },
+ "content": "heh<br /><a href=\"https://pleroma.soykaf.com/media/fd13d009-2c1f-4a53-9762-257e592a7ff6/scrot_17-01-19_18-16-40.png\">scrot_17-01-19_18-16-40.png</a>",
+ "created_at": "2019-01-17T16:28:49.000Z",
+ "emojis": [],
+ "favourited": false,
+ "favourites_count": 0,
+ "id": "10423449",
+ "in_reply_to_account_id": null,
+ "in_reply_to_id": null,
+ "language": null,
+ "media_attachments": [{
+ "description": "scrot_17-01-19_18-16-40.png",
+ "id": "-804793492",
+ "preview_url": "https://pleroma.soykaf.com/media/fd13d009-2c1f-4a53-9762-257e592a7ff6/scrot_17-01-19_18-16-40.png",
+ "remote_url": "https://pleroma.soykaf.com/media/fd13d009-2c1f-4a53-9762-257e592a7ff6/scrot_17-01-19_18-16-40.png",
+ "text_url": "https://pleroma.soykaf.com/media/fd13d009-2c1f-4a53-9762-257e592a7ff6/scrot_17-01-19_18-16-40.png",
+ "type": "image",
+ "url": "https://pleroma.soykaf.com/media/fd13d009-2c1f-4a53-9762-257e592a7ff6/scrot_17-01-19_18-16-40.png"
+ }],
+ "mentions": [{
+ "acct": "sampo@pleroma.soykaf.com",
+ "id": "14660",
+ "url": "https://pleroma.soykaf.com/users/sampo",
+ "username": "sampo"
+ }],
+ "muted": false,
+ "reblog": {
+ "account": {
+ "acct": "sampo@pleroma.soykaf.com",
+ "avatar": "https://pleroma.soykaf.com/media/f820f4cb-0fc8-4e4d-9521-c896ad2f93b5/c6e8d2e6d471616b82031e8348782e057aee7f1ed3214fe1522415e65694aa07.png",
+ "avatar_static": "https://pleroma.soykaf.com/media/f820f4cb-0fc8-4e4d-9521-c896ad2f93b5/c6e8d2e6d471616b82031e8348782e057aee7f1ed3214fe1522415e65694aa07.png",
+ "bot": false,
+ "created_at": "2018-04-02T21:19:46.000Z",
+ "display_name": "sampo",
+ "emojis": [],
+ "fields": [],
+ "followers_count": 23,
+ "following_count": 7,
+ "header": "https://pleroma.soykaf.com/media/23c3c6e1-863f-4aa2-9711-ab25bfeadd10/6aa0c00c79010d9a735c3e25d7e53da5a9fb4231720b4bd5b6904cd963b4c1cd.png",
+ "header_static": "https://pleroma.soykaf.com/media/23c3c6e1-863f-4aa2-9711-ab25bfeadd10/6aa0c00c79010d9a735c3e25d7e53da5a9fb4231720b4bd5b6904cd963b4c1cd.png",
+ "id": "14660",
+ "locked": false,
+ "note": "welcome to binlan 🇫🇮 prööh",
+ "pleroma": {
+ "confirmation_pending": false,
+ "tags": null
+ },
+ "source": {
+ "note": "",
+ "privacy": "public",
+ "sensitive": false
+ },
+ "statuses_count": 17647,
+ "url": "https://pleroma.soykaf.com/users/sampo",
+ "username": "sampo"
+ },
+ "application": {
+ "name": "Web",
+ "website": null
+ },
+ "content": "heh<br /><a href=\"https://pleroma.soykaf.com/media/fd13d009-2c1f-4a53-9762-257e592a7ff6/scrot_17-01-19_18-16-40.png\">scrot_17-01-19_18-16-40.png</a>",
+ "created_at": "2019-01-17T16:20:06.000Z",
+ "emojis": [],
+ "favourited": false,
+ "favourites_count": 7,
+ "id": "10423197",
+ "in_reply_to_account_id": null,
+ "in_reply_to_id": null,
+ "language": null,
+ "media_attachments": [{
+ "description": "scrot_17-01-19_18-16-40.png",
+ "id": "-804793492",
+ "preview_url": "https://pleroma.soykaf.com/media/fd13d009-2c1f-4a53-9762-257e592a7ff6/scrot_17-01-19_18-16-40.png",
+ "remote_url": "https://pleroma.soykaf.com/media/fd13d009-2c1f-4a53-9762-257e592a7ff6/scrot_17-01-19_18-16-40.png",
+ "text_url": "https://pleroma.soykaf.com/media/fd13d009-2c1f-4a53-9762-257e592a7ff6/scrot_17-01-19_18-16-40.png",
+ "type": "image",
+ "url": "https://pleroma.soykaf.com/media/fd13d009-2c1f-4a53-9762-257e592a7ff6/scrot_17-01-19_18-16-40.png"
+ }],
+ "mentions": [],
+ "muted": false,
+ "reblog": null,
+ "reblogged": false,
+ "reblogs_count": 2,
+ "replies_count": 0,
+ "sensitive": false,
+ "spoiler_text": "",
+ "tags": [],
+ "uri": "https://pleroma.soykaf.com/objects/bf7e43d4-5048-4176-8519-58e3e1014f8b",
+ "url": "https://pleroma.soykaf.com/objects/bf7e43d4-5048-4176-8519-58e3e1014f8b",
+ "visibility": "public"
+ },
+ "reblogged": false,
+ "reblogs_count": 0,
+ "replies_count": 0,
+ "sensitive": false,
+ "spoiler_text": "",
+ "tags": [],
+ "uri": "https://pleroma.soykaf.com/objects/bf7e43d4-5048-4176-8519-58e3e1014f8b",
+ "url": "https://pleroma.soykaf.com/objects/bf7e43d4-5048-4176-8519-58e3e1014f8b",
+ "visibility": "public"
+}, {
+ "account": {
+ "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"
+ },
+ "application": {
+ "name": "Web",
+ "website": null
+ },
+ "content": "<span><a data-user=\"982\" href=\"https://bikeshed.party/users/feld\">@<span>feld</span></a></span><br /><a href=\"https://shigusegubu.club/media/65de40a9-a99b-4207-97d0-40dd836532db/Uhh No.avi-Xrne2-gOoqU.webm\">Uhh No.avi-Xrne2-gOoqU.webm</a>",
+ "created_at": "2019-01-17T16:27:29.000Z",
+ "emojis": [],
+ "favourited": false,
+ "favourites_count": 0,
+ "id": "10423391",
+ "in_reply_to_account_id": "982",
+ "in_reply_to_id": "10423380",
+ "language": null,
+ "media_attachments": [{
+ "description": "Uhh No.avi-Xrne2-gOoqU.webm",
+ "id": "483179103",
+ "preview_url": "https://shigusegubu.club/media/65de40a9-a99b-4207-97d0-40dd836532db/Uhh No.avi-Xrne2-gOoqU.webm",
+ "remote_url": "https://shigusegubu.club/media/65de40a9-a99b-4207-97d0-40dd836532db/Uhh No.avi-Xrne2-gOoqU.webm",
+ "text_url": "https://shigusegubu.club/media/65de40a9-a99b-4207-97d0-40dd836532db/Uhh No.avi-Xrne2-gOoqU.webm",
+ "type": "video",
+ "url": "https://shigusegubu.club/media/65de40a9-a99b-4207-97d0-40dd836532db/Uhh No.avi-Xrne2-gOoqU.webm"
+ }],
+ "mentions": [{
+ "acct": "feld@bikeshed.party",
+ "id": "982",
+ "url": "https://bikeshed.party/users/feld",
+ "username": "feld"
+ }],
+ "muted": false,
+ "reblog": null,
+ "reblogged": false,
+ "reblogs_count": 0,
+ "replies_count": 0,
+ "sensitive": false,
+ "spoiler_text": "",
+ "tags": [],
+ "uri": "https://shigusegubu.club/objects/0f963ca1-a263-41ca-a43c-b5d26d0a08e9",
+ "url": "https://shigusegubu.club/objects/0f963ca1-a263-41ca-a43c-b5d26d0a08e9",
+ "visibility": "public"
+}, {
+ "account": {
+ "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"
+ },
+ "application": {
+ "name": "Web",
+ "website": null
+ },
+ "content": "<span><a data-user=\"16633\" href=\"https://social.super-niche.club/user/31\">@<span>fedebot</span></a></span> lewd",
+ "created_at": "2019-01-17T16:17:46.000Z",
+ "emojis": [],
+ "favourited": false,
+ "favourites_count": 0,
+ "id": "10423144",
+ "in_reply_to_account_id": "16633",
+ "in_reply_to_id": "10423114",
+ "language": null,
+ "media_attachments": [],
+ "mentions": [{
+ "acct": "fedebot@social.super-niche.club",
+ "id": "16633",
+ "url": "https://social.super-niche.club/user/31",
+ "username": "fedebot"
+ }],
+ "muted": false,
+ "reblog": null,
+ "reblogged": false,
+ "reblogs_count": 0,
+ "replies_count": 0,
+ "sensitive": false,
+ "spoiler_text": "",
+ "tags": [],
+ "uri": "https://shigusegubu.club/objects/3f809bd8-656f-4a29-81d4-80eed6916eb0",
+ "url": "https://shigusegubu.club/objects/3f809bd8-656f-4a29-81d4-80eed6916eb0",
+ "visibility": "public"
+}, {
+ "account": {
+ "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"
+ },
+ "application": {
+ "name": "Web",
+ "website": null
+ },
+ "content": "#<span><a href=\"https://social.super-niche.club/tag/animeirl\">animeirl</a></span> <a href=\"https://social.super-niche.club/file/c366cae1e5820d68dc1e6bc7a06a2f2bc15166de1faabea4f060490d45fdf527.jpg\" title=\"https://social.super-niche.club/file/c366cae1e5820d68dc1e6bc7a06a2f2bc15166de1faabea4f060490d45fdf527.jpg\">https://social.super-niche.club/attachment/368974</a>",
+ "created_at": "2019-01-17T14:20:42.000Z",
+ "emojis": [],
+ "favourited": false,
+ "favourites_count": 0,
+ "id": "10419966",
+ "in_reply_to_account_id": null,
+ "in_reply_to_id": null,
+ "language": null,
+ "media_attachments": [{
+ "description": null,
+ "id": "1373175917",
+ "preview_url": "https://social.super-niche.club/file/c366cae1e5820d68dc1e6bc7a06a2f2bc15166de1faabea4f060490d45fdf527.jpg",
+ "remote_url": "https://social.super-niche.club/file/c366cae1e5820d68dc1e6bc7a06a2f2bc15166de1faabea4f060490d45fdf527.jpg",
+ "text_url": "https://social.super-niche.club/file/c366cae1e5820d68dc1e6bc7a06a2f2bc15166de1faabea4f060490d45fdf527.jpg",
+ "type": "image",
+ "url": "https://social.super-niche.club/file/c366cae1e5820d68dc1e6bc7a06a2f2bc15166de1faabea4f060490d45fdf527.jpg"
+ }],
+ "mentions": [{
+ "acct": "fedebot@social.super-niche.club",
+ "id": "16633",
+ "url": "https://social.super-niche.club/user/31",
+ "username": "fedebot"
+ }],
+ "muted": false,
+ "reblog": {
+ "account": {
+ "acct": "fedebot@social.super-niche.club",
+ "avatar": "https://social.super-niche.club/avatar/31-300-20180411174535.jpeg",
+ "avatar_static": "https://social.super-niche.club/avatar/31-300-20180411174535.jpeg",
+ "bot": false,
+ "created_at": "2018-04-11T21:08:46.000Z",
+ "display_name": "Federation Bot",
+ "emojis": [],
+ "fields": [],
+ "followers_count": 6,
+ "following_count": 135,
+ "header": "https://shigusegubu.club/images/banner.png",
+ "header_static": "https://shigusegubu.club/images/banner.png",
+ "id": "16633",
+ "locked": false,
+ "note": "",
+ "pleroma": {
+ "confirmation_pending": false,
+ "tags": null
+ },
+ "source": {
+ "note": "",
+ "privacy": "public",
+ "sensitive": false
+ },
+ "statuses_count": 6335,
+ "url": "https://social.super-niche.club/user/31",
+ "username": "fedebot"
+ },
+ "application": {
+ "name": "Web",
+ "website": null
+ },
+ "content": "#<span><a href=\"https://social.super-niche.club/tag/animeirl\">animeirl</a></span> <a href=\"https://social.super-niche.club/file/c366cae1e5820d68dc1e6bc7a06a2f2bc15166de1faabea4f060490d45fdf527.jpg\" title=\"https://social.super-niche.club/file/c366cae1e5820d68dc1e6bc7a06a2f2bc15166de1faabea4f060490d45fdf527.jpg\">https://social.super-niche.club/attachment/368974</a>",
+ "created_at": "2019-01-17T14:11:08.000Z",
+ "emojis": [],
+ "favourited": false,
+ "favourites_count": 4,
+ "id": "10419912",
+ "in_reply_to_account_id": null,
+ "in_reply_to_id": null,
+ "language": null,
+ "media_attachments": [{
+ "description": null,
+ "id": "1373175917",
+ "preview_url": "https://social.super-niche.club/file/c366cae1e5820d68dc1e6bc7a06a2f2bc15166de1faabea4f060490d45fdf527.jpg",
+ "remote_url": "https://social.super-niche.club/file/c366cae1e5820d68dc1e6bc7a06a2f2bc15166de1faabea4f060490d45fdf527.jpg",
+ "text_url": "https://social.super-niche.club/file/c366cae1e5820d68dc1e6bc7a06a2f2bc15166de1faabea4f060490d45fdf527.jpg",
+ "type": "image",
+ "url": "https://social.super-niche.club/file/c366cae1e5820d68dc1e6bc7a06a2f2bc15166de1faabea4f060490d45fdf527.jpg"
+ }],
+ "mentions": [],
+ "muted": false,
+ "reblog": null,
+ "reblogged": false,
+ "reblogs_count": 3,
+ "replies_count": 0,
+ "sensitive": false,
+ "spoiler_text": "",
+ "tags": [{
+ "name": "animeirl",
+ "url": "/tag/animeirl"
+ }],
+ "uri": "tag:social.super-niche.club,2019-01-17:noticeId=2353002:objectType=note",
+ "url": "https://social.super-niche.club/notice/2353002",
+ "visibility": "public"
+ },
+ "reblogged": false,
+ "reblogs_count": 0,
+ "replies_count": 0,
+ "sensitive": false,
+ "spoiler_text": "",
+ "tags": [{
+ "name": "animeirl",
+ "url": "/tag/animeirl"
+ }],
+ "uri": "tag:social.super-niche.club,2019-01-17:noticeId=2353002:objectType=note",
+ "url": "tag:social.super-niche.club,2019-01-17:noticeId=2353002:objectType=note",
+ "visibility": "public"
+}, {
+ "account": {
+ "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"
+ },
+ "application": {
+ "name": "Web",
+ "website": null
+ },
+ "content": "ruin a distro in 3 words<br /><a href=\"https://miniwa.moe/media/4076945a-4a3b-4fb3-8b62-04eb401697ff/image.png\">image.png</a>",
+ "created_at": "2019-01-17T14:13:36.000Z",
+ "emojis": [],
+ "favourited": false,
+ "favourites_count": 0,
+ "id": "10419787",
+ "in_reply_to_account_id": null,
+ "in_reply_to_id": null,
+ "language": null,
+ "media_attachments": [{
+ "description": "image.png",
+ "id": "-948580066",
+ "preview_url": "https://miniwa.moe/media/4076945a-4a3b-4fb3-8b62-04eb401697ff/image.png",
+ "remote_url": "https://miniwa.moe/media/4076945a-4a3b-4fb3-8b62-04eb401697ff/image.png",
+ "text_url": "https://miniwa.moe/media/4076945a-4a3b-4fb3-8b62-04eb401697ff/image.png",
+ "type": "image",
+ "url": "https://miniwa.moe/media/4076945a-4a3b-4fb3-8b62-04eb401697ff/image.png"
+ }],
+ "mentions": [{
+ "acct": "ivesen@miniwa.moe",
+ "id": "39686",
+ "url": "https://miniwa.moe/users/ivesen",
+ "username": "ivesen"
+ }],
+ "muted": false,
+ "reblog": {
+ "account": {
+ "acct": "ivesen@miniwa.moe",
+ "avatar": "https://miniwa.moe/media/745353b1-89b0-4597-bd76-3ed6ec122c03/504e655c84a6a6d61d34f8daaf3baa6627088f73427763a044729dbf85cff4b8.png",
+ "avatar_static": "https://miniwa.moe/media/745353b1-89b0-4597-bd76-3ed6ec122c03/504e655c84a6a6d61d34f8daaf3baa6627088f73427763a044729dbf85cff4b8.png",
+ "bot": false,
+ "created_at": "2018-09-08T17:55:27.000Z",
+ "display_name": "tsumiki :bun:",
+ "emojis": [{
+ "shortcode": "bun",
+ "static_url": "https://miniwa.moe/emoji/custom/bun.png",
+ "url": "https://miniwa.moe/emoji/custom/bun.png",
+ "visible_in_picker": false
+ }],
+ "fields": [],
+ "followers_count": 8,
+ "following_count": 3,
+ "header": "https://miniwa.moe/media/bff7489f-22bb-4170-a68b-ba0e1c52116e/cf69fbec904a109f8c41b97ec03bfa7dbe49992fd7d61528a933740789b55eb1.gif",
+ "header_static": "https://miniwa.moe/media/bff7489f-22bb-4170-a68b-ba0e1c52116e/cf69fbec904a109f8c41b97ec03bfa7dbe49992fd7d61528a933740789b55eb1.gif",
+ "id": "39686",
+ "locked": false,
+ "note": "native resident of the black lizard planet trying to understand hoomanslocated near niflheimr xmpp: ivesen@xmpp.xyz | matrix: no | email: anything you could imagine @ ivesen.moe",
+ "pleroma": {
+ "confirmation_pending": false,
+ "tags": null
+ },
+ "source": {
+ "note": "",
+ "privacy": "public",
+ "sensitive": false
+ },
+ "statuses_count": 13174,
+ "url": "https://miniwa.moe/users/ivesen",
+ "username": "ivesen"
+ },
+ "application": {
+ "name": "Web",
+ "website": null
+ },
+ "content": "ruin a distro in 3 words<br /><a href=\"https://miniwa.moe/media/4076945a-4a3b-4fb3-8b62-04eb401697ff/image.png\">image.png</a>",
+ "created_at": "2019-01-17T13:41:35.000Z",
+ "emojis": [],
+ "favourited": false,
+ "favourites_count": 3,
+ "id": "10418865",
+ "in_reply_to_account_id": null,
+ "in_reply_to_id": null,
+ "language": null,
+ "media_attachments": [{
+ "description": "image.png",
+ "id": "-948580066",
+ "preview_url": "https://miniwa.moe/media/4076945a-4a3b-4fb3-8b62-04eb401697ff/image.png",
+ "remote_url": "https://miniwa.moe/media/4076945a-4a3b-4fb3-8b62-04eb401697ff/image.png",
+ "text_url": "https://miniwa.moe/media/4076945a-4a3b-4fb3-8b62-04eb401697ff/image.png",
+ "type": "image",
+ "url": "https://miniwa.moe/media/4076945a-4a3b-4fb3-8b62-04eb401697ff/image.png"
+ }],
+ "mentions": [],
+ "muted": false,
+ "reblog": null,
+ "reblogged": false,
+ "reblogs_count": 6,
+ "replies_count": 0,
+ "sensitive": false,
+ "spoiler_text": "",
+ "tags": [],
+ "uri": "https://miniwa.moe/objects/448e2944-0ecd-457f-92c3-cb454f2b0fab",
+ "url": "https://miniwa.moe/objects/448e2944-0ecd-457f-92c3-cb454f2b0fab",
+ "visibility": "public"
+ },
+ "reblogged": false,
+ "reblogs_count": 0,
+ "replies_count": 0,
+ "sensitive": false,
+ "spoiler_text": "",
+ "tags": [],
+ "uri": "https://miniwa.moe/objects/448e2944-0ecd-457f-92c3-cb454f2b0fab",
+ "url": "https://miniwa.moe/objects/448e2944-0ecd-457f-92c3-cb454f2b0fab",
+ "visibility": "public"
+}, {
+ "account": {
+ "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"
+ },
+ "application": {
+ "name": "Web",
+ "website": null
+ },
+ "content": "<span><a data-user=\"570\" href=\"https://social.sakamoto.gq/users/eal\">@<span>eal</span></a></span> thanks, alex jacobson :denton:",
+ "created_at": "2019-01-17T14:12:42.000Z",
+ "emojis": [{
+ "shortcode": "denton",
+ "static_url": "https://shigusegubu.club/emoji/sgsgb/denton.png",
+ "url": "https://shigusegubu.club/emoji/sgsgb/denton.png",
+ "visible_in_picker": false
+ }],
+ "favourited": false,
+ "favourites_count": 0,
+ "id": "10419752",
+ "in_reply_to_account_id": "570",
+ "in_reply_to_id": "10419729",
+ "language": null,
+ "media_attachments": [],
+ "mentions": [{
+ "acct": "eal@social.sakamoto.gq",
+ "id": "570",
+ "url": "https://social.sakamoto.gq/users/eal",
+ "username": "eal"
+ }],
+ "muted": false,
+ "reblog": null,
+ "reblogged": false,
+ "reblogs_count": 0,
+ "replies_count": 0,
+ "sensitive": false,
+ "spoiler_text": "",
+ "tags": [],
+ "uri": "https://shigusegubu.club/objects/38b1bc44-15d8-40dd-b1aa-937e0ff4a86d",
+ "url": "https://shigusegubu.club/objects/38b1bc44-15d8-40dd-b1aa-937e0ff4a86d",
+ "visibility": "public"
+}, {
+ "account": {
+ "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"
+ },
+ "application": {
+ "name": "Web",
+ "website": null
+ },
+ "content": "<span><a data-user=\"4395\" href=\"https://blovice.bahnhof.cz/users/pony\">@<span>pony</span></a></span> yeah",
+ "created_at": "2019-01-17T14:11:01.000Z",
+ "emojis": [],
+ "favourited": false,
+ "favourites_count": 1,
+ "id": "10419697",
+ "in_reply_to_account_id": "4395",
+ "in_reply_to_id": "10419694",
+ "language": null,
+ "media_attachments": [],
+ "mentions": [{
+ "acct": "pony@blovice.bahnhof.cz",
+ "id": "4395",
+ "url": "https://blovice.bahnhof.cz/users/pony",
+ "username": "pony"
+ }],
+ "muted": false,
+ "reblog": null,
+ "reblogged": false,
+ "reblogs_count": 0,
+ "replies_count": 0,
+ "sensitive": false,
+ "spoiler_text": "",
+ "tags": [],
+ "uri": "https://shigusegubu.club/objects/fbff5da4-a517-42a9-bca9-17cae8cf2542",
+ "url": "https://shigusegubu.club/objects/fbff5da4-a517-42a9-bca9-17cae8cf2542",
+ "visibility": "public"
+}, {
+ "account": {
+ "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"
+ },
+ "application": {
+ "name": "Web",
+ "website": null
+ },
+ "content": "i probably shouldn't be watchng 4-hour stream vods of joel playing doom",
+ "created_at": "2019-01-17T14:10:34.000Z",
+ "emojis": [],
+ "favourited": false,
+ "favourites_count": 0,
+ "id": "10419684",
+ "in_reply_to_account_id": null,
+ "in_reply_to_id": null,
+ "language": null,
+ "media_attachments": [],
+ "mentions": [],
+ "muted": false,
+ "reblog": null,
+ "reblogged": false,
+ "reblogs_count": 0,
+ "replies_count": 0,
+ "sensitive": false,
+ "spoiler_text": "",
+ "tags": [],
+ "uri": "https://shigusegubu.club/objects/4007d659-27c6-4577-be10-fd134f5e4e7e",
+ "url": "https://shigusegubu.club/objects/4007d659-27c6-4577-be10-fd134f5e4e7e",
+ "visibility": "public"
+}, {
+ "account": {
+ "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"
+ },
+ "application": {
+ "name": "Web",
+ "website": null
+ },
+ "content": "i got out of bed.",
+ "created_at": "2019-01-17T14:10:18.000Z",
+ "emojis": [],
+ "favourited": false,
+ "favourites_count": 1,
+ "id": "10419671",
+ "in_reply_to_account_id": null,
+ "in_reply_to_id": null,
+ "language": null,
+ "media_attachments": [],
+ "mentions": [],
+ "muted": false,
+ "reblog": null,
+ "reblogged": false,
+ "reblogs_count": 0,
+ "replies_count": 0,
+ "sensitive": false,
+ "spoiler_text": "",
+ "tags": [],
+ "uri": "https://shigusegubu.club/objects/59912d51-1cc6-4dc7-828c-f167e6c8b391",
+ "url": "https://shigusegubu.club/objects/59912d51-1cc6-4dc7-828c-f167e6c8b391",
+ "visibility": "public"
+}, {
+ "account": {
+ "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"
+ },
+ "application": {
+ "name": "Web",
+ "website": null
+ },
+ "content": "New day!",
+ "created_at": "2019-01-17T13:25:56.000Z",
+ "emojis": [],
+ "favourited": false,
+ "favourites_count": 2,
+ "id": "10418390",
+ "in_reply_to_account_id": null,
+ "in_reply_to_id": null,
+ "language": null,
+ "media_attachments": [],
+ "mentions": [],
+ "muted": false,
+ "reblog": null,
+ "reblogged": false,
+ "reblogs_count": 1,
+ "replies_count": 0,
+ "sensitive": false,
+ "spoiler_text": "",
+ "tags": [],
+ "uri": "https://shigusegubu.club/objects/62690bce-3f49-4047-9c8e-8941f2f79e10",
+ "url": "https://shigusegubu.club/objects/62690bce-3f49-4047-9c8e-8941f2f79e10",
+ "visibility": "public"
+}, {
+ "account": {
+ "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"
+ },
+ "application": {
+ "name": "Web",
+ "website": null
+ },
+ "content": "<span><a data-user=\"27194\" href=\"https://queer.hacktivis.me/users/lanodan\">@<span>lanodan</span></a></span> you either use nvidia-blob or don't use nvidia. Unless it's a very VERY old GPU.",
+ "created_at": "2019-01-16T22:21:10.000Z",
+ "emojis": [],
+ "favourited": false,
+ "favourites_count": 0,
+ "id": "10395196",
+ "in_reply_to_account_id": "27194",
+ "in_reply_to_id": "10394997",
+ "language": null,
+ "media_attachments": [],
+ "mentions": [{
+ "acct": "lanodan@queer.hacktivis.me",
+ "id": "27194",
+ "url": "https://queer.hacktivis.me/users/lanodan",
+ "username": "lanodan"
+ }],
+ "muted": false,
+ "reblog": null,
+ "reblogged": false,
+ "reblogs_count": 0,
+ "replies_count": 0,
+ "sensitive": false,
+ "spoiler_text": "",
+ "tags": [],
+ "uri": "https://shigusegubu.club/objects/818f3dd0-2ff8-4def-a170-e4d4c405f387",
+ "url": "https://shigusegubu.club/objects/818f3dd0-2ff8-4def-a170-e4d4c405f387",
+ "visibility": "public"
+}, {
+ "account": {
+ "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"
+ },
+ "application": {
+ "name": "Web",
+ "website": null
+ },
+ "content": "<span><a data-user=\"58\" href=\"https://pleroma.soykaf.com/users/lain\">@<span>lain</span></a></span> <br />1. 🔪<br />2. trigger trypophobia<br />3. sprinkle with spices<br />4. pee on it<br />5. pat pat<br />6. blanket time<br />7. bring in the cocaine<br />8. ?????????????<br />9. ???? hitler ???????<br />10. ???????????<br />11. ???? Adobe After Effects ????<br />12. !!!!!!!!!!!!!!BREAK IT!!!!!!!!!!!<br />13. !!!kindar suprize!!<br />14. (time skip for two hours, realisation that using cocaine was a bad idea)<br />15. (still not over from cocaine)<br />16. куличики! Плов, огурцы, это еще что бля<br />17. fuck it, play it cool, just put meat on the mounds<br />18. note to self: never use cocaine in culinary ever again",
+ "created_at": "2019-01-16T22:20:50.000Z",
+ "emojis": [],
+ "favourited": false,
+ "favourites_count": 1,
+ "id": "10395183",
+ "in_reply_to_account_id": "58",
+ "in_reply_to_id": "10394990",
+ "language": null,
+ "media_attachments": [],
+ "mentions": [{
+ "acct": "lain@pleroma.soykaf.com",
+ "id": "58",
+ "url": "https://pleroma.soykaf.com/users/lain",
+ "username": "lain"
+ }],
+ "muted": false,
+ "reblog": null,
+ "reblogged": false,
+ "reblogs_count": 0,
+ "replies_count": 0,
+ "sensitive": false,
+ "spoiler_text": "",
+ "tags": [],
+ "uri": "https://shigusegubu.club/objects/0783a193-c097-488d-8944-47df9372cd6e",
+ "url": "https://shigusegubu.club/objects/0783a193-c097-488d-8944-47df9372cd6e",
+ "visibility": "public"
+}, {
+ "account": {
+ "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"
+ },
+ "application": {
+ "name": "Web",
+ "website": null
+ },
+ "content": "<span><a data-user=\"55339\" href=\"https://pleroma.site/users/cathode\">@<span>cathode</span></a></span> me too",
+ "created_at": "2019-01-16T22:09:28.000Z",
+ "emojis": [],
+ "favourited": false,
+ "favourites_count": 1,
+ "id": "10394697",
+ "in_reply_to_account_id": "55339",
+ "in_reply_to_id": "10394677",
+ "language": null,
+ "media_attachments": [],
+ "mentions": [{
+ "acct": "cathode@pleroma.site",
+ "id": "55339",
+ "url": "https://pleroma.site/users/cathode",
+ "username": "cathode"
+ }],
+ "muted": false,
+ "reblog": null,
+ "reblogged": false,
+ "reblogs_count": 0,
+ "replies_count": 0,
+ "sensitive": false,
+ "spoiler_text": "",
+ "tags": [],
+ "uri": "https://shigusegubu.club/objects/145d5252-7b8e-467d-9f36-1db0818f452f",
+ "url": "https://shigusegubu.club/objects/145d5252-7b8e-467d-9f36-1db0818f452f",
+ "visibility": "public"
+}, {
+ "account": {
+ "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"
+ },
+ "application": {
+ "name": "Web",
+ "website": null
+ },
+ "content": "I love pleroma and lain",
+ "created_at": "2019-01-16T22:09:24.000Z",
+ "emojis": [],
+ "favourited": false,
+ "favourites_count": 0,
+ "id": "10394690",
+ "in_reply_to_account_id": null,
+ "in_reply_to_id": null,
+ "language": null,
+ "media_attachments": [],
+ "mentions": [{
+ "acct": "cathode@pleroma.site",
+ "id": "55339",
+ "url": "https://pleroma.site/users/cathode",
+ "username": "cathode"
+ }],
+ "muted": false,
+ "reblog": {
+ "account": {
+ "acct": "cathode@pleroma.site",
+ "avatar": "https://pleroma.site/media/cc874883-fafb-4f25-80f2-e16677e83621/c50a4ea55a99fdf3b69bf66ce2768bc2c48c6135332914bb49445aab1211d794.gif",
+ "avatar_static": "https://pleroma.site/media/cc874883-fafb-4f25-80f2-e16677e83621/c50a4ea55a99fdf3b69bf66ce2768bc2c48c6135332914bb49445aab1211d794.gif",
+ "bot": false,
+ "created_at": "2018-12-14T21:47:14.000Z",
+ "display_name": "gay hug bug loves you",
+ "emojis": [],
+ "fields": [],
+ "followers_count": 0,
+ "following_count": 0,
+ "header": "https://pleroma.site/media/8170fac2-fc17-4b9c-8624-52491d56f769/8cb558d3960254180b10860012a519253baaa1db88a0476da02be6a558015b34.jpg",
+ "header_static": "https://pleroma.site/media/8170fac2-fc17-4b9c-8624-52491d56f769/8cb558d3960254180b10860012a519253baaa1db88a0476da02be6a558015b34.jpg",
+ "id": "55339",
+ "locked": false,
+ "note": "he/him but you can call me anything really<br /><br />welcome to my profile :333<br /><br /><a href=\"https://cathode.neocities.org/\">https://cathode.neocities.org/</a>",
+ "pleroma": {
+ "confirmation_pending": false,
+ "tags": null
+ },
+ "source": {
+ "note": "",
+ "privacy": "public",
+ "sensitive": false
+ },
+ "statuses_count": 16,
+ "url": "https://pleroma.site/users/cathode",
+ "username": "cathode"
+ },
+ "application": {
+ "name": "Web",
+ "website": null
+ },
+ "content": "I love pleroma and lain",
+ "created_at": "2019-01-16T22:08:50.000Z",
+ "emojis": [],
+ "favourited": false,
+ "favourites_count": 4,
+ "id": "10394677",
+ "in_reply_to_account_id": null,
+ "in_reply_to_id": null,
+ "language": null,
+ "media_attachments": [],
+ "mentions": [],
+ "muted": false,
+ "reblog": null,
+ "reblogged": false,
+ "reblogs_count": 2,
+ "replies_count": 0,
+ "sensitive": false,
+ "spoiler_text": "",
+ "tags": [],
+ "uri": "https://pleroma.site/objects/3076c055-0e34-4cf9-86ca-2d148b9b694a",
+ "url": "https://pleroma.site/objects/3076c055-0e34-4cf9-86ca-2d148b9b694a",
+ "visibility": "public"
+ },
+ "reblogged": false,
+ "reblogs_count": 0,
+ "replies_count": 0,
+ "sensitive": false,
+ "spoiler_text": "",
+ "tags": [],
+ "uri": "https://pleroma.site/objects/3076c055-0e34-4cf9-86ca-2d148b9b694a",
+ "url": "https://pleroma.site/objects/3076c055-0e34-4cf9-86ca-2d148b9b694a",
+ "visibility": "public"
+}, {
+ "account": {
+ "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"
+ },
+ "application": {
+ "name": "Web",
+ "website": null
+ },
+ "content": "<span><a data-user=\"58\" href=\"https://pleroma.soykaf.com/users/lain\">@<span>lain</span></a></span> that trick he does in the very beginning with that smile oh my god",
+ "created_at": "2019-01-16T22:08:15.000Z",
+ "emojis": [],
+ "favourited": false,
+ "favourites_count": 2,
+ "id": "10394647",
+ "in_reply_to_account_id": "58",
+ "in_reply_to_id": "10394582",
+ "language": null,
+ "media_attachments": [],
+ "mentions": [{
+ "acct": "lain@pleroma.soykaf.com",
+ "id": "58",
+ "url": "https://pleroma.soykaf.com/users/lain",
+ "username": "lain"
+ }],
+ "muted": false,
+ "reblog": null,
+ "reblogged": false,
+ "reblogs_count": 0,
+ "replies_count": 0,
+ "sensitive": false,
+ "spoiler_text": "",
+ "tags": [],
+ "uri": "https://shigusegubu.club/objects/d4eb7c46-02f9-4b1f-83af-926cefa21f33",
+ "url": "https://shigusegubu.club/objects/d4eb7c46-02f9-4b1f-83af-926cefa21f33",
+ "visibility": "public"
+}, {
+ "account": {
+ "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"
+ },
+ "application": {
+ "name": "Web",
+ "website": null
+ },
+ "content": ".",
+ "created_at": "2019-01-16T22:08:01.000Z",
+ "emojis": [],
+ "favourited": false,
+ "favourites_count": 0,
+ "id": "10394636",
+ "in_reply_to_account_id": null,
+ "in_reply_to_id": null,
+ "language": null,
+ "media_attachments": [{
+ "description": null,
+ "id": "-1089888084",
+ "preview_url": "https://pleroma.soykaf.com/media/d985e90d-b079-4f6a-a5bc-9b5e26b1fa5a/Tusky_1547676391813_2VJ7ITWJ3T.webm",
+ "remote_url": "https://pleroma.soykaf.com/media/d985e90d-b079-4f6a-a5bc-9b5e26b1fa5a/Tusky_1547676391813_2VJ7ITWJ3T.webm",
+ "text_url": "https://pleroma.soykaf.com/media/d985e90d-b079-4f6a-a5bc-9b5e26b1fa5a/Tusky_1547676391813_2VJ7ITWJ3T.webm",
+ "type": "video",
+ "url": "https://pleroma.soykaf.com/media/d985e90d-b079-4f6a-a5bc-9b5e26b1fa5a/Tusky_1547676391813_2VJ7ITWJ3T.webm"
+ }],
+ "mentions": [{
+ "acct": "lain@pleroma.soykaf.com",
+ "id": "58",
+ "url": "https://pleroma.soykaf.com/users/lain",
+ "username": "lain"
+ }],
+ "muted": false,
+ "reblog": {
+ "account": {
+ "acct": "lain@pleroma.soykaf.com",
+ "avatar": "https://pleroma.soykaf.com/media/57abd080-73da-4541-92c7-506d0008bce3/04c76c19e87b23f759fac1118db5e5c4fe0b0ba8d67942a9a8f4e88108543abd.jpeg",
+ "avatar_static": "https://pleroma.soykaf.com/media/57abd080-73da-4541-92c7-506d0008bce3/04c76c19e87b23f759fac1118db5e5c4fe0b0ba8d67942a9a8f4e88108543abd.jpeg",
+ "bot": false,
+ "created_at": "2017-12-17T22:26:31.000Z",
+ "display_name": "you're looking at an ace",
+ "emojis": [],
+ "fields": [],
+ "followers_count": 29,
+ "following_count": 1389,
+ "header": "https://pleroma.soykaf.com/images/banner.png",
+ "header_static": "https://pleroma.soykaf.com/images/banner.png",
+ "id": "58",
+ "locked": false,
+ "note": "blushy-crushy fediverse idol + pleroma dev.<br />let's be friends <br />ぷれろまの生徒会長。謎の外人。日本語OK. <br />公主病.<br />I invented the internet.<br />xmpp: lain@trashserver.net<br />matrix: lambadalambda@matrix.heldscal.la<br />pgp: 58B48DE582EE103C964735A1F9C6698E14CCE33C",
+ "pleroma": {
+ "confirmation_pending": false,
+ "tags": null
+ },
+ "source": {
+ "note": "",
+ "privacy": "public",
+ "sensitive": false
+ },
+ "statuses_count": 35042,
+ "url": "https://pleroma.soykaf.com/users/lain",
+ "username": "lain"
+ },
+ "application": {
+ "name": "Web",
+ "website": null
+ },
+ "content": ".",
+ "created_at": "2019-01-16T22:06:35.000Z",
+ "emojis": [],
+ "favourited": false,
+ "favourites_count": 2,
+ "id": "10394582",
+ "in_reply_to_account_id": null,
+ "in_reply_to_id": null,
+ "language": null,
+ "media_attachments": [{
+ "description": null,
+ "id": "-1089888084",
+ "preview_url": "https://pleroma.soykaf.com/media/d985e90d-b079-4f6a-a5bc-9b5e26b1fa5a/Tusky_1547676391813_2VJ7ITWJ3T.webm",
+ "remote_url": "https://pleroma.soykaf.com/media/d985e90d-b079-4f6a-a5bc-9b5e26b1fa5a/Tusky_1547676391813_2VJ7ITWJ3T.webm",
+ "text_url": "https://pleroma.soykaf.com/media/d985e90d-b079-4f6a-a5bc-9b5e26b1fa5a/Tusky_1547676391813_2VJ7ITWJ3T.webm",
+ "type": "video",
+ "url": "https://pleroma.soykaf.com/media/d985e90d-b079-4f6a-a5bc-9b5e26b1fa5a/Tusky_1547676391813_2VJ7ITWJ3T.webm"
+ }],
+ "mentions": [],
+ "muted": false,
+ "reblog": null,
+ "reblogged": false,
+ "reblogs_count": 1,
+ "replies_count": 0,
+ "sensitive": false,
+ "spoiler_text": "",
+ "tags": [],
+ "uri": "https://pleroma.soykaf.com/objects/338b6bd2-3c2d-40fe-93a3-28b688782733",
+ "url": "https://pleroma.soykaf.com/objects/338b6bd2-3c2d-40fe-93a3-28b688782733",
+ "visibility": "public"
+ },
+ "reblogged": false,
+ "reblogs_count": 0,
+ "replies_count": 0,
+ "sensitive": false,
+ "spoiler_text": "",
+ "tags": [],
+ "uri": "https://pleroma.soykaf.com/objects/338b6bd2-3c2d-40fe-93a3-28b688782733",
+ "url": "https://pleroma.soykaf.com/objects/338b6bd2-3c2d-40fe-93a3-28b688782733",
+ "visibility": "public"
+}, {
+ "account": {
+ "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"
+ },
+ "application": {
+ "name": "Web",
+ "website": null
+ },
+ "content": "<span><a data-user=\"17544\" href=\"https://iscute.moe/users/proxeus\">@<span>proxeus</span></a></span> good song",
+ "created_at": "2019-01-16T22:04:23.000Z",
+ "emojis": [],
+ "favourited": false,
+ "favourites_count": 1,
+ "id": "10394506",
+ "in_reply_to_account_id": "17544",
+ "in_reply_to_id": "10393849",
+ "language": null,
+ "media_attachments": [],
+ "mentions": [{
+ "acct": "proxeus@iscute.moe",
+ "id": "17544",
+ "url": "https://iscute.moe/users/proxeus",
+ "username": "proxeus"
+ }],
+ "muted": false,
+ "reblog": null,
+ "reblogged": false,
+ "reblogs_count": 0,
+ "replies_count": 0,
+ "sensitive": false,
+ "spoiler_text": "",
+ "tags": [],
+ "uri": "https://shigusegubu.club/objects/f472f4ed-8b0b-492f-9d53-d69eda79629d",
+ "url": "https://shigusegubu.club/objects/f472f4ed-8b0b-492f-9d53-d69eda79629d",
+ "visibility": "public"
+}, {
+ "account": {
+ "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"
+ },
+ "application": {
+ "name": "Web",
+ "website": null
+ },
+ "content": "<span><a data-user=\"60913\" href=\"https://shigusegubu.club/users/cdmnky\">@<span>cdmnky</span></a></span> olivia been repeating \"hey i moved\" post like once every day or so",
+ "created_at": "2019-01-16T21:56:05.000Z",
+ "emojis": [],
+ "favourited": false,
+ "favourites_count": 1,
+ "id": "10393859",
+ "in_reply_to_account_id": "60913",
+ "in_reply_to_id": "10393808",
+ "language": null,
+ "media_attachments": [],
+ "mentions": [{
+ "acct": "cdmnky",
+ "id": "60913",
+ "url": "https://shigusegubu.club/users/cdmnky",
+ "username": "cdmnky"
+ }],
+ "muted": false,
+ "reblog": null,
+ "reblogged": false,
+ "reblogs_count": 0,
+ "replies_count": 0,
+ "sensitive": false,
+ "spoiler_text": "",
+ "tags": [],
+ "uri": "https://shigusegubu.club/objects/d6fb4fd2-1f6a-4446-a1a6-5edd34050096",
+ "url": "https://shigusegubu.club/objects/d6fb4fd2-1f6a-4446-a1a6-5edd34050096",
+ "visibility": "public"
+}]
diff --git a/test/unit/specs/modules/statuses.spec.js b/test/unit/specs/modules/statuses.spec.js
index 7d403312a1..33628b9b28 100644
--- a/test/unit/specs/modules/statuses.spec.js
+++ b/test/unit/specs/modules/statuses.spec.js
@@ -1,339 +1,298 @@
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
-const makeMockStatus = ({id, text, is_post_verb = true}) => {
+const makeMockStatus = ({id, text, type = 'status'}) => {
return {
id,
- user: {id: 0},
+ user: {id: '0'},
name: 'status',
text: text || `Text number ${id}`,
fave_num: 0,
uri: '',
- is_post_verb,
+ type,
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', () => {
- 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', () => {
- const aStatus = makeMockStatus({id: 1, text: 'Hello oniichan'})
+ const aStatus = makeMockStatus({id: '1', text: 'Hello oniichan'})
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', () => {
it('adds the status to allStatuses and to the given timeline', () => {
const state = cloneDeep(defaultState)
- const status = makeMockStatus({id: 1})
+ const status = makeMockStatus({id: '1'})
mutations.addNewStatuses(state, { statuses: [status], timeline: 'public' })
expect(state.allStatuses).to.eql([status])
expect(state.timelines.public.statuses).to.eql([status])
expect(state.timelines.public.visibleStatuses).to.eql([])
expect(state.timelines.public.newStatusCount).to.equal(1)
})
it('counts the status as new if it has not been seen on this timeline', () => {
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: 'friends' })
expect(state.allStatuses).to.eql([status])
expect(state.timelines.public.statuses).to.eql([status])
expect(state.timelines.public.visibleStatuses).to.eql([])
expect(state.timelines.public.newStatusCount).to.equal(1)
expect(state.allStatuses).to.eql([status])
expect(state.timelines.friends.statuses).to.eql([status])
expect(state.timelines.friends.visibleStatuses).to.eql([])
expect(state.timelines.friends.newStatusCount).to.equal(1)
})
it('add the statuses to allStatuses if no timeline is given', () => {
const state = cloneDeep(defaultState)
- const status = makeMockStatus({id: 1})
+ const status = makeMockStatus({id: '1'})
mutations.addNewStatuses(state, { statuses: [status] })
expect(state.allStatuses).to.eql([status])
expect(state.timelines.public.statuses).to.eql([])
expect(state.timelines.public.visibleStatuses).to.eql([])
expect(state.timelines.public.newStatusCount).to.equal(0)
})
it('adds the status to allStatuses and to the given timeline, directly visible', () => {
const state = cloneDeep(defaultState)
- const status = makeMockStatus({id: 1})
+ const status = makeMockStatus({id: '1'})
mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' })
expect(state.allStatuses).to.eql([status])
expect(state.timelines.public.statuses).to.eql([status])
expect(state.timelines.public.visibleStatuses).to.eql([status])
expect(state.timelines.public.newStatusCount).to.equal(0)
})
it('removes statuses by tag on deletion', () => {
const state = cloneDeep(defaultState)
- const status = makeMockStatus({id: 1})
- const otherStatus = makeMockStatus({id: 3})
+ const status = makeMockStatus({id: '1'})
+ const otherStatus = makeMockStatus({id: '3'})
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.uri = 'xxx'
mutations.addNewStatuses(state, { statuses: [status, otherStatus], showImmediately: true, timeline: 'public' })
mutations.addNewStatuses(state, { statuses: [deletion], showImmediately: true, timeline: 'public' })
expect(state.allStatuses).to.eql([otherStatus])
expect(state.timelines.public.statuses).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', () => {
const state = cloneDeep(defaultState)
- const status = makeMockStatus({id: 1})
- const secondStatus = makeMockStatus({id: 2})
+ const status = makeMockStatus({id: '1'})
+ const secondStatus = makeMockStatus({id: '2'})
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 })
expect(state.timelines.public.statuses).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', () => {
const state = cloneDeep(defaultState)
- const nonVisibleStatus = makeMockStatus({id: 1})
- const status = makeMockStatus({id: 3})
- const statusTwo = makeMockStatus({id: 2})
- const statusThree = makeMockStatus({id: 4})
+ const nonVisibleStatus = makeMockStatus({id: '1'})
+ const status = makeMockStatus({id: '3'})
+ const statusTwo = makeMockStatus({id: '2'})
+ const statusThree = makeMockStatus({id: '4'})
mutations.addNewStatuses(state, { statuses: [nonVisibleStatus], showImmediately: false, timeline: 'public' })
mutations.addNewStatuses(state, { statuses: [status], 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' })
expect(state.timelines.public.statuses).to.eql([statusThree, status, statusTwo, nonVisibleStatus])
expect(state.timelines.public.visibleStatuses).to.eql([statusThree, status, statusTwo])
})
it('splits retweets from their status and links them', () => {
const state = cloneDeep(defaultState)
- const status = makeMockStatus({id: 1})
- const retweet = makeMockStatus({id: 2, is_post_verb: false})
- const modStatus = makeMockStatus({id: 1, text: 'something else'})
+ const status = makeMockStatus({id: '1'})
+ const retweet = makeMockStatus({id: '2', type: 'retweet'})
+ const modStatus = makeMockStatus({id: '1', text: 'something else'})
retweet.retweeted_status = status
// It adds both statuses, but only the retweet to visible.
mutations.addNewStatuses(state, { statuses: [retweet], timeline: 'public', showImmediately: true })
expect(state.timelines.public.visibleStatuses).to.have.length(1)
expect(state.timelines.public.statuses).to.have.length(1)
expect(state.allStatuses).to.have.length(2)
- expect(state.allStatuses[0].id).to.equal(1)
- expect(state.allStatuses[1].id).to.equal(2)
+ expect(state.allStatuses[0].id).to.equal('1')
+ expect(state.allStatuses[1].id).to.equal('2')
// It refers to the modified status.
mutations.addNewStatuses(state, { statuses: [modStatus], timeline: 'public' })
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[1].id).to.equal(2)
+ expect(state.allStatuses[1].id).to.equal('2')
expect(retweet.retweeted_status.text).to.eql(modStatus.text)
})
it('replaces existing statuses with the same id', () => {
const state = cloneDeep(defaultState)
- const status = makeMockStatus({id: 1})
- const modStatus = makeMockStatus({id: 1, text: 'something else'})
+ const status = makeMockStatus({id: '1'})
+ const modStatus = makeMockStatus({id: '1', text: 'something else'})
// Add original status
mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' })
expect(state.timelines.public.visibleStatuses).to.have.length(1)
expect(state.allStatuses).to.have.length(1)
// Add new version of status
mutations.addNewStatuses(state, { statuses: [modStatus], showImmediately: true, timeline: 'public' })
expect(state.timelines.public.visibleStatuses).to.have.length(1)
expect(state.allStatuses).to.have.length(1)
expect(state.allStatuses[0].text).to.eql(modStatus.text)
})
it('replaces existing statuses with the same id, coming from a retweet', () => {
const state = cloneDeep(defaultState)
- const status = makeMockStatus({id: 1})
- const modStatus = makeMockStatus({id: 1, text: 'something else'})
- const retweet = makeMockStatus({id: 2, is_post_verb: false})
+ const status = makeMockStatus({id: '1'})
+ const modStatus = makeMockStatus({id: '1', text: 'something else'})
+ const retweet = makeMockStatus({id: '2', type: 'retweet'})
retweet.retweeted_status = modStatus
// Add original status
mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' })
expect(state.timelines.public.visibleStatuses).to.have.length(1)
expect(state.allStatuses).to.have.length(1)
// Add new version of status
mutations.addNewStatuses(state, { statuses: [retweet], showImmediately: false, timeline: 'public' })
expect(state.timelines.public.visibleStatuses).to.have.length(1)
// Don't add the retweet itself if the tweet is visible
expect(state.timelines.public.statuses).to.have.length(1)
expect(state.allStatuses).to.have.length(2)
expect(state.allStatuses[0].text).to.eql(modStatus.text)
})
it('handles favorite actions', () => {
const state = cloneDeep(defaultState)
- const status = makeMockStatus({id: 1})
+ const status = makeMockStatus({id: '1'})
const favorite = {
- id: 2,
- is_post_verb: false,
+ id: '2',
+ type: 'favorite',
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',
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: [favorite], showImmediately: true, timeline: 'public' })
expect(state.timelines.public.visibleStatuses.length).to.eql(1)
expect(state.timelines.public.visibleStatuses[0].fave_num).to.eql(1)
expect(state.timelines.public.maxId).to.eq(favorite.id)
// Adding it again does nothing
mutations.addNewStatuses(state, { statuses: [favorite], showImmediately: true, timeline: 'public' })
expect(state.timelines.public.visibleStatuses.length).to.eql(1)
expect(state.timelines.public.visibleStatuses[0].fave_num).to.eql(1)
expect(state.timelines.public.maxId).to.eq(favorite.id)
// 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 = {
- id: 1
+ id: '1'
}
const ownFavorite = {
- id: 3,
- is_post_verb: false,
+ id: '3',
+ type: 'favorite',
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',
text: 'a favorited something by b',
user
}
mutations.addNewStatuses(state, { statuses: [ownFavorite], showImmediately: true, timeline: 'public', user })
expect(state.timelines.public.visibleStatuses.length).to.eql(1)
expect(state.timelines.public.visibleStatuses[0].fave_num).to.eql(1)
expect(state.timelines.public.visibleStatuses[0].favorited).to.eql(true)
})
describe('notifications', () => {
it('removes a notification when the notice gets removed', () => {
- const user = { id: 1 }
+ const user = { id: '1' }
const state = cloneDeep(defaultState)
- const status = makeMockStatus({id: 1})
- const otherStatus = makeMockStatus({id: 3})
- const mentionedStatus = makeMockStatus({id: 2})
+ const status = makeMockStatus({id: '1'})
+ const otherStatus = makeMockStatus({id: '3'})
+ const mentionedStatus = makeMockStatus({id: '2'})
mentionedStatus.attentions = [user]
mentionedStatus.uri = 'xxx'
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.uri = 'xxx'
mutations.addNewStatuses(state, { statuses: [status, otherStatus], user })
mutations.addNewNotifications(
state,
{
notifications: [{
- ntype: 'mention',
+ from_profile: { id: '2' },
+ id: '998',
+ type: 'mention',
status: otherStatus,
- notice: otherStatus,
- is_seen: false
+ action: otherStatus,
+ seen: false
}]
})
expect(state.notifications.data.length).to.eql(1)
mutations.addNewNotifications(
state,
{
notifications: [{
- ntype: 'mention',
+ from_profile: { id: '2' },
+ id: '999',
+ type: 'mention',
status: mentionedStatus,
- notice: mentionedStatus,
- is_seen: false
+ action: mentionedStatus,
+ seen: false
}]
})
mutations.addNewStatuses(state, { statuses: [mentionedStatus], user })
expect(state.allStatuses.length).to.eql(3)
expect(state.notifications.data.length).to.eql(2)
expect(state.notifications.data[1].status).to.eql(mentionedStatus)
expect(state.notifications.data[1].action).to.eql(mentionedStatus)
expect(state.notifications.data[1].type).to.eql('mention')
mutations.addNewStatuses(state, { statuses: [deletion], user })
expect(state.allStatuses.length).to.eql(2)
expect(state.notifications.data.length).to.eql(1)
})
})
})
diff --git a/test/unit/specs/modules/users.spec.js b/test/unit/specs/modules/users.spec.js
index af60c9b399..b0f3c51eeb 100644
--- a/test/unit/specs/modules/users.spec.js
+++ b/test/unit/specs/modules/users.spec.js
@@ -1,62 +1,62 @@
import { cloneDeep } from 'lodash'
import { defaultState, mutations, getters } from '../../../../src/modules/users.js'
describe('The users module', () => {
describe('mutations', () => {
it('adds new users to the set, merging in new information for old users', () => {
const state = cloneDeep(defaultState)
- const user = { id: 1, name: 'Guy' }
- const modUser = { id: 1, name: 'Dude' }
+ const user = { id: '1', name: 'Guy' }
+ const modUser = { id: '1', name: 'Dude' }
mutations.addNewUsers(state, [user])
expect(state.users).to.have.length(1)
expect(state.users).to.eql([user])
mutations.addNewUsers(state, [modUser])
expect(state.users).to.have.length(1)
expect(state.users).to.eql([user])
expect(state.users[0].name).to.eql('Dude')
})
it('sets a mute bit on users', () => {
const state = cloneDeep(defaultState)
- const user = { id: 1, name: 'Guy' }
+ const user = { id: '1', name: 'Guy' }
mutations.addNewUsers(state, [user])
mutations.setMuted(state, {user, muted: true})
expect(user.muted).to.eql(true)
mutations.setMuted(state, {user, muted: false})
expect(user.muted).to.eql(false)
})
})
describe('getUserByName', () => {
it('returns user with matching screen_name', () => {
const state = {
users: [
- { screen_name: 'Guy', id: 1 }
+ { screen_name: 'Guy', id: '1' }
]
}
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)
})
})
describe('getUserById', () => {
it('returns user with matching id', () => {
const state = {
users: [
- { screen_name: 'Guy', id: 1 }
+ { screen_name: 'Guy', id: '1' }
]
}
- const id = 1
- const expected = { screen_name: 'Guy', id: 1 }
+ const id = '1'
+ const expected = { screen_name: 'Guy', id: '1' }
expect(getters.userById(state)(id)).to.eql(expected)
})
})
})
diff --git a/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js b/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js
new file mode 100644
index 0000000000..703fecf1c8
--- /dev/null
+++ b/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js
@@ -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')
+ })
+ })
+})
diff --git a/test/unit/specs/services/notification_utils/notification_utils.spec.js b/test/unit/specs/services/notification_utils/notification_utils.spec.js
index c44b8c9e0d..e945459edd 100644
--- a/test/unit/specs/services/notification_utils/notification_utils.spec.js
+++ b/test/unit/specs/services/notification_utils/notification_utils.spec.js
@@ -1,88 +1,88 @@
import * as NotificationUtils from 'src/services/notification_utils/notification_utils.js'
describe('NotificationUtils', () => {
describe('visibleNotificationsFromStore', () => {
it('should return sorted notifications with configured types', () => {
const store = {
state: {
statuses: {
notifications: {
data: [
{
- action: { id: 1 },
+ action: { id: '1' },
type: 'like'
},
{
- action: { id: 2 },
+ action: { id: '2' },
type: 'mention'
},
{
- action: { id: 3 },
+ action: { id: '3' },
type: 'repeat'
}
]
}
},
config: {
notificationVisibility: {
likes: true,
repeats: true,
mentions: false
}
}
}
}
const expected = [
{
- action: { id: 3 },
+ action: { id: '3' },
type: 'repeat'
},
{
- action: { id: 1 },
+ action: { id: '1' },
type: 'like'
}
]
expect(NotificationUtils.visibleNotificationsFromStore(store)).to.eql(expected)
})
})
describe('unseenNotificationsFromStore', () => {
it('should return only notifications not marked as seen', () => {
const store = {
state: {
statuses: {
notifications: {
data: [
{
- action: { id: 1 },
+ action: { id: '1' },
type: 'like',
seen: false
},
{
- action: { id: 2 },
+ action: { id: '2' },
type: 'mention',
seen: true
}
]
}
},
config: {
notificationVisibility: {
likes: true,
repeats: true,
mentions: false
}
}
}
}
const expected = [
{
- action: { id: 1 },
+ action: { id: '1' },
type: 'like',
seen: false
}
]
expect(NotificationUtils.unseenNotificationsFromStore(store)).to.eql(expected)
})
})
})

File Metadata

Mime Type
text/x-diff
Expires
Thu, Oct 29, 11:08 AM (21 h, 4 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1798024
Default Alt Text
(257 KB)

Event Timeline