...
 
Commits (42)
image: markharding/minds-front-base
services:
- docker:dind
stages:
- test
- build
......@@ -154,6 +151,8 @@ build:production:i18n:
prepare:review:
stage: prepare
image: minds/ci:latest
services:
- docker:dind
script:
- docker login -u gitlab-ci-token -p ${CI_BUILD_TOKEN} ${CI_REGISTRY}
- docker build -t $CI_REGISTRY_IMAGE/front-init:$CI_PIPELINE_ID -f containers/front-init/Dockerfile dist/.
......@@ -179,6 +178,8 @@ prepare:review:sentry:
prepare:production:
stage: prepare
image: minds/ci:latest
services:
- docker:dind
script:
- docker login -u gitlab-ci-token -p ${CI_BUILD_TOKEN} ${CI_REGISTRY}
- docker build -t $CI_REGISTRY_IMAGE/front-init:$CI_PIPELINE_ID -f containers/front-init/Dockerfile dist/.
......@@ -261,6 +262,8 @@ review:stop:
.deploy: &deploy
image: minds/ci:latest
services:
- docker:dind
script:
## Sync assets with CDN
- aws s3 sync dist $S3_REPOSITORY_URL
......
......@@ -6,7 +6,7 @@ Minds Front
Front-end web application for Minds. Please run inside of [the Minds repo](https://github.com/minds/minds).
## Documentation
Documentation for Minds can be found at [minds.org/docs](https://www.minds.org/docs)
Please see the documentation on [developers.minds.com](https://developers.minds.com) for instructions on how to [build the Minds Front-end](https://developers.minds.com/docs/guides/frontend).
### Building
Please see the documentation on Minds.org for instructions on how to [build the Minds Front-end](https://www.minds.org/docs/install/preparation.html#front-end).
......
import generateRandomId from '../support/utilities';
context('Newsfeed', () => {
before(() => {
cy.getCookie('minds_sess').then(sessionCookie => {
......@@ -14,7 +16,6 @@ context('Newsfeed', () => {
cy.route('POST', '**/api/v1/media').as('mediaPOST');
cy.route('POST', '**/api/v1/newsfeed/**').as('newsfeedEDIT');
cy.route('POST', '**/api/v1/media/**').as('mediaEDIT');
cy.visit('/newsfeed/subscriptions')
.location('pathname')
.should('eq', '/newsfeed/subscriptions');
......@@ -37,6 +38,19 @@ context('Newsfeed', () => {
cy.get('minds-newsfeed-poster textarea').type(content);
};
const attachRichEmbed = (embedUrl) => {
cy.get('minds-newsfeed-poster').should('be.visible');
cy.get('minds-newsfeed-poster textarea')
.type(embedUrl);
cy.route('GET', `**/api/v1/newsfeed/preview?url=${embedUrl}**`)
.as('previewGET')
.wait('@previewGET')
.then(xhr => {
expect(xhr.status).to.equal(200);
});
}
const attachImageToActivity = () => {
cy.uploadFile(
'#attachment-input-poster',
......@@ -511,4 +525,140 @@ context('Newsfeed', () => {
deleteActivityFromNewsfeed();
});
it('should show a rich embed post from youtube in a modal', () => {
const content = generateRandomId() + " ",
url = 'https://www.youtube.com/watch?v=jNQXAC9IVRw';
// set up post.
newActivityContent(content);
attachRichEmbed(url);
// post and await.
cy.get('.m-posterActionBar__PostButton')
.click()
.wait('@newsfeedPOST').then(xhr => {
expect(xhr.status).to.equal(200);
//get activity, click it.
cy.get(`[minds-data-activity-guid='${xhr.response.body.guid}']`)
.click();
//check modal is open.
cy.get('[data-cy=data-minds-media-modal]')
.contains(content);
// close modal and tidy.
cy.get('.m-overlay-modal--backdrop')
.click({force: true});
deleteActivityFromNewsfeed();
});
});
it('should not open vimeo in a modal', () => {
const content = generateRandomId() + " ",
url = 'https://vimeo.com/8733915';
// set up post.
newActivityContent(content);
attachRichEmbed(url);
// post and await.
cy.get('.m-posterActionBar__PostButton')
.click()
.wait('@newsfeedPOST').then(xhr => {
expect(xhr.status).to.equal(200);
//get activity, make assertions tht would not be true for modals.
cy.get(`[minds-data-activity-guid='${xhr.response.body.guid}']`)
.should('be.visible')
.get('iframe')
.should('be.visible')
.get('.minds-more')
.should('be.visible');
// tidy.
deleteActivityFromNewsfeed();
});
});
it('should not open soundcloud in a modal', () => {
const content = generateRandomId() + " ",
url = 'https://soundcloud.com/richarddjames/piano-un10-it-happened';
// set up post.
newActivityContent(content);
attachRichEmbed(url);
// post and await.
cy.get('.m-posterActionBar__PostButton')
.click()
.wait('@newsfeedPOST').then(xhr => {
expect(xhr.status).to.equal(200);
//get activity, make assertions tht would not be true for modals.
cy.get(`[minds-data-activity-guid='${xhr.response.body.guid}']`)
.should('be.visible')
.get('.m-rich-embed-action-overlay')
.should('be.visible')
.get('.minds-more')
.should('be.visible');
deleteActivityFromNewsfeed();
});
});
it('should not open spotify in a modal', () => {
const content = generateRandomId() + " ",
url = 'https://open.spotify.com/track/2MZSXhq4XDJWu6coGoXX1V?si=nvja0EfwR3q6GMQmYg6gPQ';
// set up post.
newActivityContent(content);
attachRichEmbed(url);
// post and await.
cy.get('.m-posterActionBar__PostButton')
.click()
.wait('@newsfeedPOST').then(xhr => {
expect(xhr.status).to.equal(200);
//get activity, make assertions tht would not be true for modals.
cy.get(`[minds-data-activity-guid='${xhr.response.body.guid}']`)
.should('be.visible')
.get('.m-rich-embed-action-overlay')
.should('be.visible')
.get('.minds-more')
.should('be.visible');
deleteActivityFromNewsfeed();
});
});
it('should not open spotify in a modal', () => {
const content = generateRandomId() + " ",
url = 'http://giphygifs.s3.amazonaws.com/media/IzVquL965ib4s/giphy.gif';
// set up post.
newActivityContent(content);
attachRichEmbed(url);
// post and await.
cy.get('.m-posterActionBar__PostButton')
.click()
.wait('@newsfeedPOST').then(xhr => {
expect(xhr.status).to.equal(200);
//get activity, make assertions tht would not be true for modals.
cy.get(`[minds-data-activity-guid='${xhr.response.body.guid}']`)
.should('be.visible')
.get('.m-rich-embed-action-overlay')
.should('be.visible')
.get('.minds-more')
.should('be.visible');
deleteActivityFromNewsfeed();
});
});
});
......@@ -3,32 +3,38 @@
* @desc E2E testing for Minds Pro's pages.
*/
context('Pro Page', () => {
if (Cypress.env().pro_password) { // required to run tests against pro user only.
if (Cypress.env().pro_password) {
// required to run tests against pro user only.
const topBar = '.m-proChannel__topbar';
let categories = [
{ label: 'Technology', tag: '#technology' },
{ label: 'Food', tag: '#food' },
{ label: 'News', tag: '#news' }
{ label: 'News', tag: '#news' },
];
let footerLinks = [
{ label: 'Minds', link: 'https://www.minds.com/' },
{ label: 'Careers', link: 'https://www.minds.com/careers' },
];
const proButton = 'data-minds-sidebar-admin-pro-button';
function resetSettings() {
cy.visit(`/pro/settings`);
cy.route("POST", "**/api/v2/pro/settings").as("settings");
cy.route('POST', '**/api/v2/pro/settings').as('settings');
cy.get('#title').focus().clear().type('Title');
cy.get('#headline').focus().clear().type('This is a headline');
cy.get('#title')
.focus()
.clear()
.type('Title');
cy.get('#headline')
.focus()
.clear()
.type('This is a headline');
cy.contains('Hashtags')
.click();
cy.contains('Hashtags').click();
// remove all hashtags
removeInputs();
......@@ -37,8 +43,7 @@ context('Pro Page', () => {
let cat = categories[i];
addTag(cat.label, cat.tag, i);
}
cy.contains('Footer')
.click();
cy.contains('Footer').click();
cy.get('#footer_text')
.clear()
......@@ -54,30 +59,36 @@ context('Pro Page', () => {
cy.contains('Save')
.click()
.wait('@settings').then((xhr) => {
.wait('@settings')
.then(xhr => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body).to.deep.equal({ status: 'success' });
}
);
});
}
function removeInputs() {
cy.get('.m-draggableList__list .m-proSettings__field .m-proSettings__flexInputs').should('be.visible').within($el => {
for (let i = $el.length - 1; i >= 0; i--) { // flexInput. Start from the last one
let c = $el[i];
for (let j = 0; j < c.children.length; j++) { // inputs and the X button
let cc = c.children[j];
if (cc.nodeName === 'I') { // if it's the X button, click on it
cy.wrap(cc).click();
cy.get(
'.m-draggableList__list .m-proSettings__field .m-proSettings__dragDropRow--input'
)
.should('be.visible')
.within($el => {
for (let i = $el.length - 1; i >= 0; i--) {
// flexInput. Start from the last one
let c = $el[i];
for (let j = 0; j < c.children.length; j++) {
// inputs and the X button
let cc = c.children[j];
if (cc.nodeName === 'I') {
// if it's the X button, click on it
cy.wrap(cc).click();
}
}
}
}
});
});
}
function addTag(label, tag, index) {
cy.contains('+ Add Tag')
.click();
cy.contains('+ Add Tag').click();
cy.get(`#tag-label-${index}`)
.clear()
......@@ -89,8 +100,7 @@ context('Pro Page', () => {
}
function addFooterLink(label, link, index) {
cy.contains('Add Link')
.click();
cy.contains('Add Link').click();
cy.get(`#footer_link-title-${index}`)
.clear()
......@@ -116,42 +126,51 @@ context('Pro Page', () => {
});
it('should load the feed tab', () => {
cy.route("GET", "**/api/v2/pro/content/*/activities/top**").as("activities");
cy.route('GET', '**/api/v2/pro/content/*/activities/top**').as(
'activities'
);
cy.contains('Feed')
.click()
.wait('@activities').then((xhr) => {
expect(xhr.status).to.equal(200);
});
})
.wait('@activities')
.then(xhr => {
expect(xhr.status).to.equal(200);
});
});
it('should load the videos tab', () => {
cy.route("GET", "**/api/v2/pro/content/*/videos/top**").as("videos");
cy.route('GET', '**/api/v2/pro/content/*/videos/top**').as('videos');
cy.contains('Videos')
.click()
.wait('@videos').then((xhr) => {
expect(xhr.status).to.equal(200);
});
})
.wait('@videos')
.then(xhr => {
expect(xhr.status).to.equal(200);
});
});
it('should load the images tab', () => {
cy.route("GET", "**/api/v2/pro/content/*/images/top**").as("images");
cy.route('GET', '**/api/v2/pro/content/*/images/top**').as('images');
cy.contains('Images')
.click()
.wait('@images').then((xhr) => {
expect(xhr.status).to.equal(200);
});
.wait('@images')
.then(xhr => {
expect(xhr.status).to.equal(200);
});
// should have sub-categories
cy.get('m-pro--channel--categories > .m-proChannel__category').each(($el, $index) => {
let c = categories.slice(0);
c.unshift({ label: 'All', tag: '#all' });
expect($el.text()).to.contain(c[$index].label);
});
cy.get('m-pro--channel--categories > .m-proChannel__category').each(
($el, $index) => {
let c = categories.slice(0);
c.unshift({ label: 'All', tag: '#all' });
expect($el.text()).to.contain(c[$index].label);
}
);
cy.get('m-pro--channel .m-overlay-modal').should('not.be.visible');
// click on tile
cy.get('.m-proChannelListContent__list li:first-child m-pro--channel-tile').click();
cy.get(
'.m-proChannelListContent__list li:first-child m-pro--channel-tile'
).click();
cy.wait(200);
// media modal should appear
......@@ -159,35 +178,41 @@ context('Pro Page', () => {
// close media modal
cy.get('m-pro--channel .m-overlay-modal--close').click();
})
});
it('should load the articles tab', () => {
cy.route("GET", "**/api/v2/pro/content/*/blogs/top**").as("blogs");
cy.route('GET', '**/api/v2/pro/content/*/blogs/top**').as('blogs');
cy.contains('Articles')
.click()
.wait('@blogs').then((xhr) => {
expect(xhr.status).to.equal(200);
});
})
.wait('@blogs')
.then(xhr => {
expect(xhr.status).to.equal(200);
});
});
it('should load the groups tab', () => {
cy.route("GET", "**/api/v2/pro/content/*/groups/top**").as("groups");
cy.route('GET', '**/api/v2/pro/content/*/groups/top**').as('groups');
cy.contains('Groups')
.click()
.wait('@groups').then((xhr) => {
expect(xhr.status).to.equal(200);
});
})
.wait('@groups')
.then(xhr => {
expect(xhr.status).to.equal(200);
});
});
it('should have a footer', () => {
// should have a footer text
cy.get('.m-proChannelFooter__text').contains('This is the footer text');
// should have footer links
cy.get('.m-proChannel__footer .m-proChannelFooter .m-proChannelFooter__link').should('be.visible').each(($el, $index) => {
expect($el.text()).to.contain(footerLinks[$index].label);
expect($el.attr('href')).to.contain(footerLinks[$index].link);
});
})
cy.get(
'.m-proChannel__footer .m-proChannelFooter .m-proChannelFooter__link'
)
.should('be.visible')
.each(($el, $index) => {
expect($el.text()).to.contain(footerLinks[$index].label);
expect($el.attr('href')).to.contain(footerLinks[$index].link);
});
});
}
})
});
......@@ -3,30 +3,31 @@
* @desc E2E testing for Minds Pro's settings.
*/
context('Pro Settings', () => {
if (Cypress.env().pro_password) { // required to run tests against pro user only.
if (Cypress.env().pro_password) {
// required to run tests against pro user only.
const title = '#title';
const headline = '#headline';
const previewButton = '.m-proSettings__previewBtn';
// const previewButton = '.m-proSettings__previewBtn';
const activityContainer = 'minds-activity';
const footerText = '#footer_text';
const theme = {
primaryColor: '#primary_color',
primaryColor: '#primary_color',
plainBackgroundColor: '#plain_background_color',
schemeLight: '#scheme_light',
schemeDark: '#scheme_dark',
aspectRatio: {
169: '#tile_ratio_16\:9', // 16:9
1610: '#tile_ratio_16\:10', // 16:10
43: '#tile_ratio_4\:3', // 4:3
11: '#tile_ratio_1\:1' , // 1:1
169: '#tile_ratio_16:9', // 16:9
1610: '#tile_ratio_16:10', // 16:10
43: '#tile_ratio_4:3', // 4:3
11: '#tile_ratio_1:1', // 1:1
},
}
};
const hashtags = {
labelInput0: '#tag-label-0',
labelInput0: '#tag-label-0',
hashtagInput0: '#tag-tag-0',
labelInput1: '#tag-label-1',
labelInput1: '#tag-label-1',
hashtagInput1: '#tag-tag-1',
label1: 'label1',
label2: 'label2',
......@@ -34,38 +35,38 @@ context('Pro Settings', () => {
hashtag1: '#hashtag1',
hashtag2: '#hashtag2',
hashtag3: '#hashtag3',
}
};
const footer = {
hrefInput: `#footer_link-href-0`,
titleInput: `#footer_link-title-0`,
}
};
const strings = {
title: "Minds Pro E2E",
headline: "This headline is a test",
footer: "This is a footer",
footerTitle: "Minds",
title: 'Minds Pro E2E',
headline: 'This headline is a test',
footer: 'This is a footer',
footerTitle: 'Minds',
footerHref: 'https://www.minds.com/',
}
};
before(() => {
cy.login(true, Cypress.env().pro_username, Cypress.env().pro_password);
});
after(() => {
cy.visit("/pro/settings")
cy.visit('/pro/settings')
.location('pathname')
.should('eq', '/pro/settings');
clearHashtags();
});
beforeEach(()=> {
beforeEach(() => {
cy.preserveCookies();
cy.server();
cy.route("POST", "**/api/v2/pro/settings").as("settings");
cy.route('POST', '**/api/v2/pro/settings').as('settings');
cy.visit("/pro/settings")
cy.visit('/pro/settings')
.location('pathname')
.should('eq', '/pro/settings');
});
......@@ -84,40 +85,36 @@ context('Pro Settings', () => {
saveAndPreview();
//check tab title.
cy.title()
.should('eq', strings.title+' - '+strings.headline+" | Minds");
cy.title().should(
'eq',
strings.title + ' - ' + strings.headline + ' | Minds'
);
});
// Need to find a way around the color input in Cypress.
it('should allow the user to set a dark theme for posts', () => {
cy.contains('Theme')
.click();
cy.contains('Theme').click();
cy.get(theme.schemeDark)
.click();
cy.get(theme.schemeDark).click();
saveAndPreview();
cy.contains('Feed')
.click();
cy.contains('Feed').click();
cy.get(activityContainer)
.should('have.css', 'background-color')
.and('eq', 'rgb(35, 35, 35)');
.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();
cy.contains('Theme').click();
cy.get(theme.schemeLight)
.click();
cy.get(theme.schemeLight).click();
saveAndPreview();
cy.contains('Feed')
.click();
cy.contains('Feed').click();
cy.get(activityContainer)
.should('have.css', 'background-color')
......@@ -125,12 +122,10 @@ context('Pro Settings', () => {
});
it('should allow the user to set category hashtags', () => {
cy.contains('Hashtags')
.click();
cy.contains('Hashtags').click();
cy.contains('+ Add Tag').click();
cy.contains('+ Add Tag')
.click();
cy.get(hashtags.labelInput0)
.clear()
.type(hashtags.label1);
......@@ -138,10 +133,9 @@ context('Pro Settings', () => {
cy.get(hashtags.hashtagInput0)
.clear()
.type(hashtags.hashtag1);
cy.contains('+ Add Tag')
.click();
cy.contains('+ Add Tag').click();
cy.get(hashtags.labelInput1)
.first()
.clear()
......@@ -151,7 +145,7 @@ context('Pro Settings', () => {
.first()
.clear()
.type(hashtags.hashtag2);
saveAndPreview();
//check the labels are present and clickable.
......@@ -160,20 +154,18 @@ context('Pro Settings', () => {
});
it('should allow the user to set footer', () => {
cy.contains('Footer')
.click();
cy.contains('Footer').click();
cy.get(footerText)
.clear()
.type(strings.footer);
cy.contains('Add Link')
.click();
cy.contains('Add Link').click();
cy.get(footer.hrefInput)
.clear()
.type(strings.footerHref);
cy.get(footer.titleInput)
.clear()
.type(strings.footerTitle);
......@@ -189,40 +181,37 @@ context('Pro Settings', () => {
function saveAndPreview() {
//save and await response
cy.contains('Save')
.click()
.wait('@settings').then((xhr) => {
.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.get(previewButton)
.click();
// cy.get(previewButton).click();
}
function clearHashtags() {
cy.contains('Hashtags')
.click();
cy.contains('+ Add Tag')
.click();
cy.contains('clear')
.click({multiple: true});
saveAndPreview();
cy.contains('Hashtags').click();
cy.contains('+ Add Tag').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.plainBackgroundColor).then(elem => {
// elem.val('#00dd00');
// //save and await response
// cy.contains('Save')
// .click()
// .click()
// .wait('@settings').then((xhr) => {
// expect(xhr.status).to.equal(200);
// expect(xhr.response.body).to.deep.equal({ status: 'success' });
......@@ -235,4 +224,4 @@ context('Pro Settings', () => {
// })
}
})
});
This diff is collapsed.
import { Component, EventEmitter } from '@angular/core';
import { Client } from '../../../services/api';
import { UserAvatarService } from '../../services/user-avatar.service';
import { of, Observable } from 'rxjs';
@Component({
selector: 'minds-avatar',
......@@ -14,17 +14,28 @@ import { Client } from '../../../services/api';
],
outputs: ['added'],
template: `
<div class="minds-avatar" [style.background-image]="'url(' + src + ')'">
<div
class="minds-avatar"
[ngStyle]="{ 'background-image': 'url(' + (getSrc() | async) + ')' }"
>
<img
*ngIf="!src"
*ngIf="!(userAvatarService.src$ | async)"
src="{{ minds.cdn_assets_url }}assets/avatars/blue/default-large.png"
class="mdl-shadow--4dp"
/>
<div *ngIf="editing" class="overlay">
<i class="material-icons">{{ icon }}</i>
<ng-container *ngIf="showPrompt">
<span *ngIf="src" i18n="@@COMMON__AVATAR__CHANGE">Change avatar</span>
<span *ngIf="!src" i18n="@@COMMON__AVATAR__ADD">Add an avatar</span>
<span
*ngIf="userAvatarService.src$ | async"
i18n="@@COMMON__AVATAR__CHANGE"
>Change avatar</span
>
<span
*ngIf="!(userAvatarService.src$ | async)"
i18n="@@COMMON__AVATAR__ADD"
>Add an avatar</span
>
</ng-container>
</div>
<input *ngIf="editing" type="file" #file (change)="add($event)" />
......@@ -40,18 +51,21 @@ export class MindsAvatar {
index: number = 0;
icon: string = 'camera';
showPrompt: boolean = true;
file: any;
added: EventEmitter<any> = new EventEmitter();
constructor(public userAvatarService: UserAvatarService) {}
set _object(value: any) {
if (!value) return;
value.icontime = value.icontime ? value.icontime : '';
this.object = value;
this.src = `${this.minds.cdn_url}fs/v1/avatars/${this.object.guid}/large/${this.object.icontime}`;
if (this.object.type === 'user')
if (this.object.type !== 'user') {
this.src = `${this.minds.cdn_url}fs/v1/avatars/${this.object.guid}/large/${this.object.icontime}`;
} else if (this.object.guid !== this.minds.user.guid) {
this.src = `${this.minds.cdn_url}icon/${this.object.guid}/large/${this.object.icontime}`;
}
}
set _src(value: any) {
......@@ -63,6 +77,10 @@ export class MindsAvatar {
if (!this.editing && this.file) this.done();
}
/**
* New avatar added.
* @param e - the element.
*/
add(e) {
if (!this.editing) return;
......@@ -78,6 +96,9 @@ export class MindsAvatar {
typeof reader.result === 'string'
? reader.result
: reader.result.toString();
if (this.object.type === 'user' && this.isOwnerAvatar()) {
this.userAvatarService.src$.next(this.src);
}
};
reader.readAsDataURL(this.file);
......@@ -87,9 +108,28 @@ export class MindsAvatar {
if (this.waitForDoneSignal !== true) this.done();
}
/**
* Called upon being done.
*/
done() {
console.log('sending done');
this.added.next(this.file);
this.file = null;
}
/**
* Gets the src of the image
* @returns { Observables<string> } the src for the image.
*/
getSrc(): Observable<string> {
return this.isOwnerAvatar() ? this.userAvatarService.src$ : of(this.src);
}
/**
* Determined whether this is a users avatar.
* @returns true if the object guid matches the currently logged in user guid
*/
isOwnerAvatar(): boolean {
return this.object.guid === this.minds.user.guid;
}
}
......@@ -43,8 +43,8 @@ export class ChannelBadgesComponent {
return true;
} else if (
!this.user.is_admin &&
(this.session.isAdmin() &&
this.user.guid !== this.session.getLoggedInUser().guid)
this.session.isAdmin() &&
this.user.guid !== this.session.getLoggedInUser().guid
) {
return true;
}
......
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { DashboardLayoutComponent } from './dashboard-layout.component';
describe('DashboardLayoutComponent', () => {
let component: DashboardLayoutComponent;
let fixture: ComponentFixture<DashboardLayoutComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [DashboardLayoutComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(DashboardLayoutComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
<div
class="m-draggableList__listItem m-draggableList__listHeader"
*ngIf="headers"
(click)="clickedHeaderRow($event)"
>
<ng-container *ngFor="let header of headers">
<div class="m-draggableList__cell">{{ header | titlecase }}</div>
</ng-container>
<div class="m-draggableList__cell"></div>
</div>
<ul
dndDropzone
[dndHorizontal]="false"
[dndEffectAllowed]="dndEffectAllowed"
(dndStart)="onDragStart($event)"
(dndDrop)="onDrop($event)"
class="m-draggableList__list"
[ngClass]="{ dragging: dragging }"
>
<div class="dndPlaceholder" dndPlaceholderRef></div>
<li
*ngFor="let item of data; let i = index; trackBy: trackByFunction"
[dndDraggable]="item"
[dndEffectAllowed]="'move'"
[dndDragImageOffsetFunction]="dragImageOffsetRight"
class="m-draggableList__listItem"
>
<ng-container
[ngTemplateOutlet]="template"
[ngTemplateOutletContext]="{ item: item, i: i }"
></ng-container>
<div class="m-draggableList__cell">
<i class="handle material-icons" dndHandle>open_with</i>
<i class="material-icons" (click)="removeItem(i)">
clear
</i>
</div>
</li>
</ul>
@import 'themes';
m-draggable-list {
m-draggableList {
width: 100%;
@include m-theme() {
box-shadow: 0 1px 4px 0 rgba(themed($m-black), 0.1);
}
ul.m-draggableList__list {
width: 100%;
list-style: none;
padding: 0;
padding-inline-start: 0;
margin: 0;
display: flex;
flex-direction: column;
transition: all ease 300ms;
&.dndDragover {
padding-top: 16px;
padding-bottom: 16px;
// padding-top: 16px;
// padding-bottom: 16px;
@include m-theme() {
background-color: rgba(themed($m-black), 0.05);
box-shadow: 0 1px 4px 0 rgba(themed($m-black), 0.1);
}
}
&.dragging {
li.m-draggableList__listItem {
&:first-child {
@include m-theme() {
border-top: 1px solid themed($m-grey-50);
}
}
}
}
}
li.m-draggableList__listItem {
padding: 8px;
border: 1px solid #ddd;
.m-draggableList__listItem {
display: flex;
align-items: center;
list-style-type: none;
padding: 0;
margin: 0;
@include m-theme() {
border: 1px solid themed($m-grey-50);
color: themed($m-grey-800);
}
// &:first-child {
&:not(.m-draggableList__listHeader) {
@include m-theme() {
border-top: none;
}
}
// }
&.m-draggableList__listHeader {
@include m-theme() {
// border-bottom: none;
color: themed($m-grey-300);
}
}
}
input.m-draggableList__cell {
width: 0;
min-width: 0;
}
.m-draggableList__cell {
padding: 10px 20px;
flex: 1 1 0px;
box-sizing: border-box;
@include m-theme() {
border: none;
border-right: 1px solid themed($m-grey-50);
background-color: themed($m-white);
}
&input {
width: 0;
min-width: 0;
}
&:last-child {
//icon cell
padding: 10px 15px;
flex: 0 0 80px;
max-width: 80px;
height: 40px;
display: flex;
align-items: center;
justify-content: space-between;
.handle {
@include m-theme() {
color: themed($grey-600);
}
@include m-theme() {
border-right: none;
}
}
}
i {
cursor: pointer;
width: auto;
height: auto;
transition: all 0.3s ease;
@include m-theme() {
color: themed($m-grey-300);
}
&.handle {
font-size: 20px;
padding-right: 8px;
@include m-theme() {
}
}
&:hover {
transform: scale(1.15);
@include m-theme() {
color: themed($m-grey-200);
}
}
}
.dndPlaceholder {
min-height: 100px;
@include m-theme() {
border: 1px dashed rgba(themed($m-grey-100), 0.8);
}
}
}
import { Component, ContentChild, Input, TemplateRef } from '@angular/core';
import { DndDropEvent, EffectAllowed } from 'ngx-drag-drop';
import {
Component,
ContentChild,
Input,
TemplateRef,
Output,
EventEmitter,
} from '@angular/core';
import {
DndDropEvent,
EffectAllowed,
DndDragImageOffsetFunction,
} from 'ngx-drag-drop';
@Component({
selector: 'm-draggable-list',
template: `
<ul
dndDropzone
[dndHorizontal]="false"
[dndEffectAllowed]="dndEffectAllowed"
(dndDrop)="onDrop($event)"
class="m-draggableList__list"
>
<div
class="dndPlaceholder"
dndPlaceholderRef
style="min-height:100px;border:1px dashed green;background-color:rgba(0, 0, 0, 0.1)"
></div>
<li
*ngFor="let item of data; let i = index; trackBy: trackByFunction"
[dndDraggable]="item"
[dndEffectAllowed]="'move'"
class="m-draggableList__listItem"
>
<i class="handle material-icons" dndHandle>reorder</i>
<ng-container
[ngTemplateOutlet]="template"
[ngTemplateOutletContext]="{ item: item, i: i }"
></ng-container>
</li>
</ul>
`,
selector: 'm-draggableList',
templateUrl: 'list.component.html',
})
export class DraggableListComponent {
@Input() data: Array<any>;
@Input() dndEffectAllowed: EffectAllowed = 'copyMove';
@Input() id: string;
@Input() headers: string[];
@ContentChild(TemplateRef, { static: false }) template: TemplateRef<any>;
@Output() emptyListHeaderRowClicked: EventEmitter<any> = new EventEmitter();
dragging: boolean = false;
trackByFunction(index, item) {
return this.id ? item[this.id] + index : index;
}
onDrop(event: DndDropEvent) {
this.dragging = false;
if (
this.data &&
(event.dropEffect === 'copy' || event.dropEffect === 'move')
......@@ -62,4 +52,27 @@ export class DraggableListComponent {
this.data.splice(dropIndex, 0, event.data);
}
}
onDragStart(event: DragEvent) {
this.dragging = true;
}
removeItem(index) {
this.data.splice(index, 1);
}
clickedHeaderRow($event) {
if (this.data.length === 0) {
this.emptyListHeaderRowClicked.emit($event);
}
}
dragImageOffsetRight: DndDragImageOffsetFunction = (
event: DragEvent,
dragImage: HTMLElement
) => {
return {
x: dragImage.offsetWidth - 57,
y: event.offsetY + 10,
};
};
}
import { Injectable } from '@angular/core';
import {
filter,
first,
map,
switchMap,
mergeMap,
skip,
take,
} from 'rxjs/operators';
import { filter, first, switchMap, mergeMap, skip, take } from 'rxjs/operators';
import { FeedsService } from '../../services/feeds.service';
import { Subscription } from 'rxjs';
@Injectable()
export class FeaturedContentService {
offset: number = -1;
offset = 0;
maximumOffset = 0;
feedLength = 0;
protected feedSubscription: Subscription;
constructor(protected feedsService: FeedsService) {
this.onInit();
}
onInit() {
this.feedSubscription = this.feedsService.feed.subscribe(feed => {
this.feedLength = feed.length;
this.maximumOffset = this.feedLength - 1;
});
this.feedsService
.setLimit(12)
.setOffset(0)
......@@ -23,28 +28,36 @@ export class FeaturedContentService {
}
async fetch() {
if (this.offset >= this.feedsService.rawFeed.getValue().length) {
this.offset = -1;
}
// Refetch every 2 calls, if not loading
if (this.offset % 2 && !this.feedsService.inProgress.getValue()) {
this.feedsService.clear();
this.feedsService.fetch();
}
return await this.feedsService.feed
.pipe(
filter(feed => feed.length > 0),
first(),
mergeMap(feed => feed),
filter(entities => entities.length > 0),
mergeMap(feed => feed), // Convert feed array to stream
skip(this.offset++),
take(1),
switchMap(async entity => {
if (!entity) {
return false;
} else {
const resolvedEntity = await entity.pipe(first()).toPromise();
this.resetOffsetAtEndOfStream();
return resolvedEntity;
}
return await entity.pipe(first()).toPromise();
})
)
.toPromise();
}
protected resetOffsetAtEndOfStream() {
if (this.offset >= this.maximumOffset) {
this.offset = 0;
this.fetchNextFeed();
}
}
protected fetchNextFeed() {
if (!this.feedsService.inProgress.getValue()) {
this.feedsService.clear();
this.feedsService.fetch();
}
}
}
.m-formDescriptor {
display: inline-block;
margin-left: 33px;
padding-left: 14px;
font-size: 15px;
line-height: 20px;
@include m-theme() {
......
<div class="m-formToast__wrapper" [hidden]="hidden">
<i
class="material-icons m-formToast__icon--statusSuccess"
*ngIf="status === 'success'"
>check</i
>
<i
class="material-icons m-formToast__icon--statusError"
*ngIf="status === 'error'"
>warning</i
>
<p i18n><ng-content></ng-content></p>
<div class="m-formToast__iconWrapper">
<i class="material-icons m-formToast__icon--close" (click)="hidden = true"
>close</i
>
</div>
<div class="m-formToast__toastsContainer">
<ng-container *ngFor="let toast of toasts; let i = index">
<div class="m-formToast__wrapper" *ngIf="toast.visible">
<i
class="material-icons m-formToast__icon--success"
*ngIf="toast.type === 'success'"
>check</i
>
<i
class="material-icons m-formToast__icon--error"
*ngIf="toast.type === 'error'"
>warning</i
>
<i
class="material-icons m-formToast__icon--warning"
*ngIf="toast.type === 'warning'"
>warning</i
>
<p i18n>{{ toast.message }}</p>
<div class="m-formToast__iconWrapper">
<i class="material-icons m-formToast__icon--close" (click)="dismiss(i)"
>clear</i
>
</div>
</div>
</ng-container>
</div>
m-formToast {
margin: 37px 70px 0 70px;
display: block;
display: flex;
flex-flow: column nowrap;
max-width: 522px;
position: fixed;
bottom: 24px;
}
.m-formToasts__toastsContainer {
min-width: 300px;
transition: all 0.3s ease;
}
.m-formToast__wrapper {
width: 100%;
font-size: 15px;
line-height: 20px;
padding: 13px;
margin-bottom: 16px;
display: flex;
@include m-theme() {
color: themed($m-grey-600);
......@@ -18,29 +28,34 @@ m-formToast {
}
}
[class*='m-formToast__icon--status'] {
[class*='m-formToast__icon--'] {
margin-right: 10px;
}
.m-formToast__icon--statusSuccess {
.m-formToast__icon--success {
@include m-theme() {
color: themed($m-green-dark);
}
}
.m-formToast__icon--statusError {
.m-formToast__icon--error {
@include m-theme() {
color: themed($m-red);
}
}
.m-formToast__icon--warning {
@include m-theme() {
color: themed($m-amber-dark);
}
}
.m-formToast__icon--close {
cursor: pointer;
transition: all 0.2s ease-out;
@include m-theme() {
color: themed($m-grey-600);
color: themed($m-grey-400);
}
&:hover {
transform: scale(1.2);
@include m-theme() {
color: themed($m-grey-300);
color: themed($m-grey-200);
}
}
&:active {
......@@ -53,7 +68,8 @@ m-formToast {
@media screen and (max-width: $min-tablet) {
m-formToast {
// margin: 37px 24px 0 24px;
margin: auto;
margin: 0px 24px;
max-width: 85%;
}
}
......
import { Component, OnInit, Input } from '@angular/core';
import { Component, OnInit, OnDestroy } from '@angular/core';
import { FormToast, FormToastService } from '../../services/form-toast.service';
import { Subscription } from 'rxjs';
@Component({
selector: 'm-formToast',
templateUrl: './form-toast.component.html',
})
export class FormToastComponent implements OnInit {
@Input() status: string = 'success';
@Input() hidden: boolean = false; //OJMTODO
constructor() {}
export class FormToastComponent implements OnInit, OnDestroy {
toasts: FormToast[] = [];
timeoutIds: number[] = [];
subscription: Subscription;
ngOnInit() {}
}
constructor(private service: FormToastService) {}
ngOnInit() {
this.subscription = this.service.onToast().subscribe(toast => {
if (!toast.message) {
// clear toasts when an empty toast is received
this.toasts = [];
return;
}
toast['visible'] = true;
const toastIndex = this.toasts.push(toast) - 1;
console.log('***tolll', toast);
const toastTimeout = setTimeout(() => {
this.toasts[toastIndex].visible = false;
console.log('***to', this.toasts[toastIndex]);
}, 10000);
// TODOOJM : add timer, add max-width, slide into fixed position
this.timeoutIds.push(setTimeout(() => toastTimeout));
});
}
dismiss(index) {
console.log(this.toasts[index]);
this.toasts[index].visible = false;
}
ngOnDestroy() {
this.timeoutIds.forEach(id => clearTimeout(id));
this.subscription.unsubscribe();
}
}
<p>
mini-chart works!
</p>
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { MiniChartComponent } from './mini-chart.component';
describe('MiniChartComponent', () => {
let component: MiniChartComponent;
let fixture: ComponentFixture<MiniChartComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [MiniChartComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(MiniChartComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'm-miniChart',
templateUrl: './mini-chart.component.html',
})
export class MiniChartComponent implements OnInit {
constructor() {}
ngOnInit() {}
}
......@@ -22,7 +22,7 @@ export class NSFWSelectorComponent {
@Input('service') serviceRef: string = 'consumer';
@Input('consumer') consumer: false;
@Input('expanded') expanded: false;
@Output('selected') onSelected: EventEmitter<any> = new EventEmitter();
@Output('selectedChange') onSelected: EventEmitter<any> = new EventEmitter();
constructor(
public creatorService: NSFWSelectorCreatorService,
......@@ -33,7 +33,9 @@ export class NSFWSelectorComponent {
ngOnInit() {
if (this.service.reasons) {
this.service.reasons.map(r => this.toggle(r.value));
for (const reason of this.service.reasons) {
this.toggle(reason.value, false);
}
}
}
......@@ -64,14 +66,17 @@ export class NSFWSelectorComponent {
}
}
toggle(reason) {
toggle(reason, triggerChange = true) {
if (reason.locked) {
return;
}
this.service.toggle(reason);
const reasons = this.service.reasons.filter(r => r.selected);
this.onSelected.next(reasons);
if (triggerChange) {
const reasons = this.service.reasons.filter(r => r.selected);
this.onSelected.next(reasons);
}
}
hasSelections(): boolean {
......
......@@ -21,7 +21,7 @@ m-pageLayout {
}
}
.m-tooltip--bubble {
z-index: 9999;
z-index: 99;
font-size: 11px;
@include m-theme() {
color: themed($m-white);
......
import { Component, OnInit, Input } from '@angular/core';
import { Component, OnInit, Input, HostBinding } from '@angular/core';
@Component({
selector: 'm-pageLayout',
......@@ -6,6 +6,7 @@ import { Component, OnInit, Input } from '@angular/core';
})
export class PageLayoutComponent implements OnInit {
@Input() menuId: string;
@HostBinding('class.isForm') @Input() isForm: boolean = false;
constructor() {}
ngOnInit() {}
......
......@@ -231,7 +231,7 @@
<m-nsfw-selector
service="editing"
[selected]="entity.nsfw"
(selected)="onNSFWSelected($event)"
(selectedChange)="onNSFWSelected($event)"
>
</m-nsfw-selector>
</li>
......
......@@ -55,7 +55,7 @@
[href]="src.perma_url"
target="_blank"
rel="noopener noreferrer"
class="meta mdl-color-text--blue-grey-900"
class="meta"
[ngClass]="{ 'm-rich-embed-has-thumbnail': src.thumbnail_src, 'm-rich-embed--title--no-padding': hasInlineContentLoaded() }"
>
<h2
......@@ -74,10 +74,7 @@
<a class="thumbnail" *ngIf="preview.thumbnail">
<img src="{{preview.thumbnail}}" />
</a>
<a
class="meta mdl-color-text--blue-grey-900"
[ngClass]="{ 'm-has-thumbnail': preview.thumbnail }"
>
<a class="meta" [ngClass]="{ 'm-has-thumbnail': preview.thumbnail }">
<h2 class="m-rich-embed--title mdl-card__title-text">
{{preview.title | excerpt}}
</h2>
......
......@@ -16,9 +16,6 @@ minds-rich-embed {
left: 0;
width: 100%;
height: 100%;
@include m-theme() {
background-color: rgba(themed($m-black-always), 0.2);
}
&:hover {
background: transparent;
......
......@@ -3,11 +3,14 @@ import {
ElementRef,
ChangeDetectorRef,
ChangeDetectionStrategy,
Output,
EventEmitter,
} from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
import { RichEmbedService } from '../../../services/rich-embed';
import mediaProxyUrl from '../../../helpers/media-proxy-url';
import { FeaturesService } from '../../../services/features.service';
@Component({
moduleId: module.id,
......@@ -17,18 +20,22 @@ import mediaProxyUrl from '../../../helpers/media-proxy-url';
})
export class MindsRichEmbed {
type: string = '';
mediaSource: string = '';
src: any = {};
preview: any = {};
maxheight: number = 320;
inlineEmbed: any = null;
embeddedInline: boolean = false;
cropImage: boolean = false;
modalRequestSubscribed: boolean = false;
@Output() mediaModalRequested: EventEmitter<any> = new EventEmitter();
private lastInlineEmbedParsed: string;
constructor(
private sanitizer: DomSanitizer,
private service: RichEmbedService,
private cd: ChangeDetectorRef
private cd: ChangeDetectorRef,
protected featureService: FeaturesService
) {}
set _src(value: any) {
......@@ -65,6 +72,14 @@ export class MindsRichEmbed {
// Inline Embedding
let inlineEmbed = this.parseInlineEmbed(this.inlineEmbed);
if (
this.featureService.has('media-modal') &&
this.mediaSource === 'youtube'
) {
this.modalRequestSubscribed =
this.mediaModalRequested.observers.length > 0;
}
if (
inlineEmbed &&
inlineEmbed.id &&
......@@ -80,9 +95,35 @@ export class MindsRichEmbed {
}
this.inlineEmbed = inlineEmbed;
if (
this.modalRequestSubscribed &&
this.featureService.has('media-modal') &&
this.mediaSource === 'youtube'
) {
if (this.inlineEmbed && this.inlineEmbed.htmlProvisioner) {
this.inlineEmbed.htmlProvisioner().then(html => {
this.inlineEmbed.html = html;
this.detectChanges();
});
// @todo: catch any error here and forcefully window.open to destination
}
}
}
action($event) {
if (
this.modalRequestSubscribed &&
this.featureService.has('media-modal') &&
this.mediaSource === 'youtube'
) {
$event.preventDefault();
$event.stopPropagation();
this.mediaModalRequested.emit();
return;
}
if (this.inlineEmbed && !this.embeddedInline) {
$event.preventDefault();
$event.stopPropagation();
......@@ -120,6 +161,7 @@ export class MindsRichEmbed {
if ((matches = youtube.exec(url)) !== null) {
if (matches[1]) {
this.mediaSource = 'youtube';
return {
id: `video-youtube-${matches[1]}`,
className:
......@@ -138,12 +180,13 @@ export class MindsRichEmbed {
if ((matches = vimeo.exec(url)) !== null) {
if (matches[1]) {
this.mediaSource = 'vimeo';
return {
id: `video-vimeo-${matches[1]}`,
className:
'm-rich-embed-video m-rich-embed-video-iframe m-rich-embed-video-vimeo',
html: this.sanitizer.bypassSecurityTrustHtml(`<iframe
src="https://player.vimeo.com/video/${matches[1]}?autoplay=1&title=0&byline=0&portrait=0"
src="https://player.vimeo.com/video/${matches[1]}?title=0&byline=0&portrait=0"
frameborder="0"
webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>`),
playable: true,
......@@ -156,6 +199,7 @@ export class MindsRichEmbed {
if ((matches = soundcloud.exec(url)) !== null) {
if (matches[1]) {
this.mediaSource = 'soundcloud';
return {
id: `audio-soundcloud-${matches[1]}`,
className:
......@@ -183,6 +227,7 @@ export class MindsRichEmbed {
if ((matches = spotify.exec(url)) !== null) {
if (matches[1]) {
this.mediaSource = 'spotify';
return {
id: `audio-spotify-${matches[1]}`,
className:
......@@ -207,7 +252,7 @@ export class MindsRichEmbed {
if (!id) {
return null;
}
this.mediaSource = 'giphy';
return {
id: `image-giphy-${matches[1]}`,
className:
......@@ -225,7 +270,11 @@ export class MindsRichEmbed {
}
hasInlineContentLoaded() {
return this.embeddedInline && this.inlineEmbed && this.inlineEmbed.html;
return this.featureService.has('media-modal')
? !this.modalRequestSubscribed &&
this.inlineEmbed &&
this.inlineEmbed.html
: this.embeddedInline && this.inlineEmbed && this.inlineEmbed.html;
}
detectChanges() {
......
......@@ -32,6 +32,7 @@ m-shadowboxHeader.isScrollable {
cursor: pointer;
padding: 10px 20px;
font-size: 17px;
font-weight: 300;
background-color: #4fc3a9;
height: 43px;
border: 0;
......@@ -43,10 +44,15 @@ m-shadowboxHeader.isScrollable {
}
&:hover {
transform: scale(1.02);
background-color: #4cb9a0;
// background-color: #4cb9a0;
}
&:active {
background-color: #63dac0;
background-color: #55ccb2;
@include m-theme() {
box-shadow: 0 3px 3px -2px rgba(themed($m-black), 0.2),
0 2px 5px 0 rgba(themed($m-black), 0.14),
0 1px 7px 0 rgba(themed($m-black), 0.12);
}
}
&:disabled,
&[disabled] {
......@@ -56,6 +62,9 @@ m-shadowboxHeader.isScrollable {
}
}
}
button {
outline: 0;
}
// ---------------------------------------
m-shadowboxLayout.isForm {
margin-top: 69px;
......
......@@ -9,14 +9,8 @@ export class ShadowboxLayoutComponent implements OnInit {
@Input() hasHeader: boolean = true;
@Input() headerTitle: string;
@Input() headerSubtitle: string;
@Input() isForm: boolean = false;
@HostBinding('class.isForm') @Input() isForm: boolean = false;
@HostBinding('class') get checkIsForm() {
if (!this.isForm) {
return '';
}
return 'isForm';
}
constructor() {}
ngOnInit() {}
......
const sidebarMenuCategories = [
{
header: {
id: 'analytics',
label: 'Analytics',
path: '/analytics/dashboard/',
permissions: ['admin', 'user'],
},
links: [
{
id: 'summary',
label: 'Summary',
permissions: ['admin'],
},
{
id: 'traffic',
label: 'Traffic',
permissions: ['admin', 'user'],
},
{
id: 'earnings',
label: 'Earnings',
permissions: ['admin', 'user'],
},
{
id: 'engagement',
label: 'Engagement',
permissions: ['admin', 'user'],
// path: '/some/path/outside/header/path',
},
{
id: 'trending',
label: 'Trending',
permissions: ['admin', 'user'],
},
// {
// id: 'referrers',
// label: 'Referrers',
// permissions: ['admin', 'user'],
// },
// {
// id: 'plus',
// label: 'Plus',
// permissions: ['admin'],
// },
// {
// id: 'pro',
// label: 'Pro',
// permissions: ['admin'],
// {
// id: 'boost',
// label: 'Boost',
// permissions: ['admin'],
// },
// {
// id: 'nodes',
// label: 'Nodes',
// permissions: ['admin'],
// },
],
},
];
export default sidebarMenuCategories;
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { Component, Input } from '@angular/core';
import { RouterTestingModule } from '@angular/router/testing';
import { Session } from '../../../services/session';
import { sessionMock } from '../../../../tests/session-mock.spec';
......
......@@ -62,7 +62,7 @@ const sidebarMenus = [
{
header: {
id: 'pro_settings',
label: 'PRO Settings',
label: 'Pro Settings',
path: '/pro/settings/',
permissions: ['pro'],
},
......
......@@ -4,13 +4,6 @@ import { Client } from '../../services/api/client';
import { Session } from '../../services/session';
import { Storage } from '../../services/storage';
import AsyncLock from '../../helpers/async-lock';
import MindsClientHttpAdapter from '../../lib/minds-sync/adapters/MindsClientHttpAdapter.js';
import browserStorageAdapterFactory from '../../helpers/browser-storage-adapter-factory';
import BlockListSync from '../../lib/minds-sync/services/BlockListSync.js';
import AsyncStatus from '../../helpers/async-status';
@Injectable()
export class BlockListService {
blocked: BehaviorSubject<string[]>;
......
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable, of } from 'rxjs';
import { first, catchError } from 'rxjs/operators';
import { BehaviorSubject } from 'rxjs';
import { first } from 'rxjs/operators';
import { Client } from '../../services/api';
import { BlockListService } from './block-list.service';
import MindsClientHttpAdapter from '../../lib/minds-sync/adapters/MindsClientHttpAdapter.js';
import browserStorageAdapterFactory from '../../helpers/browser-storage-adapter-factory';
import EntitiesSync from '../../lib/minds-sync/services/EntitiesSync.js';
import AsyncStatus from '../../helpers/async-status';
import normalizeUrn from '../../helpers/normalize-urn';
type EntityObservable = BehaviorSubject<Object>;
type EntityObservables = Map<string, EntityObservable>;
......
......@@ -6,40 +6,8 @@ import { Session } from '../../services/session';
import { EntitiesService } from './entities.service';
import { BlockListService } from './block-list.service';
import MindsClientHttpAdapter from '../../lib/minds-sync/adapters/MindsClientHttpAdapter.js';
import browserStorageAdapterFactory from '../../helpers/browser-storage-adapter-factory';
import FeedsSync from '../../lib/minds-sync/services/FeedsSync.js';
import hashCode from '../../helpers/hash-code';
import AsyncStatus from '../../helpers/async-status';
import { BehaviorSubject, Observable, of, forkJoin, combineLatest } from 'rxjs';
import {
take,
switchMap,
map,
tap,
skipWhile,
first,
filter,
} from 'rxjs/operators';
export type FeedsServiceGetParameters = {
endpoint: string;
timebased: boolean;
//
limit: number;
offset?: number;
//
syncPageSize?: number;
forceSync?: boolean;
};
export type FeedsServiceGetResponse = {
entities: any[];
next?: number;
};
import { BehaviorSubject, Observable, combineLatest } from 'rxjs';
import { switchMap, map, tap, first } from 'rxjs/operators';
/**
* Enables the grabbing of data through observable feeds.
......@@ -69,6 +37,7 @@ export class FeedsService {
this.pageSize = this.offset.pipe(
map(offset => this.limit.getValue() + offset)
);
this.feed = this.rawFeed.pipe(
tap(feed => {
if (feed.length) this.inProgress.next(true);
......@@ -87,6 +56,7 @@ export class FeedsService {
this.inProgress.next(false);
})
);
this.hasMore = combineLatest(
this.rawFeed,
this.inProgress,
......
import { Injectable } from '@angular/core';
import { Observable, Subject } from 'rxjs';
// import { filter } from 'rxjs/operators';
export interface FormToast {
type?: 'success' | 'error' | 'warning' | 'info' | null;
message?: string;
visible?: boolean;
}
@Injectable()
export class FormToastService {
private subject = new Subject<FormToast>();
constructor() {}
// enable subscribing to toasts observable
onToast(): Observable<FormToast> {
return this.subject.asObservable();
// .pipe(filter(x => x);
// .pipe(filter(x => x && x.toastId === toastId));
}
success(message: string) {
const toast: FormToast = {
message: message,
type: 'success',
};
this.trigger(toast);
}
error(message: string) {
const toast: FormToast = {
message: message,
type: 'error',
};
this.trigger(toast);
}
warn(message: string) {
const toast: FormToast = {
message: message,
type: 'warning',
};
this.trigger(toast);
}
inform(message: string) {
const toast: FormToast = {
message: message,
type: 'error',
};
this.trigger(toast);
}
trigger(toast: FormToast) {
toast['visible'] = true;
if (!toast.type) toast.type = 'info';
this.subject.next(toast);
}
}
/**
* @author Ben Hayward
* @desc Singleton service used to store the current user avatar as a BehaviorSubject.
*/
import { Injectable } from '@angular/core';
import { BehaviorSubject, Subscription } from 'rxjs';
import { Session } from '../../services/session';
import { MindsUser } from '../../interfaces/entities';
@Injectable({
providedIn: 'root',
})
export class UserAvatarService {
private minds = window.Minds;
private user: MindsUser;
public src$: BehaviorSubject<string> = new BehaviorSubject<string>('');
public loggedIn$: Subscription;
constructor(public session: Session) {
this.init();
// Subscribe to loggedIn$ and on login, update src$.
if (this.session.loggedinEmitter) {
this.loggedIn$ = this.session.loggedinEmitter.subscribe(is => {
if (is) {
this.src$.next(this.getSrc());
}
});
}
}
/**
* Sets the current user and avatar src.
*/
public init(): void {
this.user = this.session.getLoggedInUser();
this.src$.next(this.getSrc());
}
/**
* Gets the Src string using the global minds object and the held user object.
*/
public getSrc(): string {
return `${this.minds.cdn_url}icon/${this.user.guid}/large/${this.user.icontime}`;
}
}
......@@ -14,7 +14,8 @@ export let mindsHttpClientMock = new (function() {
}
if (
!res ||
((res.status && res.status === 'error') || res.status === 'failed')
(res.status && res.status === 'error') ||
res.status === 'failed'
)
observer.error(res);
......
......@@ -11,7 +11,9 @@
*ngIf="isNumber(value); else placeholderValue"
>
<ng-template ngSwitchCase="usd">
<div class="m-analytics__benchmarkValue">{{ value | currency }}</div>
<div class="m-analytics__benchmarkValue">
{{ value / 100 | currency }}
</div>
<div class="m-analytics__benchmarkUnit">USD</div>
</ng-template>
<ng-template ngSwitchCase="eth">
......
......@@ -34,7 +34,7 @@
{{ description }}
<ng-container *ngIf="(category$ | async) === 'earnings'">
<a *ngIf="!session.getLoggedInUser().pro" routerLink="/pro"
>Upgrade to PRO</a
>Upgrade to Pro</a
>
<a
*ngIf="
......
......@@ -23,7 +23,7 @@
></m-analytics__benchmark>
</div>
<!-- CHART TILES -->
<ng-container *ngFor="let tile of fakeTiles">
<ng-container *ngFor="let tile of tiles">
<div class="m-analyticsSummary__tile">
<m-analytics__benchmark
[label]="tile.label"
......@@ -44,7 +44,7 @@
<!-- BOOST BACKLOG -->
<div class="m-analyticsSummary__boostBacklogWrapper" *ngIf="boosts">
<div class="m-analyticsSummary__boostBacklogTitle" i18n>Boost Backlog</div>
<div class="m-analyticsSummary__boostBacklogTitle">Boost Backlog</div>
<div class="m-analyticsSummary__boostRowsContainer">
<ng-container *ngFor="let boostRow of boostRows">
<div class="m-analyticsSummary__boostRow">
......
import { Component, OnInit } from '@angular/core';
import { Component, OnInit, ChangeDetectorRef } from '@angular/core';
import fakeData from './../../fake-data';
import { Client } from '../../../../../services/api';
import { Session } from '../../../../../services/session';
......@@ -13,7 +13,7 @@ export class AnalyticsLayoutSummaryComponent implements OnInit {
boosts: Array<any>;
boostRows: Array<any> = [];
fakeTiles;
url = '/api/v2/analytics/dashboards/';
url = 'api/v2/analytics/dashboards/';
tiles = [
{
id: 'pageviews',
......@@ -29,39 +29,56 @@ export class AnalyticsLayoutSummaryComponent implements OnInit {
label: 'Daily Active Users',
unit: 'number',
interval: 'day',
endpoint:
this.url +
'traffic?metric=active_users&timespan=30d&filter=channel::all',
endpoint: this.url + 'traffic',
params: {
metric: 'active_users',
timespan: '30d',
filter: 'channel::all',
},
},
{
id: 'active_users',
label: 'Monthly Active Users',
unit: 'number',
interval: 'month',
endpoint:
this.url +
'traffic?metric=active_users&timespan=12m&filter=channel::all',
endpoint: this.url + 'traffic',
params: {
metric: 'active_users',
timespan: '1y',
filter: 'channel::all',
},
},
{
id: 'signups',
label: 'Signups',
unit: 'number',
interval: 'day',
endpoint:
this.url + 'traffic?metric=signups&timespan=30d&filter=channel::all',
endpoint: this.url + 'traffic',
params: {
metric: 'signups',
timespan: '30d',
filter: 'channel::all',
},
},
{
id: 'earnings',
id: 'earnings_total',
label: 'Total PRO Earnings',
unit: 'usd',
interval: 'day',
endpoint:
this.url +
'earnings?metric=active_users&timespan=30d&filter=platform::all,view_type::total,channel::all',
endpoint: this.url + 'earnings',
params: {
metric: 'earnings_total',
timespan: '30d',
filter: 'platform::all,view_type::total,channel::all',
},
},
];
constructor(private client: Client, public session: Session) {}
constructor(
private client: Client,
public session: Session,
private cd: ChangeDetectorRef
) {}
ngOnInit() {
// TODO: confirm how permissions/security will work
......@@ -74,25 +91,38 @@ export class AnalyticsLayoutSummaryComponent implements OnInit {
// this.boostRows = [this.boosts.slice(0, 2), this.boosts.slice(2, 4)];
this.getTiles();
this.loading = false;
}
async getTiles() {
this.tiles.forEach(tile => {
this.client
.get(tile.endpoint)
.then((response: any) => {
this.formatResponse(tile, response);
})
.catch(e => {
console.error(e);
});
this.tiles.forEach(async tile => {
try {
const response: any = await this.client.get(tile.endpoint, tile.params);
await this.formatResponse(tile, response);
} catch (e) {
console.error(e);
}
this.loading = false;
this.detectChanges();
});
}
formatResponse(tile, response) {
async formatResponse(tile, response) {
const metric = response.dashboard.metrics.find(m => m.id === tile.id);
tile['metric'] = metric;
tile['value'] = metric.visualisation.segments[0].buckets.slice(-1).value;
tile['description'] = response.description;
if (!metric) return;
tile.metric = metric;
const buckets = metric.visualisation
? metric.visualisation.segments[0].buckets
: [];
tile.value = buckets[buckets.length - 1]
? buckets[buckets.length - 1].value
: 0;
tile.description = metric.description;
tile.visualisation = metric.visualisation;
this.cd.markForCheck();
}
detectChanges() {
this.cd.markForCheck();
this.cd.detectChanges();
}
}
......@@ -133,7 +133,7 @@
</div>
<minds-button-boost
[object]="blog"
*ngIf="session.isLoggedIn()"
*ngIf="session.isLoggedIn() && !isScheduled(blog.time_created)"
></minds-button-boost>
</div>
</div>
......
......@@ -222,6 +222,10 @@ export class BlogView implements OnInit, OnDestroy {
.present();
}
isScheduled(time_created) {
return time_created && time_created * 1000 > Date.now();
}
/**
* called when the window resizes
* @param {Event} event
......
......@@ -36,11 +36,14 @@ export class CanaryPageComponent {
if (!this.user) return this.router.navigate(['/login']);
this.user.canary = true;
this.client.put('api/v2/canary');
await this.client.put('api/v2/canary');
window.location.reload();
}
turnOff() {
async turnOff() {
this.user.canary = false;
this.client.delete('api/v2/canary');
await this.client.delete('api/v2/canary');
window.location.reload();
}
}
......@@ -404,9 +404,24 @@ m-comments__tree,
font-size: 12px;
text-align: center;
margin: ($minds-padding * 2) 0;
@include m-theme() {
color: themed($m-grey-300);
}
a {
font-weight: inherit;
color: inherit;
cursor: pointer;
b {
font-weight: bold;
@include m-theme() {
color: themed($m-blue);
}
}
}
}
.m-comments--load-error-label {
......
<div class="m-comment m-comment--poster minds-block" *ngIf="!readonly">
<div
class="m-comment m-comment--poster minds-block"
*ngIf="!readonly && isLoggedIn"
>
<div class="minds-avatar">
<a [routerLink]="['/', session.getLoggedInUser().username]">
<img [src]="getAvatar()" class="mdl-shadow--2dp" />
......@@ -146,3 +149,18 @@
</div>
</div>
</div>
<div
class="m-comments--start-conversation-label"
*ngIf="!isLoggedIn && level < 1"
>
<a (click)="showLoginModal(); $event.preventDefault()">
<ng-container *ngIf="!conversation; else loggedOutConversationMessage" i18n>
<b>Log in</b> to comment
</ng-container>
<ng-template #loggedOutConversationMessage>
<ng-container i18n> <b>Log in</b> to send a message </ng-container>
</ng-template>
</a>
</div>
......@@ -18,6 +18,7 @@ import { Textarea } from '../../../common/components/editors/textarea.component'
import { SocketsService } from '../../../services/sockets';
import autobind from '../../../helpers/autobind';
import { AutocompleteSuggestionsService } from '../../suggestions/services/autocomplete-suggestions.service';
import { SignupModalService } from '../../modals/signup/service';
@Component({
selector: 'm-comment__poster',
......@@ -33,6 +34,7 @@ export class CommentPosterComponent {
@Input() readonly: boolean = false;
@Input() currentIndex: number = -1;
@Input() conversation: boolean = false;
@Input() level: number = 0;
@Output('optimisticPost') optimisticPost$: EventEmitter<
any
> = new EventEmitter();
......@@ -47,6 +49,7 @@ export class CommentPosterComponent {
constructor(
public session: Session,
public client: Client,
private signupModal: SignupModalService,
public attachment: AttachmentService,
public sockets: SocketsService,
public suggestions: AutocompleteSuggestionsService,
......@@ -196,6 +199,14 @@ export class CommentPosterComponent {
return true; // TODO: fix
}
get isLoggedIn() {
return this.session.isLoggedIn();
}
showLoginModal() {
this.signupModal.open();
}
detectChanges() {
this.cd.markForCheck();
this.cd.detectChanges();
......
......@@ -42,6 +42,7 @@
<p
class="m-comments--start-conversation-label"
*ngIf="
isLoggedIn &&
!inProgress &&
!error &&
comments?.length === 0 &&
......@@ -119,6 +120,7 @@
[entity]="entity"
[currentIndex]="comments.length - 1"
[conversation]="conversation"
[level]="level"
(posted)="onPosted($event)"
(optimisticPost)="onOptimisticPost($event)"
>
......
......@@ -315,6 +315,10 @@ export class CommentsThreadComponent implements OnInit {
return true;
}
get isLoggedIn() {
return this.session.isLoggedIn();
}
ngOnChanges(changes) {
// console.log('[comment:list]: on changes', changes);
}
......
......@@ -3,6 +3,7 @@ import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { Client } from '../../../services/api';
import { Session } from '../../../services/session';
import { UserAvatarService } from '../../../common/services/user-avatar.service';
@Component({
moduleId: module.id,
......@@ -31,7 +32,8 @@ export class LoginForm {
public session: Session,
public client: Client,
fb: FormBuilder,
private zone: NgZone
private zone: NgZone,
private userAvatarService: UserAvatarService
) {
this.form = fb.group({
username: ['', Validators.required],
......@@ -65,6 +67,7 @@ export class LoginForm {
// TODO: [emi/sprint/bison] Find a way to reset controls. Old implementation throws Exception;
this.inProgress = false;
this.session.login(data.user);
this.userAvatarService.init();
this.done.next(data.user);
})
.catch(e => {
......@@ -102,6 +105,7 @@ export class LoginForm {
})
.then((data: any) => {
this.session.login(data.user);
this.userAvatarService.init();
this.done.next(data.user);
})
.catch(e => {
......
......@@ -8,15 +8,25 @@
<div class="minds-list">
<div>
<m-sort-selector
class="m-group--sorted__SortSelector m-border"
[allowedAlgorithms]="false"
[allowedPeriods]="false"
[allowedCustomTypes]="['activities', 'images', 'videos']"
[customType]="type"
(onChange)="setFilter($event.customType)"
></m-sort-selector>
<div class="m-mindsList__tools m-border">
<div
*ngIf="isMember()"
class="m-mindsListTools__scheduled"
(click)="toggleScheduled()"
[class.selected]="viewScheduled"
>
<m-tooltip icon="date_range"> See Scheduled Activities </m-tooltip>
<span>scheduled: {{ scheduledCount }}</span>
</div>
<m-sort-selector
class="m-group--sorted__SortSelector"
[allowedAlgorithms]="false"
[allowedPeriods]="false"
[allowedCustomTypes]="['activities', 'images', 'videos']"
[customType]="type"
(onChange)="setFilter($event.customType)"
></m-sort-selector>
</div>
<ng-container
*ngIf="
isActivityFeed() &&
......
.m-group--sorted__SortSelector {
.m-mindsList__tools {
@include m-theme() {
background-color: themed($m-white);
}
display: flex;
position: relative;
padding: 8px;
margin-bottom: 16px;
flex-direction: row;
align-items: center;
.m-mindsListTools__scheduled {
cursor: pointer;
i {
vertical-align: middle;
font-size: 18px;
margin-right: 4px;
}
span {
text-transform: uppercase;
font-size: 11px;
letter-spacing: 1.25px;
text-rendering: optimizeLegibility;
}
}
.m-group--sorted__SortSelector {
margin-left: auto;
}
}
......@@ -58,8 +58,12 @@ export class GroupProfileFeedSortedComponent {
kicking: any;
viewScheduled: boolean = false;
@ViewChild('poster', { static: false }) protected poster: PosterComponent;
scheduledCount: number = 0;
constructor(
protected service: GroupsService,
public feedsService: FeedsService,
......@@ -86,11 +90,18 @@ export class GroupProfileFeedSortedComponent {
this.detectChanges();
let endpoint = 'api/v2/feeds/container';
if (this.viewScheduled) {
endpoint = 'api/v2/feeds/scheduled';
}
try {
this.feedsService
.setEndpoint(`api/v2/feeds/container/${this.group.guid}/${this.type}`)
.setEndpoint(`${endpoint}/${this.group.guid}/${this.type}`)
.setLimit(12)
.fetch();
this.getScheduledCount();
} catch (e) {
console.error('GroupProfileFeedSortedComponent.loadFeed', e);
}
......@@ -193,4 +204,16 @@ export class GroupProfileFeedSortedComponent {
this.cd.markForCheck();
this.cd.detectChanges();
}
toggleScheduled() {
this.viewScheduled = !this.viewScheduled;
this.load(true);
}
async getScheduledCount() {
const url = `api/v2/feeds/scheduled/${this.group.guid}/count`;
const response: any = await this.client.get(url);
this.scheduledCount = response.count;
this.detectChanges();
}
}
......@@ -4,14 +4,20 @@
class="m-groups--filter-selector-item"
[class.m-groups--filter-selector-active]="filter == 'activity'"
[routerLink]="['/groups/profile', group.guid, 'feed']"
i18n="group activities selector|@@GROUPS__PROFILE__FILTER_SELECTOR__FEED_OPTION"
i18n="
group activities
selector|@@GROUPS__PROFILE__FILTER_SELECTOR__FEED_OPTION
"
>Feed</a
>
<a
class="m-groups--filter-selector-item"
[class.m-groups--filter-selector-active]="filter == 'image'"
[routerLink]="['/groups/profile', group.guid, 'feed', 'image']"
i18n="group activities selector|@@GROUPS__PROFILE__FILTER_SELECTOR__FEED_OPTION"
i18n="
group activities
selector|@@GROUPS__PROFILE__FILTER_SELECTOR__FEED_OPTION
"
>Images</a
>
......
......@@ -134,7 +134,7 @@ import { Session } from '../../../services/session';
<m-nsfw-selector
service="editing"
[selected]="group.nsfw"
(selected)="onNSFWSelected($event)"
(selectedChange)="onNSFWSelected($event)"
>
</m-nsfw-selector>
</li>
......
......@@ -115,7 +115,7 @@ import { BlockListService } from '../../../../common/services/block-list.service
<m-nsfw-selector
service="editing"
[selected]="user.nsfw_lock"
(selected)="setNSFWLock($event)"
(selectedChange)="setNSFWLock($event)"
>
</m-nsfw-selector>
</li>
......
......@@ -171,7 +171,7 @@
service="editing"
[selected]="activity.nsfw"
[locked]="activity.ownerObj.nsfw_lock"
(selected)="onNSWFSelections($event)"
(selectedChange)="onNSWFSelections($event)"
>
</m-nsfw-selector>
</ng-container>
......@@ -219,7 +219,12 @@
<span i18n="@@M__COMMON__CONFIRM_18">Click to confirm you are 18+</span>
</span>
</div>
<minds-rich-embed [src]="activity" [maxheight]="480"></minds-rich-embed>
<minds-rich-embed
(mediaModalRequested)="openModal()"
[src]="activity"
[maxheight]="480"
>
</minds-rich-embed>
</div>
<div
......@@ -404,7 +409,8 @@
></m-wire-button>
<button
class="m-btn m-btn--action m-btn--slim minds-boost-button"
*ngIf="session.getLoggedInUser().guid == activity.owner_guid"
*ngIf="session.getLoggedInUser().guid == activity.owner_guid
&& !isScheduled(activity.time_created)"
id="boost-actions"
(click)="showBoost()"
>
......
......@@ -392,7 +392,7 @@ m-media--grid {
}
}
> *:not(m-post-menu) {
> *:not(m-post-menu):not(m-wire-button) {
vertical-align: middle;
margin-left: 0.35em;
......@@ -483,8 +483,13 @@ m-media--grid {
}
}
.m-wire-button > .ion-icon {
transform: scale(1.2);
.m-wire-button {
padding: 3px 6px;
& > .ion-icon {
margin-right: 4px;
transform: scale(1.2);
}
}
.m-media-content--extra {
......
<div class="m-mediaModal__wrapper">
<div class="m-mediaModal__wrapper" data-cy="data-minds-media-modal">
<div class="m-mediaModal__theater" (click)="clickedModal($event)">
<div
class="m-mediaModal m-mediaModal__clearFix"
......@@ -15,7 +15,10 @@
(touchend)="showOverlaysOnTablet()"
>
<!-- LOADING PANEL -->
<div class="m-mediaModal__loadingPanel" *ngIf="isLoading">
<div
class="m-mediaModal__loadingPanel"
*ngIf="isLoading && contentType !== 'rich-embed'"
>
<div class="mdl-spinner mdl-js-spinner is-active" [mdl]></div>
</div>
......@@ -65,6 +68,15 @@
</m-video>
</div>
<!-- RICH-EMBED -->
<div
class="m-mediaModal__mediaWrapper m-mediaModal__mediaWrapper--richEmbed"
*ngIf="contentType === 'rich-embed'"
>
<minds-rich-embed [src]="entity" [maxheight]="480">
</minds-rich-embed>
</div>
<!-- MEDIA: BLOG -->
<div
class="m-mediaModal__mediaWrapper m-mediaModal__mediaWrapper--blog"
......@@ -83,12 +95,12 @@
<!-- OVERLAY -->
<div
class="m-mediaModal__overlayContainer"
*ngIf="overlayVisible"
*ngIf="overlayVisible && contentType !== 'rich-embed'"
@fastFadeAnimation
>
<div
class="m-mediaModal__overlayTitleWrapper"
*ngIf="this.contentType !== 'blog'"
*ngIf="contentType !== 'blog'"
>
<!-- TITLE -->
<span
......
......@@ -99,6 +99,14 @@ m-overlay-modal {
}
}
.m-mediaModal__mediaWrapper--richEmbed {
width: 100%;
.meta {
display: none;
}
}
.m-mediaModal__mediaWrapper--blog {
line-height: initial;
overflow-y: auto;
......
import {
Component,
HostListener,
Injector,
Input,
OnDestroy,
OnInit,
ViewChild,
SkipSelf,
Injector,
ViewChild,
} from '@angular/core';
import { Location } from '@angular/common';
import { Event, NavigationStart, Router } from '@angular/router';
......@@ -26,6 +26,7 @@ import isMobileOrTablet from '../../../helpers/is-mobile-or-tablet';
import { ActivityService } from '../../../common/services/activity.service';
import { SiteService } from '../../../common/services/site.service';
import { ClientMetaService } from '../../../common/services/client-meta.service';
import { FeaturesService } from '../../../services/features.service';
export type MediaModalParams = {
redirectUrl?: string;
......@@ -119,8 +120,29 @@ export class MediaModalComponent implements OnInit, OnDestroy {
@ViewChild(MindsVideoComponent, { static: false })
videoComponent: MindsVideoComponent;
get videoDirectSrc() {
const sources = [
videoDirectSrc = [];
videoTorrentSrc = [];
constructor(
public session: Session,
public analyticsService: AnalyticsService,
private overlayModal: OverlayModalService,
private router: Router,
private location: Location,
private site: SiteService,
private clientMetaService: ClientMetaService,
private featureService: FeaturesService,
@SkipSelf() injector: Injector
) {
this.clientMetaService
.inherit(injector)
.setSource('single')
.setMedium('modal');
}
updateSources() {
this.videoDirectSrc = [
{
res: '720',
uri:
......@@ -129,56 +151,38 @@ export class MediaModalComponent implements OnInit, OnDestroy {
},
{
res: '360',
uri: 'api/v1/media/' + this.entity.entity_guid + '/play?s=modal',
uri: 'api/v1/media/' + this.entity.entity_guid + '/play/?s=modal',
type: 'video/mp4',
},
];
this.videoTorrentSrc = [
{ res: '720', key: this.entity.entity_guid + '/720.mp4' },
{ res: '360', key: this.entity.entity_guid + '/360.mp4' },
];
if (this.entity.custom_data.full_hd) {
sources.push({
this.videoDirectSrc.unshift({
res: '1080',
uri:
'api/v1/media/' + this.entity.entity_guid + '/play?s=modal&res=1080',
'api/v1/media/' +
this.entity.entity_guid +
'/play/' +
Date.now() +
'?s=modal&res=1080',
type: 'video/mp4',
});
}
return sources;
}
get videoTorrentSrc() {
const sources = [
{ res: '720', key: this.entity.entity_guid + '/720.mp4' },
{ res: '360', key: this.entity.entity_guid + '/360.mp4' },
];
if (this.entity.custom_data.full_hd) {
sources.push({ res: '1080', key: this.entity.entity_guid + '/1080.mp4' });
this.videoTorrentSrc.unshift({
res: '1080',
key: this.entity.entity_guid + '/1080.mp4',
});
}
return sources;
}
constructor(
public session: Session,
public analyticsService: AnalyticsService,
private overlayModal: OverlayModalService,
private router: Router,
private location: Location,
private site: SiteService,
private clientMetaService: ClientMetaService,
@SkipSelf() injector: Injector
) {
this.clientMetaService
.inherit(injector)
.setSource('single')
.setMedium('modal');
}
ngOnInit() {
// Prevent dismissal of modal when it's just been opened
this.isOpenTimeout = setTimeout(() => (this.isOpen = true), 20);
switch (this.entity.type) {
case 'activity':
this.title =
......@@ -200,12 +204,41 @@ export class MediaModalComponent implements OnInit, OnDestroy {
? this.entity.custom_data.dimensions.height
: 720;
this.entity.thumbnail_src = this.entity.custom_data.thumbnail_src;
this.updateSources();
break;
case 'batch':
this.contentType = 'image';
this.entity.width = this.entity.custom_data[0].width;
this.entity.height = this.entity.custom_data[0].height;
break;
default:
if (
this.featureService.has('media-modal') &&
this.entity.perma_url &&
this.entity.title &&
!this.entity.entity_guid
) {
this.contentType = 'rich-embed';
this.entity.width = this.entity.custom_data.dimensions
? this.entity.custom_data.dimensions.width
: 1280;
this.entity.height = this.entity.custom_data.dimensions
? this.entity.custom_data.dimensions.height
: 720;
this.entity.thumbnail_src = this.entity.custom_data.thumbnail_src;
break;
} else {
// Modal not implemented, redirect.
this.router.navigate([
this.entity.route
? `/${this.entity.route}`
: `/blog/view/${this.entity.guid}`,
]);
// Close modal.
this.clickedBackdrop(null);
}
}
break;
case 'object':
switch (this.entity.subtype) {
......@@ -213,6 +246,10 @@ export class MediaModalComponent implements OnInit, OnDestroy {
this.contentType = 'video';
this.title = this.entity.title;
this.entity.entity_guid = this.entity.guid;
this.entity.custom_data = {
full_hd: this.entity.flags ? !!this.entity.flags.full_hd : false,
};
this.updateSources();
break;
case 'image':
this.contentType = 'image';
......@@ -243,12 +280,10 @@ export class MediaModalComponent implements OnInit, OnDestroy {
if (this.redirectUrl) {
this.pageUrl = this.redirectUrl;
} else if (this.contentType !== 'blog') {
this.pageUrl = `/media/${this.entity.entity_guid}`;
} else if (this.contentType === 'rich-embed') {
this.pageUrl = `/newsfeed/${this.entity.guid}`;
} else {
this.pageUrl = this.entity.route
? `/${this.entity.route}`
: `/blog/view${this.entity.guid}`;
this.pageUrl = `/media/${this.entity.entity_guid}`;
}
this.boosted = this.entity.boosted || this.entity.p2p_boosted || false;
......@@ -559,7 +594,6 @@ export class MediaModalComponent implements OnInit, OnDestroy {
// Show overlay and video controls
onMouseEnterStage() {
this.overlayVisible = true;
if (this.contentType === 'video') {
// Make sure progress bar seeker is updating when video controls are visible
this.videoComponent.stageHover = true;
......
......@@ -204,7 +204,9 @@
</div>
<minds-button-boost
*ngIf="entity.subtype != 'album'"
*ngIf="
entity.subtype != 'album' && !isScheduled(entity.time_created)
"
class="m-media-content--button-boost"
[object]="entity"
></minds-button-boost>
......
......@@ -239,4 +239,8 @@ export class MediaViewComponent implements OnInit, OnDestroy {
this.cd.markForCheck();
this.cd.detectChanges();
}
isScheduled(time_created) {
return time_created && time_created * 1000 > Date.now();
}
}
import { Component, ViewChild } from '@angular/core';
import { Router } from '@angular/router';
import { timer, Subscription } from 'rxjs';
import { Subscription, timer } from 'rxjs';
import { Client } from '../../../../services/api';
import { Session } from '../../../../services/session';
......@@ -71,8 +71,18 @@ export class MediaTheatreComponent {
@ViewChild(MindsVideoComponent, { static: false })
videoComponent: MindsVideoComponent;
get videoDirectSrc() {
const sources = [
videoDirectSrc = [];
videoTorrentSrc = [];
constructor(
public session: Session,
public client: Client,
public router: Router,
private recommended: RecommendedService
) {}
updateSources() {
this.videoDirectSrc = [
{
res: '720',
uri: 'api/v1/media/' + this.object.guid + '/play?s=modal&res=720',
......@@ -85,40 +95,29 @@ export class MediaTheatreComponent {
},
];
this.videoTorrentSrc = [
{ res: '720', key: this.object.guid + '/720.mp4' },
{ res: '360', key: this.object.guid + '/360.mp4' },
];
if (this.object.flags.full_hd) {
sources.push({
this.videoDirectSrc.unshift({
res: '1080',
uri: 'api/v1/media/' + this.object.guid + '/play?s=modal&res=1080',
type: 'video/mp4',
});
}
return sources;
}
get videoTorrentSrc() {
const sources = [
{ res: '720', key: this.object.guid + '/720.mp4' },
{ res: '360', key: this.object.guid + '/360.mp4' },
];
if (this.object.flags.full_hd) {
sources.push({ res: '1080', key: this.object.guid + '/1080.mp4' });
this.videoTorrentSrc.unshift({
res: '1080',
key: this.object.guid + '/1080.mp4',
});
}
return sources;
}
constructor(
public session: Session,
public client: Client,
public router: Router,
private recommended: RecommendedService
) {}
set _object(value: any) {
if (!value.guid) return;
this.object = value;
this.updateSources();
}
getThumbnail() {
......
......@@ -17,7 +17,9 @@
</ng-container>
<ng-container
*ngIf="error == 'LoginException::Unknown'"
i18n="@@MINDS__LOGIN__EXCEPTION__THERE_HAS_BEEN_AN_ERROR_PLEASE_TRY_AGAIN"
i18n="
@@MINDS__LOGIN__EXCEPTION__THERE_HAS_BEEN_AN_ERROR_PLEASE_TRY_AGAIN
"
>
There's been an error. Please try again
</ng-container>
......
......@@ -10,7 +10,7 @@ m-modal-signup-on-action {
left: 0;
width: 100%;
height: 100%;
z-index: 101;
z-index: 9999990;
}
.m-modal-container {
......
......@@ -2,6 +2,7 @@
[open]="open"
(closed)="onClose($event)"
*ngIf="!session.isLoggedIn() || display != 'initial'"
(click)="$event.stopPropagation()"
>
<div
class="mdl-card__title"
......
......@@ -42,7 +42,9 @@
*ngIf="!payoutRequestInProgress && isPayoutInProgress()"
>
<ng-container
i18n="@@MONETIZATION__AD_SHARING__ANALYTICS__PAYOUT_UNDER_REVIEW_LABEL"
i18n="
@@MONETIZATION__AD_SHARING__ANALYTICS__PAYOUT_UNDER_REVIEW_LABEL
"
>Your payment request for
{{ payouts.amount | currency: 'USD':true }} is currently under review
by Minds staff. Once reviewed, it will be transferred to the bank
......
......@@ -171,7 +171,9 @@
formControlName="state"
type="text"
placeholder="State or province"
i18n-placeholder="@@MONETIZATION__ONBOARDING__STATE_PROVINCE_PLACEHOLDER"
i18n-placeholder="
@@MONETIZATION__ONBOARDING__STATE_PROVINCE_PLACEHOLDER
"
/>
</div>
......
......@@ -93,7 +93,9 @@
</p>
<ng-template #editingCaption>
<p
i18n="@@MONETIZATION__REVENUE__OPTIONS__ENTER_BANK_DETAILS_BELOW_LABEL"
i18n="
@@MONETIZATION__REVENUE__OPTIONS__ENTER_BANK_DETAILS_BELOW_LABEL
"
>
Enter your new bank account details below.
</p>
......@@ -125,7 +127,11 @@
<div class="mdl-cell mdl-cell--12-col">
<label
i18n="@@MONETIZATION__REVENUE__OPTIONS__ROUTING_NUMBER_SORT_CODE_LABEL"
i18n="
@@MONETIZATION__REVENUE__OPTIONS__ROUTING_NUMBER_SORT_CODE_LABEL
"
>Routing Number / Sort Code</label
>
<input formControlName="routingNumber" type="text" />
......
......@@ -2,7 +2,7 @@
<ng-container *mIfFeature="'top-feeds'">
<m-nsfw-selector
[consumer]="true"
(selected)="onNSFWSelected($event)"
(selectedChange)="onNSFWSelected($event)"
></m-nsfw-selector>
</ng-container>
<ng-container *ngIf="showBoostOptions">
......@@ -173,7 +173,9 @@
>
<m-tooltip
icon="help"
i18n="@@MINDS__NEWSFEED__BOOST_ROTATOR__BOOST_VISIBILITY_UPGRADE_TOOLTIP"
i18n="
@@MINDS__NEWSFEED__BOOST_ROTATOR__BOOST_VISIBILITY_UPGRADE_TOOLTIP
"
>
Upgrade to Plus in order to turn off Boost.
</m-tooltip>
......
......@@ -60,7 +60,7 @@
</label>
<ng-container *mIfFeature="'top-feeds'; else oldNSFW">
<m-nsfw-selector (selected)="onNSWFSelections($event)">
<m-nsfw-selector (selectedChange)="onNSWFSelections($event)">
</m-nsfw-selector>
</ng-container>
......
......@@ -266,7 +266,10 @@
session.getLoggedInUser().guid &&
!notification.entityObj.title
"
i18n="object belonging to user@@NOTIFICATIONS__NOTIFICATION__OTHER_OBJECT"
i18n="
object belonging to
user@@NOTIFICATIONS__NOTIFICATION__OTHER_OBJECT
"
>{{ notification.entityObj.ownerObj.name }}'s
{{ notification.entityObj.subtype }}</span
>
......@@ -277,7 +280,10 @@
session.getLoggedInUser().guid &&
!notification.entityObj.title
"
i18n="object belonging to current user@@NOTIFICATIONS__NOTIFICATION__OWN_OBJECT"
i18n="
object belonging to current
user@@NOTIFICATIONS__NOTIFICATION__OWN_OBJECT
"
>your {{ notification.entityObj.subtype }}</span
>
</p>
......@@ -295,7 +301,9 @@
>
<p>
<ng-container
i18n="@@NOTIFICATIONS__NOTIFICATION__REPLIED_TO_GROUP_CONVERSATION"
i18n="
@@NOTIFICATIONS__NOTIFICATION__REPLIED_TO_GROUP_CONVERSATION
"
*ngIf="notification.params?.is_reply"
>
{{ notification.fromObj.name }} replied to your message in
......@@ -347,7 +355,10 @@
<span
class="pseudo-link mdl-color-text--blue-grey-400"
*ngIf="!notification.entityObj.title"
i18n="object belonging to current user@@NOTIFICATIONS__NOTIFICATION__OWN_OBJECT"
i18n="
object belonging to current
user@@NOTIFICATIONS__NOTIFICATION__OWN_OBJECT
"
>your {{ notification.entityObj.subtype }}</span
>
</p>
......@@ -458,7 +469,10 @@
<span
class="pseudo-link mdl-color-text--blue-grey-400"
*ngIf="!notification.entityObj.title"
i18n="object belonging to current user@@NOTIFICATIONS__NOTIFICATION__OWN_OBJECT"
i18n="
object belonging to current
user@@NOTIFICATIONS__NOTIFICATION__OWN_OBJECT
"
>your {{ notification.entityObj.subtype }}</span
>
</p>
......@@ -478,7 +492,9 @@
</p>
<p *ngIf="!notification.entityObj.title">
<ng-container
i18n="@@NOTIFICATIONS__NOTIFICATION__DOWNVOTE__VOTED_DOWN_OWN_ACTIVITY"
i18n="
@@NOTIFICATIONS__NOTIFICATION__DOWNVOTE__VOTED_DOWN_OWN_ACTIVITY
"
>{{ notification.fromObj.name }} down voted
<span class="pseudo-link mdl-color-text--blue-grey-400"
>your activity</span
......@@ -522,7 +538,9 @@
*ngIf="notification.params?.parent?.type == 'group'"
>
<p
i18n="@@NOTIFICATIONS__NOTIFICATION__DOWNVOTE__VOTED_DOWN_CONVERSATION"
i18n="
@@NOTIFICATIONS__NOTIFICATION__DOWNVOTE__VOTED_DOWN_CONVERSATION
"
>
<span class="pseudo-link mdl-color-text--blue-grey-400">{{
notification.fromObj.name
......@@ -568,7 +586,10 @@
<span
class="pseudo-link mdl-color-text--blue-grey-400"
*ngIf="!notification.entityObj.title"
i18n="object belonging to current user@@NOTIFICATIONS__NOTIFICATION__OWN_OBJECT"
i18n="
object belonging to current
user@@NOTIFICATIONS__NOTIFICATION__OWN_OBJECT
"
>your {{ notification.entityObj.subtype }}</span
>
</p>
......@@ -785,7 +806,11 @@
>your channel</span
>
<ng-container
i18n="boost@@NOTIFICATIONS__NOTIFICATION__BOOST_SUBMITTED__AWAITING_APPROVAL"
i18n="
boost@@NOTIFICATIONS__NOTIFICATION__BOOST_SUBMITTED__AWAITING_APPROVAL
"
>
is awaiting approval.</ng-container
>
......@@ -844,7 +869,11 @@
>your channel</span
>
<ng-container
i18n="boost@@NOTIFICATIONS__NOTIFICATION__BOOST_SUBMITTED_P2P__AWAITING_APPROVAL_BY"
i18n="
boost@@NOTIFICATIONS__NOTIFICATION__BOOST_SUBMITTED_P2P__AWAITING_APPROVAL_BY
"
>
is awaiting approval by
<span class="pseudo-link mdl-color-text--blue-grey-400"
......@@ -1279,7 +1308,9 @@
<a [routerLink]="['/boost/console/peer/outbox']">
<p>
<ng-container
i18n="@@NOTIFICATIONS__NOTIFICATION__BOOST_PEER_ACCEPTED__ACCEPTED_BID"
i18n="
@@NOTIFICATIONS__NOTIFICATION__BOOST_PEER_ACCEPTED__ACCEPTED_BID
"
><b>@{{ notification.from.username }}</b> accepted your bid
of</ng-container
>
......@@ -1338,7 +1369,9 @@
<a [routerLink]="['/boost/console/peer/outbox']">
<p>
<ng-container
i18n="@@NOTIFICATIONS__NOTIFICATION__BOOST_PEER_REJECTED__DECLINED_BID"
i18n="
@@NOTIFICATIONS__NOTIFICATION__BOOST_PEER_REJECTED__DECLINED_BID
"
><b>@{{ notification.from.username }}</b> declined your bid
of</ng-container
>
......@@ -1675,7 +1708,11 @@
>
<span
class="pseudo-link mdl-color-text--blue-grey-400"
i18n="@@NOTIFICATIONS__NOTIFICATION__REPORT_ACTIONED__ACTIVITY_TITLE_BEEN"
i18n="
@@NOTIFICATIONS__NOTIFICATION__REPORT_ACTIONED__ACTIVITY_TITLE_BEEN
"
>Your post {{ notification.entityObj.title }}</span
>
has been {{ notification.params.action }}
......@@ -1722,7 +1759,11 @@
<ng-template ngSwitchCase="rewards_summary">
<a target="_blank" routerLink="/wallet/tokens/contributions">
<p
i18n="@@NOTIFICATIONS__NOTIFICATION__REWARDS__YOU_HAVE_RECEIVED_X_TOKENS_TODAY"
i18n="
@@NOTIFICATIONS__NOTIFICATION__REWARDS__YOU_HAVE_RECEIVED_X_TOKENS_TODAY
"
>
You earned {{ notification.params.amount }} tokens today.
</p>
......
......@@ -14,6 +14,11 @@ m-pro--channel {
background-blend-mode: overlay;
background-color: var(--m-pro--more-transparent-background-color) !important;
&.m-pro-channel--plainBackground {
background-blend-mode: initial;
background-color: var(--m-pro--plain-background-color) !important;
}
@media screen and (min-width: ($min-tablet + 1px)) {
m-pro__hamburger-menu {
display: none;
......
......@@ -130,7 +130,7 @@ export class ProChannelComponent implements OnInit, AfterViewInit, OnDestroy {
}
@HostBinding('style.backgroundImage') get backgroundImageCssValue() {
if (!this.channel) {
if (!this.channel || !this.channel.pro_settings.background_image) {
return 'none';
}
......@@ -142,8 +142,16 @@ export class ProChannelComponent implements OnInit, AfterViewInit, OnDestroy {
return '';
}
return `m-theme--wrapper m-theme--wrapper__${this.channel.pro_settings
.scheme || 'light'}`;
const classes = [
'm-theme--wrapper',
`m-theme--wrapper__${this.channel.pro_settings.scheme || 'light'}`,
];
if (!this.channel || !this.channel.pro_settings.background_image) {
classes.push('m-pro-channel--plainBackground');
}
return classes.join(' ');
}
constructor(
......
......@@ -6,7 +6,10 @@
></m-pro--channel--categories>
<div class="m-proChannelHome__section" *ngIf="featuredContent?.length">
<div class="m-proChannelHome__featuredContent">
<div
class="m-proChannelHome__featuredContent"
[class.m-proChannelHome__featuredContent--prominent]="false"
>
<m-pro--channel-tile
*ngFor="let entity of featuredContent"
[entity]="entity"
......
......@@ -56,15 +56,17 @@ m-proChannel__home {
.m-proChannelHome__featuredContent {
grid-template-columns: repeat(2, 1fr);
*:nth-child(1) {
grid-column: span 2;
}
@media screen and (max-width: $max-mobile) {
grid-template-columns: 100%;
}
&.m-proChannelHome__featuredContent--prominent {
*:nth-child(1) {
grid-column: initial;
grid-column: span 2;
@media screen and (max-width: $max-mobile) {
grid-column: initial;
}
}
}
}
......
......@@ -60,9 +60,9 @@ export class ProChannelListComponent implements OnInit, OnDestroy {
const entity = element.getValue();
return (
entity.type === 'group' ||
(!!entity.thumbnail_src ||
!!entity.custom_data ||
(entity.thumbnails && entity.thumbnails.length > 0))
!!entity.thumbnail_src ||
!!entity.custom_data ||
(entity.thumbnails && entity.thumbnails.length > 0)
);
});
})
......
......@@ -15,13 +15,18 @@ import { MindsTitle } from '../../../services/ux/title';
import { SiteService } from '../../../common/services/site.service';
import { debounceTime } from 'rxjs/operators';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
import { FormToastService } from '../../../common/services/form-toast.service';
@Component({
selector: 'm-pro--settings',
selector: 'm-proSettings',
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: 'settings.component.html',
})
export class ProSettingsComponent implements OnInit, OnDestroy {
//OJMTODO remove this
toastIndex: number = 0;
toastMessages = ['rye', 'wheat', '7-grain', 'bagel', 'pumpernickel'];
activeTab: any;
tabs = [
{
......@@ -62,21 +67,14 @@ export class ProSettingsComponent implements OnInit, OnDestroy {
saved: boolean = false;
// currentTab:
// | 'general'
// | 'theme'
// | 'assets'
// | 'hashtags'
// | 'footer'
// | 'domain'
// | 'cancel' = 'general';
user: string | null = null;
isDomainValid: boolean | null = null;
error: string;
saveSuccessful: boolean;
domainValidationSubject: Subject<any> = new Subject<any>();
protected paramMap$: Subscription;
......@@ -99,7 +97,8 @@ export class ProSettingsComponent implements OnInit, OnDestroy {
protected cd: ChangeDetectorRef,
protected title: MindsTitle,
protected site: SiteService,
protected sanitizer: DomSanitizer
protected sanitizer: DomSanitizer,
private formToastService: FormToastService
) {}
ngOnInit() {
......@@ -110,7 +109,6 @@ export class ProSettingsComponent implements OnInit, OnDestroy {
this.param$ = this.route.params.subscribe(params => {
if (this.session.isAdmin()) {
console.log('***', this.route.params);
this.user = params['user'] || null;
}
......@@ -128,6 +126,16 @@ export class ProSettingsComponent implements OnInit, OnDestroy {
this.domainValidation$.unsubscribe();
}
// OJMTODO remove this after testing
tempToast() {
this.formToastService.warn(this.toastMessages[this.toastIndex]);
if (this.toastIndex < 6) {
this.toastIndex++;
} else {
this.toastIndex = 0;
}
}
async load() {
this.inProgress = true;
this.detectChanges();
......@@ -235,8 +243,10 @@ export class ProSettingsComponent implements OnInit, OnDestroy {
this.settings = settings;
await this.service.set(this.settings, this.user);
this.saveSuccessful = true;
} catch (e) {
this.error = e.message;
this.saveSuccessful = false;
}
this.saved = true;
......@@ -252,9 +262,9 @@ export class ProSettingsComponent implements OnInit, OnDestroy {
this.settings.tag_list.push({ label: '', tag: '' });
}
removeTag(index: number) {
this.settings.tag_list.splice(index, 1);
}
// removeTag(index: number) {
// this.settings.tag_list.splice(index, 1);
// }
addBlankFooterLink() {
if (!this.settings) {
......@@ -264,9 +274,9 @@ export class ProSettingsComponent implements OnInit, OnDestroy {
this.settings.footer_links.push({ title: '', href: '' });
}
removeFooterLink(index: number) {
this.settings.footer_links.splice(index, 1);
}
// removeFooterLink(index: number) {
// this.settings.footer_links.splice(index, 1);
// }
detectChanges() {
this.cd.markForCheck();
......
......@@ -53,7 +53,11 @@
<m-tooltip
icon="help"
*ngIf="address.label == 'OnChain & Receiver'"
i18n="@@WALLET__BALANCES__TOKENS__ADDRESS_LABEL_TOOLTIP_ONCHAIN_AND_RECEIVER"
i18n="
@@WALLET__BALANCES__TOKENS__ADDRESS_LABEL_TOOLTIP_ONCHAIN_AND_RECEIVER
"
>
This is your currently active Web3/Metamask wallet and is also
configured to receive Boost & Wires via the blockchain.
......
......@@ -31,7 +31,7 @@
<i class="material-icons">people</i>
<div class="m-token-contributions--chart--contribution-text">
<span>Referrals</span>
<span>+50</span>
<span>+1</span>
</div>
</div>
<div class="m-token-contributions--chart--contribution">
......
......@@ -181,7 +181,9 @@
<ng-container *ngIf="downloadingMetamask">
<b
i18n="@@WALLET__TOKENS__ONBOARDING__ONCHAIN__METAMASK_NOTE_RELOAD"
i18n="
@@WALLET__TOKENS__ONBOARDING__ONCHAIN__METAMASK_NOTE_RELOAD
"
>
Note: After installing and setting up MetaMask you might need to
reload Minds.
......@@ -196,7 +198,10 @@
<button
class="m-btn m-btn--slim m-btn--action"
(click)="downloadMetamask()"
i18n="@@WALLET__TOKENS__ONBOARDING__ONCHAIN__DOWNLOAD_METAMASK_ACTION"
i18n="
@@WALLET__TOKENS__ONBOARDING__ONCHAIN__DOWNLOAD_METAMASK_ACTION
"
>
Download MetaMask
</button>
......@@ -215,7 +220,10 @@
<ng-template
#noProvidedAddress
i18n="@@WALLET__TOKENS__ONBOARDING__ONCHAIN__METAMASK_LOCKED_LEGEND"
i18n="
@@WALLET__TOKENS__ONBOARDING__ONCHAIN__METAMASK_LOCKED_LEGEND
"
>
MetaMask is either locked or connected to another network.
</ng-template>
......
......@@ -8,7 +8,7 @@
If your friend signs up for Minds within 24 hours of clicking the link you
shared with them, they’ll be added to your pending referrals. Once they sign
up for the rewards program by setting up their Minds wallet, the referral is
complete and you’ll <span>both</span> get +50 added to your contribution
complete and you’ll <span>both</span> get +1 added to your contribution
scores! (Hint: check out
<a [routerLink]="['/wallet/tokens/101']">Token 101</a> to learn how
contribution scores are converted into tokens)
......
......@@ -177,7 +177,9 @@
formControlName="state"
type="text"
placeholder="State or province"
i18n-placeholder="@@MONETIZATION__ONBOARDING__STATE_PROVINCE_PLACEHOLDER"
i18n-placeholder="
@@MONETIZATION__ONBOARDING__STATE_PROVINCE_PLACEHOLDER
"
class="m-input"
/>
</div>
......
......@@ -6,7 +6,6 @@
*/
padding: 3px;
height: auto;
line-height: 18px;
cursor: pointer;
@include m-theme() {
color: themed($m-blue);
......@@ -21,6 +20,7 @@
> .ion-icon {
// transform: scale(1.6);
font-size: 18px;
vertical-align: middle;
}
span {
......
......@@ -133,10 +133,10 @@ export class WireChannelComponent {
if (!type) {
return (
isOwner ||
(this.rewards.description ||
(this.rewards.rewards.points && this.rewards.rewards.points.length) ||
(this.rewards.rewards.money && this.rewards.rewards.money.length) ||
(this.rewards.rewards.tokens && this.rewards.rewards.tokens.length))
this.rewards.description ||
(this.rewards.rewards.points && this.rewards.rewards.points.length) ||
(this.rewards.rewards.money && this.rewards.rewards.money.length) ||
(this.rewards.rewards.tokens && this.rewards.rewards.tokens.length)
);
}
......
......@@ -22,9 +22,6 @@ export class FeaturesService {
if (typeof this._features[feature] === 'undefined') {
if (isDevMode() && !this._hasWarned(feature)) {
console.warn(
`[FeaturedService] Feature '${feature}' is not declared. Assuming false.`
);
this._warnedCache[feature] = Date.now();
}
......
......@@ -46,6 +46,7 @@ import { AuthService } from './auth.service';
import { SiteService } from '../common/services/site.service';
import { SessionsStorageService } from './session-storage.service';
import { DiagnosticsService } from './diagnostics.service';
import { FormToastService } from '../common/services/form-toast.service';
export const MINDS_PROVIDERS: any[] = [
SiteService,
......@@ -234,4 +235,5 @@ export const MINDS_PROVIDERS: any[] = [
},
DiagnosticsService,
AuthService,
FormToastService,
];
......@@ -12,7 +12,8 @@ export let clientMock = new (function() {
}
if (
!res ||
((res.status && res.status === 'error') || res.status === 'failed')
(res.status && res.status === 'error') ||
res.status === 'failed'
)
reject(res);
......
......@@ -13,7 +13,8 @@ export let uploadMock = new (function() {
}
if (
!res ||
((res.status && res.status === 'error') || res.status === 'failed')
(res.status && res.status === 'error') ||
res.status === 'failed'
)
reject(res);
......