Complete rewrite of status adding code.

This now uses nearly only mutation, to take advantage
of vue's mutation tracking.
This commit is contained in:
Roger Braun 2016-11-18 19:48:02 +01:00
parent 05afbbaf66
commit 9171b382fe
2 changed files with 120 additions and 126 deletions

View File

@ -1,4 +1,4 @@
import { reduce, map, slice, last, intersectionBy, sortBy, unionBy, toInteger, groupBy, differenceBy, each, find, flatten, maxBy, merge } from 'lodash' import { map, slice, sortBy, toInteger, each, find, flatten, maxBy, last, merge, max } from 'lodash'
import moment from 'moment' import moment from 'moment'
import apiService from '../services/api/api.service.js' import apiService from '../services/api/api.service.js'
// import parse from '../services/status_parser/status_parser.js' // import parse from '../services/status_parser/status_parser.js'
@ -37,10 +37,6 @@ export const defaultState = {
} }
} }
const statusType = (status) => {
return !status.is_post_verb && status.uri.match(/fave/) ? 'fave' : 'status'
}
export const prepareStatus = (status) => { export const prepareStatus = (status) => {
// Parse nsfw tags // Parse nsfw tags
if (status.nsfw === undefined) { if (status.nsfw === undefined) {
@ -54,71 +50,6 @@ export const prepareStatus = (status) => {
return status return status
} }
// Merges old and new status collections.
const mergeStatuses = (oldStatuses, newStatuses) => {
each(newStatuses, (status) => {
let oldStatus = find(oldStatuses, { id: status.id })
if (oldStatus) {
merge(oldStatus, status)
} else {
oldStatuses.push(status)
}
})
return oldStatuses
}
const addStatusesToTimeline = (addedStatuses, showImmediately, { statuses, visibleStatuses, newStatusCount, faves, loading, maxId }) => {
const statusesAndFaves = groupBy(addedStatuses, statusType)
const addedFaves = statusesAndFaves['fave'] || []
const unseenFaves = differenceBy(addedFaves, faves, 'id')
// Update fave count
each(unseenFaves, ({in_reply_to_status_id}) => {
const status = find(statuses, { id: toInteger(in_reply_to_status_id) })
if (status) {
status.fave_num += 1
}
})
addedStatuses = statusesAndFaves['status'] || []
// Add some html and nsfw to the statuses.
addedStatuses = map(addedStatuses, (status) => {
const statusoid = status.retweeted_status || status
prepareStatus(statusoid)
return status
})
const newStatuses = sortBy(
unionBy(addedStatuses, statuses, 'id'),
({id}) => -id
)
let newNewStatusCount = newStatusCount + (newStatuses.length - statuses.length)
let newVisibleStatuses = visibleStatuses
if (showImmediately) {
newVisibleStatuses = unionBy(addedStatuses, newVisibleStatuses, 'id')
newVisibleStatuses = sortBy(newVisibleStatuses, ({id}) => -id)
newNewStatusCount = newStatusCount
};
newVisibleStatuses = intersectionBy(newStatuses, newVisibleStatuses, 'id')
return {
statuses: newStatuses,
visibleStatuses: newVisibleStatuses,
newStatusCount: newNewStatusCount,
minVisibleId: (last(newVisibleStatuses) || { id: undefined }).id,
faves: unionBy(faves, addedFaves, 'id'),
maxId,
loading
}
}
const updateTimestampsInStatuses = (statuses) => { const updateTimestampsInStatuses = (statuses) => {
return map(statuses, (statusoid) => { return map(statuses, (statusoid) => {
const status = statusoid.retweeted_status || statusoid const status = statusoid.retweeted_status || statusoid
@ -129,66 +60,109 @@ const updateTimestampsInStatuses = (statuses) => {
}) })
} }
// const groupStatusesByType = (statuses) => { const statusType = (status) => {
// return groupBy(statuses, (status) => { if (status.is_post_verb) {
// if (status.is_post_verb) { return 'status'
// return 'status' }
// }
// if (status.retweeted_status) { if (status.retweeted_status) {
// return 'retweet' return 'retweet'
// } }
// if (typeof status.uri === 'string' && status.uri.match(/fave/)) { if (typeof status.uri === 'string' && status.uri.match(/fave/)) {
// return 'favorite' return 'favorite'
// } }
// return 'unknown' return 'unknown'
// }) }
// }
export const findMaxId = (...args) => { export const findMaxId = (...args) => {
return (maxBy(flatten(args), 'id') || {}).id return (maxBy(flatten(args), 'id') || {}).id
} }
const mergeOrAdd = (arr, item) => {
const oldItem = find(arr, {id: item.id})
if (oldItem) {
// We already have this, so only merge the new info.
merge(oldItem, item)
return oldItem
} else {
// This is a new item, prepare it
prepareStatus(item)
arr.push(item)
return item
}
}
export const mutations = { export const mutations = {
addNewStatuses (state, { statuses, showImmediately = false, timeline }) { addNewStatuses (state, { statuses, showImmediately = false, timeline }) {
const allStatuses = state.allStatuses
// Merge in the new status into the old ones.
mergeStatuses(state.allStatuses, statuses)
// Get relevant timeline
const timelineObject = state.timelines[timeline] const timelineObject = state.timelines[timeline]
// Set new maxId // Set the maxId to the new id if it's larger.
const maxId = findMaxId(statuses, timelineObject.statuses) const updateMaxId = ({id}) => {
timelineObject.maxId = maxId timelineObject.maxId = max([id, timelineObject.maxId])
// Split statuses by type
// const statusesByType = groupStatusesByType(statuses)
state.timelines[timeline] = addStatusesToTimeline(statuses, showImmediately, state.timelines[timeline])
// Set up retweets with most current status
const getRetweets = (result, status) => {
if (status.retweeted_status) {
result.push(status.retweeted_status)
}
return result
} }
const retweets = reduce(statuses, getRetweets, []) const addStatus = (status, showImmediately, addToTimeline = true) => {
// Remember the current amount of statuses
// We need that to calculate new status count.
const prevLength = timelineObject.statuses.length
// state.allStatuses = unionBy(retweets, state.allStatuses, 'id') updateMaxId(status)
mergeStatuses(state.allStatuses, retweets) status = mergeOrAdd(allStatuses, status)
each(state.allStatuses, (status) => { // Some statuses should only be added to the global status repository.
if (status.retweeted_status) { if (addToTimeline) {
const retweetedStatus = find(state.allStatuses, { id: status.retweeted_status.id }) mergeOrAdd(timelineObject.statuses, status)
status.retweeted_status = retweetedStatus
} }
if (showImmediately) {
// Add it directly to the visibleStatuses, don't change
// newStatusCount
mergeOrAdd(timelineObject.visibleStatuses, status)
} else {
// Just change newStatuscount
timelineObject.newStatusCount += (timelineObject.statuses.length - prevLength)
}
return status
}
const favoriteStatus = (favorite) => {
const status = find(allStatuses, { id: toInteger(favorite.in_reply_to_status_id) })
if (status) {
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)
const retweet = addStatus(status, showImmediately)
retweet.retweeted_status = retweetedStatus
},
'favorite': (status) => {
updateMaxId(status)
favoriteStatus(status)
}
}
each(statuses, (status) => {
const type = statusType(status)
processors[type](status)
}) })
// Keep the visible statuses sorted
timelineObject.visibleStatuses = sortBy(timelineObject.visibleStatuses, ({id}) => -id)
timelineObject.statuses = sortBy(timelineObject.statuses, ({id}) => -id)
timelineObject.minVisibleId = (last(timelineObject.statuses) || {}).id
}, },
showNewStatuses (state, { timeline }) { showNewStatuses (state, { timeline }) {
const oldTimeline = (state.timelines[timeline]) const oldTimeline = (state.timelines[timeline])

View File

@ -1,13 +1,14 @@
import { cloneDeep } from 'lodash' import { cloneDeep } from 'lodash'
import { defaultState, mutations, findMaxId, prepareStatus } from '../../../../src/modules/statuses.js' import { defaultState, mutations, findMaxId, prepareStatus } from '../../../../src/modules/statuses.js'
const makeMockStatus = ({id, text}) => { const makeMockStatus = ({id, text, is_post_verb = true}) => {
return { return {
id, id,
name: 'status', name: 'status',
text: text || `Text number ${id}`, text: text || `Text number ${id}`,
fave_num: 0, fave_num: 0,
uri: '' uri: '',
is_post_verb
} }
} }
@ -62,6 +63,7 @@ describe('The Statuses module', () => {
expect(state.allStatuses).to.eql([status]) expect(state.allStatuses).to.eql([status])
expect(state.timelines.public.statuses).to.eql([status]) expect(state.timelines.public.statuses).to.eql([status])
expect(state.timelines.public.visibleStatuses).to.eql([]) expect(state.timelines.public.visibleStatuses).to.eql([])
expect(state.timelines.public.newStatusCount).to.equal(1)
}) })
it('adds the status to allStatuses and to the given timeline, directly visible', () => { it('adds the status to allStatuses and to the given timeline, directly visible', () => {
@ -73,12 +75,30 @@ describe('The Statuses module', () => {
expect(state.allStatuses).to.eql([status]) expect(state.allStatuses).to.eql([status])
expect(state.timelines.public.statuses).to.eql([status]) expect(state.timelines.public.statuses).to.eql([status])
expect(state.timelines.public.visibleStatuses).to.eql([status]) expect(state.timelines.public.visibleStatuses).to.eql([status])
expect(state.timelines.public.newStatusCount).to.equal(0)
})
it('keeps a descending by id order in timeline.visibleStatuses and timeline.statuses', () => {
const state = cloneDeep(defaultState)
const status = makeMockStatus({id: 2})
const statusTwo = makeMockStatus({id: 1})
const statusThree = makeMockStatus({id: 3})
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(1)
mutations.addNewStatuses(state, { statuses: [statusThree], showImmediately: true, timeline: 'public' })
expect(state.timelines.public.statuses).to.eql([statusThree, status, statusTwo])
expect(state.timelines.public.visibleStatuses).to.eql([statusThree, status, statusTwo])
}) })
it('splits retweets from their status and links them', () => { it('splits retweets from their status and links them', () => {
const state = cloneDeep(defaultState) const state = cloneDeep(defaultState)
const status = makeMockStatus({id: 1}) const status = makeMockStatus({id: 1})
const retweet = makeMockStatus({id: 2}) const retweet = makeMockStatus({id: 2, is_post_verb: false})
const modStatus = makeMockStatus({id: 1, text: 'something else'}) const modStatus = makeMockStatus({id: 1, text: 'something else'})
retweet.retweeted_status = status retweet.retweeted_status = status
@ -86,12 +106,18 @@ describe('The Statuses module', () => {
// It adds both statuses, but only the retweet to visible. // It adds both statuses, but only the retweet to visible.
mutations.addNewStatuses(state, { statuses: [retweet], timeline: 'public', showImmediately: true }) mutations.addNewStatuses(state, { statuses: [retweet], timeline: 'public', showImmediately: true })
expect(state.timelines.public.visibleStatuses).to.have.length(1) expect(state.timelines.public.visibleStatuses).to.have.length(1)
expect(state.allStatuses).to.eql([retweet, status]) 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)
// It refers to the modified status. // It refers to the modified status.
mutations.addNewStatuses(state, { statuses: [modStatus], timeline: 'public' }) mutations.addNewStatuses(state, { statuses: [modStatus], timeline: 'public' })
expect(state.allStatuses).to.eql([retweet, modStatus]) expect(state.allStatuses).to.have.length(2)
expect(retweet.retweeted_status).to.eql(modStatus) 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(retweet.retweeted_status.text).to.eql(modStatus.text)
}) })
it('replaces existing statuses with the same id', () => { it('replaces existing statuses with the same id', () => {
@ -108,14 +134,14 @@ describe('The Statuses module', () => {
mutations.addNewStatuses(state, { statuses: [modStatus], showImmediately: true, timeline: 'public' }) mutations.addNewStatuses(state, { statuses: [modStatus], showImmediately: true, timeline: 'public' })
expect(state.timelines.public.visibleStatuses).to.have.length(1) expect(state.timelines.public.visibleStatuses).to.have.length(1)
expect(state.allStatuses).to.have.length(1) expect(state.allStatuses).to.have.length(1)
expect(state.allStatuses[0]).to.eql(modStatus) expect(state.allStatuses[0].text).to.eql(modStatus.text)
}) })
it('replaces existing statuses with the same id, coming from a retweet', () => { it('replaces existing statuses with the same id, coming from a retweet', () => {
const state = cloneDeep(defaultState) const state = cloneDeep(defaultState)
const status = makeMockStatus({id: 1}) const status = makeMockStatus({id: 1})
const modStatus = makeMockStatus({id: 1, text: 'something else'}) const modStatus = makeMockStatus({id: 1, text: 'something else'})
const retweet = makeMockStatus({id: 2}) const retweet = makeMockStatus({id: 2, is_post_verb: false})
retweet.retweeted_status = modStatus retweet.retweeted_status = modStatus
// Add original status // Add original status
@ -127,7 +153,7 @@ describe('The Statuses module', () => {
mutations.addNewStatuses(state, { statuses: [retweet], showImmediately: false, timeline: 'public' }) mutations.addNewStatuses(state, { statuses: [retweet], showImmediately: false, timeline: 'public' })
expect(state.timelines.public.visibleStatuses).to.have.length(1) expect(state.timelines.public.visibleStatuses).to.have.length(1)
expect(state.allStatuses).to.have.length(2) expect(state.allStatuses).to.have.length(2)
expect(state.allStatuses[0]).to.eql(modStatus) expect(state.allStatuses[0].text).to.eql(modStatus.text)
}) })
it('handles favorite actions', () => { it('handles favorite actions', () => {
@ -148,11 +174,5 @@ describe('The Statuses module', () => {
expect(state.timelines.public.visibleStatuses.length).to.eql(1) 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].fave_num).to.eql(1)
expect(state.timelines.public.maxId).to.eq(favorite.id) expect(state.timelines.public.maxId).to.eq(favorite.id)
// Adding again shouldn't change anything
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)
}) })
}) })