...
 
Commits (14)
......@@ -3,6 +3,7 @@ import 'reflect-metadata';
import { join } from 'path';
import { readFileSync } from 'fs';
import * as _url from 'url';
import { ngExpressEngine } from '@nguniversal/express-engine';
import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens';
......@@ -171,6 +172,14 @@ app.get('*', cache(), (req, res) => {
provide: 'ORIGIN_URL',
useValue: `${http}://${req.headers.host}`,
},
// for initial query params before router loads
{
provide: 'QUERY_STRING',
useFactory: () => {
return _url.parse(req.url, true).search || '';
},
deps: [],
},
],
},
(err, html) => {
......
......@@ -12,6 +12,9 @@ PlotlyModule.plotlyjs = PlotlyJS;
@NgModule({
imports: [MindsModule, PlotlyModule, CookieModule],
bootstrap: [Minds],
providers: [{ provide: 'ORIGIN_URL', useValue: location.origin }],
providers: [
{ provide: 'ORIGIN_URL', useValue: location.origin },
{ provide: 'QUERY_STRING', useValue: location.search || '' },
],
})
export class AppBrowserModule {}
import { NgModule, inject } from '@angular/core';
import { NgModule, inject, Injector } from '@angular/core';
import {
CommonModule as NgCommonModule,
isPlatformServer,
Location,
} from '@angular/common';
import { RouterModule, Router, Routes } from '@angular/router';
import { RouterModule, Router, Routes, ActivatedRoute } from '@angular/router';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MINDS_PIPES } from './pipes/pipes';
......@@ -47,7 +47,6 @@ import { ScrollLock } from './directives/scroll-lock';
import { TagsLinks } from './directives/tags';
import { Tooltip } from './directives/tooltip';
import { MindsAvatar } from './components/avatar/avatar';
import { CaptchaComponent } from './components/captcha/captcha.component';
import { Textarea } from './components/editors/textarea.component';
import { TagcloudComponent } from './components/tagcloud/tagcloud.component';
import { DropdownComponent } from './components/dropdown/dropdown.component';
......@@ -209,7 +208,6 @@ const routes: Routes = [
MDL_DIRECTIVES,
DateSelectorComponent,
MindsAvatar,
CaptchaComponent,
Textarea,
InlineEditorComponent,
......@@ -316,7 +314,6 @@ const routes: Routes = [
MDL_DIRECTIVES,
DateSelectorComponent,
MindsAvatar,
CaptchaComponent,
Textarea,
InlineEditorComponent,
......@@ -435,8 +432,9 @@ const routes: Routes = [
},
{
provide: ConfigsService,
useFactory: client => new ConfigsService(client),
deps: [Client],
useFactory: (client, injector) =>
new ConfigsService(client, injector.get('QUERY_STRING')),
deps: [Client, Injector],
},
{
provide: MetaService,
......
<div class="m-captcha--sum" *ngIf="type == 'sum'">
<div
class="m-captcha--sum-question "
*ngIf="question"
i18n="A sum (eg. 2 + 2)@@COMMON__CAPTCHA__SIMPLE_SUM"
>
What is {{ question[0] }} {{ question[1] }} {{ question[2] }}?
</div>
<input type="number" [(ngModel)]="answer" (keyup)="validate()" />
</div>
.m-captcha--sum {
text-align: left;
.m-captcha--sum-question {
font-size: 18px;
padding: 8px;
letter-spacing: 1px;
font-family: 'Roboto', Helvetica, sans-serif;
font-weight: 600;
display: inline-block;
}
input[type='number'] {
display: inline-block;
width: 46px;
font-size: 22px;
padding: 8px 0px;
text-align: center;
font-weight: 600;
font-family: 'Roboto', Helvetica, sans-serif;
box-sizing: content-box;
}
}
import { Component, Output, Input, EventEmitter } from '@angular/core';
import { Client } from '../../../services/api';
@Component({
selector: 'm-captcha',
templateUrl: 'captcha.component.html',
})
export class CaptchaComponent {
answer: string | number;
@Output('answer') emit: EventEmitter<any> = new EventEmitter();
inProgress: boolean = false;
type: string = 'sum';
question: Array<string | number>;
nonce: number;
hash: string = '';
interval;
constructor(public client: Client) {}
ngOnInit() {
this.get();
this.interval = setInterval(this.get, 1000 * 60 * 4); //refresh every 4 minutes
}
ngOnDestroy() {
clearInterval(this.interval);
}
get() {
this.client.get('api/v1/captcha').then((response: any) => {
this.type = response.question.type;
this.question = response.question.question;
this.nonce = response.question.nonce;
this.hash = response.question.hash;
});
}
validate() {
let payload = {
type: this.type,
question: this.question,
answer: this.answer,
nonce: this.nonce,
hash: this.hash,
};
this.emit.next(JSON.stringify(payload));
this.client.post('api/v1/captcha', payload).then((response: any) => {
if (response.success) console.log('success');
else console.log('error');
});
}
}
export class CaptchaService {}
......@@ -123,3 +123,64 @@
justify-content: space-between;
}
}
// v2 specific
.m-dropdown--v2 {
position: relative;
.m-dropdown__list {
width: 180px;
border-radius: $m-borderRadius;
box-shadow: 0 $m-boxShadowOffset $m-boxShadowBlur rgba(0, 0, 0, 0.2);
box-sizing: border-box;
position: absolute;
z-index: 1000;
top: 0;
right: 0;
overflow: visible;
display: block;
@include m-theme() {
background-color: themed($m-bgColor--primary);
}
.m-dropdownList__item {
box-sizing: border-box;
padding: $minds-padding $minds-padding * 2.5;
font-size: 15px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
cursor: pointer;
@include m-theme() {
color: themed($m-textColor--tertiary);
border-bottom: 1px solid themed($m-borderColor--primary);
}
&.m-dropdownList__item--selected,
&:hover {
@include m-theme() {
color: themed($m-textColor--secondary);
background-color: themed($m-bgColor--tertiary);
}
}
&.m-dropdownList__item--destructive {
@include m-theme() {
color: $m-alert;
}
}
}
li:last-of-type {
border-bottom: none !important;
}
}
}
.m-dropdown--menu {
.m-dropdownList__item {
cursor: pointer;
}
}
<label class="m-formInput__checkbox" [for]="id">
<input type="checkbox" [(ngModel)]="value" [id]="id" />
<input
type="checkbox"
[ngModel]="value"
(ngModelChange)="updateValue($event)"
[id]="id"
/>
<span class="m-formInputCheckbox__custom"></span>
<ng-content></ng-content>
</label>
......@@ -21,13 +21,17 @@ export const FORM_INPUT_CHECKBOX_VALUE_ACCESSOR: any = {
templateUrl: 'checkbox.component.html',
providers: [FORM_INPUT_CHECKBOX_VALUE_ACCESSOR],
})
export class FormInputCheckboxComponent
implements ControlValueAccessor, OnChanges {
export class FormInputCheckboxComponent implements ControlValueAccessor {
readonly id: string;
value: boolean = false;
@ViewChild('input', { static: true }) input: ElementRef;
updateValue(value: boolean) {
this.value = value;
this.propagateChange(this.value);
}
propagateChange = (_: any) => {};
constructor(private fb: FormBuilder) {
......@@ -38,10 +42,6 @@ export class FormInputCheckboxComponent
.substring(2); // Confirm duplicates not possible?
}
ngOnChanges(changes: any) {
this.propagateChange(changes);
}
writeValue(value: any): void {
this.value = value;
}
......
/////
/// This component is deprevted.
/// Use v2 for new components
/////
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
......
import { Injectable } from '@angular/core';
import { Session } from '../../../services/session';
import { Client } from '../../../services/api';
import { OverlayModalService } from '../../../services/ux/overlay-modal';
import { SignupModalService } from '../../../modules/modals/signup/service';
import { BlockListService } from '../../services/block-list.service';
import { ActivityService } from '../../services/activity.service';
import { MindsUser } from '../../../interfaces/entities';
import { BehaviorSubject } from 'rxjs';
import { ShareModalComponent } from '../../../modules/modals/share/share';
import { ReportCreatorComponent } from '../../../modules/report/creator/creator.component';
import { DialogService } from '../../services/confirm-leave-dialog.service';
@Injectable()
export class PostMenuService {
entityOwner: MindsUser;
entity: any;
isLoadingFollowing = false;
isFollowing$: BehaviorSubject<boolean> = new BehaviorSubject(null);
isLoadingBlock = false;
isBlocked$: BehaviorSubject<boolean> = new BehaviorSubject(null);
showSubscribe$: BehaviorSubject<boolean> = new BehaviorSubject(false);
showUnSubscribe$: BehaviorSubject<boolean> = new BehaviorSubject(false);
constructor(
public session: Session,
private client: Client,
private overlayModal: OverlayModalService,
public signupModal: SignupModalService,
protected blockListService: BlockListService,
protected activityService: ActivityService,
private dialogService: DialogService
) {}
setEntity(entity): PostMenuService {
this.entity = entity;
return this;
}
setEntityOwner(entityOwner: MindsUser): PostMenuService {
this.entityOwner = entityOwner;
return this;
}
/**
* Subscribe to the owner
*/
async subscribe(): Promise<void> {
if (!this.session.isLoggedIn()) {
this.signupModal
.setSubtitle('You need to have a channel in order to subscribe')
.open();
return;
}
this.entityOwner.subscribed = true;
try {
const response: any = await this.client.post(
'api/v1/subscribe/' + this.entityOwner.guid,
{}
);
if (response && response.error) {
throw 'error';
}
this.entityOwner.subscribed = true;
} catch (e) {
this.entityOwner.subscribed = false;
alert("You can't subscribe to this user.");
}
}
/**
* Unsubscribe from the owner
*/
async unSubscribe(): Promise<void> {
this.entityOwner.subscribed = false;
try {
await this.client.delete('api/v1/subscribe/' + this.entityOwner.guid, {});
this.entityOwner.subscribed = false;
} catch (e) {
this.entityOwner.subscribed = true;
}
}
/**
* Fetch the following status
*/
async fetchFollowing(): Promise<void> {
if (this.isLoadingFollowing) return;
this.isLoadingFollowing = true;
try {
const response: any = await this.client.get(
`api/v2/notifications/follow/${this.entity.guid}`
);
this.isFollowing$.next(!!response.postSubscription.following);
} catch (e) {
} finally {
this.isLoadingFollowing = false;
}
}
/**
* Loads the blocked status
*/
async fetchBlock(): Promise<void> {
if (this.isLoadingBlock) return;
this.isLoadingBlock = true;
try {
const response: any = await this.client.get(
`api/v1/block/${this.entity.ownerObj.guid}`
);
this.isBlocked$.next(response.blocked);
} catch (e) {
} finally {
this.isLoadingBlock = true;
}
}
/**
* Follows a posts notifications
*/
async follow(): Promise<void> {
this.isFollowing$.next(true);
try {
const response: any = await this.client.put(
`api/v2/notifications/follow/${this.entity.guid}`
);
if (response.done) {
return;
}
throw new Error('E_NOT_DONE');
} catch (e) {
this.isFollowing$.next(false);
}
}
/**
* Unfollows a posts notifications
*/
async unfollow(): Promise<void> {
this.isFollowing$.next(false);
try {
const response: any = this.client.delete(
`api/v2/notifications/follow/${this.entity.guid}`
);
if (response.done) {
return;
}
throw new Error('E_NOT_DONE');
} catch (e) {
this.isFollowing$.next(true);
}
}
async block(): Promise<void> {
this.isBlocked$.next(true);
try {
await this.client.put('api/v1/block/' + this.entity.ownerObj.guid, {});
this.blockListService.add(`${this.entity.ownerObj.guid}`);
} catch (e) {
this.isBlocked$.next(false);
}
}
async unBlock(): Promise<void> {
this.isBlocked$.next(false);
try {
await this.client.delete('api/v1/block/' + this.entity.ownerObj.guid, {});
this.blockListService.remove(`${this.entity.ownerObj.guid}`);
} catch (e) {
this.isBlocked$.next(true);
}
}
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;
}
}
async setNsfw(nsfw) {
await this.client.post(`api/v2/admin/nsfw/${this.entity.guid}`, { nsfw });
this.entity.nsfw = nsfw;
}
async confirmDelete(): Promise<void> {
await this.dialogService
.confirm('Are you sure you want to delete this post?')
.toPromise();
}
openShareModal(): void {
this.overlayModal
.create(ShareModalComponent, this.entity.url, {
class: 'm-overlay-modal--medium m-overlayModal__share',
})
.present();
}
openReportModal(): void {
this.overlayModal.create(ReportCreatorComponent, this.entity).present();
}
}
<div class="m-dropdown--v2">
<button class="m-postMenu__button" (click)="onButtonClick($event)">
<i class="material-icons">more_vert</i>
</button>
<ul class="m-dropdown__list" [hidden]="!(isOpened$ | async)">
<li
class="m-dropdownList__item"
*ngIf="options.indexOf('view') !== -1"
(click)="onSelectedOption('view')"
i18n="@@M__ACTION__VIEW"
>
View
</li>
<li
class="m-dropdownList__item"
*ngIf="
(options.indexOf('edit') !== -1 &&
entity.owner_guid == session.getLoggedInUser().guid) ||
session.isAdmin()
"
(click)="onSelectedOption('edit')"
i18n="@@M__ACTION__EDIT"
>
Edit Post
</li>
<li
class="m-dropdownList__item"
*ngIf="options.indexOf('share') !== -1"
(click)="onSelectedOption('share')"
i18n="@@M__ACTION__SHARE"
>
Share
</li>
<li
class="m-dropdownList__item"
*ngIf="options.indexOf('translate') !== -1 && isTranslatable"
(click)="onSelectedOption('translate')"
i18n="@@M__ACTION__TRANSLATE"
>
Translate
</li>
<!-- SUBSCRIBE -->
<ng-container *ngIf="options.indexOf('subscribe') !== -1">
<li
class="m-dropdownList__item"
*ngIf="service.showSubscribe$ | async"
(click)="onSelectedOption('subscribe')"
i18n="@@M__ACTION__SUBSCRIBE"
>
Subscribe
</li>
<li
class="m-dropdownList__item"
*ngIf="service.showUnSubscribe$ | async"
(click)="onSelectedOption('unsubscribe')"
i18n="@@COMMON__POST_MENU__UNSUBSCRIBE"
>
Unsubscribe
</li>
</ng-container>
<span class="m-dropdownMenu__divider"></span>
<!-- NOTIFICATIONS -->
<ng-container *ngIf="options.indexOf('follow') !== -1">
<li
class="m-dropdownList__item"
*ngIf="(service.isFollowing$ | async) === null"
disabled
i18n="@@COMMON__POST_MENU__FOLLOW_NOTIFICATIONS"
>
Follow post
</li>
<li
class="m-dropdownList__item"
*ngIf="(service.isFollowing$ | async) === true"
(click)="onSelectedOption('unfollow')"
i18n="@@COMMON__POST_MENU__UNFOLLOW_NOTIFICATIONS"
>
Un-follow post
</li>
<li
class="m-dropdownList__item"
*ngIf="(service.isFollowing$ | async) === false"
(click)="onSelectedOption('follow')"
i18n="@@COMMON__POST_MENU__FOLLOW_NOTIFICATIONS"
>
Follow post
</li>
</ng-container>
<!-- BLOCK -->
<ng-container
*ngIf="
options.indexOf('block') !== -1 &&
entity.ownerObj.guid != session.getLoggedInUser().guid
"
>
<li
class="m-dropdownList__item"
*ngIf="(service.isBlocked$ | async) === null"
disabled
i18n="@@COMMON__POST_MENU__BLOCK_AND_UNBLOCK"
>
Block/Unblock
</li>
<li
class="m-dropdownList__item"
*ngIf="(service.isBlocked$ | async) === false"
(click)="onSelectedOption('block')"
i18n="@@COMMON__POST_MENU__BLOCK"
>
Block user
</li>
<li
class="m-dropdownList__item"
*ngIf="(service.isBlocked$ | async) === true"
(click)="onSelectedOption('unblock')"
i18n="@@COMMON__POST_MENU__UNBLOCK"
>
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="m-dropdownList__item"
*ngIf="!entity.allow_comments"
(click)="onSelectedOption('allow-comments')"
i18n="@@COMMON__POST_MENU__ALLOW_COMMENTS"
>
Allow Comments
</li>
<li
class="m-dropdownList__item"
*ngIf="entity.allow_comments"
(click)="onSelectedOption('disable-comments')"
i18n="@@COMMON__POST_MENU__DISABLE_COMMENTS"
>
Disable Comments
</li>
</ng-container>
<!-- ADMIN EDIT FLAGS -->
<ng-container
*ngIf="
options.indexOf('set-explicit') !== -1 && (service.isOwner$ | async)
"
>
<li class="m-dropdownList__item m-postMenu__item--nsfw">
<m-nsfw-selector
service="editing"
[selected]="entity.nsfw"
(selectedChange)="onNSFWSelected($event)"
>
</m-nsfw-selector>
</li>
</ng-container>
<!-- INJECTED POST ACTIONS -->
<div (click)="isOpened$.next(false)">
<ng-content select="[post-menu]"></ng-content>
</div>
<!-- DELETE -->
<li
class="m-dropdownList__item m-dropdownList__item--destructive"
*ngIf="
(options.indexOf('delete') !== -1 &&
entity.owner_guid == session.getLoggedInUser().guid) ||
session.isAdmin() ||
canDelete
"
(click)="onSelectedOption('delete')"
i18n="@@M__ACTION__DELETE"
>
Delete Post
</li>
<!-- REPORT -->
<li
class="m-dropdownList__item m-dropdownList__item--destructive"
*ngIf="
options.indexOf('report') !== -1 &&
entity.owner_guid != session.getLoggedInUser().guid
"
(click)="onSelectedOption('report')"
i18n="Report as notify something@@M__ACTION__REPORT"
>
Report Post
</li>
</ul>
</div>
<div
class="m-bgOverlay--v2"
(click)="isOpened$.next(false)"
[hidden]="!(isOpened$ | async)"
></div>
.m-postMenu__button {
border: 0;
cursor: pointer;
@include m-theme() {
background-color: themed($m-bgColor--primary);
}
i {
font-size: 32px;
@include m-theme() {
color: themed($m-textColor--secondary);
}
}
}
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
EventEmitter,
Input,
Output,
OnInit,
} from '@angular/core';
import { Session } from '../../../../services/session';
import { FeaturesService } from '../../../../services/features.service';
import { PostMenuService } from '../post-menu.service';
import { BehaviorSubject } from 'rxjs';
type Option =
| 'edit'
| 'view'
| 'translate'
| 'share'
| 'follow'
| 'unfollow'
| 'feature'
| 'unfeature'
| 'delete'
| 'report'
| 'set-explicit'
| 'remove-explicit'
| 'monetize'
| 'unmonetize'
| 'subscribe'
| 'unsubscribe'
| 'rating'
| 'block'
| 'unblock'
| 'allow-comments'
| 'disable-comments';
@Component({
selector: 'm-postMenu--v2',
templateUrl: 'menu.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [PostMenuService],
})
export class PostMenuV2Component implements OnInit {
@Input() entity: any;
@Input() options: Array<Option>;
@Output() optionSelected: EventEmitter<Option> = new EventEmitter<Option>();
@Input() canDelete: boolean = false;
@Input() isTranslatable: boolean = false;
@Input() user: any;
isOpened$: BehaviorSubject<boolean> = new BehaviorSubject(false);
constructor(
public session: Session,
private cd: ChangeDetectorRef,
public featuresService: FeaturesService,
public service: PostMenuService
) {}
ngOnInit() {
this.service.setEntity(this.entity);
this.service.setEntityOwner(this.user);
}
onButtonClick(e: MouseEvent): void {
this.isOpened$.next(true);
this.service.fetchFollowing();
this.service.fetchBlock();
}
/**
* Router for all options
* !! Only user await when you want to pause the interactions !!
* @param option
*/
async onSelectedOption(option: Option): Promise<void> {
switch (option) {
case 'edit':
break;
case 'share':
this.service.openShareModal();
break;
case 'translate':
break;
// Async options
case 'subscribe':
this.service.subscribe();
break;
case 'unsubscribe':
this.service.unSubscribe();
break;
case 'follow':
this.service.follow();
break;
case 'unfollow':
this.service.unfollow();
break;
case 'block':
this.service.block();
break;
case 'unblock':
this.service.unBlock();
break;
// Destructive options
case 'delete':
await this.service.confirmDelete();
break;
case 'report':
this.service.openReportModal();
break;
}
this.optionSelected.emit(option);
this.isOpened$.next(false);
this.detectChanges();
}
onNSFWSelected(reasons: Array<{ label; value; selected }>) {
const nsfw = reasons.map(reason => reason.value);
this.service.setNsfw(nsfw);
this.detectChanges();
}
detectChanges() {
this.cd.markForCheck();
}
}
import { Client } from '../api/client.service';
import { Injectable } from '@angular/core';
import { Injectable, Inject, Optional } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { map, tap } from 'rxjs/operators';
import { Router, ActivatedRoute } from '@angular/router';
@Injectable()
export class ConfigsService {
private configs = {};
constructor(private client: Client) {}
constructor(
private client: Client,
@Inject('QUERY_STRING') private queryString: string
) {}
async loadFromRemote() {
try {
this.configs = await this.client.get('api/v1/minds/config');
this.configs = await this.client.get(
`api/v1/minds/config${this.queryString}`
);
} catch (err) {
console.error(err);
}
......
<ng-container *ngIf="captcha">
<img [src]="captcha.base64Image" />
<i class="material-icons m-captcha__refresh" (click)="refresh()">refresh</i>
<input
[ngModel]="captcha.clientText"
(ngModelChange)="onValueChange($event)"
type="text"
placeholder="Enter the characters above"
/>
</ng-container>
m-captcha {
display: block;
img {
margin-bottom: $minds-margin;
}
}
.m-captcha__refresh {
cursor: pointer;
position: absolute;
}
import {
async,
ComponentFixture,
fakeAsync,
TestBed,
tick,
} from '@angular/core/testing';
import { DebugElement } from '@angular/core';
import { CaptchaComponent, Captcha } from './captcha.component';
import { ReactiveFormsModule } from '@angular/forms';
import { Client } from '../../services/api';
import { clientMock } from '../../../tests/client-mock.spec';
import { By } from '@angular/platform-browser';
describe('CaptchaComponent', () => {
let comp: CaptchaComponent;
let fixture: ComponentFixture<CaptchaComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [CaptchaComponent],
imports: [ReactiveFormsModule],
providers: [{ provide: Client, useValue: clientMock }],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(CaptchaComponent);
comp = fixture.componentInstance;
fixture.detectChanges();
clientMock.response = {};
});
});
import {
Component,
ElementRef,
forwardRef,
OnChanges,
OnInit,
ViewChild,
} from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Client } from '../../services/api';
export const CAPTCHA_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CaptchaComponent),
multi: true,
};
export class Captcha {
jwtToken: string;
base64Image: string;
clientText: string; // This is what the user enters
buildClientKey(): string {
return JSON.stringify({
jwtToken: this.jwtToken,
clientText: this.clientText,
});
}
}
@Component({
selector: 'm-captcha',
templateUrl: 'captcha.component.html',
providers: [CAPTCHA_VALUE_ACCESSOR],
})
export class CaptchaComponent implements ControlValueAccessor, OnInit {
captcha = new Captcha();
image: string;
value: string = '';
propagateChange = (_: any) => {};
constructor(private client: Client) {}
ngOnInit(): void {
this.refresh();
}
async refresh(): Promise<void> {
const response: any = await this.client.get('api/v2/captcha', {
cb: Date.now(),
});
this.captcha.base64Image = response.base64_image;
this.captcha.jwtToken = response.jwt_token;
}
onValueChange(value: string) {
this.captcha.clientText = value;
this.value = this.captcha.buildClientKey();
this.propagateChange(this.value);
}
writeValue(value: any): void {
// Not required as captcha is one direction
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {}
}
import { NgModule } from '@angular/core';
import { ReCaptchaComponent } from './recaptcha/recaptcha.component';
import { RECAPTCHA_SERVICE_PROVIDER } from './recaptcha/recaptcha.service';
import { CaptchaComponent } from './captcha.component';
import { FormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';
@NgModule({
declarations: [ReCaptchaComponent],
exports: [ReCaptchaComponent],
providers: [RECAPTCHA_SERVICE_PROVIDER],
imports: [CommonModule, FormsModule],
declarations: [CaptchaComponent],
exports: [CaptchaComponent],
})
export class CaptchaModule {}
import {
Component,
OnInit,
Input,
Output,
EventEmitter,
NgZone,
ViewChild,
ElementRef,
forwardRef,
} from '@angular/core';
import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms';
import { ReCaptchaService } from './recaptcha.service';
@Component({
selector: 're-captcha',
template: '<div #target></div>',
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ReCaptchaComponent),
multi: true,
},
],
})
export class ReCaptchaComponent implements OnInit, ControlValueAccessor {
@Input() site_key: string = null;
@Input() theme = 'light';
@Input() type = 'image';
@Input() size = 'normal';
@Input() tabindex = 0;
@Input() badge = 'bottomright';
/* Available languages: https://developers.google.com/recaptcha/docs/language */
@Input() language: string = null;
@Output() captchaResponse = new EventEmitter<string>();
@Output() captchaExpired = new EventEmitter();
@ViewChild('target', { static: true }) targetRef: ElementRef;
widgetId: any = null;
onChange: Function = () => {
return;
};
onTouched: Function = () => {
return;
};
constructor(
private _zone: NgZone,
private _captchaService: ReCaptchaService
) {}
ngOnInit() {
this._captchaService.getReady(this.language).subscribe(ready => {
if (!ready) return;
// noinspection TypeScriptUnresolvedVariable,TypeScriptUnresolvedFunction
this.widgetId = (<any>window).grecaptcha.render(
this.targetRef.nativeElement,
{
sitekey: this.site_key,
badge: this.badge,
theme: this.theme,
type: this.type,
size: this.size,
tabindex: this.tabindex,
callback: <any>(
((response: any) =>
this._zone.run(this.recaptchaCallback.bind(this, response)))
),
'expired-callback': <any>(
(() => this._zone.run(this.recaptchaExpiredCallback.bind(this)))
),
}
);
});
}
// noinspection JSUnusedGlobalSymbols
public reset() {
if (this.widgetId === null) return;
// noinspection TypeScriptUnresolvedVariable
(<any>window).grecaptcha.reset(this.widgetId);
this.onChange(null);
}
// noinspection JSUnusedGlobalSymbols
public execute() {
if (this.widgetId === null) return;
// noinspection TypeScriptUnresolvedVariable
(<any>window).grecaptcha.execute(this.widgetId);
}
public getResponse(): string {
if (this.widgetId === null) return null;
// noinspection TypeScriptUnresolvedVariable
return (<any>window).grecaptcha.getResponse(this.widgetId);
}
writeValue(newValue: any): void {
/* ignore it */
}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
private recaptchaCallback(response: string) {
this.onChange(response);
this.onTouched();
this.captchaResponse.emit(response);
}
private recaptchaExpiredCallback() {
this.onChange(null);
this.onTouched();
this.captchaExpired.emit();
}
}
import { Injectable, NgZone, Optional, SkipSelf } from '@angular/core';
import { Observable, BehaviorSubject } from 'rxjs';
/*
* Common service shared by all reCaptcha component instances
* through dependency injection.
* This service has the task of loading the reCaptcha API once for all.
* Only the first instance of the component creates the service, subsequent
* components will use the existing instance.
*
* As the language is passed to the <script>, the first component
* determines the language of all subsequent components. This is a limitation
* of the present Google API.
*/
@Injectable()
export class ReCaptchaService {
private scriptLoaded = false;
private readySubject: BehaviorSubject<boolean> = new BehaviorSubject(false);
constructor(zone: NgZone) {
/* the callback needs to exist before the API is loaded */
window[<any>'reCaptchaOnloadCallback'] = <any>(
(() => zone.run(this.onloadCallback.bind(this)))
);
}
public getReady(language: string): Observable<boolean> {
if (!this.scriptLoaded) {
this.scriptLoaded = true;
let doc = <HTMLDivElement>document.body;
let script = document.createElement('script');
script.innerHTML = '';
script.src =
'https://www.google.com/recaptcha/api.js?onload=reCaptchaOnloadCallback&render=explicit' +
(language ? '&hl=' + language : '');
script.async = true;
script.defer = true;
doc.appendChild(script);
}
return this.readySubject.asObservable();
}
private onloadCallback() {
this.readySubject.next(true);
}
}
/* singleton pattern taken from https://github.com/angular/angular/issues/13854 */
export function RECAPTCHA_SERVICE_PROVIDER_FACTORY(
ngZone: NgZone,
parentDispatcher: ReCaptchaService
) {
return parentDispatcher || new ReCaptchaService(ngZone);
}
export const RECAPTCHA_SERVICE_PROVIDER = {
provide: ReCaptchaService,
deps: [NgZone, [new Optional(), new SkipSelf(), ReCaptchaService]],
useFactory: RECAPTCHA_SERVICE_PROVIDER_FACTORY,
};
......@@ -109,7 +109,7 @@ describe('ChannelComponent', () => {
{ provide: FeaturesService, useValue: featuresServiceMock },
{ provide: BlockListService, useValue: MockService(BlockListService) },
{ provide: ClientMetaService, useValue: clientMetaServiceMock },
ConfigsService,
{ provide: ConfigsService, useValue: MockService(ConfigsService) },
],
}).compileComponents(); // compile template and css
}));
......
......@@ -134,6 +134,16 @@
</ng-container>
</div>
</div>
<div
*ngIf="form.value.password"
class="mdl-cell mdl-cell--12-col m-registerForm__captcha"
>
<label for="captcha" *ngIf="showLabels" i18n>
Captcha
</label>
<m-captcha formControlName="captcha"></m-captcha>
</div>
</div>
<div
......
......@@ -17,7 +17,6 @@ import {
import { Client } from '../../../services/api';
import { Session } from '../../../services/session';
import { ReCaptchaComponent } from '../../../modules/captcha/recaptcha/recaptcha.component';
import { ExperimentsService } from '../../experiments/experiments.service';
import { RouterHistoryService } from '../../../common/services/router-history.service';
import { PopoverComponent } from '../popover-validation/popover.component';
......@@ -28,7 +27,7 @@ import { FeaturesService } from '../../../services/features.service';
selector: 'minds-form-register',
templateUrl: 'register.html',
})
export class RegisterForm implements OnInit {
export class RegisterForm {
@Input() referrer: string;
@Input() parentId: string = '';
@Input() showTitle: boolean = false;
......@@ -53,7 +52,6 @@ export class RegisterForm implements OnInit {
form: FormGroup;
fbForm: FormGroup;
@ViewChild('reCaptcha', { static: false }) reCaptcha: ReCaptchaComponent;
@ViewChild('popover', { static: false }) popover: PopoverComponent;
constructor(
......@@ -86,12 +84,6 @@ export class RegisterForm implements OnInit {
);
}
ngOnInit() {
if (this.reCaptcha) {
this.reCaptcha.reset();
}
}
showError(field: string) {
return (
this.showInlineErrors &&
......@@ -121,10 +113,6 @@ export class RegisterForm implements OnInit {
'disabled_cookies=; expires=Thu, 01 Jan 1970 00:00:01 GMT;';
if (this.form.value.password !== this.form.value.password2) {
if (this.reCaptcha) {
this.reCaptcha.reset();
}
this.errorMessage = 'Passwords must match.';
return;
}
......@@ -148,9 +136,6 @@ export class RegisterForm implements OnInit {
.catch(e => {
console.log(e);
this.inProgress = false;
if (this.reCaptcha) {
this.reCaptcha.reset();
}
if (e.status === 'failed') {
// incorrect login details
......
......@@ -148,6 +148,18 @@ minds-button-remind {
cursor: default;
}
.m-bgOverlay--v2 {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
width: 100vw;
height: 100vh;
cursor: default;
// pointer-events: none;
}
minds-button-comment a,
minds-button-remind a {
@include m-theme() {
......
......@@ -88,7 +88,7 @@ export class Activity implements OnInit {
element: any;
visible: boolean = false;
editing: boolean = false;
@Input() editing: boolean = false;
@Input() hideTabs: boolean;
@Output() _delete: EventEmitter<any> = new EventEmitter();
......
......@@ -10,6 +10,7 @@ import { Session } from '../../../../../services/session';
import { AttachmentService } from '../../../../../services/attachment';
import { ActivityService } from '../../../../../common/services/activity.service';
import { ConfigsService } from '../../../../../common/services/configs.service';
@Component({
moduleId: module.id,
......@@ -23,6 +24,10 @@ import { ActivityService } from '../../../../../common/services/activity.service
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ActivityPreview {
readonly cdnUrl: string;
readonly cdnAssetsUrl: string;
readonly siteUrl: string;
activity: any;
hideTabs: boolean;
......@@ -40,9 +45,12 @@ export class ActivityPreview {
public session: Session,
public client: Client,
public attachment: AttachmentService,
private _changeDetectorRef: ChangeDetectorRef
configs: ConfigsService
) {
this.hideTabs = true;
this.cdnUrl = configs.get('cdn_url');
this.cdnAssetsUrl = configs.get('cdn_assets_url');
this.siteUrl = configs.get('site_url');
}
set object(value: any) {
......
......@@ -2,15 +2,16 @@ $activity-padding-lr: 21px;
m-activity {
display: block;
position: relative;
&,
&.m-border {
border-radius: 2px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
border-radius: $m-borderRadius;
box-shadow: 0 $m-boxShadowOffset $m-boxShadowBlur rgba(0, 0, 0, 0.05);
}
@include m-theme() {
background: themed($m-white);
background: themed($m-bgColor--primary);
}
}
......
......@@ -9,11 +9,14 @@ m-activity__content {
display: flex;
flex-direction: column;
}
.m-activityContent__media--video,
.m-activityContent__media--image,
.m-activityContent__media--richEmbed {
flex: 1;
background: #f5f5f5;
@include m-theme() {
background: themed($m-bgColor--secondary);
}
display: flex;
flex-direction: column;
}
......@@ -31,14 +34,14 @@ m-activity__content {
padding: 0px $activity-padding-lr 16px;
@include m-theme() {
color: themed($m-grey-700);
color: themed($m-textColor--primary);
}
a {
text-decoration: none;
font-weight: 600;
@include m-theme() {
color: themed($m-grey-700);
color: themed($m-textColor--primary);
}
&:hover {
......@@ -101,6 +104,8 @@ m-activity__content {
font-size: 13px;
padding: 0;
margin: 0;
color: #9b9b9b !important;
@include m-theme() {
color: themed($m-textColor--tertiary) !important;
}
}
}
<m-postMenu--v2
[entity]="service.entity$ | async"
[canDelete]="service.canDelete$ | async"
[isTranslatable]="false"
[options]="menuOptions"
(optionSelected)="onOptionSelected($event)"
>
</m-postMenu--v2>
import {
Component,
HostListener,
ViewChild,
Input,
ElementRef,
} from '@angular/core';
import { Subscription } from 'rxjs';
import { ActivatedRoute, Router } from '@angular/router';
import { ActivityService, ActivityEntity } from '../activity.service';
import { ConfigsService } from '../../../../common/services/configs.service';
import { Session } from '../../../../services/session';
import { MindsUser, MindsGroup } from '../../../../interfaces/entities';
import { OverlayModalService } from '../../../../services/ux/overlay-modal';
import { MediaModalComponent } from '../../../media/modal/modal.component';
@Component({
selector: 'm-activity__menu',
templateUrl: 'menu.component.html',
})
export class ActivityMenuComponent {
private entitySubscription: Subscription;
entity: ActivityEntity;
constructor(
public service: ActivityService,
private overlayModal: OverlayModalService,
private router: Router
) {}
ngOnInit() {
this.entitySubscription = this.service.entity$.subscribe(
(entity: ActivityEntity) => {
this.entity = entity;
}
);
}
ngOnDestory() {
this.entitySubscription.unsubscribe();
}
get menuOptions(): Array<string> {
if (!this.entity || !this.entity.ephemeral) {
if (this.service.displayOptions.showBoostMenuOptions) {
return [
'edit',
'translate',
'share',
'follow',
'feature',
'delete',
'report',
'set-explicit',
'block',
'rating',
'allow-comments',
];
} else {
return [
'edit',
'translate',
'share',
'follow',
'feature',
'delete',
'report',
'set-explicit',
'block',
'rating',
'allow-comments',
];
}
} else {
return [
'view',
'translate',
'share',
'follow',
'feature',
'report',
'set-explicit',
'block',
'rating',
'allow-comments',
];
}
}
onOptionSelected(option): void {
switch (option) {
case 'edit':
// Load old post in editing mode
this.router.navigate([`/newsfeed/${this.entity.guid}`], {
queryParams: { editing: 1 },
});
break;
}
}
}
m-activity__ownerBlock {
display: flex;
align-items: center;
width: 100%;
padding-top: 12px;
padding-bottom: 12px;
padding-left: $activity-padding-lr;
padding-right: $activity-padding-lr;
padding-right: $activity-padding-lr / 2;
box-sizing: border-box;
}
......@@ -36,7 +37,7 @@ m-activity__ownerBlock {
font-size: 16px;
@include m-theme() {
color: themed($m-grey-700);
color: themed($m-textColor--primary);
}
> * {
......@@ -51,6 +52,6 @@ m-activity__ownerBlock {
font-weight: normal;
@include m-theme() {
color: themed($m-grey-300);
color: themed($m-textColor--tertiary);
}
}
......@@ -2,10 +2,13 @@ m-activity__toolbar {
display: block;
width: 100%;
padding: 0 25px 16px;
border-top: 1px solid #ececec;
padding-top: $minds-padding * 2;
box-sizing: border-box;
@include m-theme() {
border-top: 1px solid themed($m-borderColor--tertiary);
}
> * {
margin-right: 22px;
......
......@@ -29,9 +29,10 @@
</ng-container>
</div>
<div class="minds-list" *ngIf="false">
<div class="minds-list" *ngIf="showLegacyActivity">
<minds-activity
*ngIf="activity"
[editing]="editing"
[object]="activity"
[commentsToggle]="true"
[focusedCommentGuid]="focusedCommentGuid"
......@@ -44,7 +45,7 @@
</minds-activity>
</div>
<div class="minds-list" *ngIf="true && activity">
<div class="minds-list" *ngIf="activity && !showLegacyActivity">
<m-activity
[entity]="activity"
[displayOptions]="{ fixedHeight: true }"
......
......@@ -20,7 +20,7 @@ import { uploadMock } from '../../../../tests/upload-mock.spec';
import { Upload } from '../../../services/api/upload';
import { ContextService } from '../../../services/context.service';
import { contextServiceMock } from '../../../../tests/context-service-mock.spec';
import { of } from 'rxjs';
import { of, BehaviorSubject } from 'rxjs';
import { ActivatedRoute, convertToParamMap, Router } from '@angular/router';
import { EntitiesService } from '../../../common/services/entities.service';
import { MockService, MockComponent } from '../../../utils/mock';
......@@ -29,6 +29,7 @@ import { featuresServiceMock } from '../../../../tests/features-service-mock.spe
import { MetaService } from '../../../common/services/meta.service';
import { ConfigsService } from '../../../common/services/configs.service';
import { SocialIcons } from '../../legacy/components/social-icons/social-icons';
import { ActivityComponent } from '../activity/activity.component';
@Component({
selector: 'minds-activity',
......@@ -39,6 +40,7 @@ class MindsActivityMock {
@Input() object: any;
@Input() commentsToggle: boolean;
@Input() showRatingToggle: boolean;
@Input() editing: boolean;
}
let routerMock = new (function() {
......@@ -59,6 +61,10 @@ describe('NewsfeedSingleComponent', () => {
selector: 'm-social-icons',
inputs: ['url', 'title', 'embed'],
}),
MockComponent({
selector: 'm-activity',
inputs: ['entity', 'displayOptions'],
}),
],
imports: [RouterTestingModule, ReactiveFormsModule],
providers: [
......@@ -73,10 +79,10 @@ describe('NewsfeedSingleComponent', () => {
snapshot: {
queryParamMap: convertToParamMap({}),
},
queryParamMap: new BehaviorSubject(convertToParamMap({})),
},
},
{ provide: MetaService, useValue: MockService(MetaService) },
{ provide: Router, useValue: routerMock },
{ provide: EntitiesService, useValue: MockService(EntitiesService) },
{ provide: FeaturesService, useValue: featuresServiceMock },
{ provide: ConfigsService, useValue: MockService(ConfigsService) },
......@@ -158,7 +164,7 @@ describe('NewsfeedSingleComponent', () => {
it('it should show the activity', () => {
fixture.detectChanges();
expect(
fixture.debugElement.query(By.css('.minds-list minds-activity'))
fixture.debugElement.query(By.css('.minds-list m-activity'))
).not.toBeNull();
});
......
......@@ -27,6 +27,7 @@ export class NewsfeedSingleComponent {
paramsSubscription: Subscription;
queryParamsSubscription: Subscription;
focusedCommentGuid: string = '';
editing = false;
constructor(
public router: Router,
......@@ -64,10 +65,20 @@ export class NewsfeedSingleComponent {
this.load(params['guid']);
}
});
this.queryParamsSubscription = this.route.queryParamMap.subscribe(
params => {
if (params.has('editing')) {
this.editing = !!params.get('editing');
console.log('editing', this.editing);
}
}
);
}
ngOnDestroy() {
this.paramsSubscription.unsubscribe();
this.queryParamsSubscription.unsubscribe();
}
/**
......@@ -177,4 +188,8 @@ export class NewsfeedSingleComponent {
delete(activity) {
this.router.navigate(['/newsfeed']);
}
get showLegacyActivity(): boolean {
return this.editing;
}
}
......@@ -67,7 +67,7 @@ export class SearchBarComponent {
}
unListen() {
this.routerSubscription.unsubscribe();
if (this.routerSubscription) this.routerSubscription.unsubscribe();
}
handleUrl(url: string) {
......
......@@ -19,6 +19,8 @@ export class FeaturesService {
has(feature: string): boolean {
const features = this.configs.get('features');
if (!features) return false;
if (!feature) {
throw new Error('Invalid feature ID');
}
......
import { NgZone, RendererFactory2, PLATFORM_ID } from '@angular/core';
import { NgZone, RendererFactory2, PLATFORM_ID, Injector } from '@angular/core';
import { Router } from '@angular/router';
import { Location } from '@angular/common';
import { TransferState } from '@angular/platform-browser';
......@@ -186,8 +186,9 @@ export const MINDS_PROVIDERS: any[] = [
},
{
provide: ConfigsService,
useFactory: client => new ConfigsService(client),
deps: [Client],
useFactory: (client, injector) =>
new ConfigsService(client, injector.get('QUERY_STRING')),
deps: [Client, Injector],
},
{
provide: FeaturesService,
......
......@@ -89,7 +89,17 @@ $linkedin: #0071a1;
// e.g. m-grey-100 in light mode will become m-grey-900 in dark mode
$themes: (
light: (
m-grey-950: $grey-950,
m-textColor--primary: #4f4f50,
m-textColor--secondary: #7d7d82,
m-textColor--tertiary: #9b9b9b,
m-bgColor--primary: #ffffff,
m-bgColor--secondary: #f5f5f5,
m-bgColor--tertiary: #e3e4e9,
m-borderColor--primary: #dce2e4,
m-borderColor--secondary: #979797,
m-borderColor--tertiary: #ececec,
// legacy colors
m-grey-950: $grey-950,
m-grey-900: $grey-900,
m-grey-800: $grey-800,
m-grey-700: $grey-700,
......@@ -144,7 +154,17 @@ $themes: (
m-linkedin: $linkedin,
),
dark: (
m-grey-950: lighten($grey-50, $percent),
m-textColor--primary: #ffffff,
m-textColor--secondary: #aeb0b8,
m-textColor--tertiary: #797b82,
m-bgColor--primary: #252e31,
m-bgColor--secondary: #202527,
m-bgColor--tertiary: #404e53,
m-borderColor--primary: #404a4e,
m-borderColor--secondary: #979797,
m-borderColor--tertiary: #202527,
// legacy colors
m-grey-950: lighten($grey-50, $percent),
m-grey-900: lighten($grey-100, $percent),
m-grey-800: lighten($grey-200, $percent),
m-grey-700: lighten($grey-300, $percent),
......@@ -260,3 +280,20 @@ $m-messenger: 'm-messenger';
$m-twitter: 'm-twitter';
$m-whatsapp: 'm-whatsapp';
$m-linkedin: 'm-linkedin';
$m-textColor--primary: 'm-textColor--primary';
$m-textColor--secondary: 'm-textColor--secondary';
$m-textColor--tertiary: 'm-textColor--tertiary';
$m-bgColor--primary: 'm-bgColor--primary';
$m-bgColor--secondary: 'm-bgColor--secondary';
$m-bgColor--tertiary: 'm-bgColor--tertiary';
$m-borderColor--primary: 'm-borderColor--primary';
$m-borderColor--secondary: 'm-borderColor--secondary';
$m-borderColor--tertiary: 'm-borderColor--tertiary';
$m-alert: #e03c20;
$m-link: #1b85d6;
$m-btn--primary: #1b85d6;
$m-borderRadius: 2px;
$m-boxShadowBlur: 10px;
$m-boxShadowOffset: 2px;