...
 
Commits (15)
......@@ -38,3 +38,4 @@ cypress/videos
!/.drone.yml
!/.gitlab
!/.githooks
!/.prettierrc
......@@ -120,6 +120,107 @@ context('Blogs', () => {
cy.get('m-post-menu button.minds-more').click();
cy.get('m-post-menu ul.minds-dropdown-menu li').contains('Delete').click();
cy.get('m-post-menu m-modal-confirm .mdl-button--colored').click();
});
})
it('should be able to create a new scheduled blog', () => {
// upload avatar first
cy.visit(`/${Cypress.env().username}`);
cy.get('.m-channel--name .minds-button-edit button:first-child').click();
cy.wait(100);
cy.uploadFile('.minds-avatar input[type=file]', '../fixtures/avatar.jpeg', 'image/jpg');
cy.get('.m-channel--name .minds-button-edit button:last-child').click();
// create blog
cy.visit('/blog/edit/new');
cy.uploadFile('minds-banner #file', '../fixtures/international-space-station-1776401_1920.jpg', 'image/jpg');
cy.get('minds-textarea .m-editor').type('Title');
cy.get('m-inline-editor .medium-editor-element').type('Content\n');
// click on plus button
cy.get('.medium-editor-element > .medium-insert-buttons > button.medium-insert-buttons-show').click();
// click on camera
cy.get('ul.medium-insert-buttons-addons > li > button.medium-insert-action:first-child').contains('photo_camera').click();
// upload the image
cy.uploadFile('.medium-media-file-input', '../fixtures/international-space-station-1776401_1920.jpg', 'image/jpg');
// open license dropdown & select first license
cy.get('.m-license-info select').select('All rights reserved');
// click on hashtags dropdown
cy.get('.m-category-info m-hashtags-selector .m-dropdown--label-container').click();
// select #ART
cy.get('.m-category-info m-dropdown m-form-tags-input > div > span').contains('#art').click();
// type in another hashtag manually
cy.get('.m-category-info m-hashtags-selector m-form-tags-input input').type('hashtag{enter}').click();
// click away
cy.get('.m-category-info m-hashtags-selector .minds-bg-overlay').click();
// select visibility
cy.get('.m-visibility-info select').select('Loggedin');
// open metadata form
cy.get('.m-blog-edit--toggle-wrapper .m-blog-edit--toggle').contains('Metadata').click();
// set url slug
cy.get('.m-blog-edit--field input[name=slug]').type('123');
// set meta title
cy.get('.m-blog-edit--field input[name=custom_meta_title]').type('Test');
// set meta description
cy.get('.m-blog-edit--field textarea[name=custom_meta_description]').type('This is a test blog');
// set meta author
cy.get('.m-blog-edit--field input[name=custom_meta_author]').type('Minds Test');
// set as nsfw
cy.get('.m-mature-info a').click();
cy.get('.m-mature-info a span').contains('Mature content');
// set scheduled date
cy.get('.m-poster-date-selector__input').click();
cy.get('td.c-datepicker__day-body.c-datepicker__day--selected + td').click();
cy.get('a.c-btn.c-btn--flat.js-ok').click();
// get setted date to compare
let scheduledDate;
cy.get('div.m-poster-date-selector__input div.m-tooltip--bubble')
.invoke('text').then((text) => {
scheduledDate = text;
});
cy.wait(1000);
cy.get('.m-button--submit').click({ force: true }); // TODO: Investigate why disabled flag is being detected
cy.location('pathname', { timeout: 30000 })
.should('contains', `/${Cypress.env().username}/blog`);
cy.get('.m-blog--title').contains('Title');
cy.get('.minds-blog-body p').contains('Content');
cy.get('.m-license-info span').contains('all-rights-reserved');
cy.wait(1000);
// compare setted date with time_created
cy.get('div.m-blog-container div.mdl-grid div.minds-body span')
.invoke('text').then((text) => {
const time_created = new Date(text).getTime();
scheduledDate = new Date(scheduledDate).getTime();
expect(scheduledDate).to.equal(time_created);
});
// cleanup
//open dropdown
cy.get('m-post-menu button.minds-more').click();
cy.get('m-post-menu ul.minds-dropdown-menu li').contains('Delete').click();
cy.get('m-post-menu m-modal-confirm .mdl-button--colored').click();
});
})
/**
* @author Ben Hayward
* @desc Spec tests for comment threads.
*/
import generateRandomId from '../support/utilities';
context('Messenger', () => {
const targetUser = 'minds';
const messagePassword = 'Passw0rd!';
const messageContent = 'this is a test message!';
const undecryptedMessage = ''
const testUsername = generateRandomId();
const testPassword = generateRandomId()+'X#';
const openMessenger = '.m-messenger--dockpane-tab';
const userSearch = '.m-messenger--userlist-search > input[type=text]';
const userList = (i) => `.m-messenger--userlist-conversations > div:nth-child(${i}) > span.m-conversation-label`;
const passwordInput = (i) => `input[type=password]:nth-child(${i})`;
const submitPassword = 'm-messenger--encryption > div > button';
const messageInput = '.m-messenger--conversation-composer > textarea';
const sendButton = '[data-cy=data-minds-conversation-send]';
const messageBubble = '.m-messenger--conversation-message-bubble';
const settingsButton = '[data-cy=data-minds-conversation-options]';
const closeButton = '[data-cy=data-minds-conversation-close]';
const destroyButton = '[data-cy=data-minds-conversation-destroy]';
before(() => {
cy.newUser(testUsername, testPassword);
});
beforeEach(() => {
cy.preserveCookies();
cy.server();
cy.route('GET', '**/api/v2/messenger/search?*').as('search');
cy.route('GET', '**/api/v2/messenger/conversations/**').as('conversations');
cy.route('POST', '**/api/v2/messenger/conversations/**').as('send');
cy.route('POST', '**/api/v2/messenger/keys/setup**').as('keys')
cy.get(openMessenger)
.click();
});
afterEach(() => {
cy.get(closeButton)
.click({multiple: true});
cy.get(openMessenger)
.click({multiple: true});
});
after(() => {
cy.deleteUser(testUsername, testPassword);
});
it('should allow a new user to set a password and send a message', () => {
cy.get(userSearch)
.type(Cypress.env().username)
.wait('@search').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.equal("success");
});
cy.get(userList(1))
.click();
cy.get(passwordInput(3))
.type(messagePassword)
cy.get(passwordInput(4))
.type(messagePassword)
cy.get(submitPassword)
.click();
cy.get(messageInput)
.type(messageContent);
cy.get(sendButton)
.click()
.wait('@send').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.equal("success");
});
});
it('should allow a user to destroy their chat content', () => {
cy.get(userSearch)
.clear()
.type(Cypress.env().username)
.wait('@search').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.equal("success");
});
cy.get(userList(1))
.click();
cy.get(passwordInput(3))
.type(messagePassword)
cy.get(passwordInput(4))
.type(messagePassword)
cy.get(submitPassword)
.click()
.wait('@keys').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.equal("success");
})
.wait('@conversations').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.equal("success");
});
cy.get(settingsButton)
.click();
cy.get(destroyButton)
.first()
.click();
cy.get(messageBubble)
.should('not.exist');
});
});
......@@ -51,6 +51,93 @@ context('Newsfeed', () => {
cy.get('.minds-list > minds-activity:first-child m-post-menu m-modal-confirm .mdl-button--colored').click();
})
it('should be able to post an activity picking a scheduled date and the edit it', () => {
cy.get('minds-newsfeed-poster').should('be.visible');
cy.get('minds-newsfeed-poster textarea').type('This is a post');
// set scheduled date
cy.get('.m-poster-date-selector__input').click();
cy.get('button.c-datepicker__next').click();
cy.get('tr.c-datepicker__days-row:nth-child(2) td.c-datepicker__day-body:first-child').click();
cy.get('a.c-btn.c-btn--flat.js-ok').click();
// get setted date to compare
let scheduledDate;
cy.get('div.m-poster-date-selector__input div.m-tooltip--bubble')
.invoke('text').then((text) => {
scheduledDate = text;
});
cy.get('.m-posterActionBar__PostButton').click();
cy.wait(100);
// compare setted date with time_created
cy.get('.minds-list > minds-activity:first-child div.mdl-card__supporting-text > div.body > a.permalink > span')
.invoke('text').then((text) => {
const time_created = new Date(text).getTime();
scheduledDate = new Date(scheduledDate).getTime();
expect(scheduledDate).to.equal(time_created);
});
// prepare to listen
cy.server();
cy.route("POST", '**/api/v1/newsfeed/**').as("saveEdited");
// edit the activity
cy.get('.minds-list > minds-activity:first-child m-post-menu > button.minds-more').click();
cy.get('.minds-list > minds-activity:first-child li.mdl-menu__item:first-child').click();
cy.get('.minds-list > minds-activity:first-child .m-poster-date-selector__input').click();
cy.get('button.c-datepicker__next').click();
cy.get('tr.c-datepicker__days-row:nth-child(3) td.c-datepicker__day-body:first-child').click();
cy.get('a.c-btn.c-btn--flat.js-ok').click();
// get setted date to compare
cy.get('.minds-list > minds-activity:first-child div.m-poster-date-selector__input div.m-tooltip--bubble')
.invoke('text').then((text) => {
scheduledDate = text;
});
// compare setted date with time_created
cy.get('.minds-list > minds-activity:first-child div.mdl-card__supporting-text > div.body > a.permalink > span')
.invoke('text').then((text) => {
const time_created = new Date(text).getTime();
scheduledDate = new Date(scheduledDate).getTime();
expect(scheduledDate).to.equal(time_created);
});
// Save
cy.get('.minds-list > minds-activity:first-child button.mdl-button.mdl-button--colored').click();
cy.wait('@saveEdited', { requestTimeout: 5000 }).then((xhr) => {
expect(xhr.status).to.equal(200, '**/api/v1/newsfeed/** request status');
});
// cleanup
cy.get('.minds-list > minds-activity:first-child m-post-menu .minds-more').click();
cy.get('.minds-list > minds-activity:first-child m-post-menu .minds-dropdown-menu .mdl-menu__item:nth-child(4)').click();
cy.get('.minds-list > minds-activity:first-child m-post-menu m-modal-confirm .mdl-button--colored').click();
})
it('should list scheduled activies', () => {
cy.server();
cy.route("GET", '**/api/v2/feeds/scheduled/**/count?').as("scheduledCount");
cy.route("GET", '**/api/v2/feeds/scheduled/**/activities?**').as("scheduledActivities");
cy.visit(`/${Cypress.env().username}`);
cy.wait('@scheduledCount', { requestTimeout: 2000 }).then((xhr) => {
expect(xhr.status).to.equal(200, 'feeds/scheduled/**/count request status');
});
cy.get('div.m-mindsListTools__scheduled').click();
cy.wait('@scheduledActivities', { requestTimeout: 2000 }).then((xhr) => {
expect(xhr.status).to.equal(200, 'feeds/scheduled/**/activities request status');
});
})
it('should post an activity with an image attachment', () => {
cy.get('minds-newsfeed-poster').should('be.visible');
......
......@@ -102,6 +102,7 @@ import { SettingsService } from '../modules/settings/settings.service';
import { ThemeService } from './services/theme.service';
import { HorizontalInfiniteScroll } from './components/infinite-scroll/horizontal-infinite-scroll.component';
import { ReferralsLinksComponent } from '../modules/wallet/tokens/referrals/links/links.component';
import { PosterDateSelectorComponent } from './components/poster-date-selector/selector.component';
import { ChannelModeSelectorComponent } from './components/channel-mode-selector/channel-mode-selector.component';
import { ShareModalComponent } from '../modules/modals/share/share';
......@@ -194,6 +195,8 @@ import { ShareModalComponent } from '../modules/modals/share/share';
SwitchComponent,
FeaturedContentComponent,
PosterDateSelectorComponent,
],
exports: [
MINDS_PIPES,
......@@ -278,6 +281,7 @@ import { ShareModalComponent } from '../modules/modals/share/share';
SwitchComponent,
NSFWSelectorComponent,
FeaturedContentComponent,
PosterDateSelectorComponent,
ChannelModeSelectorComponent,
],
providers: [
......
......@@ -206,6 +206,10 @@ export class ButtonsPlugin {
let $buttons = this.$element.querySelector('.medium-insert-buttons');
let $p = this.$element.querySelector('.medium-insert-active');
if (!$buttons) {
return;
}
if ($p !== null) {
let $lastCaption = $p.classList.contains('medium-insert-images-grid')
? []
......
<div
class="m-poster-date-selector__input"
[class.selected]="hasDateSelected()"
mdl-datetime-picker
[date]="getDate()"
(dateChange)="onDateChange($event)"
>
<input
type="text"
[ngModel]="date | date: dateFormat"
(ngModelChange)="onDateChange($event)"
[hidden]="true"
/>
<m-tooltip icon="date_range">
{{ getDate() || 'Post Immediately' }}
</m-tooltip>
<span></span>
</div>
m-poster-date-selector {
.m-poster-date-selector__label {
text-transform: uppercase;
letter-spacing: 2.5px;
align-self: center;
font-size: 12px;
}
.m-poster-date-selector__input {
align-items: center;
margin-right: -4px;
input {
font-size: 12px;
text-transform: uppercase;
background-color: transparent;
padding: 8px 0;
border: none;
text-align: center;
width: auto;
height: auto;
align-self: center;
}
i {
vertical-align: middle;
cursor: pointer;
}
}
m-tooltip .m-tooltip--bubble {
width: 125px;
}
}
@media screen and (max-height: 570px) {
div.c-datepicker {
min-height: 560px;
}
}
import {
async,
ComponentFixture,
TestBed,
fakeAsync,
tick,
} from '@angular/core/testing';
import { PosterDateSelectorComponent } from './selector.component';
import { MaterialDateTimePickerDirective } from '../../directives/material/datetimepicker.directive';
import { FormsModule } from '@angular/forms';
import { MockComponent } from '../../../utils/mock';
describe('PosterDateSelectorComponent', () => {
let comp: PosterDateSelectorComponent;
let fixture: ComponentFixture<PosterDateSelectorComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
PosterDateSelectorComponent,
MaterialDateTimePickerDirective,
MockComponent({
selector: 'm-tooltip',
template: '<ng-content></ng-content>',
inputs: ['icon'],
}),
],
imports: [FormsModule],
}).compileComponents(); // compile template and css
}));
// synchronous beforeEach
beforeEach(done => {
jasmine.MAX_PRETTY_PRINT_DEPTH = 10;
jasmine.clock().uninstall();
jasmine.clock().install();
fixture = TestBed.createComponent(PosterDateSelectorComponent);
comp = fixture.componentInstance;
fixture.detectChanges();
if (fixture.isStable()) {
done();
} else {
fixture.whenStable().then(() => {
done();
});
}
});
afterEach(() => {
jasmine.clock().uninstall();
});
it('should emit when onDateChange is called', fakeAsync(() => {
spyOn(comp.dateChange, 'emit');
const testDate = new Date();
testDate.setMonth(testDate.getMonth() + 1);
comp.onDateChange(testDate.toString());
let timeDate = testDate.getTime();
timeDate = Math.floor(timeDate / 1000);
expect(comp.dateChange.emit).toHaveBeenCalledWith(timeDate);
}));
it('should emit onError when date more than 3 months', fakeAsync(() => {
spyOn(comp.onError, 'emit');
const testDate = new Date();
testDate.setMonth(testDate.getMonth() + 4);
comp.onDateChange(testDate.toString());
let timeDate = testDate.getTime();
timeDate = Math.floor(timeDate / 1000);
expect(comp.onError.emit).toHaveBeenCalledWith(
"Scheduled date can't be 3 months or more"
);
}));
it('should emit onError when date less than 5 minutes or in the past', fakeAsync(() => {
spyOn(comp.onError, 'emit');
const testDate = new Date();
comp.onDateChange(testDate.toString());
let timeDate = testDate.getTime();
timeDate = Math.floor(timeDate / 1000);
expect(comp.onError.emit).toHaveBeenCalledWith(
"Scheduled date can't be less than 5 minutes or in the past"
);
}));
});
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { DatePipe } from '@angular/common';
@Component({
moduleId: module.id,
selector: 'm-poster-date-selector',
templateUrl: 'selector.component.html',
providers: [DatePipe],
})
export class PosterDateSelectorComponent {
@Input() date: string;
@Output() dateChange: EventEmitter<any> = new EventEmitter<any>();
@Output() onError: EventEmitter<String> = new EventEmitter<String>();
@Input() dateFormat: string = 'short';
onDateChange(newDate) {
const validation = this.validate(newDate);
if (validation !== true) {
this.onError.emit(validation);
return;
}
this.date = newDate;
newDate = new Date(newDate).getTime();
newDate = Math.floor(+newDate / 1000);
this.dateChange.emit(newDate);
}
hasDateSelected() {
return this.date && this.date !== '';
}
validate(newDate) {
const date = new Date(newDate);
const threeMonths = new Date();
threeMonths.setMonth(threeMonths.getMonth() + 3);
if (date >= threeMonths) {
return "Scheduled date can't be 3 months or more";
}
const fiveMinutes = new Date();
fiveMinutes.setMinutes(fiveMinutes.getMinutes() + 5);
if (date < fiveMinutes) {
return "Scheduled date can't be less than 5 minutes or in the past";
}
return true;
}
getDate() {
const tempDate = parseInt(this.date);
if (tempDate) {
this.date = new Date(tempDate * 1000).toString();
}
return this.date;
}
}
......@@ -7,6 +7,7 @@
opacity: 0;
transition: 200ms ease opacity;
will-change: opacity;
z-index: 9999;
@include m-theme() {
background-color: rgba(themed($m-black-always), 0.541176);
}
......
......@@ -23,7 +23,11 @@ export class MaterialDateTimePickerDirective {
@HostListener('click')
onHostClick() {
if (!this.open) {
this.picker = new DateTimePicker()
let options = {};
if (this.date) {
options = { default: new Date(this.date).toString() };
}
this.picker = new DateTimePicker(options)
.on('submit', this.submitCallback.bind(this))
.on('close', this.close.bind(this));
this.open = true;
......
......@@ -309,7 +309,6 @@ minds-blog-edit {
m-wire-threshold-input {
position: relative;
padding: 8px;
}
.m-additional-block m-wire-threshold-input {
......@@ -366,6 +365,7 @@ minds-blog-edit {
}
.m-additional-block > * {
flex: auto;
margin-right: 0px;
}
}
......
......@@ -170,6 +170,15 @@
(validThreshold)="validThreshold = $event"
#thresholdInput
></m-wire-threshold-input>
<ng-container *mIfFeature="'post-scheduler'">
<m-poster-date-selector
*ngIf="checkTimePublished()"
[date]="getTimeCreated()"
(dateChange)="onTimeCreatedChange($event)"
(onError)="posterDateSelectorError($event)"
></m-poster-date-selector>
</ng-container>
</div>
<div
......
......@@ -26,7 +26,7 @@ import { By } from '@angular/platform-browser';
import { Session } from '../../../services/session';
import { sessionMock } from '../../../../tests/session-mock.spec';
import { mindsTitleMock } from '../../../mocks/services/ux/minds-title.service.mock.spec';
import { MockComponent } from '../../../utils/mock';
import { MockComponent, MockDirective } from '../../../utils/mock';
import { InMemoryStorageService } from '../../../services/in-memory-storage.service';
import { inMemoryStorageServiceMock } from '../../../../tests/in-memory-storage-service-mock.spec';
......@@ -240,6 +240,15 @@ describe('BlogEdit', () => {
inputs: ['tags', 'alignLeft'],
outputs: ['tagsChange', 'tagsAdded', 'tagsRemoved'],
}),
MockComponent({
selector: 'm-poster-date-selector',
inputs: ['date', 'dateFormat'],
outputs: ['dateChange'],
}),
MockDirective({
selector: '[mIfFeature]',
inputs: ['mIfFeature'],
}),
BlogEdit,
MDLMock,
], // declare the test component
......
......@@ -30,7 +30,7 @@ export class BlogEdit {
guid: 'new',
title: '',
description: '<p><br></p>',
time_created: Date.now(),
time_created: Math.floor(Date.now() / 1000),
access_id: 2,
tags: [],
license: 'attribution-sharealike-cc',
......@@ -69,6 +69,8 @@ export class BlogEdit {
@ViewChild('hashtagsSelector', { static: false })
hashtagsSelector: HashtagsSelectorComponent;
protected time_created: any;
constructor(
public session: Session,
public client: Client,
......@@ -207,6 +209,9 @@ export class BlogEdit {
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);
}
});
}
......@@ -234,6 +239,10 @@ export class BlogEdit {
return true;
}
posterDateSelectorError(msg) {
this.error = msg;
}
save() {
if (!this.canSave) return;
......@@ -248,6 +257,8 @@ export class BlogEdit {
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;
......@@ -258,6 +269,7 @@ export class BlogEdit {
.then((response: any) => {
this.inProgress = false;
this.canSave = true;
this.blog.time_created = null;
if (response.status !== 'success') {
this.error = response.message;
......@@ -328,4 +340,21 @@ export class BlogEdit {
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)
);
}
}
......@@ -45,11 +45,18 @@
<h2>Experiments</h2>
<ul class="m-canaryExperiments__list">
<li>
Media Modals
<a href="https://gitlab.com/minds/front/merge_requests/467"
>(front!469)</a
Multi Currency Wire
<a href="https://gitlab.com/minds/front/merge_requests/508"
>(front!508)</a
>
- 20th August '19
- 17th September '19
</li>
<li>
Post scheduler
<a href="https://gitlab.com/minds/front/merge_requests/494"
>(front!494)</a
>
- 17th September '19
</li>
</ul>
</div>
......
......@@ -6,14 +6,25 @@
<div class="minds-list">
<div>
<m-sort-selector
class="m-channel--sorted__SortSelector m-border"
[allowedAlgorithms]="false"
[allowedPeriods]="false"
[allowedCustomTypes]="['activities', 'images', 'videos', 'blogs']"
[customType]="type"
(onChange)="setFilter($event.customType)"
></m-sort-selector>
<div class="m-mindsList__tools m-border">
<div
*ngIf="isOwner()"
class="m-mindsListTools__scheduled"
(click)="toggleScheduled()"
[class.selected]="viewScheduled"
>
<m-tooltip icon="date_range"> See Scheduled Activities </m-tooltip>
<span>scheduled: {{ scheduledCount }}</span>
</div>
<m-sort-selector
class="m-channel--sorted__SortSelector"
[allowedAlgorithms]="false"
[allowedPeriods]="false"
[allowedCustomTypes]="['activities', 'images', 'videos', 'blogs']"
[customType]="type"
(onChange)="setFilter($event.customType)"
></m-sort-selector>
</div>
<m-onboarding-feed *ngIf="isOwner()"></m-onboarding-feed>
......
.m-channel--sorted__SortSelector {
.m-mindsList__tools {
@include m-theme() {
background-color: themed($m-white);
}
display: flex;
position: relative;
padding: 8px;
margin-bottom: 16px;
flex-direction: row;
align-items: center;
.m-mindsListTools__scheduled {
cursor: pointer;
i {
vertical-align: middle;
font-size: 18px;
margin-right: 4px;
}
span {
text-transform: uppercase;
font-size: 11px;
letter-spacing: 1.25px;
text-rendering: optimizeLegibility;
}
}
.m-channel--sorted__SortSelector {
margin-left: auto;
}
}
......@@ -15,6 +15,7 @@ import { Session } from '../../../services/session';
import { PosterComponent } from '../../newsfeed/poster/poster.component';
import { SortedService } from './sorted.service';
import { ClientMetaService } from '../../../common/services/client-meta.service';
import { Client } from '../../../services/api';
@Component({
selector: 'm-channel--sorted',
......@@ -60,15 +61,20 @@ export class ChannelSortedComponent implements OnInit {
initialized: boolean = false;
viewScheduled: boolean = false;
@ViewChild('poster', { static: false }) protected poster: PosterComponent;
scheduledCount: number = 0;
constructor(
public feedsService: FeedsService,
protected service: SortedService,
protected session: Session,
protected clientMetaService: ClientMetaService,
@SkipSelf() injector: Injector,
protected cd: ChangeDetectorRef
protected cd: ChangeDetectorRef,
public client: Client
) {
this.clientMetaService
.inherit(injector)
......@@ -92,11 +98,18 @@ export class ChannelSortedComponent implements OnInit {
this.detectChanges();
let endpoint = 'api/v2/feeds/container';
if (this.viewScheduled) {
endpoint = 'api/v2/feeds/scheduled';
}
try {
this.feedsService
.setEndpoint(`api/v2/feeds/container/${this.channel.guid}/${this.type}`)
.setEndpoint(`${endpoint}/${this.channel.guid}/${this.type}`)
.setLimit(12)
.fetch();
this.getScheduledCount();
} catch (e) {
console.error('ChannelsSortedComponent.load', e);
}
......@@ -170,4 +183,16 @@ export class ChannelSortedComponent implements OnInit {
this.cd.markForCheck();
this.cd.detectChanges();
}
toggleScheduled() {
this.viewScheduled = !this.viewScheduled;
this.load(true);
}
async getScheduledCount() {
const url = `api/v2/feeds/scheduled/${this.channel.guid}/count`;
const response: any = await this.client.get(url);
this.scheduledCount = response.count;
this.detectChanges();
}
}
......@@ -60,7 +60,8 @@ minds-activity {
}
}
}
m-nsfw-selector {
m-nsfw-selector,
m-poster-date-selector {
display: inline-block;
border-radius: 24px;
padding: 3px 16px;
......
......@@ -451,6 +451,11 @@ describe('Activity', () => {
inputs: ['selected'],
outputs: ['selected'],
}),
MockComponent({
selector: 'm-poster-date-selector',
inputs: ['date', 'dateFormat'],
outputs: ['dateChange'],
}),
MockDirective({
selector: '[mIfFeature]',
inputs: ['mIfFeature'],
......
......@@ -180,6 +180,12 @@
[(threshold)]="activity.wire_threshold"
[(enabled)]="activity.paywall"
></m-wire-threshold-input>
<m-poster-date-selector
*ngIf="checkCreated()"
[date]="getTimeCreated()"
(dateChange)="onTimeCreatedChange($event)"
(onError)="posterDateSelectorError($event)"
></m-poster-date-selector>
<button
class="mdl-button mdl-button--raised mdl-color--blue-grey-100"
(click)="messageEdit.value = activity.message; editing=false;"
......@@ -503,3 +509,12 @@
>
This post is awaiting moderation.
</div>
<!-- Pending block -->
<div
class="mdl-card__supporting-text m-activity--pending"
*ngIf="isScheduled(activity.time_created)"
>
This activity is scheduled to be shown on {{activity.time_created * 1000 |
date:'medium'}}.
</div>
......@@ -156,6 +156,8 @@ export class Activity implements OnInit {
@ViewChild('player', { static: false }) player: MindsVideoComponent;
@ViewChild('batchImage', { static: false }) batchImage: ElementRef;
protected time_created: any;
constructor(
public session: Session,
public client: Client,
......@@ -233,6 +235,9 @@ export class Activity implements OnInit {
(this.activity.remind_object &&
this.translationService.isTranslatable(this.activity.remind_object));
this.activity.time_created =
this.activity.time_created || Math.floor(Date.now() / 1000);
this.allowComments = this.activity.allow_comments;
}
......@@ -255,6 +260,8 @@ export class Activity implements OnInit {
console.log('trying to save your changes to the server', this.activity);
this.editing = false;
this.activity.edited = true;
this.activity.time_created =
this.activity.time_created || Math.floor(Date.now() / 1000);
let data = this.activity;
if (this.attachment.has()) {
......@@ -494,6 +501,10 @@ export class Activity implements OnInit {
return activity && activity.pending && activity.pending !== '0';
}
isScheduled(time_created) {
return time_created && time_created * 1000 > Date.now();
}
toggleMatureVisibility() {
this.activity.mature_visibility = !this.activity.mature_visibility;
......@@ -568,4 +579,24 @@ export class Activity implements OnInit {
this.cd.markForCheck();
this.cd.detectChanges();
}
onTimeCreatedChange(newDate) {
this.activity.time_created = newDate;
}
posterDateSelectorError(msg) {
throw new Error(msg);
}
getTimeCreated() {
return this.activity.time_created > Math.floor(Date.now() / 1000)
? this.activity.time_created
: null;
}
checkCreated() {
return this.activity.time_created > Math.floor(Date.now() / 1000)
? true
: false;
}
}
......@@ -78,6 +78,10 @@ export class ActivityPreview {
return false;
}
isScheduled(time_created) {
return false;
}
save() {
/* NOOP */
}
......
......@@ -140,6 +140,10 @@ export class Remind {
return activity && activity.pending && activity.pending !== '0';
}
isScheduled(time_created) {
return false;
}
openComments() {
/* NOOP */
}
......
......@@ -21,6 +21,7 @@ import { OverlayModalService } from '../../../services/ux/overlay-modal';
import { AnalyticsService } from '../../../services/analytics';
import { MindsVideoComponent } from '../components/video/video.component';
import isMobileOrTablet from '../../../helpers/is-mobile-or-tablet';
import { ActivityService } from '../../../common/services/activity.service';
@Component({
selector: 'm-media--modal',
......@@ -51,6 +52,7 @@ import isMobileOrTablet from '../../../helpers/is-mobile-or-tablet';
transition(':leave', [animate('300ms', style({ opacity: 0 }))]),
]),
],
providers: [ActivityService],
})
export class MediaModalComponent implements OnInit, OnDestroy {
minds = window.Minds;
......@@ -122,7 +124,7 @@ export class MediaModalComponent implements OnInit, OnDestroy {
this.entity.title ||
`${this.entity.ownerObj.name}'s post`;
this.entity.guid = this.entity.entity_guid || this.entity.guid;
this.thumbnail = `${this.minds.cdn_url}fs/v1/thumbnail/${this.entity.entity_guid}/xlarge`;
this.thumbnail = this.entity.thumbnails.xlarge;
switch (this.entity.custom_type) {
case 'video':
this.contentType = 'video';
......@@ -139,7 +141,8 @@ export class MediaModalComponent implements OnInit, OnDestroy {
break;
case 'image':
this.contentType = 'image';
this.thumbnail = `${this.minds.cdn_url}fs/v1/thumbnail/${this.entity.guid}/xlarge`;
// this.thumbnail = `${this.minds.cdn_url}fs/v1/thumbnail/${this.entity.guid}/xlarge`;
this.thumbnail = this.entity.thumbnail;
break;
case 'blog':
this.contentType = 'blog';
......@@ -158,7 +161,8 @@ export class MediaModalComponent implements OnInit, OnDestroy {
`${this.entity.ownerObj.name}'s post`;
this.entity.guid = this.entity.attachment_guid;
this.entity.entity_guid = this.entity.attachment_guid;
this.thumbnail = `${this.minds.cdn_url}fs/v1/thumbnail/${this.entity.attachment_guid}/xlarge`;
// this.thumbnail = `${this.minds.cdn_url}fs/v1/thumbnail/${this.entity.attachment_guid}/xlarge`;
this.thumbnail = this.entity.thumbnails.xlarge;
break;
}
......
......@@ -90,13 +90,14 @@ export class MediaTheatreComponent {
}
getThumbnail() {
const url =
this.object.paywalled ||
(this.object.wire_threshold && this.object.wire_threshold !== '0')
? this.minds.site_url
: this.minds.cdn_url;
return url + `fs/v1/thumbnail/${this.object.guid}/xlarge`;
// const url =
// this.object.paywalled ||
// (this.object.wire_threshold && this.object.wire_threshold !== '0')
// ? this.minds.site_url
// : this.minds.cdn_url;
// return url + `fs/v1/thumbnail/${this.object.guid}/xlarge`;
return this.object.thumbnail_src;
}
prev() {
......
......@@ -19,8 +19,16 @@
<i class="material-icons mdl-color-text--blue-grey-100" [hidden]="live"
>sync_problem</i
>
<i class="material-icons" (click)="ribbonToggle()">more_vert</i>
<i class="material-icons" (click)="dockpanes.close(conversation)"
<i
class="material-icons"
(click)="ribbonToggle()"
data-cy="data-minds-conversation-options"
>more_vert</i
>
<i
class="material-icons"
(click)="dockpanes.close(conversation)"
data-cy="data-minds-conversation-close"
>close</i
>
</div>
......@@ -35,6 +43,7 @@
<div
class="m-messenger--dockpane-tab-icon mdl-color-text--blue-grey-300"
(click)="deleteHistory(); ribbonOpened = false"
data-cy="data-minds-conversation-destroy"
>
<i
class="material-icons mdl-color-text--blue-grey-100"
......@@ -244,6 +253,7 @@
<i
class="material-icons mdl-color-text--blue-grey-600"
(click)="send($event); emoji.close()"
data-cy="data-minds-conversation-send"
>send_arrow</i
>
<minds-emoji [localDirective]="emoji"></minds-emoji>
......
......@@ -96,6 +96,13 @@
(validThreshold)="validThreshold = $event"
></m-wire-threshold-input>
<m-poster-date-selector
[date]="meta.time_created"
(dateChange)="onTimeCreatedChange($event)"
(onError)="posterDateSelectorError($event)"
*mIfFeature="'post-scheduler'"
></m-poster-date-selector>
<button
type="submit"
class="m-posterActionBar__PostButton"
......
......@@ -259,7 +259,7 @@ m-hashtags-selector {
}
> * {
padding: 4px 8px;
padding: 4px 4px;
position: relative;
white-space: nowrap;
text-decoration: none;
......@@ -287,6 +287,11 @@ m-hashtags-selector {
white-space: normal;
}
}
m-poster-date-selector {
padding-right: $minds-padding;
}
.m-posterActionBar__CreateBlog {
cursor: pointer;
}
......@@ -305,13 +310,15 @@ m-hashtags-selector {
}
.material-icons.m-posterActionBar__Icon,
.m-posterActionBar__IconAndLabel i.material-icons {
.m-posterActionBar__IconAndLabel i.material-icons,
.m-tooltip > i {
font-size: 20px;
transform: rotate(0.03deg) translateY(-1px); // Jagged lines hack
}
.m-posterActionBar__Label,
.m-posterActionBar__IconAndLabel > span {
.m-posterActionBar__IconAndLabel > span,
.m-poster-date-selector--input > span {
font-size: 12px;
font-family: 'Roboto', sans-serif;
text-transform: uppercase;
......
......@@ -78,6 +78,11 @@ describe('PosterComponent', () => {
selector: 'minds-rich-embed',
inputs: ['src', 'preview', 'maxheight', 'cropimage'],
}),
MockComponent({
selector: 'm-poster-date-selector',
inputs: ['date', 'dateFormat'],
outputs: ['dateChange'],
}),
MockComponent({
selector: 'm-tooltip',
template: '<ng-content></ng-content>',
......
......@@ -32,6 +32,7 @@ export class PosterComponent {
meta: any = {
message: '',
wire_threshold: null,
time_created: null,
};
tags = [];
minds = window.Minds;
......@@ -174,6 +175,9 @@ export class PosterComponent {
return;
}
this.meta.time_created =
this.meta.time_created || Math.floor(Date.now() / 1000);
this.errorMessage = '';
let data = Object.assign(this.meta, this.attachment.exportMeta());
......@@ -285,4 +289,12 @@ export class PosterComponent {
onNSWFSelections(reasons: Array<{ value; label; selected }>) {
this.attachment.setNSFW(reasons);
}
onTimeCreatedChange(newDate) {
this.meta.time_created = newDate;
}
posterDateSelectorError(msg) {
this.errorMessage = msg;
}
}
......@@ -3,12 +3,16 @@ import { Client } from '../../services/api';
import { SocketsService } from '../../services/sockets';
import { Session } from '../../services/session';
import { MindsTitle } from '../../services/ux/title';
import { Subscription, timer } from 'rxjs';
export class NotificationService {
socketSubscriptions: any = {
notification: null,
};
onReceive: EventEmitter<any> = new EventEmitter();
notificationPollTimer;
private updateNotificationCountSubscription: Subscription;
static _(
session: Session,
......@@ -71,22 +75,26 @@ export class NotificationService {
* Return the notifications
*/
getNotifications() {
var self = this;
setInterval(function() {
// console.log('getting notifications');
const pollIntervalSeconds = 60;
this.notificationPollTimer = timer(0, pollIntervalSeconds * 1000);
this.updateNotificationCountSubscription = this.notificationPollTimer.subscribe(
() => this.updateNotificationCount()
);
}
if (!self.session.isLoggedIn()) return;
updateNotificationCount() {
if (!this.session.isLoggedIn()) {
return;
}
if (!window.Minds.notifications_count)
window.Minds.notifications_count = 0;
if (!window.Minds.notifications_count) {
window.Minds.notifications_count = 0;
}
self.client
.get('api/v1/notifications/count', {})
.then((response: any) => {
window.Minds.notifications_count = response.count;
self.sync();
});
}, 60000);
this.client.get('api/v1/notifications/count', {}).then((response: any) => {
window.Minds.notifications_count = response.count;
this.sync();
});
}
/**
......@@ -101,4 +109,8 @@ export class NotificationService {
}
this.title.setCounter(window.Minds.notifications_count);
}
ngOnDestroy() {
this.notificationPollTimer.unsubscribe();
}
}
......@@ -70,7 +70,7 @@
<input
type="text"
class="m-wire--creator-wide-input--edit"
[ngModel]="wire.amount | number"
[ngModel]="wire.amount"
(ngModelChange)="setAmount($event)"
(focus)="amountEditorFocus()"
(blur)="amountEditorBlur()"
......
......@@ -322,13 +322,24 @@ export class WireCreatorComponent {
return;
}
if (amount.indexOf('.') === 0) {
if (amount.length === 1) {
return; // not propogration
}
amount = `0${amount}`;
}
if (typeof amount === 'number') {
this.wire.amount = amount;
console.log('amount is a number');
return;
}
amount = amount.replace(/,/g, '');
this.wire.amount = parseFloat(amount);
const amountAsFloat = parseFloat(amount);
if (amountAsFloat) {
this.wire.amount = amountAsFloat;
}
}
/**
......@@ -355,11 +366,11 @@ export class WireCreatorComponent {
}
/**
* Round by 4
* Round by 6
*/
roundAmount() {
this.wire.amount =
Math.round(parseFloat(`${this.wire.amount}`) * 10000) / 10000;
Math.round(parseFloat(`${this.wire.amount}`) * 1000000) / 1000000;
}
// Charge and rates
......