Tour fixes #159

Merged
NetOpWibby merged 3 commits from tour-fixes into master 2018-09-27 22:51:10 +02:00
23 changed files with 906 additions and 703 deletions

View file

@ -22,6 +22,10 @@
- [lbry/web-daemon](https://github.com/lbryio/web-daemon)
- [Node](https://nodejs.org) (version >= 10)
## Notes
- **This repo will not run locally if you do not also have the LBRY app/daemon and the [web-daemon](https://github.com/lbryio/web-daemon) running.**
- When running locally and completing the tipping example in Playground, the LBC donated to a creator comes from _your_ LBC balance. Otherwise, the example will fail. On production, the donated LBC comes from LBRY.
## Installation
`npm i`

View file

@ -13,30 +13,52 @@ if (window.location.href.search && window.location.href.split("?url=")[1]) { //
$("body").on("click", "[data-action]", event => {
event.preventDefault();
document.querySelector("body").addEventListener("click", event => {
if (event.target.dataset.action) {
event.preventDefault();
document.querySelector(".tour").classList.add("waiting");
handleExamples(event.target);
}
$(".tour").addClass("waiting");
if (
event.explicitOriginalTarget.classList &&
event.explicitOriginalTarget.classList[0] === "tour__content__meme__canvas__thumbnail"
) {
for (const thumbnail of document.querySelectorAll(".tour__content__meme__canvas__thumbnail")) {
thumbnail.classList.remove("selected");
}
setTimeout(() => {
handleExamples(event);
$(".tour").removeClass("waiting");
}, 2500); // "rate-limit" to allow example divs time to populate
event.explicitOriginalTarget.classList.add("selected");
updateCanvas(event.explicitOriginalTarget);
}
});
$("body").on("click", ".tour__content__meme__canvas__thumbnail", event => {
$(".tour__content__meme__canvas__thumbnail").removeClass("selected");
event.currentTarget.className += " selected";
updateCanvas(event.currentTarget);
});
$("#fetch-claim-uri").on("keyup", event => {
document.getElementById("fetch-claim-uri").addEventListener("keyup", event => {
const key = event.keyCode ? event.keyCode : event.which;
if (key === 13 && $("#fetch-claim-uri").val()) fetchMetadata(1, $("#fetch-claim-uri").val());
switch(true) {
case (document.querySelector("[data-example='1']").classList.contains("active")):
if (
key === 13 &&
document.getElementById("fetch-claim-uri").value.length > 0
) fetchMetadata(1, document.getElementById("fetch-claim-uri").value);
break;
case (document.querySelector("[data-example='3']").classList.contains("active")):
if (
key === 13 &&
document.getElementById("fetch-claim-uri").value.length > 0
) fetchMetadata(3, document.getElementById("fetch-claim-uri").value);
break;
}
});
$("body").on("keyup", "#meme-top-line, #meme-bottom-line", () => updateCanvas());
document.querySelector("body").addEventListener("keyup", event => {
if (
event.target.id === "meme-top-line" ||
event.target.id === "meme-bottom-line"
) updateCanvas();
});
@ -94,17 +116,18 @@ function debounce(func, wait, immediate) {
}
function initializeTour() {
$(".tour").addClass("waiting");
$("#fetch-claim-uri").val("").focus(); // reset
$(".tour__sidebar__example:nth-child(1)").addClass("active");
document.querySelector(".tour").classList.add("waiting");
document.querySelector("#fetch-claim-uri").value = "";
document.querySelector("#fetch-claim-uri").focus();
document.querySelector(".tour__navigation__example:nth-child(1)").classList.add("active");
send(JSON.stringify({
"message": "landed on tour"
}));
setTimeout(() => {
$(".tour").removeClass("waiting");
}, 2500);
document.querySelector(".tour__navigation__example:nth-child(1)").click();
}, 300);
}
@ -200,10 +223,10 @@ function getMemeInfo() { // TODO: Error handling
const handleExamples = debounce(event => {
let exampleNumber;
const data = event.currentTarget.dataset;
const data = event.dataset;
if (!parseInt($(".tour__sidebar__example.active")[0].dataset.example)) return;
exampleNumber = parseInt($(".tour__sidebar__example.active")[0].dataset.example);
if (!parseInt($(".tour__navigation__example.active")[0].dataset.example)) return;
exampleNumber = parseInt($(".tour__navigation__example.active")[0].dataset.example);
switch(data.action) {
case "choose claim":
@ -224,8 +247,8 @@ const handleExamples = debounce(event => {
$("#tour-url button").text("Resolve");
if ($("#tour-url")[0].style.display === "none") $("#tour-url").show();
$(".tour__sidebar__example").removeClass("active");
$(".tour__sidebar__example:nth-child(1)").addClass("active");
$(".tour__navigation__example").removeClass("active");
$(".tour__navigation__example:nth-child(1)").addClass("active");
$("#tour-loader").empty().show();
$("#tour-results").empty().show();
@ -244,8 +267,8 @@ const handleExamples = debounce(event => {
$("#fetch-claim-uri").val(""); // reset URL bar
$("#tour-url").hide();
$(".tour__sidebar__example").removeClass("active");
$(".tour__sidebar__example:nth-child(2)").addClass("active");
$(".tour__navigation__example").removeClass("active");
$(".tour__navigation__example:nth-child(2)").addClass("active");
$("#tour-loader").empty().show();
$("#tour-results").empty().show();
@ -266,8 +289,8 @@ const handleExamples = debounce(event => {
// $("#tour-url").after("<p>In the LBRY app, you can financially support your favorite creators by donating LBRY Coin (LBC). In this example, we are donating LBC in your stead.</p>");
if ($("#tour-url")[0].style.display === "none") $("#tour-url").show();
$(".tour__sidebar__example").removeClass("active");
$(".tour__sidebar__example:nth-child(3)").addClass("active");
$(".tour__navigation__example").removeClass("active");
$(".tour__navigation__example:nth-child(3)").addClass("active");
$("#tour-loader").empty().show();
$("#tour-results").empty().show();

View file

@ -39,30 +39,29 @@ module.exports = exports = (state, emit) => {
<meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1"/>
<!--/ Open Graph /-->
<meta property="og:type" content="website"/>
<meta property="og:title" content="${config.meta.title}"/>
<meta property="og:url" content="https://lbry.tech${state.href}"/>
<meta property="og:site_name" content="${config.meta.title}"/>
<meta property="og:image" content="/assets/images/apple-touch-icon.png"/>
<meta property="og:image" content="/assets/media/images/og-image.png"/>
<meta property="og:image:height" content="720"/>
<meta property="og:image:width" content="1280"/>
<meta property="og:locale" content="en_US"/>
<meta property="og:site_name" content="${config.meta.title}"/>
<meta property="og:title" content="${title}"/>
<meta property="og:type" content="website"/>
<meta property="og:url" content="https://lbry.tech${state.href}"/>
<!--/ Social/App Stuff /-->
<meta name="apple-mobile-web-app-title" content="${config.meta.title}"/>
<meta name="application-name" content="${config.meta.title}"/>
<meta name="msapplication-TileColor" content="${config.meta.color}"/>
<meta name="msapplication-TileImage" content="/assets/images/apple-touch-icon.png"/>
<meta name="msapplication-TileImage" content="/assets/apple-touch-icon.png"/>
<meta name="theme-color" content="${config.meta.color}"/>
<link rel="apple-touch-icon" href="/assets/images/apple-touch-icon.png"/>
<link rel="icon" href="/assets/images/favicon.svg" type="image/svg+xml"/>
<link rel="mask-icon" href="/assets/images/favicon.svg" color="${config.meta.color}"/>
<link rel="apple-touch-icon" href="/assets/apple-touch-icon.png"/>
<link rel="icon" href="/assets/favicon.svg" type="image/svg+xml"/>
<link rel="mask-icon" href="/assets/favicon.svg" color="${config.meta.color}"/>
<link rel="shortcut icon" href="/assets/favicon.ico"/>
<link href="/assets/bundle.css" rel="stylesheet"/>
<link rel="stylesheet" href="/assets/bundle.css"/>
<script src="/assets/scripts/vendor/zepto.js"></script>
<script>const ws = new WebSocket(location.origin.replace(/^http/, "ws"));</script>
<script src="/assets/scripts/sockets.js"></script>
`;
};

View file

@ -30,9 +30,9 @@ export default class Navigation extends Nanocomponent {
url: "/overview"
},
{
name: "Tour",
title: "Take a Tour",
url: "/tour"
name: "Playground",
title: "Experience LBRY",
url: "/playground"
},
{
name: "Resources",

View file

@ -0,0 +1,82 @@
"use strict";
// P A C K A G E S
import dedent from "dedent";
import html from "choo/html";
import raw from "choo/html/raw";
// E X P O R T
export default function () {
return dedent`
<section class="tour">
<ul class="tour__navigation">
${raw(navigation())}
</ul>
<p class="tour__description" id="tour-example-description"></p>
<section class="tour__content">${raw(example1())}</section>
</section>
`;
}
// H E L P E R S
function example1() {
return html`
<div class="tour__content__urlbar" id="tour-url">
<span>lbry://</span><input id="fetch-claim-uri" placeholder="&thinsp;Enter a LBRY address or select a video below" type="text"/>
<button class="button" data-action="execute claim" type="button">Resolve</button>
</div>
<div class="tour__content__trends" id="tour-loader"></div>
<div id="tour-results"></div>
<script>
document.getElementById("tour-example-description").textContent = document.querySelector("[data-action='tour, example 1']").dataset.description
</script>
`;
}
function navigation() { // TODO: Save tutorial position to localStorage
return dedent`
<li
class="tour__navigation__example"
data-action="tour, example 1"
data-description="In this example, you can see what runs under the hood when selecting content to view in the LBRY app."
data-example="1"
data-success="<strong>Success</strong> You resolved a claim, which is a <em>fancy</em> way of saying you searched for a piece of content and got back all the metadata associated with it (if it exists)."
>
<button type="button">Resolve</button>
<span>Get details of media (aka, "claim" metadata)</span>
</li>
<li
class="tour__navigation__example"
data-action="tour, example 2"
data-description="Sometimes you want to create content, not just consume it. In this example, you can create a meme and upload it to LBRY!"
data-example="2"
data-success="<strong>Meme-a-riffic</strong> You've just contributed to the growing expanse that is the meme industry. Where will your meme go next? YOU DECIDE!"
>
<button type="button">Publish</button>
<span>Create a meme and upload it to the LBRY blockchain</span>
</li>
<li
class="tour__navigation__example"
data-action="tour, example 3"
data-description="In the LBRY app, you can financially support your favorite creators by donating LBRY Coin (LBC). In this example, we are donating LBC in your stead."
data-example="3"
data-success="<strong>Kudos</strong> You've just supported a creator with LBC (or, LBRY credits) with our own stash of LBC (you'd use your own IRL). You're basically saying, 'thanks for this great content, please continue!' and that's awesome. You're awesome."
>
<button type="button">Support</button>
<span>Support creators on LBRY with a tip, on us!</span>
</li>
`;
}

View file

@ -1,62 +0,0 @@
"use strict";
// P A C K A G E S
import dedent from "dedent";
import html from "choo/html";
import raw from "choo/html/raw";
// E X P O R T
export default function () {
return dedent`
<section class="tour">
<ul class="tour__sidebar">
${raw(sidebar())}
</ul>
<section class="tour__content">${raw(example1())}</section>
</section>
`;
}
// H E L P E R S
function example1() {
return html`
<div class="tour__content__urlbar" id="tour-url">
<span>lbry://</span><input id="fetch-claim-uri" placeholder="&thinsp;Enter a LBRY address or select an example below" type="text"/>
<button class="button" data-action="execute claim" type="button">Resolve</button>
</div>
<div class="tour__content__trends" id="tour-loader"></div>
<div id="tour-results"></div>
`;
}
function sidebar() { // TODO: Save tutorial position to localStorage
return dedent`
<li class="tour__sidebar__example" data-action="tour, example 1" data-example="1">
<button type="button">Resolve a claim</button>
<span>Get details of media (aka, "claim" metadata)</span><br/>
<span>In this example, you can see what runs under the hood when selecting content to view in the LBRY app.</span>
</li>
<li class="tour__sidebar__example" data-action="tour, example 2" data-example="2">
<button type="button">Publish content</button>
<span>Create a meme and upload it to the LBRY blockchain</span><br/>
<span>Sometimes you want to create content, not just consume it. In this example, you can create a meme and upload it to LBRY!</span>
</li>
<li class="tour__sidebar__example" data-action="tour, example 3" data-example="3">
<button type="button">Support with LBC</button>
<span>Support creators on LBRY with a tip, on us!</span><br/>
<span>In the LBRY app, you can financially support your favorite creators by donating LBRY Coin (LBC). In this example, we are donating LBC in your stead.</span>
</li>
`;
}

BIN
app/dist/media/images/og-image.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View file

@ -1,34 +1,85 @@
/* global $, log, ws */ "use strict";
"use strict";
// const log = console.log; // eslint-disable-line
document.addEventListener("DOMContentLoaded", () => {
initializeWebSocketConnection();
setInterval(checkWebSocketConnection, 5000);
});
ws.onmessage = socket => {
const data = JSON.parse(socket.data);
let ws = null;
switch (true) {
case data.message === "updated html":
$(data.selector).html(data.html);
$("#emailMessage").val("");
break;
function checkWebSocketConnection() {
if (!ws || ws.readyState === 3) initializeWebSocketConnection();
}
case data.message === "notification": // TODO: Make work with appending so multiple notifications can be sent
$("#flash-container").html(`<div class="flash active${data.type ? " " + data.type : ""}">${data.details}</div>`);
function initializeWebSocketConnection() {
ws = new WebSocket(location.origin.replace(/^http/, "ws"));
setTimeout(() => {
$("#flash-container").html("");
}, 2100);
ws.onopen = () => {
console.log("WebSocket connection established"); // eslint-disable-line
};
break;
ws.onmessage = socket => {
const data = JSON.parse(socket.data);
default:
log(data);
break;
}
};
switch (true) {
case data.message === "updated html":
document.querySelector(data.selector).innerHTML = data.html;
document.getElementById("emailAddress").value = "";
document.getElementById("emailMessage").innerHTML = "";
// `data.example` is added when updating HTML.
// This is when the results of an example are sent to the client.
if (data.example) {
if (!document.querySelector(`[data-example="${data.example}"`).classList.contains("completed")) {
document.getElementById("tour-example-description").classList.remove("success");
}
document.querySelector(`[data-example="${data.example}"`).classList.add("completed");
document.getElementById("tour-example-description").classList.add("success");
document.getElementById("tour-example-description").innerHTML =
document.querySelector(`[data-example="${data.example}"`).dataset.success;
}
// If `data.example` isn't found, reset the description area.
else {
document.getElementById("tour-example-description").classList.remove("success");
document.getElementById("tour-example-description").innerHTML =
document.querySelector(".tour__navigation__example.active").dataset.description;
}
if (document.getElementById("temp-loader"))
document.getElementById("temp-loader").style.display = "none";
document.querySelector(".tour").classList.remove("waiting");
break;
case data.message === "notification": // TODO: Make work with appending so multiple notifications can be sent
document.getElementById("flash-container").innerHTML =
`<div class="flash active${data.type ? " " + data.type : ""}">${data.details}</div>`;
setTimeout(() => {
document.getElementById("flash-container").innerHTML = "";
}, 2100);
break;
default:
console.log(data); // eslint-disable-line
break;
}
};
ws.onclose = () => {
console.log("WebSocket connection lost"); // eslint-disable-line
checkWebSocketConnection(); // reconnect now
};
}
function send(msg) { // eslint-disable-line
socketReady(ws, () => ws.send(msg));
@ -36,11 +87,11 @@ function send(msg) { // eslint-disable-line
function socketReady(socket, callback) {
setTimeout(() => {
if (socket.readyState === 1) {
if (socket && socket.readyState === 1) {
if (callback !== undefined) callback();
return;
} else {
socketReady(socket, callback);
}
return socketReady(socket, callback);
}, 5);
}

View file

@ -12,11 +12,11 @@ const stringifyObject = require("stringify-object");
// V A R I A B L E S
const randomString = local("/app/helpers/random-string");
const randomString = local("app/helpers/random-string");
const loadLanguages = require("prismjs/components/");
const logSlackError = local("/app/helpers/slack");
const publishMeme = local("/app/helpers/publish-meme");
const uploadImage = local("/app/helpers/upload-image");
const logSlackError = local("app/helpers/slack");
const publishMeme = local("app/helpers/publish-meme");
const uploadImage = local("app/helpers/upload-image");
loadLanguages(["json"]);
@ -92,8 +92,6 @@ module.exports = exports = (data, socket) => {
body.file_path = uploadResponse.filename;
return publishMeme(body).then(publishResponse => {
let explorerNotice = "";
if (publishResponse.error) {
socket.send(JSON.stringify({
"details": "Meme publish failed",
@ -112,21 +110,13 @@ module.exports = exports = (data, socket) => {
return;
}
if (
publishResponse.result &&
publishResponse.result.txid
) explorerNotice = `
<p>If you want proof of the tip you just gave, <a href="https://explorer.lbry.io/tx/${publishResponse.result.txid}" target="_blank" title="Your tip, on our blockchain explorer" rel="noopener noreferrer">check it out</a> on our blockchain explorer!</p>
`;
const renderedCode = prism.highlight(stringifyObject(publishResponse, { indent: " ", singleQuotes: false }), prism.languages.json, "json");
return socket.send(JSON.stringify({
"example": data.example,
"html": raw(`
<h3>Response</h3>
${explorerNotice}
<pre><code class="language-json">${renderedCode}</code></pre>
<script>$("#temp-loader").hide();</script>
`),
"message": "updated html",
"selector": `#example${data.example}-result`
@ -141,6 +131,30 @@ module.exports = exports = (data, socket) => {
}
if (resolveMethod === "wallet_send") {
const approvedIds = [
"3db81c073f82fd1bb670c65f526faea3b8546720",
"173412f5b1b7aa63a752e8832406aafd9f1ecb4e",
"d81bac6d49b1f92e58c37a5f633a27a45b43405e",
"b4668c0bd096317b44c40738c099b6618095e75f",
"007789cc45cbb4255cf02ba77cbf84ca8e3d7561",
"1ac47b8b3def40a25850dc726a09ce23d09e7009",
"784b3c215a6f06b663fc1aa292bcb19f29c489bb",
"758dd6497cdfc401ae1f25984738d024d47b50af",
"8a7401b88d5ed0376d98f16808194d4dcb05b284"
];
if (!approvedIds.includes(claimAddress)) {
return socket.send(JSON.stringify({
"example": data.example,
"html": raw(`
<h3>Response</h3>
<pre><code class="language-text">Tipping creators not in the whitelist for this example is not allowed.</code></pre>
`),
"message": "updated html",
"selector": `#example${data.example}-result`
}));
}
apiRequestMethod = "POST";
body.amount = "0.01"; // Hardcoded tip amount
@ -148,8 +162,6 @@ module.exports = exports = (data, socket) => {
}
return new Promise((resolve, reject) => { // eslint-disable-line
let explorerNotice = "";
request({
body: body,
json: true,
@ -180,22 +192,23 @@ module.exports = exports = (data, socket) => {
return resolve(body.error);
}
/*
if (
body.result &&
body.result.txid
) explorerNotice = `
<p>If you want proof of the tip you just gave on behalf of LBRY, <a href="https://explorer.lbry.io/tx/${body.result.txid}" target="_blank" title="Your tip, on our blockchain explorer" rel="noopener noreferrer">check it out</a> on our blockchain explorer!</p>
`;
*/
if (socket) {
const renderedCode = prism.highlight(stringifyObject(body, { indent: " ", singleQuotes: false }), prism.languages.json, "json");
return socket.send(JSON.stringify({
"example": data.example,
"html": raw(`
<h3>Response</h3>
${explorerNotice}
<pre><code class="language-json">${renderedCode}</code></pre>
<script>$("#temp-loader").hide();</script>
`),
"message": "updated html",
"selector": `#example${data.example}-result`

View file

@ -2,6 +2,46 @@
// P A C K A G E S
const async = require("async");
const color = require("colorette");
const local = require("app-root-path").require;
const octokit = require("@octokit/rest")();
const redis = require("redis");
// V A R I A B L E S
const logSlackError = local("app/helpers/slack");
const relativeDate = local("app/modules/relative-date");
let client;
// R E D I S
if (typeof process.env.GITHUB_OAUTH_TOKEN !== "undefined") {
octokit.authenticate({
type: "oauth",
token: process.env.GITHUB_OAUTH_TOKEN
});
} else process.stdout.write(`${color.red("[missing]")} GitHub token`);
if (typeof process.env.REDISCLOUD_URL !== "undefined") {
client = redis.createClient(process.env.REDISCLOUD_URL);
client.on("error", redisError => {
process.env.NODE_ENV === "development" ?
process.stdout.write(`\n${color.yellow("Unable to connect to Redis client.")}\nYou may be missing an .env file or your connection was reset.`) :
logSlackError(
"\n" +
"> *REDIS ERROR:* ```" + JSON.parse(JSON.stringify(redisError)) + "```" + "\n" +
"> _Cause: Someone is trying to run LBRY.tech locally without environment variables OR Heroku is busted_\n"
)
;
});
} else process.stdout.write(`${color.red("[missing]")} Redis client URL`);
// P R O G R A M
function generateEvent(event) {
@ -84,6 +124,44 @@ function generateEvent(event) {
}
}
function generateGitHubFeed(displayGitHubFeed) {
if (typeof process.env.REDISCLOUD_URL !== "undefined") {
client.zrevrange("events", 0, 9, (err, reply) => {
if (err) return; // TODO: Render a div with nice error message
const events = [];
const renderedEvents = [];
reply.forEach(item => events.push(JSON.parse(item)));
for (const event of events) {
renderedEvents.push(`
<div class='github-feed__event'>
<a href="${generateUrl("actor", event)}" target="_blank" rel="noopener noreferrer">
<img src="${event.actor.avatar_url}" class="github-feed__event__avatar" alt=""/>
</a>
<p>
${generateEvent(event)}
<a href="${generateUrl("repo", event)}" title="View this repo on GitHub" target="_blank" rel="noopener noreferrer"><strong>${event.repo.name}</strong></a>
<em class="github-feed__event__time">${relativeDate(new Date(event.created_at))}</em>
</p>
</div>
`);
}
updateGithubFeed(); // TODO: Update `.last-updated` every minute
displayGitHubFeed(`
<h3>GitHub</h3>
<h5 class="last-updated">Last updated: ${new Date().format("YYYY-MM-DD").replace(/-/g, "&middot;")} at ${new Date().add(-4, "hours").format("UTC:H:mm:ss A").toLowerCase()} EST</h5>
${renderedEvents.join("")}
`);
});
}
}
function generateUrl(type, event) {
switch (type) {
case "actor":
@ -115,6 +193,29 @@ function generateUrl(type, event) {
}
}
function updateGithubFeed() {
octokit.activity.getEventsForOrg({
org: "lbryio",
per_page: 20,
page: 1
}).then(({ data }) => {
async.eachSeries(data, (item, callback) => {
const eventString = JSON.stringify(item);
client.zrank("events", eventString, (err, reply) => {
if (reply === null) client.zadd("events", item.id, eventString, callback);
else callback();
});
}, () => client.zremrangebyrank("events", 0, -51)); // Keep the latest 50 events
}).catch(err => {
logSlackError(
"\n" +
"> *GITHUB FEED ERROR:* ```" + JSON.parse(JSON.stringify(err)) + "```" + "\n" +
"> _Cause: GitHub feed refresh_\n"
);
});
}
// H E L P E R
@ -129,5 +230,7 @@ function refToBranch(ref) {
module.exports = exports = {
generateEvent,
generateUrl
generateGitHubFeed,
generateUrl,
updateGithubFeed
};

View file

@ -1,4 +1,2 @@
// "use strict"; require("make-promises-safe"); const app = require("./server.js"); // eslint-disable-line
"use strict"; require("@babel/register"); require("@babel/polyfill");
module.exports = exports = require("./client.js");

View file

@ -21,6 +21,7 @@
"partials/navigation",
"partials/mission-statement",
"partials/modal",
"partials/pre",
"layout",

View file

@ -137,6 +137,8 @@
.__loading {
width: 100%; height: 10rem;
cursor: wait;
position: relative;
&::before {
@ -149,12 +151,14 @@
border-top-color: $teal;
border-width: 6px;
content: "";
cursor: wait;
position: absolute;
}
&::after {
top: 7rem; left: 0;
cursor: wait;
font-size: 1rem;
position: absolute;
text-align: center;

View file

@ -179,36 +179,7 @@
}
pre {
margin-bottom: 2rem; padding: 2rem;
border-radius: 3px;
font-size: 1rem;
line-height: 1.33;
overflow-x: auto;
overflow-y: hidden;
&:not([class]),
&.language-text {
background-color: #27283e;
color: $white;
}
&.language-yaml {
background-color: #27273f;
color: #ffe066;
.atrule {
color: #f083ac;
}
.important {
color: #ffa94d;
}
.punctuation {
color: $white;
}
}
}
h2, h3, h4, h5 {

View file

@ -147,6 +147,10 @@
margin-bottom: 1rem;
}
pre {
font-size: 0.8rem;
}
table {
border: 1px solid rgba($white, 0.1);
border-radius: 0.3rem;
@ -171,16 +175,6 @@
tr:nth-child(even) {
background-color: rgba($white, 0.1);
}
pre {
margin-bottom: 2rem; padding: 1rem;
border-radius: 0.3rem;
font-size: 0.8rem;
line-height: 1.33;
overflow-x: auto;
overflow-y: hidden;
}
}
.api__content__body {

View file

@ -31,46 +31,47 @@
/**
* Tour | Sidebar
* Tour | Navigation
*
* @class .tour__sidebar
* @class .tour__navigation
*
* @class .tour__sidebar__example
* @class .tour__navigation__example
* @selector {::before}
* @selector {:last-of-type}
* @state {.active}
* @state {:hover}
*/
.tour__sidebar {
width: 250px; height: 100%;
.tour__navigation {
width: 100%;
float: left;
list-style-type: none;
padding-top: 1rem;
padding-right: 1rem;
vertical-align: top;
padding-bottom: 1rem;
padding-top: 1.5rem;
&::after {
@include clearfix;
}
}
.tour__sidebar__example {
.tour__navigation__example {
cursor: pointer;
float: left;
position: relative;
text-align: center;
width: 33.333333%;
&::before {
width: 1rem; height: 1rem;
top: 0.5rem; left: 0;
width: 100%; height: 2.5rem;
top: -0.6rem; left: 0;
border: 1px solid;
border-radius: 50%;
content: attr(data-example);
font-size: 0.8rem;
content: "example " attr(data-example);
font-size: 0.6rem;
font-style: italic;
line-height: 1.1;
position: absolute;
text-align: center;
}
&:not(:last-of-type) {
margin-bottom: 1.5rem;
text-transform: uppercase;
}
&:not(.active) {
@ -93,6 +94,20 @@
}
}
&.completed {
&::after {
width: 100%; height: 100%;
top: 0; left: 0;
background-color: rgba($white, 0.7);
content: "";
font-size: 3rem;
line-height: 0.85;
position: absolute;
z-index: 10;
}
}
&::before,
button,
span {
@ -103,7 +118,6 @@
background-color: transparent;
font-size: 1.25rem;
font-weight: 600;
padding-left: 1.3rem;
}
span {
@ -139,12 +153,10 @@
*/
.tour__content {
width: calc(100% - 250px); height: 100%; min-height: 500px;
border-left: 1px solid rgba($black, 0.05);
float: right;
padding: 1rem 0 1rem 1rem;
vertical-align: top;
border-top: 1px solid rgba($black, 0.05);
overflow-y: visible;
padding-bottom: 1rem;
padding-top: 1rem;
.loader {
@extend .__loading;
@ -153,6 +165,15 @@
content: "Processing request";
}
}
h3 {
font-size: 1.5rem;
margin-bottom: 1rem;
}
pre {
font-size: 1rem;
}
}
.tour__content__meme {
@ -167,6 +188,7 @@
.tour__content__meme__canvas {
float: left;
margin-right: 2%;
position: relative;
width: 48%;
canvas {
@ -202,7 +224,7 @@
.tour__content__meme__editor {
float: right;
width: 48%;
width: 50%;
h2.__metadata {
margin-top: 3rem;
@ -315,10 +337,11 @@
}
.tour__content__trends {
min-width: 0; min-height: 0;
display: grid;
grid-gap: 2%;
grid-template-columns: 32% 32% 32%;
overflow-y: auto;
grid-gap: 1rem;
grid-template: repeat(1, 1fr) / repeat(3, 1fr);
position: relative;
&:empty {
@ -332,7 +355,7 @@
.tour__content__trend {
img {
width: 100%; height: 175px;
width: 100%; height: 213px;
cursor: pointer;
display: block;
@ -428,3 +451,32 @@
width: 3.5rem;
}
}
/**
* Tour | Description
*
* @class .tour__description
*/
.tour__description {
cursor: default;
font-size: 1rem;
line-height: 1.33;
padding: 1rem;
&:not(.success) {
background-color: rgba($black, 0.05);
text-align: center;
}
&.success {
background-color: rgba($teal, 0.05);
strong {
display: block;
text-transform: uppercase;
}
}
}

View file

@ -50,7 +50,6 @@
letter-spacing: 0.1rem;
line-height: 1;
text-transform: uppercase;
width: 100%;
@media (min-width: 1301px) {
top: 2.15rem; left: 0;
@ -58,6 +57,7 @@
color: rgba($black, 0.045);
font-size: 4rem;
position: absolute;
width: calc(100% - (1rem + 5%));
}
@media (max-width: 1300px) {

View file

@ -0,0 +1,30 @@
pre {
margin-bottom: 2rem; padding: 2rem;
line-height: 1.33;
overflow-x: auto;
overflow-y: hidden;
&:not([class]),
&.language-text {
background-color: #27283e;
color: $white;
}
&.language-yaml {
background-color: #27273f;
color: #ffe066;
.atrule {
color: #f083ac;
}
.important {
color: #ffa94d;
}
.punctuation {
color: $white;
}
}
}

400
app/sockets.js Normal file
View file

@ -0,0 +1,400 @@
"use strict";
// P A C K A G E S
const html = require("choo/html");
const local = require("app-root-path").require;
const request = require("request-promise-native");
// V A R I A B L E S
const fetchMetadata = local("app/helpers/fetch-metadata");
const { generateGitHubFeed } = local("app/helpers/github");
const logSlackError = local("app/helpers/slack");
// P R O G R A M
module.exports = exports = (socket, action) => {
if (typeof socket !== "object" && typeof action !== "object") return;
switch(true) {
case (action.message === "fetch metadata"):
fetchMetadata(action, socket);
break;
case (action.message === "landed on homepage"):
generateGitHubFeed(result => {
socket.send(JSON.stringify({
"html": result,
"message": "updated html",
"selector": "#github-feed"
}));
});
break;
case (action.message === "landed on tour"):
generateContent(1, result => {
socket.send(JSON.stringify({
// "example": 1,
"html": result,
"message": "updated html",
"selector": "#tour-loader"
}));
});
break;
case (action.message === "request for tour, example 1"):
generateContent(1, result => {
socket.send(JSON.stringify({
// "example": 1,
"html": result,
"message": "updated html",
"selector": "#tour-loader"
}));
});
break;
case (action.message === "request for tour, example 2"):
generateMemeCreator(socket);
break;
case (action.message === "request for tour, example 3"):
generateContent(3, result => {
socket.send(JSON.stringify({
// "example": 3,
"html": result,
"message": "updated html",
"selector": "#tour-loader"
}));
});
break;
case (action.message === "subscribe"):
newsletterSubscribe(action, socket);
break;
default:
console.log(action); // eslint-disable-line
break;
}
};
// H E L P E R S
function generateContent(exampleNumber, displayTrendingContent) {
if (exampleNumber === 1) {
return getTrendingContent().then(response => {
if (!response || !response.success || response.success !== true || !response.data) return "";
const rawContentCollection = [];
const renderedContentCollection = [];
const trendingContentData = response.data;
for (const data of trendingContentData) {
rawContentCollection.push(fetchMetadata({ claim: data.url, method: "resolve", example: exampleNumber }));
}
Promise.all(rawContentCollection).then(collection => {
for (const part of collection) {
try {
renderedContentCollection.push(`
<figure class="tour__content__trend">
<img alt="${part.name}" data-action="choose claim" data-claim-id="${part.name}" src="${part.value.stream.metadata.thumbnail}"/>
<figcaption data-action="choose claim" data-claim-id="${part.name}">
${part.value.stream.metadata.title}
<span>${part.channel_name}</span>
</figcaption>
</figure>
`);
} catch (err) {
return; // TODO: Return nice error message
}
}
renderedContentCollection.push(`
<script>
document.getElementById("tour-example-description").innerHTML = document.querySelector("[data-action='tour, example 1']").dataset.description
</script>
`);
displayTrendingContent(renderedContentCollection.join(""));
});
});
}
if (exampleNumber === 3) {
const approvedUrls = [
"LBRY#3db81c073f82fd1bb670c65f526faea3b8546720",
"correlation-can-imply-causation#173412f5b1b7aa63a752e8832406aafd9f1ecb4e",
"thanos-is-the-protagonist-how-infinity#2a7f5db2678177435b1dee6c9e38e035ead450b6",
"epic-arcade-mode-duos-nickatnyte-molt#d81bac6d49b1f92e58c37a5f633a27a45b43405e",
"political-correctness-a-force-for-good-a#b4668c0bd096317b44c40738c099b6618095e75f",
"10-secrets-hidden-inside-famous-logos#007789cc45cbb4255cf02ba77cbf84ca8e3d7561",
"ever-wonder-how-bitcoin-and-other#1ac47b8b3def40a25850dc726a09ce23d09e7009",
"bankrupt-pan-am#784b3c215a6f06b663fc1aa292bcb19f29c489bb",
"minecraft-in-real-life-iron-man#758dd6497cdfc401ae1f25984738d024d47b50af",
"ethan-shows-kyle-warframe-skyvault#8a7401b88d5ed0376d98f16808194d4dcb05b284"
];
const rawContentCollection = [];
const renderedContentCollection = [];
for (const url of approvedUrls) {
rawContentCollection.push(fetchMetadata({ claim: url, method: "resolve", example: exampleNumber }));
}
return Promise.all(rawContentCollection).then(collection => {
for (const part of collection) {
if (
part &&
part.value &&
part.value.stream.metadata.thumbnail &&
part.channel_name
) {
renderedContentCollection.push(`
<figure class="tour__content__trend">
<img alt="${part.name}" data-action="choose claim" data-claim-id="${part.claim_id}" src="${part.value.stream.metadata.thumbnail}"/>
<figcaption data-action="choose claim" data-claim-id="${part.claim_id}">
${part.value.stream.metadata.title}
<span>${part.channel_name}</span>
</figcaption>
</figure>
`);
}
}
renderedContentCollection.push(`
<script>
document.getElementById("tour-example-description").innerHTML = document.querySelector("[data-action='tour, example 3']").dataset.description
</script>
`);
displayTrendingContent(renderedContentCollection.join(""));
});
}
}
function generateMemeCreator(socket) {
const images = [
{
alt: "Carl Sagan",
src: "/assets/media/images/carlsagan2.jpg"
},
{
alt: "Doge",
src: "/assets/media/images/doge-meme.jpg"
},
{
alt: "LBRY Logo With Green Background",
src: "/assets/media/images/lbry-green.png"
}
];
const memePlaceholderData = {
bottomLine: {
placeholder: "Top line",
value: "that I made"
},
description: {
placeholder: "Description",
value: "Check out this image I published to LBRY via lbry.tech"
},
topLine: {
placeholder: "Top line",
value: "This is an example meme"
},
title: {
placeholder: "Title",
value: "Dank Meme Supreme da Cheese"
}
};
const renderedImages = [];
for (const image of images) {
renderedImages.push(`<img alt="${image.alt}" class="tour__content__meme__canvas__thumbnail" src="${image.src}"/>`);
}
const memeCreator = html`
<div class="tour__content__meme__canvas">
<img alt="Base image for LBRY meme creator" id="base-image" style="height: 0; position: absolute; visibility: hidden;"/>
<canvas id="meme-canvas" height="300" width="400">Unfortunately, it looks like canvas is <strong>not supported</strong> in your browser</canvas>
${renderedImages}
</div>
<form class="tour__content__meme__editor">
<h2>Image Text</h2>
<fieldset>
<label for="meme-top-line">Top line</label>
<input id="meme-top-line" name="meme-top-line" placeholder="${memePlaceholderData.topLine.placeholder}" spellcheck="false" type="text" value="${memePlaceholderData.topLine.value}" required/>
</fieldset>
<fieldset>
<label for="meme-bottom-line">Bottom line</label>
<input id="meme-bottom-line" name="meme-bottom-line" placeholder="${memePlaceholderData.bottomLine.placeholder}" spellcheck="false" type="text" value="${memePlaceholderData.bottomLine.value}" required/>
</fieldset>
<h2 class="__metadata">Metadata</h2>
<fieldset>
<label for="meme-title">Title</label>
<input id="meme-title" name="meme-title" placeholder="${memePlaceholderData.title.placeholder}" spellcheck="false" type="text" value="${memePlaceholderData.title.value}" required/>
</fieldset>
<fieldset>
<label for="meme-description">Description</label>
<textarea id="meme-description" name="meme-description" placeholder="${memePlaceholderData.description.placeholder}" spellcheck="false" type="text" required>${memePlaceholderData.description.value}</textarea>
</fieldset>
<fieldset>
<label for="meme-language">Language</label>
<select id="meme-language" name="meme-language">
<option value="ar">Arabic</option>
<option value="zh">Chinese (Mandarin)</option>
<option value="en">English</option>
<option value="fr">French</option>
<option value="de">German</option>
<option value="it">Italian</option>
<option value="jp">Japanese</option>
<option value="ru">Russian</option>
<option value="es">Spanish</option>
<option value="">Not specified</option>
</select>
</fieldset>
<fieldset>
<label for="meme-license">License</label>
<select id="meme-license" name="meme-license" required>
<option value="Public Domain">Public Domain</option>
<option value="Creative Commons Attribution 4.0 International">Creative Commons Attribution 4.0 International</option>
<option value="Creative Commons Attribution-ShareAlike 4.0 International">Creative Commons Attribution-ShareAlike 4.0 International</option>
<option value="Creative Commons Attribution-NoDerivatives 4.0 International">Creative Commons Attribution-NoDerivatives 4.0 International</option>
<option value="Creative Commons Attribution-NonCommercial 4.0 International">Creative Commons Attribution-NonCommercial 4.0 International</option>
<option value="Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International">Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International</option>
<option value="Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International">Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International</option>
<option value="None">None</option>
</select>
</fieldset>
<fieldset>
<label><input id="meme-nsfw-flag" name="nsfw" type="checkbox"/>NSFW</label>
</fieldset>
<fieldset>
<button data-action="upload image" class="__button-black" type="button">Submit</button>
</fieldset>
</form>
<script>
detectLanguageAndUpdate();
initCanvas();
document.getElementById("tour-example-description").innerHTML = document.querySelector("[data-action='tour, example 2']").dataset.description;
setTimeout(() => {
document.querySelector(".tour__content__meme__canvas__thumbnail").click();
}, 100);
</script>
`;
return socket.send(JSON.stringify({
// "example": 2,
"html": memeCreator,
"message": "updated html",
"selector": "#tour-loader"
}));
}
function getTrendingContent() {
return new Promise((resolve, reject) => { // eslint-disable-line
request({
method: "GET",
url: "https://api.lbry.io/file/list_trending"
}, (error, response, body) => {
if (error || !JSON.parse(body)) resolve("Issue fetching content"); // error
body = JSON.parse(body);
resolve(body);
});
});
}
function newsletterSubscribe(data, socket) {
const email = data.email;
if (!validateEmail(email)) return socket.send(JSON.stringify({
"html": "Your email is invalid",
"message": "updated html",
"selector": "#emailMessage"
}));
return new Promise((resolve, reject) => {
request({
method: "POST",
url: `https://api.lbry.io/list/subscribe?email=${email}&tag=developer`
}).then(body => {
if (!body || !JSON.parse(body)) {
logSlackError(
"\n" +
"> *NEWSLETTER ERROR:* ```¯\\_(ツ)_/¯ This should be an unreachable error```" + "\n" +
`> _Cause: ${email} interacted with the form_\n`
);
return resolve(socket.send(JSON.stringify({
"html": "Something is terribly wrong",
"message": "updated html",
"selector": "#emailMessage"
})));
}
body = JSON.parse(body);
if (!body.success) {
logSlackError(
"\n" +
"> *NEWSLETTER ERROR:* ```" + JSON.parse(JSON.stringify(body.error)) + "```" + "\n" +
`> _Cause: ${email} interacted with the form_\n`
);
return reject(socket.send(JSON.stringify({
"html": body.error,
"message": "updated html",
"selector": "#emailMessage"
})));
}
return resolve(socket.send(JSON.stringify({
"html": "Thank you! Please confirm subscription in your inbox.",
"message": "updated html",
"selector": "#emailMessage"
})));
}).catch(welp => {
if (welp.statusCode === 409) {
logSlackError(
"\n" +
"> *NEWSLETTER ERROR:* ```" + JSON.parse(JSON.stringify(welp.error)) + "```" + "\n" +
`> _Cause: ${email} interacted with the form_\n`
);
return resolve(socket.send(JSON.stringify({
"html": "You have already subscribed!",
"message": "updated html",
"selector": "#emailMessage"
})));
}
});
});
}
function validateEmail(email) {
const emailRegex = /^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\\.,;:\s@"]{2,})$/i;
return emailRegex.test(String(email));
}

View file

@ -17,8 +17,8 @@ import linkGrid from "../components/link-grid";
const featureLinks = linkGrid([
{
description: "Learn how LBRY works with 3 easy examples",
destination: "/tour",
label: "Take the Tour",
destination: "/playground",
label: "Jump into Playground",
title: "New to LBRY?"
},
{

View file

@ -85,7 +85,7 @@ module.exports = exports = (state, emit) => { // eslint-disable-line
let pageScript = "";
if (path === "glossary") pageScript = "<script>" + fs.readFileSync("./app/components/client/glossary-scripts.js", "utf-8") + "</script>";
if (path === "overview") pageScript = "<script>" + fs.readFileSync("./app/components/client/ecosystem-scripts.js", "utf-8") + "</script>";
if (path === "tour") pageScript = "<script>" + fs.readFileSync("./app/components/client/tour-scripts.js", "utf-8") + "</script>";
if (path === "playground") pageScript = "<script>" + fs.readFileSync("./app/components/client/tour-scripts.js", "utf-8") + "</script>";
return html`
<article class="page" itemtype="http://schema.org/BlogPosting">

9
documents/playground.md Normal file
View file

@ -0,0 +1,9 @@
---
title: Playground
---
Check out any of the interactive examples to get a feel for the LBRY protocol!
LBRY (pronounced "library") is an application layer protocol, similar to HTTP. However, while HTTP links can direct you to decentralized content, the LBRY protocol *itself* is decentralized.
<Playground/>

479
server.js
View file

@ -4,10 +4,9 @@
// P A C K A G E S
const async = require("async");
const color = require("colorette");
const cors = require("cors");
const dedent = require("dedent");
const local = require("app-root-path").require;
const fastify = require("fastify")({
logger: {
@ -16,42 +15,10 @@ const fastify = require("fastify")({
}
});
const html = require("choo/html");
const local = require("app-root-path").require;
const octokit = require("@octokit/rest")();
const redis = require("redis");
const request = require("request-promise-native");
// V A R I A B L E S
const fetchMetadata = local("app/helpers/fetch-metadata");
const github = local("app/helpers/github");
const log = console.log; // eslint-disable-line
const handleSocketMessages = local("app/sockets");
const logSlackError = local("app/helpers/slack");
const relativeDate = local("app/modules/relative-date");
let client;
if (typeof process.env.GITHUB_OAUTH_TOKEN !== "undefined") {
octokit.authenticate({
type: "oauth",
token: process.env.GITHUB_OAUTH_TOKEN
});
} else log(`${color.red("[missing]")} GitHub token`);
if (typeof process.env.REDISCLOUD_URL !== "undefined") {
client = redis.createClient(process.env.REDISCLOUD_URL);
client.on("error", redisError => {
process.env.NODE_ENV === "development" ?
log(`\n${color.yellow("Unable to connect to Redis client.")}\nYou may be missing an .env file or your connection was reset.`) :
logSlackError(
"\n" +
"> *REDIS ERROR:* ```" + JSON.parse(JSON.stringify(redisError)) + "```" + "\n" +
"> _Cause: Someone is trying to run LBRY.tech locally without environment variables OR Heroku is busted_\n"
)
;
});
} else log(`${color.red("[missing]")} Redis client URL`);
@ -81,74 +48,10 @@ fastify.ready(err => {
fastify.ws.on("connection", socket => {
socket.on("message", data => {
data = JSON.parse(data);
switch(data.message) {
case "fetch metadata":
fetchMetadata(data, socket);
break;
case "landed on homepage":
generateGitHubFeed(result => {
socket.send(JSON.stringify({
"html": result,
"message": "updated html",
"selector": "#github-feed"
}));
});
break;
case "landed on tour":
generateContent(1, result => {
socket.send(JSON.stringify({
"html": result,
"message": "updated html",
"selector": "#tour-loader"
}));
});
break;
case "request for tour, example 1":
generateContent(1, result => {
socket.send(JSON.stringify({
"html": result,
"message": "updated html",
"selector": "#tour-loader"
}));
});
break;
case "request for tour, example 2":
generateMemeCreator(socket);
break;
case "request for tour, example 3":
generateContent(3, result => {
socket.send(JSON.stringify({
"html": result,
"message": "updated html",
"selector": "#tour-loader"
}));
});
break;
case "subscribe":
newsletterSubscribe(data, socket);
break;
default:
log(data);
break;
}
return handleSocketMessages(socket, data);
});
socket.on("close", () => {
// console.log(socket);
return socket.terminate();
});
socket.on("close", () => socket.terminate());
});
});
@ -159,387 +62,15 @@ fastify.ready(err => {
const start = async () => {
try {
await fastify.listen(process.env.PORT || 8080, process.env.IP || "0.0.0.0");
/*
await fastify.listen(
process.env.NODE_ENV === "development" ?
8080 :
process.env.PORT
);
*/
} catch (err) {
fastify.log.error(err);
process.exit(1);
}
process.env.NODE_ENV === "development" ?
log(`\n${color.green("⚡")} ${fastify.server.address().port}\n`) :
process.stdout.write(`\n${color.green("⚡")} ${fastify.server.address().port}\n`) :
logSlackError(`Server started at port \`${fastify.server.address().port}\``)
;
};
start();
// H E L P E R S
function generateGitHubFeed(displayGitHubFeed) {
if (typeof process.env.REDISCLOUD_URL !== "undefined") {
client.zrevrange("events", 0, 9, (err, reply) => {
if (err) return; // TODO: Render a div with nice error message
const events = [];
const renderedEvents = [];
reply.forEach(item => events.push(JSON.parse(item)));
for (const event of events) {
renderedEvents.push(`
<div class='github-feed__event'>
<a href="${github.generateUrl("actor", event)}" target="_blank" rel="noopener noreferrer">
<img src="${event.actor.avatar_url}" class="github-feed__event__avatar" alt=""/>
</a>
<p>
${github.generateEvent(event)}
<a href="${github.generateUrl("repo", event)}" title="View this repo on GitHub" target="_blank" rel="noopener noreferrer"><strong>${event.repo.name}</strong></a>
<em class="github-feed__event__time">${relativeDate(new Date(event.created_at))}</em>
</p>
</div>
`);
}
updateGithubFeed(); // TODO: Update `.last-updated` every minute
displayGitHubFeed(dedent`
<h3>GitHub</h3>
<h5 class="last-updated">Last updated: ${new Date().format("YYYY-MM-DD").replace(/-/g, "&middot;")} at ${new Date().add(-4, "hours").format("UTC:H:mm:ss A").toLowerCase()} EST</h5>
${renderedEvents.join("")}
`);
});
}
}
function generateMemeCreator(socket) {
const images = [
{
alt: "Carl Sagan",
src: "/assets/media/images/carlsagan2.jpg"
},
{
alt: "Doge",
src: "/assets/media/images/doge-meme.jpg"
},
{
alt: "LBRY Logo With Green Background",
src: "/assets/media/images/lbry-green.png"
}
];
const memePlaceholderData = {
bottomLine: {
placeholder: "Top line",
value: "that I made"
},
description: {
placeholder: "Description",
value: "Check out this image I published to LBRY via lbry.tech"
},
topLine: {
placeholder: "Top line",
value: "This is an example meme"
},
title: {
placeholder: "Title",
value: "Dank Meme Supreme da Cheese"
}
};
const renderedImages = [];
for (const image of images) {
renderedImages.push(`<img alt="${image.alt}" class="tour__content__meme__canvas__thumbnail" src="${image.src}"/>`);
}
const memeCreator = html`
<div class="tour__content__meme__canvas">
<img alt="Base image for LBRY meme creator" id="base-image" style="height: 0; visibility: hidden;"/>
<canvas id="meme-canvas" height="300" width="400">Unfortunately, it looks like canvas is <strong>not supported</strong> in your browser</canvas>
${renderedImages}
</div>
<form class="tour__content__meme__editor">
<h2>Image Text</h2>
<fieldset>
<label for="meme-top-line">Top line</label>
<input id="meme-top-line" name="meme-top-line" placeholder="${memePlaceholderData.topLine.placeholder}" spellcheck="false" type="text" value="${memePlaceholderData.topLine.value}" required/>
</fieldset>
<fieldset>
<label for="meme-bottom-line">Bottom line</label>
<input id="meme-bottom-line" name="meme-bottom-line" placeholder="${memePlaceholderData.bottomLine.placeholder}" spellcheck="false" type="text" value="${memePlaceholderData.bottomLine.value}" required/>
</fieldset>
<h2 class="__metadata">Metadata</h2>
<fieldset>
<label for="meme-title">Title</label>
<input id="meme-title" name="meme-title" placeholder="${memePlaceholderData.title.placeholder}" spellcheck="false" type="text" value="${memePlaceholderData.title.value}" required/>
</fieldset>
<fieldset>
<label for="meme-description">Description</label>
<textarea id="meme-description" name="meme-description" placeholder="${memePlaceholderData.description.placeholder}" spellcheck="false" type="text" required>${memePlaceholderData.description.value}</textarea>
</fieldset>
<fieldset>
<label for="meme-language">Language</label>
<select id="meme-language" name="meme-language">
<option value="ar">Arabic</option>
<option value="zh">Chinese (Mandarin)</option>
<option value="en">English</option>
<option value="fr">French</option>
<option value="de">German</option>
<option value="it">Italian</option>
<option value="jp">Japanese</option>
<option value="ru">Russian</option>
<option value="es">Spanish</option>
<option value="">Not specified</option>
</select>
</fieldset>
<fieldset>
<label for="meme-license">License</label>
<select id="meme-license" name="meme-license" required>
<option value="Public Domain">Public Domain</option>
<option value="Creative Commons Attribution 4.0 International">Creative Commons Attribution 4.0 International</option>
<option value="Creative Commons Attribution-ShareAlike 4.0 International">Creative Commons Attribution-ShareAlike 4.0 International</option>
<option value="Creative Commons Attribution-NoDerivatives 4.0 International">Creative Commons Attribution-NoDerivatives 4.0 International</option>
<option value="Creative Commons Attribution-NonCommercial 4.0 International">Creative Commons Attribution-NonCommercial 4.0 International</option>
<option value="Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International">Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International</option>
<option value="Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International">Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International</option>
<option value="None">None</option>
</select>
</fieldset>
<fieldset>
<label><input id="meme-nsfw-flag" name="nsfw" type="checkbox"/>NSFW</label>
</fieldset>
<fieldset>
<button data-action="upload image" class="__button-black" type="button">Submit</button>
</fieldset>
</form>
<script>
detectLanguageAndUpdate();
initCanvas();
setTimeout(() => {
$(".tour__content__meme__canvas__thumbnail").click();
}, 100);
</script>
`;
return socket.send(JSON.stringify({
"html": memeCreator,
"message": "updated html",
"selector": "#tour-loader"
}));
}
function generateContent(exampleNumber, displayTrendingContent) {
if (exampleNumber === 1) {
return getTrendingContent().then(response => {
if (!response || !response.success || response.success !== true || !response.data) return "";
const rawContentCollection = [];
const renderedContentCollection = [];
const trendingContentData = response.data;
for (const data of trendingContentData) {
rawContentCollection.push(fetchMetadata({ claim: data.url, method: "resolve", example: exampleNumber }));
}
Promise.all(rawContentCollection).then(collection => {
for (const part of collection) {
if (
!part.value.stream.metadata.nsfw &&
part.value.stream.metadata.thumbnail &&
part.channel_name
) {
renderedContentCollection.push(`
<figure class="tour__content__trend">
<img alt="${part.name}" data-action="choose claim" data-claim-id="${exampleNumber === 1 ? part.name : part.claim_id}" src="${part.value.stream.metadata.thumbnail}"/>
<figcaption data-action="choose claim" data-claim-id="${exampleNumber === 1 ? part.name : part.claim_id}">
${part.value.stream.metadata.title}
<span>${part.channel_name}</span>
</figcaption>
</figure>
`);
}
}
displayTrendingContent(renderedContentCollection.join(""));
});
});
}
if (exampleNumber === 3) {
const approvedUrls = [
"LBRY#3db81c073f82fd1bb670c65f526faea3b8546720",
"correlation-can-imply-causation#173412f5b1b7aa63a752e8832406aafd9f1ecb4e",
"thanos-is-the-protagonist-how-infinity#2a7f5db2678177435b1dee6c9e38e035ead450b6nyte",
"epic-arcade-mode-duos-nickatnyte-molt#d81bac6d49b1f92e58c37a5f633a27a45b43405e",
"political-correctness-a-force-for-good-a#b4668c0bd096317b44c40738c099b6618095e75f",
"10-secrets-hidden-inside-famous-logos#007789cc45cbb4255cf02ba77cbf84ca8e3d7561",
"ever-wonder-how-bitcoin-and-other#1ac47b8b3def40a25850dc726a09ce23d09e7009",
"bankrupt-pan-am#784b3c215a6f06b663fc1aa292bcb19f29c489bb",
"minecraft-in-real-life-iron-man#758dd6497cdfc401ae1f25984738d024d47b50af",
"ethan-shows-kyle-warframe-skyvault#8a7401b88d5ed0376d98f16808194d4dcb05b284"
];
const rawContentCollection = [];
const renderedContentCollection = [];
for (const url of approvedUrls) {
rawContentCollection.push(fetchMetadata({ claim: url, method: "resolve", example: exampleNumber }));
}
Promise.all(rawContentCollection).then(collection => {
for (const part of collection) {
if (
part &&
part.value &&
part.value.stream.metadata.thumbnail &&
part.channel_name
) {
renderedContentCollection.push(`
<figure class="tour__content__trend">
<img alt="${part.name}" data-action="choose claim" data-claim-id="${exampleNumber === 1 ? part.name : part.claim_id}" src="${part.value.stream.metadata.thumbnail}"/>
<figcaption data-action="choose claim" data-claim-id="${exampleNumber === 1 ? part.name : part.claim_id}">
${part.value.stream.metadata.title}
<span>${part.channel_name}</span>
</figcaption>
</figure>
`);
}
}
displayTrendingContent(renderedContentCollection.join(""));
});
}
}
function getTrendingContent() {
return new Promise((resolve, reject) => { // eslint-disable-line
request({
method: "GET",
url: "https://api.lbry.io/file/list_trending"
}, (error, response, body) => {
if (error || !JSON.parse(body)) resolve("Issue fetching content"); // error
body = JSON.parse(body);
resolve(body);
});
});
}
function newsletterSubscribe(data, socket) {
const email = data.email;
if (!validateEmail(email)) return socket.send(JSON.stringify({
"html": "Your email is invalid",
"message": "updated html",
"selector": "#emailMessage"
}));
return new Promise((resolve, reject) => {
request({
method: "POST",
url: `https://api.lbry.io/list/subscribe?email=${email}&tag=developer`
}).then(body => {
if (!body || !JSON.parse(body)) {
logSlackError(
"\n" +
"> *NEWSLETTER ERROR:* ```¯\\_(ツ)_/¯ This should be an unreachable error```" + "\n" +
`> _Cause: ${email} interacted with the form_\n`
);
return resolve(socket.send(JSON.stringify({
"html": "Something is terribly wrong",
"message": "updated html",
"selector": "#emailMessage"
})));
}
body = JSON.parse(body);
if (!body.success) {
logSlackError(
"\n" +
"> *NEWSLETTER ERROR:* ```" + JSON.parse(JSON.stringify(body.error)) + "```" + "\n" +
`> _Cause: ${email} interacted with the form_\n`
);
return reject(socket.send(JSON.stringify({
"html": body.error,
"message": "updated html",
"selector": "#emailMessage"
})));
}
return resolve(socket.send(JSON.stringify({
"html": "Thank you! Please confirm subscription in your inbox.",
"message": "updated html",
"selector": "#emailMessage"
})));
}).catch(welp => {
if (welp.statusCode === 409) {
logSlackError(
"\n" +
"> *NEWSLETTER ERROR:* ```" + JSON.parse(JSON.stringify(welp.error)) + "```" + "\n" +
`> _Cause: ${email} interacted with the form_\n`
);
return resolve(socket.send(JSON.stringify({
"html": "You have already subscribed!",
"message": "updated html",
"selector": "#emailMessage"
})));
}
});
});
}
function updateGithubFeed() {
octokit.activity.getEventsForOrg({
org: "lbryio",
per_page: 20,
page: 1
}).then(({ data }) => {
async.eachSeries(data, (item, callback) => {
const eventString = JSON.stringify(item);
client.zrank("events", eventString, (err, reply) => {
if (reply === null) client.zadd("events", item.id, eventString, callback);
else callback();
});
}, () => client.zremrangebyrank("events", 0, -51)); // Keep the latest 50 events
}).catch(err => {
logSlackError(
"\n" +
"> *GITHUB FEED ERROR:* ```" + JSON.parse(JSON.stringify(err)) + "```" + "\n" +
"> _Cause: GitHub feed refresh_\n"
);
});
}
function validateEmail(email) {
const re = /^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\\.,;:\s@"]{2,})$/i;
return re.test(String(email));
}