Merge branch 'master' into accessibility

This commit is contained in:
Baltazar Gomez 2021-07-21 20:59:12 -05:00 committed by GitHub
commit c983af927f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 401 additions and 295 deletions

View file

@ -1,36 +1,72 @@
####### .env.default #########
# Copy this file to .env to make modifications # Copy this file to .env to make modifications
# Base config # Base config
MATOMO_URL=https://analytics.lbry.com/
MATOMO_ID=4
WEBPACK_WEB_PORT=9090 WEBPACK_WEB_PORT=9090
WEBPACK_ELECTRON_PORT=9091 WEBPACK_ELECTRON_PORT=9091
WEB_SERVER_PORT=1337 WEB_SERVER_PORT=1337
## APIS
LBRY_WEB_API=https://api.na-backend.odysee.com LBRY_WEB_API=https://api.na-backend.odysee.com
LBRY_WEB_STREAMING_API=https://cdn.lbryplayer.xyz LBRY_WEB_STREAMING_API=https://cdn.lbryplayer.xyz
LBRY_WEB_BUFFER_API=https://collector-service.api.lbry.tv/api/v1/events/video LBRY_WEB_BUFFER_API=https://collector-service.api.lbry.tv/api/v1/events/video
COMMENT_SERVER_API=https://comments.lbry.com/api/v2 COMMENT_SERVER_API=https://comments.lbry.com/api/v2
THUMBNAIL_CDN_URL=https://image-processor.vanwanet.com/optimize/
WELCOME_VERSION=1.0 WELCOME_VERSION=1.0
# Custom Site info # STRIPE
DOMAIN=lbry.tv STRIPE_PUBLIC_KEY='pk_test_NoL1JWL7i1ipfhVId5KfDZgo'
URL=https://lbry.tv
THUMBNAIL_CDN_URL=https://image-processor.vanwanet.com/optimize/ # Analytics
MATOMO_URL=https://analytics.lbry.com/
MATOMO_ID=4
# OG
OG_TITLE_SUFFIX=| lbry.tv
OG_HOMEPAGE_TITLE=lbry.tv
OG_IMAGE_URL=
SITE_CANONICAL_URL=https://lbry.tv
# UI # UI
## Custom Site info
DOMAIN=lbry.tv
URL=https://lbry.tv
SITE_TITLE=lbry.tv SITE_TITLE=lbry.tv
SITE_NAME=lbry.tv SITE_NAME=lbry.tv
SITE_DESCRIPTION=Meet LBRY, an open, free, and community-controlled content wonderland. SITE_DESCRIPTION=Meet LBRY, an open, free, and community-controlled content wonderland.
SITE_HELP_EMAIL=help@lbry.com SITE_HELP_EMAIL=help@lbry.com
LOGO_TITLE=lbry.tv LOGO_TITLE=lbry.tv
SIMPLE_SITE=false
SHOW_ADS=true ## IMAGE ASSETS
YRBL_HAPPY_IMG_URL=https://cdn.lbryplayer.xyz/api/v3/streams/free/yrbl-happy/7aa50a7e5adaf48691935d55e45d697547392929/839d9a YRBL_HAPPY_IMG_URL=https://cdn.lbryplayer.xyz/api/v3/streams/free/yrbl-happy/7aa50a7e5adaf48691935d55e45d697547392929/839d9a
YRBL_SAD_IMG_URL=https://cdn.lbryplayer.xyz/api/v3/streams/free/yrbl-sad/c2d9649633d974e5ffb503925e1f17d951f1bd0f/f262dd YRBL_SAD_IMG_URL=https://cdn.lbryplayer.xyz/api/v3/streams/free/yrbl-sad/c2d9649633d974e5ffb503925e1f17d951f1bd0f/f262dd
#FAVICON=https://cdn.lbryplayer.xyz/api/v3/streams/free/yrbl-sad/c2d9649633d974e5ffb503925e1f17d951f1bd0f/f262dd #LOGIN_IMG_URL=https://cdn.lbryplayer.xyz/api/v3/streams/free/login/b671946e911c66c5fa7233afb35de2badd9eceb8/0e1d81
#LOGO=https://cdn.lbryplayer.xyz/api/v3/streams/free/yrbl-sad/c2d9649633d974e5ffb503925e1f17d951f1bd0f/f262dd #LOGO=https://cdn.lbryplayer.xyz/api/v3/streams/free/yrbl-sad/c2d9649633d974e5ffb503925e1f17d951f1bd0f/f262dd
#LOGO_TEXT_LIGHT=https://cdn.lbryplayer.xyz/api/v3/streams/free/yrbl-sad/c2d9649633d974e5ffb503925e1f17d951f1bd0f/f262dd #LOGO_TEXT_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 #LOGO_TEXT_DARK=https://cdn.lbryplayer.xyz/api/v3/streams/free/yrbl-sad/c2d9649633d974e5ffb503925e1f17d951f1bd0f/f262dd
#AVATAR_DEFAULT=
# LOCALE
DEFAULT_LANGUAGE=en
## CUSTOM SETTINGS
# Additional settings for below are found in ui/constants/settings and are for
# preventing user settings from applying to custom sites without overwriting them.
# UNSYNCED_SETTINGS='theme dark_mode_times automatic_dark_mode_enabled'
## LINKED CONTENT WHITELIST
KNOWN_APP_DOMAINS=lbry.tv,lbry.lat,odysee.com
## CUSTOM CONTENT
# If the following is true, copy custom/homepage.example.js to custom/homepage.js and modify
CUSTOM_HOMEPAGE=false
# Add channels to auto-follow on first run
AUTO_FOLLOW_CHANNELS=lbry://@lbry#3fda836a92faaceedfe398225fb9b2ee2ed1f01a
## FEATURES AND LIMITS
SIMPLE_SITE=false
ENABLE_COMMENT_REACTIONS=true ENABLE_COMMENT_REACTIONS=true
ENABLE_FILE_REACTIONS=false ENABLE_FILE_REACTIONS=false
@ -41,53 +77,26 @@ CHANNEL_STAKED_LEVEL_VIDEO_COMMENTS=4
CHANNEL_STAKED_LEVEL_LIVESTREAM=5 CHANNEL_STAKED_LEVEL_LIVESTREAM=5
WEB_PUBLISH_SIZE_LIMIT_GB=4 WEB_PUBLISH_SIZE_LIMIT_GB=4
LOADING_BAR_COLOR=#2bbb90 LOADING_BAR_COLOR=#2bbb90
SHOW_ADS=true
# OG ## SIMPLE_SITE REPLACEMENTS
OG_TITLE_SUFFIX=| lbry.tv
OG_HOMEPAGE_TITLE=lbry.tv
OG_IMAGE_URL=
SITE_CANONICAL_URL=https://lbry.tv
# LOCALE
DEFAULT_LANGUAGE=en
# Custom Settings
# Additional settings for below are found in ui/constants/settings and are for
# preventing user settings from applying to custom sites without overwriting them.
# UNSYNCED_SETTINGS='theme dark_mode_times automatic_dark_mode_enabled'
KNOWN_APP_DOMAINS=lbry.tv,lbry.lat,odysee.com
# Custom Content
# If the following is true, copy custom/homepage.example.js to custom/homepage.js and modify
CUSTOM_HOMEPAGE=false
# Add channels to auto-follow on first run
AUTO_FOLLOW_CHANNELS=lbry://@lbry#3fda836a92faaceedfe398225fb9b2ee2ed1f01a
# Add up to 2 sidebar links:
# PINNED_URI_1=@Lbrylatam#2/Integracionesporseguridad#4
# PINNED_LABEL_1=LBRY LATAM
# PINNED_URI_2=$/discover?t=lbrytvpaidbeta&fee_amount=>0&claim_type=stream&channel_ids=5af39f818f668d8c00943c9326c5201c4fe3c423,cda9c4e92f19d6fe0764524a2012056e06ca2055,760da3ba3dd85830a843beaaed543a89b7a367e7,40c36948f0da072dcba3e4833e90f71e16de78be,e8f68563d242f6ac9784dcbc41dd86c28a9391d6,7236fc5d2783ea7314d9076ae6c8a250e3992d1a,cf7792c2a37d0d76aaaff84aff0b99a8c791429d,8316ac90764fedf3147799b7b81a6575a9cc398e,8627af93c1a1219150f06b698f4b33e6ed2f1c1e,8972a1bd06de5186e5e89292b05aac8aaa817791,c5b0b17838df2f6c31162f64d55f60f34ae8bfc6,f576d5dba905fc179de880c3fe3eb3281ea74f59,97dd77c93c9603cbb2583f3589f7f5a6c92baa43,f399d873e0c37cf24de9569b5f22bbb30a5c6709,dba870d0620d41b2b9a152c961e0c06cf875ccfc,ca1fd651c9d14bf2e5088bb2aa0146ee7aeb2ae0,50ad846a4b1543b847bf3fdafb7b45f6b2f5844c,e09ff5abe9fb44dd0dd0576894a6db60a6211603,7b6f7517f6b816827d076fa0eaad550aa315a4e7,2068452c41d8da3bd68961335da0072a99258a1a,5da63df97c8255ae94a88940695b8471657dd5a1,3645cf2f5d0bdac0523f945be1c3ff60758f7845,4da85b12244839d6368b9290f1619ff9514ab2a8,4ad942982e43326c7700b1b6443049b3cfd82161,55304f219244abf82f684f759cc0c7769242f3b4,8f42e5b592bb7f7a03f4a94a86a41b1236bb099f,e2a014d885a48f5be2dc6409610996337312facb,c18996ca488753f714d36d4654715927c1d7f9c2,ebc4214424cfa683a7046e1f794fea1e44788d84,06b6d6d6a893fb589ec2ded948f5122856921ed5,07e4546674268fc0222b2ca22d31d0549dc217ee,060940e41973d4f7f16d72a2733138e931c35f41,f8d6eccd887c9cebd36b1d42aa349279b7f5c3ed,68098b8426f967b8d04cc566348b5c128823219e,2bfe6cdb24a21bdc1b76fb7c416edd50e9e85945,1f9bb08bfa2259629f4aaa9ed40f97e9a41b6fa1,2f20148495612946675fe1c8ea99171e4d950b81,bc6938fa1e09e840056c2e831abf9664f397c472,2a6194792beac5130641e932b5ac6e5a99b5ca4f,185ba2bd547a5e4a77d29fe6c1484f47db5e058f,29cc7f6081268eaa5b3f2946e0cd0b952a94812c,49389450b1241f5d8f4c8c4271a3eb56bba33965,ffdc62ac2f7549398d3aca9d2119e83d80d588d5,d7a4d2808074b0c55d6b239f69d90e7a4930f943,d58aa4a0b2f6c2504c3abce8de3f1afb71800acc,77ae23dc7eb8a75609881d4548a79e4935a89d37,f79bce8a60fbece671f6265adc39f6469f3b9b8c,051995fdf0af634e4911704057a551e9392e62b1
# PINNED_LABEL_2=Paid Beta
# Stripe
STRIPE_PUBLIC_KEY='pk_test_NoL1JWL7i1ipfhVId5KfDZgo'
# SIMPLE_SITE REPLACEMENTS
ENABLE_MATURE=true ENABLE_MATURE=true
ENABLE_UI_NOTIFICATIONS=false ENABLE_UI_NOTIFICATIONS=false
#ENABLE_LINK_TO_APP=true
#FORCE_ANALYTICS=true
#ENABLE_ADVANCED_FILTER=true
#ENABLE_PAID_CONTENT=true
#USE_FOOTER=true
#USE_DISCOVER_WHITELIST=false
#ENABLE_WILD_WEST=false
#FULL_SIDE_LINKS=true
#SHOW_TAGS_INTRO=false
# SEARCH TYPES
#VIDEO_ENABLED=true #VIDEO_ENABLED=true
#AUDIO_ENABLED=true #AUDIO_ENABLED=true
#POSTS_ENABLED=true #POSTS_ENABLED=true
#IMAGES_ENABLED=true #IMAGES_ENABLED=true
#FILES_ENABLED=true #FILES_ENABLED=true
#MODELS_ENABLED=true #MODELS_ENABLED=true
#ENABLE_LINK_TO_APP=true
#FORCE_ANALYTiCS=true
#ENABLE_ADVACNED_FILTER=true
#ENABLE_PAID_CONTENT=true
#USE_FOOTER=true
#USE_DISCOVER_WHITELIST=false
#ENABLE_WILD_WEST=false
#FULL_SIDE_LINKS=true
SHOW_TAGS_INTRO=true

View file

@ -141,6 +141,7 @@ function ClaimListDiscover(props: Props) {
const { search } = location; const { search } = location;
const [page, setPage] = React.useState(1); const [page, setPage] = React.useState(1);
const [forceRefresh, setForceRefresh] = React.useState(); const [forceRefresh, setForceRefresh] = React.useState();
const [finalUris, setFinalUris] = React.useState([]);
const isLargeScreen = useIsLargeScreen(); const isLargeScreen = useIsLargeScreen();
const [orderParamEntry, setOrderParamEntry] = usePersistedState(`entry-${location.pathname}`, CS.ORDER_BY_TRENDING); const [orderParamEntry, setOrderParamEntry] = usePersistedState(`entry-${location.pathname}`, CS.ORDER_BY_TRENDING);
const [orderParamUser, setOrderParamUser] = usePersistedState(`orderUser-${location.pathname}`, CS.ORDER_BY_TRENDING); const [orderParamUser, setOrderParamUser] = usePersistedState(`orderUser-${location.pathname}`, CS.ORDER_BY_TRENDING);
@ -176,7 +177,7 @@ function ClaimListDiscover(props: Props) {
const durationParam = urlParams.get(CS.DURATION_KEY) || null; const durationParam = urlParams.get(CS.DURATION_KEY) || null;
const channelIdsInUrl = urlParams.get(CS.CHANNEL_IDS_KEY); const channelIdsInUrl = urlParams.get(CS.CHANNEL_IDS_KEY);
const channelIdsParam = channelIdsInUrl ? channelIdsInUrl.split(',') : channelIds; const channelIdsParam = channelIdsInUrl ? channelIdsInUrl.split(',') : channelIds;
const feeAmountParam = urlParams.get('fee_amount') || feeAmount || SIMPLE_SITE ? CS.FEE_AMOUNT_ONLY_FREE : undefined; const feeAmountParam = urlParams.get('fee_amount') || feeAmount;
const originalPageSize = pageSize || CS.PAGE_SIZE; const originalPageSize = pageSize || CS.PAGE_SIZE;
const dynamicPageSize = isLargeScreen ? Math.ceil(originalPageSize * (3 / 2)) : originalPageSize; const dynamicPageSize = isLargeScreen ? Math.ceil(originalPageSize * (3 / 2)) : originalPageSize;
const historyAction = history.action; const historyAction = history.action;
@ -360,9 +361,10 @@ function ClaimListDiscover(props: Props) {
} }
const hasMatureTags = tagsParam && tagsParam.split(',').some((t) => MATURE_TAGS.includes(t)); const hasMatureTags = tagsParam && tagsParam.split(',').some((t) => MATURE_TAGS.includes(t));
const claimSearchCacheQuery = createNormalizedClaimSearchKey(options);
let claimSearchResult = claimSearchByQuery[claimSearchCacheQuery]; const mainSearchKey = createNormalizedClaimSearchKey(options);
const claimSearchResultLastPageReached = claimSearchByQueryLastPageReached[claimSearchCacheQuery]; let claimSearchResult = claimSearchByQuery[mainSearchKey];
const claimSearchResultLastPageReached = claimSearchByQueryLastPageReached[mainSearchKey];
// uncomment to fix an item on a page // uncomment to fix an item on a page
// const fixUri = 'lbry://@corbettreport#0/lbryodysee#5'; // const fixUri = 'lbry://@corbettreport#0/lbryodysee#5';
@ -380,6 +382,11 @@ function ClaimListDiscover(props: Props) {
// claimSearchResult.splice(2, 0, fixUri); // claimSearchResult.splice(2, 0, fixUri);
// } // }
const livestreamSearchKey = liveLivestreamsFirst
? createNormalizedClaimSearchKey(getLivestreamOnlyOptions(options))
: undefined;
const livestreamSearchResult = livestreamSearchKey && claimSearchByQuery[livestreamSearchKey];
const [prevOptions, setPrevOptions] = React.useState(null); const [prevOptions, setPrevOptions] = React.useState(null);
if (!isJustScrollingToNewPage(prevOptions, options)) { if (!isJustScrollingToNewPage(prevOptions, options)) {
@ -482,6 +489,17 @@ function ClaimListDiscover(props: Props) {
} }
} }
function urisEqual(prev: Array<string>, next: Array<string>) {
if (!prev || !next) {
// From 'ClaimList', "null" and "undefined" have special meaning,
// so we can't just compare array length here.
// - null = "timed out"
// - undefined = "no result".
return prev === next;
}
return prev.length === next.length && prev.every((value, index) => value === next[index]);
}
React.useEffect(() => { React.useEffect(() => {
if (shouldPerformSearch) { if (shouldPerformSearch) {
const searchOptions = JSON.parse(optionsStringForEffect); const searchOptions = JSON.parse(optionsStringForEffect);
@ -493,6 +511,21 @@ function ClaimListDiscover(props: Props) {
} }
}, [doClaimSearch, shouldPerformSearch, optionsStringForEffect, forceRefresh]); }, [doClaimSearch, shouldPerformSearch, optionsStringForEffect, forceRefresh]);
// Resolve 'finalUri'
React.useEffect(() => {
if (uris) {
if (!urisEqual(uris, finalUris)) {
setFinalUris(uris);
}
} else {
// Wait until all queries are done before updating the uris to avoid layout shifts.
const pending = claimSearchResult === undefined || (liveLivestreamsFirst && livestreamSearchResult === undefined);
if (!pending && !urisEqual(claimSearchResult, finalUris)) {
setFinalUris(claimSearchResult);
}
}
}, [uris, claimSearchResult, finalUris, setFinalUris, liveLivestreamsFirst, livestreamSearchResult]);
const headerToUse = header || ( const headerToUse = header || (
<ClaimListHeader <ClaimListHeader
channelIds={channelIds} channelIds={channelIds}
@ -503,7 +536,7 @@ function ClaimListDiscover(props: Props) {
claimType={claimType} claimType={claimType}
streamType={streamType} streamType={streamType}
defaultStreamType={defaultStreamType} defaultStreamType={defaultStreamType}
feeAmount={SIMPLE_SITE ? undefined : feeAmount} // ENABLE_PAID_CONTENT_DISCOVER or something feeAmount={feeAmount} // ENABLE_PAID_CONTENT_DISCOVER or something
orderBy={orderBy} orderBy={orderBy}
defaultOrderBy={defaultOrderBy} defaultOrderBy={defaultOrderBy}
hideAdvancedFilter={hideAdvancedFilter} hideAdvancedFilter={hideAdvancedFilter}
@ -529,9 +562,9 @@ function ClaimListDiscover(props: Props) {
)} )}
<ClaimList <ClaimList
tileLayout tileLayout
id={claimSearchCacheQuery} id={mainSearchKey}
loading={loading} loading={loading}
uris={uris || claimSearchResult} uris={finalUris}
onScrollBottom={handleScrollBottom} onScrollBottom={handleScrollBottom}
page={page} page={page}
pageSize={dynamicPageSize} pageSize={dynamicPageSize}
@ -563,10 +596,10 @@ function ClaimListDiscover(props: Props) {
</div> </div>
)} )}
<ClaimList <ClaimList
id={claimSearchCacheQuery} id={mainSearchKey}
type={type} type={type}
loading={loading} loading={loading}
uris={uris || claimSearchResult} uris={finalUris}
onScrollBottom={handleScrollBottom} onScrollBottom={handleScrollBottom}
page={page} page={page}
pageSize={dynamicPageSize} pageSize={dynamicPageSize}

View file

@ -5,7 +5,6 @@ import {
makeSelectFileInfoForUri, makeSelectFileInfoForUri,
doPrepareEdit, doPrepareEdit,
makeSelectCollectionForIdHasClaimUrl, makeSelectCollectionForIdHasClaimUrl,
makeSelectNameForCollectionId,
makeSelectCollectionIsMine, makeSelectCollectionIsMine,
COLLECTIONS_CONSTS, COLLECTIONS_CONSTS,
makeSelectEditedCollectionForId, makeSelectEditedCollectionForId,
@ -34,20 +33,28 @@ import fs from 'fs';
const select = (state, props) => { const select = (state, props) => {
const claim = makeSelectClaimForUri(props.uri, false)(state); const claim = makeSelectClaimForUri(props.uri, false)(state);
const permanentUri = claim && claim.permanent_url; const repostedClaim = claim && claim.reposted_claim;
const contentClaim = repostedClaim || claim;
const contentSigningChannel = contentClaim && contentClaim.signing_channel;
const contentPermanentUri = contentClaim && contentClaim.permanent_url;
const contentChannelUri = (contentSigningChannel && contentSigningChannel.permanent_url) || contentPermanentUri;
return { return {
claim, claim,
repostedClaim,
contentClaim,
contentSigningChannel,
contentChannelUri,
claimIsMine: makeSelectSigningIsMine(props.uri)(state), claimIsMine: makeSelectSigningIsMine(props.uri)(state),
hasClaimInWatchLater: makeSelectCollectionForIdHasClaimUrl(COLLECTIONS_CONSTS.WATCH_LATER_ID, permanentUri)(state), hasClaimInWatchLater: makeSelectCollectionForIdHasClaimUrl(COLLECTIONS_CONSTS.WATCH_LATER_ID, contentPermanentUri)(state),
hasClaimInCustom: makeSelectCollectionForIdHasClaimUrl(COLLECTIONS_CONSTS.FAVORITES_ID, permanentUri)(state), hasClaimInCustom: makeSelectCollectionForIdHasClaimUrl(COLLECTIONS_CONSTS.FAVORITES_ID, contentPermanentUri)(state),
channelIsMuted: makeSelectChannelIsMuted(props.uri)(state), channelIsMuted: makeSelectChannelIsMuted(contentChannelUri)(state),
channelIsBlocked: makeSelectChannelIsBlocked(props.uri)(state), channelIsBlocked: makeSelectChannelIsBlocked(contentChannelUri)(state),
fileInfo: makeSelectFileInfoForUri(props.uri)(state), fileInfo: makeSelectFileInfoForUri(props.uri)(state),
isSubscribed: makeSelectIsSubscribed(props.channelUri, true)(state), isSubscribed: makeSelectIsSubscribed(contentChannelUri, true)(state),
channelIsAdminBlocked: makeSelectChannelIsAdminBlocked(props.uri)(state), channelIsAdminBlocked: makeSelectChannelIsAdminBlocked(props.uri)(state),
isAdmin: selectHasAdminChannel(state), isAdmin: selectHasAdminChannel(state),
claimInCollection: makeSelectCollectionForIdHasClaimUrl(props.collectionId, permanentUri)(state), claimInCollection: makeSelectCollectionForIdHasClaimUrl(props.collectionId, contentPermanentUri)(state),
collectionName: makeSelectNameForCollectionId(props.collectionId)(state),
isMyCollection: makeSelectCollectionIsMine(props.collectionId)(state), isMyCollection: makeSelectCollectionIsMine(props.collectionId)(state),
editedCollection: makeSelectEditedCollectionForId(props.collectionId)(state), editedCollection: makeSelectEditedCollectionForId(props.collectionId)(state),
isAuthenticated: Boolean(selectUserVerifiedEmail(state)), isAuthenticated: Boolean(selectUserVerifiedEmail(state)),
@ -71,7 +78,8 @@ const perform = (dispatch) => ({
doChannelUnmute: (channelUri) => dispatch(doChannelUnmute(channelUri)), doChannelUnmute: (channelUri) => dispatch(doChannelUnmute(channelUri)),
doCommentModBlock: (channelUri) => dispatch(doCommentModBlock(channelUri)), doCommentModBlock: (channelUri) => dispatch(doCommentModBlock(channelUri)),
doCommentModUnBlock: (channelUri) => dispatch(doCommentModUnBlock(channelUri)), doCommentModUnBlock: (channelUri) => dispatch(doCommentModUnBlock(channelUri)),
doCommentModBlockAsAdmin: (commenterUri, blockerId) => dispatch(doCommentModBlockAsAdmin(commenterUri, blockerId)), doCommentModBlockAsAdmin: (commenterUri, blockerId) =>
dispatch(doCommentModBlockAsAdmin(commenterUri, blockerId)),
doCommentModUnBlockAsAdmin: (commenterUri, blockerId) => doCommentModUnBlockAsAdmin: (commenterUri, blockerId) =>
dispatch(doCommentModUnBlockAsAdmin(commenterUri, blockerId)), dispatch(doCommentModUnBlockAsAdmin(commenterUri, blockerId)),
doChannelSubscribe: (subscription) => dispatch(doChannelSubscribe(subscription)), doChannelSubscribe: (subscription) => dispatch(doChannelSubscribe(subscription)),

View file

@ -23,8 +23,11 @@ type SubscriptionArgs = {
type Props = { type Props = {
uri: string, uri: string,
channelUri: string,
claim: ?Claim, claim: ?Claim,
repostedClaim: ?Claim,
contentClaim: ?Claim,
contentSigningChannel: ?Claim,
contentChannelUri: string,
openModal: (id: string, {}) => void, openModal: (id: string, {}) => void,
inline?: boolean, inline?: boolean,
channelIsMuted: boolean, channelIsMuted: boolean,
@ -37,12 +40,10 @@ type Props = {
doCommentModUnBlock: (string) => void, doCommentModUnBlock: (string) => void,
doCommentModBlockAsAdmin: (string, string) => void, doCommentModBlockAsAdmin: (string, string) => void,
doCommentModUnBlockAsAdmin: (string, string) => void, doCommentModUnBlockAsAdmin: (string, string) => void,
isRepost: boolean,
doCollectionEdit: (string, any) => void, doCollectionEdit: (string, any) => void,
hasClaimInWatchLater: boolean, hasClaimInWatchLater: boolean,
hasClaimInCustom: boolean, hasClaimInCustom: boolean,
claimInCollection: boolean, claimInCollection: boolean,
collectionName?: string,
collectionId: string, collectionId: string,
isMyCollection: boolean, isMyCollection: boolean,
doToast: ({ message: string, isError?: boolean }) => void, doToast: ({ message: string, isError?: boolean }) => void,
@ -60,8 +61,11 @@ type Props = {
function ClaimMenuList(props: Props) { function ClaimMenuList(props: Props) {
const { const {
uri, uri,
channelUri,
claim, claim,
repostedClaim,
contentClaim,
contentSigningChannel,
contentChannelUri,
openModal, openModal,
inline = false, inline = false,
doChannelMute, doChannelMute,
@ -72,14 +76,12 @@ function ClaimMenuList(props: Props) {
isAdmin, isAdmin,
doCommentModBlock, doCommentModBlock,
doCommentModUnBlock, doCommentModUnBlock,
isRepost,
doCommentModBlockAsAdmin, doCommentModBlockAsAdmin,
doCommentModUnBlockAsAdmin, doCommentModUnBlockAsAdmin,
doCollectionEdit, doCollectionEdit,
hasClaimInWatchLater, hasClaimInWatchLater,
hasClaimInCustom, hasClaimInCustom,
collectionId, collectionId,
collectionName,
isMyCollection, isMyCollection,
doToast, doToast,
claimIsMine, claimIsMine,
@ -92,15 +94,16 @@ function ClaimMenuList(props: Props) {
editedCollection, editedCollection,
isAuthenticated, isAuthenticated,
} = props; } = props;
const repostedContent = claim && claim.reposted_claim; const incognitoClaim = contentChannelUri && !contentChannelUri.includes('@');
const contentClaim = repostedContent || claim; const isChannel = !incognitoClaim && !contentSigningChannel;
const incognitoClaim = channelUri && !channelUri.includes('@'); const { channelName } = parseURI(contentChannelUri);
const signingChannel = claim && (claim.signing_channel || claim);
const permanentUrl = String(channelUri);
const isChannel = !incognitoClaim && signingChannel === claim;
const showDelete = claimIsMine || (fileInfo && (fileInfo.written_bytes > 0 || fileInfo.blobs_completed > 0)); const showDelete = claimIsMine || (fileInfo && (fileInfo.written_bytes > 0 || fileInfo.blobs_completed > 0));
const subscriptionLabel = isSubscribed ? __('Unfollow') : __('Follow'); const subscriptionLabel = __('%action%' + '%user%', {
action: isSubscribed ? __('Unfollow') : __('Follow'),
user: repostedClaim ? __(' @' + channelName) : '',
});
const lastCollectionName = 'Favorites'; const lastCollectionName = 'Favorites';
const lastCollectionId = COLLECTIONS_CONSTS.FAVORITES_ID;
const { push, replace } = useHistory(); const { push, replace } = useHistory();
if (!claim) { if (!claim) {
@ -121,36 +124,46 @@ function ClaimMenuList(props: Props) {
// $FlowFixMe // $FlowFixMe
(contentClaim.value.stream_type === 'audio' || contentClaim.value.stream_type === 'video'); (contentClaim.value.stream_type === 'audio' || contentClaim.value.stream_type === 'video');
function handleAdd(source, name, collectionId) {
doToast({
message: source ? __('Item removed from %name%', { name }) : __('Item added to %name%', { name }),
});
doCollectionEdit(collectionId, {
claims: [contentClaim],
remove: source,
type: 'playlist',
});
}
function handleFollow() { function handleFollow() {
const { channelName } = parseURI(permanentUrl);
const subscriptionHandler = isSubscribed ? doChannelUnsubscribe : doChannelSubscribe; const subscriptionHandler = isSubscribed ? doChannelUnsubscribe : doChannelSubscribe;
subscriptionHandler({ subscriptionHandler({
channelName: '@' + channelName, channelName: '@' + channelName,
uri: permanentUrl, uri: contentChannelUri,
notificationsDisabled: true, notificationsDisabled: true,
}); });
} }
function handleToggleMute() { function handleToggleMute() {
if (channelIsMuted) { if (channelIsMuted) {
doChannelUnmute(channelUri); doChannelUnmute(contentChannelUri);
} else { } else {
doChannelMute(channelUri); doChannelMute(contentChannelUri);
} }
} }
function handleToggleBlock() { function handleToggleBlock() {
if (channelIsBlocked) { if (channelIsBlocked) {
doCommentModUnBlock(channelUri); doCommentModUnBlock(contentChannelUri);
} else { } else {
doCommentModBlock(channelUri); doCommentModBlock(contentChannelUri);
} }
} }
function handleEdit() { function handleEdit() {
if (!isChannel) { if (!isChannel) {
const signingChannelName = signingChannel && signingChannel.name; const signingChannelName = contentSigningChannel && contentSigningChannel.name;
const uriObject: { streamName: string, streamClaimId: string, channelName?: string } = { const uriObject: { streamName: string, streamClaimId: string, channelName?: string } = {
streamName: claim.name, streamName: claim.name,
@ -170,10 +183,10 @@ function ClaimMenuList(props: Props) {
} }
function handleDelete() { function handleDelete() {
if (!isRepost && !isChannel) { if (!repostedClaim && !isChannel) {
openModal(MODALS.CONFIRM_FILE_REMOVE, { uri }); openModal(MODALS.CONFIRM_FILE_REMOVE, { uri, doGoBack: false });
} else { } else {
openModal(MODALS.CONFIRM_CLAIM_REVOKE, { claim, cb: !isRepost && (() => replace(`/$/${PAGES.CHANNELS}`)) }); openModal(MODALS.CONFIRM_CLAIM_REVOKE, { claim, cb: isChannel && (() => replace(`/$/${PAGES.CHANNELS}`)) });
} }
} }
@ -183,9 +196,9 @@ function ClaimMenuList(props: Props) {
function handleToggleAdminBlock() { function handleToggleAdminBlock() {
if (channelIsAdminBlocked) { if (channelIsAdminBlocked) {
doCommentModUnBlockAsAdmin(channelUri, ''); doCommentModUnBlockAsAdmin(contentChannelUri, '');
} else { } else {
doCommentModBlockAsAdmin(channelUri, ''); doCommentModBlockAsAdmin(contentChannelUri, '');
} }
} }
@ -210,7 +223,7 @@ function ClaimMenuList(props: Props) {
function handleReportContent() { function handleReportContent() {
// $FlowFixMe // $FlowFixMe
push(`/$/${PAGES.REPORT_CONTENT}?claimId=${(repostedContent && repostedContent.claim_id) || claim.claim_id}`); push(`/$/${PAGES.REPORT_CONTENT}?claimId=${contentClaim && contentClaim.claim_id}`);
} }
return ( return (
@ -228,101 +241,79 @@ function ClaimMenuList(props: Props) {
{(!IS_WEB || (IS_WEB && isAuthenticated)) && ( {(!IS_WEB || (IS_WEB && isAuthenticated)) && (
<> <>
<> <>
{/* WATCH LATER */}
{isPlayable && !collectionId && (
<MenuItem
className="comment__menu-option"
onSelect={() => {
doToast({
message: hasClaimInWatchLater
? __('Item removed from Watch Later')
: __('Item added to Watch Later'),
});
doCollectionEdit(COLLECTIONS_CONSTS.WATCH_LATER_ID, {
claims: [contentClaim],
remove: hasClaimInWatchLater,
type: 'playlist',
});
}}
>
<div className="menu__link">
<Icon aria-hidden icon={hasClaimInWatchLater ? ICONS.DELETE : ICONS.TIME} />
{hasClaimInWatchLater ? __('In Watch Later') : __('Watch Later')}
</div>
</MenuItem>
)}
{/* CUSTOM LIST */}
{isPlayable && !collectionId && (
<MenuItem
className="comment__menu-option"
onSelect={() => {
doToast({
message: hasClaimInCustom
? __('Item removed from %lastCollectionName%', { lastCollectionName })
: __('Item added to %lastCollectionName%', { lastCollectionName }),
});
doCollectionEdit(COLLECTIONS_CONSTS.FAVORITES_ID, {
claims: [contentClaim],
remove: hasClaimInCustom,
type: 'playlist',
});
}}
>
<div className="menu__link">
<Icon aria-hidden icon={hasClaimInCustom ? ICONS.DELETE : ICONS.STAR} />
{hasClaimInCustom
? __('In %lastCollectionName%', { lastCollectionName })
: __(`${lastCollectionName}`)}
</div>
</MenuItem>
)}
{/* COLLECTION OPERATIONS */} {/* COLLECTION OPERATIONS */}
{collectionId && collectionName && isCollectionClaim && ( {collectionId && isCollectionClaim ? (
<> <>
{Boolean(editedCollection) && (
<MenuItem
className="comment__menu-option"
onSelect={() => push(`/$/${PAGES.LIST}/${collectionId}?view=edit`)}
>
<div className="menu__link">
<Icon aria-hidden iconColor={'red'} icon={ICONS.PUBLISH} />
{__('Publish')}
</div>
</MenuItem>
)}
<MenuItem className="comment__menu-option" onSelect={() => push(`/$/${PAGES.LIST}/${collectionId}`)}> <MenuItem className="comment__menu-option" onSelect={() => push(`/$/${PAGES.LIST}/${collectionId}`)}>
<div className="menu__link"> <div className="menu__link">
<Icon aria-hidden icon={ICONS.VIEW} /> <Icon aria-hidden icon={ICONS.VIEW} />
{__('View List')} {__('View List')}
</div> </div>
</MenuItem> </MenuItem>
<MenuItem {isMyCollection && (
className="comment__menu-option" <>
onSelect={() => openModal(MODALS.COLLECTION_DELETE, { collectionId })} <MenuItem
> className="comment__menu-option"
<div className="menu__link"> onSelect={() => push(`/$/${PAGES.LIST}/${collectionId}?view=edit`)}
<Icon aria-hidden icon={ICONS.DELETE} /> >
{__('Delete List')} <div className="menu__link">
</div> <Icon aria-hidden iconColor={'red'} icon={ICONS.PUBLISH} />
</MenuItem> {editedCollection ? __('Publish') : __('Edit List')}
</div>
</MenuItem>
<MenuItem
className="comment__menu-option"
onSelect={() => openModal(MODALS.COLLECTION_DELETE, { collectionId })}
>
<div className="menu__link">
<Icon aria-hidden icon={ICONS.DELETE} />
{__('Delete List')}
</div>
</MenuItem>
</>
)}
</> </>
)} ) : (
{/* CURRENTLY ONLY SUPPORT PLAYLISTS FOR PLAYABLE; LATER DIFFERENT TYPES */} isPlayable && (
{isPlayable && ( <>
<MenuItem {/* WATCH LATER */}
className="comment__menu-option" <MenuItem
onSelect={() => openModal(MODALS.COLLECTION_ADD, { uri, type: 'playlist' })} className="comment__menu-option"
> onSelect={() => handleAdd(hasClaimInWatchLater, 'Watch Later', COLLECTIONS_CONSTS.WATCH_LATER_ID)}
<div className="menu__link"> >
<Icon aria-hidden icon={ICONS.STACK} /> <div className="menu__link">
{__('Add to Lists')} <Icon aria-hidden icon={hasClaimInWatchLater ? ICONS.DELETE : ICONS.TIME} />
</div> {hasClaimInWatchLater ? __('In Watch Later') : __('Watch Later')}
</MenuItem> </div>
</MenuItem>
{/* CUSTOM LIST */}
<MenuItem
className="comment__menu-option"
onSelect={() => handleAdd(hasClaimInCustom, lastCollectionName, lastCollectionId)}
>
<div className="menu__link">
<Icon aria-hidden icon={hasClaimInCustom ? ICONS.DELETE : ICONS.STAR} />
{hasClaimInCustom ? __(`In ${lastCollectionName}`) : __(`${lastCollectionName}`)}
</div>
</MenuItem>
{/* CURRENTLY ONLY SUPPORT PLAYLISTS FOR PLAYABLE; LATER DIFFERENT TYPES */}
<MenuItem
className="comment__menu-option"
onSelect={() => openModal(MODALS.COLLECTION_ADD, { uri, type: 'playlist' })}
>
<div className="menu__link">
<Icon aria-hidden icon={ICONS.STACK} />
{__('Add to Lists')}
</div>
</MenuItem>
<hr className="menu__separator" />
</>
)
)} )}
</> </>
{!isChannelPage && ( {!isChannelPage && (
<> <>
<hr className="menu__separator" />
<MenuItem className="comment__menu-option" onSelect={handleSupport}> <MenuItem className="comment__menu-option" onSelect={handleSupport}>
<div className="menu__link"> <div className="menu__link">
<Icon aria-hidden icon={ICONS.LBC} /> <Icon aria-hidden icon={ICONS.LBC} />
@ -332,7 +323,7 @@ function ClaimMenuList(props: Props) {
</> </>
)} )}
{!incognitoClaim && !isRepost && !claimIsMine && !isChannelPage && ( {!incognitoClaim && !claimIsMine && !isChannelPage && (
<> <>
<hr className="menu__separator" /> <hr className="menu__separator" />
<MenuItem className="comment__menu-option" onSelect={handleFollow}> <MenuItem className="comment__menu-option" onSelect={handleFollow}>
@ -343,11 +334,11 @@ function ClaimMenuList(props: Props) {
</MenuItem> </MenuItem>
</> </>
)} )}
{!isMyCollection && ( {!isMyCollection && (
<> <>
{(!claimIsMine || channelIsBlocked) && channelUri ? ( {(!claimIsMine || channelIsBlocked) && contentChannelUri ? (
!incognitoClaim && !incognitoClaim && (
!isRepost && (
<> <>
<hr className="menu__separator" /> <hr className="menu__separator" />
<MenuItem className="comment__menu-option" onSelect={handleToggleBlock}> <MenuItem className="comment__menu-option" onSelect={handleToggleBlock}>
@ -376,7 +367,7 @@ function ClaimMenuList(props: Props) {
) )
) : ( ) : (
<> <>
{!isChannelPage && !isRepost && ( {!isChannelPage && !repostedClaim && (
<MenuItem className="comment__menu-option" onSelect={handleEdit}> <MenuItem className="comment__menu-option" onSelect={handleEdit}>
<div className="menu__link"> <div className="menu__link">
<Icon aria-hidden icon={ICONS.EDIT} /> <Icon aria-hidden icon={ICONS.EDIT} />
@ -384,7 +375,6 @@ function ClaimMenuList(props: Props) {
</div> </div>
</MenuItem> </MenuItem>
)} )}
{showDelete && ( {showDelete && (
<MenuItem className="comment__menu-option" onSelect={handleDelete}> <MenuItem className="comment__menu-option" onSelect={handleDelete}>
<div className="menu__link"> <div className="menu__link">
@ -412,7 +402,7 @@ function ClaimMenuList(props: Props) {
<MenuItem className="comment__menu-option" onSelect={handleCopyLink}> <MenuItem className="comment__menu-option" onSelect={handleCopyLink}>
<div className="menu__link"> <div className="menu__link">
<Icon aria-hidden icon={ICONS.COPY_LINK} /> <Icon aria-hidden icon={ICONS.SHARE} />
{__('Copy Link')} {__('Copy Link')}
</div> </div>
</MenuItem> </MenuItem>

View file

@ -150,12 +150,14 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
collectionUris, collectionUris,
disableNavigation, disableNavigation,
} = props; } = props;
const isRepost = claim && claim.repost_channel_url; const isCollection = claim && claim.value_type === 'collection';
const collectionClaimId = isCollection && claim && claim.claim_id;
const listId = collectionId || collectionClaimId;
const WrapperElement = wrapperElement || 'li'; const WrapperElement = wrapperElement || 'li';
const shouldFetch = const shouldFetch =
claim === undefined || (claim !== null && claim.value_type === 'channel' && isEmpty(claim.meta) && !pending); claim === undefined || (claim !== null && claim.value_type === 'channel' && isEmpty(claim.meta) && !pending);
const abandoned = !isResolvingUri && !claim; const abandoned = !isResolvingUri && !claim;
const isMyCollection = collectionId && (isCollectionMine || collectionId.includes('-')); const isMyCollection = listId && (isCollectionMine || listId.includes('-'));
const shouldHideActions = hideActions || isMyCollection || type === 'small' || type === 'tooltip'; const shouldHideActions = hideActions || isMyCollection || type === 'small' || type === 'tooltip';
const canonicalUrl = claim && claim.canonical_url; const canonicalUrl = claim && claim.canonical_url;
const lastCollectionIndex = collectionUris ? collectionUris.length - 1 : 0; const lastCollectionIndex = collectionUris ? collectionUris.length - 1 : 0;
@ -177,7 +179,6 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
claim.value.stream_type && claim.value.stream_type &&
// $FlowFixMe // $FlowFixMe
(claim.value.stream_type === 'audio' || claim.value.stream_type === 'video'); (claim.value.stream_type === 'audio' || claim.value.stream_type === 'video');
const isCollection = claim && claim.value_type === 'collection';
const isChannelUri = isValid ? parseURI(uri).isChannel : false; const isChannelUri = isValid ? parseURI(uri).isChannel : false;
const signingChannel = claim && claim.signing_channel; const signingChannel = claim && claim.signing_channel;
@ -208,12 +209,11 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
} }
let navigateUrl = formatLbryUrlForWeb((claim && claim.canonical_url) || uri || '/'); let navigateUrl = formatLbryUrlForWeb((claim && claim.canonical_url) || uri || '/');
if (collectionId) { if (listId) {
const collectionParams = new URLSearchParams(); const collectionParams = new URLSearchParams();
collectionParams.set(COLLECTIONS_CONSTS.COLLECTION_ID, collectionId); collectionParams.set(COLLECTIONS_CONSTS.COLLECTION_ID, listId);
navigateUrl = navigateUrl + `?` + collectionParams.toString(); navigateUrl = navigateUrl + `?` + collectionParams.toString();
} }
const channelUri = !isChannelUri ? signingChannel && signingChannel.permanent_url : claim && claim.permanent_url;
const navLinkProps = { const navLinkProps = {
to: navigateUrl, to: navigateUrl,
onClick: (e) => e.stopPropagation(), onClick: (e) => e.stopPropagation(),
@ -398,7 +398,7 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
{!pending && ( {!pending && (
<> <>
{renderActions && claim && renderActions(claim)} {renderActions && claim && renderActions(claim)}
{Boolean(isMyCollection && collectionId) && ( {Boolean(isMyCollection && listId) && (
<> <>
<div className="collection-preview__edit-buttons"> <div className="collection-preview__edit-buttons">
<div className="collection-preview__edit-group"> <div className="collection-preview__edit-group">
@ -412,7 +412,7 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
e.stopPropagation(); e.stopPropagation();
if (editCollection) { if (editCollection) {
// $FlowFixMe // $FlowFixMe
editCollection(collectionId, { editCollection(listId, {
order: { from: collectionIndex, to: Number(collectionIndex) - 1 }, order: { from: collectionIndex, to: Number(collectionIndex) - 1 },
}); });
} }
@ -428,7 +428,7 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
e.stopPropagation(); e.stopPropagation();
if (editCollection) { if (editCollection) {
// $FlowFixMe // $FlowFixMe
editCollection(collectionId, { editCollection(listId, {
order: { from: collectionIndex, to: Number(collectionIndex + 1) }, order: { from: collectionIndex, to: Number(collectionIndex + 1) },
}); });
} }
@ -443,7 +443,7 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
// $FlowFixMe // $FlowFixMe
if (editCollection) editCollection(collectionId, { claims: [claim], remove: true }); if (editCollection) editCollection(listId, { claims: [claim], remove: true });
}} }}
/> />
</div> </div>
@ -485,7 +485,7 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
</div> </div>
</div> </div>
{!hideMenu && ( {!hideMenu && (
<ClaimMenuList uri={uri} collectionId={collectionId} channelUri={channelUri} isRepost={isRepost} /> <ClaimMenuList uri={uri} collectionId={listId} />
)} )}
</> </>
</WrapperElement> </WrapperElement>

View file

@ -106,11 +106,10 @@ function ClaimPreviewTile(props: Props) {
onClick: (e) => e.stopPropagation(), onClick: (e) => e.stopPropagation(),
}; };
let isChannel;
let isValid = false; let isValid = false;
if (uri) { if (uri) {
try { try {
({ isChannel } = parseURI(uri)); parseURI(uri);
isValid = true; isValid = true;
} catch (e) { } catch (e) {
isValid = false; isValid = false;
@ -118,6 +117,7 @@ function ClaimPreviewTile(props: Props) {
} }
const signingChannel = claim && claim.signing_channel; const signingChannel = claim && claim.signing_channel;
const isChannel = claim && claim.value_type === 'channel';
const channelUri = !isChannel ? signingChannel && signingChannel.permanent_url : claim && claim.permanent_url; const channelUri = !isChannel ? signingChannel && signingChannel.permanent_url : claim && claim.permanent_url;
const channelTitle = signingChannel && (signingChannel.value.title || signingChannel.name); const channelTitle = signingChannel && (signingChannel.value.title || signingChannel.name);
@ -258,7 +258,7 @@ function ClaimPreviewTile(props: Props) {
)} )}
</h2> </h2>
</NavLink> </NavLink>
<ClaimMenuList uri={uri} collectionId={listId} channelUri={channelUri} isRepost={isRepost} /> <ClaimMenuList uri={uri} collectionId={listId} channelUri={channelUri} />
</div> </div>
<div> <div>
<div className="claim-tile__info"> <div className="claim-tile__info">

View file

@ -29,6 +29,7 @@ type Props = {
myChannels: ?Array<ChannelClaim>, myChannels: ?Array<ChannelClaim>,
doToast: ({ message: string }) => void, doToast: ({ message: string }) => void,
clearPlayingUri: () => void, clearPlayingUri: () => void,
hideRepost?: boolean,
isLivestreamClaim: boolean, isLivestreamClaim: boolean,
reactionsDisabled: boolean, reactionsDisabled: boolean,
download: (string) => void, download: (string) => void,
@ -48,6 +49,7 @@ function FileActions(props: Props) {
myChannels, myChannels,
clearPlayingUri, clearPlayingUri,
doToast, doToast,
hideRepost,
isLivestreamClaim, isLivestreamClaim,
reactionsDisabled, reactionsDisabled,
download, download,
@ -113,23 +115,26 @@ function FileActions(props: Props) {
const lhsSection = ( const lhsSection = (
<> <>
{ENABLE_FILE_REACTIONS && !reactionsDisabled && <FileReactions uri={uri} />} {ENABLE_FILE_REACTIONS && !reactionsDisabled && <FileReactions uri={uri} livestream={isLivestreamClaim} />}
<ClaimSupportButton uri={uri} fileAction /> <ClaimSupportButton uri={uri} fileAction />
<ClaimCollectionAddButton uri={uri} fileAction /> <ClaimCollectionAddButton uri={uri} fileAction />
<Button {!hideRepost && (
className="button--file-action" <Button
icon={ICONS.REPOST} button="alt"
label={ className="button--file-action"
claim.meta.reposted > 1 ? __(`%repost_total% Reposts`, { repost_total: claim.meta.reposted }) : __('Repost') icon={ICONS.REPOST}
} label={
description={__('Repost')} claim.meta.reposted > 1 ? __(`%repost_total% Reposts`, { repost_total: claim.meta.reposted }) : __('Repost')
requiresAuth={IS_WEB} }
onClick={handleRepostClick} description={__('Repost')}
/> requiresAuth={IS_WEB}
onClick={handleRepostClick}
/>
)}
<Button <Button
className="button--file-action" className="button--file-action"
icon={ICONS.SHARE} icon={ICONS.SHARE}
label={__('Share')} label={isMobile ? undefined : __('Share')}
title={__('Share')} title={__('Share')}
onClick={() => openModal(MODALS.SOCIAL_SHARE, { uri, webShareable, collectionId })} onClick={() => openModal(MODALS.SOCIAL_SHARE, { uri, webShareable, collectionId })}
/> />

View file

@ -55,6 +55,9 @@ export default function LivestreamComments(props: Props) {
const commentsLength = comments && comments.length; const commentsLength = comments && comments.length;
const commentsToDisplay = viewMode === VIEW_MODE_CHAT ? comments : superChats; const commentsToDisplay = viewMode === VIEW_MODE_CHAT ? comments : superChats;
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 // todo: implement comment_list --mine in SDK so redux can grab with selectCommentIsMine
function isMyComment(channelId: string) { function isMyComment(channelId: string) {
if (myChannels != null && channelId != null) { if (myChannels != null && channelId != null) {
@ -81,19 +84,16 @@ export default function LivestreamComments(props: Props) {
}; };
}, [claimId, uri, doCommentList, doSuperChatList, doCommentSocketConnect, doCommentSocketDisconnect]); }, [claimId, uri, doCommentList, doSuperChatList, doCommentSocketConnect, doCommentSocketDisconnect]);
React.useEffect(() => { const handleScroll = React.useCallback(() => {
const discussionElement = document.querySelector('.livestream__comments'); if (discussionElement) {
const commentElement = document.querySelector('.livestream-comment'); const negativeCommentHeight = commentElement && -1 * commentElement.offsetHeight;
const isAtRecent = negativeCommentHeight && discussionElement.scrollTop >= negativeCommentHeight;
function handleScroll() { setScrollBottom(isAtRecent);
if (discussionElement) {
const negativeCommentHeight = commentElement && -1 * commentElement.offsetHeight;
const isAtRecent = negativeCommentHeight && discussionElement.scrollTop >= negativeCommentHeight;
setScrollBottom(isAtRecent);
}
} }
}, [commentElement, discussionElement]);
React.useEffect(() => {
if (discussionElement) { if (discussionElement) {
discussionElement.addEventListener('scroll', handleScroll); discussionElement.addEventListener('scroll', handleScroll);
@ -113,15 +113,17 @@ export default function LivestreamComments(props: Props) {
return () => discussionElement.removeEventListener('scroll', handleScroll); return () => discussionElement.removeEventListener('scroll', handleScroll);
} }
}, [commentsLength, performedInitialScroll, setPerformedInitialScroll, setScrollBottom]); }, [commentsLength, discussionElement, handleScroll, performedInitialScroll, setPerformedInitialScroll]);
if (!claim) { if (!claim) {
return null; return null;
} }
function scrollBack() { function scrollBack() {
const element = document.querySelector('.livestream__comments'); if (discussionElement) {
if (element) element.scrollTop = 0; discussionElement.scrollTop = 0;
setScrollBottom(true);
}
} }
return ( return (
@ -209,7 +211,7 @@ export default function LivestreamComments(props: Props) {
button="alt" button="alt"
className="livestream__comments-scroll__down" className="livestream__comments-scroll__down"
label={__('Recent Comments')} label={__('Recent Comments')}
onClick={() => scrollBack()} onClick={scrollBack}
/> />
)} )}

View file

@ -89,7 +89,9 @@ function OptimizedImage(props: Props) {
} else { } else {
setOptimizedSrc(src); setOptimizedSrc(src);
} }
}, []); // eslint-disable-line react-hooks/exhaustive-deps // We only want to run this on (1) initial mount and (2) 'src' change. Nothing else.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [src]);
if (!src) { if (!src) {
return null; return null;

View file

@ -512,7 +512,7 @@ function PublishFile(props: Props) {
{fileSelectSource === SOURCE_UPLOAD && showFileUpload && ( {fileSelectSource === SOURCE_UPLOAD && showFileUpload && (
<> <>
<FileSelector <FileSelector
label={SIMPLE_SITE ? __('Video/audio file') : __('File')} label={__('File')}
disabled={disabled} disabled={disabled}
currentPath={currentFile} currentPath={currentFile}
onFileChosen={handleFileChange} onFileChosen={handleFileChange}

View file

@ -305,6 +305,11 @@ function SideNavigation(props: Props) {
<li className="navigation-link"> <li className="navigation-link">
<Button label={__('FAQ')} href="https://odysee.com/@OdyseeHelp:b" /> <Button label={__('FAQ')} href="https://odysee.com/@OdyseeHelp:b" />
</li> </li>
{SIMPLE_SITE && ( // GUIDELINES_URL?
<li className="navigation-link">
<Button label={__('Community Guidelines')} href="https://odysee.com/@OdyseeHelp:b/Community-Guidelines:c" />
</li>
)}
<li className="navigation-link"> <li className="navigation-link">
<Button label={__('Support --[used in footer; general help/support]--')} href="https://lbry.com/support" /> <Button label={__('Support --[used in footer; general help/support]--')} href="https://lbry.com/support" />
</li> </li>

View file

@ -12,7 +12,7 @@ import LbcSymbol from 'component/common/lbc-symbol';
type Props = { type Props = {
errorMessage: ?string, errorMessage: ?string,
isPending: boolean, isPending: boolean,
verifyUserIdentity: string => void, verifyUserIdentity: (string) => void,
verifyPhone: () => void, verifyPhone: () => void,
fetchUser: () => void, fetchUser: () => void,
skipLink?: string, skipLink?: string,
@ -74,7 +74,7 @@ class UserVerify extends React.PureComponent<Props> {
icon={ICONS.PHONE} icon={ICONS.PHONE}
title={__('Verify phone number')} title={__('Verify phone number')}
subtitle={__( subtitle={__(
'You will receive an SMS text message confirming your phone number is valid. Does not work for Canada and possibly other regions.' 'You will receive an SMS text message confirming your phone number is valid. May not be available in all regions.'
)} )}
actions={ actions={
<Fragment> <Fragment>

View file

@ -1,6 +1,6 @@
// @flow // @flow
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { SIMPLE_SITE } from 'config'; // import { SIMPLE_SITE } from 'config';
import Button from 'component/button'; import Button from 'component/button';
import * as ICONS from 'constants/icons'; import * as ICONS from 'constants/icons';
import classnames from 'classnames'; import classnames from 'classnames';
@ -13,7 +13,7 @@ import hlsQualitySelector from './plugins/videojs-hls-quality-selector/plugin';
import recsys from './plugins/videojs-recsys/plugin'; import recsys from './plugins/videojs-recsys/plugin';
import qualityLevels from 'videojs-contrib-quality-levels'; import qualityLevels from 'videojs-contrib-quality-levels';
import isUserTyping from 'util/detect-typing'; import isUserTyping from 'util/detect-typing';
import './plugins/videojs-aniview/plugin'; // import './plugins/videojs-aniview/plugin';
const isDev = process.env.NODE_ENV !== 'production'; const isDev = process.env.NODE_ENV !== 'production';
@ -192,7 +192,7 @@ export default React.memo<Props>(function VideoJs(props: Props) {
adUrl, adUrl,
claimId, claimId,
userId, userId,
allowPreRoll, // allowPreRoll,
} = props; } = props;
const [reload, setReload] = useState('initial'); const [reload, setReload] = useState('initial');
@ -586,9 +586,9 @@ export default React.memo<Props>(function VideoJs(props: Props) {
// This must be initialized earlier than everything else // This must be initialized earlier than everything else
// otherwise a race condition occurs if we place this in the onReady call back // 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 // allow if isDev because otherwise you'll never see ads when basing to master
if ((allowPreRoll && SIMPLE_SITE) || isDev) { // if ((allowPreRoll && SIMPLE_SITE) || isDev) {
vjs.aniview(); // vjs.aniview();
} // }
// fixes #3498 (https://github.com/lbryio/lbry-desktop/issues/3498) // 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 // summary: on firefox the focus would stick to the fullscreen button which caused buggy behavior with spacebar
@ -663,18 +663,19 @@ export default React.memo<Props>(function VideoJs(props: Props) {
}, [source, reload]); }, [source, reload]);
// Load IMA3 SDK for aniview // Load IMA3 SDK for aniview
useEffect(() => { // disabled for now
const script = document.createElement('script'); // useEffect(() => {
script.src = `https://imasdk.googleapis.com/js/sdkloader/ima3.js`; // const script = document.createElement('script');
script.async = true; // script.src = `https://imasdk.googleapis.com/js/sdkloader/ima3.js`;
// $FlowFixMe // script.async = true;
document.body.appendChild(script); // // $FlowFixMe
// document.body.appendChild(script);
return () => { //
// $FlowFixMe // return () => {
document.body.removeChild(script); // // $FlowFixMe
}; // document.body.removeChild(script);
}); // };
// });
return ( return (
// $FlowFixMe // $FlowFixMe

View file

@ -2,15 +2,16 @@ import { connect } from 'react-redux';
import { doDeleteFileAndMaybeGoBack } from 'redux/actions/file'; import { doDeleteFileAndMaybeGoBack } from 'redux/actions/file';
import { import {
makeSelectTitleForUri, makeSelectTitleForUri,
makeSelectClaimIsMine, doResolveUri,
makeSelectClaimForUri, makeSelectClaimForUri,
makeSelectIsAbandoningClaimForUri, makeSelectIsAbandoningClaimForUri,
} from 'lbry-redux'; } from 'lbry-redux';
import { doHideModal } from 'redux/actions/app'; import { doHideModal } from 'redux/actions/app';
import ModalRemoveFile from './view'; import ModalRemoveFile from './view';
import { makeSelectSigningIsMine } from 'redux/selectors/content';
const select = (state, props) => ({ const select = (state, props) => ({
claimIsMine: makeSelectClaimIsMine(props.uri)(state), claimIsMine: makeSelectSigningIsMine(props.uri)(state),
title: makeSelectTitleForUri(props.uri)(state), title: makeSelectTitleForUri(props.uri)(state),
claim: makeSelectClaimForUri(props.uri)(state), claim: makeSelectClaimForUri(props.uri)(state),
isAbandoning: makeSelectIsAbandoningClaimForUri(props.uri)(state), isAbandoning: makeSelectIsAbandoningClaimForUri(props.uri)(state),
@ -18,8 +19,9 @@ const select = (state, props) => ({
const perform = dispatch => ({ const perform = dispatch => ({
closeModal: () => dispatch(doHideModal()), closeModal: () => dispatch(doHideModal()),
deleteFile: (uri, deleteFromComputer, abandonClaim) => { doResolveUri: (uri) => dispatch(doResolveUri(uri)),
dispatch(doDeleteFileAndMaybeGoBack(uri, deleteFromComputer, abandonClaim)); deleteFile: (uri, deleteFromComputer, abandonClaim, doGoBack) => {
dispatch(doDeleteFileAndMaybeGoBack(uri, deleteFromComputer, abandonClaim, doGoBack));
}, },
}); });

View file

@ -12,8 +12,10 @@ type Props = {
uri: string, uri: string,
claim: StreamClaim, claim: StreamClaim,
claimIsMine: boolean, claimIsMine: boolean,
doResolveUri: (string) => void,
closeModal: () => void, closeModal: () => void,
deleteFile: (string, boolean, boolean) => void, deleteFile: (string, boolean, boolean, boolean) => void,
doGoBack: boolean,
title: string, title: string,
fileInfo?: { fileInfo?: {
outpoint: ?string, outpoint: ?string,
@ -22,10 +24,16 @@ type Props = {
}; };
function ModalRemoveFile(props: Props) { function ModalRemoveFile(props: Props) {
const { uri, claimIsMine, closeModal, deleteFile, title, claim, isAbandoning } = props; const { uri, claimIsMine, doResolveUri, closeModal, deleteFile, doGoBack = true, title, claim, isAbandoning } = props;
const [deleteChecked, setDeleteChecked] = usePersistedState('modal-remove-file:delete', true); const [deleteChecked, setDeleteChecked] = usePersistedState('modal-remove-file:delete', true);
const [abandonChecked, setAbandonChecked] = usePersistedState('modal-remove-file:abandon', true); const [abandonChecked, setAbandonChecked] = usePersistedState('modal-remove-file:abandon', true);
React.useEffect(() => {
if (uri) {
doResolveUri(uri);
}
}, [uri, doResolveUri]);
return ( return (
<Modal isOpen contentLabel={__('Confirm File Remove')} type="card" onAborted={closeModal}> <Modal isOpen contentLabel={__('Confirm File Remove')} type="card" onAborted={closeModal}>
<Card <Card
@ -52,9 +60,7 @@ function ModalRemoveFile(props: Props) {
<FormField <FormField
name="claim_abandon" name="claim_abandon"
label={ label={
<I18nMessage <I18nMessage tokens={{ lbc: <LbcSymbol postfix={claim.amount} /> }}>
tokens={{ lbc: <LbcSymbol prefix={__('reclaim %amount%', { amount: claim.amount })} /> }}
>
Remove from blockchain (%lbc%) Remove from blockchain (%lbc%)
</I18nMessage> </I18nMessage>
} }
@ -87,7 +93,7 @@ function ModalRemoveFile(props: Props) {
button="primary" button="primary"
label={isAbandoning ? __('Removing...') : __('OK')} label={isAbandoning ? __('Removing...') : __('OK')}
disabled={isAbandoning || !(deleteChecked || abandonChecked)} disabled={isAbandoning || !(deleteChecked || abandonChecked)}
onClick={() => deleteFile(uri, deleteChecked, claimIsMine ? abandonChecked : false)} onClick={() => deleteFile(uri, deleteChecked, claimIsMine ? abandonChecked : false, doGoBack)}
/> />
<Button button="link" label={__('Cancel')} onClick={closeModal} /> <Button button="link" label={__('Cancel')} onClick={closeModal} />
</div> </div>

View file

@ -218,7 +218,7 @@ function ChannelPage(props: Props) {
{!(isBlocked || isMuted) && <ClaimSupportButton uri={uri} />} {!(isBlocked || isMuted) && <ClaimSupportButton uri={uri} />}
{!(isBlocked || isMuted) && (!channelIsBlackListed || isSubscribed) && <SubscribeButton uri={permanentUrl} />} {!(isBlocked || isMuted) && (!channelIsBlackListed || isSubscribed) && <SubscribeButton uri={permanentUrl} />}
{/* TODO: add channel collections <ClaimCollectionAddButton uri={uri} fileAction /> */} {/* TODO: add channel collections <ClaimCollectionAddButton uri={uri} fileAction /> */}
<ClaimMenuList uri={claim.permanent_url} channelUri={claim.permanent_url} inline isChannelPage /> <ClaimMenuList uri={claim.permanent_url} inline isChannelPage />
</div> </div>
{cover && <img className={classnames('channel-cover__custom')} src={PlaceholderTx} />} {cover && <img className={classnames('channel-cover__custom')} src={PlaceholderTx} />}
{cover && <OptimizedImage className={classnames('channel-cover__custom')} src={cover} objectFit="cover" />} {cover && <OptimizedImage className={classnames('channel-cover__custom')} src={cover} objectFit="cover" />}

View file

@ -8,7 +8,7 @@ import ClaimTilesDiscover from 'component/claimTilesDiscover';
import ClaimListDiscover from 'component/claimListDiscover'; import ClaimListDiscover from 'component/claimListDiscover';
import * as CS from 'constants/claim_search'; import * as CS from 'constants/claim_search';
import { toCapitalCase } from 'util/string'; import { toCapitalCase } from 'util/string';
import { CUSTOM_HOMEPAGE } from 'config'; import { CUSTOM_HOMEPAGE, SIMPLE_SITE } from 'config';
const MORE_CHANNELS_ANCHOR = 'MoreChannels'; const MORE_CHANNELS_ANCHOR = 'MoreChannels';
@ -49,16 +49,6 @@ function ChannelsFollowingDiscover(props: Props) {
}, },
}); });
rowData.push({
title: 'Latest From @lbrycast',
link: `/@lbrycast:4`,
options: {
orderBy: ['release_time'],
pageSize: 8,
channelIds: ['4c29f8b013adea4d5cca1861fb2161d5089613ea'],
},
});
rowData.push({ rowData.push({
title: 'Trending Channels', title: 'Trending Channels',
link: `/$/${PAGES.DISCOVER}?claim_type=channel`, link: `/$/${PAGES.DISCOVER}?claim_type=channel`,
@ -105,29 +95,32 @@ function ChannelsFollowingDiscover(props: Props) {
return ( return (
<Page> <Page>
{rowDataWithGenericOptions.map(({ title, link, help, options = {} }) => ( {!SIMPLE_SITE &&
<div key={title} className="claim-grid__wrapper"> rowDataWithGenericOptions.map(({ title, link, help, options = {} }) => (
<h1 className="section__actions"> <div key={title} className="claim-grid__wrapper">
{link ? ( <h1 className="section__actions">
<Button {link ? (
className="claim-grid__title" <Button
button="link" className="claim-grid__title"
navigate={link} button="link"
iconRight={ICONS.ARROW_RIGHT} navigate={link}
label={__(title)} iconRight={ICONS.ARROW_RIGHT}
/> label={__(title)}
) : ( />
<span className="claim-grid__title">{__(title)}</span> ) : (
)} <span className="claim-grid__title">{__(title)}</span>
{help} )}
</h1> {help}
</h1>
<ClaimTilesDiscover {...options} /> <ClaimTilesDiscover {...options} />
</div> </div>
))} ))}
<h1 id={MORE_CHANNELS_ANCHOR} className="claim-grid__title"> {!SIMPLE_SITE && (
{__('More Channels')} <h1 id={MORE_CHANNELS_ANCHOR} className="claim-grid__title">
</h1> {__('More Channels')}
</h1>
)}
<ClaimListDiscover <ClaimListDiscover
defaultOrderBy={CS.ORDER_BY_TRENDING} defaultOrderBy={CS.ORDER_BY_TRENDING}
defaultFreshness={CS.FRESH_ALL} defaultFreshness={CS.FRESH_ALL}

View file

@ -24,6 +24,8 @@ import { makeSelectNotificationForCommentId } from 'redux/selectors/notification
import { selectActiveChannelClaim } from 'redux/selectors/app'; import { selectActiveChannelClaim } from 'redux/selectors/app';
import { toHex } from 'util/hex'; import { toHex } from 'util/hex';
import Comments from 'comments'; import Comments from 'comments';
import { selectPrefsReady } from 'redux/selectors/sync';
import { doAlertWaitingForSync } from 'redux/actions/app';
const isDev = process.env.NODE_ENV !== 'production'; const isDev = process.env.NODE_ENV !== 'production';
@ -726,8 +728,22 @@ function doCommentModToggleBlock(
) { ) {
return async (dispatch: Dispatch, getState: GetState) => { return async (dispatch: Dispatch, getState: GetState) => {
const state = getState(); const state = getState();
const ready = selectPrefsReady(state);
let blockerChannelClaims = selectMyChannelClaims(state); let blockerChannelClaims = selectMyChannelClaims(state);
if (!ready) {
return dispatch(doAlertWaitingForSync());
}
if (!blockerChannelClaims) {
return dispatch(
doToast({
message: __('Create a channel to change this setting.'),
isError: false,
})
);
}
if (blockerIds.length === 0) { if (blockerIds.length === 0) {
// Specific blockers not provided, so find one based on block-level. // Specific blockers not provided, so find one based on block-level.
switch (blockLevel) { switch (blockLevel) {

View file

@ -53,7 +53,7 @@ export function doDeleteFile(outpoint, deleteFromComputer, abandonClaim, cb) {
}; };
} }
export function doDeleteFileAndMaybeGoBack(uri, deleteFromComputer, abandonClaim) { export function doDeleteFileAndMaybeGoBack(uri, deleteFromComputer, abandonClaim, doGoBack) {
return (dispatch, getState) => { return (dispatch, getState) => {
const state = getState(); const state = getState();
const playingUri = selectPlayingUri(state); const playingUri = selectPlayingUri(state);
@ -70,7 +70,9 @@ export function doDeleteFileAndMaybeGoBack(uri, deleteFromComputer, abandonClaim
doDeleteFile(outpoint || claimOutpoint, deleteFromComputer, abandonClaim, (abandonState) => { doDeleteFile(outpoint || claimOutpoint, deleteFromComputer, abandonClaim, (abandonState) => {
if (abandonState === ABANDON_STATES.DONE) { if (abandonState === ABANDON_STATES.DONE) {
if (abandonClaim) { if (abandonClaim) {
dispatch(goBack()); if (doGoBack) {
dispatch(goBack());
}
dispatch(doHideModal()); dispatch(doHideModal());
} }
} }

View file

@ -25,7 +25,9 @@ async function redirectMiddleware(ctx, next) {
request: { url }, request: { url },
} = ctx; } = ctx;
if (STATIC_ASSET_PATHS.includes(url) || (url.startsWith('/public/ui-') && url.endsWith('.js'))) { const HASHED_JS_REGEX = /^\/public\/.*[a-fA-F0-9]{12}\.js$/i;
if (STATIC_ASSET_PATHS.includes(url) || HASHED_JS_REGEX.test(url)) {
ctx.set('Cache-Control', `public, max-age=${SIX_MONTHS_IN_SECONDS}`); ctx.set('Cache-Control', `public, max-age=${SIX_MONTHS_IN_SECONDS}`);
} }

View file

@ -1,3 +1,4 @@
const { generateDownloadUrl } = require('../../ui/util/web');
const { URL, SITE_NAME, LBRY_WEB_API } = require('../../config.js'); const { URL, SITE_NAME, LBRY_WEB_API } = require('../../config.js');
const { Lbry } = require('lbry-redux'); const { Lbry } = require('lbry-redux');
const Feed = require('feed').Feed; const Feed = require('feed').Feed;
@ -44,7 +45,9 @@ async function getClaimsFromChannel(claimId, count) {
async function getFeed(channelClaim, feedLink) { async function getFeed(channelClaim, feedLink) {
const replaceLineFeeds = (str) => str.replace(/(?:\r\n|\r|\n)/g, '<br />'); const replaceLineFeeds = (str) => str.replace(/(?:\r\n|\r|\n)/g, '<br />');
const fmtDescription = (description) => replaceLineFeeds(description); const fmtDescription = (description) => replaceLineFeeds(description);
const sanitizeThumbsUrl = (url) => { const sanitizeThumbsUrl = (url) => {
if (typeof url === 'string' && url.startsWith('https://')) { if (typeof url === 'string' && url.startsWith('https://')) {
return encodeURI(url).replace(/&/g, '%26'); return encodeURI(url).replace(/&/g, '%26');
@ -52,6 +55,29 @@ async function getFeed(channelClaim, feedLink) {
return ''; return '';
}; };
const getEnclosure = (claim) => {
const value = claim.value;
if (!value || !value.stream_type || !value.source || !value.source.media_type) {
return undefined;
}
switch (value.stream_type) {
case 'video':
case 'audio':
case 'image':
case 'document':
case 'software':
return {
url: encodeURI(generateDownloadUrl(claim.name, claim.claim_id)),
type: value.source.media_type,
length: value.source.size || 0, // Per spec, 0 is a valid fallback.
};
default:
return undefined;
}
};
const value = channelClaim.value; const value = channelClaim.value;
const title = value ? value.title : channelClaim.name; const title = value ? value.title : channelClaim.name;
@ -78,14 +104,18 @@ async function getFeed(channelClaim, feedLink) {
const meta = c.meta; const meta = c.meta;
const value = c.value; const value = c.value;
const title = value && value.title ? value.title : c.name;
const thumbnailUrl = value && value.thumbnail ? value.thumbnail.url : '';
const thumbnailHtml = thumbnailUrl ? `<p><img src="${thumbnailUrl}" alt="thumbnail" title="${title}" /></p>` : '';
feed.addItem({ feed.addItem({
id: c.claim_id, id: c.claim_id,
guid: encodeURI(URL + '/' + c.name + ':' + c.claim_id), guid: encodeURI(URL + '/' + c.name + ':' + c.claim_id),
title: value && value.title ? value.title : c.name, title: value && value.title ? value.title : c.name,
description: fmtDescription(value && value.description ? value.description : ''), description: thumbnailHtml + fmtDescription(value && value.description ? value.description : ''),
image: sanitizeThumbsUrl(value && value.thumbnail ? value.thumbnail.url : ''),
link: encodeURI(URL + '/' + c.name + ':' + c.claim_id), link: encodeURI(URL + '/' + c.name + ':' + c.claim_id),
date: new Date(meta ? meta.creation_timestamp * 1000 : null), date: new Date(meta ? meta.creation_timestamp * 1000 : null),
enclosure: getEnclosure(c),
}); });
}); });