diff --git a/sample/src/app/app.module.ts b/sample/src/app/app.module.ts index 7dceb39d5..eb921fede 100644 --- a/sample/src/app/app.module.ts +++ b/sample/src/app/app.module.ts @@ -37,6 +37,7 @@ import { MessagingComponent } from './messaging/messaging.component'; import { FunctionsComponent } from './functions/functions.component'; import { FirestoreOfflineComponent } from './firestore-offline/firestore-offline.component'; import { FirestoreOfflineModule } from './firestore-offline/firestore-offline.module'; +import { UpboatsComponent } from './upboats/upboats.component'; @NgModule({ declarations: [ @@ -50,6 +51,7 @@ import { FirestoreOfflineModule } from './firestore-offline/firestore-offline.mo AuthComponent, MessagingComponent, FunctionsComponent, + UpboatsComponent, ], imports: [ BrowserModule.withServerTransition({ appId: 'serverApp' }), diff --git a/sample/src/app/home/home.component.ts b/sample/src/app/home/home.component.ts index 231414fbf..3f580e958 100644 --- a/sample/src/app/home/home.component.ts +++ b/sample/src/app/home/home.component.ts @@ -9,6 +9,7 @@ import { FirebaseApp } from '@angular/fire'; + diff --git a/sample/src/app/protected-lazy/protected-lazy.component.html b/sample/src/app/protected-lazy/protected-lazy.component.html index ca6bdab0f..7c96e988f 100644 --- a/sample/src/app/protected-lazy/protected-lazy.component.html +++ b/sample/src/app/protected-lazy/protected-lazy.component.html @@ -1 +1,9 @@

protected-lazy works!

+ + diff --git a/sample/src/app/protected-lazy/protected-lazy.component.ts b/sample/src/app/protected-lazy/protected-lazy.component.ts index 45525dafa..f84ce2d59 100644 --- a/sample/src/app/protected-lazy/protected-lazy.component.ts +++ b/sample/src/app/protected-lazy/protected-lazy.component.ts @@ -1,4 +1,7 @@ import { Component, OnInit } from '@angular/core'; +import { DocumentChangeAction } from '@angular/fire/firestore'; +import { Observable } from 'rxjs'; +import { AngularFirestoreOffline } from '../firestore-offline/firestore-offline.module'; @Component({ selector: 'app-protected-lazy', @@ -7,7 +10,11 @@ import { Component, OnInit } from '@angular/core'; }) export class ProtectedLazyComponent implements OnInit { - constructor() { } + public snapshot: Observable[]>; + + constructor(private afs: AngularFirestoreOffline) { + this.snapshot = afs.collection('test').snapshotChanges(); + } ngOnInit(): void { } diff --git a/sample/src/app/upboats/upboats.component.css b/sample/src/app/upboats/upboats.component.css new file mode 100644 index 000000000..e69de29bb diff --git a/sample/src/app/upboats/upboats.component.html b/sample/src/app/upboats/upboats.component.html new file mode 100644 index 000000000..7081505e1 --- /dev/null +++ b/sample/src/app/upboats/upboats.component.html @@ -0,0 +1,11 @@ + + + diff --git a/sample/src/app/upboats/upboats.component.spec.ts b/sample/src/app/upboats/upboats.component.spec.ts new file mode 100644 index 000000000..d18bb2c99 --- /dev/null +++ b/sample/src/app/upboats/upboats.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { UpboatsComponent } from './upboats.component'; + +describe('UpboatsComponent', () => { + let component: UpboatsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ UpboatsComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(UpboatsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/sample/src/app/upboats/upboats.component.ts b/sample/src/app/upboats/upboats.component.ts new file mode 100644 index 000000000..4fa023171 --- /dev/null +++ b/sample/src/app/upboats/upboats.component.ts @@ -0,0 +1,65 @@ +import { Component, OnInit } from '@angular/core'; +import { Observable } from 'rxjs'; +import { map, startWith, tap } from 'rxjs/operators'; +import { AngularFirestoreOffline } from '../firestore-offline/firestore-offline.module'; +import firebase from 'firebase/app'; +import { makeStateKey, TransferState } from '@angular/platform-browser'; +import { trace } from '@angular/fire/performance'; + +type Animal = { name: string, upboats: number, id: string, hasPendingWrites: boolean }; + +@Component({ + selector: 'app-upboats', + templateUrl: './upboats.component.html', + styleUrls: ['./upboats.component.css'] +}) +export class UpboatsComponent implements OnInit { + + public animals: Observable; + + constructor(private firestore: AngularFirestoreOffline, state: TransferState) { + const collection = firestore.collection('animals', ref => + ref.orderBy('upboats', 'desc').orderBy('updatedAt', 'desc') + ); + const key = makeStateKey(collection.ref.path); + const existing = state.get(key, undefined); + this.animals = collection.snapshotChanges().pipe( + trace('animals'), + map(it => it.map(change => ({ + ...change.payload.doc.data(), + id: change.payload.doc.id, + hasPendingWrites: change.payload.doc.metadata.hasPendingWrites + }))), + existing ? startWith(existing) : tap(it => state.set(key, it)) + ); + } + + ngOnInit(): void { + } + + upboat(id: string) { + // TODO add rule + this.firestore.doc(`animals/${id}`).update({ + upboats: firebase.firestore.FieldValue.increment(1), + updatedAt: firebase.firestore.FieldValue.serverTimestamp(), + }); + } + + downboat(id: string) { + // TODO add rule + this.firestore.doc(`animals/${id}`).update({ + upboats: firebase.firestore.FieldValue.increment(-1), + updatedAt: firebase.firestore.FieldValue.serverTimestamp(), + }); + } + + newAnimal() { + // TODO add rule + this.firestore.collection('animals').add({ + name: prompt('Can haz name?'), + upboats: 1, + updatedAt: firebase.firestore.FieldValue.serverTimestamp(), + }); + } + +} diff --git a/src/firestore/collection/changes.ts b/src/firestore/collection/changes.ts index 07f0d12ba..080bff5d0 100644 --- a/src/firestore/collection/changes.ts +++ b/src/firestore/collection/changes.ts @@ -1,6 +1,6 @@ import { fromCollectionRef } from '../observable/fromRef'; import { Observable, SchedulerLike } from 'rxjs'; -import { map, scan } from 'rxjs/operators'; +import { distinctUntilChanged, map, pairwise, scan, startWith } from 'rxjs/operators'; import { DocumentChange, DocumentChangeAction, DocumentChangeType, Query } from '../interfaces'; @@ -25,8 +25,29 @@ export function sortedChanges( scheduler?: SchedulerLike): Observable[]> { return fromCollectionRef(query, scheduler) .pipe( - map(changes => changes.payload.docChanges()), - scan((current, changes) => combineChanges(current, changes, events), []), + startWith(undefined), + pairwise(), + scan((current, [priorChanges, changes]) => { + const docChanges = changes.payload.docChanges(); + const ret = combineChanges(current, docChanges, events); + // docChanges({ includeMetadataChanges: true }) does't include metadata changes... wat? + if (events.indexOf('modified') > -1 && priorChanges && + JSON.stringify(priorChanges.payload.metadata) !== JSON.stringify(changes.payload.metadata)) { + return ret.map(it => { + const partOfDocChanges = !!docChanges.find(d => d.doc.ref.isEqual(it.doc.ref)); + return { + // if it's not one of the changed docs that means we already saw it's order change + // so this is purely metadata, so don't move the doc + oldIndex: partOfDocChanges ? it.oldIndex : it.newIndex, + newIndex: it.newIndex, + type: 'modified', + doc: changes.payload.docs.find(d => d.ref.isEqual(it.doc.ref)) + }; + }); + } + return ret; + }, []), + distinctUntilChanged(), // cut down on unneed change cycles map(changes => changes.map(c => ({ type: c.type, payload: c } as DocumentChangeAction)))); } @@ -44,6 +65,21 @@ export function combineChanges(current: DocumentChange[], changes: Documen return current; } +/** + * Splice arguments on top of a sliced array, to break top-level === + * this is useful for change-detection + */ +function sliceAndSplice( + original: T[], + start: number, + deleteCount: number, + ...args: T[] +): T[] { + const returnArray = original.slice(); + returnArray.splice(start, deleteCount, ...args); + return returnArray; +} + /** * Creates a new sorted array from a new change. */ @@ -53,7 +89,7 @@ export function combineChange(combined: DocumentChange[], change: Document if (combined[change.newIndex] && combined[change.newIndex].doc.ref.isEqual(change.doc.ref)) { // Not sure why the duplicates are getting fired } else { - combined.splice(change.newIndex, 0, change); + return sliceAndSplice(combined, change.newIndex, 0, change); } break; case 'modified': @@ -61,16 +97,18 @@ export function combineChange(combined: DocumentChange[], change: Document // When an item changes position we first remove it // and then add it's new position if (change.oldIndex !== change.newIndex) { - combined.splice(change.oldIndex, 1); - combined.splice(change.newIndex, 0, change); + const copiedArray = combined.slice(); + copiedArray.splice(change.oldIndex, 1); + copiedArray.splice(change.newIndex, 0, change); + return copiedArray; } else { - combined.splice(change.newIndex, 1, change); + return sliceAndSplice(combined, change.newIndex, 1, change); } } break; case 'removed': if (combined[change.oldIndex] && combined[change.oldIndex].doc.ref.isEqual(change.doc.ref)) { - combined.splice(change.oldIndex, 1); + return sliceAndSplice(combined, change.oldIndex, 1); } break; }