diff --git a/src/src/app/common/state-service.ts b/src/src/app/common/state-service.ts index aa25da56..a40dc225 100644 --- a/src/src/app/common/state-service.ts +++ b/src/src/app/common/state-service.ts @@ -44,7 +44,7 @@ class StateService { * derived service class. */ protected getState(): TState { - return this.store.getState(); + return this.internalState$.value; } /** diff --git a/src/src/app/common/storable.ts b/src/src/app/common/storable.ts index 271aff18..27d3528c 100644 --- a/src/src/app/common/storable.ts +++ b/src/src/app/common/storable.ts @@ -1,14 +1,17 @@ export interface IStorable { readonly createdOn: string; + readonly type: StorableType; readonly value: T; } export class Storable implements IStorable { - constructor(v: T) { + constructor(v: T, type: StorableType = StorableType.modified) { this.value = v; + this.type = type; this.createdOn = new Date().toISOString(); } + type: StorableType; createdOn: string; value: T; } @@ -16,3 +19,8 @@ export class Storable implements IStorable { export interface UserVersion { version: number; } + +export enum StorableType { + initial, + modified +} diff --git a/src/src/app/services/app-state-initial-state.ts b/src/src/app/services/app-state-initial-state.ts index fa5cddd9..6a660317 100644 --- a/src/src/app/services/app-state-initial-state.ts +++ b/src/src/app/services/app-state-initial-state.ts @@ -1,9 +1,11 @@ import { AppState } from '../models/app-state'; import { Overlap } from '../common/bible-reference'; +import { StorableType } from '../common/storable'; export const initialState: AppState = { user: null, currentCards: { + type: StorableType.initial, createdOn: new Date(0).toISOString(), value: [], }, @@ -12,12 +14,14 @@ export const initialState: AppState = { currentSavedPage: null, savedPages: null, notes: { + type: StorableType.initial, createdOn: new Date(0).toISOString(), value: [], }, savedPagesLoaded: false, error: null, settings: { + type: StorableType.initial, createdOn: new Date(0).toISOString(), value: { displaySettings: { diff --git a/src/src/app/services/app.service.ts b/src/src/app/services/app.service.ts index 34b8457b..0b9eaf09 100644 --- a/src/src/app/services/app.service.ts +++ b/src/src/app/services/app.service.ts @@ -40,7 +40,73 @@ export class AppService extends createStateService(reducer, initialState) { private paragraphs: HashTable; private searchIndexArray: string[]; private autocomplete: string[]; - + private excludedWords: Map = new Map([ + ['us', 0], + ['these', 0], + ['her', 0], + ['saith', 0], + ['shalt', 0], + ['let', 0], + ['do', 0], + ['your', 0], + ['we', 0], + ['no', 0], + ['go', 0], + ['if', 0], + ['at', 0], + ['an', 0], + ['so', 0], + ['before', 0], + ['also', 0], + ['on', 0], + ['had', 0], + ['you', 0], + ['there', 0], + ['then', 0], + ['up', 0], + ['by', 0], + ['upon', 0], + ['were', 0], + ['are', 0], + ['this', 0], + ['when', 0], + ['thee', 0], + ['their', 0], + ['ye', 0], + ['will', 0], + ['as', 0], + ['thy', 0], + ['my', 0], + ['me', 0], + ['have', 0], + ['from', 0], + ['was', 0], + ['but', 0], + ['which', 0], + ['thou', 0], + ['all', 0], + ['it', 0], + ['with', 0], + ['them', 0], + ['him', 0], + ['they', 0], + ['is', 0], + ['be', 0], + ['not', 0], + ['his', 0], + ['i', 0], + ['shall', 0], + ['a', 0], + ['for', 0], + ['unto', 0], + ['he', 0], + ['in', 0], + ['to', 0], + ['that', 0], + ['of', 0], + ['and', 0], + ['the', 0], + ]); private readonly dataPath = 'assets/data'; constructor(private http: HttpClient, private localStorageService: StorageMap, private db: AngularFireDatabase) { @@ -82,6 +148,9 @@ export class AppService extends createStateService(reducer, initialState) { async addCard(qry: string, nextToItem: CardItem = null) { const card = await this.getCardByQuery(qry); + if (!card) { + return; + } this.dispatch(AppActionFactory.newAddCard(card, nextToItem)); } @@ -93,6 +162,7 @@ export class AppService extends createStateService(reducer, initialState) { } this.dispatch( AppActionFactory.newUpdateCards({ + type: queries.type, createdOn: queries.createdOn, value: cards, }) @@ -103,6 +173,9 @@ export class AppService extends createStateService(reducer, initialState) { if (qry.startsWith('note:')) { const id = qry.replace('note:', ''); const data = this.getState().notes.value.find((o) => o.id === id); + if (!data) { + return; + } return { qry, type: CardType.Note, @@ -111,6 +184,9 @@ export class AppService extends createStateService(reducer, initialState) { } else if (qry.search(/[0-9]/i) === -1) { // // its a search term. const data = await this.getWordsFromApi(qry); + if (!data) { + return; + } return { qry, type: CardType.Word, @@ -126,6 +202,9 @@ export class AppService extends createStateService(reducer, initialState) { if (qry !== '') { const myref = new BibleReference(qry.trim()); const data = await this.getPassageFromApi(myref.section); + if (!data) { + return; + } return { qry, type: CardType.Passage, @@ -263,6 +342,9 @@ export class AppService extends createStateService(reducer, initialState) { async getStrongsCard(strongsNumber: string, dict: StrongsDictionary) { const result = await this.getStrongsFromApi(strongsNumber, dict); + if (!result) { + return; + } const d = dict === 'grk' ? 'G' : 'H'; const card: CardItem = { @@ -558,12 +640,19 @@ export class AppService extends createStateService(reducer, initialState) { await this.getStemWordIndex(); } + const excluded: string[] = []; + // now carry on... const qs = this.normalizeQueryString(qry); const results: (readonly string[])[] = []; // Loop through each query term. for (const q of qs) { + if (this.excludedWords.has(q)) { + excluded.push(q); + continue; // skip this word. + } + if (!this.wordToStem.has(q)) { this.dispatch({ type: 'UPDATE_ERROR', @@ -601,6 +690,18 @@ export class AppService extends createStateService(reducer, initialState) { // Now we need to test results. If there is more than one item in the array, we need to find the set // that is shared by all of them. IF not, we can just return those refs. if (results.length === 0) { + if (excluded.length > 0) { + this.dispatch({ + type: 'UPDATE_ERROR', + error: { + msg: `The ${excluded.length > 1 ? 'words' : 'word'} "${excluded.reduce((prev, curr, idx, arr) => { + return `${prev}, ${curr}`; + })}" ${excluded.length > 1 ? 'have' : 'has'} been excluded from search because it is too common.`, + }, + }); + return; + } + this.dispatch({ type: 'UPDATE_ERROR', error: { @@ -610,6 +711,18 @@ export class AppService extends createStateService(reducer, initialState) { return; } + // // in this case, let the user know, but continue on because a result was found. + // if (excluded.length > 0) { + // this.dispatch({ + // type: 'UPDATE_ERROR', + // error: { + // msg: `The ${excluded.length > 1 ? 'words' : 'word'} "${excluded.reduce((prev, curr, idx, arr) => { + // return `${prev}, ${curr}`; + // })}" ${excluded.length > 1 ? 'have' : 'has'} been excluded from search because it is too common.`, + // }, + // }); + // } + if (results.length === 1) { // we go down this path if only one word was searched for return { diff --git a/src/src/app/services/migration0to1.service.ts b/src/src/app/services/migration0to1.service.ts index 7da45187..8750d436 100644 --- a/src/src/app/services/migration0to1.service.ts +++ b/src/src/app/services/migration0to1.service.ts @@ -8,7 +8,7 @@ import { Settings, User } from '../models/app-state'; import { SavedPage } from '../models/page-state'; import { CardType, DataReference } from '../models/card-state'; import { StorageService } from './storage.service'; -import { Storable } from '../common/storable'; +import { Storable, StorableType } from '../common/storable'; import { NoteItem } from '../models/note-state'; @Injectable({ @@ -39,6 +39,7 @@ export class MigrationVersion0to1 { settings, savedPages, { + type: StorableType.modified, createdOn: new Date(0).toISOString(), value: [], }, diff --git a/src/src/app/services/storage.service.ts b/src/src/app/services/storage.service.ts index 2fd0fa2d..75cce386 100644 --- a/src/src/app/services/storage.service.ts +++ b/src/src/app/services/storage.service.ts @@ -4,7 +4,7 @@ import { AngularFireDatabase, AngularFireObject } from '@angular/fire/database'; import { DataSnapshot } from '@angular/fire/database/interfaces'; import { SubscriberBase } from '../common/subscriber-base'; -import { IStorable, UserVersion } from '../common/storable'; +import { IStorable, StorableType, UserVersion } from '../common/storable'; import { AppService } from './app.service'; import { MigrationVersion0to1 } from './migration0to1.service'; @@ -16,6 +16,36 @@ import { isNullOrUndefined } from '../common/helpers'; import { DataReference } from '../models/card-state'; import { createSelector } from 'reselect'; +/** + * This class handles all the storage needs of the application. It handles both + * local and remote storage and syncing between the two. + * + * There are three components to the system that are important. + * + * 1) Listening for changes to the local data store (see the Local //#region) + * + * When data is written to the local data store, that data needs to be sent to + * the application state so the running storage reflects the local storage. + * + * The local storage is initialized in the app.component.ts constructor. + * + * 2) Listening for changes to the remote data store (see the Remote //#region) + * + * When data is written to the remote data store, that data needs to be sent to + * the application state so the running storage reflects the remote storage. + * + * The remote storage is initialized in the app.component.ts constructor once a user + * is available indicating that a connection has been established with the remote data + * store. + * + * 3) Listening for changes from the application state. + * + * When something happens in the application state, that state needs to be stored + * locally so the changes aren't lost, and remotely so the changes can be shared + * and persisten across devices and different installs. + * + * The listeners to the app state are initialized in the constructor. + */ @Injectable({ providedIn: 'root', }) @@ -48,6 +78,7 @@ export class StorageService extends SubscriberBase { (sync, cards) => ({ syncCardsAcrossDevices: sync, currentCards: { + type: cards.type, createdOn: cards.createdOn, value: cards.value.map((o) => { return { @@ -55,7 +86,7 @@ export class StorageService extends SubscriberBase { type: o.type, } as DataReference; }), - }, + } as IStorable, }) ) ); @@ -67,73 +98,6 @@ export class StorageService extends SubscriberBase { private v0to1: MigrationVersion0to1 ) { super(); - - // handle remote and local storage - // when the specific items change in the state, - // store them locally and remotely, if you're logged in. - this.observeStorable('Display Settings', this.settingsState$, this.settingsPath, this.settingsRemoteObject); - this.observeStorable('Page', this.savedPagesState$, this.savedPagesPath, this.savedPagesRemoteObject); - this.observeStorable('Note', this.noteItemsState$, this.noteItemsPath, this.noteItemsRemoteObject); - - // whenever the user setting or the current cards change, save the cards - // and if syncing is turned on, send them to the remote store. - this.addSubscription( - this.syncCurrentItems$.subscribe((v) => { - // set a local sync variable, so that the remote events know whether to - // accept remote state changes. - this.syncCurrentItems = v.syncCardsAcrossDevices; - - // update local - this.local.set(this.cardsPath, v.currentCards).subscribe( - () => { - // nop - }, - // error - () => { - // tslint:disable-next-line: quotemark - this.appService.dispatchError(`Something went wrong and the current items weren't saved. :(`); - } - ); - - // since you updated the local variable above, this remote update will - // get picked up by the remote subscription below. - if (v.syncCardsAcrossDevices) { - this.cardsRemoteObject.set(v.currentCards); - } - }) - ); - } - - observeStorable( - name: string, - state$: Observable>, - path: string, - remoteObject: AngularFireObject> - ) { - this.addSubscription( - state$.subscribe((data) => { - if (!data) { - return; - } - - // update local - this.local.set(path, data).subscribe( - () => { - // nop - }, - // error - () => { - // tslint:disable-next-line: quotemark - this.appService.dispatchError(`Something went wrong and the ${name} wasn't saved. :(`); - } - ); - - // update remote - if (remoteObject) { - remoteObject.set(data); - } - }) - ); } //#region Remote @@ -142,8 +106,9 @@ export class StorageService extends SubscriberBase { // do the necessary migration steps. this.handleMigration(user); - // initialize the remote store monitoring - this.updateRemote( + // initialize the remote store monitoring. This is where we listen for changes on the remote location. + + this.monitorRemote( this.settingsPath, user, (ro) => { @@ -154,7 +119,7 @@ export class StorageService extends SubscriberBase { } ); - this.updateRemote( + this.monitorRemote( this.savedPagesPath, user, (ro) => { @@ -165,7 +130,7 @@ export class StorageService extends SubscriberBase { } ); - this.updateRemote( + this.monitorRemote( this.noteItemsPath, user, (ro) => { @@ -176,6 +141,8 @@ export class StorageService extends SubscriberBase { } ); + // handle cards differently, because there is a setting that changes the behavior of how you handle + // new data from the remote. this.cardsRemoteObject = this.remote.object>(`/${this.cardsPath}/${user.uid}`); this.addSubscription( this.cardsRemoteObject @@ -189,7 +156,7 @@ export class StorageService extends SubscriberBase { ); } - private updateRemote( + private monitorRemote( path: string, user: User, setRemoteObject: (remoteObject: AngularFireObject>) => void, @@ -205,6 +172,7 @@ export class StorageService extends SubscriberBase { .subscribe((remoteData) => { if (!isNullOrUndefined(remoteData) && !isNullOrUndefined(remoteData.value)) { // update the app state with remote data if it isn't null + console.log('Data recieved from remote store', remoteData); action(remoteData); } }) @@ -219,28 +187,149 @@ export class StorageService extends SubscriberBase { initialize the local stores */ async initLocal() { - this.updateLocal(this.settingsPath, (data) => { + this.getFromLocal(this.settingsPath, (data) => { this.appService.updateSettings(data); }); - this.updateLocal(this.savedPagesPath, (data) => { + this.getFromLocal(this.savedPagesPath, (data) => { this.appService.updateSavedPages(data); }); - this.updateLocal(this.noteItemsPath, (data) => { + this.getFromLocal(this.noteItemsPath, (data) => { this.appService.updateNotes(data); }); - this.updateLocal(this.cardsPath, (data) => { + this.getFromLocal(this.cardsPath, (data) => { this.appService.updateCards(data); }); + + // we start listening after we get the local storage, to avoid overwriting local storage with the initial app state. + this.addAppStateListeners(); } - private async updateLocal(path: string, action: (storable: IStorable) => void) { + private async getFromLocal(path: string, action: (storable: IStorable) => void) { const hasStorable = await this.local.has(path).toPromise(); if (hasStorable) { - const storable = (await this.local.get(path).toPromise()) as IStorable; - action(storable); + const localData = (await this.local.get(path).toPromise()) as IStorable; + console.log('Data recieved from local store', localData); + action(localData); } } + private addAppStateListeners() { + // handle remote and local storage + // when the specific items change in the state, + // store them locally and remotely, if you're logged in. + this.addSubscription( + this.settingsState$.subscribe((data) => { + if (!data || data.type === StorableType.initial) { + return; + } + + console.log('Data saved to local store', data); + // update local + this.local.set(this.settingsPath, data).subscribe( + () => { + // nop + }, + // error + () => { + // tslint:disable-next-line: quotemark + this.appService.dispatchError(`Something went wrong and the Settings weren't saved. :(`); + } + ); + + // update remote + if (this.settingsRemoteObject) { + console.log('Data sent to remote store', data); + this.settingsRemoteObject.set(data); + } + }) + ); + + this.addSubscription( + this.savedPagesState$.subscribe((data) => { + if (!data || data.type === StorableType.initial) { + return; + } + + console.log('Data saved to local store', data); + // update local + this.local.set(this.savedPagesPath, data).subscribe( + () => { + // nop + }, + // error + () => { + // tslint:disable-next-line: quotemark + this.appService.dispatchError(`Something went wrong and the Page wasn't saved. :(`); + } + ); + + // update remote + if (this.savedPagesRemoteObject) { + console.log('Data sent to remote store', data); + this.savedPagesRemoteObject.set(data); + } + }) + ); + + this.addSubscription( + this.noteItemsState$.subscribe((data) => { + if (!data || data.type === StorableType.initial) { + return; + } + + console.log('Data saved to local store', data); + // update local + this.local.set(this.noteItemsPath, data).subscribe( + () => { + // nop + }, + // error + () => { + // tslint:disable-next-line: quotemark + this.appService.dispatchError(`Something went wrong and the Note wasn't saved. :(`); + } + ); + + // update remote + if (this.noteItemsRemoteObject) { + console.log('Data sent to remote store', data); + this.noteItemsRemoteObject.set(data); + } + }) + ); + + // whenever the user setting or the current cards change, save the cards + // and if syncing is turned on, send them to the remote store. + this.addSubscription( + this.syncCurrentItems$.subscribe((v) => { + // set a local sync variable, so that the remote events know whether to + // accept remote state changes. + this.syncCurrentItems = v.syncCardsAcrossDevices; + + if (!v.currentCards || v.currentCards.type === StorableType.initial) { + return; + } + + // update local + this.local.set(this.cardsPath, v.currentCards).subscribe( + () => { + // nop + }, + // error + () => { + // tslint:disable-next-line: quotemark + this.appService.dispatchError(`Something went wrong and the current cards weren't saved. :(`); + } + ); + + // since you updated the local variable above, this remote update will + // get picked up by the remote subscription below. + if (v.syncCardsAcrossDevices) { + this.cardsRemoteObject.set(v.currentCards); + } + }) + ); + } //#endregion //#region Migrations