...
 
Commits (52)
......@@ -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;
......@@ -103,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) => {
......@@ -112,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);
};
......@@ -132,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 =
......@@ -198,3 +205,5 @@ app.get('*', cache(), (req, res) => {
app.listen(PORT, () => {
console.log(`Node server listening on http://localhost:${PORT}`);
});
app.keepAliveTimeout = 65000;
......@@ -6,6 +6,10 @@ 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;
......@@ -15,6 +19,10 @@ PlotlyModule.plotlyjs = PlotlyJS;
providers: [
{ provide: 'ORIGIN_URL', useValue: location.origin },
{ provide: 'QUERY_STRING', useValue: location.search || '' },
{
provide: RedirectService,
useClass: BrowserRedirectService,
},
],
})
export class AppBrowserModule {}
......@@ -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;
}
}
}
......
......@@ -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],
})
......
......@@ -143,6 +143,7 @@ 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';
......@@ -438,28 +439,16 @@ const routes: Routes = [
},
{
provide: ConfigsService,
useFactory: (client, injector) =>
new ConfigsService(client, injector.get('QUERY_STRING')),
deps: [Client, Injector],
},
{
provide: MetaService,
useFactory: (
titleService,
metaService,
siteService,
location,
configsService
) =>
new MetaService(
titleService,
metaService,
siteService,
location,
configsService
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,
TopbarService,
......
......@@ -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>
......
......@@ -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
......
......@@ -165,7 +165,7 @@
</ul>
</nav>
<div class="m-sidebar--groups" *ngIf="session.isLoggedIn()">
<div class="m-sidebar--groups" [hidden]="!user">
<ng-template dynamic-host></ng-template>
</div>
</div>
......@@ -24,7 +24,8 @@ 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;
......@@ -36,6 +37,9 @@ export class SidebarNavigationComponent implements OnInit {
@HostBinding('class.m-sidebarNavigation--opened')
isOpened: boolean = false;
@HostBinding('hidden')
hidden: boolean = true;
constructor(
public navigation: NavigationService,
public session: Session,
......@@ -54,6 +58,18 @@ export class SidebarNavigationComponent implements OnInit {
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);
}
});
}
ngAfterViewInit() {
this.createGroupsSideBar();
}
......@@ -69,6 +85,8 @@ 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;
......@@ -81,6 +99,14 @@ export class SidebarNavigationComponent implements OnInit {
}
}
setVisible(value: boolean): void {
this.hidden = !value;
if (value) {
this.createGroupsSideBar();
}
}
@HostListener('window:resize')
onResize() {
if (window.innerWidth > 900) {
......
import { SidebarNavigationComponent } from './navigation.component';
import { EventEmitter } from '@angular/core';
export class SidebarNavigationService {
container: SidebarNavigationComponent;
visibleChange: EventEmitter<boolean> = new EventEmitter<boolean>();
setContainer(container: SidebarNavigationComponent): void {
this.container = container;
......@@ -12,4 +14,15 @@ export class SidebarNavigationService {
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);
}
}
}
......@@ -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 {
......
......@@ -140,6 +140,9 @@ export class V2TopbarComponent implements OnInit, OnDestroy {
if (this.timeout) {
clearTimeout(this.timeout);
}
if (this.router$) {
this.router$.unsubscribe();
}
}
private listen() {
......
......@@ -8,17 +8,21 @@
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">
<div
class="m-v3Topbar__leftColumn"
[class.m-v3Topbar__leftColumn__rightAlign]="!getCurrentUser()"
>
<nav class="m-v3Topbar__nav">
<a
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'"
......@@ -38,7 +42,7 @@
<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-v3Topbar__avatar"-->
<!-- *ngIf="getCurrentUser()"-->
......@@ -54,6 +58,27 @@
</div>
<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>
......
......@@ -31,6 +31,10 @@ m-v3topbar {
@media screen and(max-width: $m-grid-max-mobile) {
margin-left: 23px;
}
&.m-v3Topbar__leftColumn__rightAlign {
justify-content: flex-end;
}
}
.m-v3Topbar__middleColumn {
......@@ -88,6 +92,49 @@ m-v3topbar {
}
}
&.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%;
......@@ -136,6 +183,53 @@ m-v3topbar {
}
}
.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;
......
......@@ -17,6 +17,7 @@ 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-v3topbar',
......@@ -40,6 +41,10 @@ export class V3TopbarComponent implements OnInit, OnDestroy {
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,
......@@ -48,6 +53,7 @@ export class V3TopbarComponent implements OnInit, OnDestroy {
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');
......@@ -62,6 +68,8 @@ export class V3TopbarComponent implements OnInit, OnDestroy {
this.session.isLoggedIn(() => this.detectChanges());
this.topbarService.setContainer(this);
this.listen();
}
getCurrentUser() {
......@@ -107,6 +115,37 @@ 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
......@@ -142,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, Inject, Optional } 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 {
......@@ -9,7 +9,9 @@ export class ConfigsService {
constructor(
private client: Client,
@Inject('QUERY_STRING') private queryString: string
@Inject('QUERY_STRING') private queryString: string,
private redirectService: RedirectService,
private location: Location
) {}
async loadFromRemote() {
......@@ -17,6 +19,7 @@ export class ConfigsService {
this.configs = await this.client.get(
`api/v1/minds/config${this.queryString}`
);
this.redirectToRootIfInvalidDomain();
} catch (err) {
console.error(err);
}
......@@ -29,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);
}
......
@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',
})
......
......@@ -115,6 +115,7 @@ export interface MindsUser {
has_custom_background?: boolean;
};
mode: ChannelMode;
nsfw: Array<number>;
}
export interface MindsGroup {
......
......@@ -23,3 +23,7 @@ m-analytics {
}
}
}
.m-grid m-analytics .m-analytics__content {
margin: unset;
}
......@@ -5,7 +5,7 @@ m-login {
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
position: relative;
grid-area: content;
grid-area: content / span 2;
&:not(.m-login__newDesign) {
@include m-theme() {
......
......@@ -27,8 +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;
......
......@@ -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 {
......
......@@ -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;
......@@ -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);
}
}
}
}
......
<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'; else v1Channel">
<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 #v1Channel>
<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;
......
......@@ -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 },
......@@ -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 => {
......
......@@ -91,6 +91,7 @@ describe('ChannelFeed', () => {
impressions: 18200,
pinned_posts: ['a', 'b', 'c'],
mode: ChannelMode.PUBLIC,
nsfw: [],
};
comp.feed = [
{ guid: 'aaaa' },
......
......@@ -191,6 +191,7 @@ describe('ChannelSidebar', () => {
subscribers_count: 182,
impressions: 18200,
mode: ChannelMode.PUBLIC,
nsfw: [],
};
comp.editing = false;
uploadMock.response[`api/v1/channel/avatar`] = {
......
......@@ -10,7 +10,7 @@
<a
*ngIf="profile.key && profile.value"
[href]="getSocialProfileURL(profile.value)"
rel="noopener noreferrer"
rel="noopener nofollow ugc"
target="_blank"
><i [ngClass]="[ getSocialProfileIconClass(profile) ]"></i
></a>
......
......@@ -67,6 +67,7 @@ export class CommentPosterComponent {
async post(e) {
e.preventDefault();
this.attachment.resetPreviewRequests();
if (this.content.length > this.maxLength) {
return;
}
......
......@@ -2,7 +2,7 @@ import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { CommonModule as NgCommonModule } from '@angular/common';
import { CommonModule } from '../../common/common.module';
import { Client } from '../../common/api/client.service';
import { Storage } from '../../services/storage';
import { CookieService } from '../../common/services/cookie.service';
import { ExperimentDirective } from './experiment.directive';
import { ExperimentsService } from './experiments.service';
......@@ -14,9 +14,9 @@ import { ExperimentsService } from './experiments.service';
providers: [
{
provide: ExperimentsService,
useFactory: (_client, _storage) =>
new ExperimentsService(_client, _storage),
deps: [Client, Storage],
useFactory: (_client, cookieService) =>
new ExperimentsService(_client, cookieService),
deps: [Client, CookieService],
},
],
})
......
......@@ -89,7 +89,6 @@
i18n-placeholder="@@M__COMMON__USERNAME"
autocomplete="username"
(keydown.enter)="login(); $event.preventDefault();"
autofocus
/>
<div class="m-login__error" *ngIf="!!usernameError">
<ng-container
......
......@@ -39,7 +39,6 @@
readonly
onfocus="this.removeAttribute('readonly');"
[class.m-input--hide-placeholder]="showLabels"
autofocus
/>
<div
class="m-register__error"
......
......@@ -53,12 +53,22 @@ export class GroupsSidebarMarkersComponent
async ngOnInit() {
this.onResize();
if (isPlatformBrowser(this.platformId)) {
await this.load(true);
this.listenForMarkers();
this.listenForMembershipUpdates();
this.initialize();
this.session.getLoggedInUser(user => {
this.initialize();
});
} else {
this.inProgress = true; // Server side should start in loading spinner state
}
}
async initialize() {
await this.load(true);
this.listenForMarkers();
this.listenForMembershipUpdates();
}
/**
* Listens and responds to membership updates emited from groupsService.
*/
......@@ -68,6 +78,10 @@ export class GroupsSidebarMarkersComponent
return;
}
if (update.show) {
// if the group already exists in the list, don't re-add it
if (this.groups.findIndex(g => g.guid == update.guid) !== -1) {
return;
}
this.groupsService.load(update.guid).then(group => {
this.groups.unshift(group);
});
......
......@@ -38,7 +38,7 @@ export class SidebarSelectorComponent implements OnInit {
showAll: boolean = true;
loading: boolean;
showExtendedList: boolean = false;
showTrending: boolean = false;
showTrending: boolean = true;
constructor(
protected topbarHashtagsService: TopbarHashtagsService,
......
@import 'defaults';
m-helpdesk--dashboard {
grid-area: content / span 2;
}
.m-helpdesk__dashboard {
@include m-theme() {
background-color: themed($m-white);
......
......@@ -5,8 +5,6 @@
[showBottombar]="false"
[forceBackground]="false"
[class.m-homepage__formExperiment]="!!registerForm"
pageTitle="Minds Social Network"
i18n-pageTitle
>
<div class="m-marketing__main m-marketing__section--style-2">
<div class="m-grid m-marketing__wrapper">
......@@ -18,13 +16,11 @@
</h1>
<h2 ngPreserveWhitespaces i18n>
Take back control of your social media
{{ headline }}
</h2>
<p class="m-marketing__description" i18n>
A place to have open conversations and bring people together. Free
your mind and get paid for creating content, driving traffic and
referring friends.
{{ description }}
</p>
<button
......
@import 'defaults';
.m-grid m-homepage__v2 m-marketing {
margin-top: -75px;
padding-top: 75px;
}
m-homepage__v2 {
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
grid-area: content / span 2;
m-marketing {
position: relative;
margin-top: -52px;
......
import { Component, ViewChild } from '@angular/core';
import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { Client } from '../../services/api/client';
import { Router } from '@angular/router';
import { Navigation as NavigationService } from '../../services/navigation';
import { LoginReferrerService } from '../../services/login-referrer.service';
import { Session } from '../../services/session';
import { RegisterForm } from '../forms/register/register';
import { FeaturesService } from '../../services/features.service';
import { ConfigsService } from '../../common/services/configs.service';
import { OnboardingV2Service } from '../onboarding-v2/service/onboarding.service';
import { MetaService } from '../../common/services/meta.service';
import { TopbarService } from '../../common/layout/topbar.service';
import { SidebarNavigationService } from '../../common/layout/sidebar/navigation.service';
@Component({
selector: 'm-homepage__v2',
templateUrl: 'homepage-v2.component.html',
})
export class HomepageV2Component {
export class HomepageV2Component implements OnInit {
@ViewChild('registerForm', { static: false }) registerForm: RegisterForm;
readonly cdnAssetsUrl: string;
readonly siteUrl: string;
readonly headline = 'Take back control of your social media';
readonly description =
'A place to have open conversations and bring people together. Free your mind and get paid for creating content, driving traffic and referring friends.';
constructor(
public client: Client,
public router: Router,
public navigation: NavigationService,
public session: Session,
private loginReferrer: LoginReferrerService,
private featuresService: FeaturesService,
configs: ConfigsService,
private onboardingService: OnboardingV2Service
private onboardingService: OnboardingV2Service,
private metaService: MetaService,
private navigationService: SidebarNavigationService,
private topbarService: TopbarService
) {
this.cdnAssetsUrl = configs.get('cdn_assets_url');
this.siteUrl = configs.get('site_url');
......@@ -38,6 +45,14 @@ export class HomepageV2Component {
this.router.navigate(['/newsfeed']);
return;
}
this.metaService
.setTitle(`Minds - ${this.headline}`, false)
.setDescription(this.description)
.setCanonicalUrl('/')
.setOgUrl('/');
this.navigationService.setVisible(false);
this.topbarService.toggleMarketingPages(true, false, false);
}
registered() {
......
m-homepagecontainer {
display: contents;
}
.m-marketing-i18n {
.m-marketing--hero--video {
background: url('<%= APP_CDN %>/assets/photos/satellite.jpg');
background-position: -100px center;
}
m-plus--marketing {
grid-area: content / span 2;
.m-marketing-i18n {
.m-marketing--hero--video {
background: url('<%= APP_CDN %>/assets/photos/satellite.jpg');
background-position: -100px center;
}
.m-marketing--hero--actions {
.m-marketing--marketing-action-button {
a {
letter-spacing: 3px;
font-size: 18px;
line-height: 35px;
padding: 8px 24px;
font-weight: 300;
font-family: 'Roboto', Helvetica, sans-serif;
@include m-theme() {
color: themed($m-white);
.m-marketing--hero--actions {
.m-marketing--marketing-action-button {
a {
letter-spacing: 3px;
font-size: 18px;
line-height: 35px;
padding: 8px 24px;
font-weight: 300;
font-family: 'Roboto', Helvetica, sans-serif;
@include m-theme() {
color: themed($m-white);
}
}
}
}
......
import {
Component,
ChangeDetectionStrategy,
ChangeDetectorRef,
} from '@angular/core';
import { Client } from '../../common/api/client.service';
import { ChangeDetectionStrategy, Component } from '@angular/core';
@Component({
selector: 'm-plus--marketing',
......
.m-jobs--marketing {
.m-marketing--hero {
padding: 24px 0 !important;
.m-marketing--hero--inner {
padding-top: 52px;
padding-bottom: 52px;
flex-wrap: wrap;
m-jobs--marketing {
grid-area: content / span 2;
.m-jobs--marketing {
.m-marketing--hero {
padding: 24px 0 !important;
.m-marketing--hero--inner {
padding-top: 52px;
padding-bottom: 52px;
flex-wrap: wrap;
}
}
}
.m-marketing--section--subsection--first
.m-marketing--section--subsection-container {
padding-top: 32px !important;
}
.m-marketing--section--subsection--first
.m-marketing--section--subsection-container {
padding-top: 32px !important;
}
.m-marketing--hero--actions img {
max-height: 250px;
}
.m-marketing--hero--actions img {
max-height: 250px;
}
@media screen and (max-width: 662px) {
.m-marketing--hero--actions {
flex-basis: 100% !important;
margin-top: 16px;
@media screen and (max-width: 662px) {
.m-marketing--hero--actions {
flex-basis: 100% !important;
margin-top: 16px;
}
}
}
@media screen and (min-width: 500px) {
.m-marketing--hero--slogans h2 {
font-size: 62px !important;
line-height: 1.25;
@media screen and (min-width: 500px) {
.m-marketing--hero--slogans h2 {
font-size: 62px !important;
line-height: 1.25;
}
}
}
.m-job--marketing--list {
padding-left: 24px;
.m-job--marketing--list {
padding-left: 24px;
li {
font-size: 16px;
line-height: 2.5;
@include m-theme() {
color: themed($m-grey-600);
li {
font-size: 16px;
line-height: 2.5;
@include m-theme() {
color: themed($m-grey-600);
}
}
}
}
......
......@@ -21,6 +21,7 @@
>{{activity.ownerObj.name}}</strong
>
<m-channel--badges
data-nosnippet
class="m-channel--badges-activity"
[user]="activity.ownerObj"
badges="[ 'admin', 'verified' ]"
......
......@@ -272,8 +272,6 @@
class="mdl-cell mdl-cell--12-col m-media--description"
*ngIf="entity.description"
[innerHtml]="entity.description | safe"
[hidden]="!descriptionContainer.innerText.trim('')"
#descriptionContainer
></div>
</div>
......
......@@ -18,6 +18,9 @@ m-newsfeed {
}
}
.m-grid .m-newsfeed.mdl-grid {
margin: unset;
}
.m-newsfeed {
max-width: 1280px;
flex-wrap: nowrap;
......
......@@ -10,7 +10,10 @@ import { EntitiesService } from '../../../common/services/entities.service';
import { Client } from '../../../services/api/client';
import { FeaturesService } from '../../../services/features.service';
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({
......@@ -154,20 +157,38 @@ export class NewsfeedSingleComponent {
private updateMeta(): void {
const activity = this.activity.remind_object || this.activity;
const title: string =
activity.title ||
activity.message ||
`@${activity.ownerObj.username}'s post on Minds`;
let description: string;
if (title.length > 60) {
description = `...${title.substr(57)}`;
} else {
description = activity.blurb || '';
}
description += `. Subscribe to @${activity.ownerObj.username} on Minds`;
this.metaService
.setTitle(`@${activity.ownerObj.username} on Minds`)
.setDescription(
activity.title ||
activity.message ||
`Subscribe to @${activity.ownerObj.username} on Minds`
)
.setTitle(title)
.setDescription(description)
.setOgImage(
activity.custom_type === 'batch'
? activity.custom_data[0]['src']
: activity.thumbnail_src,
{ width: 2000, height: 1000 }
)
.setCanonicalUrl(`/newsfeed/${activity.guid}`)
.setRobots(
activity['thumbs:up:count'] >= MIN_METRIC_FOR_ROBOTS ? 'all' : 'noindex'
);
if (activity.nsfw.length) {
this.metaService.setNsfw(true);
}
if (activity.custom_type === 'video') {
this.metaService.setOgType('video');
this.metaService.setOgImage(activity.custom_data['thumbnail_src']);
......
......@@ -5,8 +5,18 @@ m-onboarding {
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
grid-area: content / span 2;
position: relative;
&.m-onboarding__newNavigation {
margin-top: -75px;
.m-grid {
margin-top: -75px;
padding-top: 75px;
}
}
@media screen and (max-width: $max-mobile) {
margin-bottom: 60px;
}
......@@ -383,6 +393,7 @@ m-onboarding {
line-height: 21px;
padding-left: 8px;
margin-bottom: 8px;
box-shadow: 0px 1px 4px 0px rgba(0, 0, 0, 0.1);
@include m-theme() {
color: themed($m-grey-800);
......@@ -405,6 +416,7 @@ m-onboarding {
flex-grow: 1;
display: flex;
margin-bottom: 8px;
box-shadow: 0px 1px 4px 0px rgba(0, 0, 0, 0.1);
.m-phone-input--wrapper {
justify-content: flex-start;
......@@ -421,6 +433,7 @@ m-onboarding {
font-size: 16px;
line-height: 21px;
box-shadow: none;
@include m-theme() {
color: themed($m-grey-800);
......
import { Component, HostBinding, OnDestroy } from '@angular/core';
import { Component, HostBinding, OnDestroy, OnInit } from '@angular/core';
import { Session } from '../../../services/session';
import { ActivatedRoute, Router } from '@angular/router';
import { Storage } from '../../../services/storage';
......@@ -6,12 +6,14 @@ import { OnboardingV2Service } from '../service/onboarding.service';
import { SidebarMarkersService } from '../../../common/layout/sidebar/markers.service';
import { iOSVersion } from '../../../helpers/is-safari';
import { TopbarService } from '../../../common/layout/topbar.service';
import { SidebarNavigationService } from '../../../common/layout/sidebar/navigation.service';
import { FeaturesService } from '../../../services/features.service';
@Component({
selector: 'm-onboarding',
templateUrl: 'onboarding.component.html',
})
export class OnboardingComponent implements OnDestroy {
export class OnboardingComponent implements OnInit, OnDestroy {
steps = [
{
name: 'Hashtags',
......@@ -21,14 +23,14 @@ export class OnboardingComponent implements OnDestroy {
name: 'Info',
selected: false,
},
{
name: 'Groups',
selected: false,
},
{
name: 'Channels',
selected: false,
},
// {
// name: 'Groups',
// selected: false,
// },
// {
// name: 'Channels',
// selected: false,
// },
];
showTitle: boolean = false;
shown: boolean = false;
......@@ -36,6 +38,9 @@ export class OnboardingComponent implements OnDestroy {
@HostBinding('class.m-onboarding__iosFallback')
iosFallback: boolean = false;
@HostBinding('class.m-onboarding__newNavigation')
newNavigation: boolean = false;
constructor(
private session: Session,
private router: Router,
......@@ -43,12 +48,16 @@ export class OnboardingComponent implements OnDestroy {
private route: ActivatedRoute,
private onboardingService: OnboardingV2Service,
private topbarService: TopbarService,
private sidebarMarkersService: SidebarMarkersService
) {
private navigationService: SidebarNavigationService,
private sidebarMarkersService: SidebarMarkersService,
private featuresService: FeaturesService
) {}
ngOnInit() {
this.iosFallback = iOSVersion() !== null;
route.url.subscribe(() => {
const section: string = route.snapshot.firstChild.routeConfig.path;
this.route.url.subscribe(() => {
const section: string = this.route.snapshot.firstChild.routeConfig.path;
if (section === 'notice') {
this.showTitle = false;
} else {
......@@ -70,12 +79,24 @@ export class OnboardingComponent implements OnDestroy {
return;
}
this.newNavigation = this.featuresService.has('navigation');
this.topbarService.toggleVisibility(false);
this.sidebarMarkersService.toggleVisibility(false);
if (this.newNavigation) {
this.navigationService.setVisible(false);
} else {
this.sidebarMarkersService.toggleVisibility(false);
}
}
ngOnDestroy() {
this.topbarService.toggleVisibility(true);
this.sidebarMarkersService.toggleVisibility(true);
if (this.newNavigation) {
this.navigationService.setVisible(true);
} else {
this.sidebarMarkersService.toggleVisibility(true);
}
}
}
......@@ -9,6 +9,6 @@ export class ChannelsStepComponent {
constructor(private router: Router) {}
finish() {
this.router.navigate(['/newsfeed']);
this.router.navigate(['/newsfeed/global/top']);
}
}
......@@ -4,6 +4,8 @@ m-onboarding__channelList {
.m-onboarding__channelList {
display: flex;
position: relative;
h3 {
margin: 0;
font-size: 13px;
......@@ -33,6 +35,8 @@ m-onboarding__channelList {
padding: 0;
list-style: none;
width: 100%;
max-height: 245px;
overflow-y: auto;
}
.m-channelList__item {
......@@ -43,6 +47,27 @@ m-onboarding__channelList {
border-bottom: 0;
}
&:nth-child(4) {
& ~ :last-child {
position: relative;
z-index: 1;
}
& ~ :nth-last-child(2):after {
content: '';
position: absolute;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(
to bottom,
rgba(255, 255, 255, 0) 0%,
#fff 100%
);
height: 65px;
}
}
a {
display: flex;
text-decoration: none;
......
......@@ -61,7 +61,7 @@ export class ChannelListComponent implements OnInit {
query,
nsfw,
})
.setLimit(3)
.setLimit(12)
.setExportUserCounts(true)
.fetch();
} catch (e) {
......
......@@ -4,6 +4,8 @@ m-onboarding__groupList {
.m-onboarding__groupList {
display: flex;
position: relative;
h3 {
margin: 0;
font-size: 13px;
......@@ -33,6 +35,8 @@ m-onboarding__groupList {
padding: 0;
list-style: none;
width: 100%;
max-height: 245px;
overflow-y: auto;
}
.m-groupList__item {
......@@ -43,6 +47,27 @@ m-onboarding__groupList {
border-bottom: 0;
}
&:nth-child(4) {
& ~ :last-child {
position: relative;
z-index: 1;
}
& ~ :nth-last-child(2):after {
content: '';
position: absolute;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(
to bottom,
rgba(255, 255, 255, 0) 0%,
#fff 100%
);
height: 65px;
}
}
a {
display: flex;
flex-grow: 1;
......
......@@ -61,7 +61,7 @@ export class GroupListComponent implements OnInit {
query,
nsfw,
})
.setLimit(3)
.setLimit(12)
.setCastToActivities(true)
.fetch();
} catch (e) {
......
import { Component, OnInit } from '@angular/core';
import { TopbarHashtagsService } from '../../../hashtags/service/topbar.service';
import { Router } from '@angular/router';
import { Storage } from '../../../../services/storage';
type Hashtag = {
value: string;
......@@ -16,9 +17,14 @@ export class HashtagsStepComponent implements OnInit {
error: string;
inProgress: boolean;
constructor(private service: TopbarHashtagsService, private router: Router) {}
constructor(
private service: TopbarHashtagsService,
private storage: Storage,
private router: Router
) {}
ngOnInit() {
this.storage.set('preferred_hashtag_state', '1'); // turn on preferred hashtags for discovery
this.load();
}
......
......@@ -70,7 +70,7 @@
Skip
</button>
<button class="mf-button mf-button--alt" (click)="continue()" i18n>
Continue
Finish
</button>
</div>
</div>
......@@ -24,6 +24,7 @@ export class InfoStepComponent {
locationError: string;
date: string;
dateOfBirthError: string;
dateOfBirthChanged: boolean = false;
cities: Array<any> = [];
......@@ -86,6 +87,9 @@ export class InfoStepComponent {
}
async updateDateOfBirth() {
if (!this.dateOfBirthChanged) {
return true;
}
this.dateOfBirthError = null;
try {
......@@ -102,6 +106,7 @@ export class InfoStepComponent {
selectedDateChange(date: string) {
this.date = date;
this.dateOfBirthChanged = true;
}
cancel() {
......@@ -112,12 +117,12 @@ export class InfoStepComponent {
}
skip() {
this.router.navigate(['/onboarding', 'groups']);
this.router.navigate(['/newsfeed']);
}
continue() {
if (this.saveData()) {
this.router.navigate(['/onboarding', 'groups']);
this.router.navigate(['/newsfeed']);
}
}
......
......@@ -28,7 +28,7 @@ export class NoticeStepComponent implements OnInit {
}
skip() {
this.router.navigate(['/newsfeed']);
this.router.navigate(['/newsfeed/global/top']);
}
isMobile() {
......
@import '../../foundation/grid-values';
.m-plus__marketing {
.m-plusMarketing__verify {
margin-top: 50px;
m-plus__marketing {
grid-area: content / span 2;
@include m-theme() {
color: themed($m-black);
}
.m-plus__marketing {
.m-plusMarketing__verify {
margin-top: 50px;
@media screen and (max-width: $m-grid-min-vp) {
margin-top: 30px;
text-align: center;
}
@include m-theme() {
color: themed($m-black);
}
> * {
vertical-align: middle;
color: inherit;
}
@media screen and (max-width: $m-grid-min-vp) {
margin-top: 30px;
text-align: center;
}
> i.material-icons {
margin-right: 0.35em;
> * {
vertical-align: middle;
color: inherit;
}
&.m-plusMarketingVerify__verifiedIcon {
@include m-theme() {
color: themed($m-blue);
> i.material-icons {
margin-right: 0.35em;
&.m-plusMarketingVerify__verifiedIcon {
@include m-theme() {
color: themed($m-blue);
}
}
}
&.m-plusMarketingVerify__unverifiedIcon {
opacity: 0.2;
&.m-plusMarketingVerify__unverifiedIcon {
opacity: 0.2;
}
}
}
> a {
border-bottom: 1px solid;
cursor: pointer;
> a {
border-bottom: 1px solid;
cursor: pointer;
@include m-theme() {
border-color: themed($m-grey-400);
@include m-theme() {
border-color: themed($m-grey-400);
}
}
}
}
......
.m-pro__marketing {
.m-marketing--hero--slogans ul {
font-size: 20px;
padding: 0 0 0 16px;
margin: 32px 0;
m-pro__marketing {
grid-area: content / span 2;
@include m-theme() {
color: themed($m-white);
}
.m-pro__marketing {
.m-marketing--hero--slogans ul {
font-size: 20px;
padding: 0 0 0 16px;
margin: 32px 0;
@include m-theme() {
color: themed($m-white);
}
li {
margin-bottom: 8px;
li {
margin-bottom: 8px;
&:last-child {
margin-bottom: 0;
&:last-child {
margin-bottom: 0;
}
}
}
}
......
.m-reports__marketing {
.m-marketing__hero {
padding-bottom: 72px;
@include m-theme() {
background-color: themed($m-blue-grey-800) !important;
}
.m-marketingHero__inner {
padding-top: 0;
padding-bottom: 0;
}
m-reports__marketing {
grid-area: content / span 2;
.m-btn {
margin-top: 32px;
.m-reports__marketing {
.m-marketing__hero {
padding-bottom: 72px;
@include m-theme() {
color: themed($m-white);
border-color: themed($m-white);
background-color: themed($m-blue-grey-800) !important;
}
}
}
.m-marketingHero__actions {
i {
font-size: 170px;
text-align: center;
width: 100%;
@include m-theme() {
color: themed($m-white);
.m-marketingHero__inner {
padding-top: 0;
padding-bottom: 0;
}
.m-btn {
margin-top: 32px;
@include m-theme() {
color: themed($m-white);
border-color: themed($m-white);
}
}
}
}
}
.m-reportMarketing__stats,
.m-reportMarketing__section {
padding: 32px 100px 32px 52px !important;
h4 {
margin: 0;
margin-bottom: 32px;
font-size: 16px;
letter-spacing: 2px;
text-transform: uppercase;
font-weight: 600;
@include m-theme() {
color: themed($m-grey-600);
.m-marketingHero__actions {
i {
font-size: 170px;
text-align: center;
width: 100%;
@include m-theme() {
color: themed($m-white);
}
}
}
}
.m-layout__row {
flex-wrap: wrap;
}
.m-reportMarketing__stats,
.m-reportMarketing__section {
padding: 32px 100px 32px 52px !important;
.m-layout__cell {
text-align: center;
@media screen and (max-width: 900px) {
padding: 32px;
h4 {
margin: 0;
margin-bottom: 32px;
font-size: 16px;
letter-spacing: 2px;
text-transform: uppercase;
font-weight: 600;
@include m-theme() {
color: themed($m-grey-600);
}
}
}
b {
font-size: 72px;
line-height: 1;
display: block;
@include m-theme() {
color: themed($m-grey-800);
.m-layout__row {
flex-wrap: wrap;
}
}
span {
font-size: 24px;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
font-weight: 500;
margin-top: 16px;
display: block;
@include m-theme() {
color: themed($m-grey-700);
.m-layout__cell {
text-align: center;
@media screen and (max-width: 900px) {
padding: 32px;
}
}
}
}
.m-reportMarketing__list {
list-style: decimal;
font-size: 24px;
font-weight: 800;
@include m-theme() {
color: themed($m-grey-200);
}
li {
padding: 16px;
b {
font-size: 72px;
line-height: 1;
display: block;
@include m-theme() {
color: themed($m-grey-800);
}
}
p {
span {
font-size: 24px;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
font-weight: 500;
margin-top: 16px;
display: block;
@include m-theme() {
color: themed($m-grey-700);
}
}
}
}
.m-reportMarketingList__sublist {
display: inline-flex;
flex-wrap: wrap;
max-width: 740px;
li {
width: 50%;
font-weight: 400;
font-size: 16px;
line-height: 24px;
cursor: default;
padding: 0;
.m-reportMarketing__list {
list-style: decimal;
font-size: 24px;
font-weight: 800;
@include m-theme() {
color: themed($m-grey-400);
color: themed($m-grey-200);
}
@media screen and (max-width: 890px) {
width: 100%;
li {
padding: 16px;
p {
font-size: 24px;
font-weight: 500;
@include m-theme() {
color: themed($m-grey-700);
}
}
}
}
.m-reportMarketingList__sublist {
display: inline-flex;
flex-wrap: wrap;
max-width: 740px;
li {
width: 50%;
font-weight: 400;
font-size: 16px;
line-height: 24px;
cursor: default;
padding: 0;
@include m-theme() {
color: themed($m-grey-400);
}
@media screen and (max-width: 890px) {
width: 100%;
}
}
}
}
......@@ -5,6 +5,10 @@ m-settings {
min-width: 0;
}
.m-grid .m-settings--wrapper {
margin: unset;
}
.m-settings--wrapper {
max-width: 1280px;
margin: auto;
......
......@@ -2,14 +2,16 @@ import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
Inject,
PLATFORM_ID,
} from '@angular/core';
import { Router } from '@angular/router';
import { isPlatformServer } from '@angular/common';
import { Client } from '../../../../services/api/client';
import { Session } from '../../../../services/session';
@Component({
moduleId: module.id,
selector: 'm-wallet-token--contributions',
templateUrl: 'contributions.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
......@@ -25,7 +27,8 @@ export class WalletTokenContributionsComponent {
protected client: Client,
protected cd: ChangeDetectorRef,
public session: Session,
protected router: Router
protected router: Router,
@Inject(PLATFORM_ID) protected platformId: Object
) {}
ngOnInit() {
......@@ -45,6 +48,10 @@ export class WalletTokenContributionsComponent {
return;
}
if (isPlatformServer(this.platformId)) {
return;
}
if (refresh) {
this.contributions = [];
}
......
......@@ -5,6 +5,9 @@ m-wallet {
min-width: 0;
}
.m-grid .m-wallet--wrapper {
margin: unset;
}
.m-wallet--wrapper {
max-width: 1280px;
margin: auto;
......
m-pay__marketing {
grid-area: content / span 2;
}
......@@ -15,8 +15,9 @@
</span>
</m-announcement>
<m-blockchain--wallet-address-notice></m-blockchain--wallet-address-notice>
<ng-container *mIfFeature="'navigation'; else oldLayout">
<div class="m-grid">
<div class="m-grid" [class.m-grid__noSidebar]="!isSidebarVisible">
<m-sidebar--navigation></m-sidebar--navigation>
<router-outlet></router-outlet>
</div>
......@@ -50,5 +51,3 @@
></m-modal-signup>
<m-channel--onboarding *ngIf="showOnboarding"></m-channel--onboarding>
<m-cookies-notice *ngIf="!session.isLoggedIn()"></m-cookies-notice>
......@@ -2,8 +2,7 @@ import { Component, OnInit } from '@angular/core';
import { Session } from '../services/session';
import { FeaturesService } from '../services/features.service';
import { SiteService } from '../common/services/site.service';
import { ConfigsService } from '../common/services/configs.service';
import { ChannelOnboardingService } from '../modules/onboarding/channel/onboarding.service';
import { SidebarNavigationService } from '../common/layout/sidebar/navigation.service';
@Component({
selector: 'm-page',
......@@ -14,44 +13,22 @@ export class PageComponent implements OnInit {
showOnboarding: boolean = false;
isSidebarVisible: boolean = true;
constructor(
public session: Session,
public featuresService: FeaturesService,
public onboardingService: ChannelOnboardingService,
private site: SiteService,
private configs: ConfigsService
private navigationService: SidebarNavigationService,
private site: SiteService
) {}
ngOnInit() {
this.useNewNavigation = this.featuresService.has('navigation');
}
async initialize() {
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');
if (user.language !== language) {
console.log('[app]:: language change', user.language, language);
window.location.reload(true);
}
}
});
this.onboardingService.onClose.subscribe(() => {
this.showOnboarding = false;
});
this.onboardingService.onOpen.subscribe(async () => {
this.showOnboarding = await this.onboardingService.showModal(true);
this.navigationService.visibleChange.subscribe((visible: boolean) => {
this.isSidebarVisible = visible;
});
}
get isProDomain() {
return this.site.isProDomain;
}
......
......@@ -173,4 +173,38 @@ describe('Service: Attachment Service', () => {
tick(1000);
expect(clientMock.get).toHaveBeenCalledTimes(1);
}));
it('should populate the request array', fakeAsync(() => {
spyOn(service, 'addPreviewRequest');
service.preview('https://github.com/releases');
tick(1000);
expect(service.addPreviewRequest).toHaveBeenCalledTimes(1);
}));
it('should check the request array on response', fakeAsync(() => {
spyOn(service, 'getPreviewRequests');
service.preview('https://github.com/releases');
tick(1000);
expect(service.getPreviewRequests).toHaveBeenCalledTimes(1);
}));
it('should reset the request array when called', fakeAsync(() => {
service.addPreviewRequest('https://github.com/releases');
expect(service.getPreviewRequests().length).toBe(1);
service.resetPreviewRequests();
tick(1000);
expect(service.getPreviewRequests().length).toBe(0);
}));
it('should discard changes if request array has been cleared', fakeAsync(() => {
service.preview('https://github.com/releases');
tick(1000);
expect(this.meta).toBeFalsy();
}));
});
......@@ -33,6 +33,7 @@ export class AttachmentService {
private pendingDelete: boolean = false;
private xhr: XMLHttpRequest = null;
private previewRequests: string[] = [];
constructor(
public session: Session,
......@@ -356,7 +357,37 @@ export class AttachmentService {
this.meta.description = '';
}
preview(content: string, detectChangesFn?: Function) {
/**
* Resets preview requests to null.
*/
resetPreviewRequests(): AttachmentService {
this.previewRequests = [];
return this;
}
/**
* Returns preview requests.
*/
getPreviewRequests(): string[] {
return this.previewRequests;
}
/**
* Adds a new preview request.
* @param { string } url -
*/
addPreviewRequest(url: string): AttachmentService {
this.previewRequests.push(url);
return this;
}
/**
* Gets attachment preview from content.
* @param { string } content - Content to be parsed for preview URL.
* @param { Function } detectChangesFn - Function to be ran on change emission.
* @returns void.
*/
preview(content: string, detectChangesFn?: Function): void {
let match = content.match(/(\b(https?|ftp|file):\/\/[^\s\]\)]+)/gi),
url;
......@@ -389,6 +420,7 @@ export class AttachmentService {
}
this.attachment.richUrl = url;
this.addPreviewRequest(url);
if (detectChangesFn) detectChangesFn();
......@@ -401,7 +433,7 @@ export class AttachmentService {
this.clientService
.get('api/v1/newsfeed/preview', { url })
.then((data: any) => {
if (!data) {
if (!data || this.getPreviewRequests().length < 1) {
this.resetRich();
if (detectChangesFn) detectChangesFn();
return;
......
......@@ -50,6 +50,7 @@ import { ConfigsService } from '../common/services/configs.service';
import { TransferHttpInterceptorService } from './transfer-http-interceptor.service';
import { CookieHttpInterceptorService } from './api/cookie-http-interceptor.service';
import { CookieService } from '../common/services/cookie.service';
import { RedirectService } from '../common/services/redirect.service';
export const MINDS_PROVIDERS: any[] = [
SiteService,
......@@ -186,9 +187,14 @@ export const MINDS_PROVIDERS: any[] = [
},
{
provide: ConfigsService,
useFactory: (client, injector) =>
new ConfigsService(client, injector.get('QUERY_STRING')),
deps: [Client, Injector],
useFactory: (client, injector, redirect, location) =>
new ConfigsService(
client,
injector.get('QUERY_STRING'),
redirect,
location
),
deps: [Client, Injector, RedirectService, Location],
},
{
provide: FeaturesService,
......
......@@ -24,6 +24,19 @@
sizes="16x16"
href="/en/assets/logos/bulb-16x16.png"
/>
<link
rel="preload"
href="https://cdn-assets.minds.com/front/fonts/material-icons.css"
as="style"
crossorigin
/>
<link
rel="preload"
href="https://cdn-assets.minds.com/front/fonts/material-icons.woff2"
as="font"
type="font/woff2"
crossorigin
/>
<title>Minds</title>
</head>
......
......@@ -19,6 +19,7 @@
"baseUrl": "./",
"paths": {
"fs": [ "./shims/noop" ],
"bn.js": [ "../node_modules/bn.js/lib/bn.js" ]
}
}
}
\ No newline at end of file
}