...
 
Commits (25)
......@@ -38,3 +38,4 @@ cypress/videos
!/.drone.yml
!/.gitlab
!/.githooks
!/.prettierrc
......@@ -306,6 +306,7 @@ deploy:production:
cleanup:review: # We stop the review site after the e2e tests have run
<<: *cleanup_review
stage: cleanup
when: manual
except:
refs:
- master
......
#!/usr/bin/env bash
# Boilerplate generated with create-bash-script (c) 2019 Nikita Skobov
# Available https://github.com/nikita-skobov/create-bash-script
function usage()
{
local just_help=$1
local missing_required=$2
local invalid_argument=$3
local invalid_option=$4
local help="Usage: e2e.sh [OPTIONS]
Intended to serve as an interaction wrapper around Cypress.
Example: ./e2e.sh -u nemofin -p password123 -v true -url http://www.minds.com/
Options (* indicates it is required):"
local help_options="
*\-p ,\--password \<Parameter>\ The password of the user.
\-url ,\--url \<Parameter>\ The URL of the host e.g. https://www.minds.com/ - defaults to use localhost.
\-u ,\--username \<Parameter>\ The username - defaults to cypress_e2e_test.
\-v ,\---video \<Parameter>\ true if you want video providing.
"
if [ "$missing_required" != "" ]
then
echo "Missing required argument: $missing_required"
fi
if [ "$invalid_option" != "" ] && [ "$invalid_value" = "" ]
then
echo "Invalid option: $invalid_option"
elif [ "$invalid_value" != "" ]
then
echo "Invalid value: $invalid_value for option: --$invalid_option"
fi
echo -e "
"
echo "$help"
echo "$help_options" | column -t -s'\'
return
}
function init_args()
{
REQ_ARGS=( "password" )
# get command line arguments
POSITIONAL=()
# set default arguments
url="http://localhost"
username="minds_cypress_tests"
_video=false
while [[ $# -gt 0 ]]
do
key="$1"
case $key in
-url|--url)
url="$2"
shift 2
;;
-u|--username)
username="$2"
shift 2
;;
-p|--password)
password="$2"
shift 2
;;
-v|---video)
_video="$2"
shift 2
;;
*)
POSITIONAL+=("$1") # saves unknown option in array
shift
;;
esac
done
for i in "${REQ_ARGS[@]}"; do
# $i is the string of the variable name
# ${!i} is a parameter expression to get the value
# of the variable whose name is i.
req_var=${!i}
if [ "$req_var" = "" ]
then
usage "" "--$i"
exit
fi
done
}
init_args $@
# cd to project root.
while [[ $PWD != '/' && ${PWD##*/} != 'front' ]]; do cd ..; done
#run cypress with args.
./node_modules/cypress/bin/cypress open --config baseUrl=$url,video=$_video --env username=$username,password=$password
......@@ -13,6 +13,7 @@ context('Blogs', () => {
beforeEach(()=> {
cy.preserveCookies();
cy.server();
});
it('should not be able to create a new blog if no title or banner are specified', () => {
......@@ -89,7 +90,6 @@ context('Blogs', () => {
cy.get('.m-mature-info a').click();
cy.get('.m-mature-info a span').contains('Mature content');
cy.server();
cy.route("POST", "**/api/v1/blog/new").as("postBlog");
cy.route("GET", "**/api/v1/blog/**").as("getBlog");
......@@ -120,6 +120,111 @@ 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('minds-newsfeed-poster').then((poster) => {
if (poster.find('.m-poster-date-selector__input').length > 0) {
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 E2E testing for Minds Boost Console pages.
*/
import generateRandomId from '../../support/utilities';
context('Boost Console', () => {
const postContent = "Test boost, please reject..." + Math.random().toString(36);
const postContent = "Test boost, please reject..." + generateRandomId();
before(() => {
cy.getCookie('minds_sess')
......@@ -8,27 +14,24 @@ context('Boost Console', () => {
return cy.login(true);
}
});
newBoost(postContent, 100);
});
beforeEach(() => {
cy.server();
cy.route("POST", '**/api/v2/boost/**').as('boostPost');
cy.preserveCookies();
cy.visit('/newsfeed/subscribed');
newBoost(postContent, 100);
cy.visit('/boost/console/newsfeed/history');
});
it('should show a new boost in the console', () => {
cy.visit('/boost/console/newsfeed/history');
cy.get('m-boost-console-card:nth-child(1) div.m-boost-card--manager-item.m-boost-card--state')
.should('not.contain', 'revoked');
cy.get('m-boost-console-card:nth-child(1) .m-boost-card--manager-item--buttons > button')
.click();
cy.get('m-boost-console-card:nth-child(1) .m-mature-message span')
.contains(postContent);
});
it('should allow a revoke a boost', () => {
navToConsole();
cy.get('m-boost-console-card:nth-child(1) div.m-boost-card--manager-item.m-boost-card--state')
.should('not.contain', 'revoked');
......@@ -39,15 +42,42 @@ context('Boost Console', () => {
.contains('revoked');
});
function navToConsole() {
cy.visit('/boost/console/newsfeed/history');
cy.location('pathname')
.should('eq', `/boost/console/newsfeed/history`);
}
it('should load show the user content for newsfeed boosts', () => {
cy.route("GET", "**/feeds/container/*/activities**").as("activities");
cy.contains('Create a Boost')
.click()
.location('pathname')
.should('eq', `/boost/console/newsfeed/create`)
.wait('@activities').then((xhr) => {
expect(xhr.status).to.equal(200);
});
})
it('should load show the user content for sidebar boosts', () => {
cy.route("GET", "**/api/v2/feeds/container/*/all**").as("all");
cy.visit('/boost/console/content/create')
.location('pathname')
.should('eq', `/boost/console/content/create`)
.wait('@all').then((xhr) => {
expect(xhr.status).to.equal(200);
});
})
it('should load show the user content for offers', () => {
cy.route("GET", "**/api/v2/feeds/container/*/activities**").as("all");
cy.visit('/boost/console/offers/create')
.location('pathname')
.should('eq', `/boost/console/offers/create`)
.wait('@all').then((xhr) => {
expect(xhr.status).to.equal(200);
});
})
function newBoost(text, views) {
cy.server();
cy.route("POST", '**/api/v2/boost/**').as('boostPost');
cy.visit('/newsfeed/subscribed');
cy.post(text);
cy.get('#boost-actions')
......@@ -61,7 +91,6 @@ context('Boost Console', () => {
.click();
cy.wait('@boostPost').then((xhr) => {
cy.log(xhr);
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.deep.equal("success");
});
......
context('Boost Creation', () => {
const duplicateError = "There's already an ongoing boost for this entity";
const postContent = "Test boost, please reject..." + Math.random().toString(36).substring(8);
const nonParticipationError = 'Boost target should participate in the Rewards program.'
before(() => {
cy.server();
})
beforeEach(() => {
cy.server()
.route("GET", '**/api/v2/boost/prepare/**').as('prepare')
.route("POST", '**/api/v2/boost/activity/**').as('activity')
.route("GET", '**/api/v2/blockchain/wallet/balance*').as('balance')
.route("GET", '**/api/v2/search/suggest/**').as('suggest');
cy.getCookie('minds_sess')
.then((sessionCookie) => {
if (sessionCookie === null) {
return cy.login(true);
}
});
cy.visit('/newsfeed/subscriptions');
cy.location('pathname')
.should('eq', `/newsfeed/subscriptions`);
cy.route("GET", '**/api/v2/boost/prepare/**').as('prepare');
cy.route("POST", '**/api/v2/boost/activity/**').as('activity');
cy.route("GET", '**/api/v2/blockchain/wallet/balance*').as('balance');
cy.route("GET", '**/api/v2/search/suggest/**').as('suggest');
cy.visit('/newsfeed/subscriptions')
.location('pathname')
.should('eq', `/newsfeed/subscriptions`);
});
......
context('Boost Impressions', () => {
context.skip('Boost Impressions', () => {
before(() => {
cy.getCookie('minds_sess')
......@@ -7,37 +7,43 @@ context('Boost Impressions', () => {
return cy.login(true);
}
});
cy.visit('/newsfeed/subscriptions');
cy.location('pathname')
.should('eq', `/newsfeed/subscriptions`);
});
beforeEach(()=> {
cy.server();
cy.route("POST", "**api/v2/analytics/views/boost/*").as("analytics");
cy.route("GET", "**/api/v2/feeds/subscribed/activities**").as("activities");
cy.preserveCookies();
cy.visit('/newsfeed/subscriptions')
.location('pathname')
.should('eq', `/newsfeed/subscriptions`)
.wait('@activities').then((xhr) => {
expect(xhr.status).to.equal(200);
});
});
afterEach(()=> {
cy.reload();
})
// Cannot test until env behaves consistently.
// See: https://gitlab.com/minds/front/issues/1912
it('should register views on scroll', () => {
//stub endpoint
cy.server();
cy.route("POST", "**/api/v2/analytics/views/activity/*").as("analytics");
//load, scroll, wait to trigger analytics
cy.scrollTo(0, 500);
//smooth scroll
cy.scrollTo('0', '1%', { duration: 100 });
//assert
cy.wait('@analytics').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body).to.deep.equal({ status: 'success' });
});
});
it('should register views on boost rotate', () => {
//stub endpoint
cy.server();
cy.route("POST", "**/api/v2/analytics/views/boost/*").as("analytics");
//rotate forward and wait to trigger analytics
cy.get('m-newsfeed--boost-rotator > div > ul > li:nth-child(3) > i')
cy.get('m-newsfeed--boost-rotator')
.find('chevron_right')
.click();
//assert
......@@ -47,7 +53,8 @@ context('Boost Impressions', () => {
});
//rotate forward and wait to trigger analytics
cy.get('m-newsfeed--boost-rotator > div > ul > li:nth-child(2) > i')
cy.get('m-newsfeed--boost-rotator')
.find('chevron_left')
.click();
//assert
......
context('Channel image upload', () => {
before(() => {
cy.getCookie('minds_sess')
.then((sessionCookie) => {
if (sessionCookie === null) {
return cy.login(true);
}
});
});
beforeEach(()=> {
cy.preserveCookies();
cy.server();
cy.route("POST", "**/api/v1/newsfeed").as("newsfeedPOST");
cy.route("POST", "**/api/v1/media").as("mediaPOST");
});
it('should post an activity with an image attachment', () => {
cy.get('minds-newsfeed-poster').should('be.visible');
cy.get('minds-newsfeed-poster textarea').type('This is a post with an image');
cy.uploadFile('#attachment-input-poster', '../fixtures/international-space-station-1776401_1920.jpg', 'image/jpg');
//upload image
cy.wait('@mediaPOST');
cy.get('.m-posterActionBar__PostButton').click();
//await response for activity
cy.wait('@newsfeedPOST').then((xhr) => {
expect(xhr.status).to.equal(200);
const uploadedImageGuid = xhr.response.body.activity.entity_guid
const activityGuid = xhr.response.body.guid;
cy.get('.minds-list > minds-activity:first-child .message').contains('This is a post with an image');
// assert image
cy.get('.minds-list > minds-activity:first-child .item-image img').should('be.visible');
cy.visit(`/${Cypress.env().username}`);
let mediaHref = `/media/${uploadedImageGuid}`;
cy.get("m-channels--sorted-module[title='Images']")
.find(`a[href='${mediaHref}']`);
cy.get(`[data-minds-activity-guid='${activityGuid}']`)
.find('m-post-menu .minds-more')
.click();
cy.get(`[data-minds-activity-guid='${activityGuid}']`)
.find("li:contains('Delete')")
.click();
cy.get(`[data-minds-activity-guid='${activityGuid}'] m-post-menu m-modal-confirm .mdl-button--colored`).click();
});
});
});
context('Channel', () => {
// skipped until feat release
context.skip('Channel', () => {
before(() => {
cy.getCookie('minds_sess')
.then((sessionCookie) => {
......
context('Comment Permissions', () => {
const postMenu = 'minds-activity:first > div > m-post-menu';
const deletePostOption = "m-post-menu > ul > li:visible:contains('Delete')";
const deletePostButton = ".m-modal-confirm-buttons > button:contains('Delete')";
before(() => {
//make a post new.
cy.getCookie('minds_sess')
.then((sessionCookie) => {
if (sessionCookie === null) {
return cy.login(true);
}
});
cy.visit('/newsfeed/subscriptions');
cy.location('pathname')
.should('eq', `/newsfeed/subscriptions`);
});
afterEach(() => {
//delete the post
cy.get(postMenu).click();
cy.get(deletePostOption).click();
cy.get(deletePostButton).click();
});
beforeEach(()=> {
cy.preserveCookies();
cy.post('test post');
});
it('should disable comments', () => {
cy.server();
cy.route("POST", "**/api/v2/permissions/comments/**").as("commentToggle");
cy.get(postMenu)
.click()
.find("li:visible:contains('Disable Comments')")
.click();
cy.wait('@commentToggle').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.equal("success");
expect(xhr.response.body.allowed).to.equal(false);
});
//close menu
cy.get(postMenu)
.click();
cy.get('minds-activity:first')
.find("i:contains('speaker_notes_off')")
.click();
});
it('should allow comments', () => {
cy.server();
cy.route("POST", "**/api/v2/permissions/comments/**").as("commentToggle");
cy.get(postMenu)
.click()
.find("li:visible:contains('Disable Comments')")
.click();
cy.wait('@commentToggle').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.equal("success");
expect(xhr.response.body.allowed).to.equal(false);
});
//Menu stays open
cy.get("li:visible:contains('Allow Comments')")
.click();
cy.wait('@commentToggle').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.equal("success");
expect(xhr.response.body.allowed).to.equal(true);
});
//close menu
cy.get(postMenu)
.click();
cy.get('minds-activity:first')
.find("i:contains('chat_bubble')");
});
});
\ No newline at end of file
......@@ -51,6 +51,98 @@ 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').then((poster) => {
if (poster.find('.m-poster-date-selector__input').length > 0) {
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.get('minds-newsfeed-poster').then((poster) => {
if (poster.find('.m-poster-date-selector__input').length > 0) {
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');
......
......@@ -4,7 +4,10 @@
*/
import generateRandomId from '../support/utilities';
context('Notification', () => {
// Skipped until notifications are fixed on sandboxes
// See https://gitlab.com/minds/engine/issues/732
context.skip('Notification', () => {
//secondary user for testing.
let username = '';
......
......@@ -18,6 +18,7 @@ import { FeaturesService } from './services/features.service';
import { ThemeService } from './common/services/theme.service';
import { BannedService } from './modules/report/banned/banned.service';
import { DiagnosticsService } from './services/diagnostics.service';
import { RouterHistoryService } from './common/services/router-history.service';
@Component({
moduleId: module.id,
......@@ -52,7 +53,8 @@ export class Minds {
public featuresService: FeaturesService,
public themeService: ThemeService,
private bannedService: BannedService,
private diagnostics: DiagnosticsService
private diagnostics: DiagnosticsService,
private routerHistoryService: RouterHistoryService
) {
this.name = 'Minds';
}
......
import { NgModule } from '@angular/core';
import { CommonModule as NgCommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { RouterModule, Router } from '@angular/router';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MINDS_PIPES } from './pipes/pipes';
......@@ -102,8 +102,10 @@ 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';
import { RouterHistoryService } from './services/router-history.service';
@NgModule({
imports: [NgCommonModule, RouterModule, FormsModule, ReactiveFormsModule],
......@@ -194,6 +196,8 @@ import { ShareModalComponent } from '../modules/modals/share/share';
SwitchComponent,
FeaturedContentComponent,
PosterDateSelectorComponent,
],
exports: [
MINDS_PIPES,
......@@ -278,6 +282,7 @@ import { ShareModalComponent } from '../modules/modals/share/share';
SwitchComponent,
NSFWSelectorComponent,
FeaturedContentComponent,
PosterDateSelectorComponent,
ChannelModeSelectorComponent,
],
providers: [
......@@ -338,6 +343,11 @@ import { ShareModalComponent } from '../modules/modals/share/share';
new FeaturedContentService(boostedContentService),
deps: [FeedsService],
},
{
provide: RouterHistoryService,
useFactory: router => new RouterHistoryService(router),
deps: [Router],
},
],
entryComponents: [
NotificationsToasterComponent,
......
......@@ -8,6 +8,8 @@ import {
ChangeDetectorRef,
ComponentRef,
ElementRef,
Injector,
SkipSelf,
} from '@angular/core';
import { DynamicHostDirective } from '../../directives/dynamic-host.directive';
......@@ -20,12 +22,14 @@ import { VideoCard } from '../../../modules/legacy/components/cards/object/video
import { AlbumCard } from '../../../modules/legacy/components/cards/object/album/album';
import { BlogCard } from '../../../modules/blogs/card/card';
import { CommentComponentV2 } from '../../../modules/comments/comment/comment.component';
import { ActivityService } from '../../services/activity.service';
@Component({
selector: 'minds-card',
template: `
<ng-template dynamic-host></ng-template>
`,
providers: [ActivityService],
})
export class MindsCard implements AfterViewInit {
@ViewChild(DynamicHostDirective, { static: true })
......@@ -43,7 +47,10 @@ export class MindsCard implements AfterViewInit {
private initialized: boolean = false;
constructor(private _componentFactoryResolver: ComponentFactoryResolver) {}
constructor(
private _componentFactoryResolver: ComponentFactoryResolver,
private _injector: Injector
) {}
@Input('object') set _object(value: any) {
const oldType = this.type;
......@@ -121,7 +128,11 @@ export class MindsCard implements AfterViewInit {
viewContainerRef.clear();
this.componentRef = viewContainerRef.createComponent(componentFactory);
this.componentRef = viewContainerRef.createComponent(
componentFactory,
undefined,
this._injector
);
this.componentInstance = this.componentRef.instance;
this.anchorRef = viewContainerRef.element;
......
......@@ -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')
? []
......
......@@ -34,7 +34,7 @@ export class FeaturedContentComponent implements OnInit {
protected componentFactoryResolver: ComponentFactoryResolver,
protected cd: ChangeDetectorRef,
protected clientMetaService: ClientMetaService,
@SkipSelf() injector: Injector
@SkipSelf() protected injector: Injector
) {
this.clientMetaService.inherit(injector).setMedium('featured-content');
}
......@@ -81,7 +81,11 @@ export class FeaturedContentComponent implements OnInit {
const componentRef: ComponentRef<
any
> = this.dynamicHost.viewContainerRef.createComponent(componentFactory);
> = this.dynamicHost.viewContainerRef.createComponent(
componentFactory,
void 0,
this.injector
);
injector.call(this, componentRef, this.entity);
}
}
......
......@@ -198,6 +198,31 @@
Unblock user
</li>
</ng-container>
<!-- ALLOW COMMENTS -->
<ng-container
*ngIf="
featuresService.has('allow-comments-toggle') &&
options.indexOf('allow-comments') !== -1 &&
entity.ownerObj.guid == session.getLoggedInUser().guid
"
>
<li
class="mdl-menu__item"
*ngIf="!entity.allow_comments"
(click)="allowComments(true)"
i18n="@@COMMON__POST_MENU__ALLOW_COMMENTS"
>
Allow Comments
</li>
<li
class="mdl-menu__item"
*ngIf="entity.allow_comments"
(click)="allowComments(false)"
i18n="@@COMMON__POST_MENU__DISABLE_COMMENTS"
>
Disable Comments
</li>
</ng-container>
<!-- ADMIN EDIT FLAGS -->
<ng-container
*ngIf="options.indexOf('set-explicit') !== -1 && session.isAdmin()"
......
......@@ -21,7 +21,11 @@ import { sessionMock } from '../../../../tests/session-mock.spec';
import { FormsModule } from '@angular/forms';
import { RouterTestingModule } from '@angular/router/testing';
import { BlockListService } from '../../services/block-list.service';
import { ActivityService } from '../../services/activity.service';
import { FeaturesService } from '../../../services/features.service';
import { activityServiceMock } from '../../../../tests/activity-service-mock.spec';
import { storageMock } from '../../../../tests/storage-mock.spec';
import { featuresServiceMock } from '../../../../tests/features-service-mock.spec';
/* tslint:disable */
/* Mock section */
......@@ -93,6 +97,8 @@ describe('PostMenuComponent', () => {
{ provide: Client, useValue: clientMock },
{ provide: Session, useValue: sessionMock },
{ provide: OverlayModalService, useValue: overlayModalServiceMock },
{ provide: ActivityService, useValue: activityServiceMock },
{ provide: FeaturesService, useValue: featuresServiceMock },
{ provide: Storage, useValue: storageMock },
{
provide: BlockListService,
......@@ -107,6 +113,7 @@ describe('PostMenuComponent', () => {
// synchronous beforeEach
beforeEach(() => {
featuresServiceMock.mock('allow-comments-toggle', true);
fixture = TestBed.createComponent(PostMenuComponent);
comp = fixture.componentInstance;
......@@ -152,4 +159,25 @@ describe('PostMenuComponent', () => {
'api/v1/block/1'
);
});
it('should allow comments', () => {
spyOn(comp.optionSelected, 'emit');
comp.allowComments(true);
expect(activityServiceMock.toggleAllowComments).toHaveBeenCalledWith(
comp.entity,
true
);
expect(comp.entity.allow_comments).toEqual(true);
});
it('should disable comments', () => {
spyOn(comp.optionSelected, 'emit');
comp.allowComments(false);
expect(activityServiceMock.toggleAllowComments).toHaveBeenCalledWith(
comp.entity,
false
);
expect(comp.entity.allow_comments).toEqual(false);
});
});
......@@ -5,6 +5,7 @@ import {
EventEmitter,
Input,
Output,
OnInit,
} from '@angular/core';
import { Session } from '../../../services/session';
import { OverlayModalService } from '../../../services/ux/overlay-modal';
......@@ -13,6 +14,8 @@ import { ReportCreatorComponent } from '../../../modules/report/creator/creator.
import { MindsUser } from '../../../interfaces/entities';
import { SignupModalService } from '../../../modules/modals/signup/service';
import { BlockListService } from '../../services/block-list.service';
import { ActivityService } from '../../../common/services/activity.service';
import { FeaturesService } from '../../../services/features.service';
import { ShareModalComponent } from '../../../modules/modals/share/share';
type Option =
......@@ -33,7 +36,8 @@ type Option =
| 'subscribe'
| 'unsubscribe'
| 'rating'
| 'block';
| 'block'
| 'allow-comments';
@Component({
moduleId: module.id,
......@@ -41,7 +45,7 @@ type Option =
templateUrl: 'post-menu.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PostMenuComponent {
export class PostMenuComponent implements OnInit {
@Input() entity: any;
@Input() options: Array<Option>;
@Output() optionSelected: EventEmitter<Option> = new EventEmitter<Option>();
......@@ -70,11 +74,15 @@ export class PostMenuComponent {
private cd: ChangeDetectorRef,
private overlayModal: OverlayModalService,
public signupModal: SignupModalService,
protected blockListService: BlockListService
protected blockListService: BlockListService,
protected activityService: ActivityService,
public featuresService: FeaturesService
) {
this.initCategories();
}
ngOnInit() {}
initCategories() {
for (let category in window.Minds.categories) {
this.categories.push({
......@@ -342,6 +350,17 @@ export class PostMenuComponent {
this.entity.nsfw = nsfw;
}
async allowComments(areAllowed: boolean) {
this.entity.allow_comments = areAllowed;
const result = await this.activityService.toggleAllowComments(
this.entity,
areAllowed
);
if (result !== areAllowed) {
this.entity.allow_comments = result;
}
}
openShareModal() {
this.overlayModal
.create(ShareModalComponent, this.entity.url, {
......
<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;
......
import { EventEmitter, Injectable } from '@angular/core';
import { Client } from '../../services/api/client';
import { BehaviorSubject, Observable } from 'rxjs';
@Injectable()
export class ActivityService {
public allowComment$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(
true
);
constructor(private client: Client) {}
public async toggleAllowComments(entity: any, areAllowed: boolean) {
const payload = {
allowed: areAllowed,
};
const oldValue = entity['allow_comments'];
try {
await this.client.post(
`api/v2/permissions/comments/${entity.guid}`,
payload
);
this.allowComment$.next(areAllowed);
return areAllowed;
} catch (ex) {
console.error('Error posting activity comment permissions', ex);
return oldValue;
}
}
}
import { Router, RoutesRecognized, ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs';
import { filter, pairwise } from 'rxjs/operators';
import { Injectable } from '@angular/core';
@Injectable()
export class RouterHistoryService {
eventsSubscription: Subscription;
history: Array<string> = [];
constructor(private router: Router) {
this.eventsSubscription = this.router.events
.pipe(
filter((e: any) => e instanceof RoutesRecognized),
pairwise()
)
.subscribe((e: any) => {
this.history.push(e[0].urlAfterRedirects);
});
}
getPreviousUrl(): string {
return this.history[this.history.length - 1];
}
}
......@@ -5,10 +5,12 @@ import { Subscription } from 'rxjs';
import { Client, Upload } from '../../services/api';
import { MindsTitle } from '../../services/ux/title';
import { Session } from '../../services/session';
import { ActivityService } from '../../common/services/activity.service';
@Component({
selector: 'minds-admin',
templateUrl: 'admin.html',
providers: [ActivityService],
})
export class Admin {
filter: string = '';
......
......@@ -102,6 +102,7 @@
[object]="boost.entity"
class="mdl-card mdl-shadow--8dp"
*ngIf="boost.entity.type == 'activity'"
[attr.data-minds-activity-guid]="boost.entity.guid"
></minds-activity>
<minds-card-group
[group]="boost.entity"
......
......@@ -24,6 +24,8 @@ import { CommonModule as NgCommonModule } from '@angular/common';
import { RouterTestingModule } from '@angular/router/testing';
import { TokenPipe } from '../../../common/pipes/token.pipe';
import { OverlayModalService } from '../../../services/ux/overlay-modal';
import { ActivityService } from '../../../common/services/activity.service';
import { activityServiceMock } from '../../../../tests/activity-service-mock.spec';
import { overlayModalServiceMock } from '../../../../tests/overlay-modal-service-mock.spec';
@Component({
......@@ -144,6 +146,7 @@ describe('AdminBoosts', () => {
providers: [
{ provide: Client, useValue: clientMock },
{ provide: OverlayModalService, useValue: overlayModalServiceMock },
{ provide: ActivityService, useValue: activityServiceMock },
],
}).compileComponents(); // compile template and css
}));
......
......@@ -8,6 +8,7 @@ import { RejectionReasonModalComponent } from './modal/rejection-reason-modal.co
import { Reason, rejectionReasons } from './rejection-reasons';
import { ReportCreatorComponent } from '../../../modules/report/creator/creator.component';
import { OverlayModalService } from '../../../services/ux/overlay-modal';
import { ActivityService } from '../../../common/services/activity.service';
@Component({
moduleId: module.id,
......@@ -42,7 +43,8 @@ export class AdminBoosts {
constructor(
public client: Client,
private overlayModal: OverlayModalService,
private route: ActivatedRoute
private route: ActivatedRoute,
protected activityService: ActivityService
) {}
ngOnInit() {
......
......@@ -19,6 +19,7 @@
*ngIf="entity"
[object]="entity"
class="mdl-card m-border item"
[attr.data-minds-activity-guid]="entity.guid"
></minds-activity>
</div>
<div
......
......@@ -16,8 +16,10 @@ import { RouterTestingModule } from '@angular/router/testing';
import { NewsfeedHashtagSelectorService } from '../../../modules/newsfeed/services/newsfeed-hashtag-selector.service';
import { newsfeedHashtagSelectorServiceMock } from '../../../../tests/newsfeed-hashtag-selector-service-mock.spec';
import { overlayModalServiceMock } from '../../../../tests/overlay-modal-service-mock.spec';
import { activityServiceMock } from '../../../../tests/activity-service-mock.spec';
import { OverlayModalService } from '../../../services/ux/overlay-modal';
import { EventEmitter } from '@angular/core';
import { ActivityService } from '../../../common/services/activity.service';
@Component({
selector: 'minds-activity',
......@@ -72,6 +74,7 @@ describe('AdminFirehose', () => {
useValue: newsfeedHashtagSelectorServiceMock,
},
{ provide: OverlayModalService, useValue: overlayModalServiceMock },
{ provide: ActivityService, useValue: activityServiceMock },
],
}).compileComponents();
}));
......
......@@ -6,7 +6,7 @@ import { ActivatedRoute, Router } from '@angular/router';
import { Subscription } from 'rxjs';
import { NewsfeedHashtagSelectorService } from '../../../modules/newsfeed/services/newsfeed-hashtag-selector.service';
import { ReportCreatorComponent } from '../../../modules/report/creator/creator.component';
import { ActivityService } from '../../../common/services/activity.service';
@Component({
moduleId: module.id,
selector: 'minds-admin-firehose',
......@@ -30,7 +30,8 @@ export class AdminFirehoseComponent implements OnInit, OnDestroy {
public router: Router,
public route: ActivatedRoute,
protected newsfeedHashtagSelectorService: NewsfeedHashtagSelectorService,
private overlayModal: OverlayModalService
private overlayModal: OverlayModalService,
protected activityService: ActivityService
) {
this.paramsSubscription = this.route.params.subscribe(params => {
this.algorithm = params['algorithm'] || 'latest';
......
......@@ -42,6 +42,7 @@
[object]="entity"
class="mdl-card"
*ngIf="entity.type == 'activity'"
[attr.data-minds-activity-guid]="entity.guid"
></minds-activity>
<div class="mdl-card__supporting-text m-action-buttons">
......
......@@ -6,6 +6,7 @@ import { WireRewardsStruc } from '../modules/wire/interfaces/wire.interfaces';
export interface MindsActivityObject {
activity: Array<any>;
pinned: Array<any>;
allow_comments: boolean;
}
export interface MindsBlogEntity {
......@@ -27,6 +28,7 @@ export interface MindsBlogEntity {
time_published?: number;
access_id?: number;
license?: string;
allow_comments: boolean;
}
export interface Message {}
......
......@@ -12,6 +12,7 @@
<minds-form-register
[referrer]="referrer"
(done)="registered()"
parentId="/login"
></minds-form-register>
</div>
</div>
......@@ -25,6 +25,7 @@
<minds-form-register
(done)="registered()"
[referrer]="referrer"
parentId="/register"
></minds-form-register>
</div>
</div>
......
......@@ -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)
);
}
}
///<reference path="../../../../../node_modules/@types/jasmine/index.d.ts"/>
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import {
Component,
EventEmitter,
Input,
Output,
Pipe,
PipeTransform,
NO_ERRORS_SCHEMA,
} from '@angular/core';
import { RouterTestingModule } from '@angular/router/testing';
import { CommonModule as NgCommonModule } from '@angular/common';
import { MindsBlogEntity } from '../../../interfaces/entities';
import { BlogView } from './view';
import { SafePipe } from '../../../common/pipes/safe';
import { Client } from '../../../services/api/client';
import { clientMock } from '../../../../tests/client-mock.spec';
import { sessionMock } from '../../../../tests/session-mock.spec';
import { Session } from '../../../services/session';
import { scrollServiceMock } from '../../../../tests/scroll-service-mock.spec';
import { ScrollService } from '../../../services/ux/scroll';
import { mindsTitleMock } from '../../../mocks/services/ux/minds-title.service.mock.spec';
import { MindsTitle } from '../../../services/ux/title';
import { AttachmentService } from '../../../services/attachment';
import { attachmentServiceMock } from '../../../../tests/attachment-service-mock.spec';
import { contextServiceMock } from '../../../../tests/context-service-mock.spec';
import { ContextService } from '../../../services/context.service';
import { AnalyticsService } from '../../../services/analytics';
import { analyticsServiceMock } from '../../../../tests/analytics-service-mock.spec';
import { ActivityService } from '../../../common/services/activity.service';
import { activityServiceMock } from '../../../../tests/activity-service-mock.spec';
describe('Blog view component', () => {
let comp: BlogView;
let fixture: ComponentFixture<BlogView>;
const blog: MindsBlogEntity = {
guid: '1',
title: 'test blog',
description: 'description',
ownerObj: {},
allow_comments: true,
};
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [BlogView, SafePipe], // declare the test component
imports: [NgCommonModule, RouterTestingModule],
providers: [
{ provide: ActivityService, useValue: activityServiceMock },
{ provide: AnalyticsService, useValue: analyticsServiceMock },
{ provide: AttachmentService, useValue: attachmentServiceMock },
{ provide: Client, useValue: clientMock },
{ provide: ContextService, useValue: contextServiceMock },
{ provide: MindsTitle, useValue: mindsTitleMock },
{ provide: ScrollService, useValue: scrollServiceMock },
{ provide: Session, useValue: sessionMock },
],
schemas: [NO_ERRORS_SCHEMA],
})
.overrideProvider(ActivityService, { useValue: activityServiceMock })
.compileComponents(); // compile template and css
}));
// synchronous beforeEach
beforeEach(() => {
fixture = TestBed.createComponent(BlogView);
comp = fixture.componentInstance;
comp.blog = blog;
fixture.detectChanges();
});
});
import { Component, ElementRef, ViewChild } from '@angular/core';
import {
Component,
ElementRef,
ViewChild,
ChangeDetectorRef,
OnInit,
OnDestroy,
} from '@angular/core';
import { Router } from '@angular/router';
import { Client } from '../../../services/api';
......@@ -12,6 +19,7 @@ import { AttachmentService } from '../../../services/attachment';
import { ContextService } from '../../../services/context.service';
import { optimizedResize } from '../../../utils/optimized-resize';
import { OverlayModalService } from '../../../services/ux/overlay-modal';
import { ActivityService } from '../../../common/services/activity.service';
import { ShareModalComponent } from '../../../modules/modals/share/share';
@Component({
......@@ -22,8 +30,9 @@ import { ShareModalComponent } from '../../../modules/modals/share/share';
class: 'm-blog',
},
templateUrl: 'view.html',
providers: [ActivityService],
})
export class BlogView {
export class BlogView implements OnInit, OnDestroy {
minds;
guid: string;
blog: MindsBlogEntity;
......@@ -50,6 +59,7 @@ export class BlogView {
'set-explicit',
'remove-explicit',
'rating',
'allow-comments',
];
@ViewChild('lockScreen', { read: ElementRef, static: false }) lockScreen;
......@@ -65,6 +75,8 @@ export class BlogView {
private context: ContextService,
public analytics: AnalyticsService,
public analyticsService: AnalyticsService,
protected activityService: ActivityService,
private cd: ChangeDetectorRef,
private overlayModal: OverlayModalService
) {
this.minds = window.Minds;
......@@ -78,7 +90,7 @@ export class BlogView {
}
isVisible() {
//listens every 0.6 seconds
// listens every 0.6 seconds
this.scroll_listener = this.scroll.listen(
e => {
const bounds = this.element.getBoundingClientRect();
......@@ -132,7 +144,9 @@ export class BlogView {
}
ngOnDestroy() {
if (this.scroll_listener) this.scroll.unListen(this.scroll_listener);
if (this.scroll_listener) {
this.scroll.unListen(this.scroll_listener);
}
}
menuOptionSelected(option: string) {
......@@ -165,7 +179,9 @@ export class BlogView {
}
calculateLockScreenHeight() {
if (!this.lockScreen) return;
if (!this.lockScreen) {
return;
}
const lockScreenOverlay = this.lockScreen.nativeElement.querySelector(
'.m-wire--lock-screen'
);
......
......@@ -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>
......
......@@ -20,6 +20,7 @@
class="mdl-card m-border item"
(delete)="delete(activity)"
[slot]="i + 1"
[attr.data-minds-activity-guid]="activity.guid"
></minds-activity>
<minds-activity
*ngFor="let activity of feed; let i = index"
......@@ -28,6 +29,7 @@
class="mdl-card m-border item"
(delete)="delete(activity)"
[slot]="i + (pinned?.length || 0) + 1"
[attr.data-minds-activity-guid]="activity.guid"
></minds-activity>
<infinite-scroll
distance="25%"
......
......@@ -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();
}
}
......@@ -385,7 +385,7 @@
<span *ngIf="comment.replies_count > 0"
>{{ comment.replies_count }} Replies</span
>
<span *ngIf="comment.replies_count <= 0">Reply</span>
<span *ngIf="comment.replies_count <= 0 && canReply">Reply</span>
</span>
</div>
......
......@@ -86,6 +86,7 @@ export class CommentComponent implements OnChanges {
@ViewChild('batchImage', { static: false }) batchImage: ElementRef;
@Input() canEdit: boolean = false;
@Input() canReply = true;
@Output() onReply = new EventEmitter();
......
......@@ -384,11 +384,24 @@
(click)="toggleReplies()"
*ngIf="comment.can_reply"
>
<i class="material-icons">reply</i>
<i
*ngIf="
comment.replies_count > 0 ||
(activityService.allowComment$ | async)
"
class="material-icons"
>reply</i
>
<span *ngIf="comment.replies_count > 0"
>{{ comment.replies_count }} Replies</span
>
<span *ngIf="comment.replies_count <= 0">Reply</span>
<span
*ngIf="
comment.replies_count <= 0 &&
(activityService.allowComment$ | async)
"
>Reply</span
>
</span>
</div>
......
......@@ -10,6 +10,9 @@ import {
Input,
ViewChild,
ElementRef,
OnInit,
OnDestroy,
AfterViewInit,
} from '@angular/core';
import { Session } from '../../../services/session';
......@@ -21,8 +24,9 @@ import { OverlayModalService } from '../../../services/ux/overlay-modal';
import { ReportCreatorComponent } from '../../report/creator/creator.component';
import { CommentsListComponent } from '../list/list.component';
import { TimeDiffService } from '../../../services/timediff.service';
import { Observable } from 'rxjs';
import { Observable, Subscription } from 'rxjs';
import { map } from 'rxjs/operators';
import { ActivityService } from '../../../common/services/activity.service';
import { Router } from '@angular/router';
import { FeaturesService } from '../../../services/features.service';
import { MindsVideoComponent } from '../../media/components/video/video.component';
......@@ -45,7 +49,8 @@ import isMobile from '../../../helpers/is-mobile';
},
],
})
export class CommentComponentV2 implements OnChanges {
export class CommentComponentV2
implements OnChanges, OnInit, OnDestroy, AfterViewInit {
comment: any;
editing: boolean = false;
minds = window.Minds;
......@@ -78,6 +83,7 @@ export class CommentComponentV2 implements OnChanges {
translateToggle: boolean = false;
commentAge$: Observable<number>;
canReply = true;
videoDimensions: Array<any> = null;
@ViewChild('player', { static: false }) player: MindsVideoComponent;
@ViewChild('batchImage', { static: false }) batchImage: ElementRef;
......@@ -98,6 +104,7 @@ export class CommentComponentV2 implements OnChanges {
private timeDiffService: TimeDiffService,
private el: ElementRef,
private router: Router,
protected activityService: ActivityService,
protected featuresService: FeaturesService
) {}
......@@ -118,9 +125,13 @@ export class CommentComponentV2 implements OnChanges {
}
}
ngOnDestroy() {}
@Input('comment')
set _comment(value: any) {
if (!value) return;
if (!value) {
return;
}
this.comment = value;
this.attachment.load(this.comment);
......@@ -147,7 +158,7 @@ export class CommentComponentV2 implements OnChanges {
return;
}
let data = this.attachment.exportMeta();
const data = this.attachment.exportMeta();
data['comment'] = this.comment.description;
this.editing = false;
......
......@@ -49,7 +49,8 @@
!ascendingInProgress &&
!error &&
comments?.length === 0 &&
parent.type == 'activity'
parent.type == 'activity' &&
activityService.allowComment$
"
i18n="@@MINDS__COMMENTS__START_CONVERSATION"
>
......
......@@ -7,6 +7,8 @@ import {
Input,
Renderer,
ViewChild,
OnInit,
OnDestroy,
} from '@angular/core';
import { Client } from '../../../services/api/client';
......@@ -15,6 +17,7 @@ import { Upload } from '../../../services/api/upload';
import { AttachmentService } from '../../../services/attachment';
import { Textarea } from '../../../common/components/editors/textarea.component';
import { SocketsService } from '../../../services/sockets';
import { ActivityService } from '../../../common/services/activity.service';
@Component({
moduleId: module.id,
......@@ -33,10 +36,11 @@ import { SocketsService } from '../../../services/sockets';
useFactory: AttachmentService._,
deps: [Session, Client, Upload],
},
ActivityService,
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CommentsListComponent {
export class CommentsListComponent implements OnInit, OnDestroy {
minds;
object;
guid: string = '';
......@@ -71,7 +75,6 @@ export class CommentsListComponent {
socketSubscriptions: any = {
comment: null,
};
error: string;
@Input() conversation: boolean = false;
......@@ -91,7 +94,8 @@ export class CommentsListComponent {
public attachment: AttachmentService,
public sockets: SocketsService,
private renderer: Renderer,
private cd: ChangeDetectorRef
private cd: ChangeDetectorRef,
public activityService: ActivityService
) {
this.minds = window.Minds;
}
......@@ -99,13 +103,18 @@ export class CommentsListComponent {
set _object(value: any) {
this.object = value;
this.guid = this.object.guid;
if (this.object.entity_guid) this.guid = this.object.entity_guid;
if (this.object.entity_guid) {
this.guid = this.object.entity_guid;
}
this.parent = this.object;
}
set _reversed(value: boolean) {
if (value) this.reversed = true;
else this.reversed = false;
if (value) {
this.reversed = true;
} else {
this.reversed = false;
}
}
ngOnInit() {
......@@ -165,7 +174,7 @@ export class CommentsListComponent {
} else {
this.ascendingInProgress = false;
}
//this.moreDescendingData = true;
// this.moreDescendingData = true;
if (!response.comments) {
if (descending) {
......@@ -178,8 +187,8 @@ export class CommentsListComponent {
return false;
}
let el = this.scrollView.nativeElement;
let previousScrollHeightMinusTop = el.scrollHeight - el.scrollTop;
const el = this.scrollView.nativeElement;
const previousScrollHeightMinusTop = el.scrollHeight - el.scrollTop;
if (descending) {
this.comments = response.comments.concat(this.comments);
......@@ -250,7 +259,7 @@ export class CommentsListComponent {
this.overscrollTimer = setTimeout(() => {
if (this.overscrollAmount < -75) {
//75px
// 75px
this.autoloadPrevious();
}
......@@ -319,13 +328,14 @@ export class CommentsListComponent {
}
// if the list is scrolled to the bottom
let scrolledToBottom =
const scrolledToBottom =
this.scrollView.nativeElement.scrollTop +
this.scrollView.nativeElement.clientHeight >=
this.scrollView.nativeElement.scrollHeight;
if (response.comments[0]._guid == guid)
if (response.comments[0]._guid == guid) {
this.comments.push(response.comments[0]);
}
this.detectChanges();
......@@ -352,14 +362,14 @@ export class CommentsListComponent {
) {
return;
}
let key = 'thumbs:' + direction + ':count';
const key = 'thumbs:' + direction + ':count';
for (let i = 0; i < this.comments.length; i++) {
if (this.comments[i]._guid == guid) {
this.comments[i][key]++;
this.detectChanges();
}
}
//this.comments = this.comments.slice(0);
// this.comments = this.comments.slice(0);
this.detectChanges();
});
......@@ -415,11 +425,11 @@ export class CommentsListComponent {
this.content = this.content.trim();
let data = this.attachment.exportMeta();
const data = this.attachment.exportMeta();
data['comment'] = this.content;
data['parent_path'] = this.parent.child_path || '0:0:0';
let newLength = this.comments.push({
const newLength = this.comments.push({
// Optimistic
description: this.content,
guid: 0,
......@@ -438,7 +448,7 @@ export class CommentsListComponent {
this.commentsScrollEmitter.emit('bottom');
try {
let response: any = await this.client.post(
const response: any = await this.client.post(
'api/v1/comments/' + this.guid,
data
);
......
......@@ -45,7 +45,8 @@
!inProgress &&
!error &&
comments?.length === 0 &&
parent.type == 'activity'
parent.type == 'activity' &&
(activityService.allowComment$ | async)
"
i18n="@@MINDS__COMMENTS__START_CONVERSATION"
>
......@@ -112,6 +113,7 @@
</div>
<m-comment__poster
*ngIf="activityService.allowComment$ | async"
[guid]="guid"
[parent]="parent"
[entity]="entity"
......
......@@ -8,6 +8,8 @@ import {
Output,
Renderer,
ViewChild,
OnInit,
OnDestroy,
} from '@angular/core';
import { Client } from '../../../services/api/client';
......@@ -18,6 +20,9 @@ import { Textarea } from '../../../common/components/editors/textarea.component'
import { SocketsService } from '../../../services/sockets';
import { CommentsService } from '../comments.service';
import { BlockListService } from '../../../common/services/block-list.service';
import { ActivityService } from '../../../common/services/activity.service';
import { Subscription } from 'rxjs';
import { TouchSequence } from 'selenium-webdriver';
@Component({
selector: 'm-comments__thread',
......@@ -25,7 +30,7 @@ import { BlockListService } from '../../../common/services/block-list.service';
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [CommentsService],
})
export class CommentsThreadComponent {
export class CommentsThreadComponent implements OnInit {
minds;
@Input() parent;
@Input() entity;
......@@ -67,7 +72,8 @@ export class CommentsThreadComponent {
public sockets: SocketsService,
private renderer: Renderer,
protected blockListService: BlockListService,
private cd: ChangeDetectorRef
private cd: ChangeDetectorRef,
public activityService: ActivityService
) {
this.minds = window.Minds;
}
......@@ -198,7 +204,7 @@ export class CommentsThreadComponent {
const parent_path = this.parent.child_path || '0:0:0';
let scrolledToBottom =
const scrolledToBottom =
this.scrollView.nativeElement.scrollTop +
this.scrollView.nativeElement.clientHeight >=
this.scrollView.nativeElement.scrollHeight;
......@@ -289,8 +295,6 @@ export class CommentsThreadComponent {
}
onPosted({ comment, index }) {
console.log('onPosted called');
console.log(comment, index);
this.comments[index] = comment;
this.detectChanges();
}
......
......@@ -7,6 +7,8 @@ import {
Input,
Output,
Renderer,
OnInit,
OnDestroy,
} from '@angular/core';
import {
ActivatedRoute,
......@@ -38,7 +40,7 @@ import { CommentsService } from '../comments.service';
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CommentsTreeComponent {
export class CommentsTreeComponent implements OnInit, OnDestroy {
minds;
entity;
guid: string = '';
......
......@@ -12,6 +12,7 @@ import { Client } from '../../../services/api';
import { Session } from '../../../services/session';
import { ReCaptchaComponent } from '../../../modules/captcha/recaptcha/recaptcha.component';
import { ExperimentsService } from '../../experiments/experiments.service';
import { RouterHistoryService } from '../../../common/services/router-history.service';
@Component({
moduleId: module.id,
......@@ -27,6 +28,7 @@ export class RegisterForm {
captcha: string;
takenUsername: boolean = false;
usernameValidationTimeout: any;
@Input() parentId: string = '';
showFbForm: boolean = false;
......@@ -43,7 +45,8 @@ export class RegisterForm {
public client: Client,
fb: FormBuilder,
public zone: NgZone,
private experiments: ExperimentsService
private experiments: ExperimentsService,
private routerHistoryService: RouterHistoryService
) {
this.form = fb.group({
username: ['', Validators.required],
......@@ -53,6 +56,7 @@ export class RegisterForm {
tos: [false],
exclusive_promotions: [false],
captcha: [''],
previousUrl: this.routerHistoryService.getPreviousUrl(),
});
}
......@@ -85,6 +89,7 @@ export class RegisterForm {
}
this.form.value.referrer = this.referrer;
this.form.value.parentId = this.parentId;
this.inProgress = true;
this.client
......
......@@ -39,6 +39,7 @@
[showRatingToggle]="true"
[slot]="i + 1"
class="mdl-card item"
[attr.data-minds-activity-guid]="activity.guid"
>
</minds-activity>
......@@ -49,6 +50,7 @@
[canDelete]="group['is:owner'] || group['is:moderator']"
(delete)="delete(a)"
[slot]="i + (pinned?.length || 0) + 1"
[attr.data-minds-activity-guid]="a.guid"
>
<!-- Menu Actions -->
<li
......
......@@ -52,6 +52,7 @@
[canDelete]="group['is:owner'] || group['is:moderator']"
(delete)="delete(entity)"
[slot]="i + 1"
[attr.data-minds-activity-guid]="entity.guid"
>
<!-- Menu Actions -->
......
......@@ -22,10 +22,12 @@ import { HashtagsSelectorComponent } from '../../hashtags/selector/selector.comp
import { VideoChatService } from '../../videochat/videochat.service';
import { UpdateMarkersService } from '../../../common/services/update-markers.service';
import { filter, map, startWith, throttle } from 'rxjs/operators';
import { ActivityService } from '../../../common/services/activity.service';
@Component({
selector: 'm-groups--profile',
templateUrl: 'profile.html',
providers: [ActivityService],
})
export class GroupsProfile {
guid;
......
......@@ -8,6 +8,7 @@
[canDelete]="group['is:owner'] || group['is:moderator']"
(delete)="delete(entity)"
[hideTabs]="true"
[attr.data-minds-activity-guid]="entity.guid"
>
<!-- Menu Actions -->
......
......@@ -4,5 +4,6 @@
*ngFor="let post of posts; let i = index"
class="mdl-card item"
[object]="post"
[attr.data-minds-activity-guid]="post.guid"
></minds-activity>
</div>
......@@ -24,7 +24,10 @@
</div>
<div class="m-homepage--signup" [hidden]="session.isLoggedIn()">
<minds-form-register (done)="registered()"></minds-form-register>
<minds-form-register
(done)="registered()"
parentId="/"
></minds-form-register>
<div class="m-homepage--app-buttons">
<div class="m-homepage--app-buttons__iosBanner">
......
import { Component, ChangeDetectionStrategy } from '@angular/core';
import {
Component,
ChangeDetectionStrategy,
ChangeDetectorRef,
OnInit,
OnDestroy,
} from '@angular/core';
import { Client } from '../../../../services/api';
import { ActivityService } from '../../../../common/services/activity.service';
@Component({
selector: 'minds-button-comment',
inputs: ['_object: object'],
changeDetection: ChangeDetectionStrategy.OnPush,
changeDetection: ChangeDetectionStrategy.Default,
template: `
<a [ngClass]="{ selected: object['comments:count'] > 0 }">
<i class="material-icons">chat_bubble</i>
<i
class="material-icons"
*ngIf="(activityService.allowComment$ | async) === true"
>chat_bubble</i
>
<i
class="material-icons"
*ngIf="(activityService.allowComment$ | async) === false"
title="Comments have been disabled for this post"
i18n-title="@@COMMENTS__DISABLED"
>
speaker_notes_off
</i>
<span class="minds-counter" *ngIf="object['comments:count'] > 0">{{
object['comments:count'] | number
}}</span>
</a>
`,
})
export class CommentButton {
export class CommentButton implements OnInit, OnDestroy {
object;
constructor(public client: Client) {}
constructor(
public client: Client,
public activityService: ActivityService,
protected cd: ChangeDetectorRef
) {}
ngOnInit() {}
ngOnDestroy() {}
set _object(value: any) {
this.object = value;
this.activityService.allowComment$.next(this.object.allow_comments);
}
}
......@@ -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>
......@@ -27,6 +27,7 @@ import { ActivityAnalyticsOnViewService } from './activity-analytics-on-view.ser
import { NewsfeedService } from '../../../../newsfeed/services/newsfeed.service';
import { ClientMetaService } from '../../../../../common/services/client-meta.service';
import { AutocompleteSuggestionsService } from '../../../../suggestions/services/autocomplete-suggestions.service';
import { ActivityService } from '../../../../../common/services/activity.service';
import { FeaturesService } from '../../../../../services/features.service';
import isMobile from '../../../../../helpers/is-mobile';
......@@ -45,7 +46,11 @@ import isMobile from '../../../../../helpers/is-mobile';
'showRatingToggle',
],
outputs: ['_delete: delete', 'commentsOpened', 'onViewed'],
providers: [ClientMetaService, ActivityAnalyticsOnViewService],
providers: [
ClientMetaService,
ActivityAnalyticsOnViewService,
ActivityService,
],
templateUrl: 'activity.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
......@@ -60,6 +65,7 @@ export class Activity implements OnInit {
translateToggle: boolean = false;
translateEvent: EventEmitter<any> = new EventEmitter();
showBoostOptions: boolean = false;
allowComments = true;
@Input() boost: boolean = false;
@Input('boost-toggle')
@Input()
......@@ -114,6 +120,7 @@ export class Activity implements OnInit {
'set-explicit',
'block',
'rating',
'allow-comments',
];
} else {
return [
......@@ -127,6 +134,7 @@ export class Activity implements OnInit {
'set-explicit',
'block',
'rating',
'allow-comments',
];
}
} else {
......@@ -140,6 +148,7 @@ export class Activity implements OnInit {
'set-explicit',
'block',
'rating',
'allow-comments',
];
}
}
......@@ -147,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,
......@@ -162,6 +173,7 @@ export class Activity implements OnInit {
protected clientMetaService: ClientMetaService,
protected featuresService: FeaturesService,
public suggestions: AutocompleteSuggestionsService,
protected activityService: ActivityService,
@SkipSelf() injector: Injector,
elementRef: ElementRef
) {
......@@ -222,6 +234,11 @@ export class Activity implements OnInit {
this.translationService.isTranslatable(this.activity) ||
(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;
}
getOwnerIconTime() {
......@@ -243,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()) {
......@@ -301,6 +320,9 @@ export class Activity implements OnInit {
}*/
openComments() {
if (!this.shouldShowComments()) {
return;
}
this.commentsToggle = !this.commentsToggle;
this.commentsOpened.emit(this.commentsToggle);
}
......@@ -399,10 +421,11 @@ export class Activity implements OnInit {
this.translateToggle = true;
break;
}
this.detectChanges();
}
setExplicit(value: boolean) {
let oldValue = this.activity.mature,
const oldValue = this.activity.mature,
oldMatureVisibility = this.activity.mature_visibility;
this.activity.mature = value;
......@@ -478,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;
......@@ -500,6 +527,10 @@ export class Activity implements OnInit {
this.activity.mature_visibility = !this.activity.mature_visibility;
}
shouldShowComments() {
return this.activity.allow_comments || this.activity['comments:count'] >= 0;
}
setVideoDimensions($event) {
this.videoDimensions = $event.dimensions;
this.activity.custom_data.dimensions = this.videoDimensions;
......@@ -548,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;
}
}
......@@ -9,6 +9,7 @@ import { Client } from '../../../../../services/api';
import { Session } from '../../../../../services/session';
import { AttachmentService } from '../../../../../services/attachment';
import { ActivityService } from '../../../../../common/services/activity.service';
@Component({
moduleId: module.id,
......@@ -18,6 +19,7 @@ import { AttachmentService } from '../../../../../services/attachment';
host: {
class: 'mdl-shadow--8dp',
},
providers: [ActivityService],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ActivityPreview {
......@@ -76,6 +78,10 @@ export class ActivityPreview {
return false;
}
isScheduled(time_created) {
return false;
}
save() {
/* NOOP */
}
......
......@@ -14,6 +14,7 @@ import { Router } from '@angular/router';
import { Client } from '../../../../../services/api';
import { Session } from '../../../../../services/session';
import { AttachmentService } from '../../../../../services/attachment';
import { ActivityService } from '../../../../../common/services/activity.service';
import { OverlayModalService } from '../../../../../services/ux/overlay-modal';
import { MediaModalComponent } from '../../../../media/modal/modal.component';
import { FeaturesService } from '../../../../../services/features.service';
......@@ -23,6 +24,7 @@ import isMobile from '../../../../../helpers/is-mobile';
moduleId: module.id,
selector: 'minds-remind',
inputs: ['object', '_events: events'],
providers: [ActivityService],
templateUrl: '../activity/activity.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
......@@ -138,6 +140,10 @@ export class Remind {
return activity && activity.pending && activity.pending !== '0';
}
isScheduled(time_created) {
return false;
}
openComments() {
/* NOOP */
}
......
import { Component, EventEmitter } from '@angular/core';
import { Client } from '../../../services/api';
@Component({
<<<<<<< a596794609983ebbce246788506c54ac2ace58a8
selector: 'm-channel--carousel',
inputs: ['_banners: banners', '_editMode: editMode'],
=======
selector: 'minds-carousel',
inputs: ['_banners: banners', '_editMode: editMode', 'blurred'],
>>>>>>> (feat): mark a channel profile explicit
outputs: ['done_event: done', 'delete_event: delete'],
template: `
<i class="material-icons left" (click)="prev()" [hidden]="banners.length <= 1">keyboard_arrow_left</i>
<div *ngFor="let banner of banners; let i = index">
<minds-banner
[src]="banner.src"
[top]="banner.top_offset"
[overlay]="true"
[ngClass]="{'is-hidden': i != index, 'edit-mode': editing, 'm-mature-banner': blurred}"
[editMode]="editing"
[done]="done"
(added)="added($event, i)"
></minds-banner>
<div class="delete-button" (click)="delete(i)" [hidden]="i != index || !editing">
<button class="mdl-button mdl-button--raised mdl-button--colored material-icons">X</button>
</div>
</div>
<i class="material-icons right" (click)="next()" [hidden]="banners.length <= 1">keyboard_arrow_right</i>
`
})
export class CarouselComponent {
minds: Minds = window.Minds;
banners: Array<any> = [];
editing: boolean = false;
src: string = '';
modified: Array<any> = []; //all banners should be exported to here on the done event, and sent to parent
done_event = new EventEmitter();
delete_event = new EventEmitter();
done: boolean = false; //if set to true, tells the child component to return "added"
rotate: boolean = true; //if set to true enabled rotation
rotate_timeout; //the timeout for the rotator
interval: number = 3000; //the interval for each banner to stay before rotating
index: number = 0; //the current visible index of the carousel.
blurred: boolean = false;
constructor() {
this.run();
}
/**
* A list of banners are sent from the parent, if done are sent a blank one is entered
*/
set _banners(value: any) {
if (value) {
this.banners = value;
} else {
this.banners.push({
src: null
});
}
}
/**
* If the parent set edit mode
*/
set _editMode(value: boolean) {
console.log('[carousel]: edit mode event received');
//was in edit more, now settings not in edit more
if (this.editing && !value) {
console.log('[carousel]: edit mode ended');
this._done();
return;
}
this.editing = value;
if (!this.editing) {
return;
}
console.log('[carousel]: edit mode enabled');
this.rotate = false;
this.done = false;
var blank_banner = false;
for (var i in this.banners) {
if (!this.banners[i].src)
blank_banner = true;
}
if (!blank_banner) {
this.banners.push({
src: null
});
}
}
/**
* Fired when the child component adds a new banner
*/
added(value: any, index) {
console.log(this.banners[index].guid, value.file);
if (!this.banners[index].guid && !value.file)
return; //this is our 'add new' post
//detect if we have changed
var changed = false;
if (value.top !== this.banners[index].top)
changed = false;
if (value.file)
changed = true;
if (!changed)
return;
if (!this.banners[index].src) {
this.banners[index].src = value.file;
}
this.modified.push({
guid: this.banners[index].guid,
index: index,
file: value.file,
top: value.top
});
}
delete(index) {
this.delete_event.next(this.banners[index]);
this.banners.splice(index, 1);
if (this.banners.length === 0) {
this.banners.push({ src: null });
}
this.next();
}
/**
* Once we retreive all the modified banners, we fire back to the parent the new list
*/
_done() {
this.editing = false; //this should update each banner (I'd prefer even driven but change detection works..)
this.done = true;
console.log('[carousel]: received done event');
//after one second?
setTimeout(() => {
this.done_event.next(this.modified);
this.modified = [];
let blank_banner: any = false;
for (var i in this.banners) {
if (!this.banners[i].src)
blank_banner = i;
}
if (blank_banner !== false) {
this.banners.splice(blank_banner, 1);
this.next();
}
}, 1000);
}
prev() {
var max = this.banners.length - 1;
if (this.index === 0)
this.index = max;
else
this.index--;
this.run();//resets the carousel
}
next() {
var max = this.banners.length - 1;
if (this.index >= max)
this.index = 0;
else
this.index++;
this.run();//resets the carousel
}
run() {
if (this.rotate_timeout)
clearTimeout(this.rotate_timeout);
this.rotate_timeout = setTimeout(() => {
if (this.rotate) {
var max = this.banners.length - 1;
if (this.index >= max)
this.index = 0;
else
this.index++;
}
this.run();
}, this.interval);
}
ngOnDestroy() {
clearTimeout(this.rotate_timeout);
}
}
......@@ -121,7 +121,7 @@ export class MindsVideoDirectHttpPlayer
try {
player.play();
} catch (e) {
console.error(e);
console.log(e);
}
}
......@@ -131,7 +131,7 @@ export class MindsVideoDirectHttpPlayer
try {
player.pause();
} catch (e) {
console.error(e);
console.log(e);
}
}
......@@ -146,13 +146,14 @@ export class MindsVideoDirectHttpPlayer
}
resumeFromTime(time: number = 0) {
// TODO detect if it's still transcoding
const player = this.getPlayer();
try {
player.currentTime = time;
this.play();
} catch (e) {
console.error(e);
console.log(e);
}
}
......
......@@ -195,7 +195,7 @@ export class MindsVideoTorrentPlayer
this.loadTorrent();
}
} catch (e) {
console.error(e);
console.log(e);
}
}
......@@ -205,7 +205,7 @@ export class MindsVideoTorrentPlayer
try {
player.pause();
} catch (e) {
console.error(e);
console.log(e);
}
}
......@@ -220,6 +220,7 @@ export class MindsVideoTorrentPlayer
}
resumeFromTime(time: number = 0) {
// TODO detect if it's still transcoding
const player = this.getPlayer();
try {
......@@ -234,7 +235,7 @@ export class MindsVideoTorrentPlayer
}
}
} catch (e) {
console.error(e);
console.log(e);
}
}
......
......@@ -183,7 +183,7 @@ export class MindsVideoComponent implements OnDestroy {
}
onError({ player, e }: { player?; e? } = {}) {
console.error('Received error when trying to reproduce video', e, player);
console.log('Received error when trying to reproduce video', e, player);
setTimeout(() => this.fallback(), 0);
}
......
......@@ -18,7 +18,7 @@ m-overlay-modal {
.m-overlay-modal--backdrop {
z-index: 9999995;
@include m-theme() {
background-color: rgba(themed($m-black-always), 0.85);
background-color: rgba(themed($m-black-always), 0.8);
}
}
......@@ -144,9 +144,6 @@ m-overlay-modal {
left: unquote('calc(50% - 50px)');
}
m-video--progress-bar {
.seeker-ball {
top: 4px;
}
.progress-bar {
margin-right: 8px;
}
......
......@@ -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;
}
......
......@@ -275,7 +275,7 @@
<!-- Don't show comments for albums -->
<div
class="mdl-grid m-media-content--comments mdl-color--white"
*ngIf="entity.guid && entity.subtype != 'album'"
*ngIf="canShowComments() || (activityService.allowComment$ | async)"
>
<m-comments__tree [entity]="entity"> </m-comments__tree>
</div>
......
import { ChangeDetectorRef, Component } from '@angular/core';
import { ChangeDetectorRef, Component, OnInit, OnDestroy } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Subscription } from 'rxjs';
......@@ -10,6 +10,7 @@ import { RecommendedService } from '../components/video/recommended.service';
import { AttachmentService } from '../../../services/attachment';
import { ContextService } from '../../../services/context.service';
import { MindsTitle } from '../../../services/ux/title';
import { ActivityService } from '../../../common/services/activity.service';
@Component({
moduleId: module.id,
......@@ -21,9 +22,10 @@ import { MindsTitle } from '../../../services/ux/title';
useFactory: RecommendedService._,
deps: [Client],
},
ActivityService,
],
})
export class MediaViewComponent {
export class MediaViewComponent implements OnInit, OnDestroy {
minds = window.Minds;
guid: string;
entity: any = {};
......@@ -32,6 +34,7 @@ export class MediaViewComponent {
deleteToggle: boolean = false;
theaterMode: boolean = false;
allowComments = true;
menuOptions: Array<string> = [
'edit',
......@@ -43,6 +46,8 @@ export class MediaViewComponent {
'subscribe',
'remove-explicit',
'rating',
'allow-comments',
'disable-comments',
];
paramsSubscription: Subscription;
......@@ -57,7 +62,8 @@ export class MediaViewComponent {
public route: ActivatedRoute,
public attachment: AttachmentService,
public context: ContextService,
private cd: ChangeDetectorRef
private cd: ChangeDetectorRef,
protected activityService: ActivityService
) {}
ngOnInit() {
......@@ -100,7 +106,7 @@ export class MediaViewComponent {
}
if (response.entity) {
this.entity = response.entity;
this.allowComments = this.entity['allow_comments'];
switch (this.entity.subtype) {
case 'video':
this.context.set('object:video');
......@@ -174,6 +180,14 @@ export class MediaViewComponent {
case 'remove-explicit':
this.setExplicit(false);
break;
case 'allow-comments':
this.entity.allow_comments = true;
this.activityService.toggleAllowComments(this.entity, true);
break;
case 'disable-comments':
this.entity.allow_comments = false;
this.activityService.toggleAllowComments(this.entity, false);
break;
}
}
......@@ -191,6 +205,17 @@ export class MediaViewComponent {
});
}
canShowComments() {
if (!this.entity.guid) {
return false;
}
//Don't show comments on albums
if (this.entity.subtype === 'album') {
return false;
}
return this.entity['comments:count'] >= 1;
}
private detectChanges() {
this.cd.markForCheck();
this.cd.detectChanges();
......
......@@ -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() {
......
......@@ -8,7 +8,7 @@
[hidden]="display == 'onboarding' || display == 'categories'"
>
<img
src="{{minds.cdn_assets_url}}assets/logos/logo.svg"
src="{{ minds.cdn_assets_url }}assets/logos/logo.svg"
(click)="close()"
/>
</div>
......@@ -69,6 +69,7 @@
(done)="done('register')"
(canceled)="close()"
*ngIf="display == 'register'"
parentId="/modal"
></minds-form-register>
<!-- FB Signin final phase -->
<minds-form-fb-register
......
......@@ -48,6 +48,7 @@
[showBoostMenuOptions]="true"
[slot]="i + 1"
[visibilityEvents]="false"
[attr.data-minds-activity-guid]="boost.guid"
#activities
></minds-activity>
</ng-container>
......@@ -17,6 +17,7 @@
(delete)="delete(activity)"
[slot]="i + 1"
class="mdl-card m-border item"
[attr.data-minds-activity-guid]="activity.guid"
>
</minds-activity>
</ng-container>
......
......@@ -14,6 +14,7 @@
[showRatingToggle]="true"
[slot]="slot"
class="mdl-card m-border item"
[attr.data-minds-activity-guid]="entity.guid"
></minds-activity>
</ng-container>
</ng-container>
......@@ -21,6 +21,7 @@
(delete)="delete(preActivity)"
[showRatingToggle]="true"
class="mdl-card m-border item"
[attr.minds-data-activity-guid]="preActivity.guid"
></minds-activity>
<m-newsfeed--boost-rotator
......@@ -46,6 +47,7 @@
(delete)="delete(activity)"
[showRatingToggle]="true"
[slot]="i + 1"
[attr.data-minds-activity-guid]="activity.guid"
></minds-activity>
</ng-container>
......
......@@ -7,6 +7,7 @@
[showRatingToggle]="true"
class="mdl-card m-border item"
[slot]="i + 1"
[attr.data-minds-activity-guid]="activity.guid"
></minds-activity>
<infinite-scroll
distance="25%"
......
......@@ -9,6 +9,7 @@
[showRatingToggle]="true"
[slot]="i + 1"
class="mdl-card m-border item"
[attr.data-minds-activity-guid]="activity.guid"
></minds-activity>
<infinite-scroll
distance="25%"
......
......@@ -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;
}
}
......@@ -27,9 +27,9 @@ export class NewsfeedService {
channel = null,
clientMeta = {}
) {
if (!this.session.isLoggedIn()) {
return;
}
// if (!this.session.isLoggedIn()) {
// return;
// }
// if it's a boost we record the boost view AND the activity view
if (entity.boosted_guid) {
......
......@@ -36,6 +36,7 @@
[showRatingToggle]="true"
[slot]="1"
class="mdl-card mdl-shadow--2dp item"
[attr.data-minds-activity-guid]="activity.guid"
>
</minds-activity>
</div>
......
......@@ -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()"
......
......@@ -47,6 +47,7 @@ import { sessionMock } from '../../../../tests/session-mock.spec';
import { web3WalletServiceMock } from '../../../../tests/web3-wallet-service-mock.spec';
import { IfFeatureDirective } from '../../../common/directives/if-feature.directive';
import { FeaturesService } from '../../../services/features.service';
import { featuresServiceMock } from '../../../../tests/features-service-mock.spec';
import { MockComponent } from '../../../utils/mock';
/* tslint:disable */
......@@ -219,7 +220,7 @@ describe('WireCreatorComponent', () => {
{ provide: WireContractService, useValue: wireContractServiceMock },
{ provide: WireService, useValue: wireServiceMock },
Web3WalletService,
FeaturesService,
{ provide: FeaturesService, useValue: featuresServiceMock },
{ provide: Web3WalletService, useValue: web3WalletServiceMock },
{ provide: OverlayModalService, useValue: overlayModalServiceMock },
{ provide: TokenContractService, useValue: tokenContractServiceMock },
......@@ -238,7 +239,7 @@ describe('WireCreatorComponent', () => {
jasmine.clock().uninstall();
jasmine.clock().install();
fixture = TestBed.createComponent(WireCreatorComponent);
featuresServiceMock.mock('wire-multi-currency', true);
comp = fixture.componentInstance; // LoginForm test instance
clientMock.response = {};
clientMock.response[`api/v2/boost/rates`] = {
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.