Merge pull request #2031 from lbryio/subscription-new

Improve subscriptions page
This commit is contained in:
Sean Yesmunt 2018-10-22 16:31:54 -04:00 committed by GitHub
commit c35b13cdc0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 1735 additions and 488 deletions

View file

@ -10,7 +10,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
* Focus on search bar with {cmd,ctrl} + "l" ([#2003](https://github.com/lbryio/lbry-desktop/pull/2003))
* Add support for clickable channel names on explore page headings ([#2023](https://github.com/lbryio/lbry-desktop/pull/2023))
* Content loading placeholder styles on FileCard/FileTile ([#2022](https://github.com/lbryio/lbry-desktop/pull/2022))
* Adds Persistence to Transaction List Filter Selection ([#2048](https://github.com/lbryio/lbry-desktop/pull/2048))
* Persistence to Transaction List Filter Selection ([#2048](https://github.com/lbryio/lbry-desktop/pull/2048))
* Subscription improvements ([#2031](https://github.com/lbryio/lbry-desktop/pull/2031))
### Changed
* Make tooltip smarter ([#1979](https://github.com/lbryio/lbry-desktop/pull/1979))

895
flow-typed/npm/reselect_v3.x.x.js vendored Normal file
View file

@ -0,0 +1,895 @@
// flow-typed signature: 84ab000391e0f17dd212d57ed0b180f5
// flow-typed version: 5d8678f464/reselect_v3.x.x/flow_>=v0.47.x
type ExtractReturnType = <Return>((...rest: any[]) => Return) => Return;
declare module "reselect" {
declare type InputSelector<-TState, TProps, TResult> =
(state: TState, props: TProps, ...rest: any[]) => TResult
declare type OutputSelector<-TState, TProps, TResult> =
& InputSelector<TState, TProps, TResult>
& {
recomputations(): number,
resetRecomputations(): void,
resultFunc(state: TState, props: TProps, ...rest: Array<any>): TResult,
};
declare type SelectorCreator = {
<TState, TProps, TResult, T1>(
selector1: InputSelector<TState, TProps, T1>,
resultFunc: (arg1: T1) => TResult
): OutputSelector<TState, TProps, TResult>,
<TState, TProps, TResult, T1>(
selectors: [InputSelector<TState, TProps, T1>],
resultFunc: (arg1: T1) => TResult
): OutputSelector<TState, TProps, TResult>,
<TState, TProps, TResult, T1, T2>(
selector1: InputSelector<TState, TProps, T1>,
selector2: InputSelector<TState, TProps, T2>,
resultFunc: (arg1: T1, arg2: T2) => TResult
): OutputSelector<TState, TProps, TResult>,
<TState, TProps, TResult, T1, T2>(
selectors: [InputSelector<TState, TProps, T1>, InputSelector<TState, TProps, T2>],
resultFunc: (arg1: T1, arg2: T2) => TResult
): OutputSelector<TState, TProps, TResult>,
<TState, TProps, TResult, T1, T2, T3>(
selector1: InputSelector<TState, TProps, T1>,
selector2: InputSelector<TState, TProps, T2>,
selector3: InputSelector<TState, TProps, T3>,
resultFunc: (arg1: T1, arg2: T2, arg3: T3) => TResult
): OutputSelector<TState, TProps, TResult>,
<TState, TProps, TResult, T1, T2, T3>(
selectors: [
InputSelector<TState, TProps, T1>,
InputSelector<TState, TProps, T2>,
InputSelector<TState, TProps, T3>
],
resultFunc: (arg1: T1, arg2: T2, arg3: T3) => TResult
): OutputSelector<TState, TProps, TResult>,
<TState, TProps, TResult, T1, T2, T3, T4>(
selector1: InputSelector<TState, TProps, T1>,
selector2: InputSelector<TState, TProps, T2>,
selector3: InputSelector<TState, TProps, T3>,
selector4: InputSelector<TState, TProps, T4>,
resultFunc: (arg1: T1, arg2: T2, arg3: T3, arg4: T4) => TResult
): OutputSelector<TState, TProps, TResult>,
<TState, TProps, TResult, T1, T2, T3, T4>(
selectors: [
InputSelector<TState, TProps, T1>,
InputSelector<TState, TProps, T2>,
InputSelector<TState, TProps, T3>,
InputSelector<TState, TProps, T4>
],
resultFunc: (arg1: T1, arg2: T2, arg3: T3, arg4: T4) => TResult
): OutputSelector<TState, TProps, TResult>,
<TState, TProps, TResult, T1, T2, T3, T4, T5>(
selector1: InputSelector<TState, TProps, T1>,
selector2: InputSelector<TState, TProps, T2>,
selector3: InputSelector<TState, TProps, T3>,
selector4: InputSelector<TState, TProps, T4>,
selector5: InputSelector<TState, TProps, T5>,
resultFunc: (arg1: T1, arg2: T2, arg3: T3, arg4: T4, arg5: T5) => TResult
): OutputSelector<TState, TProps, TResult>,
<TState, TProps, TResult, T1, T2, T3, T4, T5>(
selectors: [
InputSelector<TState, TProps, T1>,
InputSelector<TState, TProps, T2>,
InputSelector<TState, TProps, T3>,
InputSelector<TState, TProps, T4>,
InputSelector<TState, TProps, T5>
],
resultFunc: (arg1: T1, arg2: T2, arg3: T3, arg4: T4, arg5: T5) => TResult
): OutputSelector<TState, TProps, TResult>,
<TState, TProps, TResult, T1, T2, T3, T4, T5, T6>(
selector1: InputSelector<TState, TProps, T1>,
selector2: InputSelector<TState, TProps, T2>,
selector3: InputSelector<TState, TProps, T3>,
selector4: InputSelector<TState, TProps, T4>,
selector5: InputSelector<TState, TProps, T5>,
selector6: InputSelector<TState, TProps, T6>,
resultFunc: (
arg1: T1,
arg2: T2,
arg3: T3,
arg4: T4,
arg5: T5,
arg6: T6
) => TResult
): OutputSelector<TState, TProps, TResult>,
<TState, TProps, TResult, T1, T2, T3, T4, T5, T6>(
selectors: [
InputSelector<TState, TProps, T1>,
InputSelector<TState, TProps, T2>,
InputSelector<TState, TProps, T3>,
InputSelector<TState, TProps, T4>,
InputSelector<TState, TProps, T5>,
InputSelector<TState, TProps, T6>
],
resultFunc: (
arg1: T1,
arg2: T2,
arg3: T3,
arg4: T4,
arg5: T5,
arg6: T6
) => TResult
): OutputSelector<TState, TProps, TResult>,
<TState, TProps, TResult, T1, T2, T3, T4, T5, T6, T7>(
selector1: InputSelector<TState, TProps, T1>,
selector2: InputSelector<TState, TProps, T2>,
selector3: InputSelector<TState, TProps, T3>,
selector4: InputSelector<TState, TProps, T4>,
selector5: InputSelector<TState, TProps, T5>,
selector6: InputSelector<TState, TProps, T6>,
selector7: InputSelector<TState, TProps, T7>,
resultFunc: (
arg1: T1,
arg2: T2,
arg3: T3,
arg4: T4,
arg5: T5,
arg6: T6,
arg7: T7
) => TResult
): OutputSelector<TState, TProps, TResult>,
<TState, TProps, TResult, T1, T2, T3, T4, T5, T6, T7>(
selectors: [
InputSelector<TState, TProps, T1>,
InputSelector<TState, TProps, T2>,
InputSelector<TState, TProps, T3>,
InputSelector<TState, TProps, T4>,
InputSelector<TState, TProps, T5>,
InputSelector<TState, TProps, T6>,
InputSelector<TState, TProps, T7>
],
resultFunc: (
arg1: T1,
arg2: T2,
arg3: T3,
arg4: T4,
arg5: T5,
arg6: T6,
arg7: T7
) => TResult
): OutputSelector<TState, TProps, TResult>,
<TState, TProps, TResult, T1, T2, T3, T4, T5, T6, T7, T8>(
selector1: InputSelector<TState, TProps, T1>,
selector2: InputSelector<TState, TProps, T2>,
selector3: InputSelector<TState, TProps, T3>,
selector4: InputSelector<TState, TProps, T4>,
selector5: InputSelector<TState, TProps, T5>,
selector6: InputSelector<TState, TProps, T6>,
selector7: InputSelector<TState, TProps, T7>,
selector8: InputSelector<TState, TProps, T8>,
resultFunc: (
arg1: T1,
arg2: T2,
arg3: T3,
arg4: T4,
arg5: T5,
arg6: T6,
arg7: T7,
arg8: T8
) => TResult
): OutputSelector<TState, TProps, TResult>,
<TState, TProps, TResult, T1, T2, T3, T4, T5, T6, T7, T8>(
selectors: [
InputSelector<TState, TProps, T1>,
InputSelector<TState, TProps, T2>,
InputSelector<TState, TProps, T3>,
InputSelector<TState, TProps, T4>,
InputSelector<TState, TProps, T5>,
InputSelector<TState, TProps, T6>,
InputSelector<TState, TProps, T7>,
InputSelector<TState, TProps, T8>
],
resultFunc: (
arg1: T1,
arg2: T2,
arg3: T3,
arg4: T4,
arg5: T5,
arg6: T6,
arg7: T7,
arg8: T8
) => TResult
): OutputSelector<TState, TProps, TResult>,
<TState, TProps, TResult, T1, T2, T3, T4, T5, T6, T7, T8, T9>(
selector1: InputSelector<TState, TProps, T1>,
selector2: InputSelector<TState, TProps, T2>,
selector3: InputSelector<TState, TProps, T3>,
selector4: InputSelector<TState, TProps, T4>,
selector5: InputSelector<TState, TProps, T5>,
selector6: InputSelector<TState, TProps, T6>,
selector7: InputSelector<TState, TProps, T7>,
selector8: InputSelector<TState, TProps, T8>,
selector9: InputSelector<TState, TProps, T9>,
resultFunc: (
arg1: T1,
arg2: T2,
arg3: T3,
arg4: T4,
arg5: T5,
arg6: T6,
arg7: T7,
arg8: T8,
arg9: T9
) => TResult
): OutputSelector<TState, TProps, TResult>,
<TState, TProps, TResult, T1, T2, T3, T4, T5, T6, T7, T8, T9>(
selectors: [
InputSelector<TState, TProps, T1>,
InputSelector<TState, TProps, T2>,
InputSelector<TState, TProps, T3>,
InputSelector<TState, TProps, T4>,
InputSelector<TState, TProps, T5>,
InputSelector<TState, TProps, T6>,
InputSelector<TState, TProps, T7>,
InputSelector<TState, TProps, T8>,
InputSelector<TState, TProps, T9>
],
resultFunc: (
arg1: T1,
arg2: T2,
arg3: T3,
arg4: T4,
arg5: T5,
arg6: T6,
arg7: T7,
arg8: T8,
arg9: T9
) => TResult
): OutputSelector<TState, TProps, TResult>,
<TState, TProps, TResult, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10>(
selector1: InputSelector<TState, TProps, T1>,
selector2: InputSelector<TState, TProps, T2>,
selector3: InputSelector<TState, TProps, T3>,
selector4: InputSelector<TState, TProps, T4>,
selector5: InputSelector<TState, TProps, T5>,
selector6: InputSelector<TState, TProps, T6>,
selector7: InputSelector<TState, TProps, T7>,
selector8: InputSelector<TState, TProps, T8>,
selector9: InputSelector<TState, TProps, T9>,
selector10: InputSelector<TState, TProps, T10>,
resultFunc: (
arg1: T1,
arg2: T2,
arg3: T3,
arg4: T4,
arg5: T5,
arg6: T6,
arg7: T7,
arg8: T8,
arg9: T9,
arg10: T10
) => TResult
): OutputSelector<TState, TProps, TResult>,
<TState, TProps, TResult, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10>(
selectors: [
InputSelector<TState, TProps, T1>,
InputSelector<TState, TProps, T2>,
InputSelector<TState, TProps, T3>,
InputSelector<TState, TProps, T4>,
InputSelector<TState, TProps, T5>,
InputSelector<TState, TProps, T6>,
InputSelector<TState, TProps, T7>,
InputSelector<TState, TProps, T8>,
InputSelector<TState, TProps, T9>,
InputSelector<TState, TProps, T10>
],
resultFunc: (
arg1: T1,
arg2: T2,
arg3: T3,
arg4: T4,
arg5: T5,
arg6: T6,
arg7: T7,
arg8: T8,
arg9: T9,
arg10: T10
) => TResult
): OutputSelector<TState, TProps, TResult>,
<TState, TProps, TResult, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11>(
selector1: InputSelector<TState, TProps, T1>,
selector2: InputSelector<TState, TProps, T2>,
selector3: InputSelector<TState, TProps, T3>,
selector4: InputSelector<TState, TProps, T4>,
selector5: InputSelector<TState, TProps, T5>,
selector6: InputSelector<TState, TProps, T6>,
selector7: InputSelector<TState, TProps, T7>,
selector8: InputSelector<TState, TProps, T8>,
selector9: InputSelector<TState, TProps, T9>,
selector10: InputSelector<TState, TProps, T10>,
selector11: InputSelector<TState, TProps, T11>,
resultFunc: (
arg1: T1,
arg2: T2,
arg3: T3,
arg4: T4,
arg5: T5,
arg6: T6,
arg7: T7,
arg8: T8,
arg9: T9,
arg10: T10,
arg11: T11
) => TResult
): OutputSelector<TState, TProps, TResult>,
<TState, TProps, TResult, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11>(
selectors: [
InputSelector<TState, TProps, T1>,
InputSelector<TState, TProps, T2>,
InputSelector<TState, TProps, T3>,
InputSelector<TState, TProps, T4>,
InputSelector<TState, TProps, T5>,
InputSelector<TState, TProps, T6>,
InputSelector<TState, TProps, T7>,
InputSelector<TState, TProps, T8>,
InputSelector<TState, TProps, T9>,
InputSelector<TState, TProps, T10>,
InputSelector<TState, TProps, T11>
],
resultFunc: (
arg1: T1,
arg2: T2,
arg3: T3,
arg4: T4,
arg5: T5,
arg6: T6,
arg7: T7,
arg8: T8,
arg9: T9,
arg10: T10,
arg11: T11
) => TResult
): OutputSelector<TState, TProps, TResult>,
<
TState,
TProps,
TResult,
T1,
T2,
T3,
T4,
T5,
T6,
T7,
T8,
T9,
T10,
T11,
T12
>(
selector1: InputSelector<TState, TProps, T1>,
selector2: InputSelector<TState, TProps, T2>,
selector3: InputSelector<TState, TProps, T3>,
selector4: InputSelector<TState, TProps, T4>,
selector5: InputSelector<TState, TProps, T5>,
selector6: InputSelector<TState, TProps, T6>,
selector7: InputSelector<TState, TProps, T7>,
selector8: InputSelector<TState, TProps, T8>,
selector9: InputSelector<TState, TProps, T9>,
selector10: InputSelector<TState, TProps, T10>,
selector11: InputSelector<TState, TProps, T11>,
selector12: InputSelector<TState, TProps, T12>,
resultFunc: (
arg1: T1,
arg2: T2,
arg3: T3,
arg4: T4,
arg5: T5,
arg6: T6,
arg7: T7,
arg8: T8,
arg9: T9,
arg10: T10,
arg11: T11,
arg12: T12
) => TResult
): OutputSelector<TState, TProps, TResult>,
<
TState,
TProps,
TResult,
T1,
T2,
T3,
T4,
T5,
T6,
T7,
T8,
T9,
T10,
T11,
T12
>(
selectors: [
InputSelector<TState, TProps, T1>,
InputSelector<TState, TProps, T2>,
InputSelector<TState, TProps, T3>,
InputSelector<TState, TProps, T4>,
InputSelector<TState, TProps, T5>,
InputSelector<TState, TProps, T6>,
InputSelector<TState, TProps, T7>,
InputSelector<TState, TProps, T8>,
InputSelector<TState, TProps, T9>,
InputSelector<TState, TProps, T10>,
InputSelector<TState, TProps, T11>,
InputSelector<TState, TProps, T12>
],
resultFunc: (
arg1: T1,
arg2: T2,
arg3: T3,
arg4: T4,
arg5: T5,
arg6: T6,
arg7: T7,
arg8: T8,
arg9: T9,
arg10: T10,
arg11: T11,
arg12: T12
) => TResult
): OutputSelector<TState, TProps, TResult>,
<
TState,
TProps,
TResult,
T1,
T2,
T3,
T4,
T5,
T6,
T7,
T8,
T9,
T10,
T11,
T12,
T13
>(
selector1: InputSelector<TState, TProps, T1>,
selector2: InputSelector<TState, TProps, T2>,
selector3: InputSelector<TState, TProps, T3>,
selector4: InputSelector<TState, TProps, T4>,
selector5: InputSelector<TState, TProps, T5>,
selector6: InputSelector<TState, TProps, T6>,
selector7: InputSelector<TState, TProps, T7>,
selector8: InputSelector<TState, TProps, T8>,
selector9: InputSelector<TState, TProps, T9>,
selector10: InputSelector<TState, TProps, T10>,
selector11: InputSelector<TState, TProps, T11>,
selector12: InputSelector<TState, TProps, T12>,
selector13: InputSelector<TState, TProps, T13>,
resultFunc: (
arg1: T1,
arg2: T2,
arg3: T3,
arg4: T4,
arg5: T5,
arg6: T6,
arg7: T7,
arg8: T8,
arg9: T9,
arg10: T10,
arg11: T11,
arg12: T12,
arg13: T13
) => TResult
): OutputSelector<TState, TProps, TResult>,
<
TState,
TProps,
TResult,
T1,
T2,
T3,
T4,
T5,
T6,
T7,
T8,
T9,
T10,
T11,
T12,
T13
>(
selectors: [
InputSelector<TState, TProps, T1>,
InputSelector<TState, TProps, T2>,
InputSelector<TState, TProps, T3>,
InputSelector<TState, TProps, T4>,
InputSelector<TState, TProps, T5>,
InputSelector<TState, TProps, T6>,
InputSelector<TState, TProps, T7>,
InputSelector<TState, TProps, T8>,
InputSelector<TState, TProps, T9>,
InputSelector<TState, TProps, T10>,
InputSelector<TState, TProps, T11>,
InputSelector<TState, TProps, T12>,
InputSelector<TState, TProps, T13>
],
resultFunc: (
arg1: T1,
arg2: T2,
arg3: T3,
arg4: T4,
arg5: T5,
arg6: T6,
arg7: T7,
arg8: T8,
arg9: T9,
arg10: T10,
arg11: T11,
arg12: T12,
arg13: T13
) => TResult
): OutputSelector<TState, TProps, TResult>,
<
TState,
TProps,
TResult,
T1,
T2,
T3,
T4,
T5,
T6,
T7,
T8,
T9,
T10,
T11,
T12,
T13,
T14
>(
selector1: InputSelector<TState, TProps, T1>,
selector2: InputSelector<TState, TProps, T2>,
selector3: InputSelector<TState, TProps, T3>,
selector4: InputSelector<TState, TProps, T4>,
selector5: InputSelector<TState, TProps, T5>,
selector6: InputSelector<TState, TProps, T6>,
selector7: InputSelector<TState, TProps, T7>,
selector8: InputSelector<TState, TProps, T8>,
selector9: InputSelector<TState, TProps, T9>,
selector10: InputSelector<TState, TProps, T10>,
selector11: InputSelector<TState, TProps, T11>,
selector12: InputSelector<TState, TProps, T12>,
selector13: InputSelector<TState, TProps, T13>,
selector14: InputSelector<TState, TProps, T14>,
resultFunc: (
arg1: T1,
arg2: T2,
arg3: T3,
arg4: T4,
arg5: T5,
arg6: T6,
arg7: T7,
arg8: T8,
arg9: T9,
arg10: T10,
arg11: T11,
arg12: T12,
arg13: T13,
arg14: T14
) => TResult
): OutputSelector<TState, TProps, TResult>,
<
TState,
TProps,
TResult,
T1,
T2,
T3,
T4,
T5,
T6,
T7,
T8,
T9,
T10,
T11,
T12,
T13,
T14
>(
selectors: [
InputSelector<TState, TProps, T1>,
InputSelector<TState, TProps, T2>,
InputSelector<TState, TProps, T3>,
InputSelector<TState, TProps, T4>,
InputSelector<TState, TProps, T5>,
InputSelector<TState, TProps, T6>,
InputSelector<TState, TProps, T7>,
InputSelector<TState, TProps, T8>,
InputSelector<TState, TProps, T9>,
InputSelector<TState, TProps, T10>,
InputSelector<TState, TProps, T11>,
InputSelector<TState, TProps, T12>,
InputSelector<TState, TProps, T13>,
InputSelector<TState, TProps, T14>
],
resultFunc: (
arg1: T1,
arg2: T2,
arg3: T3,
arg4: T4,
arg5: T5,
arg6: T6,
arg7: T7,
arg8: T8,
arg9: T9,
arg10: T10,
arg11: T11,
arg12: T12,
arg13: T13,
arg14: T14
) => TResult
): OutputSelector<TState, TProps, TResult>,
<
TState,
TProps,
TResult,
T1,
T2,
T3,
T4,
T5,
T6,
T7,
T8,
T9,
T10,
T11,
T12,
T13,
T14,
T15
>(
selector1: InputSelector<TState, TProps, T1>,
selector2: InputSelector<TState, TProps, T2>,
selector3: InputSelector<TState, TProps, T3>,
selector4: InputSelector<TState, TProps, T4>,
selector5: InputSelector<TState, TProps, T5>,
selector6: InputSelector<TState, TProps, T6>,
selector7: InputSelector<TState, TProps, T7>,
selector8: InputSelector<TState, TProps, T8>,
selector9: InputSelector<TState, TProps, T9>,
selector10: InputSelector<TState, TProps, T10>,
selector11: InputSelector<TState, TProps, T11>,
selector12: InputSelector<TState, TProps, T12>,
selector13: InputSelector<TState, TProps, T13>,
selector14: InputSelector<TState, TProps, T14>,
selector15: InputSelector<TState, TProps, T15>,
resultFunc: (
arg1: T1,
arg2: T2,
arg3: T3,
arg4: T4,
arg5: T5,
arg6: T6,
arg7: T7,
arg8: T8,
arg9: T9,
arg10: T10,
arg11: T11,
arg12: T12,
arg13: T13,
arg14: T14,
arg15: T15
) => TResult
): OutputSelector<TState, TProps, TResult>,
<
TState,
TProps,
TResult,
T1,
T2,
T3,
T4,
T5,
T6,
T7,
T8,
T9,
T10,
T11,
T12,
T13,
T14,
T15
>(
selectors: [
InputSelector<TState, TProps, T1>,
InputSelector<TState, TProps, T2>,
InputSelector<TState, TProps, T3>,
InputSelector<TState, TProps, T4>,
InputSelector<TState, TProps, T5>,
InputSelector<TState, TProps, T6>,
InputSelector<TState, TProps, T7>,
InputSelector<TState, TProps, T8>,
InputSelector<TState, TProps, T9>,
InputSelector<TState, TProps, T10>,
InputSelector<TState, TProps, T11>,
InputSelector<TState, TProps, T12>,
InputSelector<TState, TProps, T13>,
InputSelector<TState, TProps, T14>,
InputSelector<TState, TProps, T15>
],
resultFunc: (
arg1: T1,
arg2: T2,
arg3: T3,
arg4: T4,
arg5: T5,
arg6: T6,
arg7: T7,
arg8: T8,
arg9: T9,
arg10: T10,
arg11: T11,
arg12: T12,
arg13: T13,
arg14: T14,
arg15: T15
) => TResult
): OutputSelector<TState, TProps, TResult>,
<
TState,
TProps,
TResult,
T1,
T2,
T3,
T4,
T5,
T6,
T7,
T8,
T9,
T10,
T11,
T12,
T13,
T14,
T15,
T16
>(
selector1: InputSelector<TState, TProps, T1>,
selector2: InputSelector<TState, TProps, T2>,
selector3: InputSelector<TState, TProps, T3>,
selector4: InputSelector<TState, TProps, T4>,
selector5: InputSelector<TState, TProps, T5>,
selector6: InputSelector<TState, TProps, T6>,
selector7: InputSelector<TState, TProps, T7>,
selector8: InputSelector<TState, TProps, T8>,
selector9: InputSelector<TState, TProps, T9>,
selector10: InputSelector<TState, TProps, T10>,
selector11: InputSelector<TState, TProps, T11>,
selector12: InputSelector<TState, TProps, T12>,
selector13: InputSelector<TState, TProps, T13>,
selector14: InputSelector<TState, TProps, T14>,
selector15: InputSelector<TState, TProps, T15>,
selector16: InputSelector<TState, TProps, T16>,
resultFunc: (
arg1: T1,
arg2: T2,
arg3: T3,
arg4: T4,
arg5: T5,
arg6: T6,
arg7: T7,
arg8: T8,
arg9: T9,
arg10: T10,
arg11: T11,
arg12: T12,
arg13: T13,
arg14: T14,
arg15: T15,
arg16: T16
) => TResult
): OutputSelector<TState, TProps, TResult>,
<
TState,
TProps,
TResult,
T1,
T2,
T3,
T4,
T5,
T6,
T7,
T8,
T9,
T10,
T11,
T12,
T13,
T14,
T15,
T16
>(
selectors: [
InputSelector<TState, TProps, T1>,
InputSelector<TState, TProps, T2>,
InputSelector<TState, TProps, T3>,
InputSelector<TState, TProps, T4>,
InputSelector<TState, TProps, T5>,
InputSelector<TState, TProps, T6>,
InputSelector<TState, TProps, T7>,
InputSelector<TState, TProps, T8>,
InputSelector<TState, TProps, T9>,
InputSelector<TState, TProps, T10>,
InputSelector<TState, TProps, T11>,
InputSelector<TState, TProps, T12>,
InputSelector<TState, TProps, T13>,
InputSelector<TState, TProps, T14>,
InputSelector<TState, TProps, T15>,
InputSelector<TState, TProps, T16>
],
resultFunc: (
arg1: T1,
arg2: T2,
arg3: T3,
arg4: T4,
arg5: T5,
arg6: T6,
arg7: T7,
arg8: T8,
arg9: T9,
arg10: T10,
arg11: T11,
arg12: T12,
arg13: T13,
arg14: T14,
arg15: T15,
arg16: T16
) => TResult
): OutputSelector<TState, TProps, TResult>
};
declare type Reselect = {
createSelector: SelectorCreator,
defaultMemoize: <TFunc: Function>(
func: TFunc,
equalityCheck?: (a: any, b: any) => boolean
) => TFunc,
createSelectorCreator: (
memoize: Function,
...memoizeOptions: any[]
) => SelectorCreator,
createStructuredSelector: <TState, TProps, InputSelectors: {[k: string | number]: InputSelector<TState, TProps, any>}>(
inputSelectors: InputSelectors,
selectorCreator?: SelectorCreator
) => OutputSelector<TState, TProps, $ObjMap<InputSelectors, ExtractReturnType>>
};
declare module.exports: Reselect;
}

View file

@ -49,8 +49,8 @@
"formik": "^0.10.4",
"hast-util-sanitize": "^1.1.2",
"keytar": "^4.2.1",
"lbry-redux": "lbryio/lbry-redux#67cae46983d9fea90dd1e4c5bd121dd5077a3f0e",
"lbryinc": "lbryio/lbryinc#de7ff055605b02a24821f0f9bab1d206eb7f235d",
"lbry-redux": "lbryio/lbry-redux#957d221c1830ecbb7a9e74fad78e711fb14539f4",
"lbryinc": "lbryio/lbryinc#3f34af546ee73ff2ee7d8ad05e540b3b0aa658fb",
"localforage": "^1.7.1",
"mammoth": "^1.4.6",
"mime": "^2.3.1",
@ -91,7 +91,7 @@
"axios": "^0.18.0",
"babel-eslint": "^8.2.2",
"babel-plugin-module-resolver": "^3.1.1",
"babel-polyfill": "^6.20.0",
"babel-polyfill": "^6.26.0",
"babel-preset-env": "^1.6.1",
"babel-preset-react": "^6.24.1",
"babel-preset-stage-2": "^6.18.0",

View file

@ -52,7 +52,12 @@ const analytics: Analytics = {
onSuccessCb: ?() => void
): void => {
if (analyticsEnabled) {
const params = {
const params: {
uri: string,
outpoint: string,
claim_id: string,
time_to_start?: number,
} = {
uri,
outpoint,
claim_id: claimId,

View file

@ -14,7 +14,7 @@ type Props = {
showLBC?: boolean,
fee?: boolean,
inheritStyle?: boolean,
filePage?: boolean,
badge?: boolean,
};
class CreditAmount extends React.PureComponent<Props> {
@ -38,7 +38,7 @@ class CreditAmount extends React.PureComponent<Props> {
fee,
showLBC,
inheritStyle,
filePage,
badge,
} = this.props;
const minimumRenderableAmount = 10 ** (-1 * precision);
@ -78,11 +78,13 @@ class CreditAmount extends React.PureComponent<Props> {
<span
title={fullPrice}
className={classnames('credit-amount', {
'credit-amount--free': !large && isFree,
'credit-amount--cost': !large && !isFree,
'credit-amount--large': large,
// TODO: remove inheritStyle prop
// It just complicates things
'credit-amount--inherit': inheritStyle,
'credit-amount--file-page': filePage,
badge: badge,
'badge--cost': badge && !isFree,
'badge--free': badge && isFree,
})}
>
{amountText}

View file

@ -27,9 +27,15 @@ type Props = {
isResolvingUri: boolean,
/* eslint-enable react/no-unused-prop-types */
isSubscribed: boolean,
showSubscribedLogo: boolean,
isNew: boolean,
};
class FileCard extends React.PureComponent<Props> {
static defaultProps = {
showSubscribedLogo: false,
};
componentWillMount() {
this.resolve(this.props);
}
@ -57,6 +63,8 @@ class FileCard extends React.PureComponent<Props> {
claimIsMine,
pending,
isSubscribed,
isNew,
showSubscribedLogo,
} = this.props;
if (!claim && !pending) {
@ -112,10 +120,15 @@ class FileCard extends React.PureComponent<Props> {
<div className="card__file-properties">
<FilePrice hideFree uri={uri} />
{isRewardContent && <Icon iconColor="red" icon={icons.FEATURED} />}
{isSubscribed && <Icon icon={icons.HEART} />}
{showSubscribedLogo && isSubscribed && <Icon icon={icons.HEART} />}
{fileInfo && <Icon icon={icons.LOCAL} />}
</div>
</div>
{isNew && (
<div className="card__subtitle">
<span className="badge badge--alert">{__('NEW')}</span>
</div>
)}
</section>
);
/* eslint-enable jsx-a11y/click-events-have-key-events */

View file

@ -147,6 +147,7 @@ class FileList extends React.PureComponent<Props, State> {
claim_id: claimId,
txid,
nout,
isNew,
} = fileInfo;
const uriParams = {};
@ -159,13 +160,13 @@ class FileList extends React.PureComponent<Props, State> {
const outpoint = `${txid}:${nout}`;
// See https://github.com/lbryio/lbry-desktop/issues/1327 for discussion around using outpoint as the key
content.push(<FileCard key={outpoint} uri={uri} checkPending={checkPending} />);
content.push(<FileCard key={outpoint} uri={uri} checkPending={checkPending} isNew={isNew} />);
});
return (
<section>
<div className="file-list__sort">
{!hideFilter && (
{!hideFilter && (
<div className="file-list__sort">
<FormField
prefix={__('Sort by')}
affixClass="form-field--align-center"
@ -177,9 +178,9 @@ class FileList extends React.PureComponent<Props, State> {
<option value="dateOld">{__('Oldest First')}</option>
<option value="title">{__('Title')}</option>
</FormField>
)}
</div>
<div className="card__list">{content}</div>
</div>
)}
<div className="card__list card__content">{content}</div>
</section>
);
}

View file

@ -10,7 +10,7 @@ type Props = {
fetching: boolean,
claim: ?{},
// below props are just passed to <CreditAmount />
filePage?: boolean,
badge?: boolean,
inheritStyle?: boolean,
showLBC?: boolean,
hideFree?: boolean, // hide the file price if it's free
@ -38,7 +38,7 @@ class FilePrice extends React.PureComponent<Props> {
};
render() {
const { costInfo, showFullPrice, filePage, inheritStyle, showLBC, hideFree } = this.props;
const { costInfo, showFullPrice, badge, inheritStyle, showLBC, hideFree } = this.props;
if (costInfo && !costInfo.cost && hideFree) {
return null;
@ -47,7 +47,7 @@ class FilePrice extends React.PureComponent<Props> {
return costInfo ? (
<CreditAmount
showFree
filePage={filePage}
badge={badge}
inheritStyle={inheritStyle}
showLBC={showLBC}
amount={costInfo.cost}

View file

@ -524,7 +524,7 @@ class PublishForm extends React.PureComponent<Props> {
step="any"
label={__('Deposit')}
postfix="LBC"
value={bid || ''}
value={bid}
error={bidError}
min="0"
disabled={!name}

View file

@ -1,11 +1,11 @@
import { connect } from 'react-redux';
import { selectNavLinks } from 'redux/selectors/app';
import { selectNotifications } from 'redux/selectors/subscriptions';
import { selectUnreadAmount } from 'redux/selectors/subscriptions';
import SideBar from './view';
const select = state => ({
navLinks: selectNavLinks(state),
notifications: selectNotifications(state),
unreadSubscriptionTotal: selectUnreadAmount(state),
});
const perform = () => ({});

View file

@ -2,7 +2,6 @@
import * as React from 'react';
import Button from 'component/button';
import classnames from 'classnames';
import * as NOTIFICATION_TYPES from 'constants/notification_types';
type SideBarLink = {
label: string,
@ -17,15 +16,11 @@ type Props = {
primary: Array<SideBarLink>,
secondary: Array<SideBarLink>,
},
notifications: {
type: string,
},
unreadSubscriptionTotal: number,
};
const SideBar = (props: Props) => {
const { navLinks, notifications } = props;
const badges = Object.keys(notifications).length;
const { navLinks, unreadSubscriptionTotal } = props;
return (
<nav className="nav">
@ -40,7 +35,11 @@ const SideBar = (props: Props) => {
>
<Button
navigate={path}
label={path === '/subscriptions' && badges ? `${label} (${badges})` : label}
label={
path === '/subscriptions' && unreadSubscriptionTotal
? `${label} (${unreadSubscriptionTotal})`
: label
}
icon={icon}
/>
</li>

View file

@ -1,15 +1,19 @@
import { connect } from 'react-redux';
import { doChannelSubscribe, doChannelUnsubscribe } from 'redux/actions/subscriptions';
import { doNotify } from 'lbry-redux';
import { selectSubscriptions } from 'redux/selectors/subscriptions';
import { selectSubscriptions, makeSelectIsSubscribed } from 'redux/selectors/subscriptions';
import SubscribeButton from './view';
const select = (state, props) => ({
subscriptions: selectSubscriptions(state),
isSubscribed: makeSelectIsSubscribed(props.uri, true)(state),
});
export default connect(select, {
doChannelSubscribe,
doChannelUnsubscribe,
doNotify,
})(SubscribeButton);
export default connect(
select,
{
doChannelSubscribe,
doChannelUnsubscribe,
doNotify,
}
)(SubscribeButton);

View file

@ -3,7 +3,6 @@ import React from 'react';
import { MODALS } from 'lbry-redux';
import * as icons from 'constants/icons';
import Button from 'component/button';
import type { Subscription } from 'types/subscription';
type SubscribtionArgs = {
channelName: string,
@ -13,7 +12,8 @@ type SubscribtionArgs = {
type Props = {
channelName: ?string,
uri: ?string,
subscriptions: Array<Subscription>,
isSubscribed: boolean,
subscriptions: Array<string>,
doChannelSubscribe: ({ channelName: string, uri: string }) => void,
doChannelUnsubscribe: SubscribtionArgs => void,
doNotify: ({ id: string }) => void,
@ -23,15 +23,13 @@ export default (props: Props) => {
const {
channelName,
uri,
subscriptions,
doChannelSubscribe,
doChannelUnsubscribe,
doNotify,
subscriptions,
isSubscribed,
} = props;
const isSubscribed =
subscriptions.map(subscription => subscription.channelName).indexOf(channelName) !== -1;
const subscriptionHandler = isSubscribed ? doChannelUnsubscribe : doChannelSubscribe;
const subscriptionLabel = isSubscribed ? __('Unsubscribe') : __('Subscribe');

View file

@ -161,7 +161,9 @@ class UserHistoryPage extends React.PureComponent<Props, State> {
</React.Fragment>
) : (
<div className="page__empty">
{__("You don't have anything saved in history yet, go check out some content on LBRY!")}
<h3 className="card__title">
{__("You don't have anything saved in history yet, go check out some content on LBRY!")}
</h3>
<div className="card__actions card__actions--center">
<Button button="primary" navigate="/discover" label={__('Explore new content')} />
</div>

View file

@ -176,14 +176,15 @@ export const CHANNEL_SUBSCRIBE = 'CHANNEL_SUBSCRIBE';
export const CHANNEL_UNSUBSCRIBE = 'CHANNEL_UNSUBSCRIBE';
export const HAS_FETCHED_SUBSCRIPTIONS = 'HAS_FETCHED_SUBSCRIPTIONS';
export const SET_SUBSCRIPTION_LATEST = 'SET_SUBSCRIPTION_LATEST';
export const SET_SUBSCRIPTION_NOTIFICATION = 'SET_SUBSCRIPTION_NOTIFICATION';
export const SET_SUBSCRIPTION_NOTIFICATIONS = 'SET_SUBSCRIPTION_NOTIFICATIONS';
export const UPDATE_SUBSCRIPTION_UNREADS = 'UPDATE_SUBSCRIPTION_UNREADS';
export const REMOVE_SUBSCRIPTION_UNREADS = 'REMOVE_SUBSCRIPTION_UNREADS';
export const CHECK_SUBSCRIPTION_STARTED = 'CHECK_SUBSCRIPTION_STARTED';
export const CHECK_SUBSCRIPTION_COMPLETED = 'CHECK_SUBSCRIPTION_COMPLETED';
export const CHECK_SUBSCRIPTIONS_SUBSCRIBE = 'CHECK_SUBSCRIPTIONS_SUBSCRIBE';
export const FETCH_SUBSCRIPTIONS_START = 'FETCH_SUBSCRIPTIONS_START';
export const FETCH_SUBSCRIPTIONS_FAIL = 'FETCH_SUBSCRIPTIONS_FAIL';
export const FETCH_SUBSCRIPTIONS_SUCCESS = 'FETCH_SUBSCRIPTIONS_SUCCESS';
export const SET_VIEW_MODE = 'SET_VIEW_MODE';
// Publishing
export const CLEAR_PUBLISH = 'CLEAR_PUBLISH';

View file

@ -32,3 +32,5 @@ export const EYE = 'Eye';
export const PLAY = 'Play';
export const FACEBOOK = 'Facebook';
export const TWITTER = 'Twitter';
export const CREDIT_CARD = 'CreditCard';
export const SETTINGS = 'Settings';

View file

@ -1,3 +1,7 @@
export const VIEW_ALL = 'view_all';
export const VIEW_LATEST_FIRST = 'view_latest_first';
// Types for unreads
export const DOWNLOADING = 'DOWNLOADING';
export const DOWNLOADED = 'DOWNLOADED';
export const NOTIFY_ONLY = 'NOTIFY_ONLY;';

View file

@ -206,4 +206,5 @@ const init = () => {
init();
/* eslint-enable react/jsx-filename-extension */
/* eslint-enable no-console */

View file

@ -31,7 +31,7 @@ class ModalWalletDecrypt extends React.PureComponent<Props> {
}
render() {
const { closeModalgaa } = this.props;
const { closeModal } = this.props;
return (
<Modal

View file

@ -2,7 +2,7 @@ import { connect } from 'react-redux';
import * as settings from 'constants/settings';
import { doNavigate } from 'redux/actions/navigation';
import { selectRewardContentClaimIds, selectPlayingUri } from 'redux/selectors/content';
import { doCheckSubscription } from 'redux/actions/subscriptions';
import { doRemoveUnreadSubscription } from 'redux/actions/subscriptions';
import { doSetClientSetting } from 'redux/actions/settings';
import { doSetContentHistoryItem } from 'redux/actions/content';
import {
@ -15,9 +15,10 @@ import {
makeSelectContentTypeForUri,
makeSelectMetadataForUri,
doNotify,
makeSelectChannelForClaimUri,
} from 'lbry-redux';
import { selectShowNsfw, makeSelectClientSetting } from 'redux/selectors/settings';
import { selectSubscriptions } from 'redux/selectors/subscriptions';
import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions';
import { doPrepareEdit } from 'redux/actions/publish';
import FilePage from './view';
@ -29,21 +30,22 @@ const select = (state, props) => ({
obscureNsfw: !selectShowNsfw(state),
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
rewardedContentClaimIds: selectRewardContentClaimIds(state, props),
subscriptions: selectSubscriptions(state),
playingUri: selectPlayingUri(state),
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
autoplay: makeSelectClientSetting(settings.AUTOPLAY)(state),
isSubscribed: makeSelectIsSubscribed(props.uri)(state),
channelUri: makeSelectChannelForClaimUri(props.uri, true)(state),
});
const perform = dispatch => ({
navigate: (path, params) => dispatch(doNavigate(path, params)),
fetchFileInfo: uri => dispatch(doFetchFileInfo(uri)),
fetchCostInfo: uri => dispatch(doFetchCostInfoForUri(uri)),
checkSubscription: uri => dispatch(doCheckSubscription(uri)),
openModal: (modal, props) => dispatch(doNotify(modal, props)),
prepareEdit: (publishData, uri) => dispatch(doPrepareEdit(publishData, uri)),
setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)),
setViewed: uri => dispatch(doSetContentHistoryItem(uri)),
markSubscriptionRead: (channel, uri) => dispatch(doRemoveUnreadSubscription(channel, uri)),
});
export default connect(

View file

@ -1,4 +1,6 @@
// @flow
import type { Claim, Metadata } from 'types/claim';
import type { FileInfo } from 'types/file_info';
import * as React from 'react';
import * as settings from 'constants/settings';
import { buildURI, normalizeURI, MODALS } from 'lbry-redux';
@ -14,8 +16,6 @@ import * as icons from 'constants/icons';
import Button from 'component/button';
import SubscribeButton from 'component/subscribeButton';
import Page from 'component/page';
import type { Claim } from 'types/claim';
import type { Subscription } from 'types/subscription';
import FileDownloadLink from 'component/fileDownloadLink';
import classnames from 'classnames';
import getMediaType from 'util/getMediaType';
@ -25,31 +25,26 @@ import ToolTip from 'component/common/tooltip';
type Props = {
claim: Claim,
fileInfo: {},
metadata: {
title: string,
thumbnail: string,
file_name: string,
nsfw: boolean,
},
fileInfo: FileInfo,
metadata: Metadata,
contentType: string,
uri: string,
rewardedContentClaimIds: Array<string>,
obscureNsfw: boolean,
claimIsMine: boolean,
costInfo: ?{},
navigate: (string, ?{}) => void,
openModal: ({ id: string }, { uri: string }) => void,
costInfo: ?{ cost: number },
fetchFileInfo: string => void,
fetchCostInfo: string => void,
prepareEdit: ({}, string) => void,
setViewed: string => void,
autoplay: boolean,
isSubscribed: ?string,
isSubscribed: boolean,
channelUri: string,
prepareEdit: ({}, string) => void,
navigate: (string, ?{}) => void,
openModal: ({ id: string }, { uri: string }) => void,
setClientSetting: (string, string | boolean | number) => void,
/* eslint-disable react/no-unused-prop-types */
checkSubscription: (uri: string) => void,
subscriptions: Array<Subscription>,
/* eslint-enable react/no-unused-prop-types */
markSubscriptionRead: (string, string) => void,
};
class FilePage extends React.Component<Props> {
@ -73,7 +68,11 @@ class FilePage extends React.Component<Props> {
}
componentDidMount() {
const { uri, fileInfo, fetchFileInfo, fetchCostInfo, setViewed } = this.props;
const { uri, fileInfo, fetchFileInfo, fetchCostInfo, setViewed, isSubscribed } = this.props;
if (isSubscribed) {
this.removeFromSubscriptionNotifications();
}
if (fileInfo === undefined) {
fetchFileInfo(uri);
@ -81,9 +80,6 @@ class FilePage extends React.Component<Props> {
// See https://github.com/lbryio/lbry-desktop/pull/1563 for discussion
fetchCostInfo(uri);
this.checkSubscription(this.props);
setViewed(uri);
}
@ -98,23 +94,22 @@ class FilePage extends React.Component<Props> {
}
}
componentDidUpdate(prevProps: Props) {
if (!prevProps.isSubscribed && this.props.isSubscribed) {
this.removeFromSubscriptionNotifications();
}
}
onAutoplayChange(event: SyntheticInputEvent<*>) {
this.props.setClientSetting(settings.AUTOPLAY, event.target.checked);
}
checkSubscription = (props: Props) => {
if (props.subscriptions.find(sub => sub.channelName === props.claim.channel_name)) {
props.checkSubscription(
buildURI(
{
contentName: props.claim.channel_name,
claimId: props.claim.value.publisherSignature.certificateId,
},
false
)
);
}
};
removeFromSubscriptionNotifications() {
// Always try to remove
// If it doesn't exist, nothing will happen
const { markSubscriptionRead, uri, channelUri } = this.props;
markSubscriptionRead(channelUri, uri);
}
render() {
const {
@ -131,11 +126,12 @@ class FilePage extends React.Component<Props> {
costInfo,
fileInfo,
autoplay,
channelUri,
} = this.props;
// File info
const { title, thumbnail } = metadata;
const { height, channel_name: channelName, value } = claim;
const { height, channel_name: channelName } = claim;
const { PLAYABLE_MEDIA_TYPES, PREVIEW_MEDIA_TYPES } = FilePage;
const isRewardContent = (rewardedContentClaimIds || []).includes(claim.claim_id);
const shouldObscureThumbnail = obscureNsfw && metadata.nsfw;
@ -143,12 +139,6 @@ class FilePage extends React.Component<Props> {
const mediaType = getMediaType(contentType, fileName);
const showFile =
PLAYABLE_MEDIA_TYPES.includes(mediaType) || PREVIEW_MEDIA_TYPES.includes(mediaType);
const channelClaimId =
value && value.publisherSignature && value.publisherSignature.certificateId;
let subscriptionUri;
if (channelName && channelClaimId) {
subscriptionUri = buildURI({ channelName, claimId: channelClaimId }, false);
}
const speechShareable =
costInfo &&
costInfo.cost === 0 &&
@ -159,7 +149,10 @@ class FilePage extends React.Component<Props> {
// We will select the claim id before they publish
let editUri;
if (claimIsMine) {
const uriObject = { contentName: claim.name, claimId: claim.claim_id };
const uriObject: { contentName: string, claimId: string, channelName: ?string } = {
contentName: claim.name,
claimId: claim.claim_id,
};
if (channelName) {
uriObject.channelName = channelName;
}
@ -193,7 +186,7 @@ class FilePage extends React.Component<Props> {
{isRewardContent && (
<Icon size={20} iconColor="red" tooltip="bottom" icon={icons.FEATURED} />
)}
<FilePrice filePage uri={normalizeURI(uri)} />
<FilePrice badge uri={normalizeURI(uri)} />
</div>
</div>
<span className="card__subtitle">
@ -214,7 +207,7 @@ class FilePage extends React.Component<Props> {
}}
/>
) : (
<SubscribeButton uri={subscriptionUri} channelName={channelName} />
<SubscribeButton uri={channelUri} channelName={channelName} />
)}
{!claimIsMine && (
<Button

View file

@ -21,7 +21,7 @@ class FileListDownloaded extends React.PureComponent<Props> {
<FileList fileInfos={fileInfos} />
) : (
<div className="page__empty">
{__("You haven't downloaded anything from LBRY yet.")}
<h3 className="card__title">{__("You haven't downloaded anything from LBRY yet.")}</h3>
<div className="card__actions card__actions--center">
<Button
button="primary"

View file

@ -29,7 +29,9 @@ class FileListPublished extends React.PureComponent<Props> {
<FileList checkPending fileInfos={claims} sortByHeight />
) : (
<div className="page__empty">
{__("It looks like you haven't published anything to LBRY yet.")}
<h3 className="card__title">
{__("It looks like you haven't published anything to LBRY yet.")}
</h3>
<div className="card__actions card__actions--center">
<Button
button="primary"

View file

@ -184,11 +184,13 @@ class SettingsPage extends React.PureComponent<Props, State> {
<section className="card card--section">
<div className="card__title">{__('Download Directory')}</div>
<span className="card__subtitle">{__('LBRY downloads will be saved here.')}</span>
<FileSelector
type="openDirectory"
currentPath={daemonSettings.download_directory}
onFileChosen={this.onDownloadDirChange}
/>
<div className="card__content">
<FileSelector
type="openDirectory"
currentPath={daemonSettings.download_directory}
onFileChosen={this.onDownloadDirChange}
/>
</div>
</section>
<section className="card card--section">
<div className="card__title">{__('Max Purchase Price')}</div>

View file

@ -5,9 +5,14 @@ import {
selectSubscriptions,
selectSubscriptionsBeingFetched,
selectIsFetchingSubscriptions,
selectNotifications,
selectUnreadSubscriptions,
selectViewMode,
} from 'redux/selectors/subscriptions';
import { setSubscriptionNotifications, doFetchMySubscriptions } from 'redux/actions/subscriptions';
import {
doUpdateUnreadSubscriptions,
doFetchMySubscriptions,
doSetViewMode,
} from 'redux/actions/subscriptions';
import { doSetClientSetting } from 'redux/actions/settings';
import { makeSelectClientSetting } from 'redux/selectors/settings';
import SubscriptionsPage from './view';
@ -16,17 +21,19 @@ const select = state => ({
loading:
selectIsFetchingSubscriptions(state) ||
Boolean(Object.keys(selectSubscriptionsBeingFetched(state)).length),
subscriptions: selectSubscriptions(state),
subscriptionClaims: selectSubscriptionClaims(state),
notifications: selectNotifications(state),
subscribedChannels: selectSubscriptions(state),
autoDownload: makeSelectClientSetting(settings.AUTO_DOWNLOAD)(state),
allSubscriptions: selectSubscriptionClaims(state),
unreadSubscriptions: selectUnreadSubscriptions(state),
viewMode: selectViewMode(state),
});
export default connect(
select,
{
setSubscriptionNotifications,
doUpdateUnreadSubscriptions,
doFetchMySubscriptions,
doSetClientSetting,
doSetViewMode,
}
)(SubscriptionsPage);

View file

@ -1,83 +1,153 @@
// @flow
import React from 'react';
import Page from 'component/page';
import type { ViewMode } from 'types/subscription';
import type { Claim } from 'types/claim';
import { VIEW_ALL, VIEW_LATEST_FIRST } from 'constants/subscriptions';
import * as settings from 'constants/settings';
import type { Subscription } from 'types/subscription';
import * as NOTIFICATION_TYPES from 'constants/notification_types';
import * as React from 'react';
import Page from 'component/page';
import Button from 'component/button';
import FileList from 'component/fileList';
import type { Claim } from 'types/claim';
import HiddenNsfwClaims from 'component/hiddenNsfwClaims';
import { FormField, FormRow } from 'component/common/form';
import { FormField } from 'component/common/form';
import FileCard from 'component/fileCard';
import { parseURI } from 'lbry-redux';
type Props = {
doFetchMySubscriptions: () => void,
setSubscriptionNotifications: ({}) => void,
subscriptions: Array<Subscription>,
subscriptionClaims: Array<{ uri: string, claims: Array<Claim> }>,
notifications: {},
subscribedChannels: Array<string>, // The channels a user is subscribed to
unreadSubscriptions: Array<{
channel: string,
uris: Array<string>,
}>,
allSubscriptions: Array<{ uri: string, ...Claim }>,
loading: boolean,
autoDownload: boolean,
viewMode: ViewMode,
doSetViewMode: ViewMode => void,
doFetchMySubscriptions: () => void,
doSetClientSetting: (string, boolean) => void,
};
export default class extends React.PureComponent<Props> {
constructor() {
super();
(this: any).onAutoDownloadChange = this.onAutoDownloadChange.bind(this);
}
componentDidMount() {
const { notifications, setSubscriptionNotifications, doFetchMySubscriptions } = this.props;
const { doFetchMySubscriptions } = this.props;
doFetchMySubscriptions();
// @sean will change this behavior when implementing new content labeling
// notifications should be cleared individually
// do we want a way to clear individual claims without viewing?
const newNotifications = {};
Object.keys(notifications).forEach(cur => {
if (notifications[cur].type === NOTIFICATION_TYPES.DOWNLOADING) {
newNotifications[cur] = { ...notifications[cur] };
}
});
setSubscriptionNotifications(newNotifications);
}
onAutoDownloadChange(event: SyntheticInputEvent<*>) {
this.props.doSetClientSetting(settings.AUTO_DOWNLOAD, event.target.checked);
}
render() {
const { subscriptions, subscriptionClaims, loading, autoDownload } = this.props;
let claimList = [];
subscriptionClaims.forEach(claimData => {
claimList = claimList.concat(claimData.claims);
});
const subscriptionUris = claimList.map(claim => `lbry://${claim.name}#${claim.claim_id}`);
renderSubscriptions() {
const { viewMode, unreadSubscriptions, allSubscriptions } = this.props;
if (viewMode === VIEW_ALL) {
return (
<React.Fragment>
<div className="card__title">{__('Your subscriptions')}</div>
<FileList hideFilter sortByHeight fileInfos={allSubscriptions} />
</React.Fragment>
);
}
return (
<Page notContained loading={loading}>
<HiddenNsfwClaims uris={subscriptionUris} />
<FormRow alignRight>
<FormField
type="checkbox"
name="auto_download"
onChange={this.onAutoDownloadChange}
checked={autoDownload}
prefix={__('Automatically download new content from your subscriptions')}
/>
</FormRow>
{!subscriptions.length && (
<React.Fragment>
{unreadSubscriptions.length ? (
unreadSubscriptions.map(({ channel, uris }) => {
const { claimName } = parseURI(channel);
return (
<section key={channel}>
<div className="card__title">
<Button
button="link"
navigate="/show"
navigateParams={{ uri: channel }}
label={claimName}
/>
</div>
<div className="card__list card__content">
{uris.map(uri => <FileCard isNew key={uri} uri={uri} />)}
</div>
</section>
);
})
) : (
<div className="page__empty">
{__("It looks like you aren't subscribed to any channels yet.")}
<div className="card__actions card__actions--center">
<h3 className="card__title">{__('You are all caught up!')}</h3>
<div className="card__actions">
<Button button="primary" navigate="/discover" label={__('Explore new content')} />
</div>
</div>
)}
{!!claimList.length && <FileList hideFilter sortByHeight fileInfos={claimList} />}
</React.Fragment>
);
}
render() {
const {
subscribedChannels,
allSubscriptions,
loading,
autoDownload,
viewMode,
doSetViewMode,
} = this.props;
return (
// Only pass in the loading prop if there are no subscriptions
// If there are any, let the page update in the background
// The loading prop removes children and shows a loading spinner
<Page notContained loading={loading && !subscribedChannels}>
<HiddenNsfwClaims
uris={allSubscriptions.reduce((arr, { name, claim_id: claimId }) => {
if (name && claimId) {
arr.push(`lbry://${name}#${claimId}`);
}
return arr;
}, [])}
/>
{!!subscribedChannels.length && (
<div className="card--space-between">
<div className="card__actions card__actions--no-margin">
<Button
disabled={viewMode === VIEW_ALL}
button="link"
label="All Subscriptions"
onClick={() => doSetViewMode(VIEW_ALL)}
/>
<Button
button="link"
disabled={viewMode === VIEW_LATEST_FIRST}
label={__('Latest Only')}
onClick={() => doSetViewMode(VIEW_LATEST_FIRST)}
/>
</div>
<FormField
type="checkbox"
name="auto_download"
onChange={this.onAutoDownloadChange}
checked={autoDownload}
prefix={__('Auto download')}
/>
</div>
)}
{!subscribedChannels.length && (
<div className="page__empty">
<h3 className="card__title">
{__("It looks like you aren't subscribed to any channels yet.")}
</h3>
<div className="card__actions">
<Button button="primary" navigate="/discover" label={__('Explore new content')} />
</div>
</div>
)}
{!!subscribedChannels.length && (
<div className="card__content">{this.renderSubscriptions()}</div>
)}
</Page>
);
}

View file

@ -1,10 +1,10 @@
// @flow
import * as NOTIFICATION_TYPES from 'constants/notification_types';
import * as NOTIFICATION_TYPES from 'constants/subscriptions';
import { ipcRenderer } from 'electron';
import { doAlertError } from 'redux/actions/app';
import { doNavigate } from 'redux/actions/navigation';
import { setSubscriptionLatest, setSubscriptionNotification } from 'redux/actions/subscriptions';
import { selectNotifications } from 'redux/selectors/subscriptions';
import { setSubscriptionLatest, doUpdateUnreadSubscriptions } from 'redux/actions/subscriptions';
import { makeSelectUnreadByChannel } from 'redux/selectors/subscriptions';
import { selectBadgeNumber } from 'redux/selectors/app';
import {
ACTIONS,
@ -21,6 +21,8 @@ import {
selectBalance,
MODALS,
doNotify,
makeSelectChannelForClaimUri,
parseURI,
} from 'lbry-redux';
import { makeSelectClientSetting, selectosNotificationsEnabled } from 'redux/selectors/settings';
import setBadge from 'util/setBadge';
@ -66,19 +68,15 @@ export function doUpdateLoadStatus(uri: string, outpoint: string) {
const totalProgress = selectTotalDownloadProgress(state);
setProgressBar(totalProgress);
const notifications = selectNotifications(state);
if (notifications[uri] && notifications[uri].type === NOTIFICATION_TYPES.DOWNLOADING) {
const count = Object.keys(notifications).reduce(
(acc, cur) =>
notifications[cur].subscription.channelName ===
notifications[uri].subscription.channelName
? acc + 1
: acc,
0
);
const channelUri = makeSelectChannelForClaimUri(uri, true)(state);
const { claimName: channelName } = parseURI(channelUri);
const unreadForChannel = makeSelectUnreadByChannel(channelUri)(state);
if (unreadForChannel.type === NOTIFICATION_TYPES.DOWNLOADING) {
const count = unreadForChannel.uris.length;
if (selectosNotificationsEnabled(state)) {
const notif = new window.Notification(notifications[uri].subscription.channelName, {
const notif = new window.Notification(channelName, {
body: `Posted ${fileInfo.metadata.title}${
count > 1 && count < 10 ? ` and ${count - 1} other new items` : ''
}${count > 9 ? ' and 9+ other new items' : ''}`,
@ -92,18 +90,12 @@ export function doUpdateLoadStatus(uri: string, outpoint: string) {
);
};
}
if (state.navigation.currentPath !== '/subscriptions') {
dispatch(
setSubscriptionNotification(
notifications[uri].subscription,
uri,
NOTIFICATION_TYPES.DOWNLOADED
)
);
}
dispatch(doUpdateUnreadSubscriptions(channelUri, null, NOTIFICATION_TYPES.DOWNLOADED));
} else {
// If notifications are disabled(false) just return
if (!selectosNotificationsEnabled(getState())) return;
const notif = new window.Notification('LBRY Download Complete', {
body: fileInfo.metadata.title,
silent: false,
@ -293,14 +285,14 @@ export function doPurchaseUri(uri, specificCostInfo, shouldRecordViewEvent) {
};
}
export function doFetchClaimsByChannel(uri, page) {
export function doFetchClaimsByChannel(uri, page, pageSize) {
return dispatch => {
dispatch({
type: ACTIONS.FETCH_CHANNEL_CLAIMS_STARTED,
data: { uri, page },
});
Lbry.claim_list_by_channel({ uri, page: page || 1, page_size: 48 }).then(result => {
Lbry.claim_list_by_channel({ uri, page: page || 1, page_size: pageSize || 48 }).then(result => {
const claimResult = result[uri] || {};
const { claims_in_channel: claimsInChannel, returned_page: returnedPage } = claimResult;
@ -321,18 +313,6 @@ export function doFetchClaimsByChannel(uri, page) {
buildURI({ contentName: latest.name, claimId: latest.claim_id }, false)
)
);
// commented out as a note for @sean, notification will be clared individually
// const notifications = selectNotifications(getState());
// const newNotifications = {};
// Object.keys(notifications).forEach(cur => {
// if (
// notifications[cur].subscription.channelName !== latest.channel_name ||
// notifications[cur].type === NOTIFICATION_TYPES.DOWNLOADING
// ) {
// newNotifications[cur] = { ...notifications[cur] };
// }
// });
// dispatch(setSubscriptionNotifications(newNotifications));
}
dispatch({

View file

@ -1,26 +1,36 @@
// @flow
import type { GetState } from 'types/redux';
import type {
Dispatch as ReduxDispatch,
SubscriptionState,
Subscription,
SubscriptionNotificationType,
ViewMode,
UnreadSubscription,
} from 'types/subscription';
import * as ACTIONS from 'constants/action_types';
import * as NOTIFICATION_TYPES from 'constants/notification_types';
import * as SETTINGS from 'constants/settings';
import * as NOTIFICATION_TYPES from 'constants/subscriptions';
import { Lbryio, rewards, doClaimRewardType } from 'lbryinc';
import type { Dispatch, SubscriptionNotifications } from 'redux/reducers/subscriptions';
import type { Subscription } from 'types/subscription';
import { selectSubscriptions } from 'redux/selectors/subscriptions';
import { selectSubscriptions, selectUnreadByChannel } from 'redux/selectors/subscriptions';
import { makeSelectClientSetting } from 'redux/selectors/settings';
import { Lbry, buildURI, parseURI, selectCurrentPage } from 'lbry-redux';
import { Lbry, buildURI, parseURI } from 'lbry-redux';
import { doPurchaseUri, doFetchClaimsByChannel } from 'redux/actions/content';
import Promise from 'bluebird';
const CHECK_SUBSCRIPTIONS_INTERVAL = 15 * 60 * 1000;
const SUBSCRIPTION_DOWNLOAD_LIMIT = 1;
export const doFetchMySubscriptions = () => (dispatch: Dispatch, getState: () => any) => {
const {
subscriptions: subscriptionState,
settings: { daemonSettings },
} = getState();
const { subscriptions: reduxSubscriptions } = subscriptionState;
const { share_usage_data: isSharingData } = daemonSettings;
export const doSetViewMode = (viewMode: ViewMode) => (dispatch: ReduxDispatch) =>
dispatch({
type: ACTIONS.SET_VIEW_MODE,
data: viewMode,
});
export const doFetchMySubscriptions = () => (dispatch: ReduxDispatch, getState: GetState) => {
const state: { subscriptions: SubscriptionState, settings: any } = getState();
const { subscriptions: reduxSubscriptions } = state.subscriptions;
const { share_usage_data: isSharingData } = state.settings.daemonSettings;
if (!isSharingData && isSharingData !== undefined) {
// They aren't sharing their data, subscriptions will be handled by persisted redux state
@ -84,13 +94,13 @@ export const doFetchMySubscriptions = () => (dispatch: Dispatch, getState: () =>
// DB is already synced, just return the subscriptions in redux
return reduxSubscriptions;
})
.then(subscriptions => {
.then((subscriptions: Array<Subscription>) => {
dispatch({
type: ACTIONS.FETCH_SUBSCRIPTIONS_SUCCESS,
data: subscriptions,
});
subscriptions.forEach(({ uri }) => dispatch(doFetchClaimsByChannel(uri)));
subscriptions.forEach(({ uri }) => dispatch(doFetchClaimsByChannel(uri, 1, 20)));
})
.catch(() => {
dispatch({
@ -100,7 +110,7 @@ export const doFetchMySubscriptions = () => (dispatch: Dispatch, getState: () =>
};
export const setSubscriptionLatest = (subscription: Subscription, uri: string) => (
dispatch: Dispatch
dispatch: ReduxDispatch
) =>
dispatch({
type: ACTIONS.SET_SUBSCRIPTION_LATEST,
@ -110,120 +120,194 @@ export const setSubscriptionLatest = (subscription: Subscription, uri: string) =
},
});
export const setSubscriptionNotification = (
subscription: Subscription,
uri: string,
notificationType: string
) => (dispatch: Dispatch) =>
// Populate a channels unread subscriptions or update the type
export const doUpdateUnreadSubscriptions = (
channelUri: string,
uris: ?Array<string>,
type: ?SubscriptionNotificationType
) => (dispatch: ReduxDispatch, getState: GetState) => {
const state = getState();
const unreadByChannel = selectUnreadByChannel(state);
const currentUnreadForChannel: UnreadSubscription = unreadByChannel[channelUri];
let newUris;
let newType;
if (!currentUnreadForChannel) {
newUris = uris;
newType = type;
} else {
if (uris) {
// If a channel currently has no unread uris, just add them all
if (!currentUnreadForChannel.uris || !currentUnreadForChannel.uris.length) {
newUris = uris;
} else {
// They already have unreads and now there are new ones
// Add the new ones to the beginning of the list
// Make sure there are no duplicates
const currentUnreadUris = currentUnreadForChannel.uris;
newUris = uris.filter(uri => !currentUnreadUris.includes(uri)).concat(currentUnreadUris);
}
} else {
newUris = currentUnreadForChannel.uris;
}
newType = type || currentUnreadForChannel.type;
}
dispatch({
type: ACTIONS.SET_SUBSCRIPTION_NOTIFICATION,
type: ACTIONS.UPDATE_SUBSCRIPTION_UNREADS,
data: {
subscription,
uri,
type: notificationType,
channel: channelUri,
uris: newUris,
type: newType,
},
});
};
export const doCheckSubscription = (subscriptionUri: string, notify?: boolean) => (
dispatch: Dispatch,
getState: () => {}
// Remove multiple files (or all) from a channels unread subscriptions
export const doRemoveUnreadSubscriptions = (channelUri: string, readUris: Array<string>) => (
dispatch: ReduxDispatch,
getState: GetState
) => {
const state = getState();
const unreadByChannel = selectUnreadByChannel(state);
const currentChannelUnread = unreadByChannel[channelUri];
if (!currentChannelUnread || !currentChannelUnread.uris) {
return;
}
// For each uri passed in, remove it from the list of unread uris
const urisToRemoveMap = readUris.reduce(
(acc, val) => ({
...acc,
[val]: true,
}),
{}
);
const filteredUris = currentChannelUnread.uris.filter(uri => !urisToRemoveMap[uri]);
const newUris = filteredUris.length ? filteredUris : null;
dispatch({
type: ACTIONS.REMOVE_SUBSCRIPTION_UNREADS,
data: {
channel: channelUri,
uris: newUris,
},
});
};
// Remove a single file from a channels unread subscriptions
export const doRemoveUnreadSubscription = (channelUri: string, readUri: string) => (
dispatch: ReduxDispatch
) => {
dispatch(doRemoveUnreadSubscriptions(channelUri, [readUri]));
};
export const doCheckSubscription = (subscriptionUri: string, shouldNotify?: boolean) => async (
dispatch: ReduxDispatch,
getState: GetState
) => {
// no dispatching FETCH_CHANNEL_CLAIMS_STARTED; causes loading issues on <SubscriptionsPage>
const state = getState();
const currentPage = selectCurrentPage(state);
const shouldAutoDownload = makeSelectClientSetting(SETTINGS.AUTO_DOWNLOAD)(state);
const savedSubscription = state.subscriptions.subscriptions.find(
sub => sub.uri === subscriptionUri
);
Lbry.claim_list_by_channel({ uri: subscriptionUri, page: 1 }).then(result => {
const claimResult = result[subscriptionUri] || {};
const { claims_in_channel: claimsInChannel } = claimResult;
// may happen if subscribed to an abandoned channel or an empty channel
if (!claimsInChannel) {
return;
}
const latestIndex = claimsInChannel.findIndex(
claim => `${claim.name}#${claim.claim_id}` === savedSubscription.latest
if (!savedSubscription) {
throw Error(
`Trying to find new content for ${subscriptionUri} but it doesn't exist in your subscriptions`
);
}
// if latest is 0, nothing has changed
// when there is no subscription latest, it is either a newly subscriubed channel or
// the user has cleared their cache. Either way, do not download or notify about new content
// as that would download/notify 10 claims per channel
if (claimsInChannel.length && latestIndex !== 0 && savedSubscription.latest) {
let downloadCount = 0;
claimsInChannel.slice(0, latestIndex === -1 ? 10 : latestIndex).forEach(claim => {
const uri = buildURI({ contentName: claim.name, claimId: claim.claim_id }, false);
const shouldDownload = Boolean(
downloadCount < SUBSCRIPTION_DOWNLOAD_LIMIT &&
!claim.value.stream.metadata.fee &&
makeSelectClientSetting(SETTINGS.AUTO_DOWNLOAD)(state)
);
if (notify && currentPage !== 'subscriptions') {
dispatch(
setSubscriptionNotification(
savedSubscription,
uri,
shouldDownload ? NOTIFICATION_TYPES.DOWNLOADING : NOTIFICATION_TYPES.NOTIFY_ONLY
)
);
}
if (shouldDownload) {
downloadCount += 1;
dispatch(doPurchaseUri(uri, { cost: 0 }, true));
}
});
}
const claimListByChannel = await Lbry.claim_list_by_channel({ uri: subscriptionUri, page: 1 });
const claimResult = claimListByChannel[subscriptionUri] || {};
const { claims_in_channel: claimsInChannel } = claimResult;
// may happen if subscribed to an abandoned channel or an empty channel
if (!claimsInChannel || !claimsInChannel.length) {
return;
}
// Determine if the latest subscription currently saved is actually the latest subscription
const latestIndex = claimsInChannel.findIndex(
claim => `${claim.name}#${claim.claim_id}` === savedSubscription.latest
);
// If latest is -1, it is a newly subscribed channel or there have been 10+ claims published since last viewed
const latestIndexToNotify = latestIndex === -1 ? 10 : latestIndex;
// If latest is 0, nothing has changed
// Do not download/notify about new content, it would download/notify 10 claims per channel
if (latestIndex !== 0 && savedSubscription.latest) {
let downloadCount = 0;
const newUnread = [];
claimsInChannel.slice(0, latestIndexToNotify).forEach(claim => {
const uri = buildURI({ contentName: claim.name, claimId: claim.claim_id }, true);
const shouldDownload =
shouldAutoDownload &&
Boolean(downloadCount < SUBSCRIPTION_DOWNLOAD_LIMIT && !claim.value.stream.metadata.fee);
// Add the new content to the list of "un-read" subscriptions
if (shouldNotify) {
newUnread.push(uri);
}
if (shouldDownload) {
downloadCount += 1;
dispatch(doPurchaseUri(uri, { cost: 0 }, true));
}
});
// always setLatest; important for newly subscribed channels
dispatch(
setSubscriptionLatest(
{
channelName: claimsInChannel[0].channel_name,
uri: buildURI(
{
channelName: claimsInChannel[0].channel_name,
claimId: claimsInChannel[0].claim_id,
},
false
),
},
buildURI(
{ contentName: claimsInChannel[0].name, claimId: claimsInChannel[0].claim_id },
false
)
doUpdateUnreadSubscriptions(
subscriptionUri,
newUnread,
downloadCount > 0 ? NOTIFICATION_TYPES.DOWNLOADING : NOTIFICATION_TYPES.NOTIFY_ONLY
)
);
}
// calling FETCH_CHANNEL_CLAIMS_COMPLETED after not calling STARTED
// means it will delete a non-existant fetchingChannelClaims[uri]
dispatch({
type: ACTIONS.FETCH_CHANNEL_CLAIMS_COMPLETED,
data: {
uri: subscriptionUri,
claims: claimsInChannel || [],
page: 1,
// Set the latest piece of content for a channel
// This allows the app to know if there has been new content since it was last set
dispatch(
setSubscriptionLatest(
{
channelName: claimsInChannel[0].channel_name,
uri: buildURI(
{
channelName: claimsInChannel[0].channel_name,
claimId: claimsInChannel[0].claim_id,
},
false
),
},
});
buildURI(
{ contentName: claimsInChannel[0].name, claimId: claimsInChannel[0].claim_id },
false
)
)
);
// calling FETCH_CHANNEL_CLAIMS_COMPLETED after not calling STARTED
// means it will delete a non-existant fetchingChannelClaims[uri]
dispatch({
type: ACTIONS.FETCH_CHANNEL_CLAIMS_COMPLETED,
data: {
uri: subscriptionUri,
claims: claimsInChannel || [],
page: 1,
},
});
};
export const setSubscriptionNotifications = (notifications: SubscriptionNotifications) => (
dispatch: Dispatch
) =>
dispatch({
type: ACTIONS.SET_SUBSCRIPTION_NOTIFICATIONS,
data: {
notifications,
},
});
export const doChannelSubscribe = (subscription: Subscription) => (
dispatch: Dispatch,
getState: () => any
dispatch: ReduxDispatch,
getState: GetState
) => {
const {
settings: { daemonSettings },
@ -251,8 +335,8 @@ export const doChannelSubscribe = (subscription: Subscription) => (
};
export const doChannelUnsubscribe = (subscription: Subscription) => (
dispatch: Dispatch,
getState: () => any
dispatch: ReduxDispatch,
getState: GetState
) => {
const {
settings: { daemonSettings },
@ -272,15 +356,16 @@ export const doChannelUnsubscribe = (subscription: Subscription) => (
}
};
export const doCheckSubscriptions = () => (dispatch: Dispatch, getState: () => any) => {
export const doCheckSubscriptions = () => (dispatch: ReduxDispatch, getState: GetState) => {
const state = getState();
const subscriptions = selectSubscriptions(state);
subscriptions.forEach((sub: Subscription) => {
dispatch(doCheckSubscription(sub.uri, true));
});
};
export const doCheckSubscriptionsInit = () => (dispatch: Dispatch) => {
export const doCheckSubscriptionsInit = () => (dispatch: ReduxDispatch) => {
// doCheckSubscriptionsInit is called by doDaemonReady
// setTimeout below is a hack to ensure redux is hydrated when subscriptions are checked
// this will be replaced with <PersistGate> which reqiures a package upgrade

View file

@ -1,98 +1,31 @@
// @flow
import * as ACTIONS from 'constants/action_types';
import * as NOTIFICATION_TYPES from 'constants/notification_types';
import { VIEW_ALL } from 'constants/subscriptions';
import { handleActions } from 'util/redux-utils';
import type { Subscription } from 'types/subscription';
import type { Dispatch as ReduxDispatch } from 'types/redux';
import type {
SubscriptionState,
Subscription,
DoChannelSubscribe,
DoChannelUnsubscribe,
SetSubscriptionLatest,
DoUpdateSubscriptionUnreads,
DoRemoveSubscriptionUnreads,
FetchedSubscriptionsSucess,
SetViewMode,
} from 'types/subscription';
export type NotificationType =
| NOTIFICATION_TYPES.DOWNLOADING
| NOTIFICATION_TYPES.DOWNLOADED
| NOTIFICATION_TYPES.NOTIFY_ONLY;
export type SubscriptionNotifications = {
[string]: {
subscription: Subscription,
type: NotificationType,
},
};
// Subscription redux types
export type SubscriptionState = {
subscriptions: Array<Subscription>,
notifications: SubscriptionNotifications,
loading: boolean,
};
// Subscription action types
type doChannelSubscribe = {
type: ACTIONS.CHANNEL_SUBSCRIBE,
data: Subscription,
};
type doChannelUnsubscribe = {
type: ACTIONS.CHANNEL_UNSUBSCRIBE,
data: Subscription,
};
type setSubscriptionLatest = {
type: ACTIONS.SET_SUBSCRIPTION_LATEST,
data: {
subscription: Subscription,
uri: string,
},
};
type setSubscriptionNotification = {
type: ACTIONS.SET_SUBSCRIPTION_NOTIFICATION,
data: {
subscription: Subscription,
uri: string,
type: NotificationType,
},
};
type setSubscriptionNotifications = {
type: ACTIONS.SET_SUBSCRIPTION_NOTIFICATIONS,
data: {
notifications: SubscriptionNotifications,
},
};
type CheckSubscriptionStarted = {
type: ACTIONS.CHECK_SUBSCRIPTION_STARTED,
};
type CheckSubscriptionCompleted = {
type: ACTIONS.CHECK_SUBSCRIPTION_COMPLETED,
};
type fetchedSubscriptionsSucess = {
type: ACTIONS.FETCH_SUBSCRIPTIONS_SUCCESS,
data: Array<Subscription>,
};
export type Action =
| doChannelSubscribe
| doChannelUnsubscribe
| setSubscriptionLatest
| setSubscriptionNotification
| CheckSubscriptionStarted
| CheckSubscriptionCompleted
| Function;
export type Dispatch = ReduxDispatch<Action>;
const defaultState = {
const defaultState: SubscriptionState = {
subscriptions: [],
notifications: {},
unread: {},
loading: false,
viewMode: VIEW_ALL,
};
export default handleActions(
{
[ACTIONS.CHANNEL_SUBSCRIBE]: (
state: SubscriptionState,
action: doChannelSubscribe
action: DoChannelSubscribe
): SubscriptionState => {
const newSubscription: Subscription = action.data;
const newSubscriptions: Array<Subscription> = state.subscriptions.slice();
@ -105,7 +38,7 @@ export default handleActions(
},
[ACTIONS.CHANNEL_UNSUBSCRIBE]: (
state: SubscriptionState,
action: doChannelUnsubscribe
action: DoChannelUnsubscribe
): SubscriptionState => {
const subscriptionToRemove: Subscription = action.data;
@ -120,7 +53,7 @@ export default handleActions(
},
[ACTIONS.SET_SUBSCRIPTION_LATEST]: (
state: SubscriptionState,
action: setSubscriptionLatest
action: SetSubscriptionLatest
): SubscriptionState => ({
...state,
subscriptions: state.subscriptions.map(
@ -130,23 +63,43 @@ export default handleActions(
: subscription
),
}),
[ACTIONS.SET_SUBSCRIPTION_NOTIFICATION]: (
[ACTIONS.UPDATE_SUBSCRIPTION_UNREADS]: (
state: SubscriptionState,
action: setSubscriptionNotification
): SubscriptionState => ({
...state,
notifications: {
...state.notifications,
[action.data.uri]: { subscription: action.data.subscription, type: action.data.type },
},
}),
[ACTIONS.SET_SUBSCRIPTION_NOTIFICATIONS]: (
action: DoUpdateSubscriptionUnreads
): SubscriptionState => {
const { channel, uris, type } = action.data;
return {
...state,
unread: {
...state.unread,
[channel]: {
uris,
type,
},
},
};
},
[ACTIONS.REMOVE_SUBSCRIPTION_UNREADS]: (
state: SubscriptionState,
action: setSubscriptionNotifications
): SubscriptionState => ({
...state,
notifications: action.data.notifications,
}),
action: DoRemoveSubscriptionUnreads
): SubscriptionState => {
const { channel, uris } = action.data;
const newUnread = { ...state.unread };
if (!uris) {
delete newUnread[channel];
} else {
newUnread[channel].uris = uris;
}
return {
...state,
unread: {
...newUnread,
},
};
},
[ACTIONS.FETCH_SUBSCRIPTIONS_START]: (state: SubscriptionState): SubscriptionState => ({
...state,
loading: true,
@ -157,12 +110,19 @@ export default handleActions(
}),
[ACTIONS.FETCH_SUBSCRIPTIONS_SUCCESS]: (
state: SubscriptionState,
action: fetchedSubscriptionsSucess
action: FetchedSubscriptionsSucess
): SubscriptionState => ({
...state,
loading: false,
subscriptions: action.data,
}),
[ACTIONS.SET_VIEW_MODE]: (
state: SubscriptionState,
action: SetViewMode
): SubscriptionState => ({
...state,
viewMode: action.data,
}),
},
defaultState
);

View file

@ -1,5 +1,6 @@
import { createSelector } from 'reselect';
import { selectCurrentPage, selectHistoryStack } from 'lbry-redux';
import * as icons from 'constants/icons';
export const selectState = state => state.app || {};
@ -198,46 +199,46 @@ export const selectNavLinks = createSelector(
label: 'Explore',
path: '/discover',
active: currentPage === 'discover',
icon: 'Compass',
icon: icons.COMPASS,
},
{
label: 'Subscriptions',
path: '/subscriptions',
active: currentPage === 'subscriptions',
icon: 'AtSign',
icon: icons.HEART,
},
],
secondary: [
{
label: 'Wallet',
icon: 'CreditCard',
icon: icons.CREDIT_CARD,
subLinks: walletSubLinks,
path: isCurrentlyWalletPage ? '/wallet' : getActiveSublink('wallet'),
active: isWalletPage(currentPage),
},
{
label: 'My LBRY',
icon: 'Folder',
icon: icons.LOCAL,
subLinks: myLbrySubLinks,
path: isCurrentlyMyLbryPage ? '/downloaded' : getActiveSublink('myLbry'),
active: isMyLbryPage(currentPage),
},
{
label: 'Publish',
icon: 'UploadCloud',
icon: icons.UPLOAD,
path: '/publish',
active: currentPage === 'publish',
},
{
label: 'Settings',
icon: 'Settings',
icon: icons.SETTINGS,
path: '/settings',
active: currentPage === 'settings',
},
{
label: 'Help',
path: '/help',
icon: 'HelpCircle',
icon: icons.HELP,
active: currentPage === 'help',
},
],

View file

@ -3,24 +3,127 @@ import {
selectAllClaimsByChannel,
selectClaimsById,
selectAllFetchingChannelClaims,
makeSelectClaimForUri,
makeSelectChannelForClaimUri,
selectClaimsByUri,
parseURI,
} from 'lbry-redux';
// get the entire subscriptions state
// Returns the entire subscriptions state
const selectState = state => state.subscriptions || {};
export const selectIsFetchingSubscriptions = createSelector(selectState, state => state.loading);
export const selectNotifications = createSelector(selectState, state => state.notifications);
// list of saved channel names and uris
// Returns the list of channel uris a user is subscribed to
export const selectSubscriptions = createSelector(selectState, state => state.subscriptions);
// Fetching list of users subscriptions
export const selectIsFetchingSubscriptions = createSelector(selectState, state => state.loading);
// The current view mode on the subscriptions page
export const selectViewMode = createSelector(selectState, state => state.viewMode);
// Fetching any claims that are a part of a users subscriptions
export const selectSubscriptionsBeingFetched = createSelector(
selectSubscriptions,
selectAllFetchingChannelClaims,
(subscriptions, fetchingChannelClaims) => {
const fetchingSubscriptionMap = {};
subscriptions.forEach(sub => {
const isFetching = fetchingChannelClaims && fetchingChannelClaims[sub.uri];
if (isFetching) {
fetchingSubscriptionMap[sub.uri] = true;
}
});
return fetchingSubscriptionMap;
}
);
export const selectUnreadByChannel = createSelector(selectState, state => state.unread);
// Returns the current total of unread subscriptions
export const selectUnreadAmount = createSelector(selectUnreadByChannel, unreadByChannel => {
const unreadChannels = Object.keys(unreadByChannel);
let badges = 0;
if (!unreadChannels.length) {
return badges;
}
unreadChannels.forEach(channel => {
badges += unreadByChannel[channel].uris.length;
});
return badges;
});
// Returns the uris with channels as an array with the channel with the newest content first
// If you just want the `unread` state, use selectUnread
export const selectUnreadSubscriptions = createSelector(
selectUnreadAmount,
selectUnreadByChannel,
selectClaimsByUri,
(unreadAmount, unreadByChannel, claimsByUri) => {
// determine which channel has the newest content
const unreadList = [];
if (!unreadAmount) {
return unreadList;
}
const channelUriList = Object.keys(unreadByChannel);
// There is only one channel with unread notifications
if (unreadAmount === 1) {
channelUriList.forEach(channel => {
const unreadChannel = {
channel,
uris: unreadByChannel[channel].uris,
};
unreadList.push(unreadChannel);
});
return unreadList;
}
channelUriList
.sort((channel1, channel2) => {
const latestUriFromChannel1 = unreadByChannel[channel1].uris[0];
const latestClaimFromChannel1 = claimsByUri[latestUriFromChannel1] || {};
const latestUriFromChannel2 = unreadByChannel[channel2].uris[0];
const latestClaimFromChannel2 = claimsByUri[latestUriFromChannel2] || {};
const latestHeightFromChannel1 = latestClaimFromChannel1.height || 0;
const latestHeightFromChannel2 = latestClaimFromChannel2.height || 0;
if (latestHeightFromChannel1 !== latestHeightFromChannel2) {
return latestHeightFromChannel2 - latestHeightFromChannel1;
}
return 0;
})
.forEach(channel => {
const unreadSubscription = unreadByChannel[channel];
const unreadChannel = {
channel,
uris: unreadSubscription.uris,
};
unreadList.push(unreadChannel);
});
return unreadList;
}
);
// Returns all unread subscriptions for a uri passed in
export const makeSelectUnreadByChannel = uri =>
createSelector(selectUnreadByChannel, unread => unread[uri]);
// Returns the first page of claims for every channel a user is subscribed to
export const selectSubscriptionClaims = createSelector(
selectAllClaimsByChannel,
selectClaimsById,
selectSubscriptions,
(channelIds, allClaims, savedSubscriptions) => {
selectUnreadByChannel,
(channelIds, allClaims, savedSubscriptions, unreadByChannel) => {
// no claims loaded yet
if (!Object.keys(channelIds).length) {
return [];
@ -34,51 +137,51 @@ export const selectSubscriptionClaims = createSelector(
// if subscribed channel has content
if (channelIds[subscription.uri] && channelIds[subscription.uri]['1']) {
// This will need to be more robust, we will want to be able to load more than the first page
// Strip out any ids that will be shown as notifications
const pageOneChannelIds = channelIds[subscription.uri]['1'];
// we have the channel ids and the corresponding claims
// loop over the list of ids and grab the claim
pageOneChannelIds.forEach(id => {
const grabbedClaim = allClaims[id];
if (
unreadByChannel[subscription.uri] &&
unreadByChannel[subscription.uri].uris.some(uri => uri.includes(id))
) {
grabbedClaim.isNew = true;
}
channelClaims = channelClaims.concat([grabbedClaim]);
});
}
fetchedSubscriptions = fetchedSubscriptions.concat([
{
claims: [...channelClaims],
channelName: subscription.channelName,
uri: subscription.uri,
},
]);
fetchedSubscriptions = fetchedSubscriptions.concat(channelClaims);
});
return [...fetchedSubscriptions];
}
);
export const selectSubscriptionsBeingFetched = createSelector(
selectSubscriptions,
selectAllFetchingChannelClaims,
(subscriptions, fetchingChannelClaims) => {
const fetchingSubscriptionMap = {};
subscriptions.forEach(sub => {
const isFetching = fetchingChannelClaims && fetchingChannelClaims[sub.uri];
if (isFetching) {
fetchingSubscriptionMap[sub.uri] = 1;
}
});
return fetchingSubscriptionMap;
return fetchedSubscriptions;
}
);
// Returns true if a user is subscribed to the channel associated with the uri passed in
// Accepts content or channel uris
export const makeSelectIsSubscribed = uri =>
createSelector(selectSubscriptions, makeSelectClaimForUri(uri), (subscriptions, claim) => {
if (!claim || !claim.channel_name) {
createSelector(
selectSubscriptions,
makeSelectChannelForClaimUri(uri, true),
(subscriptions, channelUri) => {
if (channelUri) {
return subscriptions.some(sub => sub.uri === channelUri);
}
// If we couldn't get a channel uri from the claim uri, the uri passed in might be a channel already
const { isChannel } = parseURI(uri);
if (isChannel) {
const uriWithPrefix = uri.startsWith('lbry://') ? uri : `lbry://${uri}`;
return subscriptions.some(sub => sub.uri === uriWithPrefix);
}
return false;
}
const channelUri = `${claim.channel_name}#${claim.value.publisherSignature.certificateId}`;
return subscriptions.some(sub => sub.uri === channelUri);
});
);

View file

@ -233,32 +233,8 @@ p:not(:first-of-type) {
font-weight: 600;
}
.credit-amount--file-page {
border-radius: 5px;
font-weight: 700;
padding: 5px;
}
.credit-amount--free {
&:not(.credit-amount--file-page) {
color: $lbry-blue-5;
}
&.credit-amount--file-page {
background-color: $lbry-blue-2;
color: $lbry-blue-5;
}
}
.credit-amount--cost {
&:not(.credit-amount--file-page) {
color: $lbry-gray-5;
}
&.credit-amount--file-page {
background-color: $lbry-yellow-3;
color: $lbry-black;
}
color: $lbry-gray-5;
}
.credit-amount--inherit {

View file

@ -1,5 +1,3 @@
@charset "utf-8";
@import '~@lbry/color/lbry-color', 'reset', 'type', 'vars', 'gui', 'component/syntax-highlighter',
'component/table', 'component/button', 'component/card', 'component/file-download',
'component/form-field', 'component/header', 'component/menu', 'component/tooltip',
@ -8,4 +6,4 @@
'component/markdown-editor', 'component/scrollbar', 'component/spinner', 'component/nav',
'component/file-list', 'component/file-render', 'component/search', 'component/toggle',
'component/search', 'component/dat-gui', 'component/item-list', 'component/time', 'component/icon',
'component/placeholder', 'themes/dark';
'component/placeholder', 'component/badge', 'themes/dark';

View file

@ -0,0 +1,21 @@
.badge {
border-radius: 5px;
padding: 5px;
font-weight: 800;
font-size: 0.8em;
}
.badge--alert {
background-color: #e45454;
color: white;
}
.badge--free {
background-color: $lbry-blue-2;
color: $lbry-blue-5;
}
.badge--cost {
background-color: $lbry-yellow-3;
color: $lbry-black;
}

View file

@ -57,6 +57,23 @@
background-color: $lbry-red-3;
}
&.btn--link {
padding: 0;
margin: 0;
background-color: inherit;
color: $lbry-teal-4;
font-size: 1em;
border-radius: 0;
display: inline;
min-width: 0;
box-shadow: none;
text-align: left;
&:disabled {
color: $lbry-gray-5;
}
}
&.btn--disabled:disabled {
// wtf?
cursor: default;

View file

@ -5,6 +5,11 @@
margin-top: $spacing-vertical * 2/3;
}
.file-list__sort {
display: flex;
justify-content: flex-end;
}
.file-list__header {
font-size: 24px;
padding-top: $spacing-vertical * 4/3;
@ -14,12 +19,6 @@
}
}
.file-list__sort {
display: flex;
justify-content: flex-end;
padding-bottom: 20px;
}
.file-tile {
display: flex;
font-size: 14px;

View file

@ -101,7 +101,7 @@ const compressor = createCompressor();
// We were caching so much data the app was locking up
// We can't add this back until we can perform this in a non-blocking way
// const saveClaimsFilter = createFilter('claims', ['byId', 'claimsByUri']);
const subscriptionsFilter = createFilter('subscriptions', ['subscriptions']);
const subscriptionsFilter = createFilter('subscriptions', ['subscriptions', 'unread', 'viewMode']);
const contentFilter = createFilter('content', ['positions', 'history']);
// We only need to persist the receiveAddress for the wallet

View file

@ -6,6 +6,7 @@ export type FileInfo = {
channelName: ?string,
pending?: boolean,
channel_claim_id: string,
file_name: string,
value?: {
publisherSignature: {
certificateId: string,

View file

@ -2,5 +2,5 @@
// eslint-disable-next-line no-use-before-define
export type Dispatch<T> = (action: T | Promise<T> | Array<T> | ThunkAction<T>) => any; // Need to refer to ThunkAction
export type GetState = () => {};
export type GetState = () => any;
export type ThunkAction<T> = (dispatch: Dispatch<T>, getState: GetState) => any;

View file

@ -1,7 +1,108 @@
// @flow
import type { Dispatch as ReduxDispatch } from 'types/redux';
import * as ACTIONS from 'constants/action_types';
import {
DOWNLOADED,
DOWNLOADING,
NOTIFY_ONLY,
VIEW_ALL,
VIEW_LATEST_FIRST,
} from 'constants/subscriptions';
export type Subscription = {
channelName: string, // @CryptoCandor,
uri: string, // lbry://@CryptoCandor#9152f3b054f692076a6882d1b58a30e8781cc8e6
latest: string, // substratum#b0ab143243020e7831fd070d9f871e1fda948620
latest?: string, // substratum#b0ab143243020e7831fd070d9f871e1fda948620
};
// Tracking for new content
// i.e. If a subscription has a DOWNLOADING type, we will trigger an OS notification
// to tell users there is new content from their subscriptions
export type SubscriptionNotificationType = DOWNLOADED | DOWNLOADING | NOTIFY_ONLY;
export type UnreadSubscription = {
type: SubscriptionNotificationType,
uris: Array<string>,
};
export type UnreadSubscriptions = {
[string]: UnreadSubscription,
};
export type ViewMode = VIEW_LATEST_FIRST | VIEW_ALL;
export type SubscriptionState = {
subscriptions: Array<Subscription>,
unread: UnreadSubscriptions,
loading: boolean,
viewMode: ViewMode,
};
//
// Action types
//
export type DoChannelSubscribe = {
type: ACTIONS.CHANNEL_SUBSCRIBE,
data: Subscription,
};
export type DoChannelUnsubscribe = {
type: ACTIONS.CHANNEL_UNSUBSCRIBE,
data: Subscription,
};
export type DoUpdateSubscriptionUnreads = {
type: ACTIONS.UPDATE_SUBSCRIPTION_UNREADS,
data: {
channel: string,
uris: Array<string>,
type?: SubscriptionNotificationType,
},
};
export type DoRemoveSubscriptionUnreads = {
type: ACTIONS.REMOVE_SUBSCRIPTION_UNREADS,
data: {
channel: string,
uris: Array<string>,
},
};
export type SetSubscriptionLatest = {
type: ACTIONS.SET_SUBSCRIPTION_LATEST,
data: {
subscription: Subscription,
uri: string,
},
};
export type CheckSubscriptionStarted = {
type: ACTIONS.CHECK_SUBSCRIPTION_STARTED,
};
export type CheckSubscriptionCompleted = {
type: ACTIONS.CHECK_SUBSCRIPTION_COMPLETED,
};
export type FetchedSubscriptionsSucess = {
type: ACTIONS.FETCH_SUBSCRIPTIONS_SUCCESS,
data: Array<Subscription>,
};
export type SetViewMode = {
type: ACTIONS.SET_VIEW_MODE,
data: ViewMode,
};
export type Action =
| DoChannelSubscribe
| DoChannelUnsubscribe
| DoUpdateSubscriptionUnreads
| DoRemoveSubscriptionUnreads
| SetSubscriptionLatest
| CheckSubscriptionStarted
| CheckSubscriptionCompleted
| SetViewMode
| Function;
export type Dispatch = ReduxDispatch<Action>;

View file

@ -13,6 +13,7 @@ const isDev = PROCESS_ARGV && PROCESS_ARGV.original &&
module.exports = {
// This rule is temporarily necessary until https://github.com/electron-userland/electron-webpack/issues/60 is fixed.
entry: ['babel-polyfill', `${ELECTRON_RENDERER_PROCESS_ROOT}/index.js`],
module: {
rules: [
{

View file

@ -1051,7 +1051,7 @@ babel-plugin-transform-strict-mode@^6.24.1:
babel-runtime "^6.22.0"
babel-types "^6.24.1"
babel-polyfill@^6.20.0, babel-polyfill@^6.26.0:
babel-polyfill@^6.26.0:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-polyfill/-/babel-polyfill-6.26.0.tgz#379937abc67d7895970adc621f284cd966cf2153"
dependencies:
@ -5670,16 +5670,16 @@ lbry-redux@lbryio/lbry-redux:
proxy-polyfill "0.1.6"
reselect "^3.0.0"
lbry-redux@lbryio/lbry-redux#67cae46983d9fea90dd1e4c5bd121dd5077a3f0e:
lbry-redux@lbryio/lbry-redux#957d221c1830ecbb7a9e74fad78e711fb14539f4:
version "0.0.1"
resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/67cae46983d9fea90dd1e4c5bd121dd5077a3f0e"
resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/957d221c1830ecbb7a9e74fad78e711fb14539f4"
dependencies:
proxy-polyfill "0.1.6"
reselect "^3.0.0"
lbryinc@lbryio/lbryinc#de7ff055605b02a24821f0f9bab1d206eb7f235d:
lbryinc@lbryio/lbryinc#3f34af546ee73ff2ee7d8ad05e540b3b0aa658fb:
version "0.0.1"
resolved "https://codeload.github.com/lbryio/lbryinc/tar.gz/de7ff055605b02a24821f0f9bab1d206eb7f235d"
resolved "https://codeload.github.com/lbryio/lbryinc/tar.gz/3f34af546ee73ff2ee7d8ad05e540b3b0aa658fb"
dependencies:
lbry-redux lbryio/lbry-redux
reselect "^3.0.0"