forked from cybrespace/pinafore
		
	first stab at online mode
This commit is contained in:
		
							parent
							
								
									1c354817a6
								
							
						
					
					
						commit
						90762897db
					
				
					 10 changed files with 159 additions and 97 deletions
				
			
		
							
								
								
									
										5
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										5
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							|  | @ -3282,11 +3282,6 @@ | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "idb": { |  | ||||||
|       "version": "2.0.4", |  | ||||||
|       "resolved": "https://registry.npmjs.org/idb/-/idb-2.0.4.tgz", |  | ||||||
|       "integrity": "sha512-Nw4ykKrrVje6YODRiRm/k2ucFEQeoY+zrkszfOuzVmxx8yyBMtZh2KLaRCKk9r5GzhuF0QlNCVjBewP2n5OZ7Q==" |  | ||||||
|     }, |  | ||||||
|     "ieee754": { |     "ieee754": { | ||||||
|       "version": "1.1.8", |       "version": "1.1.8", | ||||||
|       "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz", |       "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz", | ||||||
|  |  | ||||||
|  | @ -23,7 +23,6 @@ | ||||||
|     "fg-loadcss": "^2.0.1", |     "fg-loadcss": "^2.0.1", | ||||||
|     "font-awesome-svg-png": "^1.2.2", |     "font-awesome-svg-png": "^1.2.2", | ||||||
|     "glob": "^7.1.2", |     "glob": "^7.1.2", | ||||||
|     "idb": "^2.0.4", |  | ||||||
|     "intersection-observer": "^0.5.0", |     "intersection-observer": "^0.5.0", | ||||||
|     "intl-relativeformat": "^2.1.0", |     "intl-relativeformat": "^2.1.0", | ||||||
|     "lodash": "^4.17.4", |     "lodash": "^4.17.4", | ||||||
|  |  | ||||||
|  | @ -20,16 +20,17 @@ | ||||||
| 	export default { | 	export default { | ||||||
| 	  oncreate() { | 	  oncreate() { | ||||||
|       mark('onCreate Layout') |       mark('onCreate Layout') | ||||||
|  |       let node = this.refs.node | ||||||
|       this.observe('innerHeight', debounce(() => { |       this.observe('innerHeight', debounce(() => { | ||||||
|         // respond to window resize events |         // respond to window resize events | ||||||
|         this.store.set({ |         this.store.set({ | ||||||
|           offsetHeight: this.refs.node.offsetHeight |           offsetHeight: node.offsetHeight | ||||||
|         }) |         }) | ||||||
|       }, RESIZE_EVENT_DELAY)) |       }, RESIZE_EVENT_DELAY)) | ||||||
| 	    this.store.set({ | 	    this.store.set({ | ||||||
|         scrollTop: this.refs.node.scrollTop, |         scrollTop: node.scrollTop, | ||||||
|         scrollHeight: this.refs.node.scrollHeight, |         scrollHeight: node.scrollHeight, | ||||||
|         offsetHeight: this.refs.node.offsetHeight |         offsetHeight: node.offsetHeight | ||||||
|       }) |       }) | ||||||
|       stop('onCreate Layout') |       stop('onCreate Layout') | ||||||
|     }, |     }, | ||||||
|  |  | ||||||
|  | @ -1,3 +1,4 @@ | ||||||
|  | <:Window bind:online /> | ||||||
| <div class="timeline" role="feed" aria-label="{{label}}"> | <div class="timeline" role="feed" aria-label="{{label}}"> | ||||||
|   <VirtualList component="{{StatusListItem}}" |   <VirtualList component="{{StatusListItem}}" | ||||||
|                items="{{keyedStatuses}}" |                items="{{keyedStatuses}}" | ||||||
|  | @ -10,20 +11,32 @@ | ||||||
| </style> | </style> | ||||||
| <script> | <script> | ||||||
|   import { store } from '../_utils/store' |   import { store } from '../_utils/store' | ||||||
|   import { getHomeTimeline } from '../_utils/mastodon/oauth' |   import { getHomeTimeline } from '../_utils/mastodon/timelines' | ||||||
|   import StatusListItem from './StatusListItem.html' |   import StatusListItem from './StatusListItem.html' | ||||||
|   import VirtualList from './VirtualList.html' |   import VirtualList from './VirtualList.html' | ||||||
|   import { splice, push } from 'svelte-extras' |   import { splice, push } from 'svelte-extras' | ||||||
|  |   import { | ||||||
|  |     insertStatuses as insertStatusesIntoDatabase, | ||||||
|  |     getTimelineAfter as getTimelineFromDatabaseAfter | ||||||
|  |   } from '../_utils/database/statuses' | ||||||
|  |   import { mergeStatuses } from '../_utils/statuses' | ||||||
|   import { mark, stop } from '../_utils/marks' |   import { mark, stop } from '../_utils/marks' | ||||||
| 
 | 
 | ||||||
|  |   const FETCH_LIMIT = 20 | ||||||
|  | 
 | ||||||
|   export default { |   export default { | ||||||
|     async oncreate() { |     async oncreate() { | ||||||
|  |       this.observe('online', e => console.log(e)) | ||||||
|       let instanceName = this.store.get('currentInstance') |       let instanceName = this.store.get('currentInstance') | ||||||
|       let instanceData = this.store.get('currentInstanceData') |       let instanceData = this.store.get('currentInstanceData') | ||||||
|       let statuses = await getHomeTimeline(instanceName, instanceData.access_token, null, 10) |       let online = this.get('online') | ||||||
|       this.set({ |       let statuses = online ? | ||||||
|         statuses: statuses, |         await getHomeTimeline(instanceName, instanceData.access_token, null, FETCH_LIMIT) : | ||||||
|       }) |         await getTimelineFromDatabaseAfter(null, FETCH_LIMIT) | ||||||
|  |       if (!online) { | ||||||
|  |         insertStatusesIntoDatabase(statuses) | ||||||
|  |       } | ||||||
|  |       this.addStatuses(statuses) | ||||||
|     }, |     }, | ||||||
|     data: () => ({ |     data: () => ({ | ||||||
|       target: 'home', |       target: 'home', | ||||||
|  | @ -55,16 +68,30 @@ | ||||||
|         let lastStatusId = this.get('lastStatusId') |         let lastStatusId = this.get('lastStatusId') | ||||||
|         let instanceName = this.store.get('currentInstance') |         let instanceName = this.store.get('currentInstance') | ||||||
|         let instanceData = this.store.get('currentInstanceData') |         let instanceData = this.store.get('currentInstanceData') | ||||||
|         let newStatuses = await getHomeTimeline(instanceName, instanceData.access_token, lastStatusId, 10) |         let online = this.get('online') | ||||||
|  |         let newStatuses = online ? | ||||||
|  |           await getHomeTimeline(instanceName, instanceData.access_token, lastStatusId, FETCH_LIMIT) : | ||||||
|  |           await getTimelineFromDatabaseAfter(lastStatusId, FETCH_LIMIT) | ||||||
|  |         if (online) { | ||||||
|  |           insertStatusesIntoDatabase(newStatuses) | ||||||
|  |         } | ||||||
|         let statuses = this.get('statuses') |         let statuses = this.get('statuses') | ||||||
|         if (statuses) { |         if (statuses) { | ||||||
|           this.addItems(newStatuses) |           this.addStatuses(newStatuses) | ||||||
|         } |         } | ||||||
|         this.set({ runningUpdate: false }) |         this.set({ runningUpdate: false }) | ||||||
|         stop('onScrollToBottom') |         stop('onScrollToBottom') | ||||||
|       }, |       }, | ||||||
|       addItems(items) { |       addStatuses(newStatuses) { | ||||||
|         this.splice('statuses', this.get('statuses').length, 0, ...items) |         if (process.env.NODE_ENV !== 'production') { | ||||||
|  |           console.log('addStatuses()') | ||||||
|  |         } | ||||||
|  |         let statuses = this.get('statuses') | ||||||
|  |         if (!statuses) { | ||||||
|  |           return | ||||||
|  |         } | ||||||
|  |         let merged = mergeStatuses(statuses, newStatuses) | ||||||
|  |         this.set({ statuses: merged }) | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  | @ -1,2 +0,0 @@ | ||||||
| import keyval from './idb-keyval' |  | ||||||
| 
 |  | ||||||
|  | @ -1,57 +0,0 @@ | ||||||
| import idb from 'idb' |  | ||||||
| 
 |  | ||||||
| // copypasta'd from https://github.com/jakearchibald/idb#keyval-store
 |  | ||||||
| 
 |  | ||||||
| const dbPromise = idb.open('keyval-store', 1, upgradeDB => { |  | ||||||
|   upgradeDB.createObjectStore('keyval') |  | ||||||
| }) |  | ||||||
| 
 |  | ||||||
| const idbKeyval = { |  | ||||||
|   get(key) { |  | ||||||
|     return dbPromise.then(db => { |  | ||||||
|       return db.transaction('keyval').objectStore('keyval').get(key) |  | ||||||
|     }) |  | ||||||
|   }, |  | ||||||
|   set(key, val) { |  | ||||||
|     return dbPromise.then(db => { |  | ||||||
|       const tx = db.transaction('keyval', 'readwrite') |  | ||||||
|       tx.objectStore('keyval').put(val, key) |  | ||||||
|       return tx.complete |  | ||||||
|     }) |  | ||||||
|   }, |  | ||||||
|   delete(key) { |  | ||||||
|     return dbPromise.then(db => { |  | ||||||
|       const tx = db.transaction('keyval', 'readwrite') |  | ||||||
|       tx.objectStore('keyval').delete(key) |  | ||||||
|       return tx.complete |  | ||||||
|     }) |  | ||||||
|   }, |  | ||||||
|   clear() { |  | ||||||
|     return dbPromise.then(db => { |  | ||||||
|       const tx = db.transaction('keyval', 'readwrite') |  | ||||||
|       tx.objectStore('keyval').clear() |  | ||||||
|       return tx.complete |  | ||||||
|     }) |  | ||||||
|   }, |  | ||||||
|   keys() { |  | ||||||
|     return dbPromise.then(db => { |  | ||||||
|       const tx = db.transaction('keyval') |  | ||||||
|       const keys = [] |  | ||||||
|       const store = tx.objectStore('keyval') |  | ||||||
|       // This would be store.getAllKeys(), but it isn't supported by Edge or Safari.
 |  | ||||||
|       // openKeyCursor isn't supported by Safari, so we fall back
 |  | ||||||
|       const iterate = store.iterateKeyCursor || store.iterateCursor |  | ||||||
|       iterate.call(store, cursor => { |  | ||||||
|         if (!cursor) { |  | ||||||
|           return |  | ||||||
|         } |  | ||||||
|         keys.push(cursor.key) |  | ||||||
|         cursor.continue() |  | ||||||
|       }) |  | ||||||
| 
 |  | ||||||
|       return tx.complete.then(() => keys) |  | ||||||
|     }) |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| export default idbKeyval |  | ||||||
							
								
								
									
										65
									
								
								routes/_utils/database/statuses.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								routes/_utils/database/statuses.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,65 @@ | ||||||
|  | import cloneDeep from 'lodash/cloneDeep' | ||||||
|  | 
 | ||||||
|  | const STORE = 'statuses' | ||||||
|  | 
 | ||||||
|  | const dbPromise = new Promise((resolve, reject) => { | ||||||
|  |   let req = indexedDB.open(STORE, 1) | ||||||
|  |   req.onerror = reject | ||||||
|  |   req.onblocked = () => { | ||||||
|  |     console.log('idb blocked') | ||||||
|  |   } | ||||||
|  |   req.onupgradeneeded = () => { | ||||||
|  |     let db = req.result; | ||||||
|  |     let oStore = db.createObjectStore(STORE, { | ||||||
|  |       keyPath: 'id' | ||||||
|  |     }) | ||||||
|  |     oStore.createIndex('created_at', 'created_at') | ||||||
|  |     oStore.createIndex('pinafore_id_as_negative_big_int', 'pinafore_id_as_negative_big_int') | ||||||
|  |     oStore.createIndex('pinafore_id_as_big_int', 'pinafore_id_as_big_int') | ||||||
|  |   } | ||||||
|  |   req.onsuccess = () => resolve(req.result) | ||||||
|  | }) | ||||||
|  | 
 | ||||||
|  | function transformStatusForStorage(status) { | ||||||
|  |   status = cloneDeep(status) | ||||||
|  |   status.pinafore_id_as_big_int = parseInt(status, 10) | ||||||
|  |   status.pinafore_id_as_negative_big_int = -parseInt(status, 10) | ||||||
|  |   status.pinafore_stale = true | ||||||
|  |   return status | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function getTimelineAfter(max_id = null, limit = 20) { | ||||||
|  |   const db = await dbPromise | ||||||
|  |   return await new Promise((resolve, reject) => { | ||||||
|  |     const tx = db.transaction(STORE, 'readonly') | ||||||
|  |     const store = tx.objectStore(STORE) | ||||||
|  |     const index = store.index('pinafore_id_as_negative_big_int') | ||||||
|  |     let res | ||||||
|  |     let sinceAsNegativeBigInt = max_id === null ? null : -parseInt(max_id, 10) | ||||||
|  |     let query = sinceAsNegativeBigInt === null ? null : IDBKeyRange.lowerBound(sinceAsNegativeBigInt, false) | ||||||
|  | 
 | ||||||
|  |     index.getAll(query, limit).onsuccess = (e) => { | ||||||
|  |       console.log('done calling getAll()') | ||||||
|  |       res = e.target.result | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     tx.oncomplete = () => { | ||||||
|  |       console.log('complete') | ||||||
|  |       resolve(res) | ||||||
|  |     } | ||||||
|  |     tx.onerror = reject | ||||||
|  |   }) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function insertStatuses(statuses) { | ||||||
|  |   const db = await dbPromise | ||||||
|  |   return await new Promise((resolve, reject) => { | ||||||
|  |     const tx = db.transaction(STORE, 'readwrite') | ||||||
|  |     const store = tx.objectStore(STORE) | ||||||
|  |     for (let status of statuses) { | ||||||
|  |       store.put(transformStatusForStorage(status)) | ||||||
|  |     } | ||||||
|  |     tx.oncomplete = resolve | ||||||
|  |     tx.onerror = reject | ||||||
|  |   }) | ||||||
|  | } | ||||||
|  | @ -33,22 +33,3 @@ export function getAccessTokenFromAuthCode(instanceName, clientId, clientSecret, | ||||||
|     code: code |     code: code | ||||||
|   }) |   }) | ||||||
| } | } | ||||||
| 
 |  | ||||||
| export function getHomeTimeline(instanceName, accessToken, since, limit) { |  | ||||||
|   let url = `https://${instanceName}/api/v1/timelines/home` |  | ||||||
| 
 |  | ||||||
|   let params = {} |  | ||||||
|   if (since) { |  | ||||||
|     params[since] = since |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   if (limit) { |  | ||||||
|     params[limit] = limit |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   url += '?' + paramsString(params) |  | ||||||
| 
 |  | ||||||
|   return get(url, { |  | ||||||
|     'Authorization': `Bearer ${accessToken}` |  | ||||||
|   }) |  | ||||||
| } |  | ||||||
							
								
								
									
										20
									
								
								routes/_utils/mastodon/timelines.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								routes/_utils/mastodon/timelines.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,20 @@ | ||||||
|  | import { get, paramsString } from '../ajax' | ||||||
|  | 
 | ||||||
|  | export function getHomeTimeline(instanceName, accessToken, maxId, since) { | ||||||
|  |   let url = `https://${instanceName}/api/v1/timelines/home` | ||||||
|  | 
 | ||||||
|  |   let params = {} | ||||||
|  |   if (since) { | ||||||
|  |     params.since = since | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   if (maxId) { | ||||||
|  |     params.max_id = maxId | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   url += '?' + paramsString(params) | ||||||
|  | 
 | ||||||
|  |   return get(url, { | ||||||
|  |     'Authorization': `Bearer ${accessToken}` | ||||||
|  |   }) | ||||||
|  | } | ||||||
							
								
								
									
										33
									
								
								routes/_utils/statuses.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								routes/_utils/statuses.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,33 @@ | ||||||
|  | // Merge two lists of statuses for the same timeline, e.g. one from IDB
 | ||||||
|  | // and another from the network. In case of duplicates, prefer the fresh.
 | ||||||
|  | export function mergeStatuses(leftStatuses, rightStatuses) { | ||||||
|  |   let leftIndex = 0 | ||||||
|  |   let rightIndex = 0 | ||||||
|  |   let merged = [] | ||||||
|  |   while (leftIndex < leftStatuses.length || rightIndex < rightStatuses.length) { | ||||||
|  |     if (leftIndex === leftStatuses.length) { | ||||||
|  |       merged.push(rightStatuses[rightIndex]) | ||||||
|  |       rightIndex++ | ||||||
|  |       continue | ||||||
|  |     } | ||||||
|  |     if (rightIndex === rightStatuses.length) { | ||||||
|  |       merged.push(leftStatuses[leftIndex]) | ||||||
|  |       leftIndex++ | ||||||
|  |       continue | ||||||
|  |     } | ||||||
|  |     let left = leftStatuses[leftIndex] | ||||||
|  |     let right = rightStatuses[rightIndex] | ||||||
|  |     if (right.id === left.id) { | ||||||
|  |       merged.push(right.pinafore_stale ? left : right) | ||||||
|  |       rightIndex++ | ||||||
|  |       leftIndex++ | ||||||
|  |     } else if (parseInt(right.id, 10) > parseInt(left.id, 10)) { | ||||||
|  |       merged.push(right) | ||||||
|  |       rightIndex++ | ||||||
|  |     } else { | ||||||
|  |       merged.push(left) | ||||||
|  |       leftIndex++ | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   return merged | ||||||
|  | } | ||||||
		Loading…
	
	Add table
		
		Reference in a new issue