diff --git a/routes/_actions/timeline.js b/routes/_actions/timeline.js index fd877af..f15d843 100644 --- a/routes/_actions/timeline.js +++ b/routes/_actions/timeline.js @@ -12,9 +12,22 @@ import { getTimeline as getTimelineFromDatabase } from '../_database/timelines/pagination' import { getStatus, getStatusContext } from '../_api/statuses' +import { emit } from '../_utils/eventBus' const FETCH_LIMIT = 20 +async function storeFreshTimelineItemsInDatabase (instanceName, timelineName, items) { + await insertTimelineItemsInDatabase(instanceName, timelineName, items) + if (timelineName.startsWith('status/')) { + // For status threads, we want to be sure to update the favorite/reblog counts even if + // this is a stale "view" of the status. See 119-status-counts-update.js for + // an example of why we need this. + items.forEach(item => { + emit('statusUpdated', item) + }) + } +} + async function fetchTimelineItemsFromNetwork (instanceName, accessToken, timelineName, lastTimelineItemId) { if (timelineName.startsWith('status/')) { // special case - this is a list of descendents and ancestors let statusId = timelineName.split('/').slice(-1)[0] @@ -37,7 +50,7 @@ async function fetchTimelineItems (instanceName, accessToken, timelineName, last } else { try { items = await fetchTimelineItemsFromNetwork(instanceName, accessToken, timelineName, lastTimelineItemId) - /* no await */ insertTimelineItemsInDatabase(instanceName, timelineName, items) + /* no await */ storeFreshTimelineItemsInDatabase(instanceName, timelineName, items) } catch (e) { console.error(e) toast.say('Internet request failed. Showing offline content.') diff --git a/routes/_components/status/StatusDetails.html b/routes/_components/status/StatusDetails.html index 07ed2c2..f28a6b9 100644 --- a/routes/_components/status/StatusDetails.html +++ b/routes/_components/status/StatusDetails.html @@ -133,16 +133,43 @@ import ExternalLink from '../ExternalLink.html' import { store } from '../../_store/store' import { getDateFormatter, getShortDateFormatter } from '../../_utils/formatters' + import { on } from '../../_utils/eventBus' export default { + oncreate () { + let { originalStatusId } = this.get() + on('statusUpdated', this, status => { + if (status.id === originalStatusId) { + this.set({ + overrideNumReblogs: status.reblogs_count || 0, + overrideNumFavs: status.favourites_count || 0 + }) + } + }) + }, store: () => store, + data: () => ({ + overrideNumReblogs: void 0, + overrideNumFavs: void 0 + }), computed: { + originalStatusId: ({ originalStatus }) => originalStatus.id, application: ({ originalStatus }) => originalStatus.application, applicationName: ({ application }) => (application && application.name), applicationWebsite: ({ application }) => (application && application.website), createdAtDate: ({ originalStatus }) => originalStatus.created_at, - numReblogs: ({ originalStatus }) => originalStatus.reblogs_count || 0, - numFavs: ({ originalStatus }) => originalStatus.favourites_count || 0, + numReblogs: ({ overrideNumReblogs, originalStatus }) => { + if (typeof overrideNumReblogs === 'number') { + return overrideNumReblogs + } + return originalStatus.reblogs_count || 0 + }, + numFavs: ({ overrideNumFavs, originalStatus }) => { + if (typeof overrideNumFavs === 'number') { + return overrideNumFavs + } + return originalStatus.favourites_count || 0 + }, formattedDate: ({ createdAtDate, $isMobileSize }) => { let formatter = $isMobileSize ? getShortDateFormatter() : getDateFormatter() return formatter.format(new Date(createdAtDate)) diff --git a/tests/serverActions.js b/tests/serverActions.js index f5ac069..39a6400 100644 --- a/tests/serverActions.js +++ b/tests/serverActions.js @@ -7,6 +7,7 @@ import { deleteStatus } from '../routes/_api/delete' import { authorizeFollowRequest, getFollowRequests } from '../routes/_actions/followRequests' import { followAccount, unfollowAccount } from '../routes/_api/follow' import { updateCredentials } from '../routes/_api/updateCredentials' +import { reblogStatus } from '../routes/_api/reblog' global.fetch = fetch global.File = FileApi.File @@ -18,6 +19,10 @@ export async function favoriteStatusAs (username, statusId) { return favoriteStatus(instanceName, users[username].accessToken, statusId) } +export async function reblogStatusAs (username, statusId) { + return reblogStatus(instanceName, users[username].accessToken, statusId) +} + export async function postAs (username, text) { return postStatus(instanceName, users[username].accessToken, text, null, null, false, null, 'public') diff --git a/tests/spec/119-status-counts-update.js b/tests/spec/119-status-counts-update.js new file mode 100644 index 0000000..da3013f --- /dev/null +++ b/tests/spec/119-status-counts-update.js @@ -0,0 +1,87 @@ +import { loginAsFoobar } from '../roles' +import { + getFavoritesCount, + getNthStatus, getNthStatusContent, getReblogsCount, homeNavButton +} from '../utils' +import { favoriteStatusAs, postAs, reblogStatusAs } from '../serverActions' + +fixture`119-status-counts-update.js` + .page`http://localhost:4002` + +test('Fav stats update', async t => { + let status = await postAs('foobar', 'hey hello look at this toot') + let statusId = status.id + await favoriteStatusAs('admin', statusId) + await loginAsFoobar(t) + await t + .expect(getNthStatusContent(0).innerText).contains('hey hello look at this toot') + .click(getNthStatus(0)) + .expect(getFavoritesCount()).eql(1) + .click(homeNavButton) + await favoriteStatusAs('quux', statusId) + await t + .click(getNthStatus(0)) + .expect(getFavoritesCount()).eql(2) + .click(homeNavButton) + await favoriteStatusAs('baz', statusId) + await t + .click(getNthStatus(0)) + .expect(getFavoritesCount()).eql(3) + .click(homeNavButton) + await favoriteStatusAs('LockedAccount', statusId) + await t + .click(getNthStatus(0)) + .expect(getFavoritesCount()).eql(4) +}) + +test('Reblog stats update', async t => { + let status = await postAs('foobar', 'oh why hello it looks like another toot') + let statusId = status.id + await reblogStatusAs('admin', statusId) + await loginAsFoobar(t) + await t + .expect(getNthStatusContent(0).innerText).contains('oh why hello it looks like another toot') + .click(getNthStatus(0)) + .expect(getReblogsCount()).eql(1) + .click(homeNavButton) + await reblogStatusAs('quux', statusId) + await t + .click(getNthStatus(0)) + .expect(getReblogsCount()).eql(2) + .click(homeNavButton) + await reblogStatusAs('baz', statusId) + await t + .click(getNthStatus(0)) + .expect(getReblogsCount()).eql(3) + .click(homeNavButton) + await reblogStatusAs('LockedAccount', statusId) + await t + .click(getNthStatus(0)) + .expect(getReblogsCount()).eql(4) +}) + +test('Fav and reblog stats update for a boosted toot', async t => { + let status = await postAs('ExternalLinks', 'this will get boosted') + let statusId = status.id + await reblogStatusAs('admin', statusId) + await favoriteStatusAs('admin', statusId) + await favoriteStatusAs('quux', statusId) + await loginAsFoobar(t) + await t + .expect(getNthStatusContent(0).innerText).contains('this will get boosted') + .click(getNthStatus(0)) + .expect(getReblogsCount()).eql(1) + .expect(getFavoritesCount()).eql(2) + .click(homeNavButton) + await favoriteStatusAs('baz', statusId) + await t + .click(getNthStatus(0)) + .expect(getReblogsCount()).eql(1) + .expect(getFavoritesCount()).eql(3) + .click(homeNavButton) + await favoriteStatusAs('LockedAccount', statusId) + await t + .click(getNthStatus(0)) + .expect(getReblogsCount()).eql(1) + .expect(getFavoritesCount()).eql(4) +})