mirror of
https://github.com/LBRYFoundation/lbry-desktop.git
synced 2025-09-03 02:35:12 +00:00
Merge branch 'master' into accessibility
This commit is contained in:
commit
0db4e4ab51
110 changed files with 6688 additions and 1056 deletions
|
@ -6,7 +6,7 @@ MATOMO_ID=4
|
||||||
WEBPACK_WEB_PORT=9090
|
WEBPACK_WEB_PORT=9090
|
||||||
WEBPACK_ELECTRON_PORT=9091
|
WEBPACK_ELECTRON_PORT=9091
|
||||||
WEB_SERVER_PORT=1337
|
WEB_SERVER_PORT=1337
|
||||||
LBRY_WEB_API=https://api.lbry.tv
|
LBRY_WEB_API=https://api.na-backend.odysee.com
|
||||||
LBRY_WEB_STREAMING_API=https://cdn.lbryplayer.xyz
|
LBRY_WEB_STREAMING_API=https://cdn.lbryplayer.xyz
|
||||||
LBRY_WEB_BUFFER_API=https://collector-service.api.lbry.tv/api/v1/events/video
|
LBRY_WEB_BUFFER_API=https://collector-service.api.lbry.tv/api/v1/events/video
|
||||||
COMMENT_SERVER_API=https://comments.lbry.com/api/v2
|
COMMENT_SERVER_API=https://comments.lbry.com/api/v2
|
||||||
|
@ -27,6 +27,10 @@ SIMPLE_SITE=false
|
||||||
SHOW_ADS=true
|
SHOW_ADS=true
|
||||||
YRBL_HAPPY_IMG_URL=https://cdn.lbryplayer.xyz/api/v3/streams/free/yrbl-happy/7aa50a7e5adaf48691935d55e45d697547392929/839d9a
|
YRBL_HAPPY_IMG_URL=https://cdn.lbryplayer.xyz/api/v3/streams/free/yrbl-happy/7aa50a7e5adaf48691935d55e45d697547392929/839d9a
|
||||||
YRBL_SAD_IMG_URL=https://cdn.lbryplayer.xyz/api/v3/streams/free/yrbl-sad/c2d9649633d974e5ffb503925e1f17d951f1bd0f/f262dd
|
YRBL_SAD_IMG_URL=https://cdn.lbryplayer.xyz/api/v3/streams/free/yrbl-sad/c2d9649633d974e5ffb503925e1f17d951f1bd0f/f262dd
|
||||||
|
#FAVICON=https://cdn.lbryplayer.xyz/api/v3/streams/free/yrbl-sad/c2d9649633d974e5ffb503925e1f17d951f1bd0f/f262dd
|
||||||
|
#LOGO=https://cdn.lbryplayer.xyz/api/v3/streams/free/yrbl-sad/c2d9649633d974e5ffb503925e1f17d951f1bd0f/f262dd
|
||||||
|
#LOGO_TEXT_LIGHT=https://cdn.lbryplayer.xyz/api/v3/streams/free/yrbl-sad/c2d9649633d974e5ffb503925e1f17d951f1bd0f/f262dd
|
||||||
|
#LOGO_TEXT_DARK=https://cdn.lbryplayer.xyz/api/v3/streams/free/yrbl-sad/c2d9649633d974e5ffb503925e1f17d951f1bd0f/f262dd
|
||||||
|
|
||||||
ENABLE_COMMENT_REACTIONS=true
|
ENABLE_COMMENT_REACTIONS=true
|
||||||
ENABLE_FILE_REACTIONS=false
|
ENABLE_FILE_REACTIONS=false
|
||||||
|
@ -86,3 +90,4 @@ ENABLE_UI_NOTIFICATIONS=false
|
||||||
#USE_DISCOVER_WHITELIST=false
|
#USE_DISCOVER_WHITELIST=false
|
||||||
#ENABLE_WILD_WEST=false
|
#ENABLE_WILD_WEST=false
|
||||||
#FULL_SIDE_LINKS=true
|
#FULL_SIDE_LINKS=true
|
||||||
|
SHOW_TAGS_INTRO=true
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
[ignore]
|
[ignore]
|
||||||
.*\.typeface\.json
|
.*\.typeface\.json
|
||||||
.*/node_modules/findup/.*
|
.*/node_modules/findup/.*
|
||||||
|
.*/node_modules/react-plastic/.*
|
||||||
|
|
||||||
|
|
||||||
[include]
|
[include]
|
||||||
|
|
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
@ -5,6 +5,7 @@
|
||||||
Please check all that apply to this PR using "x":
|
Please check all that apply to this PR using "x":
|
||||||
|
|
||||||
- [ ] I have checked that this PR is not a duplicate of an existing PR (open, closed or merged)
|
- [ ] I have checked that this PR is not a duplicate of an existing PR (open, closed or merged)
|
||||||
|
- [ ] I added a line describing my change to CHANGELOG.md
|
||||||
- [ ] I have checked that this PR does not introduce a breaking change
|
- [ ] I have checked that this PR does not introduce a breaking change
|
||||||
- [ ] This PR introduces breaking changes and I have provided a detailed explanation below
|
- [ ] This PR introduces breaking changes and I have provided a detailed explanation below
|
||||||
|
|
||||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -33,3 +33,6 @@ package-lock.json
|
||||||
!/custom/robots.disallowall
|
!/custom/robots.disallowall
|
||||||
!/custom/robots.allowall
|
!/custom/robots.allowall
|
||||||
.env
|
.env
|
||||||
|
.env.ody
|
||||||
|
.env.desktop
|
||||||
|
.env.lbrytv
|
||||||
|
|
19
CHANGELOG.md
19
CHANGELOG.md
|
@ -1,8 +1,25 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
|
|
||||||
|
## [Unreleased for Desktop]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Show currently active playing item on playlist _community pr!_ ([#6453](https://github.com/lbryio/lbry-desktop/pull/6453))
|
||||||
|
- Add watch later to hover action for last used playlist on popup _community pr!_ ([#6274](https://github.com/lbryio/lbry-desktop/pull/6274))
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Use Canonical Url for copy link ([#6500](https://github.com/lbryio/lbry-desktop/pull/6500))
|
||||||
|
- Use better icon for copy link ([#6485](https://github.com/lbryio/lbry-desktop/pull/6485))
|
||||||
|
- Comments load paginated ([#6390](https://github.com/lbryio/lbry-desktop/pull/6390))
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- App now supports '#' and ':' for claimId separator ([#6496](https://github.com/lbryio/lbry-desktop/pull/6496))
|
||||||
|
- Fix "exact match" being applied to Recommended ([#6460](https://github.com/lbryio/lbry-desktop/pull/6460))
|
||||||
|
- Fix upload button on creator analytics _community pr!_ ([#6458](https://github.com/lbryio/lbry-desktop/pull/6458))
|
||||||
|
- Prevent sidebar shortcut activation on textarea _community pr!_ ([#6454](https://github.com/lbryio/lbry-desktop/pull/6454))
|
||||||
|
|
||||||
## [0.51.1] - [2021-06-26]
|
## [0.51.1] - [2021-06-26]
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
12
config.js
12
config.js
|
@ -8,7 +8,7 @@ const config = {
|
||||||
WEBPACK_WEB_PORT: process.env.WEBPACK_WEB_PORT,
|
WEBPACK_WEB_PORT: process.env.WEBPACK_WEB_PORT,
|
||||||
WEBPACK_ELECTRON_PORT: process.env.WEBPACK_ELECTRON_PORT,
|
WEBPACK_ELECTRON_PORT: process.env.WEBPACK_ELECTRON_PORT,
|
||||||
WEB_SERVER_PORT: process.env.WEB_SERVER_PORT,
|
WEB_SERVER_PORT: process.env.WEB_SERVER_PORT,
|
||||||
LBRY_WEB_API: process.env.LBRY_WEB_API, //api.lbry.tv',
|
LBRY_WEB_API: process.env.LBRY_WEB_API, //api.na-backend.odysee.com',
|
||||||
LBRY_API_URL: process.env.LBRY_API_URL, //api.lbry.com',
|
LBRY_API_URL: process.env.LBRY_API_URL, //api.lbry.com',
|
||||||
LBRY_WEB_STREAMING_API: process.env.LBRY_WEB_STREAMING_API, //cdn.lbryplayer.xyz',
|
LBRY_WEB_STREAMING_API: process.env.LBRY_WEB_STREAMING_API, //cdn.lbryplayer.xyz',
|
||||||
LBRY_WEB_BUFFER_API: process.env.LBRY_WEB_BUFFER_API,
|
LBRY_WEB_BUFFER_API: process.env.LBRY_WEB_BUFFER_API,
|
||||||
|
@ -22,10 +22,17 @@ const config = {
|
||||||
SITE_NAME: process.env.SITE_NAME,
|
SITE_NAME: process.env.SITE_NAME,
|
||||||
SITE_DESCRIPTION: process.env.SITE_DESCRIPTION,
|
SITE_DESCRIPTION: process.env.SITE_DESCRIPTION,
|
||||||
SITE_HELP_EMAIL: process.env.SITE_HELP_EMAIL,
|
SITE_HELP_EMAIL: process.env.SITE_HELP_EMAIL,
|
||||||
|
// LOGO
|
||||||
LOGO_TITLE: process.env.LOGO_TITLE,
|
LOGO_TITLE: process.env.LOGO_TITLE,
|
||||||
|
FAVICON: process.env.FAVICON,
|
||||||
|
LOGO_URL: process.env.LOGO_URL,
|
||||||
|
LOGO_TEXT_LIGHT_URL: process.env.LOGO_TEXT_LIGHT_URL,
|
||||||
|
LOGO_TEXT_DARK_URL: process.env.LOGO_TEXT_DARK_URL,
|
||||||
|
// OG
|
||||||
OG_TITLE_SUFFIX: process.env.OG_TITLE_SUFFIX,
|
OG_TITLE_SUFFIX: process.env.OG_TITLE_SUFFIX,
|
||||||
OG_HOMEPAGE_TITLE: process.env.OG_HOMEPAGE_TITLE,
|
OG_HOMEPAGE_TITLE: process.env.OG_HOMEPAGE_TITLE,
|
||||||
OG_IMAGE_URL: process.env.OG_IMAGE_URL,
|
OG_IMAGE_URL: process.env.OG_IMAGE_URL,
|
||||||
|
// MASCOT
|
||||||
YRBL_HAPPY_IMG_URL: process.env.YRBL_HAPPY_IMG_URL,
|
YRBL_HAPPY_IMG_URL: process.env.YRBL_HAPPY_IMG_URL,
|
||||||
YRBL_SAD_IMG_URL: process.env.YRBL_SAD_IMG_URL,
|
YRBL_SAD_IMG_URL: process.env.YRBL_SAD_IMG_URL,
|
||||||
LOGIN_IMG_URL: process.env.LOGIN_IMG_URL,
|
LOGIN_IMG_URL: process.env.LOGIN_IMG_URL,
|
||||||
|
@ -33,6 +40,8 @@ const config = {
|
||||||
DEFAULT_LANGUAGE: process.env.DEFAULT_LANGUAGE,
|
DEFAULT_LANGUAGE: process.env.DEFAULT_LANGUAGE,
|
||||||
AUTO_FOLLOW_CHANNELS: process.env.AUTO_FOLLOW_CHANNELS,
|
AUTO_FOLLOW_CHANNELS: process.env.AUTO_FOLLOW_CHANNELS,
|
||||||
UNSYNCED_SETTINGS: process.env.UNSYNCED_SETTINGS,
|
UNSYNCED_SETTINGS: process.env.UNSYNCED_SETTINGS,
|
||||||
|
|
||||||
|
// ENABLE FEATURES
|
||||||
ENABLE_COMMENT_REACTIONS: process.env.ENABLE_COMMENT_REACTIONS === 'true',
|
ENABLE_COMMENT_REACTIONS: process.env.ENABLE_COMMENT_REACTIONS === 'true',
|
||||||
ENABLE_FILE_REACTIONS: process.env.ENABLE_FILE_REACTIONS === 'true',
|
ENABLE_FILE_REACTIONS: process.env.ENABLE_FILE_REACTIONS === 'true',
|
||||||
ENABLE_CREATOR_REACTIONS: process.env.ENABLE_CREATOR_REACTIONS === 'true',
|
ENABLE_CREATOR_REACTIONS: process.env.ENABLE_CREATOR_REACTIONS === 'true',
|
||||||
|
@ -53,6 +62,7 @@ const config = {
|
||||||
ENABLE_UI_NOTIFICATIONS: process.env.ENABLE_UI_NOTIFICATIONS === 'true',
|
ENABLE_UI_NOTIFICATIONS: process.env.ENABLE_UI_NOTIFICATIONS === 'true',
|
||||||
ENABLE_MATURE: process.env.ENABLE_MATURE === 'true',
|
ENABLE_MATURE: process.env.ENABLE_MATURE === 'true',
|
||||||
CUSTOM_HOMEPAGE: process.env.CUSTOM_HOMEPAGE === 'true',
|
CUSTOM_HOMEPAGE: process.env.CUSTOM_HOMEPAGE === 'true',
|
||||||
|
SHOW_TAGS_INTRO: process.env.SHOW_TAGS_INTRO === 'true',
|
||||||
};
|
};
|
||||||
|
|
||||||
config.URL_LOCAL = `http://localhost:${config.WEB_SERVER_PORT}`;
|
config.URL_LOCAL = `http://localhost:${config.WEB_SERVER_PORT}`;
|
||||||
|
|
52
flow-typed/Comment.js
vendored
52
flow-typed/Comment.js
vendored
|
@ -14,6 +14,7 @@ declare type Comment = {
|
||||||
is_pinned: boolean,
|
is_pinned: boolean,
|
||||||
support_amount: number,
|
support_amount: number,
|
||||||
replies: number, // number of direct replies (i.e. excluding nested replies).
|
replies: number, // number of direct replies (i.e. excluding nested replies).
|
||||||
|
is_fiat?: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
declare type PerChannelSettings = {
|
declare type PerChannelSettings = {
|
||||||
|
@ -71,12 +72,33 @@ declare type CommentReactParams = {
|
||||||
remove?: boolean,
|
remove?: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
declare type CommentReactListParams = {
|
declare type ReactionReactParams = {
|
||||||
comment_ids?: string,
|
comment_ids: string,
|
||||||
|
signature?: string,
|
||||||
|
signing_ts?: string,
|
||||||
|
remove?: boolean,
|
||||||
|
clear_types?: string,
|
||||||
|
type: string,
|
||||||
|
channel_id: string,
|
||||||
|
channel_name: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
declare type ReactionReactResponse = {
|
||||||
|
Reactions: { [string]: { [string]: number} },
|
||||||
|
};
|
||||||
|
|
||||||
|
declare type ReactionListParams = {
|
||||||
|
comment_ids: string, // CSV of IDs
|
||||||
channel_id?: string,
|
channel_id?: string,
|
||||||
channel_name?: string,
|
channel_name?: string,
|
||||||
wallet_id?: string,
|
signature?: string,
|
||||||
react_types?: string,
|
signing_ts?: string,
|
||||||
|
types?: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
declare type ReactionListResponse = {
|
||||||
|
my_reactions: Array<MyReactions>,
|
||||||
|
others_reactions: Array<OthersReactions>,
|
||||||
};
|
};
|
||||||
|
|
||||||
declare type CommentListParams = {
|
declare type CommentListParams = {
|
||||||
|
@ -113,6 +135,28 @@ declare type CommentByIdResponse = {
|
||||||
ancestors: Array<Comment>,
|
ancestors: Array<Comment>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare type CommentPinParams = {
|
||||||
|
comment_id: string,
|
||||||
|
channel_id: string,
|
||||||
|
channel_name: string,
|
||||||
|
remove?: boolean,
|
||||||
|
signature: string,
|
||||||
|
signing_ts: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
declare type CommentPinResponse = {
|
||||||
|
items: Comment, // "items" is an inherited typo to match SDK. Will be "item" in a new version.
|
||||||
|
}
|
||||||
|
|
||||||
|
declare type CommentEditParams = {
|
||||||
|
comment: string,
|
||||||
|
comment_id: string,
|
||||||
|
signature: string,
|
||||||
|
signing_ts: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
declare type CommentEditResponse = Comment
|
||||||
|
|
||||||
declare type CommentAbandonParams = {
|
declare type CommentAbandonParams = {
|
||||||
comment_id: string,
|
comment_id: string,
|
||||||
creator_channel_id?: string,
|
creator_channel_id?: string,
|
||||||
|
|
1
flow-typed/homepage.js
vendored
1
flow-typed/homepage.js
vendored
|
@ -17,6 +17,7 @@ declare type RowDataItem = {
|
||||||
help?: any,
|
help?: any,
|
||||||
icon?: string,
|
icon?: string,
|
||||||
extra?: any,
|
extra?: any,
|
||||||
|
pinUrls?: Array<string>,
|
||||||
options?: {
|
options?: {
|
||||||
channelIds?: Array<string>,
|
channelIds?: Array<string>,
|
||||||
limitClaimsPerChannel?: number,
|
limitClaimsPerChannel?: number,
|
||||||
|
|
|
@ -38,6 +38,7 @@
|
||||||
"build:dir": "yarn build -- --dir -c.compression=store -c.mac.identity=null",
|
"build:dir": "yarn build -- --dir -c.compression=store -c.mac.identity=null",
|
||||||
"crossenv": "./node_modules/cross-env/dist/bin/cross-env",
|
"crossenv": "./node_modules/cross-env/dist/bin/cross-env",
|
||||||
"lint": "eslint 'ui/**/*.{js,jsx}' && eslint 'web/**/*.{js,jsx}' && eslint 'electron/**/*.js' && flow",
|
"lint": "eslint 'ui/**/*.{js,jsx}' && eslint 'web/**/*.{js,jsx}' && eslint 'electron/**/*.js' && flow",
|
||||||
|
"lint-fix": "eslint --fix 'ui/**/*.{js,jsx}' && eslint --fix 'web/**/*.{js,jsx}' && eslint --fix 'electron/**/*.js' && flow",
|
||||||
"format": "prettier 'src/**/*.{js,jsx,scss,json}' --write",
|
"format": "prettier 'src/**/*.{js,jsx,scss,json}' --write",
|
||||||
"flow-defs": "flow-typed install",
|
"flow-defs": "flow-typed install",
|
||||||
"precommit": "lint-staged",
|
"precommit": "lint-staged",
|
||||||
|
@ -56,6 +57,7 @@
|
||||||
"feed": "^4.2.2",
|
"feed": "^4.2.2",
|
||||||
"if-env": "^1.0.4",
|
"if-env": "^1.0.4",
|
||||||
"react-datetime-picker": "^3.2.1",
|
"react-datetime-picker": "^3.2.1",
|
||||||
|
"react-plastic": "^1.1.1",
|
||||||
"react-top-loading-bar": "^2.0.1",
|
"react-top-loading-bar": "^2.0.1",
|
||||||
"remove-markdown": "^0.3.0",
|
"remove-markdown": "^0.3.0",
|
||||||
"source-map-explorer": "^2.5.2",
|
"source-map-explorer": "^2.5.2",
|
||||||
|
@ -149,7 +151,7 @@
|
||||||
"imagesloaded": "^4.1.4",
|
"imagesloaded": "^4.1.4",
|
||||||
"json-loader": "^0.5.4",
|
"json-loader": "^0.5.4",
|
||||||
"lbry-format": "https://github.com/lbryio/lbry-format.git",
|
"lbry-format": "https://github.com/lbryio/lbry-format.git",
|
||||||
"lbry-redux": "lbryio/lbry-redux#8f66a2fe7c84d4587ec95698bce9f3e4360f8e88",
|
"lbry-redux": "lbryio/lbry-redux#a327385cdf71568dbd15a17f3dcf5f4b83e0966d",
|
||||||
"lbryinc": "lbryio/lbryinc#8f9a58bfc8312a65614fd7327661cdcc502c4e59",
|
"lbryinc": "lbryio/lbryinc#8f9a58bfc8312a65614fd7327661cdcc502c4e59",
|
||||||
"lint-staged": "^7.0.2",
|
"lint-staged": "^7.0.2",
|
||||||
"localforage": "^1.7.1",
|
"localforage": "^1.7.1",
|
||||||
|
|
|
@ -1200,7 +1200,7 @@
|
||||||
"Changelog": "Changelog",
|
"Changelog": "Changelog",
|
||||||
"Boost your content": "Boost your content",
|
"Boost your content": "Boost your content",
|
||||||
"Boost This Video": "Boost This Video",
|
"Boost This Video": "Boost This Video",
|
||||||
"Boost This Content": "Boost This Content",
|
"Boost This %claimTypeText%": "Boost This %claimTypeText%",
|
||||||
"Send a $%displayAmount% Tip": "Send a $%displayAmount% Tip",
|
"Send a $%displayAmount% Tip": "Send a $%displayAmount% Tip",
|
||||||
"Send a %displayAmount% Credit Tip": "Send a %displayAmount% Credit Tip",
|
"Send a %displayAmount% Credit Tip": "Send a %displayAmount% Credit Tip",
|
||||||
"Boost": "Boost",
|
"Boost": "Boost",
|
||||||
|
@ -1213,7 +1213,7 @@
|
||||||
"Buy more LBRY Credits": "Buy more LBRY Credits",
|
"Buy more LBRY Credits": "Buy more LBRY Credits",
|
||||||
"Buy or swap more LBRY Credits": "Buy or swap more LBRY Credits",
|
"Buy or swap more LBRY Credits": "Buy or swap more LBRY Credits",
|
||||||
"Buy or Swap": "Buy or Swap",
|
"Buy or Swap": "Buy or Swap",
|
||||||
"Support this content": "Support this content",
|
"Support This %claimTypeText%": "Support This %claimTypeText%",
|
||||||
"Custom support amount": "Custom support amount",
|
"Custom support amount": "Custom support amount",
|
||||||
"(%lbc_balance% Credits available)": "(%lbc_balance% Credits available)",
|
"(%lbc_balance% Credits available)": "(%lbc_balance% Credits available)",
|
||||||
"Loading your channels...": "Loading your channels...",
|
"Loading your channels...": "Loading your channels...",
|
||||||
|
@ -1458,6 +1458,8 @@
|
||||||
"Your channel is still being setup, try again in a few moments.": "Your channel is still being setup, try again in a few moments.",
|
"Your channel is still being setup, try again in a few moments.": "Your channel is still being setup, try again in a few moments.",
|
||||||
"Unable to delete this comment, please try again later.": "Unable to delete this comment, please try again later.",
|
"Unable to delete this comment, please try again later.": "Unable to delete this comment, please try again later.",
|
||||||
"Unable to edit this comment, please try again later.": "Unable to edit this comment, please try again later.",
|
"Unable to edit this comment, please try again later.": "Unable to edit this comment, please try again later.",
|
||||||
|
"No active channel selected.": "No active channel selected.",
|
||||||
|
"Unable to verify your channel. Please try again.": "Unable to verify your channel. Please try again.",
|
||||||
"Channel cannot be anonymous, please select a channel and try again.": "Channel cannot be anonymous, please select a channel and try again.",
|
"Channel cannot be anonymous, please select a channel and try again.": "Channel cannot be anonymous, please select a channel and try again.",
|
||||||
"Change to list layout": "Change to list layout",
|
"Change to list layout": "Change to list layout",
|
||||||
"Change to tile layout": "Change to tile layout",
|
"Change to tile layout": "Change to tile layout",
|
||||||
|
@ -2012,19 +2014,24 @@
|
||||||
"Chat": "Chat",
|
"Chat": "Chat",
|
||||||
"Tipped": "Tipped",
|
"Tipped": "Tipped",
|
||||||
"Fromage": "Fromage",
|
"Fromage": "Fromage",
|
||||||
"Item %action% Watch Later": "Item %action% Watch Later",
|
"In Favorites": "In Favorites",
|
||||||
"added to --[substring for \"Item %action% Watch Later\"]--": "added to",
|
|
||||||
"removed from --[substring for \"Item %action% Watch Later\"]--": "removed from",
|
|
||||||
"In Watch Later": "In Watch Later",
|
"In Watch Later": "In Watch Later",
|
||||||
|
"In %lastCollectionName%": "In %lastCollectionName%",
|
||||||
|
"Remove from Watch Later": "Remove from Watch Later",
|
||||||
|
"Add to Watch Later": "Add to Watch Later",
|
||||||
|
"Added": "Added",
|
||||||
"Item added to Watch Later": "Item added to Watch Later",
|
"Item added to Watch Later": "Item added to Watch Later",
|
||||||
|
"Item removed from Watch Later": "Item removed from Watch Later",
|
||||||
|
"Item added to %lastCollectionName%": "Item added to %lastCollectionName%",
|
||||||
|
"Item removed from %lastCollectionName%": "Item removed from %lastCollectionName%",
|
||||||
"Your publish is being confirmed and will be live soon": "Your publish is being confirmed and will be live soon",
|
"Your publish is being confirmed and will be live soon": "Your publish is being confirmed and will be live soon",
|
||||||
"Clear Edits": "Clear Edits",
|
"Clear Edits": "Clear Edits",
|
||||||
"Something not quite right..": "Something not quite right..",
|
"Something not quite right..": "Something not quite right..",
|
||||||
"See All": "See All",
|
"See All": "See All",
|
||||||
"Supporting content requires %lbc%": "Supporting content requires %lbc%",
|
"Supporting content requires %lbc%": "Supporting content requires %lbc%",
|
||||||
"With %lbc%, you can send tips to your favorite creators, or help boost their content for more people to see.": "With %lbc%, you can send tips to your favorite creators, or help boost their content for more people to see.",
|
"With %lbc%, you can send tips to your favorite creators, or help boost their content for more people to see.": "With %lbc%, you can send tips to your favorite creators, or help boost their content for more people to see.",
|
||||||
"This refundable boost will improve the discoverability of this content while active.": "This refundable boost will improve the discoverability of this content while active.",
|
"Show this channel your appreciation by sending a donation in USD.": "Show this channel your appreciation by sending a donation in USD.",
|
||||||
"Show this channel your appreciation by sending a donation of cash in USD.": "Show this channel your appreciation by sending a donation of cash in USD.",
|
"This refundable boost will improve the discoverability of this %claimTypeText% while active.": "This refundable boost will improve the discoverability of this %claimTypeText% while active.",
|
||||||
"Show this channel your appreciation by sending a donation of Credits.": "Show this channel your appreciation by sending a donation of Credits.",
|
"Show this channel your appreciation by sending a donation of Credits.": "Show this channel your appreciation by sending a donation of Credits.",
|
||||||
"Add card to tip creators in USD": "Add card to tip creators in USD",
|
"Add card to tip creators in USD": "Add card to tip creators in USD",
|
||||||
"Connect a bank account": "Connect a bank account",
|
"Connect a bank account": "Connect a bank account",
|
||||||
|
@ -2055,5 +2062,7 @@
|
||||||
"Skip Navigation": "Skip Navigation",
|
"Skip Navigation": "Skip Navigation",
|
||||||
"In Favorites": "In Favorites",
|
"In Favorites": "In Favorites",
|
||||||
"by %channelTitle%": "by %channelTitle%",
|
"by %channelTitle%": "by %channelTitle%",
|
||||||
|
"Reset": "Reset",
|
||||||
|
"Reset to original (previous) publish date": "Reset to original (previous) publish date",
|
||||||
"--end--": "--end--"
|
"--end--": "--end--"
|
||||||
}
|
}
|
||||||
|
|
BIN
static/img/placeholderTx.gif
Normal file
BIN
static/img/placeholderTx.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.2 KiB |
|
@ -7,12 +7,12 @@
|
||||||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||||
<link rel="icon" type="image/png" href="/public/favicon.png" />
|
<link rel="icon" type="image/png" href="/public/favicon.png" />
|
||||||
|
|
||||||
<link rel="preload" href="/public/font/v1/300.woff" as="font" type="font/woff" />
|
<link rel="preload" href="/public/font/v1/300.woff" as="font" type="font/woff" crossorigin />
|
||||||
<link rel="preload" href="/public/font/v1/300i.woff" as="font" type="font/woff" />
|
<link rel="preload" href="/public/font/v1/300i.woff" as="font" type="font/woff" crossorigin />
|
||||||
<link rel="preload" href="/public/font/v1/400.woff" as="font" type="font/woff" />
|
<link rel="preload" href="/public/font/v1/400.woff" as="font" type="font/woff" crossorigin />
|
||||||
<link rel="preload" href="/public/font/v1/400i.woff" as="font" type="font/woff" />
|
<link rel="preload" href="/public/font/v1/400i.woff" as="font" type="font/woff" crossorigin />
|
||||||
<link rel="preload" href="/public/font/v1/700.woff" as="font" type="font/woff" />
|
<link rel="preload" href="/public/font/v1/700.woff" as="font" type="font/woff" crossorigin />
|
||||||
<link rel="preload" href="/public/font/v1/700i.woff" as="font" type="font/woff" />
|
<link rel="preload" href="/public/font/v1/700i.woff" as="font" type="font/woff" crossorigin />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@font-face {
|
@font-face {
|
||||||
|
|
|
@ -18,6 +18,10 @@ const Comments = {
|
||||||
comment_abandon: (params: CommentAbandonParams) => fetchCommentsApi('comment.Abandon', params),
|
comment_abandon: (params: CommentAbandonParams) => fetchCommentsApi('comment.Abandon', params),
|
||||||
comment_create: (params: CommentCreateParams) => fetchCommentsApi('comment.Create', params),
|
comment_create: (params: CommentCreateParams) => fetchCommentsApi('comment.Create', params),
|
||||||
comment_by_id: (params: CommentByIdParams) => fetchCommentsApi('comment.ByID', params),
|
comment_by_id: (params: CommentByIdParams) => fetchCommentsApi('comment.ByID', params),
|
||||||
|
comment_pin: (params: CommentPinParams) => fetchCommentsApi('comment.Pin', params),
|
||||||
|
comment_edit: (params: CommentEditParams) => fetchCommentsApi('comment.Edit', params),
|
||||||
|
reaction_list: (params: ReactionListParams) => fetchCommentsApi('reaction.List', params),
|
||||||
|
reaction_react: (params: ReactionReactParams) => fetchCommentsApi('reaction.React', params),
|
||||||
setting_list: (params: SettingsParams) => fetchCommentsApi('setting.List', params),
|
setting_list: (params: SettingsParams) => fetchCommentsApi('setting.List', params),
|
||||||
setting_block_word: (params: BlockWordParams) => fetchCommentsApi('setting.BlockWord', params),
|
setting_block_word: (params: BlockWordParams) => fetchCommentsApi('setting.BlockWord', params),
|
||||||
setting_unblock_word: (params: BlockWordParams) => fetchCommentsApi('setting.UnBlockWord', params),
|
setting_unblock_word: (params: BlockWordParams) => fetchCommentsApi('setting.UnBlockWord', params),
|
||||||
|
|
|
@ -16,6 +16,7 @@ import usePrevious from 'effects/use-previous';
|
||||||
import REWARDS from 'rewards';
|
import REWARDS from 'rewards';
|
||||||
import usePersistedState from 'effects/use-persisted-state';
|
import usePersistedState from 'effects/use-persisted-state';
|
||||||
import Spinner from 'component/spinner';
|
import Spinner from 'component/spinner';
|
||||||
|
import LANGUAGES from 'constants/languages';
|
||||||
// @if TARGET='app'
|
// @if TARGET='app'
|
||||||
import useZoom from 'effects/use-zoom';
|
import useZoom from 'effects/use-zoom';
|
||||||
import useHistoryNav from 'effects/use-history-nav';
|
import useHistoryNav from 'effects/use-history-nav';
|
||||||
|
@ -176,6 +177,7 @@ function App(props: Props) {
|
||||||
const shouldMigrateLanguage = LANGUAGE_MIGRATIONS[language];
|
const shouldMigrateLanguage = LANGUAGE_MIGRATIONS[language];
|
||||||
const hasActiveChannelClaim = activeChannelClaim !== undefined;
|
const hasActiveChannelClaim = activeChannelClaim !== undefined;
|
||||||
const isPersonalized = !IS_WEB || hasVerifiedEmail;
|
const isPersonalized = !IS_WEB || hasVerifiedEmail;
|
||||||
|
const renderFiledrop = !IS_WEB || isAuthenticated;
|
||||||
|
|
||||||
let uri;
|
let uri;
|
||||||
try {
|
try {
|
||||||
|
@ -291,6 +293,10 @@ function App(props: Props) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!languages.includes(language)) {
|
if (!languages.includes(language)) {
|
||||||
setLanguage(language);
|
setLanguage(language);
|
||||||
|
|
||||||
|
if (document && document.documentElement && LANGUAGES[language].length >= 3) {
|
||||||
|
document.documentElement.dir = LANGUAGES[language][2];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [language, languages]);
|
}, [language, languages]);
|
||||||
|
@ -433,7 +439,7 @@ function App(props: Props) {
|
||||||
<Router />
|
<Router />
|
||||||
<React.Suspense fallback={null}>
|
<React.Suspense fallback={null}>
|
||||||
<ModalRouter />
|
<ModalRouter />
|
||||||
<FileDrop />
|
{renderFiledrop && <FileDrop />}
|
||||||
</React.Suspense>
|
</React.Suspense>
|
||||||
<FileRenderFloating />
|
<FileRenderFloating />
|
||||||
<React.Suspense fallback={null}>
|
<React.Suspense fallback={null}>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { SHOW_ADS, ENABLE_NO_SOURCE_CLAIMS } from 'config';
|
import { SHOW_ADS, ENABLE_NO_SOURCE_CLAIMS, SIMPLE_SITE } from 'config';
|
||||||
import * as CS from 'constants/claim_search';
|
import * as CS from 'constants/claim_search';
|
||||||
import * as ICONS from 'constants/icons';
|
import * as ICONS from 'constants/icons';
|
||||||
import React, { Fragment } from 'react';
|
import React, { Fragment } from 'react';
|
||||||
|
@ -144,7 +144,8 @@ function ChannelContent(props: Props) {
|
||||||
hideAdvancedFilter={!showFilters}
|
hideAdvancedFilter={!showFilters}
|
||||||
tileLayout={tileLayout}
|
tileLayout={tileLayout}
|
||||||
uris={searchResults}
|
uris={searchResults}
|
||||||
channelIds={[claim.claim_id]}
|
streamType={SIMPLE_SITE ? CS.CONTENT_ALL : undefined}
|
||||||
|
channelIds={[claimId]}
|
||||||
claimType={claimType}
|
claimType={claimType}
|
||||||
feeAmount={CS.FEE_AMOUNT_ANY}
|
feeAmount={CS.FEE_AMOUNT_ANY}
|
||||||
defaultOrderBy={CS.ORDER_BY_NEW}
|
defaultOrderBy={CS.ORDER_BY_NEW}
|
||||||
|
|
|
@ -5,12 +5,7 @@ import classnames from 'classnames';
|
||||||
import Gerbil from './gerbil.png';
|
import Gerbil from './gerbil.png';
|
||||||
import FreezeframeWrapper from 'component/fileThumbnail/FreezeframeWrapper';
|
import FreezeframeWrapper from 'component/fileThumbnail/FreezeframeWrapper';
|
||||||
import ChannelStakedIndicator from 'component/channelStakedIndicator';
|
import ChannelStakedIndicator from 'component/channelStakedIndicator';
|
||||||
import { getThumbnailCdnUrl } from 'util/thumbnail';
|
import OptimizedImage from 'component/optimizedImage';
|
||||||
|
|
||||||
const FONT_PX = 16.0;
|
|
||||||
const IMG_XSMALL_REM = 2.1;
|
|
||||||
const IMG_SMALL_REM = 3.0;
|
|
||||||
const IMG_NORMAL_REM = 10.0;
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
thumbnail: ?string,
|
thumbnail: ?string,
|
||||||
|
@ -53,8 +48,6 @@ function ChannelThumbnail(props: Props) {
|
||||||
const channelThumbnail = thumbnail || thumbnailPreview;
|
const channelThumbnail = thumbnail || thumbnailPreview;
|
||||||
const isGif = channelThumbnail && channelThumbnail.endsWith('gif');
|
const isGif = channelThumbnail && channelThumbnail.endsWith('gif');
|
||||||
const showThumb = (!obscure && !!thumbnail) || thumbnailPreview;
|
const showThumb = (!obscure && !!thumbnail) || thumbnailPreview;
|
||||||
const thumbnailRef = React.useRef(null);
|
|
||||||
const thumbnailSize = calcRenderedImgWidth(); // currently always 1:1
|
|
||||||
|
|
||||||
// Generate a random color class based on the first letter of the channel name
|
// Generate a random color class based on the first letter of the channel name
|
||||||
const { channelName } = parseURI(uri);
|
const { channelName } = parseURI(uri);
|
||||||
|
@ -67,20 +60,6 @@ function ChannelThumbnail(props: Props) {
|
||||||
colorClassName = `channel-thumbnail__default--4`;
|
colorClassName = `channel-thumbnail__default--4`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function calcRenderedImgWidth() {
|
|
||||||
let rem;
|
|
||||||
if (xsmall) {
|
|
||||||
rem = IMG_XSMALL_REM;
|
|
||||||
} else if (small) {
|
|
||||||
rem = IMG_SMALL_REM;
|
|
||||||
} else {
|
|
||||||
rem = IMG_NORMAL_REM;
|
|
||||||
}
|
|
||||||
|
|
||||||
const devicePixelRatio = window.devicePixelRatio || 1.0;
|
|
||||||
return Math.ceil(rem * devicePixelRatio * FONT_PX);
|
|
||||||
}
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (shouldResolve && uri) {
|
if (shouldResolve && uri) {
|
||||||
doResolveUri(uri);
|
doResolveUri(uri);
|
||||||
|
@ -94,15 +73,6 @@ function ChannelThumbnail(props: Props) {
|
||||||
</FreezeframeWrapper>
|
</FreezeframeWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let url = channelThumbnail;
|
|
||||||
// @if TARGET='web'
|
|
||||||
// Pass image urls through a compression proxy, except for GIFs.
|
|
||||||
if (thumbnail && !(isGif && allowGifs)) {
|
|
||||||
url = getThumbnailCdnUrl({ thumbnail, width: thumbnailSize, height: thumbnailSize, quality: 85 });
|
|
||||||
}
|
|
||||||
// @endif
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classnames('channel-thumbnail', className, {
|
className={classnames('channel-thumbnail', className, {
|
||||||
|
@ -113,13 +83,10 @@ function ChannelThumbnail(props: Props) {
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{!showThumb && (
|
{!showThumb && (
|
||||||
<img
|
<OptimizedImage
|
||||||
ref={thumbnailRef}
|
|
||||||
alt={__('Channel profile picture')}
|
alt={__('Channel profile picture')}
|
||||||
className="channel-thumbnail__default"
|
className="channel-thumbnail__default"
|
||||||
src={!thumbError && url ? url : Gerbil}
|
src={!thumbError && channelThumbnail ? channelThumbnail : Gerbil}
|
||||||
width={thumbnailSize}
|
|
||||||
height={thumbnailSize}
|
|
||||||
loading={noLazyLoad ? undefined : 'lazy'}
|
loading={noLazyLoad ? undefined : 'lazy'}
|
||||||
onError={() => setThumbError(true)} // if thumb fails (including due to https replace, show gerbil.
|
onError={() => setThumbError(true)} // if thumb fails (including due to https replace, show gerbil.
|
||||||
/>
|
/>
|
||||||
|
@ -129,13 +96,10 @@ function ChannelThumbnail(props: Props) {
|
||||||
{showDelayedMessage && thumbError ? (
|
{showDelayedMessage && thumbError ? (
|
||||||
<div className="chanel-thumbnail--waiting">{__('This will be visible in a few minutes.')}</div>
|
<div className="chanel-thumbnail--waiting">{__('This will be visible in a few minutes.')}</div>
|
||||||
) : (
|
) : (
|
||||||
<img
|
<OptimizedImage
|
||||||
ref={thumbnailRef}
|
|
||||||
alt={__('Channel profile picture')}
|
alt={__('Channel profile picture')}
|
||||||
className="channel-thumbnail__custom"
|
className="channel-thumbnail__custom"
|
||||||
src={!thumbError && url ? url : Gerbil}
|
src={!thumbError && channelThumbnail ? channelThumbnail : Gerbil}
|
||||||
width={thumbnailSize}
|
|
||||||
height={thumbnailSize}
|
|
||||||
loading={noLazyLoad ? undefined : 'lazy'}
|
loading={noLazyLoad ? undefined : 'lazy'}
|
||||||
onError={() => setThumbError(true)} // if thumb fails (including due to https replace, show gerbil.
|
onError={() => setThumbError(true)} // if thumb fails (including due to https replace, show gerbil.
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -21,6 +21,7 @@ type Props = {
|
||||||
headerAltControls: Node,
|
headerAltControls: Node,
|
||||||
loading: boolean,
|
loading: boolean,
|
||||||
type: string,
|
type: string,
|
||||||
|
activeUri?: string,
|
||||||
empty?: string,
|
empty?: string,
|
||||||
defaultSort?: boolean,
|
defaultSort?: boolean,
|
||||||
onScrollBottom?: (any) => void,
|
onScrollBottom?: (any) => void,
|
||||||
|
@ -50,6 +51,7 @@ type Props = {
|
||||||
|
|
||||||
export default function ClaimList(props: Props) {
|
export default function ClaimList(props: Props) {
|
||||||
const {
|
const {
|
||||||
|
activeUri,
|
||||||
uris,
|
uris,
|
||||||
headerAltControls,
|
headerAltControls,
|
||||||
loading,
|
loading,
|
||||||
|
@ -190,6 +192,7 @@ export default function ClaimList(props: Props) {
|
||||||
<ClaimPreview
|
<ClaimPreview
|
||||||
uri={uri}
|
uri={uri}
|
||||||
type={type}
|
type={type}
|
||||||
|
active={activeUri && uri === activeUri}
|
||||||
hideMenu={hideMenu}
|
hideMenu={hideMenu}
|
||||||
includeSupportAction={includeSupportAction}
|
includeSupportAction={includeSupportAction}
|
||||||
showUnresolvedClaim={showUnresolvedClaims}
|
showUnresolvedClaim={showUnresolvedClaims}
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { ENABLE_NO_SOURCE_CLAIMS } from 'config';
|
import { ENABLE_NO_SOURCE_CLAIMS, SIMPLE_SITE } from 'config';
|
||||||
import type { Node } from 'react';
|
import type { Node } from 'react';
|
||||||
import * as CS from 'constants/claim_search';
|
import * as CS from 'constants/claim_search';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import usePersistedState from 'effects/use-persisted-state';
|
import usePersistedState from 'effects/use-persisted-state';
|
||||||
import { withRouter } from 'react-router';
|
import { withRouter } from 'react-router';
|
||||||
import { createNormalizedClaimSearchKey, MATURE_TAGS } from 'lbry-redux';
|
import { createNormalizedClaimSearchKey, MATURE_TAGS, splitBySeparator } from 'lbry-redux';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import ClaimList from 'component/claimList';
|
import ClaimList from 'component/claimList';
|
||||||
|
@ -72,6 +72,8 @@ type Props = {
|
||||||
liveLivestreamsFirst?: boolean,
|
liveLivestreamsFirst?: boolean,
|
||||||
livestreamMap?: { [string]: any },
|
livestreamMap?: { [string]: any },
|
||||||
hasSource?: boolean,
|
hasSource?: boolean,
|
||||||
|
limitClaimsPerChannel?: number,
|
||||||
|
releaseTime?: string,
|
||||||
showNoSourceClaims?: boolean,
|
showNoSourceClaims?: boolean,
|
||||||
isChannel?: boolean,
|
isChannel?: boolean,
|
||||||
empty?: string,
|
empty?: string,
|
||||||
|
@ -104,8 +106,8 @@ function ClaimListDiscover(props: Props) {
|
||||||
claimType,
|
claimType,
|
||||||
pageSize,
|
pageSize,
|
||||||
defaultClaimType,
|
defaultClaimType,
|
||||||
streamType,
|
streamType = SIMPLE_SITE ? CS.FILE_VIDEO : undefined,
|
||||||
defaultStreamType,
|
defaultStreamType = SIMPLE_SITE ? CS.FILE_VIDEO : undefined, // add param for DEFAULT_STREAM_TYPE
|
||||||
freshness,
|
freshness,
|
||||||
defaultFreshness = CS.FRESH_WEEK,
|
defaultFreshness = CS.FRESH_WEEK,
|
||||||
renderProperties,
|
renderProperties,
|
||||||
|
@ -124,6 +126,8 @@ function ClaimListDiscover(props: Props) {
|
||||||
forceShowReposts = false,
|
forceShowReposts = false,
|
||||||
languageSetting,
|
languageSetting,
|
||||||
searchInLanguage,
|
searchInLanguage,
|
||||||
|
limitClaimsPerChannel,
|
||||||
|
releaseTime,
|
||||||
scrollAnchor,
|
scrollAnchor,
|
||||||
showHiddenByUser = false,
|
showHiddenByUser = false,
|
||||||
liveLivestreamsFirst,
|
liveLivestreamsFirst,
|
||||||
|
@ -147,7 +151,9 @@ function ClaimListDiscover(props: Props) {
|
||||||
(urlParams.get(CS.TAGS_KEY) !== null && urlParams.get(CS.TAGS_KEY)) ||
|
(urlParams.get(CS.TAGS_KEY) !== null && urlParams.get(CS.TAGS_KEY)) ||
|
||||||
(defaultTags && getParamFromTags(defaultTags));
|
(defaultTags && getParamFromTags(defaultTags));
|
||||||
const freshnessParam = freshness || urlParams.get(CS.FRESH_KEY) || defaultFreshness;
|
const freshnessParam = freshness || urlParams.get(CS.FRESH_KEY) || defaultFreshness;
|
||||||
const mutedAndBlockedChannelIds = Array.from(new Set(mutedUris.concat(blockedUris).map((uri) => uri.split('#')[1])));
|
const mutedAndBlockedChannelIds = Array.from(
|
||||||
|
new Set(mutedUris.concat(blockedUris).map((uri) => splitBySeparator(uri)[1]))
|
||||||
|
);
|
||||||
|
|
||||||
const langParam = urlParams.get(CS.LANGUAGE_KEY) || null;
|
const langParam = urlParams.get(CS.LANGUAGE_KEY) || null;
|
||||||
const languageParams = searchInLanguage
|
const languageParams = searchInLanguage
|
||||||
|
@ -170,12 +176,12 @@ function ClaimListDiscover(props: Props) {
|
||||||
const durationParam = urlParams.get(CS.DURATION_KEY) || null;
|
const durationParam = urlParams.get(CS.DURATION_KEY) || null;
|
||||||
const channelIdsInUrl = urlParams.get(CS.CHANNEL_IDS_KEY);
|
const channelIdsInUrl = urlParams.get(CS.CHANNEL_IDS_KEY);
|
||||||
const channelIdsParam = channelIdsInUrl ? channelIdsInUrl.split(',') : channelIds;
|
const channelIdsParam = channelIdsInUrl ? channelIdsInUrl.split(',') : channelIds;
|
||||||
const feeAmountParam = urlParams.get('fee_amount') || feeAmount;
|
const feeAmountParam = urlParams.get('fee_amount') || feeAmount || SIMPLE_SITE ? CS.FEE_AMOUNT_ONLY_FREE : undefined;
|
||||||
const originalPageSize = pageSize || CS.PAGE_SIZE;
|
const originalPageSize = pageSize || CS.PAGE_SIZE;
|
||||||
const dynamicPageSize = isLargeScreen ? Math.ceil(originalPageSize * (3 / 2)) : originalPageSize;
|
const dynamicPageSize = isLargeScreen ? Math.ceil(originalPageSize * (3 / 2)) : originalPageSize;
|
||||||
const historyAction = history.action;
|
const historyAction = history.action;
|
||||||
|
|
||||||
let orderParam = orderBy || urlParams.get(CS.ORDER_BY_KEY) || defaultOrderBy;
|
let orderParam = orderBy || urlParams.get(CS.ORDER_BY_KEY) || defaultOrderBy || orderParamEntry;
|
||||||
|
|
||||||
if (!orderParam) {
|
if (!orderParam) {
|
||||||
if (historyAction === 'POP') {
|
if (historyAction === 'POP') {
|
||||||
|
@ -219,6 +225,7 @@ function ClaimListDiscover(props: Props) {
|
||||||
fee_amount?: string,
|
fee_amount?: string,
|
||||||
has_source?: boolean,
|
has_source?: boolean,
|
||||||
has_no_source?: boolean,
|
has_no_source?: boolean,
|
||||||
|
limit_claims_per_channel?: number,
|
||||||
} = {
|
} = {
|
||||||
page_size: dynamicPageSize,
|
page_size: dynamicPageSize,
|
||||||
page,
|
page,
|
||||||
|
@ -241,6 +248,10 @@ function ClaimListDiscover(props: Props) {
|
||||||
options.has_source = true;
|
options.has_source = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (limitClaimsPerChannel) {
|
||||||
|
options.limit_claims_per_channel = limitClaimsPerChannel;
|
||||||
|
}
|
||||||
|
|
||||||
if (feeAmountParam && claimType !== CS.CLAIM_CHANNEL) {
|
if (feeAmountParam && claimType !== CS.CLAIM_CHANNEL) {
|
||||||
options.fee_amount = feeAmountParam;
|
options.fee_amount = feeAmountParam;
|
||||||
}
|
}
|
||||||
|
@ -269,8 +280,10 @@ function ClaimListDiscover(props: Props) {
|
||||||
// SDK chokes on reposted_claim_id of null or false, needs to not be present if no value
|
// SDK chokes on reposted_claim_id of null or false, needs to not be present if no value
|
||||||
options.reposted_claim_id = repostedClaimId;
|
options.reposted_claim_id = repostedClaimId;
|
||||||
}
|
}
|
||||||
|
// IF release time, set it, else set fallback release times using the hack below.
|
||||||
if (claimType !== CS.CLAIM_CHANNEL) {
|
if (releaseTime) {
|
||||||
|
options.release_time = releaseTime;
|
||||||
|
} else if (claimType !== CS.CLAIM_CHANNEL) {
|
||||||
if (orderParam === CS.ORDER_BY_TOP && freshnessParam !== CS.FRESH_ALL) {
|
if (orderParam === CS.ORDER_BY_TOP && freshnessParam !== CS.FRESH_ALL) {
|
||||||
options.release_time = `>${Math.floor(moment().subtract(1, freshnessParam).startOf('hour').unix())}`;
|
options.release_time = `>${Math.floor(moment().subtract(1, freshnessParam).startOf('hour').unix())}`;
|
||||||
} else if (orderParam === CS.ORDER_BY_NEW || orderParam === CS.ORDER_BY_TRENDING) {
|
} else if (orderParam === CS.ORDER_BY_NEW || orderParam === CS.ORDER_BY_TRENDING) {
|
||||||
|
@ -348,9 +361,25 @@ function ClaimListDiscover(props: Props) {
|
||||||
|
|
||||||
const hasMatureTags = tagsParam && tagsParam.split(',').some((t) => MATURE_TAGS.includes(t));
|
const hasMatureTags = tagsParam && tagsParam.split(',').some((t) => MATURE_TAGS.includes(t));
|
||||||
const claimSearchCacheQuery = createNormalizedClaimSearchKey(options);
|
const claimSearchCacheQuery = createNormalizedClaimSearchKey(options);
|
||||||
const claimSearchResult = claimSearchByQuery[claimSearchCacheQuery];
|
let claimSearchResult = claimSearchByQuery[claimSearchCacheQuery];
|
||||||
const claimSearchResultLastPageReached = claimSearchByQueryLastPageReached[claimSearchCacheQuery];
|
const claimSearchResultLastPageReached = claimSearchByQueryLastPageReached[claimSearchCacheQuery];
|
||||||
|
|
||||||
|
// uncomment to fix an item on a page
|
||||||
|
// const fixUri = 'lbry://@corbettreport#0/lbryodysee#5';
|
||||||
|
// if (
|
||||||
|
// orderParam === CS.ORDER_BY_NEW &&
|
||||||
|
// claimSearchResult &&
|
||||||
|
// claimSearchResult.length > 2 &&
|
||||||
|
// window.location.pathname === '/$/rabbithole'
|
||||||
|
// ) {
|
||||||
|
// if (claimSearchResult.indexOf(fixUri) !== -1) {
|
||||||
|
// claimSearchResult.splice(claimSearchResult.indexOf(fixUri), 1);
|
||||||
|
// } else {
|
||||||
|
// claimSearchResult.pop();
|
||||||
|
// }
|
||||||
|
// claimSearchResult.splice(2, 0, fixUri);
|
||||||
|
// }
|
||||||
|
|
||||||
const [prevOptions, setPrevOptions] = React.useState(null);
|
const [prevOptions, setPrevOptions] = React.useState(null);
|
||||||
|
|
||||||
if (!isJustScrollingToNewPage(prevOptions, options)) {
|
if (!isJustScrollingToNewPage(prevOptions, options)) {
|
||||||
|
@ -474,7 +503,7 @@ function ClaimListDiscover(props: Props) {
|
||||||
claimType={claimType}
|
claimType={claimType}
|
||||||
streamType={streamType}
|
streamType={streamType}
|
||||||
defaultStreamType={defaultStreamType}
|
defaultStreamType={defaultStreamType}
|
||||||
feeAmount={feeAmount}
|
feeAmount={SIMPLE_SITE ? undefined : feeAmount} // ENABLE_PAID_CONTENT_DISCOVER or something
|
||||||
orderBy={orderBy}
|
orderBy={orderBy}
|
||||||
defaultOrderBy={defaultOrderBy}
|
defaultOrderBy={defaultOrderBy}
|
||||||
hideAdvancedFilter={hideAdvancedFilter}
|
hideAdvancedFilter={hideAdvancedFilter}
|
||||||
|
|
|
@ -7,7 +7,7 @@ import React from 'react';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { Menu, MenuButton, MenuList, MenuItem } from '@reach/menu-button';
|
import { Menu, MenuButton, MenuList, MenuItem } from '@reach/menu-button';
|
||||||
import Icon from 'component/common/icon';
|
import Icon from 'component/common/icon';
|
||||||
import { generateShareUrl, generateRssUrl } from 'util/url';
|
import { generateShareUrl, generateRssUrl, generateLbryContentUrl } from 'util/url';
|
||||||
import { useHistory } from 'react-router';
|
import { useHistory } from 'react-router';
|
||||||
import { buildURI, parseURI, COLLECTIONS_CONSTS } from 'lbry-redux';
|
import { buildURI, parseURI, COLLECTIONS_CONSTS } from 'lbry-redux';
|
||||||
|
|
||||||
|
@ -107,8 +107,9 @@ function ClaimMenuList(props: Props) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const shareUrl: string = generateShareUrl(SHARE_DOMAIN, uri);
|
const lbryUrl: string = generateLbryContentUrl(claim.canonical_url, claim.permanent_url);
|
||||||
const rssUrl: string = isChannel ? generateRssUrl(URL, claim) : '';
|
const shareUrl: string = generateShareUrl(SHARE_DOMAIN, lbryUrl);
|
||||||
|
const rssUrl: string = isChannel ? generateRssUrl(SHARE_DOMAIN, claim) : '';
|
||||||
const isCollectionClaim = claim && claim.value_type === 'collection';
|
const isCollectionClaim = claim && claim.value_type === 'collection';
|
||||||
// $FlowFixMe
|
// $FlowFixMe
|
||||||
const isPlayable =
|
const isPlayable =
|
||||||
|
@ -233,11 +234,9 @@ function ClaimMenuList(props: Props) {
|
||||||
className="comment__menu-option"
|
className="comment__menu-option"
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
doToast({
|
doToast({
|
||||||
message: __('Item %action% Watch Later', {
|
message: hasClaimInWatchLater
|
||||||
action: hasClaimInWatchLater
|
? __('Item removed from Watch Later')
|
||||||
? __('removed from --[substring for "Item %action% Watch Later"]--')
|
: __('Item added to Watch Later'),
|
||||||
: __('added to --[substring for "Item %action% Watch Later"]--'),
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
doCollectionEdit(COLLECTIONS_CONSTS.WATCH_LATER_ID, {
|
doCollectionEdit(COLLECTIONS_CONSTS.WATCH_LATER_ID, {
|
||||||
claims: [contentClaim],
|
claims: [contentClaim],
|
||||||
|
@ -258,9 +257,9 @@ function ClaimMenuList(props: Props) {
|
||||||
className="comment__menu-option"
|
className="comment__menu-option"
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
doToast({
|
doToast({
|
||||||
message: __(`Item %action% ${lastCollectionName}`, {
|
message: hasClaimInCustom
|
||||||
action: hasClaimInCustom ? __('removed from') : __('added to'),
|
? __('Item removed from %lastCollectionName%', { lastCollectionName })
|
||||||
}),
|
: __('Item added to %lastCollectionName%', { lastCollectionName }),
|
||||||
});
|
});
|
||||||
doCollectionEdit(COLLECTIONS_CONSTS.FAVORITES_ID, {
|
doCollectionEdit(COLLECTIONS_CONSTS.FAVORITES_ID, {
|
||||||
claims: [contentClaim],
|
claims: [contentClaim],
|
||||||
|
@ -271,7 +270,9 @@ function ClaimMenuList(props: Props) {
|
||||||
>
|
>
|
||||||
<div className="menu__link">
|
<div className="menu__link">
|
||||||
<Icon aria-hidden icon={hasClaimInCustom ? ICONS.DELETE : ICONS.STAR} />
|
<Icon aria-hidden icon={hasClaimInCustom ? ICONS.DELETE : ICONS.STAR} />
|
||||||
{hasClaimInCustom ? __(`In ${lastCollectionName}`) : __(`${lastCollectionName}`)}
|
{hasClaimInCustom
|
||||||
|
? __('In %lastCollectionName%', { lastCollectionName })
|
||||||
|
: __(`${lastCollectionName}`)}
|
||||||
</div>
|
</div>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
|
@ -411,7 +412,7 @@ function ClaimMenuList(props: Props) {
|
||||||
|
|
||||||
<MenuItem className="comment__menu-option" onSelect={handleCopyLink}>
|
<MenuItem className="comment__menu-option" onSelect={handleCopyLink}>
|
||||||
<div className="menu__link">
|
<div className="menu__link">
|
||||||
<Icon aria-hidden icon={ICONS.SHARE} />
|
<Icon aria-hidden icon={ICONS.COPY_LINK} />
|
||||||
{__('Copy Link')}
|
{__('Copy Link')}
|
||||||
</div>
|
</div>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
|
@ -4,7 +4,7 @@ import React, { useEffect, forwardRef } from 'react';
|
||||||
import { NavLink, withRouter } from 'react-router-dom';
|
import { NavLink, withRouter } from 'react-router-dom';
|
||||||
import { lazyImport } from 'util/lazyImport';
|
import { lazyImport } from 'util/lazyImport';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { parseURI, COLLECTIONS_CONSTS } from 'lbry-redux';
|
import { parseURI, COLLECTIONS_CONSTS, isURIEqual } from 'lbry-redux';
|
||||||
import { formatLbryUrlForWeb } from 'util/url';
|
import { formatLbryUrlForWeb } from 'util/url';
|
||||||
import { isEmpty } from 'util/object';
|
import { isEmpty } from 'util/object';
|
||||||
import FileThumbnail from 'component/fileThumbnail';
|
import FileThumbnail from 'component/fileThumbnail';
|
||||||
|
@ -37,6 +37,7 @@ const AbandonedChannelPreview = lazyImport(() =>
|
||||||
type Props = {
|
type Props = {
|
||||||
uri: string,
|
uri: string,
|
||||||
claim: ?Claim,
|
claim: ?Claim,
|
||||||
|
active: boolean,
|
||||||
obscureNsfw: boolean,
|
obscureNsfw: boolean,
|
||||||
showUserBlocked: boolean,
|
showUserBlocked: boolean,
|
||||||
claimIsMine: boolean,
|
claimIsMine: boolean,
|
||||||
|
@ -119,6 +120,7 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||||
pending,
|
pending,
|
||||||
empty,
|
empty,
|
||||||
// modifiers
|
// modifiers
|
||||||
|
active,
|
||||||
customShouldHide,
|
customShouldHide,
|
||||||
showNullPlaceholder,
|
showNullPlaceholder,
|
||||||
// value from show mature content user setting
|
// value from show mature content user setting
|
||||||
|
@ -232,10 +234,10 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||||
}
|
}
|
||||||
// block stream claims
|
// block stream claims
|
||||||
if (claim && !shouldHide && !showUserBlocked && mutedUris.length && signingChannel) {
|
if (claim && !shouldHide && !showUserBlocked && mutedUris.length && signingChannel) {
|
||||||
shouldHide = mutedUris.some((blockedUri) => blockedUri === signingChannel.permanent_url);
|
shouldHide = mutedUris.some((blockedUri) => isURIEqual(blockedUri, signingChannel.permanent_url));
|
||||||
}
|
}
|
||||||
if (claim && !shouldHide && !showUserBlocked && blockedUris.length && signingChannel) {
|
if (claim && !shouldHide && !showUserBlocked && blockedUris.length && signingChannel) {
|
||||||
shouldHide = blockedUris.some((blockedUri) => blockedUri === signingChannel.permanent_url);
|
shouldHide = blockedUris.some((blockedUri) => isURIEqual(blockedUri, signingChannel.permanent_url));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!shouldHide && customShouldHide && claim) {
|
if (!shouldHide && customShouldHide && claim) {
|
||||||
|
@ -316,6 +318,7 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||||
'claim-preview__wrapper--inline': type === 'inline',
|
'claim-preview__wrapper--inline': type === 'inline',
|
||||||
'claim-preview__wrapper--small': type === 'small',
|
'claim-preview__wrapper--small': type === 'small',
|
||||||
'claim-preview__live': live,
|
'claim-preview__live': live,
|
||||||
|
'claim-preview__active': active,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<>
|
<>
|
||||||
|
@ -372,7 +375,7 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
||||||
{pending ? (
|
{pending ? (
|
||||||
<ClaimPreviewTitle uri={uri} />
|
<ClaimPreviewTitle uri={uri} />
|
||||||
) : (
|
) : (
|
||||||
<NavLink aria-label={ariaLabelData} {...navLinkProps}>
|
<NavLink aria-label={ariaLabelData} aria-current={active && 'page'} {...navLinkProps}>
|
||||||
<ClaimPreviewTitle uri={uri} />
|
<ClaimPreviewTitle uri={uri} />
|
||||||
</NavLink>
|
</NavLink>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -10,13 +10,15 @@ import ChannelThumbnail from 'component/channelThumbnail';
|
||||||
import SubscribeButton from 'component/subscribeButton';
|
import SubscribeButton from 'component/subscribeButton';
|
||||||
import useGetThumbnail from 'effects/use-get-thumbnail';
|
import useGetThumbnail from 'effects/use-get-thumbnail';
|
||||||
import { formatLbryUrlForWeb } from 'util/url';
|
import { formatLbryUrlForWeb } from 'util/url';
|
||||||
import { parseURI, COLLECTIONS_CONSTS } from 'lbry-redux';
|
import { parseURI, COLLECTIONS_CONSTS, isURIEqual } from 'lbry-redux';
|
||||||
import PreviewOverlayProperties from 'component/previewOverlayProperties';
|
import PreviewOverlayProperties from 'component/previewOverlayProperties';
|
||||||
import FileDownloadLink from 'component/fileDownloadLink';
|
import FileDownloadLink from 'component/fileDownloadLink';
|
||||||
import FileWatchLaterLink from 'component/fileWatchLaterLink';
|
import FileWatchLaterLink from 'component/fileWatchLaterLink';
|
||||||
import ClaimRepostAuthor from 'component/claimRepostAuthor';
|
import ClaimRepostAuthor from 'component/claimRepostAuthor';
|
||||||
import ClaimMenuList from 'component/claimMenuList';
|
import ClaimMenuList from 'component/claimMenuList';
|
||||||
import CollectionPreviewOverlay from 'component/collectionPreviewOverlay';
|
import CollectionPreviewOverlay from 'component/collectionPreviewOverlay';
|
||||||
|
// $FlowFixMe cannot resolve ...
|
||||||
|
import PlaceholderTx from 'static/img/placeholderTx.gif';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
uri: string,
|
uri: string,
|
||||||
|
@ -39,7 +41,6 @@ type Props = {
|
||||||
}>,
|
}>,
|
||||||
blockedChannelUris: Array<string>,
|
blockedChannelUris: Array<string>,
|
||||||
getFile: (string) => void,
|
getFile: (string) => void,
|
||||||
placeholder: boolean,
|
|
||||||
streamingUrl: string,
|
streamingUrl: string,
|
||||||
isMature: boolean,
|
isMature: boolean,
|
||||||
showMature: boolean,
|
showMature: boolean,
|
||||||
|
@ -175,12 +176,12 @@ function ClaimPreviewTile(props: Props) {
|
||||||
|
|
||||||
// block stream claims
|
// block stream claims
|
||||||
if (claim && !shouldHide && !showHiddenByUser && blockedChannelUris.length && signingChannel) {
|
if (claim && !shouldHide && !showHiddenByUser && blockedChannelUris.length && signingChannel) {
|
||||||
shouldHide = blockedChannelUris.some((blockedUri) => blockedUri === signingChannel.permanent_url);
|
shouldHide = blockedChannelUris.some((blockedUri) => isURIEqual(blockedUri, signingChannel.permanent_url));
|
||||||
}
|
}
|
||||||
// block channel claims if we can't control for them in claim search
|
// block channel claims if we can't control for them in claim search
|
||||||
// e.g. fetchRecommendedSubscriptions
|
// e.g. fetchRecommendedSubscriptions
|
||||||
if (claim && isChannel && !shouldHide && !showHiddenByUser && blockedChannelUris.length) {
|
if (claim && isChannel && !shouldHide && !showHiddenByUser && blockedChannelUris.length && signingChannel) {
|
||||||
shouldHide = blockedChannelUris.some((blockedUri) => blockedUri === claim.permanent_url);
|
shouldHide = blockedChannelUris.some((blockedUri) => isURIEqual(blockedUri, signingChannel.permanent_url));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldHide || (isLivestream && !showNoSourceClaims)) {
|
if (shouldHide || (isLivestream && !showNoSourceClaims)) {
|
||||||
|
@ -190,7 +191,9 @@ function ClaimPreviewTile(props: Props) {
|
||||||
if (placeholder || (!claim && isResolvingUri)) {
|
if (placeholder || (!claim && isResolvingUri)) {
|
||||||
return (
|
return (
|
||||||
<li className={classnames('claim-preview--tile', {})}>
|
<li className={classnames('claim-preview--tile', {})}>
|
||||||
<div className="placeholder media__thumb" />
|
<div className="placeholder media__thumb">
|
||||||
|
<img src={PlaceholderTx} alt="Placeholder" />
|
||||||
|
</div>
|
||||||
<div className="placeholder__wrapper">
|
<div className="placeholder__wrapper">
|
||||||
<div className="placeholder claim-tile__title" />
|
<div className="placeholder claim-tile__title" />
|
||||||
<div className="placeholder claim-tile__info" />
|
<div className="placeholder claim-tile__info" />
|
||||||
|
@ -255,10 +258,8 @@ function ClaimPreviewTile(props: Props) {
|
||||||
)}
|
)}
|
||||||
</h2>
|
</h2>
|
||||||
</NavLink>
|
</NavLink>
|
||||||
{/* CHECK CLAIM MENU LIST PARAMS (IS REPOST?) */}
|
|
||||||
<ClaimMenuList uri={uri} collectionId={listId} channelUri={channelUri} isRepost={isRepost} />
|
<ClaimMenuList uri={uri} collectionId={listId} channelUri={channelUri} isRepost={isRepost} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div className="claim-tile__info">
|
<div className="claim-tile__info">
|
||||||
{isChannel ? (
|
{isChannel ? (
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { ENABLE_NO_SOURCE_CLAIMS, SIMPLE_SITE } from 'config';
|
||||||
import * as CS from 'constants/claim_search';
|
import * as CS from 'constants/claim_search';
|
||||||
import type { Node } from 'react';
|
import type { Node } from 'react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { createNormalizedClaimSearchKey, MATURE_TAGS } from 'lbry-redux';
|
import { createNormalizedClaimSearchKey, MATURE_TAGS, splitBySeparator } from 'lbry-redux';
|
||||||
import ClaimPreviewTile from 'component/claimPreviewTile';
|
import ClaimPreviewTile from 'component/claimPreviewTile';
|
||||||
import { useHistory } from 'react-router';
|
import { useHistory } from 'react-router';
|
||||||
import { getLivestreamOnlyOptions } from 'util/search';
|
import { getLivestreamOnlyOptions } from 'util/search';
|
||||||
|
@ -115,6 +115,7 @@ type Props = {
|
||||||
liveLivestreamsFirst?: boolean,
|
liveLivestreamsFirst?: boolean,
|
||||||
livestreamMap?: { [string]: any },
|
livestreamMap?: { [string]: any },
|
||||||
pin?: boolean,
|
pin?: boolean,
|
||||||
|
pinUrls?: Array<string>,
|
||||||
showNoSourceClaims?: boolean,
|
showNoSourceClaims?: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -146,7 +147,8 @@ function ClaimTilesDiscover(props: Props) {
|
||||||
mutedUris,
|
mutedUris,
|
||||||
liveLivestreamsFirst,
|
liveLivestreamsFirst,
|
||||||
livestreamMap,
|
livestreamMap,
|
||||||
// pin, // let's pin from /web folder
|
pin,
|
||||||
|
pinUrls,
|
||||||
prefixUris,
|
prefixUris,
|
||||||
showNoSourceClaims,
|
showNoSourceClaims,
|
||||||
} = props;
|
} = props;
|
||||||
|
@ -155,7 +157,9 @@ function ClaimTilesDiscover(props: Props) {
|
||||||
const urlParams = new URLSearchParams(location.search);
|
const urlParams = new URLSearchParams(location.search);
|
||||||
const feeAmountInUrl = urlParams.get('fee_amount');
|
const feeAmountInUrl = urlParams.get('fee_amount');
|
||||||
const feeAmountParam = feeAmountInUrl || feeAmount;
|
const feeAmountParam = feeAmountInUrl || feeAmount;
|
||||||
const mutedAndBlockedChannelIds = Array.from(new Set(mutedUris.concat(blockedUris).map((uri) => uri.split('#')[1])));
|
const mutedAndBlockedChannelIds = Array.from(
|
||||||
|
new Set(mutedUris.concat(blockedUris).map((uri) => splitBySeparator(uri)[1]))
|
||||||
|
);
|
||||||
const liveUris = [];
|
const liveUris = [];
|
||||||
|
|
||||||
const [prevUris, setPrevUris] = React.useState([]);
|
const [prevUris, setPrevUris] = React.useState([]);
|
||||||
|
@ -286,10 +290,24 @@ function ClaimTilesDiscover(props: Props) {
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const modifiedUris = uris ? uris.slice() : [];
|
||||||
|
const fixUris = pinUrls || ['lbry://@AlisonMorrow#6/LBRY#8'];
|
||||||
|
|
||||||
|
if (pin && modifiedUris && modifiedUris.length > 2 && window.location.pathname === '/') {
|
||||||
|
fixUris.forEach((fixUri) => {
|
||||||
|
if (modifiedUris.indexOf(fixUri) !== -1) {
|
||||||
|
modifiedUris.splice(modifiedUris.indexOf(fixUri), 1);
|
||||||
|
} else {
|
||||||
|
modifiedUris.pop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
modifiedUris.splice(2, 0, ...fixUris);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ul className="claim-grid">
|
<ul className="claim-grid">
|
||||||
{uris && uris.length
|
{modifiedUris && modifiedUris.length
|
||||||
? uris.map((uri, index) => (
|
? modifiedUris.map((uri, index) => (
|
||||||
<ClaimPreviewTile key={uri} uri={uri} properties={renderProperties} live={resolveLive(index)} />
|
<ClaimPreviewTile key={uri} uri={uri} properties={renderProperties} live={resolveLive(index)} />
|
||||||
))
|
))
|
||||||
: new Array(pageSize)
|
: new Array(pageSize)
|
||||||
|
|
|
@ -4,19 +4,19 @@ import {
|
||||||
makeSelectUrlsForCollectionId,
|
makeSelectUrlsForCollectionId,
|
||||||
makeSelectNameForCollectionId,
|
makeSelectNameForCollectionId,
|
||||||
makeSelectCollectionForId,
|
makeSelectCollectionForId,
|
||||||
makeSelectClaimForClaimId,
|
makeSelectClaimForUri,
|
||||||
makeSelectClaimIsMine,
|
makeSelectClaimIsMine,
|
||||||
} from 'lbry-redux';
|
} from 'lbry-redux';
|
||||||
|
|
||||||
const select = (state, props) => {
|
const select = (state, props) => {
|
||||||
const claim = makeSelectClaimForClaimId(props.id)(state);
|
const claim = makeSelectClaimForUri(props.uri)(state);
|
||||||
const url = claim && claim.permanent_url;
|
const url = claim && claim.permanent_url;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
url,
|
||||||
collection: makeSelectCollectionForId(props.id)(state),
|
collection: makeSelectCollectionForId(props.id)(state),
|
||||||
collectionUrls: makeSelectUrlsForCollectionId(props.id)(state),
|
collectionUrls: makeSelectUrlsForCollectionId(props.id)(state),
|
||||||
collectionName: makeSelectNameForCollectionId(props.id)(state),
|
collectionName: makeSelectNameForCollectionId(props.id)(state),
|
||||||
claim,
|
|
||||||
isMine: makeSelectClaimIsMine(url)(state),
|
isMine: makeSelectClaimIsMine(url)(state),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -9,18 +9,17 @@ import * as ICONS from 'constants/icons';
|
||||||
import { COLLECTIONS_CONSTS } from 'lbry-redux';
|
import { COLLECTIONS_CONSTS } from 'lbry-redux';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
id: string,
|
||||||
|
url: string,
|
||||||
|
isMine: boolean,
|
||||||
collectionUrls: Array<Claim>,
|
collectionUrls: Array<Claim>,
|
||||||
collectionName: string,
|
collectionName: string,
|
||||||
collection: any,
|
collection: any,
|
||||||
createUnpublishedCollection: (string, Array<any>, ?string) => void,
|
createUnpublishedCollection: (string, Array<any>, ?string) => void,
|
||||||
id: string,
|
|
||||||
claim: Claim,
|
|
||||||
isMine: boolean,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function CollectionContent(props: Props) {
|
export default function CollectionContent(props: Props) {
|
||||||
const { collectionUrls, collectionName, id } = props;
|
const { collectionUrls, collectionName, id, url } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
isBodyList
|
isBodyList
|
||||||
|
@ -35,12 +34,21 @@ export default function CollectionContent(props: Props) {
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
titleActions={
|
titleActions={
|
||||||
<>
|
<div className="card__title-actions--link">
|
||||||
{/* TODO: BUTTON TO SAVE COLLECTION - Probably save/copy modal */}
|
{/* TODO: BUTTON TO SAVE COLLECTION - Probably save/copy modal */}
|
||||||
<Button label={'View List'} button="link" navigate={`/$/${PAGES.LIST}/${id}`} />
|
<Button label={'View List'} button="link" navigate={`/$/${PAGES.LIST}/${id}`} />
|
||||||
</>
|
</div>
|
||||||
|
}
|
||||||
|
body={
|
||||||
|
<ClaimList
|
||||||
|
isCardBody
|
||||||
|
type="small"
|
||||||
|
activeUri={url}
|
||||||
|
uris={collectionUrls}
|
||||||
|
collectionId={id}
|
||||||
|
empty={__('List is Empty')}
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
body={<ClaimList isCardBody type="small" uris={collectionUrls} collectionId={id} empty={__('List is Empty')} />}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,12 +11,13 @@ import { doToast } from 'redux/actions/notifications';
|
||||||
import { doSetPlayingUri } from 'redux/actions/content';
|
import { doSetPlayingUri } from 'redux/actions/content';
|
||||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||||
import { selectLinkedCommentAncestors, makeSelectOthersReactionsForComment } from 'redux/selectors/comments';
|
import { selectLinkedCommentAncestors, makeSelectOthersReactionsForComment } from 'redux/selectors/comments';
|
||||||
import { selectActiveChannelId, selectActiveChannelClaim } from 'redux/selectors/app';
|
import { selectActiveChannelClaim } from 'redux/selectors/app';
|
||||||
import { selectPlayingUri } from 'redux/selectors/content';
|
import { selectPlayingUri } from 'redux/selectors/content';
|
||||||
import Comment from './view';
|
import Comment from './view';
|
||||||
|
|
||||||
const select = (state, props) => {
|
const select = (state, props) => {
|
||||||
const activeChannelId = selectActiveChannelId(state);
|
const activeChannelClaim = selectActiveChannelClaim(state);
|
||||||
|
const activeChannelId = activeChannelClaim && activeChannelClaim.claim_id;
|
||||||
const reactionKey = activeChannelId ? `${props.commentId}:${activeChannelId}` : props.commentId;
|
const reactionKey = activeChannelId ? `${props.commentId}:${activeChannelId}` : props.commentId;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -25,7 +26,7 @@ const select = (state, props) => {
|
||||||
channelIsBlocked: props.authorUri && makeSelectChannelIsMuted(props.authorUri)(state),
|
channelIsBlocked: props.authorUri && makeSelectChannelIsMuted(props.authorUri)(state),
|
||||||
commentingEnabled: IS_WEB ? Boolean(selectUserVerifiedEmail(state)) : true,
|
commentingEnabled: IS_WEB ? Boolean(selectUserVerifiedEmail(state)) : true,
|
||||||
othersReacts: makeSelectOthersReactionsForComment(reactionKey)(state),
|
othersReacts: makeSelectOthersReactionsForComment(reactionKey)(state),
|
||||||
activeChannelClaim: selectActiveChannelClaim(state),
|
activeChannelClaim,
|
||||||
myChannels: selectMyChannelClaims(state),
|
myChannels: selectMyChannelClaims(state),
|
||||||
playingUri: selectPlayingUri(state),
|
playingUri: selectPlayingUri(state),
|
||||||
stakedLevel: makeSelectStakedLevelForChannelUri(props.authorUri)(state),
|
stakedLevel: makeSelectStakedLevelForChannelUri(props.authorUri)(state),
|
||||||
|
|
|
@ -59,6 +59,7 @@ type Props = {
|
||||||
stakedLevel: number,
|
stakedLevel: number,
|
||||||
supportAmount: number,
|
supportAmount: number,
|
||||||
numDirectReplies: number,
|
numDirectReplies: number,
|
||||||
|
isFiat: boolean
|
||||||
};
|
};
|
||||||
|
|
||||||
const LENGTH_TO_COLLAPSE = 300;
|
const LENGTH_TO_COLLAPSE = 300;
|
||||||
|
@ -91,6 +92,7 @@ function Comment(props: Props) {
|
||||||
stakedLevel,
|
stakedLevel,
|
||||||
supportAmount,
|
supportAmount,
|
||||||
numDirectReplies,
|
numDirectReplies,
|
||||||
|
isFiat,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
@ -240,7 +242,7 @@ function Comment(props: Props) {
|
||||||
label={<DateTime date={timePosted} timeAgo />}
|
label={<DateTime date={timePosted} timeAgo />}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{supportAmount > 0 && <CreditAmount amount={supportAmount} superChatLight size={12} />}
|
{supportAmount > 0 && <CreditAmount isFiat={isFiat} amount={supportAmount} superChatLight size={12} />}
|
||||||
|
|
||||||
{isPinned && (
|
{isPinned && (
|
||||||
<span className="comment__pin">
|
<span className="comment__pin">
|
||||||
|
|
|
@ -12,6 +12,7 @@ import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||||
import { selectActiveChannelClaim } from 'redux/selectors/app';
|
import { selectActiveChannelClaim } from 'redux/selectors/app';
|
||||||
import { makeSelectCommentsDisabledForUri } from 'redux/selectors/comments';
|
import { makeSelectCommentsDisabledForUri } from 'redux/selectors/comments';
|
||||||
import { CommentCreate } from './view';
|
import { CommentCreate } from './view';
|
||||||
|
import { doToast } from 'redux/actions/notifications';
|
||||||
|
|
||||||
const select = (state, props) => ({
|
const select = (state, props) => ({
|
||||||
commentingEnabled: IS_WEB ? Boolean(selectUserVerifiedEmail(state)) : true,
|
commentingEnabled: IS_WEB ? Boolean(selectUserVerifiedEmail(state)) : true,
|
||||||
|
@ -24,11 +25,12 @@ const select = (state, props) => ({
|
||||||
});
|
});
|
||||||
|
|
||||||
const perform = (dispatch, ownProps) => ({
|
const perform = (dispatch, ownProps) => ({
|
||||||
createComment: (comment, claimId, parentId, txid) =>
|
createComment: (comment, claimId, parentId, txid, payment_intent_id, environment) =>
|
||||||
dispatch(doCommentCreate(comment, claimId, parentId, ownProps.uri, ownProps.livestream, txid)),
|
dispatch(doCommentCreate(comment, claimId, parentId, ownProps.uri, ownProps.livestream, txid, payment_intent_id, environment)),
|
||||||
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
|
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
|
||||||
setActiveChannel: (claimId) => dispatch(doSetActiveChannel(claimId)),
|
setActiveChannel: (claimId) => dispatch(doSetActiveChannel(claimId)),
|
||||||
sendTip: (params, callback, errorCallback) => dispatch(doSendTip(params, false, callback, errorCallback, false)),
|
sendTip: (params, callback, errorCallback) => dispatch(doSendTip(params, false, callback, errorCallback, false)),
|
||||||
|
doToast: (options) => dispatch(doToast(options)),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(select, perform)(CommentCreate);
|
export default connect(select, perform)(CommentCreate);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// @flow
|
// @flow
|
||||||
import type { ElementRef } from 'react';
|
import type { ElementRef } from 'react';
|
||||||
import { SIMPLE_SITE } from 'config';
|
import { SIMPLE_SITE, STRIPE_PUBLIC_KEY } from 'config';
|
||||||
import * as PAGES from 'constants/pages';
|
import * as PAGES from 'constants/pages';
|
||||||
import * as ICONS from 'constants/icons';
|
import * as ICONS from 'constants/icons';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
@ -16,11 +16,22 @@ import CreditAmount from 'component/common/credit-amount';
|
||||||
import ChannelThumbnail from 'component/channelThumbnail';
|
import ChannelThumbnail from 'component/channelThumbnail';
|
||||||
import UriIndicator from 'component/uriIndicator';
|
import UriIndicator from 'component/uriIndicator';
|
||||||
import Empty from 'component/common/empty';
|
import Empty from 'component/common/empty';
|
||||||
|
import { Lbryio } from 'lbryinc';
|
||||||
|
|
||||||
|
let stripeEnvironment = 'test';
|
||||||
|
// if the key contains pk_live it's a live key
|
||||||
|
// update the environment for the calls to the backend to indicate which environment to hit
|
||||||
|
if (STRIPE_PUBLIC_KEY.indexOf('pk_live') > -1) {
|
||||||
|
stripeEnvironment = 'live';
|
||||||
|
}
|
||||||
|
|
||||||
|
const TAB_FIAT = 'TabFiat';
|
||||||
|
const TAB_LBC = 'TabLBC';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
uri: string,
|
uri: string,
|
||||||
claim: StreamClaim,
|
claim: StreamClaim,
|
||||||
createComment: (string, string, string, ?string) => Promise<any>,
|
createComment: (string, string, string, ?string, ?string, ?string) => Promise<any>,
|
||||||
commentsDisabledBySettings: boolean,
|
commentsDisabledBySettings: boolean,
|
||||||
channels: ?Array<ChannelClaim>,
|
channels: ?Array<ChannelClaim>,
|
||||||
onDoneReplying?: () => void,
|
onDoneReplying?: () => void,
|
||||||
|
@ -35,6 +46,8 @@ type Props = {
|
||||||
toast: (string) => void,
|
toast: (string) => void,
|
||||||
claimIsMine: boolean,
|
claimIsMine: boolean,
|
||||||
sendTip: ({}, (any) => void, (any) => void) => void,
|
sendTip: ({}, (any) => void, (any) => void) => void,
|
||||||
|
doToast: ({ message: string }) => void,
|
||||||
|
disabled: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function CommentCreate(props: Props) {
|
export function CommentCreate(props: Props) {
|
||||||
|
@ -53,8 +66,10 @@ export function CommentCreate(props: Props) {
|
||||||
livestream,
|
livestream,
|
||||||
claimIsMine,
|
claimIsMine,
|
||||||
sendTip,
|
sendTip,
|
||||||
|
doToast,
|
||||||
} = props;
|
} = props;
|
||||||
const buttonref: ElementRef<any> = React.useRef();
|
const buttonref: ElementRef<any> = React.useRef();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
push,
|
push,
|
||||||
location: { pathname },
|
location: { pathname },
|
||||||
|
@ -69,9 +84,15 @@ export function CommentCreate(props: Props) {
|
||||||
const [commentValue, setCommentValue] = React.useState('');
|
const [commentValue, setCommentValue] = React.useState('');
|
||||||
const [advancedEditor, setAdvancedEditor] = usePersistedState('comment-editor-mode', false);
|
const [advancedEditor, setAdvancedEditor] = usePersistedState('comment-editor-mode', false);
|
||||||
const hasChannels = channels && channels.length;
|
const hasChannels = channels && channels.length;
|
||||||
const disabled = isSubmitting || !activeChannelClaim || !commentValue.length;
|
|
||||||
const charCount = commentValue.length;
|
const charCount = commentValue.length;
|
||||||
|
|
||||||
|
const [activeTab, setActiveTab] = React.useState('');
|
||||||
|
|
||||||
|
const [tipError, setTipError] = React.useState();
|
||||||
|
|
||||||
|
const disabled = isSubmitting || !activeChannelClaim || !commentValue.length;
|
||||||
|
const [shouldDisableReviewButton, setShouldDisableReviewButton] = React.useState();
|
||||||
|
|
||||||
function handleCommentChange(event) {
|
function handleCommentChange(event) {
|
||||||
let commentValue;
|
let commentValue;
|
||||||
if (isReply) {
|
if (isReply) {
|
||||||
|
@ -123,26 +144,109 @@ export function CommentCreate(props: Props) {
|
||||||
channel_id: activeChannelClaim.claim_id,
|
channel_id: activeChannelClaim.claim_id,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const activeChannelName = activeChannelClaim && activeChannelClaim.name;
|
||||||
|
const activeChannelId = activeChannelClaim && activeChannelClaim.claim_id;
|
||||||
|
|
||||||
|
console.log(activeChannelClaim);
|
||||||
|
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
|
|
||||||
sendTip(
|
if (activeTab === TAB_LBC) {
|
||||||
params,
|
// call sendTip and then run the callback from the response
|
||||||
(response) => {
|
// second parameter is callback
|
||||||
const { txid } = response;
|
sendTip(
|
||||||
setTimeout(() => {
|
params,
|
||||||
handleCreateComment(txid);
|
(response) => {
|
||||||
}, 1500);
|
const { txid } = response;
|
||||||
setSuccessTip({ txid, tipAmount });
|
// todo: why the setTimeout?
|
||||||
},
|
setTimeout(() => {
|
||||||
() => {
|
handleCreateComment(txid);
|
||||||
setIsSubmitting(false);
|
}, 1500);
|
||||||
|
setSuccessTip({ txid, tipAmount });
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
// reset the frontend so people can send a new comment
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// setup variables for tip API
|
||||||
|
let channelClaimId, tipChannelName;
|
||||||
|
// if there is a signing channel it's on a file
|
||||||
|
if (claim.signing_channel) {
|
||||||
|
channelClaimId = claim.signing_channel.claim_id;
|
||||||
|
tipChannelName = claim.signing_channel.name;
|
||||||
|
|
||||||
|
// otherwise it's on the channel page
|
||||||
|
} else {
|
||||||
|
channelClaimId = claim.claim_id;
|
||||||
|
tipChannelName = claim.name;
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
const sourceClaimId = claim.claim_id;
|
||||||
|
|
||||||
|
var roundedAmount = Math.round(tipAmount * 100) / 100;
|
||||||
|
|
||||||
|
Lbryio.call(
|
||||||
|
'customer',
|
||||||
|
'tip',
|
||||||
|
{
|
||||||
|
amount: 100 * roundedAmount, // convert from dollars to cents
|
||||||
|
creator_channel_name: tipChannelName, // creator_channel_name
|
||||||
|
creator_channel_claim_id: channelClaimId,
|
||||||
|
tipper_channel_name: activeChannelName,
|
||||||
|
tipper_channel_claim_id: activeChannelId,
|
||||||
|
currency: 'USD',
|
||||||
|
anonymous: false,
|
||||||
|
source_claim_id: sourceClaimId,
|
||||||
|
environment: stripeEnvironment,
|
||||||
|
},
|
||||||
|
'post'
|
||||||
|
)
|
||||||
|
.then((customerTipResponse) => {
|
||||||
|
console.log(customerTipResponse);
|
||||||
|
|
||||||
|
const paymentIntendId = customerTipResponse.payment_intent_id;
|
||||||
|
|
||||||
|
handleCreateComment(null, paymentIntendId, stripeEnvironment);
|
||||||
|
|
||||||
|
setCommentValue('');
|
||||||
|
setIsReviewingSupportComment(false);
|
||||||
|
setIsSupportComment(false);
|
||||||
|
setCommentFailure(false);
|
||||||
|
setIsSubmitting(false);
|
||||||
|
|
||||||
|
doToast({
|
||||||
|
message: __("You sent $%formattedAmount% as a tip to %tipChannelName%, I'm sure they appreciate it!", {
|
||||||
|
formattedAmount: roundedAmount.toFixed(2), // force show decimal places
|
||||||
|
tipChannelName,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
// handleCreateComment(null);
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
var displayError = 'Sorry, there was an error in processing your payment!';
|
||||||
|
|
||||||
|
if (error.message !== 'payment intent failed to confirm') {
|
||||||
|
displayError = error.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
doToast({ message: displayError, isError: true });
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCreateComment(txid) {
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} [txid] Optional transaction id generated by
|
||||||
|
* @param {string} [payment_intent_id] Optional payment_intent_id from Stripe payment
|
||||||
|
* @param {string} [environment] Optional environment for Stripe (test|live)
|
||||||
|
*/
|
||||||
|
function handleCreateComment(txid, payment_intent_id, environment) {
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
createComment(commentValue, claimId, parentId, txid)
|
|
||||||
|
createComment(commentValue, claimId, parentId, txid, payment_intent_id, environment)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
|
|
||||||
|
@ -157,7 +261,7 @@ export function CommentCreate(props: Props) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch((e) => {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
setCommentFailure(true);
|
setCommentFailure(true);
|
||||||
});
|
});
|
||||||
|
@ -201,7 +305,12 @@ export function CommentCreate(props: Props) {
|
||||||
return (
|
return (
|
||||||
<div className="comment__create">
|
<div className="comment__create">
|
||||||
<div className="comment__sc-preview">
|
<div className="comment__sc-preview">
|
||||||
<CreditAmount className="comment__scpreview-amount" amount={tipAmount} size={18} />
|
<CreditAmount
|
||||||
|
className="comment__scpreview-amount"
|
||||||
|
isFiat={activeTab === TAB_FIAT}
|
||||||
|
amount={tipAmount}
|
||||||
|
size={activeTab === TAB_LBC ? 18 : 2}
|
||||||
|
/>
|
||||||
|
|
||||||
<ChannelThumbnail xsmall uri={activeChannelClaim.canonical_url} />
|
<ChannelThumbnail xsmall uri={activeChannelClaim.canonical_url} />
|
||||||
<div>
|
<div>
|
||||||
|
@ -262,15 +371,24 @@ export function CommentCreate(props: Props) {
|
||||||
autoFocus={isReply}
|
autoFocus={isReply}
|
||||||
textAreaMaxLength={livestream ? FF_MAX_CHARS_IN_LIVESTREAM_COMMENT : FF_MAX_CHARS_IN_COMMENT}
|
textAreaMaxLength={livestream ? FF_MAX_CHARS_IN_LIVESTREAM_COMMENT : FF_MAX_CHARS_IN_COMMENT}
|
||||||
/>
|
/>
|
||||||
{isSupportComment && <WalletTipAmountSelector amount={tipAmount} onChange={(amount) => setTipAmount(amount)} />}
|
{isSupportComment && (
|
||||||
|
<WalletTipAmountSelector
|
||||||
|
onTipErrorChange={setTipError}
|
||||||
|
shouldDisableReviewButton={setShouldDisableReviewButton}
|
||||||
|
claim={claim}
|
||||||
|
activeTab={activeTab}
|
||||||
|
amount={tipAmount}
|
||||||
|
onChange={(amount) => setTipAmount(amount)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<div className="section__actions section__actions--no-margin">
|
<div className="section__actions section__actions--no-margin">
|
||||||
{isSupportComment ? (
|
{isSupportComment ? (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
disabled={disabled}
|
disabled={disabled || tipError || shouldDisableReviewButton}
|
||||||
type="button"
|
type="button"
|
||||||
button="primary"
|
button="primary"
|
||||||
icon={ICONS.LBC}
|
icon={activeTab === TAB_LBC ? ICONS.LBC : ICONS.FINANCE}
|
||||||
label={__('Review')}
|
label={__('Review')}
|
||||||
onClick={() => setIsReviewingSupportComment(true)}
|
onClick={() => setIsReviewingSupportComment(true)}
|
||||||
/>
|
/>
|
||||||
|
@ -296,7 +414,28 @@ export function CommentCreate(props: Props) {
|
||||||
requiresAuth={IS_WEB}
|
requiresAuth={IS_WEB}
|
||||||
/>
|
/>
|
||||||
{!claimIsMine && (
|
{!claimIsMine && (
|
||||||
<Button disabled={disabled} button="alt" icon={ICONS.LBC} onClick={() => setIsSupportComment(true)} />
|
<Button
|
||||||
|
disabled={disabled}
|
||||||
|
button="alt"
|
||||||
|
className="thatButton"
|
||||||
|
icon={ICONS.LBC}
|
||||||
|
onClick={() => {
|
||||||
|
setIsSupportComment(true);
|
||||||
|
setActiveTab(TAB_LBC);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!claimIsMine && (
|
||||||
|
<Button
|
||||||
|
disabled={disabled}
|
||||||
|
button="alt"
|
||||||
|
className="thisButton"
|
||||||
|
icon={ICONS.FINANCE}
|
||||||
|
onClick={() => {
|
||||||
|
setIsSupportComment(true);
|
||||||
|
setActiveTab(TAB_FIAT);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{isReply && (
|
{isReply && (
|
||||||
<Button
|
<Button
|
||||||
|
|
|
@ -4,10 +4,11 @@ import { makeSelectClaimIsMine, makeSelectClaimForUri } from 'lbry-redux';
|
||||||
import { doToast } from 'redux/actions/notifications';
|
import { doToast } from 'redux/actions/notifications';
|
||||||
import { makeSelectMyReactionsForComment, makeSelectOthersReactionsForComment } from 'redux/selectors/comments';
|
import { makeSelectMyReactionsForComment, makeSelectOthersReactionsForComment } from 'redux/selectors/comments';
|
||||||
import { doCommentReact } from 'redux/actions/comments';
|
import { doCommentReact } from 'redux/actions/comments';
|
||||||
import { selectActiveChannelId } from 'redux/selectors/app';
|
import { selectActiveChannelClaim } from 'redux/selectors/app';
|
||||||
|
|
||||||
const select = (state, props) => {
|
const select = (state, props) => {
|
||||||
const activeChannelId = selectActiveChannelId(state);
|
const activeChannelClaim = selectActiveChannelClaim(state);
|
||||||
|
const activeChannelId = activeChannelClaim && activeChannelClaim.claim_id;
|
||||||
const reactionKey = activeChannelId ? `${props.commentId}:${activeChannelId}` : props.commentId;
|
const reactionKey = activeChannelId ? `${props.commentId}:${activeChannelId}` : props.commentId;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { ENABLE_CREATOR_REACTIONS } from 'config';
|
import { ENABLE_CREATOR_REACTIONS, SIMPLE_SITE } from 'config';
|
||||||
import * as ICONS from 'constants/icons';
|
import * as ICONS from 'constants/icons';
|
||||||
import * as PAGES from 'constants/pages';
|
import * as PAGES from 'constants/pages';
|
||||||
import * as REACTION_TYPES from 'constants/reactions';
|
import * as REACTION_TYPES from 'constants/reactions';
|
||||||
|
@ -50,6 +50,16 @@ export default function CommentReactions(props: Props) {
|
||||||
};
|
};
|
||||||
|
|
||||||
const creatorLiked = getCountForReact(REACTION_TYPES.CREATOR_LIKE) > 0;
|
const creatorLiked = getCountForReact(REACTION_TYPES.CREATOR_LIKE) > 0;
|
||||||
|
const likeIcon = SIMPLE_SITE
|
||||||
|
? myReacts.includes(REACTION_TYPES.LIKE)
|
||||||
|
? ICONS.FIRE_ACTIVE
|
||||||
|
: ICONS.FIRE
|
||||||
|
: ICONS.UPVOTE;
|
||||||
|
const dislikeIcon = SIMPLE_SITE
|
||||||
|
? myReacts.includes(REACTION_TYPES.DISLIKE)
|
||||||
|
? ICONS.SLIME_ACTIVE
|
||||||
|
: ICONS.SLIME
|
||||||
|
: ICONS.DOWNVOTE;
|
||||||
|
|
||||||
function handleCommentLike() {
|
function handleCommentLike() {
|
||||||
if (activeChannelId) {
|
if (activeChannelId) {
|
||||||
|
@ -77,7 +87,7 @@ export default function CommentReactions(props: Props) {
|
||||||
<Button
|
<Button
|
||||||
requiresAuth={IS_WEB}
|
requiresAuth={IS_WEB}
|
||||||
title={__('Upvote')}
|
title={__('Upvote')}
|
||||||
icon={ICONS.UPVOTE}
|
icon={likeIcon}
|
||||||
className={classnames('comment__action', {
|
className={classnames('comment__action', {
|
||||||
'comment__action--active': myReacts && myReacts.includes(REACTION_TYPES.LIKE),
|
'comment__action--active': myReacts && myReacts.includes(REACTION_TYPES.LIKE),
|
||||||
})}
|
})}
|
||||||
|
@ -87,7 +97,7 @@ export default function CommentReactions(props: Props) {
|
||||||
<Button
|
<Button
|
||||||
requiresAuth={IS_WEB}
|
requiresAuth={IS_WEB}
|
||||||
title={__('Downvote')}
|
title={__('Downvote')}
|
||||||
icon={ICONS.DOWNVOTE}
|
icon={dislikeIcon}
|
||||||
className={classnames('comment__action', {
|
className={classnames('comment__action', {
|
||||||
'comment__action--active': myReacts && myReacts.includes(REACTION_TYPES.DISLIKE),
|
'comment__action--active': myReacts && myReacts.includes(REACTION_TYPES.DISLIKE),
|
||||||
})}
|
})}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import {
|
||||||
makeSelectTopLevelCommentsForUri,
|
makeSelectTopLevelCommentsForUri,
|
||||||
makeSelectTopLevelTotalPagesForUri,
|
makeSelectTopLevelTotalPagesForUri,
|
||||||
selectIsFetchingComments,
|
selectIsFetchingComments,
|
||||||
|
selectIsFetchingReacts,
|
||||||
makeSelectTotalCommentsCountForUri,
|
makeSelectTotalCommentsCountForUri,
|
||||||
selectOthersReactsById,
|
selectOthersReactsById,
|
||||||
makeSelectCommentsDisabledForUri,
|
makeSelectCommentsDisabledForUri,
|
||||||
|
@ -12,10 +13,11 @@ import {
|
||||||
} from 'redux/selectors/comments';
|
} from 'redux/selectors/comments';
|
||||||
import { doCommentReset, doCommentList, doCommentById, doCommentReactList } from 'redux/actions/comments';
|
import { doCommentReset, doCommentList, doCommentById, doCommentReactList } from 'redux/actions/comments';
|
||||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||||
import { selectActiveChannelId } from 'redux/selectors/app';
|
import { selectActiveChannelClaim } from 'redux/selectors/app';
|
||||||
import CommentsList from './view';
|
import CommentsList from './view';
|
||||||
|
|
||||||
const select = (state, props) => {
|
const select = (state, props) => {
|
||||||
|
const activeChannelClaim = selectActiveChannelClaim(state);
|
||||||
return {
|
return {
|
||||||
myChannels: selectMyChannelClaims(state),
|
myChannels: selectMyChannelClaims(state),
|
||||||
allCommentIds: makeSelectCommentIdsForUri(props.uri)(state),
|
allCommentIds: makeSelectCommentIdsForUri(props.uri)(state),
|
||||||
|
@ -24,12 +26,13 @@ const select = (state, props) => {
|
||||||
totalComments: makeSelectTotalCommentsCountForUri(props.uri)(state),
|
totalComments: makeSelectTotalCommentsCountForUri(props.uri)(state),
|
||||||
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
||||||
isFetchingComments: selectIsFetchingComments(state),
|
isFetchingComments: selectIsFetchingComments(state),
|
||||||
|
isFetchingReacts: selectIsFetchingReacts(state),
|
||||||
commentingEnabled: IS_WEB ? Boolean(selectUserVerifiedEmail(state)) : true,
|
commentingEnabled: IS_WEB ? Boolean(selectUserVerifiedEmail(state)) : true,
|
||||||
commentsDisabledBySettings: makeSelectCommentsDisabledForUri(props.uri)(state),
|
commentsDisabledBySettings: makeSelectCommentsDisabledForUri(props.uri)(state),
|
||||||
fetchingChannels: selectFetchingMyChannels(state),
|
fetchingChannels: selectFetchingMyChannels(state),
|
||||||
myReactsByCommentId: selectMyReactionsByCommentId(state),
|
myReactsByCommentId: selectMyReactionsByCommentId(state),
|
||||||
othersReactsById: selectOthersReactsById(state),
|
othersReactsById: selectOthersReactsById(state),
|
||||||
activeChannelId: selectActiveChannelId(state),
|
activeChannelId: activeChannelClaim && activeChannelClaim.claim_id,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -37,6 +37,7 @@ type Props = {
|
||||||
claimIsMine: boolean,
|
claimIsMine: boolean,
|
||||||
myChannels: ?Array<ChannelClaim>,
|
myChannels: ?Array<ChannelClaim>,
|
||||||
isFetchingComments: boolean,
|
isFetchingComments: boolean,
|
||||||
|
isFetchingReacts: boolean,
|
||||||
linkedCommentId?: string,
|
linkedCommentId?: string,
|
||||||
totalComments: number,
|
totalComments: number,
|
||||||
fetchingChannels: boolean,
|
fetchingChannels: boolean,
|
||||||
|
@ -59,6 +60,7 @@ function CommentList(props: Props) {
|
||||||
claimIsMine,
|
claimIsMine,
|
||||||
myChannels,
|
myChannels,
|
||||||
isFetchingComments,
|
isFetchingComments,
|
||||||
|
isFetchingReacts,
|
||||||
linkedCommentId,
|
linkedCommentId,
|
||||||
totalComments,
|
totalComments,
|
||||||
fetchingChannels,
|
fetchingChannels,
|
||||||
|
@ -122,7 +124,7 @@ function CommentList(props: Props) {
|
||||||
|
|
||||||
// Fetch reacts
|
// Fetch reacts
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (totalFetchedComments > 0 && ENABLE_COMMENT_REACTIONS && !fetchingChannels) {
|
if (totalFetchedComments > 0 && ENABLE_COMMENT_REACTIONS && !fetchingChannels && !isFetchingReacts) {
|
||||||
let idsForReactionFetch;
|
let idsForReactionFetch;
|
||||||
|
|
||||||
if (!othersReactsById || !myReactsByCommentId) {
|
if (!othersReactsById || !myReactsByCommentId) {
|
||||||
|
@ -130,7 +132,7 @@ function CommentList(props: Props) {
|
||||||
} else {
|
} else {
|
||||||
idsForReactionFetch = allCommentIds.filter((commentId) => {
|
idsForReactionFetch = allCommentIds.filter((commentId) => {
|
||||||
const key = activeChannelId ? `${commentId}:${activeChannelId}` : commentId;
|
const key = activeChannelId ? `${commentId}:${activeChannelId}` : commentId;
|
||||||
return !othersReactsById[key] || !myReactsByCommentId[key];
|
return !othersReactsById[key] || (activeChannelId && !myReactsByCommentId[key]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -151,6 +153,7 @@ function CommentList(props: Props) {
|
||||||
uri,
|
uri,
|
||||||
activeChannelId,
|
activeChannelId,
|
||||||
fetchingChannels,
|
fetchingChannels,
|
||||||
|
isFetchingReacts,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Scroll to linked-comment
|
// Scroll to linked-comment
|
||||||
|
@ -298,6 +301,7 @@ function CommentList(props: Props) {
|
||||||
isPinned={comment.is_pinned}
|
isPinned={comment.is_pinned}
|
||||||
supportAmount={comment.support_amount}
|
supportAmount={comment.support_amount}
|
||||||
numDirectReplies={comment.replies}
|
numDirectReplies={comment.replies}
|
||||||
|
isFiat={comment.is_fiat}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
|
@ -72,34 +72,35 @@ function CommentsReplies(props: Props) {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{fetchedReplies && displayedComments && isExpanded && (
|
{isExpanded && (
|
||||||
<div>
|
<div>
|
||||||
<div className="comment__replies">
|
<div className="comment__replies">
|
||||||
<Button className="comment__threadline" aria-label="Hide Replies" onClick={() => setExpanded(false)} />
|
<Button className="comment__threadline" aria-label="Hide Replies" onClick={() => setExpanded(false)} />
|
||||||
|
|
||||||
<ul className="comments--replies">
|
<ul className="comments--replies">
|
||||||
{displayedComments.map((comment) => {
|
{displayedComments &&
|
||||||
return (
|
displayedComments.map((comment) => {
|
||||||
<Comment
|
return (
|
||||||
threadDepth={threadDepth}
|
<Comment
|
||||||
uri={uri}
|
threadDepth={threadDepth}
|
||||||
authorUri={comment.channel_url}
|
uri={uri}
|
||||||
author={comment.channel_name}
|
authorUri={comment.channel_url}
|
||||||
claimId={comment.claim_id}
|
author={comment.channel_name}
|
||||||
commentId={comment.comment_id}
|
claimId={comment.claim_id}
|
||||||
key={comment.comment_id}
|
commentId={comment.comment_id}
|
||||||
message={comment.comment}
|
key={comment.comment_id}
|
||||||
timePosted={comment.timestamp * 1000}
|
message={comment.comment}
|
||||||
claimIsMine={claimIsMine}
|
timePosted={comment.timestamp * 1000}
|
||||||
commentIsMine={comment.channel_id && isMyComment(comment.channel_id)}
|
claimIsMine={claimIsMine}
|
||||||
linkedCommentId={linkedCommentId}
|
commentIsMine={comment.channel_id && isMyComment(comment.channel_id)}
|
||||||
commentingEnabled={commentingEnabled}
|
linkedCommentId={linkedCommentId}
|
||||||
supportAmount={comment.support_amount}
|
commentingEnabled={commentingEnabled}
|
||||||
numDirectReplies={comment.replies}
|
supportAmount={comment.support_amount}
|
||||||
/>
|
numDirectReplies={comment.replies}
|
||||||
);
|
/>
|
||||||
})}
|
);
|
||||||
{totalReplies < numDirectReplies && (
|
})}
|
||||||
|
{!isFetchingByParentId[parentId] && totalReplies < numDirectReplies && (
|
||||||
<li className="comment comment--reply">
|
<li className="comment comment--reply">
|
||||||
<div className="comment__content">
|
<div className="comment__content">
|
||||||
<div className="comment__thumbnail-wrapper">
|
<div className="comment__thumbnail-wrapper">
|
||||||
|
|
|
@ -18,6 +18,7 @@ type Props = {
|
||||||
size?: number,
|
size?: number,
|
||||||
superChat?: boolean,
|
superChat?: boolean,
|
||||||
superChatLight?: boolean,
|
superChatLight?: boolean,
|
||||||
|
isFiat?: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
class CreditAmount extends React.PureComponent<Props> {
|
class CreditAmount extends React.PureComponent<Props> {
|
||||||
|
@ -45,6 +46,7 @@ class CreditAmount extends React.PureComponent<Props> {
|
||||||
size,
|
size,
|
||||||
superChat,
|
superChat,
|
||||||
superChatLight,
|
superChatLight,
|
||||||
|
isFiat,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const minimumRenderableAmount = 10 ** (-1 * precision);
|
const minimumRenderableAmount = 10 ** (-1 * precision);
|
||||||
const fullPrice = formatFullPrice(amount, 2);
|
const fullPrice = formatFullPrice(amount, 2);
|
||||||
|
@ -70,8 +72,10 @@ class CreditAmount extends React.PureComponent<Props> {
|
||||||
amountText = `+${amountText}`;
|
amountText = `+${amountText}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showLBC) {
|
if (showLBC && !isFiat) {
|
||||||
amountText = <LbcSymbol postfix={amountText} size={size} />;
|
amountText = <LbcSymbol postfix={amountText} size={size} />;
|
||||||
|
} else if (showLBC && isFiat) {
|
||||||
|
amountText = <p style={{display: 'inline'}}> ${(Math.round(Number(amountText) * 100) / 100).toFixed(2)}</p>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fee) {
|
if (fee) {
|
||||||
|
|
|
@ -780,6 +780,12 @@ export const icons = {
|
||||||
viewBox: '0 0 60 60',
|
viewBox: '0 0 60 60',
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
[ICONS.COPY_LINK]: buildIcon(
|
||||||
|
<g>
|
||||||
|
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
|
||||||
|
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
|
||||||
|
</g>
|
||||||
|
),
|
||||||
[ICONS.PURCHASED]: buildIcon(
|
[ICONS.PURCHASED]: buildIcon(
|
||||||
<g>
|
<g>
|
||||||
<path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4" />
|
<path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4" />
|
||||||
|
|
|
@ -161,38 +161,42 @@ function FileActions(props: Props) {
|
||||||
onClick={() => openModal(MODALS.CONFIRM_FILE_REMOVE, { uri })}
|
onClick={() => openModal(MODALS.CONFIRM_FILE_REMOVE, { uri })}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Menu>
|
{(!isLivestreamClaim || !claimIsMine) && (
|
||||||
<MenuButton
|
<Menu>
|
||||||
className="button--file-action"
|
<MenuButton
|
||||||
onClick={(e) => {
|
className="button--file-action"
|
||||||
e.stopPropagation();
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.stopPropagation();
|
||||||
}}
|
e.preventDefault();
|
||||||
>
|
}}
|
||||||
<Icon size={20} icon={ICONS.MORE} />
|
>
|
||||||
</MenuButton>
|
<Icon size={20} icon={ICONS.MORE} />
|
||||||
<MenuList className="menu__list">
|
</MenuButton>
|
||||||
{/* @if TARGET='web' */}
|
<MenuList className="menu__list">
|
||||||
<MenuItem className="comment__menu-option" onSelect={handleWebDownload}>
|
{/* @if TARGET='web' */}
|
||||||
<div className="menu__link">
|
{!isLivestreamClaim && (
|
||||||
<Icon aria-hidden icon={ICONS.DOWNLOAD} />
|
<MenuItem className="comment__menu-option" onSelect={handleWebDownload}>
|
||||||
{__('Download')}
|
<div className="menu__link">
|
||||||
</div>
|
<Icon aria-hidden icon={ICONS.DOWNLOAD} />
|
||||||
</MenuItem>
|
{__('Download')}
|
||||||
{/* @endif */}
|
</div>
|
||||||
{!claimIsMine && (
|
</MenuItem>
|
||||||
<MenuItem
|
)}
|
||||||
className="comment__menu-option"
|
{/* @endif */}
|
||||||
onSelect={() => push(`/$/${PAGES.REPORT_CONTENT}?claimId=${claimId}`)}
|
{!claimIsMine && (
|
||||||
>
|
<MenuItem
|
||||||
<div className="menu__link">
|
className="comment__menu-option"
|
||||||
<Icon aria-hidden icon={ICONS.REPORT} />
|
onSelect={() => push(`/$/${PAGES.REPORT_CONTENT}?claimId=${claimId}`)}
|
||||||
{__('Report content')}
|
>
|
||||||
</div>
|
<div className="menu__link">
|
||||||
</MenuItem>
|
<Icon aria-hidden icon={ICONS.REPORT} />
|
||||||
)}
|
{__('Report content')}
|
||||||
</MenuList>
|
</div>
|
||||||
</Menu>
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
</MenuList>
|
||||||
|
</Menu>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@ import { onFullscreenChange } from 'util/full-screen';
|
||||||
import { useIsMobile } from 'effects/use-screensize';
|
import { useIsMobile } from 'effects/use-screensize';
|
||||||
import debounce from 'util/debounce';
|
import debounce from 'util/debounce';
|
||||||
import { useHistory } from 'react-router';
|
import { useHistory } from 'react-router';
|
||||||
|
import { isURIEqual } from 'lbry-redux';
|
||||||
|
|
||||||
const IS_DESKTOP_MAC = typeof process === 'object' ? process.platform === 'darwin' : false;
|
const IS_DESKTOP_MAC = typeof process === 'object' ? process.platform === 'darwin' : false;
|
||||||
const DEBOUNCE_WINDOW_RESIZE_HANDLER_MS = 60;
|
const DEBOUNCE_WINDOW_RESIZE_HANDLER_MS = 60;
|
||||||
|
@ -55,7 +56,7 @@ export default function FileRenderFloating(props: Props) {
|
||||||
location: { pathname },
|
location: { pathname },
|
||||||
} = useHistory();
|
} = useHistory();
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const mainFilePlaying = playingUri && playingUri.uri === primaryUri;
|
const mainFilePlaying = playingUri && isURIEqual(playingUri.uri, primaryUri);
|
||||||
const [fileViewerRect, setFileViewerRect] = useState();
|
const [fileViewerRect, setFileViewerRect] = useState();
|
||||||
const [desktopPlayStartTime, setDesktopPlayStartTime] = useState();
|
const [desktopPlayStartTime, setDesktopPlayStartTime] = useState();
|
||||||
const [wasDragging, setWasDragging] = useState(false);
|
const [wasDragging, setWasDragging] = useState(false);
|
||||||
|
|
|
@ -37,12 +37,12 @@ function FileViewCount(props: Props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className="media__subtitle--centered">
|
<span className="media__subtitle--centered">
|
||||||
{isLive &&
|
{livestream &&
|
||||||
__('%viewer_count% currently %viewer_state%', {
|
__('%viewer_count% currently %viewer_state%', {
|
||||||
viewer_count: activeViewers === undefined ? '...' : activeViewers,
|
viewer_count: activeViewers === undefined ? '...' : activeViewers,
|
||||||
viewer_state: isLive ? __('watching') : __('waiting'),
|
viewer_state: isLive ? __('watching') : __('waiting'),
|
||||||
})}
|
})}
|
||||||
{!isLive &&
|
{!livestream &&
|
||||||
activeViewers === undefined &&
|
activeViewers === undefined &&
|
||||||
(viewCount !== 1 ? __('%view_count% views', { view_count: formattedViewCount }) : __('1 view'))}
|
(viewCount !== 1 ? __('%view_count% views', { view_count: formattedViewCount }) : __('1 view'))}
|
||||||
{!SIMPLE_SITE && <HelpLink href="https://lbry.com/faq/views" />}
|
{!SIMPLE_SITE && <HelpLink href="https://lbry.com/faq/views" />}
|
||||||
|
|
|
@ -26,9 +26,7 @@ function FileWatchLaterLink(props: Props) {
|
||||||
function handleWatchLater(e) {
|
function handleWatchLater(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
doToast({
|
doToast({
|
||||||
message: __('Item %action% Watch Later', {
|
message: hasClaimInWatchLater ? __('Item removed from Watch Later') : __('Item added to Watch Later'),
|
||||||
action: hasClaimInWatchLater ? __('removed from') : __('added to'),
|
|
||||||
}),
|
|
||||||
linkText: !hasClaimInWatchLater && __('See All'),
|
linkText: !hasClaimInWatchLater && __('See All'),
|
||||||
linkTarget: !hasClaimInWatchLater && '/list/watchlater',
|
linkTarget: !hasClaimInWatchLater && '/list/watchlater',
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { LOGO_TITLE, ENABLE_NO_SOURCE_CLAIMS, CHANNEL_STAKED_LEVEL_LIVESTREAM } from 'config';
|
import { LOGO_TITLE, ENABLE_NO_SOURCE_CLAIMS, CHANNEL_STAKED_LEVEL_LIVESTREAM, ENABLE_UI_NOTIFICATIONS } from 'config';
|
||||||
import * as ICONS from 'constants/icons';
|
import * as ICONS from 'constants/icons';
|
||||||
import { SETTINGS } from 'lbry-redux';
|
import { SETTINGS } from 'lbry-redux';
|
||||||
import * as PAGES from 'constants/pages';
|
import * as PAGES from 'constants/pages';
|
||||||
|
@ -107,8 +107,8 @@ const Header = (props: Props) => {
|
||||||
sidebarOpen,
|
sidebarOpen,
|
||||||
setSidebarOpen,
|
setSidebarOpen,
|
||||||
isAbsoluteSideNavHidden,
|
isAbsoluteSideNavHidden,
|
||||||
user,
|
|
||||||
hideCancel,
|
hideCancel,
|
||||||
|
user,
|
||||||
activeChannelClaim,
|
activeChannelClaim,
|
||||||
activeChannelStakedLevel,
|
activeChannelStakedLevel,
|
||||||
} = props;
|
} = props;
|
||||||
|
@ -120,7 +120,7 @@ const Header = (props: Props) => {
|
||||||
const isPwdResetPage = history.location.pathname.includes(PAGES.AUTH_PASSWORD_RESET);
|
const isPwdResetPage = history.location.pathname.includes(PAGES.AUTH_PASSWORD_RESET);
|
||||||
const hasBackout = Boolean(backout);
|
const hasBackout = Boolean(backout);
|
||||||
const { backLabel, backNavDefault, title: backTitle, simpleTitle: simpleBackTitle } = backout || {};
|
const { backLabel, backNavDefault, title: backTitle, simpleTitle: simpleBackTitle } = backout || {};
|
||||||
const notificationsEnabled = (user && user.experimental_ui) || false;
|
const notificationsEnabled = ENABLE_UI_NOTIFICATIONS || (user && user.experimental_ui);
|
||||||
const livestreamEnabled = Boolean(
|
const livestreamEnabled = Boolean(
|
||||||
ENABLE_NO_SOURCE_CLAIMS &&
|
ENABLE_NO_SOURCE_CLAIMS &&
|
||||||
user &&
|
user &&
|
||||||
|
@ -461,7 +461,12 @@ function HeaderMenuButtons(props: HeaderMenuButtonProps) {
|
||||||
<Icon aria-hidden icon={ICONS.CHANNEL} />
|
<Icon aria-hidden icon={ICONS.CHANNEL} />
|
||||||
{__('New Channel')}
|
{__('New Channel')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
{/* @if TARGET='web' */}
|
||||||
|
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.YOUTUBE_SYNC}`)}>
|
||||||
|
<Icon aria-hidden icon={ICONS.YOUTUBE} />
|
||||||
|
{__('Sync YouTube Channel')}
|
||||||
|
</MenuItem>
|
||||||
|
{/* @endif */}
|
||||||
{livestreamEnabled && (
|
{livestreamEnabled && (
|
||||||
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.LIVESTREAM}`)}>
|
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.LIVESTREAM}`)}>
|
||||||
<Icon aria-hidden icon={ICONS.VIDEO} />
|
<Icon aria-hidden icon={ICONS.VIDEO} />
|
||||||
|
|
|
@ -20,10 +20,11 @@ type Props = {
|
||||||
commentIsMine: boolean,
|
commentIsMine: boolean,
|
||||||
stakedLevel: number,
|
stakedLevel: number,
|
||||||
supportAmount: number,
|
supportAmount: number,
|
||||||
|
isFiat: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
function LivestreamComment(props: Props) {
|
function LivestreamComment(props: Props) {
|
||||||
const { claim, uri, authorUri, message, commentIsMine, commentId, stakedLevel, supportAmount } = props;
|
const { claim, uri, authorUri, message, commentIsMine, commentId, stakedLevel, supportAmount, isFiat } = props;
|
||||||
const [mouseIsHovering, setMouseHover] = React.useState(false);
|
const [mouseIsHovering, setMouseHover] = React.useState(false);
|
||||||
const commentByOwnerOfContent = claim && claim.signing_channel && claim.signing_channel.permanent_url === authorUri;
|
const commentByOwnerOfContent = claim && claim.signing_channel && claim.signing_channel.permanent_url === authorUri;
|
||||||
const { claimName } = parseURI(authorUri);
|
const { claimName } = parseURI(authorUri);
|
||||||
|
@ -39,7 +40,7 @@ function LivestreamComment(props: Props) {
|
||||||
{supportAmount > 0 && (
|
{supportAmount > 0 && (
|
||||||
<div className="super-chat livestream-superchat__banner">
|
<div className="super-chat livestream-superchat__banner">
|
||||||
<div className="livestream-superchat__banner-corner" />
|
<div className="livestream-superchat__banner-corner" />
|
||||||
<CreditAmount amount={supportAmount} superChat className="livestream-superchat__amount" />
|
<CreditAmount isFiat={isFiat} amount={supportAmount} superChat className="livestream-superchat__amount" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
@ -46,6 +46,7 @@ export default function LivestreamComments(props: Props) {
|
||||||
superChatsTotalAmount,
|
superChatsTotalAmount,
|
||||||
myChannels,
|
myChannels,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const commentsRef = React.createRef();
|
const commentsRef = React.createRef();
|
||||||
const [scrollBottom, setScrollBottom] = React.useState(true);
|
const [scrollBottom, setScrollBottom] = React.useState(true);
|
||||||
const [viewMode, setViewMode] = React.useState(VIEW_MODE_CHAT);
|
const [viewMode, setViewMode] = React.useState(VIEW_MODE_CHAT);
|
||||||
|
@ -158,7 +159,7 @@ export default function LivestreamComments(props: Props) {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div ref={commentsRef} className="livestream__comments-wrapper">
|
<div ref={commentsRef} className="livestream__comments-wrapper">
|
||||||
{viewMode === VIEW_MODE_CHAT && superChatsTotalAmount > 0 && (
|
{viewMode === VIEW_MODE_CHAT && superChatsTotalAmount > 0 && superChats && (
|
||||||
<div className="livestream-superchats__wrapper">
|
<div className="livestream-superchats__wrapper">
|
||||||
<div className="livestream-superchats__inner">
|
<div className="livestream-superchats__inner">
|
||||||
{superChats.map((superChat: Comment) => (
|
{superChats.map((superChat: Comment) => (
|
||||||
|
@ -174,6 +175,7 @@ export default function LivestreamComments(props: Props) {
|
||||||
size={10}
|
size={10}
|
||||||
className="livestream-superchat__amount-large"
|
className="livestream-superchat__amount-large"
|
||||||
amount={superChat.support_amount}
|
amount={superChat.support_amount}
|
||||||
|
isFiat={superChat.is_fiat}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -193,6 +195,7 @@ export default function LivestreamComments(props: Props) {
|
||||||
commentId={comment.comment_id}
|
commentId={comment.comment_id}
|
||||||
message={comment.comment}
|
message={comment.comment}
|
||||||
supportAmount={comment.support_amount}
|
supportAmount={comment.support_amount}
|
||||||
|
isFiat={comment.is_fiat}
|
||||||
commentIsMine={comment.channel_id && isMyComment(comment.channel_id)}
|
commentIsMine={comment.channel_id && isMyComment(comment.channel_id)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
|
@ -17,7 +17,7 @@ export default function LivestreamLink(props: Props) {
|
||||||
const { push } = useHistory();
|
const { push } = useHistory();
|
||||||
const [livestreamClaim, setLivestreamClaim] = React.useState(false);
|
const [livestreamClaim, setLivestreamClaim] = React.useState(false);
|
||||||
const [isLivestreaming, setIsLivestreaming] = React.useState(false);
|
const [isLivestreaming, setIsLivestreaming] = React.useState(false);
|
||||||
const livestreamChannelId = channelClaim.claim_id || ''; // TODO: fail in a safer way, probably
|
const livestreamChannelId = (channelClaim && channelClaim.claim_id) || ''; // TODO: fail in a safer way, probably
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (livestreamChannelId) {
|
if (livestreamChannelId) {
|
||||||
|
@ -29,7 +29,7 @@ export default function LivestreamLink(props: Props) {
|
||||||
})
|
})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
if (res && res.items && res.items.length > 0) {
|
if (res && res.items && res.items.length > 0) {
|
||||||
const claim = res.items[res.items.length - 1];
|
const claim = res.items[0];
|
||||||
setLivestreamClaim(claim);
|
setLivestreamClaim(claim);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
// @flow
|
// @flow
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
import { ENABLE_UI_NOTIFICATIONS } from 'config';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
unseenCount: number,
|
unseenCount: number,
|
||||||
|
@ -10,7 +11,7 @@ type Props = {
|
||||||
|
|
||||||
export default function NotificationHeaderButton(props: Props) {
|
export default function NotificationHeaderButton(props: Props) {
|
||||||
const { unseenCount, inline = false, user } = props;
|
const { unseenCount, inline = false, user } = props;
|
||||||
const notificationsEnabled = user && user.experimental_ui;
|
const notificationsEnabled = ENABLE_UI_NOTIFICATIONS || (user && user.experimental_ui);
|
||||||
|
|
||||||
if (unseenCount === 0 || !notificationsEnabled) {
|
if (unseenCount === 0 || !notificationsEnabled) {
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -6,6 +6,7 @@ import Icon from 'component/common/icon';
|
||||||
import NotificationBubble from 'component/notificationBubble';
|
import NotificationBubble from 'component/notificationBubble';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import { useHistory } from 'react-router';
|
import { useHistory } from 'react-router';
|
||||||
|
import { ENABLE_UI_NOTIFICATIONS } from 'config';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
unseenCount: number,
|
unseenCount: number,
|
||||||
|
@ -21,7 +22,7 @@ export default function NotificationHeaderButton(props: Props) {
|
||||||
doSeeAllNotifications,
|
doSeeAllNotifications,
|
||||||
user,
|
user,
|
||||||
} = props;
|
} = props;
|
||||||
const notificationsEnabled = user && user.experimental_ui;
|
const notificationsEnabled = ENABLE_UI_NOTIFICATIONS || (user && user.experimental_ui);
|
||||||
const { push } = useHistory();
|
const { push } = useHistory();
|
||||||
|
|
||||||
function handleMenuClick() {
|
function handleMenuClick() {
|
||||||
|
|
2
ui/component/optimizedImage/index.js
Normal file
2
ui/component/optimizedImage/index.js
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
import OptimizedImage from './view';
|
||||||
|
export default OptimizedImage;
|
108
ui/component/optimizedImage/view.jsx
Normal file
108
ui/component/optimizedImage/view.jsx
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
// @flow
|
||||||
|
import React from 'react';
|
||||||
|
import { getThumbnailCdnUrl } from 'util/thumbnail';
|
||||||
|
|
||||||
|
function scaleToDevicePixelRatio(value: number, window: any) {
|
||||||
|
const devicePixelRatio = window.devicePixelRatio || 1.0;
|
||||||
|
return Math.ceil(value * devicePixelRatio);
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
src: string,
|
||||||
|
objectFit?: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
function OptimizedImage(props: Props) {
|
||||||
|
const { objectFit, src, ...imgProps } = props;
|
||||||
|
const [optimizedSrc, setOptimizedSrc] = React.useState('');
|
||||||
|
const ref = React.useRef<any>();
|
||||||
|
|
||||||
|
function getOptimizedImgUrl(url, width, height) {
|
||||||
|
let optimizedUrl = url;
|
||||||
|
if (url && !url.startsWith('/public/')) {
|
||||||
|
optimizedUrl = url.trim().replace(/^http:\/\//i, 'https://');
|
||||||
|
|
||||||
|
// @if TARGET='web'
|
||||||
|
if (!optimizedUrl.endsWith('.gif')) {
|
||||||
|
optimizedUrl = getThumbnailCdnUrl({ thumbnail: optimizedUrl, width, height, quality: 85 });
|
||||||
|
}
|
||||||
|
// @endif
|
||||||
|
}
|
||||||
|
return optimizedUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOptimumSize(elem) {
|
||||||
|
if (!elem || !elem.parentElement || !elem.parentElement.clientWidth || !elem.parentElement.clientHeight) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let width = elem.parentElement.clientWidth;
|
||||||
|
let height = elem.parentElement.clientHeight;
|
||||||
|
|
||||||
|
width = scaleToDevicePixelRatio(width, window);
|
||||||
|
height = scaleToDevicePixelRatio(height, window);
|
||||||
|
|
||||||
|
// Round to next 100px for better caching
|
||||||
|
width = Math.ceil(width / 100) * 100;
|
||||||
|
height = Math.ceil(height / 100) * 100;
|
||||||
|
|
||||||
|
// Reminder: CDN expects integers.
|
||||||
|
return { width, height };
|
||||||
|
}
|
||||||
|
|
||||||
|
function adjustOptimizationIfNeeded(elem, objectFit, origSrc) {
|
||||||
|
if (objectFit === 'cover' && elem) {
|
||||||
|
const containerSize = getOptimumSize(elem);
|
||||||
|
if (containerSize) {
|
||||||
|
// $FlowFixMe
|
||||||
|
if (elem.naturalWidth < containerSize.width) {
|
||||||
|
// For 'cover', we don't want to stretch the image. We started off by
|
||||||
|
// filling up the container height, but the width still has a gap for
|
||||||
|
// this instance (usually due to aspect ratio mismatch).
|
||||||
|
// If the original image is much larger, we can request for a larger
|
||||||
|
// image so that "objectFit=cover" will center it without stretching and
|
||||||
|
// making it blur. The double fetch might seem wasteful, but on
|
||||||
|
// average the total transferred bytes is still less than the original.
|
||||||
|
const probablyMaxedOut = elem.naturalHeight < containerSize.height;
|
||||||
|
if (!probablyMaxedOut) {
|
||||||
|
const newOptimizedSrc = getOptimizedImgUrl(origSrc, containerSize.width, 0);
|
||||||
|
if (newOptimizedSrc && newOptimizedSrc !== optimizedSrc) {
|
||||||
|
setOptimizedSrc(newOptimizedSrc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const containerSize = getOptimumSize(ref.current);
|
||||||
|
if (containerSize) {
|
||||||
|
const width = 0; // The CDN will fill the zeroed attribute per image's aspect ratio.
|
||||||
|
const height = containerSize.height;
|
||||||
|
|
||||||
|
const newOptimizedSrc = getOptimizedImgUrl(src, width, height);
|
||||||
|
|
||||||
|
if (newOptimizedSrc !== optimizedSrc) {
|
||||||
|
setOptimizedSrc(newOptimizedSrc);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setOptimizedSrc(src);
|
||||||
|
}
|
||||||
|
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
if (!src) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
ref={ref}
|
||||||
|
{...imgProps}
|
||||||
|
src={optimizedSrc}
|
||||||
|
onLoad={() => adjustOptimizationIfNeeded(ref.current, objectFit, src)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OptimizedImage;
|
|
@ -69,6 +69,7 @@ const RewardsVerifyPage = lazyImport(() => import('page/rewardsVerify' /* webpac
|
||||||
const SearchPage = lazyImport(() => import('page/search' /* webpackChunkName: "secondary" */));
|
const SearchPage = lazyImport(() => import('page/search' /* webpackChunkName: "secondary" */));
|
||||||
const SettingsAdvancedPage = lazyImport(() => import('page/settingsAdvanced' /* webpackChunkName: "secondary" */));
|
const SettingsAdvancedPage = lazyImport(() => import('page/settingsAdvanced' /* webpackChunkName: "secondary" */));
|
||||||
const SettingsStripeCard = lazyImport(() => import('page/settingsStripeCard' /* webpackChunkName: "secondary" */));
|
const SettingsStripeCard = lazyImport(() => import('page/settingsStripeCard' /* webpackChunkName: "secondary" */));
|
||||||
|
const SettingsStripeAccount = lazyImport(() => import('page/settingsStripeAccount' /* webpackChunkName: "secondary" */));
|
||||||
const SettingsCreatorPage = lazyImport(() => import('page/settingsCreator' /* webpackChunkName: "secondary" */));
|
const SettingsCreatorPage = lazyImport(() => import('page/settingsCreator' /* webpackChunkName: "secondary" */));
|
||||||
const SettingsNotificationsPage = lazyImport(() =>
|
const SettingsNotificationsPage = lazyImport(() =>
|
||||||
import('page/settingsNotifications' /* webpackChunkName: "secondary" */)
|
import('page/settingsNotifications' /* webpackChunkName: "secondary" */)
|
||||||
|
@ -292,6 +293,7 @@ function AppRouter(props: Props) {
|
||||||
/>
|
/>
|
||||||
<PrivateRoute {...props} path={`/$/${PAGES.SETTINGS_NOTIFICATIONS}`} component={SettingsNotificationsPage} />
|
<PrivateRoute {...props} path={`/$/${PAGES.SETTINGS_NOTIFICATIONS}`} component={SettingsNotificationsPage} />
|
||||||
<PrivateRoute {...props} path={`/$/${PAGES.SETTINGS_STRIPE_CARD}`} component={SettingsStripeCard} />
|
<PrivateRoute {...props} path={`/$/${PAGES.SETTINGS_STRIPE_CARD}`} component={SettingsStripeCard} />
|
||||||
|
<PrivateRoute {...props} path={`/$/${PAGES.SETTINGS_STRIPE_ACCOUNT}`} component={SettingsStripeAccount} />
|
||||||
<PrivateRoute
|
<PrivateRoute
|
||||||
{...props}
|
{...props}
|
||||||
exact
|
exact
|
||||||
|
|
|
@ -4,6 +4,7 @@ import React, { useState } from 'react';
|
||||||
import { FormField } from 'component/common/form';
|
import { FormField } from 'component/common/form';
|
||||||
import Spinner from 'component/spinner';
|
import Spinner from 'component/spinner';
|
||||||
import SUPPORTED_LANGUAGES from 'constants/supported_languages';
|
import SUPPORTED_LANGUAGES from 'constants/supported_languages';
|
||||||
|
import LANGUAGES from 'constants/languages';
|
||||||
import { getDefaultLanguage, sortLanguageMap } from 'util/default-languages';
|
import { getDefaultLanguage, sortLanguageMap } from 'util/default-languages';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
@ -25,6 +26,13 @@ function SettingLanguage(props: Props) {
|
||||||
const { value } = e.target;
|
const { value } = e.target;
|
||||||
setPreviousLanguage(language || getDefaultLanguage());
|
setPreviousLanguage(language || getDefaultLanguage());
|
||||||
setLanguage(value);
|
setLanguage(value);
|
||||||
|
if (document && document.documentElement) {
|
||||||
|
if (LANGUAGES[value].length >= 3) {
|
||||||
|
document.documentElement.dir = LANGUAGES[value][2];
|
||||||
|
} else {
|
||||||
|
document.documentElement.dir = 'ltr';
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,260 +0,0 @@
|
||||||
// @flow
|
|
||||||
import * as ICONS from 'constants/icons';
|
|
||||||
import React from 'react';
|
|
||||||
import Button from 'component/button';
|
|
||||||
import Card from 'component/common/card';
|
|
||||||
import { Lbryio } from 'lbryinc';
|
|
||||||
import { URL, WEBPACK_WEB_PORT, STRIPE_PUBLIC_KEY } from 'config';
|
|
||||||
|
|
||||||
const isDev = process.env.NODE_ENV !== 'production';
|
|
||||||
|
|
||||||
let stripeEnvironment = 'test';
|
|
||||||
// if the key contains pk_live it's a live key
|
|
||||||
// update the environment for the calls to the backend to indicate which environment to hit
|
|
||||||
if (STRIPE_PUBLIC_KEY.indexOf('pk_live') > -1) {
|
|
||||||
stripeEnvironment = 'live';
|
|
||||||
}
|
|
||||||
|
|
||||||
let successStripeRedirectUrl, failureStripeRedirectUrl;
|
|
||||||
let successEndpoint = '/$/wallet';
|
|
||||||
let failureEndpoint = '/$/wallet';
|
|
||||||
if (isDev) {
|
|
||||||
successStripeRedirectUrl = 'http://localhost:' + WEBPACK_WEB_PORT + successEndpoint;
|
|
||||||
failureStripeRedirectUrl = 'http://localhost:' + WEBPACK_WEB_PORT + failureEndpoint;
|
|
||||||
} else {
|
|
||||||
successStripeRedirectUrl = URL + successEndpoint;
|
|
||||||
failureStripeRedirectUrl = URL + failureEndpoint;
|
|
||||||
}
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
source: string,
|
|
||||||
user: User,
|
|
||||||
};
|
|
||||||
|
|
||||||
type State = {
|
|
||||||
error: boolean,
|
|
||||||
loading: boolean,
|
|
||||||
content: ?string,
|
|
||||||
stripeConnectionUrl: string,
|
|
||||||
// alreadyUpdated: boolean,
|
|
||||||
accountConfirmed: boolean,
|
|
||||||
accountPendingConfirmation: boolean,
|
|
||||||
accountNotConfirmedButReceivedTips: boolean,
|
|
||||||
unpaidBalance: number,
|
|
||||||
};
|
|
||||||
|
|
||||||
class StripeAccountConnection extends React.Component<Props, State> {
|
|
||||||
constructor(props: Props) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
error: false,
|
|
||||||
content: null,
|
|
||||||
loading: true,
|
|
||||||
accountConfirmed: false,
|
|
||||||
accountPendingConfirmation: false,
|
|
||||||
accountNotConfirmedButReceivedTips: false,
|
|
||||||
unpaidBalance: 0,
|
|
||||||
stripeConnectionUrl: '',
|
|
||||||
// alreadyUpdated: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const { user } = this.props;
|
|
||||||
|
|
||||||
// $FlowFixMe
|
|
||||||
this.experimentalUiEnabled = user && user.experimental_ui;
|
|
||||||
|
|
||||||
var that = this;
|
|
||||||
|
|
||||||
function getAndSetAccountLink(stillNeedToConfirmAccount) {
|
|
||||||
Lbryio.call(
|
|
||||||
'account',
|
|
||||||
'link',
|
|
||||||
{
|
|
||||||
return_url: successStripeRedirectUrl,
|
|
||||||
refresh_url: failureStripeRedirectUrl,
|
|
||||||
environment: stripeEnvironment,
|
|
||||||
},
|
|
||||||
'post'
|
|
||||||
).then((accountLinkResponse) => {
|
|
||||||
// stripe link for user to navigate to and confirm account
|
|
||||||
const stripeConnectionUrl = accountLinkResponse.url;
|
|
||||||
|
|
||||||
// set connection url on frontend
|
|
||||||
that.setState({
|
|
||||||
stripeConnectionUrl,
|
|
||||||
});
|
|
||||||
|
|
||||||
// show the account confirmation link if not created already
|
|
||||||
if (stillNeedToConfirmAccount) {
|
|
||||||
that.setState({
|
|
||||||
accountPendingConfirmation: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// call the account status endpoint
|
|
||||||
Lbryio.call(
|
|
||||||
'account',
|
|
||||||
'status',
|
|
||||||
{
|
|
||||||
environment: stripeEnvironment,
|
|
||||||
},
|
|
||||||
'post'
|
|
||||||
)
|
|
||||||
.then((accountStatusResponse) => {
|
|
||||||
const yetToBeCashedOutBalance = accountStatusResponse.total_received_unpaid;
|
|
||||||
if (yetToBeCashedOutBalance) {
|
|
||||||
that.setState({
|
|
||||||
unpaidBalance: yetToBeCashedOutBalance,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// if charges already enabled, no need to generate an account link
|
|
||||||
if (accountStatusResponse.charges_enabled) {
|
|
||||||
// account has already been confirmed
|
|
||||||
|
|
||||||
that.setState({
|
|
||||||
accountConfirmed: true,
|
|
||||||
});
|
|
||||||
// user has not confirmed an account but have received payments
|
|
||||||
} else if (accountStatusResponse.total_received_unpaid > 0) {
|
|
||||||
that.setState({
|
|
||||||
accountNotConfirmedButReceivedTips: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
getAndSetAccountLink();
|
|
||||||
|
|
||||||
// user has not received any amount or confirmed an account
|
|
||||||
} else {
|
|
||||||
// get stripe link and set it on the frontend
|
|
||||||
// pass true so it updates the frontend
|
|
||||||
getAndSetAccountLink(true);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(function (error) {
|
|
||||||
// errorString passed from the API (with a 403 error)
|
|
||||||
const errorString = 'account not linked to user, please link first';
|
|
||||||
|
|
||||||
// if it's beamer's error indicating the account is not linked yet
|
|
||||||
if (error.message.indexOf(errorString) > -1) {
|
|
||||||
// get stripe link and set it on the frontend
|
|
||||||
getAndSetAccountLink();
|
|
||||||
} else {
|
|
||||||
// not an error from Beamer, throw it
|
|
||||||
throw new Error(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
stripeConnectionUrl,
|
|
||||||
accountConfirmed,
|
|
||||||
accountPendingConfirmation,
|
|
||||||
unpaidBalance,
|
|
||||||
accountNotConfirmedButReceivedTips,
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
const { user } = this.props;
|
|
||||||
|
|
||||||
if (user.fiat_enabled) {
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
title={<div className="table__header-text">{__('Connect a bank account')}</div>}
|
|
||||||
isBodyList
|
|
||||||
body={
|
|
||||||
<div>
|
|
||||||
{/* show while waiting for account status */}
|
|
||||||
{!accountConfirmed && !accountPendingConfirmation && !accountNotConfirmedButReceivedTips && (
|
|
||||||
<div className="card__body-actions">
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
<h3>{__('Getting your bank account connection status...')}</h3>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{/* user has yet to complete their integration */}
|
|
||||||
{!accountConfirmed && accountPendingConfirmation && (
|
|
||||||
<div className="card__body-actions">
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
<h3>{__('Connect your bank account to Odysee to receive donations directly from users')}</h3>
|
|
||||||
</div>
|
|
||||||
<div className="section__actions">
|
|
||||||
<a href={stripeConnectionUrl}>
|
|
||||||
<Button button="secondary" label={__('Connect your bank account')} icon={ICONS.FINANCE} />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{/* user has completed their integration */}
|
|
||||||
{accountConfirmed && (
|
|
||||||
<div className="card__body-actions">
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
<h3>{__('Congratulations! Your account has been connected with Odysee.')}</h3>
|
|
||||||
{unpaidBalance > 0 ? (
|
|
||||||
<div>
|
|
||||||
<br />
|
|
||||||
<h3>
|
|
||||||
{__(
|
|
||||||
'Your account balance is %balance% USD. Functionality to view your transactions and withdraw your balance will be landing shortly.',
|
|
||||||
{ balance: unpaidBalance / 100 }
|
|
||||||
)}
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div>
|
|
||||||
<br />
|
|
||||||
<h3>{__('Your account balance is $0 USD. When you receive a tip you will see it here.')}</h3>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{accountNotConfirmedButReceivedTips && (
|
|
||||||
<div className="card__body-actions">
|
|
||||||
<div>
|
|
||||||
<div>
|
|
||||||
<h3>{__('Congratulations, you have already begun receiving tips on Odysee!')}</h3>
|
|
||||||
<div>
|
|
||||||
<br />
|
|
||||||
<h3>
|
|
||||||
{__(
|
|
||||||
'Your pending account balance is %balance% USD. Functionality to view and receive your transactions will land soon.',
|
|
||||||
{ balance: unpaidBalance / 100 }
|
|
||||||
)}
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<br />
|
|
||||||
<div>
|
|
||||||
<h3>
|
|
||||||
{__('Connect your bank account to be able to cash your pending balance out to your account.')}
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div className="section__actions">
|
|
||||||
<a href={stripeConnectionUrl}>
|
|
||||||
<Button button="secondary" label={__('Connect your bank account')} icon={ICONS.FINANCE} />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return <></>; // probably null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default StripeAccountConnection;
|
|
|
@ -6,7 +6,7 @@ import Nag from 'component/common/nag';
|
||||||
import { parseURI } from 'lbry-redux';
|
import { parseURI } from 'lbry-redux';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import Card from 'component/common/card';
|
import Card from 'component/common/card';
|
||||||
import { AUTO_FOLLOW_CHANNELS, SIMPLE_SITE } from 'config';
|
import { AUTO_FOLLOW_CHANNELS, CUSTOM_HOMEPAGE } from 'config';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
subscribedChannels: Array<Subscription>,
|
subscribedChannels: Array<Subscription>,
|
||||||
|
@ -23,7 +23,11 @@ const channelsToSubscribe = AUTO_FOLLOW_CHANNELS.trim()
|
||||||
|
|
||||||
function UserChannelFollowIntro(props: Props) {
|
function UserChannelFollowIntro(props: Props) {
|
||||||
const { subscribedChannels, channelSubscribe, onContinue, onBack, homepageData, prefsReady } = props;
|
const { subscribedChannels, channelSubscribe, onContinue, onBack, homepageData, prefsReady } = props;
|
||||||
const { PRIMARY_CONTENT_CHANNEL_IDS } = homepageData;
|
const { PRIMARY_CONTENT } = homepageData;
|
||||||
|
let channelIds;
|
||||||
|
if (PRIMARY_CONTENT && CUSTOM_HOMEPAGE) {
|
||||||
|
channelIds = PRIMARY_CONTENT.channelIds;
|
||||||
|
}
|
||||||
const followingCount = (subscribedChannels && subscribedChannels.length) || 0;
|
const followingCount = (subscribedChannels && subscribedChannels.length) || 0;
|
||||||
|
|
||||||
// subscribe to lbry
|
// subscribe to lbry
|
||||||
|
@ -62,7 +66,7 @@ function UserChannelFollowIntro(props: Props) {
|
||||||
defaultOrderBy={CS.ORDER_BY_TOP}
|
defaultOrderBy={CS.ORDER_BY_TOP}
|
||||||
defaultFreshness={CS.FRESH_ALL}
|
defaultFreshness={CS.FRESH_ALL}
|
||||||
claimType="channel"
|
claimType="channel"
|
||||||
claimIds={SIMPLE_SITE ? undefined : PRIMARY_CONTENT_CHANNEL_IDS}
|
claimIds={CUSTOM_HOMEPAGE && channelIds ? channelIds : undefined}
|
||||||
defaultTags={followingCount > 3 ? CS.TAGS_FOLLOWED : undefined}
|
defaultTags={followingCount > 3 ? CS.TAGS_FOLLOWED : undefined}
|
||||||
/>
|
/>
|
||||||
{followingCount > 0 && (
|
{followingCount > 0 && (
|
||||||
|
|
|
@ -19,6 +19,7 @@ import YoutubeTransferStatus from 'component/youtubeTransferStatus';
|
||||||
import useFetched from 'effects/use-fetched';
|
import useFetched from 'effects/use-fetched';
|
||||||
import Confetti from 'react-confetti';
|
import Confetti from 'react-confetti';
|
||||||
import usePrevious from 'effects/use-previous';
|
import usePrevious from 'effects/use-previous';
|
||||||
|
import { SHOW_TAGS_INTRO } from 'config';
|
||||||
|
|
||||||
const REDIRECT_PARAM = 'redirect';
|
const REDIRECT_PARAM = 'redirect';
|
||||||
const REDIRECT_IMMEDIATELY_PARAM = 'immediate';
|
const REDIRECT_IMMEDIATELY_PARAM = 'immediate';
|
||||||
|
@ -118,7 +119,7 @@ function UserSignUp(props: Props) {
|
||||||
interestedInYoutubeSync);
|
interestedInYoutubeSync);
|
||||||
const showYoutubeTransfer = hasVerifiedEmail && hasYoutubeChannels && !isYoutubeTransferComplete;
|
const showYoutubeTransfer = hasVerifiedEmail && hasYoutubeChannels && !isYoutubeTransferComplete;
|
||||||
const showFollowIntro = step === 'channels' || (hasVerifiedEmail && !followingAcknowledged);
|
const showFollowIntro = step === 'channels' || (hasVerifiedEmail && !followingAcknowledged);
|
||||||
const showTagsIntro = step === 'tags' || (hasVerifiedEmail && !tagsAcknowledged);
|
const showTagsIntro = SHOW_TAGS_INTRO && (step === 'tags' || (hasVerifiedEmail && !tagsAcknowledged));
|
||||||
const canHijackSignInFlowWithSpinner = hasVerifiedEmail && !showFollowIntro && !showTagsIntro && !rewardsAcknowledged;
|
const canHijackSignInFlowWithSpinner = hasVerifiedEmail && !showFollowIntro && !showTagsIntro && !rewardsAcknowledged;
|
||||||
const isCurrentlyFetchingSomething = fetchingChannels || claimingReward || syncingWallet || creatingChannel;
|
const isCurrentlyFetchingSomething = fetchingChannels || claimingReward || syncingWallet || creatingChannel;
|
||||||
const isWaitingForSomethingToFinish =
|
const isWaitingForSomethingToFinish =
|
||||||
|
|
|
@ -95,10 +95,10 @@ class RecsysPlugin extends Component {
|
||||||
this.player = player;
|
this.player = player;
|
||||||
|
|
||||||
this.recsysEvents = [];
|
this.recsysEvents = [];
|
||||||
|
this.loadedAt = Date.now();
|
||||||
this.lastTimeUpdate = null;
|
this.lastTimeUpdate = null;
|
||||||
this.currentTimeUpdate = null;
|
this.currentTimeUpdate = null;
|
||||||
this.loadedAt = Date.now();
|
this.inPause = false;
|
||||||
this.playInitiated = false;
|
|
||||||
|
|
||||||
// Plugin event listeners
|
// Plugin event listeners
|
||||||
player.on('playing', (event) => this.onPlay(event));
|
player.on('playing', (event) => this.onPlay(event));
|
||||||
|
@ -113,83 +113,13 @@ class RecsysPlugin extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
addRecsysEvent(recsysEvent) {
|
addRecsysEvent(recsysEvent) {
|
||||||
if (!this.playInitiated) {
|
// For now, don't do client-side preprocessing. I think there
|
||||||
switch (recsysEvent.event) {
|
// are browser inconsistencies and preprocessing loses too much info.
|
||||||
case RecsysData.event.start:
|
|
||||||
this.playInitiated = true;
|
|
||||||
break;
|
|
||||||
case RecsysData.event.scrub:
|
|
||||||
// If playback hasn't started, swallow scrub events. They offer some
|
|
||||||
// information, but if there isn't a subsequent play event, it's
|
|
||||||
// mostly nonsensical.
|
|
||||||
return undefined;
|
|
||||||
case RecsysData.event.stop:
|
|
||||||
// If playback hasn't started, swallow stop events. This means
|
|
||||||
// you're going to start from an offset but the start event
|
|
||||||
// captures that information. (With the ambiguity that you can't
|
|
||||||
// tell if they scrubbed, landed at the offset, or restarted. But
|
|
||||||
// I don't think that matters much.)
|
|
||||||
return undefined;
|
|
||||||
case RecsysData.event.speed:
|
|
||||||
if (this.recsysEvents.length > 0 && this.recsysEvents[0].event === RecsysData.event.speed) {
|
|
||||||
// video.js will sometimes fire the default play speed followed by the
|
|
||||||
// user preference. This is not useful information so we can keep the latter.
|
|
||||||
this.recsysEvents.pop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const lastEvent = this.recsysEvents[this.recsysEvents.length - 1];
|
|
||||||
|
|
||||||
switch (recsysEvent.event) {
|
|
||||||
case RecsysData.event.scrub:
|
|
||||||
if (lastEvent.event === RecsysData.event.stop) {
|
|
||||||
// Video.js fires a stop before the seek. This extra information isn't
|
|
||||||
// useful to log though, so this code prunes the stop event if it was
|
|
||||||
// within 0.25 seconds.
|
|
||||||
if (Math.abs(lastEvent.offset - recsysEvent.offset) < 0.25) {
|
|
||||||
this.recsysEvents.pop();
|
|
||||||
recsysEvent.offset = lastEvent.arg;
|
|
||||||
}
|
|
||||||
} else if (lastEvent.event === RecsysData.event.start) {
|
|
||||||
// If the last event was a play and this event is a scrub close to
|
|
||||||
// that play position, I think it's just a weird emit order for
|
|
||||||
// video.js and we don't need to log the scrub.
|
|
||||||
if (Math.abs(lastEvent.offset - recsysEvent.arg) < 0.25) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case RecsysData.event.start:
|
|
||||||
if (lastEvent.event === RecsysData.event.scrub) {
|
|
||||||
// If the last event was a seek and this is a play,
|
|
||||||
// it's reasonable to just implicitly assume the play occurred,
|
|
||||||
// no need to create the play event.
|
|
||||||
return undefined;
|
|
||||||
} else if (lastEvent.event === RecsysData.event.start) {
|
|
||||||
// A start followed by a start is a buffering event.
|
|
||||||
// It may make sense to keep these. A user may abandon
|
|
||||||
// a page *not because it's bad content but because
|
|
||||||
// there are network troubles right now*.
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.recsysEvents.push(recsysEvent);
|
this.recsysEvents.push(recsysEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
getRecsysEvents() {
|
getRecsysEvents() {
|
||||||
return this.recsysEvents.map((event) => {
|
return this.recsysEvents;
|
||||||
if (event !== RecsysData.event.stop) {
|
|
||||||
return event;
|
|
||||||
}
|
|
||||||
|
|
||||||
// I used the arg in stop events to smuggle the seek time into
|
|
||||||
// the scrub events. But the backend doesn't expect it.
|
|
||||||
const dup = { ...event };
|
|
||||||
delete dup.arg;
|
|
||||||
return dup;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sendRecsysEvents() {
|
sendRecsysEvents() {
|
||||||
|
@ -207,16 +137,17 @@ class RecsysPlugin extends Component {
|
||||||
const recsysEvent = newRecsysEvent(RecsysData.event.start, this.player.currentTime());
|
const recsysEvent = newRecsysEvent(RecsysData.event.start, this.player.currentTime());
|
||||||
this.log('onPlay', recsysEvent);
|
this.log('onPlay', recsysEvent);
|
||||||
this.addRecsysEvent(recsysEvent);
|
this.addRecsysEvent(recsysEvent);
|
||||||
|
|
||||||
|
this.inPause = false;
|
||||||
|
this.lastTimeUpdate = recsysEvent.offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
onPause(event) {
|
onPause(event) {
|
||||||
// The API doesn't want an `arg` for `STOP` events. However, video.js
|
const recsysEvent = newRecsysEvent(RecsysData.event.stop, this.player.currentTime());
|
||||||
// emits these before the seek events, and that seems to be the easiest
|
|
||||||
// way to lift time you are seeking from into the scrub record (via lastTimeUpdate).
|
|
||||||
// Hacky, but works.
|
|
||||||
const recsysEvent = newRecsysEvent(RecsysData.event.stop, this.player.currentTime(), this.lastTimeUpdate);
|
|
||||||
this.log('onPause', recsysEvent);
|
this.log('onPause', recsysEvent);
|
||||||
this.addRecsysEvent(recsysEvent);
|
this.addRecsysEvent(recsysEvent);
|
||||||
|
|
||||||
|
this.inPause = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
onEnded(event) {
|
onEnded(event) {
|
||||||
|
@ -226,26 +157,50 @@ class RecsysPlugin extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
onRateChange(event) {
|
onRateChange(event) {
|
||||||
// This is actually a bug. The offset should be the offset. The change speed should be change speed.
|
|
||||||
// Otherise, you don't know where it changed and the time calc is wrong.
|
|
||||||
const recsysEvent = newRecsysEvent(RecsysData.event.speed, this.player.currentTime(), this.player.playbackRate());
|
const recsysEvent = newRecsysEvent(RecsysData.event.speed, this.player.currentTime(), this.player.playbackRate());
|
||||||
this.log('onRateChange', recsysEvent);
|
this.log('onRateChange', recsysEvent);
|
||||||
this.addRecsysEvent(recsysEvent);
|
this.addRecsysEvent(recsysEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
onTimeUpdate(event) {
|
onTimeUpdate(event) {
|
||||||
this.lastTimeUpdate = this.currentTimeUpdate;
|
const nextCurrentTime = this.player.currentTime();
|
||||||
this.currentTimeUpdate = this.player.currentTime();
|
|
||||||
|
if (!this.inPause && Math.abs(this.lastTimeUpdate - nextCurrentTime) < 0.5) {
|
||||||
|
// Don't update lastTimeUpdate if we are in a pause segment.
|
||||||
|
//
|
||||||
|
// However, if we aren't in a pause and the time jumped
|
||||||
|
// the onTimeUpdate event probably fired before the pause and seek.
|
||||||
|
// Don't update in that case, either.
|
||||||
|
this.lastTimeUpdate = this.currentTimeUpdate;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentTimeUpdate = nextCurrentTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
onSeeked(event) {
|
onSeeked(event) {
|
||||||
// The problem? `lastTimeUpdate` is wrong.
|
const curTime = this.player.currentTime();
|
||||||
// So every seeks l
|
|
||||||
|
|
||||||
// If the immediately prior event is a pause?
|
// There are three patterns for seeking:
|
||||||
const recsysEvent = newRecsysEvent(RecsysData.event.scrub, this.lastTimeUpdate, this.player.currentTime());
|
//
|
||||||
this.log('onSeeked', recsysEvent);
|
// Assuming the video is playing,
|
||||||
this.addRecsysEvent(recsysEvent);
|
//
|
||||||
|
// 1. Dragging the player head emits: onPause -> onSeeked -> onSeeked -> ... -> onPlay
|
||||||
|
// 2. Key press left right emits: onSeeked -> onPlay
|
||||||
|
// 3. Clicking a position emits: onPause -> onSeeked -> onPlay
|
||||||
|
//
|
||||||
|
// If the video is NOT playing,
|
||||||
|
//
|
||||||
|
// 1. Dragging the player head emits: onSeeked
|
||||||
|
// 2. Key press left right emits: onSeeked
|
||||||
|
// 3. Clicking a position emits: onSeeked
|
||||||
|
const fromTime = this.lastTimeUpdate;
|
||||||
|
|
||||||
|
if (fromTime !== curTime) {
|
||||||
|
// This removes duplicates that aren't useful.
|
||||||
|
const recsysEvent = newRecsysEvent(RecsysData.event.scrub, fromTime, curTime);
|
||||||
|
this.log('onSeeked', recsysEvent);
|
||||||
|
this.addRecsysEvent(recsysEvent);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onDispose(event) {
|
onDispose(event) {
|
||||||
|
|
|
@ -58,14 +58,14 @@ type Props = {
|
||||||
allowPreRoll: ?boolean,
|
allowPreRoll: ?boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
type VideoJSOptions = {
|
// type VideoJSOptions = {
|
||||||
controls: boolean,
|
// controls: boolean,
|
||||||
preload: string,
|
// preload: string,
|
||||||
playbackRates: Array<number>,
|
// playbackRates: Array<number>,
|
||||||
responsive: boolean,
|
// responsive: boolean,
|
||||||
poster?: string,
|
// poster?: string,
|
||||||
muted?: boolean,
|
// muted?: boolean,
|
||||||
};
|
// };
|
||||||
|
|
||||||
const videoPlaybackRates = [0.25, 0.5, 0.75, 1, 1.1, 1.25, 1.5, 1.75, 2];
|
const videoPlaybackRates = [0.25, 0.5, 0.75, 1, 1.1, 1.25, 1.5, 1.75, 2];
|
||||||
|
|
||||||
|
@ -74,7 +74,7 @@ const IS_IOS =
|
||||||
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1)) &&
|
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1)) &&
|
||||||
!window.MSStream;
|
!window.MSStream;
|
||||||
|
|
||||||
const VIDEO_JS_OPTIONS: VideoJSOptions = {
|
const VIDEO_JS_OPTIONS = {
|
||||||
preload: 'auto',
|
preload: 'auto',
|
||||||
playbackRates: videoPlaybackRates,
|
playbackRates: videoPlaybackRates,
|
||||||
responsive: true,
|
responsive: true,
|
||||||
|
|
|
@ -20,6 +20,8 @@ import { useGetAds } from 'effects/use-get-ads';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import I18nMessage from 'component/i18nMessage';
|
import I18nMessage from 'component/i18nMessage';
|
||||||
import { useHistory } from 'react-router';
|
import { useHistory } from 'react-router';
|
||||||
|
import { getAllIds } from 'util/buildHomepage';
|
||||||
|
import type { HomepageCat } from 'util/buildHomepage';
|
||||||
|
|
||||||
const PLAY_TIMEOUT_ERROR = 'play_timeout_error';
|
const PLAY_TIMEOUT_ERROR = 'play_timeout_error';
|
||||||
const PLAY_TIMEOUT_LIMIT = 2000;
|
const PLAY_TIMEOUT_LIMIT = 2000;
|
||||||
|
@ -48,15 +50,7 @@ type Props = {
|
||||||
setVideoPlaybackRate: (number) => void,
|
setVideoPlaybackRate: (number) => void,
|
||||||
authenticated: boolean,
|
authenticated: boolean,
|
||||||
userId: number,
|
userId: number,
|
||||||
homepageData: {
|
homepageData?: { [string]: HomepageCat },
|
||||||
PRIMARY_CONTENT_CHANNEL_IDS?: Array<string>,
|
|
||||||
ENLIGHTENMENT_CHANNEL_IDS?: Array<string>,
|
|
||||||
GAMING_CHANNEL_IDS?: Array<string>,
|
|
||||||
SCIENCE_CHANNEL_IDS?: Array<string>,
|
|
||||||
TECHNOLOGY_CHANNEL_IDS?: Array<string>,
|
|
||||||
COMMUNITY_CHANNEL_IDS?: Array<string>,
|
|
||||||
FINCANCE_CHANNEL_IDS?: Array<string>,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -91,24 +85,8 @@ function VideoViewer(props: Props) {
|
||||||
authenticated,
|
authenticated,
|
||||||
userId,
|
userId,
|
||||||
} = props;
|
} = props;
|
||||||
const {
|
|
||||||
PRIMARY_CONTENT_CHANNEL_IDS = [],
|
const adApprovedChannelIds = homepageData ? getAllIds(homepageData) : [];
|
||||||
ENLIGHTENMENT_CHANNEL_IDS = [],
|
|
||||||
GAMING_CHANNEL_IDS = [],
|
|
||||||
SCIENCE_CHANNEL_IDS = [],
|
|
||||||
TECHNOLOGY_CHANNEL_IDS = [],
|
|
||||||
COMMUNITY_CHANNEL_IDS = [],
|
|
||||||
FINCANCE_CHANNEL_IDS = [],
|
|
||||||
} = homepageData;
|
|
||||||
const adApprovedChannelIds = [
|
|
||||||
...PRIMARY_CONTENT_CHANNEL_IDS,
|
|
||||||
...ENLIGHTENMENT_CHANNEL_IDS,
|
|
||||||
...GAMING_CHANNEL_IDS,
|
|
||||||
...SCIENCE_CHANNEL_IDS,
|
|
||||||
...TECHNOLOGY_CHANNEL_IDS,
|
|
||||||
...COMMUNITY_CHANNEL_IDS,
|
|
||||||
...FINCANCE_CHANNEL_IDS,
|
|
||||||
];
|
|
||||||
const claimId = claim && claim.claim_id;
|
const claimId = claim && claim.claim_id;
|
||||||
const channelClaimId = claim && claim.signing_channel && claim.signing_channel.claim_id;
|
const channelClaimId = claim && claim.signing_channel && claim.signing_channel.claim_id;
|
||||||
const isAudio = contentType.includes('audio');
|
const isAudio = contentType.includes('audio');
|
||||||
|
|
|
@ -96,10 +96,7 @@ function WalletSendTip(props: Props) {
|
||||||
|
|
||||||
const sourceClaimId = claim.claim_id;
|
const sourceClaimId = claim.claim_id;
|
||||||
|
|
||||||
// TODO: come up with a better way to do this,
|
// check if creator has a payment method saved
|
||||||
// TODO: waiting 100ms to wait for token to populate
|
|
||||||
|
|
||||||
// check if creator has an account saved
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (channelClaimId && isAuthenticated) {
|
if (channelClaimId && isAuthenticated) {
|
||||||
Lbryio.call(
|
Lbryio.call(
|
||||||
|
@ -121,6 +118,12 @@ function WalletSendTip(props: Props) {
|
||||||
}
|
}
|
||||||
}, [channelClaimId, isAuthenticated]);
|
}, [channelClaimId, isAuthenticated]);
|
||||||
|
|
||||||
|
// check if creator has an account saved
|
||||||
|
React.useEffect(() => {
|
||||||
|
var tipInputElement = document.getElementById('tip-input');
|
||||||
|
if (tipInputElement) { tipInputElement.focus() }
|
||||||
|
}, []);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (channelClaimId) {
|
if (channelClaimId) {
|
||||||
Lbryio.call(
|
Lbryio.call(
|
||||||
|
@ -139,7 +142,7 @@ function WalletSendTip(props: Props) {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(function (error) {
|
||||||
console.log(error);
|
// console.log(error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [channelClaimId]);
|
}, [channelClaimId]);
|
||||||
|
@ -147,21 +150,36 @@ function WalletSendTip(props: Props) {
|
||||||
const noBalance = balance === 0;
|
const noBalance = balance === 0;
|
||||||
const tipAmount = useCustomTip ? customTipAmount : presetTipAmount;
|
const tipAmount = useCustomTip ? customTipAmount : presetTipAmount;
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = React.useState(TAB_LBC);
|
const [activeTab, setActiveTab] = React.useState(claimIsMine ? TAB_BOOST : TAB_LBC);
|
||||||
|
|
||||||
|
function setClaimTypeText() {
|
||||||
|
if (claim.value_type === 'stream') {
|
||||||
|
return __('Content');
|
||||||
|
} else if (claim.value_type === 'channel') {
|
||||||
|
return __('Channel');
|
||||||
|
} else if (claim.value_type === 'repost') {
|
||||||
|
return __('Repost');
|
||||||
|
} else if (claim.value_type === 'collection') {
|
||||||
|
return __('List');
|
||||||
|
} else {
|
||||||
|
return __('Claim');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const claimTypeText = setClaimTypeText();
|
||||||
|
|
||||||
let iconToUse, explainerText;
|
let iconToUse, explainerText;
|
||||||
if (activeTab === TAB_BOOST) {
|
if (activeTab === TAB_BOOST) {
|
||||||
iconToUse = ICONS.LBC;
|
iconToUse = ICONS.LBC;
|
||||||
explainerText = __('This refundable boost will improve the discoverability of this content while active.');
|
explainerText = __('This refundable boost will improve the discoverability of this %claimTypeText% while active.', {claimTypeText});
|
||||||
} else if (activeTab === TAB_FIAT) {
|
} else if (activeTab === TAB_FIAT) {
|
||||||
iconToUse = ICONS.FINANCE;
|
iconToUse = ICONS.FINANCE;
|
||||||
explainerText = __('Show this channel your appreciation by sending a donation of cash in USD.');
|
explainerText = __('Show this channel your appreciation by sending a donation in USD. ');
|
||||||
// if (!hasCardSaved) {
|
// if (!hasCardSaved) {
|
||||||
// explainerText += __('You must add a card to use this functionality.');
|
// explainerText += __('You must add a card to use this functionality.');
|
||||||
// }
|
// }
|
||||||
} else if (activeTab === TAB_LBC) {
|
} else if (activeTab === TAB_LBC) {
|
||||||
iconToUse = ICONS.LBC;
|
iconToUse = ICONS.LBC;
|
||||||
explainerText = __('Show this channel your appreciation by sending a donation of Credits.');
|
explainerText = __('Show this channel your appreciation by sending a donation of Credits. ');
|
||||||
}
|
}
|
||||||
|
|
||||||
const isSupport = claimIsMine || activeTab === TAB_BOOST;
|
const isSupport = claimIsMine || activeTab === TAB_BOOST;
|
||||||
|
@ -172,22 +190,34 @@ function WalletSendTip(props: Props) {
|
||||||
const validTipInput = regexp.test(String(tipAmount));
|
const validTipInput = regexp.test(String(tipAmount));
|
||||||
let tipError;
|
let tipError;
|
||||||
|
|
||||||
if (!tipAmount) {
|
if (tipAmount === 0) {
|
||||||
tipError = __('Amount must be a number');
|
|
||||||
} else if (tipAmount <= 0) {
|
|
||||||
tipError = __('Amount must be a positive number');
|
tipError = __('Amount must be a positive number');
|
||||||
} else if (tipAmount < MINIMUM_PUBLISH_BID) {
|
} else if (!tipAmount || typeof tipAmount !== 'number') {
|
||||||
tipError = __('Amount must be higher');
|
tipError = __('Amount must be a number');
|
||||||
} else if (!validTipInput) {
|
}
|
||||||
tipError = __('Amount must have no more than 8 decimal places');
|
|
||||||
} else if (tipAmount === balance) {
|
// if it's not fiat, aka it's boost or lbc tip
|
||||||
tipError = __('Please decrease the amount to account for transaction fees');
|
else if (activeTab !== TAB_FIAT) {
|
||||||
} else if (tipAmount > balance) {
|
if (!validTipInput) {
|
||||||
tipError = __('Not enough Credits');
|
tipError = __('Amount must have no more than 8 decimal places');
|
||||||
|
} else if (tipAmount === balance) {
|
||||||
|
tipError = __('Please decrease the amount to account for transaction fees');
|
||||||
|
} else if (tipAmount > balance) {
|
||||||
|
tipError = __('Not enough Credits');
|
||||||
|
} else if (tipAmount < MINIMUM_PUBLISH_BID) {
|
||||||
|
tipError = __('Amount must be higher');
|
||||||
|
}
|
||||||
|
// if tip fiat tab
|
||||||
|
} else {
|
||||||
|
if (tipAmount < 1) {
|
||||||
|
tipError = __('Amount must be at least one dollar');
|
||||||
|
} else if (tipAmount > 1000) {
|
||||||
|
tipError = __('Amount cannot be over 1000 dollars');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setTipError(tipError);
|
setTipError(tipError);
|
||||||
}, [tipAmount, balance, setTipError]);
|
}, [tipAmount, balance, setTipError, activeTab]);
|
||||||
|
|
||||||
//
|
//
|
||||||
function sendSupportOrConfirm(instantTipMaxAmount = null) {
|
function sendSupportOrConfirm(instantTipMaxAmount = null) {
|
||||||
|
@ -252,11 +282,15 @@ function WalletSendTip(props: Props) {
|
||||||
tipChannelName,
|
tipChannelName,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
console.log(customerTipResponse);
|
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(function(error) {
|
||||||
console.log(error);
|
var displayError = 'Sorry, there was an error in processing your payment!';
|
||||||
doToast({ message: error.message, isError: true });
|
|
||||||
|
if (error.message !== 'payment intent failed to confirm') {
|
||||||
|
displayError = error.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
doToast({ message: displayError, isError: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
closeModal();
|
closeModal();
|
||||||
|
@ -270,6 +304,7 @@ function WalletSendTip(props: Props) {
|
||||||
|
|
||||||
function handleCustomPriceChange(event: SyntheticInputEvent<*>) {
|
function handleCustomPriceChange(event: SyntheticInputEvent<*>) {
|
||||||
const tipAmount = parseFloat(event.target.value);
|
const tipAmount = parseFloat(event.target.value);
|
||||||
|
|
||||||
setCustomTipAmount(tipAmount);
|
setCustomTipAmount(tipAmount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -289,7 +324,7 @@ function WalletSendTip(props: Props) {
|
||||||
const displayAmount = !isNan(tipAmount) ? tipAmount : '';
|
const displayAmount = !isNan(tipAmount) ? tipAmount : '';
|
||||||
|
|
||||||
if (activeTab === TAB_BOOST) {
|
if (activeTab === TAB_BOOST) {
|
||||||
return __('Boost This Content');
|
return (claimIsMine ? __('Boost Your %claimTypeText%', {claimTypeText}) : __('Boost This %claimTypeText%', {claimTypeText}));
|
||||||
} else if (activeTab === TAB_FIAT) {
|
} else if (activeTab === TAB_FIAT) {
|
||||||
return __('Send a $%displayAmount% Tip', { displayAmount });
|
return __('Send a $%displayAmount% Tip', { displayAmount });
|
||||||
} else if (activeTab === TAB_LBC) {
|
} else if (activeTab === TAB_LBC) {
|
||||||
|
@ -341,7 +376,7 @@ function WalletSendTip(props: Props) {
|
||||||
) : (
|
) : (
|
||||||
// if there is lbc, the main tip/boost gui with the 3 tabs at the top
|
// if there is lbc, the main tip/boost gui with the 3 tabs at the top
|
||||||
<Card
|
<Card
|
||||||
title={<LbcSymbol postfix={claimIsMine ? __('Boost your content') : __('Support this content')} size={22} />}
|
title={<LbcSymbol postfix={claimIsMine ? __('Boost Your %claimTypeText%', {claimTypeText}) : __('Support This %claimTypeText%', {claimTypeText})} size={22} />}
|
||||||
subtitle={
|
subtitle={
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{!claimIsMine && (
|
{!claimIsMine && (
|
||||||
|
@ -353,6 +388,8 @@ function WalletSendTip(props: Props) {
|
||||||
label={__('Tip')}
|
label={__('Tip')}
|
||||||
button="alt"
|
button="alt"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
var tipInputElement = document.getElementById('tip-input');
|
||||||
|
if (tipInputElement) { tipInputElement.focus() }
|
||||||
if (!isConfirming) {
|
if (!isConfirming) {
|
||||||
setActiveTab(TAB_LBC);
|
setActiveTab(TAB_LBC);
|
||||||
}
|
}
|
||||||
|
@ -367,6 +404,8 @@ function WalletSendTip(props: Props) {
|
||||||
label={__('Tip')}
|
label={__('Tip')}
|
||||||
button="alt"
|
button="alt"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
var tipInputElement = document.getElementById('tip-input');
|
||||||
|
if (tipInputElement) { tipInputElement.focus() }
|
||||||
if (!isConfirming) {
|
if (!isConfirming) {
|
||||||
setActiveTab(TAB_FIAT);
|
setActiveTab(TAB_FIAT);
|
||||||
}
|
}
|
||||||
|
@ -381,6 +420,8 @@ function WalletSendTip(props: Props) {
|
||||||
label={__('Boost')}
|
label={__('Boost')}
|
||||||
button="alt"
|
button="alt"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
var tipInputElement = document.getElementById('tip-input');
|
||||||
|
if (tipInputElement) { tipInputElement.focus() }
|
||||||
if (!isConfirming) {
|
if (!isConfirming) {
|
||||||
setActiveTab(TAB_BOOST);
|
setActiveTab(TAB_BOOST);
|
||||||
}
|
}
|
||||||
|
@ -435,8 +476,8 @@ function WalletSendTip(props: Props) {
|
||||||
|
|
||||||
{activeTab === TAB_FIAT && !hasCardSaved && (
|
{activeTab === TAB_FIAT && !hasCardSaved && (
|
||||||
<h3 className="add-card-prompt">
|
<h3 className="add-card-prompt">
|
||||||
<Button navigate={`/$/${PAGES.SETTINGS_STRIPE_CARD}`} label={__('Add a Card')} button="link" /> To
|
<Button navigate={`/$/${PAGES.SETTINGS_STRIPE_CARD}`} label={__('Add a Card')} button="link" />
|
||||||
{__('Tip Creators')}
|
{' '}{__('To Tip Creators')}
|
||||||
</h3>
|
</h3>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@ -468,6 +509,7 @@ function WalletSendTip(props: Props) {
|
||||||
icon={iconToUse}
|
icon={iconToUse}
|
||||||
label={__('Custom')}
|
label={__('Custom')}
|
||||||
onClick={() => setUseCustomTip(true)}
|
onClick={() => setUseCustomTip(true)}
|
||||||
|
// disabled if it's receive fiat and there is no card or creator can't receive tips
|
||||||
disabled={activeTab === TAB_FIAT && (!hasCardSaved || !canReceiveFiatTip)}
|
disabled={activeTab === TAB_FIAT && (!hasCardSaved || !canReceiveFiatTip)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { connect } from 'react-redux';
|
||||||
import { selectBalance } from 'lbry-redux';
|
import { selectBalance } from 'lbry-redux';
|
||||||
import WalletSpendableBalanceHelp from './view';
|
import WalletSpendableBalanceHelp from './view';
|
||||||
|
|
||||||
const select = (state, props) => ({
|
const select = (state) => ({
|
||||||
balance: selectBalance(state),
|
balance: selectBalance(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { selectBalance } from 'lbry-redux';
|
import { selectBalance } from 'lbry-redux';
|
||||||
import WalletTipAmountSelector from './view';
|
import WalletTipAmountSelector from './view';
|
||||||
|
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||||
|
|
||||||
const select = (state, props) => ({
|
const select = (state, props) => ({
|
||||||
balance: selectBalance(state),
|
balance: selectBalance(state),
|
||||||
|
isAuthenticated: Boolean(selectUserVerifiedEmail(state)),
|
||||||
|
// claim: makeSelectClaimForUri(props.uri)(state),
|
||||||
|
// claim: makeSelectClaimForUri(props.uri, false)(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(select)(WalletTipAmountSelector);
|
export default connect(select)(WalletTipAmountSelector);
|
||||||
|
|
|
@ -10,40 +10,149 @@ import I18nMessage from 'component/i18nMessage';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import usePersistedState from 'effects/use-persisted-state';
|
import usePersistedState from 'effects/use-persisted-state';
|
||||||
import WalletSpendableBalanceHelp from 'component/walletSpendableBalanceHelp';
|
import WalletSpendableBalanceHelp from 'component/walletSpendableBalanceHelp';
|
||||||
|
import { Lbryio } from 'lbryinc';
|
||||||
|
import { STRIPE_PUBLIC_KEY } from 'config';
|
||||||
|
|
||||||
|
let stripeEnvironment = 'test';
|
||||||
|
// if the key contains pk_live it's a live key
|
||||||
|
// update the environment for the calls to the backend to indicate which environment to hit
|
||||||
|
if (STRIPE_PUBLIC_KEY.indexOf('pk_live') > -1) {
|
||||||
|
stripeEnvironment = 'live';
|
||||||
|
}
|
||||||
|
|
||||||
const DEFAULT_TIP_AMOUNTS = [1, 5, 25, 100];
|
const DEFAULT_TIP_AMOUNTS = [1, 5, 25, 100];
|
||||||
|
|
||||||
|
const TAB_FIAT = 'TabFiat';
|
||||||
|
const TAB_LBC = 'TabLBC';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
balance: number,
|
balance: number,
|
||||||
amount: number,
|
amount: number,
|
||||||
onChange: (number) => void,
|
onChange: (number) => void,
|
||||||
|
isAuthenticated: boolean,
|
||||||
|
claim: StreamClaim,
|
||||||
|
uri: string,
|
||||||
|
onTipErrorChange: (string) => void,
|
||||||
|
activeTab: string,
|
||||||
|
shouldDisableReviewButton: (boolean) => void
|
||||||
};
|
};
|
||||||
|
|
||||||
function WalletTipAmountSelector(props: Props) {
|
function WalletTipAmountSelector(props: Props) {
|
||||||
const { balance, amount, onChange } = props;
|
const { balance, amount, onChange, activeTab, claim, onTipErrorChange, shouldDisableReviewButton } = props;
|
||||||
const [useCustomTip, setUseCustomTip] = usePersistedState('comment-support:useCustomTip', false);
|
const [useCustomTip, setUseCustomTip] = usePersistedState('comment-support:useCustomTip', false);
|
||||||
const [tipError, setTipError] = React.useState();
|
const [tipError, setTipError] = React.useState();
|
||||||
|
|
||||||
|
const [canReceiveFiatTip, setCanReceiveFiatTip] = React.useState(); // dont persist because it needs to be calc'd per creator
|
||||||
|
const [hasCardSaved, setHasSavedCard] = usePersistedState('comment-support:hasCardSaved', false);
|
||||||
|
|
||||||
|
// if it's fiat but there's no card saved OR the creator can't receive fiat tips
|
||||||
|
const shouldDisableFiatSelectors = (activeTab === TAB_FIAT && (!hasCardSaved || !canReceiveFiatTip));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* whether tip amount selection/review functionality should be disabled
|
||||||
|
* @param [amount] LBC amount (optional)
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
function shouldDisableAmountSelector(amount) {
|
||||||
|
// if it's LBC but the balance isn't enough, or fiat conditions met
|
||||||
|
// $FlowFixMe
|
||||||
|
return (amount > balance && activeTab !== TAB_FIAT) || shouldDisableFiatSelectors;
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldDisableReviewButton(shouldDisableFiatSelectors);
|
||||||
|
|
||||||
|
// setup variables for tip API
|
||||||
|
let channelClaimId, tipChannelName;
|
||||||
|
// if there is a signing channel it's on a file
|
||||||
|
if (claim.signing_channel) {
|
||||||
|
channelClaimId = claim.signing_channel.claim_id;
|
||||||
|
tipChannelName = claim.signing_channel.name;
|
||||||
|
|
||||||
|
// otherwise it's on the channel page
|
||||||
|
} else {
|
||||||
|
channelClaimId = claim.claim_id;
|
||||||
|
tipChannelName = claim.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if creator has a payment method saved
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
Lbryio.call(
|
||||||
|
'customer',
|
||||||
|
'status',
|
||||||
|
{
|
||||||
|
environment: stripeEnvironment,
|
||||||
|
},
|
||||||
|
'post'
|
||||||
|
).then((customerStatusResponse) => {
|
||||||
|
const defaultPaymentMethodId =
|
||||||
|
customerStatusResponse.Customer &&
|
||||||
|
customerStatusResponse.Customer.invoice_settings &&
|
||||||
|
customerStatusResponse.Customer.invoice_settings.default_payment_method &&
|
||||||
|
customerStatusResponse.Customer.invoice_settings.default_payment_method.id;
|
||||||
|
|
||||||
|
setHasSavedCard(Boolean(defaultPaymentMethodId));
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
//
|
||||||
|
React.useEffect(() => {
|
||||||
|
Lbryio.call(
|
||||||
|
'account',
|
||||||
|
'check',
|
||||||
|
{
|
||||||
|
channel_claim_id: channelClaimId,
|
||||||
|
channel_name: tipChannelName,
|
||||||
|
environment: stripeEnvironment,
|
||||||
|
},
|
||||||
|
'post'
|
||||||
|
)
|
||||||
|
.then((accountCheckResponse) => {
|
||||||
|
if (accountCheckResponse === true && canReceiveFiatTip !== true) {
|
||||||
|
setCanReceiveFiatTip(true);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
// console.log(error);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
// setHasSavedCard(false);
|
||||||
|
// setCanReceiveFiatTip(true);
|
||||||
|
|
||||||
const regexp = RegExp(/^(\d*([.]\d{0,8})?)$/);
|
const regexp = RegExp(/^(\d*([.]\d{0,8})?)$/);
|
||||||
const validTipInput = regexp.test(String(amount));
|
const validTipInput = regexp.test(String(amount));
|
||||||
let tipError;
|
let tipError = '';
|
||||||
|
|
||||||
if (!amount) {
|
if (amount === 0) {
|
||||||
tipError = __('Amount must be a number');
|
|
||||||
} else if (amount <= 0) {
|
|
||||||
tipError = __('Amount must be a positive number');
|
tipError = __('Amount must be a positive number');
|
||||||
} else if (amount < MINIMUM_PUBLISH_BID) {
|
} else if (!amount || typeof amount !== 'number') {
|
||||||
tipError = __('Amount must be higher');
|
tipError = __('Amount must be a number');
|
||||||
} else if (!validTipInput) {
|
|
||||||
tipError = __('Amount must have no more than 8 decimal places');
|
|
||||||
} else if (amount === balance) {
|
|
||||||
tipError = __('Please decrease the amount to account for transaction fees');
|
|
||||||
} else if (amount > balance) {
|
|
||||||
tipError = __('Not enough Credits');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if it's not fiat, aka it's boost or lbc tip
|
||||||
|
else if (activeTab !== TAB_FIAT) {
|
||||||
|
if (!validTipInput) {
|
||||||
|
tipError = __('Amount must have no more than 8 decimal places');
|
||||||
|
} else if (amount === balance) {
|
||||||
|
tipError = __('Please decrease the amount to account for transaction fees');
|
||||||
|
} else if (amount > balance) {
|
||||||
|
tipError = __('Not enough Credits');
|
||||||
|
} else if (amount < MINIMUM_PUBLISH_BID) {
|
||||||
|
tipError = __('Amount must be higher');
|
||||||
|
}
|
||||||
|
// if tip fiat tab
|
||||||
|
} else {
|
||||||
|
if (amount < 1) {
|
||||||
|
tipError = __('Amount must be at least one dollar');
|
||||||
|
} else if (amount > 1000) {
|
||||||
|
tipError = __('Amount cannot be over 1000 dollars');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setTipError(tipError);
|
setTipError(tipError);
|
||||||
}, [amount, balance, setTipError]);
|
onTipErrorChange(tipError);
|
||||||
|
}, [amount, balance, setTipError, activeTab]);
|
||||||
|
|
||||||
function handleCustomPriceChange(amount: number) {
|
function handleCustomPriceChange(amount: number) {
|
||||||
const tipAmount = parseFloat(amount);
|
const tipAmount = parseFloat(amount);
|
||||||
|
@ -56,14 +165,14 @@ function WalletTipAmountSelector(props: Props) {
|
||||||
{DEFAULT_TIP_AMOUNTS.map((defaultAmount) => (
|
{DEFAULT_TIP_AMOUNTS.map((defaultAmount) => (
|
||||||
<Button
|
<Button
|
||||||
key={defaultAmount}
|
key={defaultAmount}
|
||||||
disabled={amount > balance}
|
disabled={shouldDisableAmountSelector(defaultAmount)}
|
||||||
button="alt"
|
button="alt"
|
||||||
className={classnames('button-toggle button-toggle--expandformobile', {
|
className={classnames('button-toggle button-toggle--expandformobile', {
|
||||||
'button-toggle--active': defaultAmount === amount,
|
'button-toggle--active': defaultAmount === amount && !useCustomTip,
|
||||||
'button-toggle--disabled': amount > balance,
|
'button-toggle--disabled': amount > balance,
|
||||||
})}
|
})}
|
||||||
label={defaultAmount}
|
label={defaultAmount}
|
||||||
icon={ICONS.LBC}
|
icon={activeTab === TAB_LBC ? ICONS.LBC : ICONS.FINANCE}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
handleCustomPriceChange(defaultAmount);
|
handleCustomPriceChange(defaultAmount);
|
||||||
setUseCustomTip(false);
|
setUseCustomTip(false);
|
||||||
|
@ -72,14 +181,15 @@ function WalletTipAmountSelector(props: Props) {
|
||||||
))}
|
))}
|
||||||
<Button
|
<Button
|
||||||
button="alt"
|
button="alt"
|
||||||
|
disabled={activeTab === TAB_FIAT && (!hasCardSaved || !canReceiveFiatTip)}
|
||||||
className={classnames('button-toggle button-toggle--expandformobile', {
|
className={classnames('button-toggle button-toggle--expandformobile', {
|
||||||
'button-toggle--active': !DEFAULT_TIP_AMOUNTS.includes(amount),
|
'button-toggle--active': useCustomTip,
|
||||||
})}
|
})}
|
||||||
icon={ICONS.LBC}
|
icon={activeTab === TAB_LBC ? ICONS.LBC : ICONS.FINANCE}
|
||||||
label={__('Custom')}
|
label={__('Custom')}
|
||||||
onClick={() => setUseCustomTip(true)}
|
onClick={() => setUseCustomTip(true)}
|
||||||
/>
|
/>
|
||||||
{DEFAULT_TIP_AMOUNTS.some((val) => val > balance) && (
|
{activeTab === TAB_LBC && DEFAULT_TIP_AMOUNTS.some((val) => val > balance) && (
|
||||||
<Button
|
<Button
|
||||||
button="secondary"
|
button="secondary"
|
||||||
className="button-toggle-group-action"
|
className="button-toggle-group-action"
|
||||||
|
@ -90,18 +200,58 @@ function WalletTipAmountSelector(props: Props) {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{useCustomTip && activeTab === TAB_FIAT && !hasCardSaved && (
|
||||||
|
<>
|
||||||
|
<div className="help">
|
||||||
|
<span className="help--spendable">
|
||||||
|
<Button navigate={`/$/${PAGES.SETTINGS_STRIPE_CARD}`} label={__('Add a Card ')} button="link" /> To{' '}
|
||||||
|
{__(' Tip Creators')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* has card saved but cant creator cant receive tips */}
|
||||||
|
{useCustomTip && activeTab === TAB_FIAT && hasCardSaved && !canReceiveFiatTip && (
|
||||||
|
<>
|
||||||
|
<div className="help">
|
||||||
|
<span className="help--spendable">Only select creators can receive tips at this time</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* has card saved but cant creator cant receive tips */}
|
||||||
|
{useCustomTip && activeTab === TAB_FIAT && hasCardSaved && canReceiveFiatTip && (
|
||||||
|
<>
|
||||||
|
<div className="help">
|
||||||
|
<span className="help--spendable">Send a tip directly from your attached card</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{useCustomTip && (
|
{useCustomTip && (
|
||||||
<div className="comment__tip-input">
|
<div className="comment__tip-input">
|
||||||
<FormField
|
<FormField
|
||||||
autoFocus
|
autoFocus
|
||||||
name="tip-input"
|
name="tip-input"
|
||||||
|
disabled={shouldDisableAmountSelector()}
|
||||||
label={
|
label={
|
||||||
<React.Fragment>
|
activeTab === TAB_LBC ? (
|
||||||
{__('Custom support amount')}{' '}
|
<React.Fragment>
|
||||||
<I18nMessage tokens={{ lbc_balance: <CreditAmount precision={4} amount={balance} /> }}>
|
{__('Custom support amount')}{' '}
|
||||||
(%lbc_balance% available)
|
<I18nMessage tokens={{ lbc_balance: <CreditAmount precision={4} amount={balance} /> }}>
|
||||||
</I18nMessage>
|
(%lbc_balance% available)
|
||||||
</React.Fragment>
|
</I18nMessage>
|
||||||
|
</React.Fragment>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)
|
||||||
|
|
||||||
|
// <>
|
||||||
|
// <div className="">
|
||||||
|
// <span className="help--spendable">Send a tip directly from your attached card</span>
|
||||||
|
// </div>
|
||||||
|
// </>
|
||||||
}
|
}
|
||||||
className="form-field--price-amount"
|
className="form-field--price-amount"
|
||||||
error={tipError}
|
error={tipError}
|
||||||
|
@ -115,7 +265,37 @@ function WalletTipAmountSelector(props: Props) {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!useCustomTip && <WalletSpendableBalanceHelp />}
|
{/* lbc tab */}
|
||||||
|
{activeTab === TAB_LBC && <WalletSpendableBalanceHelp />}
|
||||||
|
{/* fiat button but no card saved */}
|
||||||
|
{!useCustomTip && activeTab === TAB_FIAT && !hasCardSaved && (
|
||||||
|
<>
|
||||||
|
<div className="help">
|
||||||
|
<span className="help--spendable">
|
||||||
|
<Button navigate={`/$/${PAGES.SETTINGS_STRIPE_CARD}`} label={__('Add a Card ')} button="link" /> To{' '}
|
||||||
|
{__(' Tip Creators')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* has card saved but cant creator cant receive tips */}
|
||||||
|
{!useCustomTip && activeTab === TAB_FIAT && hasCardSaved && !canReceiveFiatTip && (
|
||||||
|
<>
|
||||||
|
<div className="help">
|
||||||
|
<span className="help--spendable">Only select creators can receive tips at this time</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* has card saved but cant creator cant receive tips */}
|
||||||
|
{!useCustomTip && activeTab === TAB_FIAT && hasCardSaved && canReceiveFiatTip && (
|
||||||
|
<>
|
||||||
|
<div className="help">
|
||||||
|
<span className="help--spendable">Send a tip directly from your attached card</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,8 +20,8 @@ export default function WebUploadList(props: Props) {
|
||||||
return (
|
return (
|
||||||
!!uploadCount && (
|
!!uploadCount && (
|
||||||
<Card
|
<Card
|
||||||
title={__('Currently uploading')}
|
title={__('Currently Uploading')}
|
||||||
subtitle={uploadCount > 1 ? __('You files are currently uploading.') : __('Your file is currently uploading.')}
|
subtitle={__('Leave the app running until upload is complete')}
|
||||||
body={
|
body={
|
||||||
<section>
|
<section>
|
||||||
{/* $FlowFixMe */}
|
{/* $FlowFixMe */}
|
||||||
|
|
|
@ -118,9 +118,7 @@ export default function YoutubeTransferStatus(props: Props) {
|
||||||
{isNotElligible && (
|
{isNotElligible && (
|
||||||
<I18nMessage
|
<I18nMessage
|
||||||
tokens={{
|
tokens={{
|
||||||
here: (
|
here: <Button button="link" href="https://lbry.com/faq/youtube" label={__('here')} />,
|
||||||
<Button button="link" href="https://lbry.com/faq/youtube" label={__('here')} />
|
|
||||||
),
|
|
||||||
email: SITE_HELP_EMAIL,
|
email: SITE_HELP_EMAIL,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
export const FF_MAX_CHARS_DEFAULT = 2000;
|
export const FF_MAX_CHARS_DEFAULT = 2000;
|
||||||
export const FF_MAX_CHARS_IN_COMMENT = 2000;
|
export const FF_MAX_CHARS_IN_COMMENT = 2000;
|
||||||
export const FF_MAX_CHARS_IN_LIVESTREAM_COMMENT = 500;
|
export const FF_MAX_CHARS_IN_LIVESTREAM_COMMENT = 300;
|
||||||
export const FF_MAX_CHARS_IN_DESCRIPTION = 5000;
|
export const FF_MAX_CHARS_IN_DESCRIPTION = 5000;
|
||||||
export const FF_MAX_CHARS_REPORT_CONTENT_DETAILS = 500;
|
export const FF_MAX_CHARS_REPORT_CONTENT_DETAILS = 500;
|
||||||
export const FF_MAX_CHARS_REPORT_CONTENT_ADDRESS = 255;
|
export const FF_MAX_CHARS_REPORT_CONTENT_ADDRESS = 255;
|
||||||
|
|
|
@ -52,6 +52,7 @@ export const LINKEDIN = 'LinkedIn';
|
||||||
export const EMBED = 'Embed';
|
export const EMBED = 'Embed';
|
||||||
export const MORE = 'More';
|
export const MORE = 'More';
|
||||||
export const SHARE_LINK = 'ShareLink';
|
export const SHARE_LINK = 'ShareLink';
|
||||||
|
export const COPY_LINK = 'CopyLink';
|
||||||
export const ACCOUNT = 'User';
|
export const ACCOUNT = 'User';
|
||||||
export const SETTINGS = 'Settings';
|
export const SETTINGS = 'Settings';
|
||||||
export const FILTER = 'Filter';
|
export const FILTER = 'Filter';
|
||||||
|
|
|
@ -5,7 +5,7 @@ const LANGUAGES = {
|
||||||
ak: ['Akan', 'Akana'],
|
ak: ['Akan', 'Akana'],
|
||||||
am: ['Amharic', 'አማርኛ'],
|
am: ['Amharic', 'አማርኛ'],
|
||||||
an: ['Aragonese', 'Aragonés'],
|
an: ['Aragonese', 'Aragonés'],
|
||||||
ar: ['Arabic', 'العربية'],
|
ar: ['Arabic', 'العربية', 'rtl'],
|
||||||
as: ['Assamese', 'অসমীয়া'],
|
as: ['Assamese', 'অসমীয়া'],
|
||||||
av: ['Avar', 'Авар'],
|
av: ['Avar', 'Авар'],
|
||||||
ay: ['Aymara', 'Aymar'],
|
ay: ['Aymara', 'Aymar'],
|
||||||
|
@ -40,7 +40,7 @@ const LANGUAGES = {
|
||||||
es: ['Spanish', 'Español'],
|
es: ['Spanish', 'Español'],
|
||||||
et: ['Estonian', 'Eesti'],
|
et: ['Estonian', 'Eesti'],
|
||||||
eu: ['Basque', 'Euskara'],
|
eu: ['Basque', 'Euskara'],
|
||||||
fa: ['Persian', 'فارسی'],
|
fa: ['Persian', 'فارسی', 'rtl'],
|
||||||
ff: ['Peul', 'Fulfulde'],
|
ff: ['Peul', 'Fulfulde'],
|
||||||
fi: ['Finnish', 'Suomi'],
|
fi: ['Finnish', 'Suomi'],
|
||||||
fil: ['Filipino', 'Filipino'],
|
fil: ['Filipino', 'Filipino'],
|
||||||
|
@ -55,7 +55,7 @@ const LANGUAGES = {
|
||||||
gu: ['Gujarati', 'ગુજરાતી'],
|
gu: ['Gujarati', 'ગુજરાતી'],
|
||||||
gv: ['Manx', 'Gaelg'],
|
gv: ['Manx', 'Gaelg'],
|
||||||
ha: ['Hausa', 'هَوُسَ'],
|
ha: ['Hausa', 'هَوُسَ'],
|
||||||
he: ['Hebrew', 'עברית'],
|
he: ['Hebrew', 'עברית', 'rtl'],
|
||||||
hi: ['Hindi', 'हिन्दी'],
|
hi: ['Hindi', 'हिन्दी'],
|
||||||
ho: ['Hiri Motu', 'Hiri Motu'],
|
ho: ['Hiri Motu', 'Hiri Motu'],
|
||||||
hr: ['Croatian', 'Hrvatski'],
|
hr: ['Croatian', 'Hrvatski'],
|
||||||
|
@ -171,7 +171,7 @@ const LANGUAGES = {
|
||||||
ty: ['Tahitian', 'Reo Mā`ohi'],
|
ty: ['Tahitian', 'Reo Mā`ohi'],
|
||||||
ug: ['Uyghur', 'Uyƣurqə / ئۇيغۇرچە'],
|
ug: ['Uyghur', 'Uyƣurqə / ئۇيغۇرچە'],
|
||||||
uk: ['Ukrainian', 'Українська'],
|
uk: ['Ukrainian', 'Українська'],
|
||||||
ur: ['Urdu', 'اردو'],
|
ur: ['Urdu', 'اردو', 'rtl'],
|
||||||
uz: ['Uzbek', 'Ўзбек'],
|
uz: ['Uzbek', 'Ўзбек'],
|
||||||
ve: ['Venda', 'Tshivenḓa'],
|
ve: ['Venda', 'Tshivenḓa'],
|
||||||
vi: ['Vietnamese', 'Tiếng Việt'],
|
vi: ['Vietnamese', 'Tiếng Việt'],
|
||||||
|
|
|
@ -45,3 +45,4 @@ export const VIEW_IMAGE = 'view_image';
|
||||||
export const CONFIRM_REMOVE_BTC_SWAP_ADDRESS = 'confirm_remove_btc_swap_address';
|
export const CONFIRM_REMOVE_BTC_SWAP_ADDRESS = 'confirm_remove_btc_swap_address';
|
||||||
export const COLLECTION_ADD = 'collection_add';
|
export const COLLECTION_ADD = 'collection_add';
|
||||||
export const COLLECTION_DELETE = 'collection_delete';
|
export const COLLECTION_DELETE = 'collection_delete';
|
||||||
|
export const CONFIRM_REMOVE_CARD = 'CONFIRM_REMOVE_CARD';
|
||||||
|
|
|
@ -39,6 +39,7 @@ exports.REPOST_NEW = 'repost';
|
||||||
exports.SEND = 'send';
|
exports.SEND = 'send';
|
||||||
exports.SETTINGS = 'settings';
|
exports.SETTINGS = 'settings';
|
||||||
exports.SETTINGS_STRIPE_CARD = 'settings/card';
|
exports.SETTINGS_STRIPE_CARD = 'settings/card';
|
||||||
|
exports.SETTINGS_STRIPE_ACCOUNT = 'settings/tip_account';
|
||||||
exports.SETTINGS_NOTIFICATIONS = 'settings/notifications';
|
exports.SETTINGS_NOTIFICATIONS = 'settings/notifications';
|
||||||
exports.SETTINGS_ADVANCED = 'settings/advanced';
|
exports.SETTINGS_ADVANCED = 'settings/advanced';
|
||||||
exports.SETTINGS_BLOCKED_MUTED = 'settings/block_and_mute';
|
exports.SETTINGS_BLOCKED_MUTED = 'settings/block_and_mute';
|
||||||
|
|
|
@ -40,6 +40,7 @@ const SUPPORTED_LANGUAGES = {
|
||||||
kn: LANGUAGES.kn[1],
|
kn: LANGUAGES.kn[1],
|
||||||
uk: LANGUAGES.uk[1],
|
uk: LANGUAGES.uk[1],
|
||||||
vi: LANGUAGES.vi[1],
|
vi: LANGUAGES.vi[1],
|
||||||
|
ar: LANGUAGES.ar[1],
|
||||||
};
|
};
|
||||||
|
|
||||||
// Properties: language code (e.g. 'ja')
|
// Properties: language code (e.g. 'ja')
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { Modal } from 'modal/modal';
|
||||||
import Card from 'component/common/card';
|
import Card from 'component/common/card';
|
||||||
import I18nMessage from 'component/i18nMessage';
|
import I18nMessage from 'component/i18nMessage';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
|
import { isURIEqual } from 'lbry-redux';
|
||||||
|
|
||||||
// This number is tied to transitions in scss/purchase.scss
|
// This number is tied to transitions in scss/purchase.scss
|
||||||
const ANIMATION_LENGTH = 2500;
|
const ANIMATION_LENGTH = 2500;
|
||||||
|
@ -16,7 +17,7 @@ type Props = {
|
||||||
uri: string,
|
uri: string,
|
||||||
cancelPurchase: () => void,
|
cancelPurchase: () => void,
|
||||||
metadata: StreamMetadata,
|
metadata: StreamMetadata,
|
||||||
analyticsPurchaseEvent: GetResponse => void,
|
analyticsPurchaseEvent: (GetResponse) => void,
|
||||||
playingUri: ?PlayingUri,
|
playingUri: ?PlayingUri,
|
||||||
setPlayingUri: (?string) => void,
|
setPlayingUri: (?string) => void,
|
||||||
};
|
};
|
||||||
|
@ -37,7 +38,7 @@ function ModalAffirmPurchase(props: Props) {
|
||||||
|
|
||||||
function onAffirmPurchase() {
|
function onAffirmPurchase() {
|
||||||
setPurchasing(true);
|
setPurchasing(true);
|
||||||
loadVideo(uri, fileInfo => {
|
loadVideo(uri, (fileInfo) => {
|
||||||
setPurchasing(false);
|
setPurchasing(false);
|
||||||
setSuccess(true);
|
setSuccess(true);
|
||||||
analyticsPurchaseEvent(fileInfo);
|
analyticsPurchaseEvent(fileInfo);
|
||||||
|
@ -49,7 +50,7 @@ function ModalAffirmPurchase(props: Props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancelPurchase() {
|
function cancelPurchase() {
|
||||||
if (playingUri && uri === playingUri.uri) {
|
if (playingUri && isURIEqual(uri, playingUri.uri)) {
|
||||||
setPlayingUri(null);
|
setPlayingUri(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -44,7 +44,7 @@ class ModalPublishSuccess extends React.PureComponent<Props> {
|
||||||
'Your livestream is now pending. You will be able to start shortly at the streaming dashboard.'
|
'Your livestream is now pending. You will be able to start shortly at the streaming dashboard.'
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
publishMessage = __('Your file is now pending on LBRY. It will take a few minutes to appear for other users.');
|
publishMessage = __('Your content will be live shortly.');
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleClose() {
|
function handleClose() {
|
||||||
|
@ -54,7 +54,7 @@ class ModalPublishSuccess extends React.PureComponent<Props> {
|
||||||
return (
|
return (
|
||||||
<Modal isOpen type="card" contentLabel={__(contentLabel)} onAborted={handleClose}>
|
<Modal isOpen type="card" contentLabel={__(contentLabel)} onAborted={handleClose}>
|
||||||
<Card
|
<Card
|
||||||
title={__('Success')}
|
title={livestream ? __('Livestream Created') : __('Success')}
|
||||||
subtitle={publishMessage}
|
subtitle={publishMessage}
|
||||||
body={
|
body={
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
|
|
19
ui/modal/modalRemoveCard/index.js
Normal file
19
ui/modal/modalRemoveCard/index.js
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { doHideModal } from 'redux/actions/app';
|
||||||
|
import { doAbandonTxo, doAbandonClaim, selectTransactionItems, doResolveUri } from 'lbry-redux';
|
||||||
|
import { doToast } from 'redux/actions/notifications';
|
||||||
|
import ModalRevokeClaim from './view';
|
||||||
|
|
||||||
|
const select = state => ({
|
||||||
|
transactionItems: selectTransactionItems(state),
|
||||||
|
});
|
||||||
|
|
||||||
|
const perform = dispatch => ({
|
||||||
|
toast: (message, isError) => dispatch(doToast({ message, isError })),
|
||||||
|
closeModal: () => dispatch(doHideModal()),
|
||||||
|
abandonTxo: (txo, cb) => dispatch(doAbandonTxo(txo, cb)),
|
||||||
|
abandonClaim: (txid, nout, cb) => dispatch(doAbandonClaim(txid, nout, cb)),
|
||||||
|
doResolveUri: (uri) => dispatch(doResolveUri(uri)),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(select, perform)(ModalRevokeClaim);
|
83
ui/modal/modalRemoveCard/view.jsx
Normal file
83
ui/modal/modalRemoveCard/view.jsx
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
// @flow
|
||||||
|
import React from 'react';
|
||||||
|
import { Modal } from 'modal/modal';
|
||||||
|
import Card from 'component/common/card';
|
||||||
|
import Button from 'component/button';
|
||||||
|
import * as ICONS from 'constants/icons';
|
||||||
|
import { Lbryio } from 'lbryinc';
|
||||||
|
import { STRIPE_PUBLIC_KEY } from 'config';
|
||||||
|
|
||||||
|
let stripeEnvironment = 'test';
|
||||||
|
// if the key contains pk_live it's a live key
|
||||||
|
// update the environment for the calls to the backend to indicate which environment to hit
|
||||||
|
if (STRIPE_PUBLIC_KEY.indexOf('pk_live') > -1) {
|
||||||
|
stripeEnvironment = 'live';
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
closeModal: () => void,
|
||||||
|
abandonTxo: (Txo, () => void) => void,
|
||||||
|
abandonClaim: (string, number, ?() => void) => void,
|
||||||
|
tx: Txo,
|
||||||
|
claim: GenericClaim,
|
||||||
|
cb: () => void,
|
||||||
|
doResolveUri: (string) => void,
|
||||||
|
uri: string,
|
||||||
|
paymentMethodId: string,
|
||||||
|
setAsConfirmingCard: () => void,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ModalRevokeClaim(props: Props) {
|
||||||
|
var that = this;
|
||||||
|
console.log(that);
|
||||||
|
|
||||||
|
console.log(props);
|
||||||
|
|
||||||
|
const { closeModal, uri, paymentMethodId, setAsConfirmingCard } = props;
|
||||||
|
|
||||||
|
console.log(uri);
|
||||||
|
|
||||||
|
console.log(setAsConfirmingCard);
|
||||||
|
|
||||||
|
function removeCard() {
|
||||||
|
console.log(paymentMethodId);
|
||||||
|
|
||||||
|
Lbryio.call(
|
||||||
|
'customer',
|
||||||
|
'detach',
|
||||||
|
{
|
||||||
|
environment: stripeEnvironment,
|
||||||
|
payment_method_id: paymentMethodId,
|
||||||
|
},
|
||||||
|
'post'
|
||||||
|
).then((removeCardResponse) => {
|
||||||
|
console.log(removeCardResponse);
|
||||||
|
|
||||||
|
// TODO: add toast here
|
||||||
|
// closeModal();
|
||||||
|
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal ariaHideApp={false} isOpen contentLabel={'hello'} type="card" onAborted={closeModal}>
|
||||||
|
<Card
|
||||||
|
title={'Confirm Remove Card'}
|
||||||
|
// body={getMsgBody(type, isSupport, name)}
|
||||||
|
actions={
|
||||||
|
<div className="section__actions">
|
||||||
|
<Button
|
||||||
|
className="stripe__confirm-remove-card"
|
||||||
|
button="secondary"
|
||||||
|
icon={ICONS.DELETE}
|
||||||
|
label={'Remove Card'}
|
||||||
|
onClick={removeCard}
|
||||||
|
/>
|
||||||
|
<Button button="link" label={__('Cancel')} onClick={closeModal} />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
|
@ -29,6 +29,7 @@ const ModalPhoneCollection = lazyImport(() => import('modal/modalPhoneCollection
|
||||||
const ModalPublish = lazyImport(() => import('modal/modalPublish' /* webpackChunkName: "modalPublish" */));
|
const ModalPublish = lazyImport(() => import('modal/modalPublish' /* webpackChunkName: "modalPublish" */));
|
||||||
const ModalPublishPreview = lazyImport(() => import('modal/modalPublishPreview' /* webpackChunkName: "modalPublishPreview" */));
|
const ModalPublishPreview = lazyImport(() => import('modal/modalPublishPreview' /* webpackChunkName: "modalPublishPreview" */));
|
||||||
const ModalRemoveBtcSwapAddress = lazyImport(() => import('modal/modalRemoveBtcSwapAddress' /* webpackChunkName: "modalRemoveBtcSwapAddress" */));
|
const ModalRemoveBtcSwapAddress = lazyImport(() => import('modal/modalRemoveBtcSwapAddress' /* webpackChunkName: "modalRemoveBtcSwapAddress" */));
|
||||||
|
const ModalRemoveCard = lazyImport(() => import('modal/modalRemoveCard' /* webpackChunkName: "modalRemoveCard" */));
|
||||||
const ModalRemoveFile = lazyImport(() => import('modal/modalRemoveFile' /* webpackChunkName: "modalRemoveFile" */));
|
const ModalRemoveFile = lazyImport(() => import('modal/modalRemoveFile' /* webpackChunkName: "modalRemoveFile" */));
|
||||||
const ModalRevokeClaim = lazyImport(() => import('modal/modalRevokeClaim' /* webpackChunkName: "modalRevokeClaim" */));
|
const ModalRevokeClaim = lazyImport(() => import('modal/modalRevokeClaim' /* webpackChunkName: "modalRevokeClaim" */));
|
||||||
const ModalRewardCode = lazyImport(() => import('modal/modalRewardCode' /* webpackChunkName: "modalRewardCode" */));
|
const ModalRewardCode = lazyImport(() => import('modal/modalRewardCode' /* webpackChunkName: "modalRewardCode" */));
|
||||||
|
@ -151,6 +152,8 @@ function ModalRouter(props: Props) {
|
||||||
return ModalClaimCollectionAdd;
|
return ModalClaimCollectionAdd;
|
||||||
case MODALS.COLLECTION_DELETE:
|
case MODALS.COLLECTION_DELETE:
|
||||||
return ModalDeleteCollection;
|
return ModalDeleteCollection;
|
||||||
|
case MODALS.CONFIRM_REMOVE_CARD:
|
||||||
|
return ModalRemoveCard;
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
// @flow
|
// @flow
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Modal } from 'modal/modal';
|
import { Modal } from 'modal/modal';
|
||||||
|
import { SITE_HELP_EMAIL } from 'config';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
closeModal: () => void,
|
closeModal: () => void,
|
||||||
|
@ -12,7 +13,11 @@ class ModalTransactionFailed extends React.PureComponent<Props> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen contentLabel={__('Transaction failed')} title={__('Transaction failed')} onConfirmed={closeModal}>
|
<Modal isOpen contentLabel={__('Transaction failed')} title={__('Transaction failed')} onConfirmed={closeModal}>
|
||||||
<p>{__('Sorry about that. Contact help@lbry.com if you continue to have issues.')}</p>
|
<p>
|
||||||
|
{__('Sorry about that. Contact %SITE_HELP_EMAIL% if you continue to have issues.', {
|
||||||
|
SITE_HELP_EMAIL,
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
// @flow
|
// @flow
|
||||||
|
import { SIMPLE_SITE } from 'config';
|
||||||
import * as PAGES from 'constants/pages';
|
import * as PAGES from 'constants/pages';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Modal } from 'modal/modal';
|
import { Modal } from 'modal/modal';
|
||||||
|
@ -15,18 +16,27 @@ const YoutubeWelcome = (props: Props) => {
|
||||||
<Modal isOpen type="card" onAborted={doHideModal}>
|
<Modal isOpen type="card" onAborted={doHideModal}>
|
||||||
<Confetti recycle={false} style={{ position: 'fixed' }} numberOfPieces={100} />
|
<Confetti recycle={false} style={{ position: 'fixed' }} numberOfPieces={100} />
|
||||||
<Card
|
<Card
|
||||||
title={__("You're free!")}
|
title={!SIMPLE_SITE ? __("You're free!") : __('Welcome to Odysee')}
|
||||||
subtitle={
|
subtitle={
|
||||||
<React.Fragment>
|
!SIMPLE_SITE ? (
|
||||||
<p>
|
<React.Fragment>
|
||||||
{__("You've escaped the land of spying, censorship, and exploitation.")}
|
<p>
|
||||||
<span className="emoji"> 💩</span>
|
{__("You've escaped the land of spying, censorship, and exploitation.")}
|
||||||
</p>
|
<span className="emoji"> 💩</span>
|
||||||
<p>
|
</p>
|
||||||
{__('Welcome to the land of content freedom.')}
|
<p>
|
||||||
<span className="emoji"> 🌈</span>
|
{__('Welcome to the land of content freedom.')}
|
||||||
</p>
|
<span className="emoji"> 🌈</span>
|
||||||
</React.Fragment>
|
</p>
|
||||||
|
</React.Fragment>
|
||||||
|
) : (
|
||||||
|
<React.Fragment>
|
||||||
|
<p>
|
||||||
|
{__('You make the party extra special!')}
|
||||||
|
<span className="emoji"> 💖</span>
|
||||||
|
</p>
|
||||||
|
</React.Fragment>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
actions={
|
actions={
|
||||||
<div className="card__actions">
|
<div className="card__actions">
|
||||||
|
|
|
@ -21,8 +21,11 @@ import HelpLink from 'component/common/help-link';
|
||||||
import ClaimSupportButton from 'component/claimSupportButton';
|
import ClaimSupportButton from 'component/claimSupportButton';
|
||||||
import ChannelStakedIndicator from 'component/channelStakedIndicator';
|
import ChannelStakedIndicator from 'component/channelStakedIndicator';
|
||||||
import ClaimMenuList from 'component/claimMenuList';
|
import ClaimMenuList from 'component/claimMenuList';
|
||||||
|
import OptimizedImage from 'component/optimizedImage';
|
||||||
import Yrbl from 'component/yrbl';
|
import Yrbl from 'component/yrbl';
|
||||||
import I18nMessage from 'component/i18nMessage';
|
import I18nMessage from 'component/i18nMessage';
|
||||||
|
// $FlowFixMe cannot resolve ...
|
||||||
|
import PlaceholderTx from 'static/img/placeholderTx.gif';
|
||||||
|
|
||||||
export const PAGE_VIEW_QUERY = `view`;
|
export const PAGE_VIEW_QUERY = `view`;
|
||||||
const CONTENT_PAGE = 'content';
|
const CONTENT_PAGE = 'content';
|
||||||
|
@ -217,7 +220,8 @@ function ChannelPage(props: Props) {
|
||||||
{/* TODO: add channel collections <ClaimCollectionAddButton uri={uri} fileAction /> */}
|
{/* TODO: add channel collections <ClaimCollectionAddButton uri={uri} fileAction /> */}
|
||||||
<ClaimMenuList uri={claim.permanent_url} channelUri={claim.permanent_url} inline isChannelPage />
|
<ClaimMenuList uri={claim.permanent_url} channelUri={claim.permanent_url} inline isChannelPage />
|
||||||
</div>
|
</div>
|
||||||
{cover && <img className={classnames('channel-cover__custom')} src={cover} />}
|
{cover && <img className={classnames('channel-cover__custom')} src={PlaceholderTx} />}
|
||||||
|
{cover && <OptimizedImage className={classnames('channel-cover__custom')} src={cover} objectFit="cover" />}
|
||||||
<div className="channel__primary-info">
|
<div className="channel__primary-info">
|
||||||
<ChannelThumbnail className="channel__thumbnail--channel-page" uri={uri} allowGifs hideStakedIndicator />
|
<ChannelThumbnail className="channel__thumbnail--channel-page" uri={uri} allowGifs hideStakedIndicator />
|
||||||
<h1 className="channel__title">
|
<h1 className="channel__title">
|
||||||
|
|
|
@ -10,6 +10,7 @@ import Page from 'component/page';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import Icon from 'component/common/icon';
|
import Icon from 'component/common/icon';
|
||||||
import useGetLivestreams from 'effects/use-get-livestreams';
|
import useGetLivestreams from 'effects/use-get-livestreams';
|
||||||
|
import { splitBySeparator } from 'lbry-redux';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
subscribedChannels: Array<Subscription>,
|
subscribedChannels: Array<Subscription>,
|
||||||
|
@ -36,7 +37,7 @@ function ChannelsFollowingPage(props: Props) {
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
defaultOrderBy={CS.ORDER_BY_NEW}
|
defaultOrderBy={CS.ORDER_BY_NEW}
|
||||||
channelIds={subscribedChannels.map((sub) => sub.uri.split('#')[1])}
|
channelIds={subscribedChannels.map((sub) => splitBySeparator(sub.uri)[1])}
|
||||||
meta={
|
meta={
|
||||||
<Button
|
<Button
|
||||||
icon={ICONS.SEARCH}
|
icon={ICONS.SEARCH}
|
||||||
|
|
|
@ -8,7 +8,7 @@ import ClaimTilesDiscover from 'component/claimTilesDiscover';
|
||||||
import ClaimListDiscover from 'component/claimListDiscover';
|
import ClaimListDiscover from 'component/claimListDiscover';
|
||||||
import * as CS from 'constants/claim_search';
|
import * as CS from 'constants/claim_search';
|
||||||
import { toCapitalCase } from 'util/string';
|
import { toCapitalCase } from 'util/string';
|
||||||
import { SIMPLE_SITE } from 'config';
|
import { CUSTOM_HOMEPAGE } from 'config';
|
||||||
|
|
||||||
const MORE_CHANNELS_ANCHOR = 'MoreChannels';
|
const MORE_CHANNELS_ANCHOR = 'MoreChannels';
|
||||||
|
|
||||||
|
@ -28,12 +28,16 @@ type ChannelsFollowingItem = {
|
||||||
|
|
||||||
function ChannelsFollowingDiscover(props: Props) {
|
function ChannelsFollowingDiscover(props: Props) {
|
||||||
const { followedTags, subscribedChannels, blockedChannels, homepageData } = props;
|
const { followedTags, subscribedChannels, blockedChannels, homepageData } = props;
|
||||||
const { PRIMARY_CONTENT_CHANNEL_IDS } = homepageData;
|
const { PRIMARY_CONTENT } = homepageData;
|
||||||
|
let channelIds;
|
||||||
|
if (PRIMARY_CONTENT && CUSTOM_HOMEPAGE) {
|
||||||
|
channelIds = PRIMARY_CONTENT.channelIds;
|
||||||
|
}
|
||||||
let rowData: Array<ChannelsFollowingItem> = [];
|
let rowData: Array<ChannelsFollowingItem> = [];
|
||||||
const notChannels = subscribedChannels
|
const notChannels = subscribedChannels
|
||||||
.map(({ uri }) => uri)
|
.map(({ uri }) => uri)
|
||||||
.concat(blockedChannels)
|
.concat(blockedChannels)
|
||||||
.map(uri => uri.split('#')[1]);
|
.map((uri) => uri.split('#')[1]);
|
||||||
|
|
||||||
rowData.push({
|
rowData.push({
|
||||||
title: 'Top Channels Of All Time',
|
title: 'Top Channels Of All Time',
|
||||||
|
@ -84,12 +88,12 @@ function ChannelsFollowingDiscover(props: Props) {
|
||||||
link: `/$/${PAGES.TAGS_FOLLOWING}?claim_type=channel`,
|
link: `/$/${PAGES.TAGS_FOLLOWING}?claim_type=channel`,
|
||||||
options: {
|
options: {
|
||||||
claimType: 'channel',
|
claimType: 'channel',
|
||||||
tags: followedTags.map(tag => tag.name),
|
tags: followedTags.map((tag) => tag.name),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const rowDataWithGenericOptions = rowData.map(row => {
|
const rowDataWithGenericOptions = rowData.map((row) => {
|
||||||
return {
|
return {
|
||||||
...row,
|
...row,
|
||||||
options: {
|
options: {
|
||||||
|
@ -124,12 +128,11 @@ function ChannelsFollowingDiscover(props: Props) {
|
||||||
<h1 id={MORE_CHANNELS_ANCHOR} className="claim-grid__title">
|
<h1 id={MORE_CHANNELS_ANCHOR} className="claim-grid__title">
|
||||||
{__('More Channels')}
|
{__('More Channels')}
|
||||||
</h1>
|
</h1>
|
||||||
{/* odysee: claimIds = PRIMARY_CONTENT_CHANNEL_IDS if simplesite CLD */}
|
|
||||||
<ClaimListDiscover
|
<ClaimListDiscover
|
||||||
defaultOrderBy={CS.ORDER_BY_TRENDING}
|
defaultOrderBy={CS.ORDER_BY_TRENDING}
|
||||||
defaultFreshness={CS.FRESH_ALL}
|
defaultFreshness={CS.FRESH_ALL}
|
||||||
claimType={CS.CLAIM_CHANNEL}
|
claimType={CS.CLAIM_CHANNEL}
|
||||||
claimIds={SIMPLE_SITE ? PRIMARY_CONTENT_CHANNEL_IDS : undefined}
|
claimIds={CUSTOM_HOMEPAGE && channelIds ? channelIds : undefined}
|
||||||
scrollAnchor={MORE_CHANNELS_ANCHOR}
|
scrollAnchor={MORE_CHANNELS_ANCHOR}
|
||||||
/>
|
/>
|
||||||
</Page>
|
</Page>
|
||||||
|
|
|
@ -103,7 +103,7 @@ export default function CollectionPage(props: Props) {
|
||||||
|
|
||||||
const subTitle = (
|
const subTitle = (
|
||||||
<div>
|
<div>
|
||||||
{uri ? <span>{collectionCount} items</span> : <span>{collectionCount} items</span>}
|
<span className="collection__subtitle">{collectionCount} items</span>
|
||||||
{uri && <ClaimAuthor uri={uri} />}
|
{uri && <ClaimAuthor uri={uri} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -8,7 +8,6 @@ import {
|
||||||
makeSelectClaimIsNsfw,
|
makeSelectClaimIsNsfw,
|
||||||
SETTINGS,
|
SETTINGS,
|
||||||
makeSelectTagInClaimOrChannelForUri,
|
makeSelectTagInClaimOrChannelForUri,
|
||||||
makeSelectClaimIsMine,
|
|
||||||
makeSelectClaimIsStreamPlaceholder,
|
makeSelectClaimIsStreamPlaceholder,
|
||||||
makeSelectCollectionForId,
|
makeSelectCollectionForId,
|
||||||
COLLECTIONS_CONSTS,
|
COLLECTIONS_CONSTS,
|
||||||
|
@ -35,7 +34,6 @@ const select = (state, props) => {
|
||||||
renderMode: makeSelectFileRenderModeForUri(props.uri)(state),
|
renderMode: makeSelectFileRenderModeForUri(props.uri)(state),
|
||||||
videoTheaterMode: makeSelectClientSetting(SETTINGS.VIDEO_THEATER_MODE)(state),
|
videoTheaterMode: makeSelectClientSetting(SETTINGS.VIDEO_THEATER_MODE)(state),
|
||||||
commentsDisabled: makeSelectTagInClaimOrChannelForUri(props.uri, DISABLE_COMMENTS_TAG)(state),
|
commentsDisabled: makeSelectTagInClaimOrChannelForUri(props.uri, DISABLE_COMMENTS_TAG)(state),
|
||||||
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
|
||||||
isLivestream: makeSelectClaimIsStreamPlaceholder(props.uri)(state),
|
isLivestream: makeSelectClaimIsStreamPlaceholder(props.uri)(state),
|
||||||
collection: makeSelectCollectionForId(collectionId)(state),
|
collection: makeSelectCollectionForId(collectionId)(state),
|
||||||
collectionId,
|
collectionId,
|
||||||
|
|
|
@ -115,6 +115,18 @@ function FilePage(props: Props) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (renderMode === RENDER_MODES.IMAGE) {
|
||||||
|
return (
|
||||||
|
<React.Fragment>
|
||||||
|
<div className="file-render--img-container">
|
||||||
|
<FileRenderInitiator uri={uri} />
|
||||||
|
<FileRenderInline uri={uri} />
|
||||||
|
</div>
|
||||||
|
<FileTitleSection uri={uri} />
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<FileRenderInitiator uri={uri} videoTheaterMode={videoTheaterMode} />
|
<FileRenderInitiator uri={uri} videoTheaterMode={videoTheaterMode} />
|
||||||
|
|
|
@ -16,6 +16,7 @@ import { GetLinksData } from 'util/buildHomepage';
|
||||||
|
|
||||||
// @if TARGET='web'
|
// @if TARGET='web'
|
||||||
import Pixel from 'web/component/pixel';
|
import Pixel from 'web/component/pixel';
|
||||||
|
import Meme from 'web/component/meme';
|
||||||
// @endif
|
// @endif
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
@ -45,7 +46,7 @@ function HomePage(props: Props) {
|
||||||
showNsfw
|
showNsfw
|
||||||
);
|
);
|
||||||
|
|
||||||
function getRowElements(title, route, link, icon, help, options, index) {
|
function getRowElements(title, route, link, icon, help, options, index, pinUrls) {
|
||||||
const tilePlaceholder = (
|
const tilePlaceholder = (
|
||||||
<ul className="claim-grid">
|
<ul className="claim-grid">
|
||||||
{new Array(options.pageSize || 8).fill(1).map((x, i) => (
|
{new Array(options.pageSize || 8).fill(1).map((x, i) => (
|
||||||
|
@ -60,6 +61,7 @@ function HomePage(props: Props) {
|
||||||
livestreamMap={livestreamMap}
|
livestreamMap={livestreamMap}
|
||||||
showNoSourceClaims={ENABLE_NO_SOURCE_CLAIMS}
|
showNoSourceClaims={ENABLE_NO_SOURCE_CLAIMS}
|
||||||
hasSource
|
hasSource
|
||||||
|
pinUrls={pinUrls}
|
||||||
pin={route === `/$/${PAGES.GENERAL}`} // use pinUrls here
|
pin={route === `/$/${PAGES.GENERAL}`} // use pinUrls here
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -140,12 +142,15 @@ function HomePage(props: Props) {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{rowData.map(({ title, route, link, icon, help, options = {} }, index) => {
|
{/* @if TARGET='web' */}
|
||||||
|
{SIMPLE_SITE && <Meme />}
|
||||||
|
{/* @endif */}
|
||||||
|
{rowData.map(({ title, route, link, icon, help, pinUrls, options = {} }, index) => {
|
||||||
// add pins here
|
// add pins here
|
||||||
return getRowElements(title, route, link, icon, help, options, index);
|
return getRowElements(title, route, link, icon, help, options, index, pinUrls);
|
||||||
})}
|
})}
|
||||||
{/* @if TARGET='web' */}
|
{/* @if TARGET='web' */}
|
||||||
<Pixel type={'retargeting'} />
|
<Pixel type={'retargeting'} />
|
||||||
{/* @endif */}
|
{/* @endif */}
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
|
|
|
@ -18,7 +18,7 @@ import {
|
||||||
} from 'redux/selectors/settings';
|
} from 'redux/selectors/settings';
|
||||||
import { doWalletStatus, selectMyChannelUrls, selectWalletIsEncrypted, SETTINGS } from 'lbry-redux';
|
import { doWalletStatus, selectMyChannelUrls, selectWalletIsEncrypted, SETTINGS } from 'lbry-redux';
|
||||||
import SettingsPage from './view';
|
import SettingsPage from './view';
|
||||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
import { selectUserVerifiedEmail, selectUser } from 'redux/selectors/user';
|
||||||
|
|
||||||
const select = (state) => ({
|
const select = (state) => ({
|
||||||
daemonSettings: selectDaemonSettings(state),
|
daemonSettings: selectDaemonSettings(state),
|
||||||
|
@ -38,6 +38,7 @@ const select = (state) => ({
|
||||||
darkModeTimes: makeSelectClientSetting(SETTINGS.DARK_MODE_TIMES)(state),
|
darkModeTimes: makeSelectClientSetting(SETTINGS.DARK_MODE_TIMES)(state),
|
||||||
language: selectLanguage(state),
|
language: selectLanguage(state),
|
||||||
myChannelUrls: selectMyChannelUrls(state),
|
myChannelUrls: selectMyChannelUrls(state),
|
||||||
|
user: selectUser(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
const perform = (dispatch) => ({
|
const perform = (dispatch) => ({
|
||||||
|
|
|
@ -72,6 +72,7 @@ type Props = {
|
||||||
enterSettings: () => void,
|
enterSettings: () => void,
|
||||||
exitSettings: () => void,
|
exitSettings: () => void,
|
||||||
myChannelUrls: ?Array<string>,
|
myChannelUrls: ?Array<string>,
|
||||||
|
user: User,
|
||||||
};
|
};
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
|
@ -189,6 +190,7 @@ class SettingsPage extends React.PureComponent<Props, State> {
|
||||||
clearCache,
|
clearCache,
|
||||||
openModal,
|
openModal,
|
||||||
myChannelUrls,
|
myChannelUrls,
|
||||||
|
user,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { storedPassword } = this.state;
|
const { storedPassword } = this.state;
|
||||||
const noDaemonSettings = !daemonSettings || Object.keys(daemonSettings).length === 0;
|
const noDaemonSettings = !daemonSettings || Object.keys(daemonSettings).length === 0;
|
||||||
|
@ -206,14 +208,32 @@ class SettingsPage extends React.PureComponent<Props, State> {
|
||||||
className="card-stack"
|
className="card-stack"
|
||||||
>
|
>
|
||||||
{/* @if TARGET='web' */}
|
{/* @if TARGET='web' */}
|
||||||
<Card
|
{user && user.fiat_enabled && <Card
|
||||||
title={__('Add card to tip creators in USD')}
|
title={__('Bank Accounts')}
|
||||||
|
subtitle={__('Connect a bank account to receive tips and compensation in your local currency')}
|
||||||
actions={
|
actions={
|
||||||
<div className="section__actions">
|
<div className="section__actions">
|
||||||
<Button
|
<Button
|
||||||
button="secondary"
|
button="secondary"
|
||||||
label={__('Manage Card')}
|
label={__('Manage')}
|
||||||
icon={ICONS.WALLET}
|
icon={ICONS.SETTINGS}
|
||||||
|
navigate={`/$/${PAGES.SETTINGS_STRIPE_ACCOUNT}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>}
|
||||||
|
{/* @endif */}
|
||||||
|
|
||||||
|
{/* @if TARGET='web' */}
|
||||||
|
<Card
|
||||||
|
title={__('Payment Methods')}
|
||||||
|
subtitle={__('Add a credit card to tip creators in their local currency')}
|
||||||
|
actions={
|
||||||
|
<div className="section__actions">
|
||||||
|
<Button
|
||||||
|
button="secondary"
|
||||||
|
label={__('Manage')}
|
||||||
|
icon={ICONS.SETTINGS}
|
||||||
navigate={`/$/${PAGES.SETTINGS_STRIPE_CARD}`}
|
navigate={`/$/${PAGES.SETTINGS_STRIPE_CARD}`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -8,6 +8,6 @@ const select = (state) => ({
|
||||||
user: selectUser(state),
|
user: selectUser(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
const perform = (dispatch) => ({});
|
// const perform = (dispatch) => ({});
|
||||||
|
|
||||||
export default withRouter(connect(select, perform)(StripeAccountConnection));
|
export default withRouter(connect(select)(StripeAccountConnection));
|
343
ui/page/settingsStripeAccount/view.jsx
Normal file
343
ui/page/settingsStripeAccount/view.jsx
Normal file
|
@ -0,0 +1,343 @@
|
||||||
|
// @flow
|
||||||
|
import * as ICONS from 'constants/icons';
|
||||||
|
import React from 'react';
|
||||||
|
import Button from 'component/button';
|
||||||
|
import Card from 'component/common/card';
|
||||||
|
import Page from 'component/page';
|
||||||
|
import { Lbryio } from 'lbryinc';
|
||||||
|
import { URL, WEBPACK_WEB_PORT, STRIPE_PUBLIC_KEY } from 'config';
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
|
const isDev = process.env.NODE_ENV !== 'production';
|
||||||
|
|
||||||
|
let stripeEnvironment = 'test';
|
||||||
|
// if the key contains pk_live it's a live key
|
||||||
|
// update the environment for the calls to the backend to indicate which environment to hit
|
||||||
|
if (STRIPE_PUBLIC_KEY.indexOf('pk_live') > -1) {
|
||||||
|
stripeEnvironment = 'live';
|
||||||
|
}
|
||||||
|
|
||||||
|
let successStripeRedirectUrl, failureStripeRedirectUrl;
|
||||||
|
let successEndpoint = '/$/settings/tip_account';
|
||||||
|
let failureEndpoint = '/$/settings/tip_account';
|
||||||
|
if (isDev) {
|
||||||
|
successStripeRedirectUrl = 'http://localhost:' + WEBPACK_WEB_PORT + successEndpoint;
|
||||||
|
failureStripeRedirectUrl = 'http://localhost:' + WEBPACK_WEB_PORT + failureEndpoint;
|
||||||
|
} else {
|
||||||
|
successStripeRedirectUrl = URL + successEndpoint;
|
||||||
|
failureStripeRedirectUrl = URL + failureEndpoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
source: string,
|
||||||
|
user: User,
|
||||||
|
};
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
error: boolean,
|
||||||
|
loading: boolean,
|
||||||
|
content: ?string,
|
||||||
|
stripeConnectionUrl: string,
|
||||||
|
// alreadyUpdated: boolean,
|
||||||
|
accountConfirmed: boolean,
|
||||||
|
accountPendingConfirmation: boolean,
|
||||||
|
accountNotConfirmedButReceivedTips: boolean,
|
||||||
|
unpaidBalance: number,
|
||||||
|
pageTitle: string,
|
||||||
|
accountTransactions: any, // define this type
|
||||||
|
};
|
||||||
|
|
||||||
|
class StripeAccountConnection extends React.Component<Props, State> {
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
error: false,
|
||||||
|
content: null,
|
||||||
|
loading: true,
|
||||||
|
accountConfirmed: false,
|
||||||
|
accountPendingConfirmation: false,
|
||||||
|
accountNotConfirmedButReceivedTips: false,
|
||||||
|
unpaidBalance: 0,
|
||||||
|
stripeConnectionUrl: '',
|
||||||
|
pageTitle: 'Add Payout Method',
|
||||||
|
accountTransactions: [],
|
||||||
|
// alreadyUpdated: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const { user } = this.props;
|
||||||
|
|
||||||
|
// $FlowFixMe
|
||||||
|
this.experimentalUiEnabled = user && user.experimental_ui;
|
||||||
|
|
||||||
|
var that = this;
|
||||||
|
|
||||||
|
function getAndSetAccountLink(stillNeedToConfirmAccount) {
|
||||||
|
Lbryio.call(
|
||||||
|
'account',
|
||||||
|
'link',
|
||||||
|
{
|
||||||
|
return_url: successStripeRedirectUrl,
|
||||||
|
refresh_url: failureStripeRedirectUrl,
|
||||||
|
environment: stripeEnvironment,
|
||||||
|
},
|
||||||
|
'post'
|
||||||
|
).then((accountLinkResponse) => {
|
||||||
|
// stripe link for user to navigate to and confirm account
|
||||||
|
const stripeConnectionUrl = accountLinkResponse.url;
|
||||||
|
|
||||||
|
// set connection url on frontend
|
||||||
|
that.setState({
|
||||||
|
stripeConnectionUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
// show the account confirmation link if not created already
|
||||||
|
if (stillNeedToConfirmAccount) {
|
||||||
|
that.setState({
|
||||||
|
accountPendingConfirmation: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// call the account status endpoint
|
||||||
|
Lbryio.call(
|
||||||
|
'account',
|
||||||
|
'status',
|
||||||
|
{
|
||||||
|
environment: stripeEnvironment,
|
||||||
|
},
|
||||||
|
'post'
|
||||||
|
)
|
||||||
|
.then((accountStatusResponse) => {
|
||||||
|
const yetToBeCashedOutBalance = accountStatusResponse.total_received_unpaid;
|
||||||
|
if (yetToBeCashedOutBalance) {
|
||||||
|
that.setState({
|
||||||
|
unpaidBalance: yetToBeCashedOutBalance,
|
||||||
|
});
|
||||||
|
|
||||||
|
Lbryio.call(
|
||||||
|
'account',
|
||||||
|
'list',
|
||||||
|
{
|
||||||
|
environment: stripeEnvironment,
|
||||||
|
},
|
||||||
|
'post'
|
||||||
|
).then((accountListResponse: any) => {
|
||||||
|
// TODO type this
|
||||||
|
that.setState({
|
||||||
|
accountTransactions: accountListResponse,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(accountListResponse);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// if charges already enabled, no need to generate an account link
|
||||||
|
if (accountStatusResponse.charges_enabled) {
|
||||||
|
// account has already been confirmed
|
||||||
|
|
||||||
|
that.setState({
|
||||||
|
accountConfirmed: true,
|
||||||
|
});
|
||||||
|
// user has not confirmed an account but have received payments
|
||||||
|
} else if (accountStatusResponse.total_received_unpaid > 0) {
|
||||||
|
that.setState({
|
||||||
|
accountNotConfirmedButReceivedTips: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
getAndSetAccountLink();
|
||||||
|
|
||||||
|
// user has not received any amount or confirmed an account
|
||||||
|
} else {
|
||||||
|
// get stripe link and set it on the frontend
|
||||||
|
// pass true so it updates the frontend
|
||||||
|
getAndSetAccountLink(true);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
// errorString passed from the API (with a 403 error)
|
||||||
|
const errorString = 'account not linked to user, please link first';
|
||||||
|
|
||||||
|
// if it's beamer's error indicating the account is not linked yet
|
||||||
|
if (error.message.indexOf(errorString) > -1) {
|
||||||
|
// get stripe link and set it on the frontend
|
||||||
|
getAndSetAccountLink();
|
||||||
|
} else {
|
||||||
|
// not an error from Beamer, throw it
|
||||||
|
throw new Error(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
stripeConnectionUrl,
|
||||||
|
accountConfirmed,
|
||||||
|
accountPendingConfirmation,
|
||||||
|
unpaidBalance,
|
||||||
|
accountNotConfirmedButReceivedTips,
|
||||||
|
pageTitle,
|
||||||
|
accountTransactions,
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const { user } = this.props;
|
||||||
|
|
||||||
|
if (user.fiat_enabled) {
|
||||||
|
return (
|
||||||
|
<Page backout={{ title: pageTitle, backLabel: __('Done') }} noFooter noSideNavigation>
|
||||||
|
<Card
|
||||||
|
title={<div className="table__header-text">{__('Connect a bank account')}</div>}
|
||||||
|
isBodyList
|
||||||
|
body={
|
||||||
|
<div>
|
||||||
|
{/* show while waiting for account status */}
|
||||||
|
{!accountConfirmed && !accountPendingConfirmation && !accountNotConfirmedButReceivedTips && (
|
||||||
|
<div className="card__body-actions">
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<h3>{__('Getting your bank account connection status...')}</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{/* user has yet to complete their integration */}
|
||||||
|
{!accountConfirmed && accountPendingConfirmation && (
|
||||||
|
<div className="card__body-actions">
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<h3>{__('Connect your bank account to Odysee to receive donations directly from users')}</h3>
|
||||||
|
</div>
|
||||||
|
<div className="section__actions">
|
||||||
|
<a href={stripeConnectionUrl}>
|
||||||
|
<Button button="secondary" label={__('Connect your bank account')} icon={ICONS.FINANCE} />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{/* user has completed their integration */}
|
||||||
|
{accountConfirmed && (
|
||||||
|
<div className="card__body-actions">
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<h3>{__('Congratulations! Your account has been connected with Odysee.')}</h3>
|
||||||
|
{unpaidBalance > 0 ? (
|
||||||
|
<div>
|
||||||
|
<br />
|
||||||
|
<h3>
|
||||||
|
{__('Your pending account balance is $%balance% USD.', { balance: unpaidBalance / 100 })}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
<br />
|
||||||
|
<h3>
|
||||||
|
{__('Your account balance is $0 USD. When you receive a tip you will see it here.')}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{accountNotConfirmedButReceivedTips && (
|
||||||
|
<div className="card__body-actions">
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<h3>{__('Congratulations, you have already begun receiving tips on Odysee!')}</h3>
|
||||||
|
<div>
|
||||||
|
<br />
|
||||||
|
<h3>
|
||||||
|
{__('Your pending account balance is $%balance% USD.', { balance: unpaidBalance / 100 })}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
<div>
|
||||||
|
<h3>
|
||||||
|
{__(
|
||||||
|
'Connect your bank account to be able to cash your pending balance out to your account.'
|
||||||
|
)}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className="section__actions">
|
||||||
|
<a href={stripeConnectionUrl}>
|
||||||
|
<Button button="secondary" label={__('Connect your bank account')} icon={ICONS.FINANCE} />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<br />
|
||||||
|
|
||||||
|
{/* customer already has transactions */}
|
||||||
|
{accountTransactions && accountTransactions.length > 0 && (
|
||||||
|
<Card
|
||||||
|
title={__('Tip History')}
|
||||||
|
body={
|
||||||
|
<>
|
||||||
|
<div className="table__wrapper">
|
||||||
|
<table className="table table--transactions">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className="date-header">{__('Date')}</th>
|
||||||
|
<th>{<>{__('Receiving Channel Name')}</>}</th>
|
||||||
|
<th>{__('Tip Location')}</th>
|
||||||
|
<th>{__('Amount (USD)')} </th>
|
||||||
|
<th>{__('Processing Fee')}</th>
|
||||||
|
<th>{__('Odysee Fee')}</th>
|
||||||
|
<th>{__('Received Amount')}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{accountTransactions &&
|
||||||
|
accountTransactions.reverse().map((transaction) => (
|
||||||
|
<tr key={transaction.name + transaction.created_at}>
|
||||||
|
<td>{moment(transaction.created_at).format('LLL')}</td>
|
||||||
|
<td>
|
||||||
|
<Button
|
||||||
|
className="stripe__card-link-text"
|
||||||
|
navigate={'/' + transaction.channel_name + ':' + transaction.channel_claim_id}
|
||||||
|
label={transaction.channel_name}
|
||||||
|
button="link"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<Button
|
||||||
|
className="stripe__card-link-text"
|
||||||
|
navigate={'/' + transaction.channel_name + ':' + transaction.source_claim_id}
|
||||||
|
label={
|
||||||
|
transaction.channel_claim_id === transaction.source_claim_id
|
||||||
|
? 'Channel Page'
|
||||||
|
: 'File Page'
|
||||||
|
}
|
||||||
|
button="link"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>${transaction.tipped_amount / 100}</td>
|
||||||
|
<td>${transaction.transaction_fee / 100}</td>
|
||||||
|
<td>${transaction.application_fee / 100}</td>
|
||||||
|
<td>${transaction.received_amount / 100}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return <></>; // probably null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default StripeAccountConnection;
|
|
@ -2,6 +2,8 @@ import { connect } from 'react-redux';
|
||||||
import { doSetClientSetting } from 'redux/actions/settings';
|
import { doSetClientSetting } from 'redux/actions/settings';
|
||||||
import { selectosNotificationsEnabled } from 'redux/selectors/settings';
|
import { selectosNotificationsEnabled } from 'redux/selectors/settings';
|
||||||
import { selectUserVerifiedEmail, selectUserEmail } from 'redux/selectors/user';
|
import { selectUserVerifiedEmail, selectUserEmail } from 'redux/selectors/user';
|
||||||
|
import { doOpenModal } from 'redux/actions/app';
|
||||||
|
import { doToast } from 'redux/actions/notifications';
|
||||||
|
|
||||||
import SettingsStripeCard from './view';
|
import SettingsStripeCard from './view';
|
||||||
|
|
||||||
|
@ -13,6 +15,9 @@ const select = (state) => ({
|
||||||
|
|
||||||
const perform = (dispatch) => ({
|
const perform = (dispatch) => ({
|
||||||
setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)),
|
setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)),
|
||||||
|
doOpenModal,
|
||||||
|
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
|
||||||
|
doToast: (options) => dispatch(doToast(options)),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(select, perform)(SettingsStripeCard);
|
export default connect(select, perform)(SettingsStripeCard);
|
||||||
|
|
|
@ -7,10 +7,10 @@ import Card from 'component/common/card';
|
||||||
import { Lbryio } from 'lbryinc';
|
import { Lbryio } from 'lbryinc';
|
||||||
import { STRIPE_PUBLIC_KEY } from 'config';
|
import { STRIPE_PUBLIC_KEY } from 'config';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
import Plastic from 'react-plastic';
|
||||||
let scriptLoading = false;
|
import Button from 'component/button';
|
||||||
// let scriptLoaded = false;
|
import * as ICONS from 'constants/icons';
|
||||||
// let scriptDidError = false; // these could probably be in state if managing locally
|
import * as MODALS from 'constants/modal_types';
|
||||||
|
|
||||||
let stripeEnvironment = 'test';
|
let stripeEnvironment = 'test';
|
||||||
// if the key contains pk_live it's a live key
|
// if the key contains pk_live it's a live key
|
||||||
|
@ -19,13 +19,17 @@ if (STRIPE_PUBLIC_KEY.indexOf('pk_live') > -1) {
|
||||||
stripeEnvironment = 'live';
|
stripeEnvironment = 'live';
|
||||||
}
|
}
|
||||||
|
|
||||||
// type Props = {
|
// eslint-disable-next-line flowtype/no-types-missing-file-annotation
|
||||||
// disabled: boolean,
|
type Props = {
|
||||||
// label: ?string,
|
disabled: boolean,
|
||||||
// email: ?string,
|
label: ?string,
|
||||||
// scriptFailedToLoad: boolean,
|
email: ?string,
|
||||||
// };
|
scriptFailedToLoad: boolean,
|
||||||
//
|
doOpenModal: (string, {}) => void,
|
||||||
|
openModal: (string, {}) => void,
|
||||||
|
setAsConfirmingCard: () => void,
|
||||||
|
};
|
||||||
|
|
||||||
// type State = {
|
// type State = {
|
||||||
// open: boolean,
|
// open: boolean,
|
||||||
// currentFlowStage: string,
|
// currentFlowStage: string,
|
||||||
|
@ -46,11 +50,16 @@ class SettingsStripeCard extends React.Component<Props, State> {
|
||||||
customerTransactions: [],
|
customerTransactions: [],
|
||||||
pageTitle: 'Add Card',
|
pageTitle: 'Add Card',
|
||||||
userCardDetails: {},
|
userCardDetails: {},
|
||||||
|
paymentMethodId: '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
var that = this;
|
let that = this;
|
||||||
|
|
||||||
|
console.log(this.props);
|
||||||
|
|
||||||
|
let doToast = this.props.doToast;
|
||||||
|
|
||||||
const script = document.createElement('script');
|
const script = document.createElement('script');
|
||||||
script.src = 'https://js.stripe.com/v3/';
|
script.src = 'https://js.stripe.com/v3/';
|
||||||
|
@ -60,10 +69,10 @@ class SettingsStripeCard extends React.Component<Props, State> {
|
||||||
document.body.appendChild(script);
|
document.body.appendChild(script);
|
||||||
|
|
||||||
// public key of the stripe account
|
// public key of the stripe account
|
||||||
var publicKey = STRIPE_PUBLIC_KEY;
|
let publicKey = STRIPE_PUBLIC_KEY;
|
||||||
|
|
||||||
// client secret of the SetupIntent (don't share with anyone but customer)
|
// client secret of the SetupIntent (don't share with anyone but customer)
|
||||||
var clientSecret = '';
|
let clientSecret = '';
|
||||||
|
|
||||||
// setting a timeout to let the client secret populate
|
// setting a timeout to let the client secret populate
|
||||||
// TODO: fix this, should be a cleaner way
|
// TODO: fix this, should be a cleaner way
|
||||||
|
@ -80,39 +89,33 @@ class SettingsStripeCard extends React.Component<Props, State> {
|
||||||
.then((customerStatusResponse) => {
|
.then((customerStatusResponse) => {
|
||||||
// user has a card saved if their defaultPaymentMethod has an id
|
// user has a card saved if their defaultPaymentMethod has an id
|
||||||
const defaultPaymentMethod = customerStatusResponse.Customer.invoice_settings.default_payment_method;
|
const defaultPaymentMethod = customerStatusResponse.Customer.invoice_settings.default_payment_method;
|
||||||
var userHasAlreadySetupPayment = Boolean(defaultPaymentMethod && defaultPaymentMethod.id);
|
let userHasAlreadySetupPayment = Boolean(defaultPaymentMethod && defaultPaymentMethod.id);
|
||||||
|
|
||||||
// show different frontend if user already has card
|
// show different frontend if user already has card
|
||||||
if (userHasAlreadySetupPayment) {
|
if (userHasAlreadySetupPayment) {
|
||||||
var card = customerStatusResponse.PaymentMethods[0].card;
|
let card = customerStatusResponse.PaymentMethods[0].card;
|
||||||
|
|
||||||
var cardDetails = {
|
let customer = customerStatusResponse.Customer;
|
||||||
|
|
||||||
|
let topOfDisplay = customer.email.split('@')[0];
|
||||||
|
let bottomOfDisplay = '@' + customer.email.split('@')[1];
|
||||||
|
|
||||||
|
console.log(customerStatusResponse.Customer);
|
||||||
|
|
||||||
|
let cardDetails = {
|
||||||
brand: card.brand,
|
brand: card.brand,
|
||||||
expiryYear: card.exp_year,
|
expiryYear: card.exp_year,
|
||||||
expiryMonth: card.exp_month,
|
expiryMonth: card.exp_month,
|
||||||
lastFour: card.last4,
|
lastFour: card.last4,
|
||||||
|
topOfDisplay: topOfDisplay,
|
||||||
|
bottomOfDisplay: bottomOfDisplay,
|
||||||
};
|
};
|
||||||
|
|
||||||
that.setState({
|
that.setState({
|
||||||
currentFlowStage: 'cardConfirmed',
|
currentFlowStage: 'cardConfirmed',
|
||||||
pageTitle: 'Tip History',
|
pageTitle: 'Tip History',
|
||||||
userCardDetails: cardDetails,
|
userCardDetails: cardDetails,
|
||||||
});
|
paymentMethodId: customerStatusResponse.PaymentMethods[0].id,
|
||||||
|
|
||||||
// get customer transactions
|
|
||||||
Lbryio.call(
|
|
||||||
'customer',
|
|
||||||
'list',
|
|
||||||
{
|
|
||||||
environment: stripeEnvironment,
|
|
||||||
},
|
|
||||||
'post'
|
|
||||||
).then((customerTransactionsResponse) => {
|
|
||||||
that.setState({
|
|
||||||
customerTransactions: customerTransactionsResponse,
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(customerTransactionsResponse);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// otherwise, prompt them to save a card
|
// otherwise, prompt them to save a card
|
||||||
|
@ -138,6 +141,22 @@ class SettingsStripeCard extends React.Component<Props, State> {
|
||||||
setupStripe();
|
setupStripe();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// get customer transactions
|
||||||
|
Lbryio.call(
|
||||||
|
'customer',
|
||||||
|
'list',
|
||||||
|
{
|
||||||
|
environment: stripeEnvironment,
|
||||||
|
},
|
||||||
|
'post'
|
||||||
|
).then((customerTransactionsResponse) => {
|
||||||
|
that.setState({
|
||||||
|
customerTransactions: customerTransactionsResponse,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(customerTransactionsResponse);
|
||||||
|
});
|
||||||
// if the status call fails, either an actual error or need to run setup first
|
// if the status call fails, either an actual error or need to run setup first
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(function (error) {
|
||||||
|
@ -147,7 +166,7 @@ class SettingsStripeCard extends React.Component<Props, State> {
|
||||||
const errorString = 'user as customer is not setup yet';
|
const errorString = 'user as customer is not setup yet';
|
||||||
|
|
||||||
// if it's beamer's error indicating the account is not linked yet
|
// if it's beamer's error indicating the account is not linked yet
|
||||||
if (error.message.indexOf(errorString) > -1) {
|
if (error.message && error.message.indexOf(errorString) > -1) {
|
||||||
// send them to save a card
|
// send them to save a card
|
||||||
that.setState({
|
that.setState({
|
||||||
currentFlowStage: 'confirmingCard',
|
currentFlowStage: 'confirmingCard',
|
||||||
|
@ -169,6 +188,9 @@ class SettingsStripeCard extends React.Component<Props, State> {
|
||||||
// instantiate stripe elements
|
// instantiate stripe elements
|
||||||
setupStripe();
|
setupStripe();
|
||||||
});
|
});
|
||||||
|
} else if (error === 'internal_apis_down') {
|
||||||
|
var displayString = 'There was an error from the server, please let support know';
|
||||||
|
doToast({ message: displayString, isError: true });
|
||||||
} else {
|
} else {
|
||||||
console.log('Unseen before error');
|
console.log('Unseen before error');
|
||||||
}
|
}
|
||||||
|
@ -300,19 +322,27 @@ class SettingsStripeCard extends React.Component<Props, State> {
|
||||||
},
|
},
|
||||||
'post'
|
'post'
|
||||||
).then((customerStatusResponse) => {
|
).then((customerStatusResponse) => {
|
||||||
var card = customerStatusResponse.PaymentMethods[0].card;
|
let card = customerStatusResponse.PaymentMethods[0].card;
|
||||||
|
|
||||||
var cardDetails = {
|
let customer = customerStatusResponse.Customer;
|
||||||
|
|
||||||
|
let topOfDisplay = customer.email.split('@')[0];
|
||||||
|
let bottomOfDisplay = '@' + customer.email.split('@')[1];
|
||||||
|
|
||||||
|
let cardDetails = {
|
||||||
brand: card.brand,
|
brand: card.brand,
|
||||||
expiryYear: card.exp_year,
|
expiryYear: card.exp_year,
|
||||||
expiryMonth: card.exp_month,
|
expiryMonth: card.exp_month,
|
||||||
lastFour: card.last4,
|
lastFour: card.last4,
|
||||||
|
topOfDisplay,
|
||||||
|
bottomOfDisplay,
|
||||||
};
|
};
|
||||||
|
|
||||||
that.setState({
|
that.setState({
|
||||||
currentFlowStage: 'cardConfirmed',
|
currentFlowStage: 'cardConfirmed',
|
||||||
pageTitle: 'Tip History',
|
pageTitle: 'Tip History',
|
||||||
userCardDetails: cardDetails,
|
userCardDetails: cardDetails,
|
||||||
|
paymentMethodId: customerStatusResponse.PaymentMethods[0].id,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -325,59 +355,18 @@ class SettingsStripeCard extends React.Component<Props, State> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate() {
|
|
||||||
if (!scriptLoading) {
|
|
||||||
this.updateStripeHandler();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
// pretty sure this doesn't exist
|
|
||||||
// $FlowFixMe
|
|
||||||
if (this.loadPromise) {
|
|
||||||
// $FlowFixMe
|
|
||||||
this.loadPromise.reject();
|
|
||||||
}
|
|
||||||
// pretty sure this doesn't exist
|
|
||||||
// $FlowFixMe
|
|
||||||
if (CardVerify.stripeHandler && this.state.open) {
|
|
||||||
// $FlowFixMe
|
|
||||||
CardVerify.stripeHandler.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onScriptLoaded = () => {
|
|
||||||
// if (!CardVerify.stripeHandler) {
|
|
||||||
// CardVerify.stripeHandler = StripeCheckout.configure({
|
|
||||||
// key: 'pk_test_NoL1JWL7i1ipfhVId5KfDZgo',
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// if (this.hasPendingClick) {
|
|
||||||
// this.showStripeDialog();
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
};
|
|
||||||
|
|
||||||
onScriptError = (...args) => {
|
|
||||||
this.setState({ scriptFailedToLoad: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
onClosed = () => {
|
|
||||||
this.setState({ open: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
updateStripeHandler() {
|
|
||||||
// if (!CardVerify.stripeHandler) {
|
|
||||||
// CardVerify.stripeHandler = StripeCheckout.configure({
|
|
||||||
// key: this.props.stripeKey,
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { scriptFailedToLoad } = this.props;
|
let that = this;
|
||||||
|
|
||||||
const { currentFlowStage, customerTransactions, pageTitle, userCardDetails } = this.state;
|
function setAsConfirmingCard() {
|
||||||
|
that.setState({
|
||||||
|
currentFlowStage: 'confirmingCard',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { scriptFailedToLoad, openModal } = this.props;
|
||||||
|
|
||||||
|
const { currentFlowStage, customerTransactions, pageTitle, userCardDetails, paymentMethodId } = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page backout={{ title: pageTitle, backLabel: __('Done') }} noFooter noSideNavigation>
|
<Page backout={{ title: pageTitle, backLabel: __('Done') }} noFooter noSideNavigation>
|
||||||
|
@ -393,6 +382,7 @@ class SettingsStripeCard extends React.Component<Props, State> {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* customer has not added a card yet */}
|
||||||
{currentFlowStage === 'confirmingCard' && (
|
{currentFlowStage === 'confirmingCard' && (
|
||||||
<div className="sr-root">
|
<div className="sr-root">
|
||||||
<div className="sr-main">
|
<div className="sr-main">
|
||||||
|
@ -411,62 +401,101 @@ class SettingsStripeCard extends React.Component<Props, State> {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* if the user has already confirmed their card */}
|
||||||
{currentFlowStage === 'cardConfirmed' && (
|
{currentFlowStage === 'cardConfirmed' && (
|
||||||
<div className="successCard">
|
<div className="successCard">
|
||||||
<Card
|
<Card
|
||||||
title={__('Card Details')}
|
title={__('Card Details')}
|
||||||
body={
|
body={
|
||||||
<>
|
<>
|
||||||
<h4 className="grey-text">
|
<Plastic
|
||||||
Brand: {userCardDetails.brand.toUpperCase()} Last 4: {userCardDetails.lastFour}
|
type={userCardDetails.brand}
|
||||||
Expires: {userCardDetails.expiryMonth}/{userCardDetails.expiryYear}
|
name={userCardDetails.topOfDisplay + ' ' + userCardDetails.bottomOfDisplay}
|
||||||
</h4>
|
expiry={userCardDetails.expiryMonth + '/' + userCardDetails.expiryYear}
|
||||||
|
number={'____________' + userCardDetails.lastFour}
|
||||||
|
/>
|
||||||
|
<br />
|
||||||
|
<Button
|
||||||
|
button="secondary"
|
||||||
|
label={__('Remove Card')}
|
||||||
|
icon={ICONS.DELETE}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
openModal(MODALS.CONFIRM_REMOVE_CARD, {
|
||||||
|
paymentMethodId: paymentMethodId,
|
||||||
|
setAsConfirmingCard: setAsConfirmingCard,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
|
{/* if a user has no transactions yet */}
|
||||||
{(!customerTransactions || customerTransactions.length === 0) && (
|
{(!customerTransactions || customerTransactions.length === 0) && (
|
||||||
<Card
|
<Card
|
||||||
title={__('Tip History')}
|
title={__('Tip History')}
|
||||||
subtitle={__('You have not sent any tips yet. When you do they will appear here. ')}
|
subtitle={__('You have not sent any tips yet. When you do they will appear here. ')}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{customerTransactions && customerTransactions.length > 0 && (
|
|
||||||
<Card
|
|
||||||
title={__('Tip History')}
|
|
||||||
body={
|
|
||||||
<>
|
|
||||||
<div className="table__wrapper">
|
|
||||||
<table className="table table--transactions">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th className="date-header">{__('Date')}</th>
|
|
||||||
<th>{<>{__('Receiving Channel Name')}</>}</th>
|
|
||||||
<th>{__('Amount (USD)')} </th>
|
|
||||||
<th>{__('Anonymous')}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{customerTransactions &&
|
|
||||||
customerTransactions.map((transaction) => (
|
|
||||||
<tr key={transaction.name + transaction.created_at}>
|
|
||||||
<td>{moment(transaction.created_at).format('LLL')}</td>
|
|
||||||
<td>{transaction.channel_name}</td>
|
|
||||||
<td>${transaction.tipped_amount / 100}</td>
|
|
||||||
<td>{transaction.private_tip ? 'Yes' : 'No'}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* customer already has transactions */}
|
||||||
|
{customerTransactions && customerTransactions.length > 0 && (
|
||||||
|
<Card
|
||||||
|
title={__('Tip History')}
|
||||||
|
body={
|
||||||
|
<>
|
||||||
|
<div className="table__wrapper">
|
||||||
|
<table className="table table--transactions">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className="date-header">{__('Date')}</th>
|
||||||
|
<th>{<>{__('Receiving Channel Name')}</>}</th>
|
||||||
|
<th>{__('Tip Location')}</th>
|
||||||
|
<th>{__('Amount (USD)')} </th>
|
||||||
|
<th>{__('Anonymous')}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{customerTransactions &&
|
||||||
|
customerTransactions.reverse().map((transaction) => (
|
||||||
|
<tr key={transaction.name + transaction.created_at}>
|
||||||
|
<td>{moment(transaction.created_at).format('LLL')}</td>
|
||||||
|
<td>
|
||||||
|
<Button
|
||||||
|
className="stripe__card-link-text"
|
||||||
|
navigate={'/' + transaction.channel_name + ':' + transaction.channel_claim_id}
|
||||||
|
label={transaction.channel_name}
|
||||||
|
button="link"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<Button
|
||||||
|
className="stripe__card-link-text"
|
||||||
|
navigate={'/' + transaction.channel_name + ':' + transaction.source_claim_id}
|
||||||
|
label={
|
||||||
|
transaction.channel_claim_id === transaction.source_claim_id
|
||||||
|
? 'Channel Page'
|
||||||
|
: 'File Page'
|
||||||
|
}
|
||||||
|
button="link"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>${transaction.tipped_amount / 100}</td>
|
||||||
|
<td>{transaction.private_tip ? 'Yes' : 'No'}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@ import React from 'react';
|
||||||
import { withRouter } from 'react-router';
|
import { withRouter } from 'react-router';
|
||||||
import WalletBalance from 'component/walletBalance';
|
import WalletBalance from 'component/walletBalance';
|
||||||
import TxoList from 'component/txoList';
|
import TxoList from 'component/txoList';
|
||||||
import StripeAccountConnection from 'component/stripeAccountConnection';
|
|
||||||
import Page from 'component/page';
|
import Page from 'component/page';
|
||||||
import Spinner from 'component/spinner';
|
import Spinner from 'component/spinner';
|
||||||
import YrblWalletEmpty from 'component/yrblWalletEmpty';
|
import YrblWalletEmpty from 'component/yrblWalletEmpty';
|
||||||
|
@ -34,9 +33,6 @@ const WalletPage = (props: Props) => {
|
||||||
) : (
|
) : (
|
||||||
<div className="card-stack">
|
<div className="card-stack">
|
||||||
<WalletBalance />
|
<WalletBalance />
|
||||||
{/* @if TARGET='web' */}
|
|
||||||
<StripeAccountConnection />
|
|
||||||
{/* @endif */}
|
|
||||||
<TxoList search={search} />
|
<TxoList search={search} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -626,7 +626,6 @@ export function doGetAndPopulatePreferences() {
|
||||||
function successCb(savedPreferences) {
|
function successCb(savedPreferences) {
|
||||||
const successState = getState();
|
const successState = getState();
|
||||||
const daemonSettings = selectDaemonSettings(successState);
|
const daemonSettings = selectDaemonSettings(successState);
|
||||||
|
|
||||||
if (savedPreferences !== null) {
|
if (savedPreferences !== null) {
|
||||||
dispatch(doPopulateSharedUserState(savedPreferences));
|
dispatch(doPopulateSharedUserState(savedPreferences));
|
||||||
// @if TARGET='app'
|
// @if TARGET='app'
|
||||||
|
@ -653,7 +652,7 @@ export function doGetAndPopulatePreferences() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function failCb() {
|
function failCb(er) {
|
||||||
dispatch(
|
dispatch(
|
||||||
doToast({
|
doToast({
|
||||||
isError: true,
|
isError: true,
|
||||||
|
@ -663,6 +662,7 @@ export function doGetAndPopulatePreferences() {
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ACTIONS.SYNC_FATAL_ERROR,
|
type: ACTIONS.SYNC_FATAL_ERROR,
|
||||||
|
error: er,
|
||||||
});
|
});
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
@ -681,6 +681,8 @@ export function doHandleSyncComplete(error, hasNewData) {
|
||||||
// we just got sync data, better update our channels
|
// we just got sync data, better update our channels
|
||||||
dispatch(doFetchChannelListMine());
|
dispatch(doFetchChannelListMine());
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
console.error('Error in doHandleSyncComplete', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,15 @@ import * as ACTIONS from 'constants/action_types';
|
||||||
import * as REACTION_TYPES from 'constants/reactions';
|
import * as REACTION_TYPES from 'constants/reactions';
|
||||||
import * as PAGES from 'constants/pages';
|
import * as PAGES from 'constants/pages';
|
||||||
import { SORT_BY, BLOCK_LEVEL } from 'constants/comment';
|
import { SORT_BY, BLOCK_LEVEL } from 'constants/comment';
|
||||||
import { Lbry, parseURI, buildURI, selectClaimsById, selectClaimsByUri, selectMyChannelClaims } from 'lbry-redux';
|
import {
|
||||||
|
Lbry,
|
||||||
|
parseURI,
|
||||||
|
buildURI,
|
||||||
|
selectClaimsById,
|
||||||
|
selectClaimsByUri,
|
||||||
|
selectMyChannelClaims,
|
||||||
|
isURIEqual,
|
||||||
|
} from 'lbry-redux';
|
||||||
import { doToast, doSeeNotifications } from 'redux/actions/notifications';
|
import { doToast, doSeeNotifications } from 'redux/actions/notifications';
|
||||||
import {
|
import {
|
||||||
makeSelectMyReactionsForComment,
|
makeSelectMyReactionsForComment,
|
||||||
|
@ -198,7 +206,7 @@ export function doSuperChatList(uri: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function doCommentReactList(commentIds: Array<string>) {
|
export function doCommentReactList(commentIds: Array<string>) {
|
||||||
return (dispatch: Dispatch, getState: GetState) => {
|
return async (dispatch: Dispatch, getState: GetState) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const activeChannelClaim = selectActiveChannelClaim(state);
|
const activeChannelClaim = selectActiveChannelClaim(state);
|
||||||
|
|
||||||
|
@ -206,24 +214,32 @@ export function doCommentReactList(commentIds: Array<string>) {
|
||||||
type: ACTIONS.COMMENT_REACTION_LIST_STARTED,
|
type: ACTIONS.COMMENT_REACTION_LIST_STARTED,
|
||||||
});
|
});
|
||||||
|
|
||||||
const params: CommentReactListParams = {
|
const params: ReactionListParams = {
|
||||||
comment_ids: commentIds.join(','),
|
comment_ids: commentIds.join(','),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (activeChannelClaim) {
|
if (activeChannelClaim) {
|
||||||
params['channel_name'] = activeChannelClaim.name;
|
const signatureData = await channelSignName(activeChannelClaim.claim_id, activeChannelClaim.name);
|
||||||
params['channel_id'] = activeChannelClaim.claim_id;
|
if (!signatureData) {
|
||||||
|
return dispatch(doToast({ isError: true, message: __('Unable to verify your channel. Please try again.') }));
|
||||||
|
}
|
||||||
|
|
||||||
|
params.channel_name = activeChannelClaim.name;
|
||||||
|
params.channel_id = activeChannelClaim.claim_id;
|
||||||
|
params.signature = signatureData.signature;
|
||||||
|
params.signing_ts = signatureData.signing_ts;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Lbry.comment_react_list(params)
|
return Comments.reaction_list(params)
|
||||||
.then((result: CommentReactListResponse) => {
|
.then((result: ReactionListResponse) => {
|
||||||
const { my_reactions: myReactions, others_reactions: othersReactions } = result;
|
const { my_reactions: myReactions, others_reactions: othersReactions } = result;
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ACTIONS.COMMENT_REACTION_LIST_COMPLETED,
|
type: ACTIONS.COMMENT_REACTION_LIST_COMPLETED,
|
||||||
data: {
|
data: {
|
||||||
myReactions: myReactions || {},
|
myReactions,
|
||||||
othersReactions,
|
othersReactions,
|
||||||
channelId: activeChannelClaim ? activeChannelClaim.claim_id : undefined,
|
channelId: activeChannelClaim ? activeChannelClaim.claim_id : undefined,
|
||||||
|
commentIds,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
@ -238,7 +254,7 @@ export function doCommentReactList(commentIds: Array<string>) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function doCommentReact(commentId: string, type: string) {
|
export function doCommentReact(commentId: string, type: string) {
|
||||||
return (dispatch: Dispatch, getState: GetState) => {
|
return async (dispatch: Dispatch, getState: GetState) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const activeChannelClaim = selectActiveChannelClaim(state);
|
const activeChannelClaim = selectActiveChannelClaim(state);
|
||||||
const pendingReacts = selectPendingCommentReacts(state);
|
const pendingReacts = selectPendingCommentReacts(state);
|
||||||
|
@ -266,11 +282,19 @@ export function doCommentReact(commentId: string, type: string) {
|
||||||
const reactKey = `${commentId}:${activeChannelClaim.claim_id}`;
|
const reactKey = `${commentId}:${activeChannelClaim.claim_id}`;
|
||||||
const myReacts = makeSelectMyReactionsForComment(reactKey)(state);
|
const myReacts = makeSelectMyReactionsForComment(reactKey)(state);
|
||||||
const othersReacts = makeSelectOthersReactionsForComment(reactKey)(state);
|
const othersReacts = makeSelectOthersReactionsForComment(reactKey)(state);
|
||||||
const params: CommentReactParams = {
|
|
||||||
|
const signatureData = await channelSignName(activeChannelClaim.claim_id, activeChannelClaim.name);
|
||||||
|
if (!signatureData) {
|
||||||
|
return dispatch(doToast({ isError: true, message: __('Unable to verify your channel. Please try again.') }));
|
||||||
|
}
|
||||||
|
|
||||||
|
const params: ReactionReactParams = {
|
||||||
comment_ids: commentId,
|
comment_ids: commentId,
|
||||||
channel_name: activeChannelClaim.name,
|
channel_name: activeChannelClaim.name,
|
||||||
channel_id: activeChannelClaim.claim_id,
|
channel_id: activeChannelClaim.claim_id,
|
||||||
react_type: type,
|
signature: signatureData.signature,
|
||||||
|
signing_ts: signatureData.signing_ts,
|
||||||
|
type: type,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (myReacts.includes(type)) {
|
if (myReacts.includes(type)) {
|
||||||
|
@ -285,6 +309,7 @@ export function doCommentReact(commentId: string, type: string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ACTIONS.COMMENT_REACT_STARTED,
|
type: ACTIONS.COMMENT_REACT_STARTED,
|
||||||
data: commentId + type,
|
data: commentId + type,
|
||||||
|
@ -304,8 +329,8 @@ export function doCommentReact(commentId: string, type: string) {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
Lbry.comment_react(params)
|
Comments.reaction_react(params)
|
||||||
.then((result: CommentReactListResponse) => {
|
.then((result: ReactionReactResponse) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ACTIONS.COMMENT_REACT_COMPLETED,
|
type: ACTIONS.COMMENT_REACT_COMPLETED,
|
||||||
data: commentId + type,
|
data: commentId + type,
|
||||||
|
@ -335,16 +360,32 @@ export function doCommentReact(commentId: string, type: string) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param comment
|
||||||
|
* @param claim_id - File claim id
|
||||||
|
* @param parent_id - What is this?
|
||||||
|
* @param uri
|
||||||
|
* @param livestream
|
||||||
|
* @param {string} [txid] Optional transaction id
|
||||||
|
* @param {string} [payment_intent_id] Optional transaction id
|
||||||
|
* @param {string} [environment] Optional environment for Stripe (test|live)
|
||||||
|
* @returns {(function(Dispatch, GetState): Promise<undefined|void|*>)|*}
|
||||||
|
*/
|
||||||
export function doCommentCreate(
|
export function doCommentCreate(
|
||||||
comment: string = '',
|
comment: string = '',
|
||||||
claim_id: string = '',
|
claim_id: string = '',
|
||||||
parent_id?: string,
|
parent_id?: string,
|
||||||
uri: string,
|
uri: string,
|
||||||
livestream?: boolean = false,
|
livestream?: boolean = false,
|
||||||
txid?: string
|
txid?: string,
|
||||||
|
payment_intent_id?: string,
|
||||||
|
environment?: string
|
||||||
) {
|
) {
|
||||||
return async (dispatch: Dispatch, getState: GetState) => {
|
return async (dispatch: Dispatch, getState: GetState) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
|
|
||||||
|
// get active channel that will receive comment and optional tip
|
||||||
const activeChannelClaim = selectActiveChannelClaim(state);
|
const activeChannelClaim = selectActiveChannelClaim(state);
|
||||||
|
|
||||||
if (!activeChannelClaim) {
|
if (!activeChannelClaim) {
|
||||||
|
@ -366,6 +407,7 @@ export function doCommentCreate(
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// send a notification
|
||||||
if (parent_id) {
|
if (parent_id) {
|
||||||
const notification = makeSelectNotificationForCommentId(parent_id)(state);
|
const notification = makeSelectNotificationForCommentId(parent_id)(state);
|
||||||
if (notification && !notification.is_seen) {
|
if (notification && !notification.is_seen) {
|
||||||
|
@ -377,6 +419,8 @@ export function doCommentCreate(
|
||||||
return dispatch(doToast({ isError: true, message: __('Unable to verify your channel. Please try again.') }));
|
return dispatch(doToast({ isError: true, message: __('Unable to verify your channel. Please try again.') }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Comments is a function which helps make calls to the backend
|
||||||
|
// these params passed in POST call.
|
||||||
return Comments.comment_create({
|
return Comments.comment_create({
|
||||||
comment: comment,
|
comment: comment,
|
||||||
claim_id: claim_id,
|
claim_id: claim_id,
|
||||||
|
@ -385,9 +429,12 @@ export function doCommentCreate(
|
||||||
parent_id: parent_id,
|
parent_id: parent_id,
|
||||||
signature: signatureData.signature,
|
signature: signatureData.signature,
|
||||||
signing_ts: signatureData.signing_ts,
|
signing_ts: signatureData.signing_ts,
|
||||||
...(txid ? { support_tx_id: txid } : {}),
|
...(txid ? { support_tx_id: txid } : {}), // add transaction id if it exists
|
||||||
|
...(payment_intent_id ? { payment_intent_id } : {}), // add payment_intent_id if it exists
|
||||||
|
...(environment ? { environment } : {}), // add environment for stripe if it exists
|
||||||
})
|
})
|
||||||
.then((result: CommentCreateResponse) => {
|
.then((result: CommentCreateResponse) => {
|
||||||
|
console.log(result);
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ACTIONS.COMMENT_CREATE_COMPLETED,
|
type: ACTIONS.COMMENT_CREATE_COMPLETED,
|
||||||
data: {
|
data: {
|
||||||
|
@ -400,6 +447,7 @@ export function doCommentCreate(
|
||||||
return result;
|
return result;
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
console.log(error);
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ACTIONS.COMMENT_CREATE_FAILED,
|
type: ACTIONS.COMMENT_CREATE_FAILED,
|
||||||
data: error,
|
data: error,
|
||||||
|
@ -454,7 +502,7 @@ export function doCommentCreate(
|
||||||
}
|
}
|
||||||
|
|
||||||
export function doCommentPin(commentId: string, claimId: string, remove: boolean) {
|
export function doCommentPin(commentId: string, claimId: string, remove: boolean) {
|
||||||
return (dispatch: Dispatch, getState: GetState) => {
|
return async (dispatch: Dispatch, getState: GetState) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const activeChannel = selectActiveChannelClaim(state);
|
const activeChannel = selectActiveChannelClaim(state);
|
||||||
|
|
||||||
|
@ -463,16 +511,25 @@ export function doCommentPin(commentId: string, claimId: string, remove: boolean
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const signedCommentId = await channelSignData(activeChannel.claim_id, commentId);
|
||||||
|
if (!signedCommentId) {
|
||||||
|
return dispatch(doToast({ isError: true, message: __('Unable to verify your channel. Please try again.') }));
|
||||||
|
}
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ACTIONS.COMMENT_PIN_STARTED,
|
type: ACTIONS.COMMENT_PIN_STARTED,
|
||||||
});
|
});
|
||||||
|
|
||||||
return Lbry.comment_pin({
|
const params: CommentPinParams = {
|
||||||
comment_id: commentId,
|
comment_id: commentId,
|
||||||
channel_name: activeChannel.name,
|
|
||||||
channel_id: activeChannel.claim_id,
|
channel_id: activeChannel.claim_id,
|
||||||
...(remove ? { remove: true } : {}),
|
channel_name: activeChannel.name,
|
||||||
})
|
remove: remove,
|
||||||
|
signature: signedCommentId.signature,
|
||||||
|
signing_ts: signedCommentId.signing_ts,
|
||||||
|
};
|
||||||
|
|
||||||
|
return Comments.comment_pin(params)
|
||||||
.then((result: CommentPinResponse) => {
|
.then((result: CommentPinResponse) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ACTIONS.COMMENT_PIN_COMPLETED,
|
type: ACTIONS.COMMENT_PIN_COMPLETED,
|
||||||
|
@ -569,15 +626,30 @@ export function doCommentUpdate(comment_id: string, comment: string) {
|
||||||
if (comment === '') {
|
if (comment === '') {
|
||||||
return doCommentAbandon(comment_id);
|
return doCommentAbandon(comment_id);
|
||||||
} else {
|
} else {
|
||||||
return (dispatch: Dispatch) => {
|
return async (dispatch: Dispatch, getState: GetState) => {
|
||||||
|
const state = getState();
|
||||||
|
|
||||||
|
const activeChannelClaim = selectActiveChannelClaim(state);
|
||||||
|
if (!activeChannelClaim) {
|
||||||
|
return dispatch(doToast({ isError: true, message: __('No active channel selected.') }));
|
||||||
|
}
|
||||||
|
|
||||||
|
const signedComment = await channelSignData(activeChannelClaim.claim_id, comment);
|
||||||
|
if (!signedComment) {
|
||||||
|
return dispatch(doToast({ isError: true, message: __('Unable to verify your channel. Please try again.') }));
|
||||||
|
}
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ACTIONS.COMMENT_UPDATE_STARTED,
|
type: ACTIONS.COMMENT_UPDATE_STARTED,
|
||||||
});
|
});
|
||||||
return Lbry.comment_update({
|
|
||||||
|
return Comments.comment_edit({
|
||||||
comment_id: comment_id,
|
comment_id: comment_id,
|
||||||
comment: comment,
|
comment: comment,
|
||||||
|
signature: signedComment.signature,
|
||||||
|
signing_ts: signedComment.signing_ts,
|
||||||
})
|
})
|
||||||
.then((result: CommentUpdateResponse) => {
|
.then((result: CommentEditResponse) => {
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
dispatch({
|
dispatch({
|
||||||
type: ACTIONS.COMMENT_UPDATE_COMPLETED,
|
type: ACTIONS.COMMENT_UPDATE_COMPLETED,
|
||||||
|
@ -630,6 +702,19 @@ async function channelSignName(channelClaimId: string, channelName: string) {
|
||||||
return signedObject;
|
return signedObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function channelSignData(channelClaimId: string, data: string) {
|
||||||
|
let signedObject;
|
||||||
|
|
||||||
|
try {
|
||||||
|
signedObject = await Lbry.channel_sign({
|
||||||
|
channel_id: channelClaimId,
|
||||||
|
hexdata: toHex(data),
|
||||||
|
});
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
return signedObject;
|
||||||
|
}
|
||||||
|
|
||||||
// Hides a users comments from all creator's claims and prevent them from commenting in the future
|
// Hides a users comments from all creator's claims and prevent them from commenting in the future
|
||||||
function doCommentModToggleBlock(
|
function doCommentModToggleBlock(
|
||||||
unblock: boolean,
|
unblock: boolean,
|
||||||
|
@ -935,7 +1020,7 @@ export function doFetchModBlockedList() {
|
||||||
claimId: blockedChannel.blocked_channel_id,
|
claimId: blockedChannel.blocked_channel_id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!blockedList.find((blockedChannel) => blockedChannel.channelUri === channelUri)) {
|
if (!blockedList.find((blockedChannel) => isURIEqual(blockedChannel.channelUri, channelUri))) {
|
||||||
blockedList.push({ channelUri, blockedAt: blockedChannel.blocked_at });
|
blockedList.push({ channelUri, blockedAt: blockedChannel.blocked_at });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
// @flow
|
// @flow
|
||||||
import * as ACTIONS from 'constants/action_types';
|
import * as ACTIONS from 'constants/action_types';
|
||||||
|
import { SEARCH_OPTIONS } from 'constants/search';
|
||||||
import { buildURI, doResolveUris, batchActions, isURIValid, makeSelectClaimForUri } from 'lbry-redux';
|
import { buildURI, doResolveUris, batchActions, isURIValid, makeSelectClaimForUri } from 'lbry-redux';
|
||||||
import { makeSelectSearchUris, selectSearchValue } from 'redux/selectors/search';
|
import { makeSelectSearchUris, selectSearchValue } from 'redux/selectors/search';
|
||||||
import handleFetchResponse from 'util/handle-fetch';
|
import handleFetchResponse from 'util/handle-fetch';
|
||||||
import { getSearchQueryString } from 'util/query-params';
|
import { getSearchQueryString } from 'util/query-params';
|
||||||
|
import { SIMPLE_SITE } from 'config';
|
||||||
|
|
||||||
type Dispatch = (action: any) => any;
|
type Dispatch = (action: any) => any;
|
||||||
type GetState = () => { search: SearchState };
|
type GetState = () => { search: SearchState };
|
||||||
|
@ -130,6 +132,11 @@ export const doFetchRecommendedContent = (uri: string, mature: boolean) => (disp
|
||||||
if (!mature) {
|
if (!mature) {
|
||||||
options['nsfw'] = false;
|
options['nsfw'] = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (SIMPLE_SITE) {
|
||||||
|
options[SEARCH_OPTIONS.CLAIM_TYPE] = SEARCH_OPTIONS.INCLUDE_FILES;
|
||||||
|
options[SEARCH_OPTIONS.MEDIA_VIDEO] = true;
|
||||||
|
}
|
||||||
const { title } = claim.value;
|
const { title } = claim.value;
|
||||||
if (title && options) {
|
if (title && options) {
|
||||||
dispatch(doSearch(title, options));
|
dispatch(doSearch(title, options));
|
||||||
|
|
|
@ -2,8 +2,7 @@
|
||||||
import * as ACTIONS from 'constants/action_types';
|
import * as ACTIONS from 'constants/action_types';
|
||||||
import { handleActions } from 'util/redux-utils';
|
import { handleActions } from 'util/redux-utils';
|
||||||
import { BLOCK_LEVEL } from 'constants/comment';
|
import { BLOCK_LEVEL } from 'constants/comment';
|
||||||
|
import { isURIEqual } from 'lbry-redux';
|
||||||
const IS_DEV = process.env.NODE_ENV !== 'production';
|
|
||||||
|
|
||||||
const defaultState: CommentsState = {
|
const defaultState: CommentsState = {
|
||||||
commentById: {}, // commentId -> Comment
|
commentById: {}, // commentId -> Comment
|
||||||
|
@ -183,12 +182,15 @@ export default handleActions(
|
||||||
},
|
},
|
||||||
|
|
||||||
[ACTIONS.COMMENT_REACTION_LIST_COMPLETED]: (state: CommentsState, action: any): CommentsState => {
|
[ACTIONS.COMMENT_REACTION_LIST_COMPLETED]: (state: CommentsState, action: any): CommentsState => {
|
||||||
const { myReactions, othersReactions, channelId } = action.data;
|
const { myReactions, othersReactions, channelId, commentIds } = action.data;
|
||||||
const myReacts = Object.assign({}, state.myReactsByCommentId);
|
const myReacts = Object.assign({}, state.myReactsByCommentId);
|
||||||
const othersReacts = Object.assign({}, state.othersReactsByCommentId);
|
const othersReacts = Object.assign({}, state.othersReactsByCommentId);
|
||||||
|
|
||||||
if (myReactions) {
|
const myReactionsEntries = myReactions ? Object.entries(myReactions) : [];
|
||||||
Object.entries(myReactions).forEach(([commentId, reactions]) => {
|
const othersReactionsEntries = othersReactions ? Object.entries(othersReactions) : [];
|
||||||
|
|
||||||
|
if (myReactionsEntries.length > 0) {
|
||||||
|
myReactionsEntries.forEach(([commentId, reactions]) => {
|
||||||
const key = channelId ? `${commentId}:${channelId}` : commentId;
|
const key = channelId ? `${commentId}:${channelId}` : commentId;
|
||||||
myReacts[key] = Object.entries(reactions).reduce((acc, [name, count]) => {
|
myReacts[key] = Object.entries(reactions).reduce((acc, [name, count]) => {
|
||||||
if (count === 1) {
|
if (count === 1) {
|
||||||
|
@ -197,13 +199,23 @@ export default handleActions(
|
||||||
return acc;
|
return acc;
|
||||||
}, []);
|
}, []);
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
commentIds.forEach((commentId) => {
|
||||||
|
const key = channelId ? `${commentId}:${channelId}` : commentId;
|
||||||
|
myReacts[key] = [];
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (othersReactions) {
|
if (othersReactionsEntries.length > 0) {
|
||||||
Object.entries(othersReactions).forEach(([commentId, reactions]) => {
|
othersReactionsEntries.forEach(([commentId, reactions]) => {
|
||||||
const key = channelId ? `${commentId}:${channelId}` : commentId;
|
const key = channelId ? `${commentId}:${channelId}` : commentId;
|
||||||
othersReacts[key] = reactions;
|
othersReacts[key] = reactions;
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
commentIds.forEach((commentId) => {
|
||||||
|
const key = channelId ? `${commentId}:${channelId}` : commentId;
|
||||||
|
othersReacts[key] = {};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -276,6 +288,15 @@ export default handleActions(
|
||||||
const totalRepliesByParentId = Object.assign({}, state.totalRepliesByParentId);
|
const totalRepliesByParentId = Object.assign({}, state.totalRepliesByParentId);
|
||||||
const isLoadingByParentId = Object.assign({}, state.isLoadingByParentId);
|
const isLoadingByParentId = Object.assign({}, state.isLoadingByParentId);
|
||||||
|
|
||||||
|
if (!parentId) {
|
||||||
|
totalCommentsById[claimId] = totalItems;
|
||||||
|
topLevelTotalCommentsById[claimId] = totalFilteredItems;
|
||||||
|
topLevelTotalPagesById[claimId] = totalPages;
|
||||||
|
} else {
|
||||||
|
totalRepliesByParentId[parentId] = totalFilteredItems;
|
||||||
|
isLoadingByParentId[parentId] = false;
|
||||||
|
}
|
||||||
|
|
||||||
const commonUpdateAction = (comment, commentById, commentIds, index) => {
|
const commonUpdateAction = (comment, commentById, commentIds, index) => {
|
||||||
// map the comment_ids to the new comments
|
// map the comment_ids to the new comments
|
||||||
commentById[comment.comment_id] = comment;
|
commentById[comment.comment_id] = comment;
|
||||||
|
@ -288,46 +309,19 @@ export default handleActions(
|
||||||
// sort comments by their timestamp
|
// sort comments by their timestamp
|
||||||
const commentIds = Array(comments.length);
|
const commentIds = Array(comments.length);
|
||||||
|
|
||||||
// totalCommentsById[claimId] = totalItems;
|
|
||||||
// --> currently, this value is only correct when done via a top-level query.
|
|
||||||
// Until this is fixed, I'm moving it downwards to **
|
|
||||||
|
|
||||||
// --- Top-level comments ---
|
// --- Top-level comments ---
|
||||||
if (!parentId) {
|
if (!parentId) {
|
||||||
totalCommentsById[claimId] = totalItems; // **
|
|
||||||
|
|
||||||
topLevelTotalCommentsById[claimId] = totalFilteredItems;
|
|
||||||
topLevelTotalPagesById[claimId] = totalPages;
|
|
||||||
|
|
||||||
if (!topLevelCommentsById[claimId]) {
|
|
||||||
topLevelCommentsById[claimId] = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const topLevelCommentIds = topLevelCommentsById[claimId];
|
|
||||||
|
|
||||||
for (let i = 0; i < comments.length; ++i) {
|
for (let i = 0; i < comments.length; ++i) {
|
||||||
const comment = comments[i];
|
const comment = comments[i];
|
||||||
commonUpdateAction(comment, commentById, commentIds, i);
|
commonUpdateAction(comment, commentById, commentIds, i);
|
||||||
|
pushToArrayInObject(topLevelCommentsById, claimId, comment.comment_id);
|
||||||
if (IS_DEV && comment['parent_id']) console.error('Invalid top-level comment:', comment); // eslint-disable-line
|
|
||||||
|
|
||||||
if (!topLevelCommentIds.includes(comment.comment_id)) {
|
|
||||||
topLevelCommentIds.push(comment.comment_id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// --- Replies ---
|
// --- Replies ---
|
||||||
else {
|
else {
|
||||||
totalRepliesByParentId[parentId] = totalFilteredItems;
|
|
||||||
isLoadingByParentId[parentId] = false;
|
|
||||||
|
|
||||||
for (let i = 0; i < comments.length; ++i) {
|
for (let i = 0; i < comments.length; ++i) {
|
||||||
const comment = comments[i];
|
const comment = comments[i];
|
||||||
commonUpdateAction(comment, commentById, commentIds, i);
|
commonUpdateAction(comment, commentById, commentIds, i);
|
||||||
|
|
||||||
if (IS_DEV && !comment['parent_id']) console.error('Missing parent_id:', comment); // eslint-disable-line
|
|
||||||
if (IS_DEV && comment.parent_id !== parentId) console.error('Black sheep in the family?:', comment); // eslint-disable-line
|
|
||||||
|
|
||||||
pushToArrayInObject(repliesByParentId, parentId, comment.comment_id);
|
pushToArrayInObject(repliesByParentId, parentId, comment.comment_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -806,7 +800,7 @@ export default handleActions(
|
||||||
for (const commentId in commentById) {
|
for (const commentId in commentById) {
|
||||||
const comment = commentById[commentId];
|
const comment = commentById[commentId];
|
||||||
|
|
||||||
if (blockedUri === comment.channel_url) {
|
if (isURIEqual(blockedUri, comment.channel_url)) {
|
||||||
delete commentById[comment.comment_id];
|
delete commentById[comment.comment_id];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,12 +3,12 @@ import moment from 'moment';
|
||||||
import { ACTIONS as LBRY_REDUX_ACTIONS, SETTINGS, SHARED_PREFERENCES } from 'lbry-redux';
|
import { ACTIONS as LBRY_REDUX_ACTIONS, SETTINGS, SHARED_PREFERENCES } from 'lbry-redux';
|
||||||
import { getSubsetFromKeysArray } from 'util/sync-settings';
|
import { getSubsetFromKeysArray } from 'util/sync-settings';
|
||||||
import { getDefaultLanguage } from 'util/default-languages';
|
import { getDefaultLanguage } from 'util/default-languages';
|
||||||
import { UNSYNCED_SETTINGS } from 'config';
|
import { UNSYNCED_SETTINGS, SIMPLE_SITE } from 'config';
|
||||||
|
|
||||||
const { CLIENT_SYNC_KEYS } = SHARED_PREFERENCES;
|
const { CLIENT_SYNC_KEYS } = SHARED_PREFERENCES;
|
||||||
const settingsToIgnore = (UNSYNCED_SETTINGS && UNSYNCED_SETTINGS.trim().split(' ')) || [];
|
const settingsToIgnore = (UNSYNCED_SETTINGS && UNSYNCED_SETTINGS.trim().split(' ')) || [];
|
||||||
const clientSyncKeys = settingsToIgnore.length
|
const clientSyncKeys = settingsToIgnore.length
|
||||||
? CLIENT_SYNC_KEYS.filter(k => !settingsToIgnore.includes(k))
|
? CLIENT_SYNC_KEYS.filter((k) => !settingsToIgnore.includes(k))
|
||||||
: CLIENT_SYNC_KEYS;
|
: CLIENT_SYNC_KEYS;
|
||||||
|
|
||||||
const reducers = {};
|
const reducers = {};
|
||||||
|
@ -70,7 +70,7 @@ const defaultState = {
|
||||||
[SETTINGS.AUTOPLAY_NEXT]: true,
|
[SETTINGS.AUTOPLAY_NEXT]: true,
|
||||||
[SETTINGS.FLOATING_PLAYER]: true,
|
[SETTINGS.FLOATING_PLAYER]: true,
|
||||||
[SETTINGS.AUTO_DOWNLOAD]: true,
|
[SETTINGS.AUTO_DOWNLOAD]: true,
|
||||||
[SETTINGS.HIDE_REPOSTS]: false,
|
[SETTINGS.HIDE_REPOSTS]: SIMPLE_SITE,
|
||||||
|
|
||||||
// OS
|
// OS
|
||||||
[SETTINGS.AUTO_LAUNCH]: true,
|
[SETTINGS.AUTO_LAUNCH]: true,
|
||||||
|
@ -89,12 +89,12 @@ reducers[ACTIONS.REHYDRATE] = (state, action) => {
|
||||||
return Object.assign({}, state, { clientSettings });
|
return Object.assign({}, state, { clientSettings });
|
||||||
};
|
};
|
||||||
|
|
||||||
reducers[ACTIONS.FINDING_FFMPEG_STARTED] = state =>
|
reducers[ACTIONS.FINDING_FFMPEG_STARTED] = (state) =>
|
||||||
Object.assign({}, state, {
|
Object.assign({}, state, {
|
||||||
findingFFmpeg: true,
|
findingFFmpeg: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
reducers[ACTIONS.FINDING_FFMPEG_COMPLETED] = state =>
|
reducers[ACTIONS.FINDING_FFMPEG_COMPLETED] = (state) =>
|
||||||
Object.assign({}, state, {
|
Object.assign({}, state, {
|
||||||
findingFFmpeg: false,
|
findingFFmpeg: false,
|
||||||
});
|
});
|
||||||
|
@ -120,7 +120,7 @@ reducers[ACTIONS.CLIENT_SETTING_CHANGED] = (state, action) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
reducers[ACTIONS.UPDATE_IS_NIGHT] = state => {
|
reducers[ACTIONS.UPDATE_IS_NIGHT] = (state) => {
|
||||||
const { from, to } = state.clientSettings[SETTINGS.DARK_MODE_TIMES];
|
const { from, to } = state.clientSettings[SETTINGS.DARK_MODE_TIMES];
|
||||||
const momentNow = moment();
|
const momentNow = moment();
|
||||||
const startNightMoment = moment(from.formattedTime, 'HH:mm');
|
const startNightMoment = moment(from.formattedTime, 'HH:mm');
|
||||||
|
@ -155,7 +155,7 @@ reducers[LBRY_REDUX_ACTIONS.SHARED_PREFERENCE_SET] = (state, action) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
reducers[ACTIONS.SYNC_CLIENT_SETTINGS] = state => {
|
reducers[ACTIONS.SYNC_CLIENT_SETTINGS] = (state) => {
|
||||||
const { clientSettings } = state;
|
const { clientSettings } = state;
|
||||||
const sharedPreferences = Object.assign({}, state.sharedPreferences);
|
const sharedPreferences = Object.assign({}, state.sharedPreferences);
|
||||||
const selectedClientSettings = getSubsetFromKeysArray(clientSettings, clientSyncKeys);
|
const selectedClientSettings = getSubsetFromKeysArray(clientSettings, clientSyncKeys);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// @flow
|
// @flow
|
||||||
import * as ACTIONS from 'constants/action_types';
|
import * as ACTIONS from 'constants/action_types';
|
||||||
import { parseURI, ACTIONS as LBRY_REDUX_ACTIONS } from 'lbry-redux';
|
import { parseURI, normalizeURI, isURIEqual, ACTIONS as LBRY_REDUX_ACTIONS } from 'lbry-redux';
|
||||||
import { handleActions } from 'util/redux-utils';
|
import { handleActions } from 'util/redux-utils';
|
||||||
|
|
||||||
const defaultState: SubscriptionState = {
|
const defaultState: SubscriptionState = {
|
||||||
|
@ -17,21 +17,21 @@ export default handleActions(
|
||||||
const newSubscriptions: Array<Subscription> = state.subscriptions.slice();
|
const newSubscriptions: Array<Subscription> = state.subscriptions.slice();
|
||||||
let newFollowing: Array<Following> = state.following.slice();
|
let newFollowing: Array<Following> = state.following.slice();
|
||||||
// prevent duplicates in the sidebar
|
// prevent duplicates in the sidebar
|
||||||
if (!newSubscriptions.some((sub) => sub.uri === newSubscription.uri)) {
|
if (!newSubscriptions.some((sub) => isURIEqual(sub.uri, newSubscription.uri))) {
|
||||||
// $FlowFixMe
|
// $FlowFixMe
|
||||||
newSubscriptions.unshift(newSubscription);
|
newSubscriptions.unshift(newSubscription);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!newFollowing.some((sub) => sub.uri === newSubscription.uri)) {
|
if (!newFollowing.some((sub) => isURIEqual(sub.uri, newSubscription.uri))) {
|
||||||
newFollowing.unshift({
|
newFollowing.unshift({
|
||||||
uri: newSubscription.uri,
|
uri: newSubscription.uri,
|
||||||
notificationsDisabled: newSubscription.notificationsDisabled,
|
notificationsDisabled: newSubscription.notificationsDisabled,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
newFollowing = newFollowing.map((following) => {
|
newFollowing = newFollowing.map((following) => {
|
||||||
if (following.uri === newSubscription.uri) {
|
if (isURIEqual(following.uri, newSubscription.uri)) {
|
||||||
return {
|
return {
|
||||||
uri: newSubscription.uri,
|
uri: normalizeURI(newSubscription.uri),
|
||||||
notificationsDisabled: newSubscription.notificationsDisabled,
|
notificationsDisabled: newSubscription.notificationsDisabled,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -5,6 +5,7 @@ import {
|
||||||
makeSelectChannelForClaimUri,
|
makeSelectChannelForClaimUri,
|
||||||
parseURI,
|
parseURI,
|
||||||
makeSelectClaimForUri,
|
makeSelectClaimForUri,
|
||||||
|
isURIEqual,
|
||||||
} from 'lbry-redux';
|
} from 'lbry-redux';
|
||||||
import { swapKeyAndValue } from 'util/swap-json';
|
import { swapKeyAndValue } from 'util/swap-json';
|
||||||
|
|
||||||
|
@ -112,7 +113,7 @@ export const makeSelectIsSubscribed = (uri) =>
|
||||||
makeSelectClaimForUri(uri),
|
makeSelectClaimForUri(uri),
|
||||||
(subscriptions, channelUri, claim) => {
|
(subscriptions, channelUri, claim) => {
|
||||||
if (channelUri) {
|
if (channelUri) {
|
||||||
return subscriptions.some((sub) => sub.uri === channelUri);
|
return subscriptions.some((sub) => isURIEqual(sub.uri, channelUri));
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we couldn't get a channel uri from the claim uri, the uri passed in might be a channel already
|
// If we couldn't get a channel uri from the claim uri, the uri passed in might be a channel already
|
||||||
|
@ -123,11 +124,11 @@ export const makeSelectIsSubscribed = (uri) =>
|
||||||
|
|
||||||
if (isChannel && claim) {
|
if (isChannel && claim) {
|
||||||
const uri = claim.permanent_url;
|
const uri = claim.permanent_url;
|
||||||
return subscriptions.some((sub) => sub.uri === uri);
|
return subscriptions.some((sub) => isURIEqual(sub.uri, uri));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isChannel && !claim) {
|
if (isChannel && !claim) {
|
||||||
return subscriptions.some((sub) => sub.uri === uri);
|
return subscriptions.some((sub) => isURIEqual(sub.uri, uri));
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -620,6 +620,12 @@ svg + .button__label {
|
||||||
border-top-left-radius: 0;
|
border-top-left-radius: 0;
|
||||||
border-bottom-left-radius: 0;
|
border-bottom-left-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.button--file-action {
|
||||||
|
&:first-child {
|
||||||
|
margin-right: var(--spacing-s);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.button--file-action {
|
.button--file-action {
|
||||||
|
|
|
@ -226,6 +226,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card__title-actions--link {
|
||||||
|
margin-top: var(--spacing-xs);
|
||||||
|
margin-right: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
|
||||||
.card__title-actions--small {
|
.card__title-actions--small {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -242,6 +242,10 @@
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
|
.claim-preview__actions {
|
||||||
|
margin-left: var(--spacing-m);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: $breakpoint-xsmall) {
|
@media (max-width: $breakpoint-xsmall) {
|
||||||
|
@ -699,6 +703,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.claim-preview__active {
|
||||||
|
background-color: var(--color-card-background-highlighted);
|
||||||
|
}
|
||||||
|
|
||||||
.claim-preview__live {
|
.claim-preview__live {
|
||||||
.claim-preview__file-property-overlay {
|
.claim-preview__file-property-overlay {
|
||||||
opacity: 1; // The original 0.7 is not visible over bright thumbnails
|
opacity: 1; // The original 0.7 is not visible over bright thumbnails
|
||||||
|
|
|
@ -4,6 +4,11 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.collection__subtitle {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: var(--spacing-s);
|
||||||
|
}
|
||||||
|
|
||||||
.collection-preview__items {
|
.collection-preview__items {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
@ -100,6 +100,11 @@
|
||||||
max-height: none;
|
max-height: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.file-render--img-container {
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
}
|
||||||
|
|
||||||
.file-render__header {
|
.file-render__header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue