Merge branch 'master' into accessibility

This commit is contained in:
Baltazar Gomez 2021-07-20 12:24:36 -05:00 committed by GitHub
commit 0db4e4ab51
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
110 changed files with 6688 additions and 1056 deletions

View file

@ -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

View file

@ -1,6 +1,7 @@
[ignore] [ignore]
.*\.typeface\.json .*\.typeface\.json
.*/node_modules/findup/.* .*/node_modules/findup/.*
.*/node_modules/react-plastic/.*
[include] [include]

View file

@ -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
View file

@ -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

View file

@ -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

View file

@ -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
View file

@ -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,

View file

@ -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,

View file

@ -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",

View file

@ -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--"
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -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 {

View file

@ -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),

View file

@ -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}>

View file

@ -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}

View file

@ -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.
/> />

View file

@ -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}

View file

@ -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}

View file

@ -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>

View file

@ -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>
)} )}

View file

@ -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 ? (

View file

@ -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)

View file

@ -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),
}; };
}; };

View file

@ -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')} />}
/> />
); );
} }

View file

@ -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),

View file

@ -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">

View file

@ -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);

View file

@ -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

View file

@ -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 {

View file

@ -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),
})} })}

View file

@ -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,
}; };
}; };

View file

@ -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}
/> />
); );
})} })}

View file

@ -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">

View file

@ -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) {

View file

@ -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" />

View file

@ -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>
)}
</> </>
); );

View file

@ -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);

View file

@ -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" />}

View file

@ -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',
}); });

View file

@ -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} />

View file

@ -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>
)} )}

View file

@ -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)}
/> />
))} ))}

View file

@ -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);
} }
}) })

View file

@ -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;

View file

@ -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() {

View file

@ -0,0 +1,2 @@
import OptimizedImage from './view';
export default OptimizedImage;

View 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;

View file

@ -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

View file

@ -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 (

View file

@ -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;

View file

@ -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 && (

View file

@ -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 =

View file

@ -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) {

View file

@ -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,

View file

@ -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');

View file

@ -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)}
/> />

View file

@ -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),
}); });

View file

@ -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);

View file

@ -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>
</>
)}
</> </>
); );
} }

View file

@ -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 */}

View file

@ -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,
}} }}
> >

View file

@ -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;

View file

@ -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';

View file

@ -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'],

View file

@ -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';

View file

@ -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';

View file

@ -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')

View file

@ -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);
} }

View file

@ -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>

View 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);

View 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>
);
}

View file

@ -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;
} }

View file

@ -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>
); );
} }

View file

@ -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">

View file

@ -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">

View file

@ -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}

View file

@ -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>

View file

@ -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>
); );

View file

@ -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,

View file

@ -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} />

View file

@ -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>
); );

View file

@ -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) => ({

View file

@ -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>

View file

@ -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));

View 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;

View file

@ -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);

View file

@ -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()} &nbsp; Last 4: {userCardDetails.lastFour} &nbsp; type={userCardDetails.brand}
Expires: {userCardDetails.expiryMonth}/{userCardDetails.expiryYear} &nbsp; 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>
); );
} }

View file

@ -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>
)} )}

View file

@ -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);
} }
}; };
} }

View file

@ -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 });
} }

View file

@ -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));

View file

@ -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];
} }
} }

View file

@ -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);

View file

@ -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 {

View file

@ -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;

View file

@ -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 {

View file

@ -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;
} }

View file

@ -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

View file

@ -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;

View file

@ -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