Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85629768
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
21 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js
index 10ad4561fa..660a87527c 100644
--- a/src/components/timeline/timeline.js
+++ b/src/components/timeline/timeline.js
@@ -1,134 +1,134 @@
import Status from '../status/status.vue'
import timelineFetcher from '../../services/timeline_fetcher/timeline_fetcher.service.js'
import StatusOrConversation from '../status_or_conversation/status_or_conversation.vue'
import UserCard from '../user_card/user_card.vue'
const Timeline = {
props: [
'timeline',
'timelineName',
'title',
'userId',
'tag'
],
data () {
return {
paused: false
}
},
computed: {
timelineError () { return this.$store.state.statuses.error },
followers () {
return this.timeline.followers
},
friends () {
return this.timeline.friends
},
viewing () {
return this.timeline.viewing
},
newStatusCount () {
return this.timeline.newStatusCount
},
newStatusCountStr () {
- if (this.timeline.flushMarker > 0) {
+ if (this.timeline.flushMarker !== 0) {
return ''
} else {
return ` (${this.newStatusCount})`
}
}
},
components: {
Status,
StatusOrConversation,
UserCard
},
created () {
const store = this.$store
const credentials = store.state.users.currentUser.credentials
const showImmediately = this.timeline.visibleStatuses.length === 0
window.addEventListener('scroll', this.scrollLoad)
timelineFetcher.fetchAndUpdate({
store,
credentials,
timeline: this.timelineName,
showImmediately,
userId: this.userId,
tag: this.tag
})
// don't fetch followers for public, friend, twkn
if (this.timelineName === 'user') {
this.fetchFriends()
this.fetchFollowers()
}
},
destroyed () {
window.removeEventListener('scroll', this.scrollLoad)
this.$store.commit('setLoading', { timeline: this.timelineName, value: false })
},
methods: {
showNewStatuses () {
- if (this.timeline.flushMarker) {
+ 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 () {
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(() => store.commit('setLoading', { timeline: this.timelineName, value: false }))
},
fetchFollowers () {
const id = this.userId
this.$store.state.api.backendInteractor.fetchFollowers({ id })
.then((followers) => this.$store.dispatch('addFollowers', { followers }))
},
fetchFriends () {
const id = this.userId
this.$store.state.api.backendInteractor.fetchFriends({ id })
.then((friends) => this.$store.dispatch('addFriends', { friends }))
},
scrollLoad (e) {
let height = Math.max(document.body.offsetHeight, document.body.scrollHeight)
if (this.timeline.loading === false &&
this.$store.state.config.autoLoad &&
this.$el.offsetHeight > 0 &&
(window.innerHeight + window.pageYOffset) >= (height - 750)) {
this.fetchOlderStatuses()
}
}
},
watch: {
newStatusCount (count) {
if (!this.$store.state.config.streaming) {
return
}
if (count > 0) {
// only 'stream' them when you're scrolled to the top
if (window.pageYOffset < 15 && !this.paused) {
this.showNewStatuses()
} else {
this.paused = true
}
}
}
}
}
export default Timeline
diff --git a/src/modules/statuses.js b/src/modules/statuses.js
index 82a7eda176..181914246e 100644
--- a/src/modules/statuses.js
+++ b/src/modules/statuses.js
@@ -1,473 +1,479 @@
import { includes, remove, slice, sortBy, toInteger, each, find, flatten, maxBy, last, merge, max, isArray } from 'lodash'
import apiService from '../services/api/api.service.js'
// import parse from '../services/status_parser/status_parser.js'
export const defaultState = {
allStatuses: [],
allStatusesObject: {},
maxId: 0,
notifications: [],
favorites: new Set(),
error: false,
- flushMarker: 0,
timelines: {
mentions: {
statuses: [],
statusesObject: {},
faves: [],
visibleStatuses: [],
visibleStatusesObject: {},
newStatusCount: 0,
maxId: 0,
minVisibleId: 0,
loading: false,
followers: [],
friends: [],
- viewing: 'statuses'
+ viewing: 'statuses',
+ flushMarker: 0
},
public: {
statuses: [],
statusesObject: {},
faves: [],
visibleStatuses: [],
visibleStatusesObject: {},
newStatusCount: 0,
maxId: 0,
minVisibleId: 0,
loading: false,
followers: [],
friends: [],
- viewing: 'statuses'
+ viewing: 'statuses',
+ flushMarker: 0
},
user: {
statuses: [],
statusesObject: {},
faves: [],
visibleStatuses: [],
visibleStatusesObject: {},
newStatusCount: 0,
maxId: 0,
minVisibleId: 0,
loading: false,
followers: [],
friends: [],
- viewing: 'statuses'
+ viewing: 'statuses',
+ flushMarker: 0
},
publicAndExternal: {
statuses: [],
statusesObject: {},
faves: [],
visibleStatuses: [],
visibleStatusesObject: {},
newStatusCount: 0,
maxId: 0,
minVisibleId: 0,
loading: false,
followers: [],
friends: [],
- viewing: 'statuses'
+ viewing: 'statuses',
+ flushMarker: 0
},
friends: {
statuses: [],
statusesObject: {},
faves: [],
visibleStatuses: [],
visibleStatusesObject: {},
newStatusCount: 0,
maxId: 0,
minVisibleId: 0,
loading: false,
followers: [],
friends: [],
- viewing: 'statuses'
+ viewing: 'statuses',
+ flushMarker: 0
},
tag: {
statuses: [],
statusesObject: {},
faves: [],
visibleStatuses: [],
visibleStatusesObject: {},
newStatusCount: 0,
maxId: 0,
minVisibleId: 0,
loading: false,
followers: [],
friends: [],
- viewing: 'statuses'
+ viewing: 'statuses',
+ flushMarker: 0
}
}
}
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)
}
// Set deleted flag
status.deleted = false
// To make the array reactive
status.attachments = status.attachments || []
return status
}
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'
}
// TODO change to status.activity_type === 'follow' when gs supports it
if (status.text.match(/started following/)) {
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 sortTimeline = (timeline) => {
timeline.visibleStatuses = sortBy(timeline.visibleStatuses, ({id}) => -id)
timeline.statuses = sortBy(timeline.statuses, ({id}) => -id)
timeline.minVisibleId = (last(timeline.visibleStatuses) || {}).id
return timeline
}
const addNewStatuses = (state, { statuses, showImmediately = false, timeline, user = {}, noIdUpdate = false }) => {
// Sanity check
if (!isArray(statuses)) {
return false
}
const allStatuses = state.allStatuses
const allStatusesObject = state.allStatusesObject
const timelineObject = state.timelines[timeline]
// Set the maxId to the new id if it's larger.
const updateMaxId = ({id}) => {
if (!timeline || noIdUpdate) { return false }
timelineObject.maxId = max([id, timelineObject.maxId])
}
const addStatus = (status, showImmediately, addToTimeline = true) => {
const result = mergeOrAdd(allStatuses, allStatusesObject, status)
status = result.item
if (result.new) {
updateMaxId(status)
if (statusType(status) === 'retweet' && status.retweeted_status.user.id === user.id) {
addNotification({ type: 'repeat', status: status.retweeted_status, action: status })
}
// We are mentioned in a post
if (statusType(status) === '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)
}
// Don't add notification for self-mention
if (status.user.id !== user.id) {
addNotification({ type: 'mention', status, action: status })
}
}
}
// 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 addNotification = ({type, status, action}) => {
// Only add a new notification if we don't have one for the same action
if (!find(state.notifications, (oldNotification) => oldNotification.action.id === action.id)) {
state.notifications.push({type, status, action, seen: false})
if ('Notification' in window && window.Notification.permission === 'granted') {
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
// 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
}
let notification = new window.Notification(title, result)
// Chrome is known for not closing notifications automatically
// according to MDN, anyway.
setTimeout(notification.close.bind(notification), 5000)
}
}
}
const favoriteStatus = (favorite) => {
const status = find(allStatuses, { id: toInteger(favorite.in_reply_to_status_id) })
if (status) {
status.fave_num += 1
// This is our favorite, so the relevant bit.
if (favorite.user.id === user.id) {
status.favorited = true
}
// Add a notification if the user's status is favorited
if (status.user.id === user.id) {
addNotification({type: 'favorite', status, action: favorite})
}
}
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.
if (!state.favorites.has(favorite.id)) {
state.favorites.add(favorite.id)
updateMaxId(favorite)
favoriteStatus(favorite)
}
},
'follow': (status) => {
let re = new RegExp(`started following ${user.name} \\(${user.statusnet_profile_url}\\)`)
let repleroma = new RegExp(`started following ${user.screen_name}$`)
if (status.text.match(re) || status.text.match(repleroma)) {
addNotification({ type: 'follow', status: status, action: status })
}
},
'deletion': (deletion) => {
const uri = deletion.uri
updateMaxId(deletion)
// Remove possible notification
const status = find(allStatuses, {uri})
if (!status) {
return
}
remove(state.notifications, ({action: {id}}) => id === status.id)
remove(allStatuses, { uri })
if (timeline) {
remove(timelineObject.statuses, { uri })
remove(timelineObject.visibleStatuses, { uri })
}
},
'default': (unknown) => {
console.log('unknown status type')
console.log(unknown)
}
}
each(statuses, (status) => {
const type = statusType(status)
const processor = processors[type] || processors['default']
processor(status)
})
// Keep the visible statuses sorted
if (timeline) {
sortTimeline(timelineObject)
}
}
export const mutations = {
addNewStatuses,
showNewStatuses (state, { timeline }) {
const oldTimeline = (state.timelines[timeline])
oldTimeline.newStatusCount = 0
oldTimeline.visibleStatuses = slice(oldTimeline.statuses, 0, 50)
oldTimeline.visibleStatusesObject = {}
each(oldTimeline.visibleStatuses, (status) => { oldTimeline.visibleStatusesObject[status.id] = status })
},
clearTimeline (state, { timeline }) {
const emptyTimeline = {
statuses: [],
statusesObject: {},
faves: [],
visibleStatuses: [],
visibleStatusesObject: {},
newStatusCount: 0,
maxId: 0,
minVisibleId: 0,
loading: false,
followers: [],
friends: [],
- viewing: 'statuses'
+ viewing: 'statuses',
+ flushMarker: 0
}
state.timelines[timeline] = emptyTimeline
},
setFavorited (state, { status, value }) {
const newStatus = state.allStatusesObject[status.id]
newStatus.favorited = value
},
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
},
setProfileView (state, { v }) {
// load followers / friends only when needed
state.timelines['user'].viewing = v
},
addFriends (state, { friends }) {
state.timelines['user'].friends = friends
},
addFollowers (state, { followers }) {
state.timelines['user'].followers = followers
},
markNotificationsAsSeen (state, notifications) {
each(notifications, (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 }) {
commit('addNewStatuses', { statuses, showImmediately, timeline, noIdUpdate, user: rootState.users.currentUser })
},
setError ({ rootState, commit }, { value }) {
commit('setError', { value })
},
addFriends ({ rootState, commit }, { friends }) {
commit('addFriends', { friends })
},
addFollowers ({ rootState, commit }, { followers }) {
commit('addFollowers', { followers })
},
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 })
},
unfavorite ({ rootState, commit }, status) {
// Optimistic favoriting...
commit('setFavorited', { status, value: false })
apiService.unfavorite({ id: status.id, credentials: rootState.users.currentUser.credentials })
},
retweet ({ rootState, commit }, status) {
// Optimistic retweeting...
commit('setRetweeted', { status, value: true })
apiService.retweet({ id: status.id, credentials: rootState.users.currentUser.credentials })
},
queueFlush ({ rootState, commit }, { timeline, id }) {
commit('queueFlush', { timeline, id })
}
},
mutations
}
export default statuses
diff --git a/src/services/timeline_fetcher/timeline_fetcher.service.js b/src/services/timeline_fetcher/timeline_fetcher.service.js
index a02d67d034..a4a80df070 100644
--- a/src/services/timeline_fetcher/timeline_fetcher.service.js
+++ b/src/services/timeline_fetcher/timeline_fetcher.service.js
@@ -1,50 +1,53 @@
import { camelCase } from 'lodash'
import apiService from '../api/api.service.js'
const update = ({store, statuses, timeline, showImmediately}) => {
const ccTimeline = camelCase(timeline)
store.dispatch('setError', { value: false })
store.dispatch('addNewStatuses', {
timeline: ccTimeline,
statuses,
showImmediately
})
}
const fetchAndUpdate = ({store, credentials, timeline = 'friends', older = false, showImmediately = false, userId = false, tag = false}) => {
const args = { timeline, credentials }
const rootState = store.rootState || store.state
const timelineData = rootState.statuses.timelines[camelCase(timeline)]
if (older) {
args['until'] = timelineData.minVisibleId
} else {
args['since'] = timelineData.maxId
}
args['userId'] = userId
args['tag'] = tag
return apiService.fetchTimeline(args)
.then((statuses) => {
if (!older && statuses.length >= 20) {
store.dispatch('queueFlush', { timeline: timeline, id: timelineData.maxId })
}
update({store, statuses, timeline, showImmediately})
}, () => store.dispatch('setError', { value: true }))
}
const startFetching = ({timeline = 'friends', credentials, store, userId = false, tag = false}) => {
- fetchAndUpdate({timeline, credentials, store, showImmediately: true, userId, tag})
+ const rootState = store.rootState || store.state
+ const timelineData = rootState.statuses.timelines[camelCase(timeline)]
+ const showImmediately = timelineData.visibleStatuses.length === 0
+ fetchAndUpdate({timeline, credentials, store, showImmediately, userId, tag})
const boundFetchAndUpdate = () => fetchAndUpdate({ timeline, credentials, store, userId, tag })
return setInterval(boundFetchAndUpdate, 10000)
}
const timelineFetcher = {
fetchAndUpdate,
startFetching
}
export default timelineFetcher
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sun, Aug 9, 4:55 AM (1 d, 15 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1724228
Default Alt Text
(21 KB)
Attached To
Mode
rPUFE pleroma-fe-upstream
Attached
Detach File
Event Timeline
Log In to Comment