Store objects to IndexedDB (#6826)
parent
28384c1771
commit
fe398a098e
@ -0,0 +1,76 @@ |
||||
import { putAccounts, putStatuses } from '../../db/modifier'; |
||||
import { normalizeAccount, normalizeStatus } from './normalizer'; |
||||
|
||||
export const ACCOUNT_IMPORT = 'ACCOUNT_IMPORT'; |
||||
export const ACCOUNTS_IMPORT = 'ACCOUNTS_IMPORT'; |
||||
export const STATUS_IMPORT = 'STATUS_IMPORT'; |
||||
export const STATUSES_IMPORT = 'STATUSES_IMPORT'; |
||||
|
||||
function pushUnique(array, object) { |
||||
if (array.every(element => element.id !== object.id)) { |
||||
array.push(object); |
||||
} |
||||
} |
||||
|
||||
export function importAccount(account) { |
||||
return { type: ACCOUNT_IMPORT, account }; |
||||
} |
||||
|
||||
export function importAccounts(accounts) { |
||||
return { type: ACCOUNTS_IMPORT, accounts }; |
||||
} |
||||
|
||||
export function importStatus(status) { |
||||
return { type: STATUS_IMPORT, status }; |
||||
} |
||||
|
||||
export function importStatuses(statuses) { |
||||
return { type: STATUSES_IMPORT, statuses }; |
||||
} |
||||
|
||||
export function importFetchedAccount(account) { |
||||
return importFetchedAccounts([account]); |
||||
} |
||||
|
||||
export function importFetchedAccounts(accounts) { |
||||
const normalAccounts = []; |
||||
|
||||
function processAccount(account) { |
||||
pushUnique(normalAccounts, normalizeAccount(account)); |
||||
|
||||
if (account.moved) { |
||||
processAccount(account); |
||||
} |
||||
} |
||||
|
||||
accounts.forEach(processAccount); |
||||
putAccounts(normalAccounts); |
||||
|
||||
return importAccounts(normalAccounts); |
||||
} |
||||
|
||||
export function importFetchedStatus(status) { |
||||
return importFetchedStatuses([status]); |
||||
} |
||||
|
||||
export function importFetchedStatuses(statuses) { |
||||
return (dispatch, getState) => { |
||||
const accounts = []; |
||||
const normalStatuses = []; |
||||
|
||||
function processStatus(status) { |
||||
pushUnique(normalStatuses, normalizeStatus(status, getState().getIn(['statuses', status.id]))); |
||||
pushUnique(accounts, status.account); |
||||
|
||||
if (status.reblog && status.reblog.id) { |
||||
processStatus(status.reblog); |
||||
} |
||||
} |
||||
|
||||
statuses.forEach(processStatus); |
||||
putStatuses(normalStatuses); |
||||
|
||||
dispatch(importFetchedAccounts(accounts)); |
||||
dispatch(importStatuses(normalStatuses)); |
||||
}; |
||||
} |
@ -0,0 +1,46 @@ |
||||
import escapeTextContentForBrowser from 'escape-html'; |
||||
import emojify from '../../features/emoji/emoji'; |
||||
|
||||
const domParser = new DOMParser(); |
||||
|
||||
export function normalizeAccount(account) { |
||||
account = { ...account }; |
||||
|
||||
const displayName = account.display_name.length === 0 ? account.username : account.display_name; |
||||
account.display_name_html = emojify(escapeTextContentForBrowser(displayName)); |
||||
account.note_emojified = emojify(account.note); |
||||
|
||||
return account; |
||||
} |
||||
|
||||
export function normalizeStatus(status, normalOldStatus) { |
||||
const normalStatus = { ...status }; |
||||
normalStatus.account = status.account.id; |
||||
|
||||
if (status.reblog && status.reblog.id) { |
||||
normalStatus.reblog = status.reblog.id; |
||||
} |
||||
|
||||
// Only calculate these values when status first encountered
|
||||
// Otherwise keep the ones already in the reducer
|
||||
if (normalOldStatus) { |
||||
normalStatus.search_index = normalOldStatus.get('search_index'); |
||||
normalStatus.contentHtml = normalOldStatus.get('contentHtml'); |
||||
normalStatus.spoilerHtml = normalOldStatus.get('spoilerHtml'); |
||||
normalStatus.hidden = normalOldStatus.get('hidden'); |
||||
} else { |
||||
const searchContent = [status.spoiler_text, status.content].join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n'); |
||||
|
||||
const emojiMap = normalStatus.emojis.reduce((obj, emoji) => { |
||||
obj[`:${emoji.shortcode}:`] = emoji; |
||||
return obj; |
||||
}, {}); |
||||
|
||||
normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent; |
||||
normalStatus.contentHtml = emojify(normalStatus.content, emojiMap); |
||||
normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(normalStatus.spoiler_text || ''), emojiMap); |
||||
normalStatus.hidden = normalStatus.sensitive; |
||||
} |
||||
|
||||
return normalStatus; |
||||
} |
@ -0,0 +1,28 @@ |
||||
import { me } from '../initial_state'; |
||||
|
||||
export default new Promise((resolve, reject) => { |
||||
// Microsoft Edge 17 does not support getAll according to:
|
||||
// Catalog of standard and vendor APIs across browsers - Microsoft Edge Development
|
||||
// https://developer.microsoft.com/en-us/microsoft-edge/platform/catalog/?q=specName%3Aindexeddb
|
||||
if (!me || !('getAll' in IDBObjectStore.prototype)) { |
||||
reject(); |
||||
return; |
||||
} |
||||
|
||||
const request = indexedDB.open('mastodon:' + me); |
||||
|
||||
request.onerror = reject; |
||||
request.onsuccess = ({ target }) => resolve(target.result); |
||||
|
||||
request.onupgradeneeded = ({ target }) => { |
||||
const accounts = target.result.createObjectStore('accounts', { autoIncrement: true }); |
||||
const statuses = target.result.createObjectStore('statuses', { autoIncrement: true }); |
||||
|
||||
accounts.createIndex('id', 'id', { unique: true }); |
||||
accounts.createIndex('moved', 'moved'); |
||||
|
||||
statuses.createIndex('id', 'id', { unique: true }); |
||||
statuses.createIndex('account', 'account'); |
||||
statuses.createIndex('reblog', 'reblog'); |
||||
}; |
||||
}); |
@ -0,0 +1,93 @@ |
||||
import asyncDB from './async'; |
||||
|
||||
const limit = 1024; |
||||
|
||||
function put(name, objects, callback) { |
||||
asyncDB.then(db => { |
||||
const putTransaction = db.transaction(name, 'readwrite'); |
||||
const putStore = putTransaction.objectStore(name); |
||||
const putIndex = putStore.index('id'); |
||||
|
||||
objects.forEach(object => { |
||||
function add() { |
||||
putStore.add(object); |
||||
} |
||||
|
||||
putIndex.getKey(object.id).onsuccess = retrieval => { |
||||
if (retrieval.target.result) { |
||||
putStore.delete(retrieval.target.result).onsuccess = add; |
||||
} else { |
||||
add(); |
||||
} |
||||
}; |
||||
}); |
||||
|
||||
putTransaction.oncomplete = () => { |
||||
const readTransaction = db.transaction(name, 'readonly'); |
||||
const readStore = readTransaction.objectStore(name); |
||||
|
||||
readStore.count().onsuccess = count => { |
||||
const excess = count.target.result - limit; |
||||
|
||||
if (excess > 0) { |
||||
readStore.getAll(null, excess).onsuccess = |
||||
retrieval => callback(retrieval.target.result.map(({ id }) => id)); |
||||
} |
||||
}; |
||||
}; |
||||
}); |
||||
} |
||||
|
||||
export function evictAccounts(ids) { |
||||
asyncDB.then(db => { |
||||
const transaction = db.transaction(['accounts', 'statuses'], 'readwrite'); |
||||
const accounts = transaction.objectStore('accounts'); |
||||
const accountsIdIndex = accounts.index('id'); |
||||
const accountsMovedIndex = accounts.index('moved'); |
||||
const statuses = transaction.objectStore('statuses'); |
||||
const statusesIndex = statuses.index('account'); |
||||
|
||||
function evict(toEvict) { |
||||
toEvict.forEach(id => { |
||||
accountsMovedIndex.getAllKeys(id).onsuccess = |
||||
({ target }) => evict(target.result); |
||||
|
||||
statusesIndex.getAll(id).onsuccess = |
||||
({ target }) => evictStatuses(target.result.map(({ id }) => id)); |
||||
|
||||
accountsIdIndex.getKey(id).onsuccess = |
||||
({ target }) => target.result && accounts.delete(target.result); |
||||
}); |
||||
} |
||||
|
||||
evict(ids); |
||||
}); |
||||
} |
||||
|
||||
export function evictStatus(id) { |
||||
return evictStatuses([id]); |
||||
} |
||||
|
||||
export function evictStatuses(ids) { |
||||
asyncDB.then(db => { |
||||
const store = db.transaction('statuses', 'readwrite').objectStore('statuses'); |
||||
const idIndex = store.index('id'); |
||||
const reblogIndex = store.index('reblog'); |
||||
|
||||
ids.forEach(id => { |
||||
reblogIndex.getAllKeys(id).onsuccess = |
||||
({ target }) => target.result.forEach(reblogKey => store.delete(reblogKey)); |
||||
|
||||
idIndex.getKey(id).onsuccess = |
||||
({ target }) => target.result && store.delete(target.result); |
||||
}); |
||||
}); |
||||
} |
||||
|
||||
export function putAccounts(records) { |
||||
put('accounts', records, evictAccounts); |
||||
} |
||||
|
||||
export function putStatuses(records) { |
||||
put('statuses', records, evictStatuses); |
||||
} |
Loading…
Reference in new issue