Skip to content
Commits on Source (44)
......@@ -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";
......@@ -280,7 +281,7 @@ import { HorizontalInfiniteScroll } from "./components/infinite-scroll/horizonta
{
provide: AttachmentService,
useFactory: AttachmentService._,
deps: [Session, Client, Upload]
deps: [Session, Client, Upload, HttpClient ]
},
{
provide: UpdateMarkersService,
......@@ -310,7 +311,7 @@ import { HorizontalInfiniteScroll } from "./components/infinite-scroll/horizonta
{
provide: FeaturedContentService,
useFactory: boostedContentService => new FeaturedContentService(boostedContentService),
deps: [ BoostedContentService ],
deps: [ FeedsService ],
}
],
entryComponents: [
......
......@@ -28,4 +28,11 @@
Founder
</m-tooltip>
</li>
<li *ngIf="showOnchainBadge()" routerLink="/tokens">
<m-tooltip icon="link" [iconClass]="{'selected': true }" i18n="Boosted OnChain in the last 7 days">
Boosted OnChain in the last 7 days
</m-tooltip>
</li>
</ul>
......@@ -8,7 +8,7 @@
li{
display: flex;
flex: 1;
//flex: 1;
flex-direction: column;
align-items: center;
//padding: 16px;
......@@ -16,8 +16,8 @@
}
i{
font-size:28px;
padding:8px;
font-size: 24px;
padding: 0 4px;
&.admin__badge {
@include m-theme(){
......
......@@ -22,7 +22,7 @@ export interface SocialProfileMeta {
export class ChannelBadgesComponent {
@Input() user;
@Input() badges: Array<string> = [ 'verified', 'plus', 'founder', 'admin' ];
@Input() badges: Array<string> = [ 'verified', 'plus', 'founder', 'admin', 'onchain_booster' ];
constructor(public session: Session, private client: Client, private router: Router) { }
......@@ -84,4 +84,10 @@ export class ChannelBadgesComponent {
});
}
showOnchainBadge() {
return this.user.onchain_booster
&& this.user.onchain_booster * 1000 > Date.now()
&& this.badges.indexOf('onchain_booster') > -1;
}
}
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,
......
......@@ -7,6 +7,8 @@
z-index: 999;
width: 48px;
height: calc(100% - 52px);
overflow: hidden;
@include m-theme(){
background-color: themed($m-white);
}
......@@ -15,7 +17,7 @@
left: 0;
height: 48px;
width: 100%;
overflow-y: hidden;
overflow-y: visible;
@include m-theme(){
border-bottom: 1px solid themed($m-grey-50);
}
......
......@@ -94,10 +94,9 @@ describe('TagPipe', () => {
expect(transformedString).toEqual('<a class="tag" href="/test1" target="_blank">@test1</a> <a class="tag" href="/test2" target="_blank">@test2</a>');
});
fit('should transform many adjacent tags', () => {
it('should transform many adjacent tags', () => {
const pipe = new TagsPipe(featuresServiceMock);
const string = '@test1 @test2 @test3 @test4 @test5 @test6 @test7 @test8 @test9 @test10 @test11 @test12 @test13 @test14 @test15';
console.log(string);
const transformedString = pipe.transform(<any>string);
expect(transformedString).toEqual(`<a class="tag" href="/test1" target="_blank">@test1</a> <a class="tag" href="/test2" target="_blank">@test2</a> `
+ `<a class="tag" href="/test3" target="_blank">@test3</a> <a class="tag" href="/test4" target="_blank">@test4</a> `
......
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,153 @@ 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>();
castToActivites: boolean = false;
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);
}
}
// Fetch entities we don't have
if (urnsToFetch.length) {
await this.fetch(urnsToFetch);
}
// Fetch entities, asynchronously, with no need to wait
if (urnsToResync.length) {
this.fetch(urnsToResync);
}
this.entitiesSync.setUp();
for (const feedItem of feed) {
if (blockedGuids.indexOf(feedItem.owner_guid) < 0)
entities.push(this.entities.get(feedItem.urn));
}
return entities;
}
/**
* 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.status.done();
this.fetch([ urn ]) // Update in the background
.then((response: any) => {
const entity = response.entities[0];
if (entity.urn !== urn) { // urns may differn so fix this
entity.urn = urn;
this.addEntity(entity);
}
});
// Garbage collection
return this.entities.get(urn);
}
this.entitiesSync.gc();
setTimeout(() => this.entitiesSync.gc(), 15 * 60 * 1000); // Every 15 minutes
/**
* Cast to activities or not
* @param cast boolean
* @return EntitiesService
*/
setCastToActivities(cast: boolean): EntitiesService {
this.castToActivites = cast;
return this;
}
async single(guid: string): Promise<Object | false> {
await this.status.untilReady();
/**
* 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,
as_activities: this.castToActivites ? 1 : 0,
});
if (!response.entities.length) {
for (const urn of urns) {
this.addNotFoundEntity(urn);
}
}
if (!entities || !entities[0]) {
return false;
for (const entity of response.entities) {
this.addEntity(entity);
}
return entities[0];
} catch (e) {
console.error('EntitiesService.get', e);
return false;
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,17 @@ 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 };
castToActivities: boolean = false;
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 +54,95 @@ 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
.setCastToActivities(this.castToActivities)
.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
setEndpoint(endpoint: string): FeedsService {
this.endpoint = endpoint;
return this;
}
this.status.done();
setLimit(limit: number): FeedsService {
this.limit.next(limit);
return this;
}
// Garbage collection
setParams(params): FeedsService {
this.params = params;
if (!params.sync) {
this.params.sync = 1;
}
return this;
}
this.feedsSync.gc();
setTimeout(() => this.feedsSync.gc(), 15 * 60 * 1000); // Every 15 minutes
setOffset(offset: number): FeedsService {
this.offset.next(offset);
return this;
}
async get(opts: FeedsServiceGetParameters): Promise<FeedsServiceGetResponse> {
await this.status.untilReady();
setCastToActivities(cast: boolean): FeedsService {
this.castToActivities = cast;
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
as_activities: this.castToActivities ? 1 : 0,
}})
.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 _(
......
......@@ -50,7 +50,7 @@ export class BlockchainEthModalComponent implements OnInit {
}
get ethRate(): number {
const tokenUsdRate = 0.15;
const tokenUsdRate = 1.25;
const tokenUsd = 1 / tokenUsdRate;
const usd = this.rate / tokenUsd;
return usd;
......
......@@ -303,9 +303,9 @@
></div>
<span class="m-blockchain--marketing-person-name">
Bill Ottman
Bill Ottman <a href="https://www.linkedin.com/company/minds-com/" target="_blank"><img [src]="minds.cdn_assets_url + 'assets/icons/linkedin.png'" alt="Linkedin"></a>
</span>
<span class="m-blockchain--marketing-person-title">
Co-founder &amp; CEO
</span>
......@@ -317,7 +317,7 @@
></div>
<span class="m-blockchain--marketing-person-name">
Mark Harding
Mark Harding <a href="https://www.linkedin.com/in/mark-harding-43303938/" target="_blank"><img [src]="minds.cdn_assets_url + 'assets/icons/linkedin.png'" alt="Linkedin"></a>
</span>
<span class="m-blockchain--marketing-person-title">
......@@ -331,7 +331,7 @@
></div>
<span class="m-blockchain--marketing-person-name">
John Ottman
John Ottman <a href="https://www.linkedin.com/in/john-ottman-091b173/" target="_blank"><img [src]="minds.cdn_assets_url + 'assets/icons/linkedin.png'" alt="Linkedin"></a>
</span>
<span class="m-blockchain--marketing-person-title">
......@@ -347,7 +347,7 @@
></div>
<span class="m-blockchain--marketing-person-name">
Jack Ottman
Jack Ottman <a href="https://www.linkedin.com/in/jack-ottman-35440464/" target="_blank"><img [src]="minds.cdn_assets_url + 'assets/icons/linkedin.png'" alt="Linkedin"></a>
</span>
<span class="m-blockchain--marketing-person-title">
......@@ -361,7 +361,7 @@
></div>
<span class="m-blockchain--marketing-person-name">
Peter Schwartz
Peter Schwartz <a href="https://www.linkedin.com/in/peter-schwartz-9b11111/" target="_blank"><img [src]="minds.cdn_assets_url + 'assets/icons/linkedin.png'" alt="Linkedin"></a>
</span>
<span class="m-blockchain--marketing-person-title">
......
......@@ -405,6 +405,16 @@ m-blockchain--marketing {
font-size: 24px;
margin-top: 16px;
font-weight: 800;
img {
max-width: 16px;
filter: grayscale(1);
opacity: 0.62;
&:hover {
filter: initial;
opacity: 1;
}
}
}
.m-blockchain--marketing-person-title {
......
......@@ -175,6 +175,7 @@
<!-- Submit -->
<section class="m-boost--creator-section m-boost--creator-section-submit"
[class.m-boost--creator-section-submit--network]="boost.type != 'p2p'"
(mouseenter)="showErrors()"
*ngIf="step >= 1 || (boost.currency !== 'usd' && boost.currency !== 'creditcard')"
>
......
......@@ -382,6 +382,12 @@
}
}
.m-boost--creator-section-submit--network {
@media screen and (min-width: 780px) {
margin-top: -72px;
}
}
.m-boost--creator--submit {
display: flex;
justify-content: flex-start;
......
......@@ -5,7 +5,7 @@
(click)="setBoostCurrency('onchain')"
[class.m-boost--creator-selector--highlight]="!boost.currency || boost.currency == 'onchain'"
>
<i class="material-icons m-boost--creator--payment-method--icon">check_circle</i>
<i class="material-icons m-boost--creator--payment-method--icon">link</i>
<h5>
<span i18n="@@M__COMMON__ONCHAIN">OnChain</span>
<m-tooltip icon="help" i18n="@@BOOST__CREATOR__PAYMENT_METHODS__ONCHAIN_DESC">
......@@ -36,6 +36,13 @@
</ng-container>
</span>
<ul class="m-boostCreatorSelector__bullets">
<li>Max 10k per boost</li>
<li>Stored on the blockchain</li>
<li>Stand out in the feeds</li>
<li>Gain "OnChain" badge</li>
</ul>
<span class="m-boost--creator-selector--selected-label" i18n="@@M__COMMON__SELECTED">Selected</span>
</li>
......@@ -70,6 +77,13 @@
</ng-container>
</span>
<ul class="m-boostCreatorSelector__bullets">
<li>Max 5k per boost</li>
<li>Stored on Minds servers</li>
<li>No transaction fee</li>
<li style="list-style:none">&nbsp;</li>
</ul>
<span class="m-boost--creator-selector--selected-label" i18n="@@M__COMMON__SELECTED">Selected</span>
</li>
......@@ -111,7 +125,4 @@
<span class="m-boost--creator-selector--selected-label" i18n="@@M__COMMON__SELECTED">Selected</span>
</li>
<li class="m-layout--spacer"
*ngIf="boost.type === 'p2p'"
></li>
</ul>