From c8cb4354e3626ea3587937e6fb8d749393ef95ea Mon Sep 17 00:00:00 2001 From: Nolan Lawson Date: Sat, 3 Feb 2018 18:06:02 -0800 Subject: [PATCH] add ability to fetch and store notifications --- routes/_actions/timeline.js | 72 ++++++++----------- .../notification/Notification.html | 7 ++ .../timeline/NotificationVirtualListItem.html | 16 +++++ routes/_components/timeline/Timeline.html | 40 ++++++++--- routes/_store/timelineComputations.js | 4 +- routes/_utils/database/cache.js | 7 +- routes/_utils/database/constants.js | 6 +- routes/_utils/database/databaseCore.js | 69 ++++++++++++------ routes/_utils/database/databaseLifecycle.js | 27 +++---- routes/_utils/mastodon/timelines.js | 2 + routes/notifications.html | 14 +++- 11 files changed, 166 insertions(+), 98 deletions(-) create mode 100644 routes/_components/notification/Notification.html create mode 100644 routes/_components/timeline/NotificationVirtualListItem.html diff --git a/routes/_actions/timeline.js b/routes/_actions/timeline.js index 9f010235..38baea91 100644 --- a/routes/_actions/timeline.js +++ b/routes/_actions/timeline.js @@ -2,55 +2,51 @@ import { store } from '../_store/store' import { database } from '../_utils/database/database' import { getTimeline } from '../_utils/mastodon/timelines' import { toast } from '../_utils/toast' -import { StatusStream } from '../_utils/mastodon/StatusStream' -import { getInstanceInfo } from '../_utils/mastodon/instance' import { mark, stop } from '../_utils/marks' import { mergeArrays } from '../_utils/arrays' const FETCH_LIMIT = 20 -let statusStream - -async function fetchStatuses(instanceName, accessToken, timelineName, lastStatusId, online) { - mark('fetchStatuses') - let statuses +async function fetchTimelineItems(instanceName, accessToken, timelineName, lastTimelineItemId, online) { + mark('fetchTimelineItems') + let items if (!online) { - statuses = await database.getTimeline(instanceName, timelineName, lastStatusId, FETCH_LIMIT) + items = await database.getTimeline(instanceName, timelineName, lastTimelineItemId, FETCH_LIMIT) } else { try { - statuses = await getTimeline(instanceName, accessToken, timelineName, lastStatusId, FETCH_LIMIT) - /* no await */ database.insertStatuses(instanceName, timelineName, statuses) + items = await getTimeline(instanceName, accessToken, timelineName, lastTimelineItemId, FETCH_LIMIT) + /* no await */ database.insertTimelineItems(instanceName, timelineName, items) } catch (e) { console.error(e) toast.say('Internet request failed. Showing offline content.') - statuses = await database.getTimeline(instanceName, timelineName, lastStatusId, FETCH_LIMIT) + items = await database.getTimeline(instanceName, timelineName, lastTimelineItemId, FETCH_LIMIT) } } - stop('fetchStatuses') - return statuses + stop('fetchTimelineItems') + return items } -async function addStatuses(instanceName, timelineName, newStatuses) { - console.log('addStatuses, length:', newStatuses.length) - mark('addStatuses') - let newStatusIds = newStatuses.map(status => status.id) - let oldStatusIds = store.getForTimeline(instanceName, timelineName, 'statusIds') || [] - let merged = mergeArrays(oldStatusIds, newStatusIds) - store.setForTimeline(instanceName, timelineName, { statusIds: merged }) - stop('addStatuses') +async function addTimelineItems(instanceName, timelineName, newItems) { + console.log('addTimelineItems, length:', newItems.length) + mark('addTimelineItems') + let newIds = newItems.map(item => item.id) + let oldIds = store.getForTimeline(instanceName, timelineName, 'timelineItemIds') || [] + let merged = mergeArrays(oldIds, newIds) + store.setForTimeline(instanceName, timelineName, { timelineItemIds: merged }) + stop('addTimelineItems') } -async function fetchStatusesAndPossiblyFallBack() { - mark('fetchStatusesAndPossiblyFallBack') +async function fetchTimelineItemsAndPossiblyFallBack() { + mark('fetchTimelineItemsAndPossiblyFallBack') let timelineName = store.get('currentTimeline') let instanceName = store.get('currentInstance') let accessToken = store.get('accessToken') - let lastStatusId = store.get('lastStatusId') + let lastTimelineItemId = store.get('lastTimelineItemId') let online = store.get('online') - let newStatuses = await fetchStatuses(instanceName, accessToken, timelineName, lastStatusId, online) - addStatuses(instanceName, timelineName, newStatuses) - stop('fetchStatusesAndPossiblyFallBack') + let newItems = await fetchTimelineItems(instanceName, accessToken, timelineName, lastTimelineItemId, online) + addTimelineItems(instanceName, timelineName, newItems) + stop('fetchTimelineItemsAndPossiblyFallBack') } export function initializeTimeline() { @@ -66,30 +62,20 @@ export function initializeTimeline() { } export async function setupTimeline() { - mark('addStatuses') + mark('setupTimeline') let timelineName = store.get('currentTimeline') let instanceName = store.get('currentInstance') let accessToken = store.get('accessToken') - if (!store.get('statusIds').length) { - await fetchStatusesAndPossiblyFallBack() + if (!store.get('timelineItemIds').length) { + await fetchTimelineItemsAndPossiblyFallBack() } - /* no await */ getInstanceInfo(instanceName).then(instanceInfo => database.setInstanceInfo(instanceName, instanceInfo)) - let instanceInfo = await database.getInstanceInfo(instanceName) - if (statusStream) { - statusStream.close() - } - /*statusStream = new StatusStream(instanceInfo.urls.streaming_api, accessToken, timelineName, { - onMessage(message) { - console.log('message', message) - } - })*/ - stop('addStatuses') + stop('setupTimeline') } -export async function fetchStatusesOnScrollToBottom() { +export async function fetchTimelineItemsOnScrollToBottom() { let timelineName = store.get('currentTimeline') let instanceName = store.get('currentInstance') store.setForTimeline(instanceName, timelineName, { runningUpdate: true }) - await fetchStatusesAndPossiblyFallBack() + await fetchTimelineItemsAndPossiblyFallBack() store.setForTimeline(instanceName, timelineName, { runningUpdate: false }) } \ No newline at end of file diff --git a/routes/_components/notification/Notification.html b/routes/_components/notification/Notification.html new file mode 100644 index 00000000..ce5a6855 --- /dev/null +++ b/routes/_components/notification/Notification.html @@ -0,0 +1,7 @@ +
+ Notification +
\ No newline at end of file diff --git a/routes/_components/timeline/NotificationVirtualListItem.html b/routes/_components/timeline/NotificationVirtualListItem.html new file mode 100644 index 00000000..18e9221e --- /dev/null +++ b/routes/_components/timeline/NotificationVirtualListItem.html @@ -0,0 +1,16 @@ + + \ No newline at end of file diff --git a/routes/_components/timeline/Timeline.html b/routes/_components/timeline/Timeline.html index ac5b2dd2..55ce421a 100644 --- a/routes/_components/timeline/Timeline.html +++ b/routes/_components/timeline/Timeline.html @@ -4,10 +4,21 @@ {{/if}} - {{#if virtual}} + {{#if timelineType === 'notifications'}} + + {{elseif virtual}} import { store } from '../../_store/store' import StatusVirtualListItem from './StatusVirtualListItem.html' + import NotificationVirtualListItem from './NotificationVirtualListItem.html' import Status from '../status/Status.html' import PseudoVirtualList from '../pseudoVirtualList/PseudoVirtualList.html' import LoadingFooter from './LoadingFooter.html' import VirtualList from '../virtualList/VirtualList.html' import { timelines } from '../../_static/timelines' import { database } from '../../_utils/database/database' - import { initializeTimeline, fetchStatusesOnScrollToBottom, setupTimeline } from '../../_actions/timeline' + import { initializeTimeline, fetchTimelineItemsOnScrollToBottom, setupTimeline } from '../../_actions/timeline' import LoadingSpinner from '../LoadingSpinner.html' export default { @@ -64,15 +76,20 @@ }, data: () => ({ StatusVirtualListItem, + NotificationVirtualListItem, LoadingFooter, Status }), computed: { - makeProps: ($currentInstance, timelineType, timelineValue) => async (statusId) => ({ - timelineType: timelineType, - timelineValue: timelineValue, - status: await database.getStatus($currentInstance, statusId) - }), + makeProps: ($currentInstance, timelineType, timelineValue) => async (itemId) => { + let res = { timelineType, timelineValue } + if (timelineType === 'notifications') { + res.notification = await database.getNotification($currentInstance, itemId) + } else { + res.status = await database.getStatus($currentInstance, itemId) + } + return res + }, label: (timeline, $currentInstance, timelineType, timelineValue) => { if (timelines[timeline]) { return `${timelines[timeline].label} timeline for ${$currentInstance}` @@ -101,11 +118,12 @@ components: { VirtualList, PseudoVirtualList, + NotificationVirtualListItem, LoadingSpinner }, methods: { initialize() { - if (this.store.get('initialized') || !this.store.get('statusIds') || !this.store.get('statusIds').length) { + if (this.store.get('initialized') || !this.store.get('timelineItemIds') || !this.store.get('timelineItemIds').length) { return } console.log('timeline initialize()') @@ -117,7 +135,7 @@ this.get('timelineType') === 'status') { // for status contexts, we've already fetched the whole thread return } - fetchStatusesOnScrollToBottom() + fetchTimelineItemsOnScrollToBottom() } } } diff --git a/routes/_store/timelineComputations.js b/routes/_store/timelineComputations.js index bad286a8..58454441 100644 --- a/routes/_store/timelineComputations.js +++ b/routes/_store/timelineComputations.js @@ -4,8 +4,8 @@ export function timelineComputations(store) { return ((timelines && timelines[currentInstance]) || {})[currentTimeline] || {} }) - store.compute('statusIds', ['currentTimelineData'], (currentTimelineData) => currentTimelineData.statusIds || []) + store.compute('timelineItemIds', ['currentTimelineData'], (currentTimelineData) => currentTimelineData.timelineItemIds || []) store.compute('runningUpdate', ['currentTimelineData'], (currentTimelineData) => currentTimelineData.runningUpdate) store.compute('initialized', ['currentTimelineData'], (currentTimelineData) => currentTimelineData.initialized) - store.compute('lastStatusId', ['statusIds'], (statusIds) => statusIds.length && statusIds[statusIds.length - 1]) + store.compute('lastTimelineItemId', ['timelineItemIds'], (timelineItemIds) => timelineItemIds.length && timelineItemIds[timelineItemIds.length - 1]) } \ No newline at end of file diff --git a/routes/_utils/database/cache.js b/routes/_utils/database/cache.js index e2deb129..e6be0c22 100644 --- a/routes/_utils/database/cache.js +++ b/routes/_utils/database/cache.js @@ -16,13 +16,18 @@ export const metaCache = { maxSize: 20, caches: {} } +export const notificationsCache = { + maxSize: 50, + caches: {} +} if (process.browser && process.env.NODE_ENV !== 'production') { window.cacheStats = { statuses: statusesCache, accounts: accountsCache, relationships: relationshipsCache, - meta: metaCache + meta: metaCache, + notifications: notificationsCache } } diff --git a/routes/_utils/database/constants.js b/routes/_utils/database/constants.js index 7bf95bb6..0959086d 100644 --- a/routes/_utils/database/constants.js +++ b/routes/_utils/database/constants.js @@ -1,5 +1,7 @@ export const STATUSES_STORE = 'statuses' -export const TIMELINE_STORE = 'timelines' +export const STATUS_TIMELINES_STORE = 'status_timelines' export const META_STORE = 'meta' export const ACCOUNTS_STORE = 'accounts' -export const RELATIONSHIPS_STORE = 'relationships' \ No newline at end of file +export const RELATIONSHIPS_STORE = 'relationships' +export const NOTIFICATIONS_STORE = 'notifications' +export const NOTIFICATION_TIMELINES_STORE = 'notification_timelines' \ No newline at end of file diff --git a/routes/_utils/database/databaseCore.js b/routes/_utils/database/databaseCore.js index 7ec23cb1..0c4cdaa1 100644 --- a/routes/_utils/database/databaseCore.js +++ b/routes/_utils/database/databaseCore.js @@ -9,10 +9,11 @@ import { import { META_STORE, - TIMELINE_STORE, + STATUS_TIMELINES_STORE, STATUSES_STORE, ACCOUNTS_STORE, - RELATIONSHIPS_STORE + RELATIONSHIPS_STORE, + NOTIFICATIONS_STORE, NOTIFICATION_TIMELINES_STORE } from './constants' import { @@ -20,6 +21,7 @@ import { relationshipsCache, accountsCache, metaCache, + notificationsCache, clearCache, getInCache, hasInCache, @@ -51,13 +53,29 @@ async function setGenericEntityWithId(store, cache, instanceName, entity) { } // -// timelines/statuses +// timelines/statuses/notifications // +function getTimelineVariables(timeline) { + if (timeline === 'notifications') { + return { + stores: [NOTIFICATION_TIMELINES_STORE, NOTIFICATIONS_STORE, ACCOUNTS_STORE], + remoteId: 'notificationId', + itemsCache: notificationsCache + } + } + return { + stores: [STATUS_TIMELINES_STORE, STATUSES_STORE, ACCOUNTS_STORE], + remoteId: 'statusId', + itemsCache: statusesCache + } +} + export async function getTimeline(instanceName, timeline, maxId = null, limit = 20) { - const db = await getDatabase(instanceName, timeline) - return await dbPromise(db, [TIMELINE_STORE, STATUSES_STORE], 'readonly', (stores, callback) => { - let [ timelineStore, statusesStore ] = stores + let { stores, remoteId } = getTimelineVariables(timeline) + const db = await getDatabase(instanceName) + return await dbPromise(db, stores, 'readonly', (stores, callback) => { + let [ timelineStore, itemsStore ] = stores let negBigInt = maxId && toReversePaddedBigInt(maxId) let start = negBigInt ? (timeline + '\u0000' + negBigInt) : (timeline + '\u0000') @@ -68,7 +86,7 @@ export async function getTimeline(instanceName, timeline, maxId = null, limit = let timelineResults = e.target.result let res = new Array(timelineResults.length) timelineResults.forEach((timelineResult, i) => { - statusesStore.get(timelineResult.statusId).onsuccess = e => { + itemsStore.get(timelineResult[remoteId]).onsuccess = e => { res[i] = e.target.result } }) @@ -77,27 +95,28 @@ export async function getTimeline(instanceName, timeline, maxId = null, limit = }) } -export async function insertStatuses(instanceName, timeline, statuses) { - for (let status of statuses) { - setInCache(statusesCache, instanceName, status.id, status) - setInCache(accountsCache, instanceName, status.account.id, status.account) - if (status.reblog) { - setInCache(accountsCache, instanceName, status.reblog.account.id, status.reblog.account) +export async function insertTimelineItems(instanceName, timeline, timelineItems) { + let { stores, remoteId, itemsCache } = getTimelineVariables(timeline) + for (let timelineItem of timelineItems) { + setInCache(itemsCache, instanceName, timelineItem.id, timelineItem) + setInCache(accountsCache, instanceName, timelineItem.account.id, timelineItem.account) + if (timelineItem.reblog) { + setInCache(accountsCache, instanceName, timelineItem.reblog.account.id, timelineItem.reblog.account) } } - const db = await getDatabase(instanceName, timeline) - await dbPromise(db, [TIMELINE_STORE, STATUSES_STORE, ACCOUNTS_STORE], 'readwrite', (stores) => { - let [ timelineStore, statusesStore, accountsStore ] = stores - for (let status of statuses) { - statusesStore.put(status) + const db = await getDatabase(instanceName) + await dbPromise(db, stores, 'readwrite', (stores) => { + let [ timelineStore, itemsStore, accountsStore ] = stores + for (let item of timelineItems) { + itemsStore.put(item) // reverse chronological order, prefixed by timeline timelineStore.put({ - id: (timeline + '\u0000' + toReversePaddedBigInt(status.id)), - statusId: status.id + id: (timeline + '\u0000' + toReversePaddedBigInt(item.id)), + [remoteId]: item.id }) - accountsStore.put(status.account) - if (status.reblog) { - accountsStore.put(status.reblog.account) + accountsStore.put(item.account) + if (item.reblog) { + accountsStore.put(item.reblog.account) } } }) @@ -107,6 +126,10 @@ export async function getStatus(instanceName, statusId) { return await getGenericEntityWithId(STATUSES_STORE, statusesCache, instanceName, statusId) } +export async function getNotification(instanceName, notificationId) { + return await getGenericEntityWithId(NOTIFICATIONS_STORE, notificationsCache, instanceName, notificationId) +} + // // meta // diff --git a/routes/_utils/database/databaseLifecycle.js b/routes/_utils/database/databaseLifecycle.js index 49495640..2e61adfb 100644 --- a/routes/_utils/database/databaseLifecycle.js +++ b/routes/_utils/database/databaseLifecycle.js @@ -1,14 +1,16 @@ const openReqs = {} const databaseCache = {} -const DB_VERSION = 2 +const DB_VERSION = 1 import { META_STORE, - TIMELINE_STORE, + STATUS_TIMELINES_STORE, STATUSES_STORE, ACCOUNTS_STORE, - RELATIONSHIPS_STORE + RELATIONSHIPS_STORE, + NOTIFICATIONS_STORE, + NOTIFICATION_TIMELINES_STORE } from './constants' export function getDatabase(instanceName) { @@ -28,16 +30,15 @@ export function getDatabase(instanceName) { } req.onupgradeneeded = (e) => { let db = req.result; - if (e.oldVersion < 1) { - db.createObjectStore(META_STORE, {keyPath: 'key'}) - db.createObjectStore(STATUSES_STORE, {keyPath: 'id'}) - db.createObjectStore(ACCOUNTS_STORE, {keyPath: 'id'}) - let timelineStore = db.createObjectStore(TIMELINE_STORE, {keyPath: 'id'}) - timelineStore.createIndex('statusId', 'statusId') - } - if (e.oldVersion < 2) { - db.createObjectStore(RELATIONSHIPS_STORE, {keyPath: 'id'}) - } + db.createObjectStore(META_STORE, {keyPath: 'key'}) + db.createObjectStore(STATUSES_STORE, {keyPath: 'id'}) + db.createObjectStore(ACCOUNTS_STORE, {keyPath: 'id'}) + db.createObjectStore(RELATIONSHIPS_STORE, {keyPath: 'id'}) + db.createObjectStore(NOTIFICATIONS_STORE, {keyPath: 'id'}) + db.createObjectStore(STATUS_TIMELINES_STORE, {keyPath: 'id'}) + .createIndex('statusId', 'statusId') + db.createObjectStore(NOTIFICATION_TIMELINES_STORE, {keyPath: 'id'}) + .createIndex('notificationId', 'notificationId') } req.onsuccess = () => resolve(req.result) }) diff --git a/routes/_utils/mastodon/timelines.js b/routes/_utils/mastodon/timelines.js index d6eae0d8..028ed325 100644 --- a/routes/_utils/mastodon/timelines.js +++ b/routes/_utils/mastodon/timelines.js @@ -8,6 +8,8 @@ function getTimelineUrlPath(timeline) { return 'timelines/public' case 'home': return 'timelines/home' + case 'notifications': + return 'notifications' } if (timeline.startsWith('tag/')) { return 'timelines/tag' diff --git a/routes/notifications.html b/routes/notifications.html index 681fe897..b250362d 100644 --- a/routes/notifications.html +++ b/routes/notifications.html @@ -2,7 +2,10 @@ Pinafore – Notifications - + + {{#if $isUserLoggedIn}} + + {{else}}

Notifications

@@ -10,18 +13,23 @@

Your notifications will appear here when logged in.

+ {{/if}}
\ No newline at end of file