...
 
Commits (11)
......@@ -5,12 +5,13 @@
context('Pro Settings', () => {
if (Cypress.env().pro_password) {
// required to run tests against pro user only.
const activityContainer = 'minds-activity';
function data(str) {
return `[data-minds=${str}]`;
}
const activityContainer = 'minds-activity';
const sidebarMenu = data('sidebarMenuLinks');
const general = {
title: data('title'),
headline: data('headline'),
......@@ -61,8 +62,8 @@ context('Pro Settings', () => {
label1: data('tag__label--1'),
tag1: data('tag__tag--1'),
strings: {
label0: 'myLabel0',
label1: 'myLabel1',
label0: 'Label0',
label1: 'Label1',
tag0: '#hashtag0',
tag1: '#hashtag1',
},
......@@ -80,24 +81,19 @@ context('Pro Settings', () => {
},
};
const payouts = {
strings: {
method: 'USD',
},
};
before(() => {
cy.login(true, Cypress.env().pro_username, Cypress.env().pro_password);
});
after(() => {
cy.visit('/pro/' + Cypress.env().pro_username + '/settings/hashtags')
.location('pathname')
.should(
'eq',
'/pro/' + Cypress.env().pro_username + '/settings/hashtags'
);
clearHashtags();
// Make a post
cy.route('POST', '**/api/v1/newsfeed').as('newsfeed');
cy.visit('/newsfeed/subscriptions');
cy.get('minds-newsfeed-poster textarea')
.click()
.type('Testing 1-2-3');
cy.get('minds-newsfeed-poster .m-posterActionBar__PostButton').click();
cy.wait('@newsfeed').then(xhr => {
expect(xhr.status).to.equal(200);
});
});
beforeEach(() => {
......@@ -137,7 +133,9 @@ context('Pro Settings', () => {
});
it('should allow the user to set theme colors', () => {
cy.contains('Theme').click();
cy.get(sidebarMenu)
.contains('Theme')
.click();
// reset colors so changes will be submitted
cy.get(theme.textColor)
......@@ -157,7 +155,7 @@ context('Pro Settings', () => {
save();
// set colors to be tested
// set theme colors to be tested
cy.get(theme.textColor)
.click()
.clear()
......@@ -175,26 +173,21 @@ context('Pro Settings', () => {
saveAndPreview();
cy.get('.m-proChannelTopbar__navItem')
.should('have.css', 'color')
.and('eq', theme.strings.textColorRgb);
cy.get('.m-pro__searchBox input').should(
'have.css',
'background-color',
theme.strings.plainBgColorRgba
);
cy.contains('Videos').should(
'have.css',
'color',
theme.strings.textColorRgb
);
// .and('eq', theme.strings.textColorRgb);
cy.get('.m-proChannelTopbar__navItem')
.contains('Videos')
.should('have.css', 'color', theme.strings.textColorRgb);
cy.contains('Videos').click();
cy.get('.m-proChannelTopbar__navItem')
.contains('Feed')
.click();
// make window narrow enough to show hamburger icon
// make window narrow enough to show hamburger icon/menu
cy.viewport('ipad-mini');
cy.get('.m-proHamburgerMenu__trigger')
.click()
......@@ -203,8 +196,11 @@ context('Pro Settings', () => {
.and('eq', theme.strings.primaryColorRgb);
});
it('should allow the user to set a dark theme for posts', () => {
cy.contains('Theme').click();
// Skipping until Emi changes feeds from 'top' to 'latest'
it.skip('should allow the user to set a dark theme for posts', () => {
cy.get(sidebarMenu)
.contains('Theme')
.click();
// Toggle radio to enable submit button
cy.get(theme.schemeLight).click({ force: true });
......@@ -212,23 +208,30 @@ context('Pro Settings', () => {
saveAndPreview();
cy.contains('Feed').click();
cy.get('.m-proChannelTopbar__navItem')
.contains('Feed')
.click();
cy.get(activityContainer)
.should('have.css', 'background-color')
.and('eq', 'rgb(35, 35, 35)');
});
it('should allow the user to set a light theme for posts', () => {
cy.contains('Theme').click();
// Skipping until Emi changes feeds from 'top' to 'latest'
it.skip('should allow the user to set a light theme for posts', () => {
cy.get(sidebarMenu)
.contains('Theme')
.click();
// Toggle radio to enable submit button
cy.contains('Dark').click();
cy.contains('Light').click();
cy.get(theme.schemeDark).click({ force: true });
cy.get(theme.schemeLight).click({ force: true });
saveAndPreview();
cy.contains('Videos').click();
cy.get('.m-proChannelTopbar__navItem')
.contains('Feed')
.click();
cy.get(activityContainer)
.should('have.css', 'background-color')
......@@ -236,7 +239,9 @@ context('Pro Settings', () => {
});
it.skip('should allow the user to upload logo and background images', () => {
cy.contains('Assets').click();
cy.get(sidebarMenu)
.contains('Assets')
.click();
cy.uploadFile(assets.logo, assets.strings.logoFixture, 'image/jpeg');
......@@ -254,7 +259,14 @@ context('Pro Settings', () => {
});
it('should allow the user to set category hashtags', () => {
cy.contains('Hashtags').click();
cy.get(sidebarMenu)
.contains('Hashtags')
.click();
cy.get(hashtags.add).click();
cy.get('m-draggableList')
.contains('clear')
.click({ multiple: true });
cy.get(hashtags.add).click();
......@@ -279,12 +291,20 @@ context('Pro Settings', () => {
saveAndPreview();
//check the labels are present and clickable.
cy.contains(hashtags.strings.label0);
cy.contains(hashtags.strings.label1);
cy.contains(hashtags.strings.label2);
});
it('should allow the user to set footer', () => {
cy.contains('Footer').click();
cy.get(sidebarMenu)
.contains('Footer')
.click();
// clear any existing footer links
cy.get(footer.add).click();
cy.get('m-draggableList')
.contains('clear')
.click({ multiple: true });
// add a new footer link
cy.get(footer.add).click();
......@@ -304,25 +324,18 @@ context('Pro Settings', () => {
saveAndPreview();
cy.contains(footer.strings.footerTitle)
cy.contains(footer.strings.linkTitle)
.should('have.attr', 'href')
.should('contain', footer.strings.footerHref);
.should('contain', footer.strings.linkHref);
cy.get(footer.text).should('contain', footer.strings.text);
});
it.skip('should allow the user to set payout method', () => {
cy.contains('Payouts').click();
cy.contains(payouts.method).check();
// TODO check something like this? session.getLoggedInUser().merchant.service: "stripe"
});
function save() {
//save and await response
cy.contains('Save')
.click()
cy.get('.m-shadowboxSubmitButton')
.contains('Save')
.click({ force: true })
.wait('@settings')
.then(xhr => {
expect(xhr.status).to.equal(200);
......@@ -336,35 +349,5 @@ context('Pro Settings', () => {
//go to pro page
cy.visit('/pro/' + Cypress.env().pro_username);
}
function clearHashtags() {
cy.contains('Hashtags').click();
cy.get(hashtags.add).click();
cy.contains('clear').click({ multiple: true });
saveAndPreview();
}
//
// it.only('should update the theme', () => {
// // nav to theme tab
// cy.contains('Theme')
// .click();
// cy.get(theme.plainBgColor).then(elem => {
// elem.val('#00dd00');
// //save and await response
// cy.contains('Save')
// .click()
// .wait('@settings').then((xhr) => {
// expect(xhr.status).to.equal(200);
// expect(xhr.response.body).to.deep.equal({ status: 'success' });
// });
// //go to pro page
// cy.contains('View Pro Channel').click();
// })
}
});
<ng-container *ngIf="!isProDomain">
<m-v2-topbar *mIfFeature="'top-feeds'; else legacyTopbar">
<ng-container search>
<m-search--bar [defaultSizes]="false"></m-search--bar>
</ng-container>
<ng-container icons>
<m-notifications--topbar-toggle
*ngIf="session.isLoggedIn()"
></m-notifications--topbar-toggle>
</ng-container>
</m-v2-topbar>
<ng-template #legacyTopbar>
<m-topbar class="m-noshadow">
<ng-container *ngIf="ready">
<ng-container *ngIf="!isProDomain">
<m-v2-topbar *mIfFeature="'top-feeds'; else legacyTopbar">
<ng-container search>
<m-search--bar></m-search--bar>
<m-search--bar [defaultSizes]="false"></m-search--bar>
</ng-container>
<ng-container icons>
<m-notifications--topbar-toggle></m-notifications--topbar-toggle>
<m-wallet--topbar-toggle></m-wallet--topbar-toggle>
<m-notifications--topbar-toggle
*ngIf="session.isLoggedIn()"
></m-notifications--topbar-toggle>
</ng-container>
</m-topbar>
</ng-template>
</m-v2-topbar>
<ng-template #legacyTopbar>
<m-topbar class="m-noshadow">
<ng-container search>
<m-search--bar></m-search--bar>
</ng-container>
<m-sidebar--markers
<ng-container icons>
<m-notifications--topbar-toggle></m-notifications--topbar-toggle>
<m-wallet--topbar-toggle></m-wallet--topbar-toggle>
</ng-container>
</m-topbar>
</ng-template>
<m-sidebar--markers
[class.has-v2-navbar]="featuresService.has('top-feeds')"
></m-sidebar--markers>
</ng-container>
<m-body
[class.has-v2-navbar]="featuresService.has('top-feeds')"
></m-sidebar--markers>
</ng-container>
[class.is-pro-domain]="isProDomain"
>
<m-announcement [id]="'blockchain:sale'" *ngIf="false">
<span
class="m-blockchain--wallet-address-notice--action"
routerLink="/tokens"
i18n="@@BLOCKCHAIN__SALE__NOTICE"
>
The MINDS token is now live. Learn more here.
</span>
</m-announcement>
<m-blockchain--wallet-address-notice></m-blockchain--wallet-address-notice>
<router-outlet></router-outlet>
</m-body>
<m-messenger *ngIf="minds.LoggedIn && !isProDomain"></m-messenger>
<m-hovercard-popup></m-hovercard-popup>
<m-body
[class.has-v2-navbar]="featuresService.has('top-feeds')"
[class.is-pro-domain]="isProDomain"
>
<m-announcement [id]="'blockchain:sale'" *ngIf="false">
<span
class="m-blockchain--wallet-address-notice--action"
routerLink="/tokens"
i18n="@@BLOCKCHAIN__SALE__NOTICE"
>
The MINDS token is now live. Learn more here.
</span>
</m-announcement>
<m-blockchain--wallet-address-notice></m-blockchain--wallet-address-notice>
<router-outlet></router-outlet>
</m-body>
<m-messenger *ngIf="minds.LoggedIn && !isProDomain"></m-messenger>
<m-hovercard-popup></m-hovercard-popup>
<m-overlay-modal></m-overlay-modal>
<m--blockchain--transaction-overlay></m--blockchain--transaction-overlay>
<m-modal--tos-updated *ngIf="session.isLoggedIn()"></m-modal--tos-updated>
<m-juryDutySession__summons
*ngIf="session.isLoggedIn() && !isProDomain"
></m-juryDutySession__summons>
<m-modal-signup-on-scroll *ngIf="!isProDomain"></m-modal-signup-on-scroll>
<m-channel--onboarding *ngIf="showOnboarding"></m-channel--onboarding>
<m-cookies-notice *ngIf="!session.isLoggedIn()"> </m-cookies-notice>
<m-overlay-modal></m-overlay-modal>
<m--blockchain--transaction-overlay></m--blockchain--transaction-overlay>
<m-modal--tos-updated *ngIf="session.isLoggedIn()"></m-modal--tos-updated>
<m-juryDutySession__summons
*ngIf="session.isLoggedIn() && !isProDomain"
></m-juryDutySession__summons>
<m-modal-signup-on-scroll *ngIf="!isProDomain"></m-modal-signup-on-scroll>
<m-channel--onboarding *ngIf="showOnboarding"></m-channel--onboarding>
<m-cookies-notice *ngIf="!session.isLoggedIn()"> </m-cookies-notice>
</ng-container>
import { Component, HostBinding } from '@angular/core';
import { ChangeDetectorRef, Component, HostBinding } from '@angular/core';
import { NotificationService } from './modules/notifications/notification.service';
import { AnalyticsService } from './services/analytics';
......@@ -18,6 +18,7 @@ import { ThemeService } from './common/services/theme.service';
import { BannedService } from './modules/report/banned/banned.service';
import { DiagnosticsService } from './services/diagnostics.service';
import { SiteService } from './common/services/site.service';
import { SsoService } from './common/services/sso.service';
import { Subscription } from 'rxjs';
import { RouterHistoryService } from './common/services/router-history.service';
import { PRO_DOMAIN_ROUTES } from './modules/pro/pro.module';
......@@ -29,8 +30,11 @@ import { PRO_DOMAIN_ROUTES } from './modules/pro/pro.module';
})
export class Minds {
name: string;
minds = window.Minds;
ready: boolean = false;
showOnboarding: boolean = false;
showTOSModal: boolean = false;
......@@ -57,7 +61,9 @@ export class Minds {
private bannedService: BannedService,
private diagnostics: DiagnosticsService,
private routerHistoryService: RouterHistoryService,
private site: SiteService
private site: SiteService,
private sso: SsoService,
private cd: ChangeDetectorRef
) {
this.name = 'Minds';
......@@ -67,8 +73,29 @@ export class Minds {
}
async ngOnInit() {
this.diagnostics.setUser(this.minds.user);
this.diagnostics.listen(); // Listen for user changes
try {
this.diagnostics.setUser(this.minds.user);
this.diagnostics.listen(); // Listen for user changes
if (this.sso.isRequired()) {
await this.sso.connect();
}
} catch (e) {
console.error('ngOnInit()', e);
}
this.ready = true;
this.detectChanges();
try {
await this.initialize();
} catch (e) {
console.error('initialize()', e);
}
}
async initialize() {
this.blockListService.fetch();
if (!this.site.isProDomain) {
this.notificationService.getNotifications();
......@@ -136,4 +163,9 @@ export class Minds {
get isProDomain() {
return this.site.isProDomain;
}
detectChanges() {
this.cd.markForCheck();
this.cd.detectChanges();
}
}
import { Cookie } from '../../services/cookie';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { environment } from '../../../environments/environment';
import { Location } from '@angular/common';
import { SiteService } from '../services/site.service';
/**
* API Class
*/
export class MindsHttpClient {
base: string = '/';
origin: string = '';
cookie: Cookie = new Cookie();
static _(http: HttpClient, site: SiteService) {
return new MindsHttpClient(http, site);
static _(http: HttpClient) {
return new MindsHttpClient(http);
}
constructor(public http: HttpClient, protected site: SiteService) {
if (this.site.isProDomain) {
this.base = window.Minds.site_url;
this.origin = document.location.host;
}
}
constructor(public http: HttpClient) {}
/**
* Return a GET request
......@@ -81,22 +73,11 @@ export class MindsHttpClient {
'X-VERSION': environment.version,
};
if (this.origin) {
const PRO_XSRF_JWT = this.cookie.get('PRO-XSRF-JWT') || '';
headers['X-MINDS-ORIGIN'] = this.origin;
headers['X-PRO-XSRF-JWT'] = PRO_XSRF_JWT;
}
const builtOptions = {
headers: new HttpHeaders(headers),
cache: true,
};
if (this.origin) {
builtOptions['withCredentials'] = true;
}
return Object.assign(options, builtOptions);
}
}
......
......@@ -121,9 +121,10 @@ import { PageLayoutComponent } from './components/page-layout/page-layout.compon
import { DashboardLayoutComponent } from './components/dashboard-layout/dashboard-layout.component';
import { ShadowboxLayoutComponent } from './components/shadowbox-layout/shadowbox-layout.component';
import { ShadowboxHeaderComponent } from './components/shadowbox-header/shadowbox-header.component';
import { ShadowboxSubmitButtonComponent } from './components/shadowbox-submit-button/shadowbox-submit-button.component';
import { FormDescriptorComponent } from './components/form-descriptor/form-descriptor.component';
import { FormToastComponent } from './components/form-toast/form-toast.component';
import { ShadowboxSubmitButtonComponent } from './components/shadowbox-submit-button/shadowbox-submit-button.component';
import { SsoService } from './services/sso.service';
PlotlyModule.plotlyjs = PlotlyJS;
......@@ -339,6 +340,7 @@ PlotlyModule.plotlyjs = PlotlyJS;
],
providers: [
SiteService,
SsoService,
{
provide: AttachmentService,
useFactory: AttachmentService._,
......@@ -354,7 +356,7 @@ PlotlyModule.plotlyjs = PlotlyJS;
{
provide: MindsHttpClient,
useFactory: MindsHttpClient._,
deps: [HttpClient, SiteService],
deps: [HttpClient],
},
{
provide: NSFWSelectorCreatorService,
......
......@@ -37,7 +37,11 @@
<h3>{{ menu.header.label }}</h3>
</div>
<nav class="m-sidebarMenu__linksContainer" *ngIf="menu.links">
<nav
class="m-sidebarMenu__linksContainer"
*ngIf="menu.links"
data-minds="sidebarMenuLinks"
>
<div class="m-sidebarMenu__link" *ngFor="let link of menu.links">
<ng-container *ngIf="link.permissionGranted">
<ng-container *ngIf="!link.newWindow">
......
......@@ -14,7 +14,6 @@ export class BlockListService {
protected storage: Storage
) {
this.blocked = new BehaviorSubject(JSON.parse(this.storage.get('blocked')));
this.fetch();
}
fetch() {
......
import { Injectable } from '@angular/core';
import { SiteService } from './site.service';
import { Client } from '../../services/api/client';
import { Session } from '../../services/session';
@Injectable()
export class SsoService {
protected readonly minds = window.Minds;
constructor(
protected site: SiteService,
protected client: Client,
protected session: Session
) {
this.listen();
}
listen() {
this.session.isLoggedIn((is: boolean) => {
if (is) {
this.auth();
}
});
}
isRequired(): boolean {
return this.site.isProDomain;
}
async connect() {
try {
const connect: any = await this.client.postRaw(
`${this.minds.site_url}api/v2/sso/connect`
);
if (connect && connect.token && connect.status === 'success') {
const authorization: any = await this.client.post(
'api/v2/sso/authorize',
{
token: connect.token,
}
);
if (authorization && authorization.user) {
this.session.inject(authorization.user);
}
}
} catch (e) {
console.error(e);
}
}
async auth() {
try {
const connect: any = await this.client.post('api/v2/sso/connect');
if (connect && connect.token && connect.status === 'success') {
await this.client.postRaw(
`${this.minds.site_url}api/v2/sso/authorize`,
{
token: connect.token,
}
);
}
} catch (e) {
console.error(e);
}
}
}
......@@ -114,9 +114,7 @@
></minds-admin-monetization>
<minds-admin-programs *ngIf="filter == 'programs'"></minds-admin-programs>
<minds-admin-payouts *ngIf="filter == 'payouts'"></minds-admin-payouts>
<minds-admin-withdrawals
*ngIf="filter == 'withdrawals'"
></minds-admin-withdrawals>
<m-admin-withdrawals *ngIf="filter == 'withdrawals'"></m-admin-withdrawals>
<minds-admin-featured *ngIf="filter == 'featured'"></minds-admin-featured>
<minds-admin-tagcloud *ngIf="filter == 'tagcloud'"></minds-admin-tagcloud>
<m-admin--verify *ngIf="filter == 'verify'"></m-admin--verify>
......
<div class="mdl-grid m-admin-withdrawals" style="max-width: 900px">
<div class="m-admin-withdrawals">
<div class="m-admin-withdrawals__legend">
<ng-container *ngIf="user" i18n>
<b>@{{ user }}</b
>'s withdrawals
</ng-container>
<ng-container *ngIf="!user" i18n>
Pending Withdrawals
</ng-container>
</div>
<div
class="m-admin-withdrawals--user"
*ngIf="user"
i18n="@@M__ADMIN__WITHDRAWALS__USERNAME_TITLE"
class="m-admin-withdrawals__card"
*ngFor="let request of withdrawals; let i = index"
>
<b>@{{ user }}</b
>'s withdrawals
</div>
<ng-container *ngIf="request">
<div class="m-admin-withdrawalsCard__cell">
<div class="m-admin-withdrawalsCardCell__label" i18n>Requested</div>
<table class="mdl-data-table mdl-js-data-table m-border" [mdl]>
<thead>
<tr class="m-admin--withdrawals--ledger-header">
<th
class="mdl-data-table__cell--non-numeric m-data-title"
i18n="@@M__ADMIN__WITHDRAWALS__DATE_COL"
>
Date
</th>
<th
class="mdl-data-table__cell--non-numeric m-data-title"
i18n="Transaction@@M__ADMIN__WITHDRAWALS__TX_COL"
>
Tx
</th>
<th
class="mdl-data-table__cell--non-numeric m-data-title"
i18n="@@M__ADMIN__WITHDRAWALS__AMOUNT_COL"
>
Amount
</th>
<th
class="mdl-data-table__cell--non-numeric m-data-title"
i18n="@@M__ADMIN__WITHDRAWALS__COMPLETED_COL"
>
Completed?
</th>
</tr>
</thead>
<tbody>
<ng-container *ngFor="let withdrawal of withdrawals; let i = index">
<tr class="m-admin--withdrawals--ledger-row">
<td class="mdl-data-table__cell--non-numeric">
{{ withdrawal.timestamp * 1000 | date: 'short' }}
</td>
<td
class="mdl-data-table__cell--non-numeric m-admin--withdrawals--ledger-tx"
<div class="m-admin-withdrawalsCardCell__body">
{{ request.timestamp * 1000 | date: 'short' }}
</div>
</div>
<div class="m-admin-withdrawalsCard__cell">
<div class="m-admin-withdrawalsCardCell__label" i18n>User</div>
<div class="m-admin-withdrawalsCardCell__body">
<a [routerLink]="['/', request.user?.username]"
>@{{ request.user?.username }}</a
>
{{ withdrawal.tx }}
</td>
<td>{{ withdrawal.amount | token: 18 | number: '1.0-4' }}</td>
<td class="mdl-data-table__cell--non-numeric">
{{ withdrawal.completed ? 'Yes' : 'No' }}
</td>
</tr>
</ng-container>
<tr
class="m-admin--withdrawals--ledger-row m-wire-console--ledger-row-placeholder"
*ngIf="!inProgress && (!withdrawals || !withdrawals.length)"
>
<td
colspan="4"
style="text-align:center"
i18n="@@M__ADMIN__WITHDRAWALS__NO_WITHDRAWALS_NOTE"
</div>
</div>
<div class="m-admin-withdrawalsCard__cell">
<div class="m-admin-withdrawalsCardCell__label" i18n>Signed up</div>
<div class="m-admin-withdrawalsCardCell__body">
{{ request.user?.time_created * 1000 | date: 'shortDate' }}
</div>
</div>
<div class="m-admin-withdrawalsCard__cell">
<div class="m-admin-withdrawalsCardCell__label" i18n>Referrer</div>
<div class="m-admin-withdrawalsCardCell__body">
<ng-container *ngIf="request.referrer; else organicText">
<a [routerLink]="['/', request.referrer.username]"
>@{{ request.referrer.username }}</a
>
</ng-container>
<ng-template #organicText>
<ng-container>(organic)</ng-container>
</ng-template>
</div>
</div>
<div class="m-admin-withdrawalsCard__cell">
<div class="m-admin-withdrawalsCardCell__label" i18n>Amount</div>
<div
class="m-admin-withdrawalsCardCell__body m-admin-withdrawalsCardCell__body--bolder"
>
No withdrawals to show. You can access a user's withdrawal ledger by
using the admin drop-down on their channel.
</td>
</tr>
</tbody>
</table>
{{ request.amount | token: 18 | number: '1.0-4' }}
</div>
</div>
<div class="m-admin-withdrawalsCard__cell">
<div class="m-admin-withdrawalsCardCell__label" i18n>Status</div>
<div class="m-admin-withdrawalsCardCell__body">
{{ request.status?.replace('_', ' ') | uppercase }}
</div>
</div>
<div
class="m-admin-withdrawalsCard__cell m-admin-withdrawalsCard__cell--actions"
*ngIf="request.status === 'pending_approval'"
>
<div class="m-admin-withdrawalsCardCell__body">
<a
[routerLink]="[
'/admin/withdrawals',
{ user: request.user?.username }
]"
>History</a
>
<button
class="mf-button mf-button--smaller"
[disabled]="inProgress"
(click)="approve(request)"
>
Approve
</button>
<button
class="mf-button mf-button--destructive mf-button--smaller"
[disabled]="inProgress"
(click)="reject(request)"
>
Reject
</button>
</div>
</div>
</ng-container>
</div>
<div
class="m-admin-withdrawals__notice"
*ngIf="!inProgress && !withdrawals?.length"
>
No withdrawals to show. You can access a user's ledger by using the admin
drop-down on their channel.
</div>
<infinite-scroll
distance="25%"
(load)="load()"
[moreData]="moreData"
[inProgress]="inProgress"
>
</infinite-scroll>
></infinite-scroll>
</div>
.m-admin-withdrawals {
.mdl-data-table {
width: 100%;
max-width: 900px;
margin: 16px auto 0;
.m-admin-withdrawals__legend {
font-size: 16px;
font-weight: 600;
text-align: center;
margin: 8px 0 16px;
letter-spacing: 1px;
}
.m-admin--withdrawals--ledger-tx {
font-size: 12px;
.m-admin-withdrawals__card {
display: flex;
flex-direction: row;
flex-wrap: wrap;
max-width: 600px;
margin: 0 auto 32px;
padding: 8px;
border: 1px solid;
border-radius: 4px;
@include m-theme() {
background: themed($m-white);
color: themed($m-black);
border-color: themed($m-grey-50);
}
}
.m-admin-withdrawalsCard__cell {
flex-grow: 1;
flex-shrink: 0;
box-sizing: border-box;
min-width: 25%;
padding: 8px;
&--actions {
width: 100%;
text-align: right;
padding-top: 24px;
margin-top: 16px;
border-top: 1px dotted;
@include m-theme() {
border-color: themed($m-grey-50);
}
}
a {
color: inherit;
font: inherit;
text-decoration: none;
border-bottom: 1px dotted;
cursor: pointer;
@include m-theme() {
border-color: themed($m-grey-300);
}
}
.m-admin-withdrawalsCardCell__label {
font-weight: 600;
margin-bottom: 2px;
@include m-theme() {
color: themed($m-grey-300);
}
}
.m-admin-withdrawalsCardCell__body {
&--bolder {
font-weight: bold;
letter-spacing: 0.5px;
}
}
&--actions .m-admin-withdrawalsCardCell__body {
> * {
display: inline-block;
vertical-align: middle;
margin: 0 8px 8px;
}
}
}
.m-admin-withdrawals--user {
width: 100%;
font-size: 14px;
font-weight: 300;
.m-admin-withdrawals__notice {
max-width: 600px;
margin: 32px auto;
padding: 16px;
border: 1px solid;
border-radius: 4px;
text-align: center;
padding: 8px 8px 32px;
@include m-theme() {
background: themed($m-white);
color: themed($m-black);
border-color: themed($m-grey-50);
}
}
}
......@@ -4,7 +4,7 @@ import { ActivatedRoute } from '@angular/router';
@Component({
moduleId: module.id,
selector: 'minds-admin-withdrawals',
selector: 'm-admin-withdrawals',
templateUrl: 'withdrawals.component.html',
})
export class AdminWithdrawals {
......@@ -45,12 +45,19 @@ export class AdminWithdrawals {
this.inProgress = true;
const params = {
limit: 50,
offset: this.offset,
};
if (this.user) {
params['user'] = this.user;
} else {
params['status'] = 'pending_approval';
}
this.client
.get(`api/v2/admin/rewards/withdrawals`, {
limit: 50,
offset: this.offset,
user: this.user,
})
.get(`api/v2/admin/rewards/withdrawals`, params)
.then((response: any) => {
if (!response.withdrawals) {
this.inProgress = false;
......@@ -71,4 +78,58 @@ export class AdminWithdrawals {
this.inProgress = false;
});
}
async approve(withdrawal) {
if (!confirm("Do you want to approve this withdrawal? There's no UNDO.")) {
return;
}
this.inProgress = true;
try {
const endpoint = `api/v2/admin/rewards/withdrawals/${[
withdrawal.user_guid,
withdrawal.timestamp,
withdrawal.tx,
].join('/')}`;
await this.client.put(endpoint);
withdrawal.status = 'approved';
} catch (e) {
alert(
`There was an issue while approving withdrawal: ${(e && e.message) ||
'Unknown server error'}`
);
}
this.inProgress = false;
}
async reject(withdrawal) {
if (!confirm("Do you want to reject this withdrawal? There's no UNDO.")) {
return;
}
this.inProgress = true;
try {
const endpoint = `api/v2/admin/rewards/withdrawals/${[
withdrawal.user_guid,
withdrawal.timestamp,
withdrawal.tx,
].join('/')}`;
await this.client.delete(endpoint);
withdrawal.status = 'rejected';
} catch (e) {
alert(
`There was an issue while rejecting withdrawal: ${(e && e.message) ||
'Unknown server error'}`
);
}
this.inProgress = false;
}
}
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { Component, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { NavigationEnd, Router } from '@angular/router';
import { Subscription } from 'rxjs';
......@@ -23,6 +23,8 @@ export class SignupOnScrollModal implements OnInit, OnDestroy {
routerSubscription: Subscription;
@Input() disableScrollListener: true;
@ViewChild('modal', { static: true }) modal: SignupModal;
constructor(
......@@ -41,6 +43,10 @@ export class SignupOnScrollModal implements OnInit, OnDestroy {
}
listen() {
if (!this.disableScrollListener) {
return;
}
this.routerSubscription = this.router.events.subscribe(
(navigationEvent: NavigationEnd) => {
try {
......@@ -90,7 +96,10 @@ export class SignupOnScrollModal implements OnInit, OnDestroy {
}
unListen() {
this.routerSubscription.unsubscribe();
if (this.routerSubscription) {
this.routerSubscription.unsubscribe();
}
this.unlistenScroll();
}
......
......@@ -124,4 +124,6 @@
</div>
<m-overlay-modal #overlayModal></m-overlay-modal>
<m-modal-signup-on-scroll></m-modal-signup-on-scroll>
<m-modal-signup-on-scroll
[disableScrollListener]="false"
></m-modal-signup-on-scroll>
......@@ -229,7 +229,7 @@ export class ProChannelComponent implements OnInit, AfterViewInit, OnDestroy {
this.detectChanges();
try {
this.channel = await this.channelService.loadAndAuth(this.username);
this.channel = await this.channelService.load(this.username);
this.bindCssVariables();
this.shouldOpenWireModal();
......
......@@ -74,21 +74,16 @@ export class ProChannelService implements OnDestroy {
this.isLoggedIn$.unsubscribe();
}
async loadAndAuth(id: string): Promise<MindsUser> {
async load(id: string): Promise<MindsUser> {
try {
this.currentChannel = void 0;
const response = (await this.client.get(`api/v2/pro/channel/${id}`)) as {
channel;
me?;
};
this.currentChannel = response.channel;
if (this.site.isProDomain && response.me) {
this.session.login(response.me);
}
if (!this.currentChannel.pro_settings.tag_list) {
this.currentChannel.pro_settings.tag_list = [];
}
......@@ -111,7 +106,6 @@ export class ProChannelService implements OnDestroy {
try {
const response = (await this.client.get(`api/v2/pro/channel/${id}`)) as {
channel;
me?;
};
this.currentChannel = response.channel;
......
......@@ -58,7 +58,8 @@
<ul class="m-marketing__points">
<li i18n>
$1 for every 1,000 pageviews
$1 for every 1,000 pageviews ($5 per 1,000 pageviews when monthly
total is between 10,000 and 1,000,000 pageviews)
</li>
<li i18n>
......@@ -66,7 +67,8 @@
</li>
<li i18n>
25% commission on all referred sales
25% commission on all referred sales of Pro or
<a routerLink="/tokens">Minds Tokens</a>
</li>
</ul>
</div>
......
......@@ -69,7 +69,7 @@
#withdrawPending
i18n="@@WALLET__TOKENS__WITHDRAW__LEDGER__PENDING_LABEL"
>
PENDING
{{ withdrawal.status.replace('_', ' ') | uppercase }}
</ng-template>
<div class="mdl-layout-spacer"></div>
......
......@@ -8,20 +8,23 @@
class="m-border mdl-color--white m-token-withdraw"
*ngIf="session.getLoggedInUser().rewards"
>
<p>
<ng-container i18n="@@WALLET__TOKENS__WITHDRAW__REQUEST_DESC">
You can request to withdraw your token rewards to your 'onchain' wallet
below. Note: a small amount of ETH will be charged to cover the
transaction fee. Withdrawals may take a few hours to complete.
</ng-container>
<ng-container
*ngIf="withholding"
i18n="@@WALLET__TOKENS__WITHDRAW__REQUEST_UNAVAILABLE_DESC"
>
{{ withholding | number }} tokens are unavailable due to credit card
payment. They will be released after 30 days the payment occurred.
</ng-container>
<p i18n>
You can request to withdraw up to {available, plural, =1
{{{available | number}} token} other {{{available | number}} tokens}} from
your rewards to your <b>OnChain</b> wallet below.
</p>
<p *ngIf="withholding" i18n>
{withholding, plural, =1
{{{withholding | number}} token} other {{{withholding | number}} tokens}}
are unavailable due to credit card payment. They will be released after 30
days the payment occurred.
</p>
<p class="m-token-withdraw__note" i18n>
Note: a small amount of ETH will be charged to cover the transaction fee.
Withdrawals <b>go through an approval process</b>
and may take up to 72 hours to complete.
</p>
<div class="m-token-withdraw--form">
......
......@@ -2,9 +2,18 @@
margin-bottom: $minds-padding;
padding: $minds-padding * 2;
> p {
margin-bottom: 8px;
&.m-token-withdraw__note {
font-size: 12px;
opacity: 0.7;
}
}
.m-token-withdraw--form {
display: flex;
margin-bottom: $minds-padding;
margin: 16px 0 8px;
.m-token-withdraw--input,
.m-token-withdraw--submit-button {
......
......@@ -2,6 +2,7 @@ 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;
......@@ -30,7 +31,10 @@ export class WebtorrentService {
protected torrentRefs: { [index: string]: number } = {};
protected torrentPurgeTimers: { [index: string]: any } = {};
constructor(protected storage: Storage) {
constructor(
protected storage: Storage,
protected featuresService: FeaturesService
) {
if (
!this.isBrowserSupported() &&
!this.storage.get('webtorrent:disabled')
......@@ -89,14 +93,15 @@ export class WebtorrentService {
});
}
// Enable/Disable; Support
isEnabled() {
if (!window.Minds.user) return false;
const enabled = window.Minds.user.p2p_media_enabled;
return enabled && this.isBrowserSupported();
}
/**
* Determines whether webtorrent is to be enabled for user
* @returns { boolean } - true if webtorrent enabled, supported and user is opted in.
*/
isEnabled = (): boolean =>
window.Minds.user &&
this.featuresService.has('webtorrent') &&
window.Minds.user.p2p_media_enabled &&
this.isBrowserSupported();
setEnabled(enabled: boolean) {
const current = this.isEnabled();
......@@ -196,9 +201,9 @@ export class WebtorrentService {
// DI
static _(storage: Storage) {
return new WebtorrentService(storage);
static _(storage: Storage, featuresService: FeaturesService) {
return new WebtorrentService(storage, featuresService);
}
static _deps: any[] = [Storage];
static _deps: any[] = [Storage, FeaturesService];
}
......@@ -2,30 +2,19 @@ import { Cookie } from '../cookie';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { environment } from '../../../environments/environment';
import { Location } from '@angular/common';
import { SiteService } from '../../common/services/site.service';
/**
* API Class
*/
export class Client {
base: string = '/';
origin: string = '';
cookie: Cookie = new Cookie();
static _(http: HttpClient, location: Location, site: SiteService) {
return new Client(http, location, site);
static _(http: HttpClient, location: Location) {
return new Client(http, location);
}
constructor(
public http: HttpClient,
public location: Location,
protected site: SiteService
) {
if (this.site.isProDomain) {
this.base = window.Minds.site_url;
this.origin = document.location.host;
}
}
constructor(public http: HttpClient, public location: Location) {}
/**
* Return a GET request
......@@ -66,21 +55,23 @@ export class Client {
getRaw(endpoint: string, data: Object = {}, options: Object = {}) {
endpoint += '?' + this.buildParams(data);
return new Promise((resolve, reject) => {
this.http.get(this.base + endpoint, this.buildOptions(options)).subscribe(
res => {
return resolve(res);
},
err => {
if (err.data && !err.data()) {
return reject(err || new Error('GET error'));
}
if (err.status === 401 && err.error.loggedin === false) {
window.location.href = '/login';
this.http
.get(this.base + endpoint, this.buildOptions(options, true))
.subscribe(
res => {
return resolve(res);
},
err => {
if (err.data && !err.data()) {
return reject(err || new Error('GET error'));
}
if (err.status === 401 && err.error.loggedin === false) {
window.location.href = '/login';
return reject(err);
}
return reject(err);
}
return reject(err);
}
);
);
});
}
......@@ -122,6 +113,40 @@ export class Client {
});
}
/**
* Return a POST request
*/
postRaw(url: string, data: Object = {}, options: Object = {}) {
return new Promise((resolve, reject) => {
this.http
.post(url, JSON.stringify(data), this.buildOptions(options, true))
.subscribe(
res => {
var data: any = res;
if (!data || data.status !== 'success') return reject(data);
return resolve(data);
},
err => {
if (err.data && !err.data()) {
return reject(err || new Error('POST error'));
}
if (err.status === 401 && err.error.loggedin === false) {
if (this.location.path() !== '/login') {
localStorage.setItem('redirect', this.location.path());
window.location.href = '/login';
}
return reject(err);
}
if (err.status !== 200) {
return reject(err.error);
}
}
);
});
}
/**
* Return a PUT request
*/
......@@ -199,7 +224,7 @@ export class Client {
/**
* Build the options
*/
private buildOptions(options: Object) {
private buildOptions(options: Object, withCredentials: boolean = false) {
const XSRF_TOKEN = this.cookie.get('XSRF-TOKEN') || '';
const headers = {
......@@ -207,19 +232,12 @@ export class Client {
'X-VERSION': environment.version,
};
if (this.origin) {
const PRO_XSRF_JWT = this.cookie.get('PRO-XSRF-JWT') || '';
headers['X-MINDS-ORIGIN'] = this.origin;
headers['X-PRO-XSRF-JWT'] = PRO_XSRF_JWT;
}
const builtOptions = {
headers: new HttpHeaders(headers),
cache: true,
};
if (this.origin) {
if (withCredentials) {
builtOptions['withCredentials'] = true;
}
......
import { Cookie } from '../cookie';
import { HttpClient } from '@angular/common/http';
import { SiteService } from '../../common/services/site.service';
/**
* API Class
*/
export class Upload {
base: string = '/';
origin: string = '';
cookie: Cookie = new Cookie();
static _(http: HttpClient, site: SiteService) {
return new Upload(http, site);
static _(http: HttpClient) {
return new Upload(http);
}
constructor(public http: HttpClient, protected site: SiteService) {
if (this.site.isProDomain) {
this.base = window.Minds.site_url;
this.origin = document.location.host;
}
}
constructor(public http: HttpClient) {}
/**
* Return a POST request
......@@ -81,16 +74,6 @@ export class Upload {
};
const XSRF_TOKEN = this.cookie.get('XSRF-TOKEN');
xhr.setRequestHeader('X-XSRF-TOKEN', XSRF_TOKEN);
if (this.origin) {
const PRO_XSRF_JWT = this.cookie.get('PRO-XSRF-JWT') || '';
xhr.withCredentials = true;
xhr.setRequestHeader('X-MINDS-ORIGIN', this.origin);
xhr.setRequestHeader('X-PRO-XSRF-JWT', PRO_XSRF_JWT);
}
xhr.send(formData);
});
}
......
......@@ -68,12 +68,12 @@ export const MINDS_PROVIDERS: any[] = [
{
provide: Client,
useFactory: Client._,
deps: [HttpClient, Location, SiteService],
deps: [HttpClient, Location],
},
{
provide: Upload,
useFactory: Upload._,
deps: [HttpClient, SiteService],
deps: [HttpClient],
},
{
provide: Storage,
......@@ -113,6 +113,7 @@ export const MINDS_PROVIDERS: any[] = [
{
provide: Session,
useFactory: Session._,
deps: [SiteService],
},
{
provide: ThirdPartyNetworksService,
......
......@@ -56,16 +56,30 @@ export class Session {
return false;
}
/**
* Emit login event
*/
login(user: any = null) {
//clear stale local storage
inject(user: any = null) {
// Clear stale localStorage
window.localStorage.clear();
// Emit new user info
this.userEmitter.next(user);
window.Minds.user = user;
if (user.admin === true) window.Minds.Admin = true;
// Set globals
window.Minds.LoggedIn = true;
window.Minds.user = user;
if (user.admin === true) {
window.Minds.Admin = true;
}
}
/**
* Inject user and emit login event
*/
login(user: any = null) {
this.inject(user);
this.loggedinEmitter.next(true);
}
......