From 7813cf99ed78d124b01ec952ba75ea425f651344 Mon Sep 17 00:00:00 2001 From: Nolan Lawson Date: Fri, 9 Mar 2018 22:31:26 -0800 Subject: [PATCH] immediately add replies to threads --- routes/_actions/addStatusOrNotification.js | 51 +++++++++++++++++----- routes/_actions/compose.js | 2 +- routes/_actions/timeline.js | 15 +++++++ routes/_components/compose/ComposeBox.html | 14 +++--- routes/_components/timeline/Timeline.html | 12 ++++- routes/_store/mixins/timelineMixins.js | 16 +++++++ tests/spec/017-compose-reply.js | 2 +- tests/spec/103-compose.js | 38 ++++++++++++++++ tests/utils.js | 1 + 9 files changed, 129 insertions(+), 22 deletions(-) create mode 100644 tests/spec/103-compose.js diff --git a/routes/_actions/addStatusOrNotification.js b/routes/_actions/addStatusOrNotification.js index d832f205..954af4b5 100644 --- a/routes/_actions/addStatusOrNotification.js +++ b/routes/_actions/addStatusOrNotification.js @@ -20,6 +20,43 @@ async function removeDuplicates (instanceName, timelineName, updates) { return updates.filter(update => !existingItemIds.has(update.id)) } +async function insertUpdatesIntoTimeline (instanceName, timelineName, updates) { + updates = await removeDuplicates(instanceName, timelineName, updates) + + await database.insertTimelineItems(instanceName, timelineName, updates) + + let itemIdsToAdd = store.getForTimeline(instanceName, timelineName, 'itemIdsToAdd') || [] + if (updates && updates.length) { + itemIdsToAdd = itemIdsToAdd.concat(updates.map(_ => _.id)) + console.log('adding ', itemIdsToAdd.length, 'items to itemIdsToAdd') + store.setForTimeline(instanceName, timelineName, {itemIdsToAdd: itemIdsToAdd}) + } +} + +async function insertUpdatesIntoThreads (instanceName, updates) { + if (!updates.length) { + return + } + + let threads = store.getThreadsForTimeline(instanceName) + + for (let timelineName of Object.keys(threads)) { + let thread = threads[timelineName] + let updatesForThisThread = updates.filter(status => { + return thread.includes(status.in_reply_to_id) && !thread.includes(status.id) + }) + let itemIdsToAdd = store.getForTimeline(instanceName, timelineName, 'itemIdsToAdd') || [] + for (let update of updatesForThisThread) { + if (!itemIdsToAdd.includes(update.id)) { + itemIdsToAdd.push(update.id) + } + } + console.log('adding ', itemIdsToAdd.length, 'items to itemIdsToAdd for thread', timelineName) + store.setForTimeline(instanceName, timelineName, {itemIdsToAdd: itemIdsToAdd}) + console.log('timelineName', timelineName, 'itemIdsToAdd', itemIdsToAdd) + } +} + async function processFreshUpdates (instanceName, timelineName) { mark('processFreshUpdates') let freshUpdates = store.getForTimeline(instanceName, timelineName, 'freshUpdates') @@ -27,18 +64,10 @@ async function processFreshUpdates (instanceName, timelineName) { let updates = freshUpdates.slice() store.setForTimeline(instanceName, timelineName, {freshUpdates: []}) - updates = await removeDuplicates(instanceName, timelineName, updates) - - await database.insertTimelineItems(instanceName, timelineName, updates) - - let itemIdsToAdd = store.getForTimeline(instanceName, timelineName, 'itemIdsToAdd') || [] - if (updates && updates.length) { - itemIdsToAdd = itemIdsToAdd.concat(updates.map(_ => _.id)) - console.log('adding ', itemIdsToAdd.length, 'items to itemIdsToAdd') - store.setForTimeline(instanceName, timelineName, {itemIdsToAdd: itemIdsToAdd}) - } - stop('processFreshUpdates') + await insertUpdatesIntoTimeline(instanceName, timelineName, updates) + await insertUpdatesIntoThreads(instanceName, updates.filter(status => status.in_reply_to_id)) } + stop('processFreshUpdates') } const lazilyProcessFreshUpdates = throttle((instanceName, timelineName) => { diff --git a/routes/_actions/compose.js b/routes/_actions/compose.js index b0873617..aadd5a6f 100644 --- a/routes/_actions/compose.js +++ b/routes/_actions/compose.js @@ -1,6 +1,6 @@ import { store } from '../_store/store' import { toast } from '../_utils/toast' -import { postStatusToServer } from '../_api/statuses' +import { postStatus as postStatusToServer } from '../_api/statuses' import { addStatusOrNotification } from './addStatusOrNotification' import { database } from '../_database/database' diff --git a/routes/_actions/timeline.js b/routes/_actions/timeline.js index 4a6a19bf..837dcbd5 100644 --- a/routes/_actions/timeline.js +++ b/routes/_actions/timeline.js @@ -95,3 +95,18 @@ export async function showMoreItemsForCurrentTimeline () { }) stop('showMoreItemsForCurrentTimeline') } + +export async function showMoreItemsForCurrentThread () { + mark('showMoreItemsForCurrentThread') + let instanceName = store.get('currentInstance') + let timelineName = store.get('currentTimeline') + let itemIdsToAdd = store.get('itemIdsToAdd') + // TODO: update database and do this merge correctly + let timelineItemIds = store.getForTimeline(instanceName, timelineName, 'timelineItemIds') + timelineItemIds = timelineItemIds.concat(itemIdsToAdd) + store.setForTimeline(instanceName, timelineName, { + itemIdsToAdd: [], + timelineItemIds: timelineItemIds + }) + stop('showMoreItemsForCurrentThread') +} diff --git a/routes/_components/compose/ComposeBox.html b/routes/_components/compose/ComposeBox.html index 3dccf038..c7a0ee65 100644 --- a/routes/_components/compose/ComposeBox.html +++ b/routes/_components/compose/ComposeBox.html @@ -66,14 +66,14 @@ if (realm !== 'home') { // if this is a reply, populate the handle immediately insertHandleForReply(realm) - } - // if this is a reply, go back immediately after it's posted - this.observe('postedStatusForRealm', postedStatusForRealm => { - if (postedStatusForRealm === realm) { - window.history.back() - } - }, {init: false}) + // if this is a reply, go back immediately after it's posted + this.observe('postedStatusForRealm', postedStatusForRealm => { + if (postedStatusForRealm === realm) { + window.history.back() + } + }, {init: false}) + } }, components: { ComposeAuthor, diff --git a/routes/_components/timeline/Timeline.html b/routes/_components/timeline/Timeline.html index 8837951a..1e4dc746 100644 --- a/routes/_components/timeline/Timeline.html +++ b/routes/_components/timeline/Timeline.html @@ -60,7 +60,12 @@ import VirtualList from '../virtualList/VirtualList.html' import { timelines } from '../../_static/timelines' import { database } from '../../_database/database' - import { initializeTimeline, fetchTimelineItemsOnScrollToBottom, setupTimeline } from '../../_actions/timeline' + import { + initializeTimeline, + fetchTimelineItemsOnScrollToBottom, + setupTimeline, + showMoreItemsForCurrentThread + } from '../../_actions/timeline' import LoadingPage from '../LoadingPage.html' import { focusWithCapture, blurWithCapture } from '../../_utils/events' import { showMoreItemsForCurrentTimeline } from '../../_actions/timeline' @@ -195,7 +200,10 @@ let scrollTop = this.get('scrollTop') let shouldShowHeader = this.store.get('shouldShowHeader') let showHeader = this.store.get('showHeader') - if (scrollTop === 0 && !shouldShowHeader && !showHeader) { + if (timelineName.startsWith('status/')) { + // this is a thread, just insert the statuses already + showMoreItemsForCurrentThread() + } else if (scrollTop === 0 && !shouldShowHeader && !showHeader) { // if the user is scrolled to the top and we're not showing the header, then // just insert the statuses. this is "chat room mode" showMoreItemsForCurrentTimeline() diff --git a/routes/_store/mixins/timelineMixins.js b/routes/_store/mixins/timelineMixins.js index 924dbbe4..aaead78d 100644 --- a/routes/_store/mixins/timelineMixins.js +++ b/routes/_store/mixins/timelineMixins.js @@ -1,3 +1,5 @@ +import pickBy from 'lodash/pickBy' + export function timelineMixins (Store) { Store.prototype.setForTimeline = function (instanceName, timelineName, obj) { let valuesToSet = {} @@ -23,4 +25,18 @@ export function timelineMixins (Store) { let timelineName = this.get('currentTimeline') this.setForTimeline(instanceName, timelineName, obj) } + + Store.prototype.getThreadsForTimeline = function (instanceName) { + let root = this.get('timelineData_timelineItemIds') || {} + let instanceData = root[instanceName] = root[instanceName] || {} + + return pickBy(instanceData, (value, key) => { + return key.startsWith('status/') + }) + } + + Store.prototype.getThreadsForCurrentTimeline = function () { + let instanceName = this.get('currentInstance') + return this.getThreadsForTimeline(instanceName) + } } diff --git a/tests/spec/017-compose-reply.js b/tests/spec/017-compose-reply.js index c858d542..3b0260ff 100644 --- a/tests/spec/017-compose-reply.js +++ b/tests/spec/017-compose-reply.js @@ -31,7 +31,7 @@ test('account handle populated correctly for replies', async t => { .expect(composeInput.value).eql('') }) -test('replying to posts wth mentions', async t => { +test('replying to posts with mentions', async t => { await t.useRole(foobarRole) .click(getNthReplyButton(1)) .expect(getUrl()).contains('/statuses') diff --git a/tests/spec/103-compose.js b/tests/spec/103-compose.js new file mode 100644 index 00000000..fe89da7e --- /dev/null +++ b/tests/spec/103-compose.js @@ -0,0 +1,38 @@ +import { foobarRole } from '../roles' +import { + composeInput, getNthReplyButton, getNthStatus, getUrl, homeNavButton, notificationsNavButton, + postStatusButton +} from '../utils' + +fixture`103-compose.js` + .page`http://localhost:4002` + +test('statuses show up in home timeline', async t => { + await t.useRole(foobarRole) + .typeText(composeInput, 'hello world', {paste: true}) + .click(postStatusButton) + .expect(getNthStatus(0).innerText).contains('hello world') + .click(notificationsNavButton) + .expect(getUrl()).contains('/notifications') + .click(homeNavButton) + .expect(getUrl()).eql('http://localhost:4002/') + .expect(getNthStatus(0).innerText).contains('hello world') + .navigateTo('/') + .expect(getNthStatus(0).innerText).contains('hello world') +}) + +test('statuses in threads show up in right order', async t => { + await t.useRole(foobarRole) + .navigateTo('/accounts/5') + .click(getNthStatus(2)) + .expect(getUrl()).contains('/statuses') + .click(getNthReplyButton(3)) + .expect(getUrl()).contains('/reply') + .typeText(composeInput, 'my reply!', {paste: true}) + .click(postStatusButton) + .expect(getUrl()).match(/statuses\/[^/]+$/) + .expect(getNthStatus(5).innerText).contains('@baz my reply!') + .navigateTo('/accounts/5') + .click(getNthStatus(2)) + .expect(getNthStatus(5).innerText).contains('@baz my reply!') +}) diff --git a/tests/utils.js b/tests/utils.js index 2b605f9e..ebbad0f1 100644 --- a/tests/utils.js +++ b/tests/utils.js @@ -25,6 +25,7 @@ export const passwordInput = $('input#user_password') export const authorizeInput = $('button[type=submit]:not(.negative)') export const logInToInstanceLink = $('a[href="/settings/instances/add"]') export const searchInput = $('.search-input') +export const postStatusButton = $('.compose-box-button') export const favoritesCountElement = $('.status-favs-reblogs:nth-child(3)').addCustomDOMProperties({ innerCount: el => parseInt(el.innerText, 10)