...
 
Commits (80)
......@@ -202,6 +202,8 @@ prepare:production:
- docker login -u gitlab-ci-token -p ${CI_BUILD_TOKEN} ${CI_REGISTRY}
- docker build -t $CI_REGISTRY_IMAGE/server:$CI_PIPELINE_ID -f containers/server/Dockerfile dist/.
- docker push $CI_REGISTRY_IMAGE/server:$CI_PIPELINE_ID
- docker tag $CI_REGISTRY_IMAGE/server:$CI_PIPELINE_ID $CI_REGISTRY_IMAGE/server:latest
- docker push $CI_REGISTRY_IMAGE/server:latest
only:
refs:
- master
......@@ -292,7 +294,7 @@ review:stop:
- docker:dind
script:
## Sync assets with CDN
- aws s3 sync dist $S3_REPOSITORY_URL
- aws s3 sync dist $S3_REPOSITORY_URL --cache-control max-age=31536000
- $(aws ecr get-login --no-include-email --region us-east-1)
## Update docker server container
- docker login -u gitlab-ci-token -p ${CI_BUILD_TOKEN} ${CI_REGISTRY}
......
FROM node:13-alpine
COPY monitor.sh /monitor.sh
COPY build.sh /build.sh
ENTRYPOINT ["sh", "/monitor.sh"]
#!/usr/bin/env sh
set -e
cd /var/www/Minds/front
echo Building...
npx ng run minds:server:production
echo Compiling...
npm run compile:server
#!/usr/bin/env sh
set -e
cd /var/www/Minds/front
npx nodemon --delay 3 --watch server.ts --watch dist/en/ --ext js,css,jpg,png,svg,mp4,webp,webm --exec "/usr/bin/env sh" /build.sh
FROM node:13-alpine
COPY monitor.sh /monitor.sh
COPY serve.sh /serve.sh
ENTRYPOINT ["sh", "/monitor.sh"]
#!/usr/bin/env sh
set -e
cd /var/www/Minds/front
npx nodemon --delay 3 --watch dist/server.js --watch dist/server --ext js,mjs --exec "/usr/bin/env sh" /serve.sh
#!/usr/bin/env sh
set -e
cd /var/www/Minds/front
echo Running...
npm run serve:ssr
......@@ -5,6 +5,10 @@ FROM node:13-alpine
COPY . /dist
CMD node /dist/server
RUN npm install pm2 -g
CMD pm2-runtime /dist/server \
--max-memory-restart 512M \
--instances 2
VOLUME ["/dist"]
\ No newline at end of file
import generateRandomId from "../../support/utilities";
context('Onboarding', () => {
const remindText = 'remind test text';
const username = generateRandomId();
const password = `${generateRandomId()}0oA!`;
const email = 'test@minds.com';
const usernameField = 'minds-form-register #username';
const emailField = 'minds-form-register #email';
const passwordField = 'minds-form-register #password';
const password2Field = 'minds-form-register #password2';
const checkbox = '[data-cy=data-minds-accept-tos-input]';
const submitButton = 'minds-form-register .mdl-card__actions button';
before(() => {
cy.getCookie('minds_sess')
.then((sessionCookie) => {
if (sessionCookie === null) {
return cy.login(true);
cy.visit('/register');
cy.location('pathname').should('eq', '/register');
cy.server();
cy.route("POST", "**/api/v1/register").as("register");
cy.get(usernameField)
.focus()
.type(username);
cy.get(emailField)
.focus()
.type(email);
cy.get(passwordField)
.focus()
.type(password);
cy.wait(500);
cy.get(password2Field)
.focus()
.type(password);
cy.get(checkbox)
.click({ force: true });
//submit
cy.get(submitButton)
.click()
.wait('@register')
.then((xhr) => {
expect(xhr.status).to.equal(200);
}
});
cy.visit(`/onboarding`);
);
cy.wait(500);
cy.location('pathname').should('eq', '/onboarding/notice');
});
// create two test groups
after(() => {
cy.deleteUser(username, password);
cy.clearCookies();
});
beforeEach(() => {
......@@ -21,7 +65,7 @@ context('Onboarding', () => {
it('should go through the process of onboarding', () => {
// notice should appear
cy.get('h1.m-onboarding__noticeTitle').contains('Welcome to the Minds Community');
cy.get('h2.m-onboarding__noticeTitle').contains(`@${Cypress.env().username}`);
cy.get('h2.m-onboarding__noticeTitle').contains(username);
// should redirect to /hashtags
cy.get('.m-onboarding__form button.mf-button').contains("Let's Get Setup").click();
......@@ -73,8 +117,8 @@ context('Onboarding', () => {
// should have a Location input
cy.get('.m-onboarding__controls > .m-onboarding__control label[data-minds=location]').contains('Location');
cy.get('.m-onboarding__controls > .m-onboarding__control input[data-minds=locationInput]').type('London');
cy.get('ul.m-onboarding__cities > li:first-child').click();
// cy.get('.m-onboarding__controls > .m-onboarding__control input[data-minds=locationInput]').type('London');
// cy.get('ul.m-onboarding__cities > li:first-child').click();
// should have Date of Birth inputs
......@@ -91,7 +135,11 @@ context('Onboarding', () => {
// should have a continue and a skip button
cy.get('button.mf-button--hollow').contains('Skip');
cy.get('button.mf-button--alt').contains('Continue').click();
cy.get('button.mf-button--alt').contains('Finish').click();
// TODO: disable the following line and uncomment the rest when we re-enable the screens
// should be in the newsfeed
cy.location('pathname').should('eq', '/newsfeed/subscriptions');
// should be in the Groups step
......@@ -105,8 +153,8 @@ context('Onboarding', () => {
// cy.get('.m-groupList__list .m-groupList__item:first-child .m-join__subscribe i').contains('add');
// should have a continue and a skip button
cy.get('button.mf-button--hollow').contains('Skip');
cy.get('button.mf-button--alt').contains('Continue').click();
// cy.get('button.mf-button--hollow').contains('Skip');
// cy.get('button.mf-button--alt').contains('Continue').click();
// should be in the Channels step
......@@ -120,10 +168,10 @@ context('Onboarding', () => {
// cy.get('.m-channelList__list .m-channelList__item:first-child .m-join__subscribe i').contains('add');
// should have a continue and a skip button
cy.get('button.mf-button--hollow').contains('Skip');
cy.get('button.mf-button--alt').contains('Finish').click();
// cy.get('button.mf-button--hollow').contains('Skip');
// cy.get('button.mf-button--alt').contains('Finish').click();
// should be in the newsfeed
cy.location('pathname').should('eq', '/newsfeed/subscriptions');
// cy.location('pathname').should('eq', '/newsfeed/subscriptions');
});
});
......@@ -60,7 +60,7 @@ context('Registration', () => {
});
cy.wait(500);
cy.location('pathname').should('eq', '/newsfeed/subscriptions');
cy.location('pathname').should('eq', '/onboarding/notice');
});
it('should display an error if passwords do not match', () => {
......
......@@ -7,7 +7,6 @@ import * as template from 'gulp-template';
import { join } from 'path';
import { argv } from 'yargs';
const AUTOPREFIXER_BROWSERS = [
'ie >= 11',
'ie_mob >= 11',
......@@ -17,32 +16,44 @@ const AUTOPREFIXER_BROWSERS = [
'opera >= 23',
'ios >= 7',
'android >= 4.4',
'bb >= 10'
'bb >= 10',
];
// --------------
// Build SASS
gulp.task('build.sass', done => {
const app_cdn = argv.deployUrl ? argv.deployUrl: '';
gulp.src(join('./src', '**', '*.scss'))
.pipe(cssGlobbing({ extensions: ['.scss'] }))
.pipe(sass({
includePaths: [join('./src', 'stylesheets')],
style: 'compressed'
}).on('error', sass.logError))
const app_cdn = argv.deployUrl ? argv.deployUrl : '';
gulp
.src(join(__dirname, 'src', '**', '*.scss'))
.pipe(cssGlobbing({ extensions: ['.scss'] }))
.pipe(
sass({
includePaths: [join(__dirname, 'src', 'stylesheets')],
style: 'compressed',
}).on('error', sass.logError)
)
.pipe(autoprefixer(AUTOPREFIXER_BROWSERS))
.pipe(template({
'APP_CDN': app_cdn,
}))
.pipe(gulp.dest('./.styles'))
.pipe(
template({
APP_CDN: app_cdn,
})
)
.pipe(gulp.dest(join(__dirname, '.styles')))
.on('end', () => {
gulp.src('./.styles/stylesheets/main.css')
.pipe(gulp.dest('./src'))
gulp
.src(join(__dirname, '.styles', 'stylesheets', 'main.css'))
.pipe(gulp.dest(join(__dirname, 'src')))
.on('end', done);
});
});
// --------------
// i18n
gulp.task('extract.i18n', require(join(__dirname, 'tasks', 'extract.i18n.xlf'))(gulp));
gulp.task('import.i18n', require(join(__dirname, 'tasks', 'import.i18n.xlf'))(gulp));
gulp.task(
'extract.i18n',
require(join(__dirname, 'tasks', 'extract.i18n.xlf'))(gulp)
);
gulp.task(
'import.i18n',
require(join(__dirname, 'tasks', 'import.i18n.xlf'))(gulp)
);
This diff is collapsed.
......@@ -8,11 +8,12 @@
"preinstall": "git config core.hooksPath .git/hooks/",
"prebuild": "gulp build.sass",
"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",
"build-dev": "echo 'Deprecated, please use build:dev'; npm run build:dev --",
"serve-dev": "echo 'Deprecated, please use serve:ssr'; npm run serve:ssr --",
"prebuild:dev": "gulp build.sass --deploy-url=http://localhost:8080/",
"build:dev": "ng build --output-path=dist/en --deploy-url=http://localhost:8080/ --watch=true --poll=800 --aot",
"preserve:dev": "gulp build.sass --deploy-url=http://localhost:4200/",
"serve:dev": "ng serve --deploy-url=http://localhost:4200/ --watch=true --poll=800 --aot --progress --proxy-config proxy.conf.js --host=0.0.0.0 --disableHostCheck=true",
"test": "ng test",
"lint": "ng lint",
"e2e": "cypress run --debug",
......@@ -22,6 +23,7 @@
"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",
"build:ssr:dev": "ng run minds:server:production && npm run compile:server",
"bundle-report": "webpack-bundle-analyzer dist/en/stats.json"
},
"private": true,
......@@ -110,7 +112,8 @@
"tslint": "~5.12.0",
"typescript": "~3.4.5",
"webpack-bundle-analyzer": "^3.6.0",
"webpack-cli": "^3.3.10"
"webpack-cli": "^3.3.10",
"nodemon": "^2.0.2"
},
"husky": {
"hooks": {
......
const engineSecure = Boolean(parseInt(process.env['ENGINE_SECURE']) || 0);
const engineHost = process.env['ENGINE_HOST'] || 'localhost';
const enginePort = process.env['ENGINE_PORT'] || (engineSecure ? 443 : 80);
const PROXY_CONFIG = [
{
context: [
'/api',
'/fs',
'/icon',
'/carousel',
],
target: {
protocol: engineSecure ? 'https:' : 'http:',
host: engineHost,
port: enginePort,
},
secure: false,
changeOrigin: true,
withCredentials: true,
logLevel: process.env['PROXY_LOG_LEVEL'] || 'info',
}
];
module.exports = PROXY_CONFIG;
......@@ -3,6 +3,7 @@ import 'reflect-metadata';
import { join } from 'path';
import { readFileSync } from 'fs';
import * as _url from 'url';
import { ngExpressEngine } from '@nguniversal/express-engine';
import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens';
......@@ -102,7 +103,10 @@ app.get('/undefined', (req, res) => {
// cache
const NodeCache = require('node-cache');
const myCache = new NodeCache({ stdTTL: 5 * 60, checkperiod: 120 });
const myCache = new NodeCache({
stdTTL: 2 * 60, // 2 minute cache
checkperiod: 60, // Check every minute
});
const cache = () => {
return (req, res, next) => {
......@@ -111,18 +115,18 @@ const cache = () => {
.filter(kv => kv[0] !== 'mwa' && kv[0] !== 'XSRF-TOKEN')
.join(':') || 'loggedout';
const key =
`__express__/${sessKey}/` +
`__express__/${req.headers.host}/${sessKey}/` +
(req.originalUrl || req.url) +
(isMobileOrTablet() ? '/mobile' : '/desktop');
const exists = myCache.has(key);
if (exists) {
console.log(`from cache: ${key}`);
const cachedBody = myCache.get(key);
res.send(cachedBody);
return;
} else {
res.sendResponse = res.send;
res.send = body => {
if (res.finished) return;
myCache.set(key, body);
res.sendResponse(body);
};
......@@ -131,6 +135,10 @@ const cache = () => {
};
};
app.get('/node-cache-stats', (req, res) => {
res.send(myCache.getStats());
});
// All regular routes use the Universal engine
app.get('*', cache(), (req, res) => {
const http =
......@@ -171,6 +179,14 @@ app.get('*', cache(), (req, res) => {
provide: 'ORIGIN_URL',
useValue: `${http}://${req.headers.host}`,
},
// for initial query params before router loads
{
provide: 'QUERY_STRING',
useFactory: () => {
return _url.parse(req.url, true).search || '';
},
deps: [],
},
],
},
(err, html) => {
......@@ -189,3 +205,5 @@ app.get('*', cache(), (req, res) => {
app.listen(PORT, () => {
console.log(`Node server listening on http://localhost:${PORT}`);
});
app.keepAliveTimeout = 65000;
......@@ -6,12 +6,23 @@ 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';
import {
RedirectService,
BrowserRedirectService,
} from './common/services/redirect.service';
PlotlyModule.plotlyjs = PlotlyJS;
@NgModule({
imports: [MindsModule, PlotlyModule, CookieModule],
bootstrap: [Minds],
providers: [{ provide: 'ORIGIN_URL', useValue: location.origin }],
providers: [
{ provide: 'ORIGIN_URL', useValue: location.origin },
{ provide: 'QUERY_STRING', useValue: location.search || '' },
{
provide: RedirectService,
useClass: BrowserRedirectService,
},
],
})
export class AppBrowserModule {}
<ng-container *ngIf="ready">
<ng-container *ngIf="!isProDomain">
<ng-container *ngIf="useNewNavigation; else v2Topbar">
<m-v3-topbar>
<ng-container search>
<m-search--bar [defaultSizes]="false"></m-search--bar>
</ng-container>
</m-v3-topbar>
</ng-container>
<ng-template #v2Topbar>
<m-v2-topbar *mIfFeature="'top-feeds'; else legacyTopbar">
<ng-container search>
<m-search--bar [defaultSizes]="false"></m-search--bar>
</ng-container>
<ng-container icons>
<m-notifications--topbar-toggle
*ngIf="session.isLoggedIn()"
></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')"
></m-sidebar--markers>
</ng-template>
<m-topbarwrapper></m-topbarwrapper>
</ng-container>
<m-body
[class.has-markers-sidebar]="
session.isLoggedIn() &&
!isProDomain &&
!featuresService.has('navigation-2020')
"
[class.has-v2-navbar]="featuresService.has('top-feeds')"
[class.has-v3-navbar]="featuresService.has('navigation-2020')"
[class.is-pro-domain]="isProDomain"
>
<m-emailConfirmation *ngIf="!isProDomain"></m-emailConfirmation>
<m-announcement [id]="'festival:sale'" *mIfFeature="'radiocity'">
<span
class="m-blockchain--wallet-address-notice--action"
i18n="@@MINDS_FESTIVAL_TICKET_SALE"
>
BREAKING: Tickets on sale for "MINDS: FESTIVAL OF IDEAS" @ Radio City on
6/13/2020. HELP US SELL OUT FAST!
</span>
</m-announcement>
<m-blockchain--wallet-address-notice></m-blockchain--wallet-address-notice>
<ng-container *ngIf="useNewNavigation; else oldLayout">
<div class="m-grid">
<m-sidebar--navigation></m-sidebar--navigation>
<router-outlet></router-outlet>
</div>
</ng-container>
<ng-template #oldLayout>
<router-outlet></router-outlet>
</ng-template>
</m-body>
<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>
<ng-container *mIfBrowser>
<m-juryDutySession__summons
*ngIf="session.isLoggedIn() && !isProDomain"
></m-juryDutySession__summons>
</ng-container>
<m-modal-signup
*ngIf="!isProDomain && !session.getLoggedInUser()"
[open]="false"
></m-modal-signup>
<m-channel--onboarding *ngIf="showOnboarding"></m-channel--onboarding>
<m-cookies-notice *ngIf="!session.isLoggedIn()"></m-cookies-notice>
<m-page></m-page>
</ng-container>
......@@ -35,7 +35,7 @@ m-app {
grid-template-areas: 'nav content side' 'footer footer footer';
grid-template-columns: 3fr 5fr 4fr;
grid-template-rows: auto 1fr auto;
grid-template-rows: 1fr;
grid-gap: 0;
height: 100%;
......@@ -48,6 +48,12 @@ m-app {
display: flex;
flex-direction: column;
}
&.m-grid__noSidebar {
grid-template-areas: 'content side' 'footer footer';
grid-template-columns: 5fr 4fr;
}
}
}
......
......@@ -44,12 +44,8 @@ export class Minds implements OnInit, OnDestroy {
ready: boolean = false;
showOnboarding: boolean = false;
showTOSModal: boolean = false;
useNewNavigation: boolean = false;
protected router$: Subscription;
protected routerConfig: Route[];
......@@ -66,7 +62,6 @@ export class Minds implements OnInit, OnDestroy {
public web3Wallet: Web3WalletService,
public client: Client,
public webtorrent: WebtorrentService,
public onboardingService: ChannelOnboardingService,
public router: Router,
public blockListService: BlockListService,
public featuresService: FeaturesService,
......@@ -90,7 +85,6 @@ 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(
......@@ -142,10 +136,6 @@ export class Minds implements OnInit, OnDestroy {
this.session.isLoggedIn(async is => {
if (is && !this.site.isProDomain) {
if (!this.site.isProDomain) {
this.showOnboarding = await this.onboardingService.showModal();
}
const user = this.session.getLoggedInUser();
const language = this.configs.get('language');
......@@ -156,14 +146,6 @@ export class Minds implements OnInit, OnDestroy {
}
});
this.onboardingService.onClose.subscribe(() => {
this.showOnboarding = false;
});
this.onboardingService.onOpen.subscribe(async () => {
this.showOnboarding = await this.onboardingService.showModal(true);
});
this.loginReferrer
.avoid([
'/login',
......
......@@ -81,6 +81,8 @@ import { CookieModule } from '@gorniv/ngx-universal';
import { HomepageModule } from './modules/homepage/homepage.module';
import { OnboardingV2Module } from './modules/onboarding-v2/onboarding.module';
import { ConfigsService } from './common/services/configs.service';
import { TopbarWrapperComponent } from './page/topbar.component';
import { PageComponent } from './page/page.component';
@Injectable()
export class SentryErrorHandler implements ErrorHandler {
......@@ -96,6 +98,8 @@ export class SentryErrorHandler implements ErrorHandler {
bootstrap: [Minds],
declarations: [
Minds,
TopbarWrapperComponent,
PageComponent,
MINDS_APP_ROUTING_DECLARATIONS,
MINDS_DECLARATIONS,
MINDS_PLUGIN_DECLARATIONS,
......
......@@ -9,6 +9,10 @@ import { MindsModule } from './app.module';
import { Minds } from './app.component';
import { PlotlyModule } from 'angular-plotly.js';
import { CookieService, CookieBackendService } from '@gorniv/ngx-universal';
import {
ServerRedirectService,
RedirectService,
} from './common/services/redirect.service';
PlotlyModule.plotlyjs = {
plot: () => {
......@@ -38,6 +42,10 @@ export class ServerXhr implements XhrFactory {
provide: CookieService,
useClass: CookieBackendService,
},
{
provide: RedirectService,
useClass: ServerRedirectService,
},
],
bootstrap: [Minds],
})
......
import { NgModule, inject } from '@angular/core';
import { NgModule, inject, Injector } from '@angular/core';
import {
CommonModule as NgCommonModule,
isPlatformServer,
......@@ -47,7 +47,6 @@ import { ScrollLock } from './directives/scroll-lock';
import { TagsLinks } from './directives/tags';
import { Tooltip } from './directives/tooltip';
import { MindsAvatar } from './components/avatar/avatar';
import { CaptchaComponent } from './components/captcha/captcha.component';
import { Textarea } from './components/editors/textarea.component';
import { TagcloudComponent } from './components/tagcloud/tagcloud.component';
import { DropdownComponent } from './components/dropdown/dropdown.component';
......@@ -133,7 +132,6 @@ 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';
......@@ -145,8 +143,10 @@ 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 { RedirectService } from './services/redirect.service';
import { V3TopbarComponent } from './layout/v3-topbar/v3-topbar.component';
import { SidebarNavigationService } from './layout/sidebar/navigation.service';
import { TopbarService } from './layout/topbar.service';
import { UserMenuV3Component } from './layout/v3-topbar/user-menu/user-menu.component';
const routes: Routes = [
......@@ -214,7 +214,6 @@ const routes: Routes = [
MDL_DIRECTIVES,
DateSelectorComponent,
MindsAvatar,
CaptchaComponent,
Textarea,
InlineEditorComponent,
......@@ -325,7 +324,6 @@ const routes: Routes = [
MDL_DIRECTIVES,
DateSelectorComponent,
MindsAvatar,
CaptchaComponent,
Textarea,
InlineEditorComponent,
......@@ -445,37 +443,19 @@ const routes: Routes = [
},
{
provide: ConfigsService,
useFactory: client => new ConfigsService(client),
deps: [Client],
},
{
provide: MetaService,
useFactory: (
titleService,
metaService,
siteService,
location,
configsService
) =>
new MetaService(
titleService,
metaService,
siteService,
location,
configsService
useFactory: (client, injector, redirect, location) =>
new ConfigsService(
client,
injector.get('QUERY_STRING'),
redirect,
location
),
deps: [Title, Meta, SiteService, Location, ConfigsService],
deps: [Client, Injector, RedirectService, Location],
},
MetaService,
MediaProxyService,
SidebarNavigationService,
{
provide: V2TopbarService,
useFactory: V2TopbarService._,
},
{
provide: SidebarNavigationService,
useFactory: SidebarNavigationService._,
},
TopbarService,
{
provide: SidebarMarkersService,
useFactory: SidebarMarkersService._,
......
<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 {}
......@@ -39,6 +39,7 @@ describe('ChannelModeSelector', () => {
subscribers_count: 182,
impressions: 18200,
mode: ChannelMode.PUBLIC,
nsfw: [],
};
clientMock.response['api/v1/channel/info'] = { status: 'success' };
......
......@@ -2,27 +2,32 @@ m-date__dropdowns {
display: flex;
justify-content: space-between;
select {
display: inline-block;
background-color: #fff;
box-sizing: border-box;
margin: 0 10px 0 0;
padding: 8px 10px;
height: 36px;
.m-dateDropdowns__selectWrapper {
position: relative;
overflow: hidden;
min-width: 80px;
max-width: 90px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
font-family: 'Roboto', 'Helvetica', 'Arial', sans-serif;
font-size: 16px;
line-height: 21px;
border-radius: 2px;
border-radius: 3px;
margin: 0 10px 0 0;
@include m-theme() {
color: themed($m-grey-800);
background-color: themed($m-white);
border: 1px solid #e2e2e2;
box-shadow: 0px 1px 4px 0px rgba(0, 0, 0, 0.1);
}
&::after {
content: '\25bc';
font-size: 10px;
padding: 10px;
position: absolute;
right: 0;
top: 0;
text-align: center;
pointer-events: none;
@include m-theme() {
color: themed($m-grey-200);
}
}
// month
......@@ -40,4 +45,28 @@ m-date__dropdowns {
min-width: 77px;
}
}
select {
display: inline-block;
background-color: #fff;
box-sizing: border-box;
padding: 8px 10px;
height: 36px;
width: 100%;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
font-family: 'Roboto', 'Helvetica', 'Arial', sans-serif;
font-size: 16px;
line-height: 21px;
border-radius: 2px;
appearance: none;
@include m-theme() {
color: themed($m-grey-800);
background-color: themed($m-white);
border: 1px solid #e2e2e2;
}
}
}
......@@ -3,27 +3,33 @@ import { Component, EventEmitter, OnInit, Output } from '@angular/core';
@Component({
selector: 'm-date__dropdowns',
template: `
<select
data-minds="monthDropdown"
[ngModel]="selectedMonth"
(ngModelChange)="selectMonth($event)"
>
<option *ngFor="let month of monthNames">{{ month }}</option>
</select>
<select
data-minds="dayDropdown"
[ngModel]="selectedDay"
(ngModelChange)="selectDay($event)"
>
<option *ngFor="let day of days">{{ day }}</option>
</select>
<select
data-minds="yearDropdown"
[ngModel]="selectedYear"
(ngModelChange)="selectYear($event)"
>
<option *ngFor="let year of years">{{ year }}</option>
</select>
<div class="m-dateDropdowns__selectWrapper">
<select
data-minds="monthDropdown"
[ngModel]="selectedMonth"
(ngModelChange)="selectMonth($event)"
>
<option *ngFor="let month of monthNames">{{ month }}</option>
</select>
</div>
<div class="m-dateDropdowns__selectWrapper">
<select
data-minds="dayDropdown"
[ngModel]="selectedDay"
(ngModelChange)="selectDay($event)"
>
<option *ngFor="let day of days">{{ day }}</option>
</select>
</div>
<div class="m-dateDropdowns__selectWrapper">
<select
data-minds="yearDropdown"
[ngModel]="selectedYear"
(ngModelChange)="selectYear($event)"
>
<option *ngFor="let year of years">{{ year }}</option>
</select>
</div>
`,
})
export class DateDropdownsComponent implements OnInit {
......@@ -56,30 +62,42 @@ export class DateDropdownsComponent implements OnInit {
ngOnInit() {
this.years = this.range(100, this.selectedYear, false);
this.selectedYear = this.years[0];
this.selectMonth('January');
this.selectMonth('January', false);
}
selectMonth(month: string) {
selectMonth(month: string, emit: boolean = true) {
this.selectedMonth = month;
this.populateDays(
this.getDaysInMonth(this.getMonthNumber(month), this.selectedYear)
);
this.selectedDateChange.emit(this.buildDate());
if (emit) {
this.emitChanges();
}
}
selectDay(day: string) {
selectDay(day: string, emit: boolean = true) {
this.selectedDay = day;
this.selectedDateChange.emit(this.buildDate());
if (emit) {
this.emitChanges();
}
}
selectYear(year) {
selectYear(year, emit: boolean = true) {
this.selectedYear = year;
this.populateDays(
this.getDaysInMonth(this.getMonthNumber(this.selectedMonth), year)
);
if (emit) {
this.emitChanges();
}
}
emitChanges() {
this.selectedDateChange.emit(this.buildDate());
}
......
......@@ -54,8 +54,8 @@
</li>
<li>
<a href="https://irl.minds.com/" target="_blank" i18n>
Minds IRL
<a href="https://change.minds.com/" target="_blank" i18n>
Events
</a>
</li>
</ul>
......
......@@ -6,7 +6,7 @@ import {
OnInit,
} from '@angular/core';
import { MetaService } from '../../services/meta.service';
import { V2TopbarService } from '../../layout/v2-topbar/v2-topbar.service';
import { TopbarService } from '../../layout/topbar.service';
@Component({
selector: 'm-marketing',
......@@ -20,7 +20,7 @@ export class MarketingComponent implements OnInit, OnDestroy {
constructor(
protected metaService: MetaService,
private topbarService: V2TopbarService
private navigationService: TopbarService
) {}
ngOnInit() {
......@@ -28,7 +28,7 @@ export class MarketingComponent implements OnInit, OnDestroy {
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,7 +9,7 @@
[ngClass]="{ 'm-inline-embed': hasInlineContentLoaded() }"
(click)="action($event)"
target="_blank"
rel="noopener noreferrer"
rel="noopener nofollow ugc"
*ngIf="src.thumbnail_src || inlineEmbed"
>
<div
......
<div
class="m-sidebarNavigation__overlay"
[class.m-sidebarNavigation--opened]="isOpened"
(click)="toggle()"
></div>
<div class="m-sidebarNavigation__overlay" (click)="toggle()"></div>
<div
class="m-sidebar--navigation"
[class.m-sidebarNavigation--slide]="isMobile"
[class.m-sidebarNavigation--opened]="isOpened"
[class.m-sidebarNavigation--slide]="layoutMode === 'phone'"
>
<nav>
<div class="m-sidebarNavigation__top">
<a routerLink="/" title="Home" i18n-title *ngIf="isMobile">
<a routerLink="/" title="Home" i18n-title>
<img [src]="cdnAssetsUrl + 'assets/logos/bulb.svg'" />
</a>
......@@ -28,14 +23,14 @@
<m-tooltip
icon="check"
i18n="@@COMMON__NEWSFEED"
[enabled]="!isDesktop && !isMobile"
[enabled]="layoutMode === 'tablet'"
>
Newsfeed
</m-tooltip>
<span
class="m-sidebar--navigation--text"
*ngIf="isDesktop || isMobile"
*ngIf="layoutMode !== 'tablet'"
i18n
>
Newsfeed
......@@ -54,14 +49,14 @@
<m-tooltip
icon="check"
i18n="@@COMMON__DISCOVER"
[enabled]="!isDesktop && !isMobile"
[enabled]="layoutMode === 'tablet'"
>
Discover
</m-tooltip>
<span
class="m-sidebar--navigation--text"
*ngIf="isDesktop || isMobile"
*ngIf="layoutMode !== 'tablet'"
i18n
>
Discover
......@@ -79,14 +74,14 @@
<m-tooltip
icon="check"
i18n="@@COMMON__WALLET"
[enabled]="!isDesktop && !isMobile"
[enabled]="layoutMode === 'tablet'"
>
Wallet
</m-tooltip>
<span
class="m-sidebar--navigation--text"
*ngIf="isDesktop || isMobile"
*ngIf="layoutMode !== 'tablet'"
i18n
>
Wallet
......@@ -104,14 +99,14 @@
<m-tooltip
icon="check"
i18n="@@COMMON__PROFILE"
[enabled]="!isDesktop && !isMobile"
[enabled]="layoutMode === 'tablet'"
>
Profile
</m-tooltip>
<span
class="m-sidebar--navigation--text"
*ngIf="isDesktop || isMobile"
*ngIf="layoutMode !== 'tablet'"
i18n
>
Profile
......@@ -129,13 +124,13 @@
<m-tooltip
icon="check"
i18n="@@COMMON__ANALYTICS"
[enabled]="!isDesktop && !isMobile"
[enabled]="layoutMode === 'tablet'"
>
Analytics
</m-tooltip>
<span
class="m-sidebar--navigation--text"
*ngIf="isDesktop || isMobile"
*ngIf="layoutMode !== 'tablet'"
i18n
>
Analytics
......@@ -153,14 +148,14 @@
<m-tooltip
icon="check"
i18n="@@COMMON__SETTINGS"
[enabled]="!isDesktop && !isMobile"
[enabled]="layoutMode === 'tablet'"
>
Settings
</m-tooltip>
<span
class="m-sidebar--navigation--text"
*ngIf="isDesktop || isMobile"
*ngIf="layoutMode !== 'tablet'"
i18n
>
Settings
......@@ -169,8 +164,8 @@
</li>
</ul>
</nav>
<h5 i18n *ngIf="isDesktop || isMobile">
Groups
</h5>
<ng-template dynamic-host></ng-template>
<div class="m-sidebar--groups" [hidden]="!user">
<ng-template dynamic-host></ng-template>
</div>
</div>
m-sidebar--navigation {
display: contents;
&.m-sidebarNavigation--opened {
.m-sidebarNavigation__overlay {
z-index: 999998;
@include m-theme() {
background-color: rgba(themed($m-grey-700), 0.2);
}
}
.m-sidebar--navigation.m-sidebarNavigation--slide {
transform: translateX(316px);
}
}
.m-sidebarNavigation__overlay {
position: fixed;
top: 0;
......@@ -32,6 +46,10 @@ m-sidebar--navigation {
border-right: 1px solid themed($m-grey-400);
}
.m-sidebarNavigation__top {
display: none;
}
&.m-sidebarNavigation--slide {
position: fixed;
top: 0;
......@@ -44,9 +62,9 @@ m-sidebar--navigation {
transition: transform 0.5s cubic-bezier(0.075, 0.82, 0.165, 1);
&.m-sidebarNavigation--opened {
transform: translateX(316px);
}
//&.m-sidebarNavigation--opened {
// transform: translateX(316px);
//}
.m-sidebarNavigation__top {
display: flex;
......@@ -60,7 +78,7 @@ m-sidebar--navigation {
padding: 0 24px;
@include m-theme() {
border-bottom: 1px solid themed($m-primary-border);
border-bottom: 1px solid themed($m-borderColor--primary);
}
a > img {
......@@ -71,7 +89,7 @@ m-sidebar--navigation {
i.material-icons {
cursor: pointer;
@include m-theme() {
color: themed($m-second-text-color);
color: themed($m-textColor--secondary);
}
}
}
......@@ -102,93 +120,6 @@ m-sidebar--navigation {
list-style: none;
padding: 0;
}
h5 {
font-size: 11px;
line-height: 30px;
letter-spacing: 1.83px;
text-transform: uppercase;
@include m-theme() {
color: rgba(themed($m-black), 0.3);
}
}
m-group--sidebar-markers {
.m-groupSidebarMarkers__list {
margin: 0;
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);
}
&.m-groupSidebarMarkers__newGroup,
&.m-groupSidebarMarkers__discoverGroups {
@include m-theme() {
color: themed($m-grey-200);
}
i.material-icons {
@include m-theme() {
color: themed($m-grey-500);
}
}
}
m-tooltip {
vertical-align: middle;
margin-right: 21px;
img {
width: 27px;
height: 27px;
}
i.material-icons {
font-size: 20px;
width: 27px;
height: 27px;
}
}
}
}
infinite-scroll {
padding: 2px 0 !important;
width: 48px !important;
.m-infinite-scroll-manual {
font-size: 10px;
padding: 2px;
background-color: transparent !important;
transform: rotate(0) !important;
text-align: left;
}
}
}
}
}
.m-sidebar--navigation--item {
......@@ -203,7 +134,7 @@ m-sidebar--navigation {
font-size: 17px;
line-height: 44px;
@include m-theme() {
color: themed($m-navigation-item);
color: themed($m-textColor--secondary);
}
&.m-sidebar--navigation--item-active {
......@@ -214,16 +145,7 @@ m-sidebar--navigation {
}
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;
}
}
}
import {
Component,
ComponentFactoryResolver,
HostBinding,
HostListener,
Inject,
OnInit,
......@@ -23,19 +24,22 @@ import { ConfigsService } from '../../services/configs.service';
export class SidebarNavigationComponent implements OnInit {
readonly cdnAssetsUrl: string;
@ViewChild(DynamicHostDirective, { static: true }) host: DynamicHostDirective;
@ViewChild(DynamicHostDirective, { static: true })
host: DynamicHostDirective;
user;
componentRef;
componentInstance: GroupsSidebarMarkersComponent;
isDesktop: boolean = true;
isMobile: boolean = false;
layoutMode: 'phone' | 'tablet' | 'desktop' = 'desktop';
@HostBinding('class.m-sidebarNavigation--opened')
isOpened: boolean = false;
@HostBinding('hidden')
hidden: boolean = true;
constructor(
public navigation: NavigationService,
public session: Session,
......@@ -47,13 +51,25 @@ export class SidebarNavigationComponent implements OnInit {
this.cdnAssetsUrl = this.configs.get('cdn_assets_url');
this.service.setContainer(this);
this.getUser();
}
ngOnInit() {
if (isPlatformBrowser(this.platformId)) {
this.onResize();
}
this.hidden = !this.session.isLoggedIn();
this.service.visibleChange.emit(!this.hidden);
this.session.isLoggedIn(async is => {
if (is) {
this.hidden = false;
this.service.visibleChange.emit(!this.hidden);
}
});
}
ngOnInit() {
ngAfterViewInit() {
this.createGroupsSideBar();
}
......@@ -69,23 +85,39 @@ export class SidebarNavigationComponent implements OnInit {
),
viewContainerRef = this.host.viewContainerRef;
viewContainerRef.clear();
this.componentRef = viewContainerRef.createComponent(componentFactory);
this.componentInstance = this.componentRef.instance;
this.componentInstance.showLabels = true;
this.componentInstance.leftSidebar = true;
}
toggle() {
if (this.isMobile) {
toggle(): void {
if (this.layoutMode === 'phone') {
this.isOpened = !this.isOpened;
}
}
setVisible(value: boolean): void {
this.hidden = !value;
if (value) {
this.createGroupsSideBar();
}
}
@HostListener('window:resize')
onResize() {
this.isDesktop = window.innerWidth > 900;
this.isMobile = window.innerWidth <= 540;
if (window.innerWidth > 900) {
this.layoutMode = 'desktop';
} else if (window.innerWidth > 540 && window.innerWidth <= 900) {
this.layoutMode = 'tablet';
} else {
this.layoutMode = 'phone';
}
if (!this.isMobile) {
if (this.layoutMode !== 'phone') {
this.isOpened = false;
}
}
......
import { Injectable } from '@angular/core';
import { SidebarNavigationComponent } from './navigation.component';
import { EventEmitter } from '@angular/core';
@Injectable()
export class SidebarNavigationService {
container: SidebarNavigationComponent;
visibleChange: EventEmitter<boolean> = new EventEmitter<boolean>();
static _() {
return new SidebarNavigationService();
}
setContainer(container: SidebarNavigationComponent) {
setContainer(container: SidebarNavigationComponent): void {
this.container = container;
}
toggle() {
toggle(): void {
if (this.container) {
this.container.toggle();
}
}
isVisible() {
return this.container ? !this.container.hidden : false;
}
setVisible(value: boolean) {
if (this.container) {
this.container.setVisible(value);
this.visibleChange.emit(value);
}
}
}
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');
}
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;
......
......@@ -56,7 +56,6 @@
}
.m-v2-topbar__Container__LoginWrapper > a {
margin-right: 40px;
@include m-theme() {
background: transparent;
border: 1px solid themed($m-black-always);
......@@ -330,9 +329,7 @@
> a.m-v2-topbarLoginWrapper__login {
padding: 0;
border: none !important;
@media screen and(max-width: $max-mobile) {
margin-right: 10px;
}
margin-right: $minds-margin * 2;
}
> a.m-v2-topbarLoginWrapper__joinMindsNow {
......
......@@ -14,9 +14,8 @@ import { DynamicHostDirective } from '../../directives/dynamic-host.directive';
import { NotificationsToasterComponent } from '../../../modules/notifications/toaster.component';
import { ThemeService } from '../../../common/services/theme.service';
import { ConfigsService } from '../../services/configs.service';
import { V2TopbarService } from './v2-topbar.service';
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
import { Location } from '@angular/common';
import { NavigationEnd, Router } from '@angular/router';
import { TopbarService } from '../topbar.service';
@Component({
selector: 'm-v2-topbar',
......@@ -50,7 +49,7 @@ export class V2TopbarComponent implements OnInit, OnDestroy {
private themeService: ThemeService,
protected componentFactoryResolver: ComponentFactoryResolver,
configs: ConfigsService,
protected topbarService: V2TopbarService,
protected topbarService: TopbarService,
protected router: Router
) {
this.cdnAssetsUrl = configs.get('cdn_assets_url');
......@@ -141,6 +140,9 @@ export class V2TopbarComponent implements OnInit, OnDestroy {
if (this.timeout) {
clearTimeout(this.timeout);
}
if (this.router$) {
this.router$.unsubscribe();
}
}
private listen() {
......
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);
}
}
......@@ -40,7 +40,7 @@ m-usermenu__v3 {
&:not(.m-userMenuDropdown__username):not(.m-userMenuDropdown__spacer):not(.m-userMenuDropdown__footer):hover {
@include m-theme() {
background-color: themed($m-navigation-item-hover);
background-color: themed($m-bgColor--primary);
}
}
......@@ -62,7 +62,7 @@ m-usermenu__v3 {
padding: 0;
@include m-theme() {
background-color: themed($m-navigation-separator);
background-color: themed($m-borderColor--primary);
}
}
......
<ng-template #searchBar>
<div class="m-v3-topbar__SearchBox">
<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"
[class.m-v3Topbar__loggedOut]="!getCurrentUser()"
[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"
[class.m-v3Topbar__leftColumn__rightAlign]="!getCurrentUser()"
>
<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"
*ngIf="shouldShowLogo(); else hamburgerMenu"
>
<img
[src]="cdnAssetsUrl + 'assets/logos/bulb.svg'"
......@@ -24,7 +33,7 @@
</a>
<ng-template #hamburgerMenu>
<div class="m-v3-topbarNav__Item" (click)="toggleSidebarNav()">
<div class="m-v3TopbarNav__item" (click)="toggleSidebarNav()">
<i class="material-icons">menu</i>
</div>
</ng-template>
......@@ -33,12 +42,12 @@
<div class="m-v3Topbar__spacer" *ngIf="isMobile"></div>
<div class="m-v3topbar__middleColumn" *ngIf="!isMobile">
<div class="m-v3Topbar__middleColumn" *ngIf="!isMobile && getCurrentUser()">
<!-- <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()"-->
......@@ -48,7 +57,28 @@
<ng-container *ngTemplateOutlet="searchBar"></ng-container>
</div>
<div class="m-v3topbar__rightColumn">
<div class="m-v3Topbar__rightColumn">
<ng-container *ngIf="!onAuthPages && !getCurrentUser()">
<div class="m-v3TopbarContainer__loginWrapper">
<a
class="m-v3TopbarLoginWrapper__login"
routerLink="/login"
title="Login"
i18n-title
>
Login
</a>
<a
class="m-v3TopbarLoginWrapper__joinMindsNow"
routerLink="/register"
title="Join Minds Now"
i18n-title
>
Join Minds Now
</a>
</div>
</ng-container>
<ng-container *ngIf="isMobile && getCurrentUser()">
<ng-container *ngTemplateOutlet="searchBar"></ng-container>
</ng-container>
......@@ -59,7 +89,7 @@
<!-- 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>
<m-usermenu__v3
[useAvatar]="true"
......@@ -70,44 +100,44 @@
</div>
</div>
<div class="m-v3-topbar__Bottom">
<div class="m-v3Topbar__bottom">
<a
class="m-v3-topbarNav__Item m-v3-topbarNav__RouterNav"
class="m-v3TopbarNav__item m-v3TopbarNav__routerNav"
routerLink="/newsfeed/subscriptions"
routerLinkActive="m-v3-topbarNav__Item--active"
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-v3-topbarNavItem__Text" i18n>Newsfeed</span>
<span class="m-v3TopbarNavItem__text" i18n>Newsfeed</span>
</a>
<a
class="m-v3-topbarNav__Item m-v3-topbarNav__RouterNav"
class="m-v3TopbarNav__item m-v3TopbarNav__routerNav"
routerLink="/newsfeed/global"
routerLinkActive="m-v3-topbarNav__Item--active"
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-v3-topbarNavItem__Text" i18n>Discovery</span>
<span class="m-v3TopbarNavItem__text" i18n>Discovery</span>
</a>
<a
class="m-v3-topbarNav__Item m-v3-topbarNav__RouterNav"
class="m-v3TopbarNav__item m-v3TopbarNav__routerNav"
routerLink="/wallet"
routerLinkActive="m-v3-topbarNav__Item--active"
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-v3-topbarNavItem__Text" i18n>Wallet</span>
<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;
......@@ -20,7 +20,7 @@ m-v3-topbar {
opacity: 0;
}
.m-v3topbar__leftColumn {
.m-v3Topbar__leftColumn {
margin-left: 33.3%;
grid-column: 1 / span 1;
min-width: 0;
......@@ -31,14 +31,18 @@ m-v3-topbar {
@media screen and(max-width: $m-grid-max-mobile) {
margin-left: 23px;
}
&.m-v3Topbar__leftColumn__rightAlign {
justify-content: flex-end;
}
}
.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;
......@@ -50,8 +54,8 @@ m-v3-topbar {
}
@media screen and(min-width: $m-grid-max-mobile) {
.m-v3topbar__leftColumn,
.m-v3topbar__middleColumn {
.m-v3Topbar__leftColumn,
.m-v3Topbar__middleColumn {
@include m-theme() {
border-right: 1px solid themed($m-grey-400);
}
......@@ -60,7 +64,7 @@ m-v3-topbar {
}
}
.m-v3-topbar__Top {
.m-v3Topbar__top {
position: fixed;
top: 0;
left: 0;
......@@ -85,7 +89,57 @@ m-v3-topbar {
width: 100%;
}
.m-v3-topbar__SearchBox {
&.m-v3Topbar__noBackground {
@include m-theme() {
background-color: transparent;
border-bottom: none;
}
}
&.m-v3Topbar__marketingPages {
flex-direction: row;
@include m-theme() {
border: none;
}
.m-grid {
> * {
border: none !important;
}
}
//.m-v2-topbar {
// padding: 15px 0 15px;
// max-width: 1084px;
// margin: 0 auto;
//
// @media screen and (max-width: 1168px) {
// margin: 0 25px;
// }
//
// .m-v2-topbarNavItem__Logo {
// margin: 0;
// padding: 0;
// }
//}
.m-v3TopbarContainer__loginWrapper > a {
@include m-theme() {
background: transparent;
border: 1px solid themed($m-black-always);
color: themed($m-black-always);
}
}
}
&.m-v3Topbar__loggedOut {
.m-grid > * {
border: none !important;
}
}
.m-v3Topbar__searchBox {
width: 100%;
height: 100%;
display: flex;
......@@ -133,6 +187,53 @@ m-v3-topbar {
}
}
.m-v3TopbarContainer__loginWrapper {
> a {
text-decoration: none;
height: 32px;
border-radius: 18px;
font-size: 12px;
letter-spacing: 1.25px;
box-shadow: none;
text-transform: uppercase;
padding: 8px 16px;
font-family: 'Roboto', sans-serif;
cursor: pointer;
@include m-theme() {
background-color: themed($m-white);
border: 1px solid themed($m-blue);
color: themed($m-blue);
}
}
> a.m-v3TopbarLoginWrapper__login,
> a.m-v3TopbarLoginWrapper__joinMindsNow {
font-size: 16px;
line-height: 21px;
font-weight: normal;
text-transform: none;
white-space: nowrap;
@include m-theme() {
color: themed($m-grey-800) !important;
}
}
> a.m-v3TopbarLoginWrapper__login {
padding: 0;
border: none !important;
margin-right: $minds-margin * 2;
}
> a.m-v3TopbarLoginWrapper__joinMindsNow {
@include m-theme() {
border: 1px solid themed($m-grey-800) !important;
}
margin-right: 0 !important;
border-radius: 4px;
}
}
m-notifications--topbar-toggle {
position: relative;
......@@ -159,51 +260,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;
......@@ -214,7 +271,7 @@ m-v3-topbar {
}
}
.m-v3-topbarNav__Item {
.m-v3TopbarNav__item {
padding: 11px 12px 14px;
display: flex;
flex-direction: row;
......@@ -225,14 +282,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) {
......@@ -262,7 +319,7 @@ m-v3-topbar {
}
}
.m-v3-topbarNavItem__Logo {
.m-v3TopbarNavItem__logo {
margin: 0 8px 0 0;
padding: 0;
//height: 62px;
......@@ -276,16 +333,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) {
......@@ -304,18 +361,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;
}
......@@ -339,7 +396,7 @@ m-v3-topbar {
}
}
.m-v3-topbar__Bottom {
.m-v3Topbar__bottom {
display: none;
position: fixed;
bottom: 0;
......@@ -366,13 +423,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;
......
......@@ -16,9 +16,11 @@ 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';
import { NavigationEnd, Router } from '@angular/router';
@Component({
selector: 'm-v3-topbar',
selector: 'm-v3topbar',
templateUrl: 'v3-topbar.component.html',
})
export class V3TopbarComponent implements OnInit, OnDestroy {
......@@ -32,8 +34,17 @@ export class V3TopbarComponent implements OnInit, OnDestroy {
componentRef;
componentInstance: NotificationsToasterComponent;
showTopbar: boolean = true;
forceBackground: boolean = true;
showBackground: boolean = true;
marketingPages: boolean = false;
isMobile: boolean = false;
onAuthPages: boolean = false; // sets to false if we're on login or register pages
router$;
constructor(
protected sidebarService: SidebarNavigationService,
protected themeService: ThemeService,
......@@ -41,6 +52,8 @@ export class V3TopbarComponent implements OnInit, OnDestroy {
protected session: Session,
protected cd: ChangeDetectorRef,
protected componentFactoryResolver: ComponentFactoryResolver,
protected topbarService: TopbarService,
protected router: Router,
@Inject(PLATFORM_ID) private platformId: Object
) {
this.cdnAssetsUrl = this.configs.get('cdn_assets_url');
......@@ -53,6 +66,10 @@ export class V3TopbarComponent implements OnInit, OnDestroy {
ngOnInit() {
this.loadComponent();
this.session.isLoggedIn(() => this.detectChanges());
this.topbarService.setContainer(this);
this.listen();
}
getCurrentUser() {
......@@ -98,6 +115,63 @@ export class V3TopbarComponent implements OnInit, OnDestroy {
this.sidebarService.toggle();
}
private listen() {
this.setOnAuthPages(this.router.url);
this.router$ = this.router.events.subscribe(
(navigationEvent: NavigationEnd) => {
if (navigationEvent instanceof NavigationEnd) {
if (!navigationEvent.urlAfterRedirects) {
return;
}
this.setOnAuthPages(
navigationEvent.urlAfterRedirects || navigationEvent.url
);
}
}
);
}
private setOnAuthPages(url) {
this.onAuthPages = url === '/login' || url === '/register';
this.detectChanges();
}
shouldShowLogo(): boolean {
if (this.marketingPages) {
return true;
} else {
return !this.isMobile;
}
}
/**
* 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;
......@@ -107,5 +181,8 @@ export class V3TopbarComponent implements OnInit, OnDestroy {
if (this.timeout) {
clearTimeout(this.timeout);
}
if (this.router$) {
this.router$.unsubscribe();
}
}
}
......@@ -20,13 +20,13 @@ export class TagsPipe implements PipeTransform {
url: {
rule: /(\b(https?|ftp|file):\/\/[^\s\]]+)/gim,
replace: m => {
return `<a href="${m.match[1]}" target="_blank" rel="noopener noreferrer">${m.match[1]}</a>`;
return `<a href="${m.match[1]}" target="_blank" rel="noopener nofollow ugc">${m.match[1]}</a>`;
},
},
mail: {
rule: /[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/gim,
replace: m => {
return `<a href="mailto:${m.match[0]}" target="_blank" rel="noopener noreferrer">${m.match[0]}</a>`;
return `<a href="mailto:${m.match[0]}" target="_blank" rel="noopener nofollow ugc">${m.match[0]}</a>`;
},
},
hash: {
......
import { Client } from '../api/client.service';
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { map, tap } from 'rxjs/operators';
import { Injectable, Inject, Optional, Injector } from '@angular/core';
import { RedirectService } from './redirect.service';
import { Location } from '@angular/common';
@Injectable()
export class ConfigsService {
private configs = {};
constructor(private client: Client) {}
constructor(
private client: Client,
@Inject('QUERY_STRING') private queryString: string,
private redirectService: RedirectService,
private location: Location
) {}
async loadFromRemote() {
try {
this.configs = await this.client.get('api/v1/minds/config');
this.configs = await this.client.get(
`api/v1/minds/config${this.queryString}`
);
this.redirectToRootIfInvalidDomain();
} catch (err) {
console.error(err);
}
......@@ -24,4 +32,17 @@ export class ConfigsService {
set(key, value): void {
this.configs[key] = value;
}
/**
* Redirect to the root domain if we have an invalid domain response from configs
* @return void
*/
private redirectToRootIfInvalidDomain(): void {
if (this.get('redirect_to_root_on_init') === true) {
const redirectTo: string =
this.get('site_url') + this.location.path().substr(1);
this.redirectService.redirect(redirectTo);
throw `Invalid domain. Redirecting to ${redirectTo}`;
}
}
}
import { Injectable, Optional } from '@angular/core';
import { Injectable, Optional, Inject } 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';
import { DOCUMENT } from '@angular/common';
const DEFAULT_META_TITLE = 'Minds';
const DEFAULT_META_DESCRIPTION = '...';
export const MIN_METRIC_FOR_ROBOTS = 5;
@Injectable()
export class MetaService {
......@@ -18,7 +20,8 @@ export class MetaService {
private metaService: Meta,
private site: SiteService,
private location: Location,
private configs: ConfigsService
private configs: ConfigsService,
@Inject(DOCUMENT) private dom
) {
this.reset();
}
......@@ -29,6 +32,12 @@ export class MetaService {
? this.site.title + ' - ' + this.site.oneLineHeadline
: DEFAULT_META_TITLE;
value = this.stripHtml(value);
if (value.length > 60) {
value = value.substr(0, 57) + '...';
}
if (value && join) {
title = [value, defaultTitle]
.filter(fragment => Boolean(fragment))
......@@ -38,12 +47,17 @@ export class MetaService {
} else {
title = defaultTitle;
}
this.title = title;
this.applyTitle();
return this;
}
setDescription(value: string): MetaService {
value = this.stripHtml(value);
if (value.length > 160) {
value = value.substr(0, 157) + '...';
}
this.metaService.updateTag({ name: 'description', content: value });
return this;
}
......@@ -54,6 +68,33 @@ export class MetaService {
return this;
}
setCanonicalUrl(value: string): MetaService {
// Find and clear or canonical links
const links: HTMLLinkElement[] = this.dom.head.querySelectorAll(
'[rel="canonical"]'
);
if (links.length) {
for (const link of links) {
this.dom.head.removeChild(link);
}
}
if (value) {
// TODO: fix duplicated code with ogUrl here...
if (value && value.indexOf('/') === 0) {
// Relative path
value = this.site.baseUrl + value.substr(1);
}
let link: HTMLLinkElement;
link = this.dom.createElement('link');
link.setAttribute('rel', 'canonical');
link.setAttribute('href', value);
this.dom.head.appendChild(link);
}
return this;
}
setOgUrl(value: string): MetaService {
if (value && value.indexOf('/') === 0) {
// Relative path
......@@ -100,11 +141,24 @@ export class MetaService {
return this;
}
setLanguage(language: string): MetaService {
return this;
}
setRobots(value: string): MetaService {
this.metaService.updateTag({ name: 'robots', content: value });
return this;
}
setNsfw(value: boolean): MetaService {
if (value) {
this.metaService.updateTag({ name: 'rating', content: 'adult' });
} else {
this.metaService.removeTag("name='rating'");
}
return this;
}
reset(
data: {
title?: string;
......@@ -119,7 +173,9 @@ export class MetaService {
.setOgType('website')
.setOgUrl(data.ogUrl || this.location.path())
.setOgImage(data.ogImage || null, { width: 0, height: 0 })
.setRobots(data.robots || 'all');
.setCanonicalUrl('') // Only user canonical when required
.setRobots(data.robots || 'all')
.setNsfw(false);
}
private applyTitle(): void {
......@@ -133,4 +189,16 @@ export class MetaService {
content: this.title,
});
}
/**
* Removes any html found and returns on text
* @param value
* @return string
*/
private stripHtml(value: string): string {
if (!value) return '';
const fakeEl = this.dom.createElement('span');
fakeEl.innerHTML = value;
return fakeEl.textContent || fakeEl.innerText;
}
}
import { Inject, Injectable } from '@angular/core';
import { RESPONSE } from '@nguniversal/express-engine/tokens';
export class RedirectService {
public redirect(url: string): void {}
}
export class BrowserRedirectService extends RedirectService {
redirect(url: string): void {
window.location.href = url;
}
}
export class ServerRedirectService extends RedirectService {
constructor(@Inject(RESPONSE) private res) {
super();
}
redirect(url: string, permanent: boolean = false): void {
const code = permanent ? 301 : 302;
this.res.redirect(code, url);
this.res.end();
}
}
......@@ -24,7 +24,7 @@ export class ThemeService {
private client: Client,
private session: Session,
private storage: Storage,
private platformId: Object
@Inject(PLATFORM_ID) private platformId: Object
) {
this.renderer = rendererFactory.createRenderer(null, null);
}
......
<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>
@import 'defaults';
minds-admin {
grid-area: content / span 2;
min-width: 0;
}
minds-admin-boosts {
minds-button-comment {
display: none !important;
......
<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();
}
}
@import 'defaults';
m-pages {
grid-area: content / span 2;
min-width: 0;
}
.m-pages {
font-family: Roboto, Helvetica, Arial, sans-serif;
@include m-theme() {
......
......@@ -9,6 +9,7 @@ import { MetaService } from '../../common/services/meta.service';
import { PagesService } from '../../common/services/pages.service';
@Component({
selector: 'm-pages',
moduleId: module.id,
templateUrl: 'pages.html',
})
......
......@@ -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,
];
......@@ -115,6 +115,7 @@ export interface MindsUser {
has_custom_background?: boolean;
};
mode: ChannelMode;
nsfw: Array<number>;
}
export interface MindsGroup {
......
......@@ -2,6 +2,9 @@
m-analytics {
display: block;
grid-area: content / span 2;
min-width: 0;
@include m-theme() {
background-color: themed($m-white);
}
......@@ -20,3 +23,7 @@ m-analytics {
}
}
}
.m-grid m-analytics .m-analytics__content {
margin: unset;
}
......@@ -5,6 +5,7 @@ m-login {
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
position: relative;
grid-area: content / span 2;
&:not(.m-login__newDesign) {
@include m-theme() {
......@@ -38,6 +39,10 @@ m-login {
&.m-login__newDesign {
margin-top: -52px;
&.m-login__newNavigation {
margin-top: -75px;
}
&.m-login__iosFallback {
.m-grid {
.m-login__wrapper {
......
......@@ -35,7 +35,7 @@ import {
import { FeaturesService } from '../../services/features.service';
import { featuresServiceMock } from '../../../tests/features-service-mock.spec';
import { IfFeatureDirective } from '../../common/directives/if-feature.directive';
import { V2TopbarService } from '../../common/layout/v2-topbar/v2-topbar.service';
import { TopbarService } from '../../common/layout/topbar.service';
import { MockService } from '../../utils/mock';
@Component({
......@@ -90,7 +90,7 @@ describe('LoginComponent', () => {
CookieService,
{ provide: COOKIE_OPTIONS, useValue: CookieOptionsProvider },
{ provide: FeaturesService, useValue: featuresServiceMock },
{ provide: V2TopbarService, useValue: MockService(V2TopbarService) },
{ provide: TopbarService, useValue: MockService(TopbarService) },
],
}).compileComponents();
}));
......
......@@ -10,8 +10,8 @@ import { LoginReferrerService } from '../../services/login-referrer.service';
import { OnboardingService } from '../onboarding/onboarding.service';
import { CookieService } from '../../common/services/cookie.service';
import { FeaturesService } from '../../services/features.service';
import { V2TopbarService } from '../../common/layout/v2-topbar/v2-topbar.service';
import { iOSVersion } from '../../helpers/is-safari';
import { TopbarService } from '../../common/layout/topbar.service';
@Component({
selector: 'm-login',
......@@ -27,6 +27,10 @@ export class LoginComponent implements OnInit, OnDestroy {
@HostBinding('class.m-login__newDesign')
newDesign: boolean = false;
@HostBinding('class.m-login__newNavigation')
newNavigation: boolean = false;
@HostBinding('class.m-login__iosFallback')
iosFallback: boolean = false;
......@@ -46,7 +50,7 @@ export class LoginComponent implements OnInit, OnDestroy {
private cookieService: CookieService,
private onboarding: OnboardingService,
private featuresService: FeaturesService,
private topbarService: V2TopbarService
private topbarService: TopbarService
) {}
ngOnInit() {
......@@ -68,6 +72,7 @@ export class LoginComponent implements OnInit, OnDestroy {
}
this.newDesign = this.featuresService.has('register_pages-december-2019');
this.newNavigation = this.featuresService.has('navigation');
if (this.newDesign) {
this.topbarService.toggleVisibility(false);
......
......@@ -5,10 +5,15 @@ m-register {
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
position: relative;
grid-area: content / span 2;
&.m-register__newDesign {
margin-top: -52px;
&.m-register__newNavigation {
margin-top: -75px;
}
&.m-register__iosFallback {
.m-grid {
.m-register__wrapper {
......
......@@ -19,7 +19,7 @@ import { MockComponent, MockService } from '../../utils/mock';
import { FeaturesService } from '../../services/features.service';
import { featuresServiceMock } from '../../../tests/features-service-mock.spec';
import { IfFeatureDirective } from '../../common/directives/if-feature.directive';
import { V2TopbarService } from '../../common/layout/v2-topbar/v2-topbar.service';
import { TopbarService } from '../../common/layout/topbar.service';
describe('RegisterComponent', () => {
let comp: RegisterComponent;
......@@ -44,7 +44,7 @@ describe('RegisterComponent', () => {
{ provide: SignupModalService, useValue: signupModalServiceMock },
{ provide: LoginReferrerService, useValue: loginReferrerServiceMock },
{ provide: FeaturesService, useValue: featuresServiceMock },
{ provide: V2TopbarService, useValue: MockService(V2TopbarService) },
{ provide: TopbarService, useValue: MockService(TopbarService) },
],
}).compileComponents();
}));
......
......@@ -12,10 +12,10 @@ import { OnboardingService } from '../onboarding/onboarding.service';
import { ConfigsService } from '../../common/services/configs.service';
import { PagesService } from '../../common/services/pages.service';
import { FeaturesService } from '../../services/features.service';
import { V2TopbarService } from '../../common/layout/v2-topbar/v2-topbar.service';
import { OnboardingV2Service } from '../onboarding-v2/service/onboarding.service';
import { MetaService } from '../../common/services/meta.service';
import { iOSVersion } from '../../helpers/is-safari';
import { TopbarService } from '../../common/layout/topbar.service';
@Component({
selector: 'm-register',
......@@ -29,8 +29,13 @@ export class RegisterComponent implements OnInit, OnDestroy {
inProgress: boolean = false;
videoError: boolean = false;
referrer: string;
@HostBinding('class.m-register__newDesign')
newDesign: boolean = false;
@HostBinding('class.m-register__newNavigation')
newNavigation: boolean = false;
@HostBinding('class.m-register__iosFallback')
iosFallback: boolean = false;
......@@ -54,7 +59,7 @@ export class RegisterComponent implements OnInit, OnDestroy {
configs: ConfigsService,
public pagesService: PagesService,
private featuresService: FeaturesService,
private topbarService: V2TopbarService,
private topbarService: TopbarService,
private onboardingService: OnboardingV2Service,
private metaService: MetaService
) {
......@@ -65,6 +70,8 @@ export class RegisterComponent implements OnInit, OnDestroy {
}
this.newDesign = this.featuresService.has('register_pages-december-2019');
this.newNavigation = this.featuresService.has('navigation');
if (this.newDesign) {
this.topbarService.toggleVisibility(false);
this.iosFallback = iOSVersion() !== null;
......
......@@ -10,7 +10,10 @@ import { AnalyticsService } from '../../../services/analytics';
import { MindsBlogResponse } from '../../../interfaces/responses';
import { MindsBlogEntity } from '../../../interfaces/entities';
import { ConfigsService } from '../../../common/services/configs.service';
import { MetaService } from '../../../common/services/meta.service';
import {
MetaService,
MIN_METRIC_FOR_ROBOTS,
} from '../../../common/services/meta.service';
@Component({
selector: 'm-blog-view-infinite',
......@@ -124,7 +127,16 @@ export class BlogViewInfinite {
.setTitle(blog.custom_meta['title'] || blog.title)
.setDescription(description)
//.setAuthor(this.blog.custom_meta['author'] || `@${this.blog.ownerObj.username}`)
.setOgType('article')
.setCanonicalUrl(blog.perma_url)
.setOgUrl(blog.perma_url)
.setOgImage(blog.thumbnail_src);
.setOgImage(blog.thumbnail_src)
.setRobots(
blog['thumbs:up:count'] >= MIN_METRIC_FOR_ROBOTS ? 'all' : 'noindex'
);
if (blog.nsfw.length) {
this.metaService.setNsfw(true);
}
}
}
@import 'defaults';
.m-branding {
.m-marketing--header {
@include m-theme() {
background-color: themed($m-white);
}
}
.m-marketing--header-inner {
text-align: center;
m-branding {
grid-area: content / span 2;
h1 {
.m-branding {
.m-marketing--header {
@include m-theme() {
color: themed($m-grey-800);
background-color: themed($m-white);
}
}
h3 {
padding-right: 0;
@include m-theme() {
color: themed($m-grey-600);
.m-marketing--header-inner {
text-align: center;
h1 {
@include m-theme() {
color: themed($m-grey-800);
}
}
}
}
.m-marketing--contents {
text-align: center;
img {
max-height: 156px;
@media only screen and (max-width: 460px) {
max-width: 332px;
h3 {
padding-right: 0;
@include m-theme() {
color: themed($m-grey-600);
}
}
}
label {
display: block;
font-weight: 600;
text-transform: uppercase;
margin: 56px 0;
@include m-theme() {
color: themed($m-grey-600);
.m-marketing--contents {
text-align: center;
img {
max-height: 156px;
@media only screen and (max-width: 460px) {
max-width: 332px;
}
}
label {
display: block;
font-weight: 600;
text-transform: uppercase;
margin: 56px 0;
@include m-theme() {
color: themed($m-grey-600);
}
}
}
}
......
......@@ -45,7 +45,7 @@
<h2>Experiments</h2>
<ul class="m-canaryExperiments__list">
<li>
Discovery algorithm by post age - 11th December '19
Server Side Rendering - 5th Feb 2020
</li>
</ul>
</div>
......
<ng-container *ngIf="captcha">
<img [src]="captcha.base64Image" />
<i class="material-icons m-captcha__refresh" (click)="refresh()">refresh</i>
<input
[ngModel]="captcha.clientText"
(ngModelChange)="onValueChange($event)"
type="text"
placeholder="Enter the characters above"
/>
</ng-container>
m-captcha {
display: block;
img {
margin-bottom: $minds-margin;
}
}
.m-captcha__refresh {
cursor: pointer;
position: absolute;
}
import {
async,
ComponentFixture,
fakeAsync,
TestBed,
tick,
} from '@angular/core/testing';
import { DebugElement } from '@angular/core';
import { CaptchaComponent, Captcha } from './captcha.component';
import { ReactiveFormsModule } from '@angular/forms';
import { Client } from '../../services/api';
import { clientMock } from '../../../tests/client-mock.spec';
import { By } from '@angular/platform-browser';
describe('CaptchaComponent', () => {
let comp: CaptchaComponent;
let fixture: ComponentFixture<CaptchaComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [CaptchaComponent],
imports: [ReactiveFormsModule],
providers: [{ provide: Client, useValue: clientMock }],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(CaptchaComponent);
comp = fixture.componentInstance;
fixture.detectChanges();
clientMock.response = {};
});
});
import {
Component,
ElementRef,
forwardRef,
OnChanges,
OnInit,
ViewChild,
} from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Client } from '../../services/api';
export const CAPTCHA_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CaptchaComponent),
multi: true,
};
export class Captcha {
jwtToken: string;
base64Image: string;
clientText: string; // This is what the user enters
buildClientKey(): string {
return JSON.stringify({
jwtToken: this.jwtToken,
clientText: this.clientText,
});
}
}
@Component({
selector: 'm-captcha',
templateUrl: 'captcha.component.html',
providers: [CAPTCHA_VALUE_ACCESSOR],
})
export class CaptchaComponent implements ControlValueAccessor, OnInit {
captcha = new Captcha();
image: string;
value: string = '';
propagateChange = (_: any) => {};
constructor(private client: Client) {}
ngOnInit(): void {
this.refresh();
}
async refresh(): Promise<void> {
const response: any = await this.client.get('api/v2/captcha', {
cb: Date.now(),
});
this.captcha.base64Image = response.base64_image;
this.captcha.jwtToken = response.jwt_token;
}
onValueChange(value: string) {
this.captcha.clientText = value;
this.value = this.captcha.buildClientKey();
this.propagateChange(this.value);
}
writeValue(value: any): void {
// Not required as captcha is one direction
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {}
}
import { NgModule } from '@angular/core';
import { ReCaptchaComponent } from './recaptcha/recaptcha.component';
import { RECAPTCHA_SERVICE_PROVIDER } from './recaptcha/recaptcha.service';
import { CaptchaComponent } from './captcha.component';
import { FormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';
@NgModule({
declarations: [ReCaptchaComponent],
exports: [ReCaptchaComponent],
providers: [RECAPTCHA_SERVICE_PROVIDER],
imports: [CommonModule, FormsModule],
declarations: [CaptchaComponent],
exports: [CaptchaComponent],
})
export class CaptchaModule {}
import {
Component,
OnInit,
Input,
Output,
EventEmitter,
NgZone,
ViewChild,
ElementRef,
forwardRef,
} from '@angular/core';
import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms';
import { ReCaptchaService } from './recaptcha.service';
@Component({
selector: 're-captcha',
template: '<div #target></div>',
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => ReCaptchaComponent),
multi: true,
},
],
})
export class ReCaptchaComponent implements OnInit, ControlValueAccessor {
@Input() site_key: string = null;
@Input() theme = 'light';
@Input() type = 'image';
@Input() size = 'normal';
@Input() tabindex = 0;
@Input() badge = 'bottomright';
/* Available languages: https://developers.google.com/recaptcha/docs/language */
@Input() language: string = null;
@Output() captchaResponse = new EventEmitter<string>();
@Output() captchaExpired = new EventEmitter();
@ViewChild('target', { static: true }) targetRef: ElementRef;
widgetId: any = null;
onChange: Function = () => {
return;
};
onTouched: Function = () => {
return;
};
constructor(
private _zone: NgZone,
private _captchaService: ReCaptchaService
) {}
ngOnInit() {
this._captchaService.getReady(this.language).subscribe(ready => {
if (!ready) return;
// noinspection TypeScriptUnresolvedVariable,TypeScriptUnresolvedFunction
this.widgetId = (<any>window).grecaptcha.render(
this.targetRef.nativeElement,
{
sitekey: this.site_key,
badge: this.badge,
theme: this.theme,
type: this.type,
size: this.size,
tabindex: this.tabindex,
callback: <any>(
((response: any) =>
this._zone.run(this.recaptchaCallback.bind(this, response)))
),
'expired-callback': <any>(
(() => this._zone.run(this.recaptchaExpiredCallback.bind(this)))
),
}
);
});
}
// noinspection JSUnusedGlobalSymbols
public reset() {
if (this.widgetId === null) return;
// noinspection TypeScriptUnresolvedVariable
(<any>window).grecaptcha.reset(this.widgetId);
this.onChange(null);
}
// noinspection JSUnusedGlobalSymbols
public execute() {
if (this.widgetId === null) return;
// noinspection TypeScriptUnresolvedVariable
(<any>window).grecaptcha.execute(this.widgetId);
}
public getResponse(): string {
if (this.widgetId === null) return null;
// noinspection TypeScriptUnresolvedVariable
return (<any>window).grecaptcha.getResponse(this.widgetId);
}
writeValue(newValue: any): void {
/* ignore it */
}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
private recaptchaCallback(response: string) {
this.onChange(response);
this.onTouched();
this.captchaResponse.emit(response);
}
private recaptchaExpiredCallback() {
this.onChange(null);
this.onTouched();
this.captchaExpired.emit();
}
}
import { Injectable, NgZone, Optional, SkipSelf } from '@angular/core';
import { Observable, BehaviorSubject } from 'rxjs';
/*
* Common service shared by all reCaptcha component instances
* through dependency injection.
* This service has the task of loading the reCaptcha API once for all.
* Only the first instance of the component creates the service, subsequent
* components will use the existing instance.
*
* As the language is passed to the <script>, the first component
* determines the language of all subsequent components. This is a limitation
* of the present Google API.
*/
@Injectable()
export class ReCaptchaService {
private scriptLoaded = false;
private readySubject: BehaviorSubject<boolean> = new BehaviorSubject(false);
constructor(zone: NgZone) {
/* the callback needs to exist before the API is loaded */
window[<any>'reCaptchaOnloadCallback'] = <any>(
(() => zone.run(this.onloadCallback.bind(this)))
);
}
public getReady(language: string): Observable<boolean> {
if (!this.scriptLoaded) {
this.scriptLoaded = true;
let doc = <HTMLDivElement>document.body;
let script = document.createElement('script');
script.innerHTML = '';
script.src =
'https://www.google.com/recaptcha/api.js?onload=reCaptchaOnloadCallback&render=explicit' +
(language ? '&hl=' + language : '');
script.async = true;
script.defer = true;
doc.appendChild(script);
}
return this.readySubject.asObservable();
}
private onloadCallback() {
this.readySubject.next(true);
}
}
/* singleton pattern taken from https://github.com/angular/angular/issues/13854 */
export function RECAPTCHA_SERVICE_PROVIDER_FACTORY(
ngZone: NgZone,
parentDispatcher: ReCaptchaService
) {
return parentDispatcher || new ReCaptchaService(ngZone);
}
export const RECAPTCHA_SERVICE_PROVIDER = {
provide: ReCaptchaService,
deps: [NgZone, [new Optional(), new SkipSelf(), ReCaptchaService]],
useFactory: RECAPTCHA_SERVICE_PROVIDER_FACTORY,
};
<ng-container *ngIf="error || channel; else loader">
<ng-container
*ngIf="
error || !proEnabled || !channel.pro_published || isOwner || isAdmin;
error ||
!proEnabled ||
!showPro ||
!channel.pro_published ||
isOwner ||
isAdmin;
else isProChannel
"
>
......
......@@ -28,6 +28,7 @@ export class ChannelContainerComponent implements OnInit, OnDestroy {
channel: MindsUser;
protected username: string;
protected showPro: boolean;
protected param$: Subscription;
......@@ -50,10 +51,11 @@ export class ChannelContainerComponent implements OnInit, OnDestroy {
this.param$ = this.route.params.subscribe(params => {
if (params['username']) {
this.username = params['username'];
this.showPro = !params['pro'] || params['pro'] !== '0';
if (
this.username &&
(!this.channel || this.channel.username != this.username)
(!this.channel || this.channel.username !== this.username)
) {
this.load();
}
......@@ -74,7 +76,7 @@ export class ChannelContainerComponent implements OnInit, OnDestroy {
}
async load() {
if (!this.username) {
if (!this.username || this.showPro === undefined) {
return;
}
......@@ -88,6 +90,7 @@ export class ChannelContainerComponent implements OnInit, OnDestroy {
this.channel = response.channel;
const shouldRedirectToProHandler =
this.showPro &&
!this.site.isProDomain &&
this.channel.pro_published &&
!this.isOwner &&
......
<div class="m--spam-notice" *ngIf="user?.spam">
<ng-container *ngIf="user?.spam" i18n="@@MINDS__CHANNELS__FLAGGED_AS_SPAM">
This user was flagged as spam.
</ng-container>
<ng-container *ngIf="user?.spam" i18n="@@MINDS__CHANNELS__FLAGGED_AS_SPAM"
>This user was flagged as spam.</ng-container
>
<ng-container
*ngIf="user?.deleted"
i18n="@@MINDS__CHANNELS__FLAGGED_AS_DELETED"
>This user was flagged as deleted.
</ng-container>
>This user was flagged as deleted.</ng-container
>
<ng-container *ngIf="!session.isAdmin()" i18n="@@M__ACTION__APPEAL_MARK">
If you wish to appeal, please check your
......@@ -22,154 +22,136 @@
></div>
</div>
<div class="m-error-splash" *ngIf="error && !user">
<img [src]="cdnAssetsUrl + 'assets/logos/logo.svg'" />
<img [src]="minds.cdn_assets_url + 'assets/logos/logo.svg'" />
<h3 class="mdl-color-text--red-300">{{ error }}</h3>
<span
class="mdl-color-text--blue-grey-300"
i18n="@@MINDS__CHANNELS__ERROR_CHECK_USERNAME"
>Please check the username</span
>
Please check the username
</span>
</div>
<ng-container *mIfFeature="'navigation-2020'; else oldChannel">
<div class="m-channel__middleColumn">
feed goes here
</div>
<div class="m-channel__rightColumn">
<m-channel__sidebarv2
[user]="user"
[editing]="editing"
(changeEditing)="toggleEditing()"
></m-channel__sidebarv2>
</div>
</ng-container>
<ng-template #oldChannel>
<header [hidden]="!isLocked"></header>
<header [hidden]="!isLocked"></header>
<header *ngIf="user" [ngClass]="{ isLocked: isLocked }">
<m-channel--carousel
[banners]="user.carousels"
[editMode]="editing"
(done)="updateCarousels($event)"
(delete)="removeCarousel($event)"
></m-channel--carousel>
</header>
<header *ngIf="user" [ngClass]="{ isLocked: isLocked }">
<m-channel--carousel
[banners]="user.carousels"
[editMode]="editing"
(done)="updateCarousels($event)"
(delete)="removeCarousel($event)"
></m-channel--carousel>
</header>
<div
class="mdl-grid channel-grid m-channel--blocked"
style="max-width:900px"
*ngIf="user && user?.blocked"
<div
class="mdl-grid channel-grid m-channel--blocked"
style="max-width:900px"
*ngIf="user && user?.blocked"
>
<h3
class="mdl-color-text--blue-grey-300"
i18n="@@MINDS__CHANNELS__BLOCKED_NOTICE"
>
<h3
class="mdl-color-text--blue-grey-300"
i18n="@@MINDS__CHANNELS__BLOCKED_NOTICE"
>
You have blocked @{{ user.username }}
</h3>
<a (click)="unBlock()" i18n="@@MINDS__CHANNELS__UNBLOCK_ACTION">
Click to Unblock
</a>
</div>
<div
class="mdl-grid channel-grid"
style="max-width:990px"
*ngIf="user && !user?.blocked"
You have blocked @{{ user.username }}
</h3>
<a (click)="unBlock()" i18n="@@MINDS__CHANNELS__UNBLOCK_ACTION"
>Click to Unblock</a
>
<section class="mdl-cell mdl-cell--4-col m-channel-sidebar">
<m-channel--sidebar
[user]="user"
[editing]="editing"
(changeEditing)="toggleEditing()"
></m-channel--sidebar>
</section>
</div>
<!-- Feed list -->
<div
class="mdl-grid channel-grid"
style="max-width:990px"
*ngIf="user && !user?.blocked"
>
<section class="mdl-cell mdl-cell--4-col m-channel-sidebar">
<m-channel--sidebar
[user]="user"
[editing]="editing"
(changeEditing)="toggleEditing($event)"
></m-channel--sidebar>
</section>
<ng-container *mIfFeature="'es-feeds'; else legacyFeed">
<section class="mdl-cell mdl-cell--8-col" *ngIf="shouldShowFeeds()">
<m-channel--sorted
[channel]="user"
[type]="getFeedType()"
(onChangeType)="setFeedType($event)"
></m-channel--sorted>
</section>
</ng-container>
<ng-template #legacyFeed>
<section
class="mdl-cell mdl-cell--8-col m-channel-feed"
*ngIf="filter == 'feed'"
>
<m-channel--feed [user]="user" #feed></m-channel--feed>
</section>
</ng-template>
<!-- Feed list -->
<!-- Supporters list -->
<section class="mdl-cell mdl-cell--8-col" *ngIf="filter == 'supporters'">
<m-channel--supporters
<ng-container *mIfFeature="'es-feeds'; else legacyFeed">
<section class="mdl-cell mdl-cell--8-col" *ngIf="shouldShowFeeds()">
<m-channel--sorted
[channel]="user"
class="mdl-grid"
></m-channel--supporters>
[type]="getFeedType()"
(onChangeType)="setFeedType($event)"
></m-channel--sorted>
</section>
</ng-container>
<ng-template #legacyFeed>
<section
class="mdl-cell mdl-cell--8-col m-channel-feed"
*ngIf="filter == 'feed'"
>
<m-channel--feed [user]="user" #feed></m-channel--feed>
</section>
</ng-template>
<!-- Supporters list -->
<section class="mdl-cell mdl-cell--8-col" *ngIf="filter == 'supporters'">
<m-channel--supporters
[channel]="user"
class="mdl-grid"
></m-channel--supporters>
</section>
<!-- Subscriptions list -->
<section class="mdl-cell mdl-cell--8-col" *ngIf="filter == 'subscriptions'">
<div class="mdl-card m-channel--subscribers-tabs">
<div class="mdl-tabs__tab-bar">
<a
[routerLink]="['/', user.username, 'subscribers']"
class="mdl-tabs__tab"
[ngClass]="{ 'is-active': filter === 'subscribers' }"
i18n="@@MINDS__CHANNELS__SUBSCRIBERS_WITH_COUNT"
>
Subscribers ({{ user.subscribers_count }})
</a>
<a
[routerLink]="['/', user.username, 'subscriptions']"
class="mdl-tabs__tab"
[ngClass]="{ 'is-active': filter === 'subscriptions' }"
i18n="@@MINDS__CHANNELS__SUBSCRIPTIONS_WITH_COUNT"
>
Subscriptions ({{ user.subscriptions_count }})
</a>
</div>
<!-- Subscriptions list -->
<section class="mdl-cell mdl-cell--8-col" *ngIf="filter == 'subscriptions'">
<div class="mdl-card m-channel--subscribers-tabs">
<div class="mdl-tabs__tab-bar">
<a
[routerLink]="['/', user.username, 'subscribers']"
class="mdl-tabs__tab"
[ngClass]="{ 'is-active': filter === 'subscribers' }"
i18n="@@MINDS__CHANNELS__SUBSCRIBERS_WITH_COUNT"
>Subscribers ({{ user.subscribers_count }})</a
>
<a
[routerLink]="['/', user.username, 'subscriptions']"
class="mdl-tabs__tab"
[ngClass]="{ 'is-active': filter === 'subscriptions' }"
i18n="@@MINDS__CHANNELS__SUBSCRIPTIONS_WITH_COUNT"
>Subscriptions ({{ user.subscriptions_count }})</a
>
</div>
</div>
<m-channel--subscriptions
[channel]="user"
class="mdl-grid"
></m-channel--subscriptions>
</section>
<m-channel--subscriptions
[channel]="user"
class="mdl-grid"
></m-channel--subscriptions>
</section>
<!-- Subscriptions list -->
<section class="mdl-cell mdl-cell--8-col" *ngIf="filter == 'subscribers'">
<div class="mdl-card m-channel--subscribers-tabs">
<div class="mdl-tabs__tab-bar">
<a
[routerLink]="['/', user.username, 'subscribers']"
class="mdl-tabs__tab"
[ngClass]="{ 'is-active': filter === 'subscribers' }"
i18n="@@MINDS__CHANNELS__SUBSCRIBERS_WITH_COUNT"
>
Subscribers ({{ user.subscribers_count }})
</a>
<a
[routerLink]="['/', user.username, 'subscriptions']"
class="mdl-tabs__tab"
[ngClass]="{ 'is-active': filter === 'subscriptions' }"
i18n="@@MINDS__CHANNELS__SUBSCRIPTIONS_WITH_COUNT"
>
Subscriptions ({{ user.subscriptions_count }})
</a>
</div>
<!-- Subscriptions list -->
<section class="mdl-cell mdl-cell--8-col" *ngIf="filter == 'subscribers'">
<div class="mdl-card m-channel--subscribers-tabs">
<div class="mdl-tabs__tab-bar">
<a
[routerLink]="['/', user.username, 'subscribers']"
class="mdl-tabs__tab"
[ngClass]="{ 'is-active': filter === 'subscribers' }"
i18n="@@MINDS__CHANNELS__SUBSCRIBERS_WITH_COUNT"
>Subscribers ({{ user.subscribers_count }})</a
>
<a
[routerLink]="['/', user.username, 'subscriptions']"
class="mdl-tabs__tab"
[ngClass]="{ 'is-active': filter === 'subscriptions' }"
i18n="@@MINDS__CHANNELS__SUBSCRIPTIONS_WITH_COUNT"
>Subscriptions ({{ user.subscriptions_count }})</a
>
</div>
</div>
<m-channel--subscribers
[channel]="user"
class="mdl-grid"
></m-channel--subscribers>
</section>
</div>
</ng-template>
<m-channel--subscribers
[channel]="user"
class="mdl-grid"
></m-channel--subscribers>
</section>
</div>
<m-channel--explicit-overlay [channel]="user"></m-channel--explicit-overlay>
......@@ -4,7 +4,8 @@
* Channel profile pages
*/
m-channel {
display: contents;
grid-area: content / span 2;
min-width: 0;
.m-channel__middleColumn {
grid-area: content;
......@@ -16,8 +17,9 @@ m-channel {
.m-channel__rightColumn {
grid-area: side;
//padding-left: 59px;
//padding-right: 33.3%;
padding-left: 56px;
padding-right: 33%;
margin-top: 25px;
min-width: 0;
}
......
......@@ -101,7 +101,8 @@ describe('ChannelComponent', () => {
{ provide: Client, useValue: clientMock },
{ provide: Upload, useValue: uploadMock },
{ provide: Session, useValue: sessionMock },
{ provide: MetaService, useValue: MockService(MetaService) },
MetaService,
SiteService,
{ provide: ScrollService, useValue: scrollServiceMock },
{ provide: RecentService, useValue: recentServiceMock },
{ provide: ContextService, useValue: contextServiceMock },
......@@ -114,7 +115,7 @@ describe('ChannelComponent', () => {
{ provide: FeaturesService, useValue: featuresServiceMock },
{ provide: BlockListService, useValue: MockService(BlockListService) },
{ provide: ClientMetaService, useValue: clientMetaServiceMock },
ConfigsService,
{ provide: ConfigsService, useValue: MockService(ConfigsService) },
],
}).compileComponents(); // compile template and css
}));
......@@ -130,7 +131,7 @@ describe('ChannelComponent', () => {
featuresServiceMock.mock('es-feeds', false);
featuresServiceMock.mock('top-feeds', false);
featuresServiceMock.mock('channel-filter-feeds', false);
featuresServiceMock.mock('navigation-2020', false);
featuresServiceMock.mock('navigation', false);
comp = fixture.componentInstance;
comp.username = 'username';
......@@ -149,6 +150,7 @@ describe('ChannelComponent', () => {
large: 'thumbs',
master: 'thumbs',
},
nsfw: [],
};
comp.editing = false;
fixture.detectChanges();
......
......@@ -18,7 +18,10 @@ import { DialogService } from '../../common/services/confirm-leave-dialog.servic
import { BlockListService } from '../../common/services/block-list.service';
import { ChannelSortedComponent } from './sorted/sorted.component';
import { ClientMetaService } from '../../common/services/client-meta.service';
import { MetaService } from '../../common/services/meta.service';
import {
MetaService,
MIN_METRIC_FOR_ROBOTS,
} from '../../common/services/meta.service';
import { ConfigsService } from '../../common/services/configs.service';
@Component({
......@@ -70,6 +73,9 @@ export class ChannelComponent {
ngOnInit() {
this.updateMeta();
if (this.user) {
this.clientMetaService.recordView(this.user);
}
this.context.set('activity');
this.onScroll();
......@@ -121,16 +127,26 @@ export class ChannelComponent {
private updateMeta(): void {
if (this.user) {
this.metaService.setTitle(`${this.user.name} (@${this.user.username})`);
this.metaService.setDescription(
this.user.briefdescription || `Subscribe to @${this.user.username}`
);
this.metaService.setOgUrl(`/${this.user.username.toLowerCase()}`);
this.metaService.setOgImage(this.user.avatar_url.master, {
width: 2000,
height: 1000,
});
this.metaService.setRobots(this.user.is_mature ? 'noindex' : 'all');
const url = `/${this.user.username.toLowerCase()}`;
this.metaService
.setTitle(`${this.user.name} (@${this.user.username})`)
.setDescription(
this.user.briefdescription || `Subscribe to @${this.user.username}`
)
.setOgUrl(url)
.setCanonicalUrl(url)
.setOgImage(this.user.avatar_url.master, {
width: 2000,
height: 1000,
})
.setRobots(
this.user['subscribers_count'] < MIN_METRIC_FOR_ROBOTS
? 'noindex'
: 'all'
);
if (this.user.is_mature || this.user.nsfw.length) {
this.metaService.setNsfw(true);
}
} else if (this.username) {
this.metaService.setTitle(this.username);
} else {
......@@ -171,6 +187,7 @@ export class ChannelComponent {
this.addRecent();
}
// this.load() is only called if this.user was not previously set
this.clientMetaService.recordView(this.user);
})
.catch(e => {
......
......@@ -29,6 +29,8 @@ import { ChannelSortedComponent } from './sorted/sorted.component';
import { ChannelSortedModuleComponent } from './sorted/module.component';
import { ReferralsModule } from '../wallet/tokens/referrals/referrals.module';
import { ChannelSidebarV2Component } from './sidebar-v2/sidebar-v2.component';
import { ChannelStatsComponent } from './sidebar-v2/stats/channel-stats.component';
import { ChannelBiofieldsComponent } from './sidebar-v2/biofields/biofields.component';
const routes: Routes = [
{ path: 'channels', redirectTo: '/newsfeed/global/top', pathMatch: 'full' },
......@@ -64,6 +66,8 @@ const routes: Routes = [
ExplicitOverlayComponent,
ChannelSortedComponent,
ChannelSortedModuleComponent,
ChannelStatsComponent,
ChannelBiofieldsComponent,
],
exports: [
ChannelModulesComponent,
......
......@@ -91,6 +91,7 @@ describe('ChannelFeed', () => {
impressions: 18200,
pinned_posts: ['a', 'b', 'c'],
mode: ChannelMode.PUBLIC,
nsfw: [],
};
comp.feed = [
{ guid: 'aaaa' },
......
<div class="m-channelStats">
<!-- <a-->
<!-- [routerLink]="['/', user.username, 'feed']"-->
<!-- class="mdl-tabs__tab mdl-color-text&#45;&#45;blue-grey-500"-->
<!-- *ngIf="user.activity_count"-->
<!-- >-->
<!-- <span i18n="Count of activities@@M__COMMON__FEED_COUNT">Feed</span>-->
<!-- <b>{{user.activity_count | number}}</b>-->
<!-- </a>-->
<a
*ngIf="user.supporters_count && session.getLoggedInUser().guid != user.guid"
>
<span class="m-channelStats__title" i18n>
Followers
</span>
<span class="m-channelStats__count">{{
user.supporters_count | abbr
}}</span>
</a>
<a
[routerLink]="['/', user.username, 'supporters']"
*ngIf="user.supporters_count && session.getLoggedInUser().guid == user.guid"
>
<span class="m-channelStats__title" i18n>
Followers
</span>
<span class="m-channelStats__count">{{
user.supporters_count | abbr
}}</span>
</a>
<a [routerLink]="['/', user.username, 'subscribers']">
<span class="m-channelStats__title" i18n>
Following
</span>
<span class="m-channelStats__count">{{
user.subscribers_count | abbr
}}</span>
</a>
<a>
<span class="m-channelStats__title" i18n>
Views
</span>
<span class="m-channelStats__count">{{ user.impressions | abbr }}</span>
</a>
</div>
......@@ -191,6 +191,7 @@ describe('ChannelSidebar', () => {
subscribers_count: 182,
impressions: 18200,
mode: ChannelMode.PUBLIC,
nsfw: [],
};
comp.editing = false;
uploadMock.response[`api/v1/channel/avatar`] = {
......
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.