93b1ab8ac6
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
470 lines
12 KiB
JavaScript
470 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;
|