...
 
Commits (22)
image: markharding/minds-front-base
services:
- docker:dind
stages:
- test
- build
......@@ -154,6 +151,8 @@ build:production:i18n:
prepare:review:
stage: prepare
image: minds/ci:latest
services:
- docker:dind
script:
- 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/.
......@@ -179,6 +178,8 @@ prepare:review:sentry:
prepare:production:
stage: prepare
image: minds/ci:latest
services:
- docker:dind
script:
- 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/.
......@@ -261,6 +262,8 @@ review:stop:
.deploy: &deploy
image: minds/ci:latest
services:
- docker:dind
script:
## Sync assets with CDN
- aws s3 sync dist $S3_REPOSITORY_URL
......
......@@ -6,7 +6,7 @@ Minds Front
Front-end web application for Minds. Please run inside of [the Minds repo](https://github.com/minds/minds).
## Documentation
Documentation for Minds can be found at [minds.org/docs](https://www.minds.org/docs)
Please see the documentation on [developers.minds.com](https://developers.minds.com) for instructions on how to [build the Minds Front-end](https://developers.minds.com/docs/guides/frontend).
### Building
Please see the documentation on Minds.org for instructions on how to [build the Minds Front-end](https://www.minds.org/docs/install/preparation.html#front-end).
......
import generateRandomId from '../support/utilities';
context('Newsfeed', () => {
before(() => {
cy.getCookie('minds_sess').then(sessionCookie => {
......@@ -14,7 +16,6 @@ context('Newsfeed', () => {
cy.route('POST', '**/api/v1/media').as('mediaPOST');
cy.route('POST', '**/api/v1/newsfeed/**').as('newsfeedEDIT');
cy.route('POST', '**/api/v1/media/**').as('mediaEDIT');
cy.visit('/newsfeed/subscriptions')
.location('pathname')
.should('eq', '/newsfeed/subscriptions');
......@@ -37,6 +38,19 @@ context('Newsfeed', () => {
cy.get('minds-newsfeed-poster textarea').type(content);
};
const attachRichEmbed = (embedUrl) => {
cy.get('minds-newsfeed-poster').should('be.visible');
cy.get('minds-newsfeed-poster textarea')
.type(embedUrl);
cy.route('GET', `**/api/v1/newsfeed/preview?url=${embedUrl}**`)
.as('previewGET')
.wait('@previewGET')
.then(xhr => {
expect(xhr.status).to.equal(200);
});
}
const attachImageToActivity = () => {
cy.uploadFile(
'#attachment-input-poster',
......@@ -511,4 +525,140 @@ context('Newsfeed', () => {
deleteActivityFromNewsfeed();
});
it('should show a rich embed post from youtube in a modal', () => {
const content = generateRandomId() + " ",
url = 'https://www.youtube.com/watch?v=jNQXAC9IVRw';
// set up post.
newActivityContent(content);
attachRichEmbed(url);
// post and await.
cy.get('.m-posterActionBar__PostButton')
.click()
.wait('@newsfeedPOST').then(xhr => {
expect(xhr.status).to.equal(200);
//get activity, click it.
cy.get(`[minds-data-activity-guid='${xhr.response.body.guid}']`)
.click();
//check modal is open.
cy.get('[data-cy=data-minds-media-modal]')
.contains(content);
// close modal and tidy.
cy.get('.m-overlay-modal--backdrop')
.click({force: true});
deleteActivityFromNewsfeed();
});
});
it('should not open vimeo in a modal', () => {
const content = generateRandomId() + " ",
url = 'https://vimeo.com/8733915';
// set up post.
newActivityContent(content);
attachRichEmbed(url);
// post and await.
cy.get('.m-posterActionBar__PostButton')
.click()
.wait('@newsfeedPOST').then(xhr => {
expect(xhr.status).to.equal(200);
//get activity, make assertions tht would not be true for modals.
cy.get(`[minds-data-activity-guid='${xhr.response.body.guid}']`)
.should('be.visible')
.get('iframe')
.should('be.visible')
.get('.minds-more')
.should('be.visible');
// tidy.
deleteActivityFromNewsfeed();
});
});
it('should not open soundcloud in a modal', () => {
const content = generateRandomId() + " ",
url = 'https://soundcloud.com/richarddjames/piano-un10-it-happened';
// set up post.
newActivityContent(content);
attachRichEmbed(url);
// post and await.
cy.get('.m-posterActionBar__PostButton')
.click()
.wait('@newsfeedPOST').then(xhr => {
expect(xhr.status).to.equal(200);
//get activity, make assertions tht would not be true for modals.
cy.get(`[minds-data-activity-guid='${xhr.response.body.guid}']`)
.should('be.visible')
.get('.m-rich-embed-action-overlay')
.should('be.visible')
.get('.minds-more')
.should('be.visible');
deleteActivityFromNewsfeed();
});
});
it('should not open spotify in a modal', () => {
const content = generateRandomId() + " ",
url = 'https://open.spotify.com/track/2MZSXhq4XDJWu6coGoXX1V?si=nvja0EfwR3q6GMQmYg6gPQ';
// set up post.
newActivityContent(content);
attachRichEmbed(url);
// post and await.
cy.get('.m-posterActionBar__PostButton')
.click()
.wait('@newsfeedPOST').then(xhr => {
expect(xhr.status).to.equal(200);
//get activity, make assertions tht would not be true for modals.
cy.get(`[minds-data-activity-guid='${xhr.response.body.guid}']`)
.should('be.visible')
.get('.m-rich-embed-action-overlay')
.should('be.visible')
.get('.minds-more')
.should('be.visible');
deleteActivityFromNewsfeed();
});
});
it('should not open spotify in a modal', () => {
const content = generateRandomId() + " ",
url = 'http://giphygifs.s3.amazonaws.com/media/IzVquL965ib4s/giphy.gif';
// set up post.
newActivityContent(content);
attachRichEmbed(url);
// post and await.
cy.get('.m-posterActionBar__PostButton')
.click()
.wait('@newsfeedPOST').then(xhr => {
expect(xhr.status).to.equal(200);
//get activity, make assertions tht would not be true for modals.
cy.get(`[minds-data-activity-guid='${xhr.response.body.guid}']`)
.should('be.visible')
.get('.m-rich-embed-action-overlay')
.should('be.visible')
.get('.minds-more')
.should('be.visible');
deleteActivityFromNewsfeed();
});
});
});
......@@ -41,13 +41,13 @@
display: flex;
flex-direction: row;
align-items: center;
padding: 0 8px 0 0;
list-style: none;
opacity: 1;
text-overflow: ellipsis;
text-align: left;
a {
> * {
padding: 0 8px 0 0;
text-decoration: none;
width: 100%;
font-weight: 400;
......@@ -94,11 +94,13 @@
.m-dropdown--list--item,
.m-dropdownList__item {
padding: 8px;
@include m-theme() {
border-bottom: 1px solid themed($m-grey-50);
}
> * {
padding: 8px;
}
&.m-dropdown--list--item--selected,
.m-dropdownList__item--selected {
@include m-theme() {
......
import { Injectable } from '@angular/core';
import {
filter,
first,
map,
switchMap,
mergeMap,
skip,
take,
} from 'rxjs/operators';
import { filter, first, switchMap, mergeMap, skip, take } from 'rxjs/operators';
import { FeedsService } from '../../services/feeds.service';
import { Subscription } from 'rxjs';
@Injectable()
export class FeaturedContentService {
offset: number = -1;
offset = 0;
maximumOffset = 0;
feedLength = 0;
protected feedSubscription: Subscription;
constructor(protected feedsService: FeedsService) {
this.onInit();
}
onInit() {
this.feedSubscription = this.feedsService.feed.subscribe(feed => {
this.feedLength = feed.length;
this.maximumOffset = this.feedLength - 1;
});
this.feedsService
.setLimit(12)
.setOffset(0)
......@@ -23,28 +28,36 @@ export class FeaturedContentService {
}
async fetch() {
if (this.offset >= this.feedsService.rawFeed.getValue().length) {
this.offset = -1;
}
// Refetch every 2 calls, if not loading
if (this.offset % 2 && !this.feedsService.inProgress.getValue()) {
this.feedsService.clear();
this.feedsService.fetch();
}
return await this.feedsService.feed
.pipe(
filter(feed => feed.length > 0),
first(),
mergeMap(feed => feed),
filter(entities => entities.length > 0),
mergeMap(feed => feed), // Convert feed array to stream
skip(this.offset++),
take(1),
switchMap(async entity => {
if (!entity) {
return false;
} else {
const resolvedEntity = await entity.pipe(first()).toPromise();
this.resetOffsetAtEndOfStream();
return resolvedEntity;
}
return await entity.pipe(first()).toPromise();
})
)
.toPromise();
}
protected resetOffsetAtEndOfStream() {
if (this.offset >= this.maximumOffset) {
this.offset = 0;
this.fetchNextFeed();
}
}
protected fetchNextFeed() {
if (!this.feedsService.inProgress.getValue()) {
this.feedsService.clear();
this.feedsService.fetch();
}
}
}
......@@ -31,6 +31,12 @@ export class NSFWSelectorComponent {
private storage: Storage
) {}
ngOnInit() {
if (this.service.reasons) {
this.service.reasons.map(r => this.toggle(r.value));
}
}
get service() {
switch (this.serviceRef) {
case 'editing':
......
......@@ -55,7 +55,7 @@
[href]="src.perma_url"
target="_blank"
rel="noopener noreferrer"
class="meta mdl-color-text--blue-grey-900"
class="meta"
[ngClass]="{ 'm-rich-embed-has-thumbnail': src.thumbnail_src, 'm-rich-embed--title--no-padding': hasInlineContentLoaded() }"
>
<h2
......@@ -74,10 +74,7 @@
<a class="thumbnail" *ngIf="preview.thumbnail">
<img src="{{preview.thumbnail}}" />
</a>
<a
class="meta mdl-color-text--blue-grey-900"
[ngClass]="{ 'm-has-thumbnail': preview.thumbnail }"
>
<a class="meta" [ngClass]="{ 'm-has-thumbnail': preview.thumbnail }">
<h2 class="m-rich-embed--title mdl-card__title-text">
{{preview.title | excerpt}}
</h2>
......
......@@ -16,9 +16,6 @@ minds-rich-embed {
left: 0;
width: 100%;
height: 100%;
@include m-theme() {
background-color: rgba(themed($m-black-always), 0.2);
}
&:hover {
background: transparent;
......
......@@ -3,11 +3,14 @@ import {
ElementRef,
ChangeDetectorRef,
ChangeDetectionStrategy,
Output,
EventEmitter,
} from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
import { RichEmbedService } from '../../../services/rich-embed';
import mediaProxyUrl from '../../../helpers/media-proxy-url';
import { FeaturesService } from '../../../services/features.service';
@Component({
moduleId: module.id,
......@@ -17,18 +20,22 @@ import mediaProxyUrl from '../../../helpers/media-proxy-url';
})
export class MindsRichEmbed {
type: string = '';
mediaSource: string = '';
src: any = {};
preview: any = {};
maxheight: number = 320;
inlineEmbed: any = null;
embeddedInline: boolean = false;
cropImage: boolean = false;
modalRequestSubscribed: boolean = false;
@Output() mediaModalRequested: EventEmitter<any> = new EventEmitter();
private lastInlineEmbedParsed: string;
constructor(
private sanitizer: DomSanitizer,
private service: RichEmbedService,
private cd: ChangeDetectorRef
private cd: ChangeDetectorRef,
protected featureService: FeaturesService
) {}
set _src(value: any) {
......@@ -65,6 +72,14 @@ export class MindsRichEmbed {
// Inline Embedding
let inlineEmbed = this.parseInlineEmbed(this.inlineEmbed);
if (
this.featureService.has('media-modal') &&
this.mediaSource === 'youtube'
) {
this.modalRequestSubscribed =
this.mediaModalRequested.observers.length > 0;
}
if (
inlineEmbed &&
inlineEmbed.id &&
......@@ -80,9 +95,35 @@ export class MindsRichEmbed {
}
this.inlineEmbed = inlineEmbed;
if (
this.modalRequestSubscribed &&
this.featureService.has('media-modal') &&
this.mediaSource === 'youtube'
) {
if (this.inlineEmbed && this.inlineEmbed.htmlProvisioner) {
this.inlineEmbed.htmlProvisioner().then(html => {
this.inlineEmbed.html = html;
this.detectChanges();
});
// @todo: catch any error here and forcefully window.open to destination
}
}
}
action($event) {
if (
this.modalRequestSubscribed &&
this.featureService.has('media-modal') &&
this.mediaSource === 'youtube'
) {
$event.preventDefault();
$event.stopPropagation();
this.mediaModalRequested.emit();
return;
}
if (this.inlineEmbed && !this.embeddedInline) {
$event.preventDefault();
$event.stopPropagation();
......@@ -120,6 +161,7 @@ export class MindsRichEmbed {
if ((matches = youtube.exec(url)) !== null) {
if (matches[1]) {
this.mediaSource = 'youtube';
return {
id: `video-youtube-${matches[1]}`,
className:
......@@ -138,12 +180,13 @@ export class MindsRichEmbed {
if ((matches = vimeo.exec(url)) !== null) {
if (matches[1]) {
this.mediaSource = 'vimeo';
return {
id: `video-vimeo-${matches[1]}`,
className:
'm-rich-embed-video m-rich-embed-video-iframe m-rich-embed-video-vimeo',
html: this.sanitizer.bypassSecurityTrustHtml(`<iframe
src="https://player.vimeo.com/video/${matches[1]}?autoplay=1&title=0&byline=0&portrait=0"
src="https://player.vimeo.com/video/${matches[1]}?title=0&byline=0&portrait=0"
frameborder="0"
webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>`),
playable: true,
......@@ -156,6 +199,7 @@ export class MindsRichEmbed {
if ((matches = soundcloud.exec(url)) !== null) {
if (matches[1]) {
this.mediaSource = 'soundcloud';
return {
id: `audio-soundcloud-${matches[1]}`,
className:
......@@ -183,6 +227,7 @@ export class MindsRichEmbed {
if ((matches = spotify.exec(url)) !== null) {
if (matches[1]) {
this.mediaSource = 'spotify';
return {
id: `audio-spotify-${matches[1]}`,
className:
......@@ -207,7 +252,7 @@ export class MindsRichEmbed {
if (!id) {
return null;
}
this.mediaSource = 'giphy';
return {
id: `image-giphy-${matches[1]}`,
className:
......@@ -225,7 +270,11 @@ export class MindsRichEmbed {
}
hasInlineContentLoaded() {
return this.embeddedInline && this.inlineEmbed && this.inlineEmbed.html;
return this.featureService.has('media-modal')
? !this.modalRequestSubscribed &&
this.inlineEmbed &&
this.inlineEmbed.html
: this.embeddedInline && this.inlineEmbed && this.inlineEmbed.html;
}
detectChanges() {
......
......@@ -159,10 +159,12 @@
*ngIf="getCurrentUser()"
(click)="toggleTheme()"
>
<i class="material-icons" *ngIf="isDark">brightness_7</i>
<i class="material-icons" *ngIf="!isDark">brightness_2</i>
<span i18n *ngIf="isDark">Light Mode</span>
<span i18n *ngIf="!isDark">Dark Mode</span>
<div>
<i class="material-icons" *ngIf="isDark">brightness_7</i>
<i class="material-icons" *ngIf="!isDark">brightness_2</i>
<span i18n *ngIf="isDark">Light Mode</span>
<span i18n *ngIf="!isDark">Dark Mode</span>
</div>
</li>
</ng-container>
......
......@@ -4,13 +4,6 @@ import { Client } from '../../services/api/client';
import { Session } from '../../services/session';
import { Storage } from '../../services/storage';
import AsyncLock from '../../helpers/async-lock';
import MindsClientHttpAdapter from '../../lib/minds-sync/adapters/MindsClientHttpAdapter.js';
import browserStorageAdapterFactory from '../../helpers/browser-storage-adapter-factory';
import BlockListSync from '../../lib/minds-sync/services/BlockListSync.js';
import AsyncStatus from '../../helpers/async-status';
@Injectable()
export class BlockListService {
blocked: BehaviorSubject<string[]>;
......
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable, of } from 'rxjs';
import { first, catchError } from 'rxjs/operators';
import { BehaviorSubject } from 'rxjs';
import { first } from 'rxjs/operators';
import { Client } from '../../services/api';
import { BlockListService } from './block-list.service';
import MindsClientHttpAdapter from '../../lib/minds-sync/adapters/MindsClientHttpAdapter.js';
import browserStorageAdapterFactory from '../../helpers/browser-storage-adapter-factory';
import EntitiesSync from '../../lib/minds-sync/services/EntitiesSync.js';
import AsyncStatus from '../../helpers/async-status';
import normalizeUrn from '../../helpers/normalize-urn';
type EntityObservable = BehaviorSubject<Object>;
type EntityObservables = Map<string, EntityObservable>;
......
......@@ -6,40 +6,8 @@ import { Session } from '../../services/session';
import { EntitiesService } from './entities.service';
import { BlockListService } from './block-list.service';
import MindsClientHttpAdapter from '../../lib/minds-sync/adapters/MindsClientHttpAdapter.js';
import browserStorageAdapterFactory from '../../helpers/browser-storage-adapter-factory';
import FeedsSync from '../../lib/minds-sync/services/FeedsSync.js';
import hashCode from '../../helpers/hash-code';
import AsyncStatus from '../../helpers/async-status';
import { BehaviorSubject, Observable, of, forkJoin, combineLatest } from 'rxjs';
import {
take,
switchMap,
map,
tap,
skipWhile,
first,
filter,
} from 'rxjs/operators';
export type FeedsServiceGetParameters = {
endpoint: string;
timebased: boolean;
//
limit: number;
offset?: number;
//
syncPageSize?: number;
forceSync?: boolean;
};
export type FeedsServiceGetResponse = {
entities: any[];
next?: number;
};
import { BehaviorSubject, Observable, combineLatest } from 'rxjs';
import { switchMap, map, tap, first } from 'rxjs/operators';
/**
* Enables the grabbing of data through observable feeds.
......@@ -69,6 +37,7 @@ export class FeedsService {
this.pageSize = this.offset.pipe(
map(offset => this.limit.getValue() + offset)
);
this.feed = this.rawFeed.pipe(
tap(feed => {
if (feed.length) this.inProgress.next(true);
......@@ -87,6 +56,7 @@ export class FeedsService {
this.inProgress.next(false);
})
);
this.hasMore = combineLatest(
this.rawFeed,
this.inProgress,
......
......@@ -219,7 +219,12 @@
<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>
<minds-rich-embed
(mediaModalRequested)="openModal()"
[src]="activity"
[maxheight]="480"
>
</minds-rich-embed>
</div>
<div
......
<div class="m-mediaModal__wrapper">
<div class="m-mediaModal__wrapper" data-cy="data-minds-media-modal">
<div class="m-mediaModal__theater" (click)="clickedModal($event)">
<div
class="m-mediaModal m-mediaModal__clearFix"
......@@ -15,7 +15,10 @@
(touchend)="showOverlaysOnTablet()"
>
<!-- LOADING PANEL -->
<div class="m-mediaModal__loadingPanel" *ngIf="isLoading">
<div
class="m-mediaModal__loadingPanel"
*ngIf="isLoading && contentType !== 'rich-embed'"
>
<div class="mdl-spinner mdl-js-spinner is-active" [mdl]></div>
</div>
......@@ -65,6 +68,15 @@
</m-video>
</div>
<!-- RICH-EMBED -->
<div
class="m-mediaModal__mediaWrapper m-mediaModal__mediaWrapper--richEmbed"
*ngIf="contentType === 'rich-embed'"
>
<minds-rich-embed [src]="entity" [maxheight]="480">
</minds-rich-embed>
</div>
<!-- MEDIA: BLOG -->
<div
class="m-mediaModal__mediaWrapper m-mediaModal__mediaWrapper--blog"
......@@ -83,12 +95,12 @@
<!-- OVERLAY -->
<div
class="m-mediaModal__overlayContainer"
*ngIf="overlayVisible"
*ngIf="overlayVisible && contentType !== 'rich-embed'"
@fastFadeAnimation
>
<div
class="m-mediaModal__overlayTitleWrapper"
*ngIf="this.contentType !== 'blog'"
*ngIf="contentType !== 'blog'"
>
<!-- TITLE -->
<span
......
......@@ -99,6 +99,14 @@ m-overlay-modal {
}
}
.m-mediaModal__mediaWrapper--richEmbed {
width: 100%;
.meta {
display: none;
}
}
.m-mediaModal__mediaWrapper--blog {
line-height: initial;
overflow-y: auto;
......
import {
Component,
HostListener,
Injector,
Input,
OnDestroy,
OnInit,
ViewChild,
SkipSelf,
Injector,
ViewChild,
} from '@angular/core';
import { Location } from '@angular/common';
import { Event, NavigationStart, Router } from '@angular/router';
......@@ -26,6 +26,7 @@ import isMobileOrTablet from '../../../helpers/is-mobile-or-tablet';
import { ActivityService } from '../../../common/services/activity.service';
import { SiteService } from '../../../common/services/site.service';
import { ClientMetaService } from '../../../common/services/client-meta.service';
import { FeaturesService } from '../../../services/features.service';
export type MediaModalParams = {
redirectUrl?: string;
......@@ -119,66 +120,78 @@ export class MediaModalComponent implements OnInit, OnDestroy {
@ViewChild(MindsVideoComponent, { static: false })
videoComponent: MindsVideoComponent;
get videoDirectSrc() {
const sources = [
videoDirectSrc = [];
videoTorrentSrc = [];
constructor(
public session: Session,
public analyticsService: AnalyticsService,
private overlayModal: OverlayModalService,
private router: Router,
private location: Location,
private site: SiteService,
private clientMetaService: ClientMetaService,
private featureService: FeaturesService,
@SkipSelf() injector: Injector
) {
this.clientMetaService
.inherit(injector)
.setSource('single')
.setMedium('modal');
}
updateSources() {
this.videoDirectSrc = [
{
res: '720',
uri:
'api/v1/media/' + this.entity.entity_guid + '/play?s=modal&res=720',
'api/v1/media/' +
this.entity.entity_guid +
'/play/' +
Date.now() +
'?s=modal&res=720',
type: 'video/mp4',
},
{
res: '360',
uri: 'api/v1/media/' + this.entity.entity_guid + '/play?s=modal',
uri:
'api/v1/media/' +
this.entity.entity_guid +
'/play/' +
Date.now() +
'?s=modal',
type: 'video/mp4',
},
];
this.videoTorrentSrc = [
{ res: '720', key: this.entity.entity_guid + '/720.mp4' },
{ res: '360', key: this.entity.entity_guid + '/360.mp4' },
];
if (this.entity.custom_data.full_hd) {
sources.push({
this.videoDirectSrc.unshift({
res: '1080',
uri:
'api/v1/media/' + this.entity.entity_guid + '/play?s=modal&res=1080',
'api/v1/media/' +
this.entity.entity_guid +
'/play/' +
Date.now() +
'?s=modal&res=1080',
type: 'video/mp4',
});
}
return sources;
}
get videoTorrentSrc() {
const sources = [
{ res: '720', key: this.entity.entity_guid + '/720.mp4' },
{ res: '360', key: this.entity.entity_guid + '/360.mp4' },
];
if (this.entity.custom_data.full_hd) {
sources.push({ res: '1080', key: this.entity.entity_guid + '/1080.mp4' });
this.videoTorrentSrc.unshift({
res: '1080',
key: this.entity.entity_guid + '/1080.mp4',
});
}
return sources;
}
constructor(
public session: Session,
public analyticsService: AnalyticsService,
private overlayModal: OverlayModalService,
private router: Router,
private location: Location,
private site: SiteService,
private clientMetaService: ClientMetaService,
@SkipSelf() injector: Injector
) {
this.clientMetaService
.inherit(injector)
.setSource('single')
.setMedium('modal');
}
ngOnInit() {
// Prevent dismissal of modal when it's just been opened
this.isOpenTimeout = setTimeout(() => (this.isOpen = true), 20);
switch (this.entity.type) {
case 'activity':
this.title =
......@@ -190,6 +203,8 @@ export class MediaModalComponent implements OnInit, OnDestroy {
? this.entity.thumbnails.xlarge
: null;
this.updateSources();
switch (this.entity.custom_type) {
case 'video':
this.contentType = 'video';
......@@ -200,12 +215,41 @@ export class MediaModalComponent implements OnInit, OnDestroy {
? this.entity.custom_data.dimensions.height
: 720;
this.entity.thumbnail_src = this.entity.custom_data.thumbnail_src;
this.updateSources();
break;
case 'batch':
this.contentType = 'image';
this.entity.width = this.entity.custom_data[0].width;
this.entity.height = this.entity.custom_data[0].height;
break;
default:
if (
this.featureService.has('media-modal') &&
this.entity.perma_url &&
this.entity.title &&
!this.entity.entity_guid
) {
this.contentType = 'rich-embed';
this.entity.width = this.entity.custom_data.dimensions
? this.entity.custom_data.dimensions.width
: 1280;
this.entity.height = this.entity.custom_data.dimensions
? this.entity.custom_data.dimensions.height
: 720;
this.entity.thumbnail_src = this.entity.custom_data.thumbnail_src;
break;
} else {
// Modal not implemented, redirect.
this.router.navigate([
this.entity.route
? `/${this.entity.route}`
: `/blog/view/${this.entity.guid}`,
]);
// Close modal.
this.clickedBackdrop(null);
}
}
break;
case 'object':
switch (this.entity.subtype) {
......@@ -213,6 +257,9 @@ export class MediaModalComponent implements OnInit, OnDestroy {
this.contentType = 'video';
this.title = this.entity.title;
this.entity.entity_guid = this.entity.guid;
this.entity.custom_data = {
full_hd: this.entity.flags.full_hd,
};
break;
case 'image':
this.contentType = 'image';
......@@ -243,12 +290,10 @@ export class MediaModalComponent implements OnInit, OnDestroy {
if (this.redirectUrl) {
this.pageUrl = this.redirectUrl;
} else if (this.contentType !== 'blog') {
this.pageUrl = `/media/${this.entity.entity_guid}`;
} else if (this.contentType === 'rich-embed') {
this.pageUrl = `/newsfeed/${this.entity.guid}`;
} else {
this.pageUrl = this.entity.route
? `/${this.entity.route}`
: `/blog/view${this.entity.guid}`;
this.pageUrl = `/media/${this.entity.entity_guid}`;
}
this.boosted = this.entity.boosted || this.entity.p2p_boosted || false;
......@@ -559,7 +604,6 @@ export class MediaModalComponent implements OnInit, OnDestroy {
// Show overlay and video controls
onMouseEnterStage() {
this.overlayVisible = true;
if (this.contentType === 'video') {
// Make sure progress bar seeker is updating when video controls are visible
this.videoComponent.stageHover = true;
......
import { Component, ViewChild } from '@angular/core';
import { Router } from '@angular/router';
import { timer, Subscription } from 'rxjs';
import { Subscription, timer } from 'rxjs';
import { Client } from '../../../../services/api';
import { Session } from '../../../../services/session';
......@@ -71,8 +71,18 @@ export class MediaTheatreComponent {
@ViewChild(MindsVideoComponent, { static: false })
videoComponent: MindsVideoComponent;
get videoDirectSrc() {
const sources = [
videoDirectSrc = [];
videoTorrentSrc = [];
constructor(
public session: Session,
public client: Client,
public router: Router,
private recommended: RecommendedService
) {}
updateSources() {
this.videoDirectSrc = [
{
res: '720',
uri: 'api/v1/media/' + this.object.guid + '/play?s=modal&res=720',
......@@ -85,40 +95,29 @@ export class MediaTheatreComponent {
},
];
this.videoTorrentSrc = [
{ res: '720', key: this.object.guid + '/720.mp4' },
{ res: '360', key: this.object.guid + '/360.mp4' },
];
if (this.object.flags.full_hd) {
sources.push({
this.videoDirectSrc.unshift({
res: '1080',
uri: 'api/v1/media/' + this.object.guid + '/play?s=modal&res=1080',
type: 'video/mp4',
});
}
return sources;
}
get videoTorrentSrc() {
const sources = [
{ res: '720', key: this.object.guid + '/720.mp4' },
{ res: '360', key: this.object.guid + '/360.mp4' },
];
if (this.object.flags.full_hd) {
sources.push({ res: '1080', key: this.object.guid + '/1080.mp4' });
this.videoTorrentSrc.unshift({
res: '1080',
key: this.object.guid + '/1080.mp4',
});
}
return sources;
}
constructor(
public session: Session,
public client: Client,
public router: Router,
private recommended: RecommendedService
) {}
set _object(value: any) {
if (!value.guid) return;
this.object = value;
this.updateSources();
}
getThumbnail() {
......
......@@ -14,6 +14,11 @@ m-pro--channel {
background-blend-mode: overlay;
background-color: var(--m-pro--more-transparent-background-color) !important;
&.m-pro-channel--plainBackground {
background-blend-mode: initial;
background-color: var(--m-pro--plain-background-color) !important;
}
@media screen and (min-width: ($min-tablet + 1px)) {
m-pro__hamburger-menu {
display: none;
......
......@@ -130,7 +130,7 @@ export class ProChannelComponent implements OnInit, AfterViewInit, OnDestroy {
}
@HostBinding('style.backgroundImage') get backgroundImageCssValue() {
if (!this.channel) {
if (!this.channel || !this.channel.pro_settings.background_image) {
return 'none';
}
......@@ -142,8 +142,16 @@ export class ProChannelComponent implements OnInit, AfterViewInit, OnDestroy {
return '';
}
return `m-theme--wrapper m-theme--wrapper__${this.channel.pro_settings
.scheme || 'light'}`;
const classes = [
'm-theme--wrapper',
`m-theme--wrapper__${this.channel.pro_settings.scheme || 'light'}`,
];
if (!this.channel || !this.channel.pro_settings.background_image) {
classes.push('m-pro-channel--plainBackground');
}
return classes.join(' ');
}
constructor(
......
......@@ -6,7 +6,10 @@
></m-pro--channel--categories>
<div class="m-proChannelHome__section" *ngIf="featuredContent?.length">
<div class="m-proChannelHome__featuredContent">
<div
class="m-proChannelHome__featuredContent"
[class.m-proChannelHome__featuredContent--prominent]="false"
>
<m-pro--channel-tile
*ngFor="let entity of featuredContent"
[entity]="entity"
......
......@@ -56,15 +56,17 @@ m-proChannel__home {
.m-proChannelHome__featuredContent {
grid-template-columns: repeat(2, 1fr);
*:nth-child(1) {
grid-column: span 2;
}
@media screen and (max-width: $max-mobile) {
grid-template-columns: 100%;
}
&.m-proChannelHome__featuredContent--prominent {
*:nth-child(1) {
grid-column: initial;
grid-column: span 2;
@media screen and (max-width: $max-mobile) {
grid-column: initial;
}
}
}
}
......
......@@ -107,6 +107,12 @@
&.m-proSettingsField__logoFilePreview {
padding: 16px;
min-width: 32px;
min-height: 32px;
@include m-theme() {
background: rgba(themed($m-black), 0.3);
}
> img {
max-width: 100%;
......@@ -116,6 +122,13 @@
}
&.m-proSettingsField__backgroundFilePreview {
width: 480px;
height: 270px;
@include m-theme() {
background: rgba(themed($m-black), 0.3);
}
> img {
width: 480px;
height: 270px;
......
......@@ -31,7 +31,7 @@
<i class="material-icons">people</i>
<div class="m-token-contributions--chart--contribution-text">
<span>Referrals</span>
<span>+50</span>
<span>+1</span>
</div>
</div>
<div class="m-token-contributions--chart--contribution">
......
......@@ -8,7 +8,7 @@
If your friend signs up for Minds within 24 hours of clicking the link you
shared with them, they’ll be added to your pending referrals. Once they sign
up for the rewards program by setting up their Minds wallet, the referral is
complete and you’ll <span>both</span> get +50 added to your contribution
complete and you’ll <span>both</span> get +1 added to your contribution
scores! (Hint: check out
<a [routerLink]="['/wallet/tokens/101']">Token 101</a> to learn how
contribution scores are converted into tokens)
......
......@@ -22,9 +22,6 @@ export class FeaturesService {
if (typeof this._features[feature] === 'undefined') {
if (isDevMode() && !this._hasWarned(feature)) {
console.warn(
`[FeaturedService] Feature '${feature}' is not declared. Assuming false.`
);
this._warnedCache[feature] = Date.now();
}
......