...
 
Commits (34)
......@@ -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,26 +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.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
......@@ -53,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();
......@@ -106,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');
......@@ -44,13 +57,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
......@@ -77,7 +95,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');
......@@ -139,6 +160,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');
......@@ -149,6 +171,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();
......@@ -157,6 +181,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();
......@@ -173,6 +199,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
......@@ -180,13 +208,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 => {
......
import { Cookie } from '../../services/cookie';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { environment } from '../../../environments/environment';
/**
* API Class
......@@ -68,6 +69,7 @@ export class MindsHttpClient {
const headers = new HttpHeaders({
'X-XSRF-TOKEN': XSRF_TOKEN,
'X-VERSION': environment.version,
});
return Object.assign(options, {
......
......@@ -102,6 +102,7 @@ import { SettingsService } from '../modules/settings/settings.service';
import { ThemeService } from './services/theme.service';
import { HorizontalInfiniteScroll } from './components/infinite-scroll/horizontal-infinite-scroll.component';
import { ReferralsLinksComponent } from '../modules/wallet/tokens/referrals/links/links.component';
import { ChannelModeSelectorComponent } from './components/channel-mode-selector/channel-mode-selector.component';
import { ShareModalComponent } from '../modules/modals/share/share';
@NgModule({
......@@ -187,6 +188,7 @@ import { ShareModalComponent } from '../modules/modals/share/share';
DynamicFormComponent,
AndroidAppDownloadComponent,
SortSelectorComponent,
ChannelModeSelectorComponent,
NSFWSelectorComponent,
SwitchComponent,
......@@ -276,6 +278,7 @@ import { ShareModalComponent } from '../modules/modals/share/share';
SwitchComponent,
NSFWSelectorComponent,
FeaturedContentComponent,
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 => {
......@@ -194,6 +197,10 @@ export class EmbedImage {
'.' + imgClass
);
if (!image) {
return;
}
const overlay = image.parentElement.querySelector(
'.m-blog--image--in-progress-overlay'
);
......@@ -227,8 +234,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 +270,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);
......
......@@ -38,7 +38,7 @@
</a>
<a
class="m-topbar--navigation--item"
routerLink="/admin/appeals"
routerLink="/moderation/juryduty/initial"
routerLinkActive="m-topbar--navigation--item-active"
>
<span i18n="@@M__ADMIN_NAV__APPEALS">Appeals</span>
......
......@@ -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',
})
......
......@@ -109,8 +109,8 @@ describe('BoostConsoleBooster', () => {
it('should not have a poster if the user has posted content', () => {
comp.feed$ = of([
BehaviorSubject.create({ id: 1, entity: true }),
BehaviorSubject.create({ id: 2, entity: true }),
BehaviorSubject.create({ id: 1 }),
BehaviorSubject.create({ id: 2 }),
]);
fixture.detectChanges();
......
......@@ -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();
}
}
......@@ -52,6 +52,15 @@
View (PDF)
</a>
</li>
<li>
Site Admin -
<a
href="https://cdn-assets.minds.com/jobs/admin.pdf"
target="_blank"
>
View (PDF)
</a>
</li>
</ul>
</div>
<div
......
......@@ -210,9 +210,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>
......@@ -283,9 +281,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>
......@@ -298,9 +294,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>
......@@ -321,9 +317,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>
......@@ -349,9 +343,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">
......
......@@ -502,6 +502,7 @@ export class Activity implements OnInit {
setVideoDimensions($event) {
this.videoDimensions = $event.dimensions;
this.activity.custom_data.dimensions = this.videoDimensions;
}
setImageDimensions() {
......@@ -511,18 +512,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'
......@@ -533,13 +534,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;
......@@ -550,10 +544,6 @@ export class Activity implements OnInit {
.present();
}
goToMediaPage() {
this.router.navigate([`/media/${this.activity.entity_guid}`]);
}
detectChanges() {
this.cd.markForCheck();
this.cd.detectChanges();
......
......@@ -166,6 +166,7 @@ export class Remind {
setVideoDimensions($event) {
this.videoDimensions = $event.dimensions;
this.activity.custom_data.dimensions = this.videoDimensions;
}
setImageDimensions() {
......@@ -175,14 +176,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 (
......@@ -195,13 +197,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;
......@@ -211,8 +206,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;
......
......@@ -24,7 +24,7 @@
></minds-activity>
<m-newsfeed--boost-rotator
interval="4"
interval="3"
*ngIf="showBoostRotator"
></m-newsfeed--boost-rotator>
......
......@@ -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;
......
......@@ -32,10 +32,16 @@
<a href="https://santaclaraprinciples.org/" target="_blank"
>Santa Clara Principles</a
>
to review appeals. The jury consists of 12 unique, active users whose
objective is to vote on appeals. If 75% or more agree with the appeal, the
administrative action is overturned. For more information about the jury
system,
to review appeals. A Jury consists of 12 randomly selected unique active
Minds users who are not subscribed to the user under review. Each juror
selected is provided with the option to participate, pass or opt-out of
the Jury pool entirely. If 75% or more of the Jury members vote to accept
the appeal of a strike, the administrative action is overturned.
</p>
<p>
If less than 75% of the Jury members vote accept the appeal, the
administrative action is upheld. The decision of the Jury will be final.
For more information about the jury system,
<a
href="https://www.minds.com/minds/blog/power-to-the-people-the-minds-jury-system-975486713993859072"
target="_blank"
......@@ -76,6 +82,25 @@
<li>Strike 3 = Ban</li>
</ul>
<p>
NSFW (not safe for work) is defined as content containing nudity,
pornography, profanity, violence, gore, or sensitive commentary on race,
religion, or gender. In general terms, it is defined as content which a
reasonable viewer may not want to be seen accessing, in a public setting,
such as in a workplace. These tags can be applied to individual content or
any group or channel. The full channel will not be marked with a NSFW
category until it has received 3 strikes in a single NSFW category.
</p>
<p>
Spam on Minds is generally defined as repeated, unwanted, and/or
unsolicited actions, automated or manual, that negatively affect Minds
users, groups, and/or Minds itself. Spam also includes content that is
designed to further unlawful acts (such as phishing) or mislead recipients
as to the source of the material (such as spoofing). Spam may result in an
immediate ban if determined to be malicious or by use of a bot.
</p>
<p>
Spam may result in an immediate ban if determined to be malicious or by
use of a bot.
......
......@@ -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 { Cookie } from '../cookie';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { environment } from '../../../environments/environment';
/**
* API Class
......@@ -175,6 +176,7 @@ export class Client {
const headers = new HttpHeaders({
'X-XSRF-TOKEN': XSRF_TOKEN,
'X-VERSION': environment.version,
});
return Object.assign(options, {
......
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;
}
......
......@@ -73,7 +73,7 @@ export class GlobalScrollService {
const viewEmitter: EventEmitter<any> = new EventEmitter<any>();
if (!subscription.viewListener) {
subscription.viewListener = subscription.scrollEvent
.pipe(debounceTime(100)) // wait 100ms before triggering
.pipe(debounceTime(30)) // wait 30ms before triggering
.subscribe(e => {
viewEmitter.next(e);
});
......
......@@ -996,7 +996,7 @@
</trans-unit>
<trans-unit id="MINDS__TOPBAR__FAQ" datatype="html">
<source>
FAQ
FAQ
</source>
<context-group purpose="location">
<context context-type="sourcefile">app/common/layout/topbar/topbar.component.ts</context>
......@@ -1888,7 +1888,7 @@
</context-group>
</trans-unit>
<trans-unit id="M__COMMON__CONFIRM_18" datatype="html">
<source>Click to confirm your are 18+</source>
<source>Click to confirm you are 18+</source>
<context-group purpose="location">
<context context-type="sourcefile">app/modules/legacy/components/cards/activity/activity.ts</context>
<context context-type="linenumber">86</context>
......@@ -3228,7 +3228,7 @@
</trans-unit>
<trans-unit id="WIRE__CHANNEL__OVERVIEW__BREAKDOWN_LINK_TOOLTIP" datatype="html">
<source>
See a breakdown of all your Wires in your wallet
See a breakdown of all your Wires in your wallet
</source>
<context-group purpose="location">
<context context-type="sourcefile">app/modules/wire/channel/overview/overview.component.ts</context>
......@@ -7838,7 +7838,7 @@
</trans-unit>
<trans-unit id="MINDS__HOME__HOMEPAGE__LAUNCH_CTA" datatype="html">
<source>
We are an open source and decentralized platform
We are an open source and decentralized platform
for Internet freedom. Get paid in crypto for your contributions
to the community.
</source>
......
......@@ -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">
......