lbry-desktop/ui/js/lbry.js
Alex Liebowitz 93b1ab8ac6 Add basics of Instant Purchase setting
Fix and simplify state management of Instant Purchas setting

Add Instant Purchase check to Watch page

Merge Max Purchase Price and Instant Purchase into one section

Wording still not finalized.

Add Instant Purchase setting names to constants

Support USD for Instant Purchase

On Settings page, use constants for new Instant Purchase settings

Convert Instant Purchase Maximum setting into FormRow

Update wording of Instant Purchase option and add helper text.

Wording still not final.

On Settings page, get Instant Purchase settings via selector

Update CHANGELOG.md
2017-09-22 18:34:16 -04:00

471 lines
12 KiB
JavaScript

import lighthouse from "./lighthouse.js";
import jsonrpc from "./jsonrpc.js";
import lbryuri from "./lbryuri.js";
/**
* The 4 get/set functions below used to be in a utils.js library when used more widely.
* They've been reduced to just this file and probably ought to be eliminated entirely.
*/
function getLocal(key, fallback = undefined) {
const itemRaw = localStorage.getItem(key);
return itemRaw === null ? fallback : JSON.parse(itemRaw);
}
function setLocal(key, value) {
localStorage.setItem(key, JSON.stringify(value));
}
function getSession(key, fallback = undefined) {
const itemRaw = sessionStorage.getItem(key);
return itemRaw === null ? fallback : JSON.parse(itemRaw);
}
function setSession(key, value) {
sessionStorage.setItem(key, JSON.stringify(value));
}
const { remote, ipcRenderer } = require("electron");
const menu = remote.require("./menu/main-menu");
let lbry = {
isConnected: false,
daemonConnectionString: "http://localhost:5279",
pendingPublishTimeout: 20 * 60 * 1000,
defaultClientSettings: {
showNsfw: false,
showUnavailable: true,
debug: false,
useCustomLighthouseServers: false,
customLighthouseServers: [],
showDeveloperMenu: false,
language: "en",
theme: "light",
themes: [],
instantPurchaseMax: null,
instantPurchaseEnabled: false,
instantPurchaseMax: { currency: "LBC", amount: 0.1 },
},
};
function apiCall(method, params, resolve, reject) {
return jsonrpc.call(
lbry.daemonConnectionString,
method,
params,
resolve,
reject,
reject
);
}
/**
* Records a publish attempt in local storage. Returns a dictionary with all the data needed to
* needed to make a dummy claim or file info object.
*/
let pendingId = 0;
function savePendingPublish({ name, channel_name }) {
let uri;
if (channel_name) {
uri = lbryuri.build({ name: channel_name, path: name }, false);
} else {
uri = lbryuri.build({ name: name }, false);
}
++pendingId;
const pendingPublishes = getLocal("pendingPublishes") || [];
const newPendingPublish = {
name,
channel_name,
claim_id: "pending-" + pendingId,
txid: "pending-" + pendingId,
nout: 0,
outpoint: "pending-" + pendingId + ":0",
time: Date.now(),
};
setLocal("pendingPublishes", [...pendingPublishes, newPendingPublish]);
return newPendingPublish;
}
/**
* If there is a pending publish with the given name or outpoint, remove it.
* A channel name may also be provided along with name.
*/
function removePendingPublishIfNeeded({ name, channel_name, outpoint }) {
function pubMatches(pub) {
return (
pub.outpoint === outpoint ||
(pub.name === name &&
(!channel_name || pub.channel_name === channel_name))
);
}
setLocal(
"pendingPublishes",
lbry.getPendingPublishes().filter(pub => !pubMatches(pub))
);
}
/**
* Gets the current list of pending publish attempts. Filters out any that have timed out and
* removes them from the list.
*/
lbry.getPendingPublishes = function() {
const pendingPublishes = getLocal("pendingPublishes") || [];
const newPendingPublishes = pendingPublishes.filter(
pub => Date.now() - pub.time <= lbry.pendingPublishTimeout
);
setLocal("pendingPublishes", newPendingPublishes);
return newPendingPublishes;
};
/**
* Gets a pending publish attempt by its name or (fake) outpoint. A channel name can also be
* provided along withe the name. If no pending publish is found, returns null.
*/
function getPendingPublish({ name, channel_name, outpoint }) {
const pendingPublishes = lbry.getPendingPublishes();
return (
pendingPublishes.find(
pub =>
pub.outpoint === outpoint ||
(pub.name === name &&
(!channel_name || pub.channel_name === channel_name))
) || null
);
}
function pendingPublishToDummyClaim({
channel_name,
name,
outpoint,
claim_id,
txid,
nout,
}) {
return { name, outpoint, claim_id, txid, nout, channel_name };
}
function pendingPublishToDummyFileInfo({ name, outpoint, claim_id }) {
return { name, outpoint, claim_id, metadata: null };
}
//core
lbry._connectPromise = null;
lbry.connect = function() {
if (lbry._connectPromise === null) {
lbry._connectPromise = new Promise((resolve, reject) => {
let tryNum = 0;
function checkDaemonStartedFailed() {
if (tryNum <= 200) {
// Move # of tries into constant or config option
setTimeout(() => {
tryNum++;
checkDaemonStarted();
}, tryNum < 50 ? 400 : 1000);
} else {
reject(new Error("Unable to connect to LBRY"));
}
}
// Check every half second to see if the daemon is accepting connections
function checkDaemonStarted() {
lbry.status().then(resolve).catch(checkDaemonStartedFailed);
}
checkDaemonStarted();
});
}
return lbry._connectPromise;
};
/**
* Publishes a file. The optional fileListedCallback is called when the file becomes available in
* lbry.file_list() during the publish process.
*
* This currently includes a work-around to cache the file in local storage so that the pending
* publish can appear in the UI immediately.
*/
lbry.publishDeprecated = function(
params,
fileListedCallback,
publishedCallback,
errorCallback
) {
lbry.publish(params).then(
result => {
if (returnPendingTimeout) clearTimeout(returnPendingTimeout);
publishedCallback(result);
},
err => {
if (returnPendingTimeout) clearTimeout(returnPendingTimeout);
errorCallback(err);
}
);
// Give a short grace period in case publish() returns right away or (more likely) gives an error
const returnPendingTimeout = setTimeout(
() => {
if (publishedCallback) {
savePendingPublish({
name: params.name,
channel_name: params.channel_name,
});
publishedCallback(true);
}
if (fileListedCallback) {
const { name, channel_name } = params;
savePendingPublish({
name: params.name,
channel_name: params.channel_name,
});
fileListedCallback(true);
}
},
2000,
{ once: true }
);
};
lbry.getClientSetting = function(setting) {
var localStorageVal = localStorage.getItem("setting_" + setting);
if (setting == "showDeveloperMenu") {
return true;
}
return localStorageVal === null
? lbry.defaultClientSettings[setting]
: JSON.parse(localStorageVal);
};
lbry.setClientSetting = function(setting, value) {
return localStorage.setItem("setting_" + setting, JSON.stringify(value));
};
lbry.formatName = function(name) {
// Converts LBRY name to standard format (all lower case, no special characters, spaces replaced by dashes)
name = name.replace("/s+/g", "-");
name = name.toLowerCase().replace(lbryuri.REGEXP_INVALID_URI, "");
return name;
};
lbry.imagePath = function(file) {
return "img/" + file;
};
lbry.getMediaType = function(contentType, fileName) {
if (contentType) {
return /^[^/]+/.exec(contentType)[0];
} else if (fileName) {
var dotIndex = fileName.lastIndexOf(".");
if (dotIndex == -1) {
return "unknown";
}
var ext = fileName.substr(dotIndex + 1);
if (/^mp4|m4v|webm|flv|f4v|ogv$/i.test(ext)) {
return "video";
} else if (/^mp3|m4a|aac|wav|flac|ogg|opus$/i.test(ext)) {
return "audio";
} else if (
/^html|htm|xml|pdf|odf|doc|docx|md|markdown|txt|epub|org$/i.test(ext)
) {
return "document";
} else {
return "unknown";
}
} else {
return "unknown";
}
};
lbry._subscribeIdCount = 0;
lbry._balanceSubscribeCallbacks = {};
lbry._balanceSubscribeInterval = 5000;
lbry._balanceUpdateInterval = null;
lbry._updateBalanceSubscribers = function() {
lbry.wallet_balance().then(function(balance) {
for (let callback of Object.values(lbry._balanceSubscribeCallbacks)) {
callback(balance);
}
});
if (
!lbry._balanceUpdateInterval &&
Object.keys(lbry._balanceSubscribeCallbacks).length
) {
lbry._balanceUpdateInterval = setInterval(() => {
lbry._updateBalanceSubscribers();
}, lbry._balanceSubscribeInterval);
}
};
lbry.balanceSubscribe = function(callback) {
const subscribeId = ++lbry._subscribeIdCount;
lbry._balanceSubscribeCallbacks[subscribeId] = callback;
lbry._updateBalanceSubscribers();
return subscribeId;
};
lbry.balanceUnsubscribe = function(subscribeId) {
delete lbry._balanceSubscribeCallbacks[subscribeId];
if (
lbry._balanceUpdateInterval &&
!Object.keys(lbry._balanceSubscribeCallbacks).length
) {
clearInterval(lbry._balanceUpdateInterval);
}
};
lbry.showMenuIfNeeded = function() {
const showingMenu = sessionStorage.getItem("menuShown") || null;
const chosenMenu = lbry.getClientSetting("showDeveloperMenu")
? "developer"
: "normal";
if (chosenMenu != showingMenu) {
menu.showMenubar(chosenMenu == "developer");
}
sessionStorage.setItem("menuShown", chosenMenu);
};
lbry.getAppVersionInfo = function() {
return new Promise((resolve, reject) => {
ipcRenderer.once("version-info-received", (event, versionInfo) => {
resolve(versionInfo);
});
ipcRenderer.send("version-info-requested");
});
};
/**
* Wrappers for API methods to simulate missing or future behavior. Unlike the old-style stubs,
* these are designed to be transparent wrappers around the corresponding API methods.
*/
/**
* Returns results from the file_list API method, plus dummy entries for pending publishes.
* (If a real publish with the same name is found, the pending publish will be ignored and removed.)
*/
lbry.file_list = function(params = {}) {
return new Promise((resolve, reject) => {
const { name, channel_name, outpoint } = params;
/**
* If we're searching by outpoint, check first to see if there's a matching pending publish.
* Pending publishes use their own faux outpoints that are always unique, so we don't need
* to check if there's a real file.
*/
if (outpoint) {
const pendingPublish = getPendingPublish({ outpoint });
if (pendingPublish) {
resolve([pendingPublishToDummyFileInfo(pendingPublish)]);
return;
}
}
apiCall(
"file_list",
params,
fileInfos => {
removePendingPublishIfNeeded({ name, channel_name, outpoint });
//if a naked file_list call, append the pending file infos
if (!name && !channel_name && !outpoint) {
const dummyFileInfos = lbry
.getPendingPublishes()
.map(pendingPublishToDummyFileInfo);
resolve([...fileInfos, ...dummyFileInfos]);
} else {
resolve(fileInfos);
}
},
reject
);
});
};
lbry.claim_list_mine = function(params = {}) {
return new Promise((resolve, reject) => {
apiCall(
"claim_list_mine",
params,
claims => {
for (let { name, channel_name, txid, nout } of claims) {
removePendingPublishIfNeeded({
name,
channel_name,
outpoint: txid + ":" + nout,
});
}
const dummyClaims = lbry
.getPendingPublishes()
.map(pendingPublishToDummyClaim);
resolve([...claims, ...dummyClaims]);
},
reject
);
});
};
lbry.claim_abandon = function(params = {}) {
return new Promise((resolve, reject) => {
apiCall("claim_abandon", params, resolve, reject);
});
};
lbry.block_show = function(params = {}) {
return new Promise((resolve, reject) => {
apiCall(
"block_show",
params,
block => {
resolve(block);
},
reject
);
});
};
lbry._resolveXhrs = {};
lbry.resolve = function(params = {}) {
return new Promise((resolve, reject) => {
if (!params.uri) {
throw __("Resolve has hacked cache on top of it that requires a URI");
}
lbry._resolveXhrs[params.uri] = apiCall(
"resolve",
params,
function(data) {
resolve(data && data[params.uri] ? data[params.uri] : {});
},
reject
);
});
};
lbry.cancelResolve = function(params = {}) {
const xhr = lbry._resolveXhrs[params.uri];
if (xhr && xhr.readyState > 0 && xhr.readyState < 4) {
xhr.abort();
}
};
lbry = new Proxy(lbry, {
get: function(target, name) {
if (name in target) {
return target[name];
}
return function(params = {}) {
return new Promise((resolve, reject) => {
apiCall(name, params, resolve, reject);
});
};
},
});
export default lbry;