...
 
Commits (24)
......@@ -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
......
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', () => {
......
This diff is collapsed.
......@@ -120,13 +120,13 @@ const cache = () => {
(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);
};
......
......@@ -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 {}
......@@ -60,6 +60,4 @@
></m-modal-signup>
<m-channel--onboarding *ngIf="showOnboarding"></m-channel--onboarding>
<m-cookies-notice *ngIf="!session.isLoggedIn()"> </m-cookies-notice>
</ng-container>
......@@ -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],
})
......
......@@ -144,6 +144,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';
const routes: Routes = [
{
......@@ -432,9 +433,14 @@ const routes: Routes = [
},
{
provide: ConfigsService,
useFactory: (client, injector) =>
new ConfigsService(client, injector.get('QUERY_STRING')),
deps: [Client, Injector],
useFactory: (client, injector, redirect, location) =>
new ConfigsService(
client,
injector.get('QUERY_STRING'),
redirect,
location
),
deps: [Client, Injector, RedirectService, Location],
},
{
provide: MetaService,
......
......@@ -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());
}
......
......@@ -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
......
......@@ -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 { 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();
}
}
......@@ -21,7 +21,7 @@ import { WireModule } from '../wire/wire.module';
import { CommentsModule } from '../comments/comments.module';
import { HashtagsModule } from '../hashtags/hashtags.module';
import { CanDeactivateGuardService } from '../../services/can-deactivate-guard';
import { CKEditorModule } from '@ckeditor/ckeditor5-angular';
const CKEditorModule = require('@ckeditor/ckeditor5-angular');
import { BlogEditComponent } from './ckeditor/edit/edit.component';
import { BlogEditorComponent } from './ckeditor/editor/editor.component';
......@@ -36,11 +36,6 @@ const routes: Routes = [
title: 'Edit Blog',
},
},
{
path: 'blog-v2/edit/:guid',
component: BlogEditComponent,
canDeactivate: [CanDeactivateGuardService],
},
{ path: 'blog/:filter', component: BlogListComponent },
{ path: 'blog', redirectTo: '/blog/top', pathMatch: 'full' },
{ path: ':username/blog/:slugid', component: BlogViewInfinite },
......
<div class="m-ck-editor-wrapper">
<ckeditor
*mIfBrowser
[ngModel]="content"
(ngModelChange)="onContentChanged($event)"
[editor]="Editor"
......
......@@ -3,9 +3,17 @@
* @desc Wrapper for CKEditor5 text editor.
*/
import { Component, Input, Output, EventEmitter } from '@angular/core';
import * as BalloonEditor from '@ckeditor/ckeditor5-build-balloon';
import {
Component,
Input,
Output,
EventEmitter,
Inject,
PLATFORM_ID,
} from '@angular/core';
const CKEditorModule = require('@ckeditor/ckeditor5-angular');
import { isPlatformBrowser } from '@angular/common';
import { isPlatformServer } from '@angular/common';
@Component({
selector: 'm-blog__editor',
......@@ -18,7 +26,7 @@ export class BlogEditorComponent {
@Input() content: string;
@Output() contentChanged: EventEmitter<Event> = new EventEmitter<Event>();
public Editor: BalloonEditor = BalloonEditor;
Editor;
// TODO: Manually adjust configuration when custom built.
editorConfig: Object = {
......@@ -43,6 +51,19 @@ export class BlogEditorComponent {
],
*/
};
constructor(@Inject(PLATFORM_ID) protected platformId: Object) {}
ngOnInit() {
if (isPlatformBrowser(this.platformId)) {
// only executed on the browser side
const BalloonEditor = require('@ckeditor/ckeditor5-build-balloon');
this.Editor = BalloonEditor;
this.Editor.defaultConfig = this.editorConfig;
}
if (isPlatformServer(this.platformId)) {
// only executed on the server side
}
}
/**
* Called on content change. Emits current content value.
......
......@@ -47,13 +47,23 @@
</div>
<div class="mdl-cell mdl-cell--12-col minds-blog-descriptions">
<m-inline-editor
<!-- <m-inline-editor
name="description"
[(ngModel)]="blog.description"
placeholder="Go ahead and write some content!"
i18n-placeholder="@@BLOGS__EDIT__INLINE_EDITOR_PLACEHOLDER"
#inlineEditor
></m-inline-editor>
-->
<ng-container *mIfBrowser>
<m-blog__editor
(contentChanged)="onContentChange($event)"
[content]="blog.description"
placeholder="Go ahead and write some content!"
i18n-placeholder="@@BLOGS__EDIT__INLINE_EDITOR_PLACEHOLDER"
#inlineEditor
></m-blog__editor>
</ng-container>
</div>
<div class="mdl-cell mdl-cell--12-col">
......
......@@ -210,6 +210,10 @@ export class BlogEdit {
onTagsRemoved(tags: Tag[]) {}
onContentChange(val) {
this.blog.description = val;
}
validate() {
this.error = '';
......@@ -354,3 +358,342 @@ export class BlogEdit {
this.blog.nsfw = nsfw.map(reason => reason.value);
}
}
export class BlogEditComponent {
readonly cdnUrl: string;
guid: string;
blog: any = {
guid: 'new',
title: '',
description: '<p><br></p>',
time_created: Math.floor(Date.now() / 1000),
access_id: 2,
tags: [],
nsfw: [],
license: 'attribution-sharealike-cc',
fileKey: 'header',
mature: 0,
monetized: 0,
published: 0,
wire_threshold: null,
custom_meta: {
title: '',
description: '',
author: '',
},
slug: '',
};
banner: any;
banner_top: number = 0;
banner_prompt: boolean = false;
editing: boolean = true;
canSave: boolean = true;
inProgress: boolean = false;
validThreshold: boolean = true;
error: string = '';
pendingUploads: string[] = [];
categories: { id; label; selected }[];
licenses = LICENSES;
access = ACCESS;
existingBanner: boolean;
paramsSubscription: Subscription;
@ViewChild('inlineEditor', { static: false })
inlineEditor: InlineEditorComponent;
@ViewChild('thresholdInput', { static: false })
thresholdInput: WireThresholdInputComponent;
@ViewChild('hashtagsSelector', { static: false })
hashtagsSelector: HashtagsSelectorComponent;
protected time_created: any;
constructor(
public session: Session,
public client: Client,
public upload: Upload,
public router: Router,
public route: ActivatedRoute,
protected inMemoryStorageService: InMemoryStorageService,
private dialogService: DialogService,
configs: ConfigsService
) {
this.cdnUrl = configs.get('cdn_url');
window.addEventListener(
'attachment-preview-loaded',
(event: CustomEvent) => {
this.pendingUploads.push(event.detail.timestamp);
}
);
window.addEventListener(
'attachment-upload-finished',
(event: CustomEvent) => {
this.pendingUploads.splice(
this.pendingUploads.findIndex(value => {
return value === event.detail.timestamp;
}),
1
);
}
);
}
ngOnInit() {
if (!this.session.isLoggedIn()) {
this.router.navigate(['/login']);
return;
}
this.paramsSubscription = this.route.params.subscribe(params => {
if (params['guid']) {
this.guid = params['guid'];
this.blog = {
guid: 'new',
title: '',
description: '<p><br></p>',
access_id: 2,
category: '',
license: '',
fileKey: 'header',
mature: 0,
nsfw: [],
monetized: 0,
published: 0,
wire_threshold: null,
custom_meta: {
title: '',
description: '',
author: '',
},
slug: '',
tags: [],
};
this.banner = void 0;
this.banner_top = 0;
this.banner_prompt = false;
this.editing = true;
this.canSave = true;
this.existingBanner = false;
if (this.guid !== 'new') {
this.editing = true;
this.load();
} else {
this.editing = false;
const description: string = this.inMemoryStorageService.once(
'newBlogContent'
);
if (description) {
let htmlDescription = description
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/\n+/g, '</p><p>');
this.blog.description = `<p>${htmlDescription}</p>`;
}
}
}
});
}
onContentChange(val) {
this.blog.description = val;
}
canDeactivate(): Observable<boolean> | boolean {
if (!this.editing || !this.session.getLoggedInUser()) {
return true;
}
return this.dialogService.confirm('Discard changes?');
}
ngOnDestroy() {
if (this.paramsSubscription) {
this.paramsSubscription.unsubscribe();
}
}
load() {
this.client.get('api/v1/blog/' + this.guid, {}).then((response: any) => {
if (response.blog) {
this.blog = response.blog;
this.guid = response.blog.guid;
if (this.blog.thumbnail_src) this.existingBanner = true;
//this.hashtagsSelector.setTags(this.blog.tags);
// draft
if (!this.blog.published && response.blog.draft_access_id) {
this.blog.access_id = response.blog.draft_access_id;
}
if (!this.blog.category) this.blog.category = '';
if (!this.blog.license) this.blog.license = '';
this.blog.time_created =
response.blog.time_created || Math.floor(Date.now() / 1000);
}
});
}
onTagsChange(tags: string[]) {
this.blog.tags = tags;
}
onTagsAdded(tags: Tag[]) {}
onTagsRemoved(tags: Tag[]) {}
validate() {
this.error = '';
if (!this.blog.description) {
this.error = 'error:no-description';
return false;
}
if (!this.blog.title) {
this.error = 'error:no-title';
return false;
}
return true;
}
posterDateSelectorError(msg) {
this.error = msg;
}
save() {
if (!this.canSave) return;
if (!this.validate()) return;
this.error = '';
// this.inlineEditor.prepareForSave().then(() => {
const blog = Object.assign({}, this.blog);
blog.editor_version = 2;
// only allowed props
blog.nsfw = this.blog.nsfw;
blog.mature = blog.mature ? 1 : 0;
blog.monetization = blog.monetization ? 1 : 0;
blog.monetized = blog.monetized ? 1 : 0;
blog.time_created = blog.time_created || Math.floor(Date.now() / 1000);
this.editing = false;
this.inProgress = true;
this.canSave = false;
this.check_for_banner()
.then(() => {
this.upload
.post('api/v1/blog/' + this.guid, [this.banner], blog)
.then((response: any) => {
this.inProgress = false;
this.canSave = true;
this.blog.time_created = null;
if (response.status !== 'success') {
this.error = response.message;
return;
}
this.router.navigate(
response.route
? ['/' + response.route]
: ['/blog/view', response.guid]
);
})
.catch(e => {
console.error(e);
this.error = e;
this.canSave = true;
this.inProgress = false;
});
})
.catch(e => {
console.error(e);
this.error = 'error:no-banner';
this.inProgress = false;
this.canSave = true;
});
// });
}
add_banner(banner: any) {
this.banner = banner.file;
this.blog.header_top = banner.top;
}
//this is a nasty hack because people don't want to click save on a banner ;@
check_for_banner() {
if (!this.banner) this.banner_prompt = true;
return new Promise((resolve, reject) => {
if (this.banner) return resolve(true);
setTimeout(() => {
this.banner_prompt = false;
if (this.banner || this.existingBanner) return resolve(true);
else return reject(false);
}, 100);
});
}
toggleMonetized() {
if (this.blog.mature) {
return;
}
this.blog.monetized = this.blog.monetized ? 0 : 1;
}
checkMonetized() {
if (this.blog.mature) {
this.blog.monetized = 0;
}
}
onCategoryClick(category) {
category.selected = !category.selected;
if (!this.blog.hasOwnProperty('categories') || !this.blog.categories) {
this.blog['categories'] = [];
}
if (category.selected) {
this.blog.categories.push(category.id);
} else {
this.blog.categories.splice(this.blog.categories.indexOf(category.id), 1);
}
}
onTimeCreatedChange(newDate) {
this.blog.time_created = newDate;
}
getTimeCreated() {
return this.blog.time_created > Math.floor(Date.now() / 1000)
? this.blog.time_created
: null;
}
checkTimePublished() {
return (
!this.blog.time_published ||
this.blog.time_published > Math.floor(Date.now() / 1000)
);
}
/**
* Sets this blog NSFW
* @param { array } nsfw - Numerical indexes for reasons in an array e.g. [1, 2].
*/
onNSFWSelections(nsfw) {
this.blog.nsfw = nsfw.map(reason => reason.value);
}
}
......@@ -176,13 +176,13 @@ export class BlogView implements OnInit, OnDestroy {
menuOptionSelected(option: string) {
switch (option) {
case 'edit':
if (
this.featuresService.has('ckeditor5') &&
Number(this.blog.editor_version) === 2
) {
this.router.navigate(['/blog-v2/edit', this.blog.guid]);
break;
}
// if (
// this.featuresService.has('ckeditor5') &&
// Number(this.blog.editor_version) === 2
// ) {
// this.router.navigate(['/blog-v2/edit', this.blog.guid]);
// break;
// }
this.router.navigate(['/blog/edit', this.blog.guid]);
break;
case 'delete':
......
......@@ -70,6 +70,9 @@ export class ChannelComponent {
ngOnInit() {
this.updateMeta();
if (this.user) {
this.clientMetaService.recordView(this.user);
}
this.context.set('activity');
this.onScroll();
......@@ -171,6 +174,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 => {
......
......@@ -10,7 +10,7 @@
<a
*ngIf="profile.key && profile.value"
[href]="getSocialProfileURL(profile.value)"
rel="noopener noreferrer"
rel="noopener nofollow ugc"
target="_blank"
><i [ngClass]="[ getSocialProfileIconClass(profile) ]"></i
></a>
......
......@@ -67,6 +67,7 @@ export class CommentPosterComponent {
async post(e) {
e.preventDefault();
this.attachment.resetPreviewRequests();
if (this.content.length > this.maxLength) {
return;
}
......
......@@ -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"
......
......@@ -4,6 +4,8 @@ import {
ViewChild,
ChangeDetectorRef,
HostListener,
OnInit,
OnDestroy,
Inject,
PLATFORM_ID,
} from '@angular/core';
......@@ -20,7 +22,7 @@ import { GroupsService } from '../groups-service';
selector: 'm-group--sidebar-markers',
templateUrl: 'sidebar-markers.component.html',
})
export class GroupsSidebarMarkersComponent {
export class GroupsSidebarMarkersComponent implements OnInit, OnDestroy {
inProgress: boolean = false;
$updateMarker;
markers = [];
......@@ -58,6 +60,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,
......
......@@ -383,6 +383,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 +406,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 +423,7 @@ m-onboarding {
font-size: 16px;
line-height: 21px;
box-shadow: none;
@include m-theme() {
color: themed($m-grey-800);
......
......@@ -21,14 +21,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;
......
......@@ -9,6 +9,6 @@ export class ChannelsStepComponent {
constructor(private router: Router) {}
finish() {
this.router.navigate(['/newsfeed']);
this.router.navigate(['/newsfeed/global/top']);
}
}
......@@ -4,6 +4,8 @@ m-onboarding__channelList {
.m-onboarding__channelList {
display: flex;
position: relative;
h3 {
margin: 0;
font-size: 13px;
......@@ -33,6 +35,8 @@ m-onboarding__channelList {
padding: 0;
list-style: none;
width: 100%;
max-height: 245px;
overflow-y: auto;
}
.m-channelList__item {
......@@ -43,6 +47,27 @@ m-onboarding__channelList {
border-bottom: 0;
}
&:nth-child(4) {
& ~ :last-child {
position: relative;
z-index: 1;
}
& ~ :nth-last-child(2):after {
content: '';
position: absolute;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(
to bottom,
rgba(255, 255, 255, 0) 0%,
#fff 100%
);
height: 65px;
}
}
a {
display: flex;
text-decoration: none;
......
......@@ -61,7 +61,7 @@ export class ChannelListComponent implements OnInit {
query,
nsfw,
})
.setLimit(3)
.setLimit(12)
.setExportUserCounts(true)
.fetch();
} catch (e) {
......
......@@ -4,6 +4,8 @@ m-onboarding__groupList {
.m-onboarding__groupList {
display: flex;
position: relative;
h3 {
margin: 0;
font-size: 13px;
......@@ -33,6 +35,8 @@ m-onboarding__groupList {
padding: 0;
list-style: none;
width: 100%;
max-height: 245px;
overflow-y: auto;
}
.m-groupList__item {
......@@ -43,6 +47,27 @@ m-onboarding__groupList {
border-bottom: 0;
}
&:nth-child(4) {
& ~ :last-child {
position: relative;
z-index: 1;
}
& ~ :nth-last-child(2):after {
content: '';
position: absolute;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(
to bottom,
rgba(255, 255, 255, 0) 0%,
#fff 100%
);
height: 65px;
}
}
a {
display: flex;
flex-grow: 1;
......
......@@ -61,7 +61,7 @@ export class GroupListComponent implements OnInit {
query,
nsfw,
})
.setLimit(3)
.setLimit(12)
.setCastToActivities(true)
.fetch();
} catch (e) {
......
import { Component, OnInit } from '@angular/core';
import { TopbarHashtagsService } from '../../../hashtags/service/topbar.service';
import { Router } from '@angular/router';
import { Storage } from '../../../../services/storage';
type Hashtag = {
value: string;
......@@ -16,9 +17,14 @@ export class HashtagsStepComponent implements OnInit {
error: string;
inProgress: boolean;
constructor(private service: TopbarHashtagsService, private router: Router) {}
constructor(
private service: TopbarHashtagsService,
private storage: Storage,
private router: Router
) {}
ngOnInit() {
this.storage.set('preferred_hashtag_state', '1'); // turn on preferred hashtags for discovery
this.load();
}
......
......@@ -70,7 +70,7 @@
Skip
</button>
<button class="mf-button mf-button--alt" (click)="continue()" i18n>
Continue
Finish
</button>
</div>
</div>
......@@ -24,6 +24,7 @@ export class InfoStepComponent {
locationError: string;
date: string;
dateOfBirthError: string;
dateOfBirthChanged: boolean = false;
cities: Array<any> = [];
......@@ -86,6 +87,9 @@ export class InfoStepComponent {
}
async updateDateOfBirth() {
if (!this.dateOfBirthChanged) {
return true;
}
this.dateOfBirthError = null;
try {
......@@ -102,6 +106,7 @@ export class InfoStepComponent {
selectedDateChange(date: string) {
this.date = date;
this.dateOfBirthChanged = true;
}
cancel() {
......@@ -112,12 +117,12 @@ export class InfoStepComponent {
}
skip() {
this.router.navigate(['/onboarding', 'groups']);
this.router.navigate(['/newsfeed']);
}
continue() {
if (this.saveData()) {
this.router.navigate(['/onboarding', 'groups']);
this.router.navigate(['/newsfeed']);
}
}
......
......@@ -28,7 +28,7 @@ export class NoticeStepComponent implements OnInit {
}
skip() {
this.router.navigate(['/newsfeed']);
this.router.navigate(['/newsfeed/global/top']);
}
isMobile() {
......
......@@ -173,4 +173,38 @@ describe('Service: Attachment Service', () => {
tick(1000);
expect(clientMock.get).toHaveBeenCalledTimes(1);
}));
it('should populate the request array', fakeAsync(() => {
spyOn(service, 'addPreviewRequest');
service.preview('https://github.com/releases');
tick(1000);
expect(service.addPreviewRequest).toHaveBeenCalledTimes(1);
}));
it('should check the request array on response', fakeAsync(() => {
spyOn(service, 'getPreviewRequests');
service.preview('https://github.com/releases');
tick(1000);
expect(service.getPreviewRequests).toHaveBeenCalledTimes(1);
}));
it('should reset the request array when called', fakeAsync(() => {
service.addPreviewRequest('https://github.com/releases');
expect(service.getPreviewRequests().length).toBe(1);
service.resetPreviewRequests();
tick(1000);
expect(service.getPreviewRequests().length).toBe(0);
}));
it('should discard changes if request array has been cleared', fakeAsync(() => {
service.preview('https://github.com/releases');
tick(1000);
expect(this.meta).toBeFalsy();
}));
});
......@@ -33,6 +33,7 @@ export class AttachmentService {
private pendingDelete: boolean = false;
private xhr: XMLHttpRequest = null;
private previewRequests: string[] = [];
constructor(
public session: Session,
......@@ -356,7 +357,37 @@ export class AttachmentService {
this.meta.description = '';
}
preview(content: string, detectChangesFn?: Function) {
/**
* Resets preview requests to null.
*/
resetPreviewRequests(): AttachmentService {
this.previewRequests = [];
return this;
}
/**
* Returns preview requests.
*/
getPreviewRequests(): string[] {
return this.previewRequests;
}
/**
* Adds a new preview request.
* @param { string } url -
*/
addPreviewRequest(url: string): AttachmentService {
this.previewRequests.push(url);
return this;
}
/**
* Gets attachment preview from content.
* @param { string } content - Content to be parsed for preview URL.
* @param { Function } detectChangesFn - Function to be ran on change emission.
* @returns void.
*/
preview(content: string, detectChangesFn?: Function): void {
let match = content.match(/(\b(https?|ftp|file):\/\/[^\s\]\)]+)/gi),
url;
......@@ -389,6 +420,7 @@ export class AttachmentService {
}
this.attachment.richUrl = url;
this.addPreviewRequest(url);
if (detectChangesFn) detectChangesFn();
......@@ -401,7 +433,7 @@ export class AttachmentService {
this.clientService
.get('api/v1/newsfeed/preview', { url })
.then((data: any) => {
if (!data) {
if (!data || this.getPreviewRequests().length < 1) {
this.resetRich();
if (detectChangesFn) detectChangesFn();
return;
......
......@@ -50,6 +50,7 @@ import { ConfigsService } from '../common/services/configs.service';
import { TransferHttpInterceptorService } from './transfer-http-interceptor.service';
import { CookieHttpInterceptorService } from './api/cookie-http-interceptor.service';
import { CookieService } from '../common/services/cookie.service';
import { RedirectService } from '../common/services/redirect.service';
export const MINDS_PROVIDERS: any[] = [
SiteService,
......@@ -186,9 +187,14 @@ export const MINDS_PROVIDERS: any[] = [
},
{
provide: ConfigsService,
useFactory: (client, injector) =>
new ConfigsService(client, injector.get('QUERY_STRING')),
deps: [Client, Injector],
useFactory: (client, injector, redirect, location) =>
new ConfigsService(
client,
injector.get('QUERY_STRING'),
redirect,
location
),
deps: [Client, Injector, RedirectService, Location],
},
{
provide: FeaturesService,
......
......@@ -24,6 +24,13 @@
sizes="16x16"
href="/en/assets/logos/bulb-16x16.png"
/>
<link
rel="preload"
href="https://cdn-assets.minds.com/front/fonts/material-icons.woff2"
as="font"
type="font/woff2"
crossorigin
/>
<title>Minds</title>
</head>
......
......@@ -19,6 +19,7 @@
"baseUrl": "./",
"paths": {
"fs": [ "./shims/noop" ],
"bn.js": [ "../node_modules/bn.js/lib/bn.js" ]
}
}
}
\ No newline at end of file
}