basic support for delete streaming
This commit is contained in:
parent
d4e48ac6fa
commit
23a247a8c2
|
@ -38,7 +38,7 @@ async function insertUpdatesIntoThreads (instanceName, updates) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let threads = store.getThreadsForTimeline(instanceName)
|
let threads = store.getThreads(instanceName)
|
||||||
|
|
||||||
for (let timelineName of Object.keys(threads)) {
|
for (let timelineName of Object.keys(threads)) {
|
||||||
let thread = threads[timelineName]
|
let thread = threads[timelineName]
|
||||||
|
|
|
@ -1,21 +1,28 @@
|
||||||
import { getIdsThatRebloggedThisStatus, getIdThatThisStatusReblogged, getNotificationIdsForStatuses } from './statuses'
|
import { getIdsThatRebloggedThisStatus, getNotificationIdsForStatuses } from './statuses'
|
||||||
import { store } from '../_store/store'
|
import { store } from '../_store/store'
|
||||||
import { scheduleIdleTask } from '../_utils/scheduleIdleTask'
|
import { scheduleIdleTask } from '../_utils/scheduleIdleTask'
|
||||||
import { database } from '../_database/database'
|
import { database } from '../_database/database'
|
||||||
|
import forEach from 'lodash/forEach'
|
||||||
|
|
||||||
function deleteStatusIdsFromStore (instanceName, idsToDelete) {
|
function deleteStatusIdsFromStore (instanceName, idsToDelete) {
|
||||||
let idsToDeleteSet = new Set(idsToDelete)
|
let idsToDeleteSet = new Set(idsToDelete)
|
||||||
let timelines = store.get('timelines')
|
let idWasNotDeleted = id => !idsToDeleteSet.has(id)
|
||||||
if (timelines && timelines[instanceName]) {
|
|
||||||
Object.keys(timelines[instanceName]).forEach(timelineName => {
|
let timelinesToTimelineItemIds = store.getAllTimelineData(instanceName, 'timelineItemIds')
|
||||||
let timelineData = timelines[instanceName][timelineName]
|
|
||||||
if (timelineName !== 'notifications') {
|
forEach(timelinesToTimelineItemIds, (timelineItemIds, timelineName) => {
|
||||||
timelineData.timelineItemIds = timelineData.timelineItemIds.filter(_ => !idsToDeleteSet.has(_))
|
store.setForTimeline(instanceName, timelineName, {
|
||||||
timelineData.itemIdsToAdd = timelineData.itemIdsToAdd.filter(_ => !idsToDeleteSet.has(_))
|
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) {
|
async function deleteStatusesAndNotifications (instanceName, statusIdsToDelete, notificationIdsToDelete) {
|
||||||
|
@ -24,9 +31,9 @@ async function deleteStatusesAndNotifications (instanceName, statusIdsToDelete,
|
||||||
}
|
}
|
||||||
|
|
||||||
async function doDeleteStatus (instanceName, statusId) {
|
async function doDeleteStatus (instanceName, statusId) {
|
||||||
let reblogId = await getIdThatThisStatusReblogged(instanceName, statusId)
|
console.log('deleting statusId', statusId)
|
||||||
let rebloggedIds = await getIdsThatRebloggedThisStatus(reblogId || statusId)
|
let rebloggedIds = await getIdsThatRebloggedThisStatus(instanceName, statusId)
|
||||||
let statusIdsToDelete = Array.from(new Set([statusId, reblogId].concat(rebloggedIds).filter(Boolean)))
|
let statusIdsToDelete = Array.from(new Set([statusId].concat(rebloggedIds).filter(Boolean)))
|
||||||
let notificationIdsToDelete = new Set(await getNotificationIdsForStatuses(instanceName, statusIdsToDelete))
|
let notificationIdsToDelete = new Set(await getNotificationIdsForStatuses(instanceName, statusIdsToDelete))
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
deleteStatusesAndNotifications(instanceName, statusIdsToDelete, notificationIdsToDelete)
|
deleteStatusesAndNotifications(instanceName, statusIdsToDelete, notificationIdsToDelete)
|
||||||
|
|
|
@ -7,16 +7,15 @@ import { addStatusOrNotification } from './addStatusOrNotification'
|
||||||
function processMessage (instanceName, timelineName, message) {
|
function processMessage (instanceName, timelineName, message) {
|
||||||
mark('processMessage')
|
mark('processMessage')
|
||||||
let { event, payload } = message
|
let { event, payload } = message
|
||||||
let parsedPayload = JSON.parse(payload)
|
|
||||||
switch (event) {
|
switch (event) {
|
||||||
case 'delete':
|
case 'delete':
|
||||||
deleteStatus(instanceName, parsedPayload)
|
deleteStatus(instanceName, payload)
|
||||||
break
|
break
|
||||||
case 'update':
|
case 'update':
|
||||||
addStatusOrNotification(instanceName, timelineName, parsedPayload)
|
addStatusOrNotification(instanceName, timelineName, JSON.parse(payload))
|
||||||
break
|
break
|
||||||
case 'notification':
|
case 'notification':
|
||||||
addStatusOrNotification(instanceName, 'notifications', parsedPayload)
|
addStatusOrNotification(instanceName, 'notifications', JSON.parse(payload))
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
stop('processMessage')
|
stop('processMessage')
|
||||||
|
|
7
routes/_api/delete.js
Normal file
7
routes/_api/delete.js
Normal file
|
@ -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))
|
||||||
|
}
|
|
@ -9,13 +9,14 @@ import {
|
||||||
PINNED_STATUSES_STORE,
|
PINNED_STATUSES_STORE,
|
||||||
TIMESTAMP,
|
TIMESTAMP,
|
||||||
REBLOG_ID,
|
REBLOG_ID,
|
||||||
THREADS_STORE
|
THREADS_STORE,
|
||||||
|
STATUS_ID
|
||||||
} from './constants'
|
} from './constants'
|
||||||
|
|
||||||
const openReqs = {}
|
const openReqs = {}
|
||||||
const databaseCache = {}
|
const databaseCache = {}
|
||||||
|
|
||||||
const DB_VERSION = 5
|
const DB_VERSION = 6
|
||||||
|
|
||||||
export function getDatabase (instanceName) {
|
export function getDatabase (instanceName) {
|
||||||
if (!instanceName) {
|
if (!instanceName) {
|
||||||
|
@ -54,6 +55,9 @@ export function getDatabase (instanceName) {
|
||||||
tx.objectStore(NOTIFICATIONS_STORE).createIndex('statusId', 'statusId')
|
tx.objectStore(NOTIFICATIONS_STORE).createIndex('statusId', 'statusId')
|
||||||
db.createObjectStore(THREADS_STORE)
|
db.createObjectStore(THREADS_STORE)
|
||||||
}
|
}
|
||||||
|
if (e.oldVersion < 6) {
|
||||||
|
tx.objectStore(NOTIFICATIONS_STORE).createIndex(STATUS_ID, STATUS_ID)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
req.onsuccess = () => resolve(req.result)
|
req.onsuccess = () => resolve(req.result)
|
||||||
})
|
})
|
||||||
|
|
|
@ -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
|
// deletes
|
||||||
//
|
//
|
||||||
|
|
|
@ -20,23 +20,22 @@ export function timelineMixins (Store) {
|
||||||
return root && root[instanceName] && root[instanceName][timelineName]
|
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) {
|
Store.prototype.setForCurrentTimeline = function (obj) {
|
||||||
let instanceName = this.get('currentInstance')
|
let instanceName = this.get('currentInstance')
|
||||||
let timelineName = this.get('currentTimeline')
|
let timelineName = this.get('currentTimeline')
|
||||||
this.setForTimeline(instanceName, timelineName, obj)
|
this.setForTimeline(instanceName, timelineName, obj)
|
||||||
}
|
}
|
||||||
|
|
||||||
Store.prototype.getThreadsForTimeline = function (instanceName) {
|
Store.prototype.getThreads = function (instanceName) {
|
||||||
let root = this.get('timelineData_timelineItemIds') || {}
|
let instanceData = this.getAllTimelineData(instanceName, 'timelineItemIds')
|
||||||
let instanceData = root[instanceName] = root[instanceName] || {}
|
|
||||||
|
|
||||||
return pickBy(instanceData, (value, key) => {
|
return pickBy(instanceData, (value, key) => {
|
||||||
return key.startsWith('status/')
|
return key.startsWith('status/')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
Store.prototype.getThreadsForCurrentTimeline = function () {
|
|
||||||
let instanceName = this.get('currentInstance')
|
|
||||||
return this.getThreadsForTimeline(instanceName)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,6 +52,17 @@ async function _get (url, headers, timeout) {
|
||||||
return throwErrorIfInvalidResponse(response)
|
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 = {}) {
|
export async function post (url, body, headers = {}) {
|
||||||
return _post(url, body, headers, false)
|
return _post(url, body, headers, false)
|
||||||
}
|
}
|
||||||
|
@ -68,6 +79,10 @@ export async function get (url, headers = {}) {
|
||||||
return _get(url, headers, false)
|
return _get(url, headers, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function deleteWithTimeout (url, headers = {}) {
|
||||||
|
return _delete(url, headers, true)
|
||||||
|
}
|
||||||
|
|
||||||
export function paramsString (paramsObject) {
|
export function paramsString (paramsObject) {
|
||||||
let res = ''
|
let res = ''
|
||||||
Object.keys(paramsObject).forEach((key, i) => {
|
Object.keys(paramsObject).forEach((key, i) => {
|
||||||
|
|
|
@ -3,16 +3,23 @@ import fetch from 'node-fetch'
|
||||||
import FileApi from 'file-api'
|
import FileApi from 'file-api'
|
||||||
import { users } from './users'
|
import { users } from './users'
|
||||||
import { postStatus } from '../routes/_api/statuses'
|
import { postStatus } from '../routes/_api/statuses'
|
||||||
|
import { deleteStatus } from '../routes/_api/delete'
|
||||||
|
|
||||||
global.fetch = fetch
|
global.fetch = fetch
|
||||||
global.File = FileApi.File
|
global.File = FileApi.File
|
||||||
global.FormData = FileApi.FormData
|
global.FormData = FileApi.FormData
|
||||||
|
|
||||||
|
const instanceName = 'localhost:3000'
|
||||||
|
|
||||||
export async function favoriteStatusAsAdmin (statusId) {
|
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) {
|
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')
|
null, null, false, null, 'public')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function deleteAsAdmin (statusId) {
|
||||||
|
return deleteStatus(instanceName, users.admin.accessToken, statusId)
|
||||||
|
}
|
||||||
|
|
15
tests/spec/105-deletes.js
Normal file
15
tests/spec/105-deletes.js
Normal file
|
@ -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")
|
||||||
|
})
|
Loading…
Reference in a new issue