Skip to content
Commits on Source (22)
......@@ -79,6 +79,11 @@ m-app {
}
}
&.m-page--wrapped {
max-width: 1280px;
margin: auto;
}
.m-page--main, .m-page__main {
padding: 16px;
flex: 1;
......
......@@ -17,7 +17,11 @@
>
<span>{{reason.label}}</span>
<div class="m-layout__spacer"></div>
<m-tooltip icon="locked" [hidden]="!reason.locked">
NSFW tags are locked for this channel
</m-tooltip>
<i class="material-icons selected" [hidden]="!reason.selected">checkmark</i>
</li>
</ul>
</m-dropdown>
......@@ -10,6 +10,7 @@ import {
NSFWSelectorEditingService,
} from './nsfw-selector.service';
import { Storage } from '../../../services/storage';
import { ifError } from 'assert';
@Component({
selector: 'm-nsfw-selector',
......@@ -52,7 +53,18 @@ export class NSFWSelectorComponent {
}
}
@Input('locked') set locked(locked: Array<number>) {
for (let i in this.service.reasons) {
if (this.service.reasons[i].selected) {
this.service.reasons[i].locked = locked.indexOf(this.service.reasons[i].value) > -1;
}
}
}
toggle(reason) {
if(reason.locked) {
return;
}
this.service.toggle(reason);
const reasons = this.service.reasons.filter(r => r.selected);
......
......@@ -5,12 +5,12 @@ export class NSFWSelectorService {
cacheKey: string = '';
reasons: Array<any> = [
{ value: 1, label: 'Nudity', selected: false, },
{ value: 2, label: 'Pornography', selected: false, },
{ value: 3, label: 'Profanity', selected: false, },
{ value: 4, label: 'Violence and Gore', selected: false, },
{ value: 5, label: 'Race and Religion', selected: false, },
{ value: 6, label: 'Other', selected: false, }
{ value: 1, label: 'Nudity', selected: false, locked: false },
{ value: 2, label: 'Pornography', selected: false, locked: false },
{ value: 3, label: 'Profanity', selected: false, locked: false },
{ value: 4, label: 'Violence and Gore', selected: false, locked: false },
{ value: 5, label: 'Race and Religion', selected: false, locked: false },
{ value: 6, label: 'Other', selected: false, locked: false }
];
constructor(
......@@ -30,6 +30,9 @@ export class NSFWSelectorService {
}
toggle(reason) {
if (reason.locked) {
return;
}
for (let r of this.reasons) {
if (r.value === reason.value)
r.selected = !r.selected;
......
<m-dropdown class="m-sort-selector--algorithm-dropdown" [expanded]="expandedAlgorithmDropdown" #algorithmDropdown>
<m-dropdown
*ngIf="shouldShowAlgorithms()"
class="m-sort-selector--algorithm-dropdown"
[expanded]="expandedAlgorithmDropdown"
#algorithmDropdown
>
<label [ngClass]="[labelClass]">
<i
*ngIf="getCurrentAlgorithmProp('icon')"
......@@ -12,9 +17,10 @@
<ul class="m-dropdown--list">
<li
*ngFor="let item of algorithms"
*ngFor="let item of getAlgorithms()"
class="m-dropdown--list--item"
[class.m-dropdown--list--item--selected]="item.id === algorithm"
[class.m-dropdown--list--item--disabled]="isDisabled(item.id)"
(click)="setAlgorithm(item.id); closeDropdowns();"
>
<i
......@@ -27,7 +33,11 @@
</ul>
</m-dropdown>
<m-dropdown class="m-sort-selector--period-dropdown" *ngIf="hasCurrentAlgorithmPeriod()" #periodDropdown>
<m-dropdown
*ngIf="shouldShowPeriods() && hasCurrentAlgorithmPeriod()"
class="m-sort-selector--period-dropdown"
#periodDropdown
>
<label [ngClass]="[labelClass]">
<span>{{getCurrentPeriodLabel()}}</span>
......@@ -36,7 +46,7 @@
<ul class="m-dropdown--list">
<li
class="m-dropdown--list--item" *ngFor="let item of periods"
class="m-dropdown--list--item" *ngFor="let item of getPeriods()"
[class.m-dropdown--list--item--selected]="item.id === period"
(click)="setPeriod(item.id); closeDropdowns();"
>
......@@ -45,7 +55,12 @@
</ul>
</m-dropdown>
<m-dropdown class="m-sort-selector--custom-type-dropdown" [expanded]="expandedCustomTypeDropdown" #customTypeDropdown>
<m-dropdown
*ngIf="shouldShowCustomTypes()"
class="m-sort-selector--custom-type-dropdown"
[expanded]="expandedCustomTypeDropdown"
#customTypeDropdown
>
<label [ngClass]="[labelClass]">
<i
*ngIf="getCurrentCustomTypeProp('icon')"
......
......@@ -25,6 +25,12 @@ m-sort-selector {
}
}
.m-dropdown--list--item--disabled {
@include m-theme(){
color: themed($m-grey-300);
}
}
.m-dropdown {
.m-dropdown--label-container {
text-transform: uppercase;
......
......@@ -97,13 +97,17 @@ export class SortSelectorComponent implements OnInit, OnDestroy, AfterViewInit {
@Input() algorithm: string;
@Input() allowedAlgorithms: string[] | boolean = true;
@Input() period: string;
@Input() allowedPeriods: string[] | boolean = true;
@Input() customType: string;
@Input() labelClass: string = "m--sort-selector-label";
@Input() allowedCustomTypes: string[] | boolean = true;
@Input() hideCustomTypesOnLatest: string[] = [];
@Input() labelClass: string = "m--sort-selector-label";
@Output() onChange: EventEmitter<{ algorithm, period, customType }> = new EventEmitter<{ algorithm, period, customType }>();
......@@ -161,12 +165,31 @@ export class SortSelectorComponent implements OnInit, OnDestroy, AfterViewInit {
}
}
getAlgorithms() {
if (this.allowedAlgorithms === true) {
return this.algorithms;
} else if (!this.allowedAlgorithms) {
return [];
}
return this.algorithms
.filter(algorithm => (<string[]>this.allowedAlgorithms).indexOf(algorithm.id) > -1);
}
shouldShowAlgorithms() {
return this.getAlgorithms().length > 0;
}
setAlgorithm(id: string) {
if (!this.algorithms.find(algorithm => id === algorithm.id)) {
console.error('Unknown algorithm');
return false;
}
if (this.isDisabled(id)) {
return false;
}
this.algorithm = id;
this.emit();
......@@ -187,6 +210,21 @@ export class SortSelectorComponent implements OnInit, OnDestroy, AfterViewInit {
return currentAlgorithm[prop];
}
getPeriods() {
if (this.allowedPeriods === true) {
return this.periods;
} else if (!this.allowedPeriods) {
return [];
}
return this.periods
.filter(period => (<string[]>this.allowedPeriods).indexOf(period.id) > -1);
}
shouldShowPeriods() {
return this.getPeriods().length > 0;
}
setPeriod(id: string) {
if (!this.periods.find(period => id === period.id)) {
console.error('Unknown period');
......@@ -224,11 +262,18 @@ export class SortSelectorComponent implements OnInit, OnDestroy, AfterViewInit {
}
getCustomTypes() {
if (this.hideCustomTypesOnLatest && this.algorithm === 'latest') {
return this.customTypes.filter(customType => this.hideCustomTypesOnLatest.indexOf(customType.id) === -1);
if (this.allowedCustomTypes === true) {
return this.customTypes;
} else if (!this.allowedCustomTypes) {
return [];
}
return this.customTypes;
return this.customTypes
.filter(customType => (<string[]>this.allowedCustomTypes).indexOf(customType.id) > -1);
}
shouldShowCustomTypes() {
return this.getCustomTypes().length > 0;
}
setCustomType(id: string) {
......@@ -278,4 +323,12 @@ export class SortSelectorComponent implements OnInit, OnDestroy, AfterViewInit {
this.customTypeDropdown.close();
}
}
isDisabled(id) {
return (id != 'top'
&& (this.customType === 'channels'
|| this.customType === 'groups')
);
}
}
......@@ -34,15 +34,15 @@ export class BoostedContentService {
this.boostedContentSync = new BoostedContentSync(
new MindsClientHttpAdapter(this.client),
await browserStorageAdapterFactory('minds-boosted-content-190314'),
5 * 60 * 60, // Stale after 5 minutes
15 * 60 * 60, // Cooldown of 15 minutes
5 * 60, // Stale after 5 minutes
15 * 60, // Cooldown of 15 minutes
500,
);
this.boostedContentSync.setResolvers({
currentUser: () => this.session.getLoggedInUser() && this.session.getLoggedInUser().guid,
blockedUserGuids: async () => await this.blockListService.getList(),
fetchEntity: async guid => await this.entitiesService.single(guid),
fetchEntities: async guids => await this.entitiesService.fetch(guids),
});
//
......@@ -75,9 +75,15 @@ export class BoostedContentService {
this.settingsService.ratingChanged.subscribe(rating => this.boostedContentSync.changeRating(rating));
}
async fetch() {
async get(opts = {}) {
await this.status.untilReady();
return await this.boostedContentSync.fetch();
return await this.boostedContentSync.get(opts);
}
async fetch(opts = {}) {
await this.status.untilReady();
return await this.boostedContentSync.fetch(opts);
}
}
......@@ -68,18 +68,6 @@ export class EntitiesService {
return await this.entitiesSync.get(urns);
}
async prefetch(guids: string[]): Promise<boolean> {
await this.status.untilReady();
if (!guids || !guids.length) {
return true;
}
const urns = guids.map(guid => normalizeUrn(guid));
return await this.entitiesSync.sync(urns);
}
static _(client: Client) {
return new EntitiesService(client);
}
......
......@@ -13,21 +13,17 @@ import FeedsSync from '../../lib/minds-sync/services/FeedsSync.js';
import hashCode from "../../helpers/hash-code";
import AsyncStatus from "../../helpers/async-status";
export type FeedsServiceSyncOptions = {
filter: string,
algorithm: string,
customType: string,
container_guid?: string,
period?: string,
hashtags?: string[],
all?: boolean | 1,
query?: string,
nsfw?: Array<number>,
export type FeedsServiceGetParameters = {
endpoint: string;
timebased: boolean;
//
limit?: number,
offset?: number,
forceSync?: boolean,
limit: number;
offset?: number;
//
syncPageSize?: number;
forceSync?: boolean;
}
export type FeedsServiceGetResponse = {
......@@ -56,7 +52,6 @@ export class FeedsService {
new MindsClientHttpAdapter(this.client),
await browserStorageAdapterFactory('minds-feeds-190314'),
15,
1500,
);
this.feedsSync.setResolvers({
......@@ -64,12 +59,11 @@ export class FeedsService {
currentUser: () => this.session.getLoggedInUser() && this.session.getLoggedInUser().guid,
blockedUserGuids: async () => await this.blockListService.getList(),
fetchEntities: async guids => await this.entitiesService.fetch(guids),
prefetchEntities: async guids => await this.entitiesService.prefetch(guids),
});
this.feedsSync.setUp();
//
// Mark as done
this.status.done();
......@@ -79,7 +73,7 @@ export class FeedsService {
setTimeout(() => this.feedsSync.gc(), 15 * 60 * 1000); // Every 15 minutes
}
async get(opts: FeedsServiceSyncOptions): Promise<FeedsServiceGetResponse> {
async get(opts: FeedsServiceGetParameters): Promise<FeedsServiceGetResponse> {
await this.status.untilReady();
try {
......
......@@ -199,11 +199,18 @@ export default class DexieStorageAdapter {
/**
* @param {string} table
* @param {{ sortBy }} opts
* @returns {Promise<*[]>}
*/
async all(table) {
return await this.db.table(table)
.toArray();
async all(table, opts = {}) {
const collection = this.db.table(table)
.toCollection();
if (opts.sortBy) {
return await collection.sortBy(opts.sortBy);
}
return await collection.toArray();
}
/**
......
......@@ -278,11 +278,26 @@ export default class InMemoryStorageAdapter {
/**
* @param {string} table
* @param {{ sortBy }} opts
* @returns {Promise<*[]>}
*/
async all(table) {
return this.db.data[table]
async all(table, opts = {}) {
const collection = this.db.data[table]
.map(row => Object.assign({}, row));
if (opts.sortBy) {
return collection.sort((a, b) => {
const aIndex = a[opts.sortBy];
const bIndex = b[opts.sortBy];
if (aIndex < bIndex) return -1;
else if (bIndex > aIndex) return -1;
return 0;
});
}
return collection;
}
/**
......
......@@ -12,7 +12,7 @@ export default class MindsClientHttpAdapter {
* @param {boolean} cache
* @returns {Promise<Object>}
*/
async get(endpoint, data = {}, cache = true) {
async get(endpoint, data = null, cache = true) {
try {
const response = await this.http.get(endpoint, data, { cache });
......
......@@ -82,13 +82,30 @@ export default class BoostedContentSync {
return true;
}
async fetch(opts = {}) {
const boosts = await this.get(Object.assign(opts, {
limit: 1
}));
return boosts[0] || null;
}
/**
* @param {Object} opts
* @returns {Promise<*>}
* @returns {Promise<*[]>}
*/
async fetch(opts = {}) {
async get(opts = {}) {
await this.db.ready();
// Default options
opts = Object.assign({
limit: 1,
offset: 0,
passive: false,
forceSync: false,
}, opts);
// Prune list
await this.prune();
......@@ -124,53 +141,82 @@ export default class BoostedContentSync {
// Fetch
let lockedUrn;
let lockedUrns = [];
try {
const rows = (await this.db.getAllLessThan('boosts', 'lastImpression', Date.now() - this.cooldown_ms, { sortBy: 'impressions' }))
.filter(row => row && row.urn && (this.locks.indexOf(row.urn) === -1));
let rows;
if (!opts.passive) {
rows = await this.db
.getAllLessThan('boosts', 'lastImpression', Date.now() - this.cooldown_ms, { sortBy: 'impressions' });
} else {
rows = await this.db
.all('boosts', { sortBy: 'impressions' });
}
rows = rows.filter(row => row && row.urn && (this.locks.indexOf(row.urn) === -1));
if (opts.exclude) {
rows = rows.filter(row => opts.exclude.indexOf(row.urn) === -1);
}
if (!rows || !rows.length) {
return null;
return [];
}
// Pick first unlocked result
// Data set
const dataSet = rows.slice(opts.offset || 0, opts.limit);
if (!dataSet.length) {
return [];
}
const { urn, impressions } = rows[0];
// Lock data set URNs
// lock this URN
lockedUrns = [...dataSet.map(row => row.urn)];
this.locks.push(...lockedUrns);
lockedUrn = urn;
this.locks.push(lockedUrn);
// Process rows
// Increase counter
for (let i = 0; i < dataSet.length; i++) {
const { urn, impressions, passiveImpressions } = dataSet[i];
await this.db.update('boosts', urn, {
impressions: impressions + 1,
lastImpression: Date.now(),
});
// Increase counters
if (!opts.passive) {
await this.db.update('boosts', urn, {
impressions: (impressions || 0) + 1,
lastImpression: Date.now(),
});
} else {
await this.db.update('boosts', urn, {
passiveImpressions: (passiveImpressions || 0) + 1,
});
}
}
// Release lock
// Release locks
if (lockedUrn) {
this.locks = this.locks.filter(lock => lock !== lockedUrn);
if (lockedUrns) {
this.locks = this.locks.filter(lock => lockedUrns.indexOf(lock) === -1);
}
// Hydrate entities
return await this.resolvers.fetchEntity(urn);
return await this.resolvers.fetchEntities(dataSet.map(row => row.urn));
} catch (e) {
console.error('BoostedContentSync.fetch', e);
// Release lock
// Release locks
if (lockedUrn) {
this.locks = this.locks.filter(lock => lock !== lockedUrn);
if (lockedUrns) {
this.locks = this.locks.filter(lock => lockedUrns.indexOf(lock) === -1);
}
// Return empty
return null;
return [];
}
}
......@@ -219,7 +265,8 @@ export default class BoostedContentSync {
await Promise.all(entities.map(entity => this.db.upsert('boosts', entity.urn, entity, {
impressions: 0,
lastImpression: 0
lastImpression: 0,
passiveImpressions: 0,
})));
// Remove stale entries
......
import asyncSleep from "../../../helpers/async-sleep";
const E_NO_RESOLVER = function () {
throw new Error('Resolver not set')
};
......@@ -13,14 +15,12 @@ export default class FeedsSync {
this.http = http;
this.db = db;
this.stale_after_ms = stale_after * 1000;
this.limit = limit;
this.resolvers = {
stringHash: E_NO_RESOLVER,
currentUser: E_NO_RESOLVER,
blockedUserGuids: E_NO_RESOLVER,
fetchEntities: E_NO_RESOLVER,
prefetchEntities: E_NO_RESOLVER,
}
}
......@@ -60,36 +60,50 @@ export default class FeedsSync {
const key = await this.buildKey(opts);
// If it's the first page or a forced refresh is needed, attempt to sync
if (!opts.offset || opts.forceSync) {
const wasSynced = await this.sync(opts);
if (!wasSynced) {
console.info('Cannot sync, using cache');
}
}
// Fetch
try {
const rows = await this.db.getAllSliced('feeds', 'key', key, {
offset: opts.offset,
limit: opts.limit,
});
let entities;
let next;
if (rows.length > 0) {
next = (opts.offset || 0) + opts.limit;
}
let attempts = 0;
while (true) {
try {
const wasSynced = await this._sync(key, opts);
if (!wasSynced) {
console.info('Sync not needed, using cache');
}
} catch (e) {
console.warn('Cannot sync, using cache');
}
const rows = await this.db.getAllSliced('feeds', 'key', key, {
offset: opts.offset,
limit: opts.limit,
});
if (!rows || !rows.length) {
break;
}
// Hydrate entities
// Hydrate entities
entities = await this.resolvers.fetchEntities(rows.map(row => row.guid));
const entities = await this.resolvers.fetchEntities(rows.map(row => row.guid));
// Calculate offset
opts.offset = (opts.offset || 0) + opts.limit;
next = opts.offset;
// Prefetch
if (entities && entities.length) {
break;
}
this.prefetch(opts, next);
if (attempts++ > 15) {
break;
}
await asyncSleep(100); // Throttle a bit
}
//
......@@ -104,67 +118,71 @@ export default class FeedsSync {
}
/**
* @param {Object} opts
* @param {Number} futureOffset
* @param key
* @param opts
* @returns {Promise<boolean>}
* @private
*/
async prefetch(opts, futureOffset) {
if (!futureOffset) {
return false;
}
async _sync(key, opts) {
await this.db.ready();
const key = await this.buildKey(opts);
// Read row (if no refresh is needed), else load defaults
const rows = await this.db.getAllSliced('feeds', 'key', key, {
offset: futureOffset,
limit: opts.limit,
});
const _syncAtRow = await this.db.get('syncAt', key);
await this.resolvers.prefetchEntities(rows.map(row => row.guid));
const syncAt = opts.offset && _syncAtRow ? _syncAtRow : {
rows: 0,
moreData: true,
next: '',
};
return true;
}
if (!opts.offset) { // Check if first-page sync is needed
const stale = !syncAt.sync || (syncAt.sync + this.stale_after_ms) < Date.now();
/**
* @param {Object} opts
* @returns {Promise<boolean>}
*/
async sync(opts) {
await this.db.ready();
if (!stale && !opts.forceSync) {
return false;
}
} else if (opts.timebased && (!syncAt.moreData || syncAt.rows >= (opts.offset + opts.limit))) { // Check if non-first-page sync is needed
return false;
} else if (!opts.timebased && opts.offset) { // If non-first-page and not timebased, sync is not needed
return false;
}
const key = await this.buildKey(opts);
// Request
// Is sync needed?
try {
// Setup parameters
if (!opts.forceSync) {
const lastSync = await this.db.get('syncAt', key);
const syncPageSize = Math.max(opts.syncPageSize || 10000, opts.limit);
const qs = ['sync=1', `limit=${syncPageSize}`];
if (lastSync && lastSync.sync && (lastSync.sync + this.stale_after_ms) >= Date.now()) {
return true;
if (syncAt.next) {
qs.push(`from_timestamp=${syncAt.next}`);
}
}
// Sync
// Setup endpoint (with parameters)
try {
const response = await this.http.get(`api/v2/feeds/global/${opts.algorithm}/${opts.customType}`, {
sync: 1,
limit: this.limit,
container_guid: opts.container_guid || '',
period: opts.period || '',
hashtags: opts.hashtags || '',
all: opts.all ? 1 : '',
query: opts.query || '',
nsfw: opts.nsfw || '',
}, true);
const endpoint = `${opts.endpoint}${opts.endpoint.indexOf('?') > -1 ? '&' : '?'}${qs.join('&')}`;
// Perform request
const response = await this.http.get(endpoint, null, true);
// Check if valid response
if (!response.entities || typeof response.entities.length === 'undefined') {
throw new Error('Invalid server response');
}
// Prune old list
// Check if offset response
const next = opts.timebased && response['load-next'];
await this.prune(key);
// Prune list, if necessary
if (!syncAt.next) {
await this.prune(key);
}
// Read blocked list
......@@ -173,25 +191,29 @@ export default class FeedsSync {
// Setup rows
const entities = response.entities
.filter(feedSyncEntity => Boolean(feedSyncEntity))
.filter(feedSyncEntity => blockedList.indexOf(feedSyncEntity.owner_guid) === -1)
.map((feedSyncEntity, index) => {
let obj = {
key,
id: `${key}:${`${index}`.padStart(24, '0')}`,
};
.map((feedSyncEntity, index) => Object.assign(feedSyncEntity, {
key,
id: `${key}:${`${syncAt.rows + index}`.padStart(24, '0')}`,
}));
obj = Object.assign(obj, feedSyncEntity);
// Insert entity refs
return obj;
});
await this.db.bulkInsert('feeds', entities);
// Insert onto DB
// Update syncAt
await this.db.bulkInsert('feeds', entities);
await this.db.insert('syncAt', { key, sync: Date.now() });
await this.db.upsert('syncAt', key, {
key,
rows: syncAt.rows + entities.length,
moreData: Boolean(next && entities.length),
next: next || '',
sync: Date.now(),
});
} catch (e) {
console.warn('FeedsSync.sync', e);
return false;
throw e;
}
return true;
......@@ -242,14 +264,7 @@ export default class FeedsSync {
return await this.resolvers.stringHash(JSON.stringify([
userGuid,
opts.container_guid || '',
opts.algorithm || '',
opts.customType || '',
opts.period || '',
opts.hashtags || '',
Boolean(opts.all),
opts.query || '',
opts.nsfw || '',
opts.endpoint,
]));
}
......
......@@ -52,6 +52,12 @@
<li>
Dark Mode - 18th April '19
</li>
<li>
Subscribed, Channel and Group Feeds - 6th May '19
</li>
<li>
Boost Rotator - 6th May '19
</li>
</ul>
</div>
......
......@@ -33,20 +33,26 @@
<section class="mdl-cell mdl-cell--4-col m-channel-sidebar">
<m-channel--sidebar [user]="user" [editing]="editing" (changeEditing)="toggleEditing($event)"></m-channel--sidebar>
</section>
<!-- Feed list -->
<section class="mdl-cell mdl-cell--8-col m-channel-feed" *ngIf="filter == 'feed'">
<m-channel--feed [user]="user" [isSorting]="!isLegacySorting && isSorting" [algorithm]="algorithm" [period]="period" [customType]="customType" #feed>
<div class="m-channel-feed__Filter m-border" *mIfFeature="'channel-filter-feeds'">
<m-sort-selector
[algorithm]="algorithm"
[period]="period"
[customType]="customType"
[hideCustomTypesOnLatest]="['images', 'videos', 'blogs', 'channels', 'groups']"
(onChange)="setSort($event.algorithm, $event.period, $event.customType)"
></m-sort-selector>
</div>
</m-channel--feed>
</section>
<ng-container *mIfFeature="'es-feeds'; else legacyFeed">
<section class="mdl-cell mdl-cell--8-col" *ngIf="shouldShowFeeds()">
<m-channel--sorted
[channel]="user"
[type]="getFeedType()"
(onChangeType)="setFeedType($event)"
></m-channel--sorted>
</section>
</ng-container>
<ng-template #legacyFeed>
<section class="mdl-cell mdl-cell--8-col m-channel-feed" *ngIf="filter == 'feed'">
<m-channel--feed
[user]="user"
#feed
></m-channel--feed>
</section>
</ng-template>
<!-- Supporters list -->
<section class="mdl-cell mdl-cell--8-col" *ngIf="filter == 'supporters'">
......
......@@ -72,9 +72,14 @@ describe('ChannelComponent', () => {
}),
MockComponent({
selector: 'm-sort-selector',
inputs: ['algorithm', 'period', 'customType', 'hideCustomTypesOnLatest'],
inputs: ['algorithm', 'period', 'customType'],
outputs: ['onChange'],
}),
MockComponent({
selector: 'm-channel--sorted',
inputs: ['channel', 'type'],
outputs: ['onChangeType'],
}),
IfFeatureDirective,
],
imports: [
......@@ -106,6 +111,7 @@ describe('ChannelComponent', () => {
fixture = TestBed.createComponent(ChannelComponent);
clientMock.response = {};
uploadMock.response = {};
featuresServiceMock.mock('es-feeds', false);
featuresServiceMock.mock('top-feeds', false);
featuresServiceMock.mock('channel-filter-feeds', false);
comp = fixture.componentInstance;
......
......@@ -11,13 +11,12 @@ import { RecentService } from '../../services/ux/recent';
import { MindsUser } from '../../interfaces/entities';
import { MindsChannelResponse } from '../../interfaces/responses';
import { ChannelFeedComponent } from './feed/feed'
import { ContextService } from '../../services/context.service';
import { FeaturesService } from "../../services/features.service";
import { PosterComponent } from '../newsfeed/poster/poster.component';
import { Observable } from 'rxjs';
import { DialogService } from '../../common/services/confirm-leave-dialog.service'
import { BlockListService } from "../../common/services/block-list.service";
import { ChannelSortedComponent } from './sorted/sorted.component';
@Component({
moduleId: module.id,
......@@ -42,13 +41,7 @@ export class ChannelComponent {
changed: boolean = false;
paramsSubscription: Subscription;
isLegacySorting: boolean = false;
isSorting: boolean = false;
algorithm: string;
period: string;
customType: string;
@ViewChild('feed') private feed: ChannelFeedComponent;
@ViewChild('feed') private feed: ChannelSortedComponent;
constructor(
public session: Session,
......@@ -70,8 +63,6 @@ export class ChannelComponent {
this.context.set('activity');
this.onScroll();
this.isLegacySorting = !this.features.has('top-feeds');
this.paramsSubscription = this.route.params.subscribe(params => {
let feedChanged = false;
......@@ -97,32 +88,6 @@ export class ChannelComponent {
this.editing = true;
}
this.isSorting = Boolean(params['algorithm']);
if (this.isSorting) {
feedChanged = this.changed ||
this.algorithm !== params['algorithm'] ||
this.period !== params['period'] ||
this.customType !== (params['type'] || 'activities');
this.filter = 'feed';
this.algorithm = params['algorithm'] || 'top';
this.period = params['period'] || '7d';
this.customType = params['type'] || 'activities';
} else {
if (!this.algorithm) {
this.algorithm = 'latest';
}
if (!this.period) {
this.period = '7d';
}
if (!this.customType) {
this.customType = 'activities';
}
}
if (this.changed) {
this.load();
} else if (feedChanged) {
......@@ -172,6 +137,27 @@ export class ChannelComponent {
return this.session.getLoggedInUser().guid === this.user.guid;
}
shouldShowFeeds() {
return ['feed', 'images', 'videos', 'blogs'].indexOf(this.filter.toLowerCase()) > -1;
}
getFeedType() {
if (this.filter === 'feed') {
return 'activities';
}
return this.filter;
}
setFeedType(type: string | null = '') {
const route = ['/', this.user.username];
if (type && type !== 'activities') {
route.push(type);
}
this.router.navigate(route);
}
toggleEditing() {
if (this.editing) {
this.update();
......@@ -246,47 +232,15 @@ export class ChannelComponent {
* In this instance, a confirmation is needed from the user
* when requesting a new page if editing === true
*
* @returns { Observable<boolean> | boolean }
* @returns { Observable<boolean> | boolean }
*/
canDeactivate(): Observable<boolean> | boolean {
if (!this.editing) {
return true;
if (this.feed && this.feed.canDeactivate && !this.feed.canDeactivate()) {
return false;
}
return this.dialogService.confirm('Discard changes?');
return !this.editing || this.dialogService.confirm('Discard changes?');
}
setSort(algorithm: string, period: string | null, customType: string | null) {
if (algorithm === 'latest') {
// Cassandra listing.
// TODO: Remove when ElasticSearch is fully implemented
this.algorithm = algorithm;
this.period = null;
this.customType = null;
this.router.navigate(['/', this.username]);
return;
}
this.algorithm = algorithm;
this.period = period;
this.customType = customType;
let route: any[] = [ '/', this.username, 'sort', algorithm ];
const params: any = {};
if (period) {
params.period = period;
}
if (customType && customType !== 'activities') {
params.type = customType;
}
route.push(params);
this.router.navigate(route);
}
}
export { ChannelSubscribers } from './subscribers/subscribers';
......
......@@ -23,6 +23,8 @@ import { PosterModule } from '../newsfeed/poster/poster.module';
import { NewsfeedModule } from '../newsfeed/newsfeed.module';
import { ExplicitOverlayComponent } from './explicit-overlay/overlay.component';
import { HashtagsModule } from '../hashtags/hashtags.module';
import { ChannelSortedComponent } from './sorted/sorted.component';
import { ChannelSortedModuleComponent } from './sorted/module.component';
const routes: Routes = [
{ path: 'channels/:filter', component: ChannelsListComponent },
......@@ -56,6 +58,8 @@ const routes: Routes = [
ChannelFeedComponent,
ChannelSidebar,
ExplicitOverlayComponent,
ChannelSortedComponent,
ChannelSortedModuleComponent,
],
exports: [
ChannelModulesComponent,
......