Page MenuHomePhorge

No OneTemporary

Size
69 KB
Referenced Files
None
Subscribers
None
diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js
index 953b4a066d..19d9a9ac2d 100644
--- a/src/components/timeline/timeline.js
+++ b/src/components/timeline/timeline.js
@@ -1,151 +1,150 @@
import Status from '../status/status.vue'
import timelineFetcher from '../../services/timeline_fetcher/timeline_fetcher.service.js'
import Conversation from '../conversation/conversation.vue'
import { throttle } from 'lodash'
const Timeline = {
props: [
'timeline',
'timelineName',
'title',
'userId',
'tag',
'embedded',
- 'count',
- 'noLoadMore'
+ 'count'
],
data () {
return {
paused: false,
unfocused: false,
bottomedOut: false
}
},
computed: {
timelineError () { return this.$store.state.statuses.error },
newStatusCount () {
return this.timeline.newStatusCount
},
newStatusCountStr () {
if (this.timeline.flushMarker !== 0) {
return ''
} else {
return ` (${this.newStatusCount})`
}
},
classes () {
return {
root: ['timeline'].concat(!this.embedded ? ['panel', 'panel-default'] : []),
header: ['timeline-heading'].concat(!this.embedded ? ['panel-heading'] : []),
body: ['timeline-body'].concat(!this.embedded ? ['panel-body'] : []),
footer: ['timeline-footer'].concat(!this.embedded ? ['panel-footer'] : [])
}
}
},
components: {
Status,
Conversation
},
created () {
const store = this.$store
const credentials = store.state.users.currentUser.credentials
const showImmediately = this.timeline.visibleStatuses.length === 0
window.addEventListener('scroll', this.scrollLoad)
if (store.state.api.fetchers[this.timelineName]) { return false }
timelineFetcher.fetchAndUpdate({
store,
credentials,
timeline: this.timelineName,
showImmediately,
userId: this.userId,
tag: this.tag
})
},
mounted () {
if (typeof document.hidden !== 'undefined') {
document.addEventListener('visibilitychange', this.handleVisibilityChange, false)
this.unfocused = document.hidden
}
window.addEventListener('keydown', this.handleShortKey)
},
destroyed () {
window.removeEventListener('scroll', this.scrollLoad)
window.removeEventListener('keydown', this.handleShortKey)
if (typeof document.hidden !== 'undefined') document.removeEventListener('visibilitychange', this.handleVisibilityChange, false)
this.$store.commit('setLoading', { timeline: this.timelineName, value: false })
},
methods: {
handleShortKey (e) {
if (e.key === '.') this.showNewStatuses()
},
showNewStatuses () {
if (this.newStatusCount === 0) return
if (this.timeline.flushMarker !== 0) {
this.$store.commit('clearTimeline', { timeline: this.timelineName })
this.$store.commit('queueFlush', { timeline: this.timelineName, id: 0 })
this.fetchOlderStatuses()
} else {
this.$store.commit('showNewStatuses', { timeline: this.timelineName })
this.paused = false
}
},
fetchOlderStatuses: throttle(function () {
const store = this.$store
const credentials = store.state.users.currentUser.credentials
store.commit('setLoading', { timeline: this.timelineName, value: true })
timelineFetcher.fetchAndUpdate({
store,
credentials,
timeline: this.timelineName,
older: true,
showImmediately: true,
userId: this.userId,
tag: this.tag
}).then(statuses => {
store.commit('setLoading', { timeline: this.timelineName, value: false })
if (statuses && statuses.length === 0) {
this.bottomedOut = true
}
})
}, 1000, this),
scrollLoad (e) {
const bodyBRect = document.body.getBoundingClientRect()
const height = Math.max(bodyBRect.height, -(bodyBRect.y))
if (this.timeline.loading === false &&
this.$store.state.config.autoLoad &&
this.$el.offsetHeight > 0 &&
(window.innerHeight + window.pageYOffset) >= (height - 750)) {
this.fetchOlderStatuses()
}
},
handleVisibilityChange () {
this.unfocused = document.hidden
}
},
watch: {
newStatusCount (count) {
if (!this.$store.state.config.streaming) {
return
}
if (count > 0) {
// only 'stream' them when you're scrolled to the top
const doc = document.documentElement
const top = (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0)
if (top < 15 &&
!this.paused &&
!(this.unfocused && this.$store.state.config.pauseOnUnfocused)
) {
this.showNewStatuses()
} else {
this.paused = true
}
}
}
}
}
export default Timeline
diff --git a/src/components/timeline/timeline.vue b/src/components/timeline/timeline.vue
index 870fe4f591..e6a8d45819 100644
--- a/src/components/timeline/timeline.vue
+++ b/src/components/timeline/timeline.vue
@@ -1,69 +1,68 @@
<template>
<div :class="classes.root">
<div :class="classes.header">
<div class="title">
{{title}}
</div>
<div @click.prevent class="loadmore-error alert error" v-if="timelineError">
{{$t('timeline.error_fetching')}}
</div>
<button @click.prevent="showNewStatuses" class="loadmore-button" v-if="timeline.newStatusCount > 0 && !timelineError">
{{$t('timeline.show_new')}}{{newStatusCountStr}}
</button>
<div @click.prevent class="loadmore-text faint" v-if="!timeline.newStatusCount > 0 && !timelineError">
{{$t('timeline.up_to_date')}}
</div>
</div>
<div :class="classes.body">
<div class="timeline">
<conversation
v-for="status in timeline.visibleStatuses"
class="status-fadein"
:key="status.id"
:statusoid="status"
- :pinned="timelineName === 'pinned'"
:collapsable="true"
/>
</div>
</div>
- <div :class="classes.footer" v-if="!noLoadMore">
+ <div :class="classes.footer">
<div v-if="count===0" class="new-status-notification text-center panel-footer faint">
{{$t('timeline.no_statuses')}}
</div>
<div v-else-if="bottomedOut" class="new-status-notification text-center panel-footer faint">
{{$t('timeline.no_more_statuses')}}
</div>
<a v-else-if="!timeline.loading" href="#" v-on:click.prevent='fetchOlderStatuses()'>
<div class="new-status-notification text-center panel-footer">{{$t('timeline.load_older')}}</div>
</a>
<div v-else class="new-status-notification text-center panel-footer">
<i class="icon-spin3 animate-spin"/>
</div>
</div>
</div>
</template>
<script src="./timeline.js"></script>
<style lang="scss">
@import '../../_variables.scss';
.timeline {
.loadmore-text {
opacity: 1;
}
}
.new-status-notification {
position:relative;
margin-top: -1px;
font-size: 1.1em;
border-width: 1px 0 0 0;
border-style: solid;
border-color: var(--border, $fallback--border);
padding: 10px;
z-index: 1;
background-color: $fallback--fg;
background-color: var(--panel, $fallback--fg);
}
</style>
diff --git a/src/components/user_profile/user_profile.js b/src/components/user_profile/user_profile.js
index 71a88dcfbc..0071fa89e2 100644
--- a/src/components/user_profile/user_profile.js
+++ b/src/components/user_profile/user_profile.js
@@ -1,140 +1,141 @@
import get from 'lodash/get'
import UserCard from '../user_card/user_card.vue'
import FollowCard from '../follow_card/follow_card.vue'
import Timeline from '../timeline/timeline.vue'
+import Conversation from '../conversation/conversation.vue'
import ModerationTools from '../moderation_tools/moderation_tools.vue'
import List from '../list/list.vue'
import withLoadMore from '../../hocs/with_load_more/with_load_more'
const FollowerList = withLoadMore({
fetch: (props, $store) => $store.dispatch('fetchFollowers', props.userId),
select: (props, $store) => get($store.getters.findUser(props.userId), 'followerIds', []).map(id => $store.getters.findUser(id)),
destroy: (props, $store) => $store.dispatch('clearFollowers', props.userId),
childPropName: 'items',
additionalPropNames: ['userId']
})(List)
const FriendList = withLoadMore({
fetch: (props, $store) => $store.dispatch('fetchFriends', props.userId),
select: (props, $store) => get($store.getters.findUser(props.userId), 'friendIds', []).map(id => $store.getters.findUser(id)),
destroy: (props, $store) => $store.dispatch('clearFriends', props.userId),
childPropName: 'items',
additionalPropNames: ['userId']
})(List)
const UserProfile = {
data () {
return {
error: false,
userId: null
}
},
created () {
const routeParams = this.$route.params
this.load(routeParams.name || routeParams.id)
},
destroyed () {
this.cleanUp()
},
computed: {
timeline () {
return this.$store.state.statuses.timelines.user
},
- pinned () {
- return this.$store.state.statuses.timelines.pinned
- },
favorites () {
return this.$store.state.statuses.timelines.favorites
},
media () {
return this.$store.state.statuses.timelines.media
},
isUs () {
return this.userId && this.$store.state.users.currentUser.id &&
this.userId === this.$store.state.users.currentUser.id
},
user () {
return this.$store.getters.findUser(this.userId)
},
+ pinnedStatuses () {
+ return this.user.pinnedStatusIds.map(id => this.$store.state.statuses.allStatusesObject[id])
+ },
isExternal () {
return this.$route.name === 'external-user-profile'
},
followsTabVisible () {
return this.isUs || !this.user.hide_follows
},
followersTabVisible () {
return this.isUs || !this.user.hide_followers
}
},
methods: {
load (userNameOrId) {
// Check if user data is already loaded in store
const user = this.$store.getters.findUser(userNameOrId)
if (user) {
this.userId = user.id
this.fetchTimelines()
} else {
this.$store.dispatch('fetchUser', userNameOrId)
.then(({ id }) => {
this.userId = id
this.fetchTimelines()
})
.catch((reason) => {
const errorMessage = get(reason, 'error.error')
if (errorMessage === 'No user with such user_id') { // Known error
this.error = this.$t('user_profile.profile_does_not_exist')
} else if (errorMessage) {
this.error = errorMessage
} else {
this.error = this.$t('user_profile.profile_loading_error')
}
})
}
},
fetchTimelines () {
const userId = this.userId
this.$store.dispatch('startFetchingTimeline', { timeline: 'user', userId })
- this.$store.dispatch('startFetchingTimeline', { timeline: 'pinned', userId })
this.$store.dispatch('startFetchingTimeline', { timeline: 'media', userId })
if (this.isUs) {
this.$store.dispatch('startFetchingTimeline', { timeline: 'favorites', userId })
}
+ this.$store.dispatch('fetchPinnedStatuses', userId)
},
cleanUp () {
this.$store.dispatch('stopFetching', 'user')
- this.$store.dispatch('stopFetching', 'pinned')
this.$store.dispatch('stopFetching', 'favorites')
this.$store.dispatch('stopFetching', 'media')
this.$store.commit('clearTimeline', { timeline: 'user' })
this.$store.commit('clearTimeline', { timeline: 'favorites' })
this.$store.commit('clearTimeline', { timeline: 'media' })
}
},
watch: {
'$route.params.id': function (newVal) {
if (newVal) {
this.cleanUp()
this.load(newVal)
}
},
'$route.params.name': function (newVal) {
if (newVal) {
this.cleanUp()
this.load(newVal)
}
},
$route () {
this.$refs.tabSwitcher.activateTab(0)()
}
},
components: {
UserCard,
Timeline,
FollowerList,
FriendList,
ModerationTools,
- FollowCard
+ FollowCard,
+ Conversation
}
}
export default UserProfile
diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue
index caf297c6db..46561e2abc 100644
--- a/src/components/user_profile/user_profile.vue
+++ b/src/components/user_profile/user_profile.vue
@@ -1,113 +1,114 @@
<template>
<div>
<div v-if="user" class="user-profile panel panel-default">
<UserCard :user="user" :switcher="true" :selected="timeline.viewing" rounded="top"/>
<tab-switcher :renderOnlyFocused="true" ref="tabSwitcher">
<div :label="$t('user_card.statuses')" :disabled="!user.statuses_count">
- <Timeline
- :count="user.statuses_count"
- :embedded="true"
- :title="$t('user_profile.timeline_title')"
- :timeline="pinned"
- :timeline-name="'pinned'"
- :user-id="userId"
- :no-load-more="true"
- />
+ <div class="timeline">
+ <Conversation
+ v-for="status in pinnedStatuses"
+ class="status-fadein"
+ :key="status.id"
+ :statusoid="status"
+ :pinned="true"
+ :collapsable="true"
+ />
+ </div>
<Timeline
:count="user.statuses_count"
:embedded="true"
:title="$t('user_profile.timeline_title')"
:timeline="timeline"
:timeline-name="'user'"
:user-id="userId"
/>
</div>
<div :label="$t('user_card.followees')" v-if="followsTabVisible" :disabled="!user.friends_count">
<FriendList :userId="userId">
<template slot="item" slot-scope="{item}">
<FollowCard :user="item" />
</template>
</FriendList>
</div>
<div :label="$t('user_card.followers')" v-if="followersTabVisible" :disabled="!user.followers_count">
<FollowerList :userId="userId">
<template slot="item" slot-scope="{item}">
<FollowCard :user="item" :noFollowsYou="isUs" />
</template>
</FollowerList>
</div>
<Timeline
:label="$t('user_card.media')"
:disabled="!media.visibleStatuses.length"
:embedded="true" :title="$t('user_card.media')"
timeline-name="media"
:timeline="media"
:user-id="userId"
/>
<Timeline
v-if="isUs"
:label="$t('user_card.favorites')"
:disabled="!favorites.visibleStatuses.length"
:embedded="true"
:title="$t('user_card.favorites')"
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">
<span v-if="error">{{ error }}</span>
<i class="icon-spin3 animate-spin" v-else></i>
</div>
</div>
</div>
</template>
<script src="./user_profile.js"></script>
<style lang="scss">
.user-profile {
flex: 2;
flex-basis: 500px;
.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/modules/statuses.js b/src/modules/statuses.js
index b93f9cb562..3b3ae9e3d4 100644
--- a/src/modules/statuses.js
+++ b/src/modules/statuses.js
@@ -1,595 +1,601 @@
-import { remove, slice, each, findIndex, find, maxBy, minBy, merge, first, last, isArray, omitBy } from 'lodash'
+import { remove, slice, each, findIndex, find, maxBy, minBy, merge, first, last, isArray, omitBy, map } from 'lodash'
import { set } from 'vue'
import apiService from '../services/api/api.service.js'
// import parse from '../services/status_parser/status_parser.js'
const emptyTl = (userId = 0) => ({
statuses: [],
statusesObject: {},
faves: [],
visibleStatuses: [],
visibleStatusesObject: {},
newStatusCount: 0,
maxId: 0,
minId: 0,
minVisibleId: 0,
loading: false,
followers: [],
friends: [],
userId,
flushMarker: 0
})
const emptyNotifications = () => ({
desktopNotificationSilence: true,
maxId: 0,
minId: Number.POSITIVE_INFINITY,
data: [],
idStore: {},
loading: false,
error: false
})
export const defaultState = () => ({
allStatuses: [],
allStatusesObject: {},
conversationsObject: {},
maxId: 0,
notifications: emptyNotifications(),
favorites: new Set(),
error: false,
timelines: {
mentions: emptyTl(),
public: emptyTl(),
user: emptyTl(),
favorites: emptyTl(),
media: emptyTl(),
publicAndExternal: emptyTl(),
friends: emptyTl(),
tag: emptyTl(),
- dms: emptyTl(),
- pinned: emptyTl()
+ dms: emptyTl()
}
})
export const prepareStatus = (status) => {
// 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(_ => _)
}
const mergeOrAdd = (arr, obj, item) => {
const oldItem = obj[item.id]
if (oldItem) {
// We already have this, so only merge the new info.
// We ignore null values to avoid overwriting existing properties with missing data
// we also skip 'user' because that is handled by users module
merge(oldItem, omitBy(item, (v, k) => v === null || k === 'user'))
// 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)
set(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 = timeline.visibleStatuses.sort(sortById)
timeline.statuses = timeline.statuses.sort(sortById)
timeline.minVisibleId = (last(timeline.visibleStatuses) || {}).id
return timeline
}
// Add status to the global storages (arrays and objects maintaining statuses) except timelines
const addStatusToGlobalStorage = (state, data) => {
const result = mergeOrAdd(state.allStatuses, state.allStatusesObject, data)
if (result.new) {
// Add to conversation
const status = result.item
const conversationsObject = state.conversationsObject
const conversationId = status.statusnet_conversation_id
if (conversationsObject[conversationId]) {
conversationsObject[conversationId].push(status)
} else {
set(conversationsObject, conversationId, [status])
}
}
return result
}
// Remove status from the global storages (arrays and objects maintaining statuses) except timelines
const removeStatusFromGlobalStorage = (state, status) => {
remove(state.allStatuses, { id: status.id })
// TODO: Need to remove from allStatusesObject?
// Remove possible notification
remove(state.notifications.data, ({action: {id}}) => id === status.id)
// Remove from conversation
const conversationId = status.statusnet_conversation_id
if (state.conversationsObject[conversationId]) {
remove(state.conversationsObject[conversationId], { id: status.id })
}
}
const addNewStatuses = (state, { statuses, showImmediately = false, timeline, user = {}, noIdUpdate = false, userId }) => {
// Sanity check
if (!isArray(statuses)) {
return false
}
const allStatuses = state.allStatuses
const timelineObject = state.timelines[timeline]
const maxNew = statuses.length > 0 ? maxBy(statuses, 'id').id : 0
const minNew = statuses.length > 0 ? minBy(statuses, 'id').id : 0
const newer = timeline && (maxNew > timelineObject.maxId || timelineObject.maxId === 0) && statuses.length > 0
const older = timeline && (minNew < timelineObject.minId || timelineObject.minId === 0) && statuses.length > 0
if (!noIdUpdate && newer) {
timelineObject.maxId = maxNew
}
if (!noIdUpdate && older) {
timelineObject.minId = minNew
}
// 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' || timeline === 'media' || timeline === 'pinned') && timelineObject.userId !== userId) {
+ if ((timeline === 'user' || timeline === 'media') && timelineObject.userId !== userId) {
return
}
const addStatus = (data, showImmediately, addToTimeline = true) => {
const result = addStatusToGlobalStorage(state, data)
const status = result.item
if (result.new) {
// We are mentioned in a post
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: 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
const status = find(allStatuses, {uri})
if (!status) {
return
}
removeStatusFromGlobalStorage(state, status)
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 = status.type
const processor = processors[type] || processors['default']
processor(status)
})
// Keep the visible statuses sorted
if (timeline) {
sortTimeline(timelineObject)
}
}
const addNewNotifications = (state, { dispatch, notifications, older, visibleNotificationTypes, rootGetters }) => {
each(notifications, (notification) => {
if (notification.type !== 'follow') {
notification.action = addStatusToGlobalStorage(state, notification.action).item
notification.status = notification.status && addStatusToGlobalStorage(state, notification.status).item
}
// Only add a new notification if we don't have one for the same action
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(notification)
state.notifications.idStore[notification.id] = notification
if ('Notification' in window && window.Notification.permission === 'granted') {
const notifObj = {}
const status = notification.status
const title = notification.from_profile.name
notifObj.icon = notification.from_profile.profile_image_url
let i18nString
switch (notification.type) {
case 'like':
i18nString = 'favorited_you'
break
case 'repeat':
i18nString = 'repeated_you'
break
case 'follow':
i18nString = 'followed_you'
break
}
if (i18nString) {
notifObj.body = rootGetters.i18n.t('notifications.' + i18nString)
} else {
notifObj.body = notification.status.text
}
// Shows first attached non-nsfw image, if any. Should add configuration for this somehow...
if (status && status.attachments && status.attachments.length > 0 && !status.nsfw &&
status.attachments[0].mimetype.startsWith('image/')) {
notifObj.image = status.attachments[0].url
}
if (!notification.seen && !state.notifications.desktopNotificationSilence && visibleNotificationTypes.includes(notification.type)) {
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)
}
}
} else if (notification.seen) {
state.notifications.idStore[notification.id].seen = true
}
})
}
const removeStatus = (state, { timeline, userId, statusId }) => {
const timelineObject = state.timelines[timeline]
const status = userId ? { user: { id: userId } } : { id: statusId }
remove(timelineObject.statuses, status)
remove(timelineObject.visibleStatuses, status)
if (statusId) {
delete timelineObject.statusesObject[statusId]
delete timelineObject.visibleStatusesObject[statusId]
sortTimeline(timelineObject)
}
timelineObject.minVisibleId = timelineObject.visibleStatuses.length > 0 ? last(timelineObject.visibleStatuses).id : 0
timelineObject.maxId = timelineObject.statuses.length > 0 ? first(timelineObject.statuses).id : 0
}
export const mutations = {
addNewStatuses,
addNewNotifications,
removeStatus,
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.minId = oldTimeline.minVisibleId
oldTimeline.visibleStatusesObject = {}
each(oldTimeline.visibleStatuses, (status) => { oldTimeline.visibleStatusesObject[status.id] = status })
},
resetStatuses (state) {
const emptyState = defaultState()
Object.entries(emptyState).forEach(([key, value]) => {
state[key] = value
})
},
clearTimeline (state, { timeline }) {
state.timelines[timeline] = emptyTl(state.timelines[timeline].userId)
},
clearNotifications (state) {
state.notifications = emptyNotifications()
},
setFavorited (state, { status, value }) {
const newStatus = state.allStatusesObject[status.id]
if (newStatus.favorited !== value) {
if (value) {
newStatus.fave_num++
} else {
newStatus.fave_num--
}
}
newStatus.favorited = value
},
setFavoritedConfirm (state, { status, user }) {
const newStatus = state.allStatusesObject[status.id]
newStatus.favorited = status.favorited
newStatus.fave_num = status.fave_num
const index = findIndex(newStatus.favoritedBy, { id: user.id })
if (index !== -1 && !newStatus.favorited) {
newStatus.favoritedBy.splice(index, 1)
} else if (index === -1 && newStatus.favorited) {
newStatus.favoritedBy.push(user)
}
},
setPinned (state, { status }) {
const newStatus = state.allStatusesObject[status.id]
newStatus.pinned = status.pinned
},
setRetweeted (state, { status, value }) {
const newStatus = state.allStatusesObject[status.id]
if (newStatus.repeated !== value) {
if (value) {
newStatus.repeat_num++
} else {
newStatus.repeat_num--
}
}
newStatus.repeated = value
},
setRetweetedConfirm (state, { status, user }) {
const newStatus = state.allStatusesObject[status.id]
newStatus.repeated = status.repeated
newStatus.repeat_num = status.repeat_num
const index = findIndex(newStatus.rebloggedBy, { id: user.id })
if (index !== -1 && !newStatus.repeated) {
newStatus.rebloggedBy.splice(index, 1)
} else if (index === -1 && newStatus.repeated) {
newStatus.rebloggedBy.push(user)
}
},
setDeleted (state, { status }) {
const newStatus = state.allStatusesObject[status.id]
newStatus.deleted = true
},
setManyDeleted (state, condition) {
Object.values(state.allStatusesObject).forEach(status => {
if (condition(status)) {
status.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
},
setNotificationsLoading (state, { value }) {
state.notifications.loading = 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
},
addFavsAndRepeats (state, { id, favoritedByUsers, rebloggedByUsers }) {
const newStatus = state.allStatusesObject[id]
newStatus.favoritedBy = favoritedByUsers.filter(_ => _)
newStatus.rebloggedBy = rebloggedByUsers.filter(_ => _)
}
}
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, rootGetters }, { notifications, older }) {
commit('addNewNotifications', { visibleNotificationTypes: visibleNotificationTypes(rootState), dispatch, notifications, older, rootGetters })
},
setError ({ rootState, commit }, { value }) {
commit('setError', { value })
},
setNotificationsLoading ({ rootState, commit }, { value }) {
commit('setNotificationsLoading', { 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 })
},
markStatusesAsDeleted ({ commit }, condition) {
commit('setManyDeleted', condition)
},
favorite ({ rootState, commit }, status) {
// Optimistic favoriting...
commit('setFavorited', { status, value: true })
rootState.api.backendInteractor.favorite(status.id)
.then(status => commit('setFavoritedConfirm', { status, user: rootState.users.currentUser }))
},
unfavorite ({ rootState, commit }, status) {
// Optimistic unfavoriting...
commit('setFavorited', { status, value: false })
rootState.api.backendInteractor.unfavorite(status.id)
.then(status => commit('setFavoritedConfirm', { status, user: rootState.users.currentUser }))
},
+ fetchPinnedStatuses ({ rootState, dispatch, commit }, userId) {
+ rootState.api.backendInteractor.fetchPinnedStatuses(userId)
+ .then(statuses => {
+ dispatch('addNewStatuses', { statuses })
+ commit('savePinnedStatusIds', { userId, statusIds: map(statuses, 'id') })
+ })
+ },
updatePinned ({ rootState, commit }, status) {
commit('setPinned', { status })
if (status.pinned) {
const statusObj = rootState.statuses.allStatusesObject[status.id]
const user = rootState.users.currentUser
commit('addNewStatuses', {
statuses: [statusObj],
showImmediately: true,
timeline: 'pinned',
user,
userId: user.id
})
} else {
commit('removeStatus', { timeline: 'pinned', statusId: status.id })
}
},
retweet ({ rootState, commit }, status) {
// Optimistic retweeting...
commit('setRetweeted', { status, value: true })
rootState.api.backendInteractor.retweet(status.id)
.then(status => commit('setRetweetedConfirm', { status: status.retweeted_status, user: rootState.users.currentUser }))
},
unretweet ({ rootState, commit }, status) {
// Optimistic unretweeting...
commit('setRetweeted', { status, value: false })
rootState.api.backendInteractor.unretweet(status.id)
.then(status => commit('setRetweetedConfirm', { status, user: rootState.users.currentUser }))
},
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
})
},
fetchFavsAndRepeats ({ rootState, commit }, id) {
Promise.all([
rootState.api.backendInteractor.fetchFavoritedByUsers(id),
rootState.api.backendInteractor.fetchRebloggedByUsers(id)
]).then(([favoritedByUsers, rebloggedByUsers]) =>
commit('addFavsAndRepeats', { id, favoritedByUsers, rebloggedByUsers })
)
}
},
mutations
}
export default statuses
diff --git a/src/modules/users.js b/src/modules/users.js
index adcab2334a..def2b38c9d 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -1,464 +1,467 @@
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
import userSearchApi from '../services/new_api/user_search.js'
import { compact, map, each, merge, last, concat, uniq } 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)
set(obj, item.id, item)
if (item.screen_name && !item.screen_name.includes('@')) {
set(obj, item.screen_name.toLowerCase(), 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)
}
const blockUser = (store, id) => {
return store.rootState.api.backendInteractor.blockUser(id)
.then((relationship) => {
store.commit('updateUserRelationship', [relationship])
store.commit('addBlockId', id)
store.commit('removeStatus', { timeline: 'friends', userId: id })
store.commit('removeStatus', { timeline: 'public', userId: id })
store.commit('removeStatus', { timeline: 'publicAndExternal', userId: id })
})
}
const unblockUser = (store, id) => {
return store.rootState.api.backendInteractor.unblockUser(id)
.then((relationship) => store.commit('updateUserRelationship', [relationship]))
}
const muteUser = (store, id) => {
return store.rootState.api.backendInteractor.muteUser(id)
.then((relationship) => {
store.commit('updateUserRelationship', [relationship])
store.commit('addMuteId', id)
})
}
const unmuteUser = (store, id) => {
return store.rootState.api.backendInteractor.unmuteUser(id)
.then((relationship) => store.commit('updateUserRelationship', [relationship]))
}
export const mutations = {
setMuted (state, { user: { id }, muted }) {
const user = state.usersObject[id]
set(user, 'muted', muted)
},
tagUser (state, { user: { id }, tag }) {
const user = state.usersObject[id]
const tags = user.tags || []
const newTags = tags.concat([tag])
set(user, 'tags', newTags)
},
untagUser (state, { user: { id }, tag }) {
const user = state.usersObject[id]
const tags = user.tags || []
const newTags = tags.filter(t => t !== tag)
set(user, 'tags', newTags)
},
updateRight (state, { user: { id }, right, value }) {
const user = state.usersObject[id]
let newRights = user.rights
newRights[right] = value
set(user, 'rights', newRights)
},
updateActivationStatus (state, { user: { id }, status }) {
const user = state.usersObject[id]
set(user, 'deactivated', !status)
},
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
},
saveFriendIds (state, { id, friendIds }) {
const user = state.usersObject[id]
user.friendIds = uniq(concat(user.friendIds, friendIds))
},
saveFollowerIds (state, { id, followerIds }) {
const user = state.usersObject[id]
user.followerIds = uniq(concat(user.followerIds, followerIds))
},
// Because frontend doesn't have a reason to keep these stuff in memory
// outside of viewing someones user profile.
clearFriends (state, userId) {
const user = state.usersObject[userId]
if (user) {
set(user, 'friendIds', [])
}
},
clearFollowers (state, userId) {
const user = state.usersObject[userId]
if (user) {
set(user, 'followerIds', [])
}
},
addNewUsers (state, users) {
each(users, (user) => mergeOrAdd(state.users, state.usersObject, user))
},
updateUserRelationship (state, relationships) {
relationships.forEach((relationship) => {
const user = state.usersObject[relationship.id]
if (user) {
user.follows_you = relationship.followed_by
user.following = relationship.following
user.muted = relationship.muting
user.statusnet_blocking = relationship.blocking
}
})
},
updateBlocks (state, blockedUsers) {
// Reset statusnet_blocking of all fetched users
each(state.users, (user) => { user.statusnet_blocking = false })
each(blockedUsers, (user) => mergeOrAdd(state.users, state.usersObject, user))
},
saveBlockIds (state, blockIds) {
state.currentUser.blockIds = blockIds
},
addBlockId (state, blockId) {
if (state.currentUser.blockIds.indexOf(blockId) === -1) {
state.currentUser.blockIds.push(blockId)
}
},
updateMutes (state, mutedUsers) {
// Reset muted of all fetched users
each(state.users, (user) => { user.muted = false })
each(mutedUsers, (user) => mergeOrAdd(state.users, state.usersObject, user))
},
saveMuteIds (state, muteIds) {
state.currentUser.muteIds = muteIds
},
+ savePinnedStatusIds (state, { userId, statusIds }) {
+ state.usersObject[userId].pinnedStatusIds = statusIds
+ },
addMuteId (state, muteId) {
if (state.currentUser.muteIds.indexOf(muteId) === -1) {
state.currentUser.muteIds.push(muteId)
}
},
setUserForStatus (state, status) {
status.user = state.usersObject[status.user.id]
},
setUserForNotification (state, notification) {
if (notification.type !== 'follow') {
notification.action.user = state.usersObject[notification.action.user.id]
}
notification.from_profile = state.usersObject[notification.from_profile.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 = {
findUser: state => query => {
const result = state.usersObject[query]
// In case it's a screen_name, we can try searching case-insensitive
if (!result && typeof query === 'string') {
return state.usersObject[query.toLowerCase()]
}
return result
}
}
export const defaultState = {
loggingIn: false,
lastLoginName: false,
currentUser: false,
users: [],
usersObject: {},
signUpPending: false,
signUpErrors: []
}
const users = {
state: defaultState,
mutations,
getters,
actions: {
fetchUser (store, id) {
return store.rootState.api.backendInteractor.fetchUser({ id })
.then((user) => {
store.commit('addNewUsers', [user])
return user
})
},
fetchUserRelationship (store, id) {
if (store.state.currentUser) {
store.rootState.api.backendInteractor.fetchUserRelationship({ id })
.then((relationships) => store.commit('updateUserRelationship', relationships))
}
},
fetchBlocks (store) {
return store.rootState.api.backendInteractor.fetchBlocks()
.then((blocks) => {
store.commit('saveBlockIds', map(blocks, 'id'))
store.commit('updateBlocks', blocks)
return blocks
})
},
blockUser (store, id) {
return blockUser(store, id)
},
unblockUser (store, id) {
return unblockUser(store, id)
},
blockUsers (store, ids = []) {
return Promise.all(ids.map(id => blockUser(store, id)))
},
unblockUsers (store, ids = []) {
return Promise.all(ids.map(id => unblockUser(store, id)))
},
fetchMutes (store) {
return store.rootState.api.backendInteractor.fetchMutes()
.then((mutes) => {
store.commit('updateMutes', mutes)
store.commit('saveMuteIds', map(mutes, 'id'))
return mutes
})
},
muteUser (store, id) {
return muteUser(store, id)
},
unmuteUser (store, id) {
return unmuteUser(store, id)
},
muteUsers (store, ids = []) {
return Promise.all(ids.map(id => muteUser(store, id)))
},
unmuteUsers (store, ids = []) {
return Promise.all(ids.map(id => unmuteUser(store, id)))
},
fetchFriends ({ rootState, commit }, id) {
const user = rootState.users.usersObject[id]
const maxId = last(user.friendIds)
return rootState.api.backendInteractor.fetchFriends({ id, maxId })
.then((friends) => {
commit('addNewUsers', friends)
commit('saveFriendIds', { id, friendIds: map(friends, 'id') })
return friends
})
},
fetchFollowers ({ rootState, commit }, id) {
const user = rootState.users.usersObject[id]
const maxId = last(user.followerIds)
return rootState.api.backendInteractor.fetchFollowers({ id, maxId })
.then((followers) => {
commit('addNewUsers', followers)
commit('saveFollowerIds', { id, followerIds: map(followers, 'id') })
return followers
})
},
clearFriends ({ commit }, userId) {
commit('clearFriends', userId)
},
clearFollowers ({ commit }, userId) {
commit('clearFollowers', userId)
},
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)
},
addNewUsers ({ commit }, users) {
commit('addNewUsers', users)
},
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 = 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)
})
},
searchUsers (store, query) {
// TODO: Move userSearch api into api.service
return userSearchApi.search({query, store: { state: store.rootState }})
.then((users) => {
store.commit('addNewUsers', users)
return users
})
},
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 {
const data = await response.json()
let errors = JSON.parse(data.error)
// replace ap_id with username
if (errors.ap_id) {
errors.username = errors.ap_id
delete errors.ap_id
}
errors = humanizeErrors(errors)
store.commit('signUpFailure', errors)
throw Error(errors)
}
},
async getCaptcha (store) {
return await store.rootState.api.backendInteractor.getCaptcha()
},
logout (store) {
store.commit('clearCurrentUser')
store.dispatch('disconnectFromChat')
store.commit('setToken', false)
store.dispatch('stopFetching', 'friends')
store.commit('setBackendInteractor', backendInteractorService())
store.dispatch('stopFetching', 'notifications')
store.commit('clearNotifications')
store.commit('resetStatuses')
},
loginUser (store, accessToken) {
return new Promise((resolve, reject) => {
const commit = store.commit
commit('beginLogin')
store.rootState.api.backendInteractor.verifyCredentials(accessToken)
.then((data) => {
if (!data.error) {
const user = data
// user.credentials = userCredentials
user.credentials = accessToken
user.blockIds = []
user.muteIds = []
commit('setCurrentUser', user)
commit('addNewUsers', [user])
getNotificationPermission()
.then(permission => commit('setNotificationPermission', permission))
// Set our new backend interactor
commit('setBackendInteractor', backendInteractorService(accessToken))
if (user.token) {
store.dispatch('setWsToken', user.token)
// Initialize the chat socket.
store.dispatch('initializeSocket')
}
// Start getting fresh posts.
store.dispatch('startFetchingTimeline', { timeline: 'friends' })
// Start fetching notifications
store.dispatch('startFetchingNotifications')
// Get user mutes
store.dispatch('fetchMutes')
// 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')
})
})
}
}
}
export default users
diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js
index 816e73c1cc..7a302f577b 100644
--- a/src/services/backend_interactor_service/backend_interactor_service.js
+++ b/src/services/backend_interactor_service/backend_interactor_service.js
@@ -1,189 +1,191 @@
import apiService from '../api/api.service.js'
import timelineFetcherService from '../timeline_fetcher/timeline_fetcher.service.js'
import notificationsFetcher from '../notifications_fetcher/notifications_fetcher.service.js'
const backendInteractorService = (credentials) => {
const fetchStatus = ({id}) => {
return apiService.fetchStatus({id, credentials})
}
const fetchConversation = ({id}) => {
return apiService.fetchConversation({id, credentials})
}
const fetchFriends = ({id, maxId, sinceId, limit}) => {
return apiService.fetchFriends({id, maxId, sinceId, limit, credentials})
}
const exportFriends = ({id}) => {
return apiService.exportFriends({id, credentials})
}
const fetchFollowers = ({id, maxId, sinceId, limit}) => {
return apiService.fetchFollowers({id, maxId, sinceId, limit, credentials})
}
const fetchAllFollowing = ({username}) => {
return apiService.fetchAllFollowing({username, credentials})
}
const fetchUser = ({id}) => {
return apiService.fetchUser({id, credentials})
}
const fetchUserRelationship = ({id}) => {
return apiService.fetchUserRelationship({id, credentials})
}
const followUser = (id) => {
return apiService.followUser({credentials, id})
}
const unfollowUser = (id) => {
return apiService.unfollowUser({credentials, id})
}
const pinOwnStatus = (id) => {
return apiService.pinOwnStatus({credentials, id})
}
const unpinOwnStatus = (id) => {
return apiService.unpinOwnStatus({ credentials, id })
}
const blockUser = (id) => {
return apiService.blockUser({credentials, id})
}
const unblockUser = (id) => {
return apiService.unblockUser({credentials, id})
}
const approveUser = (id) => {
return apiService.approveUser({credentials, id})
}
const denyUser = (id) => {
return apiService.denyUser({credentials, id})
}
const startFetchingTimeline = ({ timeline, store, userId = false, tag }) => {
return timelineFetcherService.startFetching({ timeline, store, credentials, userId, tag })
}
const startFetchingNotifications = ({ store }) => {
return notificationsFetcher.startFetching({ store, credentials })
}
const tagUser = ({screen_name}, tag) => {
return apiService.tagUser({screen_name, tag, credentials})
}
const untagUser = ({screen_name}, tag) => {
return apiService.untagUser({screen_name, tag, credentials})
}
const addRight = ({screen_name}, right) => {
return apiService.addRight({screen_name, right, credentials})
}
const deleteRight = ({screen_name}, right) => {
return apiService.deleteRight({screen_name, right, credentials})
}
const setActivationStatus = ({screen_name}, status) => {
return apiService.setActivationStatus({screen_name, status, credentials})
}
const deleteUser = ({screen_name}) => {
return apiService.deleteUser({screen_name, credentials})
}
const fetchMutes = () => apiService.fetchMutes({credentials})
const muteUser = (id) => apiService.muteUser({credentials, id})
const unmuteUser = (id) => apiService.unmuteUser({credentials, id})
const fetchBlocks = () => apiService.fetchBlocks({credentials})
const fetchFollowRequests = () => apiService.fetchFollowRequests({credentials})
const fetchOAuthTokens = () => apiService.fetchOAuthTokens({credentials})
const revokeOAuthToken = (id) => apiService.revokeOAuthToken({id, credentials})
+ const fetchPinnedStatuses = (id) => apiService.fetchPinnedStatuses({ id, credentials })
const getCaptcha = () => apiService.getCaptcha()
const register = (params) => apiService.register(params)
const updateAvatar = ({avatar}) => apiService.updateAvatar({credentials, avatar})
const updateBg = ({params}) => apiService.updateBg({credentials, params})
const updateBanner = ({banner}) => apiService.updateBanner({credentials, banner})
const updateProfile = ({params}) => apiService.updateProfile({credentials, params})
const externalProfile = (profileUrl) => apiService.externalProfile({profileUrl, credentials})
const importBlocks = (file) => apiService.importBlocks({file, credentials})
const importFollows = (file) => apiService.importFollows({file, credentials})
const deleteAccount = ({password}) => apiService.deleteAccount({credentials, password})
const changePassword = ({password, newPassword, newPasswordConfirmation}) => apiService.changePassword({credentials, password, newPassword, newPasswordConfirmation})
const fetchFavoritedByUsers = (id) => apiService.fetchFavoritedByUsers({id})
const fetchRebloggedByUsers = (id) => apiService.fetchRebloggedByUsers({id})
const reportUser = (params) => apiService.reportUser({credentials, ...params})
const favorite = (id) => apiService.favorite({id, credentials})
const unfavorite = (id) => apiService.unfavorite({id, credentials})
const retweet = (id) => apiService.retweet({id, credentials})
const unretweet = (id) => apiService.unretweet({id, credentials})
const backendInteractorServiceInstance = {
fetchStatus,
fetchConversation,
fetchFriends,
exportFriends,
fetchFollowers,
followUser,
unfollowUser,
pinOwnStatus,
unpinOwnStatus,
blockUser,
unblockUser,
fetchUser,
fetchUserRelationship,
fetchAllFollowing,
verifyCredentials: apiService.verifyCredentials,
startFetchingTimeline,
startFetchingNotifications,
fetchMutes,
muteUser,
unmuteUser,
fetchBlocks,
fetchOAuthTokens,
revokeOAuthToken,
+ fetchPinnedStatuses,
tagUser,
untagUser,
addRight,
deleteRight,
deleteUser,
setActivationStatus,
register,
getCaptcha,
updateAvatar,
updateBg,
updateBanner,
updateProfile,
externalProfile,
importBlocks,
importFollows,
deleteAccount,
changePassword,
fetchFollowRequests,
approveUser,
denyUser,
fetchFavoritedByUsers,
fetchRebloggedByUsers,
reportUser,
favorite,
unfavorite,
retweet,
unretweet
}
return backendInteractorServiceInstance
}
export default backendInteractorService
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index 97e20b997e..3636c05b7d 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -1,321 +1,323 @@
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 = data.display_name
output.name_html = addEmojis(data.display_name, data.emojis)
// output.description = ??? missing
output.description_html = addEmojis(data.note, data.emojis)
// 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 relationship = data.pleroma.relationship
if (relationship) {
output.follows_you = relationship.followed_by
output.following = relationship.following
output.statusnet_blocking = relationship.blocking
output.muted = relationship.muting
}
output.rights = {
moderator: data.pleroma.is_moderator,
admin: data.pleroma.is_admin
}
}
// TODO: handle is_local
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 = ??? missing
output.statusnet_profile_url = data.statusnet_profile_url
output.statusnet_blocking = data.statusnet_blocking
output.is_local = data.is_local
output.role = data.role
output.show_role = data.show_role
output.follows_you = data.follows_you
output.muted = data.muted
// QVITTER ONLY FOR NOW
// Really only applies to logged in user, really.. I THINK
if (data.rights) {
output.rights = {
moderator: data.rights.delete_others_notice,
admin: data.rights.admin
}
}
output.no_rich_text = data.no_rich_text
output.default_scope = data.default_scope
output.hide_follows = data.hide_follows
output.hide_followers = data.hide_followers
output.background_image = data.background_image
// on mastoapi this info is contained in a "relationship"
output.following = data.following
// Websocket token
output.token = data.token
}
output.created_at = new Date(data.created_at)
output.locked = data.locked
output.followers_count = data.followers_count
output.statuses_count = data.statuses_count
output.friendIds = []
output.followerIds = []
+ output.pinnedStatusIds = []
+
if (data.pleroma) {
output.follow_request_count = data.pleroma.follow_request_count
}
if (data.pleroma) {
output.tags = data.pleroma.tags
output.deactivated = data.pleroma.deactivated
}
output.tags = output.tags || []
return output
}
export const parseAttachment = (data) => {
const output = {}
const masto = !data.hasOwnProperty('oembed')
if (masto) {
// Not exactly same...
output.mimetype = data.pleroma ? data.pleroma.mime_type : data.type
output.meta = data.meta // not present in BE yet
output.id = data.id
} else {
output.mimetype = data.mimetype
// output.meta = ??? missing
}
output.url = data.url
output.description = data.description
return output
}
export const addEmojis = (string, emojis) => {
return emojis.reduce((acc, emoji) => {
return acc.replace(
new RegExp(`:${emoji.shortcode}:`, 'g'),
`<img src='${emoji.url}' alt='${emoji.shortcode}' class='emoji' />`
)
}, string)
}
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 = addEmojis(data.content, data.emojis)
if (data.pleroma) {
const { pleroma } = data
output.text = pleroma.content ? data.pleroma.content['text/plain'] : data.content
output.summary = pleroma.spoiler_text ? data.pleroma.spoiler_text['text/plain'] : data.spoiler_text
output.statusnet_conversation_id = data.pleroma.conversation_id
output.is_local = pleroma.local
output.in_reply_to_screen_name = data.pleroma.in_reply_to_account_acct
} else {
output.text = data.content
output.summary = data.spoiler_text
}
output.in_reply_to_status_id = data.in_reply_to_id
output.in_reply_to_user_id = data.in_reply_to_account_id
output.replies_count = data.replies_count
if (output.type === 'retweet') {
output.retweeted_status = parseStatus(data.reblog)
}
output.summary_html = addEmojis(data.spoiler_text, data.emojis)
output.external_url = data.url
output.pinned = data.pinned
} 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.card = data.card
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)
}
output.favoritedBy = []
output.rebloggedBy = []
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 = data.pleroma.is_seen
output.status = output.type === 'follow'
? null
: parseStatus(data.status)
output.action = output.status // TODO: Refactor, this is unneeded
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 = parseInt(data.id)
return output
}
const isNsfw = (status) => {
const nsfwRegex = /#nsfw/i
return (status.tags || []).includes('nsfw') || !!(status.text || '').match(nsfwRegex)
}

File Metadata

Mime Type
text/x-diff
Expires
Sat, Aug 8, 10:10 PM (1 d, 19 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1722615
Default Alt Text
(69 KB)

Event Timeline