mirror of
https://gitlab.com/walljm/dynamicbible.git
synced 2025-07-23 07:19:50 -04:00
fix issues with initial state being stored then retrieved overwriting state from previous session.
fix issue with getState() not returning the initial state. fix issue with excluded search terms not being ignored.
This commit is contained in:
parent
d93400b911
commit
ff33cdaf34
@ -44,7 +44,7 @@ class StateService<TState, TAction extends { type: string }> {
|
||||
* derived service class.
|
||||
*/
|
||||
protected getState(): TState {
|
||||
return this.store.getState();
|
||||
return this.internalState$.value;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,14 +1,17 @@
|
||||
export interface IStorable<T> {
|
||||
readonly createdOn: string;
|
||||
readonly type: StorableType;
|
||||
readonly value: T;
|
||||
}
|
||||
|
||||
export class Storable<T> implements IStorable<T> {
|
||||
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<T> implements IStorable<T> {
|
||||
export interface UserVersion {
|
||||
version: number;
|
||||
}
|
||||
|
||||
export enum StorableType {
|
||||
initial,
|
||||
modified
|
||||
}
|
||||
|
@ -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: {
|
||||
|
@ -40,7 +40,73 @@ export class AppService extends createStateService(reducer, initialState) {
|
||||
private paragraphs: HashTable<Paragraph>;
|
||||
private searchIndexArray: string[];
|
||||
private autocomplete: string[];
|
||||
|
||||
private excludedWords: Map<string, number> = 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 {
|
||||
|
@ -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: [],
|
||||
},
|
||||
|
@ -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<DataReference[]>,
|
||||
})
|
||||
)
|
||||
);
|
||||
@ -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<T>(
|
||||
name: string,
|
||||
state$: Observable<IStorable<T>>,
|
||||
path: string,
|
||||
remoteObject: AngularFireObject<IStorable<T>>
|
||||
) {
|
||||
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<Settings>(
|
||||
// initialize the remote store monitoring. This is where we listen for changes on the remote location.
|
||||
|
||||
this.monitorRemote<Settings>(
|
||||
this.settingsPath,
|
||||
user,
|
||||
(ro) => {
|
||||
@ -154,7 +119,7 @@ export class StorageService extends SubscriberBase {
|
||||
}
|
||||
);
|
||||
|
||||
this.updateRemote<SavedPage[]>(
|
||||
this.monitorRemote<SavedPage[]>(
|
||||
this.savedPagesPath,
|
||||
user,
|
||||
(ro) => {
|
||||
@ -165,7 +130,7 @@ export class StorageService extends SubscriberBase {
|
||||
}
|
||||
);
|
||||
|
||||
this.updateRemote<NoteItem[]>(
|
||||
this.monitorRemote<NoteItem[]>(
|
||||
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<IStorable<DataReference[]>>(`/${this.cardsPath}/${user.uid}`);
|
||||
this.addSubscription(
|
||||
this.cardsRemoteObject
|
||||
@ -189,7 +156,7 @@ export class StorageService extends SubscriberBase {
|
||||
);
|
||||
}
|
||||
|
||||
private updateRemote<T>(
|
||||
private monitorRemote<T>(
|
||||
path: string,
|
||||
user: User,
|
||||
setRemoteObject: (remoteObject: AngularFireObject<IStorable<T>>) => 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<Settings>(this.settingsPath, (data) => {
|
||||
this.getFromLocal<Settings>(this.settingsPath, (data) => {
|
||||
this.appService.updateSettings(data);
|
||||
});
|
||||
this.updateLocal<SavedPage[]>(this.savedPagesPath, (data) => {
|
||||
this.getFromLocal<SavedPage[]>(this.savedPagesPath, (data) => {
|
||||
this.appService.updateSavedPages(data);
|
||||
});
|
||||
this.updateLocal<NoteItem[]>(this.noteItemsPath, (data) => {
|
||||
this.getFromLocal<NoteItem[]>(this.noteItemsPath, (data) => {
|
||||
this.appService.updateNotes(data);
|
||||
});
|
||||
this.updateLocal<DataReference[]>(this.cardsPath, (data) => {
|
||||
this.getFromLocal<DataReference[]>(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<T>(path: string, action: (storable: IStorable<T>) => void) {
|
||||
private async getFromLocal<T>(path: string, action: (storable: IStorable<T>) => void) {
|
||||
const hasStorable = await this.local.has(path).toPromise();
|
||||
if (hasStorable) {
|
||||
const storable = (await this.local.get(path).toPromise()) as IStorable<T>;
|
||||
action(storable);
|
||||
const localData = (await this.local.get(path).toPromise()) as IStorable<T>;
|
||||
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
|
||||
|
Loading…
x
Reference in New Issue
Block a user