Compare commits

...

94 commits

Author SHA1 Message Date
Anthony
11a6c78928 seems to be working 2021-11-07 15:06:11 -05:00
Anthony
d5e582050a snipe changes for ios 2021-11-07 15:06:11 -05:00
Thomas Zarebczan
a77e59cb53
Adjust video state clearing settings
What's strange is that this only occurs when you refresh odysee in between the plays. Might be a bug there.
2021-10-28 11:48:39 -04:00
infinite-persistence
4b0a06cef7
Ensure filter is not expanded when disabled (#153)
## Issue
- Go to a channel page
- Go to Wild West
- Back
- Expand the search filter (valid here)
- Forward

## Fix
Resolve the 'expanded' setting on mount to ensure it is never true when 'hideAdvancedFilter' is set.
2021-10-28 10:51:13 -04:00
mayeaux
00d28fe26e
fix bug where scrolling on video player page changes volume (#154) 2021-10-28 10:46:24 -04:00
infinite-persistence
5dd5826b33
doFetchSubCount: batch support; fetch interval gap;
1. The API supports batching -- updated the code to use that. Retained string as the parameter (instead of changing it to array) so that existing clients won't be affected.

2. Make `doFetchSubCount` a batched command by default through an idle timer. This way, none of the clients need to collect IDs -- it's all done behind the scenes.

3. Added minimum of 5 minutes between each sub-count fetch for a claim ID.
2021-10-28 13:16:06 +08:00
infinite-persistence
cbedc4b933
ClaimPreviewSubtitle: fetch sub count only for channels 2021-10-28 13:11:25 +08:00
infinite-persistence
6b39fc1bbb
Make it easier for the next person to add View Count in specific pages. 2021-10-28 13:11:24 +08:00
infinite-persistence
f8f9b86cb4
FileViewCountInline: fix incorrect logic
4a22814c broke the intention of if-block (it essentially breaks the functionality in Search page if we enable view counts there in the future).

It also seems completely unrelated to the PR.
2021-10-28 13:11:23 +08:00
jessopb
1a5fb5fa51
improve channel mentions (#146)
something like @; was crashing the app.
this should be better.
2021-10-27 21:21:40 -04:00
saltrafael
c24153c6ca
[New Feature] Comment Emotes (#125)
* Refactor form-field

* Create new Emote Menu

* Add Emotes

* Add Emote Selector and Emote Comment creation ability

* Fix and Split CSS
2021-10-27 14:20:47 -04:00
Dan Peterson
762bddb158
Don't instantiate messaging SDK if service worker is unavailable (Firefox/Private) (#142) 2021-10-27 13:34:50 -04:00
infinite-persistence
2922f0f2dc Revert "Code-split homepages"
This reverts commit 310fc81bd9.

Was breaking the `get` api
2021-10-28 01:33:46 +08:00
mayeaux
3849683a59
Lots of player UI improvements (#134)
* various control bar fixes

* fixes for mobile

* hide advertisement div by default

* fix duration bar

* more frontend touchups

* more styles

* fix for advertisement bar showing

* dont use ima on each re-render
2021-10-27 11:08:12 -04:00
infinite-persistence
247ee757d1
ChunkLoadError: ask user to reload instead of automatically reloading (#139)
## Issue
We previously automatically reload when there is a chunk error. This works fine if it's the case of new code was pushed recently while the user was active. But if the failure was caused by other things like network problems or the file IS actually missing, we end up in an infinite loop of refreshes.

## New approach
Tell the user to reload instead of automatically doing it.
2021-10-27 11:07:06 -04:00
Dan Peterson
03f69eff86
Browser push notifications (#133)
* fix type error

fix is subscribed check

- Persist subscription data locally
- add / remove subscription during log in / out
- Use store directly in hook

Add toast error if subscription fails

Revert removal of v2

hotfix linting issue

Add custom notification handler

- fix isSupported flag
- make icon color compatible with light/dark theme
- fix icon on notifications blocked banner

wip: add push notification banner to notifications page.

- ignore failed deletions via internal API
- add ua parsing package
- add more robust meta data to token save

refactor naming + add push toggle to notification button

shift some code around

update css naming o proper BEM notation

update notifications UI

remove now unneeded util function

Update push notification system to sue firebase sdk

separate service worker webpack bundling

update service worker to use firebase sdk

Add firebase config

Add firebase and remove filemanager

Stub out the basics for browser push notifications.

* fix safari

* try smaller image for badge

* add token validation with server, refactor code

* remove param

* add special icon for web notification badge

* add translations

* add missing trans for toast error

* add pushRequest method that will not prompt users who have subscribed but since disabled notifications in the settings.
2021-10-27 10:38:10 -04:00
Thomas Zarebczan
08adb805e9
Fix syntax 2021-10-27 09:55:57 -04:00
infinite-persistence
3788ef58ec
Analyze all .js files to get the full picture. 2021-10-27 09:19:52 +08:00
infinite-persistence
310fc81bd9
Code-split homepages
## Ticket
97

## Issue
8% of the ui.js chunk consists of the 5 custom homepages
2021-10-27 07:55:24 +08:00
infinite-persistence
9569b6c7a5
Upgrade videojs to 7.15.4 that ads is using. (#130)
## Issue
93 videojs double bundle
2021-10-26 17:48:22 -04:00
Thomas Zarebczan
0febd32c71
Revert "player background color (#86)" (#132)
This reverts commit e14ec9b83e.
2021-10-26 14:47:02 -04:00
infinite-persistence
a90c516c71
Reduce impact of scanning blocklists (#121)
## Issue
- Each tile was checking against 4 blocklists (blacklisted, filtered, muted, commentron) on every render. Loading the front-page with Cheese alone caused 1400 calls.
- This is also part of the reason why pressing Back into the tile list takes forever.

## Fix
Since we still need to perform the checks at the app side for now, tried to memoize the operation through a selector.
2021-10-25 10:56:31 -04:00
infinite-persistence
dad7264636
Handle re-reselect warning on null/undefined key
Should be a harmless warning, but cleaning up nonetheless.
2021-10-25 13:26:10 +08:00
infinite-persistence
27f346d8f1
Don't memoize selectors without transformation
It was not meant to be used for these cases -- wasting resources creating and going through the cache for each simple direct access.
2021-10-24 13:05:06 +08:00
infinite-persistence
e2176d0566
Don't connect to the Redux store when not needed.
The subscription still costs something per update cycle even when the parameters are null or empty objects.
2021-10-24 13:04:01 +08:00
GG2015
17121b2066
Wallet swap disabled (#107)
* Disabled wallet swap tab.

* Cleaned up UI since Swap is disabled.
2021-10-22 10:48:34 -04:00
infinite-persistence
1b43c54725
Defer blocklists slightly to not block me
Now, with the exception of connecting to lbry.com after re-opening the browser (i.e. establishing first connection), refreshing odysee.com is almost instantaneous.
2021-10-22 17:31:39 +08:00
infinite-persistence
398388de10
Track duration of startup events
Tracking only `user/me` for now.
2021-10-22 16:02:21 +08:00
infinite-persistence
b8c763f749
ClaimList: fix render due to un-memo'd callback. 2021-10-22 12:20:29 +08:00
infinite-persistence
b4f62e78de
Additional GA events via redux/lbryio hook (#110) 2021-10-22 11:07:48 +08:00
infinite-persistence
8c4224f1ce
createAnalyticsMiddleware: Handle 'BATCH_ACTIONS' 2021-10-22 10:56:44 +08:00
infinite-persistence
b7685a151d
Additional GA events via redux/lbryio hook
## Issue
85 Add additional GA events

## Approach
Instead of placing analytic calls all over the GUI code (no separation of concerns), try to do it through a redux middleware instead.

## Changes
- Updated GA event and parameter naming after understanding how reporting works.
- Removed unused analytics.
2021-10-22 10:56:43 +08:00
GG2015
e14ec9b83e
player background color (#86)
Changes player background color

Co-authored-by: Thomas Zarebczan <thomas.zarebczan@gmail.com>
2021-10-21 16:43:08 -04:00
Thomas Zarebczan
23525b0baa
FAQ stuff (#109) 2021-10-21 16:21:51 -04:00
Dan Peterson
d62f63aff8
delete duplicate flow type files (#105)
* delete duplicate flow type files

* merge types from deleted files

* revert dispatch to type any. (linting issues)
2021-10-20 13:33:31 -05:00
jessopb
dcd00c2308
Fix top search for channels (#104) 2021-10-20 12:55:21 -04:00
mayeaux
c782f73f30
switch macro (#102)
* switch macro

* allow skip and other options
2021-10-20 11:14:33 -04:00
infinite-persistence
6ff9a51058
Upgrade codemirror + module sharing
## Issue
Our `<CodeViewer>` and `react-simplemde-editor` uses `codemirror`, and they were each bundling a different version.

## Change
Re-generate yarn.lock for `codemirror`. Since we are upgrading anyway, upgraded to the latest and greatest.

## Test
- [x] Markdown editor -- looks ok. It inherited several fixes for code-blocks.
- [x] Code viewer -- looks ok.
2021-10-20 15:03:11 +08:00
infinite-persistence
9041e5e38d
Incremental selector memoization fixes (#92) 2021-10-20 12:24:07 +08:00
infinite-persistence
ce1621f7ed
Use selectClaimForUri in livestreams
Only picking components that are involved in a livestream for now. Ideally, all usages of `makeSelectClaimForUri` should be replaced -- will do it incrementally.
2021-10-20 11:29:18 +08:00
infinite-persistence
da63991972
Comment-selectors: fix memoization 2021-10-20 11:29:18 +08:00
infinite-persistence
b6ad4ae974
Comment-store: Don't memoize selectors without transformation 2021-10-20 11:29:17 +08:00
infinite-persistence
5d8fc40051
Cache restoreScrollPos to avoid render
`CommentCreate` was getting marked for every comment that comes in because the parent was marked.
2021-10-20 11:29:17 +08:00
infinite-persistence
4b0318cd38
Optimize tags and followedTags
followedTags:
- Moved the filtering to the reducer side, so that we don't do it every time. We can't rely on `createSelector` because the store will be invalidated on each `USER_STATE_POPULATE`, unfortunately.

tags:
- Memoize via re-reselect for the "ForUri" selector.
2021-10-20 11:29:16 +08:00
infinite-persistence
0c2c21b67e
re-reselect proof of concept + fix Date selector as first example
## Issue
`makeSelectDataForUri` always returns a new reference, so `ClaimPreview` was constantly being rendered. It's pretty expensive since `ClaimPreview`'s rendering checks against a huge blocklist, which is another issue on it's own.

## Changes
- This commit tests the usage of `re-reselect` as the solution to the multi-instance memoization problem (https://github.com/toomuchdesign/re-reselect/blob/master/examples/1-join-selectors.md)
2021-10-20 11:29:15 +08:00
infinite-persistence
9bbd72d179
Fix reaction-selector reference invalidation
## Issue
When comments are refreshed, each `Comment` gets rendered 4-5 times due to reference invalidation for `othersReacts` (the data didn't actually change).

## Change
For selectors without transformation, there is no need to memoize using `createSelector` -- just access it directly. Also, don't do things like `return a[id] || {}` in a reducer, because the reference to the empty object will be different on each call.

Always return directly from the state so that the same reference is returned.

This simple change avoided the wasted resources needed for `createSelector`, and reduced to render to just 2 (initial render, and when reactions are fetched).
2021-10-19 21:15:26 +08:00
infinite-persistence
249b73f8c6
Skip muted list update if no change
## Issue
Components render unnecessarily due to reference invalidation from `selectMutedChannels` selector.

## Notes
`selectMutedChannels` run and return a new reference each time the app gains focus. `createSelector` will not help in this case, because we are indeed invalidating the data in the store in `USER_STATE_POPULATE`.

## Changes
- Don't update the state if the array is identical in content.
- Fixed `selectMutedChannels` to return the reference from the store, so `createSelector` is not needed.
    - Also, the filtering is not needed because we've already done it in the reducer.

## Comments
I've done some profiling on large blocklists. The time needed for the array comparison is still an order magnitude lower than the time needed to render all the Components that got incorrectly marked by this.

The ideal solution is for the sync code to return a hash or timestamp of the array, so that we can compare that instead of the array.
2021-10-19 21:15:26 +08:00
infinite-persistence
aabfc41ce9
Remove unused props and selector calls. 2021-10-19 21:15:25 +08:00
infinite-persistence
5bcf89394e
Port redux/inc consolidation (#81) 2021-10-19 20:53:24 +08:00
infinite-persistence
35072c0400
Remove unused actions and test function.
The past-tense version of the PUBLISH_* action is no longer used.
2021-10-19 20:43:11 +08:00
infinite-persistence
296febcffa
Lint '/extras/*' + fixes
- Add `/extras` to the precommit hooks (lint, prettier).
- Remove `preinstall` since these modules don't exist anymore.

- Fix missing brace if one single-line if-statement.
2021-10-19 20:40:08 +08:00
infinite-persistence
cfdfdce2fe
Hush repetitive debug errors + remove from i18n 2021-10-19 20:40:07 +08:00
infinite-persistence
2a7f89d6b5
Post-merge updates and fixes
- Put back SETTINGS.LANGUAGE.
- Update import for `doResolveUris`.
2021-10-19 20:40:07 +08:00
Merge
30023422b8
Desktop cherry-pick: "7240 Integrate lbry redux and lbryinc" 2021-10-19 20:40:07 +08:00
infinite-persistence
702297e722
Spew 'analyze' results to 'web/dist' so that it doesn't appear in git. 2021-10-18 22:26:52 +08:00
infinite-persistence
07102c4988
Update 'yarn analyze' to do Web instead of Desktop
Since it might not be obvious to the user that we need a production build, added `yarn compile` to the step directly.
2021-10-18 22:00:44 +08:00
infinite-persistence
3b442531ef
Remove matomo + restore GA (#63) 2021-10-18 08:25:30 +08:00
infinite-persistence
f6e60abbf5
Convert to GA4 format
- It is recommended to use "lowercase + underscore format" for events to keep things neat, since the dashboard will be mixed with Automated and Recommended events.

- GA4 event structure is no longer the same as UA's, and the recommendation is to retructure rather than trying to mimic the old pattern.

- Always check the Recommended events to see if there is an equivalent, and use the exact name. GA4 might add automated features for these events in the future, and we'll benefit from it without code changes and invalidating existing data.

- pageView: use default snippet behavior instead of manually sending
Start converting to GA4...

- Outbound click are automatically handled.
2021-10-17 20:45:40 +08:00
infinite-persistence
dab1ca1cb7
Remove references to Desktop and lbry.tv 2021-10-17 20:45:39 +08:00
infinite-persistence
bba3a17977
Remove matomo + restore GA
Reverted/restored stuff from the following repo, with minimal modifications (trying to keep the diffs clean for future reference):
- lbry-desktop@5008972
- lbry-desktop@7fe88d8
2021-10-17 20:45:39 +08:00
GG2015
4a22814c75
Adds sub count to search and other areas. (#10)
Add follower counts to search
2021-10-16 14:12:09 -04:00
infinite-persistence
91be939c19
Fix linked-comment scrolling (again)
## Issue
Now that we batch-resolve the comment authors before displaying the comments, the linked-comment scrolling logic didn't work well with nested replies.

## Change
Previously, I didn't want to put the logic at the lowest level (`Comment`) because it was hard for the child to know whether to scroll or not. For example, we don't want to scroll when user changes the comment filters or presses the Refresh Comments button.

Relented and moved the logic to `Comment`, and pass a flag via `window` (I know this is frowned upon by some) to indicate whether a scrolling is needed.

This is probably more efficient overall as we don't need to scan the DOM, and with minimal delay as we scroll immediately after the linked-comment is mounted.

## Known issues
In markdown posts with lots of images, a layout shift due to delayed inline-image fetching can cause the scrolling to be inaccurate. This should be fixed by reserving space for markdown post images.
2021-10-16 13:40:33 +08:00
infinite-persistence
0b0f2848da
i18n - refix total comments
Meant to re-use strings, but I forgot to change the variable name.
2021-10-16 11:20:34 +08:00
Thomas Zarebczan
055d437865
fix total comments 2021-10-15 13:08:31 -04:00
infinite-persistence
d1493d5fb3
i18n 2021-10-15 14:23:40 +08:00
Thomas Zarebczan
b86a56f75b
Update README.md 2021-10-14 13:21:49 -04:00
infinite-persistence
8498554f23
Improve aesthetics for deleted channel names.
## Issue
- Comments: no spacing between the strings.
- "Unused" is not intuitive.

## Changes
- Use "[Removed]" instead.
- Localization.
2021-10-14 22:29:50 +08:00
infinite-persistence
2505d67a7d
[Comments] Batch fixes (#65) 2021-10-14 21:16:33 +08:00
saltrafael
03ea298236
Fix expanding comments and scroll pagination 2021-10-14 21:05:01 +08:00
saltrafael
a3302b1be8
Fix expansion broken with layout change 2021-10-14 21:05:00 +08:00
saltrafael
58db9576b9
Fix infinite resolve 2021-10-14 21:04:59 +08:00
saltrafael
a9b9c3ccf0
Revert "Revert "[Comments] Batch resolve" (#61)"
This reverts commit 0e96f8d468.
2021-10-14 21:04:58 +08:00
infinite-persistence
ea516f88dc Fix 'setting.Get' runaway calls
## Issue
60 setting.Get calls spiked since October

It was called 24 times per livestream page load.

## Notes
The effect was intended to be a one-time effect, but the dependency was changed in 2f4dedfb
2021-10-14 20:26:11 +08:00
mayeaux
5f55a3f128
use insecure mode (#74) 2021-10-14 11:55:46 +03:00
saltrafael
53063931ab
Fix markdown preview word break (#70) 2021-10-13 16:31:12 -04:00
mayeaux
6727e2766b
fix channel value (#67) 2021-10-13 19:07:57 +03:00
mayeaux
c10fc675db
fix channel value (#66) 2021-10-13 19:00:32 +03:00
mayeaux
fa889112c5
Ads setup (#62)
* re enable preload ads

* switch macro to aniview

* point towards test server

* improving documentation

* bugfix and turn skip back on

* only run twenty percent of the time for unauthed users

* allow for embeds

* enable show internal feature

* working prototype

* seems to work well

* bugfix

* review old aniview setup

* change to production channelid

* final touchups
2021-10-13 11:04:03 -04:00
infinite-persistence
0e96f8d468
Revert "[Comments] Batch resolve" (#61)
This reverts commit caadd889ce, reversing
changes made to 8b2c7a2b21.

## Issue
- Infinite `resolve` loop when deleted channel is present in the comments.
- Since it was only displayed comments with resolved channels, it masked away those comments. While that may or may not be regarded as a defect, I think we should do it at Commentron instead of at the app if we want to filter deleted channels. I vote to show comments from deleted channels, since it might have good conversation thread.
2021-10-13 08:59:32 -04:00
infinite-persistence
d5ad63c6e9
Comments: handle 'disable-support' tag (#59) 2021-10-13 15:24:55 +08:00
Bradley Ray
cd8f90c82d
added semicolon to import statement 2021-10-13 15:16:20 +08:00
Bradley Ray
24eb2ef8ec
change to import instead of redefining const 2021-10-13 15:16:19 +08:00
Bradley Ray
37ddc395ea
fixed disable-support for comments 2021-10-13 15:16:18 +08:00
Thomas Zarebczan
02a8099514
Take 99 2021-10-12 22:30:05 -04:00
Thomas Zarebczan
9dbee19961
Add test embed domain to rule out cards 2021-10-12 20:55:33 -04:00
Thomas Zarebczan
b49fed4cf5
fix config for thumbs
whoops

Fix encoded URLs
2021-10-12 20:31:10 -04:00
Thomas Zarebczan
2b5d32c313
Use cards.odysee.com (#56) 2021-10-12 19:16:39 -04:00
saltrafael
7c518aa712
[Comment/Livestream] Markdown and style fixes (#55)
* Fix CSS for live chat embeds

* Fix Markdown Lists in Comments

* Disable copy link menu option on livestream comments

* Fix nested indents in Live Chat

* Fix mentions and timestamps not parsed in bullet lists

* Highlight livestream comment and menu button on hover

* Fix mention parsing
2021-10-12 17:06:20 -04:00
infinite-persistence
6f3c43c95f
Fix recsys submission when user is null (#54)
## Issue
44 tor browser crash related to recsys?

## Reproduce the exact error
Block the request for `me|new` in dev tools

## Fix
The code was trying to destructure a null object.

The existing code seems to indicate that null ID is expected (it uses null as fallback), so this change shouldn't impact recsys results (I didn't check the recsys docs to confirm).
2021-10-12 12:10:35 -04:00
infinite-persistence
a168dbcc01
Fix livestream player height cut-off (#53) 2021-10-12 01:27:16 -04:00
Anthony
3087f7c367
list 100 transactions for fiat received and outgoing instead of 25 2021-10-11 12:57:59 -04:00
Thomas Zarebczan
3f969ae20d
Merge pull request #48 from OdyseeTeam/ip/rescys
Handle recsys crash `new|me` fails
2021-10-11 12:57:20 -04:00
infinite-persistence
19797c747e
Handle recsys crash new|me fails
## Issue
Potentially closes 44 "tor browser crash related to recsys?"
2021-10-11 15:34:01 +08:00
infinite-persistence
caadd889ce
[Comments] Batch resolve 2021-10-11 09:46:29 +08:00
522 changed files with 15724 additions and 3020 deletions

View file

@ -16,6 +16,7 @@ COMMENT_SERVER_NAME=Odysee
SEARCH_SERVER_API=https://lighthouse.odysee.com/search
SOCKETY_SERVER_API=wss://sockety.odysee.com/ws
THUMBNAIL_CDN_URL=https://thumbnails.odysee.com/optimize/
THUMBNAIL_CARDS_CDN_URL=https://cards.odysee.com/
THUMBNAIL_HEIGHT=220
THUMBNAIL_WIDTH=390
THUMBNAIL_QUALITY=85
@ -25,8 +26,6 @@ WELCOME_VERSION=1.0
# STRIPE_PUBLIC_KEY='pk_test_NoL1JWL7i1ipfhVId5KfDZgo'
# Analytics
MATOMO_URL=https://analytics.lbry.com/
MATOMO_ID=4
# OG
OG_TITLE_SUFFIX=| lbry.tv
@ -116,3 +115,13 @@ ENABLE_UI_NOTIFICATIONS=false
#MODELS_ENABLED=true
BRANDED_SITE=odysee
#FIREBASE
FIREBASE_API_KEY=AIzaSyAgc-4QORyglpYZ3qH9E5pDauEDOJXgM3A
FIREBASE_AUTH_DOMAIN=lbry-mobile.firebaseapp.com
FIREBASE_PROJECT_ID=lbry-mobile
FIREBASE_STORAGE_BUCKET=lbry-mobile.appspot.com
FIREBASE_MESSAGING_SENDER_ID=638894153788
FIREBASE_APP_ID=1:638894153788:web:35b295b15297201bd2e339
FIREBASE_MEASUREMENT_ID=G-2MPJGFEEXC
FIREBASE_VAPID_KEY=BFayEBpwMTU9GQQpXgitIJkfx-SD8-ltrFb3wLTZWgA27MfBhG4948pe0eERl432NzPrMKsbkXnA7ap_vLPgLYk

View file

@ -7,13 +7,6 @@
[include]
[libs]
./flow-typed
node_modules/lbry-redux/flow-typed/
node_modules/lbryinc/flow-typed/
[untyped]
.*/node_modules/lbry-redux
.*/node_modules/lbryinc
[lints]
@ -31,7 +24,7 @@ module.name_mapper='^modal\(.*\)$' -> '<PROJECT_ROOT>/ui/modal\1'
module.name_mapper='^app\(.*\)$' -> '<PROJECT_ROOT>/ui/app\1'
module.name_mapper='^native\(.*\)$' -> '<PROJECT_ROOT>/ui/native\1'
module.name_mapper='^analytics\(.*\)$' -> '<PROJECT_ROOT>/ui/analytics\1'
module.name_mapper='^recsys\(.*\)$' -> '<PROJECT_ROOT>/ui/recsys\1'
module.name_mapper='^recsys\(.*\)$' -> '<PROJECT_ROOT>/extras/recsys\1'
module.name_mapper='^rewards\(.*\)$' -> '<PROJECT_ROOT>/ui/rewards\1'
module.name_mapper='^i18n\(.*\)$' -> '<PROJECT_ROOT>/ui/i18n\1'
module.name_mapper='^effects\(.*\)$' -> '<PROJECT_ROOT>/ui/effects\1'
@ -42,6 +35,8 @@ module.name_mapper='^web\/effects\(.*\)$' -> '<PROJECT_ROOT>/web/effects\1'
module.name_mapper='^web\/page\(.*\)$' -> '<PROJECT_ROOT>/web/page\1'
module.name_mapper='^homepage\(.*\)$' -> '<PROJECT_ROOT>/ui/util/homepage\1'
module.name_mapper='^scss\/component\(.*\)$' -> '<PROJECT_ROOT>/ui/scss/component/\1'
module.name_mapper='^\$web\(.*\)$' -> '<PROJECT_ROOT>/web\1'
module.name_mapper='^\$ui\(.*\)$' -> '<PROJECT_ROOT>/ui\1'
esproposal.optional_chaining=enable

View file

@ -41,8 +41,6 @@ jobs:
yarn compile:web
env:
# UI
MATOMO_URL: https://analytics.lbry.com/
MATOMO_ID: 4
WELCOME_VERSION: 1.0
DOMAIN: odysee.com
URL: https://odysee.com

1
.gitignore vendored
View file

@ -36,3 +36,4 @@ package-lock.json
.env.ody
.env.desktop
.env.lbrytv
analyzeResults*.html

View file

@ -1,8 +1,10 @@
{
"linters": {
"ui/**/*.{js,jsx,scss,json}": ["prettier --write", "git add"],
"extras/**/*.{js,jsx,scss,json}": ["prettier --write", "git add"],
"web/**/*.{js,jsx,scss,json}": ["prettier --write", "git add"],
"ui/**/*.{js,jsx}": ["eslint", "flow focus-check --color always", "git add"],
"extras/**/*.{js,jsx}": ["eslint", "flow focus-check --color always", "git add"],
"web/**/*.{js,jsx}": ["eslint", "git add"]
},
"ignore": ["node_modules", "web/dist/**/*", "dist/**/*", "package-lock.json"]

View file

@ -26,6 +26,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Stream Key is now hidden _community pr!_ ([#7127](https://github.com/lbryio/lbry-desktop/pull/7127))
- Fix playlist preview thumbnail ([#7178](https://github.com/lbryio/lbry-desktop/pull/7178)
- Fixed “Your Account” popup on mobile ([#7172](https://github.com/lbryio/lbry-desktop/pull/7172))
- Fix disable-support for comments ([#7245](https://github.com/lbryio/lbry-desktop/pull/7245))
## [0.51.2] - [2021-08-20]
@ -734,7 +735,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Added
- Channels page above Publishes which lists all your channels ([#2925](https://github.com/lbryio/lbry-desktop/pull/2925))
- YouTube channel claiming and transfer ([#2925](https://github.com/lbryio/lbry-desktop/pull/2925)). See our [YouTube FAQ](https://lbry.com/faq/youtube) for more information.
- YouTube channel claiming and transfer ([#2925](https://github.com/lbryio/lbry-desktop/pull/2925)). See our [YouTube FAQ](https://odysee.com/@OdyseeHelp:b/youtube-sync:b) for more information.
- New user sign in flow now includes automatic redeeming of 1 LBC and channel creation ([#2925](https://github.com/lbryio/lbry-desktop/pull/2925))
- Ability to save wallet encryption password ([#2925](https://github.com/lbryio/lbry-desktop/pull/2925))
- Sync your balance (only for users with new wallets) and preferences (subscriptions and tags) between devices ([#2925](https://github.com/lbryio/lbry-desktop/pull/2925)). See our [FAQ for more information](https://lbry.com/faq/account-sync)
@ -889,7 +890,7 @@ This release includes a breaking change that will reset many of your settings. T
### Added
- New app design for better [content discovery](https://lbry.com/faq/trending) with infinite scroll ([#2477](https://github.com/lbryio/lbry-desktop/pull/2477))
- New app design for better [content discovery](https://odysee.com/@OdyseeHelp:b/OdyseeBasics:c) with infinite scroll ([#2477](https://github.com/lbryio/lbry-desktop/pull/2477))
- First implementation of comments ([#2510](https://github.com/lbryio/lbry-desktop/pull/2510))
- Ability to edit channels with new metadata and tags ([#2584](https://github.com/lbryio/lbry-desktop/pull/2584))
- Tagging content on publish page ([#2593](https://github.com/lbryio/lbry-desktop/pull/2593))

View file

@ -3,8 +3,6 @@
require('dotenv-defaults').config({ silent: false });
const config = {
MATOMO_URL: process.env.MATOMO_URL,
MATOMO_ID: process.env.MATOMO_ID,
WEBPACK_WEB_PORT: process.env.WEBPACK_WEB_PORT,
WEBPACK_ELECTRON_PORT: process.env.WEBPACK_ELECTRON_PORT,
WEB_SERVER_PORT: process.env.WEB_SERVER_PORT,
@ -22,6 +20,7 @@ const config = {
SHARE_DOMAIN_URL: process.env.SHARE_DOMAIN_URL,
URL: process.env.URL,
THUMBNAIL_CDN_URL: process.env.THUMBNAIL_CDN_URL,
THUMBNAIL_CARDS_CDN_URL: process.env.THUMBNAIL_CARDS_CDN_URL,
THUMBNAIL_HEIGHT: process.env.THUMBNAIL_HEIGHT,
THUMBNAIL_WIDTH: process.env.THUMBNAIL_WIDTH,
THUMBNAIL_QUALITY: process.env.THUMBNAIL_QUALITY,
@ -76,6 +75,17 @@ const config = {
SHOW_TAGS_INTRO: process.env.SHOW_TAGS_INTRO === 'true',
LIGHTHOUSE_DEFAULT_TYPES: process.env.LIGHTHOUSE_DEFAULT_TYPES,
BRANDED_SITE: process.env.BRANDED_SITE,
// FIREBASE SDK
FIREBASE_API_KEY: process.env.FIREBASE_API_KEY,
FIREBASE_AUTH_DOMAIN: process.env.FIREBASE_AUTH_DOMAIN,
FIREBASE_PROJECT_ID: process.env.FIREBASE_PROJECT_ID,
FIREBASE_STORAGE_BUCKET: process.env.FIREBASE_STORAGE_BUCKET,
FIREBASE_MESSAGING_SENDER_ID: process.env.FIREBASE_MESSAGING_SENDER_ID,
FIREBASE_APP_ID: process.env.FIREBASE_APP_ID,
FIREBASE_MEASUREMENT_ID: process.env.FIREBASE_MEASUREMENT_ID,
FIREBASE_VAPID_KEY: process.env.FIREBASE_VAPID_KEY,
};
config.URL_DEV = `http://localhost:${config.WEBPACK_WEB_PORT}`;

View file

@ -1,7 +1,7 @@
import path from 'path';
import fs from 'fs';
import { spawn, execSync } from 'child_process';
import { Lbry } from 'lbry-redux';
import Lbry from 'lbry';
export default class Daemon {
static lbrynetPath =

View file

@ -6,7 +6,7 @@ import SemVer from 'semver';
import https from 'https';
import { app, dialog, ipcMain, session, shell } from 'electron';
import { autoUpdater } from 'electron-updater';
import { Lbry } from 'lbry-redux';
import Lbry from 'lbry';
import LbryFirstInstance from './LbryFirstInstance';
import Daemon from './Daemon';
import isDev from 'electron-is-dev';

View file

@ -8,7 +8,7 @@ if (typeof global.fetch === 'object') {
global.fetch = global.fetch.default;
}
const { Lbry } = require('lbry-redux');
const Lbry = require('lbry');
delete global.window;

View file

@ -0,0 +1,184 @@
// @flow
/*
LBRY FIRST does not work due to api changes
*/
import 'proxy-polyfill';
const CHECK_LBRYFIRST_STARTED_TRY_NUMBER = 200;
//
// Basic LBRYFIRST connection config
// Offers a proxy to call LBRYFIRST methods
//
const LbryFirst: LbryFirstTypes = {
isConnected: false,
connectPromise: null,
lbryFirstConnectionString: 'http://localhost:1337/rpc',
apiRequestHeaders: { 'Content-Type': 'application/json' },
// Allow overriding lbryFirst connection string (e.g. to `/api/proxy` for lbryweb)
setLbryFirstConnectionString: (value: string) => {
LbryFirst.lbryFirstConnectionString = value;
},
setApiHeader: (key: string, value: string) => {
LbryFirst.apiRequestHeaders = Object.assign(LbryFirst.apiRequestHeaders, { [key]: value });
},
unsetApiHeader: key => {
Object.keys(LbryFirst.apiRequestHeaders).includes(key) &&
delete LbryFirst.apiRequestHeaders['key'];
},
// Allow overriding Lbry methods
overrides: {},
setOverride: (methodName, newMethod) => {
LbryFirst.overrides[methodName] = newMethod;
},
getApiRequestHeaders: () => LbryFirst.apiRequestHeaders,
// LbryFirst Methods
status: (params = {}) => lbryFirstCallWithResult('status', params),
stop: () => lbryFirstCallWithResult('stop', {}),
version: () => lbryFirstCallWithResult('version', {}),
// Upload to youtube
upload: (params: { title: string, description: string, file_path: ?string } = {}) => {
// Only upload when originally publishing for now
if (!params.file_path) {
return Promise.resolve();
}
const uploadParams: {
Title: string,
Description: string,
FilePath: string,
Category: string,
Keywords: string,
} = {
Title: params.title,
Description: params.description,
FilePath: params.file_path,
Category: '',
Keywords: '',
};
return lbryFirstCallWithResult('youtube.Upload', uploadParams);
},
hasYTAuth: (token: string) => {
const hasYTAuthParams = {};
hasYTAuthParams.AuthToken = token;
return lbryFirstCallWithResult('youtube.HasAuth', hasYTAuthParams);
},
ytSignup: () => {
const emptyParams = {};
return lbryFirstCallWithResult('youtube.Signup', emptyParams);
},
remove: () => {
const emptyParams = {};
return lbryFirstCallWithResult('youtube.Remove', emptyParams);
},
// Connect to lbry-first
connect: () => {
if (LbryFirst.connectPromise === null) {
LbryFirst.connectPromise = new Promise((resolve, reject) => {
let tryNum = 0;
// Check every half second to see if the lbryFirst is accepting connections
function checkLbryFirstStarted() {
tryNum += 1;
LbryFirst.status()
.then(resolve)
.catch(() => {
if (tryNum <= CHECK_LBRYFIRST_STARTED_TRY_NUMBER) {
setTimeout(checkLbryFirstStarted, tryNum < 50 ? 400 : 1000);
} else {
reject(new Error('Unable to connect to LBRY'));
}
});
}
checkLbryFirstStarted();
});
}
// Flow thinks this could be empty, but it will always return a promise
// $FlowFixMe
return LbryFirst.connectPromise;
},
};
function checkAndParse(response) {
if (response.status >= 200 && response.status < 300) {
return response.json();
}
return response.json().then(json => {
let error;
if (json.error) {
const errorMessage = typeof json.error === 'object' ? json.error.message : json.error;
error = new Error(errorMessage);
} else {
error = new Error('Protocol error with unknown response signature');
}
return Promise.reject(error);
});
}
export function apiCall(method: string, params: ?{}, resolve: Function, reject: Function) {
const counter = new Date().getTime();
const paramsArray = [params];
const options = {
method: 'POST',
headers: LbryFirst.apiRequestHeaders,
body: JSON.stringify({
jsonrpc: '2.0',
method,
params: paramsArray,
id: counter,
}),
};
return fetch(LbryFirst.lbryFirstConnectionString, options)
.then(checkAndParse)
.then(response => {
const error = response.error || (response.result && response.result.error);
if (error) {
return reject(error);
}
return resolve(response.result);
})
.catch(reject);
}
function lbryFirstCallWithResult(name: string, params: ?{} = {}) {
return new Promise((resolve, reject) => {
apiCall(
name,
params,
result => {
resolve(result);
},
reject
);
});
}
// This is only for a fallback
// If there is a LbryFirst method that is being called by an app, it should be added to /flow-typed/LbryFirst.js
const lbryFirstProxy = new Proxy(LbryFirst, {
get(target: LbryFirstTypes, name: string) {
if (name in target) {
return target[name];
}
return (params = {}) =>
new Promise((resolve, reject) => {
apiCall(name, params, resolve, reject);
});
},
});
export default lbryFirstProxy;

View file

@ -0,0 +1,92 @@
// Claims
export const FETCH_FEATURED_CONTENT_STARTED = 'FETCH_FEATURED_CONTENT_STARTED';
export const FETCH_FEATURED_CONTENT_COMPLETED = 'FETCH_FEATURED_CONTENT_COMPLETED';
export const FETCH_TRENDING_CONTENT_STARTED = 'FETCH_TRENDING_CONTENT_STARTED';
export const FETCH_TRENDING_CONTENT_COMPLETED = 'FETCH_TRENDING_CONTENT_COMPLETED';
export const RESOLVE_URIS_STARTED = 'RESOLVE_URIS_STARTED';
export const RESOLVE_URIS_COMPLETED = 'RESOLVE_URIS_COMPLETED';
export const FETCH_CHANNEL_CLAIMS_STARTED = 'FETCH_CHANNEL_CLAIMS_STARTED';
export const FETCH_CHANNEL_CLAIMS_COMPLETED = 'FETCH_CHANNEL_CLAIMS_COMPLETED';
export const FETCH_CHANNEL_CLAIM_COUNT_STARTED = 'FETCH_CHANNEL_CLAIM_COUNT_STARTED';
export const FETCH_CHANNEL_CLAIM_COUNT_COMPLETED = 'FETCH_CHANNEL_CLAIM_COUNT_COMPLETED';
export const FETCH_CLAIM_LIST_MINE_STARTED = 'FETCH_CLAIM_LIST_MINE_STARTED';
export const FETCH_CLAIM_LIST_MINE_COMPLETED = 'FETCH_CLAIM_LIST_MINE_COMPLETED';
export const ABANDON_CLAIM_STARTED = 'ABANDON_CLAIM_STARTED';
export const ABANDON_CLAIM_SUCCEEDED = 'ABANDON_CLAIM_SUCCEEDED';
export const FETCH_CHANNEL_LIST_STARTED = 'FETCH_CHANNEL_LIST_STARTED';
export const FETCH_CHANNEL_LIST_COMPLETED = 'FETCH_CHANNEL_LIST_COMPLETED';
export const CREATE_CHANNEL_STARTED = 'CREATE_CHANNEL_STARTED';
export const CREATE_CHANNEL_COMPLETED = 'CREATE_CHANNEL_COMPLETED';
export const SET_PLAYING_URI = 'SET_PLAYING_URI';
export const SET_CONTENT_POSITION = 'SET_CONTENT_POSITION';
export const SET_CONTENT_LAST_VIEWED = 'SET_CONTENT_LAST_VIEWED';
export const CLEAR_CONTENT_HISTORY_URI = 'CLEAR_CONTENT_HISTORY_URI';
export const CLEAR_CONTENT_HISTORY_ALL = 'CLEAR_CONTENT_HISTORY_ALL';
// Subscriptions
export const CHANNEL_SUBSCRIBE = 'CHANNEL_SUBSCRIBE';
export const CHANNEL_UNSUBSCRIBE = 'CHANNEL_UNSUBSCRIBE';
export const CHANNEL_SUBSCRIPTION_ENABLE_NOTIFICATIONS = 'CHANNEL_SUBSCRIPTION_ENABLE_NOTIFICATIONS';
export const CHANNEL_SUBSCRIPTION_DISABLE_NOTIFICATIONS = 'CHANNEL_SUBSCRIPTION_DISABLE_NOTIFICATIONS';
export const HAS_FETCHED_SUBSCRIPTIONS = 'HAS_FETCHED_SUBSCRIPTIONS';
export const SET_SUBSCRIPTION_LATEST = 'SET_SUBSCRIPTION_LATEST';
export const UPDATE_SUBSCRIPTION_UNREADS = 'UPDATE_SUBSCRIPTION_UNREADS';
export const REMOVE_SUBSCRIPTION_UNREADS = 'REMOVE_SUBSCRIPTION_UNREADS';
export const CHECK_SUBSCRIPTION_STARTED = 'CHECK_SUBSCRIPTION_STARTED';
export const CHECK_SUBSCRIPTION_COMPLETED = 'CHECK_SUBSCRIPTION_COMPLETED';
export const CHECK_SUBSCRIPTIONS_SUBSCRIBE = 'CHECK_SUBSCRIPTIONS_SUBSCRIBE';
export const FETCH_SUBSCRIPTIONS_START = 'FETCH_SUBSCRIPTIONS_START';
export const FETCH_SUBSCRIPTIONS_FAIL = 'FETCH_SUBSCRIPTIONS_FAIL';
export const FETCH_SUBSCRIPTIONS_SUCCESS = 'FETCH_SUBSCRIPTIONS_SUCCESS';
export const SET_VIEW_MODE = 'SET_VIEW_MODE';
export const GET_SUGGESTED_SUBSCRIPTIONS_START = 'GET_SUGGESTED_SUBSCRIPTIONS_START';
export const GET_SUGGESTED_SUBSCRIPTIONS_SUCCESS = 'GET_SUGGESTED_SUBSCRIPTIONS_SUCCESS';
export const GET_SUGGESTED_SUBSCRIPTIONS_FAIL = 'GET_SUGGESTED_SUBSCRIPTIONS_FAIL';
export const SUBSCRIPTION_FIRST_RUN_COMPLETED = 'SUBSCRIPTION_FIRST_RUN_COMPLETED';
export const VIEW_SUGGESTED_SUBSCRIPTIONS = 'VIEW_SUGGESTED_SUBSCRIPTIONS';
// Blacklist
export const FETCH_BLACK_LISTED_CONTENT_STARTED = 'FETCH_BLACK_LISTED_CONTENT_STARTED';
export const FETCH_BLACK_LISTED_CONTENT_COMPLETED = 'FETCH_BLACK_LISTED_CONTENT_COMPLETED';
export const FETCH_BLACK_LISTED_CONTENT_FAILED = 'FETCH_BLACK_LISTED_CONTENT_FAILED';
export const BLACK_LISTED_CONTENT_SUBSCRIBE = 'BLACK_LISTED_CONTENT_SUBSCRIBE';
// Filtered list
export const FETCH_FILTERED_CONTENT_STARTED = 'FETCH_FILTERED_CONTENT_STARTED';
export const FETCH_FILTERED_CONTENT_COMPLETED = 'FETCH_FILTERED_CONTENT_COMPLETED';
export const FETCH_FILTERED_CONTENT_FAILED = 'FETCH_FILTERED_CONTENT_FAILED';
export const FILTERED_CONTENT_SUBSCRIBE = 'FILTERED_CONTENT_SUBSCRIBE';
// Cost Info
export const FETCH_COST_INFO_STARTED = 'FETCH_COST_INFO_STARTED';
export const FETCH_COST_INFO_COMPLETED = 'FETCH_COST_INFO_COMPLETED';
// Stats
export const FETCH_VIEW_COUNT_STARTED = 'FETCH_VIEW_COUNT_STARTED';
export const FETCH_VIEW_COUNT_FAILED = 'FETCH_VIEW_COUNT_FAILED';
export const FETCH_VIEW_COUNT_COMPLETED = 'FETCH_VIEW_COUNT_COMPLETED';
export const FETCH_SUB_COUNT_STARTED = 'FETCH_SUB_COUNT_STARTED';
export const FETCH_SUB_COUNT_FAILED = 'FETCH_SUB_COUNT_FAILED';
export const FETCH_SUB_COUNT_COMPLETED = 'FETCH_SUB_COUNT_COMPLETED';
// Cross-device Sync
export const GET_SYNC_STARTED = 'GET_SYNC_STARTED';
export const GET_SYNC_COMPLETED = 'GET_SYNC_COMPLETED';
export const GET_SYNC_FAILED = 'GET_SYNC_FAILED';
export const SET_SYNC_STARTED = 'SET_SYNC_STARTED';
export const SET_SYNC_FAILED = 'SET_SYNC_FAILED';
export const SET_SYNC_COMPLETED = 'SET_SYNC_COMPLETED';
export const SET_DEFAULT_ACCOUNT = 'SET_DEFAULT_ACCOUNT';
export const SYNC_APPLY_STARTED = 'SYNC_APPLY_STARTED';
export const SYNC_APPLY_COMPLETED = 'SYNC_APPLY_COMPLETED';
export const SYNC_APPLY_FAILED = 'SYNC_APPLY_FAILED';
export const SYNC_APPLY_BAD_PASSWORD = 'SYNC_APPLY_BAD_PASSWORD';
export const SYNC_RESET = 'SYNC_RESET';
// Lbry.tv
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

@ -0,0 +1,5 @@
export const MINIMUM_PUBLISH_BID = 0.00000001;
export const CHANNEL_ANONYMOUS = 'anonymous';
export const CHANNEL_NEW = 'new';
export const PAGE_SIZE = 20;

View file

@ -0,0 +1,4 @@
export const ALREADY_CLAIMED =
'once the invite reward has been claimed the referrer cannot be changed';
export const REFERRER_NOT_FOUND =
'A lbry.tv account could not be found for the referrer you provided.';

View file

@ -0,0 +1,11 @@
export const YOUTUBE_SYNC_NOT_TRANSFERRED = 'not_transferred';
export const YOUTUBE_SYNC_PENDING = 'pending';
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';

73
extras/lbryinc/index.js Normal file
View file

@ -0,0 +1,73 @@
import * as LBRYINC_ACTIONS from 'constants/action_types';
import * as YOUTUBE_STATUSES from 'constants/youtube';
import * as ERRORS from 'constants/errors';
import Lbryio from './lbryio';
export { Lbryio };
// constants
export { LBRYINC_ACTIONS, YOUTUBE_STATUSES, ERRORS };
// utils
export { doTransifexUpload } from 'util/transifex-upload';
// actions
export { doGenerateAuthToken } from './redux/actions/auth';
export { doFetchCostInfoForUri } from './redux/actions/cost_info';
export { doBlackListedOutpointsSubscribe } from './redux/actions/blacklist';
export { doFilteredOutpointsSubscribe } from './redux/actions/filtered';
// export { doFetchFeaturedUris, doFetchTrendingUris } from './redux/actions/homepage';
export { doFetchViewCount, doFetchSubCount } from './redux/actions/stats';
export {
doCheckSync,
doGetSync,
doSetSync,
doSetDefaultAccount,
doSyncApply,
doResetSync,
doSyncEncryptAndDecrypt,
} from 'redux/actions/sync';
export { doUpdateUploadProgress } from './redux/actions/web';
// reducers
export { authReducer } from './redux/reducers/auth';
export { costInfoReducer } from './redux/reducers/cost_info';
export { blacklistReducer } from './redux/reducers/blacklist';
export { filteredReducer } from './redux/reducers/filtered';
// export { homepageReducer } from './redux/reducers/homepage';
export { statsReducer } from './redux/reducers/stats';
export { syncReducer } from './redux/reducers/sync';
export { webReducer } from './redux/reducers/web';
// selectors
export { selectAuthToken, selectIsAuthenticating } from './redux/selectors/auth';
export {
makeSelectFetchingCostInfoForUri,
makeSelectCostInfoForUri,
selectAllCostInfoByUri,
selectFetchingCostInfo,
} from './redux/selectors/cost_info';
export { selectBlackListedOutpoints, selectBlacklistedOutpointMap } from './redux/selectors/blacklist';
export { selectFilteredOutpoints, selectFilteredOutpointMap } from './redux/selectors/filtered';
// export {
// selectFeaturedUris,
// selectFetchingFeaturedUris,
// selectTrendingUris,
// selectFetchingTrendingUris,
// } from './redux/selectors/homepage';
export { selectViewCount, makeSelectViewCountForUri, makeSelectSubCountForUri } from './redux/selectors/stats';
export { selectBanStateForUri } from './redux/selectors/ban';
export {
selectHasSyncedWallet,
selectSyncData,
selectSyncHash,
selectSetSyncErrorMessage,
selectGetSyncErrorMessage,
selectGetSyncIsPending,
selectSetSyncIsPending,
selectSyncApplyIsPending,
selectHashChanged,
selectSyncApplyErrorMessage,
selectSyncApplyPasswordError,
} from './redux/selectors/sync';
export { selectCurrentUploads, selectUploadCount } from './redux/selectors/web';

259
extras/lbryinc/lbryio.js Normal file
View file

@ -0,0 +1,259 @@
import * as ACTIONS from 'constants/action_types';
import Lbry from 'lbry';
import querystring from 'querystring';
import analytics from 'analytics';
const Lbryio = {
enabled: true,
authenticationPromise: null,
exchangePromise: null,
exchangeLastFetched: null,
CONNECTION_STRING: 'https://api.lbry.com/',
};
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
Lbryio.setLocalApi = (endpoint) => {
Lbryio.CONNECTION_STRING = endpoint.replace(/\/*$/, '/'); // exactly one slash at the end;
};
Lbryio.call = (resource, action, params = {}, method = 'get') => {
if (!Lbryio.enabled) {
return Promise.reject(new Error(__('LBRY internal API is disabled')));
}
if (!(method === 'get' || method === 'post')) {
return Promise.reject(new Error(__('Invalid method')));
}
function checkAndParse(response) {
if (response.status >= 200 && response.status < 300) {
return response.json();
}
if (response.status === 500) {
return Promise.reject(INTERNAL_APIS_DOWN);
}
if (response) {
return response.json().then((json) => {
let error;
if (json.error) {
error = new Error(json.error);
} else {
error = new Error('Unknown API error signature');
}
error.response = response; // This is primarily a hack used in actions/user.js
return Promise.reject(error);
});
}
}
function makeRequest(url, options) {
return fetch(url, options).then(checkAndParse);
}
return Lbryio.getAuthToken().then((token) => {
const fullParams = { auth_token: token, ...params };
Object.keys(fullParams).forEach((key) => {
const value = fullParams[key];
if (typeof value === 'object') {
fullParams[key] = JSON.stringify(value);
}
});
const qs = querystring.stringify(fullParams);
let url = `${Lbryio.CONNECTION_STRING}${resource}/${action}?${qs}`;
let options = {
method: 'GET',
};
if (method === 'post') {
options = {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: qs,
};
url = `${Lbryio.CONNECTION_STRING}${resource}/${action}`;
}
return makeRequest(url, options).then((response) => {
sendCallAnalytics(resource, action, params);
return response.data;
});
});
};
Lbryio.authToken = null;
Lbryio.getAuthToken = () =>
new Promise((resolve) => {
if (Lbryio.authToken) {
resolve(Lbryio.authToken);
} else if (Lbryio.overrides.getAuthToken) {
Lbryio.overrides.getAuthToken().then((token) => {
resolve(token);
});
} else if (typeof window !== 'undefined') {
const { store } = window;
if (store) {
const state = store.getState();
const token = state.auth ? state.auth.authToken : null;
Lbryio.authToken = token;
resolve(token);
}
resolve(null);
} else {
resolve(null);
}
});
Lbryio.getCurrentUser = () => Lbryio.call('user', 'me');
Lbryio.authenticate = (domain, language) => {
if (!Lbryio.enabled) {
const params = {
id: 1,
primary_email: 'disabled@lbry.io',
has_verified_email: true,
is_identity_verified: true,
is_reward_approved: false,
language: language || 'en',
};
return new Promise((resolve) => {
resolve(params);
});
}
if (Lbryio.authenticationPromise === null) {
Lbryio.authenticationPromise = new Promise((resolve, reject) => {
Lbryio.getAuthToken()
.then((token) => {
if (!token || token.length > 60) {
return false;
}
// check that token works
return Lbryio.getCurrentUser()
.then((user) => user)
.catch((error) => {
if (error === INTERNAL_APIS_DOWN) {
throw new Error('Internal APIS down');
}
return false;
});
})
.then((user) => {
if (user) {
return user;
}
return Lbry.status()
.then(
(status) =>
new Promise((res, rej) => {
const appId =
domain && domain !== 'lbry.tv'
? (domain.replace(/[.]/gi, '') + status.installation_id).slice(0, 66)
: status.installation_id;
Lbryio.call(
'user',
'new',
{
auth_token: '',
language: language || 'en',
app_id: appId,
},
'post'
)
.then((response) => {
if (!response.auth_token) {
throw new Error('auth_token was not set in the response');
}
const { store } = window;
if (Lbryio.overrides.setAuthToken) {
Lbryio.overrides.setAuthToken(response.auth_token);
}
if (store) {
store.dispatch({
type: ACTIONS.GENERATE_AUTH_TOKEN_SUCCESS,
data: { authToken: response.auth_token },
});
}
Lbryio.authToken = response.auth_token;
return res(response);
})
.catch((error) => rej(error));
})
)
.then((newUser) => {
if (!newUser) {
return Lbryio.getCurrentUser();
}
return newUser;
});
})
.then(resolve, reject);
});
}
return Lbryio.authenticationPromise;
};
Lbryio.getStripeToken = () =>
Lbryio.CONNECTION_STRING.startsWith('http://localhost:')
? 'pk_test_NoL1JWL7i1ipfhVId5KfDZgo'
: 'pk_live_e8M4dRNnCCbmpZzduEUZBgJO';
Lbryio.getExchangeRates = () => {
if (!Lbryio.exchangeLastFetched || Date.now() - Lbryio.exchangeLastFetched > EXCHANGE_RATE_TIMEOUT) {
Lbryio.exchangePromise = new Promise((resolve, reject) => {
Lbryio.call('lbc', 'exchange_rate', {}, 'get', true)
.then(({ lbc_usd: LBC_USD, lbc_btc: LBC_BTC, btc_usd: BTC_USD }) => {
const rates = { LBC_USD, LBC_BTC, BTC_USD };
resolve(rates);
})
.catch(reject);
});
Lbryio.exchangeLastFetched = Date.now();
}
return Lbryio.exchangePromise;
};
// Allow overriding lbryio methods
// The desktop app will need to use it for getAuthToken because we use electron's ipcRenderer
Lbryio.overrides = {};
Lbryio.setOverride = (methodName, newMethod) => {
Lbryio.overrides[methodName] = newMethod;
};
function sendCallAnalytics(resource, action, params) {
switch (resource) {
case 'customer':
if (action === 'tip') {
analytics.reportEvent('spend_virtual_currency', {
// https://developers.google.com/analytics/devguides/collection/ga4/reference/events#spend_virtual_currency
value: params.amount,
virtual_currency_name: params.currency.toLowerCase(),
item_name: 'tip',
});
}
break;
default:
// Do nothing
break;
}
}
export default Lbryio;

View file

@ -0,0 +1,38 @@
import * as ACTIONS from 'constants/action_types';
import { Lbryio } from 'lbryinc';
export function doGenerateAuthToken(installationId) {
return dispatch => {
dispatch({
type: ACTIONS.GENERATE_AUTH_TOKEN_STARTED,
});
Lbryio.call(
'user',
'new',
{
auth_token: '',
language: 'en',
app_id: installationId,
},
'post'
)
.then(response => {
if (!response.auth_token) {
dispatch({
type: ACTIONS.GENERATE_AUTH_TOKEN_FAILURE,
});
} else {
dispatch({
type: ACTIONS.GENERATE_AUTH_TOKEN_SUCCESS,
data: { authToken: response.auth_token },
});
}
})
.catch(() => {
dispatch({
type: ACTIONS.GENERATE_AUTH_TOKEN_FAILURE,
});
});
};
}

View file

@ -0,0 +1,52 @@
import { Lbryio } from 'lbryinc';
import * as ACTIONS from 'constants/action_types';
const CHECK_BLACK_LISTED_CONTENT_INTERVAL = 60 * 60 * 1000;
export function doFetchBlackListedOutpoints() {
return dispatch => {
dispatch({
type: ACTIONS.FETCH_BLACK_LISTED_CONTENT_STARTED,
});
const success = ({ outpoints }) => {
const splitOutpoints = [];
if (outpoints) {
outpoints.forEach((outpoint, index) => {
const [txid, nout] = outpoint.split(':');
splitOutpoints[index] = { txid, nout: Number.parseInt(nout, 10) };
});
}
dispatch({
type: ACTIONS.FETCH_BLACK_LISTED_CONTENT_COMPLETED,
data: {
outpoints: splitOutpoints,
success: true,
},
});
};
const failure = ({ message: error }) => {
dispatch({
type: ACTIONS.FETCH_BLACK_LISTED_CONTENT_FAILED,
data: {
error,
success: false,
},
});
};
Lbryio.call('file', 'list_blocked', {
auth_token: '',
}).then(success, failure);
};
}
export function doBlackListedOutpointsSubscribe() {
return dispatch => {
dispatch(doFetchBlackListedOutpoints());
setInterval(() => dispatch(doFetchBlackListedOutpoints()), CHECK_BLACK_LISTED_CONTENT_INTERVAL);
};
}

View file

@ -0,0 +1,35 @@
import * as ACTIONS from 'constants/action_types';
import { Lbryio } from 'lbryinc';
import { selectClaimsByUri } from 'redux/selectors/claims';
// eslint-disable-next-line import/prefer-default-export
export function doFetchCostInfoForUri(uri) {
return (dispatch, getState) => {
const state = getState();
const claim = selectClaimsByUri(state)[uri];
if (!claim) return;
function resolve(costInfo) {
dispatch({
type: ACTIONS.FETCH_COST_INFO_COMPLETED,
data: {
uri,
costInfo,
},
});
}
const fee = claim.value ? claim.value.fee : undefined;
if (fee === undefined) {
resolve({ cost: 0, includesData: true });
} else if (fee.currency === 'LBC') {
resolve({ cost: fee.amount, includesData: true });
} else {
Lbryio.getExchangeRates().then(({ LBC_USD }) => {
resolve({ cost: fee.amount / LBC_USD, includesData: true });
});
}
};
}

View file

@ -0,0 +1,47 @@
import { Lbryio } from 'lbryinc';
import * as ACTIONS from 'constants/action_types';
const CHECK_FILTERED_CONTENT_INTERVAL = 60 * 60 * 1000;
export function doFetchFilteredOutpoints() {
return dispatch => {
dispatch({
type: ACTIONS.FETCH_FILTERED_CONTENT_STARTED,
});
const success = ({ outpoints }) => {
let formattedOutpoints = [];
if (outpoints) {
formattedOutpoints = outpoints.map(outpoint => {
const [txid, nout] = outpoint.split(':');
return { txid, nout: Number.parseInt(nout, 10) };
});
}
dispatch({
type: ACTIONS.FETCH_FILTERED_CONTENT_COMPLETED,
data: {
outpoints: formattedOutpoints,
},
});
};
const failure = ({ error }) => {
dispatch({
type: ACTIONS.FETCH_FILTERED_CONTENT_FAILED,
data: {
error,
},
});
};
Lbryio.call('file', 'list_filtered', { auth_token: '' }).then(success, failure);
};
}
export function doFilteredOutpointsSubscribe() {
return dispatch => {
dispatch(doFetchFilteredOutpoints());
setInterval(() => dispatch(doFetchFilteredOutpoints()), CHECK_FILTERED_CONTENT_INTERVAL);
};
}

View file

@ -0,0 +1,79 @@
import { Lbryio } from 'lbryinc';
import { batchActions } from 'util/batch-actions';
import { doResolveUris } from 'util/lbryURI';
import * as ACTIONS from 'constants/action_types';
export function doFetchFeaturedUris(offloadResolve = false) {
return dispatch => {
dispatch({
type: ACTIONS.FETCH_FEATURED_CONTENT_STARTED,
});
const success = ({ Uris }) => {
let urisToResolve = [];
Object.keys(Uris).forEach(category => {
urisToResolve = [...urisToResolve, ...Uris[category]];
});
const actions = [
{
type: ACTIONS.FETCH_FEATURED_CONTENT_COMPLETED,
data: {
uris: Uris,
success: true,
},
},
];
if (urisToResolve.length && !offloadResolve) {
actions.push(doResolveUris(urisToResolve));
}
dispatch(batchActions(...actions));
};
const failure = () => {
dispatch({
type: ACTIONS.FETCH_FEATURED_CONTENT_COMPLETED,
data: {
uris: {},
},
});
};
Lbryio.call('file', 'list_homepage').then(success, failure);
};
}
export function doFetchTrendingUris() {
return dispatch => {
dispatch({
type: ACTIONS.FETCH_TRENDING_CONTENT_STARTED,
});
const success = data => {
const urisToResolve = data.map(uri => uri.url);
const actions = [
doResolveUris(urisToResolve),
{
type: ACTIONS.FETCH_TRENDING_CONTENT_COMPLETED,
data: {
uris: data,
success: true,
},
},
];
dispatch(batchActions(...actions));
};
const failure = () => {
dispatch({
type: ACTIONS.FETCH_TRENDING_CONTENT_COMPLETED,
data: {
uris: [],
},
});
};
Lbryio.call('file', 'list_trending').then(success, failure);
};
}

View file

@ -0,0 +1,68 @@
// @flow
import { Lbryio } from 'lbryinc';
import * as ACTIONS from 'constants/action_types';
const FETCH_SUB_COUNT_MIN_INTERVAL_MS = 5 * 60 * 1000;
const FETCH_SUB_COUNT_IDLE_FIRE_MS = 100;
export const doFetchViewCount = (claimIdCsv: string) => (dispatch: Dispatch) => {
dispatch({ type: ACTIONS.FETCH_VIEW_COUNT_STARTED });
return Lbryio.call('file', 'view_count', { claim_id: claimIdCsv })
.then((result: Array<number>) => {
const viewCounts = result;
dispatch({ type: ACTIONS.FETCH_VIEW_COUNT_COMPLETED, data: { claimIdCsv, viewCounts } });
})
.catch((error) => {
dispatch({ type: ACTIONS.FETCH_VIEW_COUNT_FAILED, data: error });
});
};
const executeFetchSubCount = (claimIdCsv: string) => (dispatch: Dispatch, getState: GetState) => {
const state = getState();
const subCountLastFetchedById = state.stats.subCountLastFetchedById;
const now = Date.now();
const claimIds = claimIdCsv.split(',').filter((id) => {
const prev = subCountLastFetchedById[id];
return !prev || now - prev > FETCH_SUB_COUNT_MIN_INTERVAL_MS;
});
if (claimIds.length === 0) {
return;
}
dispatch({ type: ACTIONS.FETCH_SUB_COUNT_STARTED });
return Lbryio.call('subscription', 'sub_count', { claim_id: claimIds.join(',') })
.then((result: Array<number>) => {
const subCounts = result;
dispatch({
type: ACTIONS.FETCH_SUB_COUNT_COMPLETED,
data: { claimIdCsv, subCounts, fetchDate: now },
});
})
.catch((error) => {
dispatch({ type: ACTIONS.FETCH_SUB_COUNT_FAILED, data: error });
});
};
let fetchSubCountTimer;
let fetchSubCountQueue = '';
export const doFetchSubCount = (claimIdCsv: string) => (dispatch: Dispatch) => {
if (fetchSubCountTimer) {
clearTimeout(fetchSubCountTimer);
}
if (fetchSubCountQueue && !fetchSubCountQueue.endsWith(',')) {
fetchSubCountQueue += ',';
}
fetchSubCountQueue += claimIdCsv;
fetchSubCountTimer = setTimeout(() => {
dispatch(executeFetchSubCount(fetchSubCountQueue));
fetchSubCountQueue = '';
}, FETCH_SUB_COUNT_IDLE_FIRE_MS);
};

View file

@ -0,0 +1,289 @@
import * as ACTIONS from 'constants/action_types';
import { Lbryio } from 'lbryinc';
import Lbry from 'lbry';
import { doWalletEncrypt, doWalletDecrypt } from 'redux/actions/wallet';
const NO_WALLET_ERROR = 'no wallet found for this user';
export function doSetDefaultAccount(success, failure) {
return dispatch => {
dispatch({
type: ACTIONS.SET_DEFAULT_ACCOUNT,
});
Lbry.account_list()
.then(accountList => {
const { lbc_mainnet: accounts } = accountList;
let defaultId;
for (let i = 0; i < accounts.length; ++i) {
if (accounts[i].satoshis > 0) {
defaultId = accounts[i].id;
break;
}
}
// In a case where there's no balance on either account
// assume the second (which is created after sync) as default
if (!defaultId && accounts.length > 1) {
defaultId = accounts[1].id;
}
// Set the default account
if (defaultId) {
Lbry.account_set({ account_id: defaultId, default: true })
.then(() => {
if (success) {
success();
}
})
.catch(err => {
if (failure) {
failure(err);
}
});
} else if (failure) {
// no default account to set
failure('Could not set a default account'); // fail
}
})
.catch(err => {
if (failure) {
failure(err);
}
});
};
}
export function doSetSync(oldHash, newHash, data) {
return dispatch => {
dispatch({
type: ACTIONS.SET_SYNC_STARTED,
});
return Lbryio.call('sync', 'set', { old_hash: oldHash, new_hash: newHash, data }, 'post')
.then(response => {
if (!response.hash) {
throw Error('No hash returned for sync/set.');
}
return dispatch({
type: ACTIONS.SET_SYNC_COMPLETED,
data: { syncHash: response.hash },
});
})
.catch(error => {
dispatch({
type: ACTIONS.SET_SYNC_FAILED,
data: { error },
});
});
};
}
export function doGetSync(passedPassword, callback) {
const password = passedPassword === null || passedPassword === undefined ? '' : passedPassword;
function handleCallback(error, hasNewData) {
if (callback) {
if (typeof callback !== 'function') {
throw new Error('Second argument passed to "doGetSync" must be a function');
}
callback(error, hasNewData);
}
}
return dispatch => {
dispatch({
type: ACTIONS.GET_SYNC_STARTED,
});
const data = {};
Lbry.wallet_status()
.then(status => {
if (status.is_locked) {
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(hash => Lbryio.call('sync', 'get', { hash }, 'post'))
.then(response => {
const syncHash = response.hash;
data.syncHash = syncHash;
data.syncData = response.data;
data.changed = response.changed;
data.hasSyncedWallet = true;
if (response.changed) {
return Lbry.sync_apply({ password, data: response.data, blocking: true });
}
})
.then(response => {
if (!response) {
dispatch({ type: ACTIONS.GET_SYNC_COMPLETED, data });
handleCallback(null, data.changed);
return;
}
const { hash: walletHash, data: walletData } = response;
if (walletHash !== data.syncHash) {
// different local hash, need to synchronise
dispatch(doSetSync(data.syncHash, walletHash, walletData));
}
dispatch({ type: ACTIONS.GET_SYNC_COMPLETED, data });
handleCallback(null, data.changed);
})
.catch(syncAttemptError => {
if (data.unlockFailed) {
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({
type: ACTIONS.GET_SYNC_FAILED,
data: {
error,
},
});
// Temp solution until we have a bad password error code
// Don't fail on blank passwords so we don't show a "password error" message
// before users have ever entered a password
if (password !== '') {
dispatch({ type: ACTIONS.SYNC_APPLY_BAD_PASSWORD });
}
handleCallback(error);
} else {
// user doesn't have a synced wallet
dispatch({
type: ACTIONS.GET_SYNC_COMPLETED,
data: { hasSyncedWallet: false, syncHash: null },
});
// call sync_apply to get data to sync
// first time sync. use any string for old hash
if (syncAttemptError.message === NO_WALLET_ERROR) {
Lbry.sync_apply({ password })
.then(({ hash: walletHash, data: syncApplyData }) => {
dispatch(doSetSync('', walletHash, syncApplyData, password));
handleCallback();
})
.catch(syncApplyError => {
handleCallback(syncApplyError);
});
}
}
});
};
}
export function doSyncApply(syncHash, syncData, password) {
return dispatch => {
dispatch({
type: ACTIONS.SYNC_APPLY_STARTED,
});
Lbry.sync_apply({ password, data: syncData })
.then(({ hash: walletHash, data: walletData }) => {
dispatch({
type: ACTIONS.SYNC_APPLY_COMPLETED,
});
if (walletHash !== syncHash) {
// different local hash, need to synchronise
dispatch(doSetSync(syncHash, walletHash, walletData));
}
})
.catch(() => {
dispatch({
type: ACTIONS.SYNC_APPLY_FAILED,
data: {
error:
'Invalid password specified. Please enter the password for your previously synchronised wallet.',
},
});
});
};
}
export function doCheckSync() {
return dispatch => {
dispatch({
type: ACTIONS.GET_SYNC_STARTED,
});
Lbry.sync_hash().then(hash => {
Lbryio.call('sync', 'get', { hash }, 'post')
.then(response => {
const data = {
hasSyncedWallet: true,
syncHash: response.hash,
syncData: response.data,
hashChanged: response.changed,
};
dispatch({ type: ACTIONS.GET_SYNC_COMPLETED, data });
})
.catch(() => {
// user doesn't have a synced wallet
dispatch({
type: ACTIONS.GET_SYNC_COMPLETED,
data: { hasSyncedWallet: false, syncHash: null },
});
});
});
};
}
export function doResetSync() {
return dispatch =>
new Promise(resolve => {
dispatch({ type: ACTIONS.SYNC_RESET });
resolve();
});
}
export function doSyncEncryptAndDecrypt(oldPassword, newPassword, encrypt) {
return dispatch => {
const data = {};
return Lbry.sync_hash()
.then(hash => Lbryio.call('sync', 'get', { hash }, 'post'))
.then(syncGetResponse => {
data.oldHash = syncGetResponse.hash;
return Lbry.sync_apply({ password: oldPassword, data: syncGetResponse.data });
})
.then(() => {
if (encrypt) {
dispatch(doWalletEncrypt(newPassword));
} else {
dispatch(doWalletDecrypt());
}
})
.then(() => Lbry.sync_apply({ password: newPassword }))
.then(syncApplyResponse => {
if (syncApplyResponse.hash !== data.oldHash) {
return dispatch(doSetSync(data.oldHash, syncApplyResponse.hash, syncApplyResponse.data));
}
})
.catch(console.error);
};
}

View file

@ -0,0 +1,12 @@
// @flow
import * as ACTIONS from 'constants/action_types';
export const doUpdateUploadProgress = (
progress: string,
params: { [key: string]: any },
xhr: any
) => (dispatch: Dispatch) =>
dispatch({
type: ACTIONS.UPDATE_UPLOAD_PROGRESS,
data: { progress, params, xhr },
});

View file

@ -0,0 +1,29 @@
import * as ACTIONS from 'constants/action_types';
const reducers = {};
const defaultState = {
authenticating: false,
};
reducers[ACTIONS.GENERATE_AUTH_TOKEN_FAILURE] = state =>
Object.assign({}, state, {
authToken: null,
authenticating: false,
});
reducers[ACTIONS.GENERATE_AUTH_TOKEN_STARTED] = state =>
Object.assign({}, state, {
authenticating: true,
});
reducers[ACTIONS.GENERATE_AUTH_TOKEN_SUCCESS] = (state, action) =>
Object.assign({}, state, {
authToken: action.data.authToken,
authenticating: false,
});
export function authReducer(state = defaultState, action) {
const handler = reducers[action.type];
if (handler) return handler(state, action);
return state;
}

View file

@ -0,0 +1,37 @@
import * as ACTIONS from 'constants/action_types';
import { handleActions } from 'util/redux-utils';
const defaultState = {
fetchingBlackListedOutpoints: false,
fetchingBlackListedOutpointsSucceed: undefined,
blackListedOutpoints: undefined,
};
export const blacklistReducer = handleActions(
{
[ACTIONS.FETCH_BLACK_LISTED_CONTENT_STARTED]: state => ({
...state,
fetchingBlackListedOutpoints: true,
}),
[ACTIONS.FETCH_BLACK_LISTED_CONTENT_COMPLETED]: (state, action) => {
const { outpoints, success } = action.data;
return {
...state,
fetchingBlackListedOutpoints: false,
fetchingBlackListedOutpointsSucceed: success,
blackListedOutpoints: outpoints,
};
},
[ACTIONS.FETCH_BLACK_LISTED_CONTENT_FAILED]: (state, action) => {
const { error, success } = action.data;
return {
...state,
fetchingBlackListedOutpoints: false,
fetchingBlackListedOutpointsSucceed: success,
fetchingBlackListedOutpointsError: error,
};
},
},
defaultState
);

View file

@ -0,0 +1,38 @@
import { handleActions } from 'util/redux-utils';
import * as ACTIONS from 'constants/action_types';
const defaultState = {
fetching: {},
byUri: {},
};
export const costInfoReducer = handleActions(
{
[ACTIONS.FETCH_COST_INFO_STARTED]: (state, action) => {
const { uri } = action.data;
const newFetching = Object.assign({}, state.fetching);
newFetching[uri] = true;
return {
...state,
fetching: newFetching,
};
},
[ACTIONS.FETCH_COST_INFO_COMPLETED]: (state, action) => {
const { uri, costInfo } = action.data;
const newByUri = Object.assign({}, state.byUri);
const newFetching = Object.assign({}, state.fetching);
newByUri[uri] = costInfo;
delete newFetching[uri];
return {
...state,
byUri: newByUri,
fetching: newFetching,
};
},
},
defaultState
);

View file

@ -0,0 +1,34 @@
import * as ACTIONS from 'constants/action_types';
import { handleActions } from 'util/redux-utils';
const defaultState = {
loading: false,
filteredOutpoints: undefined,
};
export const filteredReducer = handleActions(
{
[ACTIONS.FETCH_FILTERED_CONTENT_STARTED]: state => ({
...state,
loading: true,
}),
[ACTIONS.FETCH_FILTERED_CONTENT_COMPLETED]: (state, action) => {
const { outpoints } = action.data;
return {
...state,
loading: false,
filteredOutpoints: outpoints,
};
},
[ACTIONS.FETCH_FILTERED_CONTENT_FAILED]: (state, action) => {
const { error } = action.data;
return {
...state,
loading: false,
fetchingFilteredOutpointsError: error,
};
},
},
defaultState
);

View file

@ -0,0 +1,48 @@
import { handleActions } from 'util/redux-utils';
import * as ACTIONS from 'constants/action_types';
const defaultState = {
fetchingFeaturedContent: false,
fetchingFeaturedContentFailed: false,
featuredUris: undefined,
fetchingTrendingContent: false,
fetchingTrendingContentFailed: false,
trendingUris: undefined,
};
export const homepageReducer = handleActions(
{
[ACTIONS.FETCH_FEATURED_CONTENT_STARTED]: state => ({
...state,
fetchingFeaturedContent: true,
}),
[ACTIONS.FETCH_FEATURED_CONTENT_COMPLETED]: (state, action) => {
const { uris, success } = action.data;
return {
...state,
fetchingFeaturedContent: false,
fetchingFeaturedContentFailed: !success,
featuredUris: uris,
};
},
[ACTIONS.FETCH_TRENDING_CONTENT_STARTED]: state => ({
...state,
fetchingTrendingContent: true,
}),
[ACTIONS.FETCH_TRENDING_CONTENT_COMPLETED]: (state, action) => {
const { uris, success } = action.data;
return {
...state,
fetchingTrendingContent: false,
fetchingTrendingContentFailed: !success,
trendingUris: uris,
};
},
},
defaultState
);

View file

@ -0,0 +1,81 @@
import { handleActions } from 'util/redux-utils';
import * as ACTIONS from 'constants/action_types';
const defaultState = {
fetchingViewCount: false,
viewCountError: undefined,
viewCountById: {},
fetchingSubCount: false,
subCountError: undefined,
subCountById: {},
subCountLastFetchedById: {},
};
export const statsReducer = handleActions(
{
[ACTIONS.FETCH_VIEW_COUNT_STARTED]: (state) => ({ ...state, fetchingViewCount: true }),
[ACTIONS.FETCH_VIEW_COUNT_FAILED]: (state, action) => ({
...state,
viewCountError: action.data,
}),
[ACTIONS.FETCH_VIEW_COUNT_COMPLETED]: (state, action) => {
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];
});
}
return {
...state,
fetchingViewCount: false,
viewCountById,
};
},
[ACTIONS.FETCH_SUB_COUNT_STARTED]: (state) => ({ ...state, fetchingSubCount: true }),
[ACTIONS.FETCH_SUB_COUNT_FAILED]: (state, action) => ({
...state,
subCountError: action.data,
}),
[ACTIONS.FETCH_SUB_COUNT_COMPLETED]: (state, action) => {
const { claimIdCsv, subCounts, fetchDate } = action.data;
const subCountById = Object.assign({}, state.subCountById);
const subCountLastFetchedById = Object.assign({}, state.subCountLastFetchedById);
const claimIds = claimIdCsv.split(',');
let dataChanged = false;
if (claimIds.length === subCounts.length) {
claimIds.forEach((claimId, index) => {
if (subCountById[claimId] !== subCounts[index]) {
subCountById[claimId] = subCounts[index];
dataChanged = true;
}
subCountLastFetchedById[claimId] = fetchDate;
});
}
const newState = {
...state,
fetchingSubCount: false,
subCountLastFetchedById,
};
if (dataChanged) {
newState.subCountById = subCountById;
}
return newState;
},
},
defaultState
);

View file

@ -0,0 +1,89 @@
import * as ACTIONS from 'constants/action_types';
const reducers = {};
const defaultState = {
hasSyncedWallet: false,
syncHash: null,
syncData: null,
setSyncErrorMessage: null,
getSyncErrorMessage: null,
syncApplyErrorMessage: '',
syncApplyIsPending: false,
syncApplyPasswordError: false,
getSyncIsPending: false,
setSyncIsPending: false,
hashChanged: false,
};
reducers[ACTIONS.GET_SYNC_STARTED] = state =>
Object.assign({}, state, {
getSyncIsPending: true,
getSyncErrorMessage: null,
});
reducers[ACTIONS.GET_SYNC_COMPLETED] = (state, action) =>
Object.assign({}, state, {
syncHash: action.data.syncHash,
syncData: action.data.syncData,
hasSyncedWallet: action.data.hasSyncedWallet,
getSyncIsPending: false,
hashChanged: action.data.hashChanged,
});
reducers[ACTIONS.GET_SYNC_FAILED] = (state, action) =>
Object.assign({}, state, {
getSyncIsPending: false,
getSyncErrorMessage: action.data.error,
});
reducers[ACTIONS.SET_SYNC_STARTED] = state =>
Object.assign({}, state, {
setSyncIsPending: true,
setSyncErrorMessage: null,
});
reducers[ACTIONS.SET_SYNC_FAILED] = (state, action) =>
Object.assign({}, state, {
setSyncIsPending: false,
setSyncErrorMessage: action.data.error,
});
reducers[ACTIONS.SET_SYNC_COMPLETED] = (state, action) =>
Object.assign({}, state, {
setSyncIsPending: false,
setSyncErrorMessage: null,
hasSyncedWallet: true, // sync was successful, so the user has a synced wallet at this point
syncHash: action.data.syncHash,
});
reducers[ACTIONS.SYNC_APPLY_STARTED] = state =>
Object.assign({}, state, {
syncApplyPasswordError: false,
syncApplyIsPending: true,
syncApplyErrorMessage: '',
});
reducers[ACTIONS.SYNC_APPLY_COMPLETED] = state =>
Object.assign({}, state, {
syncApplyIsPending: false,
syncApplyErrorMessage: '',
});
reducers[ACTIONS.SYNC_APPLY_FAILED] = (state, action) =>
Object.assign({}, state, {
syncApplyIsPending: false,
syncApplyErrorMessage: action.data.error,
});
reducers[ACTIONS.SYNC_APPLY_BAD_PASSWORD] = state =>
Object.assign({}, state, {
syncApplyPasswordError: true,
});
reducers[ACTIONS.SYNC_RESET] = () => defaultState;
export function syncReducer(state = defaultState, action) {
const handler = reducers[action.type];
if (handler) return handler(state, action);
return state;
}

View file

@ -0,0 +1,62 @@
// @flow
import * as ACTIONS from 'constants/action_types';
/*
test mock:
currentUploads: {
'test#upload': {
progress: 50,
params: {
name: 'steve',
thumbnail_url: 'https://dev2.spee.ch/4/KMNtoSZ009fawGz59VG8PrID.jpeg',
},
},
},
*/
export type Params = {
channel?: string,
name: string,
thumbnail_url: ?string,
title: ?string,
};
export type UploadItem = {
progess: string,
params: Params,
xhr?: any,
};
export type TvState = {
currentUploads: { [key: string]: UploadItem },
};
const reducers = {};
const defaultState: TvState = {
currentUploads: {},
};
reducers[ACTIONS.UPDATE_UPLOAD_PROGRESS] = (state: TvState, action) => {
const { progress, params, xhr } = action.data;
const key = params.channel ? `${params.name}#${params.channel}` : `${params.name}#anonymous`;
let currentUploads;
if (!progress) {
currentUploads = Object.assign({}, state.currentUploads);
Object.keys(currentUploads).forEach(k => {
if (k === key) {
delete currentUploads[key];
}
});
} else {
currentUploads = Object.assign({}, state.currentUploads);
currentUploads[key] = { progress, params, xhr };
}
return { ...state, currentUploads };
};
export function webReducer(state: TvState = defaultState, action: any) {
const handler = reducers[action.type];
if (handler) return handler(state, action);
return state;
}

View file

@ -0,0 +1,4 @@
const selectState = (state) => state.auth || {};
export const selectAuthToken = (state) => selectState(state).authToken;
export const selectIsAuthenticating = (state) => selectState(state).authenticating;

View file

@ -0,0 +1,96 @@
// @flow
// TODO: This should be in 'redux/selectors/claim.js'. Temporarily putting it
// here to get past importing issues with 'lbryinc', which the real fix might
// involve moving it from 'extras' to 'ui' (big change).
import { createCachedSelector } from 're-reselect';
import { selectClaimForUri } from 'redux/selectors/claims';
import { selectMutedChannels } from 'redux/selectors/blocked';
import { selectModerationBlockList } from 'redux/selectors/comments';
import { selectBlackListedOutpoints, selectFilteredOutpoints } from 'lbryinc';
import { isURIEqual } from 'util/lbryURI';
const selectClaimExistsForUri = (state, uri) => {
return Boolean(selectClaimForUri(state, uri));
};
const selectTxidForUri = (state, uri) => {
const claim = selectClaimForUri(state, uri);
const signingChannel = claim && claim.signing_channel;
return signingChannel ? signingChannel.txid : claim ? claim.txid : undefined;
};
const selectNoutForUri = (state, uri) => {
const claim = selectClaimForUri(state, uri);
const signingChannel = claim && claim.signing_channel;
return signingChannel ? signingChannel.nout : claim ? claim.nout : undefined;
};
const selectPermanentUrlForUri = (state, uri) => {
const claim = selectClaimForUri(state, uri);
const signingChannel = claim && claim.signing_channel;
return signingChannel ? signingChannel.permanent_url : claim ? claim.permanent_url : undefined;
};
export const selectBanStateForUri = createCachedSelector(
// Break apart 'selectClaimForUri' into 4 cheaper selectors that return
// primitives for values that we care about. The Claim object itself is easily
// invalidated due to constantly-changing fields like 'confirmation'.
selectClaimExistsForUri,
selectTxidForUri,
selectNoutForUri,
selectPermanentUrlForUri,
selectBlackListedOutpoints,
selectFilteredOutpoints,
selectMutedChannels,
selectModerationBlockList,
(
claimExists,
txid,
nout,
permanentUrl,
blackListedOutpoints,
filteredOutpoints,
mutedChannelUris,
personalBlocklist
) => {
const banState = {};
if (!claimExists) {
return banState;
}
// This will be replaced once blocking is done at the wallet server level.
if (blackListedOutpoints) {
if (blackListedOutpoints.some((outpoint) => outpoint.txid === txid && outpoint.nout === nout)) {
banState['blacklisted'] = true;
}
}
// We're checking to see if the stream outpoint or signing channel outpoint
// is in the filter list.
if (filteredOutpoints) {
if (filteredOutpoints.some((outpoint) => outpoint.txid === txid && outpoint.nout === nout)) {
banState['filtered'] = true;
}
}
// block stream claims
// block channel claims if we can't control for them in claim search
if (mutedChannelUris.length) {
if (mutedChannelUris.some((blockedUri) => isURIEqual(blockedUri, permanentUrl))) {
banState['muted'] = true;
}
}
// Commentron blocklist
if (personalBlocklist.length) {
if (personalBlocklist.some((blockedUri) => isURIEqual(blockedUri, permanentUrl))) {
banState['blocked'] = true;
}
}
return banState;
}
)((state, uri) => String(uri));

View file

@ -0,0 +1,15 @@
import { createSelector } from 'reselect';
export const selectState = (state) => state.blacklist || {};
export const selectBlackListedOutpoints = (state) => selectState(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

@ -0,0 +1,13 @@
import { createSelector } from 'reselect';
export const selectState = state => state.costInfo || {};
export const selectAllCostInfoByUri = createSelector(selectState, state => state.byUri || {});
export const makeSelectCostInfoForUri = uri =>
createSelector(selectAllCostInfoByUri, costInfos => costInfos && costInfos[uri]);
export const selectFetchingCostInfo = createSelector(selectState, state => state.fetching || {});
export const makeSelectFetchingCostInfoForUri = uri =>
createSelector(selectFetchingCostInfo, fetchingByUri => fetchingByUri && fetchingByUri[uri]);

View file

@ -0,0 +1,15 @@
import { createSelector } from 'reselect';
export const selectState = (state) => state.filtered || {};
export const selectFilteredOutpoints = (state) => selectState(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

@ -0,0 +1,6 @@
const selectState = (state) => state.homepage || {};
export const selectFeaturedUris = (state) => selectState(state).featuredUris;
export const selectFetchingFeaturedUris = (state) => selectState(state).fetchingFeaturedContent;
export const selectTrendingUris = (state) => selectState(state).trendingUris;
export const selectFetchingTrendingUris = (state) => selectState(state).fetchingTrendingContent;

View file

@ -0,0 +1,20 @@
import { createSelector } from 'reselect';
import { makeSelectClaimForUri } from 'redux/selectors/claims';
const selectState = state => state.stats || {};
export const selectViewCount = createSelector(selectState, state => state.viewCountById);
export const selectSubCount = createSelector(selectState, state => state.subCountById);
export const makeSelectViewCountForUri = uri =>
createSelector(
makeSelectClaimForUri(uri),
selectViewCount,
(claim, viewCountById) => (claim ? viewCountById[claim.claim_id] || 0 : 0)
);
export const makeSelectSubCountForUri = uri =>
createSelector(
makeSelectClaimForUri(uri),
selectSubCount,
(claim, subCountById) => (claim ? subCountById[claim.claim_id] || 0 : 0)
);

View file

@ -0,0 +1,13 @@
const selectState = (state) => state.sync || {};
export const selectHasSyncedWallet = (state) => selectState(state).hasSyncedWallet;
export const selectSyncHash = (state) => selectState(state).syncHash;
export const selectSyncData = (state) => selectState(state).syncData;
export const selectSetSyncErrorMessage = (state) => selectState(state).setSyncErrorMessage;
export const selectGetSyncErrorMessage = (state) => selectState(state).getSyncErrorMessage;
export const selectGetSyncIsPending = (state) => selectState(state).getSyncIsPending;
export const selectSetSyncIsPending = (state) => selectState(state).setSyncIsPending;
export const selectHashChanged = (state) => selectState(state).hashChanged;
export const selectSyncApplyIsPending = (state) => selectState(state).syncApplyIsPending;
export const selectSyncApplyErrorMessage = (state) => selectState(state).syncApplyErrorMessage;
export const selectSyncApplyPasswordError = (state) => selectState(state).syncApplyPasswordError;

View file

@ -0,0 +1,10 @@
import { createSelector } from 'reselect';
const selectState = (state) => state.web || {};
export const selectCurrentUploads = (state) => selectState(state).currentUploads;
export const selectUploadCount = createSelector(
selectCurrentUploads,
(currentUploads) => currentUploads && Object.keys(currentUploads).length
);

View file

@ -0,0 +1,17 @@
// util for creating reducers
// based off of redux-actions
// https://redux-actions.js.org/docs/api/handleAction.html#handleactions
// eslint-disable-next-line import/prefer-default-export
export const handleActions = (actionMap, defaultState) => (state = defaultState, action) => {
const handler = actionMap[action.type];
if (handler) {
const newState = handler(state, action);
return Object.assign({}, state, newState);
}
// just return the original state if no handler
// returning a copy here breaks redux-persist
return state;
};

View file

@ -0,0 +1,10 @@
export function swapKeyAndValue(dict) {
const ret = {};
// eslint-disable-next-line no-restricted-syntax
for (const key in dict) {
if (dict.hasOwnProperty(key)) {
ret[dict[key]] = key;
}
}
return ret;
}

View file

@ -0,0 +1,78 @@
const apiBaseUrl = 'https://www.transifex.com/api/2/project';
const resource = 'app-strings';
export function doTransifexUpload(contents, project, token, success, fail) {
const url = `${apiBaseUrl}/${project}/resources/`;
const updateUrl = `${apiBaseUrl}/${project}/resource/${resource}/content/`;
const headers = {
Authorization: `Basic ${Buffer.from(`api:${token}`).toString('base64')}`,
'Content-Type': 'application/json',
};
const req = {
accept_translations: true,
i18n_type: 'KEYVALUEJSON',
name: resource,
slug: resource,
content: contents,
};
function handleResponse(text) {
let json;
try {
// transifex api returns Python dicts for some reason.
// Any way to get the api to return valid JSON?
json = JSON.parse(text);
} catch (e) {
// ignore
}
if (success) {
success(json || text);
}
}
function handleError(err) {
if (fail) {
fail(err.message ? err.message : 'Could not upload strings resource to Transifex');
}
}
// check if the resource exists
fetch(updateUrl, { headers })
.then(response => response.json())
.then(() => {
// perform an update
fetch(updateUrl, {
method: 'PUT',
headers,
body: JSON.stringify({ content: contents }),
})
.then(response => {
if (response.status !== 200 && response.status !== 201) {
throw new Error('failed to update transifex');
}
return response.text();
})
.then(handleResponse)
.catch(handleError);
})
.catch(() => {
// resource doesn't exist, create a fresh resource
fetch(url, {
method: 'POST',
headers,
body: JSON.stringify(req),
})
.then(response => {
if (response.status !== 200 && response.status !== 201) {
throw new Error('failed to upload to transifex');
}
return response.text();
})
.then(handleResponse)
.catch(handleError);
});
}

3
extras/recsys/index.js Normal file
View file

@ -0,0 +1,3 @@
import Recsys from './recsys';
export default Recsys;

View file

@ -1,10 +1,12 @@
import { selectUser } from 'redux/selectors/user';
import { makeSelectRecommendedRecsysIdForClaimId } from 'redux/selectors/search';
import { v4 as Uuidv4 } from 'uuid';
import { parseURI, SETTINGS, makeSelectClaimForUri } from 'lbry-redux';
import { parseURI } from 'util/lbryURI';
import * as SETTINGS from 'constants/settings';
import { makeSelectClaimForUri } from 'redux/selectors/claims';
import { selectPlayingUri, selectPrimaryUri } from 'redux/selectors/content';
import { makeSelectClientSetting, selectDaemonSettings } from 'redux/selectors/settings';
import { history } from './store';
import { history } from 'ui/store';
const recsysEndpoint = 'https://clickstream.odysee.com/log/video/view';
const recsysId = 'lighthouse-v0';
@ -71,7 +73,7 @@ const recsys = {
* Called from recommendedContent component
*/
onRecsLoaded: function (claimId, uris) {
if (window.store) {
if (window && window.store) {
const state = window.store.getState();
if (!recsys.entries[claimId]) {
recsys.createRecsysEntry(claimId);
@ -90,9 +92,10 @@ const recsys = {
* @param: parentUuid: string (optional)
*/
createRecsysEntry: function (claimId, parentUuid) {
if (window.store && claimId) {
if (window && window.store && claimId) {
const state = window.store.getState();
const { id: userId } = selectUser(state);
const user = selectUser(state);
const userId = user ? user.id : null;
if (parentUuid) {
// Make a stub entry that will be filled out on page load
recsys.entries[claimId] = {
@ -170,7 +173,7 @@ const recsys = {
* if so, send the Entry.
*/
onPlayerDispose: function (claimId, isEmbedded) {
if (window.store) {
if (window && window.store) {
const state = window.store.getState();
const playingUri = selectPlayingUri(state);
const primaryUri = selectPrimaryUri(state);
@ -193,7 +196,7 @@ const recsys = {
// * more events until player is disposed. Don't send unless floatingPlayer playingUri
// */
// onLeaveFilePage: function (primaryUri) {
// if (window.store) {
// if (window && window.store) {
// const state = window.store.getState();
// const claim = makeSelectClaimForUri(primaryUri)(state);
// const claimId = claim ? claim.claim_id : null;
@ -219,7 +222,7 @@ const recsys = {
* Send all claimIds that aren't currently playing.
*/
onNavigate: function () {
if (window.store) {
if (window && window.store) {
const state = window.store.getState();
const playingUri = selectPlayingUri(state);
const actualPlayingUri = playingUri && playingUri.uri;

10
flow-typed/Blocklist.js vendored Normal file
View file

@ -0,0 +1,10 @@
declare type BlocklistState = {
blockedChannels: Array<string>
};
declare type BlocklistAction = {
type: string,
data: {
uri: string,
},
};

214
flow-typed/Claim.js vendored Normal file
View file

@ -0,0 +1,214 @@
// @flow
declare type Claim = StreamClaim | ChannelClaim | CollectionClaim;
declare type ChannelClaim = GenericClaim & {
value: ChannelMetadata,
};
declare type CollectionClaim = GenericClaim & {
value: CollectionMetadata,
};
declare type StreamClaim = GenericClaim & {
value: StreamMetadata,
};
declare type GenericClaim = {
address: string, // address associated with tx
amount: string, // bid amount at time of tx
canonical_url: string, // URL with short id, includes channel with short id
claim_id: string, // unique claim identifier
claim_sequence: number, // not being used currently
claim_op: 'create' | 'update',
confirmations: number,
decoded_claim: boolean, // Not available currently https://github.com/lbryio/lbry/issues/2044
timestamp?: number, // date of last transaction
height: number, // block height the tx was confirmed
is_channel_signature_valid?: boolean,
is_my_output: boolean,
name: string,
normalized_name: string, // `name` normalized via unicode NFD spec,
nout: number, // index number for an output of a tx
permanent_url: string, // name + claim_id
short_url: string, // permanent_url with short id, no channel
txid: string, // unique tx id
type: 'claim' | 'update' | 'support',
value_type: 'stream' | 'channel' | 'collection',
signing_channel?: ChannelClaim,
reposted_claim?: GenericClaim,
repost_channel_url?: string,
repost_url?: string,
repost_bid_amount?: string,
purchase_receipt?: PurchaseReceipt,
meta: {
activation_height: number,
claims_in_channel?: number,
creation_height: number,
creation_timestamp: number,
effective_amount: string,
expiration_height: number,
is_controlling: boolean,
support_amount: string,
reposted: number,
trending_global: number,
trending_group: number,
trending_local: number,
trending_mixed: number,
},
};
declare type GenericMetadata = {
title?: string,
description?: string,
thumbnail?: {
url?: string,
},
languages?: Array<string>,
tags?: Array<string>,
locations?: Array<Location>,
};
declare type ChannelMetadata = GenericMetadata & {
public_key: string,
public_key_id: string,
cover_url?: string,
email?: string,
website_url?: string,
featured?: Array<string>,
};
declare type CollectionMetadata = GenericMetadata & {
claims: Array<string>,
}
declare type StreamMetadata = GenericMetadata & {
license?: string, // License "title" ex: Creative Commons, Custom copyright
license_url?: string, // Link to full license
release_time?: number, // linux timestamp
author?: string,
source: {
sd_hash: string,
media_type?: string,
hash?: string,
name?: string, // file name
size?: number, // size of file in bytes
},
// Only exists if a stream has a fee
fee?: Fee,
stream_type: 'video' | 'audio' | 'image' | 'software',
// Below correspond to `stream_type`
video?: {
duration: number,
height: number,
width: number,
},
audio?: {
duration: number,
},
image?: {
height: number,
width: number,
},
software?: {
os: string,
},
};
declare type Location = {
latitude?: number,
longitude?: number,
country?: string,
state?: string,
city?: string,
code?: string,
};
declare type Fee = {
amount: string,
currency: string,
address: string,
};
declare type PurchaseReceipt = {
address: string,
amount: string,
claim_id: string,
confirmations: number,
height: number,
nout: number,
timestamp: number,
txid: string,
type: 'purchase',
};
declare type ClaimActionResolveInfo = {
[string]: {
stream: ?StreamClaim,
channel: ?ChannelClaim,
claimsInChannel: ?number,
collection: ?CollectionClaim,
},
}
declare type ChannelUpdateParams = {
claim_id: string,
bid?: string,
title?: string,
cover_url?: string,
thumbnail_url?: string,
description?: string,
website_url?: string,
email?: string,
tags?: Array<string>,
replace?: boolean,
languages?: Array<string>,
locations?: Array<string>,
blocking?: boolean,
}
declare type ChannelPublishParams = {
name: string,
bid: string,
blocking?: true,
title?: string,
cover_url?: string,
thumbnail_url?: string,
description?: string,
website_url?: string,
email?: string,
tags?: Array<string>,
languages?: Array<string>,
}
declare type CollectionUpdateParams = {
claim_id: string,
claim_ids?: Array<string>,
bid?: string,
title?: string,
cover_url?: string,
thumbnail_url?: string,
description?: string,
website_url?: string,
email?: string,
tags?: Array<string>,
replace?: boolean,
languages?: Array<string>,
locations?: Array<string>,
blocking?: boolean,
}
declare type CollectionPublishParams = {
name: string,
bid: string,
claim_ids: Array<string>,
blocking?: true,
title?: string,
thumbnail_url?: string,
description?: string,
tags?: Array<string>,
languages?: Array<string>,
}

29
flow-typed/CoinSwap.js vendored Normal file
View file

@ -0,0 +1,29 @@
declare type CoinSwapInfo = {
chargeCode: string,
coins: Array<string>,
sendAddresses: { [string]: string},
sendAmounts: { [string]: any },
lbcAmount: number,
status?: {
status: string,
receiptCurrency: string,
receiptTxid: string,
lbcTxid: string,
},
}
declare type CoinSwapState = {
coinSwaps: Array<CoinSwapInfo>,
};
declare type CoinSwapAddAction = {
type: string,
data: CoinSwapInfo,
};
declare type CoinSwapRemoveAction = {
type: string,
data: {
chargeCode: string,
},
};

34
flow-typed/Collections.js vendored Normal file
View file

@ -0,0 +1,34 @@
declare type Collection = {
id: string,
items: Array<?string>,
name: string,
type: string,
updatedAt: number,
totalItems?: number,
sourceId?: string, // if copied, claimId of original collection
};
declare type CollectionState = {
unpublished: CollectionGroup,
resolved: CollectionGroup,
pending: CollectionGroup,
edited: CollectionGroup,
builtin: CollectionGroup,
saved: Array<string>,
isResolvingCollectionById: { [string]: boolean },
error?: string | null,
};
declare type CollectionGroup = {
[string]: Collection,
}
declare type CollectionEditParams = {
claims?: Array<Claim>,
remove?: boolean,
claimIds?: Array<string>,
replace?: boolean,
order?: { from: number, to: number },
type?: string,
name?: string,
}

View file

@ -45,6 +45,7 @@ declare type CommentsState = {
isLoading: boolean,
isLoadingById: boolean,
isLoadingByParentId: { [string]: boolean },
isCommenting: boolean,
myComments: ?Set<string>,
isFetchingReacts: boolean,
myReactsByCommentId: ?{ [string]: Array<string> }, // {"CommentId:MyChannelId": ["like", "dislike", ...]}
@ -177,7 +178,7 @@ declare type CommentCreateParams = {
claim_id: string,
parent_id?: string,
signature: string,
signing_ts: number,
signing_ts: string,
support_tx_id?: string,
};

369
flow-typed/Lbry.js vendored Normal file
View file

@ -0,0 +1,369 @@
// @flow
declare type StatusResponse = {
blob_manager: {
finished_blobs: number,
},
blockchain_headers: {
download_progress: number,
downloading_headers: boolean,
},
dht: {
node_id: string,
peers_in_routing_table: number,
},
hash_announcer: {
announce_queue_size: number,
},
installation_id: string,
is_running: boolean,
skipped_components: Array<string>,
startup_status: {
blob_manager: boolean,
blockchain_headers: boolean,
database: boolean,
dht: boolean,
exchange_rate_manager: boolean,
hash_announcer: boolean,
peer_protocol_server: boolean,
stream_manager: boolean,
upnp: boolean,
wallet: boolean,
},
stream_manager: {
managed_files: number,
},
upnp: {
aioupnp_version: string,
dht_redirect_set: boolean,
external_ip: string,
gateway: string,
peer_redirect_set: boolean,
redirects: {},
},
wallet: ?{
connected: string,
best_blockhash: string,
blocks: number,
blocks_behind: number,
is_encrypted: boolean,
is_locked: boolean,
headers_synchronization_progress: number,
available_servers: number,
},
};
declare type VersionResponse = {
build: string,
lbrynet_version: string,
os_release: string,
os_system: string,
platform: string,
processor: string,
python_version: string,
};
declare type BalanceResponse = {
available: string,
reserved: string,
reserved_subtotals: ?{
claims: string,
supports: string,
tips: string,
},
total: string,
};
declare type ResolveResponse = {
// Keys are the url(s) passed to resolve
[string]: { error?: {}, stream?: StreamClaim, channel?: ChannelClaim, collection?: CollectionClaim, claimsInChannel?: number },
};
declare type GetResponse = FileListItem & { error?: string };
declare type GenericTxResponse = {
height: number,
hex: string,
inputs: Array<{}>,
outputs: Array<{}>,
total_fee: string,
total_input: string,
total_output: string,
txid: string,
};
declare type PublishResponse = GenericTxResponse & {
// Only first value in outputs is a claim
// That's the only value we care about
outputs: Array<Claim>,
};
declare type ClaimSearchResponse = {
items: Array<Claim>,
page: number,
page_size: number,
total_items: number,
total_pages: number,
};
declare type ClaimListResponse = {
items: Array<ChannelClaim | Claim>,
page: number,
page_size: number,
total_items: number,
total_pages: number,
};
declare type ChannelCreateResponse = GenericTxResponse & {
outputs: Array<ChannelClaim>,
};
declare type ChannelUpdateResponse = GenericTxResponse & {
outputs: Array<ChannelClaim>,
};
declare type CommentCreateResponse = Comment;
declare type CommentUpdateResponse = Comment;
declare type MyReactions = {
// Keys are the commentId
[string]: Array<string>,
};
declare type OthersReactions = {
// Keys are the commentId
[string]: {
// Keys are the reaction_type, e.g. 'like'
[string]: number,
},
};
declare type CommentReactListResponse = {
my_reactions: Array<MyReactions>,
others_reactions: Array<OthersReactions>,
};
declare type CommentHideResponse = {
// keyed by the CommentIds entered
[string]: { hidden: boolean },
};
declare type CommentPinResponse = {
// keyed by the CommentIds entered
items: Comment,
};
declare type CommentAbandonResponse = {
// keyed by the CommentId given
abandoned: boolean,
};
declare type ChannelListResponse = {
items: Array<ChannelClaim>,
page: number,
page_size: number,
total_items: number,
total_pages: number,
};
declare type ChannelSignResponse = {
signature: string,
signing_ts: string,
};
declare type CollectionCreateResponse = {
outputs: Array<Claim>,
page: number,
page_size: number,
total_items: number,
total_pages: number,
}
declare type CollectionListResponse = {
items: Array<Claim>,
page: number,
page_size: number,
total_items: number,
total_pages: number,
};
declare type CollectionResolveResponse = {
items: Array<Claim>,
total_items: number,
};
declare type CollectionResolveOptions = {
claim_id: string,
};
declare type CollectionListOptions = {
page: number,
page_size: number,
resolve?: boolean,
};
declare type FileListResponse = {
items: Array<FileListItem>,
page: number,
page_size: number,
total_items: number,
total_pages: number,
};
declare type TxListResponse = {
items: Array<Transaction>,
page: number,
page_size: number,
total_items: number,
total_pages: number,
};
declare type SupportListResponse = {
items: Array<Support>,
page: number,
page_size: number,
total_items: number,
total_pages: number,
};
declare type BlobListResponse = { items: Array<string> };
declare type WalletListResponse = Array<{
id: string,
name: string,
}>;
declare type WalletStatusResponse = {
is_encrypted: boolean,
is_locked: boolean,
is_syncing: boolean,
};
declare type SyncApplyResponse = {
hash: string,
data: string,
};
declare type SupportAbandonResponse = GenericTxResponse;
declare type StreamListResponse = {
items: Array<StreamClaim>,
page: number,
page_size: number,
total_items: number,
total_pages: number,
};
declare type StreamRepostOptions = {
name: string,
bid: string,
claim_id: string,
channel_id?: string,
};
declare type StreamRepostResponse = GenericTxResponse;
declare type PurchaseListResponse = {
items: Array<PurchaseReceipt & { claim: StreamClaim }>,
page: number,
page_size: number,
total_items: number,
total_pages: number,
};
declare type PurchaseListOptions = {
page: number,
page_size: number,
resolve: boolean,
claim_id?: string,
channel_id?: string,
};
//
// Types used in the generic Lbry object that is exported
//
declare type LbryTypes = {
isConnected: boolean,
connectPromise: any, // null |
connect: () => any, // void | Promise<any> ?
daemonConnectionString: string,
alternateConnectionString: string,
methodsUsingAlternateConnectionString: Array<string>,
apiRequestHeaders: { [key: string]: string },
setDaemonConnectionString: string => void,
setApiHeader: (string, string) => void,
unsetApiHeader: string => void,
overrides: { [string]: ?Function },
setOverride: (string, Function) => void,
// getMediaType: (?string, ?string) => string,
// Lbry Methods
stop: () => Promise<string>,
status: () => Promise<StatusResponse>,
version: () => Promise<VersionResponse>,
resolve: (params: {}) => Promise<ResolveResponse>,
get: (params: {}) => Promise<GetResponse>,
publish: (params: {}) => Promise<PublishResponse>,
claim_search: (params: {}) => Promise<ClaimSearchResponse>,
claim_list: (params: {}) => Promise<ClaimListResponse>,
channel_create: (params: {}) => Promise<ChannelCreateResponse>,
channel_update: (params: {}) => Promise<ChannelUpdateResponse>,
channel_import: (params: {}) => Promise<string>,
channel_list: (params: {}) => Promise<ChannelListResponse>,
channel_sign: (params: {}) => Promise<ChannelSignResponse>,
stream_abandon: (params: {}) => Promise<GenericTxResponse>,
stream_list: (params: {}) => Promise<StreamListResponse>,
channel_abandon: (params: {}) => Promise<GenericTxResponse>,
support_create: (params: {}) => Promise<GenericTxResponse>,
support_list: (params: {}) => Promise<SupportListResponse>,
support_abandon: (params: {}) => Promise<SupportAbandonResponse>,
stream_repost: (params: StreamRepostOptions) => Promise<StreamRepostResponse>,
purchase_list: (params: PurchaseListOptions) => Promise<PurchaseListResponse>,
collection_resolve: (params: CollectionResolveOptions) => Promise<CollectionResolveResponse>,
collection_list: (params: CollectionListOptions) => Promise<CollectionListResponse>,
collection_create: (params: {}) => Promise<CollectionCreateResponse>,
collection_update: (params: {}) => Promise<CollectionCreateResponse>,
// File fetching and manipulation
file_list: (params: {}) => Promise<FileListResponse>,
file_delete: (params: {}) => Promise<boolean>,
blob_delete: (params: {}) => Promise<string>,
blob_list: (params: {}) => Promise<BlobListResponse>,
file_set_status: (params: {}) => Promise<any>,
file_reflect: (params: {}) => Promise<any>,
// Preferences
preference_get: (params?: {}) => Promise<any>,
preference_set: (params: {}) => Promise<any>,
// Commenting
comment_update: (params: {}) => Promise<CommentUpdateResponse>,
comment_hide: (params: {}) => Promise<CommentHideResponse>,
comment_abandon: (params: {}) => Promise<CommentAbandonResponse>,
comment_list: (params: {}) => Promise<any>,
comment_create: (params: {}) => Promise<any>,
// Wallet utilities
wallet_balance: (params: {}) => Promise<BalanceResponse>,
wallet_decrypt: (prams: {}) => Promise<boolean>,
wallet_encrypt: (params: {}) => Promise<boolean>,
wallet_unlock: (params: {}) => Promise<boolean>,
wallet_list: (params: {}) => Promise<WalletListResponse>,
wallet_send: (params: {}) => Promise<GenericTxResponse>,
wallet_status: (params?: {}) => Promise<WalletStatusResponse>,
address_is_mine: (params: {}) => Promise<boolean>,
address_unused: (params: {}) => Promise<string>, // New address
address_list: (params: {}) => Promise<string>,
transaction_list: (params: {}) => Promise<TxListResponse>,
txo_list: (params: {}) => Promise<any>,
account_set: (params: {}) => Promise<any>,
account_list: (params?: {}) => Promise<any>,
// Sync
sync_hash: (params?: {}) => Promise<string>,
sync_apply: (params: {}) => Promise<SyncApplyResponse>,
// syncGet
// The app shouldn't need to do this
utxo_release: () => Promise<any>,
};

99
flow-typed/LbryFirst.js vendored Normal file
View file

@ -0,0 +1,99 @@
// @flow
declare type LbryFirstStatusResponse = {
Version: string,
Message: string,
Running: boolean,
Commit: string,
};
declare type LbryFirstVersionResponse = {
build: string,
lbrynet_version: string,
os_release: string,
os_system: string,
platform: string,
processor: string,
python_version: string,
};
/* SAMPLE UPLOAD RESPONSE (FULL)
"Video": {
"etag": "\"Dn5xIderbhAnUk5TAW0qkFFir0M/xlGLrlTox7VFTRcR8F77RbKtaU4\"",
"id": "8InjtdvVmwE",
"kind": "youtube#video",
"snippet": {
"categoryId": "22",
"channelId": "UCXiVsGTU88fJjheB2rqF0rA",
"channelTitle": "Mark Beamer",
"liveBroadcastContent": "none",
"localized": {
"title": "my title"
},
"publishedAt": "2020-05-05T04:17:53.000Z",
"thumbnails": {
"default": {
"height": 90,
"url": "https://i9.ytimg.com/vi/8InjtdvVmwE/default.jpg?sqp=CMTQw_UF&rs=AOn4CLB6dlhZMSMrazDlWRsitPgCsn8fVw",
"width": 120
},
"high": {
"height": 360,
"url": "https://i9.ytimg.com/vi/8InjtdvVmwE/hqdefault.jpg?sqp=CMTQw_UF&rs=AOn4CLB-Je_7l6qvASRAR_bSGWZHaXaJWQ",
"width": 480
},
"medium": {
"height": 180,
"url": "https://i9.ytimg.com/vi/8InjtdvVmwE/mqdefault.jpg?sqp=CMTQw_UF&rs=AOn4CLCvSnDLqVznRNMKuvJ_0misY_chPQ",
"width": 320
}
},
"title": "my title"
},
"status": {
"embeddable": true,
"license": "youtube",
"privacyStatus": "private",
"publicStatsViewable": true,
"uploadStatus": "uploaded"
}
}
*/
declare type UploadResponse = {
Video: {
id: string,
snippet: {
channelId: string,
},
status: {
uploadStatus: string,
},
},
};
declare type HasYTAuthResponse = {
HashAuth: boolean,
};
declare type YTSignupResponse = {};
//
// Types used in the generic LbryFirst object that is exported
//
declare type LbryFirstTypes = {
isConnected: boolean,
connectPromise: ?Promise<any>,
connect: () => void,
lbryFirstConnectionString: string,
apiRequestHeaders: { [key: string]: string },
setApiHeader: (string, string) => void,
unsetApiHeader: string => void,
overrides: { [string]: ?Function },
setOverride: (string, Function) => void,
// LbryFirst Methods
stop: () => Promise<string>,
status: () => Promise<StatusResponse>,
version: () => Promise<VersionResponse>,
upload: any => Promise<?UploadResponse>,
hasYTAuth: string => Promise<HasYTAuthResponse>,
ytSignup: () => Promise<YTSignupResponse>,
};

5
flow-typed/Reflector.js vendored Normal file
View file

@ -0,0 +1,5 @@
declare type ReflectingUpdate = {
fileListItem: FileListItem,
progress: number | boolean,
stalled: boolean,
};

21
flow-typed/Tags.js vendored Normal file
View file

@ -0,0 +1,21 @@
declare type TagState = {
followedTags: FollowedTags,
knownTags: KnownTags,
};
declare type Tag = {
name: string,
};
declare type KnownTags = {
[string]: Tag,
};
declare type FollowedTags = Array<string>;
declare type TagAction = {
type: string,
data: {
name: string,
},
};

28
flow-typed/Transaction.js vendored Normal file
View file

@ -0,0 +1,28 @@
// @flow
declare type Transaction = {
amount: number,
claim_id: string,
claim_name: string,
fee: number,
nout: number,
txid: string,
type: string,
date: Date,
};
declare type Support = {
address: string,
amount: string,
claim_id: string,
confirmations: number,
height: string,
is_change: string,
is_mine: string,
name: string,
normalized_name: string,
nout: string,
permanent_url: string,
timestamp: number,
txid: string,
type: string,
};

27
flow-typed/Txo.js vendored Normal file
View file

@ -0,0 +1,27 @@
declare type Txo = {
amount: number,
claim_id: string,
normalized_name: string,
nout: number,
txid: string,
type: string,
value_type: string,
timestamp: number,
is_my_output: boolean,
is_my_input: boolean,
is_spent: boolean,
signing_channel?: {
channel_id: string,
},
};
declare type TxoListParams = {
page: number,
page_size: number,
type: string,
is_my_input?: boolean,
is_my_output?: boolean,
is_not_my_input?: boolean,
is_not_my_output?: boolean,
is_spent?: boolean,
};

2
flow-typed/i18n.js vendored Normal file
View file

@ -0,0 +1,2 @@
// @flow
declare function __(a: string, b?: {}): string;

21
flow-typed/lbryURI.js vendored Normal file
View file

@ -0,0 +1,21 @@
// @flow
declare type LbryUrlObj = {
// Path and channel will always exist when calling parseURI
// But they may not exist when code calls buildURI
isChannel?: boolean,
path?: string,
streamName?: string,
streamClaimId?: string,
channelName?: string,
channelClaimId?: string,
primaryClaimSequence?: number,
secondaryClaimSequence?: number,
primaryBidPosition?: number,
secondaryBidPosition?: number,
startTime?: number,
// Below are considered deprecated and should not be used due to unreliableness with claim.canonical_url
claimName?: string,
claimId?: string,
contentName?: string,
};

View file

@ -1,4 +1,97 @@
// @flow
import * as ACTIONS from 'constants/action_types';
/*
Toasts:
- First-in, first-out queue
- Simple messages that are shown in response to user interactions
- Never saved
- If they are the result of errors, use the isError flag when creating
- For errors that should interrupt user behavior, use Error
*/
declare type ToastParams = {
message: string,
title?: string,
linkText?: string,
linkTarget?: string,
isError?: boolean,
};
declare type Toast = {
id: string,
params: ToastParams,
};
declare type DoToast = {
type: ACTIONS.CREATE_TOAST,
data: Toast,
};
/*
Notifications:
- List of notifications based on user interactions/app notifications
- Always saved, but can be manually deleted
- Can happen in the background, or because of user interaction (ex: publish confirmed)
*/
declare type Notification = {
id: string, // Unique id
dateCreated: number,
isRead: boolean, // Used to display "new" notifications that a user hasn't seen yet
source?: string, // The type/area an notification is from. Used for sorting (ex: publishes, transactions)
// We may want to use priority/isDismissed in the future to specify how urgent a notification is
// and if the user should see it immediately
// isDissmied: boolean,
// priority?: number
};
declare type DoNotification = {
type: ACTIONS.CREATE_NOTIFICATION,
data: Notification,
};
declare type DoEditNotification = {
type: ACTIONS.EDIT_NOTIFICATION,
data: {
notification: Notification,
},
};
declare type DoDeleteNotification = {
type: ACTIONS.DELETE_NOTIFICATION,
data: {
id: string, // The id to delete
},
};
/*
Errors:
- First-in, first-out queue
- Errors that should interupt user behavior
- For errors that can be shown without interrupting a user, use Toast with the isError flag
*/
declare type ErrorNotification = {
title: string,
text: string,
};
declare type DoError = {
type: ACTIONS.CREATE_ERROR,
data: ErrorNotification,
};
declare type DoDismissError = {
type: ACTIONS.DISMISS_ERROR,
};
/*
NotificationState
*/
declare type NotificationState = {
notifications: Array<Notification>,
errors: Array<ErrorNotification>,
toasts: Array<Toast>,
};
declare type WebNotification = {
active_at: string,
created_at: string,

View file

@ -25,7 +25,7 @@ declare type UpdatePublishFormData = {
licenseType?: string,
uri?: string,
nsfw: boolean,
isMarkdownPost: boolean,
isMarkdownPost?: boolean,
};
declare type PublishParams = {

3
flow-typed/redux.js vendored
View file

@ -1,3 +1,6 @@
// @flow
/* eslint-disable no-use-before-define */
declare type GetState = () => any;
declare type Dispatch = any;
/* eslint-enable */

View file

@ -20,7 +20,7 @@
},
"main": "./dist/electron/main.js",
"scripts": {
"analyze": "source-map-explorer --only-mapped dist/electron/webpack/ui*.js --html dist/sourceMap.html",
"analyze": "yarn compile && source-map-explorer --no-border-checks web/dist/public/*.js --html analyzeResults.html",
"compile:electron": "node --max_old_space_size=4096 ./node_modules/webpack/bin/webpack.js --config webpack.electron.config.js",
"compile:web": "yarn copyenv && cd web && node --max_old_space_size=4096 ./node_modules/webpack/bin/webpack.js --config webpack.config.js",
"compile": "cross-env NODE_ENV=production yarn compile:electron && cross-env NODE_ENV=production yarn compile:web",
@ -38,17 +38,18 @@
"build:dir": "yarn build -- --dir -c.compression=store -c.mac.identity=null",
"crossenv": "./node_modules/cross-env/dist/bin/cross-env",
"flow": "flow",
"lint": "eslint 'ui/**/*.{js,jsx}' && eslint 'web/**/*.{js,jsx}' && eslint 'electron/**/*.js' && flow",
"lint-fix": "eslint --fix --quiet 'ui/**/*.{js,jsx}' && eslint --fix --quiet 'web/**/*.{js,jsx}' && eslint --fix --quiet 'electron/**/*.js'",
"lint": "eslint 'ui/**/*.{js,jsx}' && eslint 'extras/**/*.{js,jsx}' && eslint 'web/**/*.{js,jsx}' && eslint 'electron/**/*.js' && flow",
"lint-fix": "eslint --fix --quiet 'ui/**/*.{js,jsx}' && eslint --fix --quiet 'extras/**/*.{js,jsx}' && eslint --fix --quiet 'web/**/*.{js,jsx}' && eslint --fix --quiet 'electron/**/*.js'",
"format": "prettier 'src/**/*.{js,jsx,scss,json}' --write",
"flow-defs": "flow-typed install",
"precommit": "lint-staged",
"preinstall": "yarn cache clean lbry-redux && yarn cache clean lbryinc",
"preinstall": "",
"postinstall": "cd web && yarn && cd .. && if-env NODE_ENV=production && yarn postinstall:warning || if-env APP_ENV=web && echo 'Done installing deps' || yarn postinstall:electron",
"postinstall:electron": "electron-builder install-app-deps && node ./build/downloadDaemon.js && node ./build/downloadLBRYFirst.js",
"postinstall:warning": "echo '\n\nWARNING\n\nNot all node modules were installed because NODE_ENV is set to \"production\".\nThis should only be set after installing dependencies with \"yarn\". The app will not work.\n\n'"
},
"dependencies": {
"@ungap/from-entries": "^0.2.1",
"auto-launch": "^5.0.5",
"electron-dl": "^1.11.0",
"electron-log": "^2.2.12",
@ -59,6 +60,8 @@
"if-env": "^1.0.4",
"match-sorter": "^6.3.0",
"parse-duration": "^1.0.0",
"proxy-polyfill": "0.1.6",
"re-reselect": "^4.0.0",
"react-datetime-picker": "^3.2.1",
"react-plastic": "^1.1.1",
"react-top-loading-bar": "^2.0.1",
@ -68,6 +71,7 @@
"tempy": "^0.6.0",
"videojs-contrib-ads": "^6.9.0",
"videojs-ima": "^1.11.0",
"videojs-ima-player": "^0.5.6",
"videojs-logo": "^2.1.4"
},
"devDependencies": {
@ -84,7 +88,6 @@
"@babel/preset-flow": "^7.12.1",
"@babel/preset-react": "^7.0.0",
"@babel/register": "^7.0.0",
"@datapunt/matomo-tracker-js": "^0.1.4",
"@exponent/electron-cookies": "^2.0.0",
"@hot-loader/react-dom": "^16.13",
"@reach/auto-id": "^0.13.0",
@ -157,8 +160,6 @@
"imagesloaded": "^4.1.4",
"json-loader": "^0.5.4",
"lbry-format": "https://github.com/lbryio/lbry-format.git",
"lbry-redux": "lbryio/lbry-redux#0f930c4a7bfc7f164e6b3c6044050c1bc73f6ab8",
"lbryinc": "lbryio/lbryinc#0b4e41ef90d6347819dd3453f2f9398a5c1b4f36",
"lint-staged": "^7.0.2",
"localforage": "^1.7.1",
"lodash-es": "^4.17.14",

View file

@ -97,6 +97,7 @@
"About --[tab title in Channel Page]--": "About",
"About --[link title in Sidebar or Footer]--": "About",
"Community Guidelines": "Community Guidelines",
"FAQ and Support": "FAQ and Support",
"Share Channel": "Share Channel",
"Go to page:": "Go to page:",
"Enter a URL for your thumbnail.": "Enter a URL for your thumbnail.",
@ -468,11 +469,8 @@
"New --[clears Publish Form]--": "New",
"Loading": "Loading",
"This file is in your library.": "This file is in your library.",
"'claimName', 'channelName', and 'streamName' are all empty. One must be present to build a url.": "'claimName', 'channelName', and 'streamName' are all empty. One must be present to build a url.",
"Invalid claim ID %s.": "Invalid claim ID %s.",
"'claimId' should no longer be used. Use 'streamClaimId' or 'channelClaimId' instead": "'claimId' should no longer be used. Use 'streamClaimId' or 'channelClaimId' instead",
"View Tag": "View Tag",
"'claimName' should no longer be used. Use 'streamClaimName' or 'channelClaimName' instead": "'claimName' should no longer be used. Use 'streamClaimName' or 'channelClaimName' instead",
"Vietnamese": "Vietnamese",
"Thai": "Thai",
"Arabic": "Arabic",
@ -485,7 +483,6 @@
"Greek": "Greek",
"Hide": "Hide",
"Persian": "Persian",
"'contentName' should no longer be used. Use 'streamName' instead": "'contentName' should no longer be used. Use 'streamName' instead",
"Sorry, we can't preview this file.": "Sorry, we can't preview this file.",
"View File": "View File",
"Close": "Close",
@ -2163,6 +2160,8 @@
"Bank Accounts": "Bank Accounts",
"Connect a bank account to receive tips and compensation in your local currency.": "Connect a bank account to receive tips and compensation in your local currency.",
"Payment Methods": "Payment Methods",
"Total Received Tips": "Total Received Tips",
"Withdrawn": "Withdrawn",
"Incoming": "Incoming",
"Outgoing": "Outgoing",
"Credits --[transactions tab]--": "Credits",
@ -2178,11 +2177,29 @@
"Card Last 4": "Card Last 4",
"Search blocked channel name": "Search blocked channel name",
"Discuss": "Discuss",
"Validating...": "Validating...",
"[Removed]": "[Removed]",
"lbry.tv has been retired. You have been magically transported to Odysee.com. %more%": "lbry.tv has been retired. You have been magically transported to Odysee.com. %more%",
"Show more livestreams": "Show more livestreams",
"Creator": "Creator",
"From comments": "From comments",
"From search": "From search",
"Manage tags": "Manage tags",
"Notification Delivery": "Notification Delivery",
"Choose how you'd like to receive your Odysee notifications.": "Choose how you'd like to receive your Odysee notifications.",
"Desktop Notifications": "Desktop Notifications",
"Browser Notifications": "Browser Notifications",
"Receive push notifications in this browser, even when you're not on odysee.com": "Receive push notifications in this browser, even when you're not on odysee.com",
"Email Notification Topics": "Email Notification Topics",
"Choose which topics youd like to be emailed about.": "Choose which topics youd like to be emailed about.",
"Email Notifications": "Email Notifications",
"Receive notifications to the email address: %email%": "Receive notifications to the email address: %email%",
"Realtime push notifications straight to your browser.": "Realtime push notifications straight to your browser.",
"Don't miss another notification again.": "Don't miss another notification again.",
"Enable Push Notifications": "Enable Push Notifications",
"Dismiss": "Dismiss",
"Heads up: browser notifications are currently blocked in this browser.": "Heads up: browser notifications are currently blocked in this browser.",
"To enable push notifications please configure your browser to allow notifications on odysee.com.": "To enable push notifications please configure your browser to allow notifications on odysee.com.",
"There was an error enabling browser notifications. Please make sure your browser settings allow you to subscribe to notifications.": "There was an error enabling browser notifications. Please make sure your browser settings allow you to subscribe to notifications.",
"--end--": "--end--"
}

View file

@ -1,6 +1,19 @@
<!DOCTYPE html>
<html dir="ltr">
<head>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-BB8DNPB73F"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('consent', 'default', {
'ad_storage': 'denied',
'analytics_storage': 'denied',
});
gtag('js', new Date());
gtag('config', 'G-BB8DNPB73F');
</script>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Pragma" content="no-cache" />

View file

@ -1,15 +1,38 @@
// @flow
import { Lbryio } from 'lbryinc';
import * as Sentry from '@sentry/browser';
import MatomoTracker from '@datapunt/matomo-tracker-js';
import { history } from './store';
import * as RENDER_MODES from 'constants/file_render_modes';
import { SDK_API_PATH } from './index';
// @if TARGET='app'
import Native from 'native';
import ElectronCookies from '@exponent/electron-cookies';
import { generateInitialUrl } from 'util/url';
// @endif
import { MATOMO_ID, MATOMO_URL } from 'config';
// --- GA ---
// - Events: 500 max (cannot be deleted).
// - Dimensions: 25 max (cannot be deleted, but can be "archived"). Usually
// tied to an event parameter for reporting purposes.
//
// Given the limitations above, we need to plan ahead before adding new Events
// and Parameters.
//
// Events:
// - Find a Recommended Event that is closest to what you need.
// https://support.google.com/analytics/answer/9267735?hl=en
// - If doesn't exist, use a Custom Event.
//
// Parameters:
// - Custom parameters don't appear in automated reports until they are tied to
// a Dimension.
// - Add your entry to GA_DIMENSIONS below -- tt allows us to keep track so that
// we don't exceed the limit. Re-use existing parameters if possible.
// - Register the Dimension in GA Console to make it visible in reports.
export const GA_DIMENSIONS = {
TYPE: 'type',
ACTION: 'action',
VALUE: 'value',
START_TIME_MS: 'start_time_ms',
DURATION_MS: 'duration_ms',
END_TIME_MS: 'end_time_ms',
};
// import getConnectionSpeed from 'util/detect-user-bandwidth';
// let userDownloadBandwidthInBitsPerSecond;
@ -24,31 +47,23 @@ const isProduction = process.env.NODE_ENV === 'production';
const devInternalApis = process.env.LBRY_API_URL && process.env.LBRY_API_URL.includes('dev');
export const SHARE_INTERNAL = 'shareInternal';
const SHARE_THIRD_PARTY = 'shareThirdParty';
const WATCHMAN_BACKEND_ENDPOINT = 'https://watchman.na-backend.odysee.com/reports/playback';
const SEND_DATA_TO_WATCHMAN_INTERVAL = 10; // in seconds
// @if TARGET='app'
if (isProduction) {
ElectronCookies.enable({
origin: 'https://lbry.tv',
});
}
// @endif
type Analytics = {
appStartTime: number,
eventStartTime: any,
error: (string) => Promise<any>,
sentryError: ({} | string, {}) => Promise<any>,
pageView: (string, ?string) => void,
setUser: (Object) => void,
toggleInternal: (boolean, ?boolean) => void,
apiLogView: (string, string, string, ?number, ?() => void) => Promise<any>,
apiLogPublish: (ChannelClaim | StreamClaim) => void,
apiSyncTags: ({}) => void,
tagFollowEvent: (string, boolean, ?string) => void,
playerLoadedEvent: (?boolean) => void,
playerStartedEvent: (?boolean) => void,
playerLoadedEvent: (string, ?boolean) => void,
playerVideoStartedEvent: (?boolean) => void,
videoStartEvent: (string, number, string, number, string, any, number) => void,
videoIsPlaying: (boolean, any) => void,
videoBufferEvent: (
@ -64,15 +79,16 @@ type Analytics = {
}
) => Promise<any>,
adsFetchedEvent: () => void,
adsReceivedEvent: (any) => void,
adsErrorEvent: (any) => void,
emailProvidedEvent: () => void,
emailVerifiedEvent: () => void,
rewardEligibleEvent: () => void,
startupEvent: () => void,
initAppStartTime: (startTime: number) => void,
startupEvent: (time: number) => void,
eventStarted: (name: string, time: number, id?: string) => void,
eventCompleted: (name: string, time: number, id?: string) => void,
purchaseEvent: (number) => void,
readyEvent: (number) => void,
openUrlEvent: (string) => void,
reportEvent: (string, any) => void,
};
type LogPublishParams = {
@ -84,10 +100,8 @@ type LogPublishParams = {
let internalAnalyticsEnabled: boolean = IS_WEB || false;
// let thirdPartyAnalyticsEnabled: boolean = IS_WEB || false;
// @if TARGET='app'
if (window.localStorage.getItem(SHARE_INTERNAL) === 'true') internalAnalyticsEnabled = true;
// if (window.localStorage.getItem(SHARE_THIRD_PARTY) === 'true') thirdPartyAnalyticsEnabled = true;
// @endif
const isGaAllowed = internalAnalyticsEnabled && isProduction;
/**
* Determine the mobile device type viewing the data
@ -208,15 +222,14 @@ async function sendWatchmanData(body) {
},
body: JSON.stringify(body),
});
return response;
} catch (err) {
console.log('ERROR FROM WATCHMAN BACKEND');
console.log(err);
}
} catch (err) {}
}
const analytics: Analytics = {
appStartTime: 0,
eventStartTime: {},
// receive buffer events from tracking plugin and save buffer amounts and times for backend call
videoBufferEvent: async (claim, data) => {
amountOfBufferEvents = amountOfBufferEvents + 1;
@ -255,7 +268,7 @@ const analytics: Analytics = {
startWatchmanIntervalIfNotRunning();
}
},
videoStartEvent: (claimId, duration, poweredBy, passedUserId, canonicalUrl, passedPlayer, videoBitrate) => {
videoStartEvent: (claimId, timeToStartVideo, poweredBy, passedUserId, canonicalUrl, passedPlayer, videoBitrate) => {
// populate values for watchman when video starts
userId = passedUserId;
claimUrl = canonicalUrl;
@ -265,8 +278,7 @@ const analytics: Analytics = {
videoPlayer = passedPlayer;
bitrateAsBitsPerSecond = videoBitrate;
sendPromMetric('time_to_start', duration);
sendMatomoEvent('Media', 'TimeToStart', claimId, duration);
sendPromMetric('time_to_start', timeToStartVideo);
},
error: (message) => {
return new Promise((resolve) => {
@ -292,45 +304,17 @@ const analytics: Analytics = {
}
});
},
pageView: (path, search) => {
if (internalAnalyticsEnabled) {
const params: { href: string, customDimensions?: Array<{ id: number, value: ?string }> } = { href: `${path}` };
const dimensions = [];
const searchParams = search && new URLSearchParams(search);
if (searchParams && searchParams.get('src')) {
dimensions.push({ id: 1, value: searchParams.get('src') });
}
if (dimensions.length) {
params['customDimensions'] = dimensions;
}
MatomoInstance.trackPageView(params);
}
},
setUser: (userId) => {
if (internalAnalyticsEnabled && userId) {
window._paq.push(['setUserId', String(userId)]);
// @if TARGET='app'
Native.getAppVersionInfo().then(({ localVersion }) => {
sendMatomoEvent('Version', 'Desktop-Version', localVersion);
});
// @endif
if (isGaAllowed && userId && window.gtag) {
window.gtag('set', { user_id: userId });
}
},
toggleInternal: (enabled: boolean): void => {
// Always collect analytics on lbry.tv
// @if TARGET='app'
internalAnalyticsEnabled = enabled;
window.localStorage.setItem(SHARE_INTERNAL, enabled);
// @endif
// Always collect analytics on Odysee for now.
},
toggleThirdParty: (enabled: boolean): void => {
// Always collect analytics on lbry.tv
// @if TARGET='app'
// thirdPartyAnalyticsEnabled = enabled;
window.localStorage.setItem(SHARE_THIRD_PARTY, enabled);
// @endif
// Always collect analytics on Odysee for now.
},
apiLogView: (uri, outpoint, claimId, timeToStart) => {
@ -387,56 +371,108 @@ const analytics: Analytics = {
}
},
adsFetchedEvent: () => {
sendMatomoEvent('Media', 'AdsFetched');
sendGaEvent('ad_fetched');
},
adsReceivedEvent: (response) => {
sendMatomoEvent('Media', 'AdsReceived', JSON.stringify(response));
playerLoadedEvent: (renderMode, embedded) => {
const RENDER_MODE_TO_EVENT = (renderMode) => {
switch (renderMode) {
case RENDER_MODES.VIDEO:
return 'loaded_video';
case RENDER_MODES.AUDIO:
return 'loaded_audio';
case RENDER_MODES.MARKDOWN:
return 'loaded_markdown';
case RENDER_MODES.IMAGE:
return 'loaded_image';
default:
return 'loaded_misc';
}
};
sendGaEvent('player', {
[GA_DIMENSIONS.ACTION]: RENDER_MODE_TO_EVENT(renderMode),
[GA_DIMENSIONS.TYPE]: embedded ? 'embedded' : 'onsite',
});
},
adsErrorEvent: (response) => {
sendMatomoEvent('Media', 'AdsError', JSON.stringify(response));
},
playerLoadedEvent: (embedded) => {
sendMatomoEvent('Player', 'Loaded', embedded ? 'embedded' : 'onsite');
},
playerStartedEvent: (embedded) => {
sendMatomoEvent('Player', 'Started', embedded ? 'embedded' : 'onsite');
playerVideoStartedEvent: (embedded) => {
sendGaEvent('player', {
[GA_DIMENSIONS.ACTION]: 'started_video',
[GA_DIMENSIONS.TYPE]: embedded ? 'embedded' : 'onsite',
});
},
tagFollowEvent: (tag, following) => {
sendMatomoEvent('Tag', following ? 'Tag-Follow' : 'Tag-Unfollow', tag);
},
channelBlockEvent: (uri, blocked, location) => {
sendMatomoEvent(blocked ? 'Channel-Hidden' : 'Channel-Unhidden', uri);
sendGaEvent('tags', {
[GA_DIMENSIONS.ACTION]: following ? 'follow' : 'unfollow',
[GA_DIMENSIONS.VALUE]: tag,
});
},
emailProvidedEvent: () => {
sendMatomoEvent('Engagement', 'Email-Provided');
sendGaEvent('engagement', {
[GA_DIMENSIONS.TYPE]: 'email_provided',
});
},
emailVerifiedEvent: () => {
sendMatomoEvent('Engagement', 'Email-Verified');
sendGaEvent('engagement', {
[GA_DIMENSIONS.TYPE]: 'email_verified',
});
},
rewardEligibleEvent: () => {
sendMatomoEvent('Engagement', 'Reward-Eligible');
sendGaEvent('engagement', {
[GA_DIMENSIONS.TYPE]: 'reward_eligible',
});
},
openUrlEvent: (url: string) => {
sendMatomoEvent('Engagement', 'Open-Url', url);
sendGaEvent('engagement', {
[GA_DIMENSIONS.TYPE]: 'open_url',
url,
});
},
trendingAlgorithmEvent: (trendingAlgorithm: string) => {
sendMatomoEvent('Engagement', 'Trending-Algorithm', trendingAlgorithm);
sendGaEvent('engagement', {
[GA_DIMENSIONS.TYPE]: 'trending_algorithm',
trending_algorithm: trendingAlgorithm,
});
},
startupEvent: () => {
sendMatomoEvent('Startup', 'Startup');
initAppStartTime: (startTime: number) => {
analytics.appStartTime = startTime;
},
readyEvent: (timeToReady: number) => {
sendMatomoEvent('Startup', 'App-Ready', 'Time', timeToReady);
startupEvent: (time: number) => {
if (analytics.appStartTime !== 0) {
sendGaEvent('diag_app_ready', {
[GA_DIMENSIONS.DURATION_MS]: time - analytics.appStartTime,
});
}
},
eventStarted: (name: string, time: number, id?: string) => {
const key = id || name;
analytics.eventStartTime[key] = time;
},
eventCompleted: (name: string, time: number, id?: string) => {
const key = id || name;
if (analytics.eventStartTime[key]) {
sendGaEvent(name, {
[GA_DIMENSIONS.START_TIME_MS]: analytics.eventStartTime[key] - analytics.appStartTime,
[GA_DIMENSIONS.DURATION_MS]: time - analytics.eventStartTime[key],
[GA_DIMENSIONS.END_TIME_MS]: time - analytics.appStartTime,
});
delete analytics.eventStartTime[key];
}
},
purchaseEvent: (purchaseInt: number) => {
sendMatomoEvent('Purchase', 'Purchase-Complete', 'someLabel', purchaseInt);
sendGaEvent('purchase', {
// https://developers.google.com/analytics/devguides/collection/ga4/reference/events#purchase
[GA_DIMENSIONS.VALUE]: purchaseInt,
});
},
reportEvent: (event: string, params?: { [string]: string | number }) => {
sendGaEvent(event, params);
},
};
function sendMatomoEvent(category, action, name, value) {
if (internalAnalyticsEnabled) {
const event = { category, action, name, value };
MatomoInstance.trackEvent(event);
function sendGaEvent(event: string, params?: { [string]: string | number }) {
if (isGaAllowed && window.gtag) {
window.gtag('event', event, params);
}
}
@ -449,35 +485,12 @@ function sendPromMetric(name: string, value?: number) {
}
}
const MatomoInstance = new MatomoTracker({
urlBase: MATOMO_URL,
siteId: MATOMO_ID, // optional, default value: `1`
// heartBeat: { // optional, enabled by default
// active: true, // optional, default value: true
// seconds: 10 // optional, default value: `15
// },
// linkTracking: false // optional, default value: true
});
// Manually call the first page view
// React Router doesn't include this on `history.listen`
// @if TARGET='web'
analytics.pageView(window.location.pathname + window.location.search, window.location.search);
// @endif
// @if TARGET='app'
analytics.pageView(
window.location.pathname.split('.html')[1] + window.location.search || generateInitialUrl(window.location.hash)
);
// @endif;
// Listen for url changes and report
// This will include search queries
history.listen((location) => {
const { pathname, search } = location;
const page = `${pathname}${search}`;
analytics.pageView(page, search);
});
// Activate
if (isGaAllowed && window.gtag) {
window.gtag('consent', 'update', {
ad_storage: 'granted',
analytics_storage: 'granted',
});
}
export default analytics;

View file

@ -1,8 +1,3 @@
import { connect } from 'react-redux';
import IframeReact from './view';
const select = state => ({});
const perform = () => ({});
export default connect(select, perform)(IframeReact);
export default IframeReact;

View file

@ -1,6 +1,3 @@
import { connect } from 'react-redux';
import AbandonedChannelPreview from './view';
const select = (state, props) => ({});
export default connect(select)(AbandonedChannelPreview);
export default AbandonedChannelPreview;

View file

@ -2,7 +2,7 @@
import React from 'react';
import classnames from 'classnames';
import ChannelThumbnail from 'component/channelThumbnail';
import { parseURI } from 'lbry-redux';
import { parseURI } from 'util/lbryURI';
import ChannelBlockButton from 'component/channelBlockButton';
import ChannelMuteButton from 'component/channelMuteButton';
import SubscribeButton from 'component/subscribeButton';

View file

@ -5,13 +5,9 @@ import { selectGetSyncErrorMessage, selectSyncFatalError } from 'redux/selectors
import { doFetchAccessToken, doUserSetReferrer } from 'redux/actions/user';
import { selectUser, selectAccessToken, selectUserVerifiedEmail } from 'redux/selectors/user';
import { selectUnclaimedRewards } from 'redux/selectors/rewards';
import {
doFetchChannelListMine,
doFetchCollectionListMine,
SETTINGS,
selectMyChannelUrls,
doResolveUris,
} from 'lbry-redux';
import { doFetchChannelListMine, doFetchCollectionListMine, doResolveUris } from 'redux/actions/claims';
import { selectMyChannelUrls } from 'redux/selectors/claims';
import * as SETTINGS from 'constants/settings';
import { selectSubscriptions } from 'redux/selectors/subscriptions';
import {
makeSelectClientSetting,
@ -24,6 +20,7 @@ import {
selectAutoUpdateDownloaded,
selectModal,
selectActiveChannelClaim,
selectIsReloadRequired,
} from 'redux/selectors/app';
import { doGetWalletSyncPreference, doSetLanguage } from 'redux/actions/settings';
import { doSyncLoop } from 'redux/actions/sync';
@ -46,6 +43,7 @@ const select = (state) => ({
languages: selectLoadedLanguages(state),
autoUpdateDownloaded: selectAutoUpdateDownloaded(state),
isUpgradeAvailable: selectIsUpgradeAvailable(state),
isReloadRequired: selectIsReloadRequired(state),
syncError: selectGetSyncErrorMessage(state),
uploadCount: selectUploadCount(state),
rewards: selectUnclaimedRewards(state),

View file

@ -4,8 +4,9 @@ import React, { useEffect, useRef, useState, useLayoutEffect } from 'react';
import { lazyImport } from 'util/lazyImport';
import classnames from 'classnames';
import analytics from 'analytics';
import { buildURI, parseURI } from 'lbry-redux';
import { buildURI, parseURI } from 'util/lbryURI';
import { SIMPLE_SITE } from 'config';
import Nag from 'component/common/nag';
import Router from 'component/router/index';
import ReactModal from 'react-modal';
import { openContextMenu } from 'util/context-menu';
@ -17,10 +18,12 @@ import REWARDS from 'rewards';
import usePersistedState from 'effects/use-persisted-state';
import Spinner from 'component/spinner';
import LANGUAGES from 'constants/languages';
// @if TARGET='app'
import useZoom from 'effects/use-zoom';
import useHistoryNav from 'effects/use-history-nav';
// @endif
// @if TARGET='web'
import {
useDegradedPerformance,
@ -30,11 +33,11 @@ import {
STATUS_DOWN,
} from 'web/effects/use-degraded-performance';
// @endif
import LANGUAGE_MIGRATIONS from 'constants/language-migrations';
const FileDrop = lazyImport(() => import('component/fileDrop' /* webpackChunkName: "secondary" */));
const ModalRouter = lazyImport(() => import('modal/modalRouter' /* webpackChunkName: "secondary" */));
const Nag = lazyImport(() => import('component/common/nag' /* webpackChunkName: "secondary" */));
const NagContinueFirstRun = lazyImport(() =>
import('component/nagContinueFirstRun' /* webpackChunkName: "secondary" */)
);
@ -67,6 +70,8 @@ export const IS_MAC = navigator.userAgent.indexOf('Mac OS X') !== -1;
const MOUSE_BACK_BTN = 3;
const MOUSE_FORWARD_BTN = 4;
const imaLibraryPath = 'https://imasdk.googleapis.com/js/sdkloader/ima3.js';
type Props = {
language: string,
languages: Array<string>,
@ -88,6 +93,7 @@ type Props = {
onSignedIn: () => void,
setLanguage: (string) => void,
isUpgradeAvailable: boolean,
isReloadRequired: boolean,
autoUpdateDownloaded: boolean,
updatePreferences: () => Promise<any>,
getWalletSyncPref: () => Promise<any>,
@ -122,6 +128,7 @@ function App(props: Props) {
signIn,
autoUpdateDownloaded,
isUpgradeAvailable,
isReloadRequired,
requestDownloadUpgrade,
uploadCount,
history,
@ -325,21 +332,19 @@ function App(props: Props) {
}
}, [previousRewardApproved, isRewardApproved]);
// Load IMA3 SDK for aniview: DISABLED FOR NOW
// Load IMA3 SDK for aniview
// @if TARGET='web'
// useEffect(() => {
// if (ENABLE_PREROLL_ADS) {
// const script = document.createElement('script');
// script.src = `https://imasdk.googleapis.com/js/sdkloader/ima3.js`;
// script.async = true;
// // $FlowFixMe
// document.body.appendChild(script);
// return () => {
// // $FlowFixMe
// document.body.removeChild(script);
// };
// }
// });
useEffect(() => {
const script = document.createElement('script');
script.src = imaLibraryPath;
script.async = true;
// $FlowFixMe
document.body.appendChild(script);
return () => {
// $FlowFixMe
document.body.removeChild(script);
};
}, []);
// @endif
// @if TARGET='app'
@ -493,6 +498,14 @@ function App(props: Props) {
{user === null && <NagNoUser />}
{/* @endif */}
</React.Suspense>
{isReloadRequired && (
<Nag
message={__('A new version of Odysee is available.')}
actionText={__('Refresh')}
onClick={() => window.location.reload()}
/>
)}
</React.Fragment>
)}
</div>

View file

@ -1,5 +1,5 @@
import { connect } from 'react-redux';
import { makeSelectClaimForUri } from 'lbry-redux';
import { makeSelectClaimForUri } from 'redux/selectors/claims';
import { withRouter } from 'react-router';
import AutoplayCountdown from './view';
import { selectModal } from 'redux/selectors/app';

View file

@ -1,5 +1,5 @@
import { connect } from 'react-redux';
import { makeSelectMetadataItemForUri, makeSelectClaimForUri } from 'lbry-redux';
import { makeSelectMetadataItemForUri, makeSelectClaimForUri } from 'redux/selectors/claims';
import ChannelAbout from './view';
const select = (state, props) => ({

View file

@ -1,5 +1,5 @@
import { connect } from 'react-redux';
import { makeSelectClaimIdForUri } from 'lbry-redux';
import { makeSelectClaimIdForUri } from 'redux/selectors/claims';
import {
doCommentModUnBlock,
doCommentModBlock,

View file

@ -6,9 +6,9 @@ import {
makeSelectClaimIsMine,
makeSelectTotalPagesInChannelSearch,
makeSelectClaimForUri,
doResolveUris,
SETTINGS,
} from 'lbry-redux';
} from 'redux/selectors/claims';
import { doResolveUris } from 'redux/actions/claims';
import * as SETTINGS from 'constants/settings';
import { makeSelectChannelIsMuted } from 'redux/selectors/blocked';
import { withRouter } from 'react-router';
import { selectUserVerifiedEmail } from 'redux/selectors/user';

View file

@ -125,11 +125,11 @@ function ChannelContent(props: Props) {
<section className="card card--section">
<p>
{__(
'In response to a complaint we received under the US Digital Millennium Copyright Act, we have blocked access to this channel from our applications.'
'In response to a complaint we received under the US Digital Millennium Copyright Act, we have blocked access to this channel from our applications. Content may also be blocked due to DMCA Red Flag rules which are obvious copyright violations we come across, are discussed in public channels, or reported to us.'
)}
</p>
<div className="section__actions">
<Button button="link" href="https://lbry.com/faq/dmca" label={__('Read More')} />
<Button button="link" href="https://odysee.com/@OdyseeHelp:b/copyright:f" label={__('Read More')} />
</div>
</section>
)}

View file

@ -2,7 +2,7 @@ import { connect } from 'react-redux';
import { withRouter } from 'react-router';
import { DISABLE_COMMENTS_TAG } from 'constants/tags';
import ChannelDiscussion from './view';
import { makeSelectTagInClaimOrChannelForUri } from 'lbry-redux';
import { makeSelectTagInClaimOrChannelForUri } from 'redux/selectors/claims';
const select = (state, props) => {
const { search } = props.location;

View file

@ -4,17 +4,15 @@ import {
makeSelectThumbnailForUri,
makeSelectCoverForUri,
makeSelectMetadataItemForUri,
doUpdateChannel,
doCreateChannel,
makeSelectAmountForUri,
makeSelectClaimForUri,
selectUpdateChannelError,
selectUpdatingChannel,
selectCreateChannelError,
selectCreatingChannel,
selectBalance,
doClearChannelErrors,
} from 'lbry-redux';
} from 'redux/selectors/claims';
import { selectBalance } from 'redux/selectors/wallet';
import { doUpdateChannel, doCreateChannel, doClearChannelErrors } from 'redux/actions/claims';
import { doOpenModal } from 'redux/actions/app';
import { doUpdateBlockListForPublishedChannel } from 'redux/actions/comments';
import { doClaimInitialRewards } from 'redux/actions/rewards';

View file

@ -9,7 +9,7 @@ import TagsSearch from 'component/tagsSearch';
import { FF_MAX_CHARS_IN_DESCRIPTION } from 'constants/form-field';
import ErrorText from 'component/common/error-text';
import ChannelThumbnail from 'component/channelThumbnail';
import { isNameValid, parseURI } from 'lbry-redux';
import { isNameValid, parseURI } from 'util/lbryURI';
import ClaimAbandonButton from 'component/claimAbandonButton';
import { useHistory } from 'react-router-dom';
import { MINIMUM_PUBLISH_BID, INVALID_NAME_ERROR, ESTIMATED_FEE } from 'constants/claim';
@ -253,7 +253,7 @@ function ChannelForm(props: Props) {
let nameError;
if (!name && name !== undefined) {
nameError = __('A name is required for your url');
} else if (!isNameValid(name, false)) {
} else if (!isNameValid(name)) {
nameError = INVALID_NAME_ERROR;
}

View file

@ -1,5 +1,5 @@
import { connect } from 'react-redux';
import { makeSelectClaimForUri, makeSelectIsUriResolving } from 'lbry-redux';
import { makeSelectClaimForUri, makeSelectIsUriResolving } from 'redux/selectors/claims';
import ChannelMentionSuggestion from './view';
const select = (state, props) => ({

View file

@ -2,13 +2,14 @@ import { connect } from 'react-redux';
import { selectShowMatureContent } from 'redux/selectors/settings';
import { selectSubscriptions } from 'redux/selectors/subscriptions';
import { withRouter } from 'react-router';
import { doResolveUris, makeSelectClaimForUri } from 'lbry-redux';
import { makeSelectTopLevelCommentsForUri } from 'redux/selectors/comments';
import { makeSelectClaimForUri } from 'redux/selectors/claims';
import { doResolveUris } from 'redux/actions/claims';
import { selectTopLevelCommentsForUri } from 'redux/selectors/comments';
import ChannelMentionSuggestions from './view';
const select = (state, props) => {
const subscriptionUris = selectSubscriptions(state).map(({ uri }) => uri);
const topLevelComments = makeSelectTopLevelCommentsForUri(props.uri)(state);
const topLevelComments = selectTopLevelCommentsForUri(state, props.uri);
const commentorUris = [];
// Avoid repeated commentors

View file

@ -1,7 +1,7 @@
// @flow
import { Combobox, ComboboxInput, ComboboxPopover, ComboboxList } from '@reach/combobox';
import { Form } from 'component/common/form';
import { parseURI, regexInvalidURI } from 'lbry-redux';
import { parseURI, regexInvalidURI } from 'util/lbryURI';
import { SEARCH_OPTIONS } from 'constants/search';
import * as KEYCODES from 'constants/keycodes';
import ChannelMentionSuggestion from 'component/channelMentionSuggestion';
@ -69,8 +69,9 @@ export default function ChannelMentionSuggestions(props: Props) {
const allShownCanonical = [canonicalCreator, ...canonicalSubscriptions, ...canonicalCommentors];
const possibleMatches = allShownUris.filter((uri) => {
try {
// yuck a try catch in a filter?
const { channelName } = parseURI(uri);
return channelName.toLowerCase().includes(termToMatch);
return channelName && channelName.toLowerCase().includes(termToMatch);
} catch (e) {}
});

View file

@ -1,5 +1,6 @@
import { connect } from 'react-redux';
import { makeSelectIsUriResolving, doResolveUri } from 'lbry-redux';
import { makeSelectIsUriResolving } from 'redux/selectors/claims';
import { doResolveUri } from 'redux/actions/claims';
import { makeSelectWinningUriForQuery } from 'redux/selectors/search';
import ChannelMentionTopSuggestion from './view';

View file

@ -1,5 +1,5 @@
import { connect } from 'react-redux';
import { selectMyChannelClaims } from 'lbry-redux';
import { selectMyChannelClaims } from 'redux/selectors/claims';
import { selectActiveChannelClaim, selectIncognito } from 'redux/selectors/app';
import { doSetActiveChannel, doSetIncognito } from 'redux/actions/app';
import ChannelSelector from './view';

View file

@ -3,7 +3,7 @@ import {
makeSelectClaimForUri,
makeSelectStakedLevelForChannelUri,
makeSelectTotalStakedAmountForChannelUri,
} from 'lbry-redux';
} from 'redux/selectors/claims';
import ChannelStakedIndicator from './view';
const select = (state, props) => ({

View file

@ -1,5 +1,6 @@
import { connect } from 'react-redux';
import { makeSelectThumbnailForUri, doResolveUri, makeSelectClaimForUri, makeSelectIsUriResolving } from 'lbry-redux';
import { makeSelectThumbnailForUri, makeSelectClaimForUri, makeSelectIsUriResolving } from 'redux/selectors/claims';
import { doResolveUri } from 'redux/actions/claims';
import ChannelThumbnail from './view';
const select = (state, props) => ({

View file

@ -1,6 +1,6 @@
// @flow
import React from 'react';
import { parseURI } from 'lbry-redux';
import { parseURI } from 'util/lbryURI';
import classnames from 'classnames';
import Gerbil from './gerbil.png';
import FreezeframeWrapper from 'component/fileThumbnail/FreezeframeWrapper';
@ -10,7 +10,7 @@ import { AVATAR_DEFAULT } from 'config';
type Props = {
thumbnail: ?string,
uri: ?string,
uri: string,
className?: string,
thumbnailPreview: ?string,
obscure?: boolean,

View file

@ -1,5 +1,5 @@
import { connect } from 'react-redux';
import { makeSelectClaimForUri, makeSelectTitleForUri } from 'lbry-redux';
import { makeSelectClaimForUri, makeSelectTitleForUri } from 'redux/selectors/claims';
import ChannelTitle from './view';
const select = (state, props) => ({

View file

@ -2,7 +2,7 @@ import { connect } from 'react-redux';
import { doOpenModal } from 'redux/actions/app';
import ClaimAbandonButton from './view';
import { makeSelectClaimForUri } from 'lbry-redux';
import { makeSelectClaimForUri } from 'redux/selectors/claims';
const select = (state, props) => ({
claim: props.uri && makeSelectClaimForUri(props.uri)(state),

View file

@ -1,5 +1,5 @@
import { connect } from 'react-redux';
import { makeSelectChannelForClaimUri } from 'lbry-redux';
import { makeSelectChannelForClaimUri } from 'redux/selectors/claims';
import ClaimAuthor from './view';
const select = (state, props) => ({

View file

@ -2,12 +2,12 @@ import { connect } from 'react-redux';
import ClaimCollectionAdd from './view';
import { withRouter } from 'react-router';
import {
makeSelectClaimForUri,
doLocalCollectionCreate,
selectBuiltinCollections,
selectMyPublishedCollections,
selectMyUnpublishedCollections,
} from 'lbry-redux';
} from 'redux/selectors/collections';
import { makeSelectClaimForUri } from 'redux/selectors/claims';
import { doLocalCollectionCreate } from 'redux/actions/collections';
const select = (state, props) => ({
claim: makeSelectClaimForUri(props.uri)(state),

View file

@ -1,7 +1,8 @@
import { connect } from 'react-redux';
import { doOpenModal } from 'redux/actions/app';
import CollectionAddButton from './view';
import { makeSelectClaimForUri, makeSelectClaimUrlInCollection } from 'lbry-redux';
import { makeSelectClaimForUri } from 'redux/selectors/claims';
import { makeSelectClaimUrlInCollection } from 'redux/selectors/collections';
const select = (state, props) => {
const claim = makeSelectClaimForUri(props.uri)(state);

View file

@ -1,5 +1,5 @@
import { connect } from 'react-redux';
import { makeSelectClaimForUri } from 'lbry-redux';
import { makeSelectClaimForUri } from 'redux/selectors/claims';
import ClaimEffectiveAmount from './view';
const select = (state, props) => ({

View file

@ -1,6 +1,6 @@
import { connect } from 'react-redux';
import { makeSelectInsufficientCreditsForUri } from 'redux/selectors/content';
import { makeSelectClaimWasPurchased } from 'lbry-redux';
import { makeSelectClaimWasPurchased } from 'redux/selectors/claims';
import ClaimInsufficientCredits from './view';
const select = (state, props) => ({

View file

@ -1,5 +1,6 @@
import { connect } from 'react-redux';
import { doResolveUri, makeSelectClaimForUri, makeSelectIsUriResolving } from 'lbry-redux';
import { makeSelectClaimForUri, makeSelectIsUriResolving } from 'redux/selectors/claims';
import { doResolveUri } from 'redux/actions/claims';
import { doSetPlayingUri } from 'redux/actions/content';
import { punctuationMarks } from 'util/remark-lbry';
import { selectBlackListedOutpoints } from 'lbryinc';

View file

@ -1,6 +1,7 @@
import { connect } from 'react-redux';
import ClaimList from './view';
import { SETTINGS } from 'lbry-redux';
import * as SETTINGS from 'constants/settings';
import { makeSelectClientSetting } from 'redux/selectors/settings';
const select = (state) => ({

View file

@ -94,11 +94,21 @@ export default function ClaimList(props: Props) {
setCurrentSort(currentSort === SORT_NEW ? SORT_OLD : SORT_NEW);
}
function handleClaimClicked(e, claim, index) {
const handleClaimClicked = React.useCallback(
(e, claim, index) => {
if (onClick) {
onClick(e, claim, index);
}
}
},
[onClick]
);
const customShouldHide = React.useCallback((claim: StreamClaim) => {
// Hack to hide spee.ch thumbnail publishes
// If it meets these requirements, it was probably uploaded here:
// https://github.com/lbryio/lbry-redux/blob/master/src/redux/actions/publish.js#L74-L79
return claim.name.length === 24 && !claim.name.includes(' ') && claim.value.author === 'Spee.ch';
}, []);
useEffect(() => {
const handleScroll = debounce((e) => {
@ -195,13 +205,8 @@ export default function ClaimList(props: Props) {
showHiddenByUser={showHiddenByUser}
collectionId={collectionId}
showNoSourceClaims={showNoSourceClaims}
customShouldHide={(claim: StreamClaim) => {
// Hack to hide spee.ch thumbnail publishes
// If it meets these requirements, it was probably uploaded here:
// https://github.com/lbryio/lbry-redux/blob/master/src/redux/actions/publish.js#L74-L79
return claim.name.length === 24 && !claim.name.includes(' ') && claim.value.author === 'Spee.ch';
}}
onClick={(e, claim, index) => handleClaimClicked(e, claim, index)}
customShouldHide={customShouldHide}
onClick={handleClaimClicked}
/>
</React.Fragment>
))}

View file

@ -1,12 +1,12 @@
import { connect } from 'react-redux';
import {
doClaimSearch,
selectClaimsByUri,
selectClaimSearchByQuery,
selectClaimSearchByQueryLastPageReached,
selectFetchingClaimSearch,
SETTINGS,
} from 'lbry-redux';
} from 'redux/selectors/claims';
import { doClaimSearch } from 'redux/actions/claims';
import * as SETTINGS from 'constants/settings';
import { selectFollowedTags } from 'redux/selectors/tags';
import { selectMutedChannels } from 'redux/selectors/blocked';
import { doToggleTagFollowDesktop } from 'redux/actions/tags';

View file

@ -5,7 +5,9 @@ import * as CS from 'constants/claim_search';
import React from 'react';
import usePersistedState from 'effects/use-persisted-state';
import { withRouter } from 'react-router';
import { createNormalizedClaimSearchKey, MATURE_TAGS, splitBySeparator } from 'lbry-redux';
import { MATURE_TAGS } from 'constants/tags';
import { createNormalizedClaimSearchKey } from 'util/claim';
import { splitBySeparator } from 'util/lbryURI';
import Button from 'component/button';
import moment from 'moment';
import ClaimList from 'component/claimList';
@ -450,7 +452,9 @@ function ClaimListDiscover(props: Props) {
<p>
<I18nMessage
tokens={{
contact_support: <Button button="link" label={__('contact support')} href="https://odysee.com/@OdyseeHelp:b?view=about" />,
contact_support: (
<Button button="link" label={__('contact support')} href="https://odysee.com/@OdyseeHelp:b?view=about" />
),
}}
>
If you continue to have issues, please %contact_support%.

Some files were not shown because too many files have changed in this diff Show more