...
 
Commits (2)
......@@ -41,6 +41,9 @@ export type FeedsServiceGetResponse = {
next?: number;
};
/**
* Enables the grabbing of data through observable feeds.
*/
@Injectable()
export class FeedsService {
limit: BehaviorSubject<number> = new BehaviorSubject(12);
......@@ -98,16 +101,28 @@ export class FeedsService {
);
}
/**
* Sets the endpoint for this instance.
* @param { string } endpoint - the endpoint for this instance. For example `api/v1/entities/owner`.
*/
setEndpoint(endpoint: string): FeedsService {
this.endpoint = endpoint;
return this;
}
/**
* Sets the limit to be returned per next() call.
* @param { number } limit - the limit to retrieve.
*/
setLimit(limit: number): FeedsService {
this.limit.next(limit);
return this;
}
/**
* Sets parameters to be used.
* @param { Object } params - parameters to be used.
*/
setParams(params): FeedsService {
this.params = params;
if (!params.sync) {
......@@ -116,19 +131,31 @@ export class FeedsService {
return this;
}
/**
* Sets the offset of the request
* @param { number } offset - the offset of the request.
*/
setOffset(offset: number): FeedsService {
this.offset.next(offset);
return this;
}
/**
* Sets castToActivities
* @param { boolean } cast - whether or not to set as_activities to true.
*/
setCastToActivities(cast: boolean): FeedsService {
this.castToActivities = cast;
return this;
}
/**
* Fetches the data.
*/
fetch(): FeedsService {
if (!this.offset.getValue()) this.inProgress.next(true);
if (!this.offset.getValue()) {
this.inProgress.next(true);
}
this.client
.get(this.endpoint, {
...this.params,
......@@ -139,8 +166,12 @@ export class FeedsService {
},
})
.then((response: any) => {
if (!this.offset.getValue()) this.inProgress.next(false);
if (!this.offset.getValue()) {
this.inProgress.next(false);
}
if (!response.entities && response.activity) {
response.entities = response.activity;
}
if (response.entities.length) {
this.rawFeed.next(this.rawFeed.getValue().concat(response.entities));
this.pagingToken = response['load-next'];
......@@ -148,10 +179,13 @@ export class FeedsService {
this.canFetchMore = false;
}
})
.catch(err => {});
.catch(e => console.log(e));
return this;
}
/**
* To be called upload loading more data
*/
loadMore(): FeedsService {
if (!this.inProgress.getValue()) {
this.setOffset(this.limit.getValue() + this.offset.getValue());
......@@ -160,6 +194,9 @@ export class FeedsService {
return this;
}
/**
* To clear data.
*/
clear(): FeedsService {
this.offset.next(0);
this.pagingToken = '';
......
......@@ -8,21 +8,18 @@
<div class="m-boost-console-booster--content">
<!-- Posts -->
<ng-container *ngIf="type == 'newsfeed' || type == 'offers'">
<ng-container *ngIf="posts && posts.length > 0">
<ng-container>
<div class="m-boost-console--booster--posts-list">
<minds-card
[object]="object"
[object]="entity | async"
class="m-border"
hostClass="mdl-card"
*ngFor="let object of posts"
*ngFor="let entity of feed$ | async"
></minds-card>
</div>
</ng-container>
<h3
[hidden]="inProgress || posts.length > 0"
i18n="@@BOOST__CONSOLE__BOOSTER__POST_SOMETHING"
>
<h3 [hidden]="noContent" i18n="@@BOOST__CONSOLE__BOOSTER__POST_SOMETHING">
You have no content yet. Why don't you post something?
</h3>
<div
......@@ -30,7 +27,7 @@
class="mdl-spinner mdl-js-spinner is-active"
[mdl]
></div>
<div #poster [hidden]="!inProgress && posts.length > 0"></div>
<div #poster [hidden]="!inProgress && noContent"></div>
</ng-container>
<!-- User and Media -->
......@@ -41,23 +38,26 @@
hostClass="mdl-shadow--2dp"
></minds-card>
<ng-container *ngIf="media.length > 0">
<ng-container *ngIf="(feed$ | async)?.length != 0">
<h3 i18n="@@BOOST__CONSOLE__BOOSTER__YOUR_RECENT_MEDIA_TITLE">
Your recent media
</h3>
<div class="mdl-grid m-boost-console-booster--content-grid">
<div class="mdl-cell mdl-cell--6-col" *ngFor="let object of media">
<div
class="mdl-cell mdl-cell--6-col"
*ngFor="let entity of feed$ | async"
>
<minds-card
[object]="object"
[object]="entity | async"
hostClass="mdl-shadow--2dp"
></minds-card>
<minds-button type="boost" [object]="object"></minds-button>
<minds-button type="boost" [object]="entity | async"></minds-button>
</div>
</div>
</ng-container>
<h3
[hidden]="inProgress || media.length > 0"
[hidden]="!inProgress && noContent"
i18n="@@BOOST__CONSOLE__BOOSTER__POST_SOMETHING"
>
You have no content yet. Why don't you post something?
......@@ -67,6 +67,13 @@
class="mdl-spinner mdl-js-spinner is-active"
[mdl]
></div>
<div #poster [hidden]="inProgress && media.length > 0"></div>
<div #poster [hidden]="!inProgress && noContent"></div>
</ng-container>
<infinite-scroll
distance="25%"
(load)="loadNext()"
[moreData]="feedsService.hasMore | async"
[inProgress]="inProgress"
></infinite-scroll>
</div>
......@@ -11,10 +11,14 @@ import { Client } from '../../../../services/api';
import { Session } from '../../../../services/session';
import { ActivatedRoute } from '@angular/router';
import { of } from 'rxjs/internal/observable/of';
import { FeedsService } from '../../../../common/services/feeds.service';
import { feedsServiceMock } from '../../../../../tests/feed-service-mock.spec';
import { BehaviorSubject } from 'rxjs';
describe('BoostConsoleBooster', () => {
let comp: BoostConsoleBooster;
let fixture: ComponentFixture<BoostConsoleBooster>;
window.Minds.user = { guid: 123 };
beforeEach(async(() => {
TestBed.configureTestingModule({
......@@ -25,6 +29,11 @@ describe('BoostConsoleBooster', () => {
inputs: ['object', 'hostClass'],
}),
MockComponent({ selector: 'minds-button', inputs: ['object', 'type'] }),
MockDirective({
selector: 'infinite-scroll',
inputs: ['moreData', 'inProgress'],
outputs: ['load'],
}),
BoostConsoleBooster,
],
imports: [RouterTestingModule, ReactiveFormsModule],
......@@ -35,28 +44,15 @@ describe('BoostConsoleBooster', () => {
provide: ActivatedRoute,
useValue: { parent: { url: of([{ path: 'newsfeed' }]) } },
},
{ provide: FeedsService, useValue: feedsServiceMock },
],
}).compileComponents();
}));
beforeEach(done => {
jasmine.MAX_PRETTY_PRINT_DEPTH = 2;
fixture = TestBed.createComponent(BoostConsoleBooster);
comp = fixture.componentInstance;
clientMock.response = {};
clientMock.response['api/v1/newsfeed/personal'] = {
status: 'success',
activity: [{ guid: '123' }, { guid: '456' }],
};
clientMock.response['api/v1/entities/owner'] = {
status: 'success',
entities: [{ guid: '789' }, { guid: '101112' }],
};
fixture.detectChanges();
if (fixture.isStable()) {
......@@ -70,8 +66,7 @@ describe('BoostConsoleBooster', () => {
});
it('should have loaded the lists', () => {
expect(comp.posts).toEqual([{ guid: '123' }, { guid: '456' }]);
expect(comp.media).toEqual([{ guid: '789' }, { guid: '101112' }]);
expect(comp.feed$).not.toBeFalsy();
});
it('should have a title', () => {
......@@ -89,13 +84,14 @@ describe('BoostConsoleBooster', () => {
By.css('.m-boost-console--booster--posts-list')
);
expect(list).not.toBeNull();
expect(list.nativeElement.children.length).toBe(2);
expect(list.nativeElement.children.length).toBe(1);
});
it("should have a poster if the user hasn't posted anything yet", () => {
comp.feed$ = of([]);
fixture.detectChanges();
comp.posts = [];
fixture.detectChanges();
comp.feed$.subscribe(feed => expect(feed.length).toBe(0));
const title = fixture.debugElement.query(
By.css('.m-boost-console-booster--content h3')
......@@ -106,9 +102,31 @@ describe('BoostConsoleBooster', () => {
);
const poster = fixture.debugElement.query(
By.css('.m-boost-console-booster--content div:last-child')
By.css('.m-boost-console-booster--content > div:nth-child(3)')
);
expect(poster).not.toBeNull();
expect(poster.nativeElement.hidden).toBeFalsy();
});
it('should not have a poster if the user has posted content', () => {
comp.feed$ = of([
BehaviorSubject.create({ id: 1 }),
BehaviorSubject.create({ id: 2 }),
]);
fixture.detectChanges();
comp.feed$.subscribe(feed => expect(feed.length).toBe(2));
const title = fixture.debugElement.query(
By.css('.m-boost-console-booster--content h3')
);
expect(title).toBeDefined();
expect(title.nativeElement.textContent).toContain(
"You have no content yet. Why don't you post something?"
);
const poster = fixture.debugElement.query(
By.css('.m-boost-console-booster--content > div:nth-child(3)')
);
expect(poster).toBeDefined();
});
});
import {
Component,
ComponentFactoryResolver,
ViewRef,
ChangeDetectorRef,
Input,
ViewChild,
ViewContainerRef,
} from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { FeedsService } from '../../../../common/services/feeds.service';
import { BoostConsoleType } from '../console.component';
import { Client } from '../../../../services/api';
import { Session } from '../../../../services/session';
import { BehaviorSubject, Observable } from 'rxjs';
import { PosterComponent } from '../../../newsfeed/poster/poster.component';
/**
* The component for the boost console.
*/
@Component({
moduleId: module.id,
selector: 'm-boost-console-booster',
templateUrl: 'booster.component.html',
})
export class BoostConsoleBooster {
inProgress: boolean = false;
loaded: boolean = false;
posts: any[] = [];
media: any[] = [];
/* type of the feed to display */
@Input('type') type: BoostConsoleType;
componentRef;
componentInstance: PosterComponent;
/* poster component */
@ViewChild('poster', { read: ViewContainerRef, static: false })
poster: ViewContainerRef;
minds: Minds = window.Minds;
feed$: Observable<BehaviorSubject<Object>[]>;
componentRef;
componentInstance: PosterComponent;
inProgress = true;
loaded = false;
noContent = true;
constructor(
public client: Client,
public session: Session,
private route: ActivatedRoute,
private _componentFactoryResolver: ComponentFactoryResolver
public feedsService: FeedsService,
private cd: ChangeDetectorRef,
private componentFactoryResolver: ComponentFactoryResolver
) {}
/**
* subscribes to route parent url and loads component.
*/
ngOnInit() {
this.loaded = false;
this.route.parent.url.subscribe(segments => {
this.type = <BoostConsoleType>segments[0].path;
this.load();
this.load(true);
this.loaded = true;
this.loadPoster();
});
}
/**
* Loads the infinite feed for the respective parent route.
* @param { boolean } refresh - is the state refreshing?
*/
load(refresh?: boolean) {
if (this.inProgress) {
return Promise.resolve(false);
if (!refresh) {
return;
}
if (!refresh && this.loaded) {
return Promise.resolve(true);
if (refresh) {
this.feedsService.clear();
}
this.inProgress = true;
let promises = [
this.client.get('api/v1/newsfeed/personal'),
this.client.get('api/v1/entities/owner'),
];
return Promise.all(promises)
.then((responses: any[]) => {
this.loaded = true;
this.inProgress = false;
this.posts = responses[0].activity || [];
this.media = responses[1].entities || [];
// this.posts = [];
// this.media = [];
this.loadComponent();
})
.catch(e => {
this.inProgress = false;
return false;
});
this.feedsService
.setEndpoint(
this.type === 'content'
? `api/v2/feeds/container/${this.minds.user.guid}/all`
: `api/v2/feeds/container/${this.minds.user.guid}/activities`
)
.setParams({ sync: true })
.setLimit(12)
.fetch();
this.feed$ = this.feedsService.feed;
this.inProgress = false;
this.loaded = true;
this.feed$.subscribe(feed => (this.noContent = feed.length > 0));
}
loadComponent() {
this.poster.clear();
/**
* Loads next data in feed.
* @param feed - the feed to reload.
*/
loadNext() {
if (
((this.type === 'offers' || this.type === 'newsfeed') &&
this.posts.length === 0) ||
(this.type === 'content' && this.media.length === 0)
this.feedsService.canFetchMore &&
!this.feedsService.inProgress.getValue() &&
this.feedsService.offset.getValue()
) {
const componentFactory = this._componentFactoryResolver.resolveComponentFactory(
this.feedsService.fetch(); // load the next 150 in the background
}
this.feedsService.loadMore();
}
/**
* Detects changes if view is not destroyed.
*/
detectChanges() {
if (!(this.cd as ViewRef).destroyed) {
this.cd.markForCheck();
this.cd.detectChanges();
}
}
/**
* Detaches change detector on destroy
*/
ngOnDestroy = () => this.cd.detach();
/**
* Loads the poster component if there are no activities loaded.
* @returns {boolean} success.
*/
loadPoster() {
this.feedsService.feed.subscribe(feed => {
if (feed.length > 0 && !this.inProgress && this.loaded) {
try {
this.poster.clear();
this.componentRef.clear();
this.noContent = true;
return false;
} catch (e) {
return false;
}
}
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(
PosterComponent
);
this.componentRef = this.poster.createComponent(componentFactory);
this.componentInstance = this.componentRef.instance;
this.componentInstance.load.subscribe(() => {
this.load();
});
}
return true;
});
}
}
/**
* @author Ben Hayward
* @create date 2019-08-16 15:00:04
* @modify date 2019-08-16 15:00:04
* @desc Mock service for feed.spec.ts
*/
import { BehaviorSubject, of } from 'rxjs';
export let feedsServiceMock = {
feed: new BehaviorSubject([Promise.resolve('[1,2,3,4,5]')]),
clear() {
of({ response: false }, { response: false }, { response: true });
},
response() {
return { response: true };
},
setEndpoint(str) {
return this;
}, //chainable
setLimit(limit) {
return this;
},
setParams(params) {
return this;
},
fetch() {
return this;
},
};