Commit 44c60f1e authored by Mark Harding's avatar Mark Harding

(chore): reduce size of bundle by using lazy loading and removing unused libraries

1 merge request!785Reduces bundle size by 37%
Pipeline #118311233 passed with stages
in 89 minutes and 27 seconds
#!/bin/sh
export NODE_OPTIONS="--max-old-space-size=3584"
ng build --prod --vendor-chunk --output-path="$1/en/" --deploy-url="$2/en/" --build-optimizer=true --stats-json
ng build --prod --output-path="$1/en/" --deploy-url="$2/en/" --build-optimizer=true --stats-json
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { APP_BASE_HREF } from '@angular/common';
import { AnalyticsModuleLazyRoutes } from './modules/analytics/analytics.lazy';
import { AdminModuleLazyRoutes } from './modules/admin/admin.lazy';
import { Pages } from './controllers/pages/pages';
import { ChannelContainerComponent } from './modules/channel-container/channel-container.component';
import { CanDeactivateGuardService } from './services/can-deactivate-guard';
const routes: Routes = [
{ path: 'about', redirectTo: 'p/about' },
{ path: 'p/:page', component: Pages },
AnalyticsModuleLazyRoutes,
AdminModuleLazyRoutes,
// TODO: Find a way to move channel routes onto its own Module. They take priority and groups/blogs cannot be accessed
{ path: ':username', redirectTo: ':username/', pathMatch: 'full' },
{
path: ':username/:filter',
component: ChannelContainerComponent,
canDeactivate: [CanDeactivateGuardService],
},
];
@NgModule({
imports: [
RouterModule.forRoot(routes, {
onSameUrlNavigation: 'reload',
}),
],
exports: [RouterModule],
providers: [{ provide: APP_BASE_HREF, useValue: '/' }],
})
export class AppRoutingModule {}
......@@ -3,18 +3,14 @@ import { NgModule } from '@angular/core';
import { MindsModule } from './app.module';
import { Minds } from './app.component';
import * as PlotlyJS from 'plotly.js/dist/plotly-basic.min.js';
import { PlotlyModule } from 'angular-plotly.js';
import { CookieModule } from '@gorniv/ngx-universal';
import {
RedirectService,
BrowserRedirectService,
} from './common/services/redirect.service';
PlotlyModule.plotlyjs = PlotlyJS;
@NgModule({
imports: [MindsModule, PlotlyModule, CookieModule],
imports: [MindsModule, CookieModule],
bootstrap: [Minds],
providers: [
{ provide: 'ORIGIN_URL', useValue: location.origin },
......
......@@ -16,7 +16,6 @@ import { ScrollToTopService } from './services/scroll-to-top.service';
import { ContextService } from './services/context.service';
import { Web3WalletService } from './modules/blockchain/web3-wallet.service';
import { Client } from './services/api/client';
import { WebtorrentService } from './modules/webtorrent/webtorrent.service';
import { ActivatedRoute, NavigationEnd, Router, Route } from '@angular/router';
import { ChannelOnboardingService } from './modules/onboarding/channel/onboarding.service';
import { BlockListService } from './common/services/block-list.service';
......@@ -61,7 +60,6 @@ export class Minds {
public context: ContextService,
public web3Wallet: Web3WalletService,
public client: Client,
public webtorrent: WebtorrentService,
public onboardingService: ChannelOnboardingService,
public router: Router,
public blockListService: BlockListService,
......@@ -174,8 +172,6 @@ export class Minds {
this.web3Wallet.setUp();
this.webtorrent.setUp();
this.themeService.setUp();
this.socketsService.setUp();
......
......@@ -18,16 +18,7 @@ import { CaptchaModule } from './modules/captcha/captcha.module';
import { Minds } from './app.component';
import {
MINDS_APP_ROUTING_DECLARATIONS,
MindsAppRoutes,
MindsAppRoutingProviders,
} from './router/app';
import { MINDS_DECLARATIONS } from './declarations';
import { MINDS_PLUGIN_DECLARATIONS } from './plugin-declarations';
import { MINDS_PROVIDERS } from './services/providers';
import { MINDS_PLUGIN_PROVIDERS } from './plugin-providers';
import { CommonModule } from './common/common.module';
import { MonetizationModule } from './modules/monetization/monetization.module';
......@@ -71,7 +62,6 @@ import { MobileModule } from './modules/mobile/mobile.module';
import { IssuesModule } from './modules/issues/issues.module';
import { CanaryModule } from './modules/canary/canary.module';
import { HttpClientModule } from '@angular/common/http';
import { AnalyticsModule } from './modules/analytics/analytics.module';
import { ProModule } from './modules/pro/pro.module';
import { ChannelContainerModule } from './modules/channel-container/channel-container.module';
import { UpgradesModule } from './modules/upgrades/upgrades.module';
......@@ -81,6 +71,8 @@ import { CookieModule } from '@gorniv/ngx-universal';
import { HomepageModule } from './modules/homepage/homepage.module';
import { OnboardingV2Module } from './modules/onboarding-v2/onboarding.module';
import { ConfigsService } from './common/services/configs.service';
import { AppRoutingModule } from './app-routing.module';
import { Pages } from './controllers/pages/pages';
@Injectable()
export class SentryErrorHandler implements ErrorHandler {
......@@ -94,12 +86,7 @@ export class SentryErrorHandler implements ErrorHandler {
@NgModule({
bootstrap: [Minds],
declarations: [
Minds,
MINDS_APP_ROUTING_DECLARATIONS,
MINDS_DECLARATIONS,
MINDS_PLUGIN_DECLARATIONS,
],
declarations: [Minds, Pages],
imports: [
BrowserModule.withServerTransition({ appId: 'm-app' }),
BrowserTransferStateModule,
......@@ -109,14 +96,9 @@ export class SentryErrorHandler implements ErrorHandler {
ReactiveFormsModule,
FormsModule,
HttpClientModule,
RouterModule.forRoot(MindsAppRoutes, {
// initialNavigation: 'enabled',
onSameUrlNavigation: 'reload',
}),
CaptchaModule,
CommonModule,
ProModule, // NOTE: Pro Module should be declared _BEFORE_ anything else
AnalyticsModule,
WalletModule,
//CheckoutModule,
MonetizationModule,
......@@ -158,16 +140,14 @@ export class SentryErrorHandler implements ErrorHandler {
CanaryModule,
ChannelsModule,
UpgradesModule,
//PlotlyModule,
//last due to :username route
AppRoutingModule,
ChannelContainerModule,
],
providers: [
{ provide: ErrorHandler, useClass: SentryErrorHandler },
MindsAppRoutingProviders,
MINDS_PROVIDERS,
MINDS_PLUGIN_PROVIDERS,
{
provide: APP_INITIALIZER,
useFactory: configs => () => configs.loadFromRemote(),
......
......@@ -118,9 +118,6 @@ import { MarketingFooterComponent } from './components/marketing/footer.componen
import { ToggleComponent } from './components/toggle/toggle.component';
import { MarketingAsFeaturedInComponent } from './components/marketing/as-featured-in.component';
import { SidebarMenuComponent } from './components/sidebar-menu/sidebar-menu.component';
import { ChartV2Component } from './components/chart-v2/chart-v2.component';
//import * as PlotlyJS from 'plotly.js/dist/plotly.js';
import { PlotlyModule } from 'angular-plotly.js';
import { PageLayoutComponent } from './components/page-layout/page-layout.component';
import { DashboardLayoutComponent } from './components/dashboard-layout/dashboard-layout.component';
import { ShadowboxLayoutComponent } from './components/shadowbox-layout/shadowbox-layout.component';
......@@ -160,7 +157,6 @@ const routes: Routes = [
RouterModule,
FormsModule,
ReactiveFormsModule,
PlotlyModule,
OwlDateTimeModule,
OwlNativeDateTimeModule,
RouterModule.forChild(routes),
......@@ -260,7 +256,6 @@ const routes: Routes = [
MarketingFooterComponent,
MarketingAsFeaturedInComponent,
SidebarMenuComponent,
ChartV2Component,
PageLayoutComponent,
DashboardLayoutComponent,
ShadowboxLayoutComponent,
......@@ -364,7 +359,6 @@ const routes: Routes = [
MarketingComponent,
MarketingAsFeaturedInComponent,
SidebarMenuComponent,
ChartV2Component,
PageLayoutComponent,
DashboardLayoutComponent,
ShadowboxLayoutComponent,
......
<div
class="drag-animation mdl-color--blue-grey-600 mdl-color-text--blue-grey-50"
[hidden]="!dragging"
>
<div class="drop">
<i class="material-icons">file_upload</i>
<p i18n="@@MINDS__CAPTURE__DROP_AREA">Drop your files here</p>
</div>
</div>
<div class="mdl-grid capture-grid" style="max-width:900px">
<div class="mdl-cell mdl-cell--4-col">
<div class="mdl-card m-albums-selector" #toggle>
<div class="mdl-card__title">
<h2 class="mdl-card__title-text">
<ng-container i18n="@@MINDS__CAPTURE__ALBUM_LABEL"
>Album</ng-container
>
</h2>
<button
class="mdl-button mdl-button--fab mdl-button--colored m-album-add"
(click)="toggle.value = !toggle.value"
>
<i class="material-icons">add</i>
</button>
</div>
<div
class="mdl-card m-album m-album-create mdl-color--blue-grey-500"
*ngIf="toggle.value"
>
<div class="mdl-card__title">
<input type="text" #newalbum />
<button
class="mdl-button mdl-button--colored mdl-color-text--white"
(click)="createAlbum(newalbum); toggle.value = false"
i18n="@@M__ACTION__CREATE"
>
Create
</button>
</div>
</div>
<div
class="mdl-progress mdl-js-progress mdl-progress__indeterminate"
[hidden]="albums.length > 0 && !inProgress"
[mdl]
></div>
<div
*ngFor="let album of albums"
class="mdl-card m-album mdl-color--blue-grey-50 mdl-color-text--blue-grey-500"
[ngClass]="{'mdl-color--blue-grey-500': postMeta.album_guid == album.guid, 'mdl-color-text--blue-grey-50': postMeta.album_guid == album.guid}"
(click)="selectAlbum(album)"
>
<div
class="mdl-card__title"
[ngClass]="{'mdl-color-text--blue-grey-50': postMeta.album_guid == album.guid}"
>
<h2>{{album.title}}</h2>
</div>
<div class="mdl-card__menu">
<i class="material-icons" (click)="deleteAlbum(album)">delete</i>
</div>
</div>
</div>
</div>
<!-- Upload output -->
<div class="mdl-cell mdl-cell--8-col">
<form class="mdl-card">
<div class="mdl-card__actions" style="display:flex;">
<div class="upload-button">
<button class="mdl-button mdl-button--raised">
<i class="material-icons">attachment</i>
<span i18n="@@MINDS__CAPTURE__ADD_FILE_ACTION">Add file</span>
</button>
<input
type="file"
id="file"
#file
(change)="add(file); file.value = '';"
multiple
accept="image/*"
/>
</div>
<div class="mdl-layout-spacer"></div>
<a
class="mdl-color-text--blue-grey-300 m-capture-default-maturity"
(click)="default_maturity = default_maturity ? 0 : 1"
>
<i
class="material-icons"
[ngClass]="{ 'mdl-color-text--red-500': default_maturity }"
title="Mature content"
i18n-title="@@M__COMMON__MATURE_CONTENT"
>explicit</i
>
<span
*ngIf="default_maturity"
class="mdl-color-text--red-500"
i18n="@@M__COMMON__MATURE_CONTENT"
>Mature content</span
>
</a>
<select
name="defaultLicense"
[(ngModel)]="default_license"
class="mdl-color-text--blue-grey-300 m-form-select"
>
<option *ngFor="let l of licenses" [value]="l.value"
>{{l.text}}</option
>
</select>
<button
class="mdl-button mdl-button--raised mdl-button--colored m-capture-save-to-album-button"
(click)="publish()"
[disabled]="!uploads"
>
<span i18n="@@MINDS__CAPTURE__SAVE_TO_ALBUM">Save to album</span>
</button>
</div>
</form>
<div
class="mdl-card mdl-shadow--2dp m-upload"
*ngFor="let upload of uploads; let i = index"
>
<div class="mdl-card__title m-capture-edit-container">
<input
type="text"
name="title"
[(ngModel)]="upload.title"
(change)="modify(i)"
/>
<a
class="mdl-color-text--blue-grey-300 m-capture-mature"
(click)="upload.mature = upload.mature ? 0 : 1"
>
<i
class="material-icons"
[ngClass]="{ 'mdl-color-text--red-500': upload.mature }"
title="Mature content"
i18n-title="@@M__COMMON__MATURE_CONTENT"
>explicit</i
>
</a>
<select
name="license"
[(ngModel)]="upload.license"
(change)="modify(i)"
class="mdl-color-text--blue-grey-300 m-form-select"
>
<option *ngFor="let l of licenses" [value]="l.value"
>{{l.text}}</option
>
</select>
</div>
<div
class="mdl-progress mdl-js-progress"
[mdlUpload]
[progress]="upload.progress"
[ngClass]="{'failed': upload.state == 'failed', 'complete': upload.state == 'complete'}"
></div>
</div>
<div class="m-splash">
<i class="material-icons mdl-color-text--blue-grey-400">file_upload</i>
<h3
class="mdl-color-text--blue-grey-300"
i18n="@@MINDS__CAPTURE__DRAG_TO_UPLOAD"
>
Drag to upload
</h3>
</div>
</div>
</div>
@import 'defaults';
minds-capture {
height: calc(100vh - 80px);
display: block;
}
.drag-animation {
position: fixed;
display: flex;
flex-direction: row;
align-items: center;
-webkit-align-items: center;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0.9;
z-index: 100;
animation: fade-animation 2s infinite alternate;
.drop {
display: flex;
flex-wrap: wrap;
text-align: center;
margin: auto;
width: 50%;
height: 50%;
i {
font-size: 12em;
width: 100%;
}
p {
width: 100%;
}
}
}
@keyframes fade-animation {
from {
opacity: 0.9;
}
to {
opacity: 0.8;
}
}
.capture-grid {
.mdl-card {
min-height: 0;
width: 100%;
}
.m-capture-default-maturity {
cursor: pointer;
padding-top: $minds-padding;
i {
cursor: pointer;
vertical-align: middle;
}
span {
font-size: 11px;
text-transform: uppercase;
}
}
.m-albums-selector {
.mdl-button.m-album-add {
width: 24px;
min-width: 0;
height: 24px;
margin-left: $minds-margin;
i {
font-size: 12px;
}
}
.m-album {
cursor: pointer;
@include m-theme() {
border-bottom: 1px solid themed($m-grey-100);
}
h2 {
margin: 0;
font-size: 14px;
line-height: 16px;
}
.mdl-card__menu {
visibility: hidden;
}
&:hover {
.mdl-card__menu {
visibility: visible;
}
}
}
.m-album-create {
input {
outline: none;
border: 0;
font-size: 14px;
@include m-theme() {
color: themed($m-white);
background-color: rgba(themed($m-white), 0.2);
border-right: 1px solid themed($m-grey-100);
}
}
button {
float: left;
margin-left: $minds-margin;
}
}
}
.upload-button {
display: inline-block;
vertical-align: middle;
position: relative;
cursor: pointer;
button {
cursor: pointer;
}
input {
position: absolute;
height: 100%;
width: 100%;
top: 0;
left: 0;
cursor: pointer;
opacity: 0;
@include m-theme() {
color: themed($m-grey-400);
}
}
input::-webkit-file-upload-button {
cursor: pointer;
}
}
.m-upload {
input {
outline: none;
border: 0;
font-size: 14px;
@include m-theme() {
border-right: 1px solid themed($m-grey-100);
}
}
.mdl-progress {
width: 100%;
}
}
.m-splash {
text-align: center;
padding: 10vh;
i {
font-size: 152px;
text-align: center;
width: 100%;
}
h3 {
text-transform: uppercase;
font-weight: 100;
}
}
}
.m-capture-edit-container {
input {
flex: 2;
}
select {
width: auto;
flex: 1;
}
.m-capture-mature {
margin-left: $minds-padding;
cursor: pointer;
i {
cursor: pointer;
vertical-align: middle;
}
span {
font-size: 11px;
text-transform: uppercase;
}
}
}
.m-capture-save-to-album-button {
margin-left: 8px;
}
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { LICENSES, ACCESS } from '../../services/list-options';
import { Session } from '../../services/session';
import { Upload } from '../../services/api/upload';
import { Client } from '../../services/api/client';
@Component({
selector: 'minds-capture',
host: {
'(dragover)': 'dragover($event)',
'(dragleave)': 'dragleave($event)',
'(drop)': 'drop($event)',
},
templateUrl: 'capture.html',
})
export class Capture {
uploads: Array<any> = [];
postMeta: any = {}; //TODO: make this object
albums: Array<any> = [];
offset: string = '';
inProgress: boolean = false;
dragging: boolean = false;
control;
default_maturity: number = 0;
default_license: string = 'all-rights-reserved';
licenses = LICENSES;
access = ACCESS;
constructor(
public session: Session,
public _upload: Upload,
public client: Client,
public router: Router
) {}
ngOnInit() {
if (!this.session.isLoggedIn()) {
this.router.navigate(['/login']);
} else {
this.getAlbums();
}
}
getAlbums() {
var self = this;
this.client
.get('api/v1/media/albums/list', { limit: 5, offset: this.offset })
.then((response: any) => {
if (!response.entities) return;
console.log(response);
self.albums = response.entities;
});
}
createAlbum(album) {
var self = this;
this.inProgress = true;
this.client
.post('api/v1/media/albums', { title: album.value })
.then((response: any) => {
self.albums.unshift(response.album);
self.postMeta.album_guid = response.album.guid;
self.inProgress = false;
album.value = '';
});
}
selectAlbum(album) {
this.postMeta.album_guid = album.guid;
}
deleteAlbum(album) {
if (confirm('Are you sure?')) {
let i: any;
for (i in this.albums) {
if (album.guid === this.albums[i].guid) this.albums.splice(i, 1);
}
this.client.delete('api/v1/media/albums/' + album.guid);
}
}
/**
* Add a file to the upload queue
*/
add(file: any) {
var self = this;
for (var i = 0; i < file.files.length; i++) {
var data: any = {
guid: null,
state: 'created',
progress: 0,
license: this.default_license || 'all-rights-reserved',
mature: this.default_maturity || 0,
};
var fileInfo = file.files[i];
if (fileInfo.type && fileInfo.type.indexOf('image') > -1) {
data.type = 'image';
} else if (fileInfo.type && fileInfo.type.indexOf('video') > -1) {
data.type = 'video';
} else if (fileInfo.type && fileInfo.type.indexOf('audio') > -1) {
data.type = 'audio';
} else {
data.type = 'unknown';
}
data.name = fileInfo.name;
data.title = data.name;
var upload_i = this.uploads.push(data) - 1;
this.uploads[upload_i].index = upload_i;
this.upload(this.uploads[upload_i], fileInfo);
}
}
upload(data, fileInfo) {
var self = this;
this._upload
.post('api/v1/media', [fileInfo], this.uploads[data.index], progress => {
self.uploads[data.index].progress = progress;
if (progress === 100) {
self.uploads[data.index].state = 'uploaded';
}
})
.then((response: any) => {
self.uploads[data.index].guid = response.guid;
self.uploads[data.index].state = 'complete';
self.uploads[data.index].progress = 100;
})
.catch(function(e) {
self.uploads[data.index].state = 'failed';
console.error(e);
});
}
modify(index) {
this.uploads[index].state = 'uploaded';
//we don't always have a guid ready, so keep checking for one
var promise = new Promise((resolve, reject) => {
if (this.uploads[index].guid) {
setTimeout(() => {
resolve();
}, 300);
return;
}
var interval = setInterval(() => {
if (this.uploads[index].guid) {
resolve();
clearInterval(interval);
}
}, 1000);
});
promise.then(() => {
this.client
.post('api/v1/media/' + this.uploads[index].guid, this.uploads[index])
.then((response: any) => {
console.log('response from modify', response);
this.uploads[index].state = 'complete';
});
});
}
/**
* Publish our uploads to an album
*/
publish() {
if (!this.postMeta.album_guid)
return alert('You must select an album first');
var self = this;
var guids = this.uploads.map(upload => {
if (upload.guid !== null || upload.guid !== 'null' || !upload.guid)
return upload.guid;
});
this.client
.post('api/v1/media/albums/' + this.postMeta.album_guid, { guids: guids })
.then((response: any) => {
self.router.navigate(['/media', this.postMeta.album_guid]);
})
.catch(e => {
alert('there was a problem.');
});
}
/**
* Make sure the browser doesn't freak
*/
dragover(e) {
e.preventDefault();
this.dragging = true;
}
/**
* Tell the app we have stopped dragging
*/
dragleave(e) {
e.preventDefault();
console.log(e);
if (e.layerX < 0) this.dragging = false;
}
drop(e) {
e.preventDefault();
this.dragging = false;
this.add(e.dataTransfer);
}
}
<div class="mdl-tabs__tab-bar">
<a
[routerLink]="['/discovery', _filter, 'albums']"
class="mdl-tabs__tab"
[ngClass]="{'is-active': _type == 'albums' || _type == 'all'}"
[hidden]="_filter != 'owner'"
i18n="@@MINDS__DISCOVERY__ALBUMS_TAB"
>Albums</a
>
<a
[routerLink]="['/discovery', _filter, 'videos']"
class="mdl-tabs__tab"
[ngClass]="{'is-active': _type == 'videos' || _type == 'all'}"
i18n="@@M__FEATURE__VIDEO_PLURAL"
>Videos</a
>
<a
[routerLink]="['/discovery', _filter, 'images']"
class="mdl-tabs__tab"
[ngClass]="{'is-active': _type == 'images' || _type == 'all'}"
i18n="@@M__FEATURE__IMAGE_PLURAL"
>Images</a
>
<a
[routerLink]="['/discovery', _filter, 'channels']"
class="mdl-tabs__tab"
[ngClass]="{'is-active': _type == 'channels' || _type == 'all'}"
[hidden]="_filter == 'owner' || _owner"
i18n="@@M__FEATURE__CHANNEL__PLURAL"
>Channels</a
>
</div>
<div
class="mdl-grid m-discovery-{{_filter}} m-discovery-{{_filter}}-{{_type}}"
style="max-width:900px"
>
<a
class="mdl-cell mdl-cell--4-col"
[routerLink]="['/capture']"
style="text-decoration: none;"
[hidden]="_type == 'channels' || _filter == 'suggested'"
>
<div
class="minds-add-card mdl-card mdl-shadow--2dp mdl-color--blue-grey-400 mdl-color-text--blue-grey-100"
>
<i class="material-icons">file_upload</i>
<h3 i18n="@@M__ACTION__UPLOAD">Upload</h3>
</div>
</a>
<div
*ngIf="_filter == 'suggested' && _type == 'channels'"
class="m-discovery-suggested-location-bar"
>
<div class="m-discovery-suggested-location-inner">
<span
*ngIf="!nearby && hasNearby"
(click)="setNearby(true)"
class="m-discovery-suggested-location-toggle mdl-color--blue-grey-400 mdl-color-text--white"
i18n="@@MINDS__DISCOVERY__LOCATION_OFF_TOGGLE"
>Location off</span
>
<span
*ngIf="!nearby && !hasNearby"
(click)="setNearby(true)"
class="m-discovery-suggested-location-toggle mdl-color--grey-400 mdl-color-text--white"
i18n="@@MINDS__DISCOVERY__NO_RESULTS_LABEL"
>No results</span
>
<span
*ngIf="nearby"
(click)="setNearby(false)"
class="m-discovery-suggested-location-toggle mdl-color--blue-grey-400 mdl-color-text--white"
i18n="@@MINDS__DISCOVERY__LOCATING_LABEL"
>Locating</span
>
<!-- only show if locating is on -->
<span
*ngIf="nearby || (!nearby && !hasNearby)"
class="m-discovery-suggested-location-options"
>
<select
(change)="distance = dist.value; load(true)"
#dist
class="mdl-color-text--blue-grey-600"
>
<option value="5"
>5
<ng-container i18n="@@MINDS__DISCOVERY__MILES_UNIT"
>miles</ng-container
></option
>
<option value="25"
>25
<ng-container i18n="@@MINDS__DISCOVERY__MILES_UNIT"
>miles</ng-container
></option
>
<option value="50"
>50
<ng-container i18n="@@MINDS__DISCOVERY__MILES_UNIT"
>miles</ng-container
></option
>
<option value="100"
>100
<ng-container i18n="@@MINDS__DISCOVERY__MILES_UNIT"
>miles</ng-container
></option
>
</select>
<m-18n i18n="Distance from a city@@MINDS__DISCOVERY__OF_WORD">of</m-18n>
<input
(keyup)="findCity(city)"
name="city"
[(ngModel)]="city"
class="mdl-color--blue-grey-300 mdl-color-text--white"
placeholder="Enter your city..."
i18n-placeholder="@@M__COMMON__ENTER_CITY"
/>
</span>
<div
class="m-discovery-cities mdl-card mdl-shadow--4dp"
*ngIf="cities.length > 0"
>
<p
class="mdl-color-text--blue-grey-300"
i18n="@@M__COMMON__SELECT_CITY"
>
Select your city:
</p>
<li
(click)="setCity(c)"
*ngFor="let c of cities"
[hidden]="!(c.address.town || c.address.city)"
>
{{c.address.town}}{{c.address.city}}, {{c.address.state}}
</li>
</div>
</div>
</div>
<div
class="mdl-cell mdl-cell--4-col m-discovery-wrapper"
*ngFor="let entity of entities; let i = index"
[hidden]="i != 0 && _filter == 'suggested'"
>
<minds-card-video
[object]="entity"
*ngIf="entity.subtype == 'video'"
class="mdl-card mdl-shadow--2dp"
[ngClass]="{'mdl-shadow--6dp': _filter == 'suggested'}"
></minds-card-video>
<minds-card-image
[object]="entity"
*ngIf="entity.subtype == 'image'"
class="mdl-card mdl-shadow--2dp"
[ngClass]="{'mdl-shadow--6dp': _filter == 'suggested'}"
></minds-card-image>
<minds-card-album
[object]="entity"
*ngIf="entity.subtype == 'album'"
class="mdl-card mdl-shadow--2dp"
[ngClass]="{'mdl-shadow--6dp': _filter == 'suggested'}"
></minds-card-album>
<!-- START: User Only -->
<minds-card-user
[object]="entity"
[avatarSize]="_filter == 'suggested' ? 'large' : 'medium'"
*ngIf="entity.type == 'user'"
class="mdl-card mdl-shadow--2dp"
></minds-card-user>
<!-- END: User Only -->
<div
class="m-discovery-suggested-actions mdl-card mdl-shadow--6dp mdl-color--blue-grey-900"
*ngIf="_filter == 'suggested'"
>
<span class="minds-button-edit m-button" (click)="pass(i)">
<button i18n="@@MINDS__DISCOVERY__PASS_ACTION">Pass</button>
</span>
<minds-button-subscribe
class="m-button"
[user]="entity"
(click)="pop(i)"
[hidden]="_type != 'channels'"
></minds-button-subscribe>
<minds-button-thumbs-up
class="m-button"
[object]="entity"
(click)="pop(i)"
[hidden]="_type == 'channels'"
></minds-button-thumbs-up>
<minds-button-thumbs-down
class="m-button"
[object]="entity"
(click)="pop(i)"
[hidden]="_type == 'channels'"
></minds-button-thumbs-down>
<minds-button-remind
class="m-button"
[object]="entity"
(click)="pop(i)"
[hidden]="_type == 'channels'"
></minds-button-remind>
</div>
</div>
<infinite-scroll
distance="25%"
(load)="load()"
[moreData]="moreData"
[inProgress]="inProgress"
*ngIf="_filter != 'suggested'"
>
</infinite-scroll>
<div
class="mdl-spinner mdl-js-spinner is-active"
[mdl]
[hidden]="!inProgress"
*ngIf="_filter == 'suggested'"
></div>
</div>
.m-discovery-suggested {
margin-top: 16px;
max-width: 100%;
.m-discovery-wrapper {
overflow: hidden;
}
.mdl-cell {
min-width: 100%;
margin: auto;
.mdl-card {
z-index: 1;
display: block;
}
minds-card-user {
width: 400px;
margin: 0 auto;
.minds-usercard-banner {
display: none !important;
}
.avatar {
margin: 0 auto;
width: 100%;
height: 368px;
box-sizing: border-box;
text-align: center;
vertical-align: middle;
padding: 8px;
@include m-theme() {
background-color: themed($m-grey-50);
}
img {
height: 100%;
}
}
.minds-tabs {
//margin: -48px 12px 0 112px;
margin-bottom: 8px;
}
.minds-usercard-block {
display: block;
margin-top: 0;
width: 100%;
box-sizing: border-box;
.body {
h3 {
@include m-theme() {
color: themed($m-grey-950);
}
}
> span {
@include m-theme() {
color: themed($m-blue-grey-400);
}
}
}
@media screen and (min-width: 400px) {
h3 {
font-size: 32px;
line-height: normal;
}
}
}
@media screen and (max-width: 400px) {
.minds-usercard-block {
display: block;
@include m-theme() {
color: themed($m-grey-950);
}
}
.avatar {
width: 100%;
}
.body {
margin: 16px 0 0;
}
.m-usercard-bio {
margin-top: 0 !important;
}
}
.m-usercard-bio {
display: flex;
padding: 8px;
margin-top: 16px;
@include m-theme() {
color: themed($m-grey-950);
}
> div {
font-size: 12px;
//flex:1;
margin-right: 16px;
display: flex;
flex-direction: row;
align-items: center;
max-height: 40px;
overflow: hidden;
&.m-usercard-bio-brief {
align-items: flex-start;
}
> i {
vertical-align: middle;
margin-right: 8px;
}
}
}
minds-button-subscribe {
display: none;
}
}
minds-card-video,
minds-card-image {
.minds-video-thumbnail {
min-height: 300px;
background-repeat: no-repeat !important;
background-size: contain !important;
}
}
.m-action-tabs {
display: none;
}
}
.m-discovery-suggested-actions {
display: flex !important;
flex-flow: row nowrap;
align-items: center;
margin: -8px 12px;
padding: 16px 0;
z-index: 0 !important;
.m-button {
flex: 1;
text-align: center;
> * {
padding: 12px 32px;
> i {
vertical-align: middle;
}
}
}
}
.m-discovery-suggested-location-bar {
width: 100%;
margin: 0 auto 16px auto;
text-align: center;
.m-discovery-suggested-location-inner {
min-width: 50%;
margin: auto;
}
.m-discovery-suggested-location-toggle {
border-radius: 3px;
padding: 8px;
cursor: pointer;
}
.m-discovery-suggested-location-options {
select {
-webkit-appearance: none;
font-size: inherit;
font-weight: bold;
padding: 8px;
font-family: inherit;
border: 0;
}
input {
padding: 8px;
margin-left: 12px;
border: 0;
border-radius: 3px;
background: transparent;
font-size: inherit;
font-family: inherit;
color: inherit;
width: 132px;
@media screen and (max-width: 330px) {
width: 80px;
}
&::-webkit-input-placeholder {
@include m-theme() {
color: themed($m-white);
}
}
&::-moz-placeholder {
@include m-theme() {
color: themed($m-white);
}
}
&:-moz-placeholder {
@include m-theme() {
color: themed($m-white);
}
}
&:-ms-input-placeholder {
@include m-theme() {
color: themed($m-white);
}
}
}
}
.m-discovery-cities {
width: 246px;
text-align: left;
margin: 16px auto;
padding: 16px;
p {
margin: 0;
padding: 0;
font-size: 12px;
}
li {
list-style: none;
padding: 12px 0;
cursor: pointer;
@include m-theme() {
border-bottom: 1px solid themed($m-grey-50);
}
}
}
}
}
.m-discovery-suggested-channels {
minds-button-feature {
@include m-theme() {
background-color: themed($m-white);
}
}
.m-discovery-suggested-actions {
width: 400px !important;
padding: 24px 0 16px;
margin: -8px auto 0;
border-radius: 2px;
button {
> i.material-icons {
font-size: 20px;
}
}
.m-button-pass {
@include m-theme() {
color: themed($m-white);
border-color: themed($m-white);
background-color: themed($m-red);
}
&:hover {
@include m-theme() {
background-color: themed($m-red-dark);
}
}
}
.minds-subscribe-button {
@include m-theme() {
color: themed($m-white);
border-color: themed($m-white);
background-color: themed($m-green);
}
&:hover {
@include m-theme() {
background-color: themed($m-green-dark);
}
}
}
}
}
.m-discovery-channels-note {
max-width: 400px;
margin: 16px auto 0;
text-align: center;
}
import { Component } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs';
import { Client } from '../../services/api';
import { Session } from '../../services/session';
import { ContextService } from '../../services/context.service';
@Component({
moduleId: module.id,
selector: 'minds-discovery',
templateUrl: 'discovery.html',
})
export class Discovery {
_filter: string = 'featured';
_owner: string = '';
_type: string = 'all';
entities: Array<Object> = [];
moreData: boolean = true;
offset: string | number = '';
inProgress: boolean = false;
city: string = '';
cities: Array<any> = [];
nearby: boolean = false;
hasNearby: boolean = false;
distance: number = 5;
paramsSubscription: Subscription;
searching;
constructor(
public session: Session,
public client: Client,
public router: Router,
public route: ActivatedRoute,
private context: ContextService
) {}
ngOnInit() {
this.paramsSubscription = this.route.params.subscribe(params => {
if (params['filter']) {
this._filter = params['filter'];
switch (this._filter) {
case 'all':
break;
case 'suggested':
if (!this.session.isLoggedIn()) {
this.router.navigate(['/discovery/featured/channels']);
return;
}
this._type = 'channels';
if (this.session.getLoggedInUser().city) {
this.city = this.session.getLoggedInUser().city;
this.nearby = true;
this.hasNearby = false;
}
break;
case 'trending':
this._type = 'images';
break;
case 'featured':
this._type = 'channels';
break;
case 'owner':
break;
default:
this._owner = this._filter;
this._filter = this._filter;
}
}
if (params['type']) {
this._type = params['type'];
}
switch (this._type) {
case 'videos':
this.context.set('object:video');
break;
case 'images':
this.context.set('object:image');
break;
case 'channels':
this.context.set('user');
break;
default:
this.context.reset();
}
this.inProgress = false;
this.entities = [];
this.load(true);
});
}
ngOnDestroy() {
this.paramsSubscription.unsubscribe();
}
load(refresh: boolean = false) {
if (this.inProgress) return false;
if (refresh) this.offset = '';
this.inProgress = true;
var filter = this._filter;
if (this._owner) filter = 'owner';
this.client
.get('api/v1/entities/' + filter + '/' + this._type + '/' + this._owner, {
limit: 24,
offset: this.offset,
skip: 0,
nearby: this.nearby,
distance: this.distance,
})
.then((data: any) => {
if (!data.entities) {
if (this.nearby) {
this.hasNearby = false;
return this.setNearby(false);
}
this.moreData = false;
this.inProgress = false;
return false;
}
if (this.nearby) {
this.hasNearby = true;
}
if (refresh) {
this.entities = data.entities;
} else {
if (this.offset && filter != 'trending') data.entities.shift();
this.entities = this.entities.concat(data.entities);
}
this.offset = data['load-next'];
this.inProgress = false;
if (!this.offset) this.moreData = false;
})
.catch(e => {
this.inProgress = false;
if (this.nearby) {
this.setNearby(false);
}
});
}
pass(index: number) {
var entity: any = this.entities[index];
this.client.post('api/v1/entities/suggested/pass/' + entity.guid);
this.pop(index);
}
pop(index: number) {
this.entities.splice(index, 1);
if (this.entities.length < 3) {
this.offset = 3;
this.load(true);
}
}
findCity(q: string) {
if (this.searching) {
clearTimeout(this.searching);
}
this.searching = setTimeout(() => {
this.client
.get('api/v1/geolocation/list', { q: q })
.then((response: any) => {
this.cities = response.results;
});
}, 100);
}
setCity(row: any) {
// Deprecated
}
setNearby(nearby: boolean) {
this.nearby = nearby;
this.entities = [];
this.load(true);
}
}
<div class="m-rewards">
<div class="m-rewards--marketing-header">
<h1>{{name}}</h1>
<h3 i18n="M__REWARDS_MKT__TITLE_THANKS">Thank you for investing!</h3>
<div class="m-rewards--overlay"></div>
</div>
<div class="m-rewards--rewards mdl-grid mdl-grid--no-spacing" *ngIf="rewards">
<div class="mdl-cell mdl-cell--12-col">
<h2 i18n="M__REWARDS_MKT__YOUR_REWARDS">Your rewards</h2>
</div>
<div *ngIf="rewards.indexOf('10,000 points') != -1">
<i class="material-icons mdl-color-text--blue-grey-800"
>account_balance</i
>
<label i18n="M__REWARDS_MKT__10K_POINTS">10,000 points</label>
</div>
<div *ngIf="rewards.indexOf('an investor badge') != -1">
<i class="material-icons mdl-color-text--blue-grey-800"
><i class="material-icons">flight_takeoff</i></i
>
<label i18n="M__REWARDS_MKT__INVESTOR_BADGE">investor badge</label>
</div>
<div *ngIf="rewards.indexOf('an official founders page') != -1">
<i class="material-icons mdl-color-text--blue-grey-800">account_box</i>
<label i18n="M__REWARDS_MKT__FOUNDERS_PAGE">official founders page</label>
</div>
<div *ngIf="rewards.indexOf('Minds video chat') != -1">
<i class="material-icons mdl-color-text--blue-grey-800">video_call</i>
<label i18n="M__REWARDS_MKT__VIDEO_CHAT">Minds video chat</label>
</div>
<div
*ngIf="rewards.indexOf('a t-shirt') != -1"
i
style="align-items: center;"
>
<svg style="width:46px;height:46px" viewBox="0 0 24 24">
<path
fill="#37474f"
d="M16,21H8A1,1 0 0,1 7,20V12.07L5.7,13.12C5.31,13.5 4.68,13.5 4.29,13.12L1.46,10.29C1.07,9.9 1.07,9.27 1.46,8.88L7.34,3H9C9,4.1 10.34,5 12,5C13.66,5 15,4.1 15,3H16.66L22.54,8.88C22.93,9.27 22.93,9.9 22.54,10.29L19.71,13.12C19.32,13.5 18.69,13.5 18.3,13.12L17,12.07V20A1,1 0 0,1 16,21M20.42,9.58L16.11,5.28C15.8,5.63 15.43,5.94 15,6.2C14.16,6.7 13.13,7 12,7C10.3,7 8.79,6.32 7.89,5.28L3.58,9.58L5,11L8,9H9V19H15V9H16L19,11L20.42,9.58Z"
></path>
</svg>
<label>t-shirt</label>
</div>
<div *ngIf="rewards.indexOf('a coffee cup') != -1">
<i class="material-icons mdl-color-text--blue-grey-800">free_breakfast</i>
<label i18n="M__REWARDS_MKT__COFFEE_CUP">coffee cup</label>
</div>
<div *ngIf="rewards.indexOf('a town hall meeting') != -1">
<i class="material-icons mdl-color-text--blue-grey-800">group</i>
<label i18n="M__REWARDS_MKT__TOWN_HALL_MEETING">town hall meeting</label>
</div>
<div
*ngIf="rewards.indexOf('a dinner with bill') != -1 && !(rewards.indexOf('a wine & dine with the team'))"
>
<i class="material-icons mdl-color-text--blue-grey-800">local_dining</i>
<label i18n="M__REWARDS_MKT__DINNER_WITH_BILL">dinner with bill</label>
</div>
<div *ngIf="rewards.indexOf('a wine & dine with the team') != -1">
<i class="material-icons mdl-color-text--blue-grey-800">local_dining</i>
<label i18n="M__REWARDS_MKT__WINE_AND_DINE_W_TEAM"
>wine & dine with the team</label
>
</div>
<div *ngIf="rewards.indexOf('quarterly video chats') != -1">
<i class="material-icons mdl-color-text--blue-grey-800">video_call</i>
<label i18n="M__REWARDS_MKT__QT_VIDEO_CHATS">quarterly video chats</label>
</div>
<div *ngIf="rewards.indexOf('feature sponsorship') != -1">
<i class="material-icons mdl-color-text--blue-grey-800">highlight</i>
<label i18n="M__REWARDS_MKT__FEATURES_SPONSORSHIP"
>feature sponsorship</label
>
</div>
</div>
<div class="m-rewards--form">
<form
#claimForm="ngForm"
(submit)="onClaim()"
class="mdl-grid"
*ngIf="loggedIn"
>
<h6
class="mdl-cell mdl-cell--12-col"
*ngIf="requiresCellPhone"
i18n="M__REWARDS_MKT__NEED_MORE_DATA"
>
We need more data before we can proceed
</h6>
<div class="mdl-cell mdl-cell--6-col" *ngIf="requiresTShirtSize">
<label i18n="M__REWARDS_MKT__TSHIRT_SIZE">T-shirt Size</label>
<select name="tshirtSize" [(ngModel)]="tshirtSize" required>
<option *ngFor="let size of tshirtSizes" [ngValue]="size"
>{{ size }}</option
>
</select>
</div>
<div class="mdl-cell mdl-cell--6-col" *ngIf="requiresTShirtSize">
<label i18n="M__REWARDS_MKT__ADDRESS">Address</label>
<textarea name="address" [(ngModel)]="address"></textarea>
</div>
<div class="mdl-cell mdl-cell--6-col" *ngIf="requiresCellPhone">
<p i18n="M__REWARDS_MKT__EMAIL_BILL">
Please email bill@minds.com to arrange your Dinner reward
</p>
</div>
<div
class="m-rewards--form-action mdl-cell"
[class.mdl-cell--6-col]="requiresCellPhone"
[class.mdl-cell--12-col]="!requiresCellPhone"
>
<button
type="submit"
class="mdl-button mdl-button--colored mdl-color--green"
[disabled]="!claimForm.form.valid || inProgress"
[ngStyle]="{'margin': !requiresCellPhone ? '16px auto': '16px'}"
i18n="M__REWARDS_MKT__CLAIM_MY_REWARDS"
>
Claim my rewards
</button>
</div>
</form>
<button
class="mdl-button mdl-button--colored mdl-color--green"
*ngIf="!loggedIn"
routerLink="/login"
i18n="M__REWARDS_MKT__LOGIN_TO_CLAIM"
>
Login to claim your rewards
</button>
</div>
</div>
@import 'defaults';
.m-rewards--marketing-header {
background: url('<%= APP_CDN %>/assets/photos/galaxy.jpg');
width: 100%;
text-align: center;
display: flex;
align-content: center;
flex-direction: column;
padding: 224px 36px;
box-sizing: border-box;
position: relative;
@media only screen and (max-width: 400px) {
padding: 110px 0;
}
h1,
h3 {
font-family: 'Roboto', Helvetica, sans-serif;
@include m-theme() {
color: themed($m-white);
text-shadow: 0 0 3px themed($m-grey-900);
}
@media only screen and (max-width: 400px) {
margin: 0;
}
z-index: 1;
}
h1 {
word-spacing: 25px;
letter-spacing: 4px;
text-transform: uppercase;
@media only screen and (max-width: 400px) {
font-size: 30px;
}
}
h3 {
letter-spacing: 2px;
word-spacing: 3px;
font-weight: 300;
@media only screen and (max-width: 400px) {
font-size: 14px;
}
}
.m-rewards--marketing-action-button {
margin-bottom: -80px;
margin-top: 32px;
button {
letter-spacing: 3px;
font-size: 18px;
line-height: 35px;
height: 53px;
padding: 0 24px;
font-weight: 300;
font-family: 'Roboto', Helvetica, sans-serif;
@include m-theme() {
color: themed($m-white);
}
}
z-index: 2;
}
.m-rewards--overlay {
display: block;
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
z-index: 0;
@include m-theme() {
background-color: rgba(themed($m-black), 0.15);
}
}
}
.m-rewards--rewards {
max-width: 990px;
text-align: center;
h2 {
text-transform: uppercase;
font-size: 32px;
font-weight: 400;
letter-spacing: 4px;
}
> div {
flex: auto;
display: flex;
flex-direction: column;
padding: 24px 12px;
i {
font-size: 46px;
}
label {
padding-top: 8px;
font-size: 16px;
text-transform: capitalize;
letter-spacing: 1.25px;
font-family: 'Roboto', Helvetica, sans-serif;
}
}
}
.m-rewards--form {
max-width: 990px;
margin: 32px auto;
text-align: center;
.mdl-cell {
text-align: left;
display: flex;
align-items: center;
label {
display: block;
font-size: 16px;
text-transform: capitalize;
letter-spacing: 1.25px;
font-family: 'Roboto', Helvetica, sans-serif;
padding: 8px;
}
p {
padding: 0;
margin: 0;
}
}
textarea {
padding: 16px;
width: 100%;
@include m-theme() {
border: 1px solid themed($m-grey-50);
}
}
select {
padding: 8px;
/* -webkit-appearance: none; */
font-size: 16px;
margin-left: 8px;
}
button {
padding: 8px 16px;
max-height: none;
height: auto;
font-family: 'Roboto', Helvetica, sans-serif;
letter-spacing: 2.5px;
font-size: 16px;
margin: 16px;
@include m-theme() {
color: themed($m-white) !important;
}
}
}
import { Client } from '../../services/api';
import { Component } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Subscription } from 'rxjs';
import { Title } from '@angular/platform-browser';
import { Session } from '../../services/session';
import { CookieService } from '../../common/services/cookie.service';
@Component({
moduleId: module.id,
selector: 'minds-rewards-component',
templateUrl: 'rewards.html',
})
export class RewardsComponent {
paramsSubscription: Subscription;
uuid: string;
requiresTShirtSize: boolean;
requiresCellPhone: boolean;
rewards: any;
name: string;
loggedIn: boolean;
tshirtSize: string;
private cellPhoneNumber: string;
address: string;
loading: boolean = true;
inProgress: boolean = false;
tshirtSizes: Array<string> = ['Small', 'Medium', 'Large', 'Extra Large'];
constructor(
private session: Session,
private client: Client,
private route: ActivatedRoute,
private router: Router,
cookieService: CookieService
) {
if (cookieService.get('redirect')) cookieService.remove('redirect');
this.loggedIn = this.session.isLoggedIn();
this.paramsSubscription = this.route.params.subscribe(params => {
if (params['uuid']) {
this.uuid = params['uuid'];
}
});
this.client
.get('api/v1/rewards/data', { uuid: this.uuid })
.then((res: any) => {
this.loading = false;
if (res.hasOwnProperty('valid') && !res.valid) {
this.router.navigate(['/']);
} else {
this.requiresTShirtSize = res.requiresTShirtSize;
this.requiresCellPhone = res.requiresCellPhone;
this.rewards = res.rewards;
this.name = res.name;
}
});
}
ngOnInit() {}
ngOnDestroy() {
this.paramsSubscription.unsubscribe();
}
onClaim() {
if (this.inProgress) return;
this.inProgress = true;
const options = {
uuid: this.uuid,
user_guid: this.session.getLoggedInUser().guid,
tshirtSize: this.tshirtSize,
address: this.address,
};
this.client
.post('api/v1/rewards/claim', options)
.then(res => {
alert('Thank you. Your rewards have been claimed.');
this.router.navigate(['/newsfeed']);
})
.catch(error => {
this.inProgress = false;
console.error('error! ', error);
});
}
onLogin() {
localStorage.setItem('redirect', '/claim-rewards/' + this.uuid);
this.router.navigate(['/login']);
}
}
import { AdminReportsDownload } from './controllers/admin/reports-download/reports-download';
import { AdminBoosts } from './controllers/admin/boosts/boosts';
import { AdminFirehoseComponent } from './controllers/admin/firehose/firehose.component';
import { AdminPages } from './controllers/admin/pages/pages';
import { AdminReports } from './controllers/admin/reports/reports';
import { AdminMonetization } from './controllers/admin/monetization/monetization';
import { AdminPrograms } from './controllers/admin/programs/programs.component';
import { AdminPayouts } from './controllers/admin/payouts/payouts.component';
import { AdminFeatured } from './controllers/admin/featured/featured';
import { AdminTagcloud } from './controllers/admin/tagcloud/tagcloud.component';
import { AdminVerify } from './controllers/admin/verify/verify.component';
import { RejectionReasonModalComponent } from './controllers/admin/boosts/modal/rejection-reason-modal.component';
import { AdminInteractions } from './controllers/admin/interactions/interactions.component';
import { InteractionsTableComponent } from './controllers/admin/interactions/table/table.component';
import { AdminPurchasesComponent } from './controllers/admin/purchases/purchases.component';
import { AdminWithdrawals } from './controllers/admin/withdrawals/withdrawals.component';
import { AdminFeaturesComponent } from './controllers/admin/features/admin-features.component';
export const MINDS_DECLARATIONS: any[] = [
// Components
InteractionsTableComponent,
// Controllers; Controller-based directives
AdminInteractions,
RejectionReasonModalComponent,
AdminBoosts,
AdminFirehoseComponent,
AdminPages,
AdminReports,
AdminMonetization,
AdminPrograms,
AdminPayouts,
AdminFeatured,
AdminTagcloud,
AdminVerify,
AdminPurchasesComponent,
AdminWithdrawals,
AdminReportsDownload,
AdminFeaturesComponent,
];
......@@ -8,10 +8,10 @@ import { ActivityService } from '../../common/services/activity.service';
@Component({
selector: 'minds-admin',
templateUrl: 'admin.html',
templateUrl: 'admin.component.html',
providers: [ActivityService],
})
export class Admin {
export class AdminComponent {
filter: string = '';
paramsSubscription: Subscription;
......
export const AdminModuleLazyRoutes = {
path: 'admin',
loadChildren: () => import('./admin.module').then(m => m.AdminModule),
};
import { NgModule } from '@angular/core';
import { CommonModule as NgCommonModule } from '@angular/common';
import { AdminReportsDownload } from './reports-download/reports-download';
import { AdminBoosts } from './boosts/boosts';
import { AdminFirehoseComponent } from './firehose/firehose.component';
import { AdminPages } from './pages/pages';
import { AdminReports } from './reports/reports';
import { AdminMonetization } from './monetization/monetization';
import { AdminPrograms } from './programs/programs.component';
import { AdminPayouts } from './payouts/payouts.component';
import { AdminFeatured } from './featured/featured';
import { AdminTagcloud } from './tagcloud/tagcloud.component';
import { AdminVerify } from './verify/verify.component';
import { RejectionReasonModalComponent } from './boosts/modal/rejection-reason-modal.component';
import { AdminInteractions } from './interactions/interactions.component';
import { InteractionsTableComponent } from './interactions/table/table.component';
import { AdminPurchasesComponent } from './purchases/purchases.component';
import { AdminWithdrawals } from './withdrawals/withdrawals.component';
import { AdminFeaturesComponent } from './features/admin-features.component';
import { CommonModule } from '../../common/common.module';
import { RouterModule, Routes } from '@angular/router';
import { AdminComponent } from './admin.component';
import { LegacyModule } from '../legacy/legacy.module';
import { BlogModule } from '../blogs/blog.module';
import { GroupsModule } from '../groups/groups.module';
import { FormsModule } from '@angular/forms';
import { CommentsModule } from '../comments/comments.module';
const routes: Routes = [
{
path: ':filter/:type',
component: AdminComponent,
data: { title: 'Admin' },
},
{ path: ':filter', component: AdminComponent, data: { title: 'Admin' } },
];
@NgModule({
imports: [
NgCommonModule,
CommonModule,
RouterModule.forChild(routes),
FormsModule,
LegacyModule,
BlogModule,
GroupsModule,
CommentsModule,
],
declarations: [
AdminComponent,
InteractionsTableComponent,
AdminInteractions,
RejectionReasonModalComponent,
AdminBoosts,
AdminFirehoseComponent,
AdminPages,
AdminReports,
AdminMonetization,
AdminPrograms,
AdminPayouts,
AdminFeatured,
AdminTagcloud,
AdminVerify,
AdminPurchasesComponent,
AdminWithdrawals,
AdminReportsDownload,
AdminFeaturesComponent,
],
})
export class AdminModule {}
......@@ -5,8 +5,8 @@ import { Subscription } from 'rxjs';
import { Client } from '../../../services/api';
import { RejectionReasonModalComponent } from './modal/rejection-reason-modal.component';
import { Reason, rejectionReasons } from './rejection-reasons';
import { ReportCreatorComponent } from '../../../modules/report/creator/creator.component';
import { Reason, rejectionReasons } from '../../boost/rejection-reasons';
import { ReportCreatorComponent } from '../../report/creator/creator.component';
import { OverlayModalService } from '../../../services/ux/overlay-modal';
import { ActivityService } from '../../../common/services/activity.service';
......
......@@ -5,7 +5,7 @@ import {
Input,
Output,
} from '@angular/core';
import { Reason, rejectionReasons } from '../rejection-reasons';
import { Reason, rejectionReasons } from '../../../boost/rejection-reasons';
@Component({
moduleId: module.id,
......
......@@ -13,7 +13,7 @@ import { clientMock } from '../../../../tests/client-mock.spec';
import { AdminFirehoseComponent } from './firehose.component';
import { Session } from '../../../services/session';
import { RouterTestingModule } from '@angular/router/testing';
import { NewsfeedHashtagSelectorService } from '../../../modules/newsfeed/services/newsfeed-hashtag-selector.service';
import { NewsfeedHashtagSelectorService } from '../../newsfeed/services/newsfeed-hashtag-selector.service';
import { newsfeedHashtagSelectorServiceMock } from '../../../../tests/newsfeed-hashtag-selector-service-mock.spec';
import { overlayModalServiceMock } from '../../../../tests/overlay-modal-service-mock.spec';
import { activityServiceMock } from '../../../../tests/activity-service-mock.spec';
......
......@@ -4,8 +4,8 @@ import { Session } from '../../../services/session';
import { OverlayModalService } from '../../../services/ux/overlay-modal';
import { ActivatedRoute, Router } from '@angular/router';
import { Subscription } from 'rxjs';
import { NewsfeedHashtagSelectorService } from '../../../modules/newsfeed/services/newsfeed-hashtag-selector.service';
import { ReportCreatorComponent } from '../../../modules/report/creator/creator.component';
import { NewsfeedHashtagSelectorService } from '../../newsfeed/services/newsfeed-hashtag-selector.service';
import { ReportCreatorComponent } from '../../report/creator/creator.component';
import { ActivityService } from '../../../common/services/activity.service';
@Component({
moduleId: module.id,
......
export const AnalyticsModuleLazyRoutes = {
path: 'analytics',
loadChildren: () => import('./analytics.module').then(m => m.AnalyticsModule),
};
import { NgModule } from '@angular/core';
//import * as PlotlyJS from 'plotly.js/dist/plotly.js';
import { PlotlyModule } from 'angular-plotly.js';
import { AdminAnalyticsComponent } from './pages/admin/admin.component';
import { CommonModule } from '../../common/common.module';
import { CommonModule as NgCommonModule } from '@angular/common';
......@@ -63,11 +60,14 @@ import { FormsModule } from '@angular/forms';
import { AnalyticsSearchSuggestionsComponent } from './v2/components/search-suggestions/search-suggestions.component';
import { AnalyticsBenchmarkComponent } from './v2/components/benchmark/benchmark.component';
//PlotlyModule.plotlyjs = PlotlyJS;
import * as PlotlyJS from 'plotly.js/dist/plotly-basic.min.js';
import { PlotlyModule } from 'angular-plotly.js';
import { ChartV2Component } from './components/chart-v2/chart-v2.component';
PlotlyModule.plotlyjs = PlotlyJS;
const routes: Routes = [
{
path: 'analytics',
path: '',
component: AnalyticsComponent,
children: [
{ path: '', redirectTo: 'dashboard/traffic', pathMatch: 'full' },
......@@ -159,6 +159,7 @@ const routes: Routes = [
AnalyticsSearchComponent,
AnalyticsSearchSuggestionsComponent,
AnalyticsBenchmarkComponent,
ChartV2Component,
],
providers: [AnalyticsDashboardService],
})
......
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { UtcDatePipe } from '../../pipes/utcdate';
import { AbbrPipe } from '../../pipes/abbr';
import { MockService } from '../../../utils/mock';
import { ThemeService } from '../../services/theme.service';
import { UtcDatePipe } from '../../../../common/pipes/utcdate';
import { AbbrPipe } from '../../../../common/pipes/abbr';
import { MockService } from '../../../../utils/mock';
import { ThemeService } from '../../../../common/services/theme.service';
import { ChartV2Component } from './chart-v2.component';
describe('ChartV2Component', () => {
......
......@@ -11,9 +11,9 @@ import {
HostBinding,
} from '@angular/core';
import { Subscription } from 'rxjs';
import { ThemeService } from '../../services/theme.service';
import { ThemeService } from '../../../../common/services/theme.service';
import chartPalette from './chart-palette.default';
import isMobileOrTablet from '../../../helpers/is-mobile-or-tablet';
import isMobileOrTablet from '../../../../helpers/is-mobile-or-tablet';
@Component({
selector: 'm-chartV2',
......
import { Component, Input } from '@angular/core';
import { BoostService } from '../../boost.service';
import {
Reason,
rejectionReasons,
} from '../../../../controllers/admin/boosts/rejection-reasons';
import { Reason, rejectionReasons } from '../../rejection-reasons';
@Component({
moduleId: module.id,
......
......@@ -5,7 +5,6 @@ import {
} from '@angular/core';
import { Session } from '../../../services/session';
import { MobileService } from '../mobile.service';
import { first } from 'lodash';
import { ConfigsService } from '../../../common/services/configs.service';
@Component({
......@@ -47,9 +46,9 @@ export class MobileMarketingComponent {
this.detectChanges();
this.releases = await this.service.getReleases();
this.latestRelease = await first(
this.releases.filter(rel => rel.latest && !rel.unstable)
);
this.latestRelease = this.releases.filter(
rel => rel.latest && !rel.unstable
)[0];
} catch (e) {
console.error(e);
this.error = e.message || 'Unknown error';
......
import { Component } from '@angular/core';
import { Session } from '../../services/session';
import {
Reason,
rejectionReasons,
} from '../../controllers/admin/boosts/rejection-reasons';
import { Reason, rejectionReasons } from '../boost/rejection-reasons';
import { ConfigsService } from '../../common/services/configs.service';
@Component({
......
<div class="m-settings--section m-layout--row m-border">
<div class="m-layout--spacer"></div>
<button
class="m-btn m-btn--slim m-btn--action"
[disabled]="!changed"
(click)="save()"
i18n="@@SETTINGS__P2PMEDIA__APPLY_BUTTON"
>
Apply
</button>
</div>
<div class="m-settings--section m-border">
<h4 i18n="@@SETTINGS__P2PMEDIA__P2PMEDIA_TITLE">Peer-to-peer Media</h4>
<div class="mdl-card__supporting-text">
<input
type="checkbox"
id="enable-p2p-media"
name="enable-p2p-media"
(click)="change()"
[(ngModel)]="settings.enableP2p"
[disabled]="!supported"
/>
<label
for="enable-p2p-media"
i18n="@@SETTINGS__P2PMEDIA__DISABLE_HERE_LABEL"
>Enable peer-to-peer media on this device. (experimental)</label
>
</div>
<div
class="mdl-card__supporting-text"
*ngIf="!supported"
i18n="@@SETTINGS__P2PMEDIA__NOT_SUPPORTED"
>
Safari and mobile browsers don't support peer-to-peer media streaming.
</div>
</div>
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
} from '@angular/core';
import { Storage } from '../../../services/storage';
import { WebtorrentService } from '../../webtorrent/webtorrent.service';
import { Client } from '../../../services/api/client';
import { Session } from '../../../services/session';
@Component({
selector: 'm-settings--p2pmedia',
templateUrl: 'p2pmedia.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SettingsP2PMediaComponent {
settings = {
enableP2p: false,
};
supported: boolean = true;
changed: boolean = false;
constructor(
protected cd: ChangeDetectorRef,
protected storage: Storage,
protected webtorrent: WebtorrentService,
protected client: Client,
private session: Session
) {}
ngOnInit() {
this.supported = this.webtorrent.isBrowserSupported();
this.settings.enableP2p = this.session.getLoggedInUser().p2p_media_enabled;
}
change() {
this.changed = true;
}
async save() {
this.session.getLoggedInUser().p2p_media_enabled = this.settings.enableP2p;
this.webtorrent.setEnabled(!this.settings.enableP2p);
const url = 'api/v2/settings/p2p';
try {
if (this.settings.enableP2p) {
await this.client.post(url);
} else {
await this.client.delete(url);
}
} catch (e) {
this.session.getLoggedInUser().p2p_media_enabled = this.settings.enableP2p;
this.webtorrent.setEnabled(this.settings.enableP2p);
}
this.changed = false;
}
detectChanges() {
this.cd.markForCheck();
this.cd.detectChanges();
}
}
......@@ -73,15 +73,6 @@
<span i18n="@@SETTINGS__NAVIGATION__BILLINGS_NAV">Billing</span>
</a>
<a
class="m-page--sidebar--navigation--item"
routerLink="/settings/p2pmedia"
routerLinkActive="m-page--sidebar--navigation--item-active"
>
<i class="material-icons">ondemand_video</i>
<span i18n="@@SETTINGS__NAVIGATION__P2PMEDIA_NAV">P2P Media</span>
</a>
<a
class="m-page--sidebar--navigation--item"
routerLink="/settings/reported-content"
......
......@@ -24,7 +24,6 @@ import { SettingsReportedContentComponent } from './reported-content/reported-co
import { SettingsService } from './settings.service';
import { SettingsWireComponent } from './wire/wire.component';
import { WireModule } from '../wire/wire.module';
import { SettingsP2PMediaComponent } from './p2pmedia/p2pmedia.component';
import { SettingsBlockedChannelsComponent } from './blocked-channels/blocked-channels.component';
import { SettingsTiersComponent } from './tiers/tiers.component';
......@@ -42,7 +41,6 @@ const settingsRoutes: Routes = [
{ path: 'emails', component: SettingsEmailsComponent },
{ path: 'billing', component: SettingsBillingComponent },
{ path: 'reported-content', component: SettingsReportedContentComponent },
{ path: 'p2pmedia', component: SettingsP2PMediaComponent },
{ path: 'blocked-channels', component: SettingsBlockedChannelsComponent },
{ path: 'tiers', component: SettingsTiersComponent },
],
......@@ -76,7 +74,6 @@ const settingsRoutes: Routes = [
SettingsBillingSubscriptionsComponent,
SettingsReportedContentComponent,
SettingsWireComponent,
SettingsP2PMediaComponent,
SettingsBlockedChannelsComponent,
SettingsTiersComponent,
],
......
import WebTorrent from 'webtorrent';
import { Storage } from '../../services/storage';
import isMobile from '../../helpers/is-mobile';
import { isSafari } from '../../helpers/is-safari';
import { FeaturesService } from '../../services/features.service';
export const MAX_CONNS = 55;
export function getInfoHash(value) {
if (typeof value !== 'string') {
return value && value.toString ? value.toString() : '???';
} else if (/^[a-f0-9]+$/.test) {
return value;
} else if (value.indexOf('magnet:') !== 0) {
return `${value} [?]`;
}
return value
.split('?')[1]
.split('&')
.find(q => q.startsWith('xt='))
.substr(3);
}
const log = (magnetUri, ...args) =>
console.log(`[WebTorrent ${getInfoHash(magnetUri)}]`, ...args);
export class WebtorrentService {
protected supported: boolean;
protected client: WebTorrent;
protected torrentRefs: { [index: string]: number } = {};
protected torrentPurgeTimers: { [index: string]: any } = {};
constructor(
protected storage: Storage,
protected featuresService: FeaturesService
) {
if (
!this.isBrowserSupported() &&
!this.storage.get('webtorrent:disabled')
) {
this.storage.set('webtorrent:disabled', JSON.stringify(true));
}
}
// Life-cycle
setUp(maxConns: number = MAX_CONNS) {
if (this.client) {
this.destroy();
}
this.setUpSupport();
if (this.isSupported() && this.isEnabled()) {
this.client = new WebTorrent({
maxConns,
webSeeds: true,
});
this.client.on('error', err => {
console.error('Webtorrent client', err);
});
// TODO: Setup global event listeners, if needed
}
return this;
}
destroy() {
const client = this.client;
this.client = void 0;
this.torrentRefs = {};
for (let magnetUri in this.torrentPurgeTimers) {
clearTimeout(this.torrentPurgeTimers[magnetUri]);
delete this.torrentPurgeTimers[magnetUri];
}
if (!client) {
return Promise.resolve(this);
}
return new Promise((resolve, reject) => {
client.destroy(err => {
if (err) {
reject(err);
} else {
resolve(this);
}
});
});
}
/**
* Determines whether webtorrent is to be enabled for user
* @returns { boolean } - true if webtorrent enabled, supported and user is opted in.
*/
isEnabled = (): boolean => false; // Deprecated due to bade performance on minds
setEnabled(enabled: boolean) {
const current = this.isEnabled();
if (current && !enabled) {
this.destroy();
} else if (!current && enabled) {
this.setUp();
}
return this;
}
setUpSupport() {
this.supported = 'MediaStream' in window && WebTorrent.WEBRTC_SUPPORT;
return this;
}
isBrowserSupported() {
return !isMobile() && !isSafari();
}
isSupported() {
return this.isEnabled() && this.supported;
}
isReady() {
return this.isSupported() && !!this.client;
}
// Torrent Manager
add(torrentData, infoHash: string): Promise<any> {
log(infoHash, 'Trying to add');
if (!this.torrentRefs[infoHash]) {
this.torrentRefs[infoHash] = 0;
}
this.torrentRefs[infoHash]++;
const current = this.client.get(infoHash);
if (current) {
log(infoHash, 'Already exists');
return Promise.resolve(current);
}
return new Promise((resolve, reject) => {
log(infoHash, 'Adding new');
try {
const torrent = this.client.add(torrentData, torrent =>
resolve(torrent)
);
torrent.on('error', err => {
console.error('Torrent error', infoHash, err);
});
} catch (e) {
reject(e);
}
});
}
remove(infoHash) {
log(infoHash, 'Trying to remove');
if (this.torrentRefs[infoHash] && this.torrentRefs[infoHash] > 0) {
this.torrentRefs[infoHash]--;
}
if (!this.torrentRefs[infoHash]) {
log(infoHash, 'No references, added to purge timer');
if (this.torrentPurgeTimers[infoHash]) {
clearTimeout(this.torrentPurgeTimers[infoHash]);
}
this.torrentPurgeTimers[infoHash] = setTimeout(
() => this.purge(infoHash),
30000
);
this.torrentRefs[infoHash] = 0;
}
}
get(infoHash) {
return this.client.get(infoHash);
}
purge(infoHash) {
log(infoHash, 'Trying to purge');
if (!this.torrentRefs[infoHash]) {
log(infoHash, 'No references, purging');
this.client.remove(infoHash);
}
}
// DI
static _(storage: Storage, featuresService: FeaturesService) {
return new WebtorrentService(storage, featuresService);
}
static _deps: any[] = [Storage, FeaturesService];
}
/**
* TODO: Load these automagically from gulp
*/
export const MINDS_PLUGIN_DECLARATIONS: any[] = [];
import { MINDS_PROVIDERS } from './services/providers';
import { Client, Upload } from './services/api';
export const MINDS_PLUGIN_PROVIDERS: any[] = [];
import { Routes } from '@angular/router';
import { APP_BASE_HREF } from '@angular/common';
import { Capture } from '../controllers/capture/capture';
import { Discovery } from '../controllers/discovery/discovery';
import { Admin } from '../controllers/admin/admin';
import { Pages } from '../controllers/pages/pages';
import { CanDeactivateGuardService } from '../services/can-deactivate-guard';
import { RewardsComponent } from '../controllers/rewards/rewards';
import { ChannelContainerComponent } from '../modules/channel-container/channel-container.component';
export const MindsAppRoutes: Routes = [
{ path: 'capture', redirectTo: 'media/images/suggested' },
{ path: 'about', redirectTo: 'p/about' },
// redirectTo: 'media/:type/:filter
{ path: 'discovery/suggested/channels', redirectTo: 'channels/suggested' },
{ path: 'discovery/trending/channels', redirectTo: 'channels/suggested' },
{ path: 'discovery/all/channels', redirectTo: 'channels/suggested' },
{ path: 'discovery/suggested/:type', redirectTo: 'media/:type/suggested' },
{ path: 'discovery/trending/:type', redirectTo: 'media/:type/suggested' },
{ path: 'discovery/all/:type', redirectTo: 'media/:type/suggested' },
{ path: 'discovery/owner/:type', redirectTo: 'media/:type/my' },
{ path: 'discovery/suggested', redirectTo: 'channels/suggested' },
{ path: 'discovery/trending', redirectTo: 'media/images/suggested' },
{ path: 'discovery/featured', redirectTo: 'channels/suggested' },
/* /Legacy routes */
{ path: 'admin/:filter/:type', component: Admin, data: { title: 'Admin' } },
{ path: 'admin/:filter', component: Admin, data: { title: 'Admin' } },
{ path: 'p/:page', component: Pages },
{ path: 'claim-rewards/:uuid', component: RewardsComponent },
// TODO: Find a way to move channel routes onto its own Module. They take priority and groups/blogs cannot be accessed
{ path: ':username', redirectTo: ':username/', pathMatch: 'full' },
{
path: ':username/:filter',
component: ChannelContainerComponent,
canDeactivate: [CanDeactivateGuardService],
},
];
export const MindsAppRoutingProviders: any[] = [
{ provide: APP_BASE_HREF, useValue: '/' },
];
export const MINDS_APP_ROUTING_DECLARATIONS: any[] = [
Capture,
Discovery,
Admin,
Pages,
RewardsComponent,
];
import { ModuleWithProviders } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { APP_BASE_HREF } from '@angular/common';
import { MediaViewComponent } from '../modules/media/view/view.component';
export const MindsEmbedRoutes: Routes = [
{ path: 'api/v1/embed/:guid', component: MediaViewComponent },
];
export const MindsEmbedRoutingProviders: any[] = [
{ provide: APP_BASE_HREF, useValue: '/' },
];
export const MINDS_EMBED_ROUTING_DECLARATIONS: any[] = [MediaViewComponent];
......@@ -31,7 +31,6 @@ import { RecentService } from './ux/recent';
import { ContextService } from './context.service';
import { FeaturesService } from './features.service';
import { BlockchainService } from '../modules/blockchain/blockchain.service';
import { WebtorrentService } from '../modules/webtorrent/webtorrent.service';
import { TimeDiffService } from './timediff.service';
import { UpdateMarkersService } from '../common/services/update-markers.service';
import { HttpClient, HTTP_INTERCEPTORS } from '@angular/common/http';
......@@ -206,11 +205,6 @@ export const MINDS_PROVIDERS: any[] = [
useFactory: BlockchainService._,
deps: [Client],
},
{
provide: WebtorrentService,
useFactory: WebtorrentService._,
deps: WebtorrentService._deps,
},
{
provide: TimeDiffService,
useFactory: TimeDiffService._,
......
export const webtorrentServiceMock = new (function() {
this.isEnabled = () => false;
this.setUp = () => {};
this.destroy = () => {};
this.setEnabled = () => {};
this.setUpSupport = () => {};
this.isBrowserSupported = () => false;
this.isSupported = () => false;
this.isEnabled = () => false;
this.isReady = () => false;
this.add = () => {};
this.remove = () => {};
this.get = () => {};
this.purge = () => {};
})();
......@@ -3,7 +3,7 @@
"compilerOptions": {
"outDir": "../out-tsc/app",
"baseUrl": "./",
"module": "es2015",
"module": "esNext",
"types": ["node"]
},
"exclude": [
......
Please register or to comment