...
 
Commits (86)
image: markharding/minds-front-base
image: minds/ci-front:latest
stages:
- test
......@@ -17,7 +17,7 @@ variables:
CYPRESS_CACHE_FOLDER: '$CI_PROJECT_DIR/cache/Cypress'
test:
image: circleci/node:8-browsers
image: circleci/node:13-browsers
stage: test
script:
- npm ci
......@@ -72,6 +72,7 @@ qa:manual:
only:
refs:
- master
- epic/SSR
- production
- test/gitlab-ci
allow_failure: false
......@@ -87,8 +88,10 @@ build:review:
script:
- npm ci && npm install -g gulp-cli
- npm run postinstall
- gulp build.sass && gulp build.sass ##weird build needs to be run twice for now
- gulp build.sass --deploy-url=/en && gulp build.sass --deploy-url=/en ##weird build needs to be run twice for now
- sh build/base-locale.sh dist
- ng run minds:server:production
- npm run compile:server
artifacts:
name: '$CI_COMMIT_REF_SLUG'
paths:
......@@ -96,6 +99,7 @@ build:review:
except:
refs:
- master
- epic/SSR
- production
- test/gitlab-ci
......@@ -108,13 +112,18 @@ build:production:en:
- npm run postinstall
- gulp build.sass --deploy-url=https://cdn-assets.minds.com/front/dist/en && gulp build.sass --deploy-url=https://cdn-assets.minds.com/front/dist/en ##weird build needs to be run twice for now
- sh build/base-locale.sh dist https://cdn-assets.minds.com/front/dist
- ng run minds:server:production
- npm run compile:server
artifacts:
name: '$CI_COMMIT_REF_SLUG'
paths:
- dist/en
- dist/server.js
- dist/server
only:
refs:
- master
- epic/SSR
- production
- test/gitlab-ci
......@@ -134,6 +143,7 @@ build:production:i18n:
only:
refs:
- master
- epic/SSR
- production
- test/gitlab-ci
......@@ -159,13 +169,14 @@ prepare:review:
- docker:dind
script:
- docker login -u gitlab-ci-token -p ${CI_BUILD_TOKEN} ${CI_REGISTRY}
- docker build -t $CI_REGISTRY_IMAGE/front-init:$CI_PIPELINE_ID -f containers/front-init/Dockerfile dist/.
- docker push $CI_REGISTRY_IMAGE/front-init:$CI_PIPELINE_ID
- docker build -t $CI_REGISTRY_IMAGE/server:$CI_PIPELINE_ID -f containers/server/Dockerfile dist/.
- docker push $CI_REGISTRY_IMAGE/server:$CI_PIPELINE_ID
dependencies:
- build:review
except:
refs:
- master
- epic/SSR
- production
- test/gitlab-ci
......@@ -176,6 +187,7 @@ prepare:review:sentry:
except:
refs:
- master
- epic/SSR
- production
- test/gitlab-ci
dependencies:
......@@ -188,11 +200,12 @@ prepare:production:
- docker:dind
script:
- docker login -u gitlab-ci-token -p ${CI_BUILD_TOKEN} ${CI_REGISTRY}
- docker build -t $CI_REGISTRY_IMAGE/front-init:$CI_PIPELINE_ID -f containers/front-init/Dockerfile dist/.
- docker push $CI_REGISTRY_IMAGE/front-init:$CI_PIPELINE_ID
- docker build -t $CI_REGISTRY_IMAGE/server:$CI_PIPELINE_ID -f containers/server/Dockerfile dist/.
- docker push $CI_REGISTRY_IMAGE/server:$CI_PIPELINE_ID
only:
refs:
- master
- epic/SSR
- production
- test/gitlab-ci
dependencies:
......@@ -206,6 +219,7 @@ prepare:production:sentry:
only:
refs:
- master
- epic/SSR
- production
- test/gitlab-ci
dependencies:
......@@ -230,6 +244,7 @@ prepare:production:sentry:
except:
refs:
- master
- epic/SSR
- production
- test/gitlab-ci
......@@ -242,8 +257,8 @@ review:start:
- "helm upgrade \
--install \
--reuse-values \
--set frontInit.image.repository=$CI_REGISTRY_IMAGE/front-init \
--set-string frontInit.image.tag=$CI_PIPELINE_ID \
--set front.image.repository=$CI_REGISTRY_IMAGE/server \
--set-string front.image.tag=$CI_PIPELINE_ID \
--set domain=$CI_BUILD_REF_SLUG.$KUBE_INGRESS_BASE_DOMAIN \
--set elasticsearch.clusterName=$CI_BUILD_REF_SLUG--elasticsearch \
--wait \
......@@ -258,6 +273,7 @@ review:start:
except:
refs:
- master
- epic/SSR
- production
- test/gitlab-ci
......@@ -278,10 +294,10 @@ review:stop:
## Sync assets with CDN
- aws s3 sync dist $S3_REPOSITORY_URL
- $(aws ecr get-login --no-include-email --region us-east-1)
## Update docker front-init container
## Update docker server container
- docker login -u gitlab-ci-token -p ${CI_BUILD_TOKEN} ${CI_REGISTRY}
- docker pull $CI_REGISTRY_IMAGE/front-init:$CI_PIPELINE_ID
- docker tag $CI_REGISTRY_IMAGE/front-init:$CI_PIPELINE_ID $ECR_REPOSITORY_URL:$IMAGE_LABEL
- docker pull $CI_REGISTRY_IMAGE/server:$CI_PIPELINE_ID
- docker tag $CI_REGISTRY_IMAGE/server:$CI_PIPELINE_ID $ECR_REPOSITORY_URL:$IMAGE_LABEL
- docker push $ECR_REPOSITORY_URL:$IMAGE_LABEL
## Deploy the new container in rolling restart
- aws ecs update-service --service=$ECS_SERVICE --force-new-deployment --region us-east-1 --cluster=$ECS_CLUSTER
......@@ -290,11 +306,6 @@ review:stop:
dependencies:
- build:production:en
- build:production:i18n
only:
refs:
- master
- production
- test/gitlab-ci
staging:fpm:
<<: *deploy
......@@ -305,6 +316,28 @@ staging:fpm:
environment:
name: staging
url: https://www.minds.com # requires staging cookie
only:
refs:
- master
- epic/SSR
- test/gitlab-ci
review:preprod:
<<: *deploy
stage: review
when: manual
allow_failure: true
variables:
IMAGE_LABEL: 'preprod'
ECS_SERVICE: $ECS_APP_PREPROD_SERVICE
environment:
name: preprod
url: https://www.minds.com # requires preprod cookie
# dependencies:
# - build:review
# except:
# refs:
# - master
deploy:canary:
<<: *deploy
......@@ -317,6 +350,11 @@ deploy:canary:
url: https://www.minds.com/?canary=1 # requires canary cookie
when: manual
allow_failure: false # prevents auto deploy to full production
only:
refs:
- master
- epic/SSR
- test/gitlab-ci
deploy:production:
<<: *deploy
......@@ -329,6 +367,11 @@ deploy:production:
url: https://www.minds.com
when: delayed
start_in: 12 hours # reduce? can always be deployed manually earlier too
only:
refs:
- master
- epic/SSR
- test/gitlab-ci
#################
# Cleanup stage #
......@@ -341,5 +384,6 @@ cleanup:review: # We stop the review site after the e2e tests have run
except:
refs:
- master
- epic/SSR
- production
- test/gitlab-ci
......@@ -3,7 +3,7 @@
"version": 1,
"newProjectRoot": "projects",
"projects": {
"v5.x": {
"minds": {
"root": "",
"sourceRoot": "src",
"projectType": "application",
......@@ -12,10 +12,11 @@
"builder": "@angular-devkit/build-angular:browser",
"options": {
"outputPath": "dist",
"index": "src/index.php",
"index": "src/index.html",
"main": "src/main.ts",
"tsConfig": "src/tsconfig.app.json",
"polyfills": "src/polyfills.ts",
"extractCss": true,
"assets": ["src/assets", "src/favicon.ico"],
"styles": [
"node_modules/material-design-lite/dist/material.blue_grey-amber.min.css",
......@@ -64,22 +65,22 @@
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "v5.x:build"
"browserTarget": "minds:build"
},
"configurations": {
"production": {
"browserTarget": "v5.x:build:production"
"browserTarget": "minds:build:production"
},
"hmr": {
"hmr": true,
"browserTarget": "v5.x:build:hmr"
"browserTarget": "minds:build:hmr"
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"browserTarget": "v5.x:build"
"browserTarget": "minds:build"
}
},
"test": {
......@@ -102,10 +103,28 @@
"tsConfig": ["src/tsconfig.app.json", "src/tsconfig.spec.json"],
"exclude": ["**/node_modules/**"]
}
},
"server": {
"builder": "@angular-devkit/build-angular:server",
"options": {
"outputPath": "dist/server",
"main": "src/main.server.ts",
"tsConfig": "src/tsconfig.server.json"
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
]
}
}
}
}
},
"v5.x-e2e": {
"minds-e2e": {
"root": "e2e",
"sourceRoot": "e2e",
"projectType": "application",
......@@ -114,7 +133,7 @@
"builder": "@angular-devkit/build-angular:protractor",
"options": {
"protractorConfig": "./protractor.conf.js",
"devServerTarget": "v5.x:serve"
"devServerTarget": "minds:serve"
}
},
"lint": {
......@@ -127,7 +146,7 @@
}
}
},
"defaultProject": "v5.x",
"defaultProject": "minds",
"schematics": {
"@schematics/angular:component": {
"prefix": "m",
......
#!/bin/sh
export NODE_OPTIONS="--max-old-space-size=3584"
ng build --prod --vendor-chunk --output-path="$1/en/" --deploy-url="$2/en/" --build-optimizer=false
ng build --prod --vendor-chunk --output-path="$1/en/" --deploy-url="$2/en/" --build-optimizer=true --stats-json
FROM alpine:edge
RUN apk add --no-cache \
chromium \
git \
build-base \
libstdc++ \
make \
g++ \
python \
curl \
udev \
nodejs=8.9.1-r0 \
libsass
RUN npm install -g typescript gulp @angular/cli
ENV CHROME_BIN=/usr/bin/chromium-browser
\ No newline at end of file
FROM node:13-alpine
RUN apk add --no-cache \
git \
libsass
RUN npm install -g typescript @angular/cli
# Gitlab CI has limited memory
ENV NODE_OPTIONS="--max-old-space-size=1024"
\ No newline at end of file
###
# Before running this container, ensure the server is built
###
FROM node:13-alpine
COPY . /dist
CMD node "--max-old-space-size=2048" /dist/server
VOLUME ["/dist"]
\ No newline at end of file
This diff is collapsed.
{
"name": "v5.x",
"name": "minds",
"version": "0.0.0",
"license": "MIT",
"scripts": {
......@@ -7,37 +7,53 @@
"start": "ng serve",
"preinstall": "git config core.hooksPath .git/hooks/",
"prebuild": "gulp build.sass",
"build": "ng build --prod",
"prebuild-dev": "gulp build.sass --deploy-url=http://localhost/en",
"build-dev": "ng build --output-path dist/en --deploy-url=/en/ --watch=true --poll=800",
"serve-dev": "ng serve --host=0.0.0.0 --deploy-url=/en/ --configuration=hmr --hmr --poll=800 --progress",
"build": "sh build/base-locale.sh dist",
"prebuild-dev": "gulp build.sass --deploy-url=http://localhost",
"build-dev": "npm run build:dev",
"serve-dev": "npm run server:ssr",
"build:dev": "ng build --output-path dist --deploy-url=/ --watch=true --poll=800",
"serve:dev": "ng serve --host=0.0.0.0 --deploy-url=/ --configuration=hmr --hmr --poll=800 --progress --disableHostCheck=true",
"test": "ng test",
"lint": "ng lint",
"e2e": "cypress run --debug",
"e2e-open": "cypress open",
"postinstall": "node patch.js"
"postinstall": "node patch.js",
"compile:server": "webpack --config webpack.server.config.js --progress --colors",
"serve:ssr": "node dist/server",
"build:ssr": "npm run build:client-and-server-bundles && npm run compile:server",
"build:client-and-server-bundles": "npm run build && ng run minds:server:production",
"bundle-report": "webpack-bundle-analyzer dist/en/stats.json"
},
"private": true,
"dependencies": {
"@angular/animations": "~8.0.3",
"@angular/cdk": "^8.2.3",
"@angular/common": "~8.0.3",
"@angular/compiler": "~8.0.3",
"@angular/core": "~8.0.3",
"@angular/forms": "~8.0.3",
"@angular/animations": "~8.1.1",
"@angular/common": "~8.1.1",
"@angular/compiler": "~8.1.1",
"@angular/core": "~8.1.1",
"@angular/forms": "~8.1.1",
"@angular/http": "~7.2.0",
"@angular/platform-browser": "~8.0.3",
"@angular/platform-browser-dynamic": "~8.0.3",
"@angular/router": "~8.0.3",
"@angular/platform-browser": "~8.1.1",
"@angular/platform-browser-dynamic": "~8.1.1",
"@angular/platform-server": "~8.1.1",
"@angular/router": "~8.1.1",
"@gorniv/ngx-universal": "^2.0.1",
"@nguniversal/common": "~8.1.1",
"@nguniversal/express-engine": "^8.1.1",
"@nguniversal/module-map-ngfactory-loader": "~8.1.1",
"@nguniversal/socket-engine": "~8.1.1",
"@angular/cdk": "^8.2.3",
"@sentry/browser": "^5.11.1",
"angular-plotly.js": "^1.3.2",
"bn.js": "^4.11.8",
"braintree-web": "3.41.0",
"cookie-parser": "^1.4.4",
"core-js": "~2.6.2",
"dexie": "^2.0.4",
"ethjs": "~0.4.0",
"ethjs-account": "^0.1.4",
"ethjs-provider-signer": "^0.1.4",
"ethjs-signer": "0.1.1",
"express-http-proxy": "^1.6.0",
"global": "^4.3.2",
"material-design-icons": "~3.0.1",
"material-design-lite": "~1.3.0",
......@@ -46,6 +62,7 @@
"ng-pick-datetime": "^7.0.0",
"ngx-drag-drop": "^2.0.0",
"ngx-plyr": "^3.0.1",
"node-cache": "^5.1.0",
"plotly.js": "^1.47.4",
"plyr": "^3.5.6",
"qrcodejs2": "0.0.2",
......@@ -53,14 +70,15 @@
"socket.io-client": "^2.2.0",
"textarea-caret": "^3.1.0",
"tslib": "~1.9.3",
"webpack-node-externals": "^1.7.2",
"webtorrent": "^0.103.0",
"zone.js": "~0.9.1"
},
"devDependencies": {
"@angular-devkit/build-angular": "~0.13.9",
"@angular/cli": "^7.2.1",
"@angular/compiler-cli": "~8.0.3",
"@angular/language-service": "~8.0.3",
"@angular-devkit/build-angular": "0.803.21",
"@angular/cli": "^8.1.1",
"@angular/compiler-cli": "~8.1.1",
"@angular/language-service": "~8.1.1",
"@angularclass/hmr": "^2.1.3",
"@types/jasmine": "~2.8.8",
"@types/jasminewd2": "~2.0.4",
......@@ -90,7 +108,9 @@
"protractor": "~5.4.2",
"ts-node": "~7.0.0",
"tslint": "~5.12.0",
"typescript": "~3.4.5"
"typescript": "~3.4.5",
"webpack-bundle-analyzer": "^3.6.0",
"webpack-cli": "^3.3.10"
},
"husky": {
"hooks": {
......
import 'zone.js/dist/zone-node';
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';
import { enableProdMode } from '@angular/core';
import { XhrFactory } from '@angular/common/http';
import { NgxRequest, NgxResponce } from '@gorniv/ngx-universal';
import * as express from 'express';
import * as compression from 'compression';
import * as cookieparser from 'cookie-parser';
import isMobileOrTablet from './src/app/helpers/is-mobile-or-tablet';
const domino = require('domino');
// Faster server renders w/ Prod mode (dev mode never needed)
enableProdMode();
// Express server
const app = express();
// gzip
app.use(compression());
// cokies
app.use(cookieparser());
const PORT = process.env.PORT || 4200;
const DIST_FOLDER = join(process.cwd(), 'dist/en');
const template = readFileSync(join(DIST_FOLDER, 'index.html')).toString();
const win = domino.createWindow(template);
global['window'] = win;
global['Node'] = win.Node;
global['navigator'] = win.navigator;
global['screen'] = { width: 0, height: 0 };
global['Event'] = win.Event;
global['Event']['prototype'] = win.Event.prototype;
global['KeyboardEvent'] = global['Event'];
global['document'] = win.document;
global['window']['Promise'] = global.Promise;
global['window']['localStorage'] = {
getItem: () => null,
setItem: () => {},
removeItem: () => {},
};
global['localStorage'] = global['window']['localStorage'];
global['window']['scrollTo'] = pos => {};
Object.defineProperty(window.document, 'cookie', {
writable: true,
value: 'myCookie=omnomnom',
});
Object.defineProperty(window.document, 'referrer', {
writable: true,
value: '',
});
Object.defineProperty(window.document, 'localStorage', {
writable: true,
value: global['window']['localStorage'],
});
// * NOTE :: leave this as require() since this file is built Dynamically from webpack
const {
AppServerModuleNgFactory,
LAZY_MODULE_MAP,
} = require('./dist/server/main');
const {
provideModuleMap,
} = require('@nguniversal/module-map-ngfactory-loader');
app.engine(
'html',
ngExpressEngine({
bootstrap: AppServerModuleNgFactory,
providers: [provideModuleMap(LAZY_MODULE_MAP)],
})
);
app.set('view engine', 'html');
app.set('views', DIST_FOLDER);
// Server static files from dist folder
app.get('*.*', express.static(DIST_FOLDER));
// Socket.io hitting wrong endpoint (dev?)
app.get('/socket.io', (req, res) => {
res.send('You are using the wrong domain.');
});
// /undefined is an issue with angular
app.get('/undefined', (req, res) => {
res.send('There was problem');
});
// cache
const NodeCache = require('node-cache');
const myCache = new NodeCache({
stdTTL: 2 * 60, // 2 minute cache
checkperiod: 60, // Check every minute
});
const cache = () => {
return (req, res, next) => {
const sessKey =
Object.entries(req.cookies)
.filter(kv => kv[0] !== 'mwa' && kv[0] !== 'XSRF-TOKEN')
.join(':') || 'loggedout';
const key =
`__express__/${req.headers.host}/${sessKey}/` +
(req.originalUrl || req.url) +
(isMobileOrTablet() ? '/mobile' : '/desktop');
const exists = myCache.has(key);
if (exists) {
console.log(`from cache: ${key}`);
const cachedBody = myCache.get(key);
res.send(cachedBody);
return;
} else {
res.sendResponse = res.send;
res.send = body => {
myCache.set(key, body);
res.sendResponse(body);
};
next();
}
};
};
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 =
req.headers['x-forwarded-proto'] === undefined
? 'http'
: req.headers['x-forwarded-proto'];
const url = req.originalUrl;
// tslint:disable-next-line:no-console
console.time(`GET: ${url}`);
res.render(
'index',
{
req: req,
res: res,
// provers from server
providers: [
// for http and cookies
{
provide: REQUEST,
useValue: req,
},
{
provide: RESPONSE,
useValue: res,
},
// for cookie
{
provide: NgxRequest,
useValue: req,
},
{
provide: NgxResponce,
useValue: res,
},
// for absolute path
{
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) => {
if (!!err) {
throw err;
}
// tslint:disable-next-line:no-console
console.timeEnd(`GET: ${url}`);
res.send(html);
}
);
});
// Start up the Node server
app.listen(PORT, () => {
console.log(`Node server listening on http://localhost:${PORT}`);
});
import { NgModule } from '@angular/core';
import { MindsModule } from './app.module';
import { Minds } from './app.component';
import * as PlotlyJS from 'plotly.js/dist/plotly-basic.min.js';
import { PlotlyModule } from 'angular-plotly.js';
import { CookieModule } from '@gorniv/ngx-universal';
PlotlyModule.plotlyjs = PlotlyJS;
@NgModule({
imports: [MindsModule, PlotlyModule, CookieModule],
bootstrap: [Minds],
providers: [
{ provide: 'ORIGIN_URL', useValue: location.origin },
{ provide: 'QUERY_STRING', useValue: location.search || '' },
],
})
export class AppBrowserModule {}
<ng-container *ngIf="ready">
<ng-container *ngIf="!isProDomain">
<m-v2-topbar *mIfFeature="'top-feeds'; else legacyTopbar">
<m-v2-topbar>
<ng-container search>
<m-search--bar [defaultSizes]="false"></m-search--bar>
</ng-container>
......@@ -11,18 +11,6 @@
></m-notifications--topbar-toggle>
</ng-container>
</m-v2-topbar>
<ng-template #legacyTopbar>
<m-topbar class="m-noshadow">
<ng-container search>
<m-search--bar></m-search--bar>
</ng-container>
<ng-container icons>
<m-notifications--topbar-toggle></m-notifications--topbar-toggle>
<m-wallet--topbar-toggle></m-wallet--topbar-toggle>
</ng-container>
</m-topbar>
</ng-template>
<m-sidebar--markers
[class.has-v2-navbar]="featuresService.has('top-feeds')"
......@@ -30,10 +18,11 @@
</ng-container>
<m-body
[class.has-markers-sidebar]="session.isLoggedIn() && !isProDomain"
[class.has-v2-navbar]="featuresService.has('top-feeds')"
[class.is-pro-domain]="isProDomain"
>
<m-emailConfirmation></m-emailConfirmation>
<m-emailConfirmation *ngIf="!isProDomain"></m-emailConfirmation>
<m-announcement [id]="'festival:sale'" *mIfFeature="'radiocity'">
<span
class="m-blockchain--wallet-address-notice--action"
......@@ -47,16 +36,23 @@
<router-outlet></router-outlet>
</m-body>
<m-messenger *ngIf="minds.LoggedIn && !isProDomain"></m-messenger>
<ng-container *mIfBrowser>
<m-messenger *ngIf="session.isLoggedIn() && !isProDomain"></m-messenger>
</ng-container>
<m-hovercard-popup></m-hovercard-popup>
<m-overlay-modal></m-overlay-modal>
<m--blockchain--transaction-overlay></m--blockchain--transaction-overlay>
<m-modal--tos-updated *ngIf="session.isLoggedIn()"></m-modal--tos-updated>
<m-juryDutySession__summons
*ngIf="session.isLoggedIn() && !isProDomain"
></m-juryDutySession__summons>
<ng-container *mIfBrowser>
<m-juryDutySession__summons
*ngIf="session.isLoggedIn() && !isProDomain"
></m-juryDutySession__summons>
</ng-container>
<m-modal-signup
*ngIf="!isProDomain && !session.getLoggedInUser()"
......
import { ChangeDetectorRef, Component, HostBinding } from '@angular/core';
import {
ChangeDetectorRef,
Component,
PLATFORM_ID,
Inject,
HostBinding,
} from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
import { NotificationService } from './modules/notifications/notification.service';
import { AnalyticsService } from './services/analytics';
......@@ -10,7 +17,7 @@ import { ContextService } from './services/context.service';
import { Web3WalletService } from './modules/blockchain/web3-wallet.service';
import { Client } from './services/api/client';
import { WebtorrentService } from './modules/webtorrent/webtorrent.service';
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
import { ActivatedRoute, NavigationEnd, Router, Route } from '@angular/router';
import { ChannelOnboardingService } from './modules/onboarding/channel/onboarding.service';
import { BlockListService } from './common/services/block-list.service';
import { FeaturesService } from './services/features.service';
......@@ -22,17 +29,17 @@ import { SsoService } from './common/services/sso.service';
import { Subscription } from 'rxjs';
import { RouterHistoryService } from './common/services/router-history.service';
import { PRO_DOMAIN_ROUTES } from './modules/pro/pro.module';
import { ConfigsService } from './common/services/configs.service';
import { MetaService } from './common/services/meta.service';
import { filter, map, mergeMap, first } from 'rxjs/operators';
@Component({
moduleId: module.id,
selector: 'm-app',
templateUrl: 'app.component.html',
})
export class Minds {
name: string;
minds = window.Minds;
ready: boolean = false;
showOnboarding: boolean = false;
......@@ -41,6 +48,8 @@ export class Minds {
protected router$: Subscription;
protected routerConfig: Route[];
constructor(
public session: Session,
public route: ActivatedRoute,
......@@ -59,22 +68,44 @@ export class Minds {
public featuresService: FeaturesService,
public themeService: ThemeService,
private bannedService: BannedService,
@Inject(PLATFORM_ID) private platformId: Object,
private diagnostics: DiagnosticsService,
private routerHistoryService: RouterHistoryService,
private site: SiteService,
private sso: SsoService,
private cd: ChangeDetectorRef
private metaService: MetaService,
private configs: ConfigsService,
private cd: ChangeDetectorRef,
private socketsService: SocketsService
) {
this.name = 'Minds';
if (this.site.isProDomain) {
this.router.resetConfig(PRO_DOMAIN_ROUTES);
}
}
async ngOnInit() {
// MH: does loading meta tags before the configs have been set cause issues?
this.router$ = this.router.events
.pipe(
filter(e => e instanceof NavigationEnd),
map(() => this.route),
map(route => {
while (route.firstChild) route = route.firstChild;
return route;
}),
// filter(route => route.outlet === 'primary')
mergeMap(route => route.data)
)
.subscribe(data => {
this.metaService.reset(data);
});
try {
this.diagnostics.setUser(this.minds.user);
this.updateMeta(); // Because the router is setup before our configs
// Setup sentry/diagnostic configs
this.diagnostics.setUser(this.configs.get('user'));
this.diagnostics.listen(); // Listen for user changes
if (this.sso.isRequired()) {
......@@ -97,7 +128,9 @@ export class Minds {
async initialize() {
this.blockListService.fetch();
if (!this.site.isProDomain) {
if (this.site.isProDomain) {
this.site.listen();
} else {
this.notificationService.getNotifications();
}
......@@ -107,16 +140,12 @@ export class Minds {
this.showOnboarding = await this.onboardingService.showModal();
}
if (this.minds.user.language !== this.minds.language) {
console.log(
'[app]:: language change',
this.minds.user.language,
this.minds.language
);
const user = this.session.getLoggedInUser();
const language = this.configs.get('language');
setTimeout(() => {
window.location.reload(true);
});
if (user.language !== language) {
console.log('[app]:: language change', user.language, language);
window.location.reload(true);
}
}
});
......@@ -148,11 +177,14 @@ export class Minds {
this.webtorrent.setUp();
this.themeService.setUp();
this.socketsService.setUp();
}
ngOnDestroy() {
this.loginReferrer.unlisten();
this.scrollToTop.unlisten();
this.router$.unsubscribe();
}
@HostBinding('class') get cssColorSchemeOverride() {
......@@ -167,6 +199,12 @@ export class Minds {
return this.site.isProDomain;
}
private updateMeta(): void {
let route = this.route;
while (route.firstChild) route = route.firstChild;
this.metaService.reset(route.snapshot.data);
}
detectChanges() {
this.cd.markForCheck();
this.cd.detectChanges();
......
......@@ -3,13 +3,18 @@ import {
NgModule,
Injectable,
ErrorHandler,
APP_INITIALIZER,
APP_BOOTSTRAP_LISTENER,
} from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import {
BrowserModule,
BrowserTransferStateModule,
} from '@angular/platform-browser';
// import { TransferHttpCacheModule } from '@nguniversal/common';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
import { CaptchaModule } from './modules/captcha/captcha.module';
import { environment } from '../environments/environment';
import { Minds } from './app.component';
......@@ -72,14 +77,10 @@ import { ChannelContainerModule } from './modules/channel-container/channel-cont
import { UpgradesModule } from './modules/upgrades/upgrades.module';
import * as Sentry from '@sentry/browser';
import { CookieModule } from '@gorniv/ngx-universal';
import { HomepageModule } from './modules/homepage/homepage.module';
import { OnboardingV2Module } from './modules/onboarding-v2/onboarding.module';
Sentry.init({
dsn: 'https://3f786f8407e042db9053434a3ab527a2@sentry.io/1538008', // TODO: do not hardcard
release: environment.version,
environment: (<any>window.Minds).environment || 'development',
});
import { ConfigsService } from './common/services/configs.service';
@Injectable()
export class SentryErrorHandler implements ErrorHandler {
......@@ -100,12 +101,18 @@ export class SentryErrorHandler implements ErrorHandler {
MINDS_PLUGIN_DECLARATIONS,
],
imports: [
BrowserModule,
BrowserAnimationsModule,
BrowserModule.withServerTransition({ appId: 'm-app' }),
BrowserTransferStateModule,
CookieModule.forRoot(),
// TransferHttpCacheModule,
//BrowserAnimationsModule,
ReactiveFormsModule,
FormsModule,
HttpClientModule,
RouterModule.forRoot(MindsAppRoutes, { onSameUrlNavigation: 'reload' }),
RouterModule.forRoot(MindsAppRoutes, {
// initialNavigation: 'enabled',
onSameUrlNavigation: 'reload',
}),
CaptchaModule,
CommonModule,
ProModule, // NOTE: Pro Module should be declared _BEFORE_ anything else
......@@ -151,6 +158,7 @@ export class SentryErrorHandler implements ErrorHandler {
CanaryModule,
ChannelsModule,
UpgradesModule,
//PlotlyModule,
//last due to :username route
ChannelContainerModule,
......@@ -160,6 +168,12 @@ export class SentryErrorHandler implements ErrorHandler {
MindsAppRoutingProviders,
MINDS_PROVIDERS,
MINDS_PLUGIN_PROVIDERS,
{
provide: APP_INITIALIZER,
useFactory: configs => () => configs.loadFromRemote(),
deps: [ConfigsService],
multi: true,
},
],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
......
import { NgModule } from '@angular/core';
import { ServerModule } from '@angular/platform-server';
import { ModuleMapLoaderModule } from '@nguniversal/module-map-ngfactory-loader';
import { ServerTransferStateModule } from '@angular/platform-server';
import { XhrFactory } from '@angular/common/http';
import * as xhr2 from 'xhr2';
import { MindsModule } from './app.module';
import { Minds } from './app.component';
import { PlotlyModule } from 'angular-plotly.js';
import { CookieService, CookieBackendService } from '@gorniv/ngx-universal';
PlotlyModule.plotlyjs = {
plot: () => {
// This simply satisfies the isValid() error
},
};
// activate cookie for server-side rendering
export class ServerXhr implements XhrFactory {
build(): XMLHttpRequest {
xhr2.prototype._restrictedHeaders.cookie = false;
return new xhr2.XMLHttpRequest();
}
}
@NgModule({
imports: [
MindsModule,
ServerModule,
ModuleMapLoaderModule,
ServerTransferStateModule,
PlotlyModule,
],
providers: [
{ provide: XhrFactory, useClass: ServerXhr },
{
provide: CookieService,
useClass: CookieBackendService,
},
],
bootstrap: [Minds],
})
export class AppServerModule {}
import { Cookie } from '../../services/cookie';
import { CookieService } from '../services/cookie.service';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { environment } from '../../../environments/environment';
......@@ -7,13 +7,12 @@ import { environment } from '../../../environments/environment';
*/
export class MindsHttpClient {
base: string = '/';
cookie: Cookie = new Cookie();
static _(http: HttpClient) {
return new MindsHttpClient(http);
static _(http: HttpClient, cookie: CookieService) {
return new MindsHttpClient(http, cookie);
}
constructor(public http: HttpClient) {}
constructor(public http: HttpClient, private cookie: CookieService) {}
/**
* Return a GET request
......
import { NgModule } from '@angular/core';
import { CommonModule as NgCommonModule } from '@angular/common';
import { NgModule, inject, Injector } from '@angular/core';
import {
CommonModule as NgCommonModule,
isPlatformServer,
Location,
} from '@angular/common';
import { RouterModule, Router, Routes } from '@angular/router';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
......@@ -43,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';
......@@ -60,6 +63,7 @@ import { InlineEditorComponent } from './components/editors/inline-editor.compon
import { AttachmentService } from '../services/attachment';
import { MaterialBoundSwitchComponent } from './components/material/bound-switch.component';
import { IfFeatureDirective } from './directives/if-feature.directive';
import { IfBrowserDirective } from './directives/if-browser.directive';
import { MindsEmoji } from './components/emoji/emoji';
import { CategoriesSelectorComponent } from './components/categories/selector/selector.component';
import { CategoriesSelectedComponent } from './components/categories/selected/selected.component';
......@@ -115,7 +119,7 @@ import { ToggleComponent } from './components/toggle/toggle.component';
import { MarketingAsFeaturedInComponent } from './components/marketing/as-featured-in.component';
import { SidebarMenuComponent } from './components/sidebar-menu/sidebar-menu.component';
import { ChartV2Component } from './components/chart-v2/chart-v2.component';
import * as PlotlyJS from 'plotly.js/dist/plotly.js';
//import * as PlotlyJS from 'plotly.js/dist/plotly.js';
import { PlotlyModule } from 'angular-plotly.js';
import { PageLayoutComponent } from './components/page-layout/page-layout.component';
import { DashboardLayoutComponent } from './components/dashboard-layout/dashboard-layout.component';
......@@ -132,11 +136,15 @@ import { V2TopbarService } from './layout/v2-topbar/v2-topbar.service';
import { DateDropdownsComponent } from './components/date-dropdowns/date-dropdowns.component';
import { SidebarMarkersService } from './layout/sidebar/markers.service';
import { EmailConfirmationComponent } from './components/email-confirmation/email-confirmation.component';
import { ConfigsService } from './services/configs.service';
import { CookieService } from './services/cookie.service';
import { MetaService } from './services/meta.service';
import { Title, Meta } from '@angular/platform-browser';
import { MediaProxyService } from './services/media-proxy.service';
import { HorizontalFeedService } from './services/horizontal-feed.service';
import { FormInputCheckboxComponent } from './components/forms/checkbox/checkbox.component';
import { AttachmentPasteDirective } from './directives/paste/attachment-paste.directive';
PlotlyModule.plotlyjs = PlotlyJS;
const routes: Routes = [
{
path: 'email-confirmation',
......@@ -200,7 +208,6 @@ const routes: Routes = [
MDL_DIRECTIVES,
DateSelectorComponent,
MindsAvatar,
CaptchaComponent,
Textarea,
InlineEditorComponent,
......@@ -216,6 +223,7 @@ const routes: Routes = [
MaterialBoundSwitchComponent,
IfFeatureDirective,
IfBrowserDirective,
CategoriesSelectorComponent,
CategoriesSelectedComponent,
......@@ -262,6 +270,7 @@ const routes: Routes = [
ShadowboxSubmitButtonComponent,
EmailConfirmationComponent,
DateDropdownsComponent,
FormInputCheckboxComponent,
],
exports: [
MINDS_PIPES,
......@@ -305,7 +314,6 @@ const routes: Routes = [
MDL_DIRECTIVES,
DateSelectorComponent,
MindsAvatar,
CaptchaComponent,
Textarea,
InlineEditorComponent,
......@@ -321,6 +329,7 @@ const routes: Routes = [
MaterialBoundSwitchComponent,
IfFeatureDirective,
IfBrowserDirective,
CategoriesSelectorComponent,
CategoriesSelectedComponent,
......@@ -364,16 +373,14 @@ const routes: Routes = [
ShadowboxSubmitButtonComponent,
EmailConfirmationComponent,
DateDropdownsComponent,
FormInputCheckboxComponent,
],
providers: [
SiteService,
SsoService,
AttachmentService,
CookieService,
PagesService,
{
provide: AttachmentService,
useFactory: AttachmentService._,
deps: [Session, Client, Upload, HttpClient],
},
{
provide: UpdateMarkersService,
useFactory: (_http, _session, _sockets) => {
......@@ -384,18 +391,10 @@ const routes: Routes = [
{
provide: MindsHttpClient,
useFactory: MindsHttpClient._,
deps: [HttpClient],
},
{
provide: NSFWSelectorCreatorService,
useFactory: _storage => new NSFWSelectorCreatorService(_storage),
deps: [Storage],
},
{
provide: NSFWSelectorConsumerService,
useFactory: _storage => new NSFWSelectorConsumerService(_storage),
deps: [Storage],
deps: [HttpClient, CookieService],
},
NSFWSelectorCreatorService,
NSFWSelectorConsumerService,
{
provide: BoostedContentService,
useFactory: (
......@@ -432,9 +431,31 @@ const routes: Routes = [
deps: [Router],
},
{
provide: V2TopbarService,
useFactory: V2TopbarService._,
provide: ConfigsService,
useFactory: (client, injector) =>
new ConfigsService(client, injector.get('QUERY_STRING')),
deps: [Client, Injector],
},
{
provide: MetaService,
useFactory: (
titleService,
metaService,
siteService,
location,
configsService
) =>
new MetaService(
titleService,
metaService,
siteService,
location,
configsService
),
deps: [Title, Meta, SiteService, Location, ConfigsService],
},
MediaProxyService,
V2TopbarService,
{
provide: SidebarMarkersService,
useFactory: SidebarMarkersService._,
......
......@@ -13,7 +13,5 @@ import { Router } from '@angular/router';
`,
})
export class AndroidAppDownloadComponent {
minds = window.Minds;
constructor(private router: Router) {}
}
......@@ -2,6 +2,7 @@ import { Component, EventEmitter, Input, OnInit } from '@angular/core';
import { Storage } from '../../../services/storage';
import { Client } from '../../../services/api';
import { CookieService } from '../../../common/services/cookie.service';
@Component({
selector: 'm-announcement',
......@@ -21,21 +22,21 @@ import { Client } from '../../../services/api';
`,
})
export class AnnouncementComponent implements OnInit {
minds: Minds = window.Minds;
hidden: boolean = false;
@Input() id: string = 'default';
@Input() canClose: boolean = true;
@Input() remember: boolean = true;
constructor(private storage: Storage) {}
constructor(private cookieService: CookieService) {}
ngOnInit() {
if (this.storage.get('hide-announcement:' + this.id)) this.hidden = true;
if (this.cookieService.get('hide-announcement:' + this.id) === '1')
this.hidden = true;
}
close() {
if (this.remember) {
this.storage.set('hide-announcement:' + this.id, true);
this.cookieService.put('hide-announcement:' + this.id, '1');
}
this.hidden = true;
......
......@@ -2,9 +2,7 @@
<a href="javascript:;" (click)="selectChoice.next(choice)">
<img
class="m-postAutocompleteItemRenderer__avatar mdl-shadow--2dp"
[src]="
minds.cdn_url + 'icon/' + choice.guid + '/medium/' + choice.icontime
"
[src]="cdnUrl + 'icon/' + choice.guid + '/medium/' + choice.icontime"
/>
{{ choice.username }}
</a>
......
import { Component, Input, OnInit } from '@angular/core';
import { ConfigsService } from '../../../services/configs.service';
@Component({
selector: 'm-post-autocomplete-item-renderer',
......@@ -8,5 +9,9 @@ export class PostsAutocompleteItemRendererComponent {
@Input() choice;
@Input() selectChoice;
minds = window.Minds;
readonly cdnUrl: string;
constructor(configs: ConfigsService) {
this.cdnUrl = configs.get('cdn_url');
}
}
import { Component, EventEmitter } from '@angular/core';
import { UserAvatarService } from '../../services/user-avatar.service';
import { of, Observable } from 'rxjs';
import { ConfigsService } from '../../services/configs.service';
import { Session } from '../../../services/session';
@Component({
selector: 'minds-avatar',
......@@ -20,7 +22,7 @@ import { of, Observable } from 'rxjs';
>
<img
*ngIf="!(userAvatarService.src$ | async)"
src="{{ minds.cdn_assets_url }}assets/avatars/blue/default-large.png"
src="{{ cdnAssetsUrl }}assets/avatars/blue/default-large.png"
class="mdl-shadow--4dp"
/>
<div *ngIf="editing" class="overlay">
......@@ -43,7 +45,8 @@ import { of, Observable } from 'rxjs';
`,
})
export class MindsAvatar {
minds: Minds = window.Minds;
readonly cdnAssetsUrl: string;
readonly cdnUrl: string;
object;
editing: boolean = false;
waitForDoneSignal: boolean = true;
......@@ -54,7 +57,14 @@ export class MindsAvatar {
file: any;
added: EventEmitter<any> = new EventEmitter();
constructor(public userAvatarService: UserAvatarService) {}
constructor(
public userAvatarService: UserAvatarService,
configs: ConfigsService,
private session: Session
) {
this.cdnUrl = configs.get('cdn_url');
this.cdnAssetsUrl = configs.get('cdn_assets_url');
}
set _object(value: any) {
if (!value) return;
......@@ -62,9 +72,12 @@ export class MindsAvatar {
this.object = value;
if (this.object.type !== 'user') {
this.src = `${this.minds.cdn_url}fs/v1/avatars/${this.object.guid}/large/${this.object.icontime}`;
} else if (!this.minds.user || this.object.guid !== this.minds.user.guid) {
this.src = `${this.minds.cdn_url}icon/${this.object.guid}/large/${this.object.icontime}`;
this.src = `${this.cdnUrl}fs/v1/avatars/${this.object.guid}/large/${this.object.icontime}`;
} else if (
!this.session.getLoggedInUser() ||
this.object.guid !== this.session.getLoggedInUser().guid
) {
this.src = `${this.cdnUrl}icon/${this.object.guid}/large/${this.object.icontime}`;
}
}
......@@ -131,9 +144,9 @@ export class MindsAvatar {
*/
isOwnerAvatar(): boolean {
return (
this.minds.user &&
this.session.getLoggedInUser() &&
this.object &&
this.object.guid === this.minds.user.guid
this.object.guid === this.session.getLoggedInUser().guid
);
}
}
<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 {}
......@@ -8,11 +8,11 @@ import {
ViewChild,
} from '@angular/core';
import { TreeNode } from '../../tree/tree-node.model';
import { ConfigsService } from '../../../../common/services/configs.service';
type Category = { id: string; label: string };
@Component({
moduleId: module.id,
selector: 'm-categories--selector',
templateUrl: 'selector.component.html',
})
......@@ -28,8 +28,8 @@ export class CategoriesSelectorComponent {
Array<Category>
> = new EventEmitter<Array<Category>>();
constructor() {
this.categories = window.Minds.categories;
constructor(configs: ConfigsService) {
this.categories = configs.get('categories');
}
search(value: string) {
......
......@@ -8,6 +8,7 @@ import {
OnDestroy,
OnInit,
ViewChild,
Injector,
} from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { EmbedImage } from './plugins/embed-image.plugin';
......@@ -15,6 +16,7 @@ import { EmbedVideo } from './plugins/embed-video.plugin';
import { MediumEditor } from 'medium-editor';
import { ButtonsPlugin } from './plugins/buttons.plugin';
import { AttachmentService } from '../../../services/attachment';
import { ConfigsService } from '../../services/configs.service';
export const MEDIUM_EDITOR_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR,
......@@ -53,10 +55,13 @@ export class InlineEditorComponent
placeholder: 'Paste your link and then press Enter',
uploadFunction: this.attachment.upload.bind(this.attachment),
});
private images = new EmbedImage({
buttonText: `<i class=\"material-icons\">photo_camera</i>`,
placeholder: 'Type caption for image (optional)',
});
private images = new EmbedImage(
{
buttonText: `<i class=\"material-icons\">photo_camera</i>`,
placeholder: 'Type caption for image (optional)',
},
this.injector.get(ConfigsService)
);
private videos = new EmbedVideo({
buttonText: `<i class="material-icons">play_arrow</i>`,
});
......@@ -68,7 +73,8 @@ export class InlineEditorComponent
constructor(
el: ElementRef,
private cd: ChangeDetectorRef,
private attachment: AttachmentService
private attachment: AttachmentService,
private injector: Injector
) {
this.el = el;
}
......
import Editor = MediumEditor.MediumEditor;
import { ConfigsService } from '../../../services/configs.service';
type Options = { buttonText?: string; placeholder?: string };
export class EmbedImage {
readonly siteUrl: string;
button: HTMLButtonElement;
options: any = { placeholder: '' };
base: Editor;
......@@ -13,12 +15,13 @@ export class EmbedImage {
$currentImage;
private readonly urlRegex = /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)/;
constructor(options: Options) {
constructor(options: Options, configs: ConfigsService) {
this.options = { ...options };
this.button = document.createElement('button');
this.button.className = 'medium-editor-action';
this.button.innerHTML = options.buttonText || '</>';
this.button.onclick = this.handleClick.bind(this);
this.siteUrl = configs.get('site_url');
}
public init() {
......@@ -209,7 +212,7 @@ export class EmbedImage {
image.setAttribute(
'src',
window.Minds.site_url + 'fs/v1/thumbnail/' + event.detail.guid
this.siteUrl + 'fs/v1/thumbnail/' + event.detail.guid
);
}
);
......
......@@ -4,10 +4,14 @@ import {
Component,
OnDestroy,
OnInit,
Inject,
PLATFORM_ID,
} from '@angular/core';
import { EmailConfirmationService } from './email-confirmation.service';
import { Session } from '../../../services/session';
import { Subscription } from 'rxjs';
import { isPlatformBrowser } from '@angular/common';
import { ConfigsService } from '../../services/configs.service';
import { NavigationEnd, Router } from '@angular/router';
import { Location } from '@angular/common';
import { filter } from 'rxjs/operators';
......@@ -25,6 +29,7 @@ import { filter } from 'rxjs/operators';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class EmailConfirmationComponent implements OnInit, OnDestroy {
readonly fromEmailConfirmation: number;
sent: boolean = false;
shouldShow: boolean = false;
canClose: boolean = false;
......@@ -32,15 +37,18 @@ export class EmailConfirmationComponent implements OnInit, OnDestroy {
protected userEmitter$: Subscription;
protected routerEvent$: Subscription;
protected canCloseTimer: number;
protected minds = window.Minds;
constructor(
protected service: EmailConfirmationService,
protected session: Session,
protected cd: ChangeDetectorRef,
@Inject(PLATFORM_ID) protected platformId: Object,
configs: ConfigsService,
protected router: Router,
protected location: Location,
protected cd: ChangeDetectorRef
) {}
protected location: Location
) {
this.fromEmailConfirmation = configs.get('from_email_confirmation');
}
ngOnInit(): void {
this.setShouldShow(this.session.getLoggedInUser());
......@@ -51,10 +59,12 @@ export class EmailConfirmationComponent implements OnInit, OnDestroy {
this.detectChanges();
});
this.canCloseTimer = window.setTimeout(() => {
this.canClose = true;
this.detectChanges();
}, 3000);
if (isPlatformBrowser(this.platformId)) {
this.canCloseTimer = window.setTimeout(() => {
this.canClose = true;
this.detectChanges();
}, 3000);
}
this.routerEvent$ = this.router.events
.pipe(filter(event => event instanceof NavigationEnd))
......@@ -83,7 +93,7 @@ export class EmailConfirmationComponent implements OnInit, OnDestroy {
setShouldShow(user): void {
this.shouldShow =
!(this.location.path().indexOf('/onboarding') === 0) &&
!this.minds.from_email_confirmation &&
!this.fromEmailConfirmation &&
user &&
user.email_confirmed === false;
}
......
......@@ -9,11 +9,14 @@ import {
OnInit,
SkipSelf,
ViewChild,
Inject,
PLATFORM_ID,
} from '@angular/core';
import { FeaturedContentService } from './featured-content.service';
import { DynamicHostDirective } from '../../directives/dynamic-host.directive';
import { Activity } from '../../../modules/legacy/components/cards/activity/activity';
import { ClientMetaService } from '../../services/client-meta.service';
import { isPlatformBrowser } from '@angular/common';
@Component({
selector: 'm-featured-content',
......@@ -34,13 +37,14 @@ export class FeaturedContentComponent implements OnInit {
protected componentFactoryResolver: ComponentFactoryResolver,
protected cd: ChangeDetectorRef,
protected clientMetaService: ClientMetaService,
@SkipSelf() protected injector: Injector
@SkipSelf() protected injector: Injector,
@Inject(PLATFORM_ID) private platformId: Object
) {
this.clientMetaService.inherit(injector).setMedium('featured-content');
}
ngOnInit() {
this.load();
if (isPlatformBrowser(this.platformId)) this.load();
}
async load() {
......
<label class="m-formInput__checkbox" [for]="id">
<input
type="checkbox"
[ngModel]="value"
(ngModelChange)="updateValue($event)"
[id]="id"
/>
<span class="m-formInputCheckbox__custom"></span>
<ng-content></ng-content>
</label>
m-formInput__checkbox {
display: block;
}
.m-formInput__checkbox {
display: block;
position: relative;
cursor: pointer;
user-select: none;
font-family: 'Roboto', Helvetica, sans-serif;
@include m-theme() {
color: themed($m-grey-300);
}
font-size: 13px;
.m-formInputCheckbox__custom {
display: inline-block;
vertical-align: middle;
height: 20px;
width: 20px;
border-radius: 2px;
transition: all 0.3s ease;
margin-right: $minds-margin;
@include m-theme() {
background-color: themed($m-white);
border: 1px solid themed($m-grey-100);
}
&:after {
content: '';
position: absolute;
display: none;
-webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
left: 7px;
top: 2px;
width: 5px;
height: 12px;
@include m-theme() {
border: 1px solid themed($m-white);
border-top: none;
border-left: none;
}
}
}
// The input itself is invisible
input {
position: absolute;
opacity: 0;
cursor: pointer;
height: 0;
width: 0;
&:checked ~ .m-formInputCheckbox__custom {
@include m-theme() {
background-color: themed($m-blue);
border-color: themed($m-blue);
}
&:after {
display: block;
}
}
}
}
import {
Component,
ElementRef,
forwardRef,
OnChanges,
OnInit,
ViewChild,
} from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
export const FORM_INPUT_CHECKBOX_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => FormInputCheckboxComponent),
multi: true,
};
@Component({
selector: 'm-formInput__checkbox',
templateUrl: 'checkbox.component.html',
providers: [FORM_INPUT_CHECKBOX_VALUE_ACCESSOR],
})
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) {
this.id =
`checkbox` +
Math.random()
.toString(36)
.substring(2); // Confirm duplicates not possible?
}
writeValue(value: any): void {
this.value = value;
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {}
}
......@@ -4,7 +4,6 @@ import { Client, Upload } from '../../../../services/api';
import { Session } from '../../../../services/session';
@Component({
moduleId: module.id,
selector: 'minds-form-city-finder',
outputs: ['done'],
templateUrl: 'city-finder.component.html',
......@@ -41,14 +40,16 @@ export class CityFinderComponent {
setCity(row: any) {
this.cities = [];
if (row.address.city) window.Minds.user.city = row.address.city;
if (row.address.town) window.Minds.user.city = row.address.town;
this.city = window.Minds.user.city;
if (row.address.city)
this.session.getLoggedInUser().city = row.address.city;
if (row.address.town)
this.session.getLoggedInUser().city = row.address.town;
this.city = this.session.getLoggedInUser().city;
this.inProgress = true;
this.client
.post('api/v1/channel/info', {
coordinates: row.lat + ',' + row.lon,
city: window.Minds.user.city,
city: this.session.getLoggedInUser().city,
})
.then((response: any) => {
this.inProgress = false;
......
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { ConfigsService } from '../../services/configs.service';
@Component({
selector: 'm-marketing__asFeaturedIn',
......@@ -6,5 +7,9 @@ import { ChangeDetectionStrategy, Component } from '@angular/core';
templateUrl: 'as-featured-in.component.html',
})
export class MarketingAsFeaturedInComponent {
readonly cdnAssetsUrl: string = window.Minds.cdn_assets_url;
readonly cdnAssetsUrl: string;
constructor(private configs: ConfigsService) {
this.cdnAssetsUrl = configs.get('cdn_assets_url');
}
}
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
HostListener,
ChangeDetectorRef,
OnInit,
HostListener,
} from '@angular/core';
import { ConfigsService } from '../../services/configs.service';
@Component({
selector: 'm-marketing__footer',
......@@ -14,10 +15,15 @@ import {
export class MarketingFooterComponent implements OnInit {
readonly year: number = new Date().getFullYear();
readonly cdnAssetsUrl: string = window.Minds.cdn_assets_url;
readonly cdnAssetsUrl: string;
isMobile: boolean;
constructor(protected cd: ChangeDetectorRef) {}
constructor(
private configs: ConfigsService,
protected cd: ChangeDetectorRef
) {
this.cdnAssetsUrl = configs.get('cdn_assets_url');
}
ngOnInit() {
this.onResize();
......
......@@ -5,7 +5,7 @@ import {
OnDestroy,
OnInit,
} from '@angular/core';
import { MindsTitle } from '../../../services/ux/title';
import { MetaService } from '../../services/meta.service';
import { V2TopbarService } from '../../layout/v2-topbar/v2-topbar.service';
@Component({
......@@ -19,13 +19,13 @@ export class MarketingComponent implements OnInit, OnDestroy {
@Input() forceBackground: boolean = true;
constructor(
protected title: MindsTitle,
protected metaService: MetaService,
private topbarService: V2TopbarService
) {}
ngOnInit() {
if (this.pageTitle) {
this.title.setTitle(this.pageTitle);
this.metaService.setTitle(this.pageTitle);
}
this.topbarService.toggleMarketingPages(
......
......@@ -9,6 +9,7 @@ import {
import { Client } from '../../../services/api/client';
import { Storage } from '../../../services/storage';
import { SiteService } from '../../services/site.service';
import { CookieService } from '../../services/cookie.service';
@Component({
selector: 'm-cookies-notice',
......@@ -24,21 +25,21 @@ export class DismissableNoticeComponent {
constructor(
private client: Client,
private storage: Storage,
private cookieService: CookieService,
private site: SiteService
) {
if (this.storage.get('cookies-notice-dismissed')) {
if (this.cookieService.get('cookies-notice-dismissed') === '1') {
this.hidden = true;
}
this.checkCookies();
}
checkCookies() {
this.cookiesEnabled = document.cookie.indexOf('disable_cookies') === -1;
this.cookiesEnabled = !this.cookieService.get('disable_cookies');
}
dismiss() {
this.storage.set('cookies-notice-dismissed', 'true');
this.cookieService.put('cookies-notice-dismissed', '1');
this.hidden = true;
}
......
......@@ -10,13 +10,7 @@ import { ifError } from 'assert';
@Component({
selector: 'm-nsfw-selector',
templateUrl: 'nsfw-selector.component.html',
providers: [
{
provide: NSFWSelectorEditingService,
useFactory: _storage => new NSFWSelectorEditingService(_storage),
deps: [Storage],
},
],
providers: [NSFWSelectorEditingService],
})
export class NSFWSelectorComponent {
@Input('service') serviceRef: string = 'consumer';
......
import { Storage } from '../../../services/storage';
import { CookieService } from '../../../common/services/cookie.service';
import { Injectable } from '@angular/core';
@Injectable()
export class NSFWSelectorService {
cacheKey: string = '';
......@@ -12,12 +14,14 @@ export class NSFWSelectorService {
{ value: 6, label: 'Other', selected: false, locked: false },
];
constructor(private storage: Storage) {}
constructor(private cookieService: CookieService) {}
onInit() {}
build(): NSFWSelectorService {
let reasons = this.storage.get(`nsfw:${this.cacheKey}`) || [];
let reasons = JSON.parse(
this.cookieService.get(`nsfw:${this.cacheKey}`) || `[]`
);
for (let reason of this.reasons) {
reason.selected = reasons.indexOf(reason.value) > -1;
}
......@@ -31,16 +35,18 @@ export class NSFWSelectorService {
for (let r of this.reasons) {
if (r.value === reason.value) r.selected = !r.selected;
}
this.storage.set(
this.cookieService.put(
`nsfw:${this.cacheKey}`,
this.reasons.filter(r => r.selected).map(r => r.value)
JSON.stringify(this.reasons.filter(r => r.selected).map(r => r.value))
);
}
}
@Injectable()
export class NSFWSelectorCreatorService extends NSFWSelectorService {
cacheKey: string = 'creator';
}
@Injectable()
export class NSFWSelectorConsumerService extends NSFWSelectorService {
cacheKey: string = 'consumer';
}
......@@ -48,6 +54,7 @@ export class NSFWSelectorConsumerService extends NSFWSelectorService {
/**
* Editing service, overrides build to allow pre-setting of values.
*/
@Injectable()
export class NSFWSelectorEditingService extends NSFWSelectorService {
cacheKey: string = 'editing';
......
......@@ -77,21 +77,10 @@ export class PostMenuComponent implements OnInit {
protected blockListService: BlockListService,
protected activityService: ActivityService,
public featuresService: FeaturesService
) {
this.initCategories();
}
) {}
ngOnInit() {}
initCategories() {
for (let category in window.Minds.categories) {
this.categories.push({
id: category,
label: window.Minds.categories[category],
});
}
}
cardMenuHandler() {
this.opened = !this.opened;
this.asyncFollowFetch();
......
......@@ -9,7 +9,7 @@ import {
import { DomSanitizer } from '@angular/platform-browser';
import { RichEmbedService } from '../../../services/rich-embed';
import mediaProxyUrl from '../../../helpers/media-proxy-url';
import { MediaProxyService } from '../../../common/services/media-proxy.service';
import { FeaturesService } from '../../../services/features.service';
@Component({
......@@ -35,7 +35,8 @@ export class MindsRichEmbed {
private sanitizer: DomSanitizer,
private service: RichEmbedService,
private cd: ChangeDetectorRef,
protected featureService: FeaturesService
protected featureService: FeaturesService,
private mediaProxy: MediaProxyService
) {}
set _src(value: any) {
......@@ -47,7 +48,7 @@ export class MindsRichEmbed {
this.type = 'src';
if (this.src.thumbnail_src) {
this.src.thumbnail_src = mediaProxyUrl(this.src.thumbnail_src);
this.src.thumbnail_src = this.mediaProxy.proxy(this.src.thumbnail_src);
}
this.init();
......@@ -62,7 +63,7 @@ export class MindsRichEmbed {
this.type = 'preview';
if (this.preview.thumbnail) {
this.preview.thumbnail = mediaProxyUrl(this.preview.thumbnail);
this.preview.thumbnail = this.mediaProxy.proxy(this.preview.thumbnail);
}
this.init();
......@@ -72,10 +73,7 @@ export class MindsRichEmbed {
// Inline Embedding
let inlineEmbed = this.parseInlineEmbed(this.inlineEmbed);
if (
this.featureService.has('media-modal') &&
this.mediaSource === 'youtube'
) {
if (this.mediaSource === 'youtube') {
this.modalRequestSubscribed =
this.mediaModalRequested.observers.length > 0;
}
......@@ -96,11 +94,7 @@ export class MindsRichEmbed {
this.inlineEmbed = inlineEmbed;
if (
this.modalRequestSubscribed &&
this.featureService.has('media-modal') &&
this.mediaSource === 'youtube'
) {
if (this.modalRequestSubscribed && this.mediaSource === 'youtube') {
if (this.inlineEmbed && this.inlineEmbed.htmlProvisioner) {
this.inlineEmbed.htmlProvisioner().then(html => {
this.inlineEmbed.html = html;
......@@ -270,11 +264,9 @@ export class MindsRichEmbed {
}
hasInlineContentLoaded() {
return this.featureService.has('media-modal')
? !this.modalRequestSubscribed &&
this.inlineEmbed &&
this.inlineEmbed.html
: this.embeddedInline && this.inlineEmbed && this.inlineEmbed.html;
return (
!this.modalRequestSubscribed && this.inlineEmbed && this.inlineEmbed.html
);
}
detectChanges() {
......
......@@ -16,7 +16,7 @@
<a class="m-sidebarMenu__userWrapper" [routerLink]="['/', user.username]">
<img
class="m-sidebarMenu__userAvatar"
[src]="minds.cdn_url + 'icon/' + user.guid + '/medium/' + user.icontime"
[src]="cdnUrl + 'icon/' + user.guid + '/medium/' + user.icontime"
/>
<div class="m-sidebarMenu__userDetails">
<div class="m-sidebarMenu__userDetails__name">{{ user.name }}</div>
......
......@@ -5,6 +5,8 @@ import { Session } from '../../../services/session';
import { sessionMock } from '../../../../tests/session-mock.spec';
import { SidebarMenuComponent } from './sidebar-menu.component';
import sidebarMenu from '../../../modules/pro/settings/sidebar-menu.default';
import { ConfigsService } from '../../services/configs.service';
import { MockService } from '../../../utils/mock';
describe('SidebarMenuComponent', () => {
let component: SidebarMenuComponent;
......@@ -14,7 +16,10 @@ describe('SidebarMenuComponent', () => {
TestBed.configureTestingModule({
declarations: [SidebarMenuComponent],
imports: [RouterTestingModule],
providers: [{ provide: Session, useValue: sessionMock }],
providers: [
{ provide: Session, useValue: sessionMock },
{ provide: ConfigsService, useValue: MockService(ConfigsService) },
],
}).compileComponents();
}));
......
import { Component, OnInit, Input } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Session } from '../../../services/session';
import { ConfigsService } from '../../services/configs.service';
interface Menu {
header: MenuLink;
......@@ -27,14 +28,19 @@ export class SidebarMenuComponent implements OnInit {
@Input() menu: Menu;
mobileMenuExpanded = false;
minds: Minds;
readonly cdnUrl: string;
user;
userRoles: string[] = ['user'];
constructor(public route: ActivatedRoute, public session: Session) {}
constructor(
public route: ActivatedRoute,
public session: Session,
configs: ConfigsService
) {
this.cdnUrl = configs.get('cdn_url');
}
ngOnInit() {
this.minds = window.Minds;
this.user = this.session.getLoggedInUser();
this.getUserRoles();
this.grantPermissions();
......@@ -44,7 +50,7 @@ export class SidebarMenuComponent implements OnInit {
if (this.session.isAdmin()) {
this.userRoles.push('admin');
}
if (this.minds.user.pro) {
if (this.user.pro) {
this.userRoles.push('pro');
}
}
......
......@@ -128,6 +128,8 @@ export class SortSelectorComponent implements OnInit, OnDestroy, AfterViewInit {
expandedCustomTypeDropdown: boolean = true;
protected lastUsedPeriod: string;
protected lastWidth: number;
protected resizeSubscription: Subscription;
......@@ -144,6 +146,10 @@ export class SortSelectorComponent implements OnInit, OnDestroy, AfterViewInit {
this.resizeSubscription = this.resizeSubject
.pipe(debounceTime(1000 / 30))
.subscribe(() => this.onResize());
if (this.period) {
this.lastUsedPeriod = this.period;
}
}
ngAfterViewInit() {
......@@ -187,7 +193,9 @@ export class SortSelectorComponent implements OnInit, OnDestroy, AfterViewInit {
}
setAlgorithm(id: string) {
if (!this.algorithms.find(algorithm => id === algorithm.id)) {
const algorithm = this.algorithms.find(algorithm => id === algorithm.id);
if (!algorithm) {
console.error('Unknown algorithm');
return false;
}
......@@ -197,6 +205,11 @@ export class SortSelectorComponent implements OnInit, OnDestroy, AfterViewInit {
}
this.algorithm = id;
if (this.lastUsedPeriod && !algorithm.noPeriod) {
this.period = this.lastUsedPeriod;
}
this.emit();
return true;
......@@ -239,6 +252,7 @@ export class SortSelectorComponent implements OnInit, OnDestroy, AfterViewInit {
}
this.period = id;
this.lastUsedPeriod = this.period;
this.emit();
return true;
......
import { Component } from '@angular/core';
import { Client } from '../../../services/api';
import { ConfigsService } from '../../services/configs.service';
@Component({
moduleId: module.id,
selector: 'm-tagcloud',
templateUrl: 'tagcloud.component.html',
})
export class TagcloudComponent {
tags: Array<string> = [];
constructor(public client: Client) {}
ngOnInit() {
this.load();
}
load() {
this.tags = window.Minds.tags;
/*this.client.get('api/v1/search/tagcloud')
.then((response: any) => {
this.tags = response.tags;
});*/
constructor(public client: Client, configs: ConfigsService) {
this.tags = configs.get('tags');
}
}
import {
Directive,
EmbeddedViewRef,
Inject,
Input,
PLATFORM_ID,
TemplateRef,
ViewContainerRef,
} from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
@Directive({
selector: '[mIfBrowser]',
})
export class IfBrowserDirective {
private _elseTemplateRef: TemplateRef<any>;
private _viewRef: EmbeddedViewRef<any>;
private _elseViewRef: EmbeddedViewRef<any>;
constructor(
private _templateRef: TemplateRef<any>,
private _viewContainerRef: ViewContainerRef,
@Inject(PLATFORM_ID) private platformId: Object
) {
this._update();
}
_update() {
if (isPlatformBrowser(this.platformId)) {
if (!this._viewRef) {
this._viewContainerRef.clear();
this._elseViewRef = void 0;
if (this._templateRef) {
this._viewRef = this._viewContainerRef.createEmbeddedView(
this._templateRef
);
}
}
} else {
if (!this._elseViewRef) {
this._viewContainerRef.clear();
this._viewRef = void 0;
if (this._elseTemplateRef) {
this._elseViewRef = this._viewContainerRef.createEmbeddedView(
this._elseTemplateRef
);
}
}
}
}
}
......@@ -20,7 +20,6 @@ import { SidebarMarkersService } from './markers.service';
export class SidebarMarkersComponent implements AfterViewInit {
@ViewChild(DynamicHostDirective, { static: true }) host: DynamicHostDirective;
minds = window.Minds;
showMarkerSidebar = false;
componentRef;
......@@ -54,13 +53,9 @@ export class SidebarMarkersComponent implements AfterViewInit {
return;
}
const mBody: any = document.getElementsByTagName('m-body')[0];
if (showMarkerSidebar) {
mBody.classList.add('has-markers-sidebar');
this.createGroupsSideBar();
} else {
mBody.classList.remove('has-markers-sidebar');
this.host.viewContainerRef.clear();
}
this.showMarkerSidebar = showMarkerSidebar;
......
......@@ -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;
......
<div class="m-topbar--row">
<a class="m-topbar--logo" routerLink="/">
<img [src]="minds.cdn_assets_url + 'assets/logos/bulb.svg'" />
<img [src]="cdnAssetsUrl + 'assets/logos/bulb.svg'" />
</a>
<ng-content select="[search]"></ng-content>
......@@ -95,7 +95,7 @@
<li class="m-dropdownList__item">
<a
target="_blank"
[href]="minds.cdn_assets_url + 'assets/documents/Whitepaper-v0.5.pdf'"
[href]="cdnAssetsUrl + 'assets/documents/Whitepaper-v0.5.pdf'"
>
<i class="material-icons">description</i>
<span>Whitepaper</span>
......
......@@ -5,6 +5,7 @@ import { Sidebar } from '../../../services/ui/sidebar';
import { Session } from '../../../services/session';
import { DynamicHostDirective } from '../../directives/dynamic-host.directive';
import { NotificationsToasterComponent } from '../../../modules/notifications/toaster.component';
import { ConfigsService } from '../../services/configs.service';
@Component({
moduleId: module.id,
......@@ -14,7 +15,7 @@ import { NotificationsToasterComponent } from '../../../modules/notifications/to
export class TopbarComponent {
@ViewChild(DynamicHostDirective, { static: true }) host: DynamicHostDirective;
minds = window.Minds;
readonly cdnAssetsUrl: string;
componentRef;
componentInstance: NotificationsToasterComponent;
......@@ -23,8 +24,11 @@ export class TopbarComponent {
public session: Session,
public storage: Storage,
public sidebar: Sidebar,
private _componentFactoryResolver: ComponentFactoryResolver
) {}
private _componentFactoryResolver: ComponentFactoryResolver,
configs: ConfigsService
) {
this.cdnAssetsUrl = configs.get('cdn_assets_url');
}
ngAfterViewInit() {
this.loadComponent();
......
......@@ -18,7 +18,6 @@ import { ReferralsLinksComponent } from '../../../modules/wallet/tokens/referral
export class UserMenuComponent implements OnInit {
isOpen: boolean = false;
minds = window.Minds;
isDark: boolean = false;
themeSubscription: Subscription;
......
......@@ -52,7 +52,7 @@
i18n-title
>
<img
[src]="minds.cdn_assets_url + 'assets/logos/bulb.svg'"
[src]="cdnAssetsUrl + 'assets/logos/bulb.svg'"
(touchstart)="touchStart()"
(mouseenter)="mouseEnter()"
(mouseleave)="mouseLeave()"
......@@ -128,7 +128,7 @@
</div>
</div>
<div class="m-v2-topbar__Bottom" *ngIf="showBottombar">
<div class="m-v2-topbar__Bottom" *ngIf="getCurrentUser() && showBottombar">
<ng-container *ngTemplateOutlet="navLinks"></ng-container>
</div>
......
......@@ -13,6 +13,7 @@ import { Session } from '../../../services/session';
import { DynamicHostDirective } from '../../directives/dynamic-host.directive';
import { NotificationsToasterComponent } from '../../../modules/notifications/toaster.component';
import { ThemeService } from '../../../common/services/theme.service';
import { ConfigsService } from '../../services/configs.service';
import { V2TopbarService } from './v2-topbar.service';
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
import { Location } from '@angular/common';
......@@ -23,7 +24,7 @@ import { Location } from '@angular/common';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class V2TopbarComponent implements OnInit, OnDestroy {
minds = window.Minds;
readonly cdnAssetsUrl: string;
timeout;
isTouchScreen = false;
forceBackground: boolean = true;
......@@ -48,9 +49,12 @@ export class V2TopbarComponent implements OnInit, OnDestroy {
protected cd: ChangeDetectorRef,
private themeService: ThemeService,
protected componentFactoryResolver: ComponentFactoryResolver,
configs: ConfigsService,
protected topbarService: V2TopbarService,
protected router: Router
) {}
) {
this.cdnAssetsUrl = configs.get('cdn_assets_url');
}
ngOnInit() {
this.loadComponent();
......
......@@ -27,7 +27,7 @@ describe('TagPipe', () => {
const string = 'textstring#name';
const transformedString = pipe.transform(<any>string);
expect(transformedString).toContain(
'<a href="/newsfeed/global/top;hashtag=name;period=24h'
'<a href="/newsfeed/global/top;hashtag=name;period=7d'
);
});
......@@ -35,7 +35,7 @@ describe('TagPipe', () => {
const string = 'textstring #name';
const transformedString = pipe.transform(<any>string);
expect(transformedString).toContain(
'<a href="/newsfeed/global/top;hashtag=name;period=24h'
'<a href="/newsfeed/global/top;hashtag=name;period=7d'
);
});
......@@ -43,7 +43,7 @@ describe('TagPipe', () => {
const string = 'textstring [#name';
const transformedString = pipe.transform(<any>string);
expect(transformedString).toContain(
'<a href="/newsfeed/global/top;hashtag=name;period=24h'
'<a href="/newsfeed/global/top;hashtag=name;period=7d'
);
});
......@@ -51,7 +51,7 @@ describe('TagPipe', () => {
const string = 'textstring (#name)';
const transformedString = pipe.transform(<any>string);
expect(transformedString).toContain(
'<a href="/newsfeed/global/top;hashtag=name;period=24h'
'<a href="/newsfeed/global/top;hashtag=name;period=7d'
);
});
......@@ -59,7 +59,7 @@ describe('TagPipe', () => {
const string = 'textString #NaMe';
const transformedString = pipe.transform(<any>string);
expect(transformedString).toContain(
'<a href="/newsfeed/global/top;hashtag=name;period=24h'
'<a href="/newsfeed/global/top;hashtag=name;period=7d'
);
});
......@@ -67,10 +67,10 @@ describe('TagPipe', () => {
const string = '#hash #hashlonger';
const transformedString = pipe.transform(<any>string);
expect(transformedString).toContain(
'<a href="/newsfeed/global/top;hashtag=hash;period=24h'
'<a href="/newsfeed/global/top;hashtag=hash;period=7d'
);
expect(transformedString).toContain(
'<a href="/newsfeed/global/top;hashtag=hashlonger;period=24h'
'<a href="/newsfeed/global/top;hashtag=hashlonger;period=7d'
);
});
......@@ -188,13 +188,13 @@ describe('TagPipe', () => {
expect(transformedString).toContain('<a class="tag" href="/name"');
expect(transformedString).toContain('<a class="tag" href="/name1"');
expect(transformedString).toContain(
'<a href="/newsfeed/global/top;hashtag=hash1;period=24h'
'<a href="/newsfeed/global/top;hashtag=hash1;period=7d'
);
expect(transformedString).toContain(
'<a href="/newsfeed/global/top;hashtag=hash2;period=24h'
'<a href="/newsfeed/global/top;hashtag=hash2;period=7d'
);
expect(transformedString).toContain(
'<a href="/newsfeed/global/top;hashtag=hash3;period=24h'
'<a href="/newsfeed/global/top;hashtag=hash3;period=7d'
);
expect(transformedString).toContain('<a href="ftp://s.com"');
expect(transformedString).toContain('<a href="mailto:name@mail.com"');
......
......@@ -39,7 +39,7 @@ export class TagsPipe implements PipeTransform {
} else if (this.featureService.has('top-feeds')) {
return `${
m.match[1]
}<a href="/newsfeed/global/top;hashtag=${m.match[2].toLowerCase()};period=24h">#${
}<a href="/newsfeed/global/top;hashtag=${m.match[2].toLowerCase()};period=7d">#${
m.match[2]
}</a>`;
}
......
......@@ -13,7 +13,9 @@ export class BlockListService {
protected session: Session,
protected storage: Storage
) {
this.blocked = new BehaviorSubject(JSON.parse(this.storage.get('blocked')));
//OK to remove as SSR will handle
//this.blocked = new BehaviorSubject(JSON.parse(this.storage.get('blocked')));
this.blocked = new BehaviorSubject([]);
}
fetch() {
......@@ -24,7 +26,8 @@ export class BlockListService {
this.blocked.next(response.guids); // re-emit as we have a change
this.storage.set('blocked', JSON.stringify(response.guids)); // save to storage
});
})
.catch(err => null);
return this;
}
......
......@@ -11,8 +11,6 @@ import MindsClientHttpAdapter from '../../lib/minds-sync/adapters/MindsClientHtt
import browserStorageAdapterFactory from '../../helpers/browser-storage-adapter-factory';
import BoostedContentSync from '../../lib/minds-sync/services/BoostedContentSync.js';
import AsyncStatus from '../../helpers/async-status';
@Injectable()
export class BoostedContentService {
constructor(
......
import { Injectable, Injector } from '@angular/core';
import { Location } from '@angular/common';
import { Injectable, Injector, Inject, PLATFORM_ID } from '@angular/core';
import { Location, isPlatformServer } from '@angular/common';
import hashCode from '../../helpers/hash-code';
import { Session } from '../../services/session';
import { Client } from '../../services/api';
......@@ -27,7 +27,8 @@ export class ClientMetaService {
constructor(
protected location: Location,
protected session: Session,
protected client: Client
protected client: Client,
@Inject(PLATFORM_ID) private platformId: Object
) {
this.id = ++uniqId;
......@@ -152,6 +153,7 @@ export class ClientMetaService {
}
async recordView(entity) {
if (isPlatformServer(this.platformId)) return; // Browser will record too.
await this.client.post('api/v2/analytics/views/entity/' + entity.guid, {
client_meta: this.build(),
});
......
import { Client } from '../api/client.service';
import { Injectable, Inject, Optional } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { map, tap } from 'rxjs/operators';
@Injectable()
export class ConfigsService {
private configs = {};
constructor(
private client: Client,
@Inject('QUERY_STRING') private queryString: string
) {}
async loadFromRemote() {
try {
this.configs = await this.client.get(
`api/v1/minds/config${this.queryString}`
);
} catch (err) {
console.error(err);
}
}
get(key) {
return this.configs[key] || null;
}
set(key, value): void {
this.configs[key] = value;
}
}
export { CookieService } from '@gorniv/ngx-universal';
import { Injectable } from '@angular/core';
import { ConfigsService } from './configs.service';
@Injectable()
export class MediaProxyService {
readonly cdnUrl: string;
constructor(configs: ConfigsService) {
this.cdnUrl = configs.get('cdn_url');
}
proxy(url, size = 1920) {
if (!url || typeof url !== 'string') {
return url;
}
const encodedUrl = encodeURIComponent(url);
return `${this.cdnUrl}api/v2/media/proxy?size=${size}&src=${encodedUrl}`;
}
}
import { Injectable, Optional } from '@angular/core';
import { Title, Meta } from '@angular/platform-browser';
import { SiteService } from './site.service';
import { Location } from '@angular/common';
import { ConfigsService } from './configs.service';
const DEFAULT_META_TITLE = 'Minds';
const DEFAULT_META_DESCRIPTION = '...';
@Injectable()
export class MetaService {
private counter: number;
private sep = ' | ';
private title: string = '';
constructor(
private titleService: Title,
private metaService: Meta,
private site: SiteService,
private location: Location,
private configs: ConfigsService
) {
this.reset();
}
setTitle(value: string, join = true): MetaService {
let title;
const defaultTitle = this.site.isProDomain
? this.site.title + ' - ' + this.site.oneLineHeadline
: DEFAULT_META_TITLE;
if (value && join) {
title = [value, defaultTitle]
.filter(fragment => Boolean(fragment))
.join(this.sep);
} else if (value) {
title = value;
} else {
title = defaultTitle;
}
this.title = title;
this.applyTitle();
return this;
}
setDescription(value: string): MetaService {
this.metaService.updateTag({ name: 'description', content: value });
return this;
}
setCounter(value: number): MetaService {
this.counter = value;
this.applyTitle();
return this;
}
setOgUrl(value: string): MetaService {
if (value && value.indexOf('/') === 0) {
// Relative path
value = this.site.baseUrl + value.substr(1);
}
this.metaService.updateTag({
property: 'og:url',
content: value,
});
return this;
}
setOgImage(
value: string,
@Optional() dimensions: { width: number; height: number } = null
): MetaService {
if (value) {
if (value.indexOf('/') === 0) {
// Relative path
value = this.configs.get('cdn_assets_url') + value.substr(1);
}
this.metaService.updateTag({ property: 'og:image', content: value });
if (dimensions) {
this.metaService.updateTag({
property: 'og:image:width',
content: dimensions.width.toString(),
});
this.metaService.updateTag({
property: 'og:image:height',
content: dimensions.height.toString(),
});
}
}
return this;
}
setOgType(value: string): MetaService {
this.metaService.updateTag({
property: 'og:type',
content: value,
});
return this;
}
setRobots(value: string): MetaService {
this.metaService.updateTag({ name: 'robots', content: value });
return this;
}
reset(
data: {
title?: string;
description?: string;
ogUrl?: string;
ogImage?: string;
robots?: string;
} = {}
): void {
this.setTitle(data.title || '')
.setDescription(data.description || DEFAULT_META_DESCRIPTION)
.setOgType('website')
.setOgUrl(data.ogUrl || this.location.path())
.setOgImage(data.ogImage || null, { width: 0, height: 0 })
.setRobots(data.robots || 'all');
}
private applyTitle(): void {
if (this.counter) {
this.titleService.setTitle(`(*) ${this.title}`);
} else {
this.titleService.setTitle(this.title);
}
this.metaService.updateTag({
name: 'og:title',
content: this.title,
});
}
}
......@@ -2,11 +2,12 @@ import { Injectable } from '@angular/core';
import { NavigationEnd, Router } from '@angular/router';
import { Subscription } from 'rxjs';
import { proRoutes } from '../../modules/pro/pro.routes';
import { ConfigsService } from './configs.service';
@Injectable()
export class SiteService {
get pro() {
return window.Minds.pro;
return this.configs.get('pro');
}
get isProDomain(): boolean {
......@@ -22,18 +23,14 @@ export class SiteService {
}
get baseUrl(): string {
return window.Minds.site_url; // TODO: use SSR once merged in
return this.configs.get('site_url');
}
private router$: Subscription;
constructor(private router: Router) {
if (this.isProDomain) {
this.listen();
}
}
constructor(private router: Router, private configs: ConfigsService) {}
private listen() {
listen(): void {
this.router$ = this.router.events.subscribe(
(navigationEvent: NavigationEnd) => {
try {
......@@ -49,7 +46,7 @@ export class SiteService {
.split('?')[0];
if (!this.searchRoutes(url, proRoutes)) {
window.open(window.Minds.site_url + url, '_blank');
window.open(this.baseUrl + url, '_blank');
}
}
} catch (e) {
......
import { Injectable } from '@angular/core';
import { Injectable, Inject, PLATFORM_ID } from '@angular/core';
import { SiteService } from './site.service';
import { Client } from '../../services/api/client';
import { Session } from '../../services/session';
import { ConfigsService } from './configs.service';
import { isPlatformBrowser } from '@angular/common';
@Injectable()
export class SsoService {
protected readonly minds = window.Minds;
constructor(
protected site: SiteService,
protected client: Client,
protected session: Session
protected session: Session,
private configs: ConfigsService,
@Inject(PLATFORM_ID) private platformId: Object
) {
this.listen();
}
......@@ -24,13 +26,13 @@ export class SsoService {
}
isRequired(): boolean {
return this.site.isProDomain;
return isPlatformBrowser(this.platformId) && this.site.isProDomain;
}
async connect() {
try {
const connect: any = await this.client.postRaw(
`${this.minds.site_url}api/v2/sso/connect`
`${this.configs.get('site_url')}api/v2/sso/connect`
);
if (connect && connect.token && connect.status === 'success') {
......@@ -56,7 +58,7 @@ export class SsoService {
if (connect && connect.token && connect.status === 'success') {
await this.client.postRaw(
`${this.minds.site_url}api/v2/sso/authorize`,
`${this.configs.get('site_url')}api/v2/sso/authorize`,
{
token: connect.token,
}
......
......@@ -36,7 +36,7 @@ export class ThemeService {
setUp() {
window.addEventListener('storage', event => {
if (event.key === 'dark_theme' && window.Minds.LoggedIn) {
if (event.key === 'dark_theme' && this.session.isLoggedIn()) {
if (
(event.newValue === 'true' && this.isDark.value === false) ||
(event.newValue === 'false' && this.isDark.value === true)
......@@ -92,7 +92,7 @@ export class ThemeService {
this.renderer.removeClass(document.body, 'm-theme__dark');
this.renderer.addClass(document.body, 'm-theme__light');
}
this.clearTransitions();
//this.clearTransitions();
}
clearTransitions() {
......
......@@ -6,17 +6,17 @@ import { Injectable } from '@angular/core';
import { BehaviorSubject, Subscription } from 'rxjs';
import { Session } from '../../services/session';
import { MindsUser } from '../../interfaces/entities';
import { ConfigsService } from './configs.service';
@Injectable({
providedIn: 'root',
})
export class UserAvatarService {
private minds = window.Minds;
private user: MindsUser;
public src$: BehaviorSubject<string> = new BehaviorSubject<string>('');
public loggedIn$: Subscription;
constructor(public session: Session) {
constructor(public session: Session, private configs: ConfigsService) {
this.init();
// Subscribe to loggedIn$ and on login, update src$.
......@@ -41,6 +41,8 @@ export class UserAvatarService {
* Gets the Src string using the global minds object and the held user object.
*/
public getSrc(): string {
return `${this.minds.cdn_url}icon/${this.user.guid}/large/${this.user.icontime}`;
return `${this.configs.get('cdn_url')}icon/${this.user.guid}/large/${
this.user.icontime
}`;
}
}
<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>
......@@ -3,7 +3,6 @@ import { Location } from '@angular/common';
import { ActivatedRoute, Router } from '@angular/router';
import { Subscription } from 'rxjs';
import { Client, Upload } from '../../services/api';
import { MindsTitle } from '../../services/ux/title';
import { Session } from '../../services/session';
import { ActivityService } from '../../common/services/activity.service';
......@@ -19,7 +18,6 @@ export class Admin {
constructor(
public session: Session,
private route: ActivatedRoute,
public title: MindsTitle,
public router: Router
) {}
......@@ -28,7 +26,6 @@ export class Admin {
this.router.navigate(['/']);
}
this.title.setTitle('Admin');
this.paramsSubscription = this.route.params.subscribe((params: any) => {
if (params['filter']) {
this.filter = params['filter'];
......
......@@ -3,7 +3,6 @@ import { Component } from '@angular/core';
import { Client } from '../../../services/api';
@Component({
moduleId: module.id,
selector: 'minds-admin-featured',
templateUrl: 'featured.html',
})
......@@ -17,9 +16,7 @@ export class AdminFeatured {
constructor(public client: Client) {}
ngOnInit() {
this.loadCategories(window.Minds.categories);
}
ngOnInit() {}
load(refresh: boolean = false) {
if (this.inProgress) {
......@@ -64,15 +61,4 @@ export class AdminFeatured {
this.category = category;
this.load(true);
}
loadCategories(categories: any) {
this.categories = [];
for (let category in categories) {
this.categories.push({
id: category,
value: window.Minds.categories[category],
});
}
}
}
<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();
}
}
......@@ -2,7 +2,6 @@ import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { LICENSES, ACCESS } from '../../services/list-options';
import { MindsTitle } from '../../services/ux/title';
import { Session } from '../../services/session';
import { Upload } from '../../services/api/upload';
import { Client } from '../../services/api/client';
......@@ -37,8 +36,7 @@ export class Capture {
public session: Session,
public _upload: Upload,
public client: Client,
public router: Router,
public title: MindsTitle
public router: Router
) {}
ngOnInit() {
......@@ -47,8 +45,6 @@ export class Capture {
} else {
this.getAlbums();
}
this.title.setTitle('Capture');
}
getAlbums() {
......
......@@ -3,7 +3,6 @@ import { Router, ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs';
import { MindsTitle } from '../../services/ux/title';
import { Client } from '../../services/api';
import { Session } from '../../services/session';
import { ContextService } from '../../services/context.service';
......@@ -35,13 +34,10 @@ export class Discovery {
public client: Client,
public router: Router,
public route: ActivatedRoute,
public title: MindsTitle,
private context: ContextService
) {}
ngOnInit() {
this.title.setTitle('Discovery');
this.paramsSubscription = this.route.params.subscribe(params => {
if (params['filter']) {
this._filter = params['filter'];
......@@ -185,21 +181,7 @@ export class Discovery {
}
setCity(row: any) {
this.cities = [];
if (row.address.city) window.Minds.user.city = row.address.city;
if (row.address.town) window.Minds.user.city = row.address.town;
this.city = window.Minds.user.city;
this.entities = [];
this.inProgress = true;
this.client
.post('api/v1/channel/info', {
coordinates: row.lat + ',' + row.lon,
city: window.Minds.user.city,
})
.then((response: any) => {
this.inProgress = false;
this.setNearby(true);
});
// Deprecated
}
setNearby(nearby: boolean) {
......
......@@ -4,8 +4,8 @@ import { ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs';
import { Client } from '../../services/api';
import { MindsTitle } from '../../services/ux/title';
import { Navigation as NavigationService } from '../../services/navigation';
import { MetaService } from '../../common/services/meta.service';
import { PagesService } from '../../common/services/pages.service';
@Component({
......@@ -27,7 +27,7 @@ export class Pages {
paramsSubscription: Subscription;
constructor(
public titleService: MindsTitle,
public metaService: MetaService,
public client: Client,
public navigation: NavigationService,
public route: ActivatedRoute,
......@@ -35,7 +35,6 @@ export class Pages {
) {}
ngOnInit() {
this.titleService.setTitle('...');
this.setUpMenu();
this.paramsSubscription = this.route.params.subscribe(params => {
......@@ -57,7 +56,7 @@ export class Pages {
this.path = response.path;
this.header = response.header;
this.headerTop = response.headerTop;
this.titleService.setTitle(this.title);
this.updateMeta();
this.bodyElement.nativeElement.innerHTML = this.body;
});
}
......@@ -65,4 +64,13 @@ export class Pages {
setUpMenu() {
this.pages = this.navigation.getItems('footer');
}
private updateMeta(): void {
const description =
this.body.length > 140 ? this.body.substr(0, 140) + '...' : this.body;
this.metaService
.setTitle(this.title)
.setDescription(description)
.setOgImage(`/fs/v1/pages/${this.path}`);
}
}
......@@ -4,6 +4,7 @@ import { ActivatedRoute, Router } from '@angular/router';
import { Subscription } from 'rxjs';
import { Title } from '@angular/platform-browser';
import { Session } from '../../services/session';
import { CookieService } from '../../common/services/cookie.service';
@Component({
moduleId: module.id,
selector: 'minds-rewards-component',
......@@ -30,9 +31,9 @@ export class RewardsComponent {
private client: Client,
private route: ActivatedRoute,
private router: Router,
private title: Title
cookieService: CookieService
) {
if (localStorage.getItem('redirect')) localStorage.removeItem('redirect');
if (cookieService.get('redirect')) cookieService.remove('redirect');
this.loggedIn = this.session.isLoggedIn();
......@@ -57,9 +58,7 @@ export class RewardsComponent {
});
}
ngOnInit() {
this.title.setTitle('Claim your Rewards');
}
ngOnInit() {}
ngOnDestroy() {
this.paramsSubscription.unsubscribe();
......
......@@ -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,
];
......@@ -10,7 +10,7 @@ export default class AsyncStatus {
done(): this {
if (this.ready) {
throw new Error('Already done');
// throw new Error('Already done');
}
this.ready = true;
......@@ -40,4 +40,8 @@ export default class AsyncStatus {
});
});
}
static _() {
return new AsyncStatus();
}
}
......@@ -15,6 +15,6 @@ export default function isMobileOrTablet() {
)
)
check = true;
})(navigator.userAgent || navigator.vendor);
})(navigator.userAgent || navigator.vendor || '');
return check;
}
......@@ -15,6 +15,6 @@ export default function isMobile() {
)
)
check = true;
})(navigator.userAgent || navigator.vendor || (<any>window).opera);
})(navigator.userAgent || navigator.vendor || '');
return check;
}
export default function mediaProxyUrl(url, size = 1920) {
if (!url || typeof url !== 'string') {
return url;
}
const encodedUrl = encodeURIComponent(url);
return `${window.Minds.cdn_url}api/v2/media/proxy?size=${size}&src=${encodedUrl}`;
}
////
// Is this file used?
// Remove if not
////
import {
TRANSLATIONS,
TRANSLATIONS_FORMAT,
......
......@@ -29,7 +29,13 @@ export interface MindsBlogEntity {
access_id?: number;
license?: string;
allow_comments: boolean;
perma_url?: string;
custom_meta?: {
title: string;
description: string;
author: string;
};
perma_url: string;
thumbnail: string;
}
export interface Message {}
......@@ -57,6 +63,13 @@ export interface MindsUser {
username: string;
chat?: boolean;
icontime: number;
avatar_url?: {
tiny: string;
small: string;
medium: string;
large: string;
master: string;
};
blocked?: boolean;
carousels?: any[] | boolean;
city?: string;
......
import { Component, OnDestroy, OnInit } from '@angular/core';
import {
Component,
OnDestroy,
OnInit,
Inject,
PLATFORM_ID,
} from '@angular/core';
import { Client } from '../../services/api';
import { Session } from '../../services/session';
import { Storage } from '../../services/storage';
import { Subscription } from 'rxjs';
import { SettingsService } from '../settings/settings.service';
import { isPlatformServer } from '@angular/common';
@Component({
selector: 'm-ads-boost',
......@@ -34,7 +41,8 @@ export class BoostAds implements OnInit, OnDestroy {
public client: Client,
public session: Session,
private storage: Storage,
private settingsService: SettingsService
private settingsService: SettingsService,
@Inject(PLATFORM_ID) private platformId: Object
) {}
ngOnInit() {
......@@ -52,6 +60,7 @@ export class BoostAds implements OnInit, OnDestroy {
}
fetch() {
if (isPlatformServer(this.platformId)) return;
if (this.storage.get('boost:offset:sidebar'))
this.offset = this.storage.get('boost:offset:sidebar');
this.client
......
import { NgModule } from '@angular/core';
import * as PlotlyJS from 'plotly.js/dist/plotly.js';
//import * as PlotlyJS from 'plotly.js/dist/plotly.js';
import { PlotlyModule } from 'angular-plotly.js';
import { AdminAnalyticsComponent } from './pages/admin/admin.component';
......@@ -63,7 +63,7 @@ import { FormsModule } from '@angular/forms';
import { AnalyticsSearchSuggestionsComponent } from './v2/components/search-suggestions/search-suggestions.component';
import { AnalyticsBenchmarkComponent } from './v2/components/benchmark/benchmark.component';
PlotlyModule.plotlyjs = PlotlyJS;
//PlotlyModule.plotlyjs = PlotlyJS;
const routes: Routes = [
{
......@@ -71,22 +71,21 @@ const routes: Routes = [
component: AnalyticsComponent,
children: [
{ path: '', redirectTo: 'dashboard/traffic', pathMatch: 'full' },
{
path: 'admin',
component: AdminAnalyticsComponent,
children: [
{ path: '', redirectTo: 'network', pathMatch: 'full' },
{ path: 'network', component: SiteNetworkActivityAnalyticsComponent },
{ path: 'token', component: SiteTokenTransactionsAnalyticsComponent },
// { path: 'plus', component: OffChainBoostsCardComponent},
],
},
{
path: 'dashboard',
redirectTo: 'dashboard/traffic',
pathMatch: 'full',
},
{ path: 'dashboard/:category', component: AnalyticsDashboardComponent },
{
path: 'dashboard/:category',
component: AnalyticsDashboardComponent,
data: {
title: 'Analytics',
description:
'Track your traffic, earnings, engagement and trending analytics',
ogImage: '/assets/photos/network.jpg',
},
},
],
},
];
......
......@@ -4,7 +4,6 @@ import { Session } from '../../../../services/session';
import { Router } from '@angular/router';
@Component({
moduleId: module.id,
selector: 'm-analytics__admin',
templateUrl: 'admin.component.html',
})
......
......@@ -25,7 +25,6 @@ export class AnalyticsTableComponent implements OnInit, OnDestroy {
columns: Array<any>;
rows: Array<any>;
reformattedBuckets: Array<any> = [];
minds = window.Minds;
user;
loadingSubscription: Subscription;
loading: boolean;
......
......@@ -10,7 +10,6 @@ import { FormControl } from '@angular/forms';
import { ActivatedRoute, Router, ParamMap } from '@angular/router';
import { Subscription, Observable } from 'rxjs';
import { MindsTitle } from '../../../services/ux/title';
import { Client } from '../../../services/api';
import { Session } from '../../../services/session';
......@@ -18,6 +17,7 @@ import { AnalyticsDashboardService } from './dashboard.service';
import { Filter } from './../../../interfaces/dashboard';
import sidebarMenu from './sidebar-menu.default';
import { Menu } from '../../../common/components/sidebar-menu/sidebar-menu.component';
import { MetaService } from '../../../common/services/meta.service';
@Component({
selector: 'm-analytics__dashboard',
......@@ -48,7 +48,6 @@ export class AnalyticsDashboardComponent implements OnInit, OnDestroy {
public route: ActivatedRoute,
private router: Router,
public session: Session,
public title: MindsTitle,
public analyticsService: AnalyticsDashboardService,
private cd: ChangeDetectorRef
) {}
......@@ -59,8 +58,6 @@ export class AnalyticsDashboardComponent implements OnInit, OnDestroy {
return;
}
this.title.setTitle('Analytics');
this.route.paramMap.subscribe((params: ParamMap) => {
const cat = params.get('category');
this.updateCategory(cat);
......
......@@ -17,11 +17,29 @@ import { RegisterComponent } from './register.component';
import { ForgotPasswordComponent } from './forgot-password/forgot-password.component';
const routes: Routes = [
{ path: 'login', component: LoginComponent },
{
path: 'login',
component: LoginComponent,
data: { title: 'Login', description: 'Login to Minds or create a channel' },
},
{ path: 'logout/all', component: LogoutComponent },
{ path: 'logout', component: LogoutComponent },
{ path: 'register', component: RegisterComponent },
{ path: 'forgot-password', component: ForgotPasswordComponent },
{
path: 'register',
component: RegisterComponent,
data: {
title: 'Register',
description: 'Create a channel on Minds',
},
},
{
path: 'forgot-password',
component: ForgotPasswordComponent,
data: {
title: 'Forgot Password',
description: 'Reset your password on Minds',
},
},
];
@NgModule({
......
......@@ -18,8 +18,6 @@ import {
Output,
} from '@angular/core';
import { clientMock } from '../../../../tests/client-mock.spec';
import { mindsTitleMock } from '../../../mocks/services/ux/minds-title.service.mock.spec';
import { MindsTitle } from '../../../services/ux/title';
import { sessionMock } from '../../../../tests/session-mock.spec';
import { Session } from '../../../services/session';
import { Client } from '../../../services/api/client';
......@@ -78,7 +76,6 @@ describe('ForgotPasswordComponent', () => {
providers: [
{ provide: Session, useValue: sessionMock },
{ provide: Client, useValue: clientMock },
{ provide: MindsTitle, useValue: mindsTitleMock },
],
}).compileComponents();
}));
......
......@@ -3,7 +3,6 @@ import { ActivatedRoute, Router } from '@angular/router';
import { Subscription } from 'rxjs';
import { MindsTitle } from '../../../services/ux/title';
import { Client } from '../../../services/api';
import { Session } from '../../../services/session';
......@@ -25,13 +24,10 @@ export class ForgotPasswordComponent {
public client: Client,
public router: Router,
public route: ActivatedRoute,
public title: MindsTitle,
public session: Session
) {}
ngOnInit() {
this.title.setTitle('Forgot Password');
this.paramsSubscription = this.route.params.subscribe(params => {
if (params['code']) {
this.setCode(params['code']);
......
......@@ -22,11 +22,16 @@ import { LoginReferrerService } from '../../services/login-referrer.service';
import { loginReferrerServiceMock } from '../../mocks/services/login-referrer-service-mock.spec';
import { onboardingServiceMock } from '../../mocks/modules/onboarding/onboarding.service.mock.spec';
import { OnboardingService } from '../onboarding/onboarding.service';
import { MindsTitle } from '../../services/ux/title';
import { mindsTitleMock } from '../../mocks/services/ux/minds-title.service.mock.spec';
import { signupModalServiceMock } from '../../mocks/modules/modals/signup/signup-modal-service.mock';
import { SignupModalService } from '../modals/signup/service';
import { By } from '@angular/platform-browser';
import { Storage } from '../../services/storage';
import {
CookieService,
CookieOptionsProvider,
COOKIE_OPTIONS,
CookieModule,
} from '@gorniv/ngx-universal';
import { FeaturesService } from '../../services/features.service';
import { featuresServiceMock } from '../../../tests/features-service-mock.spec';
import { IfFeatureDirective } from '../../common/directives/if-feature.directive';
......@@ -73,14 +78,17 @@ describe('LoginComponent', () => {
ReactiveFormsModule,
CommonModule,
FormsModule,
CookieModule,
],
providers: [
{ provide: Session, useValue: sessionMock },
{ provide: Client, useValue: clientMock },
{ provide: LoginReferrerService, useValue: loginReferrerServiceMock },
{ provide: OnboardingService, useValue: onboardingServiceMock },
{ provide: MindsTitle, useValue: mindsTitleMock },
{ provide: SignupModalService, useValue: signupModalServiceMock },
Storage,
CookieService,
{ provide: COOKIE_OPTIONS, useValue: CookieOptionsProvider },
{ provide: FeaturesService, useValue: featuresServiceMock },
{ provide: V2TopbarService, useValue: MockService(V2TopbarService) },
],
......
......@@ -4,10 +4,11 @@ import { ActivatedRoute, Router } from '@angular/router';
import { Subscription } from 'rxjs';
import { SignupModalService } from '../modals/signup/service';
import { MindsTitle } from '../../services/ux/title';
import { Client } from '../../services/api';
import { Session } from '../../services/session';
import { LoginReferrerService } from '../../services/login-referrer.service';
import { OnboardingService } from '../onboarding/onboarding.service';
import { CookieService } from '../../common/services/cookie.service';
import { FeaturesService } from '../../services/features.service';
import { V2TopbarService } from '../../common/layout/v2-topbar/v2-topbar.service';
import { iOSVersion } from '../../helpers/is-safari';
......@@ -22,7 +23,7 @@ export class LoginComponent implements OnInit, OnDestroy {
hideLogin: boolean = false;
inProgress: boolean = false;
referrer: string;
minds = window.Minds;
private redirectTo: string;
@HostBinding('class.m-login__newDesign')
newDesign: boolean = false;
......@@ -35,18 +36,17 @@ export class LoginComponent implements OnInit, OnDestroy {
paramsSubscription: Subscription;
private redirectTo: string;
constructor(
public client: Client,
public router: Router,
public route: ActivatedRoute,
public title: MindsTitle,
private modal: SignupModalService,
private loginReferrer: LoginReferrerService,
public session: Session,
private cookieService: CookieService,
private onboarding: OnboardingService,
private featuresService: FeaturesService,
private topbarService: V2TopbarService,
public session: Session
private topbarService: V2TopbarService
) {}
ngOnInit() {
......@@ -55,8 +55,7 @@ export class LoginComponent implements OnInit, OnDestroy {
this.loginReferrer.navigate();
}
this.title.setTitle('Login');
this.redirectTo = localStorage.getItem('redirect');
this.redirectTo = this.cookieService.get('redirect');
this.paramsSubscription = this.route.queryParams.subscribe(params => {
if (params['referrer']) {
......
......@@ -24,13 +24,13 @@
<div class="m-register--hero--video">
<video autoplay muted loop *ngIf="!videoError; else fallback">
<source
[src]="minds.cdn_assets_url + 'assets/videos/what-1/what-1.mp4'"
[src]="cdnAssetsUrl + 'assets/videos/what-1/what-1.mp4'"
type="video/mp4"
(error)="onSourceError()"
/>
</video>
<ng-template #fallback>
<img [src]="minds.cdn_assets_url + 'assets/photos/cover.png'" />
<img [src]="cdnAssetsUrl + 'assets/photos/cover.png'" />
</ng-template>
</div>
<div class="m-register--hero--inner">
......@@ -55,7 +55,7 @@
<div class="mdl-grid mdl-grid--no-spacing m-register--footer">
<section class="mdl-cell mdl-cell--12-col m-footer">
<img [src]="minds.cdn_assets_url + 'assets/logos/logo.svg'" />
<img [src]="cdnAssetsUrl + 'assets/logos/logo.svg'" />
<ul class="m-footer-nav m-footer-nav-inline">
<li
*ngFor="let page of navigation.getItems('footer')"
......
......@@ -56,7 +56,6 @@ describe('RegisterComponent', () => {
comp = fixture.componentInstance;
featuresServiceMock.mock('register_pages-december-2019', false);
window.Minds.cdn_assets_url = 'http://dev.minds.io/';
comp.flags.canPlayInlineVideos = true;
fixture.detectChanges();
......
......@@ -9,11 +9,12 @@ import { Session } from '../../services/session';
import { SignupModalService } from '../modals/signup/service';
import { LoginReferrerService } from '../../services/login-referrer.service';
import { OnboardingService } from '../onboarding/onboarding.service';
import { ConfigsService } from '../../common/services/configs.service';
import { PagesService } from '../../common/services/pages.service';
import { MindsTitle } from '../../services/ux/title';
import { FeaturesService } from '../../services/features.service';
import { V2TopbarService } from '../../common/layout/v2-topbar/v2-topbar.service';
import { OnboardingV2Service } from '../onboarding-v2/service/onboarding.service';
import { MetaService } from '../../common/services/meta.service';
import { iOSVersion } from '../../helpers/is-safari';
@Component({
......@@ -21,7 +22,7 @@ import { iOSVersion } from '../../helpers/is-safari';
templateUrl: 'register.component.html',
})
export class RegisterComponent implements OnInit, OnDestroy {
minds = window.Minds;
readonly cdnAssetsUrl: string;
errorMessage: string = '';
twofactorToken: string = '';
hideLogin: boolean = false;
......@@ -50,12 +51,14 @@ export class RegisterComponent implements OnInit, OnDestroy {
public session: Session,
private onboarding: OnboardingService,
public navigation: NavigationService,
configs: ConfigsService,
public pagesService: PagesService,
private featuresService: FeaturesService,
private topbarService: V2TopbarService,
private onboardingService: OnboardingV2Service,
public title: MindsTitle
private metaService: MetaService
) {
this.cdnAssetsUrl = configs.get('cdn_assets_url');
if (this.session.isLoggedIn()) {
this.router.navigate(['/newsfeed']);
return;
......@@ -83,7 +86,7 @@ export class RegisterComponent implements OnInit, OnDestroy {
}
});
this.title.setTitle('Register');
this.metaService.setTitle('Register');
if (/iP(hone|od)/.test(window.navigator.userAgent)) {
this.flags.canPlayInlineVideos = false;
......
import { NgModule } from '@angular/core';
import { NgModule, Inject, PLATFORM_ID } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { CommonModule as NgCommonModule } from '@angular/common';
......@@ -27,6 +27,7 @@ import { BlockchainEthModalComponent } from './eth-modal/eth-modal.component';
import { BlockchainMarketingOnboardComponent } from './token-purchase/onboard.component';
import { BlockchainPurchaseComponent } from './token-purchase/purchase.component';
import { ModalsModule } from '../modals/modals.module';
import { ConfigsService } from '../../common/services/configs.service';
const cryptoRoutes: Routes = [
{
......@@ -70,38 +71,19 @@ const cryptoRoutes: Routes = [
{
provide: Web3WalletService,
useFactory: Web3WalletService._,
deps: [LocalWalletService, TransactionOverlayService],
},
{
provide: TokenContractService,
useFactory: TokenContractService._,
deps: [Web3WalletService, TransactionOverlayService],
},
{
provide: WireContractService,
useFactory: WireContractService._,
deps: [Web3WalletService, TokenContractService],
},
{
provide: WithdrawContractService,
useFactory: WithdrawContractService._,
deps: [Web3WalletService],
},
{
provide: BoostContractService,
useFactory: BoostContractService._,
deps: [Web3WalletService, TokenContractService],
},
{
provide: TokenDistributionEventService,
useFactory: TokenDistributionEventService._,
deps: [Web3WalletService],
},
{
provide: OffchainPaymentService,
useFactory: OffchainPaymentService._,
deps: [Client],
deps: [
LocalWalletService,
TransactionOverlayService,
PLATFORM_ID,
ConfigsService,
],
},
TokenContractService,
WireContractService,
WithdrawContractService,
BoostContractService,
TokenDistributionEventService,
OffchainPaymentService,
],
exports: [
BlockchainWalletSelector,
......
import { BlockchainService } from './blockchain.service';
import { fakeAsync } from '@angular/core/testing';
import { clientMock } from '../../../tests/client-mock.spec';
import { sessionMock } from '../../../tests/session-mock.spec';
describe('BlockchainService', () => {
let service: BlockchainService;
......@@ -8,9 +9,8 @@ describe('BlockchainService', () => {
beforeEach(() => {
jasmine.clock().uninstall();
jasmine.clock().install();
service = new BlockchainService(clientMock);
service = new BlockchainService(clientMock, sessionMock);
clientMock.response = {};
window.Minds = <any>{ user: { eth_wallet: null }, context: 'minds' };
});
afterEach(() => {
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.