...
 
Commits (23)
......@@ -31,6 +31,8 @@ tmtags
Thumbs.db
Desktop.ini
node_modules
cypress/screenshots
cypress/videos
# don't ignore travis config
!/.travis.yml
!/.drone.yml
......
......@@ -16,8 +16,8 @@ stages:
variables:
CYPRESS_INSTALL_BINARY: 0 # Speeds up the install process
npm_config_cache: "$CI_PROJECT_DIR/.npm"
CYPRESS_CACHE_FOLDER: "$CI_PROJECT_DIR/cache/Cypress"
npm_config_cache: '$CI_PROJECT_DIR/.npm'
CYPRESS_CACHE_FOLDER: '$CI_PROJECT_DIR/cache/Cypress'
test:
image: circleci/node:8-browsers
......@@ -68,14 +68,14 @@ e2e:chrome:
build:review:
stage: build
before_script:
- sed -ri "s|\"VERSION\"|\"$CI_PIPELINE_ID\"|" src/environments/environment.prod.ts
- sed -ri "s|'VERSION'|'$CI_PIPELINE_ID'|" src/environments/environment.prod.ts
script:
- npm ci && npm install -g gulp-cli
- npm run postinstall
- gulp build.sass && gulp build.sass ##weird build needs to be run twice for now
- sh build/base-locale.sh dist
artifacts:
name: "$CI_COMMIT_REF_SLUG"
name: '$CI_COMMIT_REF_SLUG'
paths:
- dist
except:
......@@ -86,14 +86,14 @@ build:review:
build:production:en:
stage: build
before_script:
- sed -ri "s|\"VERSION\"|\"$CI_PIPELINE_ID\"|" src/environments/environment.prod.ts
- sed -ri "s|'VERSION'|'$CI_PIPELINE_ID'|" src/environments/environment.prod.ts
script:
- npm ci && npm install -g gulp-cli
- npm run postinstall
- gulp build.sass --deploy-url=https://cdn-assets.minds.com/front/dist/en && gulp build.sass --deploy-url=https://cdn-assets.minds.com/front/dist/en ##weird build needs to be run twice for now
- sh build/base-locale.sh dist https://cdn-assets.minds.com/front/dist
artifacts:
name: "$CI_COMMIT_REF_SLUG"
name: '$CI_COMMIT_REF_SLUG'
paths:
- dist/en
only:
......@@ -104,14 +104,14 @@ build:production:en:
build:production:i18n:
stage: build
before_script:
- sed -ri "s|\"VERSION\"|\"$CI_PIPELINE_ID\"|" src/environments/environment.prod.ts
- sed -ri "s|'VERSION'|'$CI_PIPELINE_ID'|" src/environments/environment.prod.ts
script:
- npm ci && npm install -g gulp-cli
- npm run postinstall
- gulp build.sass --deploy-url=https://cdn-assets.minds.com/front/dist/en && gulp build.sass --deploy-url=https://cdn-assets.minds.com/front/dist/en ##weird build needs to be run twice for now
- sh build/i18n-locales-all.sh dist https://cdn-assets.minds.com/front/dist
artifacts:
name: "$CI_COMMIT_REF_SLUG"
name: '$CI_COMMIT_REF_SLUG'
paths:
- dist/vi
only:
......@@ -138,7 +138,7 @@ prepare:review:
stage: prepare
image: minds/ci:latest
script:
- docker login -u gitlab-ci-token -p ${CI_BUILD_TOKEN} ${CI_REGISTRY}
- docker login -u gitlab-ci-token -p ${CI_BUILD_TOKEN} ${CI_REGISTRY}
- docker build -t $CI_REGISTRY_IMAGE/front-init:$CI_PIPELINE_ID -f containers/front-init/Dockerfile dist/.
- docker push $CI_REGISTRY_IMAGE/front-init:$CI_PIPELINE_ID
dependencies:
......@@ -151,7 +151,7 @@ prepare:review:
prepare:review:sentry:
<<: *sentry_prepare
variables:
SOURCEMAP_PREFIX: "~/en"
SOURCEMAP_PREFIX: '~/en'
except:
refs:
- master
......@@ -163,7 +163,7 @@ prepare:production:
stage: prepare
image: minds/ci:latest
script:
- docker login -u gitlab-ci-token -p ${CI_BUILD_TOKEN} ${CI_REGISTRY}
- docker login -u gitlab-ci-token -p ${CI_BUILD_TOKEN} ${CI_REGISTRY}
- docker build -t $CI_REGISTRY_IMAGE/front-init:$CI_PIPELINE_ID -f containers/front-init/Dockerfile dist/.
- docker push $CI_REGISTRY_IMAGE/front-init:$CI_PIPELINE_ID
only:
......@@ -177,7 +177,7 @@ prepare:production:
prepare:production:sentry:
<<: *sentry_prepare
variables:
SOURCEMAP_PREFIX: "~/front/dist/en"
SOURCEMAP_PREFIX: '~/front/dist/en'
only:
refs:
- master
......@@ -185,7 +185,7 @@ prepare:production:sentry:
dependencies:
- build:production:en
- build:production:i18n
################
# Review Stage #
################
......@@ -201,7 +201,7 @@ prepare:production:sentry:
action: stop
variables:
GIT_STRATEGY: none
except:
except:
refs:
- master
- test/gitlab-ci
......@@ -213,22 +213,22 @@ review:start:
- aws eks update-kubeconfig --name=sandbox
- git clone --branch=sandbox-wip https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com/minds/helm-charts.git
- "helm upgrade \
--install \
--reuse-values \
--set frontInit.image.repository=$CI_REGISTRY_IMAGE/front-init \
--set-string frontInit.image.tag=$CI_PIPELINE_ID \
--set domain=$CI_BUILD_REF_SLUG.$KUBE_INGRESS_BASE_DOMAIN \
--set elasticsearch.clusterName=$CI_BUILD_REF_SLUG--elasticsearch \
--wait \
$CI_BUILD_REF_SLUG \
./helm-charts/minds"
--install \
--reuse-values \
--set frontInit.image.repository=$CI_REGISTRY_IMAGE/front-init \
--set-string frontInit.image.tag=$CI_PIPELINE_ID \
--set domain=$CI_BUILD_REF_SLUG.$KUBE_INGRESS_BASE_DOMAIN \
--set elasticsearch.clusterName=$CI_BUILD_REF_SLUG--elasticsearch \
--wait \
$CI_BUILD_REF_SLUG \
./helm-charts/minds"
# Update sentry
- sentry-cli releases deploys $CI_PIPELINE_ID new -e review-$CI_COMMIT_REF_SLUG
environment:
name: review/$CI_COMMIT_REF_NAME
url: https://$CI_BUILD_REF_SLUG.$KUBE_INGRESS_BASE_DOMAIN
on_stop: review:stop
except:
except:
refs:
- master
- test/gitlab-ci
......@@ -249,7 +249,7 @@ review:stop:
- aws s3 sync dist $S3_REPOSITORY_URL
- $(aws ecr get-login --no-include-email --region us-east-1)
## Update docker front-init container
- docker login -u gitlab-ci-token -p ${CI_BUILD_TOKEN} ${CI_REGISTRY}
- docker login -u gitlab-ci-token -p ${CI_BUILD_TOKEN} ${CI_REGISTRY}
- docker pull $CI_REGISTRY_IMAGE/front-init:$CI_PIPELINE_ID
- docker tag $CI_REGISTRY_IMAGE/front-init:$CI_PIPELINE_ID $ECR_REPOSITORY_URL:$IMAGE_LABEL
- docker push $ECR_REPOSITORY_URL:$IMAGE_LABEL
......@@ -269,7 +269,7 @@ staging:fpm:
<<: *deploy
stage: deploy:staging
variables:
IMAGE_LABEL: "staging"
IMAGE_LABEL: 'staging'
ECS_SERVICE: $ECS_APP_STAGING_SERVICE
environment:
name: staging
......@@ -279,7 +279,7 @@ deploy:canary:
<<: *deploy
stage: deploy:canary
variables:
IMAGE_LABEL: "canary"
IMAGE_LABEL: 'canary'
ECS_SERVICE: $ECS_APP_CANARY_SERVICE
environment:
name: canary
......@@ -291,7 +291,7 @@ deploy:production:
<<: *deploy
stage: deploy:production
variables:
IMAGE_LABEL: "production"
IMAGE_LABEL: 'production'
ECS_SERVICE: $ECS_APP_PRODUCTION_SERVICE
environment:
name: production
......@@ -306,7 +306,7 @@ deploy:production:
cleanup:review: # We stop the review site after the e2e tests have run
<<: *cleanup_review
stage: cleanup
except:
except:
refs:
- master
- test/gitlab-ci
{
"projectId": "qrjqcv",
"requestTimeout": 3600000,
"responseTimeout": 3600000,
"pageLoadTimeout": 3600000
......
// import 'cypress-file-upload';
context('Blogs', () => {
beforeEach(() => {
cy.login(true);
before(() => {
cy.clearCookies();
cy.getCookie('minds_sess')
.then((sessionCookie) => {
if (sessionCookie === null) {
return cy.login(true);
}
});
});
cy.location('pathname', { timeout: 30000 })
.should('eq', `/newsfeed/subscriptions`);
})
beforeEach(()=> {
cy.preserveCookies();
});
it('should not be able to create a new blog if no title or banner are specified', () => {
it('should not be able to create a new blog if no title or banner are specified', () => {
cy.visit('/blog/edit/new');
cy.get('.m-button--submit').click();
cy.wait(100);
cy.get('.m-blog--edit--error').contains('Error: You must provide a title');
......@@ -26,28 +31,6 @@ context('Blogs', () => {
cy.get('.m-blog--edit--error').contains('Error: You must upload a banner');
})
// TODO: remove the x when we run tests in new users each time
xit("should not be able to create a new blog if the channel doesn't have an avatar", () => {
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');
cy.wait(1000);
cy.server();
cy.route("POST", "**!/api/v1/blog/new").as("newBlog");
cy.get('.m-button--submit').click({ force: true }); // TODO: Investigate why disabled flag is being detected
cy.wait(1000);
cy.get('h1.m-blog--edit--error').contains('Error: Please ensure your channel has an avatar before creating a blog');
});
it('should be able to create a new blog', () => {
// upload avatar first
......@@ -55,8 +38,6 @@ context('Blogs', () => {
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();
......@@ -108,14 +89,24 @@ 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");
cy.get('.m-button--submit').click({ force: true }); // TODO: Investigate why disabled flag is being detected
cy.clock();
cy.clock().then((clock) => { clock.tick(1000); });
cy.wait('@postBlog').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.equal("success");
});
cy.wait(1000);
cy.wait('@getBlog').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.equal("success");
expect(xhr.response.body).to.have.property("blog");
});
cy.location('pathname', { timeout: 30000 })
cy.location('pathname')
.should('contains', `/${Cypress.env().username}/blog`);
cy.get('.m-blog--title').contains('Title');
......
context('Boost Console', () => {
const postContent = "Test boost, please reject..." + Math.random().toString(36);
beforeEach(() => {
cy.login(true);
cy.wait(5000);
cy.visit('/newsfeed/subscriptions');
cy.wait(3000);
cy.location('pathname', { timeout: 30000 })
.should('eq', `/newsfeed/subscriptions`);
before(() => {
cy.getCookie('minds_sess')
.then((sessionCookie) => {
if (sessionCookie === null) {
return cy.login(true);
}
});
});
beforeEach(() => {
cy.preserveCookies();
cy.visit('/newsfeed/subscribed');
newBoost(postContent, 100);
});
it('should show a new boost in the console', () => {
cy.visit('/boost/console/newsfeed/history');
cy.wait(3000);
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.wait(1000);
cy.get('m-boost-console-card:nth-child(1) .m-mature-message span')
.contains(postContent);
});
......@@ -36,7 +34,6 @@ context('Boost Console', () => {
cy.get('m-boost-console-card:nth-child(1) .m-boost-card--manager-item--buttons > button')
.click();
cy.wait(1000);
cy.get('m-boost-console-card:nth-child(1) div.m-boost-card--manager-item.m-boost-card--state')
.contains('revoked');
......@@ -44,29 +41,31 @@ context('Boost Console', () => {
function navToConsole() {
cy.visit('/boost/console/newsfeed/history');
cy.wait(3000);
cy.location('pathname', { timeout: 30000 })
cy.location('pathname')
.should('eq', `/boost/console/newsfeed/history`);
}
function newBoost(text, views) {
cy.server();
cy.route("POST", '**/api/v2/boost/**').as('boostPost');
cy.post(text);
cy.wait(2000);
cy.get('#boost-actions')
.first()
.click();
cy.wait(5000);
cy.get('.m-boost--creator-section-amount input')
.type(views);
cy.get('m-overlay-modal > div.m-overlay-modal > m-boost--creator button')
.click();
cy.wait(5000);
cy.wait('@boostPost').then((xhr) => {
cy.log(xhr);
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.deep.equal("success");
});
cy.get('.m-overlay-modal')
.should('not.be.visible')
}
......
context('Boost Impressions', () => {
beforeEach(() => {
cy.login(true);
before(() => {
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.location('pathname', { timeout: 30000 })
.should('eq', '/newsfeed/subscriptions');
beforeEach(()=> {
cy.preserveCookies();
});
it('should register views on scroll', () => {
......@@ -13,48 +22,36 @@ context('Boost Impressions', () => {
cy.route("POST", "**/api/v2/analytics/views/activity/*").as("analytics");
//load, scroll, wait to trigger analytics
cy.wait(3000);
cy.scrollTo(0, 500);
cy.wait(3000);
//assert
cy.wait('@analytics', { requestTimeout: 5000 }).then((xhr) => {
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 forward', () => {
it('should register views on boost rotate', () => {
//stub endpoint
cy.server();
cy.route("POST", "**/api/v2/analytics/views/boost/*").as("analytics");
cy.wait(3000);
//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 > div > ul > li:nth-child(3) > i')
.click();
cy.wait(3000);
//assert
cy.wait('@analytics', { requestTimeout: 5000 }).then((xhr) => {
cy.wait('@analytics').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.deep.equal("success");
});
});
it('should register views on boost rotate backward', () => {
//stub endpoint
cy.server();
cy.route("POST", "**/api/v2/analytics/views/boost/*").as("analytics");
cy.wait(3000);
//rotate forward and wait to trigger analytics
cy.get('m-newsfeed--boost-rotator > div > ul > li:nth-child(1) > i')
cy.get('m-newsfeed--boost-rotator > div > ul > li:nth-child(2) > i')
.click();
cy.wait(3000);
//assert
cy.wait('@analytics', { requestTimeout: 5000 }).then((xhr) => {
cy.wait('@analytics').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.deep.equal("success");
});
......
context('Channel', () => {
before(() => {
cy.getCookie('minds_sess')
.then((sessionCookie) => {
if (sessionCookie === null) {
return cy.login(true);
}
});
cy.visit(`/${Cypress.env().username}`);
});
beforeEach(()=> {
cy.preserveCookies();
});
after(()=> {
cy.get('.m-channel-mode-selector--dropdown')
.click()
.find(".m-dropdown--list--item:contains('Public')")
.should('be.visible')
.click();
});
it('should change channel mode to public', () => {
cy.get('.m-channel-mode-selector--dropdown')
.click()
.find(".m-dropdown--list--item:contains('Public')")
.should('be.visible')
.click();
cy.get('.m-channel-mode-selector--dropdown')
.find('label').contains('Public');
});
it('should change channel mode to moderated', () => {
cy.get('.m-channel-mode-selector--dropdown')
.click()
.find(".m-dropdown--list--item:contains('Moderated')")
.should('be.visible')
.click();
cy.get('.m-channel-mode-selector--dropdown')
.find('label').contains('Moderated');
});
it('should change channel mode to closed', () => {
cy.get('.m-channel-mode-selector--dropdown')
.click()
.find(".m-dropdown--list--item:contains('Closed')")
.should('be.visible')
.click();
cy.get('.m-channel-mode-selector--dropdown')
.find('label').contains('Closed');
});
});
/**
* @author Ben Hayward
* @author Ben Hayward
* @create date 2019-08-09 14:42:51
* @modify date 2019-08-09 14:42:51
* @desc Spec tests for comment threads.
......@@ -12,95 +12,90 @@ context('Comment Threads', () => {
3: 'test tier 3',
};
const hamburgerMenu = '.m-v2-topbar__UserMenu > m-user-menu > div.m-user-menu.m-dropdown > a';
const logoutButton = '.m-user-menu.m-dropdown > ul > li:nth-child(11) > a';
const postMenu = 'minds-activity:nth-child(2) > div > m-post-menu > button > i';
const deletePostOption = 'minds-activity:nth-child(2) m-post-menu > ul > li:nth-child(4)';
const deletePostButton = 'm-modal-confirm div:nth-child(1) > div > button.mdl-button--colored';
const postMenu = 'minds-activity:first > div > m-post-menu > button > i';
const deletePostOption = "m-post-menu > ul > li:visible:contains('Delete')";
const deletePostButton = ".m-modal-confirm-buttons > button:contains('Delete')";
const channelButton = '.m-v2-topbar__Top > div > a > minds-avatar > div';
const postCommentButton = 'm-comment__poster > div > div.minds-body > div > div > a.m-post-button';
const thumbsUpButtons = '.m-comment__toolbar minds-button-thumbs-up';
const thumbsDownButtons = '.m-comment__toolbar minds-button-thumbs-down';
const thumbsUpCounters = '.m-comment__toolbar > div > minds-button-thumbs-up > a > span';
const thumbsDownCounters = '.m-comment__toolbar > div > minds-button-thumbs-down > a > span';
// pass in tier / tree depth.
const replyButton = (index) => `minds-activity:nth-child(${index}) .m-comment__toolbar > div > span`;
const commentButton = (index) => `minds-activity:nth-child(${index}) minds-button-comment`;
const commentInput = (index) => `minds-activity:nth-child(${index}) m-text-input--autocomplete-container > minds-textarea > div`;
const commentContent = (index) => `minds-activity:nth-child(${index}) m-comments__tree .m-comment__bubble > p`;
const replyButton = `minds-activity:first .m-comment__toolbar > div > span`;
const commentButton = `minds-activity:first minds-button-comment`;
const commentInput = `minds-activity:first m-text-input--autocomplete-container > minds-textarea > div`;
const commentContent = `minds-activity:first m-comments__tree .m-comment__bubble > p`;
before(() => {
//make a post new.
login();
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.post('test post');
//manually sign-out.
cy.get(hamburgerMenu).click();
cy.get(logoutButton).click();
});
beforeEach(()=> {
cy.preserveCookies();
});
after(() => {
//delete the post
cy.wait(1000);
cy.get(postMenu).click();
cy.get(deletePostOption).click();
cy.get(deletePostButton).click();
});
beforeEach(() => {
login();
cy.wait(2000);
});
it('should post three tiers of comments', () => {
//Reveal the conversation
cy.get(commentButton).click();
it('should allow a user to post a tier 1 comment', () => {
cy.get(commentButton(2)).click();
cy.get(commentInput(2)).type(testMessage[1]);
//Add the first level of comments
cy.get(commentInput).type(testMessage[1]);
cy.get(postCommentButton).click();
cy.get(commentContent(2)).contains(testMessage[1]);
});
it('should allow a user to post a tier 2 comment', () => {
//expand top comment, then top reply button.
cy.get(commentButton(2)).click();
cy.get(replyButton(2)).click();
cy.get(commentInput(2)).first().type(testMessage[2]);
cy.get(postCommentButton).first().click();
cy.get(commentContent(2)).contains(testMessage[2]);
});
it('should allow a user to post a tier 3 comment', () => {
//expand top comment, then top reply button.
cy.get(commentButton(2)).click();
cy.get(replyButton(2)).click();
cy.wait(1000);
cy.get(commentContent).contains(testMessage[1]);
//there are two reply buttons now, use the last one.
cy.get(replyButton(2)).last().click();
cy.wait(1000);
//Add the second level of comments
cy.get(replyButton).click();
cy.get(commentInput)
.first()
.type(testMessage[2]);
cy.get(postCommentButton)
.first()
.click();
cy.get(commentContent).contains(testMessage[2]);
//check the comments.
cy.get(commentInput(2)).first().type(testMessage[3]);
cy.get(postCommentButton).first().click();
cy.get(commentContent(2)).contains(testMessage[3]);
});
it('should allow the user to vote up and down comments', () => {
//expand top comment, then top reply button.
cy.get(commentButton(2)).click();
cy.get(replyButton(2)).click();
cy.wait(1000);
//Add the third level of comments
cy.get('minds-activity:first')
.find('m-comments__tree m-comments__thread m-comment')
.find('m-comments__thread m-comment:nth-child(2) .m-comment__toolbar > div > span')
.last()
.click();
cy.get(commentInput)
.first()
.type(testMessage[3]);
cy.get(postCommentButton)
.first()
.click();
cy.get(commentContent).contains(testMessage[3]);
//there are two reply buttons now, use the last one.
cy.get(replyButton(2)).last().click();
cy.wait(1000);
//click thumbs up and down
cy.get(thumbsDownButtons).click({multiple: true});
cy.get(thumbsUpButtons).click({multiple: true});
cy.get('.m-comment__toolbar')
.find('minds-button-thumbs-up')
.click({multiple: true});
cy.get('.m-comment__toolbar')
.find('minds-button-thumbs-down')
.click({multiple: true});
// check the values
cy.get(thumbsUpCounters)
......@@ -109,12 +104,4 @@ context('Comment Threads', () => {
.each((counter) => expect(counter.context.innerHTML).to.eql('1'));
});
function login() {
cy.login(true);
cy.location('pathname', { timeout: 30000 })
.should('eq', `/newsfeed/subscriptions`);
cy.get(channelButton).click();
}
})
context('Discovery', () => {
beforeEach(() => {
cy.login(true);
cy.location('pathname', { timeout: 30000 })
.should('eq', `/newsfeed/subscriptions`);
before(() => {
cy.getCookie('minds_sess')
.then((sessionCookie) => {
if (sessionCookie === null) {
return cy.login(true);
}
});
cy.visit('/newsfeed/global/top');
});
beforeEach(()=> {
cy.preserveCookies();
});
it('should allow a user to post on the discovery page', () => {
cy.visit('/newsfeed/global/top');
cy.post("test!!");
});
it('should be able to filter by hot', () => {
cy.visit('/newsfeed/global/top');
cy.get('.m-sort-selector--algorithm-dropdown ul > li:nth-child(1)')
cy.get(".m-sort-selector--algorithm-dropdown ul > li:contains('Hot')")
.click()
.should('have.css', 'color', 'rgb(70, 144, 223)'); // selected color
.should('have.class', 'm-dropdown--list--item--selected'); // selected class
cy.url().should('include', '/hot');
});
it('should be able to filter by top', () => {
cy.visit('/newsfeed/global/hot');
cy.get('.m-sort-selector--algorithm-dropdown ul > li:nth-child(2)')
cy.get(".m-sort-selector--algorithm-dropdown ul > li:contains('Top')")
.click()
.should('have.css', 'color', 'rgb(70, 144, 223)'); // selected color
.should('have.class', 'm-dropdown--list--item--selected'); // selected class
cy.url().should('include', '/top');
});
it('should be able to filter by time in the top feed', () => {
cy.visit('/newsfeed/global/top');
cy.get('.m-sort-selector--period-dropdown').click();
cy.get('.m-sort-selector--period-dropdown ul > li:nth-child(5)').click();
cy.url().should('include', '=1y');
cy.get('.m-sort-selector--period-dropdown').click();
cy.get('.m-sort-selector--period-dropdown ul > li:nth-child(4)').click();
cy.get(".m-sort-selector--period-dropdown ul > li:contains('30d')").click();
cy.url().should('include', '=30d');
cy.get('.m-sort-selector--period-dropdown').click();
cy.get('.m-sort-selector--period-dropdown ul > li:nth-child(3)').click();
cy.get(".m-sort-selector--period-dropdown ul > li:contains('7d')").click();
cy.url().should('include', '=7d');
cy.get('.m-sort-selector--period-dropdown').click();
cy.get('.m-sort-selector--period-dropdown ul > li:nth-child(2)').click();
cy.get(".m-sort-selector--period-dropdown ul > li:contains('24h')").click();
cy.url().should('include', '=24h');
cy.get('.m-sort-selector--period-dropdown').click();
cy.get('.m-sort-selector--period-dropdown ul > li:nth-child(1)').click();
cy.get(".m-sort-selector--period-dropdown ul > li:contains('12h')").click();
cy.url().should('include', '=12h');
});
it('should filter by latest', () => {
cy.visit('/newsfeed/global/hot');
cy.get('.m-sort-selector--algorithm-dropdown ul > li:nth-child(3)')
cy.get(".m-sort-selector--algorithm-dropdown ul > li:contains('Latest')")
.click()
.should('have.css', 'color', 'rgb(70, 144, 223)'); // selected color
.should('have.class', 'm-dropdown--list--item--selected'); // selected class
cy.url().should('include', '/latest');
});
it('should filter by image', () => {
cy.visit('/newsfeed/global/hot');
cy.get('.m-sort-selector--custom-type-dropdown').click();
cy.get('.m-sort-selector--custom-type-dropdown ul > li:nth-child(2)').click();
cy.get(".m-sort-selector--custom-type-dropdown ul > li:contains('photo')")
.click();
cy.url().should('include', '=images');
});
it('should filter by video', () => {
cy.visit('/newsfeed/global/hot');
cy.get('.m-sort-selector--custom-type-dropdown').click();
cy.get('.m-sort-selector--custom-type-dropdown ul > li:nth-child(3)').click();
cy.get(".m-sort-selector--custom-type-dropdown ul > li:contains('videocam')")
.click();
cy.url().should('include', '=videos');
});
it('should filter by blog', () => {
cy.visit('/newsfeed/global/hot');
cy.get('.m-sort-selector--custom-type-dropdown').click();
cy.get('.m-sort-selector--custom-type-dropdown ul > li:nth-child(4)').click();
cy.get(".m-sort-selector--custom-type-dropdown ul > li:contains('subject')")
.click();
cy.url().should('include', '=blog');
});
it('should filter by channels', () => {
cy.visit('/newsfeed/global/hot');
cy.get('.m-sort-selector--custom-type-dropdown').click();
cy.get('.m-sort-selector--custom-type-dropdown ul > li:nth-child(5)').click();
cy.get(".m-sort-selector--custom-type-dropdown ul > li:contains('people')")
.click();
cy.url().should('include', '=channels');
});
it('should filter by groups', () => {
cy.visit('/newsfeed/global/hot');
cy.get('.m-sort-selector--custom-type-dropdown').click();
cy.get('.m-sort-selector--custom-type-dropdown ul > li:nth-child(6)').click();
cy.get(".m-sort-selector--custom-type-dropdown ul > li:contains('group_work')")
.click();
cy.url().should('include', '=groups');
});
it('should filter by all', () => {
cy.visit('/newsfeed/global/top?type=images');
cy.get('.m-sort-selector--custom-type-dropdown').click();
cy.get('.m-sort-selector--custom-type-dropdown ul > li:nth-child(1)').click();
cy.get(".m-sort-selector--custom-type-dropdown ul > li:contains('all_inclusive')")
.click();
cy.url().should('not.include', '=images');
});
......@@ -108,21 +106,26 @@ context('Discovery', () => {
cy.visit('/newsfeed/global/top?type=images');
cy.get('m-topbar--navigation--options').click();
cy.get('m-topbar--navigation--options label > span').click();
cy.get('m-topbar--navigation--options ul > m-nsfw-selector ul > li:nth-child(1)').click();
cy.get('m-topbar--navigation--options ul > m-nsfw-selector ul > li:nth-child(2)').click();
cy.get('m-topbar--navigation--options ul > m-nsfw-selector ul > li:nth-child(3)').click();
cy.get('m-topbar--navigation--options ul > m-nsfw-selector ul > li:nth-child(4)').click();
cy.get('m-topbar--navigation--options ul > m-nsfw-selector ul > li:nth-child(5)').click();
cy.get('m-topbar--navigation--options ul > m-nsfw-selector ul > li:nth-child(6)').click();
cy.get("m-topbar--navigation--options ul > m-nsfw-selector ul > li:contains('Nudity')").click();
cy.get("m-topbar--navigation--options ul > m-nsfw-selector ul > li:contains('Pornography')").click();
cy.get("m-topbar--navigation--options ul > m-nsfw-selector ul > li:contains('Profanity')").click();
cy.get("m-topbar--navigation--options ul > m-nsfw-selector ul > li:contains('Violence and Gore')").click();
cy.get("m-topbar--navigation--options ul > m-nsfw-selector ul > li:contains('Race and Religion')").click();
cy.get("m-topbar--navigation--options ul > m-nsfw-selector ul > li:contains('Other')").click();
});
it('should allow the user to filter by a single hashtag', () => {
cy.get('.m-hashtagsSidebarSelector__list > ul > li:nth-child(1) .m-hashtagsSidebarSelectorList__visibility > i')
.click(); // Will fail on non-configured users
cy.visit('/newsfeed/global/top');
cy.get('m-hashtagssidebarselector__item')
.first()
.click();
});
it('should allow the user to turn off single hashtag filter and view all posts', () => {
cy.get('.m-hashtagsSidebarSelector__list > ul > li:nth-child(1) .m-hashtagsSidebarSelectorList__visibility > i')
cy.visit('/newsfeed/global/top');
cy.get('m-hashtagssidebarselector__item')
.first()
.find('.m-hashtagsSidebarSelectorList__visibility > i')
.click();
})
})
\ No newline at end of file
})
context('Groups', () => {
beforeEach(() => {
cy.login(true);
before(() => {
cy.getCookie('minds_sess')
.then((sessionCookie) => {
if (sessionCookie === null) {
return cy.login(true);
}
});
});
cy.location('pathname', { timeout: 30000 })
.should('eq', `/newsfeed/subscriptions`);
})
beforeEach(()=> {
cy.preserveCookies();
});
it('should create and edit a group', () => {
cy.server();
cy.route("POST", "**/api/v1/groups/group*").as("postGroup");
cy.get('m-group--sidebar-markers li:first-child').contains('New group').click();
cy.location('pathname').should('eq', '/groups/create');
......@@ -31,7 +40,10 @@ context('Groups', () => {
cy.get('.m-groups-save > button').contains('Create').click();
cy.wait(1000);
cy.wait('@postGroup').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.equal('success');
});
cy.get('.m-groupInfo__name').contains('test');
cy.get('.m-groupInfo__description').contains('This is a test');
......@@ -58,7 +70,9 @@ context('Groups', () => {
it('should be able to toggle conversation and comment on it', () => {
cy.get('m-group--sidebar-markers li:nth-child(3)').contains('test group').click();
cy.get("m-group--sidebar-markers li:contains('test group')")
.first()
.click();
// toggle the conversation
......@@ -71,8 +85,6 @@ context('Groups', () => {
cy.get('minds-groups-profile-conversation m-comments__tree minds-textarea .m-editor').type('lvl 1 comment');
cy.get('minds-groups-profile-conversation m-comments__tree a.m-post-button').click();
cy.wait(500);
// comment should appear on the list
cy.get('minds-groups-profile-conversation m-comments__tree > m-comments__thread .m-commentBubble__message').contains('lvl 1 comment');
......@@ -82,9 +94,9 @@ context('Groups', () => {
})
it('should post an activity inside the group and record the view when scrolling', () => {
cy.get('m-group--sidebar-markers li:nth-child(3)').contains('test group').click();
cy.wait(1000);
cy.get("m-group--sidebar-markers li:contains('test group')")
.first()
.click();
cy.server();
cy.route("POST", "**/api/v2/analytics/views/activity/*").as("view");
......@@ -93,8 +105,6 @@ context('Groups', () => {
cy.get('.m-posterActionBar__PostButton').click();
cy.wait(500);
// the activity should show that it was posted in this group
cy.get('.minds-list minds-activity .body a:nth-child(2)').contains('(test group)');
......@@ -105,11 +115,9 @@ context('Groups', () => {
cy.get('.m-posterActionBar__PostButton').click();
cy.wait(200);
cy.scrollTo(0, '20px');
cy.wait('@view', { requestTimeout: 2000 }).then((xhr) => {
cy.wait('@view').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body).to.deep.equal({ status: 'success' });
});
......@@ -118,8 +126,6 @@ context('Groups', () => {
it('should delete a group', () => {
cy.get('m-group--sidebar-markers li:nth-child(3)').contains('test group').click();
cy.wait(1000);
// cleanup
cy.get('minds-groups-settings-button > button').click();
cy.get('minds-groups-settings-button ul.minds-dropdown-menu > li:nth-child(8)').contains('Delete Group').click();
......
context('Login', () => {
beforeEach(() => {
cy.clearCookies();
cy.visit('/')
})
......@@ -17,7 +18,7 @@ context('Login', () => {
cy.get('minds-form-login .m-btn--login').click();
cy.location('pathname', { timeout: 10000 })
cy.location('pathname')
.should('eq', '/newsfeed/subscriptions');
})
......
context('Newsfeed', () => {
beforeEach(() => {
cy.login(true);
cy.location('pathname', { timeout: 30000 })
.should('eq', '/newsfeed/subscriptions');
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 picking hashtags from the dropdown', () => {
cy.get('minds-newsfeed-poster').should('be.visible');
......@@ -20,14 +29,18 @@ context('Newsfeed', () => {
// type in another hashtag manually
cy.get('minds-newsfeed-poster m-hashtags-selector m-form-tags-input input').type('hashtag{enter}').click();
// click away
cy.get('minds-newsfeed-poster m-hashtags-selector .minds-bg-overlay').click();
// click away on arbitrary area.
cy.get('minds-newsfeed-poster m-hashtags-selector .minds-bg-overlay').click({force: true});
// define request
cy.get('.m-posterActionBar__PostButton').click();
//await response
cy.wait('@newsfeedPOST').then((xhr) => {
expect(xhr.status).to.equal(200);
});
cy.wait(100);
cy.get('.minds-list > minds-activity:first-child .message').contains('This is a post #art #hashtag');
cy.get('.mdl-card__supporting-text.message.m-mature-message > span').first().contains('This is a post #art #hashtag');
cy.get('.minds-list > minds-activity:first-child .message a:first-child').contains('#art').should('have.attr', 'href', '/newsfeed/global/top;hashtag=art;period=24h');
cy.get('.minds-list > minds-activity:first-child .message a:last-child').contains('#hashtag').should('have.attr', 'href', '/newsfeed/global/top;hashtag=hashtag;period=24h');
......@@ -131,13 +144,18 @@ context('Newsfeed', () => {
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');
cy.wait(1000);
cy.wait('@mediaPOST').then((xhr) => {
expect(xhr.status).to.equal(200);
});
cy.get('.m-posterActionBar__PostButton').click();
cy.wait(300);
//await response
cy.wait('@newsfeedPOST').then((xhr) => {
expect(xhr.status).to.equal(200);
});
cy.get('.minds-list > minds-activity:first-child .message').contains('This is a post with an image');
// assert image
......@@ -164,7 +182,10 @@ context('Newsfeed', () => {
cy.get('.m-posterActionBar__PostButton').click();
cy.wait(100);
//await response
cy.wait('@newsfeedPOST').then((xhr) => {
expect(xhr.status).to.equal(200);
});
// should have the mature text toggle
cy.get('.minds-list > minds-activity:first-child .message .m-mature-text-toggle').should('not.have.class', 'mdl-color-text--red-500');
......@@ -226,6 +247,7 @@ context('Newsfeed', () => {
})
it('should have a "Buy Tokens" button and it should redirect to /token', () => {
cy.visit('/');
cy.get('.m-page--sidebar--navigation a.m-page--sidebar--navigation--item:last-child span')
.contains('Buy Tokens');
......@@ -236,6 +258,8 @@ context('Newsfeed', () => {
})
it('"create blog" button in poster should redirect to /blog/edit/new', () => {
cy.visit('/');
cy.get('minds-newsfeed-poster .m-posterActionBar__CreateBlog')
.contains('Create blog')
.click();
......@@ -244,6 +268,8 @@ context('Newsfeed', () => {
})
it('clicking on "create blog" button in poster should prompt a confirm dialog and open a new blog with the currently inputted text', () => {
cy.visit('/');
cy.get('minds-newsfeed-poster textarea').type('thegreatmigration'); // TODO: fix UX issue when hashtag element is overlapping input
const stub = cy.stub();
......@@ -260,6 +286,8 @@ context('Newsfeed', () => {
})
it('should record a view when the user scrolls and an activity is visible', () => {
cy.visit('/');
cy.server();
cy.route("POST", "**/api/v2/analytics/views/activity/*").as("view");
// create the post
......@@ -267,13 +295,14 @@ context('Newsfeed', () => {
cy.get('.m-posterActionBar__PostButton').click();
cy.wait(200);
//await response
cy.wait('@newsfeedPOST').then((xhr) => {
expect(xhr.status).to.equal(200);
});
cy.scrollTo(0, '20px');
cy.wait(600);
cy.wait('@view', { requestTimeout: 2000 }).then((xhr) => {
cy.wait('@view').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body).to.deep.equal({ status: 'success' });
});
......
/**
* @author Ben Hayward
* @desc Spec tests for comment threads.
*/
import generateRandomId from '../support/utilities';
context('Notification', () => {
//secondary user for testing.
let username = '';
let password = '';
const commentText = 'test comment';
const postText = 'test comment'
const postCommentButton = 'm-comment__poster > div > div.minds-body > div > div > a.m-post-button';
const commentButton = 'minds-activity > div.tabs > minds-button-comment > a';
const commentInput = 'm-comment__poster minds-textarea > div';
const commentContent ='.m-comment__bubble > p';
const notificationBell = 'm-notifications--topbar-toggle > a > i';
/**
* Before all, generate username and password, login as the new user and log out.
* Next login to env user, make a post, and log out.
*/
before(() => {
username = generateRandomId();
password = generateRandomId()+'X#';
cy.newUser(username, password);
cy.logout();
cy.login();
cy.post(postText);
cy.clearCookies();
});
/**
* After all log into new user and delete user.
*/
after(() => {
cy.clearCookies();
cy.login(true, username, password);
cy.deleteUser(username, password);
});
/**
* Before each test login, and visit env users channel.
* When testing, this means you will be ready to make a comment, remind etc,
* then switch users and check for the notification.
*/
beforeEach(() => {
cy.clearCookies();
cy.login(false, username, password);
cy.location('pathname')
.should('eq', '/newsfeed/subscriptions');
cy.visit(`/${Cypress.env().username}`);
});
it('should alert the user that a post has been commented on', () => {
// Comment on generated 2nd users post.
cy.get(commentButton).first().click();
cy.get(commentInput).first().type(commentText);
cy.get(postCommentButton).first().click();
cy.get(commentContent).first().contains(commentText);
// Logout, log into generated user.
cy.logout();
cy.login();
// Open their notifications
cy.get(notificationBell).click();
/**
* Notifications not working on test env.
* TODO: Check for notification - follow it
* through and check it leads to the post with postText.
*/
});
})
......@@ -24,6 +24,7 @@ context('Onboarding', () => {
const getTopic = (i) => `m-onboarding--topics > div > ul > li:nth-child(${i}) span`;
before(() => {
cy.clearCookies();
cy.visit('/login');
//type values
......
......@@ -2,19 +2,23 @@ context('Remind', () => {
const remindText = 'remind test text';
beforeEach(() => {
cy.login(true);
cy.location('pathname', { timeout: 30000 })
.should('eq', `/newsfeed/subscriptions`);
before(() => {
cy.getCookie('minds_sess')
.then((sessionCookie) => {
if (sessionCookie === null) {
return cy.login(true);
}
});
cy.visit(`/${Cypress.env().username}`);
});
//nav to channel
cy.get('.m-v2-topbar__Top minds-avatar div')
.click();
beforeEach(() => {
cy.preserveCookies();
});
it('should allow a user to remind their post', () => {
cy.server();
cy.route("POST", "**/api/v2/newsfeed/remind/*").as("postRemind");
//post
cy.post("test!!");
......@@ -30,54 +34,10 @@ context('Remind', () => {
//post remind.
cy.get('.m-modal-remind-composer-send i')
.click();
softReload();
cy.wait(1000);
//expect to contain text
cy.get('m-newsfeed__entity:nth-child(3) span')
.contains(remindText);
})
it('should allow a user to delete their remind', () => {
// make sure top post has the reminded text.
cy.get('m-newsfeed__entity:nth-child(3) .m-activity--message-remind span')
.contains(remindText);
//open menu.
cy.get('m-newsfeed__entity:nth-child(3) m-post-menu > button > i')
.click();
//select delete.
cy.get('m-newsfeed__entity:nth-child(3) m-post-menu ul li:nth-child(4)')
.click();
//delete confirm.
cy.get('m-newsfeed__entity:nth-child(3) m-modal-confirm div:nth-child(1) button.mdl-button.mdl-color-text--white.mdl-button--colored.mdl-button--raised')
.click();
cy.wait(2000);
//check the post is gone.
cy.get('m-newsfeed__entity:nth-child(3) .m-activity--message-remind span')
.should('not.have.value', remindText)
cy.wait('@postRemind').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.equal("success");
});
});
/**
* Cycles by pressing home screen then back to channel
*/
function softReload() {
cy.wait(6000); //wait to let requests finish.
cy.get('.m-v2-topbarNavItem__Logo > img')
.click();
cy.get('.m-v2-topbar__Top minds-avatar div')
.click();
cy.wait(1000); //wait to let requests finish.
}
})
});
context('Topbar', () => {
beforeEach(() => {
cy.login(true);
cy.location('pathname', { timeout: 30000 })
.should('eq', '/newsfeed/subscriptions');
})
before(() => {
cy.getCookie('minds_sess')
.then((sessionCookie) => {
if (sessionCookie === null) {
return cy.login(true);
}
});
});
beforeEach(()=> {
cy.preserveCookies();
});
it("clicking on the dropdown on the right should allow to go to the user's channel", () => {
// open the menu
......@@ -15,7 +21,7 @@ context('Topbar', () => {
.click();
cy.location('pathname').should('eq', `/${Cypress.env().username}`);
})
});
it('clicking on the dropdown on the right should allow to go to settings', () => {
// open the menu
......@@ -26,41 +32,37 @@ context('Topbar', () => {
.click();
cy.location('pathname').should('eq', '/settings/general');
})
});
it('clicking on the dropdown on the right should allow to go to the boost console', () => {
// open the menu
cy.get('m-user-menu .m-user-menu__Anchor').click();
cy.get('m-user-menu .m-user-menu__Dropdown li')
.contains('Boost Console')
cy.get("m-user-menu .m-user-menu__Dropdown li:contains('Boost Console')")
.click();
// TOFIX: no boost redirects to create
// cy.location('pathname').should('eq', '/boost/console/newsfeed/history');
})
cy.location('pathname').should('contain', '/boost/console/newsfeed/');
});
it('clicking on the dropdown on the right should allow to go to the boost console', () => {
it('clicking on the dropdown on the right should allow to go to the help desk', () => {
// open the menu
cy.get('m-user-menu .m-user-menu__Anchor').click();
cy.get('m-user-menu .m-user-menu__Dropdown li')
.contains('Help Desk')
cy.get("m-user-menu .m-user-menu__Dropdown li:contains('Help Desk')")
.click();
cy.location('pathname').should('eq', '/help');
})
});
it('clicking on the dropdown on the right should redirect to /canary', () => {
// open the menu
cy.get('m-user-menu .m-user-menu__Anchor').click();
cy.get('m-user-menu .m-user-menu__Dropdown li')
.contains('Canary')
cy.get("m-user-menu .m-user-menu__Dropdown li:contains('Canary')")
.click();
cy.location('pathname').should('eq', '/canary');
})
});
it('clicking on the dropdown on the right should allow to toggle Dark Mode', () => {
// open the menu
......@@ -68,8 +70,7 @@ context('Topbar', () => {
cy.get('body.m-theme__light').should('be.visible');
cy.get('m-user-menu .m-user-menu__Dropdown li')
.contains('Dark Mode')
cy.get("m-user-menu .m-user-menu__Dropdown li:contains('Dark Mode')")
.click();
cy.get('body.m-theme__dark').should('be.visible');
......@@ -79,7 +80,9 @@ context('Topbar', () => {
.click();
cy.get('body.m-theme__light').should('be.visible');
})
cy.get('m-user-menu .m-user-menu__Anchor').click({ force: true });
});
it('clicking on the bulb on the topbar should redirect to /newsfeed/subscriptions', () => {
cy.get('.m-v2-topbarNavItem__Logo img').should('be.visible');
......@@ -87,7 +90,7 @@ context('Topbar', () => {
cy.get('.m-v2-topbarNavItem__Logo').click();
cy.location('pathname').should('eq', '/newsfeed/subscriptions');
})
});
it('clicking on the bell should open the notifications dropdown, and allow to view all notifications by redirecting to /notifications', () => {
cy.get('.m-v2-topbar__UserMenu m-notifications--flyout').should('not.be.visible');
......@@ -102,5 +105,5 @@ context('Topbar', () => {
.click();
cy.location('pathname').should('eq', '/notifications');
})
});
})
......@@ -10,12 +10,21 @@ context('Wire', () => {
const sendButton = '.m-wire--creator-section--last > div > button';
const modal = 'm-overlay-modal > div.m-overlay-modal';
beforeEach(() => {
cy.login();
cy.wait(2000);
before(() => {
cy.getCookie('minds_sess')
.then((sessionCookie) => {
if (sessionCookie === null) {
return cy.login(true);
}
});
});
beforeEach(()=> {
cy.preserveCookies();
});
it('should allow a user to send a wire to another user', () => {
//TODO: Remove me when we get user to user wires working on the review environment
it.skip('should allow a user to send a wire to another user', () => {
// Visit users page.
cy.visit('/minds');
......
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add("login", (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This is will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
// Staging requires cookie to be set
Cypress.Cookies.defaults({
whitelist: 'staging'
});
import 'cypress-file-upload';
/**
* @author Marcelo, Ben and Brian
* @create date 2019-08-09 22:54:02
* @modify date 2019-08-09 22:54:02
* @desc Custom commands for access through cy.[cmd]();
*
* For more comprehensive examples of custom
* commands please read more here:
* https://on.cypress.io/custom-commands
*
* -- This is a parent command --
* Cypress.Commands.add('login', (email, password) => { ... })
* -- This is a child command --
* Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
* -- This is a dual command --
* Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
* -- This is will overwrite an existing command --
* Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
*/
const onboarding = {
welcomeText: 'Welcome to Minds',
welcomeTextContainer: 'm-onboarding--topics > div > h2:nth-child(1)',
nextButton: '.m-channelOnboarding__next',
};
//Login and register
const registerForm = {
username: 'minds-form-register #username',
email: 'minds-form-register #email',
password: 'minds-form-register #password',
password2: 'minds-form-register #password2',
checkbox: 'minds-form-register label:nth-child(2) .mdl-ripple--center',
submitButton: 'minds-form-register .mdl-card__actions button',
};
const settings = {
deleteAccountButton: 'm-settings--disable-channel > div:nth-child(2) > div > button',
deleteSubmitButton: 'm-confirm-password--modal > div > form > div:nth-child(2) > button',
};
const nav = {
hamburgerMenu: '.m-v2-topbar__UserMenu > m-user-menu > div.m-user-menu.m-dropdown > a',
logoutButton: '.m-user-menu.m-dropdown > ul > li:nth-child(11) > a',
byIndex: (i) => `.m-user-menu.m-dropdown > ul > li:nth-child(${i}) > a`,
};
const defaults = {
email: 'test@minds.com',
}
const loginForm = {
password: 'minds-form-login .m-login-box .mdl-cell:last-child input',
username: 'minds-form-login .m-login-box .mdl-cell:first-child input',
submit: 'minds-form-login .m-btn--login',
}
const poster = {
textArea: 'm-text-input--autocomplete-container textarea',
postButton: '.m-posterActionBar__PostButton',
}
Cypress.Commands.add('login', (canary) => {
/**
* Logs a user in.
* @param { boolean } canary - Currently not required
* @param { string } username - The username.
* @param { string } password - The users password.
* @returns void
*/
Cypress.Commands.add('login', (canary = false, username, password) => {
username = username ? username : Cypress.env().username;
password = password ? password : Cypress.env().password;
cy.setCookie('staging', "1"); // Run in stagin mode. Note: does not impact review sites
cy.visit('/login');
cy.get('.m-btn--login').click();
cy.server();
cy.route("POST", "/api/v1/authenticate").as("postLogin");
cy.get('minds-form-login .m-login-box .mdl-cell:first-child input').type(Cypress.env().username);
cy.get('minds-form-login .m-login-box .mdl-cell:last-child input').type(Cypress.env().password);
cy.get(loginForm.username).type(username);
cy.get(loginForm.password).type(password);
cy.get(loginForm.submit).click();
cy.get('minds-form-login .m-btn--login').click();
cy.wait('@postLogin').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.equal('success');
});
});
/**
* Logs a user out of their session using the menu.
* @returns void
*/
Cypress.Commands.add('logout', () => {
cy.get(nav.hamburgerMenu).click();
cy.get(nav.logoutButton).click();
});
/**
* Register a user, be sure to delete the user following this.
*
* ! LOG-OUT PRIOR TO CALLING !
*
* @param { string } username - The username. Note that the requested username will NOT be freed up upon deletion
* @param { string } password - The users password.
* @returns void
*/
Cypress.Commands.add('newUser', (username = '', password = '') => {
cy.visit('/login');
cy.location('pathname', { timeout: 30000 })
.should('eq', `/login`);
cy.server();
cy.route("POST", '**/api/v1/register').as('registerPOST');
cy.get(registerForm.username).focus().type(username);
cy.get(registerForm.email).focus().type(defaults.email);
cy.get(registerForm.password).focus().type(password);
cy.wait(500); // give second password field chance to appear - not tied to a request.
cy.get(registerForm.password2).focus().type(password);
cy.get(registerForm.checkbox).click({force: true});
//submit.
cy.get(registerForm.submitButton).click({force: true})
.wait('@registerPOST').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.deep.equal("success");
});
//onboarding modal shown.
cy.get(onboarding.welcomeTextContainer)
.contains(onboarding.welcomeText);
//skip onboarding.
cy.get(onboarding.nextButton).click()
cy.get(onboarding.nextButton).click()
cy.get(onboarding.nextButton).click()
cy.get(onboarding.nextButton).click()
});
Cypress.Commands.add('preserveCookies', () => {
Cypress.Cookies.preserveOnce('staging', 'minds_sess', 'mwa', 'XSRF-TOKEN');
});
/**
* Deletes a user. Use carefully on sandbox or you may lose your favorite test account.
*
* ! LOG-IN PRIOR TO CALLING !
*
* @param { string } username - The username. TODO: when both params provided log the user in too
* @param { string } password - The password.
* @returns void
*/
Cypress.Commands.add('deleteUser', (username, password) => {
cy.server();
cy.route("POST", '**/api/v2/settings/password/validate').as('validatePost');
cy.route("POST", '**/api/v2/settings/delete').as('deletePOST');
cy.visit('/settings/disable');
cy.location('pathname', { timeout: 30000 })
.should('eq', `/settings/disable`);
cy.get(settings.deleteAccountButton).click({ force: true });
cy.get('#password').focus().type(password);
cy.get(settings.deleteSubmitButton).click({ force: true })
.wait('@validatePost').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.deep.equal("success");
})
.wait('@deletePOST').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.deep.equal("success");
});
});
/**
* Uploads a file.
* @param { string } selector - The selector.
* @param { string } fileName - the file-name.
* @param { string } type - the file-type.
* @returns void
*/
Cypress.Commands.add('uploadFile', (selector, fileName, type = '') => {
cy.get(selector).then((subject) => {
cy.fixture(fileName, 'base64').then((content) => {
const el = subject[0];
const blob = b64toBlob(content, type);
cy.window().then((win) => {
const testFile = new win.File([blob], fileName, { type });
const dataTransfer = new DataTransfer();
dataTransfer.items.add(testFile);
el.files = dataTransfer.files;
// return cy.wrap(subject).trigger('change', {force: true});
});
cy.fixture(fileName).then((content) => {
cy.log("Content", fileName);
cy.get(selector).upload({
fileContent: content,
fileName: fileName,
mimeType: type
});
});
// cy.get(selector).trigger('change', { force: true });
});
/**
* Creates a new post. Must be logged in.
* @param { string } message - The message to be posted
* @returns void
*/
Cypress.Commands.add('post', (message) => {
cy.get('m-text-input--autocomplete-container textarea').type(message);
cy.get('.m-posterActionBar__PostButton').click();
cy.server();
cy.route("POST", '**/v1/newsfeed**').as('postActivity');
cy.get(poster.textArea).type(message);
cy.get(poster.postButton).click();
cy.wait('@postActivity').then((xhr) => {
expect(xhr.status).to.equal(200);
expect(xhr.response.body.status).to.deep.equal("success");
});
});
/**
* Converts base64 to blob format
* @param { string } b64Data - The base64 data.
* @param { string } contentType - The type of content.
* @param { number } sliceSize - The size of the slice.
* @returns void
*/
function b64toBlob(b64Data, contentType, sliceSize = 512) {
const byteCharacters = atob(b64Data);
const byteArrays = [];
......
......@@ -18,3 +18,5 @@ import './commands'
// Alternatively you can use CommonJS syntax:
// require('./commands')
Cypress.Cookies.debug(true);
/**
* @author Ben Hayward
* @create date 2019-08-10 00:38:46
* @modify date 2019-08-10 00:38:46
* @desc Space to put utilities and helper functions without cluttering up commands.js
*/
/**
* @returns a random 21 character string
*/
const generateRandomId = () => {
return Math.random().toString(36).substring(2, 15)
+ Math.random().toString(36).substring(2, 15);
}
export default generateRandomId;
This diff is collapsed.
......@@ -5,7 +5,7 @@
"scripts": {
"ng": "ng",
"start": "ng serve",
"preinstall": "git config core.hooksPath .githooks",
"preinstall": "git config core.hooksPath .git/hooks/",
"prebuild": "gulp build.sass",
"build": "ng build --prod",
"prebuild-dev": "gulp build.sass --deploy-url=http://localhost/en",
......@@ -61,12 +61,14 @@
"@types/node": "~10.12.18",
"codelyzer": "^4.5.0",
"cypress": "^3.4.1",
"cypress-file-upload": "^3.3.3",
"gulp": "~4.0.0",
"gulp-autoprefixer": "^6.0.0",
"gulp-css-globbing": "^0.2.2",
"gulp-minify-css": "^1.1.6",
"gulp-sass": "^4.0.2",
"gulp-template": "^5.0.0",
"husky": "^3.0.4",
"jasmine-core": "~2.99.0",
"jasmine-spec-reporter": "~4.2.1",
"jasmine-ts-async": "^1.0.0",
......@@ -77,9 +79,15 @@
"karma-jasmine-html-reporter": "^0.2.2",
"karma-mocha-reporter": "^2.2.5",
"prettier": "1.18.2",
"pretty-quick": "^1.11.1",
"protractor": "~5.4.2",
"ts-node": "~7.0.0",
"tslint": "~5.12.0",
"typescript": "~3.4.5"
},
"husky": {
"hooks": {
"pre-commit": ".githooks/pre-commit && pretty-quick --staged --bail --pattern '**/*.*(ts|html|scss)'"
}
}
}
......@@ -17,6 +17,7 @@ import { BlockListService } from './common/services/block-list.service';
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';
@Component({
moduleId: module.id,
......@@ -50,12 +51,16 @@ export class Minds {
public blockListService: BlockListService,
public featuresService: FeaturesService,
public themeService: ThemeService,
private bannedService: BannedService
private bannedService: BannedService,
private diagnostics: DiagnosticsService
) {
this.name = 'Minds';
}
async ngOnInit() {
this.diagnostics.setUser(this.minds.user);
this.diagnostics.listen(); // Listen for user changes
this.notificationService.getNotifications();
this.session.isLoggedIn(async is => {
......
......@@ -103,6 +103,7 @@ 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';
@NgModule({
......@@ -188,6 +189,7 @@ import { ShareModalComponent } from '../modules/modals/share/share';
DynamicFormComponent,
AndroidAppDownloadComponent,
SortSelectorComponent,
ChannelModeSelectorComponent,
NSFWSelectorComponent,
SwitchComponent,
......@@ -279,8 +281,8 @@ import { ShareModalComponent } from '../modules/modals/share/share';
SwitchComponent,
NSFWSelectorComponent,
FeaturedContentComponent,
PosterDateSelectorComponent,
ChannelModeSelectorComponent,
],
providers: [
{
......
<m-dropdown #channelModeDropdown class="m-channel-mode-selector--dropdown">
<label *ngIf="user.mode === channelModes.PUBLIC">
<i class="material-icons m-dropdown--label-icon">public</i>
<span>Public</span>
<i class="material-icons m-dropdown--label-icon" *ngIf="enabled"
>keyboard_arrow_down</i
>
</label>
<label *ngIf="user.mode === channelModes.MODERATED">
<i class="material-icons m-dropdown--label-icon">person_add</i>
<span>Moderated</span>
<i class="material-icons m-dropdown--label-icon" *ngIf="enabled"
>keyboard_arrow_down</i
>
</label>
<label *ngIf="user.mode === channelModes.CLOSED">
<i class="material-icons m-dropdown--label-icon">lock</i>
<span>Closed</span>
<i class="material-icons m-dropdown--label-icon" *ngIf="enabled"
>keyboard_arrow_down</i
>
</label>
<ul class="m-dropdown--list">
<li
class="m-dropdown--list--item"
[class.m-dropdown--list--item--selected]="
user.mode === channelModes.PUBLIC
"
(click)="setChannelMode(channelModes.PUBLIC)"
>
<i class="material-icons m-dropdown--label-icon">public</i>
<span>Public</span>
</li>
<li
class="m-dropdown--list--item"
[class.m-dropdown--list--item--selected]="
user.mode === channelModes.MODERATED
"
(click)="setChannelMode(channelModes.MODERATED)"
>
<i class="material-icons m-dropdown--label-icon">person_add</i>
<span>Moderated</span>
</li>
<li
class="m-dropdown--list--item"
[class.m-dropdown--list--item--selected]="
user.mode === channelModes.CLOSED
"
(click)="setChannelMode(channelModes.CLOSED)"
>
<i class="material-icons m-dropdown--label-icon">lock</i>
<span>Closed</span>
</li>
</ul>
</m-dropdown>
m-channel-mode-selector {
padding: 16px 4px;
display: flex;
flex-wrap: wrap;
flex-direction: row;
justify-content: center;
align-items: center;
position: relative;
margin: auto;
.m-dropdown--label-container {
text-align: center;
span {
vertical-align: middle;
}
}
.m-dropdown--label-icon {
margin-right: 4px;
}
.m-sort-selector--item-label-icon {
font-size: 18px;
margin-right: 4px;
}
.m-dropdown--list--item {
cursor: pointer;
}
}
///<reference path="../../../../../node_modules/@types/jasmine/index.d.ts"/>
import {
async,
ComponentFixture,
fakeAsync,
TestBed,
tick,
} from '@angular/core/testing';
import { MockComponent } from '../../../utils/mock';
import { CommonModule as NgCommonModule } from '@angular/common';
import { ChannelModeSelectorComponent } from './channel-mode-selector.component';
import { ChannelMode } from '../../../interfaces/entities';
import { Client } from '../../../services/api/client';
import { clientMock } from '../../../../tests/client-mock.spec';
import { DropdownComponent } from '../dropdown/dropdown.component';
import { componentFactoryName } from '@angular/compiler';
describe('ChannelModeSelector', () => {
let comp: ChannelModeSelectorComponent;
let fixture: ComponentFixture<ChannelModeSelectorComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [DropdownComponent, ChannelModeSelectorComponent],
providers: [{ provide: Client, useValue: clientMock }],
}).compileComponents();
}));
beforeEach(done => {
fixture = TestBed.createComponent(ChannelModeSelectorComponent);
clientMock.response = {};
comp = fixture.componentInstance;
comp.user = {
guid: 'guidguid',
name: 'name',
username: 'username',
icontime: 11111,
subscribers_count: 182,
impressions: 18200,
mode: ChannelMode.PUBLIC,
};
clientMock.response['api/v1/channel/info'] = { status: 'success' };
fixture.detectChanges();
if (fixture.isStable()) {
done();
} else {
fixture.whenStable().then(() => {
done();
});
}
});
it('it should change mode to moderated', () => {
spyOn(comp.channelModeDropdown, 'close');
comp.setChannelMode(ChannelMode.MODERATED);
fixture.detectChanges();
expect(comp.user.mode).toEqual(ChannelMode.MODERATED);
expect(clientMock.post.calls.mostRecent().args[0]).toEqual(
'api/v1/channel/info'
);
expect(clientMock.post.calls.mostRecent().args[1]).toEqual(comp.user);
expect(comp.channelModeDropdown.close).toHaveBeenCalled();
});
it('it should change mode to closed', () => {
spyOn(comp.channelModeDropdown, 'close');
comp.setChannelMode(ChannelMode.CLOSED);
fixture.detectChanges();
expect(comp.user.mode).toEqual(ChannelMode.CLOSED);
expect(clientMock.post.calls.mostRecent().args[0]).toEqual(
'api/v1/channel/info'
);
expect(clientMock.post.calls.mostRecent().args[1]).toEqual(comp.user);
expect(comp.channelModeDropdown.close).toHaveBeenCalled();
});
it('it should change mode to open', () => {
spyOn(comp.channelModeDropdown, 'close');
comp.setChannelMode(ChannelMode.PUBLIC);
fixture.detectChanges();
expect(comp.user.mode).toEqual(ChannelMode.PUBLIC);
expect(clientMock.post.calls.mostRecent().args[0]).toEqual(
'api/v1/channel/info'
);
expect(clientMock.post.calls.mostRecent().args[1]).toEqual(comp.user);
expect(comp.channelModeDropdown.close).toHaveBeenCalled();
});
it('it should not change when disabled', () => {
spyOn(comp.channelModeDropdown, 'close');
clientMock.post.calls.reset();
comp.enabled = false;
fixture.detectChanges();
comp.setChannelMode(ChannelMode.PUBLIC);
fixture.detectChanges();
expect(comp.user.mode).toEqual(ChannelMode.PUBLIC);
expect(clientMock.post).not.toHaveBeenCalled();
expect(comp.channelModeDropdown.close).not.toHaveBeenCalled();
});
});
import { AfterViewInit, Component, Input, ViewChild } from '@angular/core';
import { DropdownComponent } from '../dropdown/dropdown.component';
import { ChannelMode, MindsUser } from '../../../interfaces/entities';
import { Client } from '../../../services/api';
@Component({
selector: 'm-channel-mode-selector',
templateUrl: './channel-mode-selector.component.html',
})
export class ChannelModeSelectorComponent implements AfterViewInit {
@ViewChild('channelModeDropdown', { static: false })
channelModeDropdown: DropdownComponent;
@Input() public enabled = true;
@Input() public user: MindsUser;
public channelModes = ChannelMode;
constructor(public client: Client) {}
/**
* Pass the enabled flag down to the ViewChild to control the dropdown functions
* Only owners can change their channel mode
*/
public ngAfterViewInit() {
this.channelModeDropdown.enabled = this.enabled;
}
/**
* @param mode ChannelMode
* Sets the channel mode on the user object and calls the api
*/
public setChannelMode(mode: ChannelMode) {
if (!this.enabled) {
return;
}
this.user.mode = mode;
this.channelModeDropdown.close();
this.update();
}
/**
* Sends the current user to the update endpoint
*/
async update() {
try {
await this.client.post('api/v1/channel/info', this.user);
} catch (ex) {
console.error(ex);
}
}
}
......@@ -35,11 +35,14 @@ import { Component, Input } from '@angular/core';
})
export class DropdownComponent {
@Input() expanded: boolean = false;
@Input() enabled: boolean = true;
toggled = false;
toggle() {
this.toggled = !this.toggled;
if (this.enabled) {
this.toggled = !this.toggled;
}
}
close() {
......
......@@ -203,22 +203,23 @@ export class ButtonsPlugin {
* Position buttons
*/
public positionButtons(activeAddon) {
let $buttons = this.$element.querySelector('.medium-insert-buttons'),
$p = this.$element.querySelector('.medium-insert-active'),
$lastCaption = $p.classList.contains('medium-insert-images-grid')
? []
: $p.querySelector(
'* .medium-insert-images:last-child .m-blog--image-caption'
),
elementsContainer = (<any>this.base).options.elementsContainer,
elementsContainerAbsolute =
['absolute', 'fixed'].indexOf(
window
.getComputedStyle(elementsContainer)
.getPropertyValue('position')
) > -1;
if ($p) {
let $buttons = this.$element.querySelector('.medium-insert-buttons');
let $p = this.$element.querySelector('.medium-insert-active');
if ($p !== null) {
let $lastCaption = $p.classList.contains('medium-insert-images-grid')
? []
: $p.querySelector(
'* .medium-insert-images:last-child .m-blog--image-caption'
),
elementsContainer = (<any>this.base).options.elementsContainer,
elementsContainerAbsolute =
['absolute', 'fixed'].indexOf(
window
.getComputedStyle(elementsContainer)
.getPropertyValue('position')
) > -1;
const pRect = $p.getBoundingClientRect();
$buttons.style.left = pRect.left + document.body.scrollLeft - 40 + 'px';
......
......@@ -150,6 +150,9 @@ export class EmbedImage {
this.base.checkContentChanged();
}
/**
* Event handler registration.
*/
public events() {
/* prevent default image drag&drop */
this.$element.addEventListener('dragover', e => {
......@@ -227,8 +230,17 @@ export class EmbedImage {
return data;
}
/**
* On image select, responds to image click.
* @param { event } e - event from DOM.
*/
public selectImage(e) {
let $image = e.target;
if (!$image || $image.tagName === null) {
return;
}
if ($image.tagName === 'SPAN') {
$image = $image.parentNode.querySelector('img');
}
......@@ -254,11 +266,15 @@ export class EmbedImage {
event.stopPropagation();
}
/**
* On image deselect, called when image clicked away from.
* @param { event } e - event from DOM.
*/
public unselectImage(e) {
let $el = e.target,
$image = document.querySelector('.medium-insert-image-active');
if (!$image) {
if (!$image || !$el || $el.tagName === null) {
return;
}
......
......@@ -4,8 +4,20 @@
m-phone-input {
position: relative;
display: inline-flex;
margin-bottom: $minds-padding;
@media (max-width: $max-mobile) {
width: 100%;
input {
width: 100%;
}
}
.m-phone-input--wrapper {
display: flex;
@media (min-width: $max-mobile) {
flex-flow: row wrap;
justify-content: flex-end;
}
margin-bottom: $minds-padding;
@include m-theme() {
background-color: themed($m-white);
......
......@@ -41,6 +41,9 @@ export type FeedsServiceGetResponse = {
next?: number;
};
/**
* Enables the grabbing of data through observable feeds.
*/
@Injectable()
export class FeedsService {
limit: BehaviorSubject<number> = new BehaviorSubject(12);
......@@ -98,16 +101,28 @@ export class FeedsService {
);
}
/**
* Sets the endpoint for this instance.
* @param { string } endpoint - the endpoint for this instance. For example `api/v1/entities/owner`.
*/
setEndpoint(endpoint: string): FeedsService {
this.endpoint = endpoint;
return this;
}
/**
* Sets the limit to be returned per next() call.
* @param { number } limit - the limit to retrieve.
*/
setLimit(limit: number): FeedsService {
this.limit.next(limit);
return this;
}
/**
* Sets parameters to be used.
* @param { Object } params - parameters to be used.
*/
setParams(params): FeedsService {
this.params = params;
if (!params.sync) {
......@@ -116,19 +131,31 @@ export class FeedsService {
return this;
}
/**
* Sets the offset of the request
* @param { number } offset - the offset of the request.
*/
setOffset(offset: number): FeedsService {
this.offset.next(offset);
return this;
}
/**
* Sets castToActivities
* @param { boolean } cast - whether or not to set as_activities to true.
*/
setCastToActivities(cast: boolean): FeedsService {
this.castToActivities = cast;
return this;
}
/**
* Fetches the data.
*/
fetch(): FeedsService {
if (!this.offset.getValue()) this.inProgress.next(true);
if (!this.offset.getValue()) {
this.inProgress.next(true);
}
this.client
.get(this.endpoint, {
...this.params,
......@@ -139,8 +166,12 @@ export class FeedsService {
},
})
.then((response: any) => {
if (!this.offset.getValue()) this.inProgress.next(false);
if (!this.offset.getValue()) {
this.inProgress.next(false);
}
if (!response.entities && response.activity) {
response.entities = response.activity;
}
if (response.entities.length) {
this.rawFeed.next(this.rawFeed.getValue().concat(response.entities));
this.pagingToken = response['load-next'];
......@@ -148,10 +179,13 @@ export class FeedsService {
this.canFetchMore = false;
}
})
.catch(err => {});
.catch(e => console.log(e));
return this;
}
/**
* To be called upload loading more data
*/
loadMore(): FeedsService {
if (!this.inProgress.getValue()) {
this.setOffset(this.limit.getValue() + this.offset.getValue());
......@@ -160,6 +194,9 @@ export class FeedsService {
return this;
}
/**
* To clear data.
*/
clear(): FeedsService {
this.offset.next(0);
this.pagingToken = '';
......
......@@ -36,6 +36,12 @@ export interface KeyVal {
value: any;
}
export enum ChannelMode {
PUBLIC = 0,
MODERATED = 1,
CLOSED = 2,
}
export interface MindsUser {
guid: string;
name: string;
......@@ -66,6 +72,7 @@ export interface MindsUser {
mature_lock?: boolean;
tags?: Array<string>;
toaster_notifications?: boolean;
mode: ChannelMode;
}
export interface MindsGroup {
......
......@@ -6,7 +6,6 @@ import { ACCESS } from '../../../services/list-options';
@Component({
moduleId: module.id,
selector: 'minds-card-blog',
inputs: ['_blog : object'],
templateUrl: 'card.html',
})
......
......@@ -8,21 +8,18 @@
<div class="m-boost-console-booster--content">
<!-- Posts -->
<ng-container *ngIf="type == 'newsfeed' || type == 'offers'">
<ng-container *ngIf="posts && posts.length > 0">
<ng-container>
<div class="m-boost-console--booster--posts-list">
<minds-card
[object]="object"
[object]="entity | async"
class="m-border"
hostClass="mdl-card"
*ngFor="let object of posts"
*ngFor="let entity of feed$ | async"
></minds-card>
</div>
</ng-container>
<h3
[hidden]="inProgress || posts.length > 0"
i18n="@@BOOST__CONSOLE__BOOSTER__POST_SOMETHING"
>
<h3 [hidden]="noContent" i18n="@@BOOST__CONSOLE__BOOSTER__POST_SOMETHING">
You have no content yet. Why don't you post something?
</h3>
<div
......@@ -30,7 +27,7 @@
class="mdl-spinner mdl-js-spinner is-active"
[mdl]
></div>
<div #poster [hidden]="!inProgress && posts.length > 0"></div>
<div #poster [hidden]="!inProgress && noContent"></div>
</ng-container>
<!-- User and Media -->
......@@ -41,23 +38,26 @@
hostClass="mdl-shadow--2dp"
></minds-card>
<ng-container *ngIf="media.length > 0">
<ng-container *ngIf="(feed$ | async)?.length != 0">
<h3 i18n="@@BOOST__CONSOLE__BOOSTER__YOUR_RECENT_MEDIA_TITLE">
Your recent media
</h3>
<div class="mdl-grid m-boost-console-booster--content-grid">
<div class="mdl-cell mdl-cell--6-col" *ngFor="let object of media">
<div
class="mdl-cell mdl-cell--6-col"
*ngFor="let entity of feed$ | async"
>
<minds-card
[object]="object"
[object]="entity | async"
hostClass="mdl-shadow--2dp"
></minds-card>
<minds-button type="boost" [object]="object"></minds-button>
<minds-button type="boost" [object]="entity | async"></minds-button>
</div>
</div>
</ng-container>
<h3
[hidden]="inProgress || media.length > 0"
[hidden]="!inProgress && noContent"
i18n="@@BOOST__CONSOLE__BOOSTER__POST_SOMETHING"
>
You have no content yet. Why don't you post something?
......@@ -67,6 +67,13 @@
class="mdl-spinner mdl-js-spinner is-active"
[mdl]
></div>
<div #poster [hidden]="inProgress && media.length > 0"></div>
<div #poster [hidden]="!inProgress && noContent"></div>
</ng-container>
<infinite-scroll
distance="25%"
(load)="loadNext()"
[moreData]="feedsService.hasMore | async"
[inProgress]="inProgress"
></infinite-scroll>
</div>
......@@ -11,10 +11,14 @@ import { Client } from '../../../../services/api';
import { Session } from '../../../../services/session';
import { ActivatedRoute } from '@angular/router';
import { of } from 'rxjs/internal/observable/of';
import { FeedsService } from '../../../../common/services/feeds.service';
import { feedsServiceMock } from '../../../../../tests/feed-service-mock.spec';
import { BehaviorSubject } from 'rxjs';
describe('BoostConsoleBooster', () => {
let comp: BoostConsoleBooster;
let fixture: ComponentFixture<BoostConsoleBooster>;
window.Minds.user = { guid: 123 };
beforeEach(async(() => {
TestBed.configureTestingModule({
......@@ -25,6 +29,11 @@ describe('BoostConsoleBooster', () => {
inputs: ['object', 'hostClass'],
}),
MockComponent({ selector: 'minds-button', inputs: ['object', 'type'] }),
MockDirective({
selector: 'infinite-scroll',
inputs: ['moreData', 'inProgress'],
outputs: ['load'],
}),
BoostConsoleBooster,
],
imports: [RouterTestingModule, ReactiveFormsModule],
......@@ -35,28 +44,15 @@ describe('BoostConsoleBooster', () => {
provide: ActivatedRoute,
useValue: { parent: { url: of([{ path: 'newsfeed' }]) } },
},
{ provide: FeedsService, useValue: feedsServiceMock },
],
}).compileComponents();
}));
beforeEach(done => {
jasmine.MAX_PRETTY_PRINT_DEPTH = 2;
fixture = TestBed.createComponent(BoostConsoleBooster);
comp = fixture.componentInstance;
clientMock.response = {};
clientMock.response['api/v1/newsfeed/personal'] = {
status: 'success',
activity: [{ guid: '123' }, { guid: '456' }],
};
clientMock.response['api/v1/entities/owner'] = {
status: 'success',
entities: [{ guid: '789' }, { guid: '101112' }],
};
fixture.detectChanges();
if (fixture.isStable()) {
......@@ -70,8 +66,7 @@ describe('BoostConsoleBooster', () => {
});
it('should have loaded the lists', () => {
expect(comp.posts).toEqual([{ guid: '123' }, { guid: '456' }]);
expect(comp.media).toEqual([{ guid: '789' }, { guid: '101112' }]);
expect(comp.feed$).not.toBeFalsy();
});
it('should have a title', () => {
......@@ -89,13 +84,14 @@ describe('BoostConsoleBooster', () => {
By.css('.m-boost-console--booster--posts-list')
);
expect(list).not.toBeNull();
expect(list.nativeElement.children.length).toBe(2);
expect(list.nativeElement.children.length).toBe(1);
});
it("should have a poster if the user hasn't posted anything yet", () => {
comp.feed$ = of([]);
fixture.detectChanges();
comp.posts = [];
fixture.detectChanges();
comp.feed$.subscribe(feed => expect(feed.length).toBe(0));
const title = fixture.debugElement.query(
By.css('.m-boost-console-booster--content h3')
......@@ -106,9 +102,31 @@ describe('BoostConsoleBooster', () => {
);
const poster = fixture.debugElement.query(
By.css('.m-boost-console-booster--content div:last-child')
By.css('.m-boost-console-booster--content > div:nth-child(3)')
);
expect(poster).not.toBeNull();
expect(poster.nativeElement.hidden).toBeFalsy();
});
it('should not have a poster if the user has posted content', () => {
comp.feed$ = of([
BehaviorSubject.create({ id: 1 }),
BehaviorSubject.create({ id: 2 }),
]);
fixture.detectChanges();
comp.feed$.subscribe(feed => expect(feed.length).toBe(2));
const title = fixture.debugElement.query(
By.css('.m-boost-console-booster--content h3')
);
expect(title).toBeDefined();
expect(title.nativeElement.textContent).toContain(
"You have no content yet. Why don't you post something?"
);
const poster = fixture.debugElement.query(
By.css('.m-boost-console-booster--content > div:nth-child(3)')
);
expect(poster).toBeDefined();
});
});
import {
Component,
ComponentFactoryResolver,
ViewRef,
ChangeDetectorRef,
Input,
ViewChild,
ViewContainerRef,
} from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { FeedsService } from '../../../../common/services/feeds.service';
import { BoostConsoleType } from '../console.component';
import { Client } from '../../../../services/api';
import { Session } from '../../../../services/session';
import { BehaviorSubject, Observable } from 'rxjs';
import { PosterComponent } from '../../../newsfeed/poster/poster.component';
/**
* The component for the boost console.
*/
@Component({
moduleId: module.id,
selector: 'm-boost-console-booster',
templateUrl: 'booster.component.html',
})
export class BoostConsoleBooster {
inProgress: boolean = false;
loaded: boolean = false;
posts: any[] = [];
media: any[] = [];
/* type of the feed to display */
@Input('type') type: BoostConsoleType;
componentRef;
componentInstance: PosterComponent;
/* poster component */
@ViewChild('poster', { read: ViewContainerRef, static: false })
poster: ViewContainerRef;
minds: Minds = window.Minds;
feed$: Observable<BehaviorSubject<Object>[]>;
componentRef;
componentInstance: PosterComponent;
inProgress = true;
loaded = false;
noContent = true;
constructor(
public client: Client,
public session: Session,
private route: ActivatedRoute,
private _componentFactoryResolver: ComponentFactoryResolver
public feedsService: FeedsService,
private cd: ChangeDetectorRef,
private componentFactoryResolver: ComponentFactoryResolver
) {}
/**
* subscribes to route parent url and loads component.
*/
ngOnInit() {
this.loaded = false;
this.route.parent.url.subscribe(segments => {
this.type = <BoostConsoleType>segments[0].path;
this.load();
this.load(true);
this.loaded = true;
this.loadPoster();
});
}
/**
* Loads the infinite feed for the respective parent route.
* @param { boolean } refresh - is the state refreshing?
*/
load(refresh?: boolean) {
if (this.inProgress) {
return Promise.resolve(false);
if (!refresh) {
return;
}
if (!refresh && this.loaded) {
return Promise.resolve(true);
if (refresh) {
this.feedsService.clear();
}
this.inProgress = true;
let promises = [
this.client.get('api/v1/newsfeed/personal'),
this.client.get('api/v1/entities/owner'),
];
return Promise.all(promises)
.then((responses: any[]) => {
this.loaded = true;
this.inProgress = false;
this.posts = responses[0].activity || [];
this.media = responses[1].entities || [];
// this.posts = [];
// this.media = [];
this.loadComponent();
})
.catch(e => {
this.inProgress = false;
return false;
});
this.feedsService
.setEndpoint(
this.type === 'content'
? `api/v2/feeds/container/${this.minds.user.guid}/all`
: `api/v2/feeds/container/${this.minds.user.guid}/activities`
)
.setParams({ sync: true })
.setLimit(12)
.fetch();
this.feed$ = this.feedsService.feed;
this.inProgress = false;
this.loaded = true;
this.feed$.subscribe(feed => (this.noContent = feed.length > 0));
}
loadComponent() {
this.poster.clear();
/**
* Loads next data in feed.
* @param feed - the feed to reload.
*/
loadNext() {
if (
((this.type === 'offers' || this.type === 'newsfeed') &&
this.posts.length === 0) ||
(this.type === 'content' && this.media.length === 0)
this.feedsService.canFetchMore &&
!this.feedsService.inProgress.getValue() &&
this.feedsService.offset.getValue()
) {
const componentFactory = this._componentFactoryResolver.resolveComponentFactory(
this.feedsService.fetch(); // load the next 150 in the background
}
this.feedsService.loadMore();
}
/**
* Detects changes if view is not destroyed.
*/
detectChanges() {
if (!(this.cd as ViewRef).destroyed) {
this.cd.markForCheck();
this.cd.detectChanges();
}
}
/**
* Detaches change detector on destroy
*/
ngOnDestroy = () => this.cd.detach();
/**
* Loads the poster component if there are no activities loaded.
* @returns {boolean} success.
*/
loadPoster() {
this.feedsService.feed.subscribe(feed => {
if (feed.length > 0 && !this.inProgress && this.loaded) {
try {
this.poster.clear();
this.componentRef.clear();
this.noContent = true;
return false;
} catch (e) {
return false;
}
}
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(
PosterComponent
);
this.componentRef = this.poster.createComponent(componentFactory);
this.componentInstance = this.componentRef.instance;
this.componentInstance.load.subscribe(() => {
this.load();
});
}
return true;
});
}
}
......@@ -36,6 +36,7 @@ import { IfFeatureDirective } from '../../common/directives/if-feature.directive
import { FeaturesService } from '../../services/features.service';
import { featuresServiceMock } from '../../../tests/features-service-mock.spec';
import { BlockListService } from '../../common/services/block-list.service';
import { ChannelMode } from '../../interfaces/entities';
describe('ChannelComponent', () => {
let comp: ChannelComponent;
......@@ -127,6 +128,7 @@ describe('ChannelComponent', () => {
icontime: 11111,
subscribers_count: 182,
impressions: 18200,
mode: ChannelMode.PUBLIC,
};
comp.editing = false;
fixture.detectChanges();
......
......@@ -31,6 +31,7 @@ import { ScrollService } from '../../../services/ux/scroll';
import { FeaturesService } from '../../../services/features.service';
import { featuresServiceMock } from '../../../../tests/features-service-mock.spec';
import { FeedsService } from '../../../common/services/feeds.service';
import { ChannelMode } from '../../../interfaces/entities';
describe('ChannelFeed', () => {
let comp: ChannelFeedComponent;
......@@ -89,6 +90,7 @@ describe('ChannelFeed', () => {
subscribers_count: 182,
impressions: 18200,
pinned_posts: ['a', 'b', 'c'],
mode: ChannelMode.PUBLIC,
};
comp.feed = [
{ guid: 'aaaa' },
......
......@@ -5,14 +5,31 @@
(added)="upload_avatar($event)"
></minds-avatar>
<div class="m-channel--name">
<h2>{{user.name}}</h2>
<h2 [hidden]="editing">{{user.name}}</h2>
<div
class="minds-editable-container mdl-card__supporting-text m-channel--name--editor"
*ngIf="editing && isOwner()"
>
<input
[autoGrow]
class="mdl-textfield__input"
type="text"
name="briefdescription"
[(ngModel)]="user.name"
/>
</div>
<span
class="minds-button-edit"
(click)="toggleEditing()"
*ngIf="session.getLoggedInUser().guid == user.guid"
>
<button class="material-icons" [hidden]="editing">edit</button>
<button class="material-icons" [hidden]="!editing">done</button>
<button
class="material-icons m-channel-button-edit--done"
[hidden]="!editing"
>
done
</button>
</span>
<minds-button-user-dropdown
[(user)]="user"
......@@ -205,6 +222,16 @@
</div>
</div>
<div
class="m-channel__channel-mode-selector"
*ngIf="featuresService.has('permissions')"
>
<m-channel-mode-selector
[user]="user"
[enabled]="session.isLoggedIn() && session.getLoggedInUser().guid === user.guid"
></m-channel-mode-selector>
</div>
<div class="m-channel--action-buttons">
<minds-button-subscribe
[user]="user"
......
......@@ -4,6 +4,30 @@
border: 0 !important;
}
.m-channel--name--editor {
max-width: 88%;
}
.m-channel-button-edit--done {
position: relative;
right: 8px;
bottom: 8px;
}
.m-channel--name > div > input {
margin: 0;
font-size: 28px;
font-weight: 800;
letter-spacing: 0.25px;
line-height: 32px;
text-rendering: optimizeLegibilty;
-webkit-font-smoothing: antialiased;
text-align: center;
@include m-theme() {
border: 1px solid themed($m-grey-100);
}
}
.m-channel-bio-field {
div > i {
vertical-align: middle;
......
......@@ -35,6 +35,9 @@ import { featuresServiceMock } from '../../../../tests/features-service-mock.spe
import { IfFeatureDirective } from '../../../common/directives/if-feature.directive';
import { overlayModalServiceMock } from '../../../../tests/overlay-modal-service-mock.spec';
import { OverlayModalService } from '../../../services/ux/overlay-modal';
import { ChannelMode } from '../../../interfaces/entities';
import { ifStmt } from '@angular/compiler/src/output/output_ast';
import { ChannelModulesComponent } from '../modules/modules';
describe('ChannelSidebar', () => {
let comp: ChannelSidebar;
......@@ -99,6 +102,10 @@ describe('ChannelSidebar', () => {
inputs: ['title', 'type', 'channel', 'linksTo', 'size'],
outputs: [],
}),
MockComponent({
selector: 'm-channel-mode-selector',
inputs: ['user', 'enabled'],
}),
IfFeatureDirective,
],
imports: [FormsModule, RouterTestingModule, NgCommonModule],
......@@ -132,6 +139,7 @@ describe('ChannelSidebar', () => {
jasmine.clock().install();
fixture = TestBed.createComponent(ChannelSidebar);
featuresServiceMock.mock('es-feeds', false);
featuresServiceMock.mock('permissions', true);
clientMock.response = {};
uploadMock.response = {};
comp = fixture.componentInstance;
......@@ -143,6 +151,7 @@ describe('ChannelSidebar', () => {
icontime: 11111,
subscribers_count: 182,
impressions: 18200,
mode: ChannelMode.PUBLIC,
};
comp.editing = false;
uploadMock.response[`api/v1/channel/avatar`] = {
......@@ -335,4 +344,9 @@ describe('ChannelSidebar', () => {
fixture.detectChanges();
expect(comp.changeEditing.next).toHaveBeenCalled();
});
it('should set a channel to public', () => {
fixture.detectChanges();
expect(comp.user.mode).toEqual(ChannelMode.PUBLIC);
});
});
import { Component, EventEmitter, Output } from '@angular/core';
import { Component, EventEmitter, Output, ViewChild } from '@angular/core';
import { Client, Upload } from '../../../services/api';
import { Session } from '../../../services/session';
import { MindsUser } from '../../../interfaces/entities';
......@@ -7,6 +7,7 @@ import { ChannelOnboardingService } from '../../onboarding/channel/onboarding.se
import { Storage } from '../../../services/storage';
import { OverlayModalService } from '../../../services/ux/overlay-modal';
import { ReferralsLinksComponent } from '../../wallet/tokens/referrals/links/links.component';
import { FeaturesService } from '../../../services/features.service';
@Component({
moduleId: module.id,
......@@ -28,7 +29,7 @@ export class ChannelSidebar {
@Output() changeEditing = new EventEmitter<boolean>();
//@todo make a re-usable city selection module to avoid duplication here
// @todo make a re-usable city selection module to avoid duplication here
cities: Array<any> = [];
constructor(
......@@ -37,13 +38,15 @@ export class ChannelSidebar {
public session: Session,
public onboardingService: ChannelOnboardingService,
protected storage: Storage,
private overlayModal: OverlayModalService
private overlayModal: OverlayModalService,
public featuresService: FeaturesService
) {
if (onboardingService && onboardingService.onClose)
if (onboardingService && onboardingService.onClose) {
onboardingService.onClose.subscribe(progress => {
this.onboardingProgress = -1;
this.checkProgress();
});
}
}
ngOnInit() {
......@@ -84,6 +87,7 @@ export class ChannelSidebar {
}
this.changeEditing.next(!this.editing);
this.minds.user.name = this.user.name; //no need to refresh for other pages to update.
}
upload_avatar(file) {
......
......@@ -136,7 +136,7 @@
>explicit</i
>
<span i18n="@@M__COMMON__CONFIRM_18"
>Click to confirm your are 18+</span
>Click to confirm you are 18+</span
>
</span>
</div>
......@@ -169,7 +169,7 @@
>explicit</i
>
<span i18n="@@M__COMMON__CONFIRM_18"
>Click to confirm your are 18+</span
>Click to confirm you are 18+</span
>
</span>
</div>
......@@ -193,6 +193,9 @@
[torrent]="[
{ res: '360', key: comment.custom_data.guid + '/360.mp4' }
]"
[shouldPlayInModal]="true"
(videoMetadataLoaded)="setVideoDimensions($event)"
(mediaModalRequested)="openModal()"
>
</m-video>
</div>
......@@ -218,22 +221,21 @@
>explicit</i
>
<span i18n="@@M__COMMON__CONFIRM_18"
>Click to confirm your are 18+</span
>Click to confirm you are 18+</span
>
</span>
</div>
<a
target="_blank"
[routerLink]="['/media', comment.attachment_guid]"
*ngIf="comment.attachment_guid"
>
<a *ngIf="comment.attachment_guid">
<img
[src]="comment.custom_data[0].src"
class="mdl-shadow--2dp"
(error)="
comment.custom_data[0].src =
minds.cdn_assets_url + 'assets/logos/medium.png'
"
*ngIf="comment.custom_data"
class="mdl-shadow--2dp"
(click)="clickedImage()"
#batchImage
/>
</a>
......
......@@ -8,6 +8,8 @@ import {
ChangeDetectionStrategy,
OnChanges,
Input,
ViewChild,
ElementRef,
} from '@angular/core';
import { Session } from '../../../services/session';
......@@ -21,6 +23,11 @@ import { CommentsListComponent } from '../list/list.component';
import { TimeDiffService } from '../../../services/timediff.service';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { Router } from '@angular/router';
import { FeaturesService } from '../../../services/features.service';
import { MindsVideoComponent } from '../../media/components/video/video.component';
import { MediaModalComponent } from '../../media/modal/modal.component';
import isMobile from '../../../helpers/is-mobile';
@Component({
moduleId: module.id,
......@@ -73,6 +80,11 @@ export class CommentComponent implements OnChanges {
translationInProgress: boolean;
translateToggle: boolean = false;
commentAge$: Observable<number>;
videoDimensions: Array<any> = null;
@ViewChild('player', { static: false }) player: MindsVideoComponent;
@ViewChild('batchImage', { static: false }) batchImage: ElementRef;
@Input() canEdit: boolean = false;
@Output() onReply = new EventEmitter();
......@@ -84,7 +96,9 @@ export class CommentComponent implements OnChanges {
public translationService: TranslationService,
private overlayModal: OverlayModalService,
private cd: ChangeDetectorRef,
private timeDiffService: TimeDiffService
private router: Router,
private timeDiffService: TimeDiffService,
protected featuresService: FeaturesService
) {}
ngOnInit() {
......@@ -300,4 +314,50 @@ export class CommentComponent implements OnChanges {
this.cd.detectChanges();
}
}
// * ATTACHMENT MEDIA MODAL * ---------------------------------------------------------------------
setVideoDimensions($event) {
this.videoDimensions = $event.dimensions;
this.comment.custom_data.dimensions = this.videoDimensions;
}
setImageDimensions() {
const img: HTMLImageElement = this.batchImage.nativeElement;
this.comment.custom_data[0].width = img.naturalWidth;
this.comment.custom_data[0].height = img.naturalHeight;
}
clickedImage() {
const isNotTablet = Math.min(screen.width, screen.height) < 768;
const pageUrl = `/media/${this.comment.entity_guid}`;
if (isMobile() && isNotTablet) {
this.router.navigate([pageUrl]);
return;
}
if (!this.featuresService.has('media-modal')) {
this.router.navigate([pageUrl]);
return;
} else {
if (
this.comment.custom_data[0].width === '0' ||
this.comment.custom_data[0].height === '0'
) {
this.setImageDimensions();
}
this.openModal();
}
}
openModal() {
this.comment.modal_source_url = this.router.url;
this.overlayModal
.create(MediaModalComponent, this.comment, {
class: 'm-overlayModal--media',
})
.present();
}
}
......@@ -138,7 +138,7 @@
>explicit</i
>
<span i18n="@@M__COMMON__CONFIRM_18"
>Click to confirm your are 18+</span
>Click to confirm you are 18+</span
>
</span>
</div>
......@@ -171,7 +171,7 @@
>explicit</i
>
<span i18n="@@M__COMMON__CONFIRM_18"
>Click to confirm your are 18+</span
>Click to confirm you are 18+</span
>
</span>
</div>
......@@ -196,6 +196,9 @@
[torrent]="[
{ res: '360', key: comment.custom_data.guid + '/360.mp4' }
]"
[shouldPlayInModal]="true"
(videoMetadataLoaded)="setVideoDimensions($event)"
(mediaModalRequested)="openModal()"
>
</m-video>
</div>
......@@ -221,22 +224,21 @@
>explicit</i
>
<span i18n="@@M__COMMON__CONFIRM_18"
>Click to confirm your are 18+</span
>Click to confirm you are 18+</span
>
</span>
</div>
<a
target="_blank"
[routerLink]="['/media', comment.attachment_guid]"
*ngIf="comment.attachment_guid"
>
<a *ngIf="comment.attachment_guid">
<img
[src]="comment.custom_data[0].src"
class="mdl-shadow--2dp"
(error)="
comment.custom_data[0].src =
minds.cdn_assets_url + 'assets/logos/medium.png'
"
*ngIf="comment.custom_data"
class="mdl-shadow--2dp"
(click)="clickedImage()"
#batchImage
/>
</a>
......
......@@ -8,6 +8,7 @@ import {
ChangeDetectionStrategy,
OnChanges,
Input,
ViewChild,
ElementRef,
} from '@angular/core';
......@@ -22,6 +23,11 @@ import { CommentsListComponent } from '../list/list.component';
import { TimeDiffService } from '../../../services/timediff.service';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { Router } from '@angular/router';
import { FeaturesService } from '../../../services/features.service';
import { MindsVideoComponent } from '../../media/components/video/video.component';
import { MediaModalComponent } from '../../media/modal/modal.component';
import isMobile from '../../../helpers/is-mobile';
@Component({
selector: 'm-comment',
......@@ -71,6 +77,11 @@ export class CommentComponentV2 implements OnChanges {
translationInProgress: boolean;
translateToggle: boolean = false;
commentAge$: Observable<number>;
videoDimensions: Array<any> = null;
@ViewChild('player', { static: false }) player: MindsVideoComponent;
@ViewChild('batchImage', { static: false }) batchImage: ElementRef;
@Input() canEdit: boolean = false;
@Input() canDelete: boolean = false;
@Input() hideToolbar: boolean = false;
......@@ -85,7 +96,9 @@ export class CommentComponentV2 implements OnChanges {
private overlayModal: OverlayModalService,
private cd: ChangeDetectorRef,
private timeDiffService: TimeDiffService,
private el: ElementRef
private el: ElementRef,
private router: Router,
protected featuresService: FeaturesService
) {}
ngOnInit() {
......@@ -310,4 +323,50 @@ export class CommentComponentV2 implements OnChanges {
this.cd.detectChanges();
}
}
// * ATTACHMENT MEDIA MODAL * ---------------------------------------------------------------------
setVideoDimensions($event) {
this.videoDimensions = $event.dimensions;
this.comment.custom_data.dimensions = this.videoDimensions;
}
setImageDimensions() {
const img: HTMLImageElement = this.batchImage.nativeElement;
this.comment.custom_data[0].width = img.naturalWidth;
this.comment.custom_data[0].height = img.naturalHeight;
}
clickedImage() {
const isNotTablet = Math.min(screen.width, screen.height) < 768;
const pageUrl = `/media/${this.comment.entity_guid}`;
if (isMobile() && isNotTablet) {
this.router.navigate([pageUrl]);
return;
}
if (!this.featuresService.has('media-modal')) {
this.router.navigate([pageUrl]);
return;
} else {
if (
this.comment.custom_data[0].width === '0' ||
this.comment.custom_data[0].height === '0'
) {
this.setImageDimensions();
}
this.openModal();
}
}
openModal() {
this.comment.modal_source_url = this.router.url;
this.overlayModal
.create(MediaModalComponent, this.comment, {
class: 'm-overlayModal--media',
})
.present();
}
}
......@@ -215,9 +215,7 @@
i18n-title="@@M__COMMON__MATURE_CONTENT"
>explicit</i
>
<span i18n="@@M__COMMON__CONFIRM_18"
>Click to confirm your are 18+</span
>
<span i18n="@@M__COMMON__CONFIRM_18">Click to confirm you are 18+</span>
</span>
</div>
<minds-rich-embed [src]="activity" [maxheight]="480"></minds-rich-embed>
......@@ -288,9 +286,7 @@
i18n-title="@@M__COMMON__MATURE_CONTENT"
>explicit</i
>
<span i18n="@@M__COMMON__CONFIRM_18"
>Click to confirm your are 18+</span
>
<span i18n="@@M__COMMON__CONFIRM_18">Click to confirm you are 18+</span>
</span>
</div>
......@@ -303,9 +299,9 @@
[guid]="activity.custom_data.guid"
[playCount]="activity['play:count']"
[torrent]="[{ res: '360', key: activity.custom_data.guid + '/360.mp4' }]"
[isActivity]="true"
[shouldPlayInModal]="true"
(videoMetadataLoaded)="setVideoDimensions($event)"
(mediaModalRequested)="clickedVideo()"
(mediaModalRequested)="openModal()"
#player
>
<video-ads [player]="player" *ngIf="activity.monetized"></video-ads>
......@@ -326,9 +322,7 @@
i18n-title="@@M__COMMON__MATURE_CONTENT"
>explicit</i
>
<span i18n="@@M__COMMON__CONFIRM_18"
>Click to confirm your are 18+</span
>
<span i18n="@@M__COMMON__CONFIRM_18">Click to confirm you are 18+</span>
</span>
</div>
......@@ -354,9 +348,7 @@
i18n-title="@@M__COMMON__MATURE_CONTENT"
>explicit</i
>
<span i18n="@@M__COMMON__CONFIRM_18"
>Click to confirm your are 18+</span
>
<span i18n="@@M__COMMON__CONFIRM_18">Click to confirm you are 18+</span>
</span>
</div>
<a class="m-activity--image-link">
......
......@@ -513,6 +513,7 @@ export class Activity implements OnInit {
setVideoDimensions($event) {
this.videoDimensions = $event.dimensions;
this.activity.custom_data.dimensions = this.videoDimensions;
}
setImageDimensions() {
......@@ -522,18 +523,18 @@ export class Activity implements OnInit {
}
clickedImage() {
// Check if is mobile (not tablet)
if (isMobile() && Math.min(screen.width, screen.height) < 768) {
this.goToMediaPage();
const isNotTablet = Math.min(screen.width, screen.height) < 768;
const pageUrl = `/media/${this.activity.entity_guid}`;
if (isMobile() && isNotTablet) {
this.router.navigate([pageUrl]);
return;
}
if (!this.featuresService.has('media-modal')) {
// Non-canary
this.goToMediaPage();
this.router.navigate([pageUrl]);
return;
} else {
// Canary
if (
this.activity.custom_data[0].width === '0' ||
this.activity.custom_data[0].height === '0'
......@@ -544,13 +545,6 @@ export class Activity implements OnInit {
}
}
clickedVideo() {
// Already filtered out mobile users/non-canary in video.component.ts
// So this is just applicable to desktop/tablet in canary and should always show modal
this.activity.custom_data.dimensions = this.videoDimensions;
this.openModal();
}
openModal() {
this.activity.modal_source_url = this.router.url;
......@@ -561,10 +555,6 @@ export class Activity implements OnInit {
.present();
}
goToMediaPage() {
this.router.navigate([`/media/${this.activity.entity_guid}`]);
}
detectChanges() {
this.cd.markForCheck();
this.cd.detectChanges();
......
......@@ -170,6 +170,7 @@ export class Remind {
setVideoDimensions($event) {
this.videoDimensions = $event.dimensions;
this.activity.custom_data.dimensions = this.videoDimensions;
}
setImageDimensions() {
......@@ -179,14 +180,15 @@ export class Remind {
}
clickedImage() {
// Check if is mobile (not tablet)
if (isMobile() && Math.min(screen.width, screen.height) < 768) {
this.goToMediaPage();
const isNotTablet = Math.min(screen.width, screen.height) < 768;
const pageUrl = `/media/${this.activity.entity_guid}`;
if (isMobile() && isNotTablet) {
this.router.navigate([pageUrl]);
}
if (!this.featuresService.has('media-modal')) {
// Non-canary
this.goToMediaPage();
this.router.navigate([pageUrl]);
} else {
// Canary
if (
......@@ -199,13 +201,6 @@ export class Remind {
}
}
clickedVideo() {
// Already filtered out mobile users/non-canary in video.component.ts
// So this is just applicable to desktop/tablet in canary and should always show modal
this.activity.custom_data.dimensions = this.videoDimensions;
this.openModal();
}
openModal() {
this.activity.modal_source_url = this.router.url;
......@@ -215,8 +210,4 @@ export class Remind {
})
.present();
}
goToMediaPage() {
this.router.navigate([`/media/${this.activity.entity_guid}`]);
}
}
......@@ -214,7 +214,7 @@ describe('MindsVideo', () => {
// video.src = 'thisisavideo.mp4';
const video = new HTMLVideoElementMock();
comp.playerRef.getPlayer = () => <any>video;
comp.isActivity = false;
comp.shouldPlayInModal = false;
comp.showControls = true;
fixture.detectChanges(); // re-render
......
......@@ -64,9 +64,8 @@ export class MindsVideoComponent implements OnDestroy {
@Input() log: string | number;
@Input() muted: boolean = false;
@Input() poster: string = '';
@Input() isActivity: boolean = false;
@Input() isModal: boolean = false;
// @Input() isTheatre: boolean = false;
@Input() shouldPlayInModal: boolean = false;
@Output('finished') finished: EventEmitter<any> = new EventEmitter();
......@@ -217,7 +216,7 @@ export class MindsVideoComponent implements OnDestroy {
}
onMouseEnter() {
if (this.isActivity && this.featuresService.has('media-modal')) {
if (this.shouldPlayInModal && this.featuresService.has('media-modal')) {
return;
}
if (this.videoMetadataLoaded) {
......@@ -230,7 +229,7 @@ export class MindsVideoComponent implements OnDestroy {
onMouseLeave() {
if (
this.featuresService.has('media-modal') &&
(this.stageHover || this.isActivity)
(this.stageHover || this.shouldPlayInModal)
) {
return;
}
......@@ -440,13 +439,15 @@ export class MindsVideoComponent implements OnDestroy {
return;
}
if (isMobile() && Math.min(screen.width, screen.height) < 768) {
const isNotTablet = Math.min(screen.width, screen.height) < 768;
if (isMobile() && isNotTablet) {
this.isMobile = true;
this.toggle();
return;
}
if (this.isActivity && this.featuresService.has('media-modal')) {
if (this.shouldPlayInModal && this.featuresService.has('media-modal')) {
this.mediaModalRequested.emit();
return;
}
......
......@@ -28,6 +28,7 @@ import { MediaModalComponent } from './modal/modal.component';
import { ThumbnailSelectorComponent } from './components/thumbnail-selector.component';
import { CommentsModule } from '../comments/comments.module';
import { HashtagsModule } from '../hashtags/hashtags.module';
import { BlogModule } from '../blogs/blog.module';
const routes: Routes = [
{ path: 'media/videos/:filter', component: MediaVideosListComponent },
......@@ -60,6 +61,7 @@ const routes: Routes = [
PostMenuModule,
VideoModule,
HashtagsModule,
BlogModule,
],
declarations: [
MediaVideosListComponent,
......
......@@ -603,7 +603,9 @@ m-media--grid {
.m-comment__attachment {
img,
minds-video {
minds-video,
m-video {
max-width: 50%;
cursor: pointer;
}
}
......@@ -5,7 +5,6 @@
[style.width]="modalWidth + 'px'"
[style.height]="stageHeight + 'px'"
>
<!-- The stageWrapper is the element that goes into fullscreen -->
<div
class="m-mediaModal__stageWrapper"
[style.width]="stageWidth + 'px'"
......@@ -23,8 +22,8 @@
<div class="m-mediaModal__stage">
<!-- MEDIA: IMAGE -->
<div
class="m-mediaModal__mediaWrapper m-mediaModal__mediaWrapper--image"
*ngIf="!isVideo"
class="m-mediaModal__mediaWrapper"
*ngIf="contentType === 'image'"
[style.width]="mediaWidth + 'px'"
[style.height]="mediaHeight + 'px'"
[@slowFadeAnimation]="isLoading ? 'out' : 'in'"
......@@ -40,8 +39,8 @@
<!-- MEDIA: VIDEO -->
<div
class="m-mediaModal__mediaWrapper m-mediaModal__mediaWrapper--video"
*ngIf="isVideo"
class="m-mediaModal__mediaWrapper"
*ngIf="contentType === 'video'"
[style.width]="mediaWidth + 'px'"
[style.height]="mediaHeight + 'px'"
>
......@@ -72,6 +71,17 @@
</m-video>
</div>
<!-- MEDIA: BLOG -->
<div
class="m-mediaModal__mediaWrapper m-mediaModal__mediaWrapper--blog"
*ngIf="contentType === 'blog'"
[style.width]="mediaWidth + 'px'"
[style.height]="mediaHeight + 'px'"
[@slowFadeAnimation]="isLoading ? 'out' : 'in'"
>
<m-blog-view [blog]="entity"></m-blog-view>
</div>
<!-- OVERLAY -->
<div
class="m-mediaModal__overlayContainer"
......@@ -85,7 +95,7 @@
*ngIf="!isFullscreen"
>
<a
[routerLink]="['/media', entity.entity_guid]"
[routerLink]="[pageUrl]"
(click)="$event.stopPropagation()"
>{{ title }}</a
>
......@@ -116,7 +126,7 @@
</a>
<div class="m-mediaModal__overlayTitleSeparator"></div>
<a
[routerLink]="['/media', entity.entity_guid]"
[routerLink]="[pageUrl]"
(click)="$event.stopPropagation()"
>{{ title }}</a
>
......@@ -212,7 +222,7 @@
</a>
<!-- PERMALINK -->
<a
[routerLink]="['/newsfeed', permalinkGuid]"
[routerLink]="[pageUrl]"
class="permalink m-ownerBlock__permalink"
>
<span class="m-ownerBlock__permalinkDate">{{
......@@ -251,12 +261,17 @@
<div
class="m-mediaModal__message mdl-card__supporting-text"
m-read-more
*ngIf="hasMessage"
*ngIf="
this.contentType !== 'blog' &&
(this.entity.title ||
this.entity.message ||
this.entity.description)
"
[maxHeightAllowed]="136"
>
<span
class="m-mature-message-content"
[innerHtml]="message | tags"
[innerHtml]="title | tags"
>
</span>
<m-read-more--button></m-read-more--button>
......@@ -264,17 +279,17 @@
<!-- ACTION BUTTONS -->
<div class="m-mediaModal__actionButtonsWrapper">
<div class="m-mediaModal__actionButtonsRow m-action-tabs">
<m-wire-button
*ngIf="session.getLoggedInUser().guid != entity.owner_guid"
[object]="entity"
(done)="wireSubmitted($event)"
></m-wire-button>
<minds-button-thumbs-up
[object]="entity"
></minds-button-thumbs-up>
<minds-button-thumbs-down
[object]="entity"
></minds-button-thumbs-down>
<m-wire-button
*ngIf="session.getLoggedInUser().guid != entity.owner_guid"
[object]="entity"
(done)="wireSubmitted($event)"
></m-wire-button>
<minds-button-remind [object]="entity"></minds-button-remind>
</div>
</div>
......
......@@ -70,7 +70,6 @@ m-overlay-modal {
@include m-theme() {
box-shadow: 0 12px 24px rgba(themed($m-black-always), 0.3);
}
// .m-mediaModal {} // has inline width/height
}
}
}
......@@ -88,7 +87,6 @@ m-overlay-modal {
}
.m-mediaModal__stageWrapper {
// Has inline width/line-height
float: left;
height: 100%;
min-height: 480px;
......@@ -104,23 +102,19 @@ m-overlay-modal {
.m-mediaModal__stage {
display: flex;
align-items: center;
font-size: 0;
height: 100%;
min-height: 402px;
position: relative;
text-align: center;
width: 100%;
}
.m-mediaModal__mediaWrapper {
// Has inline width/height
display: inline-block;
margin: 0 auto;
vertical-align: middle;
.m-mediaModal__media--image,
m-video {
// Has inline width/height
display: inline-block;
max-height: 100%;
max-width: 100%;
......@@ -158,6 +152,22 @@ m-overlay-modal {
}
}
}
&.m-mediaModal__mediaWrapper--blog {
overflow-x: hidden;
overflow-y: scroll;
line-height: 1.58 !important;
text-align: left;
.m-blog--image > img {
max-width: 100%;
}
.m-actions-block,
m-comments__tree {
display: none;
}
}
}
.m-mediaModal__overlayContainer {
......@@ -492,8 +502,14 @@ m-overlay-modal {
}
}
.m-mediaModal__message a {
text-decoration: none;
.m-mediaModal__message {
span {
white-space: pre-line;
word-wrap: break-word;
}
a {
text-decoration: none;
}
}
.m-mediaModal__actionButtonsWrapper {
......
......@@ -68,7 +68,7 @@
>explicit</i
>
<span i18n="@@M__COMMON__CONFIRM_18"
>Click to confirm your are 18+</span
>Click to confirm you are 18+</span
>
</span>
</div>
......
......@@ -122,6 +122,7 @@ export class NewsfeedBoostRotatorComponent {
load() {
try {
this.feedsService.clear(); // Fresh each time
this.feedsService
.setEndpoint('api/v2/boost/feed')
.setParams({
......@@ -262,8 +263,7 @@ export class NewsfeedBoostRotatorComponent {
if (this.currentPosition + 1 > this.boosts.length - 1) {
//this.currentPosition = 0;
try {
this.feedsService.fetch();
this.feedsService.loadMore();
this.load();
this.currentPosition++;
} catch (e) {
this.currentPosition = 0;
......
......@@ -45,6 +45,7 @@ m-notifications--flyout {
max-height: calc(95vh - 200px);
overflow-y: scroll;
padding: 0;
white-space: pre-line;
.mdl-cell--12-col {
padding: 0;
......
......@@ -4,6 +4,10 @@ m-videochat {
max-height: calc(100vh - 90px);
}
> div {
height: calc(100vh - 90px);
}
display: block;
position: relative;
}
.m-token--onboarding {
@media screen and (max-width: $max-mobile) {
h2 {
line-height: 30px;
font-size: 20px;
}
}
.m-token--onboarding--slide {
padding: 24px;
display: flex;
......@@ -24,6 +30,9 @@
line-height: 20px;
margin: 8px 0 16px;
padding: 0;
@media screen and (max-width: $max-mobile) {
font-size: 12px;
}
&.m-token--onboarding--subtext-note {
font-size: 12px;
......
......@@ -52,7 +52,15 @@
}
.m-token--onboarding--slide {
@media (max-width: $max-mobile) {
div p {
font-size: 12pt;
}
}
.m-phone-input {
@media (max-width: $max-mobile) {
width: 100%;
}
input {
max-width: 140px;
}
......
import { Injectable } from '@angular/core';
import * as Sentry from '@sentry/browser';
import { Session } from './session';
@Injectable()
export class DiagnosticsService {
constructor(protected session: Session) {}
listen() {
this.session.getLoggedInUser(currentUser => {
this.setUser(currentUser);
});
}
setUser(currentUser) {
let userId = null;
if (currentUser) {
userId = currentUser.guid || null;
}
Sentry.setUser({
id: userId,
});
console.info('Diagnostics ID:', userId);
}
}
......@@ -42,6 +42,7 @@ import { InMemoryStorageService } from './in-memory-storage.service';
import { FeedsService } from '../common/services/feeds.service';
import { ThemeService } from '../common/services/theme.service';
import { GlobalScrollService } from './ux/global-scroll.service';
import { DiagnosticsService } from './diagnostics.service';
export const MINDS_PROVIDERS: any[] = [
{
......@@ -222,4 +223,5 @@ export const MINDS_PROVIDERS: any[] = [
useFactory: ThemeService._,
deps: [RendererFactory2, Client, Session, Storage],
},
DiagnosticsService,
];
......@@ -2,7 +2,6 @@
* Sessions
*/
import { EventEmitter } from '@angular/core';
import * as Sentry from '@sentry/browser';
export class Session {
loggedinEmitter: EventEmitter<any> = new EventEmitter();
......@@ -51,9 +50,6 @@ export class Session {
if (window.Minds.user) {
// Attach user_guid to debug logs
Sentry.setUser({
id: window.Minds.user.guid,
});
return window.Minds.user;
}
......
This diff is collapsed.
......@@ -2463,7 +2463,7 @@
<target>Ver más</target>
</trans-unit>
<trans-unit id="M__COMMON__CONFIRM_18">
<source>Click to confirm your are 18+</source>
<source>Click to confirm you are 18+</source>
<target>Haz clic para confirmar que eres mayor de 18 años</target>
</trans-unit>
<trans-unit id="M__COMMON__VIEWS_WITH_COUNT">
......
......@@ -2539,7 +2539,7 @@
<target>Xem thêm</target>
</trans-unit>
<trans-unit id="M__COMMON__CONFIRM_18">
<source>Click to confirm your are 18+</source>
<source>Click to confirm you are 18+</source>
<target>Nhấn để xác nhận bạn trên 18 tuổi</target>
</trans-unit>
<trans-unit id="M__COMMON__VIEWS_WITH_COUNT">
......
This diff is collapsed.