...
 
Commits (17)
......@@ -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,
......@@ -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';
......@@ -212,7 +211,6 @@ const routes: Routes = [
MDL_DIRECTIVES,
DateSelectorComponent,
MindsAvatar,
CaptchaComponent,
Textarea,
InlineEditorComponent,
......@@ -322,7 +320,6 @@ const routes: Routes = [
MDL_DIRECTIVES,
DateSelectorComponent,
MindsAvatar,
CaptchaComponent,
Textarea,
InlineEditorComponent,
......@@ -441,8 +438,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 {}
......@@ -78,7 +78,7 @@ m-sidebar--navigation {
padding: 0 24px;
@include m-theme() {
border-bottom: 1px solid themed($m-primary-border);
border-bottom: 1px solid themed($m-borderColor--primary);
}
a > img {
......@@ -89,7 +89,7 @@ m-sidebar--navigation {
i.material-icons {
cursor: pointer;
@include m-theme() {
color: themed($m-second-text-color);
color: themed($m-textColor--secondary);
}
}
}
......@@ -134,7 +134,7 @@ m-sidebar--navigation {
font-size: 17px;
line-height: 44px;
@include m-theme() {
color: themed($m-navigation-item);
color: themed($m-textColor--secondary);
}
&.m-sidebar--navigation--item-active {
......
......@@ -11,6 +11,12 @@
flex-wrap: wrap;
}
&.m-topbar--navigation__centered {
max-width: 100%;
justify-content: center;
flex-wrap: wrap;
}
&:not(.m-topbar--navigation--text-only) .m-topbar--navigation--item span {
@media screen and (max-width: 840px) {
display: none;
......
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';
......@@ -7,11 +7,16 @@ import { map, tap } from 'rxjs/operators';
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);
}
......
<div class="m-toolbar">
<div class="m-topbar--row">
<div class="m-topbar--navigation m-topbar--navigation--text-only">
<div
class="m-topbar--navigation m-topbar--navigation__centered m-topbar--navigation--text-only"
>
<a
class="m-topbar--navigation--item"
routerLink="/analytics/admin"
......@@ -99,6 +101,13 @@
>
<span i18n="@@M__ADMIN_NAV__REPORTS">Reports</span>
</a>
<a
class="m-topbar--navigation--item"
routerLink="/admin/features"
routerLinkActive="m-topbar--navigation--item-active"
>
<span i18n="@@M__ADMIN_NAV__FEATURES">Features</span>
</a>
</div>
</div>
</div>
......@@ -122,3 +131,4 @@
<m-admin--reports-download
*ngIf="filter == 'reports-download'"
></m-admin--reports-download>
<m-admin--features *ngIf="filter == 'features'"></m-admin--features>
<div class="m-adminFeatures">
<ng-container *ngIf="!isLoading && !error">
<div class="m-adminFeatures--label" i18n>
<b>Environment</b>: {{ environment }}
</div>
<div class="m-adminFeatures--label" i18n>
<b>Features for</b>: {{ readableFor }}
</div>
<table class="m-adminFeatures--table" cellspacing="0" cellpadding="0">
<thead>
<tr>
<th class="m-adminFeaturesTable--cell__first">Feature</th>
<th *ngFor="let service of services">{{ service }}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let feature of features">
<td class="m-adminFeaturesTable--cell__first">{{ feature.name }}</td>
<td
*ngFor="let service of services"
class="m-adminFeaturesTable--cell__value"
[class.m-adminFeaturesTable--cell__bestValue]="
isBestService(service, feature.services)
"
>
{{ labelForValue(feature.services[service]) }}
</td>
</tr>
</tbody>
</table>
</ng-container>
<ng-container *ngIf="isLoading">
<div class="m-adminFeatures--loader">
<div class="mdl-spinner mdl-js-spinner is-active" [mdl]></div>
</div>
</ng-container>
<ng-container *ngIf="error">
<div class="m-adminFeatures--error">
{{ error }}
</div>
</ng-container>
</div>
.m-adminFeatures {
max-width: 960px;
margin: 0 auto;
padding: 16px;
.m-adminFeatures--label {
margin-bottom: 8px;
padding: 0 4px;
font-size: 14px;
text-transform: uppercase;
letter-spacing: 0.5px;
@include m-theme() {
color: themed($m-grey-400);
}
}
.m-adminFeatures--table {
width: 100%;
margin-top: 24px;
th,
td {
text-align: center;
&.m-adminFeaturesTable--cell__first {
text-align: left;
}
}
th {
padding: 4px;
font-size: 14px;
text-transform: uppercase;
letter-spacing: 0.5px;
border-bottom: 1px solid;
@include m-theme() {
color: themed($m-grey-400);
border-color: themed($m-black);
}
}
td {
padding: 8px 4px;
&.m-adminFeaturesTable--cell__value {
font-size: 14px;
text-transform: uppercase;
letter-spacing: 0.5px;
@include m-theme() {
color: themed($m-grey-400);
}
}
&.m-adminFeaturesTable--cell__bestValue {
font-weight: bold;
@include m-theme() {
text-shadow: 0 0 3px rgba(themed($m-blue), 0.6);
color: themed($m-black);
}
}
}
}
.m-adminFeatures--loader {
text-align: center;
margin: 64px 0;
}
.m-adminFeatures--error {
text-align: center;
margin: 100px 0;
font-size: 28px;
@include m-theme() {
color: themed($m-red);
}
}
}
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
OnDestroy,
OnInit,
} from '@angular/core';
import { Client } from '../../../services/api/client';
import { ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs';
type ServicesEntityStruc = {
[service: string]: boolean | null;
};
type ResponseFeaturesStruc = Array<{
name: string;
services: ServicesEntityStruc;
}>;
@Component({
selector: 'm-admin--features',
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: 'admin-features.component.html',
})
export class AdminFeaturesComponent implements OnInit, OnDestroy {
isLoading: boolean;
for: string;
environment: string;
services: Array<string>;
features: ResponseFeaturesStruc;
error: string;
protected params$: Subscription;
constructor(
protected client: Client,
protected cd: ChangeDetectorRef,
protected route: ActivatedRoute
) {}
ngOnInit(): void {
this.params$ = this.route.params.subscribe(params => {
if (typeof params.for !== 'undefined') {
this.for = params.for;
this.load();
}
});
this.load();
}
ngOnDestroy(): void {
this.params$.unsubscribe();
}
async load(): Promise<void> {
this.isLoading = true;
this.error = '';
this.detectChanges();
try {
const response: any = await this.client.get('api/v2/admin/features', {
for: this.for || '',
});
this.environment = response.environment;
this.for = response.for;
this.services = response.services;
this.features = response.features;
} catch (e) {
this.error = (e && e.message) || 'Internal server error';
}
this.isLoading = false;
this.detectChanges();
}
get readableFor(): string {
if (!this.for) {
return 'Anonymous user';
}
return `@${this.for}`;
}
isBestService(
currentService: string,
services: ServicesEntityStruc
): boolean {
let bestService = this.services[0];
for (const service of this.services) {
if (services[service] !== null) {
bestService = service;
}
}
return currentService == bestService;
}
labelForValue(value: any): string {
if (value === false) {
return 'OFF';
} else if (value === null) {
return '\xa0';
} else if (!value) {
return '???';
}
return 'ON';
}
detectChanges(): void {
this.cd.markForCheck();
this.cd.detectChanges();
}
}
......@@ -14,6 +14,7 @@ import { AdminInteractions } from './controllers/admin/interactions/interactions
import { InteractionsTableComponent } from './controllers/admin/interactions/table/table.component';
import { AdminPurchasesComponent } from './controllers/admin/purchases/purchases.component';
import { AdminWithdrawals } from './controllers/admin/withdrawals/withdrawals.component';
import { AdminFeaturesComponent } from './controllers/admin/features/admin-features.component';
export const MINDS_DECLARATIONS: any[] = [
// Components
......@@ -35,4 +36,5 @@ export const MINDS_DECLARATIONS: any[] = [
AdminPurchasesComponent,
AdminWithdrawals,
AdminReportsDownload,
AdminFeaturesComponent,
];
......@@ -45,7 +45,7 @@
<h2>Experiments</h2>
<ul class="m-canaryExperiments__list">
<li>
Discovery algorithm by post age - 11th December '19
Server Side Rendering - 5th Feb 2020
</li>
</ul>
</div>
......
<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,
};
<ng-container *ngIf="error || channel; else loader">
<ng-container
*ngIf="
error || !proEnabled || !channel.pro_published || isOwner || isAdmin;
error ||
!proEnabled ||
!showPro ||
!channel.pro_published ||
isOwner ||
isAdmin;
else isProChannel
"
>
......
......@@ -28,6 +28,7 @@ export class ChannelContainerComponent implements OnInit, OnDestroy {
channel: MindsUser;
protected username: string;
protected showPro: boolean;
protected param$: Subscription;
......@@ -50,10 +51,11 @@ export class ChannelContainerComponent implements OnInit, OnDestroy {
this.param$ = this.route.params.subscribe(params => {
if (params['username']) {
this.username = params['username'];
this.showPro = !params['pro'] || params['pro'] !== '0';
if (
this.username &&
(!this.channel || this.channel.username != this.username)
(!this.channel || this.channel.username !== this.username)
) {
this.load();
}
......@@ -74,7 +76,7 @@ export class ChannelContainerComponent implements OnInit, OnDestroy {
}
async load() {
if (!this.username) {
if (!this.username || this.showPro === undefined) {
return;
}
......@@ -88,6 +90,7 @@ export class ChannelContainerComponent implements OnInit, OnDestroy {
this.channel = response.channel;
const shouldRedirectToProHandler =
this.showPro &&
!this.site.isProDomain &&
this.channel.pro_published &&
!this.isOwner &&
......
......@@ -114,7 +114,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
......
......@@ -18,7 +18,7 @@
*ngIf="group && (group['is:member'] || group.membership == 2)"
>
<!-- Sidebar -->
<div class="m-groupGrid__left">
<div class="m-groupGrid__left" *mIfBrowser>
<m-videochat></m-videochat>
<header
......@@ -328,7 +328,7 @@
<div class="m-groupGrid__right" [hidden]="!showRight">
<!-- Conversation filter -->
<minds-groups-profile-conversation [group]="group">
<minds-groups-profile-conversation [group]="group" *mIfBrowser>
</minds-groups-profile-conversation>
<!-- END: Conversation filter -->
</div>
......
......@@ -183,8 +183,9 @@ export class GroupsProfile {
}
async load() {
if (isPlatformServer(this.platformId)) return;
this.resetMarkers();
if (isPlatformBrowser(this.platformId)) {
this.resetMarkers();
}
this.error = '';
this.group = null;
......@@ -199,38 +200,41 @@ export class GroupsProfile {
if (this.updateMarkersSubscription)
this.updateMarkersSubscription.unsubscribe();
this.updateMarkersSubscription = this.updateMarkers
.getByEntityGuid(this.guid)
.subscribe(
(marker => {
// this.updateMarkersSubscription = this.updateMarkers.markers.subscribe(markers => {
if (!marker) return;
this.group.hasGathering$ = interval(1000).pipe(
throttle(() => interval(2000)), //only allow once per 2 seconds
startWith(0),
map(
() =>
[marker].filter(
marker =>
marker.entity_guid == this.group.guid &&
marker.marker == 'gathering-heartbeat' &&
marker.updated_timestamp > Date.now() / 1000 - 60 //1 minute tollerance
).length > 0
)
);
let hasMarker =
marker.read_timestamp < marker.updated_timestamp &&
marker.entity_guid == this.group.guid &&
marker.marker != 'gathering-heartbeat';
if (hasMarker) this.resetMarkers();
}).bind(this)
);
// Check for comment updates
this.joinCommentsSocketRoom();
if (isPlatformBrowser(this.platformId)) {
this.updateMarkersSubscription = this.updateMarkers
.getByEntityGuid(this.guid)
.subscribe(
(marker => {
// this.updateMarkersSubscription = this.updateMarkers.markers.subscribe(markers => {
if (!marker) return;
this.group.hasGathering$ = interval(1000).pipe(
throttle(() => interval(2000)), //only allow once per 2 seconds
startWith(0),
map(
() =>
[marker].filter(
marker =>
marker.entity_guid == this.group.guid &&
marker.marker == 'gathering-heartbeat' &&
marker.updated_timestamp > Date.now() / 1000 - 60 //1 minute tollerance
).length > 0
)
);
let hasMarker =
marker.read_timestamp < marker.updated_timestamp &&
marker.entity_guid == this.group.guid &&
marker.marker != 'gathering-heartbeat';
if (hasMarker) this.resetMarkers();
}).bind(this)
);
// Check for comment updates
this.joinCommentsSocketRoom();
}
this.updateMeta();
this.context.set('activity', {
......
......@@ -37,6 +37,12 @@
</div>
<div class="m-proChannelFooter__items m-proChannelFooter__socialItems">
<a [routerLink]="['/' + user.username + '/feed', { pro: '0' }]">
<img
alt="Minds"
src="https://cdn-assets.minds.com/front/dist/en/assets/logos/bulb.svg"
/>
</a>
<span *ngFor="let profile of footerSocialProfiles">
<a
*ngIf="profile.key && profile.value"
......
......@@ -3,10 +3,17 @@
margin-bottom: 24px;
color: var(--m-pro--text-color);
.m-proChannelFooter__items > * {
display: inline-block;
margin: 8px 16px;
vertical-align: middle;
.m-proChannelFooter__items {
> * {
display: inline-block;
margin: 8px 16px;
vertical-align: middle;
}
img {
height: 24px;
filter: grayscale(100%);
}
}
.m-proChannelFooter__socialItems {
......@@ -14,9 +21,13 @@
font-size: 1.5em;
}
.m-proChannelFooter__text {
text-transform: uppercase;
letter-spacing: 0.1em;
.m-proChannelFooter__static {
margin-bottom: 16px;
.m-proChannelFooter__text {
text-transform: uppercase;
letter-spacing: 0.1em;
}
}
a {
......
......@@ -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, Inject } 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,
......
......@@ -21,7 +21,7 @@ export class SocketsService {
subscriptions: any = {};
rooms: string[] = [];
debug: boolean = false;
public error$: BehaviorSubject<boolean>;
public error$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
constructor(
public session: Session,
......@@ -71,9 +71,7 @@ export class SocketsService {
setUpDefaultListeners() {
this.socket.on('connect', () => {
this.error$
? this.error$.next(false)
: (this.error$ = new BehaviorSubject<boolean>(false));
this.error$.next(false);
this.nz.run(() => {
if (this.debug)
console.log(`[ws]::connected to ${this.SOCKET_IO_SERVER}`);
......
src/assets/email-2020/confirmation-splash.jpg

26.6 KB

src/assets/email-2020/dl-android-app.png

4.88 KB

src/assets/email-2020/dl-ios-app.png

5.69 KB

This diff is collapsed.
src/assets/email-2020/sep.png

157 Bytes

src/assets/email-2020/verify-account-btn.png

4.62 KB

......@@ -85,20 +85,24 @@ $twitter: #03b3ee;
$whatsapp: #25d366;
$linkedin: #0071a1;
$navigation-item: #aeb0b8;
$navigation-item-hover: #a6a6a6;
$second-text-color: #7d7d82;
$second-text-color-dark: #aeb0b8;
$primary-border: #dce2e4;
$primary-border-dark: #404a4e;
// Theme maps
// 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,
m-alert: #e03c20,
m-link: #1b85d6,
m-btn--primary: #1b85d6,
// legacy colors
m-grey-950: $grey-950,
m-grey-900: $grey-900,
m-grey-800: $grey-800,
m-grey-700: $grey-700,
......@@ -151,13 +155,22 @@ $themes: (
m-twitter: $twitter,
m-whatsapp: $whatsapp,
m-linkedin: $linkedin,
m-navigation-item: $navigation-item,
m-navigation-item-hover: $navigation-item-hover,
m-second-text-color: $second-text-color,
m-primary-border: $primary-border,
),
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,
m-alert: #e03c20,
m-link: #1b85d6,
m-btn--primary: #1b85d6,
// 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),
......@@ -210,10 +223,6 @@ $themes: (
m-twitter: $twitter,
m-whatsapp: $whatsapp,
m-linkedin: $linkedin,
m-navigation-item: lighten($navigation-item, $percent),
m-navigation-item-hover: lighten($navigation-item-hover, $percent),
m-second-text-color: $second-text-color-dark,
m-primary-border: $primary-border-dark,
),
);
......@@ -278,9 +287,19 @@ $m-twitter: 'm-twitter';
$m-whatsapp: 'm-whatsapp';
$m-linkedin: 'm-linkedin';
$m-navigation-item: 'm-navigation-item';
$m-navigation-item-hover: 'm-navigation-item-hover';
$m-second-text-color: 'm-second-text-color';
$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: 'm-alert';
$m-link: 'm-link';
$m-btn--primary: 'm-btn--primary';
$m-primary-border: 'm-primary-border';
$m-borderRadius: 2px;
$m-boxShadowBlur: 10px;
$m-boxShadowOffset: 2px;