Merge branch 'master' into accessibility

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

View file

@ -6,7 +6,7 @@ MATOMO_ID=4
WEBPACK_WEB_PORT=9090
WEBPACK_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

View file

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

View file

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

@ -33,3 +33,6 @@ package-lock.json
!/custom/robots.disallowall
!/custom/robots.allowall
.env
.env.ody
.env.desktop
.env.lbrytv

View file

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

View file

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

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

View file

@ -17,6 +17,7 @@ declare type RowDataItem = {
help?: any,
icon?: string,
extra?: any,
pinUrls?: Array<string>,
options?: {
channelIds?: Array<string>,
limitClaimsPerChannel?: number,

View file

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

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -7,12 +7,12 @@
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
<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 {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,108 @@
// @flow
import React from 'react';
import { getThumbnailCdnUrl } from 'util/thumbnail';
function scaleToDevicePixelRatio(value: number, window: any) {
const devicePixelRatio = window.devicePixelRatio || 1.0;
return Math.ceil(value * devicePixelRatio);
}
type Props = {
src: string,
objectFit?: string,
};
function OptimizedImage(props: Props) {
const { objectFit, src, ...imgProps } = props;
const [optimizedSrc, setOptimizedSrc] = React.useState('');
const ref = React.useRef<any>();
function getOptimizedImgUrl(url, width, height) {
let optimizedUrl = url;
if (url && !url.startsWith('/public/')) {
optimizedUrl = url.trim().replace(/^http:\/\//i, 'https://');
// @if TARGET='web'
if (!optimizedUrl.endsWith('.gif')) {
optimizedUrl = getThumbnailCdnUrl({ thumbnail: optimizedUrl, width, height, quality: 85 });
}
// @endif
}
return optimizedUrl;
}
function getOptimumSize(elem) {
if (!elem || !elem.parentElement || !elem.parentElement.clientWidth || !elem.parentElement.clientHeight) {
return null;
}
let width = elem.parentElement.clientWidth;
let height = elem.parentElement.clientHeight;
width = scaleToDevicePixelRatio(width, window);
height = scaleToDevicePixelRatio(height, window);
// Round to next 100px for better caching
width = Math.ceil(width / 100) * 100;
height = Math.ceil(height / 100) * 100;
// Reminder: CDN expects integers.
return { width, height };
}
function adjustOptimizationIfNeeded(elem, objectFit, origSrc) {
if (objectFit === 'cover' && elem) {
const containerSize = getOptimumSize(elem);
if (containerSize) {
// $FlowFixMe
if (elem.naturalWidth < containerSize.width) {
// For 'cover', we don't want to stretch the image. We started off by
// filling up the container height, but the width still has a gap for
// this instance (usually due to aspect ratio mismatch).
// If the original image is much larger, we can request for a larger
// image so that "objectFit=cover" will center it without stretching and
// making it blur. The double fetch might seem wasteful, but on
// average the total transferred bytes is still less than the original.
const probablyMaxedOut = elem.naturalHeight < containerSize.height;
if (!probablyMaxedOut) {
const newOptimizedSrc = getOptimizedImgUrl(origSrc, containerSize.width, 0);
if (newOptimizedSrc && newOptimizedSrc !== optimizedSrc) {
setOptimizedSrc(newOptimizedSrc);
}
}
}
}
}
}
React.useEffect(() => {
const containerSize = getOptimumSize(ref.current);
if (containerSize) {
const width = 0; // The CDN will fill the zeroed attribute per image's aspect ratio.
const height = containerSize.height;
const newOptimizedSrc = getOptimizedImgUrl(src, width, height);
if (newOptimizedSrc !== optimizedSrc) {
setOptimizedSrc(newOptimizedSrc);
}
} else {
setOptimizedSrc(src);
}
}, []); // eslint-disable-line react-hooks/exhaustive-deps
if (!src) {
return null;
}
return (
<img
ref={ref}
{...imgProps}
src={optimizedSrc}
onLoad={() => adjustOptimizationIfNeeded(ref.current, objectFit, src)}
/>
);
}
export default OptimizedImage;

View file

@ -69,6 +69,7 @@ const RewardsVerifyPage = lazyImport(() => import('page/rewardsVerify' /* webpac
const SearchPage = lazyImport(() => import('page/search' /* webpackChunkName: "secondary" */));
const 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

View file

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

View file

@ -1,260 +0,0 @@
// @flow
import * as ICONS from 'constants/icons';
import React from 'react';
import Button from 'component/button';
import Card from 'component/common/card';
import { Lbryio } from 'lbryinc';
import { URL, WEBPACK_WEB_PORT, STRIPE_PUBLIC_KEY } from 'config';
const isDev = process.env.NODE_ENV !== 'production';
let stripeEnvironment = 'test';
// if the key contains pk_live it's a live key
// update the environment for the calls to the backend to indicate which environment to hit
if (STRIPE_PUBLIC_KEY.indexOf('pk_live') > -1) {
stripeEnvironment = 'live';
}
let successStripeRedirectUrl, failureStripeRedirectUrl;
let successEndpoint = '/$/wallet';
let failureEndpoint = '/$/wallet';
if (isDev) {
successStripeRedirectUrl = 'http://localhost:' + WEBPACK_WEB_PORT + successEndpoint;
failureStripeRedirectUrl = 'http://localhost:' + WEBPACK_WEB_PORT + failureEndpoint;
} else {
successStripeRedirectUrl = URL + successEndpoint;
failureStripeRedirectUrl = URL + failureEndpoint;
}
type Props = {
source: string,
user: User,
};
type State = {
error: boolean,
loading: boolean,
content: ?string,
stripeConnectionUrl: string,
// alreadyUpdated: boolean,
accountConfirmed: boolean,
accountPendingConfirmation: boolean,
accountNotConfirmedButReceivedTips: boolean,
unpaidBalance: number,
};
class StripeAccountConnection extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
error: false,
content: null,
loading: true,
accountConfirmed: false,
accountPendingConfirmation: false,
accountNotConfirmedButReceivedTips: false,
unpaidBalance: 0,
stripeConnectionUrl: '',
// alreadyUpdated: false,
};
}
componentDidMount() {
const { user } = this.props;
// $FlowFixMe
this.experimentalUiEnabled = user && user.experimental_ui;
var that = this;
function getAndSetAccountLink(stillNeedToConfirmAccount) {
Lbryio.call(
'account',
'link',
{
return_url: successStripeRedirectUrl,
refresh_url: failureStripeRedirectUrl,
environment: stripeEnvironment,
},
'post'
).then((accountLinkResponse) => {
// stripe link for user to navigate to and confirm account
const stripeConnectionUrl = accountLinkResponse.url;
// set connection url on frontend
that.setState({
stripeConnectionUrl,
});
// show the account confirmation link if not created already
if (stillNeedToConfirmAccount) {
that.setState({
accountPendingConfirmation: true,
});
}
});
}
// call the account status endpoint
Lbryio.call(
'account',
'status',
{
environment: stripeEnvironment,
},
'post'
)
.then((accountStatusResponse) => {
const yetToBeCashedOutBalance = accountStatusResponse.total_received_unpaid;
if (yetToBeCashedOutBalance) {
that.setState({
unpaidBalance: yetToBeCashedOutBalance,
});
}
// if charges already enabled, no need to generate an account link
if (accountStatusResponse.charges_enabled) {
// account has already been confirmed
that.setState({
accountConfirmed: true,
});
// user has not confirmed an account but have received payments
} else if (accountStatusResponse.total_received_unpaid > 0) {
that.setState({
accountNotConfirmedButReceivedTips: true,
});
getAndSetAccountLink();
// user has not received any amount or confirmed an account
} else {
// get stripe link and set it on the frontend
// pass true so it updates the frontend
getAndSetAccountLink(true);
}
})
.catch(function (error) {
// errorString passed from the API (with a 403 error)
const errorString = 'account not linked to user, please link first';
// if it's beamer's error indicating the account is not linked yet
if (error.message.indexOf(errorString) > -1) {
// get stripe link and set it on the frontend
getAndSetAccountLink();
} else {
// not an error from Beamer, throw it
throw new Error(error);
}
});
}
render() {
const {
stripeConnectionUrl,
accountConfirmed,
accountPendingConfirmation,
unpaidBalance,
accountNotConfirmedButReceivedTips,
} = this.state;
const { user } = this.props;
if (user.fiat_enabled) {
return (
<Card
title={<div className="table__header-text">{__('Connect a bank account')}</div>}
isBodyList
body={
<div>
{/* show while waiting for account status */}
{!accountConfirmed && !accountPendingConfirmation && !accountNotConfirmedButReceivedTips && (
<div className="card__body-actions">
<div>
<div>
<h3>{__('Getting your bank account connection status...')}</h3>
</div>
</div>
</div>
)}
{/* user has yet to complete their integration */}
{!accountConfirmed && accountPendingConfirmation && (
<div className="card__body-actions">
<div>
<div>
<h3>{__('Connect your bank account to Odysee to receive donations directly from users')}</h3>
</div>
<div className="section__actions">
<a href={stripeConnectionUrl}>
<Button button="secondary" label={__('Connect your bank account')} icon={ICONS.FINANCE} />
</a>
</div>
</div>
</div>
)}
{/* user has completed their integration */}
{accountConfirmed && (
<div className="card__body-actions">
<div>
<div>
<h3>{__('Congratulations! Your account has been connected with Odysee.')}</h3>
{unpaidBalance > 0 ? (
<div>
<br />
<h3>
{__(
'Your account balance is %balance% USD. Functionality to view your transactions and withdraw your balance will be landing shortly.',
{ balance: unpaidBalance / 100 }
)}
</h3>
</div>
) : (
<div>
<br />
<h3>{__('Your account balance is $0 USD. When you receive a tip you will see it here.')}</h3>
</div>
)}
</div>
</div>
</div>
)}
{accountNotConfirmedButReceivedTips && (
<div className="card__body-actions">
<div>
<div>
<h3>{__('Congratulations, you have already begun receiving tips on Odysee!')}</h3>
<div>
<br />
<h3>
{__(
'Your pending account balance is %balance% USD. Functionality to view and receive your transactions will land soon.',
{ balance: unpaidBalance / 100 }
)}
</h3>
</div>
<br />
<div>
<h3>
{__('Connect your bank account to be able to cash your pending balance out to your account.')}
</h3>
</div>
<div className="section__actions">
<a href={stripeConnectionUrl}>
<Button button="secondary" label={__('Connect your bank account')} icon={ICONS.FINANCE} />
</a>
</div>
</div>
</div>
</div>
)}
</div>
}
/>
);
} else {
return <></>; // probably null;
}
}
}
export default StripeAccountConnection;

View file

@ -6,7 +6,7 @@ import Nag from 'component/common/nag';
import { parseURI } from 'lbry-redux';
import 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 && (

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -45,3 +45,4 @@ export const VIEW_IMAGE = 'view_image';
export const CONFIRM_REMOVE_BTC_SWAP_ADDRESS = 'confirm_remove_btc_swap_address';
export const COLLECTION_ADD = 'collection_add';
export const COLLECTION_DELETE = 'collection_delete';
export const CONFIRM_REMOVE_CARD = 'CONFIRM_REMOVE_CARD';

View file

@ -39,6 +39,7 @@ exports.REPOST_NEW = 'repost';
exports.SEND = 'send';
exports.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';

View file

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

View file

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

View file

@ -44,7 +44,7 @@ class ModalPublishSuccess extends React.PureComponent<Props> {
'Your livestream is now pending. You will be able to start shortly at the streaming dashboard.'
);
} 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>

View file

@ -0,0 +1,19 @@
import { connect } from 'react-redux';
import { doHideModal } from 'redux/actions/app';
import { doAbandonTxo, doAbandonClaim, selectTransactionItems, doResolveUri } from 'lbry-redux';
import { doToast } from 'redux/actions/notifications';
import ModalRevokeClaim from './view';
const select = state => ({
transactionItems: selectTransactionItems(state),
});
const perform = dispatch => ({
toast: (message, isError) => dispatch(doToast({ message, isError })),
closeModal: () => dispatch(doHideModal()),
abandonTxo: (txo, cb) => dispatch(doAbandonTxo(txo, cb)),
abandonClaim: (txid, nout, cb) => dispatch(doAbandonClaim(txid, nout, cb)),
doResolveUri: (uri) => dispatch(doResolveUri(uri)),
});
export default connect(select, perform)(ModalRevokeClaim);

View file

@ -0,0 +1,83 @@
// @flow
import React from 'react';
import { Modal } from 'modal/modal';
import Card from 'component/common/card';
import Button from 'component/button';
import * as ICONS from 'constants/icons';
import { Lbryio } from 'lbryinc';
import { STRIPE_PUBLIC_KEY } from 'config';
let stripeEnvironment = 'test';
// if the key contains pk_live it's a live key
// update the environment for the calls to the backend to indicate which environment to hit
if (STRIPE_PUBLIC_KEY.indexOf('pk_live') > -1) {
stripeEnvironment = 'live';
}
type Props = {
closeModal: () => void,
abandonTxo: (Txo, () => void) => void,
abandonClaim: (string, number, ?() => void) => void,
tx: Txo,
claim: GenericClaim,
cb: () => void,
doResolveUri: (string) => void,
uri: string,
paymentMethodId: string,
setAsConfirmingCard: () => void,
};
export default function ModalRevokeClaim(props: Props) {
var that = this;
console.log(that);
console.log(props);
const { closeModal, uri, paymentMethodId, setAsConfirmingCard } = props;
console.log(uri);
console.log(setAsConfirmingCard);
function removeCard() {
console.log(paymentMethodId);
Lbryio.call(
'customer',
'detach',
{
environment: stripeEnvironment,
payment_method_id: paymentMethodId,
},
'post'
).then((removeCardResponse) => {
console.log(removeCardResponse);
// TODO: add toast here
// closeModal();
window.location.reload();
});
}
return (
<Modal ariaHideApp={false} isOpen contentLabel={'hello'} type="card" onAborted={closeModal}>
<Card
title={'Confirm Remove Card'}
// body={getMsgBody(type, isSupport, name)}
actions={
<div className="section__actions">
<Button
className="stripe__confirm-remove-card"
button="secondary"
icon={ICONS.DELETE}
label={'Remove Card'}
onClick={removeCard}
/>
<Button button="link" label={__('Cancel')} onClick={closeModal} />
</div>
}
/>
</Modal>
);
}

View file

@ -29,6 +29,7 @@ const ModalPhoneCollection = lazyImport(() => import('modal/modalPhoneCollection
const ModalPublish = lazyImport(() => import('modal/modalPublish' /* webpackChunkName: "modalPublish" */));
const 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;
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -115,6 +115,18 @@ function FilePage(props: Props) {
);
}
if (renderMode === RENDER_MODES.IMAGE) {
return (
<React.Fragment>
<div className="file-render--img-container">
<FileRenderInitiator uri={uri} />
<FileRenderInline uri={uri} />
</div>
<FileTitleSection uri={uri} />
</React.Fragment>
);
}
return (
<React.Fragment>
<FileRenderInitiator uri={uri} videoTheaterMode={videoTheaterMode} />

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,343 @@
// @flow
import * as ICONS from 'constants/icons';
import React from 'react';
import Button from 'component/button';
import Card from 'component/common/card';
import Page from 'component/page';
import { Lbryio } from 'lbryinc';
import { URL, WEBPACK_WEB_PORT, STRIPE_PUBLIC_KEY } from 'config';
import moment from 'moment';
const isDev = process.env.NODE_ENV !== 'production';
let stripeEnvironment = 'test';
// if the key contains pk_live it's a live key
// update the environment for the calls to the backend to indicate which environment to hit
if (STRIPE_PUBLIC_KEY.indexOf('pk_live') > -1) {
stripeEnvironment = 'live';
}
let successStripeRedirectUrl, failureStripeRedirectUrl;
let successEndpoint = '/$/settings/tip_account';
let failureEndpoint = '/$/settings/tip_account';
if (isDev) {
successStripeRedirectUrl = 'http://localhost:' + WEBPACK_WEB_PORT + successEndpoint;
failureStripeRedirectUrl = 'http://localhost:' + WEBPACK_WEB_PORT + failureEndpoint;
} else {
successStripeRedirectUrl = URL + successEndpoint;
failureStripeRedirectUrl = URL + failureEndpoint;
}
type Props = {
source: string,
user: User,
};
type State = {
error: boolean,
loading: boolean,
content: ?string,
stripeConnectionUrl: string,
// alreadyUpdated: boolean,
accountConfirmed: boolean,
accountPendingConfirmation: boolean,
accountNotConfirmedButReceivedTips: boolean,
unpaidBalance: number,
pageTitle: string,
accountTransactions: any, // define this type
};
class StripeAccountConnection extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
error: false,
content: null,
loading: true,
accountConfirmed: false,
accountPendingConfirmation: false,
accountNotConfirmedButReceivedTips: false,
unpaidBalance: 0,
stripeConnectionUrl: '',
pageTitle: 'Add Payout Method',
accountTransactions: [],
// alreadyUpdated: false,
};
}
componentDidMount() {
const { user } = this.props;
// $FlowFixMe
this.experimentalUiEnabled = user && user.experimental_ui;
var that = this;
function getAndSetAccountLink(stillNeedToConfirmAccount) {
Lbryio.call(
'account',
'link',
{
return_url: successStripeRedirectUrl,
refresh_url: failureStripeRedirectUrl,
environment: stripeEnvironment,
},
'post'
).then((accountLinkResponse) => {
// stripe link for user to navigate to and confirm account
const stripeConnectionUrl = accountLinkResponse.url;
// set connection url on frontend
that.setState({
stripeConnectionUrl,
});
// show the account confirmation link if not created already
if (stillNeedToConfirmAccount) {
that.setState({
accountPendingConfirmation: true,
});
}
});
}
// call the account status endpoint
Lbryio.call(
'account',
'status',
{
environment: stripeEnvironment,
},
'post'
)
.then((accountStatusResponse) => {
const yetToBeCashedOutBalance = accountStatusResponse.total_received_unpaid;
if (yetToBeCashedOutBalance) {
that.setState({
unpaidBalance: yetToBeCashedOutBalance,
});
Lbryio.call(
'account',
'list',
{
environment: stripeEnvironment,
},
'post'
).then((accountListResponse: any) => {
// TODO type this
that.setState({
accountTransactions: accountListResponse,
});
console.log(accountListResponse);
});
}
// if charges already enabled, no need to generate an account link
if (accountStatusResponse.charges_enabled) {
// account has already been confirmed
that.setState({
accountConfirmed: true,
});
// user has not confirmed an account but have received payments
} else if (accountStatusResponse.total_received_unpaid > 0) {
that.setState({
accountNotConfirmedButReceivedTips: true,
});
getAndSetAccountLink();
// user has not received any amount or confirmed an account
} else {
// get stripe link and set it on the frontend
// pass true so it updates the frontend
getAndSetAccountLink(true);
}
})
.catch(function (error) {
// errorString passed from the API (with a 403 error)
const errorString = 'account not linked to user, please link first';
// if it's beamer's error indicating the account is not linked yet
if (error.message.indexOf(errorString) > -1) {
// get stripe link and set it on the frontend
getAndSetAccountLink();
} else {
// not an error from Beamer, throw it
throw new Error(error);
}
});
}
render() {
const {
stripeConnectionUrl,
accountConfirmed,
accountPendingConfirmation,
unpaidBalance,
accountNotConfirmedButReceivedTips,
pageTitle,
accountTransactions,
} = this.state;
const { user } = this.props;
if (user.fiat_enabled) {
return (
<Page backout={{ title: pageTitle, backLabel: __('Done') }} noFooter noSideNavigation>
<Card
title={<div className="table__header-text">{__('Connect a bank account')}</div>}
isBodyList
body={
<div>
{/* show while waiting for account status */}
{!accountConfirmed && !accountPendingConfirmation && !accountNotConfirmedButReceivedTips && (
<div className="card__body-actions">
<div>
<div>
<h3>{__('Getting your bank account connection status...')}</h3>
</div>
</div>
</div>
)}
{/* user has yet to complete their integration */}
{!accountConfirmed && accountPendingConfirmation && (
<div className="card__body-actions">
<div>
<div>
<h3>{__('Connect your bank account to Odysee to receive donations directly from users')}</h3>
</div>
<div className="section__actions">
<a href={stripeConnectionUrl}>
<Button button="secondary" label={__('Connect your bank account')} icon={ICONS.FINANCE} />
</a>
</div>
</div>
</div>
)}
{/* user has completed their integration */}
{accountConfirmed && (
<div className="card__body-actions">
<div>
<div>
<h3>{__('Congratulations! Your account has been connected with Odysee.')}</h3>
{unpaidBalance > 0 ? (
<div>
<br />
<h3>
{__('Your pending account balance is $%balance% USD.', { balance: unpaidBalance / 100 })}
</h3>
</div>
) : (
<div>
<br />
<h3>
{__('Your account balance is $0 USD. When you receive a tip you will see it here.')}
</h3>
</div>
)}
</div>
</div>
</div>
)}
{accountNotConfirmedButReceivedTips && (
<div className="card__body-actions">
<div>
<div>
<h3>{__('Congratulations, you have already begun receiving tips on Odysee!')}</h3>
<div>
<br />
<h3>
{__('Your pending account balance is $%balance% USD.', { balance: unpaidBalance / 100 })}
</h3>
</div>
<br />
<div>
<h3>
{__(
'Connect your bank account to be able to cash your pending balance out to your account.'
)}
</h3>
</div>
<div className="section__actions">
<a href={stripeConnectionUrl}>
<Button button="secondary" label={__('Connect your bank account')} icon={ICONS.FINANCE} />
</a>
</div>
</div>
</div>
</div>
)}
</div>
}
/>
<br />
{/* customer already has transactions */}
{accountTransactions && accountTransactions.length > 0 && (
<Card
title={__('Tip History')}
body={
<>
<div className="table__wrapper">
<table className="table table--transactions">
<thead>
<tr>
<th className="date-header">{__('Date')}</th>
<th>{<>{__('Receiving Channel Name')}</>}</th>
<th>{__('Tip Location')}</th>
<th>{__('Amount (USD)')} </th>
<th>{__('Processing Fee')}</th>
<th>{__('Odysee Fee')}</th>
<th>{__('Received Amount')}</th>
</tr>
</thead>
<tbody>
{accountTransactions &&
accountTransactions.reverse().map((transaction) => (
<tr key={transaction.name + transaction.created_at}>
<td>{moment(transaction.created_at).format('LLL')}</td>
<td>
<Button
className="stripe__card-link-text"
navigate={'/' + transaction.channel_name + ':' + transaction.channel_claim_id}
label={transaction.channel_name}
button="link"
/>
</td>
<td>
<Button
className="stripe__card-link-text"
navigate={'/' + transaction.channel_name + ':' + transaction.source_claim_id}
label={
transaction.channel_claim_id === transaction.source_claim_id
? 'Channel Page'
: 'File Page'
}
button="link"
/>
</td>
<td>${transaction.tipped_amount / 100}</td>
<td>${transaction.transaction_fee / 100}</td>
<td>${transaction.application_fee / 100}</td>
<td>${transaction.received_amount / 100}</td>
</tr>
))}
</tbody>
</table>
</div>
</>
}
/>
)}
</Page>
);
} else {
return <></>; // probably null;
}
}
}
export default StripeAccountConnection;

View file

@ -2,6 +2,8 @@ import { connect } from 'react-redux';
import { doSetClientSetting } from 'redux/actions/settings';
import { 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);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -226,6 +226,11 @@
}
}
.card__title-actions--link {
margin-top: var(--spacing-xs);
margin-right: var(--spacing-xs);
}
.card__title-actions--small {
padding: 0;
}

View file

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

View file

@ -4,6 +4,11 @@
position: relative;
}
.collection__subtitle {
display: flex;
margin-bottom: var(--spacing-s);
}
.collection-preview__items {
display: flex;
align-items: center;

View file

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