Add channel subscriptions count below author. #6867
84 changed files with 2673 additions and 1312 deletions
.env.defaultsCHANGELOG.mdconfig.js
flow-typed
package.jsonstatic
ui
analytics.jscomments.js
component
channelContent
channelEdit
claimListDiscover
comment
commentCreate
commentsList
commentsReplies
common
fileThumbnail
livestreamComment
livestreamComments
notification
publishForm
repostCreate
settingCommentsServer
userChannelFollowIntro
viewers/videoViewer/internal
walletFiatAccountHistory
walletFiatBalance
walletFiatPaymentBalance
walletFiatPaymentHistory
walletSendTip
walletTipAmountSelector
constants
effects
modal/modalRemoveCard
page
embedWrapper
file
livestreamCurrent
search
settings
settingsAdvanced
settingsCreator
settingsStripeAccount
settingsStripeCard
show
wallet
redux
actions
reducers
selectors
scss/component
util
web
|
@ -12,13 +12,14 @@ 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.odysee.com/api/v2
|
||||
COMMENT_SERVER_NAME=Odysee
|
||||
SEARCH_SERVER_API=https://lighthouse.odysee.com/search
|
||||
SOCKETY_SERVER_API=wss://sockety.odysee.com/ws
|
||||
THUMBNAIL_CDN_URL=https://image-processor.vanwanet.com/optimize/
|
||||
WELCOME_VERSION=1.0
|
||||
|
||||
# STRIPE
|
||||
STRIPE_PUBLIC_KEY='pk_test_NoL1JWL7i1ipfhVId5KfDZgo'
|
||||
# STRIPE_PUBLIC_KEY='pk_test_NoL1JWL7i1ipfhVId5KfDZgo'
|
||||
|
||||
# Analytics
|
||||
MATOMO_URL=https://analytics.lbry.com/
|
||||
|
@ -51,6 +52,7 @@ YRBL_SAD_IMG_URL=https://cdn.lbryplayer.xyz/api/v3/streams/free/yrbl-sad/c2d9649
|
|||
#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
|
||||
#AVATAR_DEFAULT=
|
||||
#MISSING_THUMB_DEFAULT=
|
||||
#FAVICON=
|
||||
|
||||
# LOCALE
|
||||
|
|
|
@ -19,6 +19,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
- Update lighthouse search api _community pr!_ ([#6731](https://github.com/lbryio/lbry-desktop/pull/6731))
|
||||
- Update sockety api _community pr!_ ([#6747](https://github.com/lbryio/lbry-desktop/pull/6747))
|
||||
- Use resolve for OG metadata instead of chainquery _community pr!_ ([#6787](https://github.com/lbryio/lbry-desktop/pull/6787))
|
||||
- Improved clickability of notification links _community pr!_ ([#6711](https://github.com/lbryio/lbry-desktop/pull/6711))
|
||||
|
||||
### Fixed
|
||||
- App now supports '#' and ':' for claimId separator ([#6496](https://github.com/lbryio/lbry-desktop/pull/6496))
|
||||
|
@ -36,6 +37,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
- Don't break words in chat + fix text overflow past 3 dot menu _community pr!_ ([#6602](https://github.com/lbryio/lbry-desktop/pull/6602))
|
||||
- Fix embed shows wrong OG metadata _community pr!_ ([#6815](https://github.com/lbryio/lbry-desktop/pull/6815))
|
||||
- Fix OG: "Unparsable data structure - Truncated Unicode character" _community pr!_ ([#6839](https://github.com/lbryio/lbry-desktop/pull/6839))
|
||||
- Fix Paid embed warning overlay redirection button now links to odysee _community pr!_ ([#6819](https://github.com/lbryio/lbry-desktop/pull/6819))
|
||||
- Fix comment section redirection to create channel _community pr!_ ([#6557](https://github.com/lbryio/lbry-desktop/pull/6557))
|
||||
|
||||
## [0.51.1] - [2021-06-26]
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ const config = {
|
|||
LBRY_WEB_BUFFER_API: process.env.LBRY_WEB_BUFFER_API,
|
||||
SEARCH_SERVER_API: process.env.SEARCH_SERVER_API,
|
||||
COMMENT_SERVER_API: process.env.COMMENT_SERVER_API,
|
||||
COMMENT_SERVER_NAME: process.env.COMMENT_SERVER_NAME,
|
||||
SOCKETY_SERVER_API: process.env.SOCKETY_SERVER_API,
|
||||
WELCOME_VERSION: process.env.WELCOME_VERSION,
|
||||
DOMAIN: process.env.DOMAIN,
|
||||
|
@ -34,6 +35,7 @@ const config = {
|
|||
LOGO_TEXT_LIGHT: process.env.LOGO_TEXT_LIGHT,
|
||||
LOGO_TEXT_DARK: process.env.LOGO_TEXT_DARK,
|
||||
AVATAR_DEFAULT: process.env.AVATAR_DEFAULT,
|
||||
MISSING_THUMB_DEFAULT: process.env.MISSING_THUMB_DEFAULT,
|
||||
// OG
|
||||
OG_TITLE_SUFFIX: process.env.OG_TITLE_SUFFIX,
|
||||
OG_HOMEPAGE_TITLE: process.env.OG_HOMEPAGE_TITLE,
|
||||
|
|
21
flow-typed/Comment.js
vendored
21
flow-typed/Comment.js
vendored
|
@ -14,6 +14,9 @@ declare type Comment = {
|
|||
is_pinned: boolean,
|
||||
support_amount: number,
|
||||
replies: number, // number of direct replies (i.e. excluding nested replies).
|
||||
is_moderator: boolean,
|
||||
is_creator: boolean,
|
||||
is_global_mod: boolean,
|
||||
is_fiat?: boolean,
|
||||
};
|
||||
|
||||
|
@ -38,6 +41,7 @@ declare type CommentsState = {
|
|||
topLevelTotalCommentsById: { [string]: number }, // ClaimID -> total top level comments in commentron.
|
||||
commentById: { [string]: Comment },
|
||||
linkedCommentAncestors: { [string]: Array<string> }, // {"linkedCommentId": ["parentId", "grandParentId", ...]}
|
||||
pinnedCommentsById: {}, // ClaimId -> array of pinned comment IDs
|
||||
isLoading: boolean,
|
||||
isLoadingByParentId: { [string]: boolean },
|
||||
myComments: ?Set<string>,
|
||||
|
@ -57,7 +61,6 @@ declare type CommentsState = {
|
|||
blockingByUri: {},
|
||||
unBlockingByUri: {},
|
||||
togglingForDelegatorMap: {[string]: Array<string>}, // {"blockedUri": ["delegatorUri1", ""delegatorUri2", ...]}
|
||||
commentsDisabledChannelIds: Array<string>,
|
||||
settingsByChannelId: { [string]: PerChannelSettings }, // ChannelID -> settings
|
||||
fetchingSettings: boolean,
|
||||
fetchingBlockedWords: boolean,
|
||||
|
@ -221,10 +224,20 @@ declare type ModerationAmIParams = {
|
|||
};
|
||||
|
||||
declare type SettingsParams = {
|
||||
channel_name: string,
|
||||
channel_name?: string,
|
||||
channel_id: string,
|
||||
signature: string,
|
||||
signing_ts: string,
|
||||
signature?: string,
|
||||
signing_ts?: string,
|
||||
};
|
||||
|
||||
declare type SettingsResponse = {
|
||||
words?: string,
|
||||
comments_enabled: boolean,
|
||||
min_tip_amount_comment: number,
|
||||
min_tip_amount_super_chat: number,
|
||||
slow_mode_min_gap: number,
|
||||
curse_jar_amount: number,
|
||||
filters_enabled?: boolean,
|
||||
};
|
||||
|
||||
declare type UpdateSettingsParams = {
|
||||
|
|
3
flow-typed/search.js
vendored
3
flow-typed/search.js
vendored
|
@ -28,7 +28,7 @@ declare type SearchOptions = {
|
|||
|
||||
declare type SearchState = {
|
||||
options: SearchOptions,
|
||||
urisByQuery: {},
|
||||
resultsByQuery: {},
|
||||
hasReachedMaxResultsLength: {},
|
||||
searching: boolean,
|
||||
};
|
||||
|
@ -40,6 +40,7 @@ declare type SearchSuccess = {
|
|||
from: number,
|
||||
size: number,
|
||||
uris: Array<string>,
|
||||
recsys: string,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "lbry",
|
||||
"version": "0.51.2-rc.1",
|
||||
"version": "0.51.2-rc.4",
|
||||
"description": "A browser for the LBRY network, a digital marketplace controlled by its users.",
|
||||
"keywords": [
|
||||
"lbry"
|
||||
|
@ -38,7 +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",
|
||||
"lint-fix": "eslint --fix --quiet 'ui/**/*.{js,jsx}' && eslint --fix --quiet 'web/**/*.{js,jsx}' && eslint --fix --quiet 'electron/**/*.js'",
|
||||
"format": "prettier 'src/**/*.{js,jsx,scss,json}' --write",
|
||||
"flow-defs": "flow-typed install",
|
||||
"precommit": "lint-staged",
|
||||
|
|
|
@ -1729,6 +1729,7 @@
|
|||
"Are you sure you want to view this content? Viewing will not unmute @%channel%": "Are you sure you want to view this content? Viewing will not unmute @%channel%",
|
||||
"View Content": "View Content",
|
||||
"Global": "Global",
|
||||
"Global Admin": "Global Admin",
|
||||
"Moderator": "Moderator",
|
||||
"Global Unblock Channel": "Global Unblock Channel",
|
||||
"Global Block Channel": "Global Block Channel",
|
||||
|
@ -2038,6 +2039,13 @@
|
|||
"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..",
|
||||
"Comments server": "Comments server",
|
||||
"Default comments server (%name%)": "Default comments server (%name%)",
|
||||
"Custom comments server": "Custom comments server",
|
||||
"Failed to fetch comments.": "Failed to fetch comments.",
|
||||
"Failed to fetch comments. Verify custom server settings.": "Failed to fetch comments. Verify custom server settings.",
|
||||
"Commenting server is not set.": "Commenting server is not set.",
|
||||
"Comments are not currently enabled.": "Comments are not currently enabled.",
|
||||
"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.",
|
||||
|
@ -2084,5 +2092,8 @@
|
|||
"Load More": "Load More",
|
||||
"%channelSubCount% Followers": "%channelSubCount% Followers",
|
||||
"%channelSubCount% Follower": "%channelSubCount% Follower",
|
||||
"Collection": "Collection",
|
||||
"%channelName% isn't live right now, but the chat is! Check back later to watch the stream.": "%channelName% isn't live right now, but the chat is! Check back later to watch the stream.",
|
||||
"Review": "Review",
|
||||
"--end--": "--end--"
|
||||
}
|
||||
|
|
|
@ -87,16 +87,20 @@ if (window.localStorage.getItem(SHARE_INTERNAL) === 'true') internalAnalyticsEna
|
|||
* @returns {String}
|
||||
*/
|
||||
function getDeviceType() {
|
||||
var userAgent = navigator.userAgent || navigator.vendor || window.opera;
|
||||
|
||||
if (/android/i.test(userAgent)) {
|
||||
return 'and';
|
||||
}
|
||||
|
||||
// iOS detection from: http://stackoverflow.com/a/9039885/177710
|
||||
if (/iPad|iPhone|iPod/.test(userAgent) && !window.MSStream) {
|
||||
return 'ios';
|
||||
}
|
||||
// We may not care what the device is if it's in a web browser. Commenting out for now.
|
||||
// if (!IS_WEB) {
|
||||
// return 'elt';
|
||||
// }
|
||||
// const userAgent = navigator.userAgent || navigator.vendor || window.opera;
|
||||
//
|
||||
// if (/android/i.test(userAgent)) {
|
||||
// return 'adr';
|
||||
// }
|
||||
//
|
||||
// // iOS detection from: http://stackoverflow.com/a/9039885/177710
|
||||
// if (/iPad|iPhone|iPod/.test(userAgent) && !window.MSStream) {
|
||||
// return 'ios';
|
||||
// }
|
||||
|
||||
// default as web, this can be optimized
|
||||
return 'web';
|
||||
|
@ -165,7 +169,7 @@ function startWatchmanIntervalIfNotRunning() {
|
|||
lastSentTime = new Date();
|
||||
|
||||
// only set an interval if analytics are enabled and is prod
|
||||
if (internalAnalyticsEnabled && isProduction) {
|
||||
if (isProduction && IS_WEB) {
|
||||
watchmanInterval = setInterval(sendAndResetWatchmanData, 1000 * SEND_DATA_TO_WATCHMAN_INTERVAL);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,13 @@ import { COMMENT_SERVER_API } from 'config';
|
|||
const Comments = {
|
||||
url: COMMENT_SERVER_API,
|
||||
enabled: Boolean(COMMENT_SERVER_API),
|
||||
isCustomServer: false,
|
||||
|
||||
setServerUrl: (customUrl: ?string) => {
|
||||
Comments.url = customUrl === undefined ? COMMENT_SERVER_API : customUrl;
|
||||
Comments.enabled = Boolean(Comments.url);
|
||||
Comments.isCustomServer = Comments.url !== COMMENT_SERVER_API;
|
||||
},
|
||||
|
||||
moderation_block: (params: ModerationBlockParams) => fetchCommentsApi('moderation.Block', params),
|
||||
moderation_unblock: (params: ModerationBlockParams) => fetchCommentsApi('moderation.UnBlock', params),
|
||||
|
@ -27,12 +34,15 @@ const Comments = {
|
|||
setting_unblock_word: (params: BlockWordParams) => fetchCommentsApi('setting.UnBlockWord', params),
|
||||
setting_list_blocked_words: (params: SettingsParams) => fetchCommentsApi('setting.ListBlockedWords', params),
|
||||
setting_update: (params: UpdateSettingsParams) => fetchCommentsApi('setting.Update', params),
|
||||
setting_get: (params: SettingsParams) => fetchCommentsApi('setting.Get', params),
|
||||
super_list: (params: SuperListParams) => fetchCommentsApi('comment.SuperChatList', params),
|
||||
};
|
||||
|
||||
function fetchCommentsApi(method: string, params: {}) {
|
||||
if (!Comments.enabled) {
|
||||
return Promise.reject('Comments are not currently enabled'); // eslint-disable-line
|
||||
if (!Comments.url) {
|
||||
return Promise.reject(new Error('Commenting server is not set.'));
|
||||
} else if (!Comments.enabled) {
|
||||
return Promise.reject('Comments are not currently enabled.'); // eslint-disable-line
|
||||
}
|
||||
|
||||
const url = `${Comments.url}?m=${method}`;
|
||||
|
|
|
@ -81,7 +81,7 @@ function ChannelContent(props: Props) {
|
|||
!showMature ? '&nsfw=false&size=50&from=0' : ''
|
||||
}`
|
||||
)
|
||||
.then((results) => {
|
||||
.then(({ body: results }) => {
|
||||
const urls = results.map(({ name, claimId }) => {
|
||||
return `lbry://${name}#${claimId}`;
|
||||
});
|
||||
|
|
|
@ -17,6 +17,8 @@ import {
|
|||
} from 'lbry-redux';
|
||||
import { doOpenModal } from 'redux/actions/app';
|
||||
import { doUpdateBlockListForPublishedChannel } from 'redux/actions/comments';
|
||||
import { doClaimInitialRewards } from 'redux/actions/rewards';
|
||||
import { selectIsClaimingInitialRewards, selectHasClaimedInitialRewards } from 'redux/selectors/rewards';
|
||||
import ChannelForm from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
|
@ -36,6 +38,8 @@ const select = (state, props) => ({
|
|||
createError: selectCreateChannelError(state),
|
||||
creatingChannel: selectCreatingChannel(state),
|
||||
balance: selectBalance(state),
|
||||
isClaimingInitialRewards: selectIsClaimingInitialRewards(state),
|
||||
hasClaimedInitialRewards: selectHasClaimedInitialRewards(state),
|
||||
});
|
||||
|
||||
const perform = (dispatch) => ({
|
||||
|
@ -50,6 +54,7 @@ const perform = (dispatch) => ({
|
|||
);
|
||||
},
|
||||
clearChannelErrors: () => dispatch(doClearChannelErrors()),
|
||||
claimInitialRewards: () => dispatch(doClaimInitialRewards()),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(ChannelForm);
|
||||
|
|
|
@ -48,6 +48,7 @@ type Props = {
|
|||
createError: string,
|
||||
creatingChannel: boolean,
|
||||
clearChannelErrors: () => void,
|
||||
claimInitialRewards: () => void,
|
||||
onDone: () => void,
|
||||
openModal: (
|
||||
id: string,
|
||||
|
@ -55,6 +56,8 @@ type Props = {
|
|||
) => void,
|
||||
uri: string,
|
||||
disabled: boolean,
|
||||
isClaimingInitialRewards: boolean,
|
||||
hasClaimedInitialRewards: boolean,
|
||||
};
|
||||
|
||||
function ChannelForm(props: Props) {
|
||||
|
@ -79,8 +82,11 @@ function ChannelForm(props: Props) {
|
|||
creatingChannel,
|
||||
createError,
|
||||
clearChannelErrors,
|
||||
claimInitialRewards,
|
||||
openModal,
|
||||
disabled,
|
||||
isClaimingInitialRewards,
|
||||
hasClaimedInitialRewards,
|
||||
} = props;
|
||||
const [nameError, setNameError] = React.useState(undefined);
|
||||
const [bidError, setBidError] = React.useState('');
|
||||
|
@ -94,6 +100,22 @@ function ChannelForm(props: Props) {
|
|||
const languageParam = params.languages;
|
||||
const primaryLanguage = Array.isArray(languageParam) && languageParam.length && languageParam[0];
|
||||
const secondaryLanguage = Array.isArray(languageParam) && languageParam.length >= 2 && languageParam[1];
|
||||
const submitLabel = React.useMemo(() => {
|
||||
if (isClaimingInitialRewards) {
|
||||
return __('Claiming credits...');
|
||||
}
|
||||
return creatingChannel || updatingChannel ? __('Submitting') : __('Submit');
|
||||
}, [isClaimingInitialRewards, creatingChannel, updatingChannel]);
|
||||
const submitDisabled = React.useMemo(() => {
|
||||
return (
|
||||
isClaimingInitialRewards ||
|
||||
creatingChannel ||
|
||||
updatingChannel ||
|
||||
nameError ||
|
||||
bidError ||
|
||||
(isNewChannel && !params.name)
|
||||
);
|
||||
}, [isClaimingInitialRewards, creatingChannel, updatingChannel, nameError, bidError, isNewChannel, params]);
|
||||
|
||||
function getChannelParams() {
|
||||
// fill this in with sdk data
|
||||
|
@ -219,6 +241,12 @@ function ChannelForm(props: Props) {
|
|||
clearChannelErrors();
|
||||
}, [clearChannelErrors]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!hasClaimedInitialRewards) {
|
||||
claimInitialRewards();
|
||||
}
|
||||
}, [hasClaimedInitialRewards, claimInitialRewards]);
|
||||
|
||||
// TODO clear and bail after submit
|
||||
return (
|
||||
<>
|
||||
|
@ -453,14 +481,7 @@ function ChannelForm(props: Props) {
|
|||
actions={
|
||||
<>
|
||||
<div className="section__actions">
|
||||
<Button
|
||||
button="primary"
|
||||
disabled={
|
||||
creatingChannel || updatingChannel || nameError || bidError || (isNewChannel && !params.name)
|
||||
}
|
||||
label={creatingChannel || updatingChannel ? __('Submitting') : __('Submit')}
|
||||
onClick={handleSubmit}
|
||||
/>
|
||||
<Button button="primary" disabled={submitDisabled} label={submitLabel} onClick={handleSubmit} />
|
||||
<Button button="link" label={__('Cancel')} onClick={onDone} />
|
||||
</div>
|
||||
{errorMsg ? (
|
||||
|
|
|
@ -141,7 +141,6 @@ function ClaimListDiscover(props: Props) {
|
|||
const { search } = location;
|
||||
const [page, setPage] = React.useState(1);
|
||||
const [forceRefresh, setForceRefresh] = React.useState();
|
||||
const [finalUris, setFinalUris] = React.useState([]);
|
||||
const isLargeScreen = useIsLargeScreen();
|
||||
const [orderParamEntry, setOrderParamEntry] = usePersistedState(`entry-${location.pathname}`, CS.ORDER_BY_TRENDING);
|
||||
const [orderParamUser, setOrderParamUser] = usePersistedState(`orderUser-${location.pathname}`, CS.ORDER_BY_TRENDING);
|
||||
|
@ -387,6 +386,9 @@ function ClaimListDiscover(props: Props) {
|
|||
: undefined;
|
||||
const livestreamSearchResult = livestreamSearchKey && claimSearchByQuery[livestreamSearchKey];
|
||||
|
||||
const [finalUris, setFinalUris] = React.useState(
|
||||
getFinalUrisInitialState(history.action === 'POP', claimSearchResult)
|
||||
);
|
||||
const [prevOptions, setPrevOptions] = React.useState(null);
|
||||
|
||||
if (!isJustScrollingToNewPage(prevOptions, options)) {
|
||||
|
@ -448,6 +450,10 @@ function ClaimListDiscover(props: Props) {
|
|||
</div>
|
||||
);
|
||||
|
||||
// **************************************************************************
|
||||
// Helpers
|
||||
// **************************************************************************
|
||||
|
||||
// Returns true if the change in 'options' indicate that we are simply scrolling
|
||||
// down to a new page; false otherwise.
|
||||
function isJustScrollingToNewPage(prevOptions, options) {
|
||||
|
@ -500,6 +506,17 @@ function ClaimListDiscover(props: Props) {
|
|||
return prev.length === next.length && prev.every((value, index) => value === next[index]);
|
||||
}
|
||||
|
||||
function getFinalUrisInitialState(isNavigatingBack, claimSearchResult) {
|
||||
if (isNavigatingBack && claimSearchResult && claimSearchResult.length > 0) {
|
||||
return claimSearchResult;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// **************************************************************************
|
||||
// **************************************************************************
|
||||
|
||||
React.useEffect(() => {
|
||||
if (shouldPerformSearch) {
|
||||
const searchOptions = JSON.parse(optionsStringForEffect);
|
||||
|
|
|
@ -10,6 +10,7 @@ import DateTime from 'component/dateTime';
|
|||
import Button from 'component/button';
|
||||
import Expandable from 'component/expandable';
|
||||
import MarkdownPreview from 'component/common/markdown-preview';
|
||||
import Tooltip from 'component/common/tooltip';
|
||||
import ChannelThumbnail from 'component/channelThumbnail';
|
||||
import { Menu, MenuButton } from '@reach/menu-button';
|
||||
import Icon from 'component/common/icon';
|
||||
|
@ -59,7 +60,9 @@ type Props = {
|
|||
stakedLevel: number,
|
||||
supportAmount: number,
|
||||
numDirectReplies: number,
|
||||
isFiat: boolean
|
||||
isModerator: boolean,
|
||||
isGlobalMod: boolean,
|
||||
isFiat: boolean,
|
||||
};
|
||||
|
||||
const LENGTH_TO_COLLAPSE = 300;
|
||||
|
@ -92,6 +95,8 @@ function Comment(props: Props) {
|
|||
stakedLevel,
|
||||
supportAmount,
|
||||
numDirectReplies,
|
||||
isModerator,
|
||||
isGlobalMod,
|
||||
isFiat,
|
||||
} = props;
|
||||
|
||||
|
@ -225,6 +230,22 @@ function Comment(props: Props) {
|
|||
<div className="comment__body-container">
|
||||
<div className="comment__meta">
|
||||
<div className="comment__meta-information">
|
||||
{isGlobalMod && (
|
||||
<Tooltip label={__('Admin')}>
|
||||
<span className="comment__badge comment__badge--global-mod">
|
||||
<Icon icon={ICONS.BADGE_MOD} size={20} />
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{isModerator && (
|
||||
<Tooltip label={__('Moderator')}>
|
||||
<span className="comment__badge comment__badge--mod">
|
||||
<Icon icon={ICONS.BADGE_MOD} size={20} />
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{!author ? (
|
||||
<span className="comment__author">{__('Anonymous')}</span>
|
||||
) : (
|
||||
|
|
|
@ -7,30 +7,42 @@ import {
|
|||
doSendTip,
|
||||
} from 'lbry-redux';
|
||||
import { doOpenModal, doSetActiveChannel } from 'redux/actions/app';
|
||||
import { doCommentCreate } from 'redux/actions/comments';
|
||||
import { doCommentCreate, doFetchCreatorSettings } from 'redux/actions/comments';
|
||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||
import { selectActiveChannelClaim } from 'redux/selectors/app';
|
||||
import { makeSelectCommentsDisabledForUri } from 'redux/selectors/comments';
|
||||
import { selectSettingsByChannelId } 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,
|
||||
commentsDisabledBySettings: makeSelectCommentsDisabledForUri(props.uri)(state),
|
||||
claim: makeSelectClaimForUri(props.uri)(state),
|
||||
channels: selectMyChannelClaims(state),
|
||||
isFetchingChannels: selectFetchingMyChannels(state),
|
||||
activeChannelClaim: selectActiveChannelClaim(state),
|
||||
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
||||
settingsByChannelId: selectSettingsByChannelId(state),
|
||||
});
|
||||
|
||||
const perform = (dispatch, ownProps) => ({
|
||||
createComment: (comment, claimId, parentId, txid, payment_intent_id, environment) =>
|
||||
dispatch(doCommentCreate(comment, claimId, parentId, ownProps.uri, ownProps.livestream, 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)),
|
||||
doFetchCreatorSettings: (channelClaimId) => dispatch(doFetchCreatorSettings(channelClaimId)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(CommentCreate);
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
// @flow
|
||||
import type { ElementRef } from 'react';
|
||||
import { SIMPLE_SITE, STRIPE_PUBLIC_KEY } from 'config';
|
||||
import { SIMPLE_SITE } from 'config';
|
||||
import * as PAGES from 'constants/pages';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import React from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { FormField, Form } from 'component/common/form';
|
||||
import Icon from 'component/common/icon';
|
||||
import Button from 'component/button';
|
||||
import SelectChannel from 'component/selectChannel';
|
||||
import usePersistedState from 'effects/use-persisted-state';
|
||||
|
@ -14,16 +15,14 @@ import { useHistory } from 'react-router';
|
|||
import WalletTipAmountSelector from 'component/walletTipAmountSelector';
|
||||
import CreditAmount from 'component/common/credit-amount';
|
||||
import ChannelThumbnail from 'component/channelThumbnail';
|
||||
import I18nMessage from 'component/i18nMessage';
|
||||
import UriIndicator from 'component/uriIndicator';
|
||||
import Empty from 'component/common/empty';
|
||||
import { getChannelIdFromClaim } from 'util/claim';
|
||||
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';
|
||||
}
|
||||
import { getStripeEnvironment } from 'util/stripe';
|
||||
let stripeEnvironment = getStripeEnvironment();
|
||||
|
||||
const TAB_FIAT = 'TabFiat';
|
||||
const TAB_LBC = 'TabLBC';
|
||||
|
@ -32,7 +31,6 @@ type Props = {
|
|||
uri: string,
|
||||
claim: StreamClaim,
|
||||
createComment: (string, string, string, ?string, ?string, ?string) => Promise<any>,
|
||||
commentsDisabledBySettings: boolean,
|
||||
channels: ?Array<ChannelClaim>,
|
||||
onDoneReplying?: () => void,
|
||||
onCancelReplying?: () => void,
|
||||
|
@ -49,13 +47,13 @@ type Props = {
|
|||
claimIsMine: boolean,
|
||||
sendTip: ({}, (any) => void, (any) => void) => void,
|
||||
doToast: ({ message: string }) => void,
|
||||
disabled: boolean,
|
||||
doFetchCreatorSettings: (channelId: string) => Promise<any>,
|
||||
settingsByChannelId: { [channelId: string]: PerChannelSettings },
|
||||
};
|
||||
|
||||
export function CommentCreate(props: Props) {
|
||||
const {
|
||||
createComment,
|
||||
commentsDisabledBySettings,
|
||||
claim,
|
||||
channels,
|
||||
onDoneReplying,
|
||||
|
@ -71,6 +69,8 @@ export function CommentCreate(props: Props) {
|
|||
claimIsMine,
|
||||
sendTip,
|
||||
doToast,
|
||||
doFetchCreatorSettings,
|
||||
settingsByChannelId,
|
||||
} = props;
|
||||
const buttonRef: ElementRef<any> = React.useRef();
|
||||
const {
|
||||
|
@ -90,8 +90,42 @@ export function CommentCreate(props: Props) {
|
|||
const charCount = commentValue.length;
|
||||
const [activeTab, setActiveTab] = React.useState('');
|
||||
const [tipError, setTipError] = React.useState();
|
||||
const disabled = isSubmitting || !activeChannelClaim || !commentValue.length;
|
||||
const disabled = isSubmitting || isFetchingChannels || !commentValue.length;
|
||||
const [shouldDisableReviewButton, setShouldDisableReviewButton] = React.useState();
|
||||
const channelId = getChannelIdFromClaim(claim);
|
||||
const channelSettings = channelId ? settingsByChannelId[channelId] : undefined;
|
||||
const minSuper = (channelSettings && channelSettings.min_tip_amount_super_chat) || 0;
|
||||
const minTip = (channelSettings && channelSettings.min_tip_amount_comment) || 0;
|
||||
const minAmount = minTip || minSuper || 0;
|
||||
const minAmountMet = minAmount === 0 || tipAmount >= minAmount;
|
||||
|
||||
const minAmountRef = React.useRef(minAmount);
|
||||
minAmountRef.current = minAmount;
|
||||
|
||||
const MinAmountNotice = minAmount ? (
|
||||
<div className="help--notice comment--min-amount-notice">
|
||||
<I18nMessage tokens={{ lbc: <CreditAmount noFormat amount={minAmount} /> }}>
|
||||
{minTip ? 'Comment min: %lbc%' : minSuper ? 'HyperChat min: %lbc%' : ''}
|
||||
</I18nMessage>
|
||||
<Icon
|
||||
customTooltipText={
|
||||
minTip
|
||||
? __('This channel requires a minimum tip for each comment.')
|
||||
: minSuper
|
||||
? __('This channel requires a minimum amount for HyperChats to be visible.')
|
||||
: ''
|
||||
}
|
||||
className="icon--help"
|
||||
icon={ICONS.HELP}
|
||||
tooltip
|
||||
size={16}
|
||||
/>
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
// **************************************************************************
|
||||
// Functions
|
||||
// **************************************************************************
|
||||
|
||||
function handleCommentChange(event) {
|
||||
let commentValue;
|
||||
|
@ -131,6 +165,15 @@ export function CommentCreate(props: Props) {
|
|||
return;
|
||||
}
|
||||
|
||||
if (!channelId) {
|
||||
doToast({
|
||||
message: __('Unable to verify channel settings. Try refreshing the page.'),
|
||||
isError: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// if comment post didn't work, but tip was already made, try again to create comment
|
||||
if (commentFailure && tipAmount === successTip.tipAmount) {
|
||||
handleCreateComment(successTip.txid);
|
||||
return;
|
||||
|
@ -138,6 +181,29 @@ export function CommentCreate(props: Props) {
|
|||
setSuccessTip({ txid: undefined, tipAmount: undefined });
|
||||
}
|
||||
|
||||
// !! Beware of stale closure when editing the then-block, including doSubmitTip().
|
||||
doFetchCreatorSettings(channelId).then(() => {
|
||||
const lockedMinAmount = minAmount; // value during closure.
|
||||
const currentMinAmount = minAmountRef.current; // value from latest doFetchCreatorSettings().
|
||||
|
||||
if (lockedMinAmount !== currentMinAmount) {
|
||||
doToast({
|
||||
message: __('The creator just updated the minimum setting. Please revise or double-check your tip amount.'),
|
||||
isError: true,
|
||||
});
|
||||
setIsReviewingSupportComment(false);
|
||||
return;
|
||||
}
|
||||
|
||||
doSubmitTip();
|
||||
});
|
||||
}
|
||||
|
||||
function doSubmitTip() {
|
||||
if (!activeChannelClaim) {
|
||||
return;
|
||||
}
|
||||
|
||||
const params = {
|
||||
amount: tipAmount,
|
||||
claim_id: claimId,
|
||||
|
@ -147,6 +213,19 @@ export function CommentCreate(props: Props) {
|
|||
const activeChannelName = activeChannelClaim && activeChannelClaim.name;
|
||||
const activeChannelId = activeChannelClaim && activeChannelClaim.claim_id;
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
|
||||
if (activeTab === TAB_LBC) {
|
||||
|
@ -160,6 +239,17 @@ export function CommentCreate(props: Props) {
|
|||
setTimeout(() => {
|
||||
handleCreateComment(txid);
|
||||
}, 1500);
|
||||
|
||||
doToast({
|
||||
message: __(
|
||||
"You sent %tipAmount% LBRY Credits as a tip to %tipChannelName%, I'm sure they appreciate it!",
|
||||
{
|
||||
tipAmount: tipAmount, // force show decimal places
|
||||
tipChannelName,
|
||||
}
|
||||
),
|
||||
});
|
||||
|
||||
setSuccessTip({ txid, tipAmount });
|
||||
},
|
||||
() => {
|
||||
|
@ -168,19 +258,6 @@ export function CommentCreate(props: Props) {
|
|||
}
|
||||
);
|
||||
} 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;
|
||||
const roundedAmount = Math.round(tipAmount * 100) / 100;
|
||||
|
||||
|
@ -188,7 +265,8 @@ export function CommentCreate(props: Props) {
|
|||
'customer',
|
||||
'tip',
|
||||
{
|
||||
amount: 100 * roundedAmount, // convert from dollars to cents
|
||||
// round to deal with floating point precision
|
||||
amount: Math.round(100 * roundedAmount), // convert from dollars to cents
|
||||
creator_channel_name: tipChannelName, // creator_channel_name
|
||||
creator_channel_claim_id: channelClaimId,
|
||||
tipper_channel_name: activeChannelName,
|
||||
|
@ -259,6 +337,12 @@ export function CommentCreate(props: Props) {
|
|||
.catch(() => {
|
||||
setIsSubmitting(false);
|
||||
setCommentFailure(true);
|
||||
|
||||
if (channelId) {
|
||||
// It could be that the creator added a minimum tip setting.
|
||||
// Manually update for now until a websocket msg is available.
|
||||
doFetchCreatorSettings(channelId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -266,11 +350,26 @@ export function CommentCreate(props: Props) {
|
|||
setAdvancedEditor(!advancedEditor);
|
||||
}
|
||||
|
||||
if (commentsDisabledBySettings) {
|
||||
// **************************************************************************
|
||||
// Effects
|
||||
// **************************************************************************
|
||||
|
||||
// Fetch channel constraints if not already.
|
||||
React.useEffect(() => {
|
||||
if (!channelSettings && channelId) {
|
||||
doFetchCreatorSettings(channelId);
|
||||
}
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
// **************************************************************************
|
||||
// Render
|
||||
// **************************************************************************
|
||||
|
||||
if (channelSettings && !channelSettings.comments_enabled) {
|
||||
return <Empty padded text={__('This channel has disabled comments on their page.')} />;
|
||||
}
|
||||
|
||||
if (!hasChannels) {
|
||||
if (!isFetchingChannels && !hasChannels) {
|
||||
return (
|
||||
<div
|
||||
role="button"
|
||||
|
@ -288,12 +387,7 @@ export function CommentCreate(props: Props) {
|
|||
}
|
||||
}}
|
||||
>
|
||||
<FormField
|
||||
type="textarea"
|
||||
name={'comment_signup_prompt'}
|
||||
placeholder={__('Say something about this...')}
|
||||
label={isFetchingChannels ? __('Comment') : undefined}
|
||||
/>
|
||||
<FormField type="textarea" name={'comment_signup_prompt'} placeholder={__('Say something about this...')} />
|
||||
<div className="section__actions--no-margin">
|
||||
<Button disabled button="primary" label={__('Post --[button to submit something]--')} requiresAuth={IS_WEB} />
|
||||
</div>
|
||||
|
@ -322,7 +416,7 @@ export function CommentCreate(props: Props) {
|
|||
<Button
|
||||
autoFocus
|
||||
button="primary"
|
||||
disabled={disabled}
|
||||
disabled={disabled || !minAmountMet}
|
||||
label={
|
||||
isSubmitting
|
||||
? __('Sending...')
|
||||
|
@ -338,6 +432,7 @@ export function CommentCreate(props: Props) {
|
|||
label={__('Cancel')}
|
||||
onClick={() => setIsReviewingSupportComment(false)}
|
||||
/>
|
||||
{MinAmountNotice}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -353,7 +448,7 @@ export function CommentCreate(props: Props) {
|
|||
})}
|
||||
>
|
||||
<FormField
|
||||
disabled={!activeChannelClaim}
|
||||
disabled={isFetchingChannels}
|
||||
type={SIMPLE_SITE ? 'textarea' : advancedEditor && !isReply ? 'markdown' : 'textarea'}
|
||||
name={isReply ? 'content_reply' : 'content_description'}
|
||||
label={
|
||||
|
@ -391,34 +486,37 @@ export function CommentCreate(props: Props) {
|
|||
{isSupportComment ? (
|
||||
<>
|
||||
<Button
|
||||
disabled={disabled || tipError || shouldDisableReviewButton}
|
||||
disabled={disabled || tipError || shouldDisableReviewButton || !minAmountMet}
|
||||
type="button"
|
||||
button="primary"
|
||||
icon={activeTab === TAB_LBC ? ICONS.LBC : ICONS.FINANCE}
|
||||
label={__('Review')}
|
||||
onClick={() => setIsReviewingSupportComment(true)}
|
||||
requiresAuth={IS_WEB}
|
||||
/>
|
||||
|
||||
<Button disabled={disabled} button="link" label={__('Cancel')} onClick={() => setIsSupportComment(false)} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
ref={buttonRef}
|
||||
button="primary"
|
||||
disabled={disabled}
|
||||
type="submit"
|
||||
label={
|
||||
isReply
|
||||
? isSubmitting
|
||||
? __('Replying...')
|
||||
: __('Reply')
|
||||
: isSubmitting
|
||||
? __('Commenting...')
|
||||
: __('Comment --[button to submit something]--')
|
||||
}
|
||||
requiresAuth={IS_WEB}
|
||||
/>
|
||||
{(!minTip || claimIsMine) && (
|
||||
<Button
|
||||
ref={buttonRef}
|
||||
button="primary"
|
||||
disabled={disabled}
|
||||
type="submit"
|
||||
label={
|
||||
isReply
|
||||
? isSubmitting
|
||||
? __('Replying...')
|
||||
: __('Reply')
|
||||
: isSubmitting
|
||||
? __('Commenting...')
|
||||
: __('Comment --[button to submit something]--')
|
||||
}
|
||||
requiresAuth={IS_WEB}
|
||||
/>
|
||||
)}
|
||||
{!claimIsMine && (
|
||||
<Button
|
||||
disabled={disabled}
|
||||
|
@ -431,7 +529,8 @@ export function CommentCreate(props: Props) {
|
|||
}}
|
||||
/>
|
||||
)}
|
||||
{!claimIsMine && (
|
||||
{/* @if TARGET='web' */}
|
||||
{!claimIsMine && stripeEnvironment && (
|
||||
<Button
|
||||
disabled={disabled}
|
||||
button="alt"
|
||||
|
@ -443,7 +542,8 @@ export function CommentCreate(props: Props) {
|
|||
}}
|
||||
/>
|
||||
)}
|
||||
{isReply && (
|
||||
{/* @endif */}
|
||||
{isReply && !minTip && (
|
||||
<Button
|
||||
button="link"
|
||||
label={__('Cancel')}
|
||||
|
@ -456,6 +556,7 @@ export function CommentCreate(props: Props) {
|
|||
)}
|
||||
</>
|
||||
)}
|
||||
{MinAmountNotice}
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { makeSelectClaimIsMine, selectFetchingMyChannels, selectMyChannelClaims } from 'lbry-redux';
|
||||
import {
|
||||
makeSelectClaimForUri,
|
||||
makeSelectClaimIsMine,
|
||||
selectFetchingMyChannels,
|
||||
selectMyChannelClaims,
|
||||
} from 'lbry-redux';
|
||||
import {
|
||||
makeSelectTopLevelCommentsForUri,
|
||||
makeSelectTopLevelTotalPagesForUri,
|
||||
|
@ -7,12 +12,12 @@ import {
|
|||
selectIsFetchingReacts,
|
||||
makeSelectTotalCommentsCountForUri,
|
||||
selectOthersReactsById,
|
||||
makeSelectCommentsDisabledForUri,
|
||||
selectMyReactionsByCommentId,
|
||||
makeSelectCommentIdsForUri,
|
||||
selectSettingsByChannelId,
|
||||
makeSelectPinnedCommentsForUri,
|
||||
} from 'redux/selectors/comments';
|
||||
import { doCommentReset, doCommentList, doCommentById, doCommentReactList } from 'redux/actions/comments';
|
||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||
import { selectActiveChannelClaim } from 'redux/selectors/app';
|
||||
import CommentsList from './view';
|
||||
|
||||
|
@ -21,15 +26,16 @@ const select = (state, props) => {
|
|||
return {
|
||||
myChannels: selectMyChannelClaims(state),
|
||||
allCommentIds: makeSelectCommentIdsForUri(props.uri)(state),
|
||||
pinnedComments: makeSelectPinnedCommentsForUri(props.uri)(state),
|
||||
topLevelComments: makeSelectTopLevelCommentsForUri(props.uri)(state),
|
||||
topLevelTotalPages: makeSelectTopLevelTotalPagesForUri(props.uri)(state),
|
||||
totalComments: makeSelectTotalCommentsCountForUri(props.uri)(state),
|
||||
claim: makeSelectClaimForUri(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),
|
||||
settingsByChannelId: selectSettingsByChannelId(state),
|
||||
myReactsByCommentId: selectMyReactionsByCommentId(state),
|
||||
othersReactsById: selectOthersReactsById(state),
|
||||
activeChannelId: activeChannelClaim && activeChannelClaim.claim_id,
|
||||
|
|
|
@ -14,6 +14,7 @@ import { ENABLE_COMMENT_REACTIONS } from 'config';
|
|||
import Empty from 'component/common/empty';
|
||||
import debounce from 'util/debounce';
|
||||
import { useIsMobile } from 'effects/use-screensize';
|
||||
import { getChannelIdFromClaim } from 'util/claim';
|
||||
|
||||
const DEBOUNCE_SCROLL_HANDLER_MS = 200;
|
||||
|
||||
|
@ -27,14 +28,15 @@ function scaleToDevicePixelRatio(value) {
|
|||
|
||||
type Props = {
|
||||
allCommentIds: any,
|
||||
pinnedComments: Array<Comment>,
|
||||
topLevelComments: Array<Comment>,
|
||||
topLevelTotalPages: number,
|
||||
commentsDisabledBySettings: boolean,
|
||||
fetchTopLevelComments: (string, number, number, number) => void,
|
||||
fetchComment: (string) => void,
|
||||
fetchReacts: (Array<string>) => Promise<any>,
|
||||
resetComments: (string) => void,
|
||||
uri: string,
|
||||
claim: ?Claim,
|
||||
claimIsMine: boolean,
|
||||
myChannels: ?Array<ChannelClaim>,
|
||||
isFetchingComments: boolean,
|
||||
|
@ -45,6 +47,7 @@ type Props = {
|
|||
myReactsByCommentId: ?{ [string]: Array<string> }, // "CommentId:MyChannelId" -> reaction array (note the ID concatenation)
|
||||
othersReactsById: ?{ [string]: { [REACTION_TYPES.LIKE | REACTION_TYPES.DISLIKE]: number } },
|
||||
activeChannelId: ?string,
|
||||
settingsByChannelId: { [channelId: string]: PerChannelSettings },
|
||||
};
|
||||
|
||||
function CommentList(props: Props) {
|
||||
|
@ -55,9 +58,10 @@ function CommentList(props: Props) {
|
|||
fetchReacts,
|
||||
resetComments,
|
||||
uri,
|
||||
pinnedComments,
|
||||
topLevelComments,
|
||||
topLevelTotalPages,
|
||||
commentsDisabledBySettings,
|
||||
claim,
|
||||
claimIsMine,
|
||||
myChannels,
|
||||
isFetchingComments,
|
||||
|
@ -68,6 +72,7 @@ function CommentList(props: Props) {
|
|||
myReactsByCommentId,
|
||||
othersReactsById,
|
||||
activeChannelId,
|
||||
settingsByChannelId,
|
||||
} = props;
|
||||
|
||||
const commentRef = React.useRef();
|
||||
|
@ -78,6 +83,8 @@ function CommentList(props: Props) {
|
|||
const isMobile = useIsMobile();
|
||||
const [expandedComments, setExpandedComments] = React.useState(!isMobile);
|
||||
const totalFetchedComments = allCommentIds ? allCommentIds.length : 0;
|
||||
const channelId = getChannelIdFromClaim(claim);
|
||||
const channelSettings = channelId ? settingsByChannelId[channelId] : undefined;
|
||||
|
||||
// Display comments immediately if not fetching reactions
|
||||
// If not, wait to show comments until reactions are fetched
|
||||
|
@ -106,6 +113,34 @@ function CommentList(props: Props) {
|
|||
}
|
||||
}
|
||||
|
||||
function getCommentElems(comments) {
|
||||
return comments.map((comment) => {
|
||||
return (
|
||||
<CommentView
|
||||
isTopLevel
|
||||
threadDepth={3}
|
||||
key={comment.comment_id}
|
||||
uri={uri}
|
||||
authorUri={comment.channel_url}
|
||||
author={comment.channel_name}
|
||||
claimId={comment.claim_id}
|
||||
commentId={comment.comment_id}
|
||||
message={comment.comment}
|
||||
timePosted={comment.timestamp * 1000}
|
||||
claimIsMine={claimIsMine}
|
||||
commentIsMine={comment.channel_id && isMyComment(comment.channel_id)}
|
||||
linkedCommentId={linkedCommentId}
|
||||
isPinned={comment.is_pinned}
|
||||
supportAmount={comment.support_amount}
|
||||
numDirectReplies={comment.replies}
|
||||
isModerator={comment.is_moderator}
|
||||
isGlobalMod={comment.is_global_mod}
|
||||
isFiat={comment.is_fiat}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Reset comments
|
||||
useEffect(() => {
|
||||
if (page === 0) {
|
||||
|
@ -218,8 +253,6 @@ function CommentList(props: Props) {
|
|||
topLevelTotalPages,
|
||||
]);
|
||||
|
||||
const displayedComments = readyToDisplayComments ? topLevelComments : [];
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={
|
||||
|
@ -279,7 +312,7 @@ function CommentList(props: Props) {
|
|||
<>
|
||||
<CommentCreate uri={uri} />
|
||||
|
||||
{!commentsDisabledBySettings && !isFetchingComments && hasNoComments && (
|
||||
{channelSettings && channelSettings.comments_enabled && !isFetchingComments && hasNoComments && (
|
||||
<Empty padded text={__('That was pretty deep. What do you think?')} />
|
||||
)}
|
||||
|
||||
|
@ -290,31 +323,8 @@ function CommentList(props: Props) {
|
|||
})}
|
||||
ref={commentRef}
|
||||
>
|
||||
{topLevelComments &&
|
||||
displayedComments &&
|
||||
displayedComments.map((comment) => {
|
||||
return (
|
||||
<CommentView
|
||||
isTopLevel
|
||||
threadDepth={3}
|
||||
key={comment.comment_id}
|
||||
uri={uri}
|
||||
authorUri={comment.channel_url}
|
||||
author={comment.channel_name}
|
||||
claimId={comment.claim_id}
|
||||
commentId={comment.comment_id}
|
||||
message={comment.comment}
|
||||
timePosted={comment.timestamp * 1000}
|
||||
claimIsMine={claimIsMine}
|
||||
commentIsMine={comment.channel_id && isMyComment(comment.channel_id)}
|
||||
linkedCommentId={linkedCommentId}
|
||||
isPinned={comment.is_pinned}
|
||||
supportAmount={comment.support_amount}
|
||||
numDirectReplies={comment.replies}
|
||||
isFiat={comment.is_fiat}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{readyToDisplayComments && pinnedComments && getCommentElems(pinnedComments)}
|
||||
{readyToDisplayComments && topLevelComments && getCommentElems(topLevelComments)}
|
||||
</ul>
|
||||
|
||||
{isMobile && (
|
||||
|
|
|
@ -97,6 +97,8 @@ function CommentsReplies(props: Props) {
|
|||
commentingEnabled={commentingEnabled}
|
||||
supportAmount={comment.support_amount}
|
||||
numDirectReplies={comment.replies}
|
||||
isModerator={comment.is_moderator}
|
||||
isGlobalMod={comment.is_global_mod}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
|
|
@ -2331,4 +2331,37 @@ export const icons = {
|
|||
<path d="M4.954 14.753l3.535 3.535-1.768 1.768-3.535-3.535z" />
|
||||
</g>
|
||||
),
|
||||
[ICONS.BADGE_MOD]: (props: IconProps) => (
|
||||
<svg
|
||||
{...props}
|
||||
version="1.1"
|
||||
id="Layer_1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
x="0px"
|
||||
y="0px"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
xmlSpace="preserve"
|
||||
>
|
||||
<style type="text/css">{'.st0{fill:FF3850}.st1{fill:#181021}.st2{fill:#FFFFFF}'}</style>
|
||||
<g>
|
||||
<g>
|
||||
<path
|
||||
className="st0"
|
||||
d="M11.69,6.77c4.86,0,7.55,0.9,8.52,1.31c1.29-1.46,3.28-4.14,3.28-6.76c0,0-4.17,4.86-6.92,5.12 c-1.25-0.87-2.77-1.38-4.41-1.38c0,0-3.21-0.06-4.63,1.31C4.81,6.44,0.51,1.32,0.51,1.32c0,2.61,1.97,5.27,3.25,6.74 C4.71,7.59,7.03,6.77,11.69,6.77z M19.87,19.38c0.02-0.13,0.04-0.27,0.04-0.4V12.8c0-1.03-0.21-2.02-0.58-2.92 c-0.83-0.33-3.25-1.11-7.64-1.11c-4.29,0-6.33,0.75-7,1.06c-0.38,0.91-0.6,1.91-0.6,2.97v6.18c0,0.13,0.02,0.26,0.04,0.39 C1.6,19.73,0,22.54,0,22.54L12,24l12-1.46C24,22.54,22.36,19.79,19.87,19.38z"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<path
|
||||
className="st1"
|
||||
d="M13,18.57H11c-2.27,0-4.12-0.82-4.12-2.88v-2.46c0-2.77,2.17-3.94,5.11-3.94s5.11,1.17,5.11,3.94v2.46 C17.11,17.75,15.27,18.57,13,18.57z"
|
||||
/>
|
||||
<path
|
||||
className="st2"
|
||||
d="M15.06,15.25c-0.28,0-0.5-0.22-0.5-0.5v-1.42c0-0.32,0-1.31-1.63-1.31c-0.28,0-0.5-0.22-0.5-0.5 s0.22-0.5,0.5-0.5c1.65,0,2.63,0.86,2.63,2.31v1.42C15.56,15.02,15.33,15.25,15.06,15.25z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
};
|
||||
|
|
|
@ -28,7 +28,7 @@ import { useRect } from '@reach/rect';
|
|||
// </TabPanels>
|
||||
// </Tabs>
|
||||
//
|
||||
// the base @reach/tabs components handle all the focus/accessibilty labels
|
||||
// the base @reach/tabs components handle all the focus/accessibility labels
|
||||
// We're just adding some styling
|
||||
|
||||
type TabsProps = {
|
||||
|
|
|
@ -6,14 +6,16 @@ import useLazyLoading from 'effects/use-lazy-loading';
|
|||
|
||||
type Props = {
|
||||
thumb: string,
|
||||
fallback: ?string,
|
||||
children?: Node,
|
||||
className?: string,
|
||||
};
|
||||
|
||||
const Thumb = (props: Props) => {
|
||||
const { thumb, children, className } = props;
|
||||
const { thumb, fallback, children, className } = props;
|
||||
const thumbnailRef = React.useRef(null);
|
||||
useLazyLoading(thumbnailRef);
|
||||
|
||||
useLazyLoading(thumbnailRef, fallback || '');
|
||||
|
||||
return (
|
||||
<div ref={thumbnailRef} data-background-image={thumb} className={classnames('media__thumb', className)}>
|
||||
|
|
|
@ -4,6 +4,7 @@ import { getThumbnailCdnUrl } from 'util/thumbnail';
|
|||
import React from 'react';
|
||||
import FreezeframeWrapper from './FreezeframeWrapper';
|
||||
import Placeholder from './placeholder.png';
|
||||
import { MISSING_THUMB_DEFAULT } from 'config';
|
||||
import classnames from 'classnames';
|
||||
import Thumb from './thumb';
|
||||
|
||||
|
@ -42,6 +43,8 @@ function FileThumbnail(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
const fallback = MISSING_THUMB_DEFAULT ? getThumbnailCdnUrl({ thumbnail: MISSING_THUMB_DEFAULT }) : undefined;
|
||||
|
||||
let url = thumbnail || (hasResolvedClaim ? Placeholder : '');
|
||||
// @if TARGET='web'
|
||||
// Pass image urls through a compression proxy
|
||||
|
@ -54,7 +57,7 @@ function FileThumbnail(props: Props) {
|
|||
|
||||
if (hasResolvedClaim || thumbnailUrl) {
|
||||
return (
|
||||
<Thumb thumb={thumbnailUrl} className={className}>
|
||||
<Thumb thumb={thumbnailUrl} fallback={fallback} className={className}>
|
||||
{children}
|
||||
</Thumb>
|
||||
);
|
||||
|
|
|
@ -3,6 +3,7 @@ import * as ICONS from 'constants/icons';
|
|||
import React from 'react';
|
||||
import { parseURI } from 'lbry-redux';
|
||||
import MarkdownPreview from 'component/common/markdown-preview';
|
||||
import Tooltip from 'component/common/tooltip';
|
||||
import ChannelThumbnail from 'component/channelThumbnail';
|
||||
import { Menu, MenuButton } from '@reach/menu-button';
|
||||
import Icon from 'component/common/icon';
|
||||
|
@ -20,11 +21,27 @@ type Props = {
|
|||
commentIsMine: boolean,
|
||||
stakedLevel: number,
|
||||
supportAmount: number,
|
||||
isModerator: boolean,
|
||||
isGlobalMod: boolean,
|
||||
isFiat: boolean,
|
||||
isPinned: boolean,
|
||||
};
|
||||
|
||||
function LivestreamComment(props: Props) {
|
||||
const { claim, uri, authorUri, message, commentIsMine, commentId, stakedLevel, supportAmount, isFiat } = props;
|
||||
const {
|
||||
claim,
|
||||
uri,
|
||||
authorUri,
|
||||
message,
|
||||
commentIsMine,
|
||||
commentId,
|
||||
stakedLevel,
|
||||
supportAmount,
|
||||
isModerator,
|
||||
isGlobalMod,
|
||||
isFiat,
|
||||
isPinned,
|
||||
} = props;
|
||||
const [mouseIsHovering, setMouseHover] = React.useState(false);
|
||||
const commentByOwnerOfContent = claim && claim.signing_channel && claim.signing_channel.permanent_url === authorUri;
|
||||
const { claimName } = parseURI(authorUri);
|
||||
|
@ -47,6 +64,22 @@ function LivestreamComment(props: Props) {
|
|||
<div className="livestream-comment__body">
|
||||
{supportAmount > 0 && <ChannelThumbnail uri={authorUri} xsmall />}
|
||||
<div className="livestream-comment__info">
|
||||
{isGlobalMod && (
|
||||
<Tooltip label={__('Admin')}>
|
||||
<span className="comment__badge comment__badge--global-mod">
|
||||
<Icon icon={ICONS.BADGE_MOD} size={16} />
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{isModerator && (
|
||||
<Tooltip label={__('Moderator')}>
|
||||
<span className="comment__badge comment__badge--mod">
|
||||
<Icon icon={ICONS.BADGE_MOD} size={16} />
|
||||
</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<Button
|
||||
className={classnames('button--uri-indicator comment__author', {
|
||||
'comment__author--creator': commentByOwnerOfContent,
|
||||
|
@ -57,6 +90,13 @@ function LivestreamComment(props: Props) {
|
|||
{claimName}
|
||||
</Button>
|
||||
|
||||
{isPinned && (
|
||||
<span className="comment__pin">
|
||||
<Icon icon={ICONS.PIN} size={14} />
|
||||
{__('Pinned')}
|
||||
</span>
|
||||
)}
|
||||
|
||||
<div className="livestream-comment__text">
|
||||
<MarkdownPreview content={message} promptLinks stakedLevel={stakedLevel} />
|
||||
</div>
|
||||
|
@ -78,6 +118,8 @@ function LivestreamComment(props: Props) {
|
|||
authorUri={authorUri}
|
||||
commentIsMine={commentIsMine}
|
||||
disableEdit
|
||||
isTopLevel
|
||||
isPinned={isPinned}
|
||||
disableRemove={supportAmount > 0}
|
||||
/>
|
||||
</Menu>
|
||||
|
|
|
@ -7,12 +7,14 @@ import {
|
|||
selectIsFetchingComments,
|
||||
makeSelectSuperChatsForUri,
|
||||
makeSelectSuperChatTotalAmountForUri,
|
||||
makeSelectPinnedCommentsForUri,
|
||||
} from 'redux/selectors/comments';
|
||||
import LivestreamComments from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
claim: makeSelectClaimForUri(props.uri)(state),
|
||||
comments: makeSelectTopLevelCommentsForUri(props.uri)(state).slice(0, 75),
|
||||
pinnedComments: makeSelectPinnedCommentsForUri(props.uri)(state),
|
||||
fetchingComments: selectIsFetchingComments(state),
|
||||
superChats: makeSelectSuperChatsForUri(props.uri)(state),
|
||||
superChatsTotalAmount: makeSelectSuperChatTotalAmountForUri(props.uri)(state),
|
||||
|
|
|
@ -19,16 +19,15 @@ type Props = {
|
|||
doCommentSocketDisconnect: (string) => void,
|
||||
doCommentList: (string, string, number, number) => void,
|
||||
comments: Array<Comment>,
|
||||
pinnedComments: Array<Comment>,
|
||||
fetchingComments: boolean,
|
||||
doSuperChatList: (string) => void,
|
||||
superChats: Array<Comment>,
|
||||
superChatsTotalAmount: number,
|
||||
myChannels: ?Array<ChannelClaim>,
|
||||
};
|
||||
|
||||
const VIEW_MODE_CHAT = 'view_chat';
|
||||
const VIEW_MODE_SUPER_CHAT = 'view_superchat';
|
||||
const COMMENT_SCROLL_OFFSET = 100;
|
||||
const COMMENT_SCROLL_TIMEOUT = 25;
|
||||
|
||||
export default function LivestreamComments(props: Props) {
|
||||
|
@ -38,36 +37,32 @@ export default function LivestreamComments(props: Props) {
|
|||
embed,
|
||||
doCommentSocketConnect,
|
||||
doCommentSocketDisconnect,
|
||||
comments,
|
||||
comments: commentsByChronologicalOrder,
|
||||
pinnedComments,
|
||||
doCommentList,
|
||||
fetchingComments,
|
||||
doSuperChatList,
|
||||
superChats,
|
||||
superChatsTotalAmount,
|
||||
myChannels,
|
||||
superChats: superChatsByTipAmount,
|
||||
} = props;
|
||||
|
||||
let superChatsFiatAmount, superChatsTotalAmount;
|
||||
|
||||
const commentsRef = React.createRef();
|
||||
const [scrollBottom, setScrollBottom] = React.useState(true);
|
||||
const [viewMode, setViewMode] = React.useState(VIEW_MODE_CHAT);
|
||||
const [performedInitialScroll, setPerformedInitialScroll] = React.useState(false);
|
||||
const [scrollPos, setScrollPos] = React.useState(0);
|
||||
const claimId = claim && claim.claim_id;
|
||||
const commentsLength = comments && comments.length;
|
||||
const commentsToDisplay = viewMode === VIEW_MODE_CHAT ? comments : superChats;
|
||||
const commentsLength = commentsByChronologicalOrder && commentsByChronologicalOrder.length;
|
||||
const commentsToDisplay = viewMode === VIEW_MODE_CHAT ? commentsByChronologicalOrder : superChatsByTipAmount;
|
||||
|
||||
const discussionElement = document.querySelector('.livestream__comments');
|
||||
const commentElement = document.querySelector('.livestream-comment');
|
||||
|
||||
// todo: implement comment_list --mine in SDK so redux can grab with selectCommentIsMine
|
||||
function isMyComment(channelId: string) {
|
||||
if (myChannels != null && channelId != null) {
|
||||
for (let i = 0; i < myChannels.length; i++) {
|
||||
if (myChannels[i].claim_id === channelId) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
const pinnedComment = pinnedComments.length > 0 ? pinnedComments[0] : null;
|
||||
|
||||
function restoreScrollPos() {
|
||||
if (discussionElement) {
|
||||
discussionElement.scrollTop = 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
|
@ -84,87 +79,142 @@ export default function LivestreamComments(props: Props) {
|
|||
};
|
||||
}, [claimId, uri, doCommentList, doSuperChatList, doCommentSocketConnect, doCommentSocketDisconnect]);
|
||||
|
||||
const handleScroll = React.useCallback(() => {
|
||||
if (discussionElement) {
|
||||
const negativeCommentHeight = commentElement && -1 * commentElement.offsetHeight;
|
||||
const isAtRecent = negativeCommentHeight && discussionElement.scrollTop >= negativeCommentHeight;
|
||||
|
||||
setScrollBottom(isAtRecent);
|
||||
}
|
||||
}, [commentElement, discussionElement]);
|
||||
|
||||
// Register scroll handler (TODO: Should throttle/debounce)
|
||||
React.useEffect(() => {
|
||||
if (discussionElement) {
|
||||
discussionElement.addEventListener('scroll', handleScroll);
|
||||
|
||||
if (commentsLength > 0) {
|
||||
// Only update comment scroll if the user hasn't scrolled up to view old comments
|
||||
// If they have, do nothing
|
||||
if (!performedInitialScroll) {
|
||||
setTimeout(
|
||||
() =>
|
||||
(discussionElement.scrollTop =
|
||||
discussionElement.scrollHeight - discussionElement.offsetHeight + COMMENT_SCROLL_OFFSET),
|
||||
COMMENT_SCROLL_TIMEOUT
|
||||
);
|
||||
setPerformedInitialScroll(true);
|
||||
function handleScroll() {
|
||||
if (discussionElement) {
|
||||
const scrollTop = discussionElement.scrollTop;
|
||||
if (scrollTop !== scrollPos) {
|
||||
setScrollPos(scrollTop);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (discussionElement) {
|
||||
discussionElement.addEventListener('scroll', handleScroll);
|
||||
return () => discussionElement.removeEventListener('scroll', handleScroll);
|
||||
}
|
||||
}, [commentsLength, discussionElement, handleScroll, performedInitialScroll, setPerformedInitialScroll]);
|
||||
}, [discussionElement, scrollPos]);
|
||||
|
||||
// Retain scrollPos=0 when receiving new messages.
|
||||
React.useEffect(() => {
|
||||
if (discussionElement && commentsLength > 0) {
|
||||
// Only update comment scroll if the user hasn't scrolled up to view old comments
|
||||
if (scrollPos >= 0) {
|
||||
// +ve scrollPos: not scrolled (Usually, there'll be a few pixels beyond 0).
|
||||
// -ve scrollPos: user scrolled.
|
||||
const timer = setTimeout(() => {
|
||||
// Use a timer here to ensure we reset after the new comment has been rendered.
|
||||
discussionElement.scrollTop = 0;
|
||||
}, COMMENT_SCROLL_TIMEOUT);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [commentsLength]); // (Just respond to 'commentsLength' updates and nothing else)
|
||||
|
||||
// sum total amounts for fiat tips and lbc tips
|
||||
if (superChatsByTipAmount) {
|
||||
let fiatAmount = 0;
|
||||
let LBCAmount = 0;
|
||||
for (const superChat of superChatsByTipAmount) {
|
||||
if (superChat.is_fiat) {
|
||||
fiatAmount = fiatAmount + superChat.support_amount;
|
||||
} else {
|
||||
LBCAmount = LBCAmount + superChat.support_amount;
|
||||
}
|
||||
}
|
||||
|
||||
superChatsFiatAmount = fiatAmount;
|
||||
superChatsTotalAmount = LBCAmount;
|
||||
}
|
||||
|
||||
let superChatsReversed;
|
||||
// array of superchats organized by fiat or not first, then support amount
|
||||
if (superChatsByTipAmount) {
|
||||
const clonedSuperchats = JSON.parse(JSON.stringify(superChatsByTipAmount));
|
||||
|
||||
// sort by fiat first then by support amount
|
||||
superChatsReversed = clonedSuperchats
|
||||
.sort((a, b) => {
|
||||
// if both are fiat, organize by support
|
||||
if (a.is_fiat === b.is_fiat) {
|
||||
return b.support_amount - a.support_amount;
|
||||
// otherwise, if they are not both fiat, put the fiat transaction first
|
||||
} else {
|
||||
return a.is_fiat === b.is_fiat ? 0 : a.is_fiat ? -1 : 1;
|
||||
}
|
||||
})
|
||||
.reverse();
|
||||
}
|
||||
|
||||
// todo: implement comment_list --mine in SDK so redux can grab with selectCommentIsMine
|
||||
function isMyComment(channelId: string) {
|
||||
if (myChannels != null && channelId != null) {
|
||||
for (let i = 0; i < myChannels.length; i++) {
|
||||
if (myChannels[i].claim_id === channelId) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!claim) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function scrollBack() {
|
||||
if (discussionElement) {
|
||||
discussionElement.scrollTop = 0;
|
||||
setScrollBottom(true);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="card livestream__discussion">
|
||||
<div className="card__header--between livestream-discussion__header">
|
||||
<div className="livestream-discussion__title">{__('Live discussion')}</div>
|
||||
{superChatsTotalAmount > 0 && (
|
||||
{(superChatsTotalAmount || 0) > 0 && (
|
||||
<div className="recommended-content__toggles">
|
||||
{/* the superchats in chronological order button */}
|
||||
<Button
|
||||
className={classnames('button-toggle', {
|
||||
'button-toggle--active': viewMode === VIEW_MODE_CHAT,
|
||||
})}
|
||||
label={__('Chat')}
|
||||
onClick={() => setViewMode(VIEW_MODE_CHAT)}
|
||||
onClick={() => {
|
||||
setViewMode(VIEW_MODE_CHAT);
|
||||
const livestreamCommentsDiv = document.getElementsByClassName('livestream__comments')[0];
|
||||
livestreamCommentsDiv.scrollTop = livestreamCommentsDiv.scrollHeight;
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* the button to show superchats listed by most to least support amount */}
|
||||
<Button
|
||||
className={classnames('button-toggle', {
|
||||
'button-toggle--active': viewMode === VIEW_MODE_SUPER_CHAT,
|
||||
})}
|
||||
label={
|
||||
<>
|
||||
<CreditAmount amount={superChatsTotalAmount} size={8} /> {__('Tipped')}
|
||||
<CreditAmount amount={superChatsTotalAmount || 0} size={8} /> /
|
||||
<CreditAmount amount={superChatsFiatAmount || 0} size={8} isFiat /> {__('Tipped')}
|
||||
</>
|
||||
}
|
||||
onClick={() => setViewMode(VIEW_MODE_SUPER_CHAT)}
|
||||
onClick={() => {
|
||||
setViewMode(VIEW_MODE_SUPER_CHAT);
|
||||
const livestreamCommentsDiv = document.getElementsByClassName('livestream__comments')[0];
|
||||
const divHeight = livestreamCommentsDiv.scrollHeight;
|
||||
livestreamCommentsDiv.scrollTop = divHeight * -1;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<>
|
||||
{fetchingComments && !comments && (
|
||||
{fetchingComments && !commentsByChronologicalOrder && (
|
||||
<div className="main--empty">
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
<div ref={commentsRef} className="livestream__comments-wrapper">
|
||||
{viewMode === VIEW_MODE_CHAT && superChatsTotalAmount > 0 && superChats && (
|
||||
{viewMode === VIEW_MODE_CHAT && superChatsByTipAmount && (superChatsTotalAmount || 0) > 0 && (
|
||||
<div className="livestream-superchats__wrapper">
|
||||
<div className="livestream-superchats__inner">
|
||||
{superChats.map((superChat: Comment) => (
|
||||
{superChatsByTipAmount.map((superChat: Comment) => (
|
||||
<Tooltip key={superChat.comment_id} label={superChat.comment}>
|
||||
<div className="livestream-superchat">
|
||||
<div className="livestream-superchat__thumbnail">
|
||||
|
@ -187,36 +237,75 @@ export default function LivestreamComments(props: Props) {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{!fetchingComments && comments.length > 0 ? (
|
||||
{pinnedComment && (
|
||||
<div className="livestream-pinned__wrapper">
|
||||
<LivestreamComment
|
||||
key={pinnedComment.comment_id}
|
||||
uri={uri}
|
||||
authorUri={pinnedComment.channel_url}
|
||||
commentId={pinnedComment.comment_id}
|
||||
message={pinnedComment.comment}
|
||||
supportAmount={pinnedComment.support_amount}
|
||||
isModerator={pinnedComment.is_moderator}
|
||||
isGlobalMod={pinnedComment.is_global_mod}
|
||||
isFiat={pinnedComment.is_fiat}
|
||||
isPinned={pinnedComment.is_pinned}
|
||||
commentIsMine={pinnedComment.channel_id && isMyComment(pinnedComment.channel_id)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* top to bottom comment display */}
|
||||
{!fetchingComments && commentsByChronologicalOrder.length > 0 ? (
|
||||
<div className="livestream__comments">
|
||||
{commentsToDisplay.map((comment) => (
|
||||
<LivestreamComment
|
||||
key={comment.comment_id}
|
||||
uri={uri}
|
||||
authorUri={comment.channel_url}
|
||||
commentId={comment.comment_id}
|
||||
message={comment.comment}
|
||||
supportAmount={comment.support_amount}
|
||||
isFiat={comment.is_fiat}
|
||||
commentIsMine={comment.channel_id && isMyComment(comment.channel_id)}
|
||||
/>
|
||||
))}
|
||||
{viewMode === VIEW_MODE_CHAT &&
|
||||
commentsToDisplay.map((comment) => (
|
||||
<LivestreamComment
|
||||
key={comment.comment_id}
|
||||
uri={uri}
|
||||
authorUri={comment.channel_url}
|
||||
commentId={comment.comment_id}
|
||||
message={comment.comment}
|
||||
supportAmount={comment.support_amount}
|
||||
isModerator={comment.is_moderator}
|
||||
isGlobalMod={comment.is_global_mod}
|
||||
isFiat={comment.is_fiat}
|
||||
commentIsMine={comment.channel_id && isMyComment(comment.channel_id)}
|
||||
/>
|
||||
))}
|
||||
|
||||
{viewMode === VIEW_MODE_SUPER_CHAT &&
|
||||
superChatsReversed &&
|
||||
superChatsReversed.map((comment) => (
|
||||
<LivestreamComment
|
||||
key={comment.comment_id}
|
||||
uri={uri}
|
||||
authorUri={comment.channel_url}
|
||||
commentId={comment.comment_id}
|
||||
message={comment.comment}
|
||||
supportAmount={comment.support_amount}
|
||||
isModerator={comment.is_moderator}
|
||||
isGlobalMod={comment.is_global_mod}
|
||||
isFiat={comment.is_fiat}
|
||||
commentIsMine={comment.channel_id && isMyComment(comment.channel_id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="main--empty" style={{ flex: 1 }} />
|
||||
)}
|
||||
|
||||
{!scrollBottom && (
|
||||
{scrollPos < 0 && (
|
||||
<Button
|
||||
button="alt"
|
||||
className="livestream__comments-scroll__down"
|
||||
label={__('Recent Comments')}
|
||||
onClick={scrollBack}
|
||||
onClick={restoreScrollPos}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="livestream__comment-create">
|
||||
<CommentCreate livestream bottom embed={embed} uri={uri} />
|
||||
<CommentCreate livestream bottom embed={embed} uri={uri} onDoneReplying={restoreScrollPos} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
|
|
@ -16,6 +16,8 @@ import FileThumbnail from 'component/fileThumbnail';
|
|||
import { Menu, MenuList, MenuButton, MenuItem } from '@reach/menu-button';
|
||||
import NotificationContentChannelMenu from 'component/notificationContentChannelMenu';
|
||||
import LbcMessage from 'component/common/lbc-message';
|
||||
import UriIndicator from 'component/uriIndicator';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
|
||||
type Props = {
|
||||
notification: WebNotification,
|
||||
|
@ -29,13 +31,12 @@ export default function Notification(props: Props) {
|
|||
const { notification, menuButton = false, doReadNotifications, doDeleteNotification } = props;
|
||||
const { push } = useHistory();
|
||||
const { notification_rule, notification_parameters, is_read, id } = notification;
|
||||
|
||||
const isCommentNotification =
|
||||
notification_rule === RULE.COMMENT ||
|
||||
notification_rule === RULE.COMMENT_REPLY ||
|
||||
notification_rule === RULE.CREATOR_COMMENT;
|
||||
const commentText = isCommentNotification && notification_parameters.dynamic.comment;
|
||||
const channelUrl =
|
||||
(notification_rule === RULE.NEW_CONTENT && notification.notification_parameters.dynamic.channel_url) || '';
|
||||
|
||||
let notificationTarget;
|
||||
switch (notification_rule) {
|
||||
|
@ -51,21 +52,14 @@ export default function Notification(props: Props) {
|
|||
notificationTarget = notification_parameters.device.target;
|
||||
}
|
||||
|
||||
let notificationLink = formatLbryUrlForWeb(notificationTarget);
|
||||
let urlParams = new URLSearchParams();
|
||||
if (isCommentNotification && notification_parameters.dynamic.hash) {
|
||||
urlParams.append('lc', notification_parameters.dynamic.hash);
|
||||
}
|
||||
|
||||
try {
|
||||
const { isChannel } = parseURI(notificationTarget);
|
||||
if (isChannel) {
|
||||
urlParams.append(PAGE_VIEW_QUERY, DISCUSSION_PAGE);
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
notificationLink += `?${urlParams.toString()}`;
|
||||
|
||||
const creatorIcon = (channelUrl) => {
|
||||
return (
|
||||
<UriIndicator uri={channelUrl} link>
|
||||
<ChannelThumbnail small uri={channelUrl} />
|
||||
</UriIndicator>
|
||||
);
|
||||
};
|
||||
let channelUrl;
|
||||
let icon;
|
||||
switch (notification_rule) {
|
||||
case RULE.CREATOR_SUBSCRIBER:
|
||||
|
@ -73,16 +67,20 @@ export default function Notification(props: Props) {
|
|||
break;
|
||||
case RULE.COMMENT:
|
||||
case RULE.CREATOR_COMMENT:
|
||||
icon = <ChannelThumbnail small uri={notification_parameters.dynamic.comment_author} />;
|
||||
channelUrl = notification_parameters.dynamic.comment_author;
|
||||
icon = creatorIcon(channelUrl);
|
||||
break;
|
||||
case RULE.COMMENT_REPLY:
|
||||
icon = <ChannelThumbnail small uri={notification_parameters.dynamic.reply_author} />;
|
||||
channelUrl = notification_parameters.dynamic.reply_author;
|
||||
icon = creatorIcon(channelUrl);
|
||||
break;
|
||||
case RULE.NEW_CONTENT:
|
||||
icon = <ChannelThumbnail small uri={notification_parameters.dynamic.channel_url} />;
|
||||
channelUrl = notification_parameters.dynamic.channel_url;
|
||||
icon = creatorIcon(channelUrl);
|
||||
break;
|
||||
case RULE.NEW_LIVESTREAM:
|
||||
icon = <ChannelThumbnail small uri={notification_parameters.dynamic.channel_url} />;
|
||||
channelUrl = notification_parameters.dynamic.channel_url;
|
||||
icon = creatorIcon(channelUrl);
|
||||
break;
|
||||
case RULE.DAILY_WATCH_AVAILABLE:
|
||||
case RULE.DAILY_WATCH_REMIND:
|
||||
|
@ -97,12 +95,54 @@ export default function Notification(props: Props) {
|
|||
icon = <Icon icon={ICONS.NOTIFICATION} sectionIcon />;
|
||||
}
|
||||
|
||||
let notificationLink = formatLbryUrlForWeb(notificationTarget);
|
||||
let urlParams = new URLSearchParams();
|
||||
if (isCommentNotification && notification_parameters.dynamic.hash) {
|
||||
urlParams.append('lc', notification_parameters.dynamic.hash);
|
||||
}
|
||||
|
||||
let channelName = channelUrl && '@' + channelUrl.split('@')[1].split('#')[0];
|
||||
|
||||
const notificationTitle = notification_parameters.device.title;
|
||||
const titleSplit = notificationTitle.split(' ');
|
||||
let fullTitle = [' '];
|
||||
let uriIndicator;
|
||||
const title = titleSplit.map((message, index) => {
|
||||
if (channelName === message) {
|
||||
uriIndicator = <UriIndicator uri={channelUrl} link />;
|
||||
fullTitle.push(' ');
|
||||
const resultTitle = fullTitle;
|
||||
fullTitle = [' '];
|
||||
|
||||
return [resultTitle.join(' '), uriIndicator];
|
||||
} else {
|
||||
fullTitle.push(message);
|
||||
|
||||
if (index === titleSplit.length - 1) {
|
||||
return <LbcMessage>{fullTitle.join(' ')}</LbcMessage>;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const { isChannel } = parseURI(notificationTarget);
|
||||
if (isChannel) {
|
||||
urlParams.append(PAGE_VIEW_QUERY, DISCUSSION_PAGE);
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
notificationLink += `?${urlParams.toString()}`;
|
||||
const navLinkProps = {
|
||||
to: notificationLink,
|
||||
onClick: (e) => e.stopPropagation(),
|
||||
};
|
||||
|
||||
function handleNotificationClick() {
|
||||
if (!is_read) {
|
||||
doReadNotifications([id]);
|
||||
}
|
||||
|
||||
if (notificationLink) {
|
||||
if (menuButton && notificationLink) {
|
||||
push(notificationLink);
|
||||
}
|
||||
}
|
||||
|
@ -120,9 +160,11 @@ export default function Notification(props: Props) {
|
|||
)
|
||||
: notificationLink
|
||||
? (props: { children: any }) => (
|
||||
<a className="menu__link--notification" onClick={handleNotificationClick}>
|
||||
{props.children}
|
||||
</a>
|
||||
<NavLink {...navLinkProps}>
|
||||
<a className="menu__link--notification" onClick={handleNotificationClick}>
|
||||
{props.children}
|
||||
</a>
|
||||
</NavLink>
|
||||
)
|
||||
: (props: { children: any }) => (
|
||||
<span
|
||||
|
@ -145,17 +187,11 @@ export default function Notification(props: Props) {
|
|||
<div className="notification__content-wrapper">
|
||||
<div className="notification__content">
|
||||
<div className="notification__text-wrapper">
|
||||
{!isCommentNotification && (
|
||||
<div className="notification__title">
|
||||
<LbcMessage>{notification_parameters.device.title}</LbcMessage>
|
||||
</div>
|
||||
)}
|
||||
{!isCommentNotification && <div className="notification__title">{title}</div>}
|
||||
|
||||
{isCommentNotification && commentText ? (
|
||||
<>
|
||||
<div className="notification__title">
|
||||
<LbcMessage>{notification_parameters.device.title}</LbcMessage>
|
||||
</div>
|
||||
<div className="notification__title">{title}</div>
|
||||
<div title={commentText} className="notification__text mobile-hidden">
|
||||
{commentText}
|
||||
</div>
|
||||
|
@ -193,7 +229,13 @@ export default function Notification(props: Props) {
|
|||
|
||||
<div className="notification__menu">
|
||||
<Menu>
|
||||
<MenuButton className={'menu__button notification__menu-button'} onClick={(e) => e.stopPropagation()}>
|
||||
<MenuButton
|
||||
className={'menu__button notification__menu-button'}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<Icon size={18} icon={ICONS.MORE_VERTICAL} />
|
||||
</MenuButton>
|
||||
<MenuList className="menu__list">
|
||||
|
|
|
@ -18,7 +18,12 @@ import {
|
|||
} from 'lbry-redux';
|
||||
import * as RENDER_MODES from 'constants/file_render_modes';
|
||||
import { doPublishDesktop } from 'redux/actions/publish';
|
||||
import { selectUnclaimedRewardValue } from 'redux/selectors/rewards';
|
||||
import { doClaimInitialRewards } from 'redux/actions/rewards';
|
||||
import {
|
||||
selectUnclaimedRewardValue,
|
||||
selectIsClaimingInitialRewards,
|
||||
selectHasClaimedInitialRewards,
|
||||
} from 'redux/selectors/rewards';
|
||||
import {
|
||||
selectModal,
|
||||
selectActiveChannelClaim,
|
||||
|
@ -27,8 +32,8 @@ import {
|
|||
} from 'redux/selectors/app';
|
||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||
import { makeSelectFileRenderModeForUri } from 'redux/selectors/content';
|
||||
import PublishPage from './view';
|
||||
import { selectUser } from 'redux/selectors/user';
|
||||
import PublishPage from './view';
|
||||
|
||||
const select = (state) => {
|
||||
const myClaimForUri = selectMyClaimForUri(state);
|
||||
|
@ -59,6 +64,8 @@ const select = (state) => {
|
|||
myChannels: selectMyChannelClaims(state),
|
||||
incognito: selectIncognito(state),
|
||||
activeChannelStakedLevel: selectActiveChannelStakedLevel(state),
|
||||
isClaimingInitialRewards: selectIsClaimingInitialRewards(state),
|
||||
hasClaimedInitialRewards: selectHasClaimedInitialRewards(state),
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -70,6 +77,7 @@ const perform = (dispatch) => ({
|
|||
prepareEdit: (claim, uri) => dispatch(doPrepareEdit(claim, uri)),
|
||||
resetThumbnailStatus: () => dispatch(doResetThumbnailStatus()),
|
||||
checkAvailability: (name) => dispatch(doCheckPublishNameAvailability(name)),
|
||||
claimInitialRewards: () => dispatch(doClaimInitialRewards()),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(PublishPage);
|
||||
|
|
|
@ -90,6 +90,9 @@ type Props = {
|
|||
isPostClaim: boolean,
|
||||
permanentUrl: ?string,
|
||||
remoteUrl: ?string,
|
||||
isClaimingInitialRewards: boolean,
|
||||
claimInitialRewards: () => void,
|
||||
hasClaimedInitialRewards: boolean,
|
||||
};
|
||||
|
||||
function PublishForm(props: Props) {
|
||||
|
@ -128,6 +131,9 @@ function PublishForm(props: Props) {
|
|||
isPostClaim,
|
||||
permanentUrl,
|
||||
remoteUrl,
|
||||
isClaimingInitialRewards,
|
||||
claimInitialRewards,
|
||||
hasClaimedInitialRewards,
|
||||
} = props;
|
||||
|
||||
const { replace, location } = useHistory();
|
||||
|
@ -263,6 +269,12 @@ function PublishForm(props: Props) {
|
|||
}
|
||||
}, [activeChannelClaimStr, setSignedMessage]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasClaimedInitialRewards) {
|
||||
claimInitialRewards();
|
||||
}
|
||||
}, [hasClaimedInitialRewards, claimInitialRewards]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!modal) {
|
||||
setTimeout(() => {
|
||||
|
@ -298,7 +310,10 @@ function PublishForm(props: Props) {
|
|||
|
||||
const isLivestreamMode = mode === PUBLISH_MODES.LIVESTREAM;
|
||||
let submitLabel;
|
||||
if (publishing) {
|
||||
|
||||
if (isClaimingInitialRewards) {
|
||||
submitLabel = __('Claiming credits...');
|
||||
} else if (publishing) {
|
||||
if (isStillEditing) {
|
||||
submitLabel = __('Saving...');
|
||||
} else if (isLivestreamMode) {
|
||||
|
@ -623,6 +638,7 @@ function PublishForm(props: Props) {
|
|||
onClick={handlePublish}
|
||||
label={submitLabel}
|
||||
disabled={
|
||||
isClaimingInitialRewards ||
|
||||
formDisabled ||
|
||||
!formValid ||
|
||||
uploadThumbnailStatus === THUMBNAIL_STATUSES.IN_PROGRESS ||
|
||||
|
|
|
@ -15,6 +15,7 @@ import ClaimPreview from 'component/claimPreview';
|
|||
import { URL as SITE_URL, URL_LOCAL, URL_DEV } from 'config';
|
||||
import HelpLink from 'component/common/help-link';
|
||||
import WalletSpendableBalanceHelp from 'component/walletSpendableBalanceHelp';
|
||||
import BidHelpText from 'component/publishBid/bid-help-text';
|
||||
import Spinner from 'component/spinner';
|
||||
|
||||
type Props = {
|
||||
|
@ -77,7 +78,6 @@ function RepostCreate(props: Props) {
|
|||
|
||||
const [repostBid, setRepostBid] = React.useState(0.01);
|
||||
const [repostBidError, setRepostBidError] = React.useState(undefined);
|
||||
const [takeoverAmount, setTakeoverAmount] = React.useState(0);
|
||||
const [enteredRepostName, setEnteredRepostName] = React.useState(defaultName);
|
||||
const [available, setAvailable] = React.useState(true);
|
||||
const [enteredContent, setEnteredContentUri] = React.useState(undefined);
|
||||
|
@ -189,10 +189,9 @@ function RepostCreate(props: Props) {
|
|||
: Number(passedRepostAmount) + 0.01;
|
||||
|
||||
if (repostTakeoverAmount) {
|
||||
setTakeoverAmount(Number(repostTakeoverAmount.toFixed(2)));
|
||||
setAutoRepostBid(repostTakeoverAmount);
|
||||
}
|
||||
}, [setTakeoverAmount, enteredRepostAmount, passedRepostAmount]);
|
||||
}, [enteredRepostAmount, passedRepostAmount]);
|
||||
|
||||
// repost bid error
|
||||
React.useEffect(() => {
|
||||
|
@ -381,9 +380,11 @@ function RepostCreate(props: Props) {
|
|||
error={repostBidError}
|
||||
helper={
|
||||
<>
|
||||
{__('Winning amount: %amount%', {
|
||||
amount: Number(takeoverAmount).toFixed(2),
|
||||
})}
|
||||
<BidHelpText
|
||||
uri={'lbry://' + enteredRepostName}
|
||||
amountNeededForTakeover={enteredRepostAmount}
|
||||
isResolvingUri={isResolvingEnteredRepost}
|
||||
/>
|
||||
<WalletSpendableBalanceHelp inline />
|
||||
</>
|
||||
}
|
||||
|
|
17
ui/component/settingCommentsServer/index.js
Normal file
17
ui/component/settingCommentsServer/index.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { SETTINGS } from 'lbry-redux';
|
||||
import { doSetClientSetting } from 'redux/actions/settings';
|
||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||
import SettingCommentsServer from './view';
|
||||
|
||||
const select = (state) => ({
|
||||
customServerEnabled: makeSelectClientSetting(SETTINGS.CUSTOM_COMMENTS_SERVER_ENABLED)(state),
|
||||
customServerUrl: makeSelectClientSetting(SETTINGS.CUSTOM_COMMENTS_SERVER_URL)(state),
|
||||
});
|
||||
|
||||
const perform = (dispatch) => ({
|
||||
setCustomServerEnabled: (val) => dispatch(doSetClientSetting(SETTINGS.CUSTOM_COMMENTS_SERVER_ENABLED, val, true)),
|
||||
setCustomServerUrl: (url) => dispatch(doSetClientSetting(SETTINGS.CUSTOM_COMMENTS_SERVER_URL, url, true)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(SettingCommentsServer);
|
70
ui/component/settingCommentsServer/view.jsx
Normal file
70
ui/component/settingCommentsServer/view.jsx
Normal file
|
@ -0,0 +1,70 @@
|
|||
// @flow
|
||||
import { COMMENT_SERVER_NAME } from 'config';
|
||||
import React from 'react';
|
||||
import Comments from 'comments';
|
||||
import { FormField } from 'component/common/form';
|
||||
|
||||
const DEBOUNCE_TEXT_INPUT_MS = 500;
|
||||
|
||||
type Props = {
|
||||
customServerEnabled: boolean,
|
||||
customServerUrl: string,
|
||||
setCustomServerEnabled: (boolean) => void,
|
||||
setCustomServerUrl: (string) => void,
|
||||
};
|
||||
|
||||
function SettingCommentsServer(props: Props) {
|
||||
const { customServerEnabled, customServerUrl, setCustomServerEnabled, setCustomServerUrl } = props;
|
||||
const [customUrl, setCustomUrl] = React.useState(customServerUrl);
|
||||
|
||||
React.useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setCustomServerUrl(customUrl);
|
||||
Comments.url = customUrl;
|
||||
}, DEBOUNCE_TEXT_INPUT_MS);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [customUrl, setCustomServerUrl]);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<fieldset-section>
|
||||
<FormField
|
||||
type="radio"
|
||||
name="use_default_comments_server"
|
||||
label={__('Default comments server (%name%)', { name: COMMENT_SERVER_NAME })}
|
||||
checked={!customServerEnabled}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
setCustomServerEnabled(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<FormField
|
||||
type="radio"
|
||||
name="use_custom_comments_server"
|
||||
label={__('Custom comments server')}
|
||||
checked={customServerEnabled}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
setCustomServerEnabled(true);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{customServerEnabled && (
|
||||
<div className="section__body">
|
||||
<FormField
|
||||
type="text"
|
||||
placeholder="https://comment.mysite.com"
|
||||
value={customUrl}
|
||||
onChange={(e) => setCustomUrl(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</fieldset-section>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
export default SettingCommentsServer;
|
|
@ -28,6 +28,9 @@ function UserChannelFollowIntro(props: Props) {
|
|||
channelIds = PRIMARY_CONTENT.channelIds;
|
||||
}
|
||||
const followingCount = (subscribedChannels && subscribedChannels.length) || 0;
|
||||
const followingCountIgnoringAutoFollows = (subscribedChannels || []).filter(
|
||||
(channel) => !channelsToSubscribe.includes(channel.uri)
|
||||
).length;
|
||||
|
||||
// subscribe to lbry
|
||||
useEffect(() => {
|
||||
|
@ -74,9 +77,13 @@ function UserChannelFollowIntro(props: Props) {
|
|||
<Nag
|
||||
type="helpful"
|
||||
message={
|
||||
followingCount === 1
|
||||
? __('Nice! You are currently following %followingCount% creator', { followingCount })
|
||||
: __('Nice! You are currently following %followingCount% creators', { followingCount })
|
||||
followingCountIgnoringAutoFollows === 1
|
||||
? __('Nice! You are currently following %followingCount% creator', {
|
||||
followingCount: followingCountIgnoringAutoFollows,
|
||||
})
|
||||
: __('Nice! You are currently following %followingCount% creators', {
|
||||
followingCount: followingCountIgnoringAutoFollows,
|
||||
})
|
||||
}
|
||||
actionText={__('Continue')}
|
||||
onClick={onContinue}
|
||||
|
|
|
@ -97,7 +97,7 @@ class AniviewPlugin extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
videojs.registerComponent('recsys', AniviewPlugin);
|
||||
videojs.registerComponent('aniview', AniviewPlugin);
|
||||
|
||||
const onPlayerReady = (player, options) => {
|
||||
player.aniview = new AniviewPlugin(player, options);
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
makeSelectRecommendedClaimIds,
|
||||
makeSelectRecommendationClicks,
|
||||
} from 'redux/selectors/content';
|
||||
import { makeSelectRecommendedRecsysIdForClaimId } from 'redux/selectors/search';
|
||||
|
||||
const VERSION = '0.0.1';
|
||||
|
||||
|
@ -36,7 +37,7 @@ function createRecsys(claimId, userId, events, loadedAt, isEmbed) {
|
|||
claimId: claimId,
|
||||
pageLoadedAt: pageLoadedAt,
|
||||
pageExitedAt: pageExitedAt,
|
||||
recsysId: recsysId,
|
||||
recsysId: makeSelectRecommendedRecsysIdForClaimId(claimId)(state) || recsysId,
|
||||
recClaimIds: makeSelectRecommendedClaimIds(claimId)(state),
|
||||
recClickedVideoIdx: makeSelectRecommendationClicks(claimId)(state),
|
||||
events: events,
|
||||
|
|
|
@ -13,8 +13,9 @@ import hlsQualitySelector from './plugins/videojs-hls-quality-selector/plugin';
|
|||
import recsys from './plugins/videojs-recsys/plugin';
|
||||
import qualityLevels from 'videojs-contrib-quality-levels';
|
||||
import isUserTyping from 'util/detect-typing';
|
||||
// @if TARGET='web'
|
||||
import './plugins/videojs-aniview/plugin';
|
||||
|
||||
// @endif
|
||||
const isDev = process.env.NODE_ENV !== 'production';
|
||||
|
||||
export type Player = {
|
||||
|
@ -585,9 +586,11 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
|||
// This must be initialized earlier than everything else
|
||||
// otherwise a race condition occurs if we place this in the onReady call back
|
||||
// allow if isDev because otherwise you'll never see ads when basing to master
|
||||
// @if TARGET='web'
|
||||
if ((allowPreRoll && SIMPLE_SITE) || isDev) {
|
||||
vjs.aniview();
|
||||
}
|
||||
// @endif
|
||||
|
||||
// fixes #3498 (https://github.com/lbryio/lbry-desktop/issues/3498)
|
||||
// summary: on firefox the focus would stick to the fullscreen button which caused buggy behavior with spacebar
|
||||
|
@ -662,6 +665,7 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
|||
}, [source, reload]);
|
||||
|
||||
// Load IMA3 SDK for aniview
|
||||
// @if TARGET='web'
|
||||
useEffect(() => {
|
||||
const script = document.createElement('script');
|
||||
script.src = `https://imasdk.googleapis.com/js/sdkloader/ima3.js`;
|
||||
|
@ -674,6 +678,7 @@ export default React.memo<Props>(function VideoJs(props: Props) {
|
|||
document.body.removeChild(script);
|
||||
};
|
||||
});
|
||||
// @endif
|
||||
|
||||
return (
|
||||
// $FlowFixMe
|
||||
|
|
3
ui/component/walletFiatAccountHistory/index.js
Normal file
3
ui/component/walletFiatAccountHistory/index.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
import FiatAccountHistory from './view';
|
||||
|
||||
export default FiatAccountHistory;
|
88
ui/component/walletFiatAccountHistory/view.jsx
Normal file
88
ui/component/walletFiatAccountHistory/view.jsx
Normal file
|
@ -0,0 +1,88 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import Button from 'component/button';
|
||||
import Card from 'component/common/card';
|
||||
import moment from 'moment';
|
||||
|
||||
type Props = {
|
||||
accountDetails: any,
|
||||
transactions: any,
|
||||
};
|
||||
|
||||
const WalletBalance = (props: Props) => {
|
||||
// receive transactions from parent component
|
||||
const { transactions } = props;
|
||||
|
||||
let accountTransactions;
|
||||
|
||||
// reverse so most recent payments come first
|
||||
if (transactions && transactions.length) {
|
||||
accountTransactions = transactions.reverse();
|
||||
}
|
||||
|
||||
// if there are more than 10 transactions, limit it to 10 for the frontend
|
||||
if (accountTransactions && accountTransactions.length > 10) {
|
||||
accountTransactions.length = 10;
|
||||
}
|
||||
|
||||
return (
|
||||
<><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.map((transaction) => (
|
||||
<tr key={transaction.name + transaction.created_at}>
|
||||
<td>{moment(transaction.created_at).format('LLL')}</td>
|
||||
<td>
|
||||
<Button
|
||||
className=""
|
||||
navigate={'/' + transaction.channel_name + ':' + transaction.channel_claim_id}
|
||||
label={transaction.channel_name}
|
||||
button="link"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<Button
|
||||
className=""
|
||||
navigate={'/' + transaction.channel_name + ':' + transaction.source_claim_id}
|
||||
label={
|
||||
transaction.channel_claim_id === transaction.source_claim_id
|
||||
? 'Channel Page'
|
||||
: 'Content 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>
|
||||
{!accountTransactions && <p style={{textAlign: 'center', marginTop: '20px', fontSize: '13px', color: 'rgb(171, 171, 171)'}}>No Transactions</p>}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default WalletBalance;
|
3
ui/component/walletFiatBalance/index.js
Normal file
3
ui/component/walletFiatBalance/index.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
import WalletFiatBalance from './view';
|
||||
|
||||
export default WalletFiatBalance;
|
94
ui/component/walletFiatBalance/view.jsx
Normal file
94
ui/component/walletFiatBalance/view.jsx
Normal file
|
@ -0,0 +1,94 @@
|
|||
// @flow
|
||||
import * as ICONS from 'constants/icons';
|
||||
import * as PAGES from 'constants/pages';
|
||||
import React from 'react';
|
||||
import Button from 'component/button';
|
||||
import Card from 'component/common/card';
|
||||
import Icon from 'component/common/icon';
|
||||
import I18nMessage from 'component/i18nMessage';
|
||||
|
||||
type Props = {
|
||||
accountDetails: any,
|
||||
};
|
||||
|
||||
const WalletBalance = (props: Props) => {
|
||||
const {
|
||||
accountDetails,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<>{<Card
|
||||
title={<><Icon size={18} icon={ICONS.FINANCE} />{(accountDetails && ((accountDetails.total_received_unpaid - accountDetails.total_paid_out) / 100)) || 0} USD</>}
|
||||
subtitle={accountDetails && accountDetails.total_received_unpaid > 0 &&
|
||||
<I18nMessage>
|
||||
This is your pending balance that will be automatically sent to your bank account
|
||||
</I18nMessage>
|
||||
}
|
||||
actions={
|
||||
<>
|
||||
<h2 className="section__title--small">
|
||||
${(accountDetails && (accountDetails.total_received_unpaid / 100)) || 0} Total Received Tips
|
||||
</h2>
|
||||
|
||||
<h2 className="section__title--small">
|
||||
${(accountDetails && (accountDetails.total_paid_out / 100)) || 0} Withdrawn
|
||||
{/* <Button */}
|
||||
{/* button="link" */}
|
||||
{/* label={detailsExpanded ? __('View less') : __('View more')} */}
|
||||
{/* iconRight={detailsExpanded ? ICONS.UP : ICONS.DOWN} */}
|
||||
{/* onClick={() => setDetailsExpanded(!detailsExpanded)} */}
|
||||
{/* /> */}
|
||||
</h2>
|
||||
|
||||
{/* view more section */}
|
||||
{/* commenting out because not implemented, but could be used in the future */}
|
||||
{/* {detailsExpanded && ( */}
|
||||
{/* <div className="section__subtitle"> */}
|
||||
{/* <dl> */}
|
||||
{/* <dt> */}
|
||||
{/* <span className="dt__text">{__('Earned from uploads')}</span> */}
|
||||
{/* /!* <span className="help--dt">({__('Earned from channel page')})</span> *!/ */}
|
||||
{/* </dt> */}
|
||||
{/* <dd> */}
|
||||
{/* <span className="dd__text"> */}
|
||||
{/* {Boolean(1) && ( */}
|
||||
{/* <Button */}
|
||||
{/* button="link" */}
|
||||
{/* className="dd__button" */}
|
||||
{/* icon={ICONS.UNLOCK} */}
|
||||
{/* /> */}
|
||||
{/* )} */}
|
||||
{/* <CreditAmount amount={1} precision={4} /> */}
|
||||
{/* </span> */}
|
||||
{/* </dd> */}
|
||||
|
||||
{/* <dt> */}
|
||||
{/* <span className="dt__text">{__('Earned from channel page')}</span> */}
|
||||
{/* /!* <span className="help--dt">({__('Delete or edit past content to spend')})</span> *!/ */}
|
||||
{/* </dt> */}
|
||||
{/* <dd> */}
|
||||
{/* <CreditAmount amount={1} precision={4} /> */}
|
||||
{/* </dd> */}
|
||||
|
||||
{/* /!* <dt> *!/ */}
|
||||
{/* /!* <span className="dt__text">{__('...supporting content')}</span> *!/ */}
|
||||
{/* /!* <span className="help--dt">({__('Delete supports to spend')})</span> *!/ */}
|
||||
{/* /!* </dt> *!/ */}
|
||||
{/* /!* <dd> *!/ */}
|
||||
{/* /!* <CreditAmount amount={1} precision={4} /> *!/ */}
|
||||
{/* /!* </dd> *!/ */}
|
||||
{/* </dl> */}
|
||||
{/* </div> */}
|
||||
{/* )} */}
|
||||
|
||||
<div className="section__actions">
|
||||
{/* <Button button="primary" label={__('Receive Payout')} icon={ICONS.SEND} /> */}
|
||||
<Button button="secondary" label={__('Account Configuration')} icon={ICONS.SETTINGS} navigate={`/$/${PAGES.SETTINGS_STRIPE_ACCOUNT}`} />
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
/>}</>
|
||||
);
|
||||
};
|
||||
|
||||
export default WalletBalance;
|
3
ui/component/walletFiatPaymentBalance/index.js
Normal file
3
ui/component/walletFiatPaymentBalance/index.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
import WalletFiatPaymentBalance from './view';
|
||||
|
||||
export default WalletFiatPaymentBalance;
|
76
ui/component/walletFiatPaymentBalance/view.jsx
Normal file
76
ui/component/walletFiatPaymentBalance/view.jsx
Normal file
|
@ -0,0 +1,76 @@
|
|||
// @flow
|
||||
import * as ICONS from 'constants/icons';
|
||||
import * as PAGES from 'constants/pages';
|
||||
import React from 'react';
|
||||
import Button from 'component/button';
|
||||
import Card from 'component/common/card';
|
||||
|
||||
type Props = {
|
||||
totalTippedAmount: number,
|
||||
accountDetails: any,
|
||||
transactions: any,
|
||||
};
|
||||
|
||||
const WalletBalance = (props: Props) => {
|
||||
const {
|
||||
// accountDetails,
|
||||
transactions,
|
||||
} = props;
|
||||
|
||||
// let cardDetails = {
|
||||
// brand: card.brand,
|
||||
// expiryYear: card.exp_year,
|
||||
// expiryMonth: card.exp_month,
|
||||
// lastFour: card.last4,
|
||||
// topOfDisplay: topOfDisplay,
|
||||
// bottomOfDisplay: bottomOfDisplay,
|
||||
// };
|
||||
|
||||
// const [detailsExpanded, setDetailsExpanded] = React.useState(false);
|
||||
const [totalCreatorsSupported, setTotalCreatorsSupported] = React.useState(false);
|
||||
|
||||
// calculate how many unique users tipped
|
||||
React.useEffect(() => {
|
||||
if (transactions) {
|
||||
let channelNames = [];
|
||||
|
||||
for (const transaction of transactions) {
|
||||
channelNames.push(transaction.channel_name);
|
||||
}
|
||||
|
||||
let unique = [...new Set(channelNames)];
|
||||
setTotalCreatorsSupported(unique.length);
|
||||
}
|
||||
}, [transactions]);
|
||||
|
||||
return (
|
||||
<>{<Card
|
||||
// TODO: implement hasActiveCard and show the current card the user would charge to
|
||||
// subtitle={hasActiveCard && <h2>Hello</h2>
|
||||
// // <Plastic
|
||||
// // type={userCardDetails.brand}
|
||||
// // name={userCardDetails.topOfDisplay + ' ' + userCardDetails.bottomOfDisplay}
|
||||
// // expiry={userCardDetails.expiryMonth + '/' + userCardDetails.expiryYear}
|
||||
// // number={'____________' + userCardDetails.lastFour}
|
||||
// // />
|
||||
// }
|
||||
actions={
|
||||
<>
|
||||
<h2 className="section__title--small">
|
||||
{(transactions && transactions.length) || 0} Total Tips
|
||||
</h2>
|
||||
|
||||
<h2 className="section__title--small">
|
||||
{totalCreatorsSupported || 0} Creators Supported
|
||||
</h2>
|
||||
|
||||
<div className="section__actions">
|
||||
<Button button="secondary" label={__('Manage Cards')} icon={ICONS.SETTINGS} navigate={`/$/${PAGES.SETTINGS_STRIPE_CARD}`} />
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
/>}</>
|
||||
);
|
||||
};
|
||||
|
||||
export default WalletBalance;
|
3
ui/component/walletFiatPaymentHistory/index.js
Normal file
3
ui/component/walletFiatPaymentHistory/index.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
import WalletFiatPaymentHistory from './view';
|
||||
|
||||
export default WalletFiatPaymentHistory;
|
113
ui/component/walletFiatPaymentHistory/view.jsx
Normal file
113
ui/component/walletFiatPaymentHistory/view.jsx
Normal file
|
@ -0,0 +1,113 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import Button from 'component/button';
|
||||
import Card from 'component/common/card';
|
||||
import { Lbryio } from 'lbryinc';
|
||||
import moment from 'moment';
|
||||
import { getStripeEnvironment } from 'util/stripe';
|
||||
let stripeEnvironment = getStripeEnvironment();
|
||||
|
||||
type Props = {
|
||||
accountDetails: any,
|
||||
transactions: any,
|
||||
};
|
||||
|
||||
const WalletBalance = (props: Props) => {
|
||||
// receive transactions from parent component
|
||||
const { transactions: accountTransactions } = props;
|
||||
|
||||
// const [accountStatusResponse, setAccountStatusResponse] = React.useState();
|
||||
|
||||
// const [subscriptions, setSubscriptions] = React.useState();
|
||||
|
||||
const [lastFour, setLastFour] = React.useState();
|
||||
|
||||
function getCustomerStatus() {
|
||||
return Lbryio.call(
|
||||
'customer',
|
||||
'status',
|
||||
{
|
||||
environment: stripeEnvironment,
|
||||
},
|
||||
'post'
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: this is actually incorrect, last4 should be populated based on the transaction not the current customer details
|
||||
React.useEffect(() => {
|
||||
(async function () {
|
||||
const customerStatusResponse = await getCustomerStatus();
|
||||
|
||||
const lastFour =
|
||||
customerStatusResponse.PaymentMethods &&
|
||||
customerStatusResponse.PaymentMethods.length &&
|
||||
customerStatusResponse.PaymentMethods[0].card.last4;
|
||||
|
||||
setLastFour(lastFour);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card
|
||||
title={__('Payment 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>{__('Card Last 4')}</th>
|
||||
<th>{__('Anonymous')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{accountTransactions &&
|
||||
accountTransactions.map((transaction) => (
|
||||
<tr key={transaction.name + transaction.created_at}>
|
||||
<td>{moment(transaction.created_at).format('LLL')}</td>
|
||||
<td>
|
||||
<Button
|
||||
className=""
|
||||
navigate={'/' + transaction.channel_name + ':' + transaction.channel_claim_id}
|
||||
label={transaction.channel_name}
|
||||
button="link"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<Button
|
||||
className=""
|
||||
navigate={'/' + transaction.channel_name + ':' + transaction.source_claim_id}
|
||||
label={
|
||||
transaction.channel_claim_id === transaction.source_claim_id
|
||||
? 'Channel Page'
|
||||
: 'Content Page'
|
||||
}
|
||||
button="link"
|
||||
/>
|
||||
</td>
|
||||
<td>${transaction.tipped_amount / 100}</td>
|
||||
<td>{lastFour}</td>
|
||||
<td>{transaction.private_tip ? 'Yes' : 'No'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{(!accountTransactions || accountTransactions.length === 0) && (
|
||||
<p style={{ textAlign: 'center', marginTop: '20px', fontSize: '13px', color: 'rgb(171, 171, 171)' }}>
|
||||
No Transactions
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default WalletBalance;
|
|
@ -15,14 +15,8 @@ import LbcSymbol from 'component/common/lbc-symbol';
|
|||
import { parseURI } from 'lbry-redux';
|
||||
import usePersistedState from 'effects/use-persisted-state';
|
||||
import WalletSpendableBalanceHelp from 'component/walletSpendableBalanceHelp';
|
||||
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';
|
||||
}
|
||||
import { getStripeEnvironment } from 'util/stripe';
|
||||
let stripeEnvironment = getStripeEnvironment();
|
||||
|
||||
const DEFAULT_TIP_AMOUNTS = [1, 5, 25, 100];
|
||||
const MINIMUM_FIAT_TIP = 1;
|
||||
|
@ -100,7 +94,7 @@ function WalletSendTip(props: Props) {
|
|||
|
||||
// check if creator has a payment method saved
|
||||
React.useEffect(() => {
|
||||
if (channelClaimId && isAuthenticated) {
|
||||
if (channelClaimId && isAuthenticated && stripeEnvironment) {
|
||||
Lbryio.call(
|
||||
'customer',
|
||||
'status',
|
||||
|
@ -118,16 +112,18 @@ function WalletSendTip(props: Props) {
|
|||
setHasSavedCard(Boolean(defaultPaymentMethodId));
|
||||
});
|
||||
}
|
||||
}, [channelClaimId, isAuthenticated]);
|
||||
}, [channelClaimId, isAuthenticated, stripeEnvironment]);
|
||||
|
||||
// check if creator has an account saved
|
||||
React.useEffect(() => {
|
||||
var tipInputElement = document.getElementById('tip-input');
|
||||
if (tipInputElement) { tipInputElement.focus() }
|
||||
const tipInputElement = document.getElementById('tip-input');
|
||||
if (tipInputElement) {
|
||||
tipInputElement.focus();
|
||||
}
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (channelClaimId) {
|
||||
if (channelClaimId && stripeEnvironment) {
|
||||
Lbryio.call(
|
||||
'account',
|
||||
'check',
|
||||
|
@ -141,20 +137,22 @@ function WalletSendTip(props: Props) {
|
|||
.then((accountCheckResponse) => {
|
||||
if (accountCheckResponse === true && canReceiveFiatTip !== true) {
|
||||
setCanReceiveFiatTip(true);
|
||||
} else {
|
||||
setCanReceiveFiatTip(false);
|
||||
}
|
||||
})
|
||||
.catch(function (error) {
|
||||
// console.log(error);
|
||||
});
|
||||
}
|
||||
}, [channelClaimId]);
|
||||
}, [channelClaimId, stripeEnvironment]);
|
||||
|
||||
const noBalance = balance === 0;
|
||||
const tipAmount = useCustomTip ? customTipAmount : presetTipAmount;
|
||||
|
||||
const [activeTab, setActiveTab] = React.useState(claimIsMine ? TAB_BOOST : TAB_LBC);
|
||||
|
||||
function setClaimTypeText() {
|
||||
function getClaimTypeText() {
|
||||
if (claim.value_type === 'stream') {
|
||||
return __('Content');
|
||||
} else if (claim.value_type === 'channel') {
|
||||
|
@ -167,21 +165,24 @@ function WalletSendTip(props: Props) {
|
|||
return __('Claim');
|
||||
}
|
||||
}
|
||||
const claimTypeText = setClaimTypeText();
|
||||
const claimTypeText = getClaimTypeText();
|
||||
|
||||
let iconToUse, explainerText;
|
||||
let iconToUse;
|
||||
let explainerText = '';
|
||||
if (activeTab === TAB_BOOST) {
|
||||
iconToUse = ICONS.LBC;
|
||||
explainerText = __('This refundable boost will improve the discoverability of this %claimTypeText% while active.', {claimTypeText});
|
||||
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 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;
|
||||
|
@ -213,7 +214,7 @@ function WalletSendTip(props: Props) {
|
|||
} else if (tipAmount < MINIMUM_PUBLISH_BID) {
|
||||
tipError = __('Amount must be higher');
|
||||
}
|
||||
// if tip fiat tab
|
||||
// if tip fiat tab
|
||||
} else {
|
||||
regexp = RegExp(/^(\d*([.]\d{0,2})?)$/);
|
||||
const validTipInput = regexp.test(String(tipAmount));
|
||||
|
@ -274,7 +275,8 @@ function WalletSendTip(props: Props) {
|
|||
'customer',
|
||||
'tip',
|
||||
{
|
||||
amount: 100 * tipAmount, // convert from dollars to cents
|
||||
// round to fix issues with floating point numbers
|
||||
amount: Math.round(100 * tipAmount), // convert from dollars to cents
|
||||
creator_channel_name: tipChannelName, // creator_channel_name
|
||||
creator_channel_claim_id: channelClaimId,
|
||||
tipper_channel_name: sendAnonymously ? '' : activeChannelName,
|
||||
|
@ -294,8 +296,8 @@ function WalletSendTip(props: Props) {
|
|||
}),
|
||||
});
|
||||
})
|
||||
.catch(function(error) {
|
||||
var displayError = 'Sorry, there was an error in processing your payment!';
|
||||
.catch(function (error) {
|
||||
let displayError = 'Sorry, there was an error in processing your payment!';
|
||||
|
||||
if (error.message !== 'payment intent failed to confirm') {
|
||||
displayError = error.message;
|
||||
|
@ -313,10 +315,53 @@ function WalletSendTip(props: Props) {
|
|||
}
|
||||
}
|
||||
|
||||
function handleCustomPriceChange(event: SyntheticInputEvent<*>) {
|
||||
const tipAmount = parseFloat(event.target.value);
|
||||
const countDecimals = function (value) {
|
||||
const text = value.toString();
|
||||
const index = text.indexOf('.');
|
||||
return text.length - index - 1;
|
||||
};
|
||||
|
||||
setCustomTipAmount(tipAmount);
|
||||
function handleCustomPriceChange(event: SyntheticInputEvent<*>) {
|
||||
let tipAmountAsString = event.target.value;
|
||||
|
||||
let tipAmount = parseFloat(tipAmountAsString);
|
||||
|
||||
const howManyDecimals = countDecimals(tipAmountAsString);
|
||||
|
||||
// fiat tip input
|
||||
if (activeTab === TAB_FIAT) {
|
||||
if (Number.isNaN(tipAmount)) {
|
||||
setCustomTipAmount('');
|
||||
}
|
||||
|
||||
// allow maximum of two decimal places
|
||||
if (howManyDecimals > 2) {
|
||||
tipAmount = Math.floor(tipAmount * 100) / 100;
|
||||
}
|
||||
|
||||
// remove decimals, and then get number of digits
|
||||
const howManyDigits = Math.trunc(tipAmount).toString().length;
|
||||
|
||||
if (howManyDigits > 4 && tipAmount !== 1000) {
|
||||
setTipError('Amount cannot be over 1000 dollars');
|
||||
setCustomTipAmount(tipAmount);
|
||||
} else if (tipAmount > 1000) {
|
||||
setTipError('Amount cannot be over 1000 dollars');
|
||||
setCustomTipAmount(tipAmount);
|
||||
} else {
|
||||
setCustomTipAmount(tipAmount);
|
||||
}
|
||||
// LBC tip input
|
||||
} else {
|
||||
// TODO: this is a bit buggy, needs a touchup
|
||||
// if (howManyDecimals > 9) {
|
||||
// // only allows up to 8 decimal places
|
||||
// tipAmount = Number(tipAmount.toString().match(/^-?\d+(?:\.\d{0,8})?/)[0]);
|
||||
//
|
||||
// setTipError('Please only use up to 8 decimals');
|
||||
// }
|
||||
setCustomTipAmount(tipAmount);
|
||||
}
|
||||
}
|
||||
|
||||
function buildButtonText() {
|
||||
|
@ -331,11 +376,19 @@ function WalletSendTip(props: Props) {
|
|||
return false;
|
||||
}
|
||||
|
||||
function convertToTwoDecimals(number) {
|
||||
return (Math.round(number * 100) / 100).toFixed(2);
|
||||
}
|
||||
|
||||
const amountToShow = activeTab === TAB_FIAT ? convertToTwoDecimals(tipAmount) : tipAmount;
|
||||
|
||||
// if it's a valid number display it, otherwise do an empty string
|
||||
const displayAmount = !isNan(tipAmount) ? tipAmount : '';
|
||||
const displayAmount = !isNan(tipAmount) ? amountToShow : '';
|
||||
|
||||
if (activeTab === TAB_BOOST) {
|
||||
return (claimIsMine ? __('Boost Your %claimTypeText%', {claimTypeText}) : __('Boost This %claimTypeText%', {claimTypeText}));
|
||||
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) {
|
||||
|
@ -362,240 +415,263 @@ function WalletSendTip(props: Props) {
|
|||
return (
|
||||
<Form onSubmit={handleSubmit}>
|
||||
{/* if there is no LBC balance, show user frontend to get credits */}
|
||||
{noBalance ? (
|
||||
<Card
|
||||
title={<I18nMessage tokens={{ lbc: <LbcSymbol size={22} /> }}>Supporting content requires %lbc%</I18nMessage>}
|
||||
subtitle={
|
||||
<I18nMessage tokens={{ lbc: <LbcSymbol /> }}>
|
||||
With %lbc%, you can send tips to your favorite creators, or help boost their content for more people to
|
||||
see.
|
||||
</I18nMessage>
|
||||
}
|
||||
actions={
|
||||
<div className="section__actions">
|
||||
<Button
|
||||
icon={ICONS.REWARDS}
|
||||
button="primary"
|
||||
label={__('Earn Rewards')}
|
||||
navigate={`/$/${PAGES.REWARDS}`}
|
||||
/>
|
||||
<Button icon={ICONS.BUY} button="secondary" label={__('Buy/Swap Credits')} navigate={`/$/${PAGES.BUY}`} />
|
||||
<Button button="link" label={__('Nevermind')} onClick={closeModal} />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
// if there is lbc, the main tip/boost gui with the 3 tabs at the top
|
||||
<Card
|
||||
title={<LbcSymbol postfix={claimIsMine ? __('Boost Your %claimTypeText%', {claimTypeText}) : __('Support This %claimTypeText%', {claimTypeText})} size={22} />}
|
||||
subtitle={
|
||||
<React.Fragment>
|
||||
{!claimIsMine && (
|
||||
<div className="section">
|
||||
{/* tip LBC tab button */}
|
||||
<Button
|
||||
key="tip"
|
||||
icon={ICONS.LBC}
|
||||
label={__('Tip')}
|
||||
button="alt"
|
||||
onClick={() => {
|
||||
var tipInputElement = document.getElementById('tip-input');
|
||||
if (tipInputElement) { tipInputElement.focus() }
|
||||
if (!isConfirming) {
|
||||
setActiveTab(TAB_LBC);
|
||||
}
|
||||
}}
|
||||
className={classnames('button-toggle', { 'button-toggle--active': activeTab === TAB_LBC })}
|
||||
/>
|
||||
{/* tip fiat tab button */}
|
||||
{/* @if TARGET='web' */}
|
||||
{/* if there is lbc, the main tip/boost gui with the 3 tabs at the top */}
|
||||
<Card
|
||||
title={
|
||||
<LbcSymbol
|
||||
postfix={
|
||||
claimIsMine
|
||||
? __('Boost Your %claimTypeText%', { claimTypeText })
|
||||
: __('Support This %claimTypeText%', { claimTypeText })
|
||||
}
|
||||
size={22}
|
||||
/>
|
||||
}
|
||||
subtitle={
|
||||
<React.Fragment>
|
||||
{!claimIsMine && (
|
||||
<div className="section">
|
||||
{/* tip LBC tab button */}
|
||||
<Button
|
||||
key="tip"
|
||||
icon={ICONS.LBC}
|
||||
label={__('Tip')}
|
||||
button="alt"
|
||||
onClick={() => {
|
||||
const tipInputElement = document.getElementById('tip-input');
|
||||
if (tipInputElement) {
|
||||
tipInputElement.focus();
|
||||
}
|
||||
if (!isConfirming) {
|
||||
setActiveTab(TAB_LBC);
|
||||
}
|
||||
}}
|
||||
className={classnames('button-toggle', { 'button-toggle--active': activeTab === TAB_LBC })}
|
||||
/>
|
||||
{/* tip fiat tab button */}
|
||||
{/* @if TARGET='web' */}
|
||||
{stripeEnvironment && (
|
||||
<Button
|
||||
key="tip-fiat"
|
||||
icon={ICONS.FINANCE}
|
||||
label={__('Tip')}
|
||||
button="alt"
|
||||
onClick={() => {
|
||||
var tipInputElement = document.getElementById('tip-input');
|
||||
if (tipInputElement) { tipInputElement.focus() }
|
||||
const tipInputElement = document.getElementById('tip-input');
|
||||
if (tipInputElement) {
|
||||
tipInputElement.focus();
|
||||
}
|
||||
if (!isConfirming) {
|
||||
setActiveTab(TAB_FIAT);
|
||||
}
|
||||
}}
|
||||
className={classnames('button-toggle', { 'button-toggle--active': activeTab === TAB_FIAT })}
|
||||
/>
|
||||
{/* @endif */}
|
||||
{/* tip LBC tab button */}
|
||||
)}
|
||||
{/* @endif */}
|
||||
{/* tip LBC tab button */}
|
||||
<Button
|
||||
key="boost"
|
||||
icon={ICONS.TRENDING}
|
||||
label={__('Boost')}
|
||||
button="alt"
|
||||
onClick={() => {
|
||||
const tipInputElement = document.getElementById('tip-input');
|
||||
if (tipInputElement) {
|
||||
tipInputElement.focus();
|
||||
}
|
||||
if (!isConfirming) {
|
||||
setActiveTab(TAB_BOOST);
|
||||
}
|
||||
}}
|
||||
className={classnames('button-toggle', { 'button-toggle--active': activeTab === TAB_BOOST })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* short explainer under the button */}
|
||||
<div className="section__subtitle">
|
||||
{explainerText + ' '}
|
||||
{/* {activeTab === TAB_FIAT && !hasCardSaved && <Button navigate={`/$/${PAGES.SETTINGS_STRIPE_CARD}`} label={__('Add A Card')} button="link" />} */}
|
||||
{<Button label={__('Learn more')} button="link" href="https://lbry.com/faq/tipping" />}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
}
|
||||
actions={
|
||||
// confirmation modal, allow user to confirm or cancel transaction
|
||||
isConfirming ? (
|
||||
<>
|
||||
<div className="section section--padded card--inline confirm__wrapper">
|
||||
<div className="section">
|
||||
<div className="confirm__label">{__('To --[the tip recipient]--')}</div>
|
||||
<div className="confirm__value">{channelName || title}</div>
|
||||
<div className="confirm__label">{__('From --[the tip sender]--')}</div>
|
||||
<div className="confirm__value">
|
||||
{activeChannelClaim && !incognito ? activeChannelClaim.name : __('Anonymous')}
|
||||
</div>
|
||||
<div className="confirm__label">{setConfirmLabel()}</div>
|
||||
<div className="confirm__value">
|
||||
{activeTab === TAB_FIAT ? (
|
||||
<p>$ {(Math.round(tipAmount * 100) / 100).toFixed(2)}</p>
|
||||
) : (
|
||||
<LbcSymbol postfix={tipAmount} size={22} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="section__actions">
|
||||
<Button autoFocus onClick={handleSubmit} button="primary" disabled={isPending} label={__('Confirm')} />
|
||||
<Button button="link" label={__('Cancel')} onClick={() => setIsConfirming(false)} />
|
||||
</div>
|
||||
</>
|
||||
) : !((activeTab === TAB_LBC || activeTab === TAB_BOOST) && noBalance) ? (
|
||||
<>
|
||||
<div className="section">
|
||||
<ChannelSelector />
|
||||
</div>
|
||||
|
||||
{activeTab === TAB_FIAT && !hasCardSaved && (
|
||||
<h3 className="add-card-prompt">
|
||||
<Button navigate={`/$/${PAGES.SETTINGS_STRIPE_CARD}`} label={__('Add a Card')} button="link" />{' '}
|
||||
{__('To Tip Creators')}
|
||||
</h3>
|
||||
)}
|
||||
|
||||
{/* section to pick tip/boost amount */}
|
||||
<div className="section">
|
||||
{DEFAULT_TIP_AMOUNTS.map((amount) => (
|
||||
<Button
|
||||
key="boost"
|
||||
icon={ICONS.TRENDING}
|
||||
label={__('Boost')}
|
||||
key={amount}
|
||||
disabled={shouldDisableAmountSelector(amount)}
|
||||
button="alt"
|
||||
className={classnames('button-toggle button-toggle--expandformobile', {
|
||||
'button-toggle--active': tipAmount === amount && !useCustomTip,
|
||||
'button-toggle--disabled': amount > balance,
|
||||
})}
|
||||
label={amount}
|
||||
icon={iconToUse}
|
||||
onClick={() => {
|
||||
var tipInputElement = document.getElementById('tip-input');
|
||||
if (tipInputElement) { tipInputElement.focus() }
|
||||
if (!isConfirming) {
|
||||
setActiveTab(TAB_BOOST);
|
||||
}
|
||||
setPresetTipAmount(amount);
|
||||
setUseCustomTip(false);
|
||||
}}
|
||||
className={classnames('button-toggle', { 'button-toggle--active': activeTab === TAB_BOOST })}
|
||||
/>
|
||||
))}
|
||||
|
||||
<Button
|
||||
button="alt"
|
||||
className={classnames('button-toggle button-toggle--expandformobile', {
|
||||
'button-toggle--active': useCustomTip, // set as active
|
||||
})}
|
||||
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)}
|
||||
/>
|
||||
|
||||
{DEFAULT_TIP_AMOUNTS.some((val) => val > balance) && activeTab !== TAB_FIAT && (
|
||||
<Button
|
||||
button="secondary"
|
||||
className="button-toggle-group-action"
|
||||
icon={ICONS.BUY}
|
||||
title={__('Buy or swap more LBRY Credits')}
|
||||
navigate={`/$/${PAGES.BUY}`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{useCustomTip && (
|
||||
<div className="section">
|
||||
<FormField
|
||||
autoFocus
|
||||
name="tip-input"
|
||||
label={
|
||||
<React.Fragment>
|
||||
{__('Custom support amount')}{' '}
|
||||
{activeTab !== TAB_FIAT ? (
|
||||
<I18nMessage
|
||||
tokens={{ lbc_balance: <CreditAmount precision={4} amount={balance} showLBC={false} /> }}
|
||||
>
|
||||
(%lbc_balance% Credits available)
|
||||
</I18nMessage>
|
||||
) : (
|
||||
'in USD'
|
||||
)}
|
||||
</React.Fragment>
|
||||
}
|
||||
error={tipError}
|
||||
min="0"
|
||||
step="any"
|
||||
type="number"
|
||||
style={{
|
||||
width: activeTab === TAB_FIAT ? '99px' : '160px',
|
||||
}}
|
||||
placeholder="1.23"
|
||||
value={customTipAmount}
|
||||
onChange={(event) => handleCustomPriceChange(event)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* short explainer under the button */}
|
||||
<div className="section__subtitle">
|
||||
{explainerText}
|
||||
{/* {activeTab === TAB_FIAT && !hasCardSaved && <Button navigate={`/$/${PAGES.SETTINGS_STRIPE_CARD}`} label={__('Add A Card')} button="link" />} */}
|
||||
{<Button label={__('Learn more')} button="link" href="https://lbry.com/faq/tipping" />}
|
||||
{/* send tip/boost button */}
|
||||
<div className="section__actions">
|
||||
<Button
|
||||
autoFocus
|
||||
icon={isSupport ? ICONS.TRENDING : ICONS.SUPPORT}
|
||||
button="primary"
|
||||
type="submit"
|
||||
disabled={
|
||||
fetchingChannels ||
|
||||
isPending ||
|
||||
tipError ||
|
||||
!tipAmount ||
|
||||
(activeTab === TAB_FIAT && (!hasCardSaved || !canReceiveFiatTip))
|
||||
}
|
||||
label={buildButtonText()}
|
||||
/>
|
||||
{fetchingChannels && <span className="help">{__('Loading your channels...')}</span>}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
}
|
||||
actions={
|
||||
// confirmation modal, allow user to confirm or cancel transaction
|
||||
isConfirming ? (
|
||||
<>
|
||||
<div className="section section--padded card--inline confirm__wrapper">
|
||||
<div className="section">
|
||||
<div className="confirm__label">{__('To --[the tip recipient]--')}</div>
|
||||
<div className="confirm__value">{channelName || title}</div>
|
||||
<div className="confirm__label">{__('From --[the tip sender]--')}</div>
|
||||
<div className="confirm__value">
|
||||
{activeChannelClaim && !incognito ? activeChannelClaim.name : __('Anonymous')}
|
||||
</div>
|
||||
<div className="confirm__label">{setConfirmLabel()}</div>
|
||||
<div className="confirm__value">
|
||||
{activeTab === TAB_FIAT ? <p>$ {tipAmount}</p> : <LbcSymbol postfix={tipAmount} size={22} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{activeTab !== TAB_FIAT ? (
|
||||
<WalletSpendableBalanceHelp />
|
||||
) : !canReceiveFiatTip ? (
|
||||
<div className="help">{__('Only creators that verify cash accounts can receive tips')}</div>
|
||||
) : (
|
||||
<div className="help">{__('The payment will be made from your saved card')}</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
// if it's LBC and there is no balance, you can prompt to purchase LBC
|
||||
<Card
|
||||
title={
|
||||
<I18nMessage tokens={{ lbc: <LbcSymbol size={22} /> }}>Supporting content requires %lbc%</I18nMessage>
|
||||
}
|
||||
subtitle={
|
||||
<I18nMessage tokens={{ lbc: <LbcSymbol /> }}>
|
||||
With %lbc%, you can send tips to your favorite creators, or help boost their content for more people
|
||||
to see.
|
||||
</I18nMessage>
|
||||
}
|
||||
actions={
|
||||
<div className="section__actions">
|
||||
<Button
|
||||
autoFocus
|
||||
onClick={handleSubmit}
|
||||
icon={ICONS.REWARDS}
|
||||
button="primary"
|
||||
disabled={isPending}
|
||||
label={__('Confirm')}
|
||||
label={__('Earn Rewards')}
|
||||
navigate={`/$/${PAGES.REWARDS}`}
|
||||
/>
|
||||
<Button button="link" label={__('Cancel')} onClick={() => setIsConfirming(false)} />
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="section">
|
||||
<ChannelSelector />
|
||||
</div>
|
||||
|
||||
{activeTab === TAB_FIAT && !hasCardSaved && (
|
||||
<h3 className="add-card-prompt">
|
||||
<Button navigate={`/$/${PAGES.SETTINGS_STRIPE_CARD}`} label={__('Add a Card')} button="link" />
|
||||
{' '}{__('To Tip Creators')}
|
||||
</h3>
|
||||
)}
|
||||
|
||||
{/* section to pick tip/boost amount */}
|
||||
<div className="section">
|
||||
{DEFAULT_TIP_AMOUNTS.map((amount) => (
|
||||
<Button
|
||||
key={amount}
|
||||
disabled={shouldDisableAmountSelector(amount)}
|
||||
button="alt"
|
||||
className={classnames('button-toggle button-toggle--expandformobile', {
|
||||
'button-toggle--active': tipAmount === amount && !useCustomTip,
|
||||
'button-toggle--disabled': amount > balance,
|
||||
})}
|
||||
label={amount}
|
||||
icon={iconToUse}
|
||||
onClick={() => {
|
||||
setPresetTipAmount(amount);
|
||||
setUseCustomTip(false);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
||||
<Button
|
||||
button="alt"
|
||||
className={classnames('button-toggle button-toggle--expandformobile', {
|
||||
'button-toggle--active': useCustomTip, // set as active
|
||||
})}
|
||||
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)}
|
||||
icon={ICONS.BUY}
|
||||
button="secondary"
|
||||
label={__('Buy/Swap Credits')}
|
||||
navigate={`/$/${PAGES.BUY}`}
|
||||
/>
|
||||
|
||||
{DEFAULT_TIP_AMOUNTS.some((val) => val > balance) && activeTab !== TAB_FIAT && (
|
||||
<Button
|
||||
button="secondary"
|
||||
className="button-toggle-group-action"
|
||||
icon={ICONS.BUY}
|
||||
title={__('Buy or swap more LBRY Credits')}
|
||||
navigate={`/$/${PAGES.BUY}`}
|
||||
/>
|
||||
)}
|
||||
<Button button="link" label={__('Nevermind')} onClick={closeModal} />
|
||||
</div>
|
||||
|
||||
{useCustomTip && (
|
||||
<div className="section">
|
||||
<FormField
|
||||
autoFocus
|
||||
name="tip-input"
|
||||
label={
|
||||
<React.Fragment>
|
||||
{__('Custom support amount')}{' '}
|
||||
{activeTab !== TAB_FIAT ? (
|
||||
<I18nMessage
|
||||
tokens={{ lbc_balance: <CreditAmount precision={4} amount={balance} showLBC={false} /> }}
|
||||
>
|
||||
(%lbc_balance% Credits available)
|
||||
</I18nMessage>
|
||||
) : (
|
||||
'in USD'
|
||||
)}
|
||||
</React.Fragment>
|
||||
}
|
||||
className="form-field--price-amount"
|
||||
error={tipError}
|
||||
min="0"
|
||||
step="any"
|
||||
type="number"
|
||||
placeholder="1.23"
|
||||
value={customTipAmount}
|
||||
onChange={(event) => handleCustomPriceChange(event)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* send tip/boost button */}
|
||||
<div className="section__actions">
|
||||
<Button
|
||||
autoFocus
|
||||
icon={isSupport ? ICONS.TRENDING : ICONS.SUPPORT}
|
||||
button="primary"
|
||||
type="submit"
|
||||
disabled={
|
||||
fetchingChannels ||
|
||||
isPending ||
|
||||
tipError ||
|
||||
!tipAmount ||
|
||||
(activeTab === TAB_FIAT && (!hasCardSaved || !canReceiveFiatTip))
|
||||
}
|
||||
label={buildButtonText()}
|
||||
/>
|
||||
{fetchingChannels && <span className="help">{__('Loading your channels...')}</span>}
|
||||
</div>
|
||||
{activeTab !== TAB_FIAT ? (
|
||||
<WalletSpendableBalanceHelp />
|
||||
) : !canReceiveFiatTip ? (
|
||||
<div className="help">{__('Only select creators can receive tips at this time')}</div>
|
||||
) : (
|
||||
<div className="help">{__('The payment will be made from your saved card')}</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -11,14 +11,8 @@ 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';
|
||||
}
|
||||
import { getStripeEnvironment } from 'util/stripe';
|
||||
let stripeEnvironment = getStripeEnvironment();
|
||||
|
||||
const DEFAULT_TIP_AMOUNTS = [1, 5, 25, 100];
|
||||
|
||||
|
@ -76,45 +70,49 @@ function WalletTipAmountSelector(props: Props) {
|
|||
|
||||
// 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;
|
||||
if (stripeEnvironment) {
|
||||
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));
|
||||
});
|
||||
}, []);
|
||||
setHasSavedCard(Boolean(defaultPaymentMethodId));
|
||||
});
|
||||
}
|
||||
}, [stripeEnvironment]);
|
||||
|
||||
//
|
||||
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);
|
||||
});
|
||||
}, []);
|
||||
if (stripeEnvironment) {
|
||||
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);
|
||||
});
|
||||
}
|
||||
}, [stripeEnvironment]);
|
||||
|
||||
React.useEffect(() => {
|
||||
// setHasSavedCard(false);
|
||||
|
@ -214,7 +212,7 @@ function WalletTipAmountSelector(props: Props) {
|
|||
<div className="help">
|
||||
<span className="help--spendable">
|
||||
<Button navigate={`/$/${PAGES.SETTINGS_STRIPE_CARD}`} label={__('Add a Card ')} button="link" /> To{' '}
|
||||
{__(' Tip Creators')}
|
||||
{__('Tip Creators')}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
|
@ -224,7 +222,7 @@ function WalletTipAmountSelector(props: Props) {
|
|||
{useCustomTip && activeTab === TAB_FIAT && hasCardSaved && !canReceiveFiatTip && (
|
||||
<>
|
||||
<div className="help">
|
||||
<span className="help--spendable">Only select creators can receive tips at this time</span>
|
||||
<span className="help--spendable">Only creators that verify cash accounts can receive tips</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
@ -263,7 +261,6 @@ function WalletTipAmountSelector(props: Props) {
|
|||
// </div>
|
||||
// </>
|
||||
}
|
||||
className="form-field--price-amount"
|
||||
error={tipError}
|
||||
min="0"
|
||||
step="any"
|
||||
|
@ -283,7 +280,7 @@ function WalletTipAmountSelector(props: Props) {
|
|||
<div className="help">
|
||||
<span className="help--spendable">
|
||||
<Button navigate={`/$/${PAGES.SETTINGS_STRIPE_CARD}`} label={__('Add a Card ')} button="link" /> To{' '}
|
||||
{__(' Tip Creators')}
|
||||
{__('Tip Creators')}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
|
@ -293,7 +290,7 @@ function WalletTipAmountSelector(props: Props) {
|
|||
{!useCustomTip && activeTab === TAB_FIAT && hasCardSaved && !canReceiveFiatTip && (
|
||||
<>
|
||||
<div className="help">
|
||||
<span className="help--spendable">Only select creators can receive tips at this time</span>
|
||||
<span className="help--spendable">Only creators that verify cash accounts can receive tips</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
|
|
@ -165,3 +165,4 @@ export const GLOBE = 'globe';
|
|||
export const RSS = 'rss';
|
||||
export const STAR = 'star';
|
||||
export const MUSIC = 'MusicCategory';
|
||||
export const BADGE_MOD = 'BadgeMod';
|
||||
|
|
2
ui/constants/stripe.js
Normal file
2
ui/constants/stripe.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
export const TEST = 'test';
|
||||
export const LIVE = 'live';
|
|
@ -5,11 +5,13 @@ import React, { useEffect } from 'react';
|
|||
/**
|
||||
* Helper React hook for lazy loading images
|
||||
* @param elementRef - A React useRef instance to the element to lazy load.
|
||||
* @param backgroundFallback
|
||||
* @param yOffsetPx - Number of pixels from the viewport to start loading.
|
||||
* @param {Array<>} [deps=[]] - The dependencies this lazy-load is reliant on.
|
||||
*/
|
||||
export default function useLazyLoading(
|
||||
elementRef: { current: ?ElementRef<any> },
|
||||
backgroundFallback: string = '',
|
||||
yOffsetPx: number = 500,
|
||||
deps: Array<any> = []
|
||||
) {
|
||||
|
@ -42,11 +44,19 @@ export default function useLazyLoading(
|
|||
// $FlowFixMe
|
||||
target.src = target.dataset.src;
|
||||
setSrcLoaded(true);
|
||||
// No fallback handling here (clients have access to 'onerror' on the image ref).
|
||||
return;
|
||||
}
|
||||
|
||||
// useful for lazy loading background images on divs
|
||||
if (target.dataset.backgroundImage) {
|
||||
if (backgroundFallback) {
|
||||
const tmpImage = new Image();
|
||||
tmpImage.onerror = () => {
|
||||
target.style.backgroundImage = `url(${backgroundFallback})`;
|
||||
};
|
||||
tmpImage.src = target.dataset.backgroundImage;
|
||||
}
|
||||
target.style.backgroundImage = `url(${target.dataset.backgroundImage})`;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ export default function useLighthouse(
|
|||
let isSubscribed = true;
|
||||
lighthouse
|
||||
.search(throttledQuery)
|
||||
.then((results) => {
|
||||
.then(({ body: results }) => {
|
||||
if (isSubscribed) {
|
||||
setResults(
|
||||
results.map((result) => `lbry://${result.name}#${result.claimId}`).filter((uri) => isURIValid(uri))
|
||||
|
|
|
@ -5,43 +5,19 @@ 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';
|
||||
}
|
||||
import { getStripeEnvironment } from 'util/stripe';
|
||||
let stripeEnvironment = getStripeEnvironment();
|
||||
|
||||
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,
|
||||
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);
|
||||
export default function ModalRemoveCard(props: Props) {
|
||||
const { closeModal, paymentMethodId } = props;
|
||||
|
||||
function removeCard() {
|
||||
console.log(paymentMethodId);
|
||||
|
||||
Lbryio.call(
|
||||
'customer',
|
||||
'detach',
|
||||
|
@ -51,11 +27,9 @@ export default function ModalRevokeClaim(props: Props) {
|
|||
},
|
||||
'post'
|
||||
).then((removeCardResponse) => {
|
||||
console.log(removeCardResponse);
|
||||
|
||||
// TODO: add toast here
|
||||
// closeModal();
|
||||
|
||||
// Is there a better way to handle this? Why reload?
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
|
@ -63,15 +37,14 @@ export default function ModalRevokeClaim(props: Props) {
|
|||
return (
|
||||
<Modal ariaHideApp={false} isOpen contentLabel={'hello'} type="card" onAborted={closeModal}>
|
||||
<Card
|
||||
title={'Confirm Remove Card'}
|
||||
// body={getMsgBody(type, isSupport, name)}
|
||||
title={__('Confirm Remove Card')}
|
||||
actions={
|
||||
<div className="section__actions">
|
||||
<Button
|
||||
className="stripe__confirm-remove-card"
|
||||
button="secondary"
|
||||
icon={ICONS.DELETE}
|
||||
label={'Remove Card'}
|
||||
label={__('Remove Card')}
|
||||
onClick={removeCard}
|
||||
/>
|
||||
<Button button="link" label={__('Cancel')} onClick={closeModal} />
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
// @flow
|
||||
import { SITE_NAME } from 'config';
|
||||
import React, { useEffect } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import FileRender from 'component/fileRender';
|
||||
|
@ -110,7 +111,7 @@ const EmbedWrapperPage = (props: Props) => {
|
|||
<div>
|
||||
<h1>{__('Paid content cannot be embedded.')}</h1>
|
||||
<div className="section__actions--centered">
|
||||
<Button label={__('Watch on lbry.tv')} button="primary" href={contentLink} />
|
||||
<Button label={__('Watch on %SITE_NAME%', { SITE_NAME })} button="primary" href={contentLink} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -12,7 +12,6 @@ import FileRenderDownload from 'component/fileRenderDownload';
|
|||
import RecommendedContent from 'component/recommendedContent';
|
||||
import CollectionContent from 'component/collectionContentSidebar';
|
||||
import CommentsList from 'component/commentsList';
|
||||
import { Redirect } from 'react-router';
|
||||
import Button from 'component/button';
|
||||
import I18nMessage from 'component/i18nMessage';
|
||||
import Empty from 'component/common/empty';
|
||||
|
@ -145,10 +144,6 @@ function FilePage(props: Props) {
|
|||
);
|
||||
}
|
||||
|
||||
if (!claimIsMine && isLivestream) {
|
||||
return <Redirect to={`/$/${PAGES.LIVESTREAM}`} />;
|
||||
}
|
||||
|
||||
if (obscureNsfw && isMature) {
|
||||
return (
|
||||
<Page className="file-page" filePage isMarkdown={isMarkdown}>
|
||||
|
|
|
@ -11,7 +11,7 @@ type Props = {
|
|||
|
||||
export default function LivestreamCurrentPage(props: Props) {
|
||||
const { user } = props;
|
||||
const canView = user && user.global_mod;
|
||||
const canView = process.env.ENABLE_WIP_FEATURES || (user && user.global_mod);
|
||||
|
||||
return (
|
||||
<Page>
|
||||
|
|
|
@ -3,7 +3,7 @@ import { withRouter } from 'react-router';
|
|||
import { doSearch } from 'redux/actions/search';
|
||||
import {
|
||||
selectIsSearching,
|
||||
makeSelectSearchUris,
|
||||
makeSelectSearchUrisForQuery,
|
||||
selectSearchOptions,
|
||||
makeSelectHasReachedMaxResultsLength,
|
||||
} from 'redux/selectors/search';
|
||||
|
@ -28,7 +28,7 @@ const select = (state, props) => {
|
|||
};
|
||||
|
||||
const query = getSearchQueryString(urlQuery, searchOptions);
|
||||
const uris = makeSelectSearchUris(query)(state);
|
||||
const uris = makeSelectSearchUrisForQuery(query)(state);
|
||||
const hasReachedMaxResultsLength = makeSelectHasReachedMaxResultsLength(query)(state);
|
||||
|
||||
return {
|
||||
|
|
|
@ -20,6 +20,7 @@ import { SIMPLE_SITE } from 'config';
|
|||
import homepages from 'homepages';
|
||||
import { Lbryio } from 'lbryinc';
|
||||
import Yrbl from 'component/yrbl';
|
||||
import { getStripeEnvironment } from 'util/stripe';
|
||||
|
||||
type Price = {
|
||||
currency: string,
|
||||
|
@ -208,44 +209,6 @@ class SettingsPage extends React.PureComponent<Props, State> {
|
|||
}}
|
||||
className="card-stack"
|
||||
>
|
||||
{/* @if TARGET='web' */}
|
||||
{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')}
|
||||
icon={ICONS.SETTINGS}
|
||||
navigate={`/$/${PAGES.SETTINGS_STRIPE_ACCOUNT}`}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{/* @endif */}
|
||||
|
||||
{/* @if TARGET='web' */}
|
||||
{isAuthenticated && (
|
||||
<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>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{/* @endif */}
|
||||
|
||||
<Card title={__('Language')} actions={<SettingLanguage />} />
|
||||
{homepages && Object.keys(homepages).length > 1 && (
|
||||
<Card title={__('Homepage')} actions={<HomepageSelector />} />
|
||||
|
@ -488,6 +451,44 @@ class SettingsPage extends React.PureComponent<Props, State> {
|
|||
/>
|
||||
{/* @endif */}
|
||||
|
||||
{/* @if TARGET='web' */}
|
||||
{user && getStripeEnvironment() && (
|
||||
<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')}
|
||||
icon={ICONS.SETTINGS}
|
||||
navigate={`/$/${PAGES.SETTINGS_STRIPE_ACCOUNT}`}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{/* @endif */}
|
||||
|
||||
{/* @if TARGET='web' */}
|
||||
{isAuthenticated && getStripeEnvironment() && (
|
||||
<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>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{/* @endif */}
|
||||
|
||||
{(isAuthenticated || !IS_WEB) && (
|
||||
<>
|
||||
<Card
|
||||
|
|
|
@ -5,6 +5,7 @@ import { FormField, FormFieldPrice } from 'component/common/form';
|
|||
import Button from 'component/button';
|
||||
import I18nMessage from 'component/i18nMessage';
|
||||
import Page from 'component/page';
|
||||
import SettingCommentsServer from 'component/settingCommentsServer';
|
||||
import SettingWalletServer from 'component/settingWalletServer';
|
||||
import SettingAutoLaunch from 'component/settingAutoLaunch';
|
||||
import SettingClosingBehavior from 'component/settingClosingBehavior';
|
||||
|
@ -507,6 +508,10 @@ class SettingsAdvancedPage extends React.PureComponent<Props, State> {
|
|||
/>
|
||||
)}
|
||||
|
||||
{/* @if TARGET='app' */}
|
||||
<Card title={__('Comments server')} actions={<SettingCommentsServer />} />
|
||||
{/* @endif */}
|
||||
|
||||
<Card title={__('Upload settings')} actions={<PublishSettings />} />
|
||||
|
||||
{/* @if TARGET='app' */}
|
||||
|
|
|
@ -29,7 +29,7 @@ const select = (state) => ({
|
|||
const perform = (dispatch) => ({
|
||||
commentBlockWords: (channelClaim, words) => dispatch(doCommentBlockWords(channelClaim, words)),
|
||||
commentUnblockWords: (channelClaim, words) => dispatch(doCommentUnblockWords(channelClaim, words)),
|
||||
fetchCreatorSettings: (channelClaimIds) => dispatch(doFetchCreatorSettings(channelClaimIds)),
|
||||
fetchCreatorSettings: (channelClaimId) => dispatch(doFetchCreatorSettings(channelClaimId)),
|
||||
updateCreatorSettings: (channelClaim, settings) => dispatch(doUpdateCreatorSettings(channelClaim, settings)),
|
||||
commentModAddDelegate: (modChanId, modChanName, creatorChannelClaim) =>
|
||||
dispatch(doCommentModAddDelegate(modChanId, modChanName, creatorChannelClaim)),
|
||||
|
|
|
@ -11,11 +11,17 @@ import LbcSymbol from 'component/common/lbc-symbol';
|
|||
import I18nMessage from 'component/i18nMessage';
|
||||
import { isNameValid, parseURI } from 'lbry-redux';
|
||||
import ClaimPreview from 'component/claimPreview';
|
||||
import debounce from 'util/debounce';
|
||||
import { getUriForSearchTerm } from 'util/search';
|
||||
|
||||
const DEBOUNCE_REFRESH_MS = 1000;
|
||||
|
||||
const FEATURE_IS_READY = false;
|
||||
const LBC_MAX = 21000000;
|
||||
const LBC_MIN = 0;
|
||||
const LBC_STEP = 1.0;
|
||||
|
||||
// ****************************************************************************
|
||||
// ****************************************************************************
|
||||
|
||||
type Props = {
|
||||
activeChannelClaim: ChannelClaim,
|
||||
|
@ -28,7 +34,7 @@ type Props = {
|
|||
commentModAddDelegate: (string, string, ChannelClaim) => void,
|
||||
commentModRemoveDelegate: (string, string, ChannelClaim) => void,
|
||||
commentModListDelegates: (ChannelClaim) => void,
|
||||
fetchCreatorSettings: (Array<string>) => void,
|
||||
fetchCreatorSettings: (channelId: string) => void,
|
||||
updateCreatorSettings: (ChannelClaim, PerChannelSettings) => void,
|
||||
doToast: ({ message: string }) => void,
|
||||
};
|
||||
|
@ -54,26 +60,28 @@ export default function SettingsCreatorPage(props: Props) {
|
|||
const [moderatorSearchTerm, setModeratorSearchTerm] = React.useState('');
|
||||
const [moderatorSearchError, setModeratorSearchError] = React.useState('');
|
||||
const [moderatorSearchClaimUri, setModeratorSearchClaimUri] = React.useState('');
|
||||
const [minTipAmountComment, setMinTipAmountComment] = React.useState(0);
|
||||
const [minTipAmountSuperChat, setMinTipAmountSuperChat] = React.useState(0);
|
||||
const [slowModeMinGap, setSlowModeMinGap] = React.useState(0);
|
||||
const [minTip, setMinTip] = React.useState(0);
|
||||
const [minSuper, setMinSuper] = React.useState(0);
|
||||
const [slowModeMin, setSlowModeMin] = React.useState(0);
|
||||
const [lastUpdated, setLastUpdated] = React.useState(1);
|
||||
|
||||
function settingsToStates(settings: PerChannelSettings) {
|
||||
if (settings.comments_enabled !== undefined) {
|
||||
setCommentsEnabled(settings.comments_enabled);
|
||||
}
|
||||
if (settings.min_tip_amount_comment !== undefined) {
|
||||
setMinTipAmountComment(settings.min_tip_amount_comment);
|
||||
}
|
||||
if (settings.min_tip_amount_super_chat !== undefined) {
|
||||
setMinTipAmountSuperChat(settings.min_tip_amount_super_chat);
|
||||
}
|
||||
if (settings.slow_mode_min_gap !== undefined) {
|
||||
setSlowModeMinGap(settings.slow_mode_min_gap);
|
||||
}
|
||||
if (settings.words) {
|
||||
const tagArray = Array.from(new Set(settings.words));
|
||||
const pushSlowModeMinDebounced = React.useMemo(() => debounce(pushSlowModeMin, 1000), []);
|
||||
const pushMinTipDebounced = React.useMemo(() => debounce(pushMinTip, 1000), []);
|
||||
const pushMinSuperDebounced = React.useMemo(() => debounce(pushMinSuper, 1000), []);
|
||||
|
||||
// **************************************************************************
|
||||
// **************************************************************************
|
||||
|
||||
/**
|
||||
* Updates corresponding GUI states with the given PerChannelSettings values.
|
||||
*
|
||||
* @param settings
|
||||
* @param fullSync If true, update all states and consider 'undefined' settings as "cleared/false";
|
||||
* if false, only update defined settings.
|
||||
*/
|
||||
function settingsToStates(settings: PerChannelSettings, fullSync: boolean) {
|
||||
const doSetMutedWordTags = (words: Array<string>) => {
|
||||
const tagArray = Array.from(new Set(words));
|
||||
setMutedWordTags(
|
||||
tagArray
|
||||
.filter((t) => t !== '')
|
||||
|
@ -81,15 +89,51 @@ export default function SettingsCreatorPage(props: Props) {
|
|||
return { name: x };
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
if (fullSync) {
|
||||
setCommentsEnabled(settings.comments_enabled || false);
|
||||
setMinTip(settings.min_tip_amount_comment || 0);
|
||||
setMinSuper(settings.min_tip_amount_super_chat || 0);
|
||||
setSlowModeMin(settings.slow_mode_min_gap || 0);
|
||||
doSetMutedWordTags(settings.words || []);
|
||||
} else {
|
||||
if (settings.comments_enabled !== undefined) {
|
||||
setCommentsEnabled(settings.comments_enabled);
|
||||
}
|
||||
if (settings.min_tip_amount_comment !== undefined) {
|
||||
setMinTip(settings.min_tip_amount_comment);
|
||||
}
|
||||
if (settings.min_tip_amount_super_chat !== undefined) {
|
||||
setMinSuper(settings.min_tip_amount_super_chat);
|
||||
}
|
||||
if (settings.slow_mode_min_gap !== undefined) {
|
||||
setSlowModeMin(settings.slow_mode_min_gap);
|
||||
}
|
||||
if (settings.words) {
|
||||
doSetMutedWordTags(settings.words);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setSettings(newSettings: PerChannelSettings) {
|
||||
settingsToStates(newSettings);
|
||||
settingsToStates(newSettings, false);
|
||||
updateCreatorSettings(activeChannelClaim, newSettings);
|
||||
setLastUpdated(Date.now());
|
||||
}
|
||||
|
||||
function pushSlowModeMin(value: number, activeChannelClaim: ChannelClaim) {
|
||||
updateCreatorSettings(activeChannelClaim, { slow_mode_min_gap: value });
|
||||
}
|
||||
|
||||
function pushMinTip(value: number, activeChannelClaim: ChannelClaim) {
|
||||
updateCreatorSettings(activeChannelClaim, { min_tip_amount_comment: value });
|
||||
}
|
||||
|
||||
function pushMinSuper(value: number, activeChannelClaim: ChannelClaim) {
|
||||
updateCreatorSettings(activeChannelClaim, { min_tip_amount_super_chat: value });
|
||||
}
|
||||
|
||||
function addMutedWords(newTags: Array<Tag>) {
|
||||
const validatedNewTags = [];
|
||||
newTags.forEach((newTag) => {
|
||||
|
@ -153,6 +197,9 @@ export default function SettingsCreatorPage(props: Props) {
|
|||
}
|
||||
}
|
||||
|
||||
// **************************************************************************
|
||||
// **************************************************************************
|
||||
|
||||
// 'moderatorSearchTerm' to 'moderatorSearchClaimUri'
|
||||
React.useEffect(() => {
|
||||
if (!moderatorSearchTerm) {
|
||||
|
@ -211,20 +258,23 @@ export default function SettingsCreatorPage(props: Props) {
|
|||
|
||||
if (activeChannelClaim && settingsByChannelId && settingsByChannelId[activeChannelClaim.claim_id]) {
|
||||
const channelSettings = settingsByChannelId[activeChannelClaim.claim_id];
|
||||
settingsToStates(channelSettings);
|
||||
settingsToStates(channelSettings, true);
|
||||
}
|
||||
}, [activeChannelClaim, settingsByChannelId, lastUpdated]);
|
||||
|
||||
// Re-sync list, mainly to correct any invalid settings.
|
||||
// Re-sync list on first idle time; mainly to correct any invalid settings.
|
||||
React.useEffect(() => {
|
||||
if (lastUpdated && activeChannelClaim) {
|
||||
const timer = setTimeout(() => {
|
||||
fetchCreatorSettings([activeChannelClaim.claim_id]);
|
||||
fetchCreatorSettings(activeChannelClaim.claim_id);
|
||||
}, DEBOUNCE_REFRESH_MS);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [lastUpdated, activeChannelClaim, fetchCreatorSettings]);
|
||||
|
||||
// **************************************************************************
|
||||
// **************************************************************************
|
||||
|
||||
const isBusy =
|
||||
!activeChannelClaim || !settingsByChannelId || settingsByChannelId[activeChannelClaim.claim_id] === undefined;
|
||||
const isDisabled =
|
||||
|
@ -272,8 +322,13 @@ export default function SettingsCreatorPage(props: Props) {
|
|||
step={1}
|
||||
type="number"
|
||||
placeholder="1"
|
||||
value={slowModeMinGap}
|
||||
onChange={(e) => setSettings({ slow_mode_min_gap: parseInt(e.target.value) })}
|
||||
value={slowModeMin}
|
||||
onChange={(e) => {
|
||||
const value = parseInt(e.target.value);
|
||||
setSlowModeMin(value);
|
||||
pushSlowModeMinDebounced(value, activeChannelClaim);
|
||||
}}
|
||||
onBlur={() => setLastUpdated(Date.now())}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
|
@ -297,47 +352,70 @@ export default function SettingsCreatorPage(props: Props) {
|
|||
</div>
|
||||
}
|
||||
/>
|
||||
{FEATURE_IS_READY && (
|
||||
<Card
|
||||
title={__('Tip')}
|
||||
actions={
|
||||
<>
|
||||
<FormField
|
||||
name="min_tip_amount_comment"
|
||||
label={
|
||||
<I18nMessage tokens={{ lbc: <LbcSymbol /> }}>Minimum %lbc% tip amount for comments</I18nMessage>
|
||||
<Card
|
||||
title={__('Tip')}
|
||||
actions={
|
||||
<>
|
||||
<FormField
|
||||
name="min_tip_amount_comment"
|
||||
label={
|
||||
<I18nMessage tokens={{ lbc: <LbcSymbol /> }}>Minimum %lbc% tip amount for comments</I18nMessage>
|
||||
}
|
||||
helper={__(
|
||||
'Enabling a minimum amount to comment will force all comments, including livestreams, to have tips associated with them. This can help prevent spam.'
|
||||
)}
|
||||
className="form-field--price-amount"
|
||||
max={LBC_MAX}
|
||||
min={LBC_MIN}
|
||||
step={LBC_STEP}
|
||||
type="number"
|
||||
placeholder="1"
|
||||
value={minTip}
|
||||
onChange={(e) => {
|
||||
const newMinTip = parseFloat(e.target.value);
|
||||
setMinTip(newMinTip);
|
||||
pushMinTipDebounced(newMinTip, activeChannelClaim);
|
||||
if (newMinTip !== 0 && minSuper !== 0) {
|
||||
setMinSuper(0);
|
||||
pushMinSuperDebounced(0, activeChannelClaim);
|
||||
}
|
||||
helper={__(
|
||||
'Enabling a minimum amount to comment will force all comments, including livestreams, to have tips associated with them. This can help prevent spam.'
|
||||
)}
|
||||
className="form-field--price-amount"
|
||||
min={0}
|
||||
step="any"
|
||||
type="number"
|
||||
placeholder="1"
|
||||
value={minTipAmountComment}
|
||||
onChange={(e) => setSettings({ min_tip_amount_comment: parseFloat(e.target.value) })}
|
||||
/>
|
||||
<FormField
|
||||
name="min_tip_amount_super_chat"
|
||||
label={
|
||||
<I18nMessage tokens={{ lbc: <LbcSymbol /> }}>Minimum %lbc% tip amount for hyperchats</I18nMessage>
|
||||
}
|
||||
helper={__(
|
||||
'Enabling a minimum amount to hyperchat will force all TIPPED comments to have this value in order to be shown. This still allows regular comments to be posted.'
|
||||
)}
|
||||
className="form-field--price-amount"
|
||||
min={0}
|
||||
step="any"
|
||||
type="number"
|
||||
placeholder="1"
|
||||
value={minTipAmountSuperChat}
|
||||
onChange={(e) => setSettings({ min_tip_amount_super_chat: parseFloat(e.target.value) })}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
}}
|
||||
onBlur={() => setLastUpdated(Date.now())}
|
||||
/>
|
||||
<FormField
|
||||
name="min_tip_amount_super_chat"
|
||||
label={
|
||||
<I18nMessage tokens={{ lbc: <LbcSymbol /> }}>Minimum %lbc% tip amount for hyperchats</I18nMessage>
|
||||
}
|
||||
helper={
|
||||
<>
|
||||
{__(
|
||||
'Enabling a minimum amount to hyperchat will force all TIPPED comments to have this value in order to be shown. This still allows regular comments to be posted.'
|
||||
)}
|
||||
{minTip !== 0 && (
|
||||
<p className="help--inline">
|
||||
<em>{__('(This settings is not applicable if all comments require a tip.)')}</em>
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
className="form-field--price-amount"
|
||||
min={0}
|
||||
step="any"
|
||||
type="number"
|
||||
placeholder="1"
|
||||
value={minSuper}
|
||||
disabled={minTip !== 0}
|
||||
onChange={(e) => {
|
||||
const newMinSuper = parseFloat(e.target.value);
|
||||
setMinSuper(newMinSuper);
|
||||
pushMinSuperDebounced(newMinSuper, activeChannelClaim);
|
||||
}}
|
||||
onBlur={() => setLastUpdated(Date.now())}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Card
|
||||
title={__('Delegation')}
|
||||
className="card--enable-overflow"
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { withRouter } from 'react-router';
|
||||
import StripeAccountConnection from './view';
|
||||
import { selectUser } from 'redux/selectors/user';
|
||||
import { doToast } from 'redux/actions/notifications';
|
||||
|
||||
// function that receives state parameter and returns object of functions that accept state
|
||||
const select = (state) => ({
|
||||
user: selectUser(state),
|
||||
const select = (state) => ({});
|
||||
|
||||
const perform = (dispatch) => ({
|
||||
doToast: (options) => dispatch(doToast(options)),
|
||||
});
|
||||
|
||||
// const perform = (dispatch) => ({});
|
||||
|
||||
export default withRouter(connect(select)(StripeAccountConnection));
|
||||
export default withRouter(connect(select, perform)(StripeAccountConnection));
|
||||
|
|
|
@ -1,22 +1,18 @@
|
|||
// @flow
|
||||
import * as ICONS from 'constants/icons';
|
||||
import * as PAGES from 'constants/pages';
|
||||
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';
|
||||
import { URL, WEBPACK_WEB_PORT } from 'config';
|
||||
import { getStripeEnvironment } from 'util/stripe';
|
||||
|
||||
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 stripeEnvironment = getStripeEnvironment();
|
||||
let successStripeRedirectUrl, failureStripeRedirectUrl;
|
||||
let successEndpoint = '/$/settings/tip_account';
|
||||
let failureEndpoint = '/$/settings/tip_account';
|
||||
|
@ -30,7 +26,8 @@ if (isDev) {
|
|||
|
||||
type Props = {
|
||||
source: string,
|
||||
user: User,
|
||||
doOpenModal: (string, {}) => void,
|
||||
doToast: ({ message: string }) => void,
|
||||
};
|
||||
|
||||
type State = {
|
||||
|
@ -38,13 +35,13 @@ type State = {
|
|||
loading: boolean,
|
||||
content: ?string,
|
||||
stripeConnectionUrl: string,
|
||||
// alreadyUpdated: boolean,
|
||||
accountConfirmed: boolean,
|
||||
accountPendingConfirmation: boolean,
|
||||
accountNotConfirmedButReceivedTips: boolean,
|
||||
unpaidBalance: number,
|
||||
pageTitle: string,
|
||||
accountTransactions: any, // define this type
|
||||
stillRequiringVerification: boolean,
|
||||
accountTransactions: any,
|
||||
};
|
||||
|
||||
class StripeAccountConnection extends React.Component<Props, State> {
|
||||
|
@ -60,16 +57,13 @@ class StripeAccountConnection extends React.Component<Props, State> {
|
|||
unpaidBalance: 0,
|
||||
stripeConnectionUrl: '',
|
||||
pageTitle: 'Add Payout Method',
|
||||
stillRequiringVerification: false,
|
||||
accountTransactions: [],
|
||||
// alreadyUpdated: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { user } = this.props;
|
||||
|
||||
// $FlowFixMe
|
||||
this.experimentalUiEnabled = user && user.experimental_ui;
|
||||
let doToast = this.props.doToast;
|
||||
|
||||
var that = this;
|
||||
|
||||
|
@ -127,10 +121,8 @@ class StripeAccountConnection extends React.Component<Props, State> {
|
|||
).then((accountListResponse: any) => {
|
||||
// TODO type this
|
||||
that.setState({
|
||||
accountTransactions: accountListResponse,
|
||||
accountTransactions: accountListResponse.reverse(),
|
||||
});
|
||||
|
||||
console.log(accountListResponse);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -138,9 +130,25 @@ class StripeAccountConnection extends React.Component<Props, State> {
|
|||
if (accountStatusResponse.charges_enabled) {
|
||||
// account has already been confirmed
|
||||
|
||||
that.setState({
|
||||
const eventuallyDueInformation = accountStatusResponse.account_info.requirements.eventually_due;
|
||||
|
||||
const currentlyDueInformation = accountStatusResponse.account_info.requirements.currently_due;
|
||||
|
||||
let objectToUpdateState = {
|
||||
accountConfirmed: true,
|
||||
});
|
||||
stillRequiringVerification: false,
|
||||
};
|
||||
|
||||
if (
|
||||
(eventuallyDueInformation && eventuallyDueInformation.length) ||
|
||||
(currentlyDueInformation && currentlyDueInformation.length)
|
||||
) {
|
||||
objectToUpdateState.stillRequiringVerification = true;
|
||||
getAndSetAccountLink(false);
|
||||
}
|
||||
|
||||
that.setState(objectToUpdateState);
|
||||
|
||||
// user has not confirmed an account but have received payments
|
||||
} else if (accountStatusResponse.total_received_unpaid > 0) {
|
||||
that.setState({
|
||||
|
@ -165,6 +173,9 @@ class StripeAccountConnection extends React.Component<Props, State> {
|
|||
// get stripe link and set it on the frontend
|
||||
getAndSetAccountLink(true);
|
||||
} else {
|
||||
// probably an error from stripe
|
||||
const displayString = __('There was an error getting your account setup, please try again later');
|
||||
doToast({ message: displayString, isError: true });
|
||||
// not an error from Beamer, throw it
|
||||
throw new Error(error);
|
||||
}
|
||||
|
@ -179,35 +190,79 @@ class StripeAccountConnection extends React.Component<Props, State> {
|
|||
unpaidBalance,
|
||||
accountNotConfirmedButReceivedTips,
|
||||
pageTitle,
|
||||
accountTransactions,
|
||||
stillRequiringVerification,
|
||||
} = 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">
|
||||
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>
|
||||
<div>
|
||||
<h3>{__('Getting your bank account connection status...')}</h3>
|
||||
</div>
|
||||
<h3>{__('Getting your bank account connection status...')}</h3>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* user has yet to complete their integration */}
|
||||
{!accountConfirmed && accountPendingConfirmation && (
|
||||
<div className="card__body-actions">
|
||||
</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>
|
||||
{stillRequiringVerification && (
|
||||
<>
|
||||
<h3 style={{ marginTop: '10px' }}>
|
||||
Although your account is connected it still requires verification to begin receiving tips.
|
||||
</h3>
|
||||
<h3 style={{ marginTop: '10px' }}>
|
||||
Please use the button below to complete your verification process and enable tipping for
|
||||
your account.
|
||||
</h3>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* TODO: hopefully we won't be using this anymore and can remove it */}
|
||||
{accountNotConfirmedButReceivedTips && (
|
||||
<div className="card__body-actions">
|
||||
<div>
|
||||
<div>
|
||||
<h3>{__('Congratulations, you have already begun receiving tips on Odysee!')}</h3>
|
||||
<div>
|
||||
<h3>{__('Connect your bank account to Odysee to receive donations directly from users')}</h3>
|
||||
<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}>
|
||||
|
@ -216,127 +271,33 @@ class StripeAccountConnection extends React.Component<Props, State> {
|
|||
</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;
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
actions={
|
||||
<>
|
||||
{stillRequiringVerification && (
|
||||
<Button
|
||||
button="primary"
|
||||
label={__('Complete Verification')}
|
||||
icon={ICONS.SETTINGS}
|
||||
navigate={stripeConnectionUrl}
|
||||
className="stripe__complete-verification-button"
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
button="secondary"
|
||||
label={__('View Transactions')}
|
||||
icon={ICONS.SETTINGS}
|
||||
navigate={`/$/${PAGES.WALLET}?tab=fiat-payment-history`}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<br />
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,19 +5,17 @@ import React from 'react';
|
|||
import Page from 'component/page';
|
||||
import Card from 'component/common/card';
|
||||
import { Lbryio } from 'lbryinc';
|
||||
import { STRIPE_PUBLIC_KEY } from 'config';
|
||||
import moment from 'moment';
|
||||
import Plastic from 'react-plastic';
|
||||
import Button from 'component/button';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import * as MODALS from 'constants/modal_types';
|
||||
import * as PAGES from 'constants/pages';
|
||||
import { STRIPE_PUBLIC_KEY } from 'config';
|
||||
import { getStripeEnvironment } from 'util/stripe';
|
||||
let stripeEnvironment = getStripeEnvironment();
|
||||
|
||||
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 APIS_DOWN_ERROR_RESPONSE = __('There was an error from the server, please try again later');
|
||||
const CARD_SETUP_ERROR_RESPONSE = __('There was an error getting your card setup, please try again later');
|
||||
|
||||
// eslint-disable-next-line flowtype/no-types-missing-file-annotation
|
||||
type Props = {
|
||||
|
@ -57,8 +55,6 @@ class SettingsStripeCard extends React.Component<Props, State> {
|
|||
componentDidMount() {
|
||||
let that = this;
|
||||
|
||||
console.log(this.props);
|
||||
|
||||
let doToast = this.props.doToast;
|
||||
|
||||
const script = document.createElement('script');
|
||||
|
@ -78,123 +74,116 @@ class SettingsStripeCard extends React.Component<Props, State> {
|
|||
// TODO: fix this, should be a cleaner way
|
||||
setTimeout(function () {
|
||||
// check if customer has card setup already
|
||||
Lbryio.call(
|
||||
'customer',
|
||||
'status',
|
||||
{
|
||||
environment: stripeEnvironment,
|
||||
},
|
||||
'post'
|
||||
)
|
||||
.then((customerStatusResponse) => {
|
||||
// user has a card saved if their defaultPaymentMethod has an id
|
||||
const defaultPaymentMethod = customerStatusResponse.Customer.invoice_settings.default_payment_method;
|
||||
let userHasAlreadySetupPayment = Boolean(defaultPaymentMethod && defaultPaymentMethod.id);
|
||||
if (stripeEnvironment) {
|
||||
Lbryio.call(
|
||||
'customer',
|
||||
'status',
|
||||
{
|
||||
environment: stripeEnvironment,
|
||||
},
|
||||
'post'
|
||||
)
|
||||
.then((customerStatusResponse) => {
|
||||
// user has a card saved if their defaultPaymentMethod has an id
|
||||
const defaultPaymentMethod = customerStatusResponse.Customer.invoice_settings.default_payment_method;
|
||||
let userHasAlreadySetupPayment = Boolean(defaultPaymentMethod && defaultPaymentMethod.id);
|
||||
|
||||
// show different frontend if user already has card
|
||||
if (userHasAlreadySetupPayment) {
|
||||
let card = customerStatusResponse.PaymentMethods[0].card;
|
||||
// show different frontend if user already has card
|
||||
if (userHasAlreadySetupPayment) {
|
||||
let card = customerStatusResponse.PaymentMethods[0].card;
|
||||
|
||||
let customer = customerStatusResponse.Customer;
|
||||
let customer = customerStatusResponse.Customer;
|
||||
|
||||
let topOfDisplay = customer.email.split('@')[0];
|
||||
let bottomOfDisplay = '@' + customer.email.split('@')[1];
|
||||
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,
|
||||
};
|
||||
|
||||
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,
|
||||
paymentMethodId: customerStatusResponse.PaymentMethods[0].id,
|
||||
});
|
||||
|
||||
that.setState({
|
||||
currentFlowStage: 'cardConfirmed',
|
||||
pageTitle: 'Tip History',
|
||||
userCardDetails: cardDetails,
|
||||
paymentMethodId: customerStatusResponse.PaymentMethods[0].id,
|
||||
});
|
||||
// otherwise, prompt them to save a card
|
||||
} else {
|
||||
that.setState({
|
||||
currentFlowStage: 'confirmingCard',
|
||||
});
|
||||
|
||||
// otherwise, prompt them to save a card
|
||||
} else {
|
||||
that.setState({
|
||||
currentFlowStage: 'confirmingCard',
|
||||
});
|
||||
// get a payment method secret for frontend
|
||||
Lbryio.call(
|
||||
'customer',
|
||||
'setup',
|
||||
{
|
||||
environment: stripeEnvironment,
|
||||
},
|
||||
'post'
|
||||
).then((customerSetupResponse) => {
|
||||
clientSecret = customerSetupResponse.client_secret;
|
||||
|
||||
// get a payment method secret for frontend
|
||||
// instantiate stripe elements
|
||||
setupStripe();
|
||||
});
|
||||
}
|
||||
|
||||
// get customer transactions
|
||||
Lbryio.call(
|
||||
'customer',
|
||||
'setup',
|
||||
'list',
|
||||
{
|
||||
environment: stripeEnvironment,
|
||||
},
|
||||
'post'
|
||||
).then((customerSetupResponse) => {
|
||||
console.log(customerSetupResponse);
|
||||
|
||||
clientSecret = customerSetupResponse.client_secret;
|
||||
|
||||
// instantiate stripe elements
|
||||
setupStripe();
|
||||
).then((customerTransactionsResponse) => {
|
||||
that.setState({
|
||||
customerTransactions: customerTransactionsResponse,
|
||||
});
|
||||
});
|
||||
}
|
||||
// if the status call fails, either an actual error or need to run setup first
|
||||
})
|
||||
.catch(function (error) {
|
||||
// errorString passed from the API (with a 403 error)
|
||||
const errorString = 'user as customer is not setup yet';
|
||||
|
||||
// get customer transactions
|
||||
Lbryio.call(
|
||||
'customer',
|
||||
'list',
|
||||
{
|
||||
environment: stripeEnvironment,
|
||||
},
|
||||
'post'
|
||||
).then((customerTransactionsResponse) => {
|
||||
that.setState({
|
||||
customerTransactions: customerTransactionsResponse,
|
||||
});
|
||||
// if it's beamer's error indicating the account is not linked yet
|
||||
if (error.message && error.message.indexOf(errorString) > -1) {
|
||||
// send them to save a card
|
||||
that.setState({
|
||||
currentFlowStage: 'confirmingCard',
|
||||
});
|
||||
|
||||
console.log(customerTransactionsResponse);
|
||||
// get a payment method secret for frontend
|
||||
Lbryio.call(
|
||||
'customer',
|
||||
'setup',
|
||||
{
|
||||
environment: stripeEnvironment,
|
||||
},
|
||||
'post'
|
||||
).then((customerSetupResponse) => {
|
||||
clientSecret = customerSetupResponse.client_secret;
|
||||
|
||||
// instantiate stripe elements
|
||||
setupStripe();
|
||||
});
|
||||
// 500 error from the backend being down
|
||||
} else if (error === 'internal_apis_down') {
|
||||
doToast({ message: APIS_DOWN_ERROR_RESPONSE, isError: true });
|
||||
} else {
|
||||
// probably an error from stripe
|
||||
doToast({ message: CARD_SETUP_ERROR_RESPONSE, isError: true });
|
||||
}
|
||||
});
|
||||
// if the status call fails, either an actual error or need to run setup first
|
||||
})
|
||||
.catch(function (error) {
|
||||
console.log(error);
|
||||
|
||||
// errorString passed from the API (with a 403 error)
|
||||
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 && error.message.indexOf(errorString) > -1) {
|
||||
// send them to save a card
|
||||
that.setState({
|
||||
currentFlowStage: 'confirmingCard',
|
||||
});
|
||||
|
||||
// get a payment method secret for frontend
|
||||
Lbryio.call(
|
||||
'customer',
|
||||
'setup',
|
||||
{
|
||||
environment: stripeEnvironment,
|
||||
},
|
||||
'post'
|
||||
).then((customerSetupResponse) => {
|
||||
console.log(customerSetupResponse);
|
||||
|
||||
clientSecret = customerSetupResponse.client_secret;
|
||||
|
||||
// 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');
|
||||
}
|
||||
});
|
||||
}
|
||||
}, 250);
|
||||
|
||||
function setupStripe() {
|
||||
|
@ -261,8 +250,6 @@ class SettingsStripeCard extends React.Component<Props, State> {
|
|||
})
|
||||
.then(function (result) {
|
||||
if (result.error) {
|
||||
console.log(result);
|
||||
|
||||
changeLoadingState(false);
|
||||
var displayError = document.getElementById('card-errors');
|
||||
displayError.textContent = result.error.message;
|
||||
|
@ -346,8 +333,6 @@ class SettingsStripeCard extends React.Component<Props, State> {
|
|||
});
|
||||
});
|
||||
|
||||
console.log(result);
|
||||
|
||||
changeLoadingState(false);
|
||||
});
|
||||
};
|
||||
|
@ -366,13 +351,13 @@ class SettingsStripeCard extends React.Component<Props, State> {
|
|||
|
||||
const { scriptFailedToLoad, openModal } = this.props;
|
||||
|
||||
const { currentFlowStage, customerTransactions, pageTitle, userCardDetails, paymentMethodId } = this.state;
|
||||
const { currentFlowStage, pageTitle, userCardDetails, paymentMethodId } = this.state;
|
||||
|
||||
return (
|
||||
<Page backout={{ title: pageTitle, backLabel: __('Done') }} noFooter noSideNavigation>
|
||||
<div>
|
||||
{scriptFailedToLoad && (
|
||||
<div className="error__text">There was an error connecting to Stripe. Please try again later.</div>
|
||||
<div className="error__text">{__('There was an error connecting to Stripe. Please try again later.')}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
@ -430,72 +415,18 @@ class SettingsStripeCard extends React.Component<Props, State> {
|
|||
/>
|
||||
</>
|
||||
}
|
||||
actions={
|
||||
<Button
|
||||
button="primary"
|
||||
label={__('View Transactions')}
|
||||
icon={ICONS.SETTINGS}
|
||||
navigate={`/$/${PAGES.WALLET}?tab=fiat-account-history`}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<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. ')}
|
||||
/>
|
||||
)}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// @flow
|
||||
import { DOMAIN } from 'config';
|
||||
import { DOMAIN, ENABLE_NO_SOURCE_CLAIMS } from 'config';
|
||||
import * as PAGES from 'constants/pages';
|
||||
import React, { useEffect } from 'react';
|
||||
import { lazyImport } from 'util/lazyImport';
|
||||
|
@ -199,7 +199,7 @@ function ShowPage(props: Props) {
|
|||
/>
|
||||
</Page>
|
||||
);
|
||||
} else if (isLivestream) {
|
||||
} else if (isLivestream && ENABLE_NO_SOURCE_CLAIMS) {
|
||||
innerContent = <LivestreamPage uri={uri} />;
|
||||
} else {
|
||||
innerContent = <FilePage uri={uri} location={location} />;
|
||||
|
|
|
@ -1,11 +1,29 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import { withRouter } from 'react-router';
|
||||
import { useHistory } from 'react-router';
|
||||
import WalletBalance from 'component/walletBalance';
|
||||
import WalletFiatBalance from 'component/walletFiatBalance';
|
||||
import WalletFiatPaymentBalance from 'component/walletFiatPaymentBalance';
|
||||
import WalletFiatAccountHistory from 'component/walletFiatAccountHistory';
|
||||
import WalletFiatPaymentHistory from 'component/walletFiatPaymentHistory';
|
||||
import TxoList from 'component/txoList';
|
||||
import Page from 'component/page';
|
||||
import * as PAGES from 'constants/pages';
|
||||
import Spinner from 'component/spinner';
|
||||
import YrblWalletEmpty from 'component/yrblWalletEmpty';
|
||||
import { Lbryio } from 'lbryinc';
|
||||
import { Tabs, TabList, Tab, TabPanels, TabPanel } from 'component/common/tabs';
|
||||
import { getStripeEnvironment } from 'util/stripe';
|
||||
|
||||
const TAB_QUERY = 'tab';
|
||||
|
||||
const TABS = {
|
||||
LBRY_CREDITS_TAB: 'credits',
|
||||
ACCOUNT_HISTORY: 'fiat-account-history',
|
||||
PAYMENT_HISTORY: 'fiat-payment-history',
|
||||
};
|
||||
|
||||
let stripeEnvironment = getStripeEnvironment();
|
||||
|
||||
type Props = {
|
||||
history: { action: string, push: (string) => void, replace: (string) => void },
|
||||
|
@ -14,32 +32,209 @@ type Props = {
|
|||
};
|
||||
|
||||
const WalletPage = (props: Props) => {
|
||||
const { location, totalBalance } = props;
|
||||
const { search } = location;
|
||||
const {
|
||||
location: { search },
|
||||
push,
|
||||
} = useHistory();
|
||||
|
||||
// @if TARGET='web'
|
||||
const urlParams = new URLSearchParams(search);
|
||||
|
||||
const currentView = urlParams.get(TAB_QUERY) || TABS.LBRY_CREDITS_TAB;
|
||||
|
||||
let tabIndex;
|
||||
switch (currentView) {
|
||||
case TABS.LBRY_CREDITS_TAB:
|
||||
tabIndex = 0;
|
||||
break;
|
||||
case TABS.PAYMENT_HISTORY:
|
||||
tabIndex = 1;
|
||||
break;
|
||||
case TABS.ACCOUNT_HISTORY:
|
||||
tabIndex = 2;
|
||||
break;
|
||||
default:
|
||||
tabIndex = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
function onTabChange(newTabIndex) {
|
||||
let url = `/$/${PAGES.WALLET}?`;
|
||||
|
||||
if (newTabIndex === 0) {
|
||||
url += `${TAB_QUERY}=${TABS.LBRY_CREDITS_TAB}`;
|
||||
} else if (newTabIndex === 1) {
|
||||
url += `${TAB_QUERY}=${TABS.PAYMENT_HISTORY}`;
|
||||
} else if (newTabIndex === 2) {
|
||||
url += `${TAB_QUERY}=${TABS.ACCOUNT_HISTORY}`;
|
||||
} else {
|
||||
url += `${TAB_QUERY}=${TABS.LBRY_CREDITS_TAB}`;
|
||||
}
|
||||
push(url);
|
||||
}
|
||||
|
||||
const [accountStatusResponse, setAccountStatusResponse] = React.useState();
|
||||
const [accountTransactionResponse, setAccountTransactionResponse] = React.useState([]);
|
||||
const [customerTransactions, setCustomerTransactions] = React.useState([]);
|
||||
|
||||
function getPaymentHistory() {
|
||||
return Lbryio.call(
|
||||
'customer',
|
||||
'list',
|
||||
{
|
||||
environment: stripeEnvironment,
|
||||
},
|
||||
'post'
|
||||
);
|
||||
}
|
||||
|
||||
function getAccountStatus() {
|
||||
return Lbryio.call(
|
||||
'account',
|
||||
'status',
|
||||
{
|
||||
environment: stripeEnvironment,
|
||||
},
|
||||
'post'
|
||||
);
|
||||
}
|
||||
|
||||
function getAccountTransactionsa() {
|
||||
return Lbryio.call(
|
||||
'account',
|
||||
'list',
|
||||
{
|
||||
environment: stripeEnvironment,
|
||||
},
|
||||
'post'
|
||||
);
|
||||
}
|
||||
|
||||
// calculate account transactions section
|
||||
React.useEffect(() => {
|
||||
(async function () {
|
||||
try {
|
||||
if (!stripeEnvironment) {
|
||||
return;
|
||||
}
|
||||
const response = await getAccountStatus();
|
||||
|
||||
setAccountStatusResponse(response);
|
||||
|
||||
// TODO: some weird naming clash hence getAccountTransactionsa
|
||||
const getAccountTransactions = await getAccountTransactionsa();
|
||||
|
||||
setAccountTransactionResponse(getAccountTransactions);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
})();
|
||||
}, [stripeEnvironment]);
|
||||
|
||||
// populate customer payment data
|
||||
React.useEffect(() => {
|
||||
(async function () {
|
||||
try {
|
||||
// get card payments customer has made
|
||||
if (!stripeEnvironment) {
|
||||
return;
|
||||
}
|
||||
let customerTransactionResponse = await getPaymentHistory();
|
||||
if (customerTransactionResponse && customerTransactionResponse.length) {
|
||||
customerTransactionResponse.reverse();
|
||||
}
|
||||
|
||||
setCustomerTransactions(customerTransactionResponse);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
})();
|
||||
}, [stripeEnvironment]);
|
||||
|
||||
// @endif
|
||||
|
||||
const { totalBalance } = props;
|
||||
const showIntro = totalBalance === 0;
|
||||
const loading = totalBalance === undefined;
|
||||
|
||||
return (
|
||||
<Page>
|
||||
{loading && (
|
||||
<div className="main--empty">
|
||||
<Spinner delayed />
|
||||
</div>
|
||||
<>
|
||||
{stripeEnvironment && (
|
||||
<Page>
|
||||
<Tabs onChange={onTabChange} index={tabIndex}>
|
||||
<TabList className="tabs__list--collection-edit-page">
|
||||
<Tab>{__('LBRY Credits')}</Tab>
|
||||
<Tab>{__('Account History')}</Tab>
|
||||
<Tab>{__('Payment History')}</Tab>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
<div className="section card-stack">
|
||||
<div className="lbc-transactions">
|
||||
{/* if the transactions are loading */}
|
||||
{loading && (
|
||||
<div className="main--empty">
|
||||
<Spinner delayed />
|
||||
</div>
|
||||
)}
|
||||
{/* when the transactions are finished loading */}
|
||||
{!loading && (
|
||||
<>
|
||||
{showIntro ? (
|
||||
<YrblWalletEmpty includeWalletLink />
|
||||
) : (
|
||||
<div className="card-stack">
|
||||
<WalletBalance />
|
||||
<TxoList search={search} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<div className="section card-stack">
|
||||
<WalletFiatBalance accountDetails={accountStatusResponse} />
|
||||
<WalletFiatAccountHistory transactions={accountTransactionResponse} />
|
||||
</div>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<div className="section card-stack">
|
||||
<WalletFiatPaymentBalance
|
||||
transactions={customerTransactions}
|
||||
accountDetails={accountStatusResponse}
|
||||
/>
|
||||
<WalletFiatPaymentHistory transactions={customerTransactions} />
|
||||
</div>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</Page>
|
||||
)}
|
||||
{!loading && (
|
||||
<>
|
||||
{showIntro ? (
|
||||
<YrblWalletEmpty includeWalletLink />
|
||||
) : (
|
||||
<div className="card-stack">
|
||||
<WalletBalance />
|
||||
<TxoList search={search} />
|
||||
{!stripeEnvironment && (
|
||||
<Page>
|
||||
{loading && (
|
||||
<div className="main--empty">
|
||||
<Spinner delayed />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
{!loading && (
|
||||
<>
|
||||
{showIntro ? (
|
||||
<YrblWalletEmpty includeWalletLink />
|
||||
) : (
|
||||
<div className="card-stack">
|
||||
<WalletBalance />
|
||||
<TxoList search={search} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Page>
|
||||
)}
|
||||
</Page>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default withRouter(WalletPage);
|
||||
export default WalletPage;
|
||||
|
|
|
@ -28,6 +28,7 @@ import { selectPrefsReady } from 'redux/selectors/sync';
|
|||
import { doAlertWaitingForSync } from 'redux/actions/app';
|
||||
|
||||
const isDev = process.env.NODE_ENV !== 'production';
|
||||
const FETCH_API_FAILED_TO_FETCH = 'Failed to fetch';
|
||||
|
||||
const COMMENTRON_MSG_REMAP = {
|
||||
// <-- Commentron msg --> : <-- App msg -->
|
||||
|
@ -103,7 +104,7 @@ export function doCommentList(
|
|||
totalFilteredItems: total_filtered_items,
|
||||
totalPages: total_pages,
|
||||
claimId: claimId,
|
||||
authorClaimId: authorChannelClaim ? authorChannelClaim.claim_id : undefined,
|
||||
commenterClaimId: authorChannelClaim ? authorChannelClaim.claim_id : undefined,
|
||||
uri: uri,
|
||||
},
|
||||
});
|
||||
|
@ -111,20 +112,32 @@ export function doCommentList(
|
|||
return result;
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error.message === 'comments are disabled by the creator') {
|
||||
dispatch({
|
||||
type: ACTIONS.COMMENT_LIST_COMPLETED,
|
||||
data: {
|
||||
authorClaimId: authorChannelClaim ? authorChannelClaim.claim_id : undefined,
|
||||
disabled: true,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
devToast(dispatch, `doCommentList: ${error.message}`);
|
||||
dispatch({
|
||||
type: ACTIONS.COMMENT_LIST_FAILED,
|
||||
data: error,
|
||||
});
|
||||
switch (error.message) {
|
||||
case 'comments are disabled by the creator':
|
||||
dispatch({
|
||||
type: ACTIONS.COMMENT_LIST_COMPLETED,
|
||||
data: {
|
||||
authorClaimId: authorChannelClaim ? authorChannelClaim.claim_id : undefined,
|
||||
disabled: true,
|
||||
},
|
||||
});
|
||||
break;
|
||||
|
||||
case FETCH_API_FAILED_TO_FETCH:
|
||||
dispatch(
|
||||
doToast({
|
||||
isError: true,
|
||||
message: Comments.isCustomServer
|
||||
? __('Failed to fetch comments. Verify custom server settings.')
|
||||
: __('Failed to fetch comments.'),
|
||||
})
|
||||
);
|
||||
dispatch({ type: ACTIONS.COMMENT_LIST_FAILED, data: error });
|
||||
break;
|
||||
|
||||
default:
|
||||
dispatch(doToast({ isError: true, message: `${error.message}` }));
|
||||
dispatch({ type: ACTIONS.COMMENT_LIST_FAILED, data: error });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -261,7 +274,6 @@ export function doCommentReactList(commentIds: Array<string>) {
|
|||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
devToast(dispatch, `doCommentReactList: ${error.message}`);
|
||||
dispatch({
|
||||
type: ACTIONS.COMMENT_REACTION_LIST_FAILED,
|
||||
data: error,
|
||||
|
@ -1345,7 +1357,7 @@ export function doFetchCommentModAmIList(channelClaim: ChannelClaim) {
|
|||
};
|
||||
}
|
||||
|
||||
export const doFetchCreatorSettings = (channelClaimIds: Array<string> = []) => {
|
||||
export const doFetchCreatorSettings = (channelId: string) => {
|
||||
return async (dispatch: Dispatch, getState: GetState) => {
|
||||
const state = getState();
|
||||
const myChannels = selectMyChannelClaims(state);
|
||||
|
@ -1354,84 +1366,49 @@ export const doFetchCreatorSettings = (channelClaimIds: Array<string> = []) => {
|
|||
type: ACTIONS.COMMENT_FETCH_SETTINGS_STARTED,
|
||||
});
|
||||
|
||||
let channelSignatures = [];
|
||||
let signedName;
|
||||
|
||||
if (myChannels) {
|
||||
for (const channelClaim of myChannels) {
|
||||
if (channelClaimIds.length !== 0 && !channelClaimIds.includes(channelClaim.claim_id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const channelSignature = await Lbry.channel_sign({
|
||||
channel_id: channelClaim.claim_id,
|
||||
hexdata: toHex(channelClaim.name),
|
||||
});
|
||||
|
||||
channelSignatures.push({ ...channelSignature, claim_id: channelClaim.claim_id, name: channelClaim.name });
|
||||
} catch (e) {}
|
||||
const index = myChannels.findIndex((myChannel) => myChannel.claim_id === channelId);
|
||||
if (index > -1) {
|
||||
signedName = await channelSignName(channelId, myChannels[index].name);
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.all(
|
||||
channelSignatures.map((signatureData) =>
|
||||
Comments.setting_list({
|
||||
channel_name: signatureData.name,
|
||||
channel_id: signatureData.claim_id,
|
||||
signature: signatureData.signature,
|
||||
signing_ts: signatureData.signing_ts,
|
||||
})
|
||||
)
|
||||
)
|
||||
.then((settings) => {
|
||||
const settingsByChannelId = {};
|
||||
|
||||
for (let i = 0; i < channelSignatures.length; ++i) {
|
||||
const channelId = channelSignatures[i].claim_id;
|
||||
settingsByChannelId[channelId] = settings[i];
|
||||
|
||||
if (settings[i].words) {
|
||||
settingsByChannelId[channelId].words = settings[i].words.split(',');
|
||||
}
|
||||
|
||||
delete settingsByChannelId[channelId].channel_name;
|
||||
delete settingsByChannelId[channelId].channel_id;
|
||||
delete settingsByChannelId[channelId].signature;
|
||||
delete settingsByChannelId[channelId].signing_ts;
|
||||
}
|
||||
const cmd = signedName ? Comments.setting_list : Comments.setting_get;
|
||||
|
||||
return cmd({
|
||||
channel_id: channelId,
|
||||
channel_name: (signedName && signedName.name) || undefined,
|
||||
signature: (signedName && signedName.signature) || undefined,
|
||||
signing_ts: (signedName && signedName.signing_ts) || undefined,
|
||||
})
|
||||
.then((response: SettingsResponse) => {
|
||||
dispatch({
|
||||
type: ACTIONS.COMMENT_FETCH_SETTINGS_COMPLETED,
|
||||
data: settingsByChannelId,
|
||||
data: {
|
||||
channelId: channelId,
|
||||
settings: response,
|
||||
partialUpdate: !signedName,
|
||||
},
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
// TODO: Use error codes when available.
|
||||
// TODO: The "validation is disallowed" thing ideally should just be a
|
||||
// success case that returns a null setting, instead of an error.
|
||||
// As we are using 'Promise.all', if one channel fails, everyone
|
||||
// fails. This forces us to remove the batch functionality of this
|
||||
// function. However, since this "validation is disallowed" thing
|
||||
// is potentially a temporary one to handle spammers, I retained
|
||||
// the batch functionality for now.
|
||||
if (err.message === 'validation is disallowed for non controlling channels') {
|
||||
const settingsByChannelId = {};
|
||||
for (let i = 0; i < channelSignatures.length; ++i) {
|
||||
const channelId = channelSignatures[i].claim_id;
|
||||
// 'undefined' means "fetching or have not fetched";
|
||||
// 'null' means "feature not available for this channel";
|
||||
settingsByChannelId[channelId] = null;
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: ACTIONS.COMMENT_FETCH_SETTINGS_COMPLETED,
|
||||
data: settingsByChannelId,
|
||||
data: {
|
||||
channelId: channelId,
|
||||
settings: null,
|
||||
partialUpdate: !signedName,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
devToast(dispatch, `Creator: ${err}`);
|
||||
dispatch({
|
||||
type: ACTIONS.COMMENT_FETCH_SETTINGS_FAILED,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: ACTIONS.COMMENT_FETCH_SETTINGS_FAILED,
|
||||
});
|
||||
});
|
||||
};
|
||||
};
|
||||
|
@ -1442,22 +1419,13 @@ export const doFetchCreatorSettings = (channelClaimIds: Array<string> = []) => {
|
|||
*
|
||||
* @param channelClaim
|
||||
* @param settings
|
||||
* @returns {function(Dispatch, GetState): Promise<R>|Promise<unknown>|*}
|
||||
* @returns {function(Dispatch, GetState): any}
|
||||
*/
|
||||
export const doUpdateCreatorSettings = (channelClaim: ChannelClaim, settings: PerChannelSettings) => {
|
||||
return async (dispatch: Dispatch, getState: GetState) => {
|
||||
let channelSignature: ?{
|
||||
signature: string,
|
||||
signing_ts: string,
|
||||
};
|
||||
try {
|
||||
channelSignature = await Lbry.channel_sign({
|
||||
channel_id: channelClaim.claim_id,
|
||||
hexdata: toHex(channelClaim.name),
|
||||
});
|
||||
} catch (e) {}
|
||||
|
||||
const channelSignature = await channelSignName(channelClaim.claim_id, channelClaim.name);
|
||||
if (!channelSignature) {
|
||||
devToast(dispatch, 'doUpdateCreatorSettings: failed to sign channel name');
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1468,12 +1436,7 @@ export const doUpdateCreatorSettings = (channelClaim: ChannelClaim, settings: Pe
|
|||
signing_ts: channelSignature.signing_ts,
|
||||
...settings,
|
||||
}).catch((err) => {
|
||||
dispatch(
|
||||
doToast({
|
||||
message: err.message,
|
||||
isError: true,
|
||||
})
|
||||
);
|
||||
dispatch(doToast({ message: err.message, isError: true }));
|
||||
});
|
||||
};
|
||||
};
|
||||
|
|
|
@ -6,13 +6,13 @@ import { doFetchInviteStatus } from 'redux/actions/user';
|
|||
import rewards from 'rewards';
|
||||
|
||||
export function doRewardList() {
|
||||
return dispatch => {
|
||||
return (dispatch) => {
|
||||
dispatch({
|
||||
type: ACTIONS.FETCH_REWARDS_STARTED,
|
||||
});
|
||||
|
||||
Lbryio.call('reward', 'list', { multiple_rewards_per_type: true })
|
||||
.then(userRewards => {
|
||||
.then((userRewards) => {
|
||||
dispatch({
|
||||
type: ACTIONS.FETCH_REWARDS_COMPLETED,
|
||||
data: { userRewards },
|
||||
|
@ -35,7 +35,7 @@ export function doClaimRewardType(rewardType, options = {}) {
|
|||
const reward =
|
||||
rewardType === rewards.TYPE_REWARD_CODE || rewardType === rewards.TYPE_NEW_ANDROID
|
||||
? { reward_type: rewards.TYPE_REWARD_CODE }
|
||||
: unclaimedRewards.find(ur => ur.reward_type === rewardType);
|
||||
: unclaimedRewards.find((ur) => ur.reward_type === rewardType);
|
||||
|
||||
// Try to claim the email reward right away, even if we haven't called reward_list yet
|
||||
if (
|
||||
|
@ -74,7 +74,7 @@ export function doClaimRewardType(rewardType, options = {}) {
|
|||
data: { reward },
|
||||
});
|
||||
|
||||
const success = successReward => {
|
||||
const success = (successReward) => {
|
||||
// Temporary timeout to ensure the sdk has the correct balance after claiming a reward
|
||||
setTimeout(() => {
|
||||
dispatch(doUpdateBalance()).then(() => {
|
||||
|
@ -99,7 +99,7 @@ export function doClaimRewardType(rewardType, options = {}) {
|
|||
}, 2000);
|
||||
};
|
||||
|
||||
const failure = error => {
|
||||
const failure = (error) => {
|
||||
dispatch({
|
||||
type: ACTIONS.CLAIM_REWARD_FAILURE,
|
||||
data: {
|
||||
|
@ -121,6 +121,13 @@ export function doClaimRewardType(rewardType, options = {}) {
|
|||
};
|
||||
}
|
||||
|
||||
export function doClaimInitialRewards() {
|
||||
return (dispatch) => {
|
||||
dispatch(doClaimRewardType(rewards.TYPE_NEW_USER));
|
||||
dispatch(doClaimRewardType(rewards.TYPE_CONFIRM_EMAIL));
|
||||
};
|
||||
}
|
||||
|
||||
export function doClaimEligiblePurchaseRewards() {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
|
@ -131,10 +138,10 @@ export function doClaimEligiblePurchaseRewards() {
|
|||
return;
|
||||
}
|
||||
|
||||
if (unclaimedRewards.find(ur => ur.reward_type === rewards.TYPE_FIRST_STREAM)) {
|
||||
if (unclaimedRewards.find((ur) => ur.reward_type === rewards.TYPE_FIRST_STREAM)) {
|
||||
dispatch(doClaimRewardType(rewards.TYPE_FIRST_STREAM));
|
||||
} else {
|
||||
[rewards.TYPE_MANY_DOWNLOADS, rewards.TYPE_DAILY_VIEW].forEach(type => {
|
||||
[rewards.TYPE_MANY_DOWNLOADS, rewards.TYPE_DAILY_VIEW].forEach((type) => {
|
||||
dispatch(doClaimRewardType(type, { failSilently: true }));
|
||||
});
|
||||
}
|
||||
|
@ -142,7 +149,7 @@ export function doClaimEligiblePurchaseRewards() {
|
|||
}
|
||||
|
||||
export function doClaimRewardClearError(reward) {
|
||||
return dispatch => {
|
||||
return (dispatch) => {
|
||||
dispatch({
|
||||
type: ACTIONS.CLAIM_REWARD_CLEAR_ERROR,
|
||||
data: { reward },
|
||||
|
@ -151,8 +158,8 @@ export function doClaimRewardClearError(reward) {
|
|||
}
|
||||
|
||||
export function doFetchRewardedContent() {
|
||||
return dispatch => {
|
||||
const success = nameToClaimId => {
|
||||
return (dispatch) => {
|
||||
const success = (nameToClaimId) => {
|
||||
dispatch({
|
||||
type: ACTIONS.FETCH_REWARD_CONTENT_COMPLETED,
|
||||
data: {
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
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 { makeSelectSearchUrisForQuery, selectSearchValue } from 'redux/selectors/search';
|
||||
import handleFetchResponse from 'util/handle-fetch';
|
||||
import { getSearchQueryString } from 'util/query-params';
|
||||
import { SIMPLE_SITE, SEARCH_SERVER_API } from 'config';
|
||||
|
@ -48,7 +48,7 @@ export const doSearch = (rawQuery: string, searchOptions: SearchOptions) => (
|
|||
const from = searchOptions.from;
|
||||
|
||||
// If we have already searched for something, we don't need to do anything
|
||||
const urisForQuery = makeSelectSearchUris(queryWithOptions)(state);
|
||||
const urisForQuery = makeSelectSearchUrisForQuery(queryWithOptions)(state);
|
||||
if (urisForQuery && !!urisForQuery.length) {
|
||||
if (!size || !from || from + size < urisForQuery.length) {
|
||||
return;
|
||||
|
@ -61,13 +61,14 @@ export const doSearch = (rawQuery: string, searchOptions: SearchOptions) => (
|
|||
|
||||
lighthouse
|
||||
.search(queryWithOptions)
|
||||
.then((data: Array<{ name: string, claimId: string }>) => {
|
||||
.then((data: { body: Array<{ name: string, claimId: string }>, poweredBy: string }) => {
|
||||
const { body: result, poweredBy } = data;
|
||||
const uris = [];
|
||||
const actions = [];
|
||||
|
||||
data.forEach((result) => {
|
||||
if (result) {
|
||||
const { name, claimId } = result;
|
||||
result.forEach((item) => {
|
||||
if (item) {
|
||||
const { name, claimId } = item;
|
||||
const urlObj: LbryUrlObj = {};
|
||||
|
||||
if (name.startsWith('@')) {
|
||||
|
@ -94,6 +95,7 @@ export const doSearch = (rawQuery: string, searchOptions: SearchOptions) => (
|
|||
from: from,
|
||||
size: size,
|
||||
uris,
|
||||
recsys: poweredBy,
|
||||
},
|
||||
});
|
||||
dispatch(batchActions(...actions));
|
||||
|
|
|
@ -3,9 +3,10 @@ import { Lbryio } from 'lbryinc';
|
|||
import { SETTINGS, Lbry, doWalletEncrypt, doWalletDecrypt } from 'lbry-redux';
|
||||
import { selectGetSyncIsPending, selectSetSyncIsPending, selectSyncIsLocked } from 'redux/selectors/sync';
|
||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||
import { getSavedPassword } from 'util/saved-passwords';
|
||||
import { getSavedPassword, getAuthToken } from 'util/saved-passwords';
|
||||
import { doAnalyticsTagSync, doHandleSyncComplete } from 'redux/actions/app';
|
||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||
import { X_LBRY_AUTH_TOKEN } from 'constants/token';
|
||||
|
||||
let syncTimer = null;
|
||||
const SYNC_INTERVAL = 1000 * 60 * 5; // 5 minutes
|
||||
|
@ -148,6 +149,17 @@ export function doGetSync(passedPassword, callback) {
|
|||
}
|
||||
}
|
||||
|
||||
// @if TARGET='web'
|
||||
const xAuth =
|
||||
Lbry.getApiRequestHeaders() && Object.keys(Lbry.getApiRequestHeaders()).includes(X_LBRY_AUTH_TOKEN)
|
||||
? Lbry.getApiRequestHeaders()[X_LBRY_AUTH_TOKEN]
|
||||
: '';
|
||||
if (xAuth && xAuth !== getAuthToken()) {
|
||||
window.location.reload();
|
||||
return;
|
||||
}
|
||||
// @endif
|
||||
|
||||
return (dispatch) => {
|
||||
dispatch({
|
||||
type: ACTIONS.GET_SYNC_STARTED,
|
||||
|
|
|
@ -110,6 +110,17 @@ export const doCommentSocketConnect = (uri, claimId) => (dispatch) => {
|
|||
data: { connected, claimId },
|
||||
});
|
||||
}
|
||||
if (response.type === 'pinned') {
|
||||
const pinnedComment = response.data.comment;
|
||||
dispatch({
|
||||
type: ACTIONS.COMMENT_PIN_COMPLETED,
|
||||
data: {
|
||||
pinnedComment: pinnedComment,
|
||||
claimId,
|
||||
unpin: !pinnedComment.is_pinned,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ const defaultState: CommentsState = {
|
|||
commentsByUri: {}, // URI -> claimId
|
||||
linkedCommentAncestors: {}, // {"linkedCommentId": ["parentId", "grandParentId", ...]}
|
||||
superChatsByUri: {},
|
||||
pinnedCommentsById: {}, // ClaimId -> array of pinned comment IDs
|
||||
isLoading: false,
|
||||
isLoadingByParentId: {},
|
||||
isCommenting: false,
|
||||
|
@ -40,7 +41,6 @@ const defaultState: CommentsState = {
|
|||
blockingByUri: {},
|
||||
unBlockingByUri: {},
|
||||
togglingForDelegatorMap: {},
|
||||
commentsDisabledChannelIds: [],
|
||||
settingsByChannelId: {}, // ChannelId -> PerChannelSettings
|
||||
fetchingSettings: false,
|
||||
fetchingBlockedWords: false,
|
||||
|
@ -250,32 +250,8 @@ export default handleActions(
|
|||
claimId,
|
||||
uri,
|
||||
disabled,
|
||||
authorClaimId,
|
||||
commenterClaimId,
|
||||
} = action.data;
|
||||
const commentsDisabledChannelIds = [...state.commentsDisabledChannelIds];
|
||||
|
||||
if (disabled) {
|
||||
if (!commentsDisabledChannelIds.includes(authorClaimId)) {
|
||||
commentsDisabledChannelIds.push(authorClaimId);
|
||||
}
|
||||
|
||||
const isLoadingByParentId = Object.assign({}, state.isLoadingByParentId);
|
||||
if (parentId) {
|
||||
isLoadingByParentId[parentId] = false;
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
commentsDisabledChannelIds,
|
||||
isLoading: false,
|
||||
isLoadingByParentId,
|
||||
};
|
||||
} else {
|
||||
const index = commentsDisabledChannelIds.indexOf(authorClaimId);
|
||||
if (index > -1) {
|
||||
commentsDisabledChannelIds.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
const commentById = Object.assign({}, state.commentById);
|
||||
const byId = Object.assign({}, state.byId);
|
||||
|
@ -285,49 +261,65 @@ export default handleActions(
|
|||
const commentsByUri = Object.assign({}, state.commentsByUri);
|
||||
const repliesByParentId = Object.assign({}, state.repliesByParentId);
|
||||
const totalCommentsById = Object.assign({}, state.totalCommentsById);
|
||||
const pinnedCommentsById = Object.assign({}, state.pinnedCommentsById);
|
||||
const totalRepliesByParentId = Object.assign({}, state.totalRepliesByParentId);
|
||||
const isLoadingByParentId = Object.assign({}, state.isLoadingByParentId);
|
||||
const settingsByChannelId = Object.assign({}, state.settingsByChannelId);
|
||||
|
||||
if (!parentId) {
|
||||
totalCommentsById[claimId] = totalItems;
|
||||
topLevelTotalCommentsById[claimId] = totalFilteredItems;
|
||||
topLevelTotalPagesById[claimId] = totalPages;
|
||||
} else {
|
||||
totalRepliesByParentId[parentId] = totalFilteredItems;
|
||||
settingsByChannelId[commenterClaimId] = {
|
||||
...(settingsByChannelId[commenterClaimId] || {}),
|
||||
comments_enabled: !disabled,
|
||||
};
|
||||
|
||||
if (parentId) {
|
||||
isLoadingByParentId[parentId] = false;
|
||||
}
|
||||
|
||||
const commonUpdateAction = (comment, commentById, commentIds, index) => {
|
||||
// map the comment_ids to the new comments
|
||||
commentById[comment.comment_id] = comment;
|
||||
commentIds[index] = comment.comment_id;
|
||||
};
|
||||
|
||||
if (comments) {
|
||||
// we use an Array to preserve order of listing
|
||||
// in reality this doesn't matter and we can just
|
||||
// sort comments by their timestamp
|
||||
const commentIds = Array(comments.length);
|
||||
|
||||
// --- Top-level comments ---
|
||||
if (!parentId) {
|
||||
for (let i = 0; i < comments.length; ++i) {
|
||||
const comment = comments[i];
|
||||
commonUpdateAction(comment, commentById, commentIds, i);
|
||||
pushToArrayInObject(topLevelCommentsById, claimId, comment.comment_id);
|
||||
}
|
||||
}
|
||||
// --- Replies ---
|
||||
else {
|
||||
for (let i = 0; i < comments.length; ++i) {
|
||||
const comment = comments[i];
|
||||
commonUpdateAction(comment, commentById, commentIds, i);
|
||||
pushToArrayInObject(repliesByParentId, parentId, comment.comment_id);
|
||||
}
|
||||
if (!disabled) {
|
||||
if (parentId) {
|
||||
totalRepliesByParentId[parentId] = totalFilteredItems;
|
||||
} else {
|
||||
totalCommentsById[claimId] = totalItems;
|
||||
topLevelTotalCommentsById[claimId] = totalFilteredItems;
|
||||
topLevelTotalPagesById[claimId] = totalPages;
|
||||
}
|
||||
|
||||
byId[claimId] ? byId[claimId].push(...commentIds) : (byId[claimId] = commentIds);
|
||||
commentsByUri[uri] = claimId;
|
||||
const commonUpdateAction = (comment, commentById, commentIds, index) => {
|
||||
// map the comment_ids to the new comments
|
||||
commentById[comment.comment_id] = comment;
|
||||
commentIds[index] = comment.comment_id;
|
||||
};
|
||||
|
||||
if (comments) {
|
||||
// we use an Array to preserve order of listing
|
||||
// in reality this doesn't matter and we can just
|
||||
// sort comments by their timestamp
|
||||
const commentIds = Array(comments.length);
|
||||
|
||||
// --- Top-level comments ---
|
||||
if (!parentId) {
|
||||
for (let i = 0; i < comments.length; ++i) {
|
||||
const comment = comments[i];
|
||||
commonUpdateAction(comment, commentById, commentIds, i);
|
||||
if (comment.is_pinned) {
|
||||
pushToArrayInObject(pinnedCommentsById, claimId, comment.comment_id);
|
||||
} else {
|
||||
pushToArrayInObject(topLevelCommentsById, claimId, comment.comment_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
// --- Replies ---
|
||||
else {
|
||||
for (let i = 0; i < comments.length; ++i) {
|
||||
const comment = comments[i];
|
||||
commonUpdateAction(comment, commentById, commentIds, i);
|
||||
pushToArrayInObject(repliesByParentId, parentId, comment.comment_id);
|
||||
}
|
||||
}
|
||||
|
||||
byId[claimId] ? byId[claimId].push(...commentIds) : (byId[claimId] = commentIds);
|
||||
commentsByUri[uri] = claimId;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -337,13 +329,14 @@ export default handleActions(
|
|||
topLevelTotalPagesById,
|
||||
repliesByParentId,
|
||||
totalCommentsById,
|
||||
pinnedCommentsById,
|
||||
totalRepliesByParentId,
|
||||
byId,
|
||||
commentById,
|
||||
commentsByUri,
|
||||
commentsDisabledChannelIds,
|
||||
isLoading: false,
|
||||
isLoadingByParentId,
|
||||
settingsByChannelId,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -621,21 +614,51 @@ export default handleActions(
|
|||
const { pinnedComment, claimId, unpin } = action.data;
|
||||
const commentById = Object.assign({}, state.commentById);
|
||||
const topLevelCommentsById = Object.assign({}, state.topLevelCommentsById);
|
||||
const pinnedCommentsById = Object.assign({}, state.pinnedCommentsById);
|
||||
|
||||
if (pinnedComment && topLevelCommentsById[claimId]) {
|
||||
const index = topLevelCommentsById[claimId].indexOf(pinnedComment.comment_id);
|
||||
if (index > -1) {
|
||||
topLevelCommentsById[claimId].splice(index, 1);
|
||||
|
||||
if (unpin) {
|
||||
// Without the sort score, I have no idea where to put it. Just
|
||||
// dump it at the bottom. Users can refresh if they want it back to
|
||||
// the correct sorted position.
|
||||
topLevelCommentsById[claimId].push(pinnedComment.comment_id);
|
||||
} else {
|
||||
topLevelCommentsById[claimId].unshift(pinnedComment.comment_id);
|
||||
if (pinnedComment) {
|
||||
if (topLevelCommentsById[claimId]) {
|
||||
const index = topLevelCommentsById[claimId].indexOf(pinnedComment.comment_id);
|
||||
if (index > -1) {
|
||||
topLevelCommentsById[claimId].splice(index, 1);
|
||||
}
|
||||
} else {
|
||||
topLevelCommentsById[claimId] = [];
|
||||
}
|
||||
|
||||
if (pinnedCommentsById[claimId]) {
|
||||
const index = pinnedCommentsById[claimId].indexOf(pinnedComment.comment_id);
|
||||
if (index > -1) {
|
||||
pinnedCommentsById[claimId].splice(index, 1);
|
||||
}
|
||||
} else {
|
||||
pinnedCommentsById[claimId] = [];
|
||||
}
|
||||
|
||||
if (unpin) {
|
||||
// Without the sort score, I have no idea where to put it. Just
|
||||
// dump it at the top. Users can refresh if they want it back to
|
||||
// the correct sorted position.
|
||||
topLevelCommentsById[claimId].unshift(pinnedComment.comment_id);
|
||||
} else {
|
||||
pinnedCommentsById[claimId].unshift(pinnedComment.comment_id);
|
||||
}
|
||||
|
||||
if (commentById[pinnedComment.comment_id]) {
|
||||
// Commentron's `comment.Pin` response places the creator's credentials
|
||||
// in the 'channel_*' fields, which doesn't make sense. Maybe it is to
|
||||
// show who signed/pinned it, but even if so, it shouldn't overload
|
||||
// these variables which are already used by existing comment data structure.
|
||||
// Ensure we don't override the existing/correct values, but fallback
|
||||
// to whatever was given.
|
||||
const { channel_id, channel_name, channel_url } = commentById[pinnedComment.comment_id];
|
||||
commentById[pinnedComment.comment_id] = {
|
||||
...pinnedComment,
|
||||
channel_id: channel_id || pinnedComment.channel_id,
|
||||
channel_name: channel_name || pinnedComment.channel_name,
|
||||
channel_url: channel_url || pinnedComment.channel_url,
|
||||
};
|
||||
} else {
|
||||
commentById[pinnedComment.comment_id] = pinnedComment;
|
||||
}
|
||||
}
|
||||
|
@ -644,6 +667,7 @@ export default handleActions(
|
|||
...state,
|
||||
commentById,
|
||||
topLevelCommentsById,
|
||||
pinnedCommentsById,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -986,14 +1010,26 @@ export default handleActions(
|
|||
fetchingSettings: false,
|
||||
}),
|
||||
[ACTIONS.COMMENT_FETCH_SETTINGS_COMPLETED]: (state: CommentsState, action: any) => {
|
||||
// TODO: This is incorrect, as it could make 'settingsByChannelId' store
|
||||
// only 1 channel with other channel's data purged. It works for now
|
||||
// because the GUI only shows 1 channel's setting at a time, and *always*
|
||||
// re-fetches to get latest data before displaying. Either rename this to
|
||||
// 'activeChannelCreatorSettings', or append the new data properly.
|
||||
const { channelId, settings, partialUpdate } = action.data;
|
||||
const settingsByChannelId = Object.assign({}, state.settingsByChannelId);
|
||||
|
||||
if (partialUpdate) {
|
||||
settingsByChannelId[channelId] = {
|
||||
// The existing may contain additional Creator Settings (e.g. 'words')
|
||||
...(settingsByChannelId[channelId] || {}),
|
||||
// Spread new settings.
|
||||
...settings,
|
||||
};
|
||||
} else {
|
||||
settingsByChannelId[channelId] = settings;
|
||||
if (settings.words) {
|
||||
settingsByChannelId[channelId].words = settings.words.split(',');
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
settingsByChannelId: action.data,
|
||||
settingsByChannelId,
|
||||
fetchingSettings: false,
|
||||
};
|
||||
},
|
||||
|
|
|
@ -17,7 +17,7 @@ const defaultState: SearchState = {
|
|||
[SEARCH_OPTIONS.MEDIA_IMAGE]: defaultSearchTypes.includes(SEARCH_OPTIONS.MEDIA_IMAGE),
|
||||
[SEARCH_OPTIONS.MEDIA_APPLICATION]: defaultSearchTypes.includes(SEARCH_OPTIONS.MEDIA_APPLICATION),
|
||||
},
|
||||
urisByQuery: {},
|
||||
resultsByQuery: {},
|
||||
hasReachedMaxResultsLength: {},
|
||||
searching: false,
|
||||
};
|
||||
|
@ -29,21 +29,23 @@ export default handleActions(
|
|||
searching: true,
|
||||
}),
|
||||
[ACTIONS.SEARCH_SUCCESS]: (state: SearchState, action: SearchSuccess): SearchState => {
|
||||
const { query, uris, from, size } = action.data;
|
||||
const { query, uris, from, size, recsys } = action.data;
|
||||
const normalizedQuery = createNormalizedSearchKey(query);
|
||||
const urisForQuery = state.resultsByQuery[normalizedQuery] && state.resultsByQuery[normalizedQuery]['uris'];
|
||||
|
||||
let newUris = uris;
|
||||
if (from !== 0 && state.urisByQuery[normalizedQuery]) {
|
||||
newUris = Array.from(new Set(state.urisByQuery[normalizedQuery].concat(uris)));
|
||||
if (from !== 0 && urisForQuery) {
|
||||
newUris = Array.from(new Set(urisForQuery.concat(uris)));
|
||||
}
|
||||
|
||||
// The returned number of urls is less than the page size, so we're on the last page
|
||||
const noMoreResults = size && uris.length < size;
|
||||
|
||||
const results = { uris: newUris, recsys };
|
||||
return {
|
||||
...state,
|
||||
searching: false,
|
||||
urisByQuery: Object.assign({}, state.urisByQuery, { [normalizedQuery]: newUris }),
|
||||
resultsByQuery: Object.assign({}, state.resultsByQuery, { [normalizedQuery]: results }),
|
||||
hasReachedMaxResultsLength: Object.assign({}, state.hasReachedMaxResultsLength, {
|
||||
[normalizedQuery]: noMoreResults,
|
||||
}),
|
||||
|
|
|
@ -4,6 +4,7 @@ import { ACTIONS as LBRY_REDUX_ACTIONS, SETTINGS, SHARED_PREFERENCES } from 'lbr
|
|||
import { getSubsetFromKeysArray } from 'util/sync-settings';
|
||||
import { getDefaultLanguage } from 'util/default-languages';
|
||||
import { UNSYNCED_SETTINGS, SIMPLE_SITE } from 'config';
|
||||
import Comments from 'comments';
|
||||
|
||||
const { CLIENT_SYNC_KEYS } = SHARED_PREFERENCES;
|
||||
const settingsToIgnore = (UNSYNCED_SETTINGS && UNSYNCED_SETTINGS.trim().split(' ')) || [];
|
||||
|
@ -51,6 +52,8 @@ const defaultState = {
|
|||
[SETTINGS.VIDEO_THEATER_MODE]: false,
|
||||
[SETTINGS.VIDEO_PLAYBACK_RATE]: 1,
|
||||
[SETTINGS.DESKTOP_WINDOW_ZOOM]: 1,
|
||||
[SETTINGS.CUSTOM_COMMENTS_SERVER_ENABLED]: false,
|
||||
[SETTINGS.CUSTOM_COMMENTS_SERVER_URL]: '',
|
||||
|
||||
[SETTINGS.DARK_MODE_TIMES]: {
|
||||
from: { hour: '21', min: '00', formattedTime: '21:00' },
|
||||
|
@ -169,6 +172,13 @@ reducers[LBRY_REDUX_ACTIONS.USER_STATE_POPULATE] = (state, action) => {
|
|||
const selectedSettings = sharedPreferences ? getSubsetFromKeysArray(sharedPreferences, clientSyncKeys) : {};
|
||||
const mergedClientSettings = { ...currentClientSettings, ...selectedSettings };
|
||||
const newSharedPreferences = sharedPreferences || {};
|
||||
|
||||
Comments.setServerUrl(
|
||||
mergedClientSettings[SETTINGS.CUSTOM_COMMENTS_SERVER_ENABLED]
|
||||
? mergedClientSettings[SETTINGS.CUSTOM_COMMENTS_SERVER_URL]
|
||||
: undefined
|
||||
);
|
||||
|
||||
return Object.assign({}, state, {
|
||||
sharedPreferences: newSharedPreferences,
|
||||
clientSettings: mergedClientSettings,
|
||||
|
|
|
@ -3,7 +3,7 @@ import { createSelector } from 'reselect';
|
|||
import { selectMutedChannels } from 'redux/selectors/blocked';
|
||||
import { selectShowMatureContent } from 'redux/selectors/settings';
|
||||
import { selectBlacklistedOutpointMap, selectFilteredOutpointMap } from 'lbryinc';
|
||||
import { selectClaimsById, isClaimNsfw, selectMyActiveClaims, makeSelectClaimForUri } from 'lbry-redux';
|
||||
import { selectClaimsById, isClaimNsfw, selectMyActiveClaims } from 'lbry-redux';
|
||||
|
||||
const selectState = (state) => state.comments || {};
|
||||
|
||||
|
@ -12,12 +12,29 @@ export const selectIsFetchingComments = createSelector(selectState, (state) => s
|
|||
export const selectIsFetchingCommentsByParentId = createSelector(selectState, (state) => state.isLoadingByParentId);
|
||||
export const selectIsPostingComment = createSelector(selectState, (state) => state.isCommenting);
|
||||
export const selectIsFetchingReacts = createSelector(selectState, (state) => state.isFetchingReacts);
|
||||
export const selectCommentsDisabledChannelIds = createSelector(
|
||||
selectState,
|
||||
(state) => state.commentsDisabledChannelIds
|
||||
);
|
||||
export const selectOthersReactsById = createSelector(selectState, (state) => state.othersReactsByCommentId);
|
||||
|
||||
export const selectPinnedCommentsById = createSelector(selectState, (state) => state.pinnedCommentsById);
|
||||
export const makeSelectPinnedCommentsForUri = (uri: string) =>
|
||||
createSelector(
|
||||
selectCommentsByUri,
|
||||
selectCommentsById,
|
||||
selectPinnedCommentsById,
|
||||
(byUri, byId, pinnedCommentsById) => {
|
||||
const claimId = byUri[uri];
|
||||
const pinnedCommentIds = pinnedCommentsById && pinnedCommentsById[claimId];
|
||||
const pinnedComments = [];
|
||||
|
||||
if (pinnedCommentIds) {
|
||||
pinnedCommentIds.forEach((commentId) => {
|
||||
pinnedComments.push(byId[commentId]);
|
||||
});
|
||||
}
|
||||
|
||||
return pinnedComments;
|
||||
}
|
||||
);
|
||||
|
||||
export const selectModerationBlockList = createSelector(selectState, (state) =>
|
||||
state.moderationBlockList ? state.moderationBlockList.reverse() : []
|
||||
);
|
||||
|
@ -425,15 +442,3 @@ export const makeSelectSuperChatTotalAmountForUri = (uri: string) =>
|
|||
|
||||
return superChatData.totalAmount;
|
||||
});
|
||||
|
||||
export const makeSelectCommentsDisabledForUri = (uri: string) =>
|
||||
createSelector(selectCommentsDisabledChannelIds, makeSelectClaimForUri(uri), (commentsDisabledChannelIds, claim) => {
|
||||
const channelClaim = !claim
|
||||
? null
|
||||
: claim.value_type === 'channel'
|
||||
? claim
|
||||
: claim.signing_channel && claim.is_channel_signature_valid
|
||||
? claim.signing_channel
|
||||
: null;
|
||||
return channelClaim && channelClaim.claim_id && commentsDisabledChannelIds.includes(channelClaim.claim_id);
|
||||
});
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import { createSelector } from 'reselect';
|
||||
import REWARDS from 'rewards';
|
||||
|
||||
const selectState = state => state.rewards || {};
|
||||
const selectState = (state) => state.rewards || {};
|
||||
|
||||
export const selectUnclaimedRewardsByType = createSelector(selectState, state => state.unclaimedRewardsByType);
|
||||
export const selectUnclaimedRewardsByType = createSelector(selectState, (state) => state.unclaimedRewardsByType);
|
||||
|
||||
export const selectClaimedRewardsById = createSelector(selectState, state => state.claimedRewardsById);
|
||||
export const selectClaimedRewardsById = createSelector(selectState, (state) => state.claimedRewardsById);
|
||||
|
||||
export const selectClaimedRewards = createSelector(selectClaimedRewardsById, byId => Object.values(byId) || []);
|
||||
export const selectClaimedRewards = createSelector(selectClaimedRewardsById, (byId) => Object.values(byId) || []);
|
||||
|
||||
export const selectClaimedRewardsByTransactionId = createSelector(selectClaimedRewards, rewards =>
|
||||
export const selectClaimedRewardsByTransactionId = createSelector(selectClaimedRewards, (rewards) =>
|
||||
rewards.reduce((mapParam, reward) => {
|
||||
const map = mapParam;
|
||||
map[reward.transaction_id] = reward;
|
||||
|
@ -17,47 +17,58 @@ export const selectClaimedRewardsByTransactionId = createSelector(selectClaimedR
|
|||
}, {})
|
||||
);
|
||||
|
||||
export const selectUnclaimedRewards = createSelector(selectState, state => state.unclaimedRewards);
|
||||
export const selectUnclaimedRewards = createSelector(selectState, (state) => state.unclaimedRewards);
|
||||
|
||||
export const selectFetchingRewards = createSelector(selectState, state => !!state.fetching);
|
||||
export const selectFetchingRewards = createSelector(selectState, (state) => !!state.fetching);
|
||||
|
||||
export const selectUnclaimedRewardValue = createSelector(selectUnclaimedRewards, rewards =>
|
||||
export const selectUnclaimedRewardValue = createSelector(selectUnclaimedRewards, (rewards) =>
|
||||
rewards.reduce((sum, reward) => sum + reward.reward_amount, 0)
|
||||
);
|
||||
|
||||
export const selectClaimsPendingByType = createSelector(selectState, state => state.claimPendingByType);
|
||||
export const selectClaimsPendingByType = createSelector(selectState, (state) => state.claimPendingByType);
|
||||
|
||||
const selectIsClaimRewardPending = (state, props) => selectClaimsPendingByType(state, props)[props.reward_type];
|
||||
|
||||
export const makeSelectIsRewardClaimPending = () =>
|
||||
createSelector(selectIsClaimRewardPending, isClaiming => isClaiming);
|
||||
createSelector(selectIsClaimRewardPending, (isClaiming) => isClaiming);
|
||||
|
||||
export const selectClaimErrorsByType = createSelector(selectState, state => state.claimErrorsByType);
|
||||
export const selectClaimErrorsByType = createSelector(selectState, (state) => state.claimErrorsByType);
|
||||
|
||||
const selectClaimRewardError = (state, props) => selectClaimErrorsByType(state, props)[props.reward_type];
|
||||
|
||||
export const makeSelectClaimRewardError = () => createSelector(selectClaimRewardError, errorMessage => errorMessage);
|
||||
export const makeSelectClaimRewardError = () => createSelector(selectClaimRewardError, (errorMessage) => errorMessage);
|
||||
|
||||
const selectRewardByType = (state, rewardType) =>
|
||||
selectUnclaimedRewards(state).find(reward => reward.reward_type === rewardType);
|
||||
selectUnclaimedRewards(state).find((reward) => reward.reward_type === rewardType);
|
||||
|
||||
export const makeSelectRewardByType = () => createSelector(selectRewardByType, reward => reward);
|
||||
export const makeSelectRewardByType = () => createSelector(selectRewardByType, (reward) => reward);
|
||||
|
||||
const selectRewardByClaimCode = (state, claimCode) =>
|
||||
selectUnclaimedRewards(state).find(reward => reward.claim_code === claimCode);
|
||||
selectUnclaimedRewards(state).find((reward) => reward.claim_code === claimCode);
|
||||
|
||||
export const makeSelectRewardByClaimCode = () => createSelector(selectRewardByClaimCode, reward => reward);
|
||||
export const makeSelectRewardByClaimCode = () => createSelector(selectRewardByClaimCode, (reward) => reward);
|
||||
|
||||
export const makeSelectRewardAmountByType = () =>
|
||||
createSelector(selectRewardByType, reward => (reward ? reward.reward_amount : 0));
|
||||
createSelector(selectRewardByType, (reward) => (reward ? reward.reward_amount : 0));
|
||||
|
||||
export const selectRewardContentClaimIds = createSelector(selectState, state => state.rewardedContentClaimIds);
|
||||
export const selectRewardContentClaimIds = createSelector(selectState, (state) => state.rewardedContentClaimIds);
|
||||
|
||||
export const selectReferralReward = createSelector(
|
||||
selectUnclaimedRewards,
|
||||
unclaimedRewards => unclaimedRewards.filter(reward => reward.reward_type === REWARDS.TYPE_REFERRAL)[0]
|
||||
(unclaimedRewards) => unclaimedRewards.filter((reward) => reward.reward_type === REWARDS.TYPE_REFERRAL)[0]
|
||||
);
|
||||
|
||||
export const selectHasUnclaimedRefereeReward = createSelector(selectUnclaimedRewards, unclaimedRewards =>
|
||||
unclaimedRewards.some(reward => reward.reward_type === REWARDS.TYPE_REFEREE)
|
||||
export const selectHasUnclaimedRefereeReward = createSelector(selectUnclaimedRewards, (unclaimedRewards) =>
|
||||
unclaimedRewards.some((reward) => reward.reward_type === REWARDS.TYPE_REFEREE)
|
||||
);
|
||||
|
||||
export const selectIsClaimingInitialRewards = createSelector(selectClaimsPendingByType, (claimsPendingByType) => {
|
||||
return !!(claimsPendingByType[REWARDS.TYPE_NEW_USER] || claimsPendingByType[REWARDS.TYPE_CONFIRM_EMAIL]);
|
||||
});
|
||||
|
||||
export const selectHasClaimedInitialRewards = createSelector(selectClaimedRewardsById, (claimedRewardsById) => {
|
||||
const claims = Object.values(claimedRewardsById);
|
||||
const newUserClaimed = !!claims.find((claim) => claim && claim.reward_type === REWARDS.TYPE_NEW_USER);
|
||||
const confirmEmailClaimed = !!claims.find((claim) => claim && claim.reward_type === REWARDS.TYPE_CONFIRM_EMAIL);
|
||||
return newUserClaimed && confirmEmailClaimed;
|
||||
});
|
||||
|
|
|
@ -4,6 +4,7 @@ import { selectShowMatureContent } from 'redux/selectors/settings';
|
|||
import {
|
||||
parseURI,
|
||||
makeSelectClaimForUri,
|
||||
makeSelectClaimForClaimId,
|
||||
makeSelectClaimIsNsfw,
|
||||
buildURI,
|
||||
isClaimNsfw,
|
||||
|
@ -26,9 +27,9 @@ export const selectSearchOptions: (state: State) => SearchOptions = createSelect
|
|||
|
||||
export const selectIsSearching: (state: State) => boolean = createSelector(selectState, (state) => state.searching);
|
||||
|
||||
export const selectSearchUrisByQuery: (state: State) => { [string]: Array<string> } = createSelector(
|
||||
export const selectSearchResultByQuery: (state: State) => { [string]: Array<string> } = createSelector(
|
||||
selectState,
|
||||
(state) => state.urisByQuery
|
||||
(state) => state.resultsByQuery
|
||||
);
|
||||
|
||||
export const selectHasReachedMaxResultsLength: (state: State) => { [boolean]: Array<boolean> } = createSelector(
|
||||
|
@ -36,15 +37,15 @@ export const selectHasReachedMaxResultsLength: (state: State) => { [boolean]: Ar
|
|||
(state) => state.hasReachedMaxResultsLength
|
||||
);
|
||||
|
||||
export const makeSelectSearchUris = (query: string): ((state: State) => Array<string>) =>
|
||||
export const makeSelectSearchUrisForQuery = (query: string): ((state: State) => Array<string>) =>
|
||||
// replace statement below is kind of ugly, and repeated in doSearch action
|
||||
createSelector(selectSearchUrisByQuery, (byQuery) => {
|
||||
createSelector(selectSearchResultByQuery, (byQuery) => {
|
||||
if (query) {
|
||||
query = query.replace(/^lbry:\/\//i, '').replace(/\//, ' ');
|
||||
const normalizedQuery = createNormalizedSearchKey(query);
|
||||
return byQuery[normalizedQuery];
|
||||
return byQuery[normalizedQuery] && byQuery[normalizedQuery]['uris'];
|
||||
}
|
||||
return byQuery[query];
|
||||
return byQuery[query] && byQuery[query]['uris'];
|
||||
});
|
||||
|
||||
export const makeSelectHasReachedMaxResultsLength = (query: string): ((state: State) => boolean) =>
|
||||
|
@ -60,7 +61,7 @@ export const makeSelectHasReachedMaxResultsLength = (query: string): ((state: St
|
|||
export const makeSelectRecommendedContentForUri = (uri: string) =>
|
||||
createSelector(
|
||||
makeSelectClaimForUri(uri),
|
||||
selectSearchUrisByQuery,
|
||||
selectSearchResultByQuery,
|
||||
makeSelectClaimIsNsfw(uri),
|
||||
(claim, searchUrisByQuery, isMature) => {
|
||||
let recommendedContent;
|
||||
|
@ -84,16 +85,47 @@ export const makeSelectRecommendedContentForUri = (uri: string) =>
|
|||
const searchQuery = getSearchQueryString(title.replace(/\//, ' '), options);
|
||||
const normalizedSearchQuery = createNormalizedSearchKey(searchQuery);
|
||||
|
||||
let searchUris = searchUrisByQuery[normalizedSearchQuery];
|
||||
if (searchUris) {
|
||||
searchUris = searchUris.filter((searchUri) => searchUri !== currentUri);
|
||||
recommendedContent = searchUris;
|
||||
let searchResult = searchUrisByQuery[normalizedSearchQuery];
|
||||
if (searchResult) {
|
||||
recommendedContent = searchResult['uris'].filter((searchUri) => searchUri !== currentUri);
|
||||
}
|
||||
}
|
||||
return recommendedContent;
|
||||
}
|
||||
);
|
||||
|
||||
export const makeSelectRecommendedRecsysIdForClaimId = (claimId: string) =>
|
||||
createSelector(makeSelectClaimForClaimId(claimId), selectSearchResultByQuery, (claim, searchUrisByQuery) => {
|
||||
// TODO: DRY this out.
|
||||
let poweredBy;
|
||||
if (claim) {
|
||||
const isMature = isClaimNsfw(claim);
|
||||
const { title } = claim.value;
|
||||
|
||||
if (!title) {
|
||||
return;
|
||||
}
|
||||
|
||||
const options: {
|
||||
related_to?: string,
|
||||
nsfw?: boolean,
|
||||
isBackgroundSearch?: boolean,
|
||||
} = { related_to: claim.claim_id, isBackgroundSearch: true };
|
||||
|
||||
options['nsfw'] = isMature;
|
||||
const searchQuery = getSearchQueryString(title.replace(/\//, ' '), options);
|
||||
const normalizedSearchQuery = createNormalizedSearchKey(searchQuery);
|
||||
|
||||
let searchResult = searchUrisByQuery[normalizedSearchQuery];
|
||||
if (searchResult) {
|
||||
poweredBy = searchResult.recsys;
|
||||
} else {
|
||||
return normalizedSearchQuery;
|
||||
}
|
||||
}
|
||||
return poweredBy;
|
||||
});
|
||||
|
||||
export const makeSelectWinningUriForQuery = (query: string) => {
|
||||
const uriFromQuery = `lbry://${query}`;
|
||||
|
||||
|
|
|
@ -690,7 +690,8 @@
|
|||
}
|
||||
}
|
||||
|
||||
.button--file-action {
|
||||
.button--file-action,
|
||||
.download-text {
|
||||
display: block;
|
||||
margin: 0 0;
|
||||
padding: var(--spacing-xxs) var(--spacing-xxs);
|
||||
|
|
|
@ -217,12 +217,35 @@ $thumbnailWidthSmall: 1rem;
|
|||
|
||||
.comment__pin {
|
||||
margin-left: var(--spacing-s);
|
||||
font-size: var(--font-xsmall);
|
||||
|
||||
.icon {
|
||||
padding-top: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.comment__badge {
|
||||
padding-right: var(--spacing-xxs);
|
||||
|
||||
.icon {
|
||||
margin-bottom: -3px;
|
||||
}
|
||||
}
|
||||
|
||||
.comment__badge--global-mod {
|
||||
.st0 {
|
||||
// @see: ICONS.BADGE_MOD
|
||||
fill: #fe7500;
|
||||
}
|
||||
}
|
||||
|
||||
.comment__badge--mod {
|
||||
.st0 {
|
||||
// @see: ICONS.BADGE_MOD
|
||||
fill: #ff3850;
|
||||
}
|
||||
}
|
||||
|
||||
.comment__message {
|
||||
word-break: break-word;
|
||||
max-width: 35rem;
|
||||
|
@ -437,3 +460,9 @@ $thumbnailWidthSmall: 1rem;
|
|||
.comment--blocked {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.comment--min-amount-notice {
|
||||
.icon {
|
||||
margin-bottom: -3px; // TODO fix few instances of these (find "-2px")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -223,6 +223,27 @@ $discussion-header__height: 3rem;
|
|||
}
|
||||
}
|
||||
|
||||
.livestream-pinned__wrapper {
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
padding: var(--spacing-s) var(--spacing-xs);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
font-size: var(--font-small);
|
||||
background-color: var(--color-card-background-highlighted);
|
||||
width: 100%;
|
||||
|
||||
.livestream-comment {
|
||||
padding-top: var(--spacing-xs);
|
||||
max-height: 6rem;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
@media (min-width: $breakpoint-small) {
|
||||
padding: var(--spacing-xs);
|
||||
width: var(--livestream-comments-width);
|
||||
}
|
||||
}
|
||||
|
||||
.livestream-superchat__amount-large {
|
||||
.credit-amount {
|
||||
display: flex;
|
||||
|
|
|
@ -42,6 +42,7 @@ $contentMaxWidth: 60rem;
|
|||
width: 100%;
|
||||
display: flex;
|
||||
padding: var(--spacing-m) 0;
|
||||
justify-content: space-between;
|
||||
|
||||
.channel-thumbnail {
|
||||
@include handleChannelGif(3rem);
|
||||
|
@ -60,6 +61,7 @@ $contentMaxWidth: 60rem;
|
|||
|
||||
.notification__wrapper--unread {
|
||||
background-color: var(--color-card-background-highlighted);
|
||||
justify-content: space-between;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-button-secondary-bg);
|
||||
|
@ -120,6 +122,7 @@ $contentMaxWidth: 60rem;
|
|||
}
|
||||
|
||||
.notification__title {
|
||||
position: relative;
|
||||
font-size: var(--font-small);
|
||||
color: var(--color-text);
|
||||
margin-bottom: var(--spacing-s);
|
||||
|
@ -135,6 +138,7 @@ $contentMaxWidth: 60rem;
|
|||
|
||||
.notification__text {
|
||||
font-size: var(--font-body);
|
||||
color: var(--color-text);
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
|
|
|
@ -340,3 +340,7 @@ pre {
|
|||
//.successCard, .toConfirmCard {
|
||||
// max-width: 94%
|
||||
//}
|
||||
|
||||
.stripe__complete-verification-button {
|
||||
margin-right: 10px !important;
|
||||
}
|
||||
|
|
11
ui/util/claim.js
Normal file
11
ui/util/claim.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
// @flow
|
||||
|
||||
export function getChannelIdFromClaim(claim: ?Claim) {
|
||||
if (claim) {
|
||||
if (claim.value_type === 'channel') {
|
||||
return claim.claim_id;
|
||||
} else if (claim.signing_channel) {
|
||||
return claim.signing_channel.claim_id;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,9 @@
|
|||
// @flow
|
||||
export default function handleFetchResponse(response: Response): Promise<any> {
|
||||
return response.status === 200 ? Promise.resolve(response.json()) : Promise.reject(new Error(response.statusText));
|
||||
const headers = response.headers;
|
||||
const poweredBy = headers.get('x-powered-by');
|
||||
|
||||
return response.status === 200
|
||||
? response.json().then((body) => ({ body, poweredBy }))
|
||||
: Promise.reject(new Error(response.statusText));
|
||||
}
|
||||
|
|
13
ui/util/stripe.js
Normal file
13
ui/util/stripe.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
// @flow
|
||||
import { STRIPE_PUBLIC_KEY } from 'config';
|
||||
import * as STRIPE_CONSTS from 'constants/stripe';
|
||||
export function getStripeEnvironment() {
|
||||
if (STRIPE_PUBLIC_KEY) {
|
||||
if (STRIPE_PUBLIC_KEY.indexOf('pk_live') > -1) {
|
||||
return STRIPE_CONSTS.LIVE;
|
||||
} else {
|
||||
return STRIPE_CONSTS.TEST;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
|
@ -3589,12 +3589,7 @@ minimatch@^3.0.4:
|
|||
dependencies:
|
||||
brace-expansion "^1.1.7"
|
||||
|
||||
minimist@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
|
||||
integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=
|
||||
|
||||
minimist@^1.2.5:
|
||||
minimist@^1.2.0, minimist@^1.2.5:
|
||||
version "1.2.5"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
|
||||
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
|
||||
|
|
Loading…
Add table
Reference in a new issue