...
 
Commits (19)
......@@ -5,6 +5,6 @@ FROM node:13-alpine
COPY . /dist
CMD node /dist/server
CMD node "--max-old-space-size=2048" /dist/server
VOLUME ["/dist"]
\ No newline at end of file
......@@ -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';
......@@ -102,7 +103,10 @@ app.get('/undefined', (req, res) => {
// cache
const NodeCache = require('node-cache');
const myCache = new NodeCache({ stdTTL: 5 * 60, checkperiod: 120 });
const myCache = new NodeCache({
stdTTL: 2 * 60, // 2 minute cache
checkperiod: 60, // Check every minute
});
const cache = () => {
return (req, res, next) => {
......@@ -111,7 +115,7 @@ const cache = () => {
.filter(kv => kv[0] !== 'mwa' && kv[0] !== 'XSRF-TOKEN')
.join(':') || 'loggedout';
const key =
`__express__/${sessKey}/` +
`__express__/${req.headers.host}/${sessKey}/` +
(req.originalUrl || req.url) +
(isMobileOrTablet() ? '/mobile' : '/desktop');
const exists = myCache.has(key);
......@@ -131,6 +135,10 @@ const cache = () => {
};
};
app.get('node-cache-stats', (req, res) => {
res.sendResponse(myCache.getStats());
});
// All regular routes use the Universal engine
app.get('*', cache(), (req, res) => {
const http =
......@@ -171,6 +179,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,
......@@ -432,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,
......
......@@ -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="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 &&
......
......@@ -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
}));
......
......@@ -2,7 +2,7 @@ import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { CommonModule as NgCommonModule } from '@angular/common';
import { CommonModule } from '../../common/common.module';
import { Client } from '../../common/api/client.service';
import { Storage } from '../../services/storage';
import { CookieService } from '../../common/services/cookie.service';
import { ExperimentDirective } from './experiment.directive';
import { ExperimentsService } from './experiments.service';
......@@ -14,9 +14,9 @@ import { ExperimentsService } from './experiments.service';
providers: [
{
provide: ExperimentsService,
useFactory: (_client, _storage) =>
new ExperimentsService(_client, _storage),
deps: [Client, Storage],
useFactory: (_client, cookieService) =>
new ExperimentsService(_client, cookieService),
deps: [Client, CookieService],
},
],
})
......
......@@ -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 } 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

......@@ -89,7 +89,20 @@ $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,
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,
......@@ -144,7 +157,20 @@ $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,
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),
......@@ -260,3 +286,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: 'm-alert';
$m-link: 'm-link';
$m-btn--primary: 'm-btn--primary';
$m-borderRadius: 2px;
$m-boxShadowBlur: 10px;
$m-boxShadowOffset: 2px;