...
 
Commits (13)
......@@ -95,10 +95,6 @@ export class Minds {
this.webtorrent.setUp();
this.themeService.setUp();
if (this.session.isLoggedIn()) {
this.blockListService.sync();
}
}
ngOnDestroy() {
......
......@@ -94,6 +94,7 @@ import { UserMenuComponent } from "./layout/v2-topbar/user-menu.component";
import { FeaturedContentComponent } from "./components/featured-content/featured-content.component";
import { FeaturedContentService } from "./components/featured-content/featured-content.service";
import { BoostedContentService } from "./services/boosted-content.service";
import { FeedsService } from './services/feeds.service';
import { EntitiesService } from "./services/entities.service";
import { BlockListService } from "./services/block-list.service";
import { SettingsService } from "../modules/settings/settings.service";
......@@ -310,7 +311,7 @@ import { HorizontalInfiniteScroll } from "./components/infinite-scroll/horizonta
{
provide: FeaturedContentService,
useFactory: boostedContentService => new FeaturedContentService(boostedContentService),
deps: [ BoostedContentService ],
deps: [ FeedsService ],
}
],
entryComponents: [
......
import { Injectable } from "@angular/core";
import { BoostedContentService } from "../../services/boosted-content.service";
import { filter, first, map, switchMap } from 'rxjs/operators';
import { FeedsService } from "../../services/feeds.service";
@Injectable()
export class FeaturedContentService {
offset: number = -1;
constructor(
protected boostedContentService: BoostedContentService,
protected feedsService: FeedsService,
) {
this.feedsService
.setLimit(50)
.setOffset(0)
.setEndpoint('api/v2/boost/feed')
.fetch();
}
async fetch() {
return await this.boostedContentService.fetch();
return await this.feedsService.feed
.pipe(
filter(feed => feed.length > 0),
first(),
map(feed => feed[this.offset++]),
switchMap(async entity => {
if (!entity)
return false;
return await entity.pipe(first()).toPromise();
}),
).toPromise();
}
}
......@@ -15,6 +15,7 @@ import { sessionMock } from '../../../../tests/session-mock.spec';
import { FormsModule } from '@angular/forms';
import { RouterTestingModule } from '@angular/router/testing';
import { BlockListService } from '../../services/block-list.service';
import { storageMock } from '../../../../tests/storage-mock.spec';
/* tslint:disable */
/* Mock section */
......@@ -94,7 +95,11 @@ describe('PostMenuComponent', () => {
{ provide: Client, useValue: clientMock },
{ provide: Session, useValue: sessionMock },
{ provide: OverlayModalService, useValue: overlayModalServiceMock },
BlockListService,
{ provide: Storage, useValue: storageMock },
{ provide: BlockListService, useFactory: () => {
return BlockListService._(clientMock, sessionMock, storageMock);
}
}
],
schemas: [
NO_ERRORS_SCHEMA,
......
import { Injectable } from "@angular/core";
import { BehaviorSubject } from 'rxjs';
import { Client } from "../../services/api/client";
import { Session } from "../../services/session";
import { Storage } from '../../services/storage';
import AsyncLock from "../../helpers/async-lock";
......@@ -12,88 +14,60 @@ import AsyncStatus from "../../helpers/async-status";
@Injectable()
export class BlockListService {
protected blockListSync: BlockListSync;
protected syncLock = new AsyncLock();
protected status = new AsyncStatus();
blocked: BehaviorSubject<string[]>;
constructor(
protected client: Client,
protected session: Session,
protected storage: Storage
) {
this.setUp();
this.blocked = new BehaviorSubject(JSON.parse(this.storage.get('blocked')));
this.fetch();
}
async setUp() {
this.blockListSync = new BlockListSync(
new MindsClientHttpAdapter(this.client),
await browserStorageAdapterFactory('minds-block-190314'),
);
this.blockListSync.setUp();
//
fetch() {
this.client.get('api/v1/block', { sync: 1, limit: 10000 })
.then((response: any) => {
if (response.guids !== this.blocked.getValue())
this.blocked.next(response.guids); // re-emit as we have a change
this.status.done();
// Prune on session changes
this.session.isLoggedIn((is: boolean) => {
if (is) {
this.sync();
} else {
this.prune();
}
});
this.storage.set('blocked', JSON.stringify(response.guids)); // save to storage
});
return this;
}
async sync() {
await this.status.untilReady();
if (this.syncLock.isLocked()) {
return false;
}
this.syncLock.lock();
this.blockListSync.sync();
this.syncLock.unlock();
}
async prune() {
await this.status.untilReady();
if (this.syncLock.isLocked()) {
return false;
}
}
this.syncLock.lock();
this.blockListSync.prune();
this.syncLock.unlock();
async get() {
}
async getList() {
await this.status.untilReady();
await this.syncLock.untilUnlocked();
return await this.blockListSync.getList();
return this.blocked.getValue();
}
async add(guid: string) {
await this.status.untilReady();
await this.syncLock.untilUnlocked();
return await this.blockListSync.add(guid);
const guids = this.blocked.getValue();
if (guids.indexOf(guid) < 0)
this.blocked.next([...guids, ...[ guid ]]);
this.storage.set('blocked', JSON.stringify(this.blocked.getValue()));
}
async remove(guid: string) {
await this.status.untilReady();
await this.syncLock.untilUnlocked();
const guids = this.blocked.getValue();
const index = guids.indexOf(guid);
if (index > -1) {
guids.splice(index, 1);
}
return await this.blockListSync.remove(guid);
this.blocked.next(guids);
this.storage.set('blocked', JSON.stringify(this.blocked.getValue()));
}
static _(client: Client, session: Session) {
return new BlockListService(client, session);
static _(client: Client, session: Session, storage: Storage) {
return new BlockListService(client, session, storage);
}
}
......@@ -16,10 +16,6 @@ import AsyncStatus from "../../helpers/async-status";
@Injectable()
export class BoostedContentService {
protected boostedContentSync: BoostedContentSync;
protected status = new AsyncStatus();
constructor(
protected client: Client,
protected session: Session,
......@@ -31,59 +27,27 @@ export class BoostedContentService {
}
async setUp() {
this.boostedContentSync = new BoostedContentSync(
new MindsClientHttpAdapter(this.client),
await browserStorageAdapterFactory('minds-boosted-content-190314'),
5 * 60, // Stale after 5 minutes
15 * 60, // Cooldown of 15 minutes
500,
);
this.boostedContentSync.setResolvers({
currentUser: () => this.session.getLoggedInUser() && this.session.getLoggedInUser().guid,
blockedUserGuids: async () => await this.blockListService.getList(),
fetchEntities: async guids => await this.entitiesService.fetch(guids),
});
//
this.boostedContentSync.setUp();
//
this.status.done();
// User session / rating handlers
if (this.session.isLoggedIn()) {
this.boostedContentSync.setRating(this.session.getLoggedInUser().boost_rating || null);
// this.boostedContentSync.setRating(this.session.getLoggedInUser().boost_rating || null);
}
this.session.isLoggedIn((is: boolean) => {
if (is) {
this.boostedContentSync.setRating(this.session.getLoggedInUser().boost_rating || null);
} else {
this.boostedContentSync.destroy();
// this.boostedContentSync.setRating(this.session.getLoggedInUser().boost_rating || null);
}
});
// Garbage collection
this.boostedContentSync.gc();
setTimeout(() => this.boostedContentSync.gc(), 5 * 60 * 1000); // Every 5 minutes
// Rating changes hook
this.settingsService.ratingChanged.subscribe(rating => this.boostedContentSync.changeRating(rating));
//this.settingsService.ratingChanged.subscribe(rating => this.boostedContentSync.changeRating(rating));
}
async get(opts = {}) {
await this.status.untilReady();
return await this.boostedContentSync.get(opts);
setEndpoint(endpoint: string) {
}
async fetch(opts = {}) {
await this.status.untilReady();
return await this.boostedContentSync.fetch(opts);
fetch(opts = {}): BoostedContentService {
return this;
}
}
import { Injectable } from "@angular/core";
import { BehaviorSubject, Observable } from 'rxjs';
import { first } from 'rxjs/operators';
import { Client } from "../../services/api";
import { BlockListService } from './block-list.service';
import MindsClientHttpAdapter from '../../lib/minds-sync/adapters/MindsClientHttpAdapter.js';
import browserStorageAdapterFactory from "../../helpers/browser-storage-adapter-factory";
......@@ -7,68 +10,132 @@ import EntitiesSync from '../../lib/minds-sync/services/EntitiesSync.js';
import AsyncStatus from "../../helpers/async-status";
import normalizeUrn from "../../helpers/normalize-urn";
type EntityObservable = BehaviorSubject<Object>;
type EntityObservables = Map<string, EntityObservable>
@Injectable()
export class EntitiesService {
protected entitiesSync: EntitiesSync;
protected status = new AsyncStatus();
entities: EntityObservables = new Map<string, EntityObservable>();
constructor(
protected client: Client
protected client: Client,
protected blockListService: BlockListService,
) {
this.setUp();
}
async setUp() {
this.entitiesSync = new EntitiesSync(
new MindsClientHttpAdapter(this.client),
await browserStorageAdapterFactory('minds-entities-190314'),
15,
);
async getFromFeed(feed): Promise<EntityObservable[]> {
if (!feed || !feed.length) {
return [];
}
const blockedGuids = await this.blockListService.blocked.pipe(first()).toPromise();
const urnsToFetch = [];
const urnsToResync = [];
const entities = [];
for (const feedItem of feed) {
if (feedItem.entity) {
this.addEntity(feedItem.entity);
}
if (!this.entities.has(feedItem.urn)) {
urnsToFetch.push(feedItem.urn);
}
if (this.entities.has(feedItem.urn) && !feedItem.entity) {
urnsToResync.push(feedItem.urn);
}
}
this.entitiesSync.setUp();
// Fetch entities we don't have
//
if (urnsToFetch.length) {
await this.fetch(urnsToFetch);
}
this.status.done();
// Fetch entities, asynchronously, with no need to wait
// Garbage collection
if (urnsToResync.length) {
this.fetch(urnsToResync);
}
this.entitiesSync.gc();
setTimeout(() => this.entitiesSync.gc(), 15 * 60 * 1000); // Every 15 minutes
for (const feedItem of feed) {
if (blockedGuids.indexOf(feedItem.owner_guid) < 0)
entities.push(this.entities.get(feedItem.urn));
}
return entities;
}
async single(guid: string): Promise<Object | false> {
await this.status.untilReady();
/**
* Return and fetch a single entity via a urn
* @param urn string
* @return Object
*/
single(urn: string): EntityObservable {
if (urn.indexOf('urn:') < 0) { // not a urn, so treat as a guid
urn = `urn:activity:${urn}`; // and assume activity
}
this.entities.set(urn, new BehaviorSubject(null));
this.fetch([ urn ]); // Update in the background
return this.entities.get(urn);
}
/**
* Fetch entities
* @param urns string[]
* @return []
*/
async fetch(urns: string[]): Promise<Array<Object>> {
try {
const entities = await this.fetch([guid]);
const response: any = await this.client.get('api/v2/entities/', { urns });
if (!entities || !entities[0]) {
return false;
if (!response.entities.length) {
for (const urn of urns) {
this.addNotFoundEntity(urn);
}
}
return entities[0];
} catch (e) {
console.error('EntitiesService.get', e);
return false;
for (const entity of response.entities) {
this.addEntity(entity);
}
return response;
} catch (err) {
// TODO: find a good way of sending server errors to subscribers
}
}
async fetch(guids: string[]): Promise<Object[]> {
await this.status.untilReady();
if (!guids || !guids.length) {
return [];
/**
* Add or resync an entity
* @param entity
* @return void
*/
addEntity(entity): void {
if (this.entities.has(entity.urn)) {
this.entities.get(entity.urn).next(entity);
} else {
this.entities.set(entity.urn, new BehaviorSubject(entity));
}
}
const urns = guids.map(guid => normalizeUrn(guid));
return await this.entitiesSync.get(urns);
/**
* Register a urn as not found
* @param urn string
* @return void
*/
addNotFoundEntity(urn): void {
if (!this.entities.has(urn)) {
this.entities.set(urn, new BehaviorSubject(null));
}
this.entities.get(urn).error("Not found");
}
static _(client: Client) {
return new EntitiesService(client);
static _(client: Client, blockListService: BlockListService) {
return new EntitiesService(client, blockListService);
}
}
......@@ -12,6 +12,8 @@ import FeedsSync from '../../lib/minds-sync/services/FeedsSync.js';
import hashCode from "../../helpers/hash-code";
import AsyncStatus from "../../helpers/async-status";
import { BehaviorSubject, Observable, of, forkJoin, combineLatest } from "rxjs";
import { take, switchMap, map, tap, skipWhile, first, filter } from "rxjs/operators";
export type FeedsServiceGetParameters = {
endpoint: string;
......@@ -34,9 +36,16 @@ export type FeedsServiceGetResponse = {
@Injectable()
export class FeedsService {
protected feedsSync: FeedsSync;
limit: BehaviorSubject<number> = new BehaviorSubject(12);
offset: BehaviorSubject<number> = new BehaviorSubject(0);
pageSize: Observable<number>;
endpoint: string = '';
params: any = { sync: 1 };
protected status = new AsyncStatus();
rawFeed: BehaviorSubject<Object[]> = new BehaviorSubject([]);
feed: Observable<BehaviorSubject<Object>[]>;
inProgress: BehaviorSubject<boolean> = new BehaviorSubject(true);
hasMore: Observable<boolean>;
constructor(
protected client: Client,
......@@ -44,54 +53,83 @@ export class FeedsService {
protected entitiesService: EntitiesService,
protected blockListService: BlockListService,
) {
this.setUp();
}
async setUp() {
this.feedsSync = new FeedsSync(
new MindsClientHttpAdapter(this.client),
await browserStorageAdapterFactory('minds-feeds-190314'),
15,
this.pageSize = this.offset.pipe(
map(offset => this.limit.getValue() + offset)
);
this.feed = this.rawFeed.pipe(
tap(feed => {
if (feed.length)
this.inProgress.next(true);
}),
switchMap(async feed => {
return feed.slice(0, await this.pageSize.pipe(first()).toPromise())
}),
switchMap(feed => this.entitiesService.getFromFeed(feed)),
tap(feed => {
if (feed.length) // We should have skipped but..
this.inProgress.next(false);
}),
);
this.hasMore = combineLatest(this.rawFeed, this.inProgress, this.offset).pipe(
map(values => {
const feed = values[0];
const inProgress = values[1];
const offset = values[2];
return inProgress || feed.length > offset;
}),
);
}
this.feedsSync.setResolvers({
stringHash: value => hashCode(value),
currentUser: () => this.session.getLoggedInUser() && this.session.getLoggedInUser().guid,
blockedUserGuids: async () => await this.blockListService.getList(),
fetchEntities: async guids => await this.entitiesService.fetch(guids),
});
this.feedsSync.setUp();
// Mark as done
this.status.done();
setEndpoint(endpoint: string): FeedsService {
this.endpoint = endpoint;
return this;
}
// Garbage collection
setLimit(limit: number): FeedsService {
this.limit.next(limit);
return this;
}
this.feedsSync.gc();
setTimeout(() => this.feedsSync.gc(), 15 * 60 * 1000); // Every 15 minutes
setParams(params): FeedsService {
this.params = params;
if (!params.sync) {
this.params.sync = 1;
}
return this;
}
async get(opts: FeedsServiceGetParameters): Promise<FeedsServiceGetResponse> {
await this.status.untilReady();
setOffset(offset: number): FeedsService {
this.offset.next(offset);
return this;
}
try {
const { entities, next } = await this.feedsSync.get(opts);
fetch(): FeedsService {
this.inProgress.next(true);
this.client.get(this.endpoint, {...this.params, ...{ limit: 150 }}) // Over 12 scrolls
.then((response: any) => {
this.inProgress.next(false);
this.rawFeed.next(response.entities);
})
.catch(err => {
});
return this;
}
return {
entities,
next,
}
} catch (e) {
console.error('FeedsService.get', e);
throw e;
loadMore(): FeedsService {
if (!this.inProgress.getValue()) {
this.setOffset(this.limit.getValue() + this.offset.getValue());
this.rawFeed.next(this.rawFeed.getValue());
}
return this;
}
clear(): FeedsService {
this.offset.next(0);
this.rawFeed.next([]);
return this;
}
async destroy() {
await this.status.untilReady();
return await this.feedsSync.destroy();
}
static _(
......
......@@ -21,22 +21,22 @@
<ng-container *ngIf="['images', 'videos'].indexOf(type) > -1; else entityListView">
<m-newsfeed__tiles
[entities]="getAllEntities()"
[entities]="feedsService.feed | async"
></m-newsfeed__tiles>
</ng-container>
<ng-template #entityListView>
<m-newsfeed__entity
*ngFor="let entity of getAllEntities(); let i = index"
[entity]="entity"
*ngFor="let entity of (feedsService.feed | async); let i = index"
[entity]="entity | async"
[slot]="i + 1"
></m-newsfeed__entity>
</ng-template>
<infinite-scroll
distance="25%"
(load)="load()"
[moreData]="moreData"
[inProgress]="inProgress"
(load)="loadNext()"
[moreData]="feedsService.hasMore | async"
[inProgress]="feedsService.inProgress | async"
></infinite-scroll>
</div>
</div>
......@@ -17,7 +17,11 @@ import { ClientMetaService } from "../../../common/services/client-meta.service"
@Component({
selector: 'm-channel--sorted',
providers: [SortedService, ClientMetaService],
providers: [
SortedService,
ClientMetaService,
FeedsService,
],
templateUrl: 'sorted.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
......@@ -63,7 +67,7 @@ export class ChannelSortedComponent implements OnInit {
@ViewChild('poster', { static: false }) protected poster: PosterComponent;
constructor(
protected feedsService: FeedsService,
public feedsService: FeedsService,
protected service: SortedService,
protected session: Session,
protected clientMetaService: ClientMetaService,
......@@ -81,88 +85,33 @@ export class ChannelSortedComponent implements OnInit {
this.load(true);
}
getAllEntities() {
const pinned = this.channel.pinned_posts || [];
return [
...this.pinned,
...this.entities.filter(entity => pinned.indexOf(entity.guid) === -1),
];
}
async load(refresh: boolean = false) {
if (!refresh && this.inProgress) {
if (!refresh) {
return;
}
if (refresh) {
this.entities = [];
this.moreData = true;
this.offset = '';
this.feedsService.clear();
}
this.inProgress = true;
this.detectChanges();
if (!this.offset) {
// Load Pinned posts in parallel
this.loadPinned();
}
try {
const limit = 12;
const { entities, next } = await this.feedsService.get({
endpoint: `api/v2/feeds/container/${this.channel.guid}/${this.type}`,
timebased: true,
limit,
offset: this.offset,
syncPageSize: limit * 20,
});
if (!entities || !entities.length) {
this.moreData = false;
this.inProgress = false;
this.detectChanges();
return false;
}
if (this.entities && !refresh) {
this.entities.push(...entities);
} else {
this.entities = entities;
}
if (!next) {
this.moreData = false;
}
this.offset = next;
this.feedsService
.setEndpoint(`api/v2/feeds/container/${this.channel.guid}/${this.type}`)
.setLimit(12)
.fetch();
} catch (e) {
console.error('ChannelsSortedComponent.load', e);
}
this.inProgress = false;
this.detectChanges();
}
async loadPinned() {
this.pinned = [];
if (!this.isActivityFeed()) {
this.detectChanges();
return;
}
try {
this.pinned = (await this.service.getPinnedPosts(this.channel)) || [];
} catch (e) {
console.error('ChannelsSortedComponent.loadPinned', e);
}
this.detectChanges();
loadNext() {
this.feedsService.loadMore();
}
setFilter(type: string) {
......
......@@ -10,27 +10,6 @@ export class SortedService {
) {
}
async getPinnedPosts(channel: any) {
if (!channel || !channel.pinned_posts || !channel.pinned_posts.length) {
return [];
}
try {
const entities = await this.entitiesService.fetch(channel.pinned_posts);
if (!entities) {
return [];
}
return entities
.filter(entity => Boolean(entity))
.map(entity => ({ ...entity, pinned: true }));
} catch (e) {
console.error('Error fetching pinned posts', e);
return [];
}
}
async getMedia(channel: any, type: string, limit: number) {
try {
const response: any = await this.client.get(`api/v2/feeds/container/${channel.guid}/${type}`, {
......@@ -44,7 +23,9 @@ export class SortedService {
throw new Error('Invalid server response');
}
return response.entities;
return response.entities.map(entity => {
return entity.entity
});
} catch (e) {
console.error('SortedService.getMedia', e);
return [];
......
......@@ -31,14 +31,15 @@
<ng-container *ngIf="['images', 'videos'].indexOf(type) > -1; else entityListView">
<m-newsfeed__tiles
[entities]="getAllEntities()"
[entities]="feedsService.feed | async"
></m-newsfeed__tiles>
</ng-container>
<ng-template #entityListView>
<ng-container *ngFor="let entity of (feedsService.feed | async); let i = index">
<minds-activity
*ngFor="let entity of getAllEntities(); let i = index"
*ngIf="entity | async"
class="mdl-card item"
[object]="entity"
[object]="entity | async"
[canDelete]="group['is:owner'] || group['is:moderator']"
(delete)="delete(entity)"
[slot]="i + 1"
......@@ -48,18 +49,19 @@
<li
post-menu
class="mdl-menu__item"
*ngIf="group['is:owner']"
*ngIf="group['is:owner'] && (entity | async)"
(click)="kick(entity?.ownerObj)"
i18n="@@GROUPS__PROFILE__FEED__REMOVE_USER"
>Remove user</li>
</minds-activity>
</ng-container>
</ng-template>
<infinite-scroll
distance="25%"
(load)="load()"
[moreData]="moreData"
[inProgress]="inProgress"
(load)="loadMore()"
[moreData]="feedsService.hasMore | async"
[inProgress]="feedsService.inProgress | async"
></infinite-scroll>
</div>
</div>
......
......@@ -12,10 +12,14 @@ import { Session } from "../../../../services/session";
import { SortedService } from "./sorted.service";
import { Client } from "../../../../services/api/client";
import { GroupsService } from "../../groups-service";
import { Observable } from "rxjs";
@Component({
selector: 'm-group-profile-feed__sorted',
providers: [SortedService],
providers: [
SortedService,
FeedsService,
],
templateUrl: 'sorted.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
......@@ -61,7 +65,7 @@ export class GroupProfileFeedSortedComponent {
constructor(
protected service: GroupsService,
protected feedsService: FeedsService,
public feedsService: FeedsService,
protected sortedService: SortedService,
protected session: Session,
protected router: Router,
......@@ -75,65 +79,24 @@ export class GroupProfileFeedSortedComponent {
this.load(true);
}
getAllEntities() {
const pinned = this.group.pinned_posts || [];
return [
...this.pinned,
...this.entities.filter(entity => pinned.indexOf(entity.guid) === -1),
];
}
async load(refresh: boolean = false) {
if (!refresh && this.inProgress) {
if (!refresh) {
return;
}
if (refresh) {
this.entities = [];
this.moreData = true;
this.offset = '';
this.feedsService.clear();
}
this.inProgress = true;
this.detectChanges();
if (!this.offset) {
// Load Pinned posts in parallel
this.loadPinned();
}
try {
const limit = 12;
const { entities, next } = await this.feedsService.get({
endpoint: `api/v2/feeds/container/${this.group.guid}/${this.type}`,
timebased: true,
limit,
offset: this.offset,
syncPageSize: limit * 20,
});
if (!entities || !entities.length) {
this.moreData = false;
this.inProgress = false;
this.detectChanges();
return false;
}
if (this.entities && !refresh) {
this.entities.push(...entities);
} else {
this.entities = entities;
}
if (!next) {
this.moreData = false;
}
this.offset = next;
this.feedsService
.setEndpoint(`api/v2/feeds/container/${this.group.guid}/${this.type}`)
.setLimit(12)
.fetch();
} catch (e) {
console.error('GroupProfileFeedSortedComponent.loadFeed', e);
}
......@@ -142,21 +105,8 @@ export class GroupProfileFeedSortedComponent {
this.detectChanges();
}
async loadPinned() {
this.pinned = [];
if (!this.isActivityFeed()) {
this.detectChanges();
return;
}
try {
this.pinned = (await this.sortedService.getPinnedPosts(this.group)) || [];
} catch (e) {
console.error('ChannelsSortedComponent.loadPinned', e);
}
this.detectChanges();
loadMore() {
this.feedsService.loadMore();
}
setFilter(type: string) {
......
......@@ -8,24 +8,4 @@ export class SortedService {
) {
}
async getPinnedPosts(group: any) {
if (!group || !group.pinned_posts || !group.pinned_posts.length) {
return [];
}
try {
const entities = await this.entitiesService.fetch(group.pinned_posts);
if (!entities) {
return [];
}
return entities
.filter(entity => Boolean(entity))
.map(entity => ({ ...entity, pinned: true }));
} catch (e) {
console.error('Error fetching pinned posts', e);
return [];
}
}
}
......@@ -23,9 +23,9 @@
<ng-container *ngIf="!disabled">
<minds-activity
*ngFor="let boost of boosts; let i = index"
*ngFor="let boost of boosts; let i = index"
[object]="boost"
[boostToggle]="boost.boostToggle"
[boostToggle]="true"
[class]="'mdl-card m-border item m-boost-rotator-item m-boost-rotator-item-' + i"
visible="true"
[hidden]="i != currentPosition"
......
import { ChangeDetectorRef, Component, ElementRef, Injector, QueryList, SkipSelf, ViewChildren } from '@angular/core';
import { first } from 'rxjs/operators';
import { ScrollService } from '../../../services/ux/scroll';
import { Client } from '../../../services/api';
......@@ -12,6 +13,7 @@ import { NewsfeedBoostService } from '../newsfeed-boost.service';
import { SettingsService } from '../../settings/settings.service';
import { FeaturesService } from "../../../services/features.service";
import { BoostedContentService } from "../../../common/services/boosted-content.service";
import { FeedsService } from "../../../common/services/feeds.service";
import { ClientMetaService } from "../../../common/services/client-meta.service";
@Component({
......@@ -24,7 +26,10 @@ import { ClientMetaService } from "../../../common/services/client-meta.service"
'(mouseout)': 'mouseOut()'
},
inputs: ['interval', 'channel'],
providers: [ ClientMetaService ],
providers: [
ClientMetaService,
FeedsService,
],
templateUrl: 'boost-rotator.component.html',
})
......@@ -66,7 +71,7 @@ export class NewsfeedBoostRotatorComponent {
public service: NewsfeedBoostService,
private cd: ChangeDetectorRef,
protected featuresService: FeaturesService,
protected boostedContentService: BoostedContentService,
public feedsService: FeedsService,
protected clientMetaService: ClientMetaService,
@SkipSelf() injector: Injector,
) {
......@@ -91,43 +96,32 @@ export class NewsfeedBoostRotatorComponent {
this.scroll_listener = this.scroll.listenForView().subscribe(() => this.isVisible());
this.paused = this.service.isBoostPaused();
}
async load() {
if (this.featuresService.has('es-feeds')) {
return await this.loadFromService();
} else {
return await this.loadLegacy();
}
this.feedsService.feed.subscribe(async boosts => {
if (!boosts.length)
return;
for (const boost of boosts) {
if (boost)
this.boosts.push(await boost.pipe(first()).toPromise());
}
if (this.currentPosition === 0) {
this.recordImpression(this.currentPosition, true);
}
});
}
async loadFromService() {
load() {
try {
const boosts = await this.boostedContentService.get({
limit: 10,
offset: 8,
exclude: this.boosts.map(boost => boost.urn),
passive: true,
});
if (!boosts || !boosts.length) {
throw new Error(''); // Legacy behavior
}
this.boosts.push(...boosts);
if (this.boosts.length >= 40) {
this.boosts.splice(0, 20);
this.currentPosition = 0;
}
if (!this.running) {
if (this.currentPosition === 0) {
this.recordImpression(this.currentPosition, true);
}
this.start();
this.isVisible();
}
this.feedsService
.setEndpoint('api/v2/boost/feed')
.setParams({
rating: this.rating,
})
.setLimit(10)
.setOffset(0)
.fetch();
} catch (e) {
if (e && e.message) {
console.warn(e);
......@@ -139,61 +133,7 @@ export class NewsfeedBoostRotatorComponent {
this.inProgress = false;
return true;
}
/**
* Load newsfeed
*/
loadLegacy() {
return new Promise((resolve, reject) => {
if (this.inProgress) {
return reject(false);
}
this.inProgress = true;
if (this.storage.get('boost:offset:rotator')) {
this.offset = this.storage.get('boost:offset:rotator');
}
let show = 'all';
if (!this.channel || !this.channel.merchant) {
show = 'points';
}
this.client.get('api/v1/boost/fetch/newsfeed', {
limit: 10,
rating: this.rating,
offset: this.offset,
show: show
})
.then((response: any) => {
if (!response.boosts) {
this.inProgress = false;
return reject(false);
}
this.boosts = this.boosts.concat(response.boosts);
if (this.boosts.length >= 40) {
this.boosts.splice(0, 20);
this.currentPosition = 0;
}
if (!this.running) {
if (this.currentPosition === 0) {
this.recordImpression(this.currentPosition, true);
}
this.start();
this.isVisible();
}
this.offset = response['load-next'];
this.storage.set('boost:offset:rotator', this.offset);
this.inProgress = false;
return resolve(true);
})
.catch((e) => {
this.inProgress = false;
return reject();
});
});
}
onExplicitChanged(value: boolean) {
this.load();
}
......@@ -292,11 +232,11 @@ export class NewsfeedBoostRotatorComponent {
}
async next() {
this.activities.toArray()[this.currentPosition].hide();
//this.activities.toArray()[this.currentPosition].hide();
if (this.currentPosition + 1 > this.boosts.length - 1) {
//this.currentPosition = 0;
try {
await this.load();
this.feedsService.loadMore();
this.currentPosition++;
} catch(e) {
this.currentPosition = 0;
......
<ng-container [ngSwitch]="entity.type">
<ng-container [ngSwitch]="entity.type" *ngIf="entity">
<ng-container *ngSwitchCase="'user'">
<ng-template dynamic-host></ng-template>
</ng-container>
......
......@@ -49,7 +49,9 @@ export class NewsfeedEntityComponent {
// Update the component
updateComponents() {
if (this.entity.type === 'user' || this.entity.type === 'group') {
if (this.entity
&& (this.entity.type === 'user' || this.entity.type === 'group')
) {
this.clear();
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.getComponent(this.entity.type));
......
......@@ -21,7 +21,7 @@
</div>
<div class="m-feeds-sorted__List" [class.m-feeds-sortedList__flex]="!isActivityFeed()">
<ng-container *ngFor="let entity of newsfeed; let i = index">
<ng-container *ngFor="let entity of (feedsService.feed | async); let i = index">
<ng-container *ngIf="isActivityFeed()">
<m-featured-content
*ngIf="shouldShowBoost(i)"
......@@ -30,16 +30,16 @@
</ng-container>
<m-newsfeed__entity
[entity]="entity"
[entity]="entity | async"
[slot]="i + 1"
></m-newsfeed__entity>
</ng-container>
<infinite-scroll
distance="25%"
(load)="load()"
[moreData]="moreData"
[inProgress]="inProgress"
(load)="loadMore()"
[moreData]="feedsService.hasMore | async"
[inProgress]="feedsService.inProgress | async"
>
</infinite-scroll>
</div>
......
import { Component, Injector, OnDestroy, OnInit, SkipSelf, ViewChild } from '@angular/core';
import { Subject, Subscription } from 'rxjs';
import { Observable, Subscription } from 'rxjs';
import { take, map, mergeMap } from 'rxjs/operators';
import { ActivatedRoute, Router } from '@angular/router';
......@@ -21,7 +22,10 @@ import { ClientMetaService } from "../../../common/services/client-meta.service"
@Component({
selector: 'm-newsfeed--sorted',
providers: [ ClientMetaService ],
providers: [
ClientMetaService,
FeedsService, // Fresh feeds per component
],
templateUrl: 'sorted.component.html',
})
......@@ -31,7 +35,6 @@ export class NewsfeedSortedComponent implements OnInit, OnDestroy {
customType: string = 'activities';
hashtag: string | null = null;
all: boolean = false;
newsfeed: Array<Object>;
prepended: Array<any> = [];
offset: number = 0;
inProgress: boolean = false;
......@@ -63,7 +66,7 @@ export class NewsfeedSortedComponent implements OnInit, OnDestroy {
protected newsfeedService: NewsfeedService,
protected topbarHashtagsService: TopbarHashtagsService,
protected newsfeedHashtagSelectorService: NewsfeedHashtagSelectorService,
protected feedsService: FeedsService,
public feedsService: FeedsService,
protected featuresService: FeaturesService,
protected clientMetaService: ClientMetaService,
@SkipSelf() injector: Injector,
......@@ -149,6 +152,7 @@ export class NewsfeedSortedComponent implements OnInit, OnDestroy {
this.updateSortRoute();
}, 300);
}
ngOnDestroy() {
......@@ -178,15 +182,7 @@ export class NewsfeedSortedComponent implements OnInit, OnDestroy {
* @param {Boolean} forceSync
*/
async load(refresh: boolean = false, forceSync: boolean = false) {
if (this.inProgress) {
return false;
}
if (this.featuresService.has('sync-feeds')) {
return await this.loadFromFeedsService(refresh, forceSync);
} else {
return await this.loadLegacy(refresh);
}
return await this.loadFromFeedsService(refresh, forceSync);
}
/**
......@@ -194,48 +190,32 @@ export class NewsfeedSortedComponent implements OnInit, OnDestroy {
* @param {Boolean} forceSync
*/
async loadFromFeedsService(refresh: boolean = false, forceSync: boolean = false) {
if (forceSync) {
// TODO: Find a selective way to do it, in the future
await this.feedsService.destroy();
refresh = true;
}
if (refresh) {
this.moreData = true;
this.offset = 0;
this.newsfeed = [];
this.feedsService.clear();
}
this.inProgress = true;
try {
const limit = 12;
const hashtags = this.hashtag ? encodeURIComponent(this.hashtag) : '';
const period = this.period || '';
const all = this.all ? '1' : '';
const query = this.query ? encodeURIComponent(this.query) : '';
const nsfw = (this.newsfeedService.nsfw || []).join(',');
const { entities, next } = await this.feedsService.get({
endpoint: `api/v2/feeds/global/${this.algorithm}/${this.customType}?hashtags=${hashtags}&period=${period}&all=${all}&query=${query}&nsfw=${nsfw}`,
timebased: false,
limit,
offset: <number> this.offset,
forceSync,
});
if (this.newsfeed && !refresh) {
this.newsfeed.push(...entities);
} else {
this.newsfeed = entities;
}
this.offset = next;
this.feedsService
.setEndpoint(`api/v2/feeds/global/${this.algorithm}/${this.customType}`)
.setParams({
hashtags,
period,
all,
query,
nsfw,
})
.setLimit(12)
.fetch();
if (!this.offset) {
this.moreData = false;
}
} catch (e) {
console.error('SortedComponent', e);
}
......@@ -243,50 +223,8 @@ export class NewsfeedSortedComponent implements OnInit, OnDestroy {
this.inProgress = false;
}
/**
* @deprecated
* @param {Boolean} refresh
*/
loadLegacy(refresh: boolean = false) {
if (refresh) {
this.moreData = true;
this.offset = null;
this.newsfeed = [];
}
this.inProgress = true;
this.client.get(`api/v2/feeds/global/${this.algorithm}/${this.customType}`, {
limit: 12,
offset: this.offset || '',
rating: this.rating || '',
hashtags: this.hashtag ? [this.hashtag] : '',
period: this.period || '',
all: this.all ? 1 : '',
query: this.query ? encodeURIComponent(this.query) : '',
nsfw: this.newsfeedService.nsfw,
}, {
cache: true
})
.then((data: any) => {
if (!data.entities || !data.entities.length) {
this.moreData = false;
this.inProgress = false;
return false;
}
if (this.newsfeed && !refresh) {
this.newsfeed = this.newsfeed.concat(data.entities);
} else {
this.newsfeed = data.entities;
}
this.offset = data['load-next'];
this.inProgress = false;
})
.catch((e) => {
console.log(e);
this.inProgress = false;
});
loadMore() {
this.feedsService.loadMore();
}
delete(activity) {
......@@ -297,12 +235,12 @@ export class NewsfeedSortedComponent implements OnInit, OnDestroy {
return;
}
}
for (i in this.newsfeed) {
if (this.newsfeed[i] === activity) {
this.newsfeed.splice(i, 1);
return;
}
}
// for (i in this.newsfeed) {
// if (this.newsfeed[i] === activity) {
// this.newsfeed.splice(i, 1);
// return;
// }
// }
}
......
......@@ -8,7 +8,7 @@
<m-newsfeed--boost-rotator interval="4" *ngIf="showBoostRotator"></m-newsfeed--boost-rotator>
<ng-container *ngFor="let activity of newsfeed; let i = index">
<ng-container *ngFor="let activity of (feedsService.feed | async); let i = index">
<ng-container *mIfFeature="'es-feeds'">
<m-featured-content
*ngIf="(i > 0 && (i % 8) === 0 && i <= 40) || i === 2"
......@@ -18,7 +18,7 @@
<minds-activity
class="mdl-card m-border item"
[object]="activity"
[object]="activity | async"
[boostToggle]="activity.boostToggle"
(delete)="delete(activity)"
[showRatingToggle]="true"
......@@ -28,8 +28,8 @@
<infinite-scroll
distance="25%"
(load)="load()"
[moreData]="moreData"
[inProgress]="inProgress">
(load)="loadNext()"
[moreData]="feedsService.hasMore | async"
[inProgress]="feedsService.inProgress | async">
</infinite-scroll>
</div>
import { Component, Injector, SkipSelf, ViewChild } from '@angular/core';
import { Subscription } from 'rxjs';
import { Subscription, BehaviorSubject } from 'rxjs';
import { ActivatedRoute, Router } from '@angular/router';
......@@ -19,13 +19,17 @@ import { ClientMetaService } from "../../../common/services/client-meta.service"
@Component({
selector: 'm-newsfeed--subscribed',
providers: [ ClientMetaService ],
providers: [
ClientMetaService,
FeedsService,
],
templateUrl: 'subscribed.component.html',
})
export class NewsfeedSubscribedComponent {
newsfeed: Array<Object>;
feed: BehaviorSubject<Array<Object>> = new BehaviorSubject([]);
prepended: Array<any> = [];
offset: string | number = '';
showBoostRotator: boolean = true;
......@@ -61,7 +65,7 @@ export class NewsfeedSubscribedComponent {
private storage: Storage,
private context: ContextService,
protected featuresService: FeaturesService,
protected feedsService: FeedsService,
public feedsService: FeedsService,
protected newsfeedService: NewsfeedService,
protected clientMetaService: ClientMetaService,
@SkipSelf() injector: Injector,
......@@ -114,15 +118,17 @@ export class NewsfeedSubscribedComponent {
}
}
async loadFromService(refresh: boolean = false, forceSync: boolean = false) {
if (forceSync) {
this.inProgress = true;
// TODO: Find a selective way to do it, in the future
await this.feedsService.destroy();
refresh = true;
loadNext() {
if (this.featuresService.has('es-feeds')) {
this.feedsService.loadMore();
} else {
this.loadLegacy();
}
}
async loadFromService(refresh: boolean = false, forceSync: boolean = false) {
if (!refresh && this.inProgress) {
if (!refresh) {
return;
}
......@@ -135,71 +141,69 @@ export class NewsfeedSubscribedComponent {
this.inProgress = true;
try {
const limit = 12;
const { entities, next } = await this.feedsService.get({
endpoint: `api/v2/feeds/subscribed/activities`,
timebased: true,
limit,
offset: <number> this.offset,
syncPageSize: limit * 20,
forceSync,
});
if (!entities || !entities.length) {
this.moreData = false;
this.inProgress = false;
this.feedsService
.setEndpoint(`api/v2/feeds/subscribed/activities`)
.setLimit(12)
.fetch();
return false;
}
if (this.newsfeed && !refresh) {
this.newsfeed.push(...entities);
} else {
this.newsfeed = entities;
}
this.offset = next;
if (!this.offset) {
this.moreData = false;
}
} catch (e) {
console.error('SortedComponent', e);
}
this.inProgress = false;
}
/**
* Load newsfeed
*/
loadLegacy(refresh: boolean = false) {
if (this.inProgress)
return false;
return;
if (refresh) {
this.offset = '';
this.feedsService.clear();
}
this.feedsService.inProgress.next(true);
if (!this.offset) {
this.feedsService.setOffset(0);
} else {
this.feedsService.setOffset(this.feedsService.rawFeed.getValue().length);
}
this.inProgress = true;
this.client.get('api/v1/newsfeed', { limit: 12, offset: this.offset }, { cache: true })
.then((data: MindsActivityObject) => {
if (!data.activity) {
this.moreData = false;
this.inProgress = false;
this.feedsService.inProgress.next(false);
return false;
}
if (this.newsfeed && !refresh) {
this.newsfeed = this.newsfeed.concat(data.activity);
const feedItems = [];
for (const entity of data.activity) {
feedItems.push({
urn: entity.urn,
guid: entity.guid,
owner_guid: entity.owner_guid,
entity: entity,
});
}
if (this.feedsService.rawFeed.getValue() && !refresh) {
this.feedsService.rawFeed.next([...this.feedsService.rawFeed.getValue(), ...feedItems]);
} else {
this.newsfeed = data.activity;
this.feedsService.rawFeed.next(feedItems);
}
this.feedsService.inProgress.next(false);
//this.feedsService.setOffset(this.feedsService.offset.getValue() + 12); // Hacky!
this.offset = data['load-next'];
this.inProgress = false;
})
.catch((e) => {
console.error(e);
this.inProgress = false;
});
}
......
......@@ -2,14 +2,16 @@
[class.m-border]="entities && entities.length"
[class.m-newsfeed__tiles--has-elements]="entities && entities.length"
>
<ng-container *ngFor="let entity$ of entities">
<a
*ngFor="let entity of entities"
*ngIf="(entity$ | async) as entity"
class="m-newsfeed-tiles__Tile"
[ngClass]="{ 'm-newsfeed-tiles__Tile--is-mature': attachment.shouldBeBlurred(entity) }"
[routerLink]="['/newsfeed', entity.guid]"
>
<img [src]="getThumbnailSrc(entity)" />
<img [src]="getThumbnailSrc(entity$ | async)" />
<i *ngIf="attachment.shouldBeBlurred(entity)" class="material-icons mature-icon">explicit</i>
<i *ngIf="isUnlisted(entity)" class="material-icons unlisted-icon">visibility_off</i>
</a>
</ng-container>
</div>
import { Component, Injector, SkipSelf } from '@angular/core';
import { Component, Injector, SkipSelf, EventEmitter } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs';
import { first } from 'rxjs/operators';
import { Session } from '../../../services/session';
import { ContextService } from '../../../services/context.service';
......@@ -74,7 +75,11 @@ export class NewsfeedSingleComponent {
this.loadFromFeedsService(guid) :
this.loadLegacy(guid);
fetchSingleGuid.then((activity: any) => {
fetchSingleGuid.subscribe((activity: any) => {
if (activity === null) {
return; // Not yet loaded
}
this.activity = activity;
switch (this.activity.subtype) {
......@@ -104,30 +109,30 @@ export class NewsfeedSingleComponent {
} else {
this.context.reset();
}
})
.catch(e => {
}, err => {
this.inProgress = false;
if (e.status === 0) {
if (err.status === 0) {
this.error = 'Sorry, there was a timeout error.';
} else {
this.error = 'Sorry, we couldn\'t load the activity';
}
});
});
}
async loadFromFeedsService(guid: string) {
const activity = await this.entitiesService.single(guid);
loadFromFeedsService(guid: string) {
return this.entitiesService.single(guid);
}
if (!activity) {
throw new Error('Activity not found');
}
loadLegacy(guid: string) {
const fakeEmitter = new EventEmitter();
return activity;
}
this.client.get('api/v1/newsfeed/single/' + guid, {}, { cache: true })
.then((response: any) => {
fakeEmitter.next(response.activity);
});
async loadLegacy(guid: string) {
return (<any>await this.client.get('api/v1/newsfeed/single/' + guid, {}, { cache: true })).activity;
return fakeEmitter;
}
delete(activity) {
......
......@@ -2,7 +2,7 @@
<h4 i18n>Blocked Channels</h4>
<div class="m-settingsBlockedChannels__List">
<div *ngFor="let channel of channels" class="m-settingsBlockedChannels__Channel m-border">
<div *ngFor="let channel of channels | async" class="m-settingsBlockedChannels__Channel m-border">
<div class="m-settingsBlockedChannelsChannel__Avatar">
<a [routerLink]="['/', channel.username]">
<img [src]="getChannelIcon(channel)" />
......@@ -33,7 +33,7 @@
<infinite-scroll
distance="25%"
(load)="load()"
(load)="loadMore()"
[moreData]="moreData"
[inProgress]="inProgress">
</infinite-scroll>
......
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from "@angular/core";
import { tap, filter, switchMap } from 'rxjs/operators';
import { BlockListService } from "../../../common/services/block-list.service";
import { EntitiesService } from "../../../common/services/entities.service";
import { Client } from "../../../services/api/client";
......@@ -11,7 +12,7 @@ import { Client } from "../../../services/api/client";
export class SettingsBlockedChannelsComponent implements OnInit {
blockedGuids: any[] = [];
channels: any[] = [];
channels;
offset: number = 0;
......@@ -30,46 +31,30 @@ export class SettingsBlockedChannelsComponent implements OnInit {
ngOnInit() {
this.load(true);
this.channels = this.blockListService.blocked.pipe(
tap(() => {
this.inProgress = true;
this.moreData = false; // Support pagination in the future
}),
filter(list => list.length > 0),
switchMap(async guids => {
const response: any = await this.entitiesService.fetch(guids);
return response.entities;
}),
tap((blocked) => {
this.inProgress = false;
})
);
}
async load(refresh: boolean = false) {
const limit = 24;
if (!refresh && this.inProgress) {
return false;
}
try {
this.inProgress = true;
if (refresh) {
this.blockedGuids = [];
this.channels = [];
this.offset = 0;
this.moreData = true;
}
if (!this.offset) {
this.blockedGuids = (await this.blockListService.getList()) || [];
}
const next = this.offset + limit;
const guids = this.blockedGuids.slice(this.offset, next);
const channels = (await this.entitiesService.fetch(guids)) || [];
if (!channels.length) {
this.moreData = false;
}
this.channels.push(...channels);
this.offset = next;
} catch (e) {
this.moreData = false;
}
if (this.inProgress)
return;
this.blockListService.fetch(); // Get latest
}
this.inProgress = false;
this.detectChanges();
loadMore() {
// Implement soon
}
getChannelIcon(channel) {
......
......@@ -201,12 +201,12 @@ export const MINDS_PROVIDERS : any[] = [
{
provide: BlockListService,
useFactory: BlockListService._,
deps: [ Client, Session ],
deps: [ Client, Session, Storage ],
},
{
provide: EntitiesService,
useFactory: EntitiesService._,
deps: [ Client ],
deps: [ Client, BlockListService ],
},
{
provide: FeedsService,
......