Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • 1721-implement-cypress-code-coverage-and-generate-a-report-of-current-e2e-coverage-and-areas-that
  • 1991-enable-pro-to-general-public-with-feature-flag
  • 2676-get-required-checkmark-on-e2e
  • 3272-i-should-not-be-suggested-thai-groups-or-channels
  • 3869-mobile-web-keyboard-issue
  • 3988-user-auto-suggest-list-is-appearing-behind-other-stuff
  • 3991-add-24-hour-text-to-label-top-scheduler
  • 4030-m-topbarwrapper-is-behind-other-elements
  • 4157-invalid-email-error-from-minds-spanish
  • 6073-replace-title-line-editor-in-the-composer-on-ios-mobile-with-the-same-editor-as-the-description
  • add-nvmrc
  • analytics-service-1932
  • animationTweak
  • btc-settings-2346
  • bug-darkmode-color
  • chart-refactor-2170
  • chore/1351-skip-captcha-e2e
  • chore/ChannelButtonAdjust
  • chore/RobotoFallback
  • chore/add-missing-i18n-major-components
  • chore/add-scroll-to-settings
  • chore/api-serv-generics
  • chore/better-lint-command
  • chore/better-mr-template
  • chore/block-nsfw-boosts-4487
  • chore/cypress-6-upgrde
  • chore/cypress-test-jul-2021
  • chore/env-settings
  • chore/ethers-implementation-3548
  • chore/ethers-implementation-3548-3
  • chore/feature-cleanup-m4779
  • chore/feature-template
  • chore/fix-shared-key-cypress
  • chore/handle-mobile-page-as-case-insentitive
  • chore/helm-master
  • chore/hindi-merge
  • chore/husky-migrate-4949
  • chore/isolate-broken-e2e-tests
  • chore/l10n-sync
  • chore/language-service
  • chore/layout-chores
  • chore/nav-fixes
  • chore/ng9-bump
  • chore/no-nsfw-onboarding
  • chore/parallel-cypress-runners
  • chore/parallel-karma
  • chore/plyr-upgrade-2572
  • chore/porting-confirmation-dialog
  • chore/reduce-build-time
  • chore/register-modal-improvements-2965
  • chore/remove-legacy-code
  • chore/remove-rotator-styles-f6169
  • chore/revert-admin-confirm-4819
  • chore/sentry-source-maps
  • chore/settings-tests
  • chore/sourcemap-root-sentry-1782
  • chore/split-cypress-repo-test
  • chore/ssr-envs
  • chore/ssr-local-serve
  • chore/test-branch-delete
  • chore/test-pipeline-btns
  • chore/update-chat-1-4-19
  • chore/update-earnings-ux
  • chore/wire-support-tiers-2
  • chore/wire-to-pay-3151
  • chore/z-index-3790
  • confirm-leave-dialog-blogs
  • design-system-v2.0.0
  • design-system-v2.0.1
  • design-system-v2.0.2
  • design-system-v2.0.3
  • design-system-v2.1.2
  • design-system-v2.2.3
  • e2e-maintainence-2-2676
  • e2e-maintainence-3-2676
  • emoji-picker-e8ccf1
  • emoji-picker-e8ccf1-healthy
  • entity-centric-metrics
  • epic/SSR
  • epic/angular-9
  • epic/boost-campaign
  • epic/ckeditor5-blogs
  • epic/composer
  • epic/minds-redesign-110
  • epic/onboarding-v3
  • epic/permissions-28
  • epic/skale-integration
  • epic/totp-auth-modal
  • epic/totp-support-2021
  • epic/transcoder-improvements
  • epic/upgrades-page
  • epic/upgrades-page-updates
  • experiment-material-fonts
  • feat-campaign-cap-1169
  • feat-embeddable-video-player
  • feat/1170-admin-approve-campaign
  • feat/1732-block-refactor
  • feat/2511-activity-v2
  • feat/3996-plus-7-day-trial
  • feat/588-state-on-user
  • dev-0.1c
101 results

Target

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