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