diff --git a/static/app-strings.json b/static/app-strings.json
index b4cc13f9e..f00d1fbc4 100644
--- a/static/app-strings.json
+++ b/static/app-strings.json
@@ -701,7 +701,11 @@
"Bid position must be a number.": "Bid position must be a number.",
"Copy": "Copy",
"Copy Link": "Copy Link",
+ "Link copied.": "Link copied.",
+ "Failed to copy link.": "Failed to copy link.",
"Copy RSS URL": "Copy RSS URL",
+ "RSS URL copied.": "RSS URL copied.",
+ "Failed to copy RSS URL.": "Failed to copy RSS URL.",
"Text copied": "Text copied",
"Rewards Disabled": "Rewards Disabled",
"Wallet servers are used to relay data to and from the LBRY blockchain. They also determine what content shows in trending or is blocked. %learn_more%.": "Wallet servers are used to relay data to and from the LBRY blockchain. They also determine what content shows in trending or is blocked. %learn_more%.",
diff --git a/ui/component/claimMenuList/view.jsx b/ui/component/claimMenuList/view.jsx
index 9463d8675..822bd13f8 100644
--- a/ui/component/claimMenuList/view.jsx
+++ b/ui/component/claimMenuList/view.jsx
@@ -44,7 +44,7 @@ type Props = {
collectionName?: string,
collectionId: string,
isMyCollection: boolean,
- doToast: ({ message: string }) => void,
+ doToast: ({ message: string, isError?: boolean }) => void,
claimIsMine: boolean,
fileInfo: FileListItem,
prepareEdit: ({}, string, {}) => void,
@@ -103,7 +103,7 @@ function ClaimMenuList(props: Props) {
}
const shareUrl: string = generateShareUrl(SHARE_DOMAIN, uri);
- const rssUrl: string = generateRssUrl(URL, uri);
+ const rssUrl: string = isChannel ? generateRssUrl(URL, uri) : '';
const isCollectionClaim = claim && claim.value_type === 'collection';
// $FlowFixMe
const isPlayable =
@@ -183,12 +183,23 @@ function ClaimMenuList(props: Props) {
}
}
+ function copyToClipboard(textToCopy, successMsg, failureMsg) {
+ navigator.clipboard
+ .writeText(textToCopy)
+ .then(() => {
+ doToast({ message: __(successMsg) });
+ })
+ .catch(() => {
+ doToast({ message: __(failureMsg), isError: true });
+ });
+ }
+
function handleCopyRssLink() {
- navigator.clipboard.writeText(rssUrl);
+ copyToClipboard(rssUrl, 'RSS URL copied.', 'Failed to copy RSS URL.');
}
function handleCopyLink() {
- navigator.clipboard.writeText(shareUrl);
+ copyToClipboard(shareUrl, 'Link copied.', 'Failed to copy link.');
}
function handleReportContent() {
diff --git a/ui/util/url.js b/ui/util/url.js
index ef950d5de..89646ce07 100644
--- a/ui/util/url.js
+++ b/ui/util/url.js
@@ -150,6 +150,6 @@ export const generateShareUrl = (
export const generateRssUrl = (domain, lbryUrl) => {
const { channelName, channelClaimId } = parseURI(lbryUrl);
- const url = `${domain}/$/rss/@${channelName}/${channelClaimId}`;
+ const url = `${domain}/$/rss/@${channelName}/${channelClaimId.slice(0, 2)}`;
return url;
};
diff --git a/web/src/rss.js b/web/src/rss.js
index 241521c3b..d5f6d3077 100644
--- a/web/src/rss.js
+++ b/web/src/rss.js
@@ -6,6 +6,8 @@ const SDK_API_PATH = `${LBRY_WEB_API}/api/v1`;
const proxyURL = `${SDK_API_PATH}/proxy`;
Lbry.setDaemonConnectionString(proxyURL);
+const NUM_ENTRIES = 500;
+
async function doClaimSearch(options) {
let results;
try {
@@ -14,15 +16,17 @@ async function doClaimSearch(options) {
return results ? results.items : undefined;
}
-async function getChannelClaim(claimId) {
- const options = {
- claim_ids: [claimId],
- page_size: 1,
- no_totals: true,
- };
+async function getChannelClaim(name, claimId) {
+ let claim;
+ try {
+ const url = `lbry://${name}#${claimId}`;
+ const response = await Lbry.resolve({ urls: [url] });
- const claims = await doClaimSearch(options);
- return claims ? claims[0] : undefined;
+ if (response && response[url] && !response[url].error) {
+ claim = response && response[url];
+ }
+ } catch {}
+ return claim || 'The RSS URL is invalid or is not associated with any channel.';
}
async function getClaimsFromChannel(claimId, count) {
@@ -38,40 +42,49 @@ async function getClaimsFromChannel(claimId, count) {
return await doClaimSearch(options);
}
-async function getFeed(channelClaim) {
- const replaceLineFeeds = (str) => str.replace(/(?:\r\n|\r|\n)/g, '
');
+async function getFeed(channelClaim, feedLink) {
+ const replaceLineFeeds = (str) => str.replace(/(?:\r\n|\r|\n)/g, '
');
+ const fmtDescription = (description) => replaceLineFeeds(description);
+ const sanitizeThumbsUrl = (url) => {
+ if (typeof url === 'string' && url.startsWith('https://')) {
+ return encodeURI(url).replace(/&/g, '%26');
+ }
+ return '';
+ };
const value = channelClaim.value;
const title = value ? value.title : channelClaim.name;
const options = {
- title: title + ' on ' + SITE_NAME,
- description: value && value.description ? replaceLineFeeds(value.description) : '',
- link: `${URL}/${channelClaim.name}:${channelClaim.claim_id}`,
favicon: URL + '/public/favicon.png',
generator: SITE_NAME + ' RSS Feed',
- image: value && value.thumbnail ? value.thumbnail.url : '',
+ title: title + ' on ' + SITE_NAME,
+ description: fmtDescription(value && value.description ? value.description : ''),
+ link: encodeURI(`${URL}/${channelClaim.name}:${channelClaim.claim_id}`),
+ image: sanitizeThumbsUrl(value && value.thumbnail ? value.thumbnail.url : ''),
+ feedLinks: {
+ rss: encodeURI(feedLink),
+ },
author: {
- name: channelClaim.name,
- link: URL + '/' + channelClaim.name + ':' + channelClaim.claim_id,
+ name: encodeURI(channelClaim.name),
+ link: encodeURI(URL + '/' + channelClaim.name + ':' + channelClaim.claim_id),
},
};
const feed = new Feed(options);
-
- const latestClaims = await getClaimsFromChannel(channelClaim.claim_id, 50);
+ const latestClaims = await getClaimsFromChannel(channelClaim.claim_id, NUM_ENTRIES);
latestClaims.forEach((c) => {
const meta = c.meta;
const value = c.value;
feed.addItem({
- guid: c.claim_id,
id: c.claim_id,
- title: value ? value.title : c.name,
- description: value && value.description ? replaceLineFeeds(value.description) : '',
- image: value && value.thumbnail ? value.thumbnail.url : '',
- link: URL + '/' + c.name + ':' + c.claim_id,
+ guid: encodeURI(URL + '/' + c.name + ':' + c.claim_id),
+ title: value && value.title ? value.title : c.name,
+ description: fmtDescription(value && value.description ? value.description : ''),
+ image: sanitizeThumbsUrl(value && value.thumbnail ? value.thumbnail.url : ''),
+ link: encodeURI(URL + '/' + c.name + ':' + c.claim_id),
date: new Date(meta ? meta.creation_timestamp * 1000 : null),
});
});
@@ -79,18 +92,24 @@ async function getFeed(channelClaim) {
return feed;
}
+function postProcess(feed) {
+ // Handle 'Feed' creating an invalid MIME type when trying to guess
+ // from 'https://thumbnails.lbry.com/UCgQ8eREJzR1dO' style of URLs.
+ return feed.replace(/type="image\/\/.*"\/>/g, 'type="image/*"/>');
+}
+
async function getRss(ctx) {
if (!ctx.params.claimName || !ctx.params.claimId) {
return 'Invalid URL';
}
- const channelClaim = await getChannelClaim(ctx.params.claimId);
+ const channelClaim = await getChannelClaim(ctx.params.claimName, ctx.params.claimId);
if (typeof channelClaim === 'string' || !channelClaim) {
return channelClaim;
}
- const feed = await getFeed(channelClaim);
- return feed.rss2();
+ const feed = await getFeed(channelClaim, `${URL}/$/rss/${ctx.params.claimName}/${ctx.params.claimId}`);
+ return postProcess(feed.rss2());
}
module.exports = { getRss };