...
 
Commits (100)
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,8 @@ qa:manual:
only:
refs:
- master
- epic/SSR
- production
- test/gitlab-ci
allow_failure: false
......@@ -86,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:
......@@ -95,6 +99,8 @@ build:review:
except:
refs:
- master
- epic/SSR
- production
- test/gitlab-ci
build:production:en:
......@@ -106,13 +112,19 @@ 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
build:production:i18n:
......@@ -131,6 +143,8 @@ build:production:i18n:
only:
refs:
- master
- epic/SSR
- production
- test/gitlab-ci
#################
......@@ -155,13 +169,15 @@ 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
prepare:review:sentry:
......@@ -171,6 +187,8 @@ prepare:review:sentry:
except:
refs:
- master
- epic/SSR
- production
- test/gitlab-ci
dependencies:
- build:review
......@@ -182,11 +200,13 @@ 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:
- build:production:en
......@@ -199,6 +219,8 @@ prepare:production:sentry:
only:
refs:
- master
- epic/SSR
- production
- test/gitlab-ci
dependencies:
- build:production:en
......@@ -222,6 +244,8 @@ prepare:production:sentry:
except:
refs:
- master
- epic/SSR
- production
- test/gitlab-ci
review:start:
......@@ -233,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 \
......@@ -249,6 +273,8 @@ review:start:
except:
refs:
- master
- epic/SSR
- production
- test/gitlab-ci
review:stop:
......@@ -268,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
......@@ -280,10 +306,6 @@ review:stop:
dependencies:
- build:production:en
- build:production:i18n
only:
refs:
- master
- test/gitlab-ci
staging:fpm:
<<: *deploy
......@@ -294,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
......@@ -306,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
......@@ -318,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 #
......@@ -330,4 +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 /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,38 +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",
......@@ -47,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",
......@@ -54,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",
......@@ -91,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: 5 * 60, checkperiod: 120 });
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__/${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();
}
};
};
// 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">
<ng-container *ngIf="useNewNavigation; else v2Topbar">
<m-v3-topbar>
<m-v3topbar>
<ng-container search>
<m-search--bar
[defaultSizes]="false"
[showCleanIcon]="true"
></m-search--bar>
</ng-container>
</m-v3-topbar>
</m-v3topbar>
</ng-container>
<ng-template #v2Topbar>
<m-v2-topbar *mIfFeature="'top-feeds'; else legacyTopbar">
<m-v2-topbar>
<ng-container search>
<m-search--bar [defaultSizes]="false"></m-search--bar>
</ng-container>
......@@ -23,18 +23,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')"
......@@ -43,11 +31,12 @@
</ng-container>
<m-body
[class.has-markers-sidebar]="hasMarkersSidebar()"
[class.has-v2-navbar]="featuresService.has('top-feeds')"
[class.has-v3-navbar]="featuresService.has('navigation-2020')"
[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"
......@@ -69,16 +58,23 @@
</ng-template>
</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 'defaults';
@import 'themes';
@import 'foundation/grid-values.scss';
html,
body {
......@@ -38,6 +39,15 @@ m-app {
grid-gap: 0;
height: 100%;
@media screen and (max-width: $m-grid-max-tablet) {
grid-template-columns: 71px 5fr 4fr;
}
@media screen and(max-width: $m-grid-max-mobile) {
display: flex;
flex-direction: column;
}
}
}
......
import {
ChangeDetectorRef,
Component,
PLATFORM_ID,
Inject,
HostBinding,
OnDestroy,
OnInit,
} from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
import { NotificationService } from './modules/notifications/notification.service';
import { AnalyticsService } from './services/analytics';
......@@ -16,7 +19,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, 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';
......@@ -28,17 +31,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 implements OnInit, OnDestroy {
name: string;
minds = window.Minds;
ready: boolean = false;
showOnboarding: boolean = false;
......@@ -49,6 +52,8 @@ export class Minds implements OnInit, OnDestroy {
protected router$: Subscription;
protected routerConfig: Route[];
constructor(
public session: Session,
public route: ActivatedRoute,
......@@ -67,11 +72,15 @@ export class Minds implements OnInit, OnDestroy {
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';
......@@ -82,8 +91,27 @@ export class Minds implements OnInit, OnDestroy {
async ngOnInit() {
this.useNewNavigation = this.featuresService.has('navigation-2020');
// 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()) {
......@@ -106,7 +134,9 @@ export class Minds implements OnInit, OnDestroy {
async initialize() {
this.blockListService.fetch();
if (!this.site.isProDomain) {
if (this.site.isProDomain) {
this.site.listen();
} else {
this.notificationService.getNotifications();
}
......@@ -116,16 +146,12 @@ export class Minds implements OnInit, OnDestroy {
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);
}
}
});
......@@ -157,11 +183,22 @@ export class Minds implements OnInit, OnDestroy {
this.webtorrent.setUp();
this.themeService.setUp();
this.socketsService.setUp();
}
hasMarkersSidebar() {
return (
this.session.isLoggedIn() &&
!this.isProDomain &&
!this.featuresService.has('navigation-2020')
);
}
ngOnDestroy() {
this.loginReferrer.unlisten();
this.scrollToTop.unlisten();
this.router$.unsubscribe();
}
@HostBinding('class') get cssColorSchemeOverride() {
......@@ -176,6 +213,12 @@ export class Minds implements OnInit, OnDestroy {
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';
......@@ -128,14 +132,20 @@ import { FormDescriptorComponent } from './components/form-descriptor/form-descr
import { FormToastComponent } from './components/form-toast/form-toast.component';
import { SsoService } from './services/sso.service';
import { PagesService } from './services/pages.service';
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';
import { V3TopbarComponent } from './layout/v3-topbar/v3-topbar.component';
PlotlyModule.plotlyjs = PlotlyJS;
import { SidebarNavigationService } from './layout/sidebar/navigation.service';
import { TopbarService } from './layout/topbar.service';
const routes: Routes = [
{
......@@ -201,7 +211,6 @@ const routes: Routes = [
MDL_DIRECTIVES,
DateSelectorComponent,
MindsAvatar,
CaptchaComponent,
Textarea,
InlineEditorComponent,
......@@ -217,6 +226,7 @@ const routes: Routes = [
MaterialBoundSwitchComponent,
IfFeatureDirective,
IfBrowserDirective,
CategoriesSelectorComponent,
CategoriesSelectedComponent,
......@@ -244,6 +254,7 @@ const routes: Routes = [
SwitchComponent,
FeaturedContentComponent,
AttachmentPasteDirective,
PosterDateSelectorComponent,
DraggableListComponent,
ToggleComponent,
......@@ -262,6 +273,7 @@ const routes: Routes = [
ShadowboxSubmitButtonComponent,
EmailConfirmationComponent,
DateDropdownsComponent,
FormInputCheckboxComponent,
],
exports: [
MINDS_PIPES,
......@@ -308,7 +320,6 @@ const routes: Routes = [
MDL_DIRECTIVES,
DateSelectorComponent,
MindsAvatar,
CaptchaComponent,
Textarea,
InlineEditorComponent,
......@@ -324,6 +335,7 @@ const routes: Routes = [
MaterialBoundSwitchComponent,
IfFeatureDirective,
IfBrowserDirective,
CategoriesSelectorComponent,
CategoriesSelectedComponent,
......@@ -349,6 +361,7 @@ const routes: Routes = [
SwitchComponent,
NSFWSelectorComponent,
FeaturedContentComponent,
AttachmentPasteDirective,
PosterDateSelectorComponent,
ChannelModeSelectorComponent,
DraggableListComponent,
......@@ -366,16 +379,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) => {
......@@ -386,18 +397,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: (
......@@ -434,9 +437,32 @@ 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,
SidebarNavigationService,
TopbarService,
{
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
);
}
);
......
......@@ -6,7 +6,6 @@ import {
EventEmitter,
Input,
Output,
SimpleChange,
} from '@angular/core';
declare var tinymce;
......@@ -22,6 +21,9 @@ declare var tinymce;
(keyup)="change()"
(blur)="change()"
(paste)="paste($event); change()"
(filePaste)="filePaste.emit($event)"
m-attachment-paste
tabindex="0"
></div>
<span *ngIf="placeholder && model.length === 0" class="m-placeholder">{{
placeholder
......@@ -35,8 +37,11 @@ export class Textarea implements OnChanges {
@Input('mModel') model: string = '';
@Output('mModelChange') update: EventEmitter<any> = new EventEmitter();
@Input('disabled') disabled: boolean = false;
@Input('placeholder') placeholder: string = '';
@Input() disabled: boolean = false;
@Input() placeholder: string = '';
@Output()
filePaste: EventEmitter<File> = new EventEmitter<File>();
getControlText(): string {
return this.editorControl.nativeElement.innerText;
......
......@@ -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,8 +5,8 @@ import {
OnDestroy,
OnInit,
} from '@angular/core';
import { MindsTitle } from '../../../services/ux/title';
import { V2TopbarService } from '../../layout/v2-topbar/v2-topbar.service';
import { MetaService } from '../../services/meta.service';
import { TopbarService } from '../../layout/topbar.service';
@Component({
selector: 'm-marketing',
......@@ -19,16 +19,16 @@ export class MarketingComponent implements OnInit, OnDestroy {
@Input() forceBackground: boolean = true;
constructor(
protected title: MindsTitle,
private topbarService: V2TopbarService
protected metaService: MetaService,
private navigationService: TopbarService
) {}
ngOnInit() {
if (this.pageTitle) {
this.title.setTitle(this.pageTitle);
this.metaService.setTitle(this.pageTitle);
}
this.topbarService.toggleMarketingPages(
this.navigationService.toggleMarketingPages(
true,
this.showBottombar,
this.forceBackground
......@@ -36,6 +36,6 @@ export class MarketingComponent implements OnInit, OnDestroy {
}
ngOnDestroy() {
this.topbarService.toggleMarketingPages(false);
this.navigationService.toggleMarketingPages(false);
}
}
......@@ -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');
}
}
......@@ -13,6 +13,7 @@ export class TooltipComponent {
@Input() anchor: 'top' | 'bottom' | 'right' | 'left';
@Input() iconClass;
@Input() useParentPosition: boolean = false;
@Input() enabled: boolean = true;
hidden: boolean = true;
offsetTop: number = 0;
......@@ -22,6 +23,10 @@ export class TooltipComponent {
constructor(private element: ElementRef) {}
setHidden(value: boolean) {
if (!value && !this.enabled) {
return;
}
this.hidden = value;
if (!this.hidden && this.useParentPosition) {
......
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
);
}
}
}
}
}
import { Directive, EventEmitter, HostListener, Output } from '@angular/core';
@Directive({ selector: '[m-attachment-paste]' })
export class AttachmentPasteDirective {
@Output()
filePaste: EventEmitter<File> = new EventEmitter<File>();
private focused: boolean = false;
@HostListener('focus') onFocus() {
this.focused = true;
}
@HostListener('focusout') onFocusOut() {
this.focused = false;
}
@HostListener('window:paste', ['$event']) onPaste(event: ClipboardEvent) {
if (this.focused) {
for (const index in event.clipboardData.items) {
const item: DataTransferItem = event.clipboardData.items[index];
if (item.kind === 'file') {
this.filePaste.emit(item.getAsFile());
break;
}
}
}
}
}
......@@ -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;
......
<nav>
<ul>
<li>
<a
class="m-sidebar--navigation--item"
[routerLink]="['/newsfeed/subscriptions']"
[routerLinkActive]="'m-sidebar--navigation--item-active'"
[routerLinkActiveOptions]="{ exact: true }"
[title]="'Newsfeed'"
>
<i class="material-icons">check</i>
<span class="m-sidebar--navigation--text" i18n>Newsfeed</span>
<div class="m-sidebarNavigation__overlay" (click)="toggle()"></div>
<div
class="m-sidebar--navigation"
[class.m-sidebarNavigation--slide]="layoutMode === 'phone'"
>
<nav>
<div class="m-sidebarNavigation__top">
<a routerLink="/" title="Home" i18n-title>
<img [src]="cdnAssetsUrl + 'assets/logos/bulb.svg'" />
</a>
</li>
<li>
<a
class="m-sidebar--navigation--item"
[routerLink]="['/newsfeed/global/top']"
[routerLinkActive]="'m-sidebar--navigation--item-active'"
[routerLinkActiveOptions]="{ exact: true }"
[title]="'Discover'"
>
<i class="material-icons">check</i>
<i class="material-icons" (click)="toggle()">close</i>
</div>
<ul>
<li>
<a
class="m-sidebar--navigation--item"
[routerLink]="['/newsfeed/subscriptions']"
[routerLinkActive]="'m-sidebar--navigation--item-active'"
[routerLinkActiveOptions]="{ exact: true }"
(click)="toggle()"
>
<m-tooltip
icon="check"
i18n="@@COMMON__NEWSFEED"
[enabled]="layoutMode === 'tablet'"
>
Newsfeed
</m-tooltip>
<span class="m-sidebar--navigation--text" i18n>Discover</span>
</a>
</li>
<span
class="m-sidebar--navigation--text"
*ngIf="layoutMode !== 'tablet'"
i18n
>
Newsfeed
</span>
</a>
</li>
<li>
<a
class="m-sidebar--navigation--item"
[routerLink]="['/wallet']"
[routerLinkActive]="'m-sidebar--navigation--item-active'"
[title]="'Wallet'"
>
<i class="material-icons">check</i>
<li>
<a
class="m-sidebar--navigation--item"
[routerLink]="['/newsfeed/global/top']"
[routerLinkActive]="'m-sidebar--navigation--item-active'"
[routerLinkActiveOptions]="{ exact: true }"
(click)="toggle()"
>
<m-tooltip
icon="check"
i18n="@@COMMON__DISCOVER"
[enabled]="layoutMode === 'tablet'"
>
Discover
</m-tooltip>
<span class="m-sidebar--navigation--text" i18n>Wallet</span>
</a>
</li>
<span
class="m-sidebar--navigation--text"
*ngIf="layoutMode !== 'tablet'"
i18n
>
Discover
</span>
</a>
</li>
<li>
<a
class="m-sidebar--navigation--item"
[routerLink]="['/' + user.username]"
[routerLinkActive]="'m-sidebar--navigation--item-active'"
[title]="'Profile'"
>
<i class="material-icons">check</i>
<li>
<a
class="m-sidebar--navigation--item"
[routerLink]="['/wallet']"
[routerLinkActive]="'m-sidebar--navigation--item-active'"
(click)="toggle()"
>
<m-tooltip
icon="check"
i18n="@@COMMON__WALLET"
[enabled]="layoutMode === 'tablet'"
>
Wallet
</m-tooltip>
<span class="m-sidebar--navigation--text" i18n>Profile</span>
</a>
</li>
<span
class="m-sidebar--navigation--text"
*ngIf="layoutMode !== 'tablet'"
i18n
>
Wallet
</span>
</a>
</li>
<li>
<a
class="m-sidebar--navigation--item"
[routerLink]="['/analytics']"
[routerLinkActive]="'m-sidebar--navigation--item-active'"
[title]="'Analytics'"
>
<i class="material-icons">check</i>
<li>
<a
class="m-sidebar--navigation--item"
[routerLink]="['/' + user.username]"
[routerLinkActive]="'m-sidebar--navigation--item-active'"
(click)="toggle()"
>
<m-tooltip
icon="check"
i18n="@@COMMON__PROFILE"
[enabled]="layoutMode === 'tablet'"
>
Profile
</m-tooltip>
<span class="m-sidebar--navigation--text" i18n>Analytics</span>
</a>
</li>
<span
class="m-sidebar--navigation--text"
*ngIf="layoutMode !== 'tablet'"
i18n
>
Profile
</span>
</a>
</li>
<li>
<a
class="m-sidebar--navigation--item"
[routerLink]="['/settings']"
[routerLinkActive]="'m-sidebar--navigation--item-active'"
[title]="'Settings'"
>
<i class="material-icons">check</i>
<li>
<a
class="m-sidebar--navigation--item"
[routerLink]="['/analytics']"
[routerLinkActive]="'m-sidebar--navigation--item-active'"
(click)="toggle()"
>
<m-tooltip
icon="check"
i18n="@@COMMON__ANALYTICS"
[enabled]="layoutMode === 'tablet'"
>
Analytics
</m-tooltip>
<span
class="m-sidebar--navigation--text"
*ngIf="layoutMode !== 'tablet'"
i18n
>
Analytics
</span>
</a>
</li>
<span class="m-sidebar--navigation--text" i18n>Settings</span>
</a>
</li>
</ul>
</nav>
<h5 i18n>
Groups
</h5>
<ng-template dynamic-host></ng-template>
<li>
<a
class="m-sidebar--navigation--item"
[routerLink]="['/settings']"
[routerLinkActive]="'m-sidebar--navigation--item-active'"
(click)="toggle()"
>
<m-tooltip
icon="check"
i18n="@@COMMON__SETTINGS"
[enabled]="layoutMode === 'tablet'"
>
Settings
</m-tooltip>
<span
class="m-sidebar--navigation--text"
*ngIf="layoutMode !== 'tablet'"
i18n
>
Settings
</span>
</a>
</li>
</ul>
</nav>
<ng-template dynamic-host></ng-template>
</div>
m-sidebar--navigation {
grid-area: nav;
display: contents;
padding-top: 8px !important;
padding-left: 33.3%;
min-width: 0;
&.m-sidebarNavigation--opened {
.m-sidebarNavigation__overlay {
z-index: 999998;
@include m-theme() {
background-color: themed($m-body-bg);
border-right: 1px solid themed($m-grey-400);
}
@include m-theme() {
background-color: rgba(themed($m-grey-700), 0.2);
}
}
nav {
//min-height: 100vh;
//height: 100%;
.m-sidebar--navigation.m-sidebarNavigation--slide {
transform: translateX(316px);
}
}
ul {
list-style: none;
padding: 0;
.m-sidebarNavigation__overlay {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: -1;
background-color: transparent;
transition: background-color 0.5s cubic-bezier(0.075, 0.82, 0.165, 1);
&.m-sidebarNavigation--opened {
z-index: 999998;
@include m-theme() {
background-color: rgba(themed($m-grey-700), 0.2);
}
}
}
h5 {
font-size: 11px;
line-height: 30px;
letter-spacing: 1.83px;
text-transform: uppercase;
.m-sidebar--navigation {
grid-area: nav;
padding-top: 8px !important;
padding-left: 33.3%;
min-width: 0;
box-sizing: border-box;
@include m-theme() {
color: rgba(themed($m-black), 0.3);
background-color: themed($m-body-bg);
border-right: 1px solid themed($m-grey-400);
}
.m-sidebarNavigation__top {
display: none;
}
}
m-group--sidebar-markers {
.m-groupSidebarMarkers__list {
margin: 0;
&.m-sidebarNavigation--slide {
position: fixed;
top: 0;
bottom: 0;
left: -316px;
width: 316px;
max-width: 79vw;
z-index: 999999;
padding: 0;
width: auto !important;
height: auto !important;
max-width: unset;
min-height: unset;
box-shadow: unset !important;
display: block !important;
overflow: unset !important;
& > li {
width: auto;
height: auto;
border: unset !important;
a {
padding: 8px 0;
font-size: 14px;
font-weight: normal;
line-height: 30px;
white-space: nowrap;
text-overflow: ellipsis;
overflow-x: hidden;
@include m-theme() {
color: themed($m-black);
}
transition: transform 0.5s cubic-bezier(0.075, 0.82, 0.165, 1);
//&.m-sidebarNavigation--opened {
// transform: translateX(316px);
//}
.m-sidebarNavigation__top {
display: flex;
align-items: center;
justify-content: space-between;
m-tooltip {
vertical-align: middle;
margin-right: 21px;
box-sizing: border-box;
img {
width: 27px;
height: 27px;
}
height: 65px;
padding: 0 24px;
@include m-theme() {
border-bottom: 1px solid themed($m-borderColor--primary);
}
a > img {
height: 36px;
cursor: pointer;
}
i.material-icons {
font-size: 20px;
width: 27px;
height: 27px;
}
i.material-icons {
cursor: pointer;
@include m-theme() {
color: themed($m-textColor--secondary);
}
}
}
infinite-scroll {
padding: 2px 0 !important;
width: 48px !important;
ul {
padding: 37px 35px 0;
.m-sidebar--navigation--item {
margin: 4px 0;
}
.m-infinite-scroll-manual {
font-size: 10px;
padding: 2px;
background-color: transparent !important;
transform: rotate(0) !important;
&.m-groupSidebarMarkers__list {
padding: 0 35px 35px !important;
}
}
h5 {
padding: 0 35px;
}
}
}
}
.m-sidebar--navigation--item {
display: flex;
//flex-direction: column;
align-items: center;
text-decoration: none;
font-family: 'Roboto', Helvetica, sans-serif;
margin: 15px 0;
font-weight: bold;
font-size: 17px;
line-height: 44px;
@include m-theme() {
color: themed($m-navigation-item);
nav {
//min-height: 100vh;
//height: 100%;
}
ul {
list-style: none;
padding: 0;
}
}
&.m-sidebar--navigation--item-active {
.m-sidebar--navigation--item {
display: flex;
//flex-direction: column;
align-items: center;
text-decoration: none;
font-family: 'Roboto', Helvetica, sans-serif;
margin: 15px 0;
font-weight: bold;
font-size: 17px;
line-height: 44px;
@include m-theme() {
color: themed($m-black);
color: themed($m-textColor--secondary);
}
}
i.material-icons {
//font-size:17px;
//line-height:44px;
margin-right: 30px;
}
&.m-sidebar--navigation--item-active {
font-weight: bold;
@include m-theme() {
color: themed($m-black);
}
}
i.material-icons {
//font-size:17px;
//line-height:44px;
margin-right: 30px;
}
span {
//text-transform: uppercase;
//font-size: 8px;
//letter-spacing: 1.25px;
//padding-top: 4px;
span {
//text-transform: uppercase;
//font-size: 8px;
//letter-spacing: 1.25px;
//padding-top: 4px;
}
}
}
import {
Component,
ComponentFactoryResolver,
HostBinding,
HostListener,
Inject,
OnInit,
PLATFORM_ID,
ViewChild,
} from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
import { Navigation as NavigationService } from '../../../services/navigation';
import { Session } from '../../../services/session';
import { GroupsSidebarMarkersComponent } from '../../../modules/groups/sidebar-markers/sidebar-markers.component';
import { DynamicHostDirective } from '../../directives/dynamic-host.directive';
import { SidebarNavigationService } from './navigation.service';
import { ConfigsService } from '../../services/configs.service';
@Component({
selector: 'm-sidebar--navigation',
templateUrl: 'navigation.component.html',
})
export class SidebarNavigationComponent implements OnInit {
readonly cdnAssetsUrl: string;
@ViewChild(DynamicHostDirective, { static: true }) host: DynamicHostDirective;
user;
......@@ -22,15 +31,29 @@ export class SidebarNavigationComponent implements OnInit {
componentRef;
componentInstance: GroupsSidebarMarkersComponent;
layoutMode: 'phone' | 'tablet' | 'desktop' = 'desktop';
@HostBinding('class.m-sidebarNavigation--opened')
isOpened: boolean = false;
constructor(
public navigation: NavigationService,
public session: Session,
private _componentFactoryResolver: ComponentFactoryResolver
private service: SidebarNavigationService,
protected configs: ConfigsService,
private _componentFactoryResolver: ComponentFactoryResolver,
@Inject(PLATFORM_ID) private platformId: Object
) {
this.cdnAssetsUrl = this.configs.get('cdn_assets_url');
this.service.setContainer(this);
this.getUser();
}
ngOnInit() {
if (isPlatformBrowser(this.platformId)) {
this.onResize();
}
this.createGroupsSideBar();
}
......@@ -49,5 +72,27 @@ export class SidebarNavigationComponent implements OnInit {
this.componentRef = viewContainerRef.createComponent(componentFactory);
this.componentInstance = this.componentRef.instance;
this.componentInstance.showLabels = true;
this.componentInstance.leftSidebar = true;
}
toggle(): void {
if (this.layoutMode === 'phone') {
this.isOpened = !this.isOpened;
}
}
@HostListener('window:resize')
onResize() {
if (window.innerWidth > 900) {
this.layoutMode = 'desktop';
} else if (window.innerWidth > 540 && window.innerWidth <= 900) {
this.layoutMode = 'tablet';
} else {
this.layoutMode = 'phone';
}
if (this.layoutMode !== 'phone') {
this.isOpened = false;
}
}
}
import { SidebarNavigationComponent } from './navigation.component';
export class SidebarNavigationService {
container: SidebarNavigationComponent;
setContainer(container: SidebarNavigationComponent): void {
this.container = container;
}
toggle(): void {
if (this.container) {
this.container.toggle();
}
}
}
import { V2TopbarComponent } from './v2-topbar/v2-topbar.component';
import { V3TopbarComponent } from './v3-topbar/v3-topbar.component';
import { FeaturesService } from '../../services/features.service';
import { Injectable } from '@angular/core';
@Injectable()
export class TopbarService {
private container: V2TopbarComponent | V3TopbarComponent;
private useV3Topbar: boolean;
static _(featuresService: FeaturesService) {
return new TopbarService(featuresService);
}
constructor(private featuresService: FeaturesService) {
this.useV3Topbar = this.featuresService.has('navigation-2020');
}
setContainer(container: V2TopbarComponent | V3TopbarComponent) {
this.container = container;
return this;
}
toggleMarketingPages(
value: boolean,
showBottombar: boolean = true,
forceBackground: boolean = true
): void {
if (this.container) {
if (this.useV3Topbar) {
this.container.toggleMarketingPages(value, forceBackground);
} else {
this.container.toggleMarketingPages(
value,
showBottombar,
forceBackground
);
}
}
}
toggleVisibility(visible: boolean): void {
if (this.container) {
if (this.useV3Topbar) {
this.container.toggleVisibility(visible);
} else {
this.container.toggleVisibility(visible);
}
}
}
}
......@@ -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,9 +13,9 @@ 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 { V2TopbarService } from './v2-topbar.service';
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
import { Location } from '@angular/common';
import { ConfigsService } from '../../services/configs.service';
import { NavigationEnd, Router } from '@angular/router';
import { TopbarService } from '../topbar.service';
@Component({
selector: 'm-v2-topbar',
......@@ -23,7 +23,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 +48,12 @@ export class V2TopbarComponent implements OnInit, OnDestroy {
protected cd: ChangeDetectorRef,
private themeService: ThemeService,
protected componentFactoryResolver: ComponentFactoryResolver,
protected topbarService: V2TopbarService,
configs: ConfigsService,
protected topbarService: TopbarService,
protected router: Router
) {}
) {
this.cdnAssetsUrl = configs.get('cdn_assets_url');
}
ngOnInit() {
this.loadComponent();
......
import { V2TopbarComponent } from './v2-topbar.component';
export class V2TopbarService {
private container: V2TopbarComponent;
static _() {
return new V2TopbarService();
}
setContainer(container: V2TopbarComponent) {
this.container = container;
return this;
}
toggleMarketingPages(
value: boolean,
showBottombar: boolean = true,
forceBackground: boolean = true
) {
if (this.container) {
this.container.toggleMarketingPages(
value,
showBottombar,
forceBackground
);
}
}
toggleVisibility(visible: boolean) {
this.container.toggleVisibility(visible);
}
}
<ng-template #navLinks>
<a
class="m-v3-topbarNav__Item m-v3-topbarNav__RouterNav"
routerLink="/newsfeed/subscriptions"
routerLinkActive="m-v3-topbarNav__Item--active"
title="Newsfeed"
i18n-title
data-cy="data-minds-nav-newsfeed-button"
>
<i class="material-icons">home</i>
<span class="m-v3-topbarNavItem__Text" i18n>Newsfeed</span>
</a>
<a
class="m-v3-topbarNav__Item m-v3-topbarNav__RouterNav"
routerLink="/newsfeed/global"
routerLinkActive="m-v3-topbarNav__Item--active"
title="Discovery"
i18n-title
data-cy="data-minds-nav-discovery-button"
>
<i class="material-icons">search</i>
<span class="m-v3-topbarNavItem__Text" i18n>Discovery</span>
</a>
<a
class="m-v3-topbarNav__Item m-v3-topbarNav__RouterNav"
routerLink="/wallet"
routerLinkActive="m-v3-topbarNav__Item--active"
title="Wallet"
i18n-title
data-cy="data-minds-nav-wallet-button"
>
<i class="material-icons">account_balance</i>
<span class="m-v3-topbarNavItem__Text" i18n>Wallet</span>
</a>
<ng-template #searchBar>
<div class="m-v3Topbar__searchBox">
<ng-content select="[search]"></ng-content>
</div>
</ng-template>
<div class="m-v3-topbar__Top">
<div
class="m-v3Topbar__top"
[class.m-v3Topbar__marketingPages]="marketingPages"
[class.m-v3Topbar__noBackground]="!showBackground"
[style.visibility]="showTopbar ? 'visible' : 'hidden'"
>
<div class="m-grid">
<div class="m-v3topbar__leftColumn">
<nav class="m-v3-topbar__Nav">
<div class="m-v3Topbar__leftColumn">
<nav class="m-v3Topbar__nav">
<a
class="m-v3-topbarNav__Item m-v3-topbarNavItem__Logo"
class="m-v3TopbarNav__item m-v3TopbarNavItem__logo"
routerLink="/"
title="Home"
i18n-title
*ngIf="!isMobile; else hamburgerMenu"
>
<img
[src]="minds.cdn_assets_url + 'assets/logos/bulb.svg'"
[src]="cdnAssetsUrl + 'assets/logos/bulb.svg'"
(touchstart)="touchStart()"
(mouseenter)="mouseEnter()"
(mouseleave)="mouseLeave()"
/>
</a>
<ng-template #hamburgerMenu>
<div class="m-v3TopbarNav__item" (click)="toggleSidebarNav()">
<i class="material-icons">menu</i>
</div>
</ng-template>
</nav>
</div>
<div class="m-v3topbar__middleColumn">
<div class="m-v3Topbar__spacer" *ngIf="isMobile"></div>
<div class="m-v3Topbar__middleColumn" *ngIf="!isMobile">
<!-- <a-->
<!-- class="m-v3-topbar__Avatar"-->
<!-- class="m-v3Topbar__avatar"-->
<!-- *ngIf="getCurrentUser()"-->
<!-- [routerLink]="['/', getCurrentUser()?.username]"-->
<!-- routerLinkActive="m-v3-topbar__Avatar&#45;&#45;active"-->
<!-- routerLinkActive="m-v3Topbar__avatar&#45;&#45;active"-->
<!-- >-->
<!-- <minds-avatar-->
<!-- [object]="getCurrentUser()"-->
<!-- [editMode]="false"-->
<!-- ></minds-avatar>-->
<!-- </a>-->
<ng-container *ngIf="getCurrentUser()">
<div class="m-v3-topbar__SearchBox">
<ng-content select="[search]"></ng-content>
</div>
</ng-container>
<ng-container *ngTemplateOutlet="searchBar"></ng-container>
</div>
<div class="m-v3topbar__rightColumn">
<div class="m-v3Topbar__rightColumn">
<ng-container *ngIf="isMobile && getCurrentUser()">
<ng-container *ngTemplateOutlet="searchBar"></ng-container>
</ng-container>
<!-- edit -->
<!-- notifications -->
<!-- logged in user avatar -->
<div class="m-v3-topbar__UserMenu" *ngIf="getCurrentUser()">
<div class="m-v3Topbar__userMenu" *ngIf="getCurrentUser()">
<ng-content select="[icons]"></ng-content>
</div>
</div>
</div>
</div>
<div class="m-v3-topbar__Bottom">
<ng-container *ngTemplateOutlet="navLinks"></ng-container>
<div class="m-v3Topbar__bottom">
<a
class="m-v3TopbarNav__item m-v3TopbarNav__routerNav"
routerLink="/newsfeed/subscriptions"
routerLinkActive="m-v3TopbarNav__item--active"
title="Newsfeed"
i18n-title
data-cy="data-minds-nav-newsfeed-button"
>
<i class="material-icons">home</i>
<span class="m-v3TopbarNavItem__text" i18n>Newsfeed</span>
</a>
<a
class="m-v3TopbarNav__item m-v3TopbarNav__routerNav"
routerLink="/newsfeed/global"
routerLinkActive="m-v3TopbarNav__item--active"
title="Discovery"
i18n-title
data-cy="data-minds-nav-discovery-button"
>
<i class="material-icons">search</i>
<span class="m-v3TopbarNavItem__text" i18n>Discovery</span>
</a>
<a
class="m-v3TopbarNav__item m-v3TopbarNav__routerNav"
routerLink="/wallet"
routerLinkActive="m-v3TopbarNav__item--active"
title="Wallet"
i18n-title
data-cy="data-minds-nav-wallet-button"
>
<i class="material-icons">account_balance</i>
<span class="m-v3TopbarNavItem__text" i18n>Wallet</span>
</a>
</div>
<div class="m-v3-topbar__NotificationsToaster">
<div class="m-v3Topbar__notificationsToaster">
<ng-template dynamic-host></ng-template>
</div>
m-v3-topbar {
m-v3topbar {
.m-grid {
grid-gap: 0;
grid-template-columns: 3fr 5fr 4fr;
@media screen and (max-width: $m-grid-max-tablet) {
grid-template-columns: 71px 5fr 4fr;
}
@media screen and(max-width: $m-grid-max-mobile) {
display: flex;
}
&,
& > * {
height: 75px;
}
.m-v3topbar__leftColumn {
.m-v3Topbar__spacer {
flex-grow: 1;
opacity: 0;
}
.m-v3Topbar__leftColumn {
margin-left: 33.3%;
grid-column: 1 / span 1;
min-width: 0;
display: flex;
align-items: center;
@media screen and(max-width: $m-grid-max-mobile) {
margin-left: 23px;
}
}
.m-v3topbar__middleColumn {
.m-v3Topbar__middleColumn {
grid-column: 2 / span 1;
min-width: 0;
}
.m-v3topbar__rightColumn {
.m-v3Topbar__rightColumn {
grid-column: 3 / span 1;
display: flex;
align-items: center;
min-width: 0;
}
.m-v3topbar__leftColumn,
.m-v3topbar__middleColumn {
@include m-theme() {
border-right: 1px solid themed($m-grey-400);
@media screen and(min-width: $m-grid-max-mobile) {
.m-v3Topbar__leftColumn,
.m-v3Topbar__middleColumn {
@include m-theme() {
border-right: 1px solid themed($m-grey-400);
}
}
}
}
}
.m-v3-topbar__Top {
.m-v3Topbar__top {
position: fixed;
top: 0;
left: 0;
......@@ -63,7 +81,14 @@ m-v3-topbar {
width: 100%;
}
.m-v3-topbar__SearchBox {
&.m-v3Topbar__noBackground {
@include m-theme() {
background-color: transparent;
border-bottom: none;
}
}
.m-v3Topbar__searchBox {
width: 100%;
height: 100%;
display: flex;
......@@ -82,6 +107,15 @@ m-v3-topbar {
font-size: 20px;
}
@media screen and(max-width: $m-grid-max-mobile) {
width: 20px;
transition: width ease-in-out 0.2s;
&.is-focused {
width: 270px;
}
}
input {
border: 0;
font-size: 16px;
......@@ -130,51 +164,7 @@ m-v3-topbar {
}
}
.m-v3-topbar {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
width: 100%;
height: 75px;
max-width: 1296px;
margin: 0 auto;
padding: 0 8px;
//padding-right: 46px;
box-sizing: border-box;
@media screen and (min-width: 1296px) {
padding: 0 46px 0 0;
}
@media screen and (max-width: 480px) {
.m-v3-topbarNav__RouterNav {
display: none;
}
.m-v3-topbar__SearchBox {
display: none;
}
}
}
.m-v3-topbar__Container--left,
.m-v3-topbar__Container--right {
display: flex;
flex-direction: row;
align-items: center;
width: 100%;
}
.m-v3-topbar__Container--left {
justify-content: flex-start;
}
.m-v3-topbar__Container--right {
justify-content: flex-end;
}
.m-v3-topbar__Nav {
.m-v3Topbar__nav {
display: flex;
flex-direction: row;
align-items: center;
......@@ -185,7 +175,7 @@ m-v3-topbar {
}
}
.m-v3-topbarNav__Item {
.m-v3TopbarNav__item {
padding: 11px 12px 14px;
display: flex;
flex-direction: row;
......@@ -196,14 +186,14 @@ m-v3-topbar {
color: themed($m-grey-800);
}
&.m-v3-topbarNav__Item--active {
&.m-v3TopbarNav__item--active {
@include m-theme() {
color: themed($m-blue);
border-color: themed($m-blue);
}
}
&.m-v3-topbarNav__Item--no-padding {
&.m-v3TopbarNav__item--no-padding {
padding: 0 12px;
@media screen and (max-width: 480px) {
......@@ -233,9 +223,9 @@ m-v3-topbar {
}
}
.m-v3-topbarNavItem__Logo {
.m-v3TopbarNavItem__logo {
margin: 0 8px 0 0;
padding: 0 12px;
padding: 0;
//height: 62px;
box-sizing: border-box;
border: 0;
......@@ -247,16 +237,16 @@ m-v3-topbar {
}
}
// .m-theme__dark .m-v3-topbarNavItem__Logo,
// .m-theme__light .m-v3-topbarNavItem__Logo:hover {
// .m-theme__dark .m-v3TopbarNavItem__logo,
// .m-theme__light .m-v3TopbarNavItem__logo:hover {
// filter: grayscale(100%);
// }
//
// .m-theme__dark .m-v3-topbarNavItem__Logo:hover {
// .m-theme__dark .m-v3TopbarNavItem__logo:hover {
// filter: grayscale(0%);
// }
.m-v3-topbar__Avatar {
.m-v3Topbar__avatar {
cursor: pointer;
@media screen and (max-width: 810px) {
......@@ -275,18 +265,18 @@ m-v3-topbar {
}
}
&.m-v3-topbar__Avatar--active .minds-avatar {
&.m-v3Topbar__avatar--active .minds-avatar {
@include m-theme() {
box-shadow: 0 0 0 2px themed($m-blue);
}
}
}
.m-v3-topbar__UserMenu {
.m-v3Topbar__userMenu {
min-width: 80px;
}
.m-v3-topbar__Container__LoginWrapper {
.m-v3TopbarContainer__loginWrapper {
@media screen and (max-width: 480px) {
// display: none;
}
......@@ -310,7 +300,7 @@ m-v3-topbar {
}
}
.m-v3-topbar__Bottom {
.m-v3Topbar__bottom {
display: none;
position: fixed;
bottom: 0;
......@@ -337,13 +327,13 @@ m-v3-topbar {
display: flex;
}
.m-v3-topbarNav__RouterNav {
.m-v3TopbarNav__routerNav {
flex-grow: 1;
justify-content: center;
}
}
.m-v3-topbar__NotificationsToaster {
.m-v3Topbar__notificationsToaster {
.m-notifications--toaster {
z-index: 500;
right: 65px;
......
......@@ -2,21 +2,28 @@ import {
ChangeDetectorRef,
Component,
ComponentFactoryResolver,
HostListener,
Inject,
OnDestroy,
OnInit,
PLATFORM_ID,
ViewChild,
} from '@angular/core';
import { DynamicHostDirective } from '../../directives/dynamic-host.directive';
import { NotificationsToasterComponent } from '../../../modules/notifications/toaster.component';
import { Session } from '../../../services/session';
import { ThemeService } from '../../services/theme.service';
import { ConfigsService } from '../../services/configs.service';
import { isPlatformBrowser } from '@angular/common';
import { SidebarNavigationService } from '../sidebar/navigation.service';
import { TopbarService } from '../topbar.service';
@Component({
selector: 'm-v3-topbar',
selector: 'm-v3topbar',
templateUrl: 'v3-topbar.component.html',
})
export class V3TopbarComponent implements OnInit, OnDestroy {
minds = window.Minds;
readonly cdnAssetsUrl: string;
timeout;
isTouchScreen = false;
......@@ -26,16 +33,35 @@ export class V3TopbarComponent implements OnInit, OnDestroy {
componentRef;
componentInstance: NotificationsToasterComponent;
showTopbar: boolean = true;
forceBackground: boolean = true;
showBackground: boolean = true;
marketingPages: boolean = false;
isMobile: boolean = false;
constructor(
protected sidebarService: SidebarNavigationService,
protected themeService: ThemeService,
protected configs: ConfigsService,
protected session: Session,
protected cd: ChangeDetectorRef,
private themeService: ThemeService,
protected componentFactoryResolver: ComponentFactoryResolver
) {}
protected componentFactoryResolver: ComponentFactoryResolver,
protected topbarService: TopbarService,
@Inject(PLATFORM_ID) private platformId: Object
) {
this.cdnAssetsUrl = this.configs.get('cdn_assets_url');
if (isPlatformBrowser(this.platformId)) {
this.onResize();
}
}
ngOnInit() {
this.loadComponent();
this.session.isLoggedIn(() => this.detectChanges());
this.topbarService.setContainer(this);
}
getCurrentUser() {
......@@ -77,6 +103,41 @@ export class V3TopbarComponent implements OnInit, OnDestroy {
clearTimeout(this.timeout);
}
toggleSidebarNav() {
this.sidebarService.toggle();
}
/**
* Marketing pages set this to true in order to change how the topbar looks
* @param value
* @param forceBackground
*/
toggleMarketingPages(value: boolean, forceBackground: boolean = true) {
this.marketingPages = value;
this.forceBackground = forceBackground;
this.onScroll();
this.detectChanges();
}
@HostListener('window:scroll')
onScroll() {
this.showBackground = this.forceBackground
? true
: this.marketingPages
? window.document.body.scrollTop > 52
: true;
}
toggleVisibility(visible: boolean) {
this.showTopbar = visible;
this.detectChanges();
}
@HostListener('window:resize')
onResize() {
this.isMobile = window.innerWidth <= 540;
}
ngOnDestroy() {
if (this.timeout) {
clearTimeout(this.timeout);
......
......@@ -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';
......@@ -173,7 +173,7 @@ export class HorizontalFeedService {
return {
index: index,
entity: this.entities.setCastToActivities(true).single(entity.urn),
entity: this.entities.single(entity.urn),
};
}
......@@ -289,14 +289,15 @@ export class HorizontalFeedService {
*/
protected async _fetchContainer() {
const baseEntity = this.baseEntity;
const baseEntityTimestamp = baseEntity.time_created * 1000;
const guid = baseEntity.container_guid || baseEntity.owner_guid;
const endpoint = `api/v2/feeds/container/${guid}/all`;
const params = {
sync: 1,
as_activities: 1,
force_public: 1,
limit: this.limit,
from_timestamp: baseEntityTimestamp,
};
// TODO: Make this less convoluted
......@@ -305,22 +306,28 @@ export class HorizontalFeedService {
? this._fetchFromServer(endpoint, {
...params,
reverse_sort: 1,
from_timestamp: baseEntity.time_created * 1000 + 1,
})
: Promise.resolve(null),
this.pools.next.moreData
? this._fetchFromServer(endpoint, {
...params,
from_timestamp: baseEntity.time_created * 1000 - 1,
})
? this._fetchFromServer(endpoint, params)
: Promise.resolve(null),
]);
const baseGuids = [
this.baseEntity.guid,
this.baseEntity.entity_guid,
this.baseEntity.remind_object && this.baseEntity.remind_object.guid,
this.baseEntity.remind_object &&
this.baseEntity.remind_object.entity_guid,
].filter(Boolean);
let changed = false;
if (prev !== null) {
this.pools.prev = {
entities: prev,
entities: prev.filter(
entity => entity.guid && !baseGuids.includes(entity.guid)
),
moreData: false,
};
......@@ -329,7 +336,9 @@ export class HorizontalFeedService {
if (next !== null) {
this.pools.next = {
entities: next,
entities: next.filter(
entity => entity.guid && !baseGuids.includes(entity.guid)
),
moreData: false,
};
......
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,
}
......
import { Injectable, Renderer2, RendererFactory2 } from '@angular/core';
import {
Inject,
Injectable,
PLATFORM_ID,
Renderer2,
RendererFactory2,
} from '@angular/core';
import { Client } from '../../services/api/client';
import { Session } from '../../services/session';
import { Storage } from '../../services/storage';
import { BehaviorSubject, Subscription } from 'rxjs';
import { isPlatformBrowser } from '@angular/common';
@Injectable()
export class ThemeService {
......@@ -16,7 +23,8 @@ export class ThemeService {
private rendererFactory: RendererFactory2,
private client: Client,
private session: Session,
private storage: Storage
private storage: Storage,
private platformId: Object
) {
this.renderer = rendererFactory.createRenderer(null, null);
}
......@@ -25,9 +33,16 @@ export class ThemeService {
rendererFactory: RendererFactory2,
client: Client,
session: Session,
storage: Storage
storage: Storage,
platformId: Object
) {
return new ThemeService(rendererFactory, client, session, storage);
return new ThemeService(
rendererFactory,
client,
session,
storage,
platformId
);
}
// TODO after release of MacOS 10.14.4
......@@ -36,7 +51,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)
......@@ -96,9 +111,11 @@ export class ThemeService {
}
clearTransitions() {
clearTimeout(this.timer);
this.timer = setTimeout(() => {
this.renderer.removeClass(document.body, 'm-theme-in-transition');
}, 1000);
if (isPlatformBrowser(this.platformId)) {
clearTimeout(this.timer);
this.timer = setTimeout(() => {
this.renderer.removeClass(document.body, 'm-theme-in-transition');
}, 1000);
}
}
}
......@@ -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
......
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.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.