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)) * 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)) * 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)) * 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 ### Changed
* Make tooltip smarter ([#1979](https://github.com/lbryio/lbry-desktop/pull/1979)) * 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", "formik": "^0.10.4",
"hast-util-sanitize": "^1.1.2", "hast-util-sanitize": "^1.1.2",
"keytar": "^4.2.1", "keytar": "^4.2.1",
"lbry-redux": "lbryio/lbry-redux#67cae46983d9fea90dd1e4c5bd121dd5077a3f0e", "lbry-redux": "lbryio/lbry-redux#957d221c1830ecbb7a9e74fad78e711fb14539f4",
"lbryinc": "lbryio/lbryinc#de7ff055605b02a24821f0f9bab1d206eb7f235d", "lbryinc": "lbryio/lbryinc#3f34af546ee73ff2ee7d8ad05e540b3b0aa658fb",
"localforage": "^1.7.1", "localforage": "^1.7.1",
"mammoth": "^1.4.6", "mammoth": "^1.4.6",
"mime": "^2.3.1", "mime": "^2.3.1",
@ -91,7 +91,7 @@
"axios": "^0.18.0", "axios": "^0.18.0",
"babel-eslint": "^8.2.2", "babel-eslint": "^8.2.2",
"babel-plugin-module-resolver": "^3.1.1", "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-env": "^1.6.1",
"babel-preset-react": "^6.24.1", "babel-preset-react": "^6.24.1",
"babel-preset-stage-2": "^6.18.0", "babel-preset-stage-2": "^6.18.0",

View file

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

View file

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

View file

@ -27,9 +27,15 @@ type Props = {
isResolvingUri: boolean, isResolvingUri: boolean,
/* eslint-enable react/no-unused-prop-types */ /* eslint-enable react/no-unused-prop-types */
isSubscribed: boolean, isSubscribed: boolean,
showSubscribedLogo: boolean,
isNew: boolean,
}; };
class FileCard extends React.PureComponent<Props> { class FileCard extends React.PureComponent<Props> {
static defaultProps = {
showSubscribedLogo: false,
};
componentWillMount() { componentWillMount() {
this.resolve(this.props); this.resolve(this.props);
} }
@ -57,6 +63,8 @@ class FileCard extends React.PureComponent<Props> {
claimIsMine, claimIsMine,
pending, pending,
isSubscribed, isSubscribed,
isNew,
showSubscribedLogo,
} = this.props; } = this.props;
if (!claim && !pending) { if (!claim && !pending) {
@ -112,10 +120,15 @@ class FileCard extends React.PureComponent<Props> {
<div className="card__file-properties"> <div className="card__file-properties">
<FilePrice hideFree uri={uri} /> <FilePrice hideFree uri={uri} />
{isRewardContent && <Icon iconColor="red" icon={icons.FEATURED} />} {isRewardContent && <Icon iconColor="red" icon={icons.FEATURED} />}
{isSubscribed && <Icon icon={icons.HEART} />} {showSubscribedLogo && isSubscribed && <Icon icon={icons.HEART} />}
{fileInfo && <Icon icon={icons.LOCAL} />} {fileInfo && <Icon icon={icons.LOCAL} />}
</div> </div>
</div> </div>
{isNew && (
<div className="card__subtitle">
<span className="badge badge--alert">{__('NEW')}</span>
</div>
)}
</section> </section>
); );
/* eslint-enable jsx-a11y/click-events-have-key-events */ /* 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, claim_id: claimId,
txid, txid,
nout, nout,
isNew,
} = fileInfo; } = fileInfo;
const uriParams = {}; const uriParams = {};
@ -159,13 +160,13 @@ class FileList extends React.PureComponent<Props, State> {
const outpoint = `${txid}:${nout}`; const outpoint = `${txid}:${nout}`;
// See https://github.com/lbryio/lbry-desktop/issues/1327 for discussion around using outpoint as the key // 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 ( return (
<section> <section>
<div className="file-list__sort"> {!hideFilter && (
{!hideFilter && ( <div className="file-list__sort">
<FormField <FormField
prefix={__('Sort by')} prefix={__('Sort by')}
affixClass="form-field--align-center" affixClass="form-field--align-center"
@ -177,9 +178,9 @@ class FileList extends React.PureComponent<Props, State> {
<option value="dateOld">{__('Oldest First')}</option> <option value="dateOld">{__('Oldest First')}</option>
<option value="title">{__('Title')}</option> <option value="title">{__('Title')}</option>
</FormField> </FormField>
)} </div>
</div> )}
<div className="card__list">{content}</div> <div className="card__list card__content">{content}</div>
</section> </section>
); );
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -161,7 +161,9 @@ class UserHistoryPage extends React.PureComponent<Props, State> {
</React.Fragment> </React.Fragment>
) : ( ) : (
<div className="page__empty"> <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"> <div className="card__actions card__actions--center">
<Button button="primary" navigate="/discover" label={__('Explore new content')} /> <Button button="primary" navigate="/discover" label={__('Explore new content')} />
</div> </div>

View file

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

View file

@ -32,3 +32,5 @@ export const EYE = 'Eye';
export const PLAY = 'Play'; export const PLAY = 'Play';
export const FACEBOOK = 'Facebook'; export const FACEBOOK = 'Facebook';
export const TWITTER = 'Twitter'; 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 DOWNLOADING = 'DOWNLOADING';
export const DOWNLOADED = 'DOWNLOADED'; export const DOWNLOADED = 'DOWNLOADED';
export const NOTIFY_ONLY = 'NOTIFY_ONLY;'; export const NOTIFY_ONLY = 'NOTIFY_ONLY;';

View file

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

View file

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

View file

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

View file

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

View file

@ -21,7 +21,7 @@ class FileListDownloaded extends React.PureComponent<Props> {
<FileList fileInfos={fileInfos} /> <FileList fileInfos={fileInfos} />
) : ( ) : (
<div className="page__empty"> <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"> <div className="card__actions card__actions--center">
<Button <Button
button="primary" button="primary"

View file

@ -29,7 +29,9 @@ class FileListPublished extends React.PureComponent<Props> {
<FileList checkPending fileInfos={claims} sortByHeight /> <FileList checkPending fileInfos={claims} sortByHeight />
) : ( ) : (
<div className="page__empty"> <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"> <div className="card__actions card__actions--center">
<Button <Button
button="primary" button="primary"

View file

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

View file

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

View file

@ -1,83 +1,153 @@
// @flow // @flow
import React from 'react'; import type { ViewMode } from 'types/subscription';
import Page from 'component/page'; import type { Claim } from 'types/claim';
import { VIEW_ALL, VIEW_LATEST_FIRST } from 'constants/subscriptions';
import * as settings from 'constants/settings'; import * as settings from 'constants/settings';
import type { Subscription } from 'types/subscription'; import * as React from 'react';
import * as NOTIFICATION_TYPES from 'constants/notification_types'; import Page from 'component/page';
import Button from 'component/button'; import Button from 'component/button';
import FileList from 'component/fileList'; import FileList from 'component/fileList';
import type { Claim } from 'types/claim';
import HiddenNsfwClaims from 'component/hiddenNsfwClaims'; 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 = { type Props = {
doFetchMySubscriptions: () => void, subscribedChannels: Array<string>, // The channels a user is subscribed to
setSubscriptionNotifications: ({}) => void, unreadSubscriptions: Array<{
subscriptions: Array<Subscription>, channel: string,
subscriptionClaims: Array<{ uri: string, claims: Array<Claim> }>, uris: Array<string>,
notifications: {}, }>,
allSubscriptions: Array<{ uri: string, ...Claim }>,
loading: boolean, loading: boolean,
autoDownload: boolean, autoDownload: boolean,
viewMode: ViewMode,
doSetViewMode: ViewMode => void,
doFetchMySubscriptions: () => void,
doSetClientSetting: (string, boolean) => void, doSetClientSetting: (string, boolean) => void,
}; };
export default class extends React.PureComponent<Props> { export default class extends React.PureComponent<Props> {
constructor() { constructor() {
super(); super();
(this: any).onAutoDownloadChange = this.onAutoDownloadChange.bind(this); (this: any).onAutoDownloadChange = this.onAutoDownloadChange.bind(this);
} }
componentDidMount() { componentDidMount() {
const { notifications, setSubscriptionNotifications, doFetchMySubscriptions } = this.props; const { doFetchMySubscriptions } = this.props;
doFetchMySubscriptions(); 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<*>) { onAutoDownloadChange(event: SyntheticInputEvent<*>) {
this.props.doSetClientSetting(settings.AUTO_DOWNLOAD, event.target.checked); this.props.doSetClientSetting(settings.AUTO_DOWNLOAD, event.target.checked);
} }
render() { renderSubscriptions() {
const { subscriptions, subscriptionClaims, loading, autoDownload } = this.props; const { viewMode, unreadSubscriptions, allSubscriptions } = this.props;
let claimList = [];
subscriptionClaims.forEach(claimData => {
claimList = claimList.concat(claimData.claims);
});
const subscriptionUris = claimList.map(claim => `lbry://${claim.name}#${claim.claim_id}`);
if (viewMode === VIEW_ALL) {
return (
<React.Fragment>
<div className="card__title">{__('Your subscriptions')}</div>
<FileList hideFilter sortByHeight fileInfos={allSubscriptions} />
</React.Fragment>
);
}
return ( return (
<Page notContained loading={loading}> <React.Fragment>
<HiddenNsfwClaims uris={subscriptionUris} /> {unreadSubscriptions.length ? (
<FormRow alignRight> unreadSubscriptions.map(({ channel, uris }) => {
<FormField const { claimName } = parseURI(channel);
type="checkbox" return (
name="auto_download" <section key={channel}>
onChange={this.onAutoDownloadChange} <div className="card__title">
checked={autoDownload} <Button
prefix={__('Automatically download new content from your subscriptions')} button="link"
/> navigate="/show"
</FormRow> navigateParams={{ uri: channel }}
{!subscriptions.length && ( label={claimName}
/>
</div>
<div className="card__list card__content">
{uris.map(uri => <FileCard isNew key={uri} uri={uri} />)}
</div>
</section>
);
})
) : (
<div className="page__empty"> <div className="page__empty">
{__("It looks like you aren't subscribed to any channels yet.")} <h3 className="card__title">{__('You are all caught up!')}</h3>
<div className="card__actions card__actions--center"> <div className="card__actions">
<Button button="primary" navigate="/discover" label={__('Explore new content')} /> <Button button="primary" navigate="/discover" label={__('Explore new content')} />
</div> </div>
</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> </Page>
); );
} }

View file

@ -1,10 +1,10 @@
// @flow // @flow
import * as NOTIFICATION_TYPES from 'constants/notification_types'; import * as NOTIFICATION_TYPES from 'constants/subscriptions';
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
import { doAlertError } from 'redux/actions/app'; import { doAlertError } from 'redux/actions/app';
import { doNavigate } from 'redux/actions/navigation'; import { doNavigate } from 'redux/actions/navigation';
import { setSubscriptionLatest, setSubscriptionNotification } from 'redux/actions/subscriptions'; import { setSubscriptionLatest, doUpdateUnreadSubscriptions } from 'redux/actions/subscriptions';
import { selectNotifications } from 'redux/selectors/subscriptions'; import { makeSelectUnreadByChannel } from 'redux/selectors/subscriptions';
import { selectBadgeNumber } from 'redux/selectors/app'; import { selectBadgeNumber } from 'redux/selectors/app';
import { import {
ACTIONS, ACTIONS,
@ -21,6 +21,8 @@ import {
selectBalance, selectBalance,
MODALS, MODALS,
doNotify, doNotify,
makeSelectChannelForClaimUri,
parseURI,
} from 'lbry-redux'; } from 'lbry-redux';
import { makeSelectClientSetting, selectosNotificationsEnabled } from 'redux/selectors/settings'; import { makeSelectClientSetting, selectosNotificationsEnabled } from 'redux/selectors/settings';
import setBadge from 'util/setBadge'; import setBadge from 'util/setBadge';
@ -66,19 +68,15 @@ export function doUpdateLoadStatus(uri: string, outpoint: string) {
const totalProgress = selectTotalDownloadProgress(state); const totalProgress = selectTotalDownloadProgress(state);
setProgressBar(totalProgress); setProgressBar(totalProgress);
const notifications = selectNotifications(state); const channelUri = makeSelectChannelForClaimUri(uri, true)(state);
if (notifications[uri] && notifications[uri].type === NOTIFICATION_TYPES.DOWNLOADING) { const { claimName: channelName } = parseURI(channelUri);
const count = Object.keys(notifications).reduce(
(acc, cur) => const unreadForChannel = makeSelectUnreadByChannel(channelUri)(state);
notifications[cur].subscription.channelName === if (unreadForChannel.type === NOTIFICATION_TYPES.DOWNLOADING) {
notifications[uri].subscription.channelName const count = unreadForChannel.uris.length;
? acc + 1
: acc,
0
);
if (selectosNotificationsEnabled(state)) { if (selectosNotificationsEnabled(state)) {
const notif = new window.Notification(notifications[uri].subscription.channelName, { const notif = new window.Notification(channelName, {
body: `Posted ${fileInfo.metadata.title}${ body: `Posted ${fileInfo.metadata.title}${
count > 1 && count < 10 ? ` and ${count - 1} other new items` : '' count > 1 && count < 10 ? ` and ${count - 1} other new items` : ''
}${count > 9 ? ' and 9+ 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( dispatch(doUpdateUnreadSubscriptions(channelUri, null, NOTIFICATION_TYPES.DOWNLOADED));
setSubscriptionNotification(
notifications[uri].subscription,
uri,
NOTIFICATION_TYPES.DOWNLOADED
)
);
}
} else { } else {
// If notifications are disabled(false) just return // If notifications are disabled(false) just return
if (!selectosNotificationsEnabled(getState())) return; if (!selectosNotificationsEnabled(getState())) return;
const notif = new window.Notification('LBRY Download Complete', { const notif = new window.Notification('LBRY Download Complete', {
body: fileInfo.metadata.title, body: fileInfo.metadata.title,
silent: false, 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 => { return dispatch => {
dispatch({ dispatch({
type: ACTIONS.FETCH_CHANNEL_CLAIMS_STARTED, type: ACTIONS.FETCH_CHANNEL_CLAIMS_STARTED,
data: { uri, page }, 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 claimResult = result[uri] || {};
const { claims_in_channel: claimsInChannel, returned_page: returnedPage } = claimResult; 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) 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({ dispatch({

View file

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

View file

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

View file

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

View file

@ -3,24 +3,127 @@ import {
selectAllClaimsByChannel, selectAllClaimsByChannel,
selectClaimsById, selectClaimsById,
selectAllFetchingChannelClaims, selectAllFetchingChannelClaims,
makeSelectClaimForUri, makeSelectChannelForClaimUri,
selectClaimsByUri,
parseURI,
} from 'lbry-redux'; } from 'lbry-redux';
// get the entire subscriptions state // Returns the entire subscriptions state
const selectState = state => state.subscriptions || {}; const selectState = state => state.subscriptions || {};
export const selectIsFetchingSubscriptions = createSelector(selectState, state => state.loading); // Returns the list of channel uris a user is subscribed to
export const selectNotifications = createSelector(selectState, state => state.notifications);
// list of saved channel names and uris
export const selectSubscriptions = createSelector(selectState, state => state.subscriptions); 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( export const selectSubscriptionClaims = createSelector(
selectAllClaimsByChannel, selectAllClaimsByChannel,
selectClaimsById, selectClaimsById,
selectSubscriptions, selectSubscriptions,
(channelIds, allClaims, savedSubscriptions) => { selectUnreadByChannel,
(channelIds, allClaims, savedSubscriptions, unreadByChannel) => {
// no claims loaded yet // no claims loaded yet
if (!Object.keys(channelIds).length) { if (!Object.keys(channelIds).length) {
return []; return [];
@ -34,51 +137,51 @@ export const selectSubscriptionClaims = createSelector(
// if subscribed channel has content // if subscribed channel has content
if (channelIds[subscription.uri] && channelIds[subscription.uri]['1']) { 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 // 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']; const pageOneChannelIds = channelIds[subscription.uri]['1'];
// we have the channel ids and the corresponding claims // we have the channel ids and the corresponding claims
// loop over the list of ids and grab the claim // loop over the list of ids and grab the claim
pageOneChannelIds.forEach(id => { pageOneChannelIds.forEach(id => {
const grabbedClaim = allClaims[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]); channelClaims = channelClaims.concat([grabbedClaim]);
}); });
} }
fetchedSubscriptions = fetchedSubscriptions.concat([ fetchedSubscriptions = fetchedSubscriptions.concat(channelClaims);
{
claims: [...channelClaims],
channelName: subscription.channelName,
uri: subscription.uri,
},
]);
}); });
return [...fetchedSubscriptions]; 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;
} }
); );
// 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 => export const makeSelectIsSubscribed = uri =>
createSelector(selectSubscriptions, makeSelectClaimForUri(uri), (subscriptions, claim) => { createSelector(
if (!claim || !claim.channel_name) { 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; 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; 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 { .credit-amount--cost {
&:not(.credit-amount--file-page) { color: $lbry-gray-5;
color: $lbry-gray-5;
}
&.credit-amount--file-page {
background-color: $lbry-yellow-3;
color: $lbry-black;
}
} }
.credit-amount--inherit { .credit-amount--inherit {

View file

@ -1,5 +1,3 @@
@charset "utf-8";
@import '~@lbry/color/lbry-color', 'reset', 'type', 'vars', 'gui', 'component/syntax-highlighter', @import '~@lbry/color/lbry-color', 'reset', 'type', 'vars', 'gui', 'component/syntax-highlighter',
'component/table', 'component/button', 'component/card', 'component/file-download', 'component/table', 'component/button', 'component/card', 'component/file-download',
'component/form-field', 'component/header', 'component/menu', 'component/tooltip', 'component/form-field', 'component/header', 'component/menu', 'component/tooltip',
@ -8,4 +6,4 @@
'component/markdown-editor', 'component/scrollbar', 'component/spinner', 'component/nav', 'component/markdown-editor', 'component/scrollbar', 'component/spinner', 'component/nav',
'component/file-list', 'component/file-render', 'component/search', 'component/toggle', 'component/file-list', 'component/file-render', 'component/search', 'component/toggle',
'component/search', 'component/dat-gui', 'component/item-list', 'component/time', 'component/icon', '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; 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 { &.btn--disabled:disabled {
// wtf? // wtf?
cursor: default; cursor: default;

View file

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

View file

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

View file

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

View file

@ -2,5 +2,5 @@
// eslint-disable-next-line no-use-before-define // 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 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; export type ThunkAction<T> = (dispatch: Dispatch<T>, getState: GetState) => any;

View file

@ -1,7 +1,108 @@
// @flow // @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 = { export type Subscription = {
channelName: string, // @CryptoCandor, channelName: string, // @CryptoCandor,
uri: string, // lbry://@CryptoCandor#9152f3b054f692076a6882d1b58a30e8781cc8e6 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 = { module.exports = {
// This rule is temporarily necessary until https://github.com/electron-userland/electron-webpack/issues/60 is fixed. // 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: { module: {
rules: [ rules: [
{ {

View file

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