...
 
Commits (40)
context('Boost Creation', () => {
const duplicateError = "There's already an ongoing boost for this entity";
const postContent = "Test boost, please reject..." + Math.random().toString(36).substring(8);
const nonParticipationError = 'Boost target should participate in the Rewards program.'
before(() => {
cy.server();
})
beforeEach(() => {
cy.getCookie('minds_sess')
.then((sessionCookie) => {
if (sessionCookie === null) {
return cy.login(true);
}
});
cy.visit('/newsfeed/subscriptions');
cy.location('pathname')
.should('eq', `/newsfeed/subscriptions`);
cy.route("GET", '**/api/v2/boost/prepare/**').as('prepare');
cy.route("POST", '**/api/v2/boost/activity/**').as('activity');
cy.route("GET", '**/api/v2/blockchain/wallet/balance*').as('balance');
cy.route("GET", '**/api/v2/search/suggest/**').as('suggest');
});
it('should redirect a user to buy tokens when clicked', () => {
openTopModal();
cy.get('m-boost--creator-payment-methods li h5 span')
.contains('Buy Tokens')
.click();
cy.location('pathname', { timeout: 30000 })
.should('eq', `/token`);
});
it('should allow a user to make an offchain boost for 5000 tokens', () => {
cy.post(postContent);
openTopModal();
cy.get('.m-boost--creator-section-amount input')
.type(5000);
cy.get('m-overlay-modal > div.m-overlay-modal > m-boost--creator button')
.click()
.wait('@prepare').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.deep.equal("success");
}).wait('@activity').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.deep.equal("success");
});
cy.get('.m-overlay-modal')
.should('not.be.visible')
});
it('should error if the boost is a duplicate', () => {
openTopModal();
cy.get('.m-boost--creator-section-amount input')
.type(5000);
cy.get('m-overlay-modal > div.m-overlay-modal > m-boost--creator button')
.click()
.wait('@prepare').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.deep.equal("success");
}).wait('@activity').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.deep.equal("error");
});
cy.get('[data-cy=data-minds-boost-creation-error]')
.contains(duplicateError);
});
it('should display an error if boost offer receiver has not signed up for rewards', () => {
openTopModal();
cy.get('h4')
.contains('Offers')
.click();
cy.get('m-boost--creator-p2p-search .m-boost--creator-wide-input input')
.type("minds").wait('@suggest').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.deep.equal("success");
});
cy.get('.m-boost--creator-autocomplete--results .m-boost--creator-autocomplete--result-content')
.first()
.click({force: true});
cy.get('[data-cy=data-minds-boost-creation-error]')
.contains(nonParticipationError);
});
function openTopModal() {
cy.get('#boost-actions')
.first()
.click()
.wait('@balance').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.deep.equal("success");
});
}
})
context('Channel image upload', () => {
before(() => {
cy.getCookie('minds_sess')
.then((sessionCookie) => {
if (sessionCookie === null) {
return cy.login(true);
}
});
});
beforeEach(()=> {
cy.preserveCookies();
cy.server();
cy.route("POST", "**/api/v1/newsfeed").as("newsfeedPOST");
cy.route("POST", "**/api/v1/media").as("mediaPOST");
});
it('should post an activity with an image attachment', () => {
cy.get('minds-newsfeed-poster').should('be.visible');
cy.get('minds-newsfeed-poster textarea').type('This is a post with an image');
cy.uploadFile('#attachment-input-poster', '../fixtures/international-space-station-1776401_1920.jpg', 'image/jpg');
//upload image
cy.wait('@mediaPOST');
cy.get('.m-posterActionBar__PostButton').click();
//await response for activity
cy.wait('@newsfeedPOST').then((xhr) => {
expect(xhr.status).to.equal(200);
const uploadedImageGuid = xhr.response.body.activity.entity_guid
const activityGuid = xhr.response.body.guid;
cy.get('.minds-list > minds-activity:first-child .message').contains('This is a post with an image');
// assert image
cy.get('.minds-list > minds-activity:first-child .item-image img').should('be.visible');
cy.visit(`/${Cypress.env().username}`);
let mediaHref = `/media/${uploadedImageGuid}`;
cy.get("m-channels--sorted-module[title='Images']")
.find(`a[href='${mediaHref}']`);
cy.get(`[data-minds-activity-guid='${activityGuid}']`)
.find('m-post-menu .minds-more')
.click();
cy.get(`[data-minds-activity-guid='${activityGuid}']`)
.find("li:contains('Delete')")
.click();
cy.get(`[data-minds-activity-guid='${activityGuid}'] m-post-menu m-modal-confirm .mdl-button--colored`).click();
});
});
});
context('Comment Permissions', () => {
const postMenu = 'minds-activity:first > div > m-post-menu';
const deletePostOption = "m-post-menu > ul > li:visible:contains('Delete')";
const deletePostButton = ".m-modal-confirm-buttons > button:contains('Delete')";
before(() => {
//make a post new.
cy.getCookie('minds_sess')
.then((sessionCookie) => {
if (sessionCookie === null) {
return cy.login(true);
}
});
cy.visit('/newsfeed/subscriptions');
cy.location('pathname')
.should('eq', `/newsfeed/subscriptions`);
});
afterEach(() => {
//delete the post
cy.get(postMenu).click();
cy.get(deletePostOption).click();
cy.get(deletePostButton).click();
});
beforeEach(()=> {
cy.preserveCookies();
cy.post('test post');
});
it('should disable comments', () => {
cy.server();
cy.route("POST", "**/api/v2/permissions/comments/**").as("commentToggle");
cy.get(postMenu)
.click()
.find("li:visible:contains('Disable Comments')")
.click();
cy.wait('@commentToggle').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.equal("success");
expect(xhr.response.body.allowed).to.equal(false);
});
//close menu
cy.get(postMenu)
.click();
cy.get('minds-activity:first')
.find("i:contains('speaker_notes_off')")
.click();
});
it('should allow comments', () => {
cy.server();
cy.route("POST", "**/api/v2/permissions/comments/**").as("commentToggle");
cy.get(postMenu)
.click()
.find("li:visible:contains('Disable Comments')")
.click();
cy.wait('@commentToggle').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.equal("success");
expect(xhr.response.body.allowed).to.equal(false);
});
//Menu stays open
cy.get("li:visible:contains('Allow Comments')")
.click();
cy.wait('@commentToggle').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.equal("success");
expect(xhr.response.body.allowed).to.equal(true);
});
//close menu
cy.get(postMenu)
.click();
cy.get('minds-activity:first')
.find("i:contains('chat_bubble')");
});
});
\ No newline at end of file
/**
* @author Ben Hayward
* @desc Spec tests for comment threads.
*/
import generateRandomId from '../support/utilities';
context('Messenger', () => {
const targetUser = 'minds';
const messagePassword = 'Passw0rd!';
const messageContent = 'this is a test message!';
const undecryptedMessage = ''
const testUsername = generateRandomId();
const testPassword = generateRandomId()+'X#';
const openMessenger = '.m-messenger--dockpane-tab';
const userSearch = '.m-messenger--userlist-search > input[type=text]';
const userList = (i) => `.m-messenger--userlist-conversations > div:nth-child(${i}) > span.m-conversation-label`;
const passwordInput = (i) => `input[type=password]:nth-child(${i})`;
const submitPassword = 'm-messenger--encryption > div > button';
const messageInput = '.m-messenger--conversation-composer > textarea';
const sendButton = '[data-cy=data-minds-conversation-send]';
const messageBubble = '.m-messenger--conversation-message-bubble';
const settingsButton = '[data-cy=data-minds-conversation-options]';
const closeButton = '[data-cy=data-minds-conversation-close]';
const destroyButton = '[data-cy=data-minds-conversation-destroy]';
before(() => {
cy.newUser(testUsername, testPassword);
});
beforeEach(() => {
cy.preserveCookies();
cy.server();
cy.route('GET', '**/api/v2/messenger/search?*').as('search');
cy.route('GET', '**/api/v2/messenger/conversations/**').as('conversations');
cy.route('POST', '**/api/v2/messenger/conversations/**').as('send');
cy.route('POST', '**/api/v2/messenger/keys/setup**').as('keys')
cy.get(openMessenger)
.click();
});
afterEach(() => {
cy.get(closeButton)
.click({multiple: true});
cy.get(openMessenger)
.click({multiple: true});
});
after(() => {
cy.deleteUser(testUsername, testPassword);
});
it('should allow a new user to set a password and send a message', () => {
cy.get(userSearch)
.type(Cypress.env().username)
.wait('@search').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.equal("success");
});
cy.get(userList(1))
.click();
cy.get(passwordInput(3))
.type(messagePassword)
cy.get(passwordInput(4))
.type(messagePassword)
cy.get(submitPassword)
.click();
cy.get(messageInput)
.type(messageContent);
cy.get(sendButton)
.click()
.wait('@send').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.equal("success");
});
});
it('should allow a user to destroy their chat content', () => {
cy.get(userSearch)
.clear()
.type(Cypress.env().username)
.wait('@search').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.equal("success");
});
cy.get(userList(1))
.click();
cy.get(passwordInput(3))
.type(messagePassword)
cy.get(passwordInput(4))
.type(messagePassword)
cy.get(submitPassword)
.click()
.wait('@keys').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.equal("success");
})
.wait('@conversations').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.equal("success");
});
cy.get(settingsButton)
.click();
cy.get(destroyButton)
.first()
.click();
cy.get(messageBubble)
.should('not.exist');
});
});
import generateRandomId from '../support/utilities';
context('Registration', () => {
const username = generateRandomId();
const password = `${generateRandomId()}0oA!`;
const email = 'test@minds.com';
const noSymbolPass = 'Passw0rd';
const welcomeText = "Welcome to Minds!";
const passwordDontMatch = "Passwords must match.";
const passwordInvalid = " Password must have more than 8 characters. Including uppercase, numbers, special characters (ie. !,#,@), and cannot have spaces. ";
const usernameField = 'minds-form-register #username';
const emailField = 'minds-form-register #email';
const passwordField = 'minds-form-register #password';
const password2Field = 'minds-form-register #password2';
const checkbox = '[data-cy=data-minds-accept-tos-input]';
const submitButton = 'minds-form-register .mdl-card__actions button';
beforeEach(() => {
cy.visit('/login');
cy.location('pathname').should('eq', '/login');
cy.server();
cy.route("POST", "**/api/v1/register").as("register");
});
after(() => {
cy.visit('/login');
cy.location('pathname').should('eq', '/login');
cy.login(false, username, password);
cy.deleteUser(username, password);
})
it('should allow a user to register', () => {
//type values
cy.get(usernameField)
.focus()
.type(username);
cy.get(emailField)
.focus()
.type(email);
cy.get(passwordField)
.focus()
.type(password);
cy.wait(500);
cy.get(password2Field)
.focus()
.type(password);
cy.get(checkbox)
.click({force: true});
//submit
cy.get(submitButton)
.click()
.wait('@register').then((xhr) => {
expect(xhr.status).to.equal(200);
});
//onboarding modal shown
cy.contains(welcomeText);
});
it('should display an error if password is invalid', () => {
cy.get(usernameField)
.focus()
.type(generateRandomId());
cy.get(emailField)
.focus()
.type(email);
cy.get(passwordField)
.focus()
.type(noSymbolPass);
cy.wait(500);
cy.get(password2Field)
.focus()
.type(noSymbolPass);
cy.get(checkbox)
.click({force: true});
//submit
cy.get(submitButton)
.click()
.wait('@register').then((xhr) => {
expect(xhr.status).to.equal(200);
});
cy.scrollTo('top');
cy.contains(passwordInvalid);
});
it('should display an error if passwords do not match', () => {
cy.get(usernameField)
.focus()
.type(generateRandomId());
cy.get(emailField)
.focus()
.type(email);
cy.get('minds-form-register #password')
.focus()
.type(password);
cy.wait(500);
cy.get(password2Field)
.focus()
.type(password + '!');
cy.get(checkbox)
.click({force: true});
//submit
cy.get(submitButton).click();
cy.scrollTo('top');
cy.contains(passwordDontMatch);
});
})
context('Subscription', () => {
const user = 'minds';
const subscribeButton = 'minds-button-subscribe > button';
const messageButton = 'm-messenger--channel-button > button';
const userDropdown = 'minds-button-user-dropdown > button';
beforeEach(()=> {
cy.login(true);
cy.location('pathname', { timeout: 30000 })
.should('eq', `/newsfeed/subscriptions`);
cy.visit(`/${user}`);
})
it('should allow a user to subscribe to another', () => {
subscribe();
});
it('should allow a user to unsubscribe',() => {
unsubscribe();
})
function subscribe() {
cy.get(subscribeButton).click();
cy.get(messageButton).should('be.visible');
}
function unsubscribe() {
cy.get(userDropdown).click();
cy.contains('Unsubscribe').click();
cy.get(subscribeButton).should('be.visible');
}
});
......@@ -83,6 +83,7 @@ export class SentryErrorHandler implements ErrorHandler {
handleError(error) {
// const eventId = Sentry.captureException(error.originalError || error);
// Sentry.showReportDialog({ eventId });
console.error(error);
}
}
......
......@@ -8,6 +8,8 @@ import {
ChangeDetectorRef,
ComponentRef,
ElementRef,
Injector,
SkipSelf,
} from '@angular/core';
import { DynamicHostDirective } from '../../directives/dynamic-host.directive';
......@@ -20,12 +22,14 @@ import { VideoCard } from '../../../modules/legacy/components/cards/object/video
import { AlbumCard } from '../../../modules/legacy/components/cards/object/album/album';
import { BlogCard } from '../../../modules/blogs/card/card';
import { CommentComponentV2 } from '../../../modules/comments/comment/comment.component';
import { ActivityService } from '../../services/activity.service';
@Component({
selector: 'minds-card',
template: `
<ng-template dynamic-host></ng-template>
`,
providers: [ActivityService],
})
export class MindsCard implements AfterViewInit {
@ViewChild(DynamicHostDirective, { static: true })
......@@ -43,7 +47,10 @@ export class MindsCard implements AfterViewInit {
private initialized: boolean = false;
constructor(private _componentFactoryResolver: ComponentFactoryResolver) {}
constructor(
private _componentFactoryResolver: ComponentFactoryResolver,
private _injector: Injector
) {}
@Input('object') set _object(value: any) {
const oldType = this.type;
......@@ -121,7 +128,11 @@ export class MindsCard implements AfterViewInit {
viewContainerRef.clear();
this.componentRef = viewContainerRef.createComponent(componentFactory);
this.componentRef = viewContainerRef.createComponent(
componentFactory,
undefined,
this._injector
);
this.componentInstance = this.componentRef.instance;
this.anchorRef = viewContainerRef.element;
......
......@@ -206,6 +206,10 @@ export class ButtonsPlugin {
let $buttons = this.$element.querySelector('.medium-insert-buttons');
let $p = this.$element.querySelector('.medium-insert-active');
if (!$buttons) {
return;
}
if ($p !== null) {
let $lastCaption = $p.classList.contains('medium-insert-images-grid')
? []
......
......@@ -197,6 +197,10 @@ export class EmbedImage {
'.' + imgClass
);
if (!image) {
return;
}
const overlay = image.parentElement.querySelector(
'.m-blog--image--in-progress-overlay'
);
......@@ -243,9 +247,7 @@ export class EmbedImage {
if ($image.tagName === 'SPAN') {
$image = $image.parentNode.querySelector('img');
}
if ($image.tagName !== 'IMG') {
} else if ($image.tagName !== 'IMG') {
return;
}
......
......@@ -34,7 +34,7 @@ export class FeaturedContentComponent implements OnInit {
protected componentFactoryResolver: ComponentFactoryResolver,
protected cd: ChangeDetectorRef,
protected clientMetaService: ClientMetaService,
@SkipSelf() injector: Injector
@SkipSelf() protected injector: Injector
) {
this.clientMetaService.inherit(injector).setMedium('featured-content');
}
......@@ -81,7 +81,11 @@ export class FeaturedContentComponent implements OnInit {
const componentRef: ComponentRef<
any
> = this.dynamicHost.viewContainerRef.createComponent(componentFactory);
> = this.dynamicHost.viewContainerRef.createComponent(
componentFactory,
void 0,
this.injector
);
injector.call(this, componentRef, this.entity);
}
}
......
......@@ -198,6 +198,31 @@
Unblock user
</li>
</ng-container>
<!-- ALLOW COMMENTS -->
<ng-container
*ngIf="
featuresService.has('allow-comments-toggle') &&
options.indexOf('allow-comments') !== -1 &&
entity.ownerObj.guid == session.getLoggedInUser().guid
"
>
<li
class="mdl-menu__item"
*ngIf="!entity.allow_comments"
(click)="allowComments(true)"
i18n="@@COMMON__POST_MENU__ALLOW_COMMENTS"
>
Allow Comments
</li>
<li
class="mdl-menu__item"
*ngIf="entity.allow_comments"
(click)="allowComments(false)"
i18n="@@COMMON__POST_MENU__DISABLE_COMMENTS"
>
Disable Comments
</li>
</ng-container>
<!-- ADMIN EDIT FLAGS -->
<ng-container
*ngIf="options.indexOf('set-explicit') !== -1 && session.isAdmin()"
......
......@@ -21,7 +21,11 @@ import { sessionMock } from '../../../../tests/session-mock.spec';
import { FormsModule } from '@angular/forms';
import { RouterTestingModule } from '@angular/router/testing';
import { BlockListService } from '../../services/block-list.service';
import { ActivityService } from '../../services/activity.service';
import { FeaturesService } from '../../../services/features.service';
import { activityServiceMock } from '../../../../tests/activity-service-mock.spec';
import { storageMock } from '../../../../tests/storage-mock.spec';
import { featuresServiceMock } from '../../../../tests/features-service-mock.spec';
/* tslint:disable */
/* Mock section */
......@@ -93,6 +97,8 @@ describe('PostMenuComponent', () => {
{ provide: Client, useValue: clientMock },
{ provide: Session, useValue: sessionMock },
{ provide: OverlayModalService, useValue: overlayModalServiceMock },
{ provide: ActivityService, useValue: activityServiceMock },
{ provide: FeaturesService, useValue: featuresServiceMock },
{ provide: Storage, useValue: storageMock },
{
provide: BlockListService,
......@@ -107,6 +113,7 @@ describe('PostMenuComponent', () => {
// synchronous beforeEach
beforeEach(() => {
featuresServiceMock.mock('allow-comments-toggle', true);
fixture = TestBed.createComponent(PostMenuComponent);
comp = fixture.componentInstance;
......@@ -152,4 +159,25 @@ describe('PostMenuComponent', () => {
'api/v1/block/1'
);
});
it('should allow comments', () => {
spyOn(comp.optionSelected, 'emit');
comp.allowComments(true);
expect(activityServiceMock.toggleAllowComments).toHaveBeenCalledWith(
comp.entity,
true
);
expect(comp.entity.allow_comments).toEqual(true);
});
it('should disable comments', () => {
spyOn(comp.optionSelected, 'emit');
comp.allowComments(false);
expect(activityServiceMock.toggleAllowComments).toHaveBeenCalledWith(
comp.entity,
false
);
expect(comp.entity.allow_comments).toEqual(false);
});
});
......@@ -5,6 +5,7 @@ import {
EventEmitter,
Input,
Output,
OnInit,
} from '@angular/core';
import { Session } from '../../../services/session';
import { OverlayModalService } from '../../../services/ux/overlay-modal';
......@@ -13,6 +14,8 @@ import { ReportCreatorComponent } from '../../../modules/report/creator/creator.
import { MindsUser } from '../../../interfaces/entities';
import { SignupModalService } from '../../../modules/modals/signup/service';
import { BlockListService } from '../../services/block-list.service';
import { ActivityService } from '../../../common/services/activity.service';
import { FeaturesService } from '../../../services/features.service';
import { ShareModalComponent } from '../../../modules/modals/share/share';
type Option =
......@@ -33,7 +36,8 @@ type Option =
| 'subscribe'
| 'unsubscribe'
| 'rating'
| 'block';
| 'block'
| 'allow-comments';
@Component({
moduleId: module.id,
......@@ -41,7 +45,7 @@ type Option =
templateUrl: 'post-menu.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PostMenuComponent {
export class PostMenuComponent implements OnInit {
@Input() entity: any;
@Input() options: Array<Option>;
@Output() optionSelected: EventEmitter<Option> = new EventEmitter<Option>();
......@@ -70,11 +74,15 @@ export class PostMenuComponent {
private cd: ChangeDetectorRef,
private overlayModal: OverlayModalService,
public signupModal: SignupModalService,
protected blockListService: BlockListService
protected blockListService: BlockListService,
protected activityService: ActivityService,
public featuresService: FeaturesService
) {
this.initCategories();
}
ngOnInit() {}
initCategories() {
for (let category in window.Minds.categories) {
this.categories.push({
......@@ -342,6 +350,17 @@ export class PostMenuComponent {
this.entity.nsfw = nsfw;
}
async allowComments(areAllowed: boolean) {
this.entity.allow_comments = areAllowed;
const result = await this.activityService.toggleAllowComments(
this.entity,
areAllowed
);
if (result !== areAllowed) {
this.entity.allow_comments = result;
}
}
openShareModal() {
this.overlayModal
.create(ShareModalComponent, this.entity.url, {
......
import { EventEmitter, Injectable } from '@angular/core';
import { Client } from '../../services/api/client';
import { BehaviorSubject, Observable } from 'rxjs';
@Injectable()
export class ActivityService {
public allowComment$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(
true
);
constructor(private client: Client) {}
public async toggleAllowComments(entity: any, areAllowed: boolean) {
const payload = {
allowed: areAllowed,
};
const oldValue = entity['allow_comments'];
try {
await this.client.post(
`api/v2/permissions/comments/${entity.guid}`,
payload
);
this.allowComment$.next(areAllowed);
return areAllowed;
} catch (ex) {
console.error('Error posting activity comment permissions', ex);
return oldValue;
}
}
}
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { first } from 'rxjs/operators';
import { BehaviorSubject, Observable, of } from 'rxjs';
import { first, catchError } from 'rxjs/operators';
import { Client } from '../../services/api';
import { BlockListService } from './block-list.service';
......@@ -64,8 +64,17 @@ export class EntitiesService {
}
for (const feedItem of feed) {
if (!blockedGuids || blockedGuids.indexOf(feedItem.owner_guid) < 0)
entities.push(this.entities.get(feedItem.urn));
if (
this.entities.has(feedItem.urn) &&
(!blockedGuids || blockedGuids.indexOf(feedItem.owner_guid) < 0)
) {
const entity = this.entities.get(feedItem.urn);
try {
if (await entity.pipe(first()).toPromise()) {
entities.push(entity);
}
} catch (err) {}
}
}
return entities;
......
......@@ -38,7 +38,7 @@
</a>
<a
class="m-topbar--navigation--item"
routerLink="/admin/appeals"
routerLink="/moderation/juryduty/initial"
routerLinkActive="m-topbar--navigation--item-active"
>
<span i18n="@@M__ADMIN_NAV__APPEALS">Appeals</span>
......
......@@ -102,6 +102,7 @@
[object]="boost.entity"
class="mdl-card mdl-shadow--8dp"
*ngIf="boost.entity.type == 'activity'"
[attr.data-minds-activity-guid]="boost.entity.guid"
></minds-activity>
<minds-card-group
[group]="boost.entity"
......
......@@ -19,6 +19,7 @@
*ngIf="entity"
[object]="entity"
class="mdl-card m-border item"
[attr.data-minds-activity-guid]="entity.guid"
></minds-activity>
</div>
<div
......
......@@ -42,6 +42,7 @@
[object]="entity"
class="mdl-card"
*ngIf="entity.type == 'activity'"
[attr.data-minds-activity-guid]="entity.guid"
></minds-activity>
<div class="mdl-card__supporting-text m-action-buttons">
......
......@@ -6,6 +6,7 @@ import { WireRewardsStruc } from '../modules/wire/interfaces/wire.interfaces';
export interface MindsActivityObject {
activity: Array<any>;
pinned: Array<any>;
allow_comments: boolean;
}
export interface MindsBlogEntity {
......@@ -27,6 +28,7 @@ export interface MindsBlogEntity {
time_published?: number;
access_id?: number;
license?: string;
allow_comments: boolean;
}
export interface Message {}
......
///<reference path="../../../../../node_modules/@types/jasmine/index.d.ts"/>
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import {
Component,
EventEmitter,
Input,
Output,
Pipe,
PipeTransform,
NO_ERRORS_SCHEMA,
} from '@angular/core';
import { RouterTestingModule } from '@angular/router/testing';
import { CommonModule as NgCommonModule } from '@angular/common';
import { MindsBlogEntity } from '../../../interfaces/entities';
import { BlogView } from './view';
import { SafePipe } from '../../../common/pipes/safe';
import { Client } from '../../../services/api/client';
import { clientMock } from '../../../../tests/client-mock.spec';
import { sessionMock } from '../../../../tests/session-mock.spec';
import { Session } from '../../../services/session';
import { scrollServiceMock } from '../../../../tests/scroll-service-mock.spec';
import { ScrollService } from '../../../services/ux/scroll';
import { mindsTitleMock } from '../../../mocks/services/ux/minds-title.service.mock.spec';
import { MindsTitle } from '../../../services/ux/title';
import { AttachmentService } from '../../../services/attachment';
import { attachmentServiceMock } from '../../../../tests/attachment-service-mock.spec';
import { contextServiceMock } from '../../../../tests/context-service-mock.spec';
import { ContextService } from '../../../services/context.service';
import { AnalyticsService } from '../../../services/analytics';
import { analyticsServiceMock } from '../../../../tests/analytics-service-mock.spec';
import { ActivityService } from '../../../common/services/activity.service';
import { activityServiceMock } from '../../../../tests/activity-service-mock.spec';
describe('Blog view component', () => {
let comp: BlogView;
let fixture: ComponentFixture<BlogView>;
const blog: MindsBlogEntity = {
guid: '1',
title: 'test blog',
description: 'description',
ownerObj: {},
allow_comments: true,
};
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [BlogView, SafePipe], // declare the test component
imports: [NgCommonModule, RouterTestingModule],
providers: [
{ provide: ActivityService, useValue: activityServiceMock },
{ provide: AnalyticsService, useValue: analyticsServiceMock },
{ provide: AttachmentService, useValue: attachmentServiceMock },
{ provide: Client, useValue: clientMock },
{ provide: ContextService, useValue: contextServiceMock },
{ provide: MindsTitle, useValue: mindsTitleMock },
{ provide: ScrollService, useValue: scrollServiceMock },
{ provide: Session, useValue: sessionMock },
],
schemas: [NO_ERRORS_SCHEMA],
})
.overrideProvider(ActivityService, { useValue: activityServiceMock })
.compileComponents(); // compile template and css
}));
// synchronous beforeEach
beforeEach(() => {
fixture = TestBed.createComponent(BlogView);
comp = fixture.componentInstance;
comp.blog = blog;
fixture.detectChanges();
});
});
import { Component, ElementRef, ViewChild } from '@angular/core';
import {
Component,
ElementRef,
ViewChild,
ChangeDetectorRef,
OnInit,
OnDestroy,
} from '@angular/core';
import { Router } from '@angular/router';
import { Client } from '../../../services/api';
......@@ -12,6 +19,7 @@ import { AttachmentService } from '../../../services/attachment';
import { ContextService } from '../../../services/context.service';
import { optimizedResize } from '../../../utils/optimized-resize';
import { OverlayModalService } from '../../../services/ux/overlay-modal';
import { ActivityService } from '../../../common/services/activity.service';
import { ShareModalComponent } from '../../../modules/modals/share/share';
@Component({
......@@ -22,8 +30,9 @@ import { ShareModalComponent } from '../../../modules/modals/share/share';
class: 'm-blog',
},
templateUrl: 'view.html',
providers: [ActivityService],
})
export class BlogView {
export class BlogView implements OnInit, OnDestroy {
minds;
guid: string;
blog: MindsBlogEntity;
......@@ -50,6 +59,7 @@ export class BlogView {
'set-explicit',
'remove-explicit',
'rating',
'allow-comments',
];
@ViewChild('lockScreen', { read: ElementRef, static: false }) lockScreen;
......@@ -65,6 +75,8 @@ export class BlogView {
private context: ContextService,
public analytics: AnalyticsService,
public analyticsService: AnalyticsService,
protected activityService: ActivityService,
private cd: ChangeDetectorRef,
private overlayModal: OverlayModalService
) {
this.minds = window.Minds;
......@@ -78,7 +90,7 @@ export class BlogView {
}
isVisible() {
//listens every 0.6 seconds
// listens every 0.6 seconds
this.scroll_listener = this.scroll.listen(
e => {
const bounds = this.element.getBoundingClientRect();
......@@ -132,7 +144,9 @@ export class BlogView {
}
ngOnDestroy() {
if (this.scroll_listener) this.scroll.unListen(this.scroll_listener);
if (this.scroll_listener) {
this.scroll.unListen(this.scroll_listener);
}
}
menuOptionSelected(option: string) {
......@@ -165,7 +179,9 @@ export class BlogView {
}
calculateLockScreenHeight() {
if (!this.lockScreen) return;
if (!this.lockScreen) {
return;
}
const lockScreenOverlay = this.lockScreen.nativeElement.querySelector(
'.m-wire--lock-screen'
);
......
......@@ -10,12 +10,13 @@
<ng-container *ngIf="type == 'newsfeed' || type == 'offers'">
<ng-container>
<div class="m-boost-console--booster--posts-list">
<minds-card
[object]="entity | async"
class="m-border"
hostClass="mdl-card"
*ngFor="let entity of feed$ | async"
></minds-card>
<ng-container *ngFor="let entity of feed$ | async">
<minds-card
[object]="entity | async"
class="m-border"
hostClass="mdl-card"
></minds-card>
</ng-container>
</div>
</ng-container>
......@@ -47,11 +48,13 @@
class="mdl-cell mdl-cell--6-col"
*ngFor="let entity of feed$ | async"
>
<minds-card
[object]="entity | async"
hostClass="mdl-shadow--2dp"
></minds-card>
<minds-button type="boost" [object]="entity | async"></minds-button>
<ng-container *ngIf="(entity | async)?.thumbnail_src">
<minds-card
[object]="entity | async"
hostClass="mdl-shadow--2dp"
></minds-card>
<minds-button type="boost" [object]="entity | async"></minds-button>
</ng-container>
</div>
</div>
</ng-container>
......
......@@ -370,7 +370,7 @@
class="m-boost--creator--submit-error"
>
<i class="material-icons">close</i>
<span>{{ error }}</span>
<span data-cy="data-minds-boost-creation-error">{{ error }}</span>
</div>
</div>
</section>
......
......@@ -20,6 +20,7 @@
class="mdl-card m-border item"
(delete)="delete(activity)"
[slot]="i + 1"
[attr.data-minds-activity-guid]="activity.guid"
></minds-activity>
<minds-activity
*ngFor="let activity of feed; let i = index"
......@@ -28,6 +29,7 @@
class="mdl-card m-border item"
(delete)="delete(activity)"
[slot]="i + (pinned?.length || 0) + 1"
[attr.data-minds-activity-guid]="activity.guid"
></minds-activity>
<infinite-scroll
distance="25%"
......
......@@ -385,7 +385,7 @@
<span *ngIf="comment.replies_count > 0"
>{{ comment.replies_count }} Replies</span
>
<span *ngIf="comment.replies_count <= 0">Reply</span>
<span *ngIf="comment.replies_count <= 0 && canReply">Reply</span>
</span>
</div>
......
......@@ -86,6 +86,7 @@ export class CommentComponent implements OnChanges {
@ViewChild('batchImage', { static: false }) batchImage: ElementRef;
@Input() canEdit: boolean = false;
@Input() canReply = true;
@Output() onReply = new EventEmitter();
......
......@@ -384,11 +384,24 @@
(click)="toggleReplies()"
*ngIf="comment.can_reply"
>
<i class="material-icons">reply</i>
<i
*ngIf="
comment.replies_count > 0 ||
(activityService.allowComment$ | async)
"
class="material-icons"
>reply</i
>
<span *ngIf="comment.replies_count > 0"
>{{ comment.replies_count }} Replies</span
>
<span *ngIf="comment.replies_count <= 0">Reply</span>
<span
*ngIf="
comment.replies_count <= 0 &&
(activityService.allowComment$ | async)
"
>Reply</span
>
</span>
</div>
......
......@@ -10,6 +10,9 @@ import {
Input,
ViewChild,
ElementRef,
OnInit,
OnDestroy,
AfterViewInit,
} from '@angular/core';
import { Session } from '../../../services/session';
......@@ -21,8 +24,9 @@ import { OverlayModalService } from '../../../services/ux/overlay-modal';
import { ReportCreatorComponent } from '../../report/creator/creator.component';
import { CommentsListComponent } from '../list/list.component';
import { TimeDiffService } from '../../../services/timediff.service';
import { Observable } from 'rxjs';
import { Observable, Subscription } from 'rxjs';
import { map } from 'rxjs/operators';
import { ActivityService } from '../../../common/services/activity.service';
import { Router } from '@angular/router';
import { FeaturesService } from '../../../services/features.service';
import { MindsVideoComponent } from '../../media/components/video/video.component';
......@@ -45,7 +49,8 @@ import isMobile from '../../../helpers/is-mobile';
},
],
})
export class CommentComponentV2 implements OnChanges {
export class CommentComponentV2
implements OnChanges, OnInit, OnDestroy, AfterViewInit {
comment: any;
editing: boolean = false;
minds = window.Minds;
......@@ -78,6 +83,7 @@ export class CommentComponentV2 implements OnChanges {
translateToggle: boolean = false;
commentAge$: Observable<number>;
canReply = true;
videoDimensions: Array<any> = null;
@ViewChild('player', { static: false }) player: MindsVideoComponent;
@ViewChild('batchImage', { static: false }) batchImage: ElementRef;
......@@ -98,6 +104,7 @@ export class CommentComponentV2 implements OnChanges {
private timeDiffService: TimeDiffService,
private el: ElementRef,
private router: Router,
protected activityService: ActivityService,
protected featuresService: FeaturesService
) {}
......@@ -118,9 +125,13 @@ export class CommentComponentV2 implements OnChanges {
}
}
ngOnDestroy() {}
@Input('comment')
set _comment(value: any) {
if (!value) return;
if (!value) {
return;
}
this.comment = value;
this.attachment.load(this.comment);
......@@ -147,7 +158,7 @@ export class CommentComponentV2 implements OnChanges {
return;
}
let data = this.attachment.exportMeta();
const data = this.attachment.exportMeta();
data['comment'] = this.comment.description;
this.editing = false;
......
......@@ -49,7 +49,8 @@
!ascendingInProgress &&
!error &&
comments?.length === 0 &&
parent.type == 'activity'
parent.type == 'activity' &&
activityService.allowComment$
"
i18n="@@MINDS__COMMENTS__START_CONVERSATION"
>
......
......@@ -7,6 +7,8 @@ import {
Input,
Renderer,
ViewChild,
OnInit,
OnDestroy,
} from '@angular/core';
import { Client } from '../../../services/api/client';
......@@ -15,6 +17,7 @@ import { Upload } from '../../../services/api/upload';
import { AttachmentService } from '../../../services/attachment';
import { Textarea } from '../../../common/components/editors/textarea.component';
import { SocketsService } from '../../../services/sockets';
import { ActivityService } from '../../../common/services/activity.service';
@Component({
moduleId: module.id,
......@@ -33,10 +36,11 @@ import { SocketsService } from '../../../services/sockets';
useFactory: AttachmentService._,
deps: [Session, Client, Upload],
},
ActivityService,
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CommentsListComponent {
export class CommentsListComponent implements OnInit, OnDestroy {
minds;
object;
guid: string = '';
......@@ -71,7 +75,6 @@ export class CommentsListComponent {
socketSubscriptions: any = {
comment: null,
};
error: string;
@Input() conversation: boolean = false;
......@@ -91,7 +94,8 @@ export class CommentsListComponent {
public attachment: AttachmentService,
public sockets: SocketsService,
private renderer: Renderer,
private cd: ChangeDetectorRef
private cd: ChangeDetectorRef,
public activityService: ActivityService
) {
this.minds = window.Minds;
}
......@@ -99,13 +103,18 @@ export class CommentsListComponent {
set _object(value: any) {
this.object = value;
this.guid = this.object.guid;
if (this.object.entity_guid) this.guid = this.object.entity_guid;
if (this.object.entity_guid) {
this.guid = this.object.entity_guid;
}
this.parent = this.object;
}
set _reversed(value: boolean) {
if (value) this.reversed = true;
else this.reversed = false;
if (value) {
this.reversed = true;
} else {
this.reversed = false;
}
}
ngOnInit() {
......@@ -165,7 +174,7 @@ export class CommentsListComponent {
} else {
this.ascendingInProgress = false;
}
//this.moreDescendingData = true;
// this.moreDescendingData = true;
if (!response.comments) {
if (descending) {
......@@ -178,8 +187,8 @@ export class CommentsListComponent {
return false;
}
let el = this.scrollView.nativeElement;
let previousScrollHeightMinusTop = el.scrollHeight - el.scrollTop;
const el = this.scrollView.nativeElement;
const previousScrollHeightMinusTop = el.scrollHeight - el.scrollTop;
if (descending) {
this.comments = response.comments.concat(this.comments);
......@@ -250,7 +259,7 @@ export class CommentsListComponent {
this.overscrollTimer = setTimeout(() => {
if (this.overscrollAmount < -75) {
//75px
// 75px
this.autoloadPrevious();
}
......@@ -319,13 +328,14 @@ export class CommentsListComponent {
}
// if the list is scrolled to the bottom
let scrolledToBottom =
const scrolledToBottom =
this.scrollView.nativeElement.scrollTop +
this.scrollView.nativeElement.clientHeight >=
this.scrollView.nativeElement.scrollHeight;
if (response.comments[0]._guid == guid)
if (response.comments[0]._guid == guid) {
this.comments.push(response.comments[0]);
}
this.detectChanges();
......@@ -352,14 +362,14 @@ export class CommentsListComponent {
) {
return;
}
let key = 'thumbs:' + direction + ':count';
const key = 'thumbs:' + direction + ':count';
for (let i = 0; i < this.comments.length; i++) {
if (this.comments[i]._guid == guid) {
this.comments[i][key]++;
this.detectChanges();
}
}
//this.comments = this.comments.slice(0);
// this.comments = this.comments.slice(0);
this.detectChanges();
});
......@@ -415,11 +425,11 @@ export class CommentsListComponent {
this.content = this.content.trim();
let data = this.attachment.exportMeta();
const data = this.attachment.exportMeta();
data['comment'] = this.content;
data['parent_path'] = this.parent.child_path || '0:0:0';
let newLength = this.comments.push({
const newLength = this.comments.push({
// Optimistic
description: this.content,
guid: 0,
......@@ -438,7 +448,7 @@ export class CommentsListComponent {
this.commentsScrollEmitter.emit('bottom');
try {
let response: any = await this.client.post(
const response: any = await this.client.post(
'api/v1/comments/' + this.guid,
data
);
......
......@@ -45,7 +45,8 @@
!inProgress &&
!error &&
comments?.length === 0 &&
parent.type == 'activity'
parent.type == 'activity' &&
(activityService.allowComment$ | async)
"
i18n="@@MINDS__COMMENTS__START_CONVERSATION"
>
......@@ -112,6 +113,7 @@
</div>
<m-comment__poster
*ngIf="activityService.allowComment$ | async"
[guid]="guid"
[parent]="parent"
[entity]="entity"
......
......@@ -8,6 +8,8 @@ import {
Output,
Renderer,
ViewChild,
OnInit,
OnDestroy,
} from '@angular/core';
import { Client } from '../../../services/api/client';
......@@ -18,6 +20,9 @@ import { Textarea } from '../../../common/components/editors/textarea.component'
import { SocketsService } from '../../../services/sockets';
import { CommentsService } from '../comments.service';
import { BlockListService } from '../../../common/services/block-list.service';
import { ActivityService } from '../../../common/services/activity.service';
import { Subscription } from 'rxjs';
import { TouchSequence } from 'selenium-webdriver';
@Component({
selector: 'm-comments__thread',
......@@ -25,7 +30,7 @@ import { BlockListService } from '../../../common/services/block-list.service';
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [CommentsService],
})
export class CommentsThreadComponent {
export class CommentsThreadComponent implements OnInit {
minds;
@Input() parent;
@Input() entity;
......@@ -67,7 +72,8 @@ export class CommentsThreadComponent {
public sockets: SocketsService,
private renderer: Renderer,
protected blockListService: BlockListService,
private cd: ChangeDetectorRef
private cd: ChangeDetectorRef,
public activityService: ActivityService
) {
this.minds = window.Minds;
}
......@@ -198,7 +204,7 @@ export class CommentsThreadComponent {
const parent_path = this.parent.child_path || '0:0:0';
let scrolledToBottom =
const scrolledToBottom =
this.scrollView.nativeElement.scrollTop +
this.scrollView.nativeElement.clientHeight >=
this.scrollView.nativeElement.scrollHeight;
......@@ -289,8 +295,6 @@ export class CommentsThreadComponent {
}
onPosted({ comment, index }) {
console.log('onPosted called');
console.log(comment, index);
this.comments[index] = comment;
this.detectChanges();
}
......
......@@ -7,6 +7,8 @@ import {
Input,
Output,
Renderer,
OnInit,
OnDestroy,
} from '@angular/core';
import {
ActivatedRoute,
......@@ -38,7 +40,7 @@ import { CommentsService } from '../comments.service';
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CommentsTreeComponent {
export class CommentsTreeComponent implements OnInit, OnDestroy {
minds;
entity;
guid: string = '';
......
......@@ -125,6 +125,7 @@
type="checkbox"
class="mdl-checkbox__input"
formControlName="tos"
data-cy="data-minds-accept-tos-input"
/>
<span
......
......@@ -39,6 +39,7 @@
[showRatingToggle]="true"
[slot]="i + 1"
class="mdl-card item"
[attr.data-minds-activity-guid]="activity.guid"
>
</minds-activity>
......@@ -49,6 +50,7 @@
[canDelete]="group['is:owner'] || group['is:moderator']"
(delete)="delete(a)"
[slot]="i + (pinned?.length || 0) + 1"
[attr.data-minds-activity-guid]="a.guid"
>
<!-- Menu Actions -->
<li
......
......@@ -52,6 +52,7 @@
[canDelete]="group['is:owner'] || group['is:moderator']"
(delete)="delete(entity)"
[slot]="i + 1"
[attr.data-minds-activity-guid]="entity.guid"
>
<!-- Menu Actions -->
......
......@@ -22,10 +22,12 @@ import { HashtagsSelectorComponent } from '../../hashtags/selector/selector.comp
import { VideoChatService } from '../../videochat/videochat.service';
import { UpdateMarkersService } from '../../../common/services/update-markers.service';
import { filter, map, startWith, throttle } from 'rxjs/operators';
import { ActivityService } from '../../../common/services/activity.service';
@Component({
selector: 'm-groups--profile',
templateUrl: 'profile.html',
providers: [ActivityService],
})
export class GroupsProfile {
guid;
......
......@@ -8,6 +8,7 @@
[canDelete]="group['is:owner'] || group['is:moderator']"
(delete)="delete(entity)"
[hideTabs]="true"
[attr.data-minds-activity-guid]="entity.guid"
>
<!-- Menu Actions -->
......
......@@ -4,5 +4,6 @@
*ngFor="let post of posts; let i = index"
class="mdl-card item"
[object]="post"
[attr.data-minds-activity-guid]="post.guid"
></minds-activity>
</div>
import { Component, ChangeDetectionStrategy } from '@angular/core';
import {
Component,
ChangeDetectionStrategy,
ChangeDetectorRef,
OnInit,
OnDestroy,
} from '@angular/core';
import { Client } from '../../../../services/api';
import { ActivityService } from '../../../../common/services/activity.service';
@Component({
selector: 'minds-button-comment',
inputs: ['_object: object'],
changeDetection: ChangeDetectionStrategy.OnPush,
changeDetection: ChangeDetectionStrategy.Default,
template: `
<a [ngClass]="{ selected: object['comments:count'] > 0 }">
<i class="material-icons">chat_bubble</i>
<i
class="material-icons"
*ngIf="(activityService.allowComment$ | async) === true"
>chat_bubble</i
>
<i
class="material-icons"
*ngIf="(activityService.allowComment$ | async) === false"
title="Comments have been disabled for this post"
i18n-title="@@COMMENTS__DISABLED"
>
speaker_notes_off
</i>
<span class="minds-counter" *ngIf="object['comments:count'] > 0">{{
object['comments:count'] | number
}}</span>
</a>
`,
})
export class CommentButton {
export class CommentButton implements OnInit, OnDestroy {
object;
constructor(public client: Client) {}
constructor(
public client: Client,
public activityService: ActivityService,
protected cd: ChangeDetectorRef
) {}
ngOnInit() {}
ngOnDestroy() {}
set _object(value: any) {
this.object = value;
this.activityService.allowComment$.next(this.object.allow_comments);
}
}
......@@ -27,6 +27,7 @@ import { ActivityAnalyticsOnViewService } from './activity-analytics-on-view.ser
import { NewsfeedService } from '../../../../newsfeed/services/newsfeed.service';
import { ClientMetaService } from '../../../../../common/services/client-meta.service';
import { AutocompleteSuggestionsService } from '../../../../suggestions/services/autocomplete-suggestions.service';
import { ActivityService } from '../../../../../common/services/activity.service';
import { FeaturesService } from '../../../../../services/features.service';
import isMobile from '../../../../../helpers/is-mobile';
......@@ -45,7 +46,11 @@ import isMobile from '../../../../../helpers/is-mobile';
'showRatingToggle',
],
outputs: ['_delete: delete', 'commentsOpened', 'onViewed'],
providers: [ClientMetaService, ActivityAnalyticsOnViewService],
providers: [
ClientMetaService,
ActivityAnalyticsOnViewService,
ActivityService,
],
templateUrl: 'activity.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
......@@ -60,6 +65,7 @@ export class Activity implements OnInit {
translateToggle: boolean = false;
translateEvent: EventEmitter<any> = new EventEmitter();
showBoostOptions: boolean = false;
allowComments = true;
@Input() boost: boolean = false;
@Input('boost-toggle')
@Input()
......@@ -114,6 +120,7 @@ export class Activity implements OnInit {
'set-explicit',
'block',
'rating',
'allow-comments',
];
} else {
return [
......@@ -127,6 +134,7 @@ export class Activity implements OnInit {
'set-explicit',
'block',
'rating',
'allow-comments',
];
}
} else {
......@@ -140,6 +148,7 @@ export class Activity implements OnInit {
'set-explicit',
'block',
'rating',
'allow-comments',
];
}
}
......@@ -164,6 +173,7 @@ export class Activity implements OnInit {
protected clientMetaService: ClientMetaService,
protected featuresService: FeaturesService,
public suggestions: AutocompleteSuggestionsService,
protected activityService: ActivityService,
@SkipSelf() injector: Injector,
elementRef: ElementRef
) {
......@@ -227,6 +237,8 @@ export class Activity implements OnInit {
this.activity.time_created =
this.activity.time_created || Math.floor(Date.now() / 1000);
this.allowComments = this.activity.allow_comments;
}
getOwnerIconTime() {
......@@ -308,6 +320,9 @@ export class Activity implements OnInit {
}*/
openComments() {
if (!this.shouldShowComments()) {
return;
}
this.commentsToggle = !this.commentsToggle;
this.commentsOpened.emit(this.commentsToggle);
}
......@@ -406,10 +421,11 @@ export class Activity implements OnInit {
this.translateToggle = true;
break;
}
this.detectChanges();
}
setExplicit(value: boolean) {
let oldValue = this.activity.mature,
const oldValue = this.activity.mature,
oldMatureVisibility = this.activity.mature_visibility;
this.activity.mature = value;
......@@ -511,6 +527,10 @@ export class Activity implements OnInit {
this.activity.mature_visibility = !this.activity.mature_visibility;
}
shouldShowComments() {
return this.activity.allow_comments || this.activity['comments:count'] >= 0;
}
setVideoDimensions($event) {
this.videoDimensions = $event.dimensions;
this.activity.custom_data.dimensions = this.videoDimensions;
......
......@@ -9,6 +9,7 @@ import { Client } from '../../../../../services/api';
import { Session } from '../../../../../services/session';
import { AttachmentService } from '../../../../../services/attachment';
import { ActivityService } from '../../../../../common/services/activity.service';
@Component({
moduleId: module.id,
......@@ -18,6 +19,7 @@ import { AttachmentService } from '../../../../../services/attachment';
host: {
class: 'mdl-shadow--8dp',
},
providers: [ActivityService],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ActivityPreview {
......
......@@ -14,6 +14,7 @@ import { Router } from '@angular/router';
import { Client } from '../../../../../services/api';
import { Session } from '../../../../../services/session';
import { AttachmentService } from '../../../../../services/attachment';
import { ActivityService } from '../../../../../common/services/activity.service';
import { OverlayModalService } from '../../../../../services/ux/overlay-modal';
import { MediaModalComponent } from '../../../../media/modal/modal.component';
import { FeaturesService } from '../../../../../services/features.service';
......@@ -23,6 +24,7 @@ import isMobile from '../../../../../helpers/is-mobile';
moduleId: module.id,
selector: 'minds-remind',
inputs: ['object', '_events: events'],
providers: [ActivityService],
templateUrl: '../activity/activity.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
......
import { Component, EventEmitter } from '@angular/core';
import { Client } from '../../../services/api';
@Component({
<<<<<<< a596794609983ebbce246788506c54ac2ace58a8
selector: 'm-channel--carousel',
inputs: ['_banners: banners', '_editMode: editMode'],
=======
selector: 'minds-carousel',
inputs: ['_banners: banners', '_editMode: editMode', 'blurred'],
>>>>>>> (feat): mark a channel profile explicit
outputs: ['done_event: done', 'delete_event: delete'],
template: `
<i class="material-icons left" (click)="prev()" [hidden]="banners.length <= 1">keyboard_arrow_left</i>
<div *ngFor="let banner of banners; let i = index">
<minds-banner
[src]="banner.src"
[top]="banner.top_offset"
[overlay]="true"
[ngClass]="{'is-hidden': i != index, 'edit-mode': editing, 'm-mature-banner': blurred}"
[editMode]="editing"
[done]="done"
(added)="added($event, i)"
></minds-banner>
<div class="delete-button" (click)="delete(i)" [hidden]="i != index || !editing">
<button class="mdl-button mdl-button--raised mdl-button--colored material-icons">X</button>
</div>
</div>
<i class="material-icons right" (click)="next()" [hidden]="banners.length <= 1">keyboard_arrow_right</i>
`
})
export class CarouselComponent {
minds: Minds = window.Minds;
banners: Array<any> = [];
editing: boolean = false;
src: string = '';
modified: Array<any> = []; //all banners should be exported to here on the done event, and sent to parent
done_event = new EventEmitter();
delete_event = new EventEmitter();
done: boolean = false; //if set to true, tells the child component to return "added"
rotate: boolean = true; //if set to true enabled rotation
rotate_timeout; //the timeout for the rotator
interval: number = 3000; //the interval for each banner to stay before rotating
index: number = 0; //the current visible index of the carousel.
blurred: boolean = false;
constructor() {
this.run();
}
/**
* A list of banners are sent from the parent, if done are sent a blank one is entered
*/
set _banners(value: any) {
if (value) {
this.banners = value;
} else {
this.banners.push({
src: null
});
}
}
/**
* If the parent set edit mode
*/
set _editMode(value: boolean) {
console.log('[carousel]: edit mode event received');
//was in edit more, now settings not in edit more
if (this.editing && !value) {
console.log('[carousel]: edit mode ended');
this._done();
return;
}
this.editing = value;
if (!this.editing) {
return;
}
console.log('[carousel]: edit mode enabled');
this.rotate = false;
this.done = false;
var blank_banner = false;
for (var i in this.banners) {
if (!this.banners[i].src)
blank_banner = true;
}
if (!blank_banner) {
this.banners.push({
src: null
});
}
}
/**
* Fired when the child component adds a new banner
*/
added(value: any, index) {
console.log(this.banners[index].guid, value.file);
if (!this.banners[index].guid && !value.file)
return; //this is our 'add new' post
//detect if we have changed
var changed = false;
if (value.top !== this.banners[index].top)
changed = false;
if (value.file)
changed = true;
if (!changed)
return;
if (!this.banners[index].src) {
this.banners[index].src = value.file;
}
this.modified.push({
guid: this.banners[index].guid,
index: index,
file: value.file,
top: value.top
});
}
delete(index) {
this.delete_event.next(this.banners[index]);
this.banners.splice(index, 1);
if (this.banners.length === 0) {
this.banners.push({ src: null });
}
this.next();
}
/**
* Once we retreive all the modified banners, we fire back to the parent the new list
*/
_done() {
this.editing = false; //this should update each banner (I'd prefer even driven but change detection works..)
this.done = true;
console.log('[carousel]: received done event');
//after one second?
setTimeout(() => {
this.done_event.next(this.modified);
this.modified = [];
let blank_banner: any = false;
for (var i in this.banners) {
if (!this.banners[i].src)
blank_banner = i;
}
if (blank_banner !== false) {
this.banners.splice(blank_banner, 1);
this.next();
}
}, 1000);
}
prev() {
var max = this.banners.length - 1;
if (this.index === 0)
this.index = max;
else
this.index--;
this.run();//resets the carousel
}
next() {
var max = this.banners.length - 1;
if (this.index >= max)
this.index = 0;
else
this.index++;
this.run();//resets the carousel
}
run() {
if (this.rotate_timeout)
clearTimeout(this.rotate_timeout);
this.rotate_timeout = setTimeout(() => {
if (this.rotate) {
var max = this.banners.length - 1;
if (this.index >= max)
this.index = 0;
else
this.index++;
}
this.run();
}, this.interval);
}
ngOnDestroy() {
clearTimeout(this.rotate_timeout);
}
}
......@@ -21,6 +21,7 @@ import { OverlayModalService } from '../../../services/ux/overlay-modal';
import { AnalyticsService } from '../../../services/analytics';
import { MindsVideoComponent } from '../components/video/video.component';
import isMobileOrTablet from '../../../helpers/is-mobile-or-tablet';
import { ActivityService } from '../../../common/services/activity.service';
@Component({
selector: 'm-media--modal',
......@@ -51,6 +52,7 @@ import isMobileOrTablet from '../../../helpers/is-mobile-or-tablet';
transition(':leave', [animate('300ms', style({ opacity: 0 }))]),
]),
],
providers: [ActivityService],
})
export class MediaModalComponent implements OnInit, OnDestroy {
minds = window.Minds;
......@@ -122,7 +124,7 @@ export class MediaModalComponent implements OnInit, OnDestroy {
this.entity.title ||
`${this.entity.ownerObj.name}'s post`;
this.entity.guid = this.entity.entity_guid || this.entity.guid;
this.thumbnail = `${this.minds.cdn_url}fs/v1/thumbnail/${this.entity.entity_guid}/xlarge`;
this.thumbnail = this.entity.thumbnails.xlarge;
switch (this.entity.custom_type) {
case 'video':
this.contentType = 'video';
......@@ -139,7 +141,8 @@ export class MediaModalComponent implements OnInit, OnDestroy {
break;
case 'image':
this.contentType = 'image';
this.thumbnail = `${this.minds.cdn_url}fs/v1/thumbnail/${this.entity.guid}/xlarge`;
// this.thumbnail = `${this.minds.cdn_url}fs/v1/thumbnail/${this.entity.guid}/xlarge`;
this.thumbnail = this.entity.thumbnail;
break;
case 'blog':
this.contentType = 'blog';
......@@ -158,7 +161,8 @@ export class MediaModalComponent implements OnInit, OnDestroy {
`${this.entity.ownerObj.name}'s post`;
this.entity.guid = this.entity.attachment_guid;
this.entity.entity_guid = this.entity.attachment_guid;
this.thumbnail = `${this.minds.cdn_url}fs/v1/thumbnail/${this.entity.attachment_guid}/xlarge`;
// this.thumbnail = `${this.minds.cdn_url}fs/v1/thumbnail/${this.entity.attachment_guid}/xlarge`;
this.thumbnail = this.entity.thumbnails.xlarge;
break;
}
......
......@@ -275,7 +275,7 @@
<!-- Don't show comments for albums -->
<div
class="mdl-grid m-media-content--comments mdl-color--white"
*ngIf="entity.guid && entity.subtype != 'album'"
*ngIf="canShowComments() || (activityService.allowComment$ | async)"
>
<m-comments__tree [entity]="entity"> </m-comments__tree>
</div>
......
import { ChangeDetectorRef, Component } from '@angular/core';
import { ChangeDetectorRef, Component, OnInit, OnDestroy } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Subscription } from 'rxjs';
......@@ -10,6 +10,7 @@ import { RecommendedService } from '../components/video/recommended.service';
import { AttachmentService } from '../../../services/attachment';
import { ContextService } from '../../../services/context.service';
import { MindsTitle } from '../../../services/ux/title';
import { ActivityService } from '../../../common/services/activity.service';
@Component({
moduleId: module.id,
......@@ -21,9 +22,10 @@ import { MindsTitle } from '../../../services/ux/title';
useFactory: RecommendedService._,
deps: [Client],
},
ActivityService,
],
})
export class MediaViewComponent {
export class MediaViewComponent implements OnInit, OnDestroy {
minds = window.Minds;
guid: string;
entity: any = {};
......@@ -32,6 +34,7 @@ export class MediaViewComponent {
deleteToggle: boolean = false;
theaterMode: boolean = false;
allowComments = true;
menuOptions: Array<string> = [
'edit',
......@@ -43,6 +46,8 @@ export class MediaViewComponent {
'subscribe',
'remove-explicit',
'rating',
'allow-comments',
'disable-comments',
];
paramsSubscription: Subscription;
......@@ -57,7 +62,8 @@ export class MediaViewComponent {
public route: ActivatedRoute,
public attachment: AttachmentService,
public context: ContextService,
private cd: ChangeDetectorRef
private cd: ChangeDetectorRef,
protected activityService: ActivityService
) {}
ngOnInit() {
......@@ -100,7 +106,7 @@ export class MediaViewComponent {
}
if (response.entity) {
this.entity = response.entity;
this.allowComments = this.entity['allow_comments'];
switch (this.entity.subtype) {
case 'video':
this.context.set('object:video');
......@@ -174,6 +180,14 @@ export class MediaViewComponent {
case 'remove-explicit':
this.setExplicit(false);
break;
case 'allow-comments':
this.entity.allow_comments = true;
this.activityService.toggleAllowComments(this.entity, true);
break;
case 'disable-comments':
this.entity.allow_comments = false;
this.activityService.toggleAllowComments(this.entity, false);
break;
}
}
......@@ -191,6 +205,17 @@ export class MediaViewComponent {
});
}
canShowComments() {
if (!this.entity.guid) {
return false;
}
//Don't show comments on albums
if (this.entity.subtype === 'album') {
return false;
}
return this.entity['comments:count'] >= 1;
}
private detectChanges() {
this.cd.markForCheck();
this.cd.detectChanges();
......
......@@ -90,13 +90,14 @@ export class MediaTheatreComponent {
}
getThumbnail() {
const url =
this.object.paywalled ||
(this.object.wire_threshold && this.object.wire_threshold !== '0')
? this.minds.site_url
: this.minds.cdn_url;
return url + `fs/v1/thumbnail/${this.object.guid}/xlarge`;
// const url =
// this.object.paywalled ||
// (this.object.wire_threshold && this.object.wire_threshold !== '0')
// ? this.minds.site_url
// : this.minds.cdn_url;
// return url + `fs/v1/thumbnail/${this.object.guid}/xlarge`;
return this.object.thumbnail_src;
}
prev() {
......
......@@ -19,8 +19,16 @@
<i class="material-icons mdl-color-text--blue-grey-100" [hidden]="live"
>sync_problem</i
>
<i class="material-icons" (click)="ribbonToggle()">more_vert</i>
<i class="material-icons" (click)="dockpanes.close(conversation)"
<i
class="material-icons"
(click)="ribbonToggle()"
data-cy="data-minds-conversation-options"
>more_vert</i
>
<i
class="material-icons"
(click)="dockpanes.close(conversation)"
data-cy="data-minds-conversation-close"
>close</i
>
</div>
......@@ -35,6 +43,7 @@
<div
class="m-messenger--dockpane-tab-icon mdl-color-text--blue-grey-300"
(click)="deleteHistory(); ribbonOpened = false"
data-cy="data-minds-conversation-destroy"
>
<i
class="material-icons mdl-color-text--blue-grey-100"
......@@ -244,6 +253,7 @@
<i
class="material-icons mdl-color-text--blue-grey-600"
(click)="send($event); emoji.close()"
data-cy="data-minds-conversation-send"
>send_arrow</i
>
<minds-emoji [localDirective]="emoji"></minds-emoji>
......
......@@ -41,8 +41,10 @@
</p>
<p>In order to install this version you must:</p>
<ul>
<li>Be running a minimum of Android 5 (Lollipop).</li>
<li>
Update Phone settings to "enable downloads from unverified source"
Update Phone settings to "Allow installation of apps from unknown
sources".
</li>
<li>Download and install!</li>
</ul>
......
......@@ -16,11 +16,13 @@
<a
class="m-page--sidebar--navigation--item"
routerLink="/wallet/usd/earnings"
routerLink="/wallet/usd/transactions"
routerLinkActive="m-page--sidebar--navigation--item-active"
>
<i class="material-icons">history</i>
<span i18n="@@MONETIZATION__REVENUE__CONSOLE__EARNINGS">Earnings</span>
<span i18n="@@MONETIZATION__REVENUE__CONSOLE__EARNINGS"
>Transactions</span
>
</a>
<a
......@@ -50,6 +52,7 @@
</i>
<span>Back to the Token Wallet</span>
</a>
<m-walletUsd__balance></m-walletUsd__balance>
<router-outlet></router-outlet>
</div>
</div>
<div class="m-revenue--options m-border">
<div class="m-revenue--options-requirements" *ngIf="account.requirement">
<h3>Account Requirements</h3>
<p>
You are required to complete the following action in order to receive
payouts:
<ng-container
*ngIf="account.requirement === 'individual.verification.document'"
>
<b (click)="file.click()">Upload Photo ID</b>
<br />
<br />
<button (click)="file.click()" class="m-btn m-btn--action m-btn--slim">
<ng-container *ngIf="!editing">Select & Upload</ng-container>
<ng-container *ngIf="editing">Uploading...</ng-container>
</button>
<input
type="file"
#file
name="file"
(change)="uploadRequirement(file)"
accept="image/*"
style="display: none;"
/>
</ng-container>
<ng-container *ngIf="account.requirement.indexOf('tos_acceptance.') > -1">
<b>
Accept the
<a href="https://stripe.com/legal" target="_blank">
Stripe Services Agreement
</a>
</b>
<br />
<br />
<button (click)="acceptTos()" class="m-btn m-btn--action m-btn--slim">
<ng-container *ngIf="!editing">Accept Terms</ng-container>
<ng-container *ngIf="editing">Accepting...</ng-container>
</button>
</ng-container>
</p>
</div>
<div class="m-revenue--options-payout-method">
<h3 i18n="@@MONETIZATION__REVENUE__OPTIONS__PAYOUT_METHOD_TITLE">
Payout Method
......
......@@ -102,8 +102,12 @@
border-radius: 2px;
}
.m-revenue--options-cancel {
.m-revenue--options-cancel,
.m-revenue--options-requirements {
margin-top: 32px;
&.m-revenue--options-requirements {
margin-top: 0;
}
padding: 16px;
h3 {
......
......@@ -3,7 +3,7 @@ import { ActivatedRoute, Router } from '@angular/router';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ChartColumn } from '../../../common/components/chart/chart.component';
import { Client } from '../../../services/api';
import { Client, Upload } from '../../../services/api';
@Component({
moduleId: module.id,
......@@ -18,12 +18,14 @@ export class RevenueOptionsComponent {
account: null,
country: 'US',
};
account;
error: string = '';
leaving: boolean = false;
leaveError: string = '';
constructor(
private client: Client,
private upload: Upload,
private cd: ChangeDetectorRef,
private fb: FormBuilder,
private router: Router
......@@ -42,6 +44,7 @@ export class RevenueOptionsComponent {
this.inProgress = true;
this.client.get('api/v2/payments/stripe/connect').then(({ account }) => {
this.inProgress = false;
this.account = account;
this.payoutMethod.country = account.country;
this.form.controls.country.setValue(account.country);
if (account.bankAccount.last4) {
......@@ -97,6 +100,25 @@ export class RevenueOptionsComponent {
this.detectChanges();
}
async uploadRequirement(fileInput: HTMLInputElement) {
const file = fileInput ? fileInput.files[0] : null;
this.editing = true;
this.detectChanges();
await this.upload.post('api/v2/payments/stripe/connect/photoid', [file]);
this.editing = false;
this.account = null;
this.getSettings();
}
async acceptTos() {
this.editing = true;
this.detectChanges();
await this.client.put('api/v2/payments/stripe/connect/terms');
this.editing = false;
this.account = null;
this.getSettings();
}
detectChanges() {
this.cd.markForCheck();
this.cd.detectChanges();
......
......@@ -48,6 +48,7 @@
[showBoostMenuOptions]="true"
[slot]="i + 1"
[visibilityEvents]="false"
[attr.data-minds-activity-guid]="boost.guid"
#activities
></minds-activity>
</ng-container>
......@@ -17,6 +17,7 @@
(delete)="delete(activity)"
[slot]="i + 1"
class="mdl-card m-border item"
[attr.data-minds-activity-guid]="activity.guid"
>
</minds-activity>
</ng-container>
......
......@@ -14,6 +14,7 @@
[showRatingToggle]="true"
[slot]="slot"
class="mdl-card m-border item"
[attr.data-minds-activity-guid]="entity.guid"
></minds-activity>
</ng-container>
</ng-container>
......@@ -21,6 +21,7 @@
(delete)="delete(preActivity)"
[showRatingToggle]="true"
class="mdl-card m-border item"
[attr.minds-data-activity-guid]="preActivity.guid"
></minds-activity>
<m-newsfeed--boost-rotator
......@@ -46,6 +47,7 @@
(delete)="delete(activity)"
[showRatingToggle]="true"
[slot]="i + 1"
[attr.data-minds-activity-guid]="activity.guid"
></minds-activity>
</ng-container>
......
......@@ -7,6 +7,7 @@
[showRatingToggle]="true"
class="mdl-card m-border item"
[slot]="i + 1"
[attr.data-minds-activity-guid]="activity.guid"
></minds-activity>
<infinite-scroll
distance="25%"
......
......@@ -9,6 +9,7 @@
[showRatingToggle]="true"
[slot]="i + 1"
class="mdl-card m-border item"
[attr.data-minds-activity-guid]="activity.guid"
></minds-activity>
<infinite-scroll
distance="25%"
......
......@@ -36,6 +36,7 @@
[showRatingToggle]="true"
[slot]="1"
class="mdl-card mdl-shadow--2dp item"
[attr.data-minds-activity-guid]="activity.guid"
>
</minds-activity>
</div>
......
......@@ -45,7 +45,6 @@ m-notifications--flyout {
max-height: calc(95vh - 200px);
overflow-y: scroll;
padding: 0;
white-space: pre-line;
.mdl-cell--12-col {
padding: 0;
......
......@@ -3,12 +3,16 @@ import { Client } from '../../services/api';
import { SocketsService } from '../../services/sockets';
import { Session } from '../../services/session';
import { MindsTitle } from '../../services/ux/title';
import { Subscription, timer } from 'rxjs';
export class NotificationService {
socketSubscriptions: any = {
notification: null,
};
onReceive: EventEmitter<any> = new EventEmitter();
notificationPollTimer;
private updateNotificationCountSubscription: Subscription;
static _(
session: Session,
......@@ -71,22 +75,26 @@ export class NotificationService {
* Return the notifications
*/
getNotifications() {
var self = this;
setInterval(function() {
// console.log('getting notifications');
const pollIntervalSeconds = 60;
this.notificationPollTimer = timer(0, pollIntervalSeconds * 1000);
this.updateNotificationCountSubscription = this.notificationPollTimer.subscribe(
() => this.updateNotificationCount()
);
}
if (!self.session.isLoggedIn()) return;
updateNotificationCount() {
if (!this.session.isLoggedIn()) {
return;
}
if (!window.Minds.notifications_count)
window.Minds.notifications_count = 0;
if (!window.Minds.notifications_count) {
window.Minds.notifications_count = 0;
}
self.client
.get('api/v1/notifications/count', {})
.then((response: any) => {
window.Minds.notifications_count = response.count;
self.sync();
});
}, 60000);
this.client.get('api/v1/notifications/count', {}).then((response: any) => {
window.Minds.notifications_count = response.count;
this.sync();
});
}
/**
......@@ -101,4 +109,8 @@ export class NotificationService {
}
this.title.setCounter(window.Minds.notifications_count);
}
ngOnDestroy() {
this.notificationPollTimer.unsubscribe();
}
}
......@@ -32,10 +32,16 @@
<a href="https://santaclaraprinciples.org/" target="_blank"
>Santa Clara Principles</a
>
to review appeals. The jury consists of 12 unique, active users whose
objective is to vote on appeals. If 75% or more agree with the appeal, the
administrative action is overturned. For more information about the jury
system,
to review appeals. A Jury consists of 12 randomly selected unique active
Minds users who are not subscribed to the user under review. Each juror
selected is provided with the option to participate, pass or opt-out of
the Jury pool entirely. If 75% or more of the Jury members vote to accept
the appeal of a strike, the administrative action is overturned.
</p>
<p>
If less than 75% of the Jury members vote accept the appeal, the
administrative action is upheld. The decision of the Jury will be final.
For more information about the jury system,
<a
href="https://www.minds.com/minds/blog/power-to-the-people-the-minds-jury-system-975486713993859072"
target="_blank"
......@@ -76,6 +82,25 @@
<li>Strike 3 = Ban</li>
</ul>
<p>
NSFW (not safe for work) is defined as content containing nudity,
pornography, profanity, violence, gore, or sensitive commentary on race,
religion, or gender. In general terms, it is defined as content which a
reasonable viewer may not want to be seen accessing, in a public setting,
such as in a workplace. These tags can be applied to individual content or
any group or channel. The full channel will not be marked with a NSFW
category until it has received 3 strikes in a single NSFW category.
</p>
<p>
Spam on Minds is generally defined as repeated, unwanted, and/or
unsolicited actions, automated or manual, that negatively affect Minds
users, groups, and/or Minds itself. Spam also includes content that is
designed to further unlawful acts (such as phishing) or mislead recipients
as to the source of the material (such as spoofing). Spam may result in an
immediate ban if determined to be malicious or by use of a bot.
</p>
<p>
Spam may result in an immediate ban if determined to be malicious or by
use of a bot.
......
......@@ -53,6 +53,17 @@
>
</a>
<a
class="m-page--sidebar--navigation--item"
routerLink="/settings/tiers"
routerLinkActive="m-page--sidebar--navigation--item-active"
>
<i class="material-icons">card_giftcard</i>
<span i18n="@@SETTINGS__NAVIGATION__SUBSCRIPTIONS_NAV"
>Subscription Tiers</span
>
</a>
<a
class="m-page--sidebar--navigation--item"
routerLink="/settings/billing"
......
......@@ -26,6 +26,7 @@ import { SettingsWireComponent } from './wire/wire.component';
import { WireModule } from '../wire/wire.module';
import { SettingsP2PMediaComponent } from './p2pmedia/p2pmedia.component';
import { SettingsBlockedChannelsComponent } from './blocked-channels/blocked-channels.component';
import { SettingsTiersComponent } from './tiers/tiers.component';
const settingsRoutes: Routes = [
{
......@@ -43,6 +44,7 @@ const settingsRoutes: Routes = [
{ path: 'reported-content', component: SettingsReportedContentComponent },
{ path: 'p2pmedia', component: SettingsP2PMediaComponent },
{ path: 'blocked-channels', component: SettingsBlockedChannelsComponent },
{ path: 'tiers', component: SettingsTiersComponent },
],
},
];
......@@ -76,6 +78,7 @@ const settingsRoutes: Routes = [
SettingsWireComponent,
SettingsP2PMediaComponent,
SettingsBlockedChannelsComponent,
SettingsTiersComponent,
],
providers: [SettingsService],
exports: [
......
<div class="m-settings--section m-layout__row m-border">
<div
class="minds-error mdl-color--red mdl-color-text--white"
[hidden]="!error"
>
{{ error }}
</div>
<div class="m-layout__spacer"></div>
<button
class="m-btn m-btn--slim m-btn--action"
*ngIf="!isSaving && !isSaved"
(click)="save()"
>
<ng-container i18n="@@M__ACTION__SAVE">Save</ng-container>
</button>
<button class="m-btn m-btn--slim" *ngIf="isSaved">
<ng-container i18n="@@M__COMMON__SAVED">Saved</ng-container>
</button>
<div
id="p2"
class="mdl-progress mdl-js-progress mdl-progress__indeterminate"
[hidden]="!isSaving"
></div>
</div>
<div class="m-border m-settings--section">
<h4>Subscription Tiers</h4>
<p>
Create tiers below to incentivise supporters to wire you. These tiers will
be displayed on the sidebar of your channel page on in the wire screen.
</p>
<m-wire__subscriptionTiers
[user]="session.getLoggedInUser()"
[editing]="true"
(isSaving)="isSaving = $event"
(isSaved)="isSaved = $event"
[saveEventListener]="triggerSave"
>
</m-wire__subscriptionTiers>
</div>
m-settings__tiers .m-settings--section {
display: block;
@include m-theme() {
background-color: themed($m-white);
}
textarea {
padding: 16px;
width: 100%;
}
m-wire-channel-table {
display: block;
margin-top: $minds-margin * 2;
table {
width: 100%;
.m-wire-channel--reward-amount {
display: inline-flex;
width: 100%;
}
}
}
}
import { Component, EventEmitter } from '@angular/core';
import { Session } from '../../../services/session';
import { Client } from '../../../services/api';
@Component({
selector: 'm-settings__tiers',
templateUrl: 'tiers.component.html',
})
export class SettingsTiersComponent {
isSaving = false;
isSaved = false;
triggerSave = new EventEmitter(true);
error: string;
constructor(public session: Session, public client: Client) {}
save() {
this.triggerSave.next(true);
}
}
<div class="m-walletUsd__balance m-border">
<div class="m-layout__row">
<div class="m-layout__cell">
<label>Future Payouts</label>
<span>{{ balance | number }} {{ currency | uppercase }}</span>
</div>
<div class="m-layout__spacer"></div>
<div class="m-layout_cell">
<label>Payout Schedule:</label>
<span>{{ interval | titlecase }}</span>
<m-tooltip icon="help" i18n="@@MINDS__WALLET__USD__PAYOUT__SCHEDULE">
<ng-container *ngIf="interval === 'daily'">
Daily payouts are sent {{ delay }} days after a payment has been
processed
</ng-container>
<ng-container *ngIf="interval === 'monthly'">
Monthly payouts will be issued on the {{ anchor }} day of the month
</ng-container>
</m-tooltip>
</div>
</div>
</div>
.m-walletUsd__balance {
padding: $minds-padding * 2;
margin-bottom: $minds-margin * 2;
@include m-theme() {
background-color: themed($m-white);
}
label {
font-weight: 600;
}
span {
margin-left: 8px;
}
m-tooltip {
vertical-align: middle;
i {
font-size: 16px;
color: #666;
margin-left: 4px;
}
}
}
import { Component, ViewChild, ComponentFactoryResolver } from '@angular/core';
import { RevenueLedgerComponent } from '../../monetization/revenue/ledger.component';
import { Client } from '../../../services/api/client';
@Component({
selector: 'm-walletUsd__balance',
templateUrl: './balance.component.html',
})
export class WalletUSDBalanceComponent {
balance: number;
currency: string;
interval: string;
delay: number;
anchor: number;
constructor(private client: Client) {}
ngOnInit() {
this.load();
}
async load() {
const { account } = <any>(
await this.client.get('api/v2/payments/stripe/connect')
);
this.balance =
(account.totalBalance.amount + account.pendingBalance.amount) / 100;
this.currency = account.totalBalance.currency;
this.interval = account.payoutInterval;
this.delay = account.payoutDelay;
this.anchor = account.payoutAnchor;
}
}
<table class="m-walletUsdTransactions__table m-border">
<tr>
<td>Date</td>
<td>User</td>
<td>Gross</td>
<td>Net</td>
</tr>
<tr *ngFor="let transaction of transactions">
<td>{{ transaction.timestamp * 1000 | date: 'medium' }}</td>
<td>
<a
*ngIf="transaction.customer_user"
[routerLink]="['/', transaction.customer_user.username]"
>
<span>@</span>{{ transaction.customer_user.username }}
</a>
</td>
<td>{{ transaction.gross / 100 | number: '1.2-2' }} USD</td>
<td>
{{ transaction.net / 100 | number: '1.2-2' }}
{{ transaction.currency | uppercase }}
</td>
</tr>
</table>
.m-walletUsdTransactions__table {
width: 100%;
@include m-theme() {
background-color: themed($m-white);
}
> tr:first-of-type() {
font-weight: bold;
}
td {
padding: $minds-padding * 2;
font-size: 13px;
@include m-theme() {
border-bottom: 1px solid themed($m-grey-50);
}
}
}
import { Component, ViewChild, ComponentFactoryResolver } from '@angular/core';
import { Router } from '@angular/router';
import { DynamicHostDirective } from '../../../common/directives/dynamic-host.directive';
import { RevenueLedgerComponent } from '../../monetization/revenue/ledger.component';
import { Session } from '../../../services/session';
import { Client } from '../../../services/api/client';
@Component({
selector: 'm-walletUsd__transactions',
templateUrl: './transactions.component.html',
})
export class WalletUSDTransactionsComponent {
transactions = [];
constructor(
private router: Router,
private session: Session,
private client: Client
) {}
ngOnInit() {
this.load();
}
async load() {
const { transactions } = <any>(
await this.client.get('api/v2/payments/stripe/transactions')
);
this.transactions = transactions;
}
}
......@@ -33,6 +33,7 @@ import { WalletBalanceTokensComponent } from './balances/tokens/balance.componen
import { WalletBalanceRewardsComponent } from './balances/rewards/balance.component';
import { WalletUSDComponent } from './usd/usd.component';
import { WalletUSDEarningsComponent } from './usd/earnings.component';
import { WalletUSDTransactionsComponent } from './usd/transactions.component';
import { WalletUSDPayoutsComponent } from './usd/payouts.component';
import { WalletUSDSettingsComponent } from './usd/settings.component';
import { WalletUSDOnboardingComponent } from './usd/onboarding/onboarding.component';
......@@ -47,6 +48,7 @@ import { ModalsModule } from '../modals/modals.module';
import { WalletTokenTestnetComponent } from './tokens/testnet/testnet.component';
import { ReferralsModule } from './tokens/referrals/referrals.module';
import { ReferralsComponent } from './tokens/referrals/referrals.component';
import { WalletUSDBalanceComponent } from './usd/balance.component';
const walletRoutes: Routes = [
{
......@@ -85,7 +87,8 @@ const walletRoutes: Routes = [
path: 'usd',
component: WalletUSDComponent,
children: [
{ path: '', redirectTo: 'earnings', pathMatch: 'full' },
{ path: '', redirectTo: 'transactions', pathMatch: 'full' },
{ path: 'transactions', component: WalletUSDTransactionsComponent },
{ path: 'earnings', component: WalletUSDEarningsComponent },
{ path: 'payouts', component: WalletUSDPayoutsComponent },
{ path: 'settings', component: WalletUSDSettingsComponent },
......@@ -139,6 +142,7 @@ const walletRoutes: Routes = [
WalletBalanceRewardsComponent,
WalletUSDComponent,
WalletUSDEarningsComponent,
WalletUSDTransactionsComponent,
WalletUSDPayoutsComponent,
WalletUSDSettingsComponent,
WalletUSDOnboardingComponent,
......@@ -148,6 +152,7 @@ const walletRoutes: Routes = [
WalletTokenContributionsChartComponent,
WalletToken101Component,
WalletTokenTestnetComponent,
WalletUSDBalanceComponent,
],
exports: [
WalletComponent,
......@@ -160,6 +165,7 @@ const walletRoutes: Routes = [
WalletFlyoutComponent,
WalletBalanceUSDComponent,
WalletBalanceTokensComponent,
WalletUSDBalanceComponent,
],
entryComponents: [WalletComponent, WalletUSDTermsComponent],
})
......
......@@ -34,38 +34,8 @@
</span>
</div>
<div
class="mdl-card__supporting-text mdl-color-text--grey-600 m-wire-channel--rewards"
>
<p
*ngIf="!editing && rewards.description"
class="m-wire-channel--description"
[innerText]="rewards.description"
></p>
<textarea
*ngIf="editing"
class="m-wire-channel--description-editor"
[(ngModel)]="rewards.description"
[autoGrow]
placeholder="About your offerings. eg. Describe what your supporters will receive…"
i18n-placeholder="@@WIRE__CHANNEL__REWARD_DESCRIPTION_PLACEHOLDER"
></textarea>
<m-wire-channel-table
type="tokens"
[(rewards)]="rewards.rewards.tokens"
[(editing)]="editing"
[channel]="channel"
></m-wire-channel-table>
<p
class="m-wire-channel--disclaimer"
*ngIf="false"
i18n="@@WIRE__CHANNEL__REWARD_CHANGE_NOTICE"
>
<span>@</span>{{ channel.username }} reserves the right to change rewards
at anytime without notice
</p>
</div>
<m-wire__subscriptionTiers
[user]="channel"
[editing]="editing"
></m-wire__subscriptionTiers>
</div>
......@@ -86,7 +86,8 @@ m-wire-channel {
.m-wire-channel--description-editor {
line-height: 1.3;
letter-spacing: 0.5px;
margin: 0 0 ($minds-padding * 2);
margin: 0;
padding: $minds-padding * 2;
}
.m-wire-channel--description {
......
<table
class="m-wire-channel--rewards"
[ngClass]="['m-wire-channel--rewards-' + type]"
[class.m-wire-channel--rewards--is-editing]="editing"
cellspacing="0"
cellspacing="0"
>
......@@ -8,7 +9,8 @@
*ngFor="let reward of rewards; let i = index"
(click)="openWireModal(reward)"
>
<td>
<td [class.m-border]="editing">
<label *ngIf="editing">Threshold</label>
<div class="m-wire-channel--reward-amount">
<span *ngIf="!editing && type == 'money'"
>{{ reward.amount || 0 | currency: 'USD':true:'1.0-0' }}+</span
......@@ -30,6 +32,7 @@
[ngModel]="reward.amount"
(ngModelChange)="setAmount(i, $event)"
[placeholder]="getAmountPlaceholder()"
class="m-input"
/>
<ng-container *ngIf="type == 'points'" i18n="@@M__COMMON__POINTS_SUFFIX"
>points</ng-container
......@@ -43,26 +46,29 @@
<div class="m-wire-channel--reward-description">
<p *ngIf="!editing">{{ reward.description }}</p>
<textarea
*ngIf="editing"
[ngModel]="reward.description"
(ngModelChange)="setDescription(i, $event)"
class="m-border"
[autoGrow]
placeholder="Describe the reward"
i18n-placeholder="@@WIRE__CHANNEL__TABLE__REWARD_PLACEHOLDER"
>
</textarea>
<ng-container *ngIf="editing">
<label>Description</label>
<textarea
*ngIf="editing"
[ngModel]="reward.description"
(ngModelChange)="setDescription(i, $event)"
class="m-border"
[autoGrow]
placeholder="Describe the reward"
i18n-placeholder="@@WIRE__CHANNEL__TABLE__REWARD_PLACEHOLDER"
>
</textarea>
</ng-container>
</div>
</td>
</tr>
</table>
<div
class="m-wire-channel--rewards--add-tier m-border"
class="m-wire-channel--rewards--add-tier"
*ngIf="session.getLoggedInUser().guid == channel.guid"
>
<a (click)="addTier()">
<a (click)="addTier()" class="m-btn m-btn--action m-btn--slim">
<i class="material-icons">playlist_add</i>
<span i18n="@@WIRE__CHANNEL__TABLE__ADD_REWARD_ACTION">Add Reward</span>
</a>
......
.m-wire-channel--rewards > tr > td {
padding: $minds-padding * 2;
margin-bottom: $minds-margin * 2;
label {
font-weight: 600;
font-size: 13px;
line-height: 26px;
}
}
.m-wire-channel--reward-amount {
display: inline-flex;
align-items: center;
.m-input {
flex: 1;
border-radius: 24px;
}
> span {
flex: 1;
}
}
.m-wire-channel--rewards--is-editing {
.m-wire-channel--rewards {
padding: $minds-padding $minds-padding * 2;
}
.m-wire-channel--reward-amount > span {
margin-left: $minds-margin * 2;
}
}
.m-wire-channel--reward-description {
margin-top: $minds-margin * 2;
}
.m-wire-channel--rewards--add-tier {
line-height: 56px;
.m-btn > * {
vertical-align: middle;
}
}
<div class="m-wire-channel--rewards">
<p
*ngIf="!editing && rewards.description"
class="m-wire-channel--description"
[innerText]="rewards.description"
></p>
<textarea
*ngIf="editing"
class="m-wire-channel--description-editor m-border"
[(ngModel)]="rewards.description"
[autoGrow]
placeholder="About your offerings. eg. Describe what your supporters will receive…"
i18n-placeholder="@@WIRE__CHANNEL__REWARD_DESCRIPTION_PLACEHOLDER"
></textarea>
<m-wire-channel-table
type="tokens"
[(rewards)]="rewards.rewards.tokens"
[(editing)]="editing"
[channel]="user"
></m-wire-channel-table>
<m-wire-channel-table
type="money"
[(rewards)]="rewards.rewards.money"
[(editing)]="editing"
[channel]="user"
*mIfFeature="'wire-multi-currency'"
></m-wire-channel-table>
</div>
import { Component, Input, EventEmitter, Output } from '@angular/core';
import { Session } from '../../../services/session';
import { Client } from '../../../services/api';
import {
WireRewardsType,
WireRewardsStruc,
} from '../interfaces/wire.interfaces';
import { WireTypeLabels } from '../wire';
@Component({
selector: 'm-wire__subscriptionTiers',
templateUrl: 'tiers.component.html',
})
export class WireSubscriptionTiersComponent {
@Input() user;
@Input() editing;
@Input() saveEventListener = new EventEmitter();
@Output() isSaving = new EventEmitter();
@Output() isSaved = new EventEmitter();
rewards = {
description: '',
rewards: {
points: [],
money: [],
tokens: [],
},
};
constructor(public session: Session, private client: Client) {}
ngOnInit() {
if (this.user && this.user.wire_rewards) {
this.rewards = this.user.wire_rewards;
}
this.saveEventListener.subscribe(() => this.save());
}
async save() {
this.rewards.rewards.points = this._cleanAndSortRewards(
this.rewards.rewards.points
);
this.rewards.rewards.money = this._cleanAndSortRewards(
this.rewards.rewards.money
);
this.rewards.rewards.tokens = this._cleanAndSortRewards(
this.rewards.rewards.tokens
);
try {
await this.client.post('api/v1/wire/rewards', {
rewards: this.rewards,
});
this.session.getLoggedInUser().wire_rewards = this.rewards;
this.isSaved.next(true);
} catch (e) {
alert((e && e.message) || 'Server error');
} finally {
this.isSaving.next(false);
}
}
// Internal
private _cleanAndSortRewards(rewards: any[]) {
if (!rewards) {
return [];
}
return rewards
.filter(reward => reward.amount || `${reward.description}`.trim())
.map(reward => ({
...reward,
amount: Math.abs(Math.floor(reward.amount || 0)),
}))
.sort((a, b) => (a.amount > b.amount ? 1 : -1));
}
}
......@@ -70,7 +70,7 @@
<input
type="text"
class="m-wire--creator-wide-input--edit"
[ngModel]="wire.amount | number"
[ngModel]="wire.amount"
(ngModelChange)="setAmount($event)"
(focus)="amountEditorFocus()"
(blur)="amountEditorBlur()"
......
......@@ -47,6 +47,7 @@ import { sessionMock } from '../../../../tests/session-mock.spec';
import { web3WalletServiceMock } from '../../../../tests/web3-wallet-service-mock.spec';
import { IfFeatureDirective } from '../../../common/directives/if-feature.directive';
import { FeaturesService } from '../../../services/features.service';
import { featuresServiceMock } from '../../../../tests/features-service-mock.spec';
import { MockComponent } from '../../../utils/mock';
/* tslint:disable */
......@@ -219,7 +220,7 @@ describe('WireCreatorComponent', () => {
{ provide: WireContractService, useValue: wireContractServiceMock },
{ provide: WireService, useValue: wireServiceMock },
Web3WalletService,
FeaturesService,
{ provide: FeaturesService, useValue: featuresServiceMock },
{ provide: Web3WalletService, useValue: web3WalletServiceMock },
{ provide: OverlayModalService, useValue: overlayModalServiceMock },
{ provide: TokenContractService, useValue: tokenContractServiceMock },
......@@ -238,7 +239,7 @@ describe('WireCreatorComponent', () => {
jasmine.clock().uninstall();
jasmine.clock().install();
fixture = TestBed.createComponent(WireCreatorComponent);
featuresServiceMock.mock('wire-multi-currency', true);
comp = fixture.componentInstance; // LoginForm test instance
clientMock.response = {};
clientMock.response[`api/v2/boost/rates`] = {
......
......@@ -322,13 +322,24 @@ export class WireCreatorComponent {
return;
}
if (amount.indexOf('.') === 0) {
if (amount.length === 1) {
return; // not propogration
}
amount = `0${amount}`;
}
if (typeof amount === 'number') {
this.wire.amount = amount;
console.log('amount is a number');
return;
}
amount = amount.replace(/,/g, '');
this.wire.amount = parseFloat(amount);
const amountAsFloat = parseFloat(amount);
if (amountAsFloat) {
this.wire.amount = amountAsFloat;
}
}
/**
......@@ -355,11 +366,11 @@ export class WireCreatorComponent {
}
/**
* Round by 4
* Round by 6
*/
roundAmount() {
this.wire.amount =
Math.round(parseFloat(`${this.wire.amount}`) * 10000) / 10000;
Math.round(parseFloat(`${this.wire.amount}`) * 1000000) / 1000000;
}
// Charge and rates
......@@ -416,6 +427,7 @@ export class WireCreatorComponent {
switch (this.wire.payloadType) {
case 'onchain':
case 'eth':
if (!this.wire.payload && !this.wire.payload.receiver) {
throw new Error('Invalid receiver.');
}
......@@ -447,9 +459,11 @@ export class WireCreatorComponent {
break;
case 'usd':
//if (!this.wire.payload) {
// throw new Error('Payment method not processed.');
//}
if (!this.owner.merchant || !this.owner.merchant.id) {
throw new VisibleWireError(
'This channel is not able to receive USD at the moment'
);
}
break;
case 'btc':
if (!this.wire.payload.receiver) {
......
......@@ -67,6 +67,11 @@ export class WireCreatorRewardsComponent {
@Input('rewards') set _rewards(rewards) {
this.rewards = [];
if (!rewards || !rewards.rewards) {
return;
}
const methodsMap = [{ method: 'tokens', currency: 'tokens' }];
if (this.featuresService.has('wire-multi-currency')) {
......@@ -74,6 +79,9 @@ export class WireCreatorRewardsComponent {
}
for (const { method, currency } of methodsMap) {
if (!rewards.rewards[method]) {
continue;
}
for (const reward of rewards.rewards[method]) {
this.rewards.push({
amount: parseInt(reward.amount),
......
......@@ -26,6 +26,7 @@ import { WireMarketingComponent } from './marketing.component';
import { WireConsoleOverviewComponent } from './console/overview/overview.component';
import { WireConsoleRewardsInputsComponent } from './console/rewards-table/inputs/wire-console-rewards-inputs.component';
import { WireConsoleRewardsComponent } from './console/rewards-table/rewards.component';
import { WireSubscriptionTiersComponent } from './channel/tiers.component';
const wireRoutes: Routes = [
{ path: 'wire', component: WireMarketingComponent },
......@@ -60,6 +61,7 @@ const wireRoutes: Routes = [
WireConsoleSettingsComponent,
WireMarketingComponent,
WireConsoleOverviewComponent,
WireSubscriptionTiersComponent,
],
providers: [WireService],
exports: [
......@@ -74,6 +76,7 @@ const wireRoutes: Routes = [
WireConsoleRewardsComponent,
WireConsoleSettingsComponent,
WireConsoleOverviewComponent,
WireSubscriptionTiersComponent,
],
entryComponents: [
WireCreatorComponent,
......
......@@ -23,12 +23,12 @@ export class FeaturesService {
if (typeof this._features[feature] === 'undefined') {
if (isDevMode() && !this._hasWarned(feature)) {
console.warn(
`[FeaturedService] Feature '${feature}' is not declared. Assuming true.`
`[FeaturedService] Feature '${feature}' is not declared. Assuming false.`
);
this._warnedCache[feature] = Date.now();
}
return true;
return false;
}
if (this._features[feature] === 'admin' && this.session.isAdmin()) {
......
src/assets/marketing/mobile-dl-button.png

8.2 KB | W: 300px | H: 105px

src/assets/marketing/mobile-dl-button.png

8.37 KB | W: 300px | H: 105px

src/assets/marketing/mobile-dl-button.png
src/assets/marketing/mobile-dl-button.png
src/assets/marketing/mobile-dl-button.png
src/assets/marketing/mobile-dl-button.png
  • 2-up
  • Swipe
  • Onion skin
export let activityServiceMock = new (function() {
this.toggleAllowComments = jasmine
.createSpy('toggleAllowComponents')
.and.stub();
})();
export let analyticsServiceMock = new (function() {
this.send = jasmine.createSpy('send').and.stub();
this.onRouterInit = jasmine.createSpy('onRouterInit').and.stub();
this.onRouteChanged = jasmine.createSpy('onRouteChanged').and.stub();
this.preventDefault = jasmine.createSpy('preventDefault').and.stub();
this.wasDefaultPrevented = jasmine
.createSpy('wasDefaultPrevented')
.and.stub();
})();