...
 
Commits (60)
......@@ -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 {}
<ng-container *ngIf="ready">
<ng-container *ngIf="!isProDomain">
<ng-container *ngIf="useNewNavigation; else v2Topbar">
<m-v3topbar>
<ng-container search>
<m-search--bar
[defaultSizes]="false"
[showCleanIcon]="true"
></m-search--bar>
</ng-container>
</m-v3topbar>
</ng-container>
<ng-template #v2Topbar>
<m-v2-topbar>
<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>
<m-sidebar--markers
[class.has-v2-navbar]="featuresService.has('top-feeds')"
></m-sidebar--markers>
</ng-template>
</ng-container>
<m-body
[class.has-markers-sidebar]="hasMarkersSidebar()"
[class.has-v2-navbar]="featuresService.has('top-feeds')"
[class.has-v3-navbar]="featuresService.has('navigation-2020')"
[class.is-pro-domain]="isProDomain"
>
<m-emailConfirmation *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>
<m-topbarwrapper></m-topbarwrapper>
</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',
......@@ -187,14 +169,6 @@ export class Minds implements OnInit, OnDestroy {
this.socketsService.setUp();
}
hasMarkersSidebar() {
return (
this.session.isLoggedIn() &&
!this.isProDomain &&
!this.featuresService.has('navigation-2020')
);
}
ngOnDestroy() {
this.loginReferrer.unlisten();
this.scrollToTop.unlisten();
......
......@@ -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],
})
......
......@@ -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,5 +165,7 @@
</ul>
</nav>
<ng-template dynamic-host></ng-template>
<div class="m-sidebar--groups" [hidden]="!user">
<ng-template dynamic-host></ng-template>
</div>
</div>
......@@ -145,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;
}
}
}
......@@ -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);
}
}
}
......@@ -14,7 +14,7 @@ export class TopbarService {
}
constructor(private featuresService: FeaturesService) {
this.useV3Topbar = this.featuresService.has('navigation-2020');
this.useV3Topbar = this.featuresService.has('navigation');
}
setContainer(container: V2TopbarComponent | V3TopbarComponent) {
......
......@@ -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%;
......@@ -138,6 +185,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';
minds-admin {
grid-area: content / span 2;
min-width: 0;
}
minds-admin-boosts {
minds-button-comment {
display: none !important;
......
@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 {
......
......@@ -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,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() {
......
......@@ -101,7 +101,7 @@ describe('LoginComponent', () => {
jasmine.clock().install();
featuresServiceMock.mock('register_pages-december-2019', false);
featuresServiceMock.mock('navigation-2020', false);
featuresServiceMock.mock('navigation', false);
fixture = TestBed.createComponent(LoginComponent);
......
......@@ -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;
......@@ -70,7 +72,7 @@ export class LoginComponent implements OnInit, OnDestroy {
}
this.newDesign = this.featuresService.has('register_pages-december-2019');
this.newNavigation = this.featuresService.has('navigation-2020');
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 {
......
......@@ -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-2020'; 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;
......@@ -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 },
......@@ -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-channelBiofields">
<!-- [ngClass]="{'m-channelBiofields&#45;&#45;flex': !editing}"-->
<!-- City / Location -->
<div
class="m-channelBiofields__field m-channelBiofields__city"
[hidden]="editing || !user.city"
>
<i class="material-icons">location_on</i>
<span>{{ user.city }}</span>
</div>
<div class="m-channelBiofields__editor" *ngIf="editing && isOwner()">
<div class="m-channelBiofields__cityInput">
<i class="material-icons">location_on</i>
<input
(keyup)="findCity(user.city)"
name="city"
[(ngModel)]="user.city"
class=""
placeholder="Enter your city..."
i18n-placeholder="@@M__COMMON__ENTER_CITY"
/>
</div>
<div
class="m-channelBiofields__cityAutocomplete"
*ngIf="editing && cities.length > 0"
>
<p i18n="@@M__COMMON__SELECT_CITY">
Select your city:
</p>
<li
(click)="setCity(c)"
*ngFor="let c of cities"
[hidden]="!(c.address.town || c.address.city)"
>
{{ c.address.town }}{{ c.address.city }}, {{ c.address.state }}
</li>
</div>
</div>
<!-- END City / Location -->
<!-- <div-->
<!-- class="m-channelBiofields__field"-->
<!-- *ngIf="user.tags && !editing"-->
<!-- >-->
<!-- <div-->
<!-- class="m-channelBiofields__field__tags-container"-->
<!-- [hidden]="editing || !user.tags"-->
<!-- >-->
<!-- <i class="material-icons">local_offer</i>-->
<!-- <span *ngFor="let tag of user.tags">#{{tag}}</span>-->
<!-- </div>-->
<!-- </div>-->
<!-- <div-->
<!-- class="m-channel-bio-editor m-channel-city-editor"-->
<!-- *ngIf="editing && isOwner()"-->
<!-- >-->
<!-- <div class="m-channel-bio-input">-->
<!-- <i class="material-icons">local_offer</i>-->
<!-- <m-hashtags-selector-->
<!-- #hashtagsSelector-->
<!-- [alignLeft]="true"-->
<!-- [tags]="user.tags"-->
<!-- (tagsChange)="onTagsChange($event)"-->
<!-- (tagsAdded)="onTagsAdded($event)"-->
<!-- (tagsRemoved)="onTagsRemoved($event)"-->
<!-- >-->
<!-- </m-hashtags-selector>-->
<!-- </div>-->
<!-- <div-->
<!-- class="mdl-card mdl-shadow&#45;&#45;2dp"-->
<!-- style="min-height:0;"-->
<!-- *ngIf="errorMessage"-->
<!-- >-->
<!-- <div class="mdl-card__supporting-text">-->
<!-- {{errorMessage}}-->
<!-- </div>-->
<!-- </div>-->
<!-- </div>-->
<!-- <m-channel&#45;&#45;social-profiles-->
<!-- [user]="user"-->
<!-- [editing]="editing && isOwner()"-->
<!-- (changed)="setSocialProfile($event)"-->
<!-- ></m-channel&#45;&#45;social-profiles>-->
</div>
<div class="m-channelSidebar__briefDescription">
<span [innerHtml]="user.briefdescription | tags" [hidden]="editing"> </span>
<div *ngIf="editing && isOwner()">
<!-- OWNER EDIT FORM -->
<textarea
[autoGrow]
class="mdl-textfield__input"
type="text"
name="briefdescription"
[(ngModel)]="user.briefdescription"
></textarea>
</div>
</div>
m-channelbiofields {
.m-channelBiofields {
margin: 7px 0;
//&.m-channelBiofields--flex {
// display: flex;
// flex-flow: row wrap;
// align-items: center;
// margin: auto;
// justify-content: center;
//}
.m-channelBiofields__field {
@include m-theme() {
color: themed($m-black);
}
}
.m-channelBiofields__city {
i.material-icons {
font-size: 22px;
line-height: 30px;
vertical-align: middle;
margin-right: 10px;
}
span {
font-size: 14px;
line-height: 30px;
}
}
.m-channelBiofields__editor {
}
.m-channelBiofields__cityInput {
display: flex;
input {
border: 0;
padding: 0 8px;
}
}
.m-channelBiofields__cityAutocomplete {
}
}
.m-channelSidebar__briefDescription {
margin: 7px 0;
span {
white-space: pre-line;
text-align: center;
}
textarea {
box-sizing: border-box;
padding: 8px;
text-align: left;
width: 100%;
border: 0 !important;
}
}
}
import { Component, Input, OnInit } from '@angular/core';
import { MindsUser } from '../../../../interfaces/entities';
import { Session } from '../../../../services/session';
import { Tag } from '../../../hashtags/types/tag';
import { Client } from '../../../../services/api/client';
@Component({
selector: 'm-channelbiofields',
templateUrl: 'biofields.component.html',
})
export class ChannelBiofieldsComponent implements OnInit {
@Input() user: MindsUser;
@Input() editing: boolean;
searching;
amountOfTags: number = 0;
tooManyTags: boolean = false;
errorMessage: string = '';
// @todo make a re-usable city selection module to avoid duplication here
cities: Array<any> = [];
constructor(public session: Session, public client: Client) {}
ngOnInit() {}
isOwner() {
return this.session.getLoggedInUser().guid === this.user.guid;
}
findCity(q: string) {
if (this.searching) {
clearTimeout(this.searching);
}
this.searching = setTimeout(() => {
this.client
.get('api/v1/geolocation/list', { q: q })
.then((response: any) => {
this.cities = response.results;
});
}, 100);
}
setCity(row: any) {
this.cities = [];
if (row.address.city) {
this.user.city = row.address.city;
}
if (row.address.town) {
this.user.city = row.address.town;
}
if (window.Minds) {
window.Minds.user.city = this.user.city;
}
this.client.post('api/v1/channel/info', {
coordinates: row.lat + ',' + row.lon,
city: window.Minds.user.city,
});
}
onTagsChange(tags: string[]) {
this.amountOfTags = tags.length;
if (this.amountOfTags > 5) {
this.errorMessage = 'You can only select up to 5 hashtags';
this.tooManyTags = true;
} else {
this.tooManyTags = false;
this.user.tags = tags;
if (this.errorMessage === 'You can only select up to 5 hashtags') {
this.errorMessage = '';
}
}
}
onTagsAdded(tags: Tag[]) {}
onTagsRemoved(tags: Tag[]) {}
setSocialProfile(value: any) {
this.user.social_profiles = value;
}
}
<div class="m-channelSidebar__buttons">
<button class="m-channelSidebar__message" i18n>
<i class="material-icons">sentiment_satisfied_alt</i>
<button class="m-channelSidebar__messageButton" i18n>
<img [src]="cdnAssetsUrl + 'assets/icons/wire.svg'" />
<span>Message</span>
</button>
<button class="m-channelSidebar__pay" i18n>
<i class="material-icons">sentiment_satisfied_alt</i>
<button class="m-channelSidebar__payButton" i18n>
<img [src]="cdnAssetsUrl + 'assets/icons/wire.svg'" />
<span>Pay</span>
</button>
</div>
<div class="m-channelSidebar__stats">
<!-- <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-channelSidebarStats__title" i18n>
Followers
</span>
<span class="m-channelSidebarStats__count">{{
user.supporters_count | abbr
}}</span>
</a>
<a
[routerLink]="['/', user.username, 'supporters']"
*ngIf="user.supporters_count && session.getLoggedInUser().guid == user.guid"
>
<span class="m-channelSidebarStats__title" i18n>
Followers
</span>
<span class="m-channelSidebarStats__count">{{
user.supporters_count | abbr
}}</span>
</a>
<a [routerLink]="['/', user.username, 'subscribers']">
<span class="m-channelSidebarStats__title" i18n>
Following
</span>
<span class="m-channelSidebarStats__count">{{
user.subscribers_count | abbr
}}</span>
</a>
<a>
<span class="m-channelSidebarStats__title" i18n>
Views
</span>
<span class="m-channelSidebarStats__count">{{
user.impressions | abbr
}}</span>
</a>
</div>
<m-channelstats [user]="user"></m-channelstats>
<div class="m-channelSidebar__bioFields">
<!-- [ngClass]="{'m-channelSidebar__bioFields&#45;&#45;flex': !editing}"-->
<!-- City / Location -->
<div
class="m-channelSidebar__bioField m-channelSidebar__city"
[hidden]="editing || !user.city"
>
<i class="material-icons">location_on</i>
<span>{{ user.city }}</span>
</div>
<m-channelbiofields [user]="user" [editing]="editing"></m-channelbiofields>
<div
class="m-channel-bio-editor m-channel-city-editor"
*ngIf="editing && isOwner()"
>
<div class="m-channel-bio-input m-channel-city-input">
<i class="material-icons">location_on</i>
<input
(keyup)="findCity(user.city)"
name="city"
[(ngModel)]="user.city"
class=""
placeholder="Enter your city..."
i18n-placeholder="@@M__COMMON__ENTER_CITY"
/>
</div>
<div
class="m-discovery-cities mdl-card m-border"
*ngIf="editing && cities.length > 0"
>
<p i18n="@@M__COMMON__SELECT_CITY">
Select your city:
</p>
<li
(click)="setCity(c)"
*ngFor="let c of cities"
[hidden]="!(c.address.town || c.address.city)"
>
{{ c.address.town }}{{ c.address.city }}, {{ c.address.state }}
</li>
</div>
</div>
<!-- END City / Location -->
<m-channels--sorted-module
title="Images"
i18n-title
type="images"
[channel]="user"
[linksTo]="['/', user.username, 'images']"
[size]="8"
*mIfBrowser
></m-channels--sorted-module>
<!-- <div-->
<!-- class="m-channelSidebar__bioField"-->
<!-- *ngIf="user.tags && !editing"-->
<!-- >-->
<!-- <div-->
<!-- class="m-channelSidebar__bioField__tags-container"-->
<!-- [hidden]="editing || !user.tags"-->
<!-- >-->
<!-- <i class="material-icons">local_offer</i>-->
<!-- <span *ngFor="let tag of user.tags">#{{tag}}</span>-->
<!-- </div>-->
<!-- </div>-->
<m-channels--sorted-module
title="Videos"
i18n-title
type="videos"
[channel]="user"
[linksTo]="['/', user.username, 'videos']"
[size]="8"
*mIfBrowser
></m-channels--sorted-module>
<!-- <div-->
<!-- class="m-channel-bio-editor m-channel-city-editor"-->
<!-- *ngIf="editing && isOwner()"-->
<!-- >-->
<!-- <div class="m-channel-bio-input">-->
<!-- <i class="material-icons">local_offer</i>-->
<!-- <m-hashtags-selector-->
<!-- #hashtagsSelector-->
<!-- [alignLeft]="true"-->
<!-- [tags]="user.tags"-->
<!-- (tagsChange)="onTagsChange($event)"-->
<!-- (tagsAdded)="onTagsAdded($event)"-->
<!-- (tagsRemoved)="onTagsRemoved($event)"-->
<!-- >-->
<!-- </m-hashtags-selector>-->
<!-- </div>-->
<!-- <div-->
<!-- class="mdl-card mdl-shadow&#45;&#45;2dp"-->
<!-- style="min-height:0;"-->
<!-- *ngIf="errorMessage"-->
<!-- >-->
<!-- <div class="mdl-card__supporting-text">-->
<!-- {{errorMessage}}-->
<!-- </div>-->
<!-- </div>-->
<!-- </div>-->
<button (click)="changeEditing.emit(true)">toggle editing</button>
<!-- <m-channel&#45;&#45;social-profiles-->
<!-- [user]="user"-->
<!-- [editing]="editing && isOwner()"-->
<!-- (changed)="setSocialProfile($event)"-->
<!-- ></m-channel&#45;&#45;social-profiles>-->
</div>
<m-wire-channel
*mIfBrowser
#wire
[(rewards)]="user.wire_rewards"
[channel]="user"
[editing]="editing"
[channelV2Design]="true"
></m-wire-channel>
m-channel__sidebarv2 {
.m-channelSidebar__buttons {
display: flex;
margin: 7px 0;
button:first-child {
margin-right: 47px;
......@@ -15,64 +16,77 @@ m-channel__sidebarv2 {
@include m-theme() {
color: themed($m-black);
}
img {
height: 14px;
margin-right: 8px;
}
}
}
.m-channelSidebar__stats {
display: flex;
align-items: center;
justify-content: space-between;
a {
text-decoration: none;
display: flex;
flex-direction: column;
height: 60px;
m-channels--sorted-module {
background-color: transparent !important;
border: 0 !important;
margin: 7px 0;
max-width: 245px;
@include m-theme() {
color: themed($m-black);
}
.m-channels--sorted-module__Title {
padding: 0;
span {
.mdl-card__title-text {
font-size: 14px;
line-height: 30px;
font-weight: bold;
margin-bottom: 9px;
}
}
&.m-channelSidebarStats__title {
font-size: 13px;
}
.m-channels--sorted-module__Tiles {
> a {
height: 45px;
width: 45px;
flex-grow: 0;
flex-shrink: 0;
padding: 0;
margin-right: 15px;
margin-bottom: 15px;
box-sizing: content-box;
&.m-channelSidebarStats__count {
font-size: 18px;
&.m-mature-module-thumbnail {
i.material-icons {
font-size: 2em !important;
}
}
}
}
}
.m-channelSidebar__bioFields {
//&.m-channelSidebar__bioFields--flex {
// display: flex;
// flex-flow: row wrap;
// align-items: center;
// margin: auto;
// justify-content: center;
//}
.m-channels--sorted-module__viewMore {
padding: 0 0 16px;
}
}
.m-channelSidebar__bioField {
@include m-theme() {
color: themed($m-black);
}
m-wire-channel {
.mdl-card {
background-color: transparent !important;
box-shadow: none !important;
}
.mdl-card__title {
padding: 8px 0;
}
.m-channelSidebar__city {
i.material-icons {
font-size: 22px;
line-height: 30px;
vertical-align: middle;
margin-right: 10px;
.m-wire-channel--rewards {
//padding: 0 16px;
.m-wire-channel--description {
padding: 16px 0;
}
span {
font-size: 14px;
line-height: 30px;
m-wire-channel-table {
table {
td {
padding: 8px 0;
}
}
}
}
}
......
......@@ -3,83 +3,28 @@ import { MindsUser } from '../../../interfaces/entities';
import { Session } from '../../../services/session';
import { Tag } from '../../hashtags/types/tag';
import { Client } from '../../../services/api/client';
import { ConfigsService } from '../../../common/services/configs.service';
@Component({
selector: 'm-channel__sidebarv2',
templateUrl: 'sidebar-v2.component.html',
})
export class ChannelSidebarV2Component implements OnInit {
minds = window.Minds;
export class ChannelSidebarV2Component {
readonly cdnAssetsUrl;
@Input() user: MindsUser;
@Input() editing: boolean = false;
@Output() changeEditing = new EventEmitter<boolean>();
searching;
errorMessage: string = '';
amountOfTags: number = 0;
tooManyTags: boolean = false;
// @todo make a re-usable city selection module to avoid duplication here
cities: Array<any> = [];
constructor(public client: Client, public session: Session) {}
ngOnInit() {}
constructor(
public client: Client,
public session: Session,
private configs: ConfigsService
) {
this.cdnAssetsUrl = this.configs.get('cdn_assets_url');
}
isOwner() {
return this.session.getLoggedInUser().guid === this.user.guid;
}
findCity(q: string) {
if (this.searching) {
clearTimeout(this.searching);
}
this.searching = setTimeout(() => {
this.client
.get('api/v1/geolocation/list', { q: q })
.then((response: any) => {
this.cities = response.results;
});
}, 100);
}
setCity(row: any) {
this.cities = [];
if (row.address.city) {
this.user.city = row.address.city;
}
if (row.address.town) {
this.user.city = row.address.town;
}
if (window.Minds) {
window.Minds.user.city = this.user.city;
}
this.client.post('api/v1/channel/info', {
coordinates: row.lat + ',' + row.lon,
city: window.Minds.user.city,
});
}
onTagsChange(tags: string[]) {
this.amountOfTags = tags.length;
if (this.amountOfTags > 5) {
this.errorMessage = 'You can only select up to 5 hashtags';
this.tooManyTags = true;
} else {
this.tooManyTags = false;
this.user.tags = tags;
if (this.errorMessage === 'You can only select up to 5 hashtags') {
this.errorMessage = '';
}
}
}
onTagsAdded(tags: Tag[]) {}
onTagsRemoved(tags: Tag[]) {}
setSocialProfile(value: any) {
this.user.social_profiles = value;
}
}
<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>
m-channelstats {
.m-channelStats {
display: flex;
align-items: center;
justify-content: space-between;
margin: 7px 0;
a {
text-decoration: none;
display: flex;
flex-direction: column;
height: 60px;
@include m-theme() {
color: themed($m-black);
}
span {
line-height: 30px;
&.m-channelStats__title {
font-size: 13px;
}
&.m-channelStats__count {
font-size: 18px;
}
}
}
}
}
import { Component, Input } from '@angular/core';
import { MindsUser } from '../../../../interfaces/entities';
import { Session } from '../../../../services/session';
@Component({
selector: 'm-channelstats',
templateUrl: 'channel-stats.component.html',
})
export class ChannelStatsComponent {
@Input() user: MindsUser;
constructor(public session: Session) {}
}
......@@ -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>
......
......@@ -40,7 +40,7 @@
></div>
<a
class="mdl-card__supporting-text mdl-color-text--grey-600"
class="mdl-card__supporting-text mdl-color-text--grey-600 m-channels--sorted-module__viewMore"
[routerLink]="linksTo"
>
<ng-container i18n>View More</ng-container>
......
......@@ -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>
......
@import 'defaults';
m-newsfeed {
grid-area: content;
grid-area: content / span 2;
min-width: 0;
}
......@@ -18,6 +18,9 @@ m-newsfeed {
}
}
.m-grid .m-newsfeed.mdl-grid {
margin: unset;
}
.m-newsfeed {
max-width: 1280px;
flex-wrap: nowrap;
......
......@@ -122,7 +122,7 @@ describe('NewsfeedComponent', () => {
featuresServiceMock.mock('top-feeds', false);
featuresServiceMock.mock('suggested-users', false);
featuresServiceMock.mock('pro', false);
featuresServiceMock.mock('navigation-2020', false);
featuresServiceMock.mock('navigation', false);
sessionMock.user.admin = false;
sessionMock.loggedIn = true;
......
......@@ -82,7 +82,7 @@ export class NewsfeedComponent implements OnInit, OnDestroy {
protected newsfeedService: NewsfeedService,
protected newsfeedHashtagSelectorService: NewsfeedHashtagSelectorService
) {
this.newNavigation = this.featuresService.has('navigation-2020');
this.newNavigation = this.featuresService.has('navigation');
this.urlSubscription = this.route.url.subscribe(() => {
this.tag = null;
......
......@@ -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']);
......
......@@ -48,6 +48,9 @@ minds-notification.mdl-card {
}
minds-notifications {
grid-area: content/span 2;
min-width: 0;
.m-notifications--load-new {
text-transform: uppercase;
font-family: 'Roboto', Helvetica, sans-serif;
......
......@@ -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) {
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.