diff --git a/routes/_actions/addStatusOrNotification.js b/routes/_actions/addStatusOrNotification.js index ba4e28d..c774a1f 100644 --- a/routes/_actions/addStatusOrNotification.js +++ b/routes/_actions/addStatusOrNotification.js @@ -38,7 +38,7 @@ async function insertUpdatesIntoThreads (instanceName, updates) { return } - let threads = store.getThreadsForTimeline(instanceName) + let threads = store.getThreads(instanceName) for (let timelineName of Object.keys(threads)) { let thread = threads[timelineName] diff --git a/routes/_actions/deleteStatuses.js b/routes/_actions/deleteStatuses.js index 9cc9d26..d0cb6a4 100644 --- a/routes/_actions/deleteStatuses.js +++ b/routes/_actions/deleteStatuses.js @@ -1,21 +1,28 @@ -import { getIdsThatRebloggedThisStatus, getIdThatThisStatusReblogged, getNotificationIdsForStatuses } from './statuses' +import { getIdsThatRebloggedThisStatus, getNotificationIdsForStatuses } from './statuses' import { store } from '../_store/store' import { scheduleIdleTask } from '../_utils/scheduleIdleTask' import { database } from '../_database/database' +import forEach from 'lodash/forEach' function deleteStatusIdsFromStore (instanceName, idsToDelete) { let idsToDeleteSet = new Set(idsToDelete) - let timelines = store.get('timelines') - if (timelines && timelines[instanceName]) { - Object.keys(timelines[instanceName]).forEach(timelineName => { - let timelineData = timelines[instanceName][timelineName] - if (timelineName !== 'notifications') { - timelineData.timelineItemIds = timelineData.timelineItemIds.filter(_ => !idsToDeleteSet.has(_)) - timelineData.itemIdsToAdd = timelineData.itemIdsToAdd.filter(_ => !idsToDeleteSet.has(_)) - } + let idWasNotDeleted = id => !idsToDeleteSet.has(id) + + let timelinesToTimelineItemIds = store.getAllTimelineData(instanceName, 'timelineItemIds') + + forEach(timelinesToTimelineItemIds, (timelineItemIds, timelineName) => { + store.setForTimeline(instanceName, timelineName, { + timelineItemIds: timelineItemIds.filter(idWasNotDeleted) }) - store.set({timelines: timelines}) - } + }) + + let timelinesToItemIdsToAdd = store.getAllTimelineData(instanceName, 'itemIdsToAdd') + + forEach(timelinesToItemIdsToAdd, (itemIdsToAdd, timelineName) => { + store.setForTimeline(instanceName, timelineName, { + itemIdsToAdd: itemIdsToAdd.filter(idWasNotDeleted) + }) + }) } async function deleteStatusesAndNotifications (instanceName, statusIdsToDelete, notificationIdsToDelete) { @@ -24,9 +31,9 @@ async function deleteStatusesAndNotifications (instanceName, statusIdsToDelete, } async function doDeleteStatus (instanceName, statusId) { - let reblogId = await getIdThatThisStatusReblogged(instanceName, statusId) - let rebloggedIds = await getIdsThatRebloggedThisStatus(reblogId || statusId) - let statusIdsToDelete = Array.from(new Set([statusId, reblogId].concat(rebloggedIds).filter(Boolean))) + console.log('deleting statusId', statusId) + let rebloggedIds = await getIdsThatRebloggedThisStatus(instanceName, statusId) + let statusIdsToDelete = Array.from(new Set([statusId].concat(rebloggedIds).filter(Boolean))) let notificationIdsToDelete = new Set(await getNotificationIdsForStatuses(instanceName, statusIdsToDelete)) await Promise.all([ deleteStatusesAndNotifications(instanceName, statusIdsToDelete, notificationIdsToDelete) diff --git a/routes/_actions/streaming.js b/routes/_actions/streaming.js index 8a97263..7f181b7 100644 --- a/routes/_actions/streaming.js +++ b/routes/_actions/streaming.js @@ -7,16 +7,15 @@ import { addStatusOrNotification } from './addStatusOrNotification' function processMessage (instanceName, timelineName, message) { mark('processMessage') let { event, payload } = message - let parsedPayload = JSON.parse(payload) switch (event) { case 'delete': - deleteStatus(instanceName, parsedPayload) + deleteStatus(instanceName, payload) break case 'update': - addStatusOrNotification(instanceName, timelineName, parsedPayload) + addStatusOrNotification(instanceName, timelineName, JSON.parse(payload)) break case 'notification': - addStatusOrNotification(instanceName, 'notifications', parsedPayload) + addStatusOrNotification(instanceName, 'notifications', JSON.parse(payload)) break } stop('processMessage') diff --git a/routes/_api/delete.js b/routes/_api/delete.js new file mode 100644 index 0000000..b952634 --- /dev/null +++ b/routes/_api/delete.js @@ -0,0 +1,7 @@ +import { auth, basename } from './utils' +import { deleteWithTimeout } from '../_utils/ajax' + +export async function deleteStatus (instanceName, accessToken, statusId) { + let url = `${basename(instanceName)}/api/v1/statuses/${statusId}` + return deleteWithTimeout(url, auth(accessToken)) +} diff --git a/routes/_database/databaseLifecycle.js b/routes/_database/databaseLifecycle.js index 7c7d4d7..685ec1e 100644 --- a/routes/_database/databaseLifecycle.js +++ b/routes/_database/databaseLifecycle.js @@ -9,13 +9,14 @@ import { PINNED_STATUSES_STORE, TIMESTAMP, REBLOG_ID, - THREADS_STORE + THREADS_STORE, + STATUS_ID } from './constants' const openReqs = {} const databaseCache = {} -const DB_VERSION = 5 +const DB_VERSION = 6 export function getDatabase (instanceName) { if (!instanceName) { @@ -54,6 +55,9 @@ export function getDatabase (instanceName) { tx.objectStore(NOTIFICATIONS_STORE).createIndex('statusId', 'statusId') db.createObjectStore(THREADS_STORE) } + if (e.oldVersion < 6) { + tx.objectStore(NOTIFICATIONS_STORE).createIndex(STATUS_ID, STATUS_ID) + } } req.onsuccess = () => resolve(req.result) }) diff --git a/routes/_database/timelines.js b/routes/_database/timelines.js index ec10389..e693fd8 100644 --- a/routes/_database/timelines.js +++ b/routes/_database/timelines.js @@ -288,6 +288,24 @@ export async function getReblogsForStatus (instanceName, id) { }) } +// +// lookups by statusId +// + +export async function getNotificationIdsForStatuses (instanceName, statusIds) { + const db = await getDatabase(instanceName) + await dbPromise(db, NOTIFICATIONS_STORE, 'readonly', (notificationsStore, callback) => { + let res = [] + callback(res) + statusIds.forEach(statusId => { + let req = notificationsStore.index(STATUS_ID).getAllKeys(IDBKeyRange.only(statusId)) + req.onsuccess = e => { + res = res.concat(e.target.result) + } + }) + }) +} + // // deletes // diff --git a/routes/_store/mixins/timelineMixins.js b/routes/_store/mixins/timelineMixins.js index aaead78..d965e28 100644 --- a/routes/_store/mixins/timelineMixins.js +++ b/routes/_store/mixins/timelineMixins.js @@ -20,23 +20,22 @@ export function timelineMixins (Store) { return root && root[instanceName] && root[instanceName][timelineName] } + Store.prototype.getAllTimelineData = function (instanceName, key) { + let root = this.get(`timelineData_${key}`) || {} + return root[instanceName] || {} + } + Store.prototype.setForCurrentTimeline = function (obj) { let instanceName = this.get('currentInstance') 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] || {} + Store.prototype.getThreads = function (instanceName) { + let instanceData = this.getAllTimelineData(instanceName, 'timelineItemIds') 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/routes/_utils/ajax.js b/routes/_utils/ajax.js index f28c9e7..a467b57 100644 --- a/routes/_utils/ajax.js +++ b/routes/_utils/ajax.js @@ -52,6 +52,17 @@ async function _get (url, headers, timeout) { return throwErrorIfInvalidResponse(response) } +async function _delete (url, headers, timeout) { + let fetchFunc = timeout ? fetchWithTimeout : fetch + let response = await fetchFunc(url, { + method: 'DELETE', + headers: Object.assign(headers, { + 'Accept': 'application/json' + }) + }) + return throwErrorIfInvalidResponse(response) +} + export async function post (url, body, headers = {}) { return _post(url, body, headers, false) } @@ -68,6 +79,10 @@ export async function get (url, headers = {}) { return _get(url, headers, false) } +export async function deleteWithTimeout (url, headers = {}) { + return _delete(url, headers, true) +} + export function paramsString (paramsObject) { let res = '' Object.keys(paramsObject).forEach((key, i) => { diff --git a/tests/serverActions.js b/tests/serverActions.js index c19ca85..81e7c14 100644 --- a/tests/serverActions.js +++ b/tests/serverActions.js @@ -3,16 +3,23 @@ import fetch from 'node-fetch' import FileApi from 'file-api' import { users } from './users' import { postStatus } from '../routes/_api/statuses' +import { deleteStatus } from '../routes/_api/delete' global.fetch = fetch global.File = FileApi.File global.FormData = FileApi.FormData +const instanceName = 'localhost:3000' + export async function favoriteStatusAsAdmin (statusId) { - return favoriteStatus('localhost:3000', users.admin.accessToken, statusId) + return favoriteStatus(instanceName, users.admin.accessToken, statusId) } export async function postAsAdmin (text) { - return postStatus('localhost:3000', users.admin.accessToken, text, + return postStatus(instanceName, users.admin.accessToken, text, null, null, false, null, 'public') } + +export async function deleteAsAdmin (statusId) { + return deleteStatus(instanceName, users.admin.accessToken, statusId) +} diff --git a/tests/spec/105-deletes.js b/tests/spec/105-deletes.js new file mode 100644 index 0000000..3cb8310 --- /dev/null +++ b/tests/spec/105-deletes.js @@ -0,0 +1,15 @@ +import { foobarRole } from '../roles' +import { getNthStatus } from '../utils' +import { deleteAsAdmin, postAsAdmin } from '../serverActions' + +fixture`105-deletes.js` + .page`http://localhost:4002` + +test('deleted statuses are removed from the timeline', async t => { + await t.useRole(foobarRole) + .hover(getNthStatus(0)) + let status = await postAsAdmin("I'm gonna delete this") + await t.expect(getNthStatus(0).innerText).contains("I'm gonna delete this") + await deleteAsAdmin(status.id) + await t.expect(getNthStatus(0).innerText).notContains("I'm gonna delete this") +})