Compare commits

..

83 commits

Author SHA1 Message Date
infiinte-persistence
0b4e41ef90
doFetchViewCount: support multiple claimIds
## Issue
Show content view counts on channel pages (https://github.com/lbryio/lbry-desktop/issues/3587)

## Changes
Updated the action and reducer to handle CSV input. This wouldn't affect existing clients as it is backward compatible with a single claimId.

In the unlikely event that requested count !== results count, we just fail silently for now.
2021-09-09 13:41:56 +08:00
mayeaux
6fa738e3b8 Update README.md 2021-06-14 11:12:17 -04:00
Thomas Zarebczan
8f9a58bfc8
Merge pull request #108 from lbryio/ip/blacklist-build
Update build for blacklist speed-up
2021-04-15 01:23:37 -04:00
infiinte-persistence
560b97e9a5
Update build for blacklist speed-up 2021-04-15 11:00:51 +08:00
Sean Yesmunt
c0cadee18d
Merge pull request #107 from lbryio/fix-blacklistMap
fix blacklist performance
2021-04-14 12:59:34 -04:00
zeppi
f5a0df82fc fix blacklist performance 2021-04-14 12:53:13 -04:00
Sean Yesmunt
7faea40d87 update build 2021-03-09 16:46:23 -05:00
Sean Yesmunt
767ffe90b0
Update youtube.js 2021-03-09 16:45:57 -05:00
Sean Yesmunt
eee2cb730e
Merge pull request #106 from lbryio/feat-authTakesLanguage
auth takes language for user new
2020-11-13 13:23:25 -05:00
jessop
57b8625454 auth takes language for user new 2020-11-12 14:27:58 -05:00
Sean Yesmunt
2a9d04b2ef omit auth_token for file/list_filtered and file/list_blocked 2020-11-11 15:13:02 -05:00
Sean Yesmunt
9371b3181f
Merge pull request #105 from lbryio/fix-domainAppid
pass domain into appid for user new
2020-11-10 10:23:47 -05:00
jessop
904e630d43 pass domain into appid for user new 2020-11-09 14:56:56 -05:00
Sean Yesmunt
801f159059 remove unused type 2020-11-09 13:29:05 -05:00
Sean Yesmunt
9bdf18eef6 remove extra type 2020-10-30 15:57:13 -04:00
Sean Yesmunt
d913ca0344 remove old subscription types 2020-10-30 15:53:57 -04:00
Sean Yesmunt
517c28a183 only call sync_apply if sync_get fails with 'no wallet found for this user' 2020-10-23 21:15:58 -04:00
Sean Yesmunt
d91b971bfe update build 2020-10-23 20:50:10 -04:00
Sean Yesmunt
1b6f280371 don't call user/new if user/me returns 500 2020-10-23 20:49:37 -04:00
Sean Yesmunt
316e42f7b7 Revert "remove sync code from 'lbryinc'"
This reverts commit 886f5f8f70.
2020-10-23 20:49:02 -04:00
Sean Yesmunt
886f5f8f70 remove sync code from 'lbryinc' 2020-10-01 12:25:36 -04:00
Sean Yesmunt
db0663fcc4
Merge pull request #104 from lbryio/sync-password-error
propagate sync password error
2020-09-21 12:57:48 -04:00
Sean Yesmunt
c78d416e16 Revert "remove app_id on user/new call"
This reverts commit 478b428a0f.
2020-09-21 12:57:03 -04:00
jessop
4834749423 propagate sync password error 2020-09-18 13:09:50 -04:00
Sean Yesmunt
478b428a0f remove app_id on user/new call 2020-09-16 13:13:36 -04:00
Sean Yesmunt
9440717a00
Merge pull request #103 from lbryio/doAuth-changes
doAuth changes
2020-09-09 10:39:38 -04:00
jessop
35df87d1e6 doAuth changes 2020-09-07 08:52:35 -04:00
Sean Yesmunt
c8f3fe0511 fix typo 2020-09-03 14:10:11 -04:00
Sean Yesmunt
1404901313 add youtube sync constants 2020-09-01 13:30:56 -04:00
Sean Yesmunt
b96561477a
Merge pull request #102 from lbryio/fix-filterMapSelectors
bugfix
2020-08-18 10:20:54 -04:00
jessop
6217edccf7 bugfix 2020-08-17 15:54:03 -04:00
Sean Yesmunt
c1a08e2e97
Merge pull request #101 from lbryio/feat-filteredSelectors
selectors for blocklist objects
2020-08-14 13:20:23 -04:00
jessop
63acb48182 selectors for blocklist objects 2020-08-13 13:53:49 -04:00
Sean Yesmunt
cff5dd6093 delete old flow type 2020-07-07 16:41:18 -04:00
Sean Yesmunt
9a9bc951db remove old flow type 2020-06-30 01:58:36 -04:00
Sean Yesmunt
0f6fd2c338
Merge pull request #100 from lbryio/remove-user-and-rewards
remove user/rewards code from lbryinc so they can be added to the desktop repo
2020-06-29 15:07:36 -04:00
Sean Yesmunt
72eee35f51 remove user/rewards code from lbryinc so they can be added to the desktop repo 2020-06-15 16:29:48 -04:00
jessopb
3ceb09549c
Merge pull request #99 from lbryio/install_id
customize web install_/app_ids for domains
2020-06-11 13:23:40 -04:00
jessop
41e94d4ce8 customize web app_ids for domains 2020-06-10 11:11:28 -04:00
jessopb
ca5984cd79
Merge pull request #98 from lbryio/feat-installNewDomain
add domain to install new
2020-06-05 10:11:28 -04:00
jessop
9c4f620ec0 add domain to install new 2020-06-03 20:13:32 -04:00
Sean Yesmunt
c21bc3075c fix build 2020-06-02 10:32:11 -04:00
Sean Yesmunt
cb8ffc86a4 decouple doUserFetch and doRewardList 2020-06-02 10:12:20 -04:00
Sean Yesmunt
13068eca5c
Merge pull request #97 from lbryio/country
add country to User and create doUserSetCountry
2020-06-01 13:03:52 -04:00
Sean Yesmunt
0529fb4635 add country to User and create doUserSetCountry 2020-06-01 12:27:07 -04:00
Sean Yesmunt
39510e8b21 remove address sort for youtube transfer so it doesn't accidentally grab the wrong address 2020-05-29 11:15:16 -04:00
Sean Yesmunt
9c2939ab37
Merge pull request #95 from lbryio/renameTvToWeb 2020-05-26 10:41:21 -04:00
jessop
8bdf1ac44d rename lbrytv to web 2020-05-23 13:56:29 -04:00
Sean Yesmunt
6a52f8026c
Merge pull request #96 from lbryio/reward-fix 2020-05-20 11:53:55 -04:00
Sean Yesmunt
132455395e fix for rewards 2020-05-20 11:52:59 -04:00
Sean Yesmunt
efde3e6914 fix typo 2020-05-20 11:12:46 -04:00
Sean Yesmunt
31c51b804f add paid_content reward type 2020-05-20 11:01:36 -04:00
Sean Yesmunt
cc62a4eec1 allow users to signin after they abandoned a previous signup 2020-04-23 14:14:43 -04:00
Sean Yesmunt
edd43c8dff add latest_claimed_email to User 2020-04-20 16:45:21 -04:00
Sean Yesmunt
43fadef68d
Merge pull request #94 from lbryio/signup
trigger signup email if user doesn't have a password
2020-04-16 10:54:03 -04:00
Sean Yesmunt
7b1973dbad trigger signup email if user doesn't have a password 2020-04-16 10:52:52 -04:00
Sean Yesmunt
11ebc51ab6 add promise to userSignUp 2020-04-15 12:07:43 -04:00
Sean Yesmunt
12aefaa143
Merge pull request #93 from lbryio/fixes
password sign in cleanup
2020-04-13 13:45:01 -04:00
Sean Yesmunt
deb0303f62 password sign in cleanup 2020-04-13 13:43:15 -04:00
Sean Yesmunt
75f992ef02
Merge pull request #91 from lbryio/passwords
password support
2020-04-13 11:06:43 -04:00
Sean Yesmunt
b411291da7 new signin/signup methods with password support 2020-04-13 09:40:25 -04:00
Sean Yesmunt
0addc624db
Merge pull request #92 from lbryio/window
check if window exists before using
2020-04-08 12:49:35 -04:00
Sean Yesmunt
546ecd1f77 check if window exists before using 2020-04-08 12:17:40 -04:00
Thomas Zarebczan
19260fac56
fix: claim code from params 2020-03-27 17:13:21 -04:00
Sean Yesmunt
e75f009af4
Merge pull request #90 from lbryio/add-claim-code
add claim code selector
2020-03-26 15:19:06 -04:00
Thomas Zarebczan
402a9a199f
add claim code selector
fix export
2020-03-26 14:50:21 -04:00
Akinwale Ariwodola
e2bba80797 new_android reward 2020-03-24 10:31:58 -04:00
Sean Yesmunt
9259233c6f
Merge pull request #88 from lbryio/changed-callback
include if sync data has changed in response callback
2020-03-19 13:50:37 -04:00
seanyesmunt
49eb8a4df3 include if sync data has changed in response callback 2020-03-19 13:49:47 -04:00
Akinwale Ariwodola
54906cb768
add flag for dispatching doInstallNew from doAuthenticate (#87)
* add doInstallNewWithParams action
* return dispatch for doInstallNewWithParams
2020-03-19 08:55:45 +01:00
Sean Yesmunt
f82ef0e5cc
Merge pull request #85 from lbryio/status-callback
add callback param for doAuthenticate that returns the status
2020-03-16 18:01:03 -04:00
seanyesmunt
a53cffb0d8 update variable name 2020-03-16 18:00:46 -04:00
Sean Yesmunt
b27a99aaa3 add callback param for doAuthenticate that returns the status 2020-03-12 12:19:42 -04:00
Sean Yesmunt
275f35b31e
Merge pull request #84 from lbryio/stop-install-new
allow preventing lbryinc calls on authenticate
2020-02-24 12:06:29 -05:00
Sean Yesmunt
3fc6530531 allow preventing lbryinc calls on authenticate 2020-02-24 12:02:43 -05:00
Jeremy Kauffman
0dc8829a31
Merge pull request #83 from ykris45/patch-1
Update LICENSE
2020-02-03 15:00:26 -05:00
YULIUS KURNIAWAN KRISTIANTO
7813853736
Update LICENSE 2020-02-03 04:38:56 +07:00
Sean Yesmunt
1932c30a36
trigger sync error correctly with locked wallets (#82)
trigger sync error correctly with locked wallets
2020-01-28 12:31:39 -05:00
Sean Yesmunt
6a59102c52 trigger sync error correctly with locked wallets 2020-01-23 17:11:07 -05:00
Thomas Zarebczan
138a053754 fix: referral name 2020-01-18 15:56:35 -05:00
Sean Yesmunt
bd48a138c9
improves ux around referrals (#81)
improves ux around referrals
2020-01-17 11:21:51 -05:00
jessop
1b19fdf56a improve referral ux
provide for resetting setReferrerErrors
fix bugs in referrer resolve logic
2020-01-16 20:59:47 -05:00
Jeremy Kauffman
866a80a552
Merge pull request #80 from lbryio/user-fix
add back user to state when calling doAuthenticate
2020-01-15 10:34:11 -05:00
36 changed files with 888 additions and 8956 deletions

View file

@ -1,6 +1,6 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2017-2019 LBRY Inc Copyright (c) 2017-2020 LBRY Inc
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish,

View file

@ -17,6 +17,9 @@ yarn link lbryinc
### Build ### Build
Run `$ yarn build`. If the symlink does not work, just build the file and move the `bundle.js` file in to the `node_modules/` folder. Run `$ yarn build`. If the symlink does not work, just build the file and move the `bundle.js` file in to the `node_modules/` folder.
### Automatic rebuild
To have the code automatically rebuild upon changes you can run `$ yarn dev` which will use `rollup` to watch the files and build upon detection of updated source code.
## License ## License
[MIT © LBRY](LICENSE) [MIT © LBRY](LICENSE)

2571
dist/bundle.es.js vendored

File diff suppressed because it is too large Load diff

3854
dist/bundle.js vendored

File diff suppressed because it is too large Load diff

View file

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

View file

@ -1,137 +0,0 @@
// @flow
import type { Dispatch } from 'flow-typed/Redux';
import * as ACTIONS from 'constants/action_types';
import {
DOWNLOADED,
DOWNLOADING,
NOTIFY_ONLY,
VIEW_ALL,
VIEW_LATEST_FIRST,
SUGGESTED_TOP_BID,
SUGGESTED_TOP_SUBSCRIBED,
SUGGESTED_FEATURED,
} from 'constants/subscriptions';
declare type Subscription = {
channelName: string, // @CryptoCandor,
uri: string, // lbry://@CryptoCandor#9152f3b054f692076a6882d1b58a30e8781cc8e6
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
declare type SubscriptionNotificationType = DOWNLOADED | DOWNLOADING | NOTIFY_ONLY;
declare type UnreadSubscription = {
type: SubscriptionNotificationType,
uris: Array<string>,
};
declare type UnreadSubscriptions = {
[string]: UnreadSubscription,
};
declare type ViewMode = VIEW_LATEST_FIRST | VIEW_ALL;
declare type SuggestedType = SUGGESTED_TOP_BID | SUGGESTED_TOP_SUBSCRIBED | SUGGESTED_FEATURED;
declare type SuggestedSubscriptions = {
[SuggestedType]: string,
};
declare type SubscriptionState = {
enabledChannelNotifications: Array<string>,
subscriptions: Array<Subscription>,
unread: UnreadSubscriptions,
loading: boolean,
viewMode: ViewMode,
suggested: SuggestedSubscriptions,
loadingSuggested: boolean,
firstRunCompleted: boolean,
showSuggestedSubs: boolean,
};
//
// Action types
//
declare type DoChannelSubscriptionEnableNotifications = {
type: ACTIONS.CHANNEL_SUBSCRIPTION_ENABLE_NOTIFICATIONS,
data: string,
};
declare type DoChannelSubscriptionDisableNotifications = {
type: ACTIONS.CHANNEL_SUBSCRIPTION_DISABLE_NOTIFICATIONS,
data: string,
};
declare type DoChannelSubscribe = {
type: ACTIONS.CHANNEL_SUBSCRIBE,
data: Subscription,
};
declare type DoChannelUnsubscribe = {
type: ACTIONS.CHANNEL_UNSUBSCRIBE,
data: Subscription,
};
declare type DoUpdateSubscriptionUnreads = {
type: ACTIONS.UPDATE_SUBSCRIPTION_UNREADS,
data: {
channel: string,
uris: Array<string>,
type?: SubscriptionNotificationType,
},
};
declare type DoRemoveSubscriptionUnreads = {
type: ACTIONS.REMOVE_SUBSCRIPTION_UNREADS,
data: {
channel: string,
uris: Array<string>,
},
};
declare type SetSubscriptionLatest = {
type: ACTIONS.SET_SUBSCRIPTION_LATEST,
data: {
subscription: Subscription,
uri: string,
},
};
declare type CheckSubscriptionStarted = {
type: ACTIONS.CHECK_SUBSCRIPTION_STARTED,
};
declare type CheckSubscriptionCompleted = {
type: ACTIONS.CHECK_SUBSCRIPTION_COMPLETED,
};
declare type FetchedSubscriptionsSucess = {
type: ACTIONS.FETCH_SUBSCRIPTIONS_SUCCESS,
data: Array<Subscription>,
};
declare type SetViewMode = {
type: ACTIONS.SET_VIEW_MODE,
data: ViewMode,
};
declare type GetSuggestedSubscriptionsSuccess = {
type: ACTIONS.GET_SUGGESTED_SUBSCRIPTIONS_START,
data: SuggestedSubscriptions,
};
declare type SubscriptionAction =
| DoChannelSubscribe
| DoChannelUnsubscribe
| DoUpdateSubscriptionUnreads
| DoRemoveSubscriptionUnreads
| SetSubscriptionLatest
| CheckSubscriptionStarted
| CheckSubscriptionCompleted
| SetViewMode
| Function;
declare type SubscriptionDispatch = Dispatch<SubscriptionAction>;

View file

@ -1,26 +0,0 @@
// @flow
type DeviceType = 'mobile' | 'web' | 'desktop';
declare type User = {
created_at: string,
family_name: ?string,
given_name: ?string,
groups: Array<string>,
has_verified_email: boolean,
id: number,
invite_reward_claimed: boolean,
invited_at: ?number,
invited_by_id: number,
invites_remaining: number,
is_email_enabled: boolean,
is_identity_verified: boolean,
is_reward_approved: boolean,
language: string,
manual_approval_user_id: ?number,
primary_email: string,
reward_status_change_trigger: string,
updated_at: string,
youtube_channels: ?Array<string>,
device_types: Array<DeviceType>,
};

5
flow-typed/Redux.js vendored
View file

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

View file

@ -1,137 +0,0 @@
// @flow
import type { Dispatch } from 'flow-typed/Redux';
import * as ACTIONS from 'constants/action_types';
import {
DOWNLOADED,
DOWNLOADING,
NOTIFY_ONLY,
VIEW_ALL,
VIEW_LATEST_FIRST,
SUGGESTED_TOP_BID,
SUGGESTED_TOP_SUBSCRIBED,
SUGGESTED_FEATURED,
} from 'constants/subscriptions';
declare type Subscription = {
channelName: string, // @CryptoCandor,
uri: string, // lbry://@CryptoCandor#9152f3b054f692076a6882d1b58a30e8781cc8e6
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
declare type SubscriptionNotificationType = DOWNLOADED | DOWNLOADING | NOTIFY_ONLY;
declare type UnreadSubscription = {
type: SubscriptionNotificationType,
uris: Array<string>,
};
declare type UnreadSubscriptions = {
[string]: UnreadSubscription,
};
declare type ViewMode = VIEW_LATEST_FIRST | VIEW_ALL;
declare type SuggestedType = SUGGESTED_TOP_BID | SUGGESTED_TOP_SUBSCRIBED | SUGGESTED_FEATURED;
declare type SuggestedSubscriptions = {
[SuggestedType]: string,
};
declare type SubscriptionState = {
enabledChannelNotifications: Array<string>,
subscriptions: Array<Subscription>,
unread: UnreadSubscriptions,
loading: boolean,
viewMode: ViewMode,
suggested: SuggestedSubscriptions,
loadingSuggested: boolean,
firstRunCompleted: boolean,
showSuggestedSubs: boolean,
};
//
// Action types
//
declare type DoChannelSubscriptionEnableNotifications = {
type: ACTIONS.CHANNEL_SUBSCRIPTION_ENABLE_NOTIFICATIONS,
data: string,
};
declare type DoChannelSubscriptionDisableNotifications = {
type: ACTIONS.CHANNEL_SUBSCRIPTION_DISABLE_NOTIFICATIONS,
data: string,
};
declare type DoChannelSubscribe = {
type: ACTIONS.CHANNEL_SUBSCRIBE,
data: Subscription,
};
declare type DoChannelUnsubscribe = {
type: ACTIONS.CHANNEL_UNSUBSCRIBE,
data: Subscription,
};
declare type DoUpdateSubscriptionUnreads = {
type: ACTIONS.UPDATE_SUBSCRIPTION_UNREADS,
data: {
channel: string,
uris: Array<string>,
type?: SubscriptionNotificationType,
},
};
declare type DoRemoveSubscriptionUnreads = {
type: ACTIONS.REMOVE_SUBSCRIPTION_UNREADS,
data: {
channel: string,
uris: Array<string>,
},
};
declare type SetSubscriptionLatest = {
type: ACTIONS.SET_SUBSCRIPTION_LATEST,
data: {
subscription: Subscription,
uri: string,
},
};
declare type CheckSubscriptionStarted = {
type: ACTIONS.CHECK_SUBSCRIPTION_STARTED,
};
declare type CheckSubscriptionCompleted = {
type: ACTIONS.CHECK_SUBSCRIPTION_COMPLETED,
};
declare type FetchedSubscriptionsSucess = {
type: ACTIONS.FETCH_SUBSCRIPTIONS_SUCCESS,
data: Array<Subscription>,
};
declare type SetViewMode = {
type: ACTIONS.SET_VIEW_MODE,
data: ViewMode,
};
declare type GetSuggestedSubscriptionsSuccess = {
type: ACTIONS.GET_SUGGESTED_SUBSCRIPTIONS_START,
data: SuggestedSubscriptions,
};
declare type SubscriptionAction =
| DoChannelSubscribe
| DoChannelUnsubscribe
| DoUpdateSubscriptionUnreads
| DoRemoveSubscriptionUnreads
| SetSubscriptionLatest
| CheckSubscriptionStarted
| CheckSubscriptionCompleted
| SetViewMode
| Function;
declare type SubscriptionDispatch = Dispatch<SubscriptionAction>;

26
flow-typed/User.js vendored
View file

@ -1,26 +0,0 @@
// @flow
type DeviceType = 'mobile' | 'web' | 'desktop';
declare type User = {
created_at: string,
family_name: ?string,
given_name: ?string,
groups: Array<string>,
has_verified_email: boolean,
id: number,
invite_reward_claimed: boolean,
invited_at: ?number,
invited_by_id: number,
invites_remaining: number,
is_email_enabled: boolean,
is_identity_verified: boolean,
is_reward_approved: boolean,
language: string,
manual_approval_user_id: ?number,
primary_email: string,
reward_status_change_trigger: string,
updated_at: string,
youtube_channels: ?Array<string>,
device_types: Array<DeviceType>,
};

View file

@ -1,49 +1,3 @@
// User
export const GENERATE_AUTH_TOKEN_FAILURE = 'GENERATE_AUTH_TOKEN_FAILURE';
export const GENERATE_AUTH_TOKEN_STARTED = 'GENERATE_AUTH_TOKEN_STARTED';
export const GENERATE_AUTH_TOKEN_SUCCESS = 'GENERATE_AUTH_TOKEN_SUCCESS';
export const AUTHENTICATION_STARTED = 'AUTHENTICATION_STARTED';
export const AUTHENTICATION_SUCCESS = 'AUTHENTICATION_SUCCESS';
export const AUTHENTICATION_FAILURE = 'AUTHENTICATION_FAILURE';
export const USER_EMAIL_DECLINE = 'USER_EMAIL_DECLINE';
export const USER_EMAIL_NEW_STARTED = 'USER_EMAIL_NEW_STARTED';
export const USER_EMAIL_NEW_SUCCESS = 'USER_EMAIL_NEW_SUCCESS';
export const USER_EMAIL_NEW_EXISTS = 'USER_EMAIL_NEW_EXISTS';
export const USER_EMAIL_NEW_FAILURE = 'USER_EMAIL_NEW_FAILURE';
export const USER_EMAIL_VERIFY_SET = 'USER_EMAIL_VERIFY_SET';
export const USER_EMAIL_VERIFY_STARTED = 'USER_EMAIL_VERIFY_STARTED';
export const USER_EMAIL_VERIFY_SUCCESS = 'USER_EMAIL_VERIFY_SUCCESS';
export const USER_EMAIL_VERIFY_FAILURE = 'USER_EMAIL_VERIFY_FAILURE';
export const USER_EMAIL_VERIFY_RETRY_STARTED = 'USER_EMAIL_VERIFY_RETRY_STARTED';
export const USER_EMAIL_VERIFY_RETRY_FAILURE = 'USER_EMAIL_VERIFY_RETRY_FAILURE';
export const USER_EMAIL_VERIFY_RETRY_SUCCESS = 'USER_EMAIL_VERIFY_RETRY_SUCCESS';
export const USER_PHONE_RESET = 'USER_PHONE_RESET';
export const USER_PHONE_NEW_STARTED = 'USER_PHONE_NEW_STARTED';
export const USER_PHONE_NEW_SUCCESS = 'USER_PHONE_NEW_SUCCESS';
export const USER_PHONE_NEW_FAILURE = 'USER_PHONE_NEW_FAILURE';
export const USER_PHONE_VERIFY_STARTED = 'USER_PHONE_VERIFY_STARTED';
export const USER_PHONE_VERIFY_SUCCESS = 'USER_PHONE_VERIFY_SUCCESS';
export const USER_PHONE_VERIFY_FAILURE = 'USER_PHONE_VERIFY_FAILURE';
export const USER_IDENTITY_VERIFY_STARTED = 'USER_IDENTITY_VERIFY_STARTED';
export const USER_IDENTITY_VERIFY_SUCCESS = 'USER_IDENTITY_VERIFY_SUCCESS';
export const USER_IDENTITY_VERIFY_FAILURE = 'USER_IDENTITY_VERIFY_FAILURE';
export const USER_FETCH_STARTED = 'USER_FETCH_STARTED';
export const USER_FETCH_SUCCESS = 'USER_FETCH_SUCCESS';
export const USER_FETCH_FAILURE = 'USER_FETCH_FAILURE';
export const USER_INVITE_STATUS_FETCH_STARTED = 'USER_INVITE_STATUS_FETCH_STARTED';
export const USER_INVITE_STATUS_FETCH_SUCCESS = 'USER_INVITE_STATUS_FETCH_SUCCESS';
export const USER_INVITE_STATUS_FETCH_FAILURE = 'USER_INVITE_STATUS_FETCH_FAILURE';
export const USER_INVITE_NEW_STARTED = 'USER_INVITE_NEW_STARTED';
export const USER_INVITE_NEW_SUCCESS = 'USER_INVITE_NEW_SUCCESS';
export const USER_INVITE_NEW_FAILURE = 'USER_INVITE_NEW_FAILURE';
export const FETCH_ACCESS_TOKEN_SUCCESS = 'FETCH_ACCESS_TOKEN_SUCCESS';
export const USER_YOUTUBE_IMPORT_STARTED = 'USER_YOUTUBE_IMPORT_STARTED';
export const USER_YOUTUBE_IMPORT_FAILURE = 'USER_YOUTUBE_IMPORT_FAILURE';
export const USER_YOUTUBE_IMPORT_SUCCESS = 'USER_YOUTUBE_IMPORT_SUCCESS';
export const USER_SET_REFERRER_STARTED = 'USER_SET_REFERRER_STARTED';
export const USER_SET_REFERRER_SUCCESS = 'USER_SET_REFERRER_SUCCESS';
export const USER_SET_REFERRER_FAILURE = 'USER_SET_REFERRER_FAILURE';
// Claims // Claims
export const FETCH_FEATURED_CONTENT_STARTED = 'FETCH_FEATURED_CONTENT_STARTED'; export const FETCH_FEATURED_CONTENT_STARTED = 'FETCH_FEATURED_CONTENT_STARTED';
export const FETCH_FEATURED_CONTENT_COMPLETED = 'FETCH_FEATURED_CONTENT_COMPLETED'; export const FETCH_FEATURED_CONTENT_COMPLETED = 'FETCH_FEATURED_CONTENT_COMPLETED';
@ -136,3 +90,8 @@ export const SYNC_RESET = 'SYNC_RESET';
// Lbry.tv // Lbry.tv
export const UPDATE_UPLOAD_PROGRESS = 'UPDATE_UPLOAD_PROGRESS'; export const UPDATE_UPLOAD_PROGRESS = 'UPDATE_UPLOAD_PROGRESS';
// User
export const GENERATE_AUTH_TOKEN_FAILURE = 'GENERATE_AUTH_TOKEN_FAILURE';
export const GENERATE_AUTH_TOKEN_STARTED = 'GENERATE_AUTH_TOKEN_STARTED';
export const GENERATE_AUTH_TOKEN_SUCCESS = 'GENERATE_AUTH_TOKEN_SUCCESS';

View file

@ -1,12 +0,0 @@
export const VIEW_ALL = 'view_all';
export const VIEW_LATEST_FIRST = 'view_latest_first';
// Types for unreads
export const DOWNLOADING = 'DOWNLOADING';
export const DOWNLOADED = 'DOWNLOADED';
export const NOTIFY_ONLY = 'NOTIFY_ONLY;';
// Suggested types
export const SUGGESTED_TOP_BID = 'top_bid';
export const SUGGESTED_TOP_SUBSCRIBED = 'top_subscribed';
export const SUGGESTED_FEATURED = 'featured';

View file

@ -1,3 +1,11 @@
export const NOT_TRANSFERRED = 'not_transferred'; export const YOUTUBE_SYNC_NOT_TRANSFERRED = 'not_transferred';
export const PENDING_TRANSFER = 'pending_transfer'; export const YOUTUBE_SYNC_PENDING = 'pending';
export const COMPLETED_TRANSFER = 'completed_transfer'; export const YOUTUBE_SYNC_PENDING_EMAIL = 'pendingemail';
export const YOUTUBE_SYNC_PENDING_TRANSFER = 'pending_transfer';
export const YOUTUBE_SYNC_COMPLETED_TRANSFER = 'completed_transfer';
export const YOUTUBE_SYNC_QUEUED = 'queued';
export const YOUTUBE_SYNC_SYNCING = 'syncing';
export const YOUTUBE_SYNC_SYNCED = 'synced';
export const YOUTUBE_SYNC_FAILED = 'failed';
export const YOUTUBE_SYNC_PENDINGUPGRADE = 'pendingupgrade';
export const YOUTUBE_SYNC_ABANDONDED = 'abandoned';

View file

@ -2,70 +2,17 @@ import * as LBRYINC_ACTIONS from 'constants/action_types';
import * as YOUTUBE_STATUSES from 'constants/youtube'; import * as YOUTUBE_STATUSES from 'constants/youtube';
import * as ERRORS from 'constants/errors'; import * as ERRORS from 'constants/errors';
import Lbryio from 'lbryio'; import Lbryio from 'lbryio';
import rewards from 'rewards';
import subscriptionsReducer from 'redux/reducers/subscriptions';
// middleware export { Lbryio };
export { userStateSyncMiddleware } from 'redux/middleware/sync';
// constants // constants
export { LBRYINC_ACTIONS, YOUTUBE_STATUSES, ERRORS }; export { LBRYINC_ACTIONS, YOUTUBE_STATUSES, ERRORS };
// Lbryio and rewards
export { Lbryio, rewards };
// utils // utils
export { doTransifexUpload } from 'util/transifex-upload'; export { doTransifexUpload } from 'util/transifex-upload';
// actions // actions
export { doGenerateAuthToken } from 'redux/actions/auth'; export { doGenerateAuthToken } from 'redux/actions/auth';
export {
doRewardList,
doClaimRewardType,
doClaimEligiblePurchaseRewards,
doClaimRewardClearError,
doFetchRewardedContent,
} from 'redux/actions/rewards';
export {
doChannelSubscribe,
doChannelUnsubscribe,
doChannelSubscriptionEnableNotifications,
doChannelSubscriptionDisableNotifications,
doCheckSubscription,
doCheckSubscriptions,
doCheckSubscriptionsInit,
doCompleteFirstRun,
doFetchMySubscriptions,
doFetchRecommendedSubscriptions,
doRemoveUnreadSubscription,
doRemoveUnreadSubscriptions,
doSetViewMode,
doShowSuggestedSubs,
doUpdateUnreadSubscriptions,
setSubscriptionLatest,
} from 'redux/actions/subscriptions';
export {
doFetchInviteStatus,
doInstallNew,
doAuthenticate,
doUserFetch,
doUserEmailNew,
doUserCheckEmailVerified,
doUserEmailToVerify,
doUserEmailVerifyFailure,
doUserEmailVerify,
doUserPhoneNew,
doUserPhoneReset,
doUserPhoneVerifyFailure,
doUserPhoneVerify,
doFetchAccessToken,
doUserResendVerificationEmail,
doUserIdentityVerify,
doUserInviteNew,
doClaimYoutubeChannels,
doCheckYoutubeTransfer,
doUserSetReferrer,
} from 'redux/actions/user';
export { doFetchCostInfoForUri } from 'redux/actions/cost_info'; export { doFetchCostInfoForUri } from 'redux/actions/cost_info';
export { doBlackListedOutpointsSubscribe } from 'redux/actions/blacklist'; export { doBlackListedOutpointsSubscribe } from 'redux/actions/blacklist';
export { doFilteredOutpointsSubscribe } from 'redux/actions/filtered'; export { doFilteredOutpointsSubscribe } from 'redux/actions/filtered';
@ -80,114 +27,42 @@ export {
doResetSync, doResetSync,
doSyncEncryptAndDecrypt, doSyncEncryptAndDecrypt,
} from 'redux/actions/sync'; } from 'redux/actions/sync';
export { doUpdateUploadProgress } from 'redux/actions/lbrytv'; export { doUpdateUploadProgress } from 'redux/actions/web';
// reducers // reducers
export { authReducer } from 'redux/reducers/auth'; export { authReducer } from 'redux/reducers/auth';
export { rewardsReducer } from 'redux/reducers/rewards';
export { subscriptionsReducer };
export { userReducer } from 'redux/reducers/user';
export { costInfoReducer } from 'redux/reducers/cost_info'; export { costInfoReducer } from 'redux/reducers/cost_info';
export { blacklistReducer } from 'redux/reducers/blacklist'; export { blacklistReducer } from 'redux/reducers/blacklist';
export { filteredReducer } from 'redux/reducers/filtered'; export { filteredReducer } from 'redux/reducers/filtered';
export { homepageReducer } from 'redux/reducers/homepage'; export { homepageReducer } from 'redux/reducers/homepage';
export { statsReducer } from 'redux/reducers/stats'; export { statsReducer } from 'redux/reducers/stats';
export { syncReducer } from 'redux/reducers/sync'; export { syncReducer } from 'redux/reducers/sync';
export { lbrytvReducer } from 'redux/reducers/lbrytv'; export { webReducer } from 'redux/reducers/web';
// selectors // selectors
export { selectAuthToken, selectIsAuthenticating } from 'redux/selectors/auth'; export { selectAuthToken, selectIsAuthenticating } from 'redux/selectors/auth';
export {
makeSelectClaimRewardError,
makeSelectIsRewardClaimPending,
makeSelectRewardAmountByType,
makeSelectRewardByType,
selectUnclaimedRewardsByType,
selectClaimedRewardsById,
selectClaimedRewards,
selectClaimedRewardsByTransactionId,
selectUnclaimedRewards,
selectFetchingRewards,
selectUnclaimedRewardValue,
selectClaimsPendingByType,
selectClaimErrorsByType,
selectRewardContentClaimIds,
selectReferralReward,
} from 'redux/selectors/rewards';
export {
makeSelectIsNew,
makeSelectIsSubscribed,
makeSelectUnreadByChannel,
selectEnabledChannelNotifications,
selectSubscriptions,
selectIsFetchingSubscriptions,
selectViewMode,
selectSuggested,
selectIsFetchingSuggested,
selectSuggestedChannels,
selectFirstRunCompleted,
selectShowSuggestedSubs,
selectSubscriptionsBeingFetched,
selectUnreadByChannel,
selectUnreadAmount,
selectUnreadSubscriptions,
selectSubscriptionClaims,
} from 'redux/selectors/subscriptions';
export {
selectAuthenticationIsPending,
selectUserIsPending,
selectUser,
selectUserEmail,
selectUserPhone,
selectUserCountryCode,
selectEmailToVerify,
selectPhoneToVerify,
selectUserIsRewardApproved,
selectEmailNewIsPending,
selectEmailNewErrorMessage,
selectPhoneNewErrorMessage,
selectPhoneNewIsPending,
selectEmailVerifyIsPending,
selectEmailVerifyErrorMessage,
selectEmailAlreadyExists,
selectResendingVerificationEmail,
selectPhoneVerifyErrorMessage,
selectPhoneVerifyIsPending,
selectIdentityVerifyIsPending,
selectIdentityVerifyErrorMessage,
selectUserIsVerificationCandidate,
selectAccessToken,
selectUserInviteStatusIsPending,
selectUserInvitesRemaining,
selectUserInvitees,
selectUserInviteStatusFailed,
selectUserInviteNewIsPending,
selectUserInviteNewErrorMessage,
selectUserInviteReferralLink,
selectUserInviteReferralCode,
selectUserVerifiedEmail,
selectYoutubeChannels,
selectYouTubeImportPending,
selectYouTubeImportError,
selectYouTubeImportVideosComplete,
selectSetReferrerPending,
selectSetReferrerError,
} from 'redux/selectors/user';
export { export {
makeSelectFetchingCostInfoForUri, makeSelectFetchingCostInfoForUri,
makeSelectCostInfoForUri, makeSelectCostInfoForUri,
selectAllCostInfoByUri, selectAllCostInfoByUri,
selectFetchingCostInfo, selectFetchingCostInfo,
} from 'redux/selectors/cost_info'; } from 'redux/selectors/cost_info';
export { selectBlackListedOutpoints } from 'redux/selectors/blacklist'; export {
export { selectFilteredOutpoints } from 'redux/selectors/filtered'; selectBlackListedOutpoints,
selectBlacklistedOutpointMap,
} from 'redux/selectors/blacklist';
export { selectFilteredOutpoints, selectFilteredOutpointMap } from 'redux/selectors/filtered';
export { export {
selectFeaturedUris, selectFeaturedUris,
selectFetchingFeaturedUris, selectFetchingFeaturedUris,
selectTrendingUris, selectTrendingUris,
selectFetchingTrendingUris, selectFetchingTrendingUris,
} from 'redux/selectors/homepage'; } from 'redux/selectors/homepage';
export { makeSelectViewCountForUri, makeSelectSubCountForUri } from 'redux/selectors/stats'; export {
selectViewCount,
makeSelectViewCountForUri,
makeSelectSubCountForUri,
} from 'redux/selectors/stats';
export { export {
selectHasSyncedWallet, selectHasSyncedWallet,
selectSyncData, selectSyncData,
@ -201,4 +76,4 @@ export {
selectSyncApplyErrorMessage, selectSyncApplyErrorMessage,
selectSyncApplyPasswordError, selectSyncApplyPasswordError,
} from 'redux/selectors/sync'; } from 'redux/selectors/sync';
export { selectCurrentUploads, selectUploadCount } from 'redux/selectors/lbrytv'; export { selectCurrentUploads, selectUploadCount } from 'redux/selectors/web';

View file

@ -11,6 +11,7 @@ const Lbryio = {
}; };
const EXCHANGE_RATE_TIMEOUT = 20 * 60 * 1000; const EXCHANGE_RATE_TIMEOUT = 20 * 60 * 1000;
const INTERNAL_APIS_DOWN = 'internal_apis_down';
// We can't use env's because they aren't passed into node_modules // We can't use env's because they aren't passed into node_modules
Lbryio.setLocalApi = endpoint => { Lbryio.setLocalApi = endpoint => {
@ -30,6 +31,12 @@ Lbryio.call = (resource, action, params = {}, method = 'get') => {
if (response.status >= 200 && response.status < 300) { if (response.status >= 200 && response.status < 300) {
return response.json(); return response.json();
} }
if (response.status === 500) {
return Promise.reject(INTERNAL_APIS_DOWN);
}
if (response)
return response.json().then(json => { return response.json().then(json => {
let error; let error;
if (json.error) { if (json.error) {
@ -87,7 +94,7 @@ Lbryio.getAuthToken = () =>
Lbryio.overrides.getAuthToken().then(token => { Lbryio.overrides.getAuthToken().then(token => {
resolve(token); resolve(token);
}); });
} else { } else if (typeof window !== 'undefined') {
const { store } = window; const { store } = window;
if (store) { if (store) {
const state = store.getState(); const state = store.getState();
@ -96,23 +103,27 @@ Lbryio.getAuthToken = () =>
resolve(token); resolve(token);
} }
resolve(null);
} else {
resolve(null); resolve(null);
} }
}); });
Lbryio.getCurrentUser = () => Lbryio.call('user', 'me'); Lbryio.getCurrentUser = () => Lbryio.call('user', 'me');
Lbryio.authenticate = () => { Lbryio.authenticate = (domain, language) => {
if (!Lbryio.enabled) { if (!Lbryio.enabled) {
return new Promise(resolve => { const params = {
resolve({
id: 1, id: 1,
language: 'en',
primary_email: 'disabled@lbry.io', primary_email: 'disabled@lbry.io',
has_verified_email: true, has_verified_email: true,
is_identity_verified: true, is_identity_verified: true,
is_reward_approved: false, is_reward_approved: false,
}); language: language || 'en',
};
return new Promise(resolve => {
resolve(params);
}); });
} }
@ -127,27 +138,34 @@ Lbryio.authenticate = () => {
// check that token works // check that token works
return Lbryio.getCurrentUser() return Lbryio.getCurrentUser()
.then(user => user) .then(user => user)
.catch(() => false); .catch(error => {
if (error === INTERNAL_APIS_DOWN) {
throw new Error('Internal APIS down');
}
return false;
});
}) })
.then(user => { .then(user => {
if (user) { if (user) {
return user; return user;
} }
return Lbry.status().then(status => { return Lbry.status()
if (Lbryio.overrides.setAuthToken) { .then(
return Lbryio.overrides.setAuthToken(status); status =>
} new Promise((res, rej) => {
const appId =
// simply call the logic to create a new user, and obtain the auth token domain && domain !== 'lbry.tv'
return new Promise((res, rej) => { ? (domain.replace(/[.]/gi, '') + status.installation_id).slice(0, 66)
: status.installation_id;
Lbryio.call( Lbryio.call(
'user', 'user',
'new', 'new',
{ {
auth_token: '', auth_token: '',
language: 'en', language: language || 'en',
app_id: status.installation_id, app_id: appId,
}, },
'post' 'post'
) )
@ -157,26 +175,28 @@ Lbryio.authenticate = () => {
} }
const { store } = window; const { store } = window;
if (Lbryio.overrides.setAuthToken) {
Lbryio.overrides.setAuthToken(response.auth_token);
}
if (store) { if (store) {
store.dispatch({ store.dispatch({
type: ACTIONS.GENERATE_AUTH_TOKEN_SUCCESS, type: ACTIONS.GENERATE_AUTH_TOKEN_SUCCESS,
data: { authToken: response.auth_token }, data: { authToken: response.auth_token },
}); });
} }
Lbryio.authToken = response.auth_token; Lbryio.authToken = response.auth_token;
res(response); return res(response);
}) })
.catch(error => rej(error)); .catch(error => rej(error));
});
});
}) })
.then(user => { )
if (!user) { .then(newUser => {
if (!newUser) {
return Lbryio.getCurrentUser(); return Lbryio.getCurrentUser();
} }
return newUser;
return user; });
}) })
.then(resolve, reject); .then(resolve, reject);
}); });

View file

@ -38,7 +38,9 @@ export function doFetchBlackListedOutpoints() {
}); });
}; };
Lbryio.call('file', 'list_blocked').then(success, failure); Lbryio.call('file', 'list_blocked', {
auth_token: '',
}).then(success, failure);
}; };
} }

View file

@ -35,7 +35,7 @@ export function doFetchFilteredOutpoints() {
}); });
}; };
Lbryio.call('file', 'list_filtered').then(success, failure); Lbryio.call('file', 'list_filtered', { auth_token: '' }).then(success, failure);
}; };
} }

View file

@ -1,176 +0,0 @@
import Lbryio from 'lbryio';
import { ACTIONS, doToast, doUpdateBalance } from 'lbry-redux';
import { selectUnclaimedRewards } from 'redux/selectors/rewards';
import { selectUserIsRewardApproved } from 'redux/selectors/user';
import { doFetchInviteStatus } from 'redux/actions/user';
import rewards from 'rewards';
export function doRewardList() {
return dispatch => {
dispatch({
type: ACTIONS.FETCH_REWARDS_STARTED,
});
Lbryio.call('reward', 'list', { multiple_rewards_per_type: true })
.then(userRewards => {
dispatch({
type: ACTIONS.FETCH_REWARDS_COMPLETED,
data: { userRewards },
});
})
.catch(() => {
dispatch({
type: ACTIONS.FETCH_REWARDS_COMPLETED,
data: { userRewards: [] },
});
});
};
}
export function doClaimRewardType(rewardType, options = {}) {
return (dispatch, getState) => {
const state = getState();
const userIsRewardApproved = selectUserIsRewardApproved(state);
const unclaimedRewards = selectUnclaimedRewards(state);
const reward =
rewardType === rewards.TYPE_REWARD_CODE
? { reward_type: rewards.TYPE_REWARD_CODE }
: unclaimedRewards.find(ur => ur.reward_type === rewardType);
// Try to claim the email reward right away, even if we haven't called reward_list yet
if (
rewardType !== rewards.TYPE_REWARD_CODE ||
rewardType !== rewards.TYPE_CONFIRM_EMAIL ||
rewardType !== rewards.TYPE_DAILY_VIEW
) {
if (!reward || reward.transaction_id) {
// already claimed or doesn't exist, do nothing
return;
}
}
if (
!userIsRewardApproved &&
rewardType !== rewards.TYPE_CONFIRM_EMAIL &&
rewardType !== rewards.TYPE_REWARD_CODE
) {
if (!options || (!options.failSilently && rewards.callbacks.rewardApprovalRequested)) {
rewards.callbacks.rewardApprovalRequested();
}
return;
}
// Set `claim_code` so the api knows which reward to give if there are multiple of the same type
const params = options.params || {};
params.claim_code = reward.claim_code;
dispatch({
type: ACTIONS.CLAIM_REWARD_STARTED,
data: { reward },
});
const success = successReward => {
// Temporary timeout to ensure the sdk has the correct balance after claiming a reward
setTimeout(() => {
dispatch(doUpdateBalance()).then(() => {
dispatch({
type: ACTIONS.CLAIM_REWARD_SUCCESS,
data: {
reward: successReward,
},
});
if (
successReward.reward_type === rewards.TYPE_NEW_USER &&
rewards.callbacks.claimFirstRewardSuccess
) {
rewards.callbacks.claimFirstRewardSuccess();
} else if (successReward.reward_type === rewards.TYPE_REFERRAL) {
dispatch(doFetchInviteStatus());
}
dispatch(doRewardList());
if (options.callback) {
options.callback();
}
});
}, 2000);
};
const failure = error => {
dispatch({
type: ACTIONS.CLAIM_REWARD_FAILURE,
data: {
reward,
error: !options || !options.failSilently ? error : undefined,
},
});
if (options.notifyError) {
dispatch(doToast({ message: error.message, isError: true }));
}
if (options.callback) {
options.callback(error);
}
};
return rewards.claimReward(rewardType, params).then(success, failure);
};
}
export function doClaimEligiblePurchaseRewards() {
return (dispatch, getState) => {
const state = getState();
const unclaimedRewards = selectUnclaimedRewards(state);
const userIsRewardApproved = selectUserIsRewardApproved(state);
if (!userIsRewardApproved || !Lbryio.enabled) {
return;
}
if (unclaimedRewards.find(ur => ur.reward_type === rewards.TYPE_FIRST_STREAM)) {
dispatch(doClaimRewardType(rewards.TYPE_FIRST_STREAM));
} else {
[rewards.TYPE_MANY_DOWNLOADS, rewards.TYPE_DAILY_VIEW].forEach(type => {
dispatch(doClaimRewardType(type, { failSilently: true }));
});
}
};
}
export function doClaimRewardClearError(reward) {
return dispatch => {
dispatch({
type: ACTIONS.CLAIM_REWARD_CLEAR_ERROR,
data: { reward },
});
};
}
export function doFetchRewardedContent() {
return dispatch => {
const success = nameToClaimId => {
dispatch({
type: ACTIONS.FETCH_REWARD_CONTENT_COMPLETED,
data: {
claimIds: Object.values(nameToClaimId),
success: true,
},
});
};
const failure = () => {
dispatch({
type: ACTIONS.FETCH_REWARD_CONTENT_COMPLETED,
data: {
claimIds: [],
success: false,
},
});
};
Lbryio.call('reward', 'list_featured').then(success, failure);
};
}

View file

@ -2,13 +2,13 @@
import Lbryio from 'lbryio'; import Lbryio from 'lbryio';
import * as ACTIONS from 'constants/action_types'; import * as ACTIONS from 'constants/action_types';
export const doFetchViewCount = (claimId: string) => dispatch => { export const doFetchViewCount = (claimIdCsv: string) => dispatch => {
dispatch({ type: ACTIONS.FETCH_VIEW_COUNT_STARTED }); dispatch({ type: ACTIONS.FETCH_VIEW_COUNT_STARTED });
return Lbryio.call('file', 'view_count', { claim_id: claimId }) return Lbryio.call('file', 'view_count', { claim_id: claimIdCsv })
.then((result: Array<number>) => { .then((result: Array<number>) => {
const viewCount = result[0]; const viewCounts = result;
dispatch({ type: ACTIONS.FETCH_VIEW_COUNT_COMPLETED, data: { claimId, viewCount } }); dispatch({ type: ACTIONS.FETCH_VIEW_COUNT_COMPLETED, data: { claimIdCsv, viewCounts } });
}) })
.catch(error => { .catch(error => {
dispatch({ type: ACTIONS.FETCH_VIEW_COUNT_FAILED, data: error }); dispatch({ type: ACTIONS.FETCH_VIEW_COUNT_FAILED, data: error });

View file

@ -1,445 +0,0 @@
// @flow
import type { SubscriptionDispatch } from 'flow-typed/Subscription';
import { PAGE_SIZE } from 'constants/claim';
import { doClaimRewardType } from 'redux/actions/rewards';
import { selectSubscriptions, selectUnreadByChannel } from 'redux/selectors/subscriptions';
import { Lbry, parseURI, doResolveUris, doPurchaseUri } from 'lbry-redux';
import * as ACTIONS from 'constants/action_types';
import * as NOTIFICATION_TYPES from 'constants/subscriptions';
import Lbryio from 'lbryio';
import rewards from 'rewards';
const CHECK_SUBSCRIPTIONS_INTERVAL = 15 * 60 * 1000;
const SUBSCRIPTION_DOWNLOAD_LIMIT = 1;
export const doSetViewMode = (viewMode: ViewMode) => (dispatch: SubscriptionDispatch) =>
dispatch({
type: ACTIONS.SET_VIEW_MODE,
data: viewMode,
});
export const setSubscriptionLatest = (subscription: Subscription, uri: string) => (
dispatch: SubscriptionDispatch
) =>
dispatch({
type: ACTIONS.SET_SUBSCRIPTION_LATEST,
data: {
subscription,
uri,
},
});
// Populate a channels unread subscriptions or update the type
export const doUpdateUnreadSubscriptions = (
channelUri: string,
uris: ?Array<string>,
type: ?SubscriptionNotificationType
) => (dispatch: SubscriptionDispatch, getState: GetState) => {
const state = getState();
const unreadByChannel = selectUnreadByChannel(state);
const currentUnreadForChannel: UnreadSubscription = unreadByChannel[channelUri];
let newUris: Array = [];
let newType: string = null;
if (!currentUnreadForChannel) {
newUris = uris;
newType = type;
} else {
if (uris) {
// If a channel currently has no unread uris, just add them all
if (!currentUnreadForChannel.uris || !currentUnreadForChannel.uris.length) {
newUris = uris;
} else {
// They already have unreads and now there are new ones
// Add the new ones to the beginning of the list
// Make sure there are no duplicates
const currentUnreadUris = currentUnreadForChannel.uris;
newUris = uris.filter(uri => !currentUnreadUris.includes(uri)).concat(currentUnreadUris);
}
} else {
newUris = currentUnreadForChannel.uris;
}
newType = type || currentUnreadForChannel.type;
}
dispatch({
type: ACTIONS.UPDATE_SUBSCRIPTION_UNREADS,
data: {
channel: channelUri,
uris: newUris,
type: newType,
},
});
};
// Remove multiple files (or all) from a channels unread subscriptions
export const doRemoveUnreadSubscriptions = (channelUri: ?string, readUris: ?Array<string>) => (
dispatch: SubscriptionDispatch,
getState: GetState
) => {
const state = getState();
const unreadByChannel = selectUnreadByChannel(state);
// If no channel is passed in, remove all unread subscriptions from all channels
if (!channelUri) {
return dispatch({
type: ACTIONS.REMOVE_SUBSCRIPTION_UNREADS,
data: { channel: null },
});
}
const currentChannelUnread = unreadByChannel[channelUri];
if (!currentChannelUnread || !currentChannelUnread.uris) {
// Channel passed in doesn't have any unreads
return null;
}
// For each uri passed in, remove it from the list of unread uris
// If no uris are passed in, remove them all
let newUris;
if (readUris) {
const urisToRemoveMap = readUris.reduce(
(acc, val) => ({
...acc,
[val]: true,
}),
{}
);
const filteredUris = currentChannelUnread.uris.filter(uri => !urisToRemoveMap[uri]);
newUris = filteredUris.length ? filteredUris : null;
} else {
newUris = null;
}
return 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: SubscriptionDispatch
) => {
dispatch(doRemoveUnreadSubscriptions(channelUri, [readUri]));
};
export const doCheckSubscription = (subscriptionUri: string, shouldNotify?: boolean) => (
dispatch: SubscriptionDispatch,
getState: GetState
) => {
// no dispatching FETCH_CHANNEL_CLAIMS_STARTED; causes loading issues on <SubscriptionsPage>
const state = getState();
const shouldAutoDownload = false; // makeSelectClientSetting(SETTINGS.AUTO_DOWNLOAD)(state);
const savedSubscription = state.subscriptions.subscriptions.find(
sub => sub.uri === subscriptionUri
);
const subscriptionLatest = state.subscriptions.latest[subscriptionUri];
if (!savedSubscription) {
throw Error(
`Trying to find new content for ${subscriptionUri} but it doesn't exist in your subscriptions`
);
}
// We may be duplicating calls here. Can this logic be baked into doFetchClaimsByChannel?
Lbry.claim_search({
channel: subscriptionUri,
valid_channel_signature: true,
order_by: ['release_time'],
page: 1,
page_size: PAGE_SIZE,
}).then(claimListByChannel => {
const { items: claimsInChannel } = claimListByChannel;
// may happen if subscribed to an abandoned channel or an empty channel
if (!claimsInChannel || !claimsInChannel.length) {
return;
}
// Determine if the latest subscription currently saved is actually the latest subscription
const latestIndex = claimsInChannel.findIndex(
claim => claim.permanent_url === subscriptionLatest
);
// If latest is -1, it is a newly subscribed channel or there have been 10+ claims published since last viewed
const latestIndexToNotify = latestIndex === -1 ? 10 : latestIndex;
// If latest is 0, nothing has changed
// Do not download/notify about new content, it would download/notify 10 claims per channel
if (latestIndex !== 0 && subscriptionLatest) {
let downloadCount = 0;
const newUnread = [];
claimsInChannel.slice(0, latestIndexToNotify).forEach(claim => {
const uri = claim.permanent_url;
const shouldDownload =
shouldAutoDownload &&
Boolean(downloadCount < SUBSCRIPTION_DOWNLOAD_LIMIT && !claim.value.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));
}
});
dispatch(
doUpdateUnreadSubscriptions(
subscriptionUri,
newUnread,
downloadCount > 0 ? NOTIFICATION_TYPES.DOWNLOADING : NOTIFICATION_TYPES.NOTIFY_ONLY
)
);
}
// Set the latest piece of content for a channel
// This allows the app to know if there has been new content since it was last set
const latest = claimsInChannel[0];
dispatch(
setSubscriptionLatest(
{
channelName: latest.signing_channel.name,
uri: latest.signing_channel.permanent_url,
},
latest.permanent_url
)
);
// 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 doChannelSubscribe = (subscription: Subscription) => (
dispatch: SubscriptionDispatch,
getState: GetState
) => {
const {
settings: { daemonSettings },
} = getState();
const isSharingData = daemonSettings ? daemonSettings.share_usage_data : true;
const subscriptionUri = subscription.uri;
if (!subscriptionUri.startsWith('lbry://')) {
throw Error(
`Subscription uris must inclue the "lbry://" prefix.\nTried to subscribe to ${subscriptionUri}`
);
}
dispatch({
type: ACTIONS.CHANNEL_SUBSCRIBE,
data: subscription,
});
// if the user isn't sharing data, keep the subscriptions entirely in the app
if (isSharingData) {
const { channelClaimId } = parseURI(subscription.uri);
// They are sharing data, we can store their subscriptions in our internal database
Lbryio.call('subscription', 'new', {
channel_name: subscription.channelName,
claim_id: channelClaimId,
});
dispatch(doClaimRewardType(rewards.TYPE_SUBSCRIPTION, { failSilently: true }));
}
dispatch(doCheckSubscription(subscription.uri, true));
};
export const doChannelUnsubscribe = (subscription: Subscription) => (
dispatch: SubscriptionDispatch,
getState: GetState
) => {
const {
settings: { daemonSettings },
} = getState();
const isSharingData = daemonSettings ? daemonSettings.share_usage_data : true;
dispatch({
type: ACTIONS.CHANNEL_UNSUBSCRIBE,
data: subscription,
});
if (isSharingData) {
const { channelClaimId } = parseURI(subscription.uri);
Lbryio.call('subscription', 'delete', {
claim_id: channelClaimId,
});
}
};
export const doCheckSubscriptions = () => (dispatch: SubscriptionDispatch, getState: GetState) => {
const state = getState();
const subscriptions = selectSubscriptions(state);
subscriptions.forEach((sub: Subscription) => {
dispatch(doCheckSubscription(sub.uri, true));
});
};
export const doFetchMySubscriptions = () => (
dispatch: SubscriptionDispatch,
getState: GetState
) => {
const state: { subscriptions: SubscriptionState, settings: any } = getState();
const { subscriptions: reduxSubscriptions } = state.subscriptions;
// default to true if daemonSettings not found
const isSharingData =
state.settings && state.settings.daemonSettings
? state.settings.daemonSettings.share_usage_data
: true;
if (!isSharingData && isSharingData !== undefined) {
// They aren't sharing their data, subscriptions will be handled by persisted redux state
return;
}
// most of this logic comes from scenarios where the db isn't synced with redux
// this will happen if the user stops sharing data
dispatch({ type: ACTIONS.FETCH_SUBSCRIPTIONS_START });
Lbryio.call('subscription', 'list')
.then(dbSubscriptions => {
const storedSubscriptions = dbSubscriptions || [];
// User has no subscriptions in db or redux
if (!storedSubscriptions.length && (!reduxSubscriptions || !reduxSubscriptions.length)) {
return [];
}
// There is some mismatch between redux state and db state
// If something is in the db, but not in redux, add it to redux
// If something is in redux, but not in the db, add it to the db
if (storedSubscriptions.length !== reduxSubscriptions.length) {
const dbSubMap = {};
const reduxSubMap = {};
const subsNotInDB = [];
const subscriptionsToReturn = reduxSubscriptions.slice();
storedSubscriptions.forEach(sub => {
dbSubMap[sub.claim_id] = 1;
});
reduxSubscriptions.forEach(sub => {
const { channelClaimId } = parseURI(sub.uri);
reduxSubMap[channelClaimId] = 1;
});
storedSubscriptions.forEach(sub => {
if (!reduxSubMap[sub.claim_id]) {
const uri = `lbry://${sub.channel_name}#${sub.claim_id}`;
subscriptionsToReturn.push({ uri, channelName: sub.channel_name });
}
});
return Promise.all(subsNotInDB.map(payload => Lbryio.call('subscription', 'new', payload)))
.then(() => subscriptionsToReturn)
.catch(
() =>
// let it fail, we will try again when the navigate to the subscriptions page
subscriptionsToReturn
);
}
// DB is already synced, just return the subscriptions in redux
return reduxSubscriptions;
})
.then((subscriptions: Array<Subscription>) => {
dispatch({
type: ACTIONS.FETCH_SUBSCRIPTIONS_SUCCESS,
data: subscriptions,
});
dispatch(doResolveUris(subscriptions.map(({ uri }) => uri)));
dispatch(doCheckSubscriptions());
})
.catch(() => {
dispatch({
type: ACTIONS.FETCH_SUBSCRIPTIONS_FAIL,
});
});
};
export const doCheckSubscriptionsInit = () => (dispatch: SubscriptionDispatch) => {
// doCheckSubscriptionsInit is called by doDaemonReady
// setTimeout below is a hack to ensure redux is hydrated when subscriptions are checked
// this will be replaced with <PersistGate> which reqiures a package upgrade
setTimeout(() => dispatch(doFetchMySubscriptions()), 5000);
const checkSubscriptionsTimer = setInterval(
() => dispatch(doCheckSubscriptions()),
CHECK_SUBSCRIPTIONS_INTERVAL
);
dispatch({
type: ACTIONS.CHECK_SUBSCRIPTIONS_SUBSCRIBE,
data: { checkSubscriptionsTimer },
});
setInterval(() => dispatch(doCheckSubscriptions()), CHECK_SUBSCRIPTIONS_INTERVAL);
};
export const doFetchRecommendedSubscriptions = () => (dispatch: SubscriptionDispatch) => {
dispatch({
type: ACTIONS.GET_SUGGESTED_SUBSCRIPTIONS_START,
});
return Lbryio.call('subscription', 'suggest')
.then(suggested =>
dispatch({
type: ACTIONS.GET_SUGGESTED_SUBSCRIPTIONS_SUCCESS,
data: suggested,
})
)
.catch(error =>
dispatch({
type: ACTIONS.GET_SUGGESTED_SUBSCRIPTIONS_FAIL,
error,
})
);
};
export const doCompleteFirstRun = () => (dispatch: SubscriptionDispatch) =>
dispatch({
type: ACTIONS.SUBSCRIPTION_FIRST_RUN_COMPLETED,
});
export const doShowSuggestedSubs = () => (dispatch: SubscriptionDispatch) =>
dispatch({
type: ACTIONS.VIEW_SUGGESTED_SUBSCRIPTIONS,
});
export const doChannelSubscriptionEnableNotifications = (channelName: string) => (
dispatch: SubscriptionDispatch
) =>
dispatch({
type: ACTIONS.CHANNEL_SUBSCRIPTION_ENABLE_NOTIFICATIONS,
data: channelName,
});
export const doChannelSubscriptionDisableNotifications = (channelName: string) => (
dispatch: SubscriptionDispatch
) =>
dispatch({
type: ACTIONS.CHANNEL_SUBSCRIPTION_DISABLE_NOTIFICATIONS,
data: channelName,
});

View file

@ -2,6 +2,8 @@ import * as ACTIONS from 'constants/action_types';
import Lbryio from 'lbryio'; import Lbryio from 'lbryio';
import { Lbry, doWalletEncrypt, doWalletDecrypt } from 'lbry-redux'; import { Lbry, doWalletEncrypt, doWalletDecrypt } from 'lbry-redux';
const NO_WALLET_ERROR = 'no wallet found for this user';
export function doSetDefaultAccount(success, failure) { export function doSetDefaultAccount(success, failure) {
return dispatch => { return dispatch => {
dispatch({ dispatch({
@ -80,13 +82,13 @@ export function doSetSync(oldHash, newHash, data) {
export function doGetSync(passedPassword, callback) { export function doGetSync(passedPassword, callback) {
const password = passedPassword === null || passedPassword === undefined ? '' : passedPassword; const password = passedPassword === null || passedPassword === undefined ? '' : passedPassword;
function handleCallback(error) { function handleCallback(error, hasNewData) {
if (callback) { if (callback) {
if (typeof callback !== 'function') { if (typeof callback !== 'function') {
throw new Error('Second argument passed to "doGetSync" must be a function'); throw new Error('Second argument passed to "doGetSync" must be a function');
} }
callback(error); callback(error, hasNewData);
} }
} }
@ -102,13 +104,23 @@ export function doGetSync(passedPassword, callback) {
if (status.is_locked) { if (status.is_locked) {
return Lbry.wallet_unlock({ password }); return Lbry.wallet_unlock({ password });
} }
// Wallet is already unlocked
return true;
})
.then(isUnlocked => {
if (isUnlocked) {
return Lbry.sync_hash();
}
data.unlockFailed = true;
throw new Error();
}) })
.then(() => Lbry.sync_hash())
.then(hash => Lbryio.call('sync', 'get', { hash }, 'post')) .then(hash => Lbryio.call('sync', 'get', { hash }, 'post'))
.then(response => { .then(response => {
const syncHash = response.hash; const syncHash = response.hash;
data.syncHash = syncHash; data.syncHash = syncHash;
data.syncData = response.data; data.syncData = response.data;
data.changed = response.changed;
data.hasSyncedWallet = true; data.hasSyncedWallet = true;
if (response.changed) { if (response.changed) {
@ -118,7 +130,7 @@ export function doGetSync(passedPassword, callback) {
.then(response => { .then(response => {
if (!response) { if (!response) {
dispatch({ type: ACTIONS.GET_SYNC_COMPLETED, data }); dispatch({ type: ACTIONS.GET_SYNC_COMPLETED, data });
handleCallback(); handleCallback(null, data.changed);
return; return;
} }
@ -130,11 +142,20 @@ export function doGetSync(passedPassword, callback) {
} }
dispatch({ type: ACTIONS.GET_SYNC_COMPLETED, data }); dispatch({ type: ACTIONS.GET_SYNC_COMPLETED, data });
handleCallback(); handleCallback(null, data.changed);
}) })
.catch(() => { .catch(syncAttemptError => {
if (data.hasSyncedWallet) { if (data.unlockFailed) {
const error = 'Error getting synced wallet'; dispatch({ type: ACTIONS.GET_SYNC_FAILED, data: { error: syncAttemptError } });
if (password !== '') {
dispatch({ type: ACTIONS.SYNC_APPLY_BAD_PASSWORD });
}
handleCallback(syncAttemptError);
} else if (data.hasSyncedWallet) {
const error =
(syncAttemptError && syncAttemptError.message) || 'Error getting synced wallet';
dispatch({ dispatch({
type: ACTIONS.GET_SYNC_FAILED, type: ACTIONS.GET_SYNC_FAILED,
data: { data: {
@ -159,15 +180,17 @@ export function doGetSync(passedPassword, callback) {
// call sync_apply to get data to sync // call sync_apply to get data to sync
// first time sync. use any string for old hash // first time sync. use any string for old hash
if (syncAttemptError.message === NO_WALLET_ERROR) {
Lbry.sync_apply({ password }) Lbry.sync_apply({ password })
.then(({ hash: walletHash, data: syncApplyData }) => { .then(({ hash: walletHash, data: syncApplyData }) => {
dispatch(doSetSync('', walletHash, syncApplyData, password)); dispatch(doSetSync('', walletHash, syncApplyData, password));
handleCallback(); handleCallback();
}) })
.catch(error => { .catch(syncApplyError => {
handleCallback(error); handleCallback(syncApplyError);
}); });
} }
}
}); });
}; };
} }

View file

@ -1,518 +0,0 @@
import {
Lbry,
doToast,
doFetchChannelListMine,
batchActions,
makeSelectClaimForUri,
parseURI,
} from 'lbry-redux';
import * as ACTIONS from 'constants/action_types';
import { doClaimRewardType, doRewardList } from 'redux/actions/rewards';
import {
selectEmailToVerify,
selectPhoneToVerify,
selectUserCountryCode,
} from 'redux/selectors/user';
import rewards from 'rewards';
import Lbryio from 'lbryio';
export function doFetchInviteStatus() {
return dispatch => {
dispatch({
type: ACTIONS.USER_INVITE_STATUS_FETCH_STARTED,
});
Promise.all([Lbryio.call('user', 'invite_status'), Lbryio.call('user_referral_code', 'list')])
.then(([status, code]) => {
dispatch(doRewardList());
dispatch({
type: ACTIONS.USER_INVITE_STATUS_FETCH_SUCCESS,
data: {
invitesRemaining: status.invites_remaining ? status.invites_remaining : 0,
invitees: status.invitees,
referralLink: `${Lbryio.CONNECTION_STRING}user/refer?r=${code}`,
referralCode: code,
},
});
})
.catch(error => {
dispatch({
type: ACTIONS.USER_INVITE_STATUS_FETCH_FAILURE,
data: { error },
});
});
};
}
export function doInstallNew(appVersion, os = null, firebaseToken = null) {
const payload = { app_version: appVersion };
if (firebaseToken) {
payload.firebase_token = firebaseToken;
}
Lbry.status().then(status => {
payload.app_id = status.installation_id;
payload.node_id = status.lbry_id;
Lbry.version().then(version => {
payload.daemon_version = version.lbrynet_version;
payload.operating_system = os || version.os_system;
payload.platform = version.platform;
Lbryio.call('install', 'new', payload);
});
});
}
// TODO: Call doInstallNew separately so we don't have to pass appVersion and os_system params?
export function doAuthenticate(appVersion, os = null, firebaseToken = null) {
return dispatch => {
dispatch({
type: ACTIONS.AUTHENTICATION_STARTED,
});
Lbryio.authenticate()
.then(user => {
Lbryio.getAuthToken().then(token => {
dispatch({
type: ACTIONS.AUTHENTICATION_SUCCESS,
data: { user, accessToken: token },
});
dispatch(doRewardList());
dispatch(doFetchInviteStatus());
doInstallNew(appVersion, os, firebaseToken);
});
})
.catch(error => {
dispatch({
type: ACTIONS.AUTHENTICATION_FAILURE,
data: { error },
});
});
};
}
export function doUserFetch() {
return dispatch => {
dispatch({
type: ACTIONS.USER_FETCH_STARTED,
});
Lbryio.getCurrentUser()
.then(user => {
dispatch(doRewardList());
dispatch({
type: ACTIONS.USER_FETCH_SUCCESS,
data: { user },
});
})
.catch(error => {
dispatch({
type: ACTIONS.USER_FETCH_FAILURE,
data: { error },
});
});
};
}
export function doUserCheckEmailVerified() {
// This will happen in the background so we don't need loading booleans
return dispatch => {
Lbryio.getCurrentUser().then(user => {
if (user.has_verified_email) {
dispatch(doRewardList());
dispatch({
type: ACTIONS.USER_FETCH_SUCCESS,
data: { user },
});
}
});
};
}
export function doUserPhoneReset() {
return {
type: ACTIONS.USER_PHONE_RESET,
};
}
export function doUserPhoneNew(phone, countryCode) {
return dispatch => {
dispatch({
type: ACTIONS.USER_PHONE_NEW_STARTED,
data: { phone, country_code: countryCode },
});
const success = () => {
dispatch({
type: ACTIONS.USER_PHONE_NEW_SUCCESS,
data: { phone },
});
};
const failure = error => {
dispatch({
type: ACTIONS.USER_PHONE_NEW_FAILURE,
data: { error },
});
};
Lbryio.call(
'user',
'phone_number_new',
{ phone_number: phone, country_code: countryCode },
'post'
).then(success, failure);
};
}
export function doUserPhoneVerifyFailure(error) {
return {
type: ACTIONS.USER_PHONE_VERIFY_FAILURE,
data: { error },
};
}
export function doUserPhoneVerify(verificationCode) {
return (dispatch, getState) => {
const phoneNumber = selectPhoneToVerify(getState());
const countryCode = selectUserCountryCode(getState());
dispatch({
type: ACTIONS.USER_PHONE_VERIFY_STARTED,
code: verificationCode,
});
Lbryio.call(
'user',
'phone_number_confirm',
{
verification_code: verificationCode,
phone_number: phoneNumber,
country_code: countryCode,
},
'post'
)
.then(user => {
if (user.is_identity_verified) {
dispatch({
type: ACTIONS.USER_PHONE_VERIFY_SUCCESS,
data: { user },
});
dispatch(doClaimRewardType(rewards.TYPE_NEW_USER));
}
})
.catch(error => dispatch(doUserPhoneVerifyFailure(error)));
};
}
export function doUserEmailToVerify(email) {
return dispatch => {
dispatch({
type: ACTIONS.USER_EMAIL_VERIFY_SET,
data: { email },
});
};
}
export function doUserEmailNew(email) {
return dispatch => {
dispatch({
type: ACTIONS.USER_EMAIL_NEW_STARTED,
email,
});
const success = () => {
dispatch({
type: ACTIONS.USER_EMAIL_NEW_SUCCESS,
data: { email },
});
dispatch(doUserFetch());
};
const failure = error => {
dispatch({
type: ACTIONS.USER_EMAIL_NEW_FAILURE,
data: { error },
});
};
Lbryio.call('user_email', 'new', { email, send_verification_email: true }, 'post')
.catch(error => {
if (error.response && error.response.status === 409) {
dispatch({
type: ACTIONS.USER_EMAIL_NEW_EXISTS,
});
return Lbryio.call(
'user_email',
'resend_token',
{ email, only_if_expired: true },
'post'
).then(success, failure);
}
throw error;
})
.then(success, failure);
};
}
export function doUserResendVerificationEmail(email) {
return dispatch => {
dispatch({
type: ACTIONS.USER_EMAIL_VERIFY_RETRY_STARTED,
});
const success = () => {
dispatch({
type: ACTIONS.USER_EMAIL_VERIFY_RETRY_SUCCESS,
});
};
const failure = error => {
dispatch({
type: ACTIONS.USER_EMAIL_VERIFY_RETRY_FAILURE,
data: { error },
});
};
Lbryio.call('user_email', 'resend_token', { email }, 'post')
.catch(error => {
if (error.response && error.response.status === 409) {
throw error;
}
})
.then(success, failure);
};
}
export function doUserEmailVerifyFailure(error) {
return {
type: ACTIONS.USER_EMAIL_VERIFY_FAILURE,
data: { error },
};
}
export function doUserEmailVerify(verificationToken, recaptcha) {
return (dispatch, getState) => {
const email = selectEmailToVerify(getState());
dispatch({
type: ACTIONS.USER_EMAIL_VERIFY_STARTED,
code: verificationToken,
recaptcha,
});
Lbryio.call(
'user_email',
'confirm',
{
verification_token: verificationToken,
email,
recaptcha,
},
'post'
)
.then(userEmail => {
if (userEmail.is_verified) {
dispatch({
type: ACTIONS.USER_EMAIL_VERIFY_SUCCESS,
data: { email },
});
dispatch(doUserFetch());
} else {
throw new Error('Your email is still not verified.'); // shouldn't happen
}
})
.catch(error => dispatch(doUserEmailVerifyFailure(error)));
};
}
export function doFetchAccessToken() {
return dispatch => {
const success = token =>
dispatch({
type: ACTIONS.FETCH_ACCESS_TOKEN_SUCCESS,
data: { token },
});
Lbryio.getAuthToken().then(success);
};
}
export function doUserIdentityVerify(stripeToken) {
return dispatch => {
dispatch({
type: ACTIONS.USER_IDENTITY_VERIFY_STARTED,
token: stripeToken,
});
Lbryio.call('user', 'verify_identity', { stripe_token: stripeToken }, 'post')
.then(user => {
if (user.is_identity_verified) {
dispatch({
type: ACTIONS.USER_IDENTITY_VERIFY_SUCCESS,
data: { user },
});
dispatch(doClaimRewardType(rewards.TYPE_NEW_USER));
} else {
throw new Error('Your identity is still not verified. This should not happen.'); // shouldn't happen
}
})
.catch(error => {
dispatch({
type: ACTIONS.USER_IDENTITY_VERIFY_FAILURE,
data: { error: error.toString() },
});
});
};
}
export function doUserInviteNew(email) {
return dispatch => {
dispatch({
type: ACTIONS.USER_INVITE_NEW_STARTED,
});
return Lbryio.call('user', 'invite', { email }, 'post')
.then(success => {
dispatch({
type: ACTIONS.USER_INVITE_NEW_SUCCESS,
data: { email },
});
dispatch(
doToast({
message: __(`Invite sent to ${email}`),
})
);
dispatch(doFetchInviteStatus());
return success;
})
.catch(error => {
dispatch({
type: ACTIONS.USER_INVITE_NEW_FAILURE,
data: { error },
});
});
};
}
export function doUserSetReferrer(referrer, shouldClaim) {
return async (dispatch, getState) => {
dispatch({
type: ACTIONS.USER_SET_REFERRER_STARTED,
});
let claim;
let referrerCode = referrer;
const isClaim = parseURI(referrer).claimId;
if (isClaim) {
const uri = `lbry://${referrer}`;
claim = makeSelectClaimForUri(uri)(getState());
if (!claim) {
try {
const response = await Lbry.resolve({ urls: [uri] });
claim = response && response[uri];
} catch (error) {
dispatch({
type: ACTIONS.USER_SET_REFERRER_FAILURE,
data: { error },
});
}
}
referrerCode = claim && claim.permanent_url.replace('lbry://', '');
}
try {
await Lbryio.call('user', 'referral', { referrer: referrerCode }, 'post');
dispatch({
type: ACTIONS.USER_SET_REFERRER_SUCCESS,
});
if (shouldClaim) {
dispatch(doClaimRewardType(rewards.TYPE_REFEREE));
dispatch(doUserFetch());
} else {
dispatch(doUserFetch());
}
} catch (error) {
dispatch({
type: ACTIONS.USER_SET_REFERRER_FAILURE,
data: { error },
});
}
};
}
export function doClaimYoutubeChannels() {
return dispatch => {
dispatch({
type: ACTIONS.USER_YOUTUBE_IMPORT_STARTED,
});
let transferResponse;
return Lbry.address_list({ page: 1, page_size: 99999 })
.then(addressList => addressList.items.sort((a, b) => a.used_times - b.used_times)[0])
.then(address =>
Lbryio.call('yt', 'transfer', {
address: address.address,
public_key: address.pubkey,
}).then(response => {
if (response && response.length) {
transferResponse = response;
return Promise.all(
response.map(channelMeta => {
if (channelMeta && channelMeta.channel && channelMeta.channel.channel_certificate) {
return Lbry.channel_import({
channel_data: channelMeta.channel.channel_certificate,
});
}
return null;
})
).then(() => {
const actions = [
{
type: ACTIONS.USER_YOUTUBE_IMPORT_SUCCESS,
data: transferResponse,
},
];
actions.push(doUserFetch());
actions.push(doFetchChannelListMine());
dispatch(batchActions(...actions));
});
}
})
)
.catch(error => {
dispatch({
type: ACTIONS.USER_YOUTUBE_IMPORT_FAILURE,
data: String(error),
});
});
};
}
export function doCheckYoutubeTransfer() {
return dispatch => {
dispatch({
type: ACTIONS.USER_YOUTUBE_IMPORT_STARTED,
});
return Lbryio.call('yt', 'transfer')
.then(response => {
if (response && response.length) {
dispatch({
type: ACTIONS.USER_YOUTUBE_IMPORT_SUCCESS,
data: response,
});
} else {
throw new Error();
}
})
.catch(error => {
dispatch({
type: ACTIONS.USER_YOUTUBE_IMPORT_FAILURE,
data: String(error),
});
});
};
}

View file

@ -1,58 +0,0 @@
import {
ACTIONS as LBRY_REDUX_ACTIONS,
makeSelectIsFollowingTag,
selectFollowedTags,
} from 'lbry-redux';
import Lbryio from 'lbryio';
import * as ACTIONS from 'constants/action_types';
import { selectSubscriptions } from 'redux/selectors/subscriptions';
const persistShape = {
version: '0',
shared: {},
};
export function userStateSyncMiddleware() {
return ({ getState }) => next => action => {
if (
action.type === ACTIONS.CHANNEL_SUBSCRIBE ||
action.type === ACTIONS.CHANNEL_UNSUBSCRIBE ||
action.type === LBRY_REDUX_ACTIONS.TOGGLE_TAG_FOLLOW
) {
const newShape = { ...persistShape };
const state = getState();
const subscriptions = selectSubscriptions(state).map(({ uri }) => uri);
const tags = selectFollowedTags(state);
newShape.shared.subscriptions = subscriptions;
newShape.shared.tags = tags;
const { uri } = action.data;
if (action.type === ACTIONS.CHANNEL_SUBSCRIBE) {
const newSubscriptions = subscriptions.slice();
newSubscriptions.push(uri);
newShape.shared.subscriptions = newSubscriptions;
} else if (action.type === ACTIONS.CHANNEL_UNSUBSCRIBE) {
let newSubscriptions = subscriptions.slice();
newSubscriptions = newSubscriptions.filter(subscribedUri => subscribedUri !== uri);
newShape.shared.subscriptions = newSubscriptions;
} else {
const toggledTag = action.data.name;
const followedTags = selectFollowedTags(state).map(({ name }) => name);
const isFollowing = makeSelectIsFollowingTag(toggledTag)(state);
let newTags = followedTags.slice();
if (isFollowing) {
newTags = newTags.filter(followedTag => followedTag.name !== toggledTag);
} else {
newTags.push(toggledTag);
}
newShape.shared.tags = newTags;
}
Lbryio.call('user_settings', 'set', { settings: newShape });
}
return next(action);
};
}

View file

@ -1,110 +0,0 @@
import { ACTIONS } from 'lbry-redux';
const reducers = {};
const defaultState = {
fetching: false,
claimedRewardsById: {}, // id => reward
unclaimedRewards: [],
claimPendingByType: {},
claimErrorsByType: {},
rewardedContentClaimIds: [],
};
reducers[ACTIONS.FETCH_REWARDS_STARTED] = state =>
Object.assign({}, state, {
fetching: true,
});
reducers[ACTIONS.FETCH_REWARDS_COMPLETED] = (state, action) => {
const { userRewards } = action.data;
const unclaimedRewards = [];
const claimedRewards = {};
userRewards.forEach(reward => {
if (reward.transaction_id) {
claimedRewards[reward.id] = reward;
} else {
unclaimedRewards.push(reward);
}
});
return Object.assign({}, state, {
claimedRewardsById: claimedRewards,
unclaimedRewards,
fetching: false,
});
};
function setClaimRewardState(state, reward, isClaiming, errorMessage = '') {
const newClaimPendingByType = Object.assign({}, state.claimPendingByType);
const newClaimErrorsByType = Object.assign({}, state.claimErrorsByType);
// Currently, for multiple rewards of the same type, they will both show "claiming" when one is beacuse we track this by `reward_type`
// To fix this we will need to use `claim_code` instead, and change all selectors to match
if (isClaiming) {
newClaimPendingByType[reward.reward_type] = isClaiming;
} else {
delete newClaimPendingByType[reward.reward_type];
}
if (errorMessage) {
newClaimErrorsByType[reward.reward_type] = errorMessage;
} else {
delete newClaimErrorsByType[reward.reward_type];
}
return Object.assign({}, state, {
claimPendingByType: newClaimPendingByType,
claimErrorsByType: newClaimErrorsByType,
});
}
reducers[ACTIONS.CLAIM_REWARD_STARTED] = (state, action) => {
const { reward } = action.data;
return setClaimRewardState(state, reward, true, '');
};
reducers[ACTIONS.CLAIM_REWARD_SUCCESS] = (state, action) => {
const { reward } = action.data;
const { unclaimedRewards } = state;
const index = unclaimedRewards.findIndex(ur => ur.claim_code === reward.claim_code);
unclaimedRewards.splice(index, 1);
const { claimedRewardsById } = state;
claimedRewardsById[reward.id] = reward;
const newState = {
...state,
unclaimedRewards: [...unclaimedRewards],
claimedRewardsById: { ...claimedRewardsById },
};
return setClaimRewardState(newState, reward, false, '');
};
reducers[ACTIONS.CLAIM_REWARD_FAILURE] = (state, action) => {
const { reward, error } = action.data;
return setClaimRewardState(state, reward, false, error ? error.message : '');
};
reducers[ACTIONS.CLAIM_REWARD_CLEAR_ERROR] = (state, action) => {
const { reward } = action.data;
return setClaimRewardState(state, reward, state.claimPendingByType[reward.reward_type], '');
};
reducers[ACTIONS.FETCH_REWARD_CONTENT_COMPLETED] = (state, action) => {
const { claimIds } = action.data;
return Object.assign({}, state, {
rewardedContentClaimIds: claimIds,
});
};
export function rewardsReducer(state = defaultState, action) {
const handler = reducers[action.type];
if (handler) return handler(state, action);
return state;
}

View file

@ -18,9 +18,17 @@ export const statsReducer = handleActions(
viewCountError: action.data, viewCountError: action.data,
}), }),
[ACTIONS.FETCH_VIEW_COUNT_COMPLETED]: (state, action) => { [ACTIONS.FETCH_VIEW_COUNT_COMPLETED]: (state, action) => {
const { claimId, viewCount } = action.data; const { claimIdCsv, viewCounts } = action.data;
const viewCountById = Object.assign({}, state.viewCountById);
const claimIds = claimIdCsv.split(',');
if (claimIds.length === viewCounts.length) {
claimIds.forEach((claimId, index) => {
viewCountById[claimId] = viewCounts[index];
});
}
const viewCountById = { ...state.viewCountById, [claimId]: viewCount };
return { return {
...state, ...state,
fetchingViewCount: false, fetchingViewCount: false,

View file

@ -1,237 +0,0 @@
// @flow
import * as ACTIONS from 'constants/action_types';
import { parseURI, ACTIONS as LBRY_REDUX_ACTIONS } from 'lbry-redux';
import { VIEW_ALL } from 'constants/subscriptions';
import { handleActions } from 'util/redux-utils';
const defaultState: SubscriptionState = {
enabledChannelNotifications: [],
subscriptions: [],
latest: {},
unread: {},
suggested: {},
loading: false,
viewMode: VIEW_ALL,
loadingSuggested: false,
firstRunCompleted: false,
showSuggestedSubs: false,
};
export default handleActions(
{
[ACTIONS.CHANNEL_SUBSCRIBE]: (
state: SubscriptionState,
action: DoChannelSubscribe
): SubscriptionState => {
const newSubscription: Subscription = action.data;
const newSubscriptions: Array<Subscription> = state.subscriptions.slice();
if (!newSubscriptions.some(sub => sub.uri === newSubscription.uri)) {
newSubscriptions.unshift(newSubscription);
}
return {
...state,
subscriptions: newSubscriptions,
};
},
[ACTIONS.CHANNEL_UNSUBSCRIBE]: (
state: SubscriptionState,
action: DoChannelUnsubscribe
): SubscriptionState => {
const subscriptionToRemove: Subscription = action.data;
const newSubscriptions = state.subscriptions
.slice()
.filter(subscription => subscription.channelName !== subscriptionToRemove.channelName);
// Check if we need to remove it from the 'unread' state
const { unread } = state;
if (unread[subscriptionToRemove.uri]) {
delete unread[subscriptionToRemove.uri];
}
return {
...state,
unread: { ...unread },
subscriptions: newSubscriptions,
};
},
[ACTIONS.SET_SUBSCRIPTION_LATEST]: (
state: SubscriptionState,
action: SetSubscriptionLatest
): SubscriptionState => {
const { subscription, uri } = action.data;
const newLatest = Object.assign({}, state.latest);
newLatest[subscription.uri] = uri;
return {
...state,
latest: newLatest,
};
},
[ACTIONS.UPDATE_SUBSCRIPTION_UNREADS]: (
state: SubscriptionState,
action: DoUpdateSubscriptionUnreads
): SubscriptionState => {
const { channel, uris, type } = action.data;
return {
...state,
unread: {
...state.unread,
[channel]: {
uris,
type,
},
},
};
},
[ACTIONS.REMOVE_SUBSCRIPTION_UNREADS]: (
state: SubscriptionState,
action: DoRemoveSubscriptionUnreads
): SubscriptionState => {
const { channel, uris } = action.data;
// If no channel is passed in, remove all unreads
let newUnread;
if (channel) {
newUnread = { ...state.unread };
if (!uris) {
delete newUnread[channel];
} else {
newUnread[channel].uris = uris;
}
} else {
newUnread = {};
}
return {
...state,
unread: {
...newUnread,
},
};
},
[ACTIONS.CHANNEL_SUBSCRIPTION_ENABLE_NOTIFICATIONS]: (
state: SubscriptionState,
action: DoChannelSubscriptionEnableNotifications
): SubscriptionState => {
const channelName = action.data;
const newEnabledChannelNotifications: Array<
string
> = state.enabledChannelNotifications.slice();
if (
channelName &&
channelName.trim().length > 0 &&
newEnabledChannelNotifications.indexOf(channelName) === -1
) {
newEnabledChannelNotifications.push(channelName);
}
return {
...state,
enabledChannelNotifications: newEnabledChannelNotifications,
};
},
[ACTIONS.CHANNEL_SUBSCRIPTION_DISABLE_NOTIFICATIONS]: (
state: SubscriptionState,
action: DoChannelSubscriptionDisableNotifications
): SubscriptionState => {
const channelName = action.data;
const newEnabledChannelNotifications: Array<
string
> = state.enabledChannelNotifications.slice();
const index = newEnabledChannelNotifications.indexOf(channelName);
if (index > -1) {
newEnabledChannelNotifications.splice(index, 1);
}
return {
...state,
enabledChannelNotifications: newEnabledChannelNotifications,
};
},
[ACTIONS.FETCH_SUBSCRIPTIONS_START]: (state: SubscriptionState): SubscriptionState => ({
...state,
loading: true,
}),
[ACTIONS.FETCH_SUBSCRIPTIONS_FAIL]: (state: SubscriptionState): SubscriptionState => ({
...state,
loading: false,
}),
[ACTIONS.FETCH_SUBSCRIPTIONS_SUCCESS]: (
state: SubscriptionState,
action: FetchedSubscriptionsSucess
): SubscriptionState => ({
...state,
loading: false,
subscriptions: action.data,
}),
[ACTIONS.SET_VIEW_MODE]: (
state: SubscriptionState,
action: SetViewMode
): SubscriptionState => ({
...state,
viewMode: action.data,
}),
[ACTIONS.GET_SUGGESTED_SUBSCRIPTIONS_START]: (state: SubscriptionState): SubscriptionState => ({
...state,
loadingSuggested: true,
}),
[ACTIONS.GET_SUGGESTED_SUBSCRIPTIONS_SUCCESS]: (
state: SubscriptionState,
action: GetSuggestedSubscriptionsSuccess
): SubscriptionState => ({
...state,
suggested: action.data,
loadingSuggested: false,
}),
[ACTIONS.GET_SUGGESTED_SUBSCRIPTIONS_FAIL]: (state: SubscriptionState): SubscriptionState => ({
...state,
loadingSuggested: false,
}),
[ACTIONS.SUBSCRIPTION_FIRST_RUN_COMPLETED]: (state: SubscriptionState): SubscriptionState => ({
...state,
firstRunCompleted: true,
}),
[ACTIONS.VIEW_SUGGESTED_SUBSCRIPTIONS]: (state: SubscriptionState): SubscriptionState => ({
...state,
showSuggestedSubs: true,
}),
[LBRY_REDUX_ACTIONS.USER_STATE_POPULATE]: (
state: SubscriptionState,
action: { data: { subscriptions: ?Array<string> } }
) => {
const { subscriptions } = action.data;
let newSubscriptions;
if (!subscriptions) {
newSubscriptions = state.subscriptions;
} else {
const parsedSubscriptions = subscriptions.map(uri => {
const { channelName } = parseURI(uri);
return {
uri,
channelName: `@${channelName}`,
};
});
if (!state.subscriptions || !state.subscriptions.length) {
newSubscriptions = parsedSubscriptions;
} else {
const map = {};
newSubscriptions = parsedSubscriptions.concat(state.subscriptions).filter(sub => {
return map[sub.uri] ? false : (map[sub.uri] = true);
}, {});
}
}
return {
...state,
subscriptions: newSubscriptions,
};
},
},
defaultState
);

View file

@ -1,297 +0,0 @@
import * as ACTIONS from 'constants/action_types';
const reducers = {};
const defaultState = {
authenticationIsPending: false,
userIsPending: false,
emailNewIsPending: false,
emailNewErrorMessage: '',
emailToVerify: '',
emailAlreadyExists: false,
resendingVerificationEmail: false,
inviteNewErrorMessage: '',
inviteNewIsPending: false,
inviteStatusIsPending: false,
invitesRemaining: undefined,
invitees: undefined,
referralLink: undefined,
referralCode: undefined,
user: undefined,
accessToken: undefined,
youtubeChannelImportPending: false,
youtubeChannelImportErrorMessage: '',
referrerSetIsPending: false,
referrerSetError: '',
};
reducers[ACTIONS.AUTHENTICATION_STARTED] = state =>
Object.assign({}, state, {
authenticationIsPending: true,
userIsPending: true,
accessToken: defaultState.accessToken,
});
reducers[ACTIONS.AUTHENTICATION_SUCCESS] = (state, action) =>
Object.assign({}, state, {
authenticationIsPending: false,
userIsPending: false,
accessToken: action.data.accessToken,
user: action.data.user,
});
reducers[ACTIONS.AUTHENTICATION_FAILURE] = state =>
Object.assign({}, state, {
authenticationIsPending: false,
userIsPending: false,
user: null,
});
reducers[ACTIONS.USER_FETCH_STARTED] = state =>
Object.assign({}, state, {
userIsPending: true,
});
reducers[ACTIONS.USER_FETCH_SUCCESS] = (state, action) =>
Object.assign({}, state, {
userIsPending: false,
user: action.data.user,
emailToVerify: action.data.user.has_verified_email ? null : state.emailToVerify,
});
reducers[ACTIONS.USER_FETCH_FAILURE] = state =>
Object.assign({}, state, {
userIsPending: true,
user: null,
});
reducers[ACTIONS.USER_PHONE_NEW_STARTED] = (state, action) => {
const user = Object.assign({}, state.user);
user.country_code = action.data.country_code;
return Object.assign({}, state, {
phoneNewIsPending: true,
phoneNewErrorMessage: '',
user,
});
};
reducers[ACTIONS.USER_PHONE_NEW_SUCCESS] = (state, action) =>
Object.assign({}, state, {
phoneToVerify: action.data.phone,
phoneNewIsPending: false,
});
reducers[ACTIONS.USER_PHONE_RESET] = state =>
Object.assign({}, state, {
phoneToVerify: null,
});
reducers[ACTIONS.USER_PHONE_NEW_FAILURE] = (state, action) =>
Object.assign({}, state, {
phoneNewIsPending: false,
phoneNewErrorMessage: action.data.error,
});
reducers[ACTIONS.USER_PHONE_VERIFY_STARTED] = state =>
Object.assign({}, state, {
phoneVerifyIsPending: true,
phoneVerifyErrorMessage: '',
});
reducers[ACTIONS.USER_PHONE_VERIFY_SUCCESS] = (state, action) =>
Object.assign({}, state, {
phoneToVerify: '',
phoneVerifyIsPending: false,
user: action.data.user,
});
reducers[ACTIONS.USER_PHONE_VERIFY_FAILURE] = (state, action) =>
Object.assign({}, state, {
phoneVerifyIsPending: false,
phoneVerifyErrorMessage: action.data.error,
});
reducers[ACTIONS.USER_EMAIL_NEW_STARTED] = state =>
Object.assign({}, state, {
emailNewIsPending: true,
emailNewErrorMessage: '',
emailAlreadyExists: false,
});
reducers[ACTIONS.USER_EMAIL_NEW_SUCCESS] = (state, action) => {
const user = Object.assign({}, state.user);
user.primary_email = action.data.email;
return Object.assign({}, state, {
emailToVerify: action.data.email,
emailNewIsPending: false,
user,
});
};
reducers[ACTIONS.USER_EMAIL_NEW_EXISTS] = state =>
Object.assign({}, state, {
emailAlreadyExists: true,
});
reducers[ACTIONS.USER_EMAIL_NEW_FAILURE] = (state, action) =>
Object.assign({}, state, {
emailNewIsPending: false,
emailNewErrorMessage: action.data.error,
});
reducers[ACTIONS.USER_EMAIL_VERIFY_STARTED] = state =>
Object.assign({}, state, {
emailVerifyIsPending: true,
emailVerifyErrorMessage: '',
});
reducers[ACTIONS.USER_EMAIL_VERIFY_SUCCESS] = (state, action) => {
const user = Object.assign({}, state.user);
user.primary_email = action.data.email;
return Object.assign({}, state, {
emailToVerify: '',
emailVerifyIsPending: false,
user,
});
};
reducers[ACTIONS.USER_EMAIL_VERIFY_FAILURE] = (state, action) =>
Object.assign({}, state, {
emailVerifyIsPending: false,
emailVerifyErrorMessage: action.data.error,
});
reducers[ACTIONS.USER_EMAIL_VERIFY_SET] = (state, action) =>
Object.assign({}, state, {
emailToVerify: action.data.email,
});
reducers[ACTIONS.USER_IDENTITY_VERIFY_STARTED] = state =>
Object.assign({}, state, {
identityVerifyIsPending: true,
identityVerifyErrorMessage: '',
});
reducers[ACTIONS.USER_IDENTITY_VERIFY_SUCCESS] = (state, action) =>
Object.assign({}, state, {
identityVerifyIsPending: false,
identityVerifyErrorMessage: '',
user: action.data.user,
});
reducers[ACTIONS.USER_IDENTITY_VERIFY_FAILURE] = (state, action) =>
Object.assign({}, state, {
identityVerifyIsPending: false,
identityVerifyErrorMessage: action.data.error,
});
reducers[ACTIONS.FETCH_ACCESS_TOKEN_SUCCESS] = (state, action) => {
const { token } = action.data;
return Object.assign({}, state, {
accessToken: token,
});
};
reducers[ACTIONS.USER_INVITE_STATUS_FETCH_STARTED] = state =>
Object.assign({}, state, {
inviteStatusIsPending: true,
});
reducers[ACTIONS.USER_INVITE_STATUS_FETCH_SUCCESS] = (state, action) =>
Object.assign({}, state, {
inviteStatusIsPending: false,
invitesRemaining: action.data.invitesRemaining,
invitees: action.data.invitees,
referralLink: action.data.referralLink,
referralCode: action.data.referralCode,
});
reducers[ACTIONS.USER_INVITE_NEW_STARTED] = state =>
Object.assign({}, state, {
inviteNewIsPending: true,
inviteNewErrorMessage: '',
});
reducers[ACTIONS.USER_INVITE_NEW_SUCCESS] = state =>
Object.assign({}, state, {
inviteNewIsPending: false,
inviteNewErrorMessage: '',
});
reducers[ACTIONS.USER_INVITE_NEW_FAILURE] = (state, action) =>
Object.assign({}, state, {
inviteNewIsPending: false,
inviteNewErrorMessage: action.data.error.message,
});
reducers[ACTIONS.USER_INVITE_STATUS_FETCH_FAILURE] = state =>
Object.assign({}, state, {
inviteStatusIsPending: false,
invitesRemaining: null,
invitees: null,
});
reducers[ACTIONS.USER_YOUTUBE_IMPORT_STARTED] = state =>
Object.assign({}, state, {
youtubeChannelImportPending: true,
youtubeChannelImportErrorMessage: '',
});
reducers[ACTIONS.USER_YOUTUBE_IMPORT_SUCCESS] = (state, action) => {
const total = action.data.reduce((acc, value) => acc + value.total_published_videos, 0);
const complete = action.data.reduce((acc, value) => acc + value.total_transferred, 0);
return Object.assign({}, state, {
youtubeChannelImportPending: false,
youtubeChannelImportErrorMessage: '',
youtubeChannelImportTotal: total,
youtubeChannelImportComplete: complete,
});
};
reducers[ACTIONS.USER_YOUTUBE_IMPORT_FAILURE] = (state, action) =>
Object.assign({}, state, {
youtubeChannelImportPending: false,
youtubeChannelImportErrorMessage: action.data,
});
reducers[ACTIONS.USER_EMAIL_VERIFY_RETRY_STARTED] = state =>
Object.assign({}, state, {
resendingVerificationEmail: true,
});
reducers[ACTIONS.USER_EMAIL_VERIFY_RETRY_SUCCESS] = state =>
Object.assign({}, state, {
resendingVerificationEmail: false,
});
reducers[ACTIONS.USER_EMAIL_VERIFY_RETRY_FAILURE] = state =>
Object.assign({}, state, {
resendingVerificationEmail: false,
});
reducers[ACTIONS.USER_SET_REFERRER_STARTED] = state =>
Object.assign({}, state, {
referrerSetIsPending: true,
referrerSetError: defaultState.referrerSetError,
});
reducers[ACTIONS.USER_SET_REFERRER_SUCCESS] = state =>
Object.assign({}, state, {
referrerSetIsPending: false,
referrerSetError: defaultState.referrerSetError,
});
reducers[ACTIONS.USER_SET_REFERRER_FAILURE] = (state, action) =>
Object.assign({}, state, {
referrerSetIsPending: false,
referrerSetError: action.data.error.message,
});
export function userReducer(state = defaultState, action) {
const handler = reducers[action.type];
if (handler) return handler(state, action);
return state;
}

View file

@ -55,7 +55,7 @@ reducers[ACTIONS.UPDATE_UPLOAD_PROGRESS] = (state: TvState, action) => {
return { ...state, currentUploads }; return { ...state, currentUploads };
}; };
export function lbrytvReducer(state = defaultState, action) { export function webReducer(state = defaultState, action) {
const handler = reducers[action.type]; const handler = reducers[action.type];
if (handler) return handler(state, action); if (handler) return handler(state, action);
return state; return state;

View file

@ -6,3 +6,15 @@ export const selectBlackListedOutpoints = createSelector(
selectState, selectState,
state => state.blackListedOutpoints state => state.blackListedOutpoints
); );
export const selectBlacklistedOutpointMap = createSelector(
selectBlackListedOutpoints,
outpoints =>
outpoints
? outpoints.reduce((acc, val) => {
const outpoint = `${val.txid}:${val.nout}`;
acc[outpoint] = 1;
return acc;
}, {})
: {}
);

View file

@ -6,3 +6,15 @@ export const selectFilteredOutpoints = createSelector(
selectState, selectState,
state => state.filteredOutpoints state => state.filteredOutpoints
); );
export const selectFilteredOutpointMap = createSelector(
selectFilteredOutpoints,
outpoints =>
outpoints
? outpoints.reduce((acc, val) => {
const outpoint = `${val.txid}:${val.nout}`;
acc[outpoint] = 1;
return acc;
}, {})
: {}
);

View file

@ -1,76 +0,0 @@
import { createSelector } from 'reselect';
import REWARDS from 'rewards';
const selectState = state => state.rewards || {};
export const selectUnclaimedRewardsByType = createSelector(
selectState,
state => state.unclaimedRewardsByType
);
export const selectClaimedRewardsById = createSelector(
selectState,
state => state.claimedRewardsById
);
export const selectClaimedRewards = createSelector(
selectClaimedRewardsById,
byId => Object.values(byId) || []
);
export const selectClaimedRewardsByTransactionId = createSelector(selectClaimedRewards, rewards =>
rewards.reduce((mapParam, reward) => {
const map = mapParam;
map[reward.transaction_id] = reward;
return map;
}, {})
);
export const selectUnclaimedRewards = createSelector(selectState, state => state.unclaimedRewards);
export const selectFetchingRewards = createSelector(selectState, state => !!state.fetching);
export const selectUnclaimedRewardValue = createSelector(selectUnclaimedRewards, rewards =>
rewards.reduce((sum, reward) => sum + reward.reward_amount, 0)
);
export const selectClaimsPendingByType = createSelector(
selectState,
state => state.claimPendingByType
);
const selectIsClaimRewardPending = (state, props) =>
selectClaimsPendingByType(state, props)[props.reward_type];
export const makeSelectIsRewardClaimPending = () =>
createSelector(selectIsClaimRewardPending, isClaiming => isClaiming);
export const selectClaimErrorsByType = createSelector(
selectState,
state => state.claimErrorsByType
);
const selectClaimRewardError = (state, props) =>
selectClaimErrorsByType(state, props)[props.reward_type];
export const makeSelectClaimRewardError = () =>
createSelector(selectClaimRewardError, errorMessage => errorMessage);
const selectRewardByType = (state, rewardType) =>
selectUnclaimedRewards(state).find(reward => reward.reward_type === rewardType);
export const makeSelectRewardByType = () => createSelector(selectRewardByType, reward => reward);
export const makeSelectRewardAmountByType = () =>
createSelector(selectRewardByType, reward => (reward ? reward.reward_amount : 0));
export const selectRewardContentClaimIds = createSelector(
selectState,
state => state.rewardedContentClaimIds
);
export const selectReferralReward = createSelector(
selectUnclaimedRewards,
unclaimedRewards =>
unclaimedRewards.filter(reward => reward.reward_type === REWARDS.TYPE_REFERRAL)[0]
);

View file

@ -1,283 +0,0 @@
import { SUGGESTED_FEATURED, SUGGESTED_TOP_SUBSCRIBED } from 'constants/subscriptions';
import { createSelector } from 'reselect';
import {
selectAllClaimsByChannel,
selectClaimsById,
selectAllFetchingChannelClaims,
makeSelectChannelForClaimUri,
selectClaimsByUri,
parseURI,
} from 'lbry-redux';
import { swapKeyAndValue } from 'util/swap-json';
// Returns the entire subscriptions state
const selectState = state => state.subscriptions || {};
// Returns the list of channel uris a user is subscribed to
export const selectSubscriptions = createSelector(selectState, state => state.subscriptions);
// Fetching list of users subscriptions
export const selectIsFetchingSubscriptions = createSelector(selectState, state => state.loading);
// The current view mode on the subscriptions page
export const selectViewMode = createSelector(selectState, state => state.viewMode);
// Suggested subscriptions from internal apis
export const selectSuggested = createSelector(selectState, state => state.suggested);
export const selectIsFetchingSuggested = createSelector(
selectState,
state => state.loadingSuggested
);
export const selectSuggestedChannels = createSelector(
selectSubscriptions,
selectSuggested,
(userSubscriptions, suggested) => {
if (!suggested) {
return null;
}
// Swap the key/value because we will use the uri for everything, this just makes it easier
// suggested is returned from the api with the form:
// {
// featured: { "Channel label": uri, ... },
// top_subscribed: { "@channel": uri, ... }
// top_bid: { "@channel": uri, ... }
// }
// To properly compare the suggested subscriptions from our current subscribed channels
// We only care about the uri, not the label
// We also only care about top_subscribed and featured
// top_bid could just be porn or a channel with no content
const topSubscribedSuggestions = swapKeyAndValue(suggested[SUGGESTED_TOP_SUBSCRIBED]);
const featuredSuggestions = swapKeyAndValue(suggested[SUGGESTED_FEATURED]);
// Make sure there are no duplicates
// If a uri isn't already in the suggested object, add it
const suggestedChannels = { ...topSubscribedSuggestions };
Object.keys(featuredSuggestions).forEach(uri => {
if (!suggestedChannels[uri]) {
const channelLabel = featuredSuggestions[uri];
suggestedChannels[uri] = channelLabel;
}
});
userSubscriptions.forEach(({ uri }) => {
// Note to passer bys:
// Maybe we should just remove the `lbry://` prefix from subscription uris
// Most places don't store them like that
const subscribedUri = uri.slice('lbry://'.length);
if (suggestedChannels[subscribedUri]) {
delete suggestedChannels[subscribedUri];
}
});
return Object.keys(suggestedChannels)
.map(uri => ({
uri,
label: suggestedChannels[uri],
}))
.slice(0, 5);
}
);
export const selectFirstRunCompleted = createSelector(
selectState,
state => state.firstRunCompleted
);
export const selectShowSuggestedSubs = createSelector(
selectState,
state => state.showSuggestedSubs
);
// Fetching any claims that are a part of a users subscriptions
export const selectSubscriptionsBeingFetched = createSelector(
selectSubscriptions,
selectAllFetchingChannelClaims,
(subscriptions, fetchingChannelClaims) => {
const fetchingSubscriptionMap = {};
subscriptions.forEach(sub => {
const isFetching = fetchingChannelClaims && fetchingChannelClaims[sub.uri];
if (isFetching) {
fetchingSubscriptionMap[sub.uri] = true;
}
});
return fetchingSubscriptionMap;
}
);
export const selectUnreadByChannel = createSelector(selectState, state => state.unread);
// Returns the current total of unread subscriptions
export const selectUnreadAmount = createSelector(selectUnreadByChannel, unreadByChannel => {
const unreadChannels = Object.keys(unreadByChannel);
let badges = 0;
if (!unreadChannels.length) {
return badges;
}
unreadChannels.forEach(channel => {
badges += unreadByChannel[channel].uris.length;
});
return badges;
});
// Returns the uris with channels as an array with the channel with the newest content first
// If you just want the `unread` state, use selectUnread
export const selectUnreadSubscriptions = createSelector(
selectUnreadAmount,
selectUnreadByChannel,
selectClaimsByUri,
(unreadAmount, unreadByChannel, claimsByUri) => {
// determine which channel has the newest content
const unreadList = [];
if (!unreadAmount) {
return unreadList;
}
const channelUriList = Object.keys(unreadByChannel);
// There is only one channel with unread notifications
if (unreadAmount === 1) {
channelUriList.forEach(channel => {
const unreadChannel = {
channel,
uris: unreadByChannel[channel].uris,
};
unreadList.push(unreadChannel);
});
return unreadList;
}
channelUriList
.sort((channel1, channel2) => {
const latestUriFromChannel1 = unreadByChannel[channel1].uris[0];
const latestClaimFromChannel1 = claimsByUri[latestUriFromChannel1] || {};
const latestUriFromChannel2 = unreadByChannel[channel2].uris[0];
const latestClaimFromChannel2 = claimsByUri[latestUriFromChannel2] || {};
const latestHeightFromChannel1 = latestClaimFromChannel1.height || 0;
const latestHeightFromChannel2 = latestClaimFromChannel2.height || 0;
if (latestHeightFromChannel1 !== latestHeightFromChannel2) {
return latestHeightFromChannel2 - latestHeightFromChannel1;
}
return 0;
})
.forEach(channel => {
const unreadSubscription = unreadByChannel[channel];
const unreadChannel = {
channel,
uris: unreadSubscription.uris,
};
unreadList.push(unreadChannel);
});
return unreadList;
}
);
// Returns all unread subscriptions for a uri passed in
export const makeSelectUnreadByChannel = uri =>
createSelector(selectUnreadByChannel, unread => unread[uri]);
// Returns the first page of claims for every channel a user is subscribed to
export const selectSubscriptionClaims = createSelector(
selectAllClaimsByChannel,
selectClaimsById,
selectSubscriptions,
selectUnreadByChannel,
(channelIds, allClaims, savedSubscriptions, unreadByChannel) => {
// no claims loaded yet
if (!Object.keys(channelIds).length) {
return [];
}
let fetchedSubscriptions = [];
savedSubscriptions.forEach(subscription => {
let channelClaims = [];
// if subscribed channel has content
if (channelIds[subscription.uri] && channelIds[subscription.uri]['1']) {
// This will need to be more robust, we will want to be able to load more than the first page
// Strip out any ids that will be shown as notifications
const pageOneChannelIds = channelIds[subscription.uri]['1'];
// we have the channel ids and the corresponding claims
// loop over the list of ids and grab the claim
pageOneChannelIds.forEach(id => {
const grabbedClaim = allClaims[id];
if (
unreadByChannel[subscription.uri] &&
unreadByChannel[subscription.uri].uris.some(uri => uri.includes(id))
) {
grabbedClaim.isNew = true;
}
channelClaims = channelClaims.concat([grabbedClaim]);
});
}
fetchedSubscriptions = fetchedSubscriptions.concat(channelClaims);
});
return fetchedSubscriptions;
}
);
// Returns true if a user is subscribed to the channel associated with the uri passed in
// Accepts content or channel uris
export const makeSelectIsSubscribed = uri =>
createSelector(
selectSubscriptions,
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;
}
);
export const makeSelectIsNew = uri =>
createSelector(
makeSelectIsSubscribed(uri),
makeSelectChannelForClaimUri(uri),
selectUnreadByChannel,
(isSubscribed, channel, unreadByChannel) => {
if (!isSubscribed) {
return false;
}
const unreadForChannel = unreadByChannel[`lbry://${channel}`];
if (unreadForChannel) {
return unreadForChannel.uris.includes(uri);
}
return false;
// If they are subscribed, check to see if this uri is in the list of unreads
}
);
export const selectEnabledChannelNotifications = createSelector(
selectState,
state => state.enabledChannelNotifications
);

View file

@ -1,184 +0,0 @@
import { createSelector } from 'reselect';
export const selectState = state => state.user || {};
export const selectAuthenticationIsPending = createSelector(
selectState,
state => state.authenticationIsPending
);
export const selectUserIsPending = createSelector(selectState, state => state.userIsPending);
export const selectUser = createSelector(selectState, state => state.user);
export const selectEmailAlreadyExists = createSelector(
selectState,
state => state.emailAlreadyExists
);
export const selectResendingVerificationEmail = createSelector(
selectState,
state => state.resendingVerificationEmail
);
export const selectUserEmail = createSelector(
selectUser,
user => (user ? user.primary_email : null)
);
export const selectUserPhone = createSelector(
selectUser,
user => (user ? user.phone_number : null)
);
export const selectUserCountryCode = createSelector(
selectUser,
user => (user ? user.country_code : null)
);
export const selectEmailToVerify = createSelector(
selectState,
selectUserEmail,
(state, userEmail) => state.emailToVerify || userEmail
);
export const selectPhoneToVerify = createSelector(
selectState,
selectUserPhone,
(state, userPhone) => state.phoneToVerify || userPhone
);
export const selectYoutubeChannels = createSelector(
selectUser,
user => (user ? user.youtube_channels : null)
);
export const selectUserIsRewardApproved = createSelector(
selectUser,
user => user && user.is_reward_approved
);
export const selectEmailNewIsPending = createSelector(
selectState,
state => state.emailNewIsPending
);
export const selectEmailNewErrorMessage = createSelector(
selectState,
state => state.emailNewErrorMessage
);
export const selectPhoneNewErrorMessage = createSelector(
selectState,
state => state.phoneNewErrorMessage
);
export const selectEmailVerifyIsPending = createSelector(
selectState,
state => state.emailVerifyIsPending
);
export const selectEmailVerifyErrorMessage = createSelector(
selectState,
state => state.emailVerifyErrorMessage
);
export const selectPhoneNewIsPending = createSelector(
selectState,
state => state.phoneNewIsPending
);
export const selectPhoneVerifyIsPending = createSelector(
selectState,
state => state.phoneVerifyIsPending
);
export const selectPhoneVerifyErrorMessage = createSelector(
selectState,
state => state.phoneVerifyErrorMessage
);
export const selectIdentityVerifyIsPending = createSelector(
selectState,
state => state.identityVerifyIsPending
);
export const selectIdentityVerifyErrorMessage = createSelector(
selectState,
state => state.identityVerifyErrorMessage
);
export const selectUserVerifiedEmail = createSelector(
selectUser,
user => user && user.has_verified_email
);
export const selectUserIsVerificationCandidate = createSelector(
selectUser,
user => user && (!user.has_verified_email || !user.is_identity_verified)
);
export const selectAccessToken = createSelector(selectState, state => state.accessToken);
export const selectUserInviteStatusIsPending = createSelector(
selectState,
state => state.inviteStatusIsPending
);
export const selectUserInvitesRemaining = createSelector(
selectState,
state => state.invitesRemaining
);
export const selectUserInvitees = createSelector(selectState, state => state.invitees);
export const selectUserInviteStatusFailed = createSelector(
selectUserInvitesRemaining,
() => selectUserInvitesRemaining === null
);
export const selectUserInviteNewIsPending = createSelector(
selectState,
state => state.inviteNewIsPending
);
export const selectUserInviteNewErrorMessage = createSelector(
selectState,
state => state.inviteNewErrorMessage
);
export const selectUserInviteReferralLink = createSelector(
selectState,
state => state.referralLink
);
export const selectUserInviteReferralCode = createSelector(
selectState,
state => (state.referralCode ? state.referralCode[0] : '')
);
export const selectYouTubeImportPending = createSelector(
selectState,
state => state.youtubeChannelImportPending
);
export const selectYouTubeImportError = createSelector(
selectState,
state => state.youtubeChannelImportErrorMessage
);
export const selectSetReferrerPending = createSelector(
selectState,
state => state.referrerSetIsPending
);
export const selectSetReferrerError = createSelector(selectState, state => state.referrerSetError);
export const selectYouTubeImportVideosComplete = createSelector(selectState, state => {
const total = state.youtubeChannelImportTotal;
const complete = state.youtubeChannelImportComplete || 0;
if (total) {
return [complete, total];
}
});

View file

@ -1,6 +1,6 @@
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
const selectState = state => state.lbrytv || {}; const selectState = state => state.web || {};
export const selectCurrentUploads = createSelector(selectState, state => state.currentUploads); export const selectCurrentUploads = createSelector(selectState, state => state.currentUploads);

View file

@ -1,127 +0,0 @@
import { Lbry, doToast } from 'lbry-redux';
import Lbryio from 'lbryio';
const rewards = {};
rewards.TYPE_NEW_DEVELOPER = 'new_developer';
rewards.TYPE_NEW_USER = 'new_user';
rewards.TYPE_CONFIRM_EMAIL = 'email_provided';
rewards.TYPE_FIRST_CHANNEL = 'new_channel';
rewards.TYPE_FIRST_STREAM = 'first_stream';
rewards.TYPE_MANY_DOWNLOADS = 'many_downloads';
rewards.TYPE_FIRST_PUBLISH = 'first_publish';
rewards.TYPE_REFERRAL = 'referral';
rewards.TYPE_REFEREE = 'referee';
rewards.TYPE_REWARD_CODE = 'reward_code';
rewards.TYPE_SUBSCRIPTION = 'subscription';
rewards.YOUTUBE_CREATOR = 'youtube_creator';
rewards.TYPE_DAILY_VIEW = 'daily_view';
rewards.claimReward = (type, rewardParams) => {
function requestReward(resolve, reject, params) {
if (!Lbryio.enabled) {
reject(new Error(__('Rewards are not enabled.')));
return;
}
Lbryio.call('reward', 'claim', params, 'post').then(reward => {
const message =
reward.reward_notification || `You have claimed a ${reward.reward_amount} LBC reward.`;
// Display global notice
const action = doToast({
message,
linkText: __('Show All'),
linkTarget: '/rewards',
});
window.store.dispatch(action);
if (rewards.callbacks.claimRewardSuccess) {
rewards.callbacks.claimRewardSuccess();
}
resolve(reward);
}, reject);
}
return new Promise((resolve, reject) => {
Lbry.address_unused().then(address => {
const params = {
reward_type: type,
wallet_address: address,
...rewardParams,
};
switch (type) {
case rewards.TYPE_FIRST_CHANNEL:
Lbry.channel_list({ page: 1, page_size: 10 })
.then(claims => {
const claim =
claims.items &&
claims.items.find(
foundClaim =>
foundClaim.name.length &&
foundClaim.name[0] === '@' &&
foundClaim.txid.length &&
foundClaim.type === 'claim'
);
if (claim) {
params.transaction_id = claim.txid;
requestReward(resolve, reject, params);
} else {
reject(new Error(__('Please create a channel identity first.')));
}
})
.catch(reject);
break;
case rewards.TYPE_FIRST_PUBLISH:
Lbry.stream_list({ page: 1, page_size: 10 })
.then(claims => {
const claim =
claims.items &&
claims.items.find(
foundClaim =>
foundClaim.name.length &&
foundClaim.name[0] !== '@' &&
foundClaim.txid.length &&
foundClaim.type === 'claim'
);
if (claim) {
params.transaction_id = claim.txid;
requestReward(resolve, reject, params);
} else {
reject(
claims.length
? new Error(
__(
'Please publish something and wait for confirmation by the network to claim this reward.'
)
)
: new Error(__('Please publish something to claim this reward.'))
);
}
})
.catch(reject);
break;
case rewards.TYPE_FIRST_STREAM:
case rewards.TYPE_NEW_USER:
default:
requestReward(resolve, reject, params);
}
});
});
};
rewards.callbacks = {
// Set any callbacks that require code not found in this project
claimRewardSuccess: null,
claimFirstRewardSuccess: null,
rewardApprovalRequired: null,
};
rewards.setCallback = (name, method) => {
rewards.callbacks[name] = method;
};
export default rewards;