Skip to content
Commits on Source (20)
......@@ -26,7 +26,6 @@
"scripts": [
"../node_modules/material-design-lite/dist/material.min.js",
"../node_modules/medium-editor/dist/js/medium-editor.min.js",
"shims/fontawesome.js",
"shims/jitsi-api.min.js"
],
"environmentSource": "environments/environment.ts",
......
......@@ -12,6 +12,7 @@ stages:
- test:e2e
- deploy:canary
- deploy:production
- cleanup
variables:
CYPRESS_INSTALL_BINARY: 0 # Speeds up the install process
......@@ -25,33 +26,6 @@ test:
- npm ci
- npm run test -- --no-watch --no-progress --browsers=ChromeHeadlessCI
e2e:base:
image: cypress/base:10
stage: test:e2e
variables:
CYPRESS_INSTALL_BINARY: 3.4.1
script:
- npm ci
- >
if [ "$CI_BUILD_REF_NAME" == "master" ]; then
export E2E_DOMAIN=https://www.minds.com
else
export E2E_DOMAIN=https://$CI_BUILD_REF_SLUG.$KUBE_INGRESS_BASE_DOMAIN
fi
- export CYPRESS_baseUrl=$E2E_DOMAIN
- echo "E2E tests for $CI_BUILD_REF_NAME running against $E2E_DOMAIN with user $CYPRESS_username"
- $(npm bin)/cypress run --record --key $CYPRESS_RECORD_ID --config CYPRESS_baseUrl=$E2E_DOMAIN
artifacts:
when: always
paths:
- cypress/screenshots
- cypress/videos
cache:
paths:
- .npm
- cache/Cypress
allow_failure: true #manual inspection in case of timeouts
e2e:chrome:
image: cypress/browsers:chrome67
stage: test:e2e
......@@ -79,10 +53,14 @@ e2e:chrome:
- cache/Cypress
allow_failure: true #manual inspection in case of timeouts
###############
# Build Stage #
###############
build:review:
stage: build
before_script:
- sed -ri "s|\"VERSION\"|\"$CI_COMMIT_SHA\"|" 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
......@@ -100,7 +78,7 @@ build:review:
build:production:en:
stage: build
before_script:
- sed -ri "s|\"VERSION\"|\"$CI_COMMIT_SHA\"|" 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
......@@ -118,7 +96,7 @@ build:production:en:
build:production:i18n:
stage: build
before_script:
- sed -ri "s|\"VERSION\"|\"$CI_COMMIT_SHA\"|" 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
......@@ -133,24 +111,28 @@ build:production:i18n:
- master
- test/gitlab-ci
#################
# Prepare Stage #
#################
.sentry_prepare: &sentry_prepare
stage: prepare
image: getsentry/sentry-cli
script:
- echo "Create a new release $CI_COMMIT_SHA"
- sentry-cli releases new $CI_COMMIT_SHA
- sentry-cli releases set-commits --auto $CI_COMMIT_SHA
- sentry-cli releases files $CI_COMMIT_SHA upload-sourcemaps $CI_PROJECT_DIR/dist/en -x .js -x .map --validate --url-prefix $SOURCEMAP_PREFIX
- sentry-cli releases finalize $CI_COMMIT_SHA
- echo "Finalized release for $CI_COMMIT_SHA"
- echo "Create a new release $CI_PIPELINE_ID"
- sentry-cli releases new $CI_PIPELINE_ID
- sentry-cli releases set-commits --auto $CI_PIPELINE_ID
- sentry-cli releases files $CI_PIPELINE_ID upload-sourcemaps $CI_PROJECT_DIR/dist/en -x .js -x .map --validate --url-prefix $SOURCEMAP_PREFIX
- sentry-cli releases finalize $CI_PIPELINE_ID
- echo "Finalized release for $CI_PIPELINE_ID"
prepare:review:
stage: prepare
image: minds/ci:latest
script:
- docker login -u gitlab-ci-token -p ${CI_BUILD_TOKEN} ${CI_REGISTRY}
- docker build -t $CI_REGISTRY_IMAGE/front-init:$CI_BUILD_REF -f containers/front-init/Dockerfile dist/.
- docker push $CI_REGISTRY_IMAGE/front-init:$CI_BUILD_REF
- 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:
- build:review
except:
......@@ -159,7 +141,7 @@ prepare:review:
- test/gitlab-ci
prepare:review:sentry:
<<: *sentry_prepare
<<: *sentry_prepare
variables:
SOURCEMAP_PREFIX: "~/en"
except:
......@@ -174,8 +156,8 @@ prepare:production:
image: minds/ci:latest
script:
- docker login -u gitlab-ci-token -p ${CI_BUILD_TOKEN} ${CI_REGISTRY}
- docker build -t $CI_REGISTRY_IMAGE/front-init:$CI_BUILD_REF -f containers/front-init/Dockerfile dist/.
- docker push $CI_REGISTRY_IMAGE/front-init:$CI_BUILD_REF
- 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:
refs:
- master
......@@ -196,6 +178,26 @@ prepare:production:sentry:
- build:production:en
- build:production:i18n
################
# Review Stage #
################
.cleanup_review: &cleanup_review
image: minds/helm-eks:latest
script:
- aws eks update-kubeconfig --name=sandbox
- helm del --purge $CI_BUILD_REF_SLUG
environment:
name: review/$CI_COMMIT_REF_NAME
url: https://$CI_BUILD_REF_SLUG.$KUBE_INGRESS_BASE_DOMAIN
action: stop
variables:
GIT_STRATEGY: none
except:
refs:
- master
- test/gitlab-ci
review:start:
stage: review
image: minds/helm-eks:latest
......@@ -206,14 +208,14 @@ review:start:
--install \
--reuse-values \
--set frontInit.image.repository=$CI_REGISTRY_IMAGE/front-init \
--set frontInit.image.tag=$CI_BUILD_REF \
--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_COMMIT_SHA new -e review-$CI_COMMIT_REF_SLUG
- 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
......@@ -224,75 +226,53 @@ review:start:
- test/gitlab-ci
review:stop:
<<: *cleanup_review
stage: review
image: minds/helm-eks:latest
script:
- aws eks update-kubeconfig --name=sandbox
- helm del --purge $CI_BUILD_REF_SLUG
environment:
name: review/$CI_COMMIT_REF_NAME
url: https://$CI_BUILD_REF_SLUG.$KUBE_INGRESS_BASE_DOMAIN
action: stop
variables:
GIT_STRATEGY: none
when: manual
except:
refs:
- master
- test/gitlab-ci
staging:fpm:
stage: deploy:staging
################
# Deploy Stage #
################
.deploy: &deploy
image: minds/ci:latest
script:
- IMAGE_LABEL="staging"
## Sync assets with CDN
- 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 pull $CI_REGISTRY_IMAGE/front-init:$CI_BUILD_REF
- docker tag $CI_REGISTRY_IMAGE/front-init:$CI_BUILD_REF $ECR_REPOSITORY_URL:$IMAGE_LABEL
- 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
## Deploy the new container in rolling restart
- aws ecs update-service --service=$ECS_APP_STAGING_SERVICE --force-new-deployment --region us-east-1 --cluster=$ECS_CLUSTER
- aws ecs update-service --service=$ECS_SERVICE --force-new-deployment --region us-east-1 --cluster=$ECS_CLUSTER
## Update sentry
- sentry-cli releases deploys $CI_COMMIT_SHA new -e $IMAGE_LABEL
- sentry-cli releases deploys $CI_PIPELINE_ID new -e $IMAGE_LABEL
dependencies:
- build:production:en
- build:production:i18n
only:
refs:
- master
- test/gitlab-ci
dependencies:
- build:production:en
- build:production:i18n
staging:fpm:
<<: *deploy
stage: deploy:staging
variables:
IMAGE_LABEL: "staging"
ECS_SERVICE: $ECS_APP_STAGING_SERVICE
environment:
name: staging
url: https://www.minds.com # requires staging cookie
deploy:canary:
<<: *deploy
stage: deploy:canary
image: minds/ci:latest
script:
- IMAGE_LABEL="canary"
## Sync assets with CDN
- 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 pull $CI_REGISTRY_IMAGE/front-init:$CI_BUILD_REF
- docker tag $CI_REGISTRY_IMAGE/front-init:$CI_BUILD_REF $ECR_REPOSITORY_URL:$IMAGE_LABEL
- docker push $ECR_REPOSITORY_URL:$IMAGE_LABEL
## Deploy the new container in rolling restart
- aws ecs update-service --service=$ECS_APP_CANARY_SERVICE --force-new-deployment --region us-east-1 --cluster=$ECS_CLUSTER
## Update sentry
- sentry-cli releases deploys $CI_COMMIT_SHA new -e $IMAGE_LABEL
only:
refs:
- master
- test/gitlab-ci
dependencies:
- build:production:en
- build:production:i18n
variables:
IMAGE_LABEL: "canary"
ECS_SERVICE: $ECS_APP_CANARY_SERVICE
environment:
name: canary
url: https://www.minds.com/?canary=1 # requires canary cookie
......@@ -300,30 +280,25 @@ deploy:canary:
allow_failure: false # prevents auto deploy to full production
deploy:production:
<<: *deploy
stage: deploy:production
image: minds/ci:latest
script:
- IMAGE_LABEL="production"
- $(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 pull $CI_REGISTRY_IMAGE/front-init:$CI_BUILD_REF
- docker tag $CI_REGISTRY_IMAGE/front-init:$CI_BUILD_REF $ECR_REPOSITORY_URL:$IMAGE_LABEL
- docker push $ECR_REPOSITORY_URL:$IMAGE_LABEL
## Deploy the new container in rolling restart
- aws ecs update-service --service=$ECS_APP_PRODUCTION_SERVICE --force-new-deployment --region us-east-1 --cluster=$ECS_CLUSTER
## Update sentry
- sentry-cli releases deploys $CI_COMMIT_SHA new -e $IMAGE_LABEL
only:
refs:
- master
- test/gitlab-ci
dependencies:
- build:production:en
- build:production:i18n
variables:
IMAGE_LABEL: "production"
ECS_SERVICE: $ECS_APP_PRODUCTION_SERVICE
environment:
name: production
url: https://www.minds.com
when: delayed
start_in: 2 hours # reduce? can always be deployed manually earlier too
#################
# Cleanup stage #
#################
cleanup:review: # We stop the review site after the e2e tests have run
<<: *cleanup_review
stage: cleanup
except:
refs:
- master
- test/gitlab-ci
\ No newline at end of file
......@@ -28,7 +28,6 @@
"scripts": [
"node_modules/material-design-lite/dist/material.min.js",
"node_modules/medium-editor/dist/js/medium-editor.min.js",
"src/shims/fontawesome.js",
"src/shims/jitsi-api.min.js"
]
},
......@@ -82,7 +81,6 @@
"scripts": [
"node_modules/material-design-lite/dist/material.min.js",
"node_modules/medium-editor/dist/js/medium-editor.min.js",
"src/shims/fontawesome.js",
"src/shims/jitsi-api.min.js"
],
"assets": [
......
......@@ -70,7 +70,7 @@ export class MindsHttpClient {
* Build the options
*/
private buildOptions(options: Object) {
const XSRF_TOKEN = this.cookie.get('XSRF-TOKEN');
const XSRF_TOKEN = this.cookie.get('XSRF-TOKEN') || '';
const headers = new HttpHeaders({
'X-XSRF-TOKEN': XSRF_TOKEN,
......
......@@ -153,7 +153,7 @@ export class EntitiesService {
if (!this.entities.has(urn)) {
this.entities.set(urn, new BehaviorSubject(null));
}
console.warn(`Entity ${urn} not found`);
this.entities.get(urn).error("Not found");
}
static _(client: Client, blockListService: BlockListService) {
......
......@@ -53,6 +53,8 @@ export class UpdateMarkersService {
if (this.isLoggedIn) {
this.get()
.subscribe((markers: any) => {
if (!markers)
return;
this.data = markers; //cache
for (let i in this.data) {
......
......@@ -218,7 +218,7 @@ m-hashtags--sidebar-selector {
width: 48px;
z-index: 1;
@include m-theme(){
background: linear-gradient(to right, rgba(themed($m-white-always), 0) 0%, rgba(themed($m-body-bg), 1) 50%);
background: linear-gradient(to right, rgba(themed($m-white), 0) 0%, rgba(themed($m-body-bg), 1) 50%);
}
.m-hashtagsSidebarSelectorCompactListOverflow__Arrow {
......
......@@ -175,7 +175,7 @@
[torrent]="[{ res: '360', key: activity.custom_data.guid + '/360.mp4' }]"
[isActivity]="true"
(videoMetadataLoaded)="setVideoDimensions($event)"
(mediaModalRequested)="showMediaModal()"
(mediaModalRequested)="clickedVideo()"
#player>
<video-ads [player]="player" *ngIf="activity.monetized"></video-ads>
</m-video>
......@@ -212,7 +212,7 @@
style="width:100%"
(error)="activity.custom_data[0].src = minds.cdn_assets_url + 'assets/logos/placeholder-bulb.jpg'"
*ngIf="activity.custom_data"
(click)="showMediaModal()"
(click)="clickedImage()"
#batchImage
>
</a>
......
......@@ -449,30 +449,43 @@ export class Activity implements OnInit {
this.activity.custom_data[0].height = img.naturalHeight;
}
showMediaModal() {
if (this.featuresService.has('media-modal')) {
// Mobile (not tablet) users go to media page instead of modal
if (isMobile() && Math.min(screen.width, screen.height) < 768) {
this.router.navigate([`/media/${this.activity.entity_guid}`]);
}
clickedImage() {
// Check if is mobile (not tablet)
if (isMobile() && Math.min(screen.width, screen.height) < 768) {
this.goToMediaPage();
return;
}
if (this.activity.custom_type === 'video') {
this.activity.custom_data.dimensions = this.videoDimensions;
} else { // Image
// Set image dimensions if they're not already there
if (this.activity.custom_data[0].width === '0' || this.activity.custom_data[0].height === '0') {
this.setImageDimensions();
}
if (!this.featuresService.has('media-modal')) {
// Non-canary
this.goToMediaPage();
return;
} else {
// Canary
if (this.activity.custom_data[0].width === '0' || this.activity.custom_data[0].height === '0') {
this.setImageDimensions();
}
this.openModal();
}
}
this.activity.modal_source_url = this.router.url;
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();
}
this.overlayModal.create(MediaModalComponent, this.activity, {
class: 'm-overlayModal--media'
}).present();
} else {
this.router.navigate([`/media/${this.activity.entity_guid}`]);
}
openModal() {
this.activity.modal_source_url = this.router.url;
this.overlayModal.create(MediaModalComponent, this.activity, {
class: 'm-overlayModal--media'
}).present();
}
goToMediaPage() {
this.router.navigate([`/media/${this.activity.entity_guid}`]);
}
detectChanges() {
......
......@@ -159,29 +159,40 @@ export class Remind {
this.activity.custom_data[0].height = img.naturalHeight;
}
showMediaModal() {
if (this.featuresService.has('media-modal')) {
// Mobile (not tablet) users go to media page instead of modal
if (isMobile() && Math.min(screen.width, screen.height) < 768) {
this.router.navigate([`/media/${this.activity.entity_guid}`]);
}
clickedImage() {
// Check if is mobile (not tablet)
if (isMobile() && Math.min(screen.width, screen.height) < 768) {
this.goToMediaPage();
}
if (this.activity.custom_type === 'video') {
this.activity.custom_data.dimensions = this.videoDimensions;
} else { // Image
// Set image dimensions if they're not already there
if (this.activity.custom_data[0].width === '0' || this.activity.custom_data[0].height === '0') {
this.setImageDimensions();
}
if (!this.featuresService.has('media-modal')) {
// Non-canary
this.goToMediaPage();
} else {
// Canary
if (this.activity.custom_data[0].width === '0' || this.activity.custom_data[0].height === '0') {
this.setImageDimensions();
}
this.openModal();
}
}
this.activity.modal_source_url = this.router.url;
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();
}
this.overlayModal.create(MediaModalComponent, this.activity, {
class: 'm-overlayModal--media'
}).present();
} else {
this.router.navigate([`/media/${this.activity.entity_guid}`]);
}
openModal() {
this.activity.modal_source_url = this.router.url;
this.overlayModal.create(MediaModalComponent, this.activity, {
class: 'm-overlayModal--media'
}).present();
}
goToMediaPage() {
this.router.navigate([`/media/${this.activity.entity_guid}`]);
}
}
......@@ -80,10 +80,6 @@ export class MindsVideoTorrentPlayer implements OnInit, AfterViewInit, OnDestroy
this._emitCanPlayThrough();
};
protected _dblClick = () => {
this.requestFullScreen();
};
protected _onError = e => {
this.loading = false;
this.detectChanges();
......@@ -131,7 +127,6 @@ export class MindsVideoTorrentPlayer implements OnInit, AfterViewInit, OnDestroy
ngOnInit() {
const player = this.getPlayer();
player.addEventListener('dblclick', this._dblClick);
player.addEventListener('playing', this._emitPlay);
player.addEventListener('pause', this._emitPause);
player.addEventListener('ended', this._emitEnd);
......@@ -161,7 +156,6 @@ export class MindsVideoTorrentPlayer implements OnInit, AfterViewInit, OnDestroy
const player = this.getPlayer();
if (player) {
player.removeEventListener('dblclick', this._dblClick);
player.removeEventListener('playing', this._emitPlay);
player.removeEventListener('pause', this._emitPause);
player.removeEventListener('ended', this._emitEnd);
......
......@@ -12,7 +12,7 @@
(onError)="onError()"
(onCanPlayThrough)="onCanPlayThrough()"
(onLoadedMetadata)="loadedMetadata()"
(click)="isModal ? toggle() : requestMediaModal()"
(click)="clickedVideo()"
#player
></m-video--direct-http-player>
......@@ -29,14 +29,14 @@
(onError)="onError()"
(onCanPlayThrough)="onCanPlayThrough()"
(onLoadedMetadata)="loadedMetadata()"
(click)="isModal ? toggle() : requestMediaModal()"
(click)="clickedVideo()"
#player
></m-video--torrent-player>
<ng-container *ngIf="playerRef">
<i *ngIf="!playerRef.isPlaying() && !playerRef.isLoading() || isActivity && metadataLoaded"
<i *ngIf="(!playerRef.isPlaying() && !playerRef.isLoading()) || (isActivity && metadataLoaded && !playerRef.isPlaying())"
class="material-icons minds-video-play-icon"
(click)="isModal ? toggle() : requestMediaModal()"
(click)="clickedVideo()"
>play_circle_outline</i>
<ng-content></ng-content>
......@@ -97,7 +97,7 @@
(select)="selectedQuality($event)"
></m-video--quality-selector>
<i *ngIf="!isModal" class="material-icons" (click)="playerRef.requestFullScreen()">tv</i>
<i *ngIf="!isModal && !isActivity" class="material-icons" (click)="toggleFullscreen($event)">tv</i>
</div>
<div class="m-video--torrent-info" *ngIf="torrentInfo && current.type === 'torrent'">
......
......@@ -22,6 +22,10 @@ import { webtorrentServiceMock } from '../../../../../tests/webtorrent-service-m
import { MindsPlayerInterface } from './players/player.interface';
import { MediaModalComponent } from '../../modal/modal.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { FeaturesService } from '../../../../services/features.service';
import { featuresServiceMock } from '../../../../../tests/features-service-mock.spec';
@Component({
selector: 'm-video--direct-http-player',
......@@ -174,7 +178,8 @@ describe('MindsVideo', () => {
providers: [
{ provide: ScrollService, useValue: scrollServiceMock },
{ provide: Client, useValue: clientMock },
{ provide: WebtorrentService, useValue: webtorrentServiceMock }
{ provide: WebtorrentService, useValue: webtorrentServiceMock },
{ provide: FeaturesService, useValue: featuresServiceMock },
]
})
.compileComponents(); // compile template and css
......
import { Component, ElementRef, Input, Output, EventEmitter, ViewChild, ChangeDetectorRef, OnDestroy } from '@angular/core';
import { Component, ElementRef, Input, Output, EventEmitter, ViewChild, ChangeDetectorRef, OnDestroy, HostListener } from '@angular/core';
import { Router } from '@angular/router';
import { trigger, state, style, animate, transition } from '@angular/animations';
import { MindsVideoProgressBar } from './progress-bar/progress-bar.component';
......@@ -9,6 +9,7 @@ import { ScrollService } from '../../../../services/ux/scroll';
import { MindsPlayerInterface } from './players/player.interface';
import { WebtorrentService } from '../../../webtorrent/webtorrent.service';
import { SOURCE_CANDIDATE_PICK_ZIGZAG, SourceCandidates } from './source-candidates';
import { FeaturesService } from '../../../../services/features.service';
import isMobile from '../../../../helpers/is-mobile';
@Component({
......@@ -16,7 +17,7 @@ import isMobile from '../../../../helpers/is-mobile';
host: {
'(mouseenter)': 'onMouseEnter()',
'(mouseleave)': 'onMouseLeave()',
'[class.clickable]':'canPlayThrough'
'[class.clickable]':'metadataLoaded'
},
templateUrl: 'video.component.html',
animations: [
......@@ -43,6 +44,8 @@ export class MindsVideoComponent implements OnDestroy {
@Input() poster: string = '';
@Input() isActivity: boolean = false;
@Input() isModal: boolean = false;
// @Input() isTheatre: boolean = false;
@Output('finished') finished: EventEmitter<any> = new EventEmitter();
......@@ -82,6 +85,8 @@ export class MindsVideoComponent implements OnDestroy {
stopSeekerTimeout: any = null;
metadataLoaded: boolean = false;
canPlayThrough: boolean = false;
isFullscreen: boolean = false;
isMobile: boolean = false;
current: { type: 'torrent' | 'direct-http', src: string };
protected candidates: SourceCandidates = new SourceCandidates();
......@@ -100,7 +105,8 @@ export class MindsVideoComponent implements OnDestroy {
public client: Client,
protected webtorrent: WebtorrentService,
protected cd: ChangeDetectorRef,
private router: Router,
protected featuresService: FeaturesService,
private router: Router
) { }
ngOnInit() {
......@@ -185,17 +191,18 @@ export class MindsVideoComponent implements OnDestroy {
}
onMouseEnter() {
if (this.isActivity) {
if (this.isActivity && this.featuresService.has('media-modal')) {
return;
}
this.progressBar.getSeeker();
this.progressBar.enableKeyControls();
this.showControls = true;
if (this.videoMetadataLoaded){
this.progressBar.getSeeker();
this.progressBar.enableKeyControls();
this.showControls = true;
}
}
onMouseLeave() {
if (this.stageHover || this.isActivity) {
if (this.featuresService.has('media-modal') && (this.stageHover || this.isActivity)) {
return;
}
......@@ -391,28 +398,85 @@ export class MindsVideoComponent implements OnDestroy {
this.toggle();
}
requestMediaModal() {
if (!this.canPlayThrough) {
clickedVideo() {
if (!this.metadataLoaded) {
return;
}
if (this.isModal) {
if (isMobile() && Math.min(screen.width, screen.height) < 768) {
this.isMobile = true;
this.toggle();
return;
}
// Mobile (not tablet) users go to media page instead of modal
if (isMobile() && Math.min(screen.width, screen.height) < 768) {
this.router.navigate([`/media/${this.guid}`]);
if (this.isActivity && this.featuresService.has('media-modal')){
this.mediaModalRequested.emit();
return;
}
this.mediaModalRequested.emit();
this.toggle();
}
detectChanges() {
this.cd.markForCheck();
this.cd.detectChanges();
}
// * FULLSCREEN * --------------------------------------------------------------------------------
// Listen for fullscreen change event in case user enters/exits full screen without clicking button
@HostListener('document:fullscreenchange', ['$event'])
@HostListener('document:webkitfullscreenchange', ['$event'])
@HostListener('document:mozfullscreenchange', ['$event'])
@HostListener('document:MSFullscreenChange', ['$event'])
onFullscreenChange(event) {
if ( !document.fullscreenElement &&
!document['webkitFullscreenElement'] &&
!document['mozFullScreenElement'] &&
!document['msFullscreenElement'] ) {
this.isFullscreen = false;
} else {
this.isFullscreen = true;
}
}
toggleFullscreen($event) {
// This will only work on the main video on a media page (not comment attachments)
// TODO: make this work on pages with more than one m-video (i.e. feeds)
const elem = document.querySelector('m-video');
// If fullscreen is not already enabled
if ( !document['fullscreenElement'] &&
!document['webkitFullscreenElement'] &&
!document['mozFullScreenElement'] &&
!document['msFullscreenElement'] ) {
// Request full screen
if (elem.requestFullscreen) {
elem.requestFullscreen();
} else if (elem['webkitRequestFullscreen']) {
elem['webkitRequestFullscreen']();
} else if (elem['mozRequestFullScreen']) {
elem['mozRequestFullScreen']();
} else if (elem['msRequestFullscreen']) {
elem['msRequestFullscreen']();
}
this.isFullscreen = true;
return;
}
// If fullscreen is already enabled, exit it
if ( document.exitFullscreen ) {
document.exitFullscreen();
} else if (document['webkitExitFullscreen']) {
document['webkitExitFullscreen']();
} else if (document['mozCancelFullScreen']) {
document['mozCancelFullScreen']();
} else if (document['msExitFullscreen']) {
document['msExitFullscreen']();
}
this.isFullscreen = false;
}
}
export { VideoAds } from './ads.component';
......@@ -596,3 +596,9 @@ m-media--grid{
.m-media-content--button-boost{
padding-left: $minds-padding;
}
.m-comment__attachment {
img, minds-video {
max-width: 50%;
}
}
......@@ -12,7 +12,7 @@
[style.line-height]="stageHeight + 'px'"
(mouseenter)="onMouseEnterStage()"
(mouseleave)="onMouseLeaveStage()"
(touchend)="showOverlays()"
(touchend)="showOverlaysOnTablet()"
>
<!-- LOADING PANEL -->
<div class="m-mediaModal__loadingPanel" *ngIf="isLoading">
......@@ -31,8 +31,8 @@
<img class="m-mediaModal__media--image"
[src]="thumbnail"
(load)="isLoaded()"
[style.height]="entity.height + 'px'"
[style.width]="entity.width + 'px'"
[style.height]="entityHeight + 'px'"
[style.width]="entityWidth + 'px'"
/>
</div>
......@@ -43,8 +43,8 @@
[style.height]="mediaHeight + 'px'"
>
<m-video class="m-mediaModal__media--video"
[style.height]="entity.height + 'px'"
[style.width]="entity.width + 'px'"
[style.height]="entityHeight + 'px'"
[style.width]="entityWidth + 'px'"
[isModal]="true"
[autoplay]="true"
[muted]="false"
......@@ -62,22 +62,28 @@
<!-- OVERLAY -->
<div class="m-mediaModal__overlayContainer"
*ngIf="showOverlay"
*ngIf="overlayVisible"
@fastFadeAnimation
>
<div class="m-mediaModal__overlayTitleWrapper">
<!-- TITLE -->
<span class="m-mediaModal__overlayTitle m-mediaModal__overlayTitle--notFullscreen" *ngIf="!isFullscreen">
<a [routerLink]="['/media', entity.entity_guid]">{{title}}</a>
<a [routerLink]="['/media', entity.entity_guid]"
(click)="$event.stopPropagation()"
>{{title}}</a>
</span>
<!-- TITLE: FULLSCREEN -->
<span class="m-mediaModal__overlayTitle m-mediaModal__overlayTitle--fullscreen" *ngIf="isFullscreen">
<a [routerLink]="['/', entity.ownerObj.username]">
<a [routerLink]="['/', entity.ownerObj.username]"
(click)="$event.stopPropagation()"
>
<img class="avatar" [src]="minds.cdn_url + 'icon/' + entity.ownerObj.guid + '/small/' + ownerIconTime" class="mdl-shadow--2dp"/>
<span title={{entity.ownerObj.name}}>{{entity.ownerObj.name}}</span>
</a>
<div class="m-mediaModal__overlayTitleSeparator"></div>
<a [routerLink]="['/media', entity.entity_guid]">{{title}}</a>
<a [routerLink]="['/media', entity.entity_guid]"
(click)="$event.stopPropagation()"
>{{title}}</a>
</span>
</div>
<!-- FULLSCREEN BUTTON -->
......
......@@ -55,6 +55,8 @@ export class MediaModalComponent implements OnInit, OnDestroy {
stageHeight: number;
mediaWidth: number;
mediaHeight: number;
entityWidth: number;
entityHeight: number;
maxStageWidth: number;
maxHeight: number;
......@@ -76,15 +78,15 @@ export class MediaModalComponent implements OnInit, OnDestroy {
isOpen: boolean = false;
isOpenTimeout: any = null;
showOverlay: boolean = false;
overlayVisible: boolean = false;
tabletOverlayTimeout: any = null;
routerSubscription: Subscription;
@Input('entity') set data(entity) {
this.entity = entity;
this.entity.width = 0;
this.entity.height = 0;
this.entityWidth = 0;
this.entityHeight = 0;
}
// Used to make sure video progress bar seeker / hover works
......@@ -145,7 +147,6 @@ export class MediaModalComponent implements OnInit, OnDestroy {
// (but don't actually redirect)
this.location.replaceState(`/media/${this.entity.entity_guid}`);
// When user clicks a link from inside the modal
this.routerSubscription = this.router.events.subscribe((event: Event) => {
if (event instanceof NavigationStart) {
......@@ -168,16 +169,16 @@ export class MediaModalComponent implements OnInit, OnDestroy {
if (!this.isVideo) {
// Image
this.entity.width = this.entity.custom_data[0].width;
this.entity.height = this.entity.custom_data[0].height;
this.entityWidth = this.entity.custom_data[0].width;
this.entityHeight = this.entity.custom_data[0].height;
this.thumbnail = `${this.minds.cdn_url}fs/v1/thumbnail/${this.entity.entity_guid}/xlarge`;
} else {
this.entity.width = this.entity.custom_data.dimensions.width;
this.entity.height = this.entity.custom_data.dimensions.height;
this.entityWidth = this.entity.custom_data.dimensions.width;
this.entityHeight = this.entity.custom_data.dimensions.height;
this.thumbnail = this.entity.custom_data.thumbnail_src; // Not currently used
}
this.aspectRatio = this.entity.width / this.entity.height;
this.aspectRatio = this.entityWidth / this.entityHeight;
this.calculateDimensions();
}
......@@ -211,7 +212,7 @@ export class MediaModalComponent implements OnInit, OnDestroy {
// If black stage background is visible on top/bottom, each strip should be at least 20px high
const heightDiff = this.stageHeight - this.mediaHeight;
if ( 0 < heightDiff && heightDiff <= this.padding * 2) {
this.stageHeight += 40;
this.stageHeight += (this.padding * 2);
}
} else { // isFullscreen
......@@ -221,8 +222,15 @@ export class MediaModalComponent implements OnInit, OnDestroy {
this.stageWidth = windowWidth;
this.stageHeight = windowHeight;
// Set mediaHeight as tall as possible but not taller than instrinsic height
this.mediaHeight = this.entity.height < windowHeight ? this.entity.height : windowHeight;
if (this.entity.custom_type === 'image') {
// For images, set mediaHeight as tall as possible but not taller than instrinsic height
this.mediaHeight = this.entityHeight < windowHeight ? this.entityHeight : windowHeight;
} else {
// It's ok if videos are taller than intrinsic height
this.mediaHeight = windowHeight;
}
this.mediaWidth = this.scaleWidth();
if ( this.mediaWidth > windowWidth ) {
......@@ -233,8 +241,8 @@ export class MediaModalComponent implements OnInit, OnDestroy {
}
if (this.isVideo) {
this.entity.height = this.mediaHeight;
this.entity.width = this.mediaWidth;
this.entityHeight = this.mediaHeight;
this.entityWidth = this.mediaWidth;
}
this.modalWidth = this.stageWidth + this.contentWidth;
......@@ -248,13 +256,13 @@ export class MediaModalComponent implements OnInit, OnDestroy {
this.stageHeight = Math.max(this.maxHeight, this.minStageHeight);
// Set mediaHeight as tall as stage but no larger than intrinsic height
if (!this.isVideo && this.entity.height < this.stageHeight) {
if (!this.isVideo && this.entityHeight < this.stageHeight) {
// Image is shorter than stage; scale down stage
this.mediaHeight = this.entity.height;
this.mediaHeight = this.entityHeight;
this.stageHeight = Math.max(this.mediaHeight, this.minStageHeight);
} else {
// Image is taller than stage; scale it down so it fits inside stage
// All videos should be as tall as possible but not taller than stage
// Either: Image is taller than stage; scale it down so it fits inside stage
// Or: Video should be as tall as possible but not taller than stage
this.mediaHeight = this.stageHeight;
}
......@@ -276,13 +284,11 @@ export class MediaModalComponent implements OnInit, OnDestroy {
// shrink vertically until it hits minStageHeight
// When window is narrower than this, start to shrink height
const verticalShrinkWidthThreshold = this.mediaWidth + this.contentWidth + (this.padding * 4); // + 2;
const verticalShrinkWidthThreshold = this.mediaWidth + this.contentWidth + (this.padding * 4);
const widthDiff = verticalShrinkWidthThreshold - window.innerWidth;
// Is window narrow enough to start shrinking vertically?
if ( widthDiff >= 1 ) {
if (widthDiff >= 1) {
// What mediaHeight would be if it shrunk proportionally to difference in width
const mediaHeightPreview = Math.round((this.mediaWidth - widthDiff) / this.aspectRatio);
......@@ -293,7 +299,7 @@ export class MediaModalComponent implements OnInit, OnDestroy {
this.stageHeight = this.mediaHeight;
} else {
this.stageHeight = this.minStageHeight;
this.mediaHeight = Math.min(this.minStageHeight, this.entity.height);
this.mediaHeight = Math.min(this.minStageHeight, this.entityHeight);
this.mediaWidth = this.scaleWidth();
}
}
......@@ -316,7 +322,7 @@ export class MediaModalComponent implements OnInit, OnDestroy {
onFullscreenChange(event) {
this.calculateDimensions();
if ( !document.fullscreenElement &&
!document['webkitFullScreenElement'] &&
!document['webkitFullscreenElement'] &&
!document['mozFullScreenElement'] &&
!document['msFullscreenElement'] ) {
this.isFullscreen = false;
......@@ -332,10 +338,10 @@ export class MediaModalComponent implements OnInit, OnDestroy {
// If fullscreen is not already enabled
if ( !document['fullscreenElement'] &&
!document['webkitFullScreenElement'] &&
!document['webkitFullscreenElement'] &&
!document['mozFullScreenElement'] &&
!document['msFullscreenElement'] ) {
// Request full screen
// Request full screen
if (elem.requestFullscreen) {
elem.requestFullscreen();
} else if (elem['webkitRequestFullscreen']) {
......@@ -384,7 +390,7 @@ export class MediaModalComponent implements OnInit, OnDestroy {
// Show overlay and video controls
onMouseEnterStage() {
this.showOverlay = true;
this.overlayVisible = true;
if (this.isVideo) {
// Make sure progress bar seeker is updating when video controls are visible
......@@ -393,9 +399,8 @@ export class MediaModalComponent implements OnInit, OnDestroy {
}
}
// Hide overlay and video controls
onMouseLeaveStage() {
this.showOverlay = false;
this.overlayVisible = false;
if (this.isVideo) {
// Stop updating progress bar seeker when controls aren't visible
......@@ -407,7 +412,7 @@ export class MediaModalComponent implements OnInit, OnDestroy {
// * TABLETS ONLY: SHOW OVERLAY & VIDEO CONTROLS * -------------------------------------------
// Briefly display title overlay and video controls when finished loading and stage touch
showOverlays() {
showOverlaysOnTablet() {
this.onMouseEnterStage();
if (this.tabletOverlayTimeout) {
......@@ -425,7 +430,7 @@ export class MediaModalComponent implements OnInit, OnDestroy {
this.isLoading = false;
if ( this.isTablet ) {
this.showOverlays();
this.showOverlaysOnTablet();
}
}
......
......@@ -42,7 +42,6 @@ import { MindsVideoComponent } from '../../components/video/video.component';
[torrent]="[{ res: '720', key: object.guid + '/720.mp4' }, { res: '360', key: object.guid + '/360.mp4' }]"
[log]="object.guid"
[playCount]="false"
(click)="togglePlay($event)"
#player>
<video-ads [player]="player" *ngIf="object.monetized"></video-ads>
......
<div class="m-marketing m-mobile--marketing">
<!--Top / Hero -->
<div class="m-marketing--hero">
<div class="m-marketing--hero--video">
<img [src]="minds.cdn_assets_url + 'assets/photos/circles.png'">
</div>
<div class="m-marketing--hero--inner">
<div class="m-marketing--hero--slogans">
<h2>
Minds Android App
</h2>
<h1>
Minds Mobile App
</h1>
</div>
<div class="m-marketing--hero--actions">
<i class="material-icons m-mobile__giantDroid">android</i>
<!-- Space for an icon -->
</div>
</div>
</div>
<section class="m-marketing--section mdl-color--white" style="padding-bottom: 0; padding-top: 0">
<div class="m-marketing--downloadPlatform">
<h2>
For Android:
</h2>
<!--Android APK -->
<div class="m-marketing--downloadOption">
<div class="m-marketing--downloadButton">
<a [href]="latestRelease.href" target="_blank"><img [src]="minds.cdn_assets_url + 'assets/marketing/mobile-dl-button.png'"/></a>
<span>(recommended)</span>
</div>
<p>Download the mobile app directly from Minds for the best experience. All content is accessible.
<p>In order to install this version you must:</p>
<ul>
<li>Update Phone settings to "enable downloads from unverified source"</li>
<li>Download and install!</li>
</ul>
</div>
<!--Play Store -->
<div class="m-marketing--downloadOption">
<div class="m-marketing--downloadButton">
<a href='https://play.google.com/store/apps/details?id=com.minds.mobile&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1' target="_blank"><img alt='Get it on Google Play' src='https://play.google.com/intl/en_gb/badges/images/generic/en_badge_web_generic.png'/></a>
</div>
<p>You may also download the mobile app from the Google Play Store. Due to the Google Play Store terms of service, NSFW content is not accessible in this version.</p>
</div>
</div>
<!--iOS Download -->
<div class="m-marketing--downloadPlatform">
<h2>
For iPhone:
</h2>
<div class="m-marketing--downloadOption">
<div class="m-marketing--downloadButton m-marketing--downloadButton--iOS">
<a href="https://geo.itunes.apple.com/us/app/minds-com/id961771928?mt=8&amp;uo=6" target="_blank"><img alt="iOS App" src="https://devimages-cdn.apple.com/app-store/marketing/guidelines/images/badge-download-on-the-app-store.svg"></a>
</div>
<p>iPhone users may download the mobile app directly from the iOS App Store. All features of the Minds Token (wallet, boost, wire, etc) are not currently supported in this version. All content is accessible.</p>
</div>
</div>
<div class="m-marketing--section--subsection m-marketing--section--subsection--first">
<div class="m-marketing--section--subsection-container">
<div class="m-marketing--section--subsection-left m-marketing--section--subsection-text">
<div class="mdl-grid">
<h2>
Release History:
</h2>
<div class="mdl-cell mdl-cell--12-col m-mobile--marketing--spinner" *ngIf="inProgress">
<div class="mdl-spinner mdl-js-spinner is-active"></div>
</div>
......@@ -43,7 +75,6 @@
<ng-container *ngIf="release.unstable">Canary -</ng-container>
{{release.version}}
</h2>
<p>Released: {{release.timestamp * 1000 | date:'longDate'}} -
<a [href]="release.href" target="_blank">
Download
......
.m-mobile--marketing {
.m-marketing--hero {
padding:52px 0 !important;
margin-bottom: 32px;
padding: 0;
@media screen and (min-width: $max-mobile) {
padding:52px 0 !important;
}
.m-marketing--hero--inner {
padding-top: 52px;
padding-bottom: 52px;
flex-wrap: wrap;
padding: 52 0 52 0;
.m-marketing--hero--slogans h1 {
text-align: left;
padding-left: $minds-padding * 2;
font-size: xx-large;
font-weight: 700;
@media screen and (min-width: $min-tablet) {
margin-left: 120px;
font-size: 42px;
}
@include m-theme(){
color: themed($m-white-always);
}
}
}
img {
......@@ -24,7 +41,31 @@
}
.m-marketing--section--subsection--first .m-marketing--section--subsection-container {
padding-top: 32px !important;
padding-top: 0;
display: flex;
flex-direction: row;
align-items: center;
text-align: left;
width: 100%;
@media screen and (min-width: $min-tablet) {
width: unset;
margin: 0 25%;
}
.m-marketing--section--subsection-text {
max-width: 100%;
min-width: 100%;
margin: 0;
padding: 0;
h2 {
text-align: left;
font-size: x-large;
font-weight: 700;
padding: $minds-padding * 1.5 0;
margin: 0 0 ($minds-padding * 2) ($minds-padding * 2);
}
}
}
.m-marketing--hero--actions img {
......@@ -47,6 +88,72 @@
}
}
.m-marketing--section {
text-align: left;
margin-left: $minds-padding * 2;
h2 {
text-align: left;
font-size: x-large;
font-weight: 700;
padding-left: 12px;
}
.m-marketing--downloadPlatform {
@media screen and (min-width: $min-tablet) {
margin: 0 25%;
}
.m-marketing--downloadOption {
padding-bottom: $minds-padding * 2;
@media screen and (max-width: $min-tablet) {
padding-bottom: $minds-padding * 1;
}
.m-marketing--downloadButton--iOS {
img {
padding-left: 14px;
}
}
.m-marketing--downloadButton {
display: flex;
flex-wrap: wrap;
padding-bottom: $minds-padding * 4;
@media screen and (max-width: $min-tablet) {
padding-bottom: $minds-padding * 2;
}
img {
max-width: 200px;
min-width: 175px;
}
}
span {
font-size: large;
align-self: center;
padding-left: $minds-padding * 2;
font-weight: 400;
}
p {
padding: 0 ($minds-padding * 1.5);
line-height: 24pt;
@include m-theme(){
color: themed($m-grey-600);
}
}
li {
@include m-theme(){
color: themed($m-grey-600);
}
}
}
}
}
@media screen and (min-width: 500px) {
.m-marketing--hero--slogans h2 {
font-size: 60px !important;
......@@ -94,10 +201,3 @@
}
}
.m-mobile__giantDroid {
font-size: 152px;
@include m-theme(){
color: themed($m-white);
}
}