3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
node_modules/
|
||||
*.build.js
|
||||
*.build.js
|
||||
Shared (Extension)/Resources/options.build.css
|
||||
|
||||
@@ -71,6 +71,18 @@
|
||||
941B04342978CDF900CA291E /* Icon-16.png in Resources */ = {isa = PBXBuildFile; fileRef = 941B042F2978CDF900CA291E /* Icon-16.png */; };
|
||||
941B04352978CDF900CA291E /* Icon-64.png in Resources */ = {isa = PBXBuildFile; fileRef = 941B04302978CDF900CA291E /* Icon-64.png */; };
|
||||
941B04362978CDF900CA291E /* Icon-64.png in Resources */ = {isa = PBXBuildFile; fileRef = 941B04302978CDF900CA291E /* Icon-64.png */; };
|
||||
948C69D9297F887600FB3574 /* options.html in Resources */ = {isa = PBXBuildFile; fileRef = 948C69D8297F887600FB3574 /* options.html */; };
|
||||
948C69DA297F887600FB3574 /* options.html in Resources */ = {isa = PBXBuildFile; fileRef = 948C69D8297F887600FB3574 /* options.html */; };
|
||||
948C69DD297F88A200FB3574 /* options.css in Resources */ = {isa = PBXBuildFile; fileRef = 948C69DB297F88A200FB3574 /* options.css */; };
|
||||
948C69DE297F88A200FB3574 /* options.css in Resources */ = {isa = PBXBuildFile; fileRef = 948C69DB297F88A200FB3574 /* options.css */; };
|
||||
948C69DF297F88A200FB3574 /* options.js in Resources */ = {isa = PBXBuildFile; fileRef = 948C69DC297F88A200FB3574 /* options.js */; };
|
||||
948C69E0297F88A200FB3574 /* options.js in Resources */ = {isa = PBXBuildFile; fileRef = 948C69DC297F88A200FB3574 /* options.js */; };
|
||||
948C69E2297F891F00FB3574 /* options.build.js in Resources */ = {isa = PBXBuildFile; fileRef = 948C69E1297F891F00FB3574 /* options.build.js */; };
|
||||
948C69E3297F891F00FB3574 /* options.build.js in Resources */ = {isa = PBXBuildFile; fileRef = 948C69E1297F891F00FB3574 /* options.build.js */; };
|
||||
948C69E5297F8BA600FB3574 /* options.build.css in Resources */ = {isa = PBXBuildFile; fileRef = 948C69E4297F8BA600FB3574 /* options.build.css */; };
|
||||
948C69E6297F8BA600FB3574 /* options.build.css in Resources */ = {isa = PBXBuildFile; fileRef = 948C69E4297F8BA600FB3574 /* options.build.css */; };
|
||||
948C69E82982DFE900FB3574 /* background.html in Resources */ = {isa = PBXBuildFile; fileRef = 948C69E72982DFE900FB3574 /* background.html */; };
|
||||
948C69E92982DFE900FB3574 /* background.html in Resources */ = {isa = PBXBuildFile; fileRef = 948C69E72982DFE900FB3574 /* background.html */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -159,6 +171,12 @@
|
||||
941B042E2978CDF900CA291E /* Icon-32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Icon-32.png"; sourceTree = "<group>"; };
|
||||
941B042F2978CDF900CA291E /* Icon-16.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Icon-16.png"; sourceTree = "<group>"; };
|
||||
941B04302978CDF900CA291E /* Icon-64.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Icon-64.png"; sourceTree = "<group>"; };
|
||||
948C69D8297F887600FB3574 /* options.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = options.html; sourceTree = "<group>"; };
|
||||
948C69DB297F88A200FB3574 /* options.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; path = options.css; sourceTree = "<group>"; };
|
||||
948C69DC297F88A200FB3574 /* options.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = options.js; sourceTree = "<group>"; };
|
||||
948C69E1297F891F00FB3574 /* options.build.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = options.build.js; sourceTree = "<group>"; };
|
||||
948C69E4297F8BA600FB3574 /* options.build.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; path = options.build.css; sourceTree = "<group>"; };
|
||||
948C69E72982DFE900FB3574 /* background.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = background.html; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -240,6 +258,11 @@
|
||||
941B03A2296FA90400CA291E /* Resources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
948C69E4297F8BA600FB3574 /* options.build.css */,
|
||||
948C69E1297F891F00FB3574 /* options.build.js */,
|
||||
948C69DB297F88A200FB3574 /* options.css */,
|
||||
948C69DC297F88A200FB3574 /* options.js */,
|
||||
948C69D8297F887600FB3574 /* options.html */,
|
||||
941B04162971138F00CA291E /* content.build.js */,
|
||||
941B04152971138F00CA291E /* nostr.build.js */,
|
||||
941B04172971138F00CA291E /* popup.build.js */,
|
||||
@@ -253,6 +276,7 @@
|
||||
941B03A8296FA90400CA291E /* popup.html */,
|
||||
941B03A9296FA90400CA291E /* popup.css */,
|
||||
941B03AA296FA90400CA291E /* popup.js */,
|
||||
948C69E72982DFE900FB3574 /* background.html */,
|
||||
);
|
||||
path = Resources;
|
||||
sourceTree = "<group>";
|
||||
@@ -475,7 +499,11 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
941B0413297110F100CA291E /* background.build.js in Resources */,
|
||||
948C69E82982DFE900FB3574 /* background.html in Resources */,
|
||||
948C69DF297F88A200FB3574 /* options.js in Resources */,
|
||||
948C69DD297F88A200FB3574 /* options.css in Resources */,
|
||||
941B03F2296FA90400CA291E /* background.js in Resources */,
|
||||
948C69E2297F891F00FB3574 /* options.build.js in Resources */,
|
||||
941B03F8296FA90400CA291E /* popup.css in Resources */,
|
||||
941B04292977A28700CA291E /* Icon-128.png in Resources */,
|
||||
941B04182971138F00CA291E /* nostr.build.js in Resources */,
|
||||
@@ -489,6 +517,8 @@
|
||||
941B041C2971139000CA291E /* popup.build.js in Resources */,
|
||||
941B03EC296FA90400CA291E /* _locales in Resources */,
|
||||
941B04222977A25700CA291E /* Icon-512.png in Resources */,
|
||||
948C69E5297F8BA600FB3574 /* options.build.css in Resources */,
|
||||
948C69D9297F887600FB3574 /* options.html in Resources */,
|
||||
941B03F4296FA90400CA291E /* content.js in Resources */,
|
||||
941B04262977A25700CA291E /* Icon-1024.png in Resources */,
|
||||
941B04352978CDF900CA291E /* Icon-64.png in Resources */,
|
||||
@@ -503,7 +533,11 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
941B0414297110F100CA291E /* background.build.js in Resources */,
|
||||
948C69E92982DFE900FB3574 /* background.html in Resources */,
|
||||
948C69E0297F88A200FB3574 /* options.js in Resources */,
|
||||
948C69DE297F88A200FB3574 /* options.css in Resources */,
|
||||
941B03F3296FA90400CA291E /* background.js in Resources */,
|
||||
948C69E3297F891F00FB3574 /* options.build.js in Resources */,
|
||||
941B03F9296FA90400CA291E /* popup.css in Resources */,
|
||||
941B042A2977A28700CA291E /* Icon-128.png in Resources */,
|
||||
941B04192971138F00CA291E /* nostr.build.js in Resources */,
|
||||
@@ -517,6 +551,8 @@
|
||||
941B041D2971139000CA291E /* popup.build.js in Resources */,
|
||||
941B03ED296FA90400CA291E /* _locales in Resources */,
|
||||
941B04232977A25700CA291E /* Icon-512.png in Resources */,
|
||||
948C69E6297F8BA600FB3574 /* options.build.css in Resources */,
|
||||
948C69DA297F887600FB3574 /* options.html in Resources */,
|
||||
941B03F5296FA90400CA291E /* content.js in Resources */,
|
||||
941B04272977A25700CA291E /* Icon-1024.png in Resources */,
|
||||
941B04362978CDF900CA291E /* Icon-64.png in Resources */,
|
||||
@@ -805,7 +841,7 @@
|
||||
INFOPLIST_KEY_UIMainStoryboardFile = Main;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.4;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
@@ -847,7 +883,7 @@
|
||||
INFOPLIST_KEY_UIMainStoryboardFile = Main;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.4;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
|
||||
@@ -29,7 +29,8 @@ This extension does not collect any user data, or transmit any data over a netwo
|
||||
2. Open project folder in terminal.
|
||||
3. Run `npm install` to install the dependencies.
|
||||
4. Run `npm run watch` to watch and build the necessary extension files.
|
||||
5. After every rebuild, execute Run in XCode to deploy the latest changes to Safari.
|
||||
5. Run `npm run watch-tailwind` to watch and build the pages with tailwinds CSS.
|
||||
6. After every rebuild, execute Run in XCode to deploy the latest changes to Safari.
|
||||
|
||||
If you do not see the Nostore extension in your Safari toolbar, you need to activate unsigned extensions and Nostore:
|
||||
|
||||
|
||||
1
Shared (Extension)/Resources/background.html
Normal file
1
Shared (Extension)/Resources/background.html
Normal file
@@ -0,0 +1 @@
|
||||
<script src="background.build.js"></script>
|
||||
@@ -6,11 +6,15 @@ import {
|
||||
nip19,
|
||||
} from 'nostr-tools';
|
||||
|
||||
import { getProfileIndex, get, getProfile } from './utils';
|
||||
|
||||
const storage = browser.storage.local;
|
||||
const log = msg => console.log('Background: ', msg);
|
||||
|
||||
browser.runtime.onInstalled.addListener(async ({ reason }) => {
|
||||
// I would like to be able to skip this for development purposes
|
||||
let ignoreHook = (await storage.get('ignoreInstallHook')).ignoreInstallHook;
|
||||
let ignoreHook = (await storage.get({ ignoreInstallHook: false }))
|
||||
.ignoreInstallHook;
|
||||
if (ignoreHook === true) {
|
||||
return;
|
||||
}
|
||||
@@ -23,56 +27,36 @@ browser.runtime.onInstalled.addListener(async ({ reason }) => {
|
||||
|
||||
browser.runtime.onMessage.addListener(
|
||||
async (message, _sender, sendResponse) => {
|
||||
console.log(message);
|
||||
log(message);
|
||||
|
||||
switch (message.kind) {
|
||||
case 'init':
|
||||
await initialize();
|
||||
// General
|
||||
case 'log':
|
||||
console.log(
|
||||
message.payload.module ? `${module}: ` : '',
|
||||
message.payload.msg
|
||||
);
|
||||
break;
|
||||
case 'setProfileIndex':
|
||||
await setProfileIndex(message.payload);
|
||||
case 'generatePrivateKey':
|
||||
sendResponse(generatePrivateKey());
|
||||
break;
|
||||
case 'getProfileIndex':
|
||||
let profileIndex = await getProfileIndex();
|
||||
sendResponse(profileIndex);
|
||||
case 'savePrivateKey':
|
||||
await savePrivateKey(message.payload);
|
||||
break;
|
||||
case 'getNsecKey':
|
||||
let nsecKey = await getNsecKey();
|
||||
sendResponse(nsecKey);
|
||||
case 'getNpub':
|
||||
let npub = await getNpub(message.payload);
|
||||
sendResponse(npub);
|
||||
break;
|
||||
case 'getNpubKey':
|
||||
let npubKey = await getNpubKey();
|
||||
sendResponse(npubKey);
|
||||
case 'getNsec':
|
||||
let nsec = await getNsec(message.payload);
|
||||
sendResponse(nsec);
|
||||
break;
|
||||
case 'getPubKey':
|
||||
let pubKey = await getPubKey();
|
||||
sendResponse(pubKey);
|
||||
break;
|
||||
case 'getHosts':
|
||||
let hosts = await getHosts();
|
||||
sendResponse(hosts);
|
||||
break;
|
||||
case 'getName':
|
||||
let name = await getName();
|
||||
sendResponse(name);
|
||||
break;
|
||||
case 'getProfileNames':
|
||||
let profileNames = await getProfileNames();
|
||||
sendResponse(profileNames);
|
||||
break;
|
||||
case 'newProfile':
|
||||
let newIndex = await newProfile();
|
||||
sendResponse(newIndex);
|
||||
break;
|
||||
case 'saveProfile':
|
||||
await saveProfile(message.payload);
|
||||
break;
|
||||
case 'clearData':
|
||||
await browser.storage.local.clear();
|
||||
break;
|
||||
case 'deleteProfile':
|
||||
await deleteProfile();
|
||||
break;
|
||||
|
||||
// window.nostr
|
||||
case 'signEvent':
|
||||
let event = await signEvent_(message.payload);
|
||||
sendResponse(event);
|
||||
@@ -86,38 +70,38 @@ browser.runtime.onMessage.addListener(
|
||||
sendResponse(plainText);
|
||||
break;
|
||||
case 'getRelays':
|
||||
sendResponse({});
|
||||
let relays = await getRelays();
|
||||
sendResponse(relays);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
);
|
||||
|
||||
async function get(item) {
|
||||
return (await storage.get(item))[item];
|
||||
}
|
||||
|
||||
async function getOrSetDefault(key, def) {
|
||||
let val = (await storage.get(key))[key];
|
||||
if (val == null || val == undefined) {
|
||||
await storage.set({ [key]: def });
|
||||
return def;
|
||||
// Options
|
||||
async function savePrivateKey([index, privKey]) {
|
||||
if (privKey.startsWith('nsec')) {
|
||||
privKey = nip19.decode(privKey).data;
|
||||
}
|
||||
|
||||
return val;
|
||||
let profiles = await get('profiles');
|
||||
profiles[index].privKey = privKey;
|
||||
await storage.set({ profiles });
|
||||
}
|
||||
|
||||
async function initialize() {
|
||||
await getOrSetDefault('profileIndex', 0);
|
||||
await getOrSetDefault('profiles', [
|
||||
{ name: 'Default', privKey: generatePrivateKey(), hosts: [] },
|
||||
]);
|
||||
async function getNsec(index) {
|
||||
let profile = await getProfile(index);
|
||||
let nsec = nip19.nsecEncode(profile.privKey);
|
||||
return nsec;
|
||||
}
|
||||
|
||||
async function getNsecKey() {
|
||||
let profile = await currentProfile();
|
||||
return profile.nsecKey;
|
||||
async function getNpub(index) {
|
||||
let profile = await getProfile(index);
|
||||
let pubKey = getPublicKey(profile.privKey);
|
||||
let npub = nip19.npubEncode(pubKey);
|
||||
return npub;
|
||||
}
|
||||
|
||||
async function getPrivKey() {
|
||||
@@ -125,81 +109,18 @@ async function getPrivKey() {
|
||||
return profile.privKey;
|
||||
}
|
||||
|
||||
async function getNpubKey() {
|
||||
let pubKey = await getPubKey();
|
||||
console.log('pubKey: ', pubKey);
|
||||
let npubKey = nip19.npubEncode(pubKey);
|
||||
console.log('npub key: ', npubKey);
|
||||
return npubKey;
|
||||
}
|
||||
|
||||
async function getPubKey() {
|
||||
let privKey = await getPrivKey();
|
||||
let pubKey = getPublicKey(privKey);
|
||||
return pubKey;
|
||||
}
|
||||
|
||||
async function getHosts() {
|
||||
let profile = await currentProfile();
|
||||
return profile.hosts;
|
||||
}
|
||||
|
||||
async function getName() {
|
||||
let profile = await currentProfile();
|
||||
return profile.name;
|
||||
}
|
||||
|
||||
async function getProfileNames() {
|
||||
let profiles = await get('profiles');
|
||||
return profiles.map(p => p.name);
|
||||
}
|
||||
|
||||
async function setProfileIndex(profileIndex) {
|
||||
await storage.set({ profileIndex });
|
||||
}
|
||||
|
||||
async function getProfileIndex() {
|
||||
return await get('profileIndex');
|
||||
}
|
||||
|
||||
async function currentProfile() {
|
||||
let index = await get('profileIndex');
|
||||
let index = await getProfileIndex();
|
||||
let profiles = await get('profiles');
|
||||
let currentProfile = profiles[index];
|
||||
currentProfile.nsecKey = nip19.nsecEncode(currentProfile.privKey);
|
||||
return profiles[index];
|
||||
}
|
||||
|
||||
async function newProfile() {
|
||||
let profiles = await get('profiles');
|
||||
const newProfile = {
|
||||
name: 'New Profile',
|
||||
privKey: generatePrivateKey(),
|
||||
hosts: [],
|
||||
};
|
||||
profiles.push(newProfile);
|
||||
await storage.set({ profiles });
|
||||
return profiles.length - 1;
|
||||
}
|
||||
|
||||
async function saveProfile(profile) {
|
||||
if (profile.privKey.startsWith('nsec')) {
|
||||
profile.privKey = nip19.decode(profile.privKey).data;
|
||||
}
|
||||
let index = await getProfileIndex();
|
||||
let profiles = await get('profiles');
|
||||
profiles[index] = profile;
|
||||
await storage.set({ profiles });
|
||||
}
|
||||
|
||||
async function deleteProfile() {
|
||||
let index = await getProfileIndex();
|
||||
let profiles = await get('profiles');
|
||||
profiles.splice(index, 1);
|
||||
let profileIndex = Math.max(index - 1, 0);
|
||||
await storage.set({ profiles, profileIndex });
|
||||
}
|
||||
|
||||
async function signEvent_(event) {
|
||||
event = { ...event };
|
||||
let privKey = await getPrivKey();
|
||||
@@ -216,3 +137,15 @@ async function nip04Decrypt({ pubKey, cipherText }) {
|
||||
let privKey = await getPrivKey();
|
||||
return nip04.decrypt(privKey, pubKey, cipherText);
|
||||
}
|
||||
|
||||
async function getRelays() {
|
||||
let profile = await currentProfile();
|
||||
let relays = profile.relays;
|
||||
let relayObj = {};
|
||||
// The getRelays call expects this to be returned as an object, not array
|
||||
relays.forEach(relay => {
|
||||
let { url, read, write } = relay;
|
||||
relayObj[url] = { read, write };
|
||||
});
|
||||
return relayObj;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"default_locale": "en",
|
||||
|
||||
"name": "__MSG_extension_name__",
|
||||
"description": "__MSG_extension_description__",
|
||||
"version": "1.0",
|
||||
|
||||
"icons": {
|
||||
"48": "images/icon-48.png",
|
||||
"96": "images/icon-96.png",
|
||||
@@ -13,17 +11,19 @@
|
||||
"256": "images/icon-256.png",
|
||||
"512": "images/icon-512.png"
|
||||
},
|
||||
|
||||
"background": {
|
||||
"service_worker": "background.build.js",
|
||||
"type": "module"
|
||||
"page": "background.html"
|
||||
},
|
||||
|
||||
"content_scripts": [{
|
||||
"js": [ "content.build.js" ],
|
||||
"matches": [ "<all_urls>" ]
|
||||
}],
|
||||
|
||||
"content_scripts": [
|
||||
{
|
||||
"js": [
|
||||
"content.build.js"
|
||||
],
|
||||
"matches": [
|
||||
"<all_urls>"
|
||||
]
|
||||
}
|
||||
],
|
||||
"action": {
|
||||
"default_popup": "popup.html",
|
||||
"default_icon": {
|
||||
@@ -35,17 +35,32 @@
|
||||
"72": "images/toolbar-72.png"
|
||||
}
|
||||
},
|
||||
|
||||
"permissions": [ "storage" ],
|
||||
|
||||
"web_accessible_resources": [
|
||||
{
|
||||
"resources": ["nostr.build.js", "popup.build.js"],
|
||||
"matches": ["<all_urls>"]
|
||||
}
|
||||
"options_ui": {
|
||||
"page": "options.html"
|
||||
},
|
||||
"permissions": [
|
||||
"storage"
|
||||
],
|
||||
"web_accessible_resources": [
|
||||
{
|
||||
"resources": [
|
||||
"nostr.build.js",
|
||||
"popup.build.js",
|
||||
"options.build.js",
|
||||
"options.build.css",
|
||||
"options.html"
|
||||
],
|
||||
"matches": [
|
||||
"<all_urls>"
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
"content_security_policy": {
|
||||
"extension_pages": "script-src 'self' 'unsafe-eval'"
|
||||
},
|
||||
"browser_specific_settings": {
|
||||
"safari": {
|
||||
"strict_min_version": "15.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ window.nostr = {
|
||||
let reqId = Math.random().toString();
|
||||
return new Promise((resolve, _reject) => {
|
||||
this.requests[reqId] = resolve;
|
||||
console.log(`Event ${reqId}: ${kind}, payload: `, payload);
|
||||
window.postMessage({ kind, reqId, payload }, '*');
|
||||
});
|
||||
},
|
||||
@@ -57,7 +56,6 @@ window.addEventListener('message', message => {
|
||||
|
||||
if (!validEvents.includes(kind)) return;
|
||||
|
||||
console.log(`Event ${reqId}: Received payload:`, payload);
|
||||
window.nostr.requests[reqId](payload);
|
||||
delete window.nostr.requests[reqId];
|
||||
});
|
||||
|
||||
37
Shared (Extension)/Resources/options.css
Normal file
37
Shared (Extension)/Resources/options.css
Normal file
@@ -0,0 +1,37 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer components {
|
||||
.button {
|
||||
/* Colors */
|
||||
@apply bg-fuchsia-900 hover:bg-fuchsia-800 active:bg-fuchsia-700 text-fuchsia-200 disabled:bg-gray-200 disabled:text-black;
|
||||
|
||||
/* Sizing and padding */
|
||||
@apply rounded-lg p-1 md:p-1.5 md:w-24 min-w-fit text-center;
|
||||
}
|
||||
|
||||
.input {
|
||||
/* Colors */
|
||||
@apply bg-fuchsia-200 text-fuchsia-800 disabled:bg-gray-200 disabled:text-black focus:border-fuchsia-800;
|
||||
|
||||
/* Sizing and padding */
|
||||
@apply rounded-lg p-1 lg:p-1.5 w-full md:w-64;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
/* Colors */
|
||||
@apply text-fuchsia-800 bg-fuchsia-200 rounded-full accent-fuchsia-200;
|
||||
|
||||
/* Sizing and padding */
|
||||
@apply w-4 h-4 lg:w-5 lg:h-5;
|
||||
}
|
||||
|
||||
.section {
|
||||
@apply border-2 border-fuchsia-700 rounded-lg p-1 md:p-5 mt-6 shadow-md;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
@apply text-2xl lg:text-5xl font-bold;
|
||||
}
|
||||
}
|
||||
121
Shared (Extension)/Resources/options.html
Normal file
121
Shared (Extension)/Resources/options.html
Normal file
@@ -0,0 +1,121 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="options.build.css">
|
||||
<script defer src="options.build.js"></script>
|
||||
|
||||
</head>
|
||||
|
||||
<body x-data="options" class="text-fuchsia-900 p-3.5 lg:p-32">
|
||||
<h1 class="text-3xl lg:text-6xl font-bold md:text-center">Settings</h1>
|
||||
|
||||
<!-- PROFILES -->
|
||||
<div class="mt-6">
|
||||
<label for="profiles">Profile</label>
|
||||
<br>
|
||||
<select class="input" x-model.number="profileIndex" id="profiles">
|
||||
<template x-for="(name, index) in profileNames" :key="index">
|
||||
<option x-text="name" :value="index"></option>
|
||||
</template>
|
||||
</select>
|
||||
<div class="block md:inline p-3 pl-0 md:p-0">
|
||||
<button class="button" @click="await newProfile()">New</button>
|
||||
<button class="button" @click="confirmDelete = true" x-show="!confirmDelete">Delete</button>
|
||||
<button class="button" @click="deleteProfile" x-show="confirmDelete">Confirm Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- KEYS -->
|
||||
<div class="section">
|
||||
<h2 class="section-header">Keys</h2>
|
||||
<p class="text-sm italic">Provide your <code class="not-italic">nsec</code> or legacy (hexadecimal) private keys.
|
||||
</p>
|
||||
<form @submit.prevent="saveProfile">
|
||||
<div class="mt-3">
|
||||
<label for="profile-name">Profile Name</label>
|
||||
<br>
|
||||
<input x-model="profileName" type="text" class="input" autocapitalize="off" autocomplete="off" spellcheck="off">
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<label for="priv-key">Private Key</label>
|
||||
<br>
|
||||
<input x-model="privKey" type="text" class="input" autocapitalize="off" autocomplete="off" spellcheck="off">
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<label for="pub-key">Public Key</label>
|
||||
<br>
|
||||
<input x-model="pubKey" type="text" class="input" disabled>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<button class="button" :disabled="!needsSave" @click="saveProfile">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- RELAYS -->
|
||||
<div class="section">
|
||||
<h2 class="section-header">Relays</h2>
|
||||
<p class="text-sm italic">Add relay suggestions for clients.</p>
|
||||
<template x-if="hasRelays">
|
||||
<table class="mt-3 text-xs md:text-base table-auto md:table-fixed">
|
||||
<thead class="font-bold text-lg">
|
||||
<td class="p-2 text-center">URL</td>
|
||||
<td class="p-2 text-center">Read</td>
|
||||
<td class="p-2 text-center">Write</td>
|
||||
<td class="p-2 text-center">Actions</td>
|
||||
</thead>
|
||||
<template x-for="(relay, index) in relays" :key="index">
|
||||
<tr>
|
||||
<td class="p-2 w-1/3" x-text="relay.url"></td>
|
||||
<td class="p-2 text-center">
|
||||
<input class="checkbox" type="checkbox" x-model="relay.read" @change="await saveRelays()">
|
||||
</td>
|
||||
<td class="p-2 text-center">
|
||||
<input class="checkbox" type="checkbox" x-model="relay.write" @change="await saveRelays()">
|
||||
</td>
|
||||
<td class="p-2 text-center">
|
||||
<button class="button" @click="await deleteRelay(index)">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<template x-if="!hasRelays">
|
||||
<div class="mt-3">
|
||||
There are no relays assigned to this profile.
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="mt-3" x-show="hasRecommendedRelays">
|
||||
<select x-model="recommendedRelay" class="input">
|
||||
<option value="" disabled selected>Recommended Relays</option>
|
||||
<template x-for="relay in recommendedRelays">
|
||||
<option :value="relay" x-text="relay"></option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<input class="mt-3 input" x-model="newRelay" type="text" @keyup.enter="await addRelay()" placeholder="wss://..."
|
||||
autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="off">
|
||||
<div class="block md:inline p-3 pl-0 md:p-0">
|
||||
<button class="button" @click="await addRelay()">Add</button>
|
||||
</div>
|
||||
<div class="text-red-500 font-bold" x-show="urlError.length > 0" x-text="urlError"></div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<button class="button" @click="window.close()">Close</button>
|
||||
<button class="button" @click="confirmClear = true" x-show="!confirmClear">Clear Data</button>
|
||||
<button class="button" @click="clearData" x-show="confirmClear">Confirm Clear</button>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
196
Shared (Extension)/Resources/options.js
Normal file
196
Shared (Extension)/Resources/options.js
Normal file
@@ -0,0 +1,196 @@
|
||||
import Alpine from 'alpinejs';
|
||||
import {
|
||||
clearData,
|
||||
deleteProfile,
|
||||
getProfileIndex,
|
||||
getProfileNames,
|
||||
getRelays,
|
||||
initialize,
|
||||
newProfile,
|
||||
savePrivateKey,
|
||||
saveProfileName,
|
||||
saveRelays,
|
||||
RECOMMENDED_RELAYS,
|
||||
} from './utils';
|
||||
|
||||
const log = console.log;
|
||||
|
||||
Alpine.data('options', () => ({
|
||||
profileNames: ['Poop'],
|
||||
profileIndex: 0,
|
||||
profileName: '',
|
||||
pristineProfileName: '',
|
||||
privKey: '',
|
||||
pristinePrivKey: '',
|
||||
pubKey: '',
|
||||
relays: [],
|
||||
newRelay: '',
|
||||
urlError: '',
|
||||
recommendedRelay: '',
|
||||
confirmDelete: false,
|
||||
confirmClear: false,
|
||||
|
||||
async init(watch = true) {
|
||||
log('Initialize backend.');
|
||||
await initialize();
|
||||
|
||||
if (watch) {
|
||||
this.$watch('profileIndex', async () => {
|
||||
await this.refreshProfile();
|
||||
});
|
||||
|
||||
this.$watch('recommendedRelay', async () => {
|
||||
if (this.recommendedRelay.length == 0) return;
|
||||
await this.addRelay(this.recommendedRelay);
|
||||
this.recommendedRelay = '';
|
||||
});
|
||||
}
|
||||
|
||||
// We need to refresh the names BEFORE setting the profile index, or it won't work
|
||||
// on init to set the correct profile.
|
||||
await this.getProfileNames();
|
||||
await this.getProfileIndex();
|
||||
await this.refreshProfile();
|
||||
},
|
||||
|
||||
async refreshProfile() {
|
||||
await this.getProfileNames();
|
||||
await this.getProfileName();
|
||||
await this.getNsec();
|
||||
await this.getNpub();
|
||||
await this.getRelays();
|
||||
this.confirmClear = false;
|
||||
this.confirmDelete = false;
|
||||
},
|
||||
|
||||
// Profile functions
|
||||
|
||||
async getProfileNames() {
|
||||
this.profileNames = await getProfileNames();
|
||||
},
|
||||
|
||||
async getProfileName() {
|
||||
let names = await getProfileNames();
|
||||
let name = names[this.profileIndex];
|
||||
this.profileName = name;
|
||||
this.pristineProfileName = name;
|
||||
},
|
||||
|
||||
async getProfileIndex() {
|
||||
this.profileIndex = await getProfileIndex();
|
||||
},
|
||||
|
||||
async newProfile() {
|
||||
let newIndex = await newProfile();
|
||||
await this.getProfileNames();
|
||||
this.profileIndex = newIndex;
|
||||
},
|
||||
|
||||
async deleteProfile() {
|
||||
await deleteProfile(this.profileIndex);
|
||||
await this.init(false);
|
||||
},
|
||||
|
||||
// Key functions
|
||||
|
||||
async saveProfile() {
|
||||
if (!this.needsSave) return;
|
||||
|
||||
await savePrivateKey(this.profileIndex, this.privKey);
|
||||
await saveProfileName(this.profileIndex, this.profileName);
|
||||
await this.getProfileNames();
|
||||
await this.refreshProfile();
|
||||
},
|
||||
|
||||
async getNpub() {
|
||||
this.pubKey = await browser.runtime.sendMessage({
|
||||
kind: 'getNpub',
|
||||
payload: this.profileIndex,
|
||||
});
|
||||
},
|
||||
|
||||
async getNsec() {
|
||||
this.privKey = await browser.runtime.sendMessage({
|
||||
kind: 'getNsec',
|
||||
payload: this.profileIndex,
|
||||
});
|
||||
this.pristinePrivKey = this.privKey;
|
||||
},
|
||||
|
||||
// Relay functions
|
||||
|
||||
async getRelays() {
|
||||
this.relays = await getRelays(this.profileIndex);
|
||||
},
|
||||
|
||||
async saveRelays() {
|
||||
await saveRelays(this.profileIndex, this.relays);
|
||||
await this.getRelays();
|
||||
},
|
||||
|
||||
async addRelay(relayToAdd = null) {
|
||||
let newRelay = relayToAdd || this.newRelay;
|
||||
try {
|
||||
let url = new URL(newRelay);
|
||||
if (url.protocol !== 'wss:') {
|
||||
this.setUrlError('Must be a websocket url');
|
||||
return;
|
||||
}
|
||||
let urls = this.relays.map(v => v.url);
|
||||
if (urls.includes(url.href)) {
|
||||
this.setUrlError('URL already exists');
|
||||
return;
|
||||
}
|
||||
this.relays.push({ url: url.href, read: true, write: true });
|
||||
await this.saveRelays();
|
||||
this.newRelay = '';
|
||||
} catch (error) {
|
||||
this.setUrlError('Invalid websocket URL');
|
||||
}
|
||||
},
|
||||
|
||||
async deleteRelay(index) {
|
||||
this.relays.splice(index, 1);
|
||||
await this.saveRelays();
|
||||
},
|
||||
|
||||
setUrlError(message) {
|
||||
this.urlError = message;
|
||||
setTimeout(() => {
|
||||
this.urlError = '';
|
||||
}, 3000);
|
||||
},
|
||||
|
||||
// General
|
||||
|
||||
async clearData() {
|
||||
await clearData();
|
||||
await this.init(false);
|
||||
},
|
||||
|
||||
// Properties
|
||||
|
||||
get recommendedRelays() {
|
||||
let relays = this.relays.map(r => new URL(r.url)).map(r => r.href);
|
||||
return RECOMMENDED_RELAYS.filter(r => !relays.includes(r.href)).map(
|
||||
r => r.href
|
||||
);
|
||||
},
|
||||
|
||||
get hasRelays() {
|
||||
return this.relays.length > 0;
|
||||
},
|
||||
|
||||
get hasRecommendedRelays() {
|
||||
return this.recommendedRelays.length > 0;
|
||||
},
|
||||
|
||||
get needsSave() {
|
||||
return (
|
||||
this.privKey !== this.pristinePrivKey ||
|
||||
this.profileName !== this.pristineProfileName
|
||||
);
|
||||
},
|
||||
}));
|
||||
|
||||
Alpine.start();
|
||||
@@ -5,61 +5,23 @@
|
||||
|
||||
body {
|
||||
width: 300px;
|
||||
padding: 10px;
|
||||
padding: 15px;
|
||||
|
||||
font-family: system-ui;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.profile-buttons {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#priv-key, #pub-key {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.profiles {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.profile-name {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.key {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
td:first-child {
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
td:nth-child(2) {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
tr {
|
||||
margin-bottom: 10px;
|
||||
.relay {
|
||||
margin-top: 10px;
|
||||
font-size: 80%;
|
||||
color: darkred;
|
||||
}
|
||||
|
||||
.help {
|
||||
margin-top: 15px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.disclaimer {
|
||||
margin-top: 10px;
|
||||
font-size: 50%;
|
||||
color: green;
|
||||
}
|
||||
@@ -17,50 +17,20 @@
|
||||
<option x-text="prof" :value="index"></option>
|
||||
</template>
|
||||
</select>
|
||||
<button @click="newProfile">New</button>
|
||||
<button @click="confirmDelete = true" x-show="!confirmDelete"
|
||||
:disabled="profileNames.length <= 1">Delete</button>
|
||||
<button @click="await deleteProfile()" x-show="confirmDelete">Confirm Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="profile-name">
|
||||
<label for="profile-name">Profile Name</label>
|
||||
<input type="text" id="profile-name" x-model="name">
|
||||
</div>
|
||||
|
||||
<div class="key">
|
||||
<label for="priv-key">Private Key</label>
|
||||
<input id="priv-key" x-model="privKey" :type="visibleKey ? 'text' : 'password'">
|
||||
</div>
|
||||
|
||||
<div class="buttons">
|
||||
<button @click="visibleKey = !visibleKey" x-text="visibleKey ? 'Hide' : 'Show'"></button>
|
||||
<button @click="await saveProfile()" :disabled="!needsSaving">Save</button>
|
||||
<button @click="confirmClear = true" x-show="!confirmClear">Clear Data</button>
|
||||
<button @click="await clearData()" x-show="confirmClear">Confirm Clear</button>
|
||||
</div>
|
||||
|
||||
<div x-show="hasValidPubKey">
|
||||
<label for="pub-key">Pub Key:</label>
|
||||
<input type="text" id="pub-key" x-model="pubKey" disabled>
|
||||
</div>
|
||||
|
||||
<div class="allowed-sites" x-show="hosts.length > 0">
|
||||
<h3>Allowed Sites</h3>
|
||||
<table>
|
||||
<template x-for="(host, index) in hosts" :key="host.host">
|
||||
<tr>
|
||||
<td class="allowed" x-text="host.allowed ? 'Yes' : 'No'"></td>
|
||||
<td x-text="host.host"></td>
|
||||
<td><button @click="deleteSite(index)">Delete</button></td>
|
||||
</tr>
|
||||
</template>
|
||||
</table>
|
||||
<div class="relay" x-show="relayCount < 1">
|
||||
<span>
|
||||
You do not have any relays setup for this profile. Would you like to add some recommended
|
||||
relays now?
|
||||
</span>
|
||||
<br>
|
||||
<button @click="await addRelays()">Add Relays</button>
|
||||
</div>
|
||||
|
||||
<div class="help">
|
||||
<button @click='window.open("https://ursus.camp/nostore", "_blank")'>Get Help</button>
|
||||
<button @click="await openOptions()">Settings</button>
|
||||
</div>
|
||||
|
||||
<div class="disclaimer">
|
||||
|
||||
@@ -1,131 +1,76 @@
|
||||
import {
|
||||
bglog,
|
||||
getProfileNames,
|
||||
setProfileIndex,
|
||||
getProfileIndex,
|
||||
getRelays,
|
||||
RECOMMENDED_RELAYS,
|
||||
saveRelays,
|
||||
} from './utils';
|
||||
import Alpine from 'alpinejs';
|
||||
window.Alpine = Alpine;
|
||||
|
||||
const log = console.log;
|
||||
|
||||
Alpine.data('popup', () => ({
|
||||
privKey: '',
|
||||
pubKey: '',
|
||||
pristinePrivKey: '',
|
||||
hosts: [],
|
||||
name: '',
|
||||
pristineName: '',
|
||||
profileNames: ['Default'],
|
||||
profileIndex: 0,
|
||||
visibleKey: false,
|
||||
confirmClear: false,
|
||||
confirmDelete: false,
|
||||
relayCount: 0,
|
||||
|
||||
async init() {
|
||||
console.log('Initializing backend.');
|
||||
log('Initializing backend.');
|
||||
await browser.runtime.sendMessage({ kind: 'init' });
|
||||
|
||||
this.$watch('profileIndex', async () => {
|
||||
await this.loadNames();
|
||||
await this.setProfileIndex();
|
||||
await this.refreshProfile();
|
||||
this.confirmClear = false;
|
||||
this.confirmDelete = false;
|
||||
await this.countRelays();
|
||||
});
|
||||
|
||||
// Even though getProfileIndex will immediately trigger a profile refresh, we still
|
||||
// Even though loadProfileIndex will immediately trigger a profile refresh, we still
|
||||
// need to do an initial profile refresh first. This will pull the latest data from
|
||||
// the background scripts. Specifically, this pulls the list of profile names,
|
||||
// otherwise it generates a rendering error where it may not show the correct selected
|
||||
// profile when first loading the popup.
|
||||
await this.refreshProfile();
|
||||
await this.getProfileIndex();
|
||||
},
|
||||
|
||||
async refreshProfile() {
|
||||
await this.getNsecKey();
|
||||
await this.getNpubKey();
|
||||
await this.getHosts();
|
||||
await this.getName();
|
||||
await this.getProfileNames();
|
||||
await this.loadNames();
|
||||
await this.loadProfileIndex();
|
||||
await this.countRelays();
|
||||
},
|
||||
|
||||
async setProfileIndex() {
|
||||
// Becauset the popup state resets every time it open, we use null as a guard. That way
|
||||
// whenever the user opens the popup, it doesn't automatically reset the current profile
|
||||
if (this.profileIndex !== null) {
|
||||
await browser.runtime.sendMessage({
|
||||
kind: 'setProfileIndex',
|
||||
payload: this.profileIndex,
|
||||
});
|
||||
await setProfileIndex(this.profileIndex);
|
||||
}
|
||||
},
|
||||
|
||||
async getNsecKey() {
|
||||
this.privKey = await browser.runtime.sendMessage({
|
||||
kind: 'getNsecKey',
|
||||
});
|
||||
this.pristinePrivKey = this.privKey;
|
||||
async loadNames() {
|
||||
this.profileNames = await getProfileNames();
|
||||
},
|
||||
|
||||
async getNpubKey() {
|
||||
this.pubKey = await browser.runtime.sendMessage({ kind: 'getNpubKey' });
|
||||
async loadProfileIndex() {
|
||||
this.profileIndex = await getProfileIndex();
|
||||
},
|
||||
|
||||
async getHosts() {
|
||||
this.hosts = await browser.runtime.sendMessage({ kind: 'getHosts' });
|
||||
async openOptions() {
|
||||
await browser.runtime.openOptionsPage();
|
||||
window.close();
|
||||
},
|
||||
|
||||
async getProfileNames() {
|
||||
this.profileNames = await browser.runtime.sendMessage({
|
||||
kind: 'getProfileNames',
|
||||
});
|
||||
async countRelays() {
|
||||
let relays = await getRelays(this.profileIndex);
|
||||
this.relayCount = relays.length;
|
||||
},
|
||||
|
||||
async getName() {
|
||||
this.name = await browser.runtime.sendMessage({ kind: 'getName' });
|
||||
this.pristineName = this.name;
|
||||
},
|
||||
|
||||
async getProfileIndex() {
|
||||
this.profileIndex = await browser.runtime.sendMessage({
|
||||
kind: 'getProfileIndex',
|
||||
});
|
||||
},
|
||||
|
||||
async newProfile() {
|
||||
let newIndex = await browser.runtime.sendMessage({
|
||||
kind: 'newProfile',
|
||||
});
|
||||
await this.refreshProfile();
|
||||
this.profileIndex = newIndex;
|
||||
},
|
||||
|
||||
async saveProfile() {
|
||||
let { name, privKey, hosts } = this;
|
||||
let profile = { name, privKey, hosts };
|
||||
await browser.runtime.sendMessage({
|
||||
kind: 'saveProfile',
|
||||
payload: profile,
|
||||
});
|
||||
await this.refreshProfile();
|
||||
},
|
||||
|
||||
async clearData() {
|
||||
await browser.runtime.sendMessage({ kind: 'clearData' });
|
||||
await this.init(); // Re-initialize after clearing
|
||||
this.confirmClear = false;
|
||||
},
|
||||
|
||||
async deleteProfile() {
|
||||
await browser.runtime.sendMessage({ kind: 'deleteProfile' });
|
||||
await this.init();
|
||||
this.confirmDelete = false;
|
||||
},
|
||||
|
||||
// Properties
|
||||
|
||||
get hasValidPubKey() {
|
||||
return typeof this.pubKey === 'string' && this.pubKey.length > 0;
|
||||
},
|
||||
|
||||
get needsSaving() {
|
||||
return (
|
||||
this.privKey !== this.pristinePrivKey ||
|
||||
this.name !== this.pristineName
|
||||
);
|
||||
async addRelays() {
|
||||
let relays = RECOMMENDED_RELAYS.map(r => ({
|
||||
url: r.href,
|
||||
read: true,
|
||||
write: true,
|
||||
}));
|
||||
await saveRelays(this.profileIndex, relays);
|
||||
await this.countRelays();
|
||||
},
|
||||
}));
|
||||
|
||||
|
||||
129
Shared (Extension)/Resources/utils.js
Normal file
129
Shared (Extension)/Resources/utils.js
Normal file
@@ -0,0 +1,129 @@
|
||||
const storage = browser.storage.local;
|
||||
export const RECOMMENDED_RELAYS = [
|
||||
new URL('wss://relay.damus.io'),
|
||||
new URL('wss://eden.nostr.land'),
|
||||
new URL('wss://nostr-relay.derekross.me'),
|
||||
new URL('wss://relay.snort.social'),
|
||||
];
|
||||
|
||||
export async function initialize() {
|
||||
await getOrSetDefault('profileIndex', 0);
|
||||
await getOrSetDefault('profiles', [await generateProfile()]);
|
||||
await getOrSetDefault('version', 0);
|
||||
}
|
||||
|
||||
export async function bglog(msg, module = null) {
|
||||
await browser.runtime.sendMessage({
|
||||
kind: 'log',
|
||||
payload: { msg, module },
|
||||
});
|
||||
}
|
||||
|
||||
export async function getProfiles() {
|
||||
let profiles = await storage.get({ profiles: [] });
|
||||
return profiles.profiles;
|
||||
}
|
||||
|
||||
export async function getProfile(index) {
|
||||
let profiles = await getProfiles();
|
||||
return profiles[index];
|
||||
}
|
||||
|
||||
export async function getProfileNames() {
|
||||
let profiles = await getProfiles();
|
||||
return profiles.map(p => p.name);
|
||||
}
|
||||
|
||||
export async function getProfileIndex() {
|
||||
const index = await storage.get({ profileIndex: 0 });
|
||||
return index.profileIndex;
|
||||
}
|
||||
|
||||
export async function setProfileIndex(profileIndex) {
|
||||
await storage.set({ profileIndex });
|
||||
}
|
||||
|
||||
export async function deleteProfile(index) {
|
||||
let profiles = await getProfiles();
|
||||
let profileIndex = await getProfileIndex();
|
||||
profiles.splice(index, 1);
|
||||
if (profiles.length == 0) {
|
||||
await clearData(); // If we have deleted all of the profiles, let's just start fresh with all new data
|
||||
await initialize();
|
||||
} else {
|
||||
// If the index deleted was the active profile, change the active profile to the next one
|
||||
let newIndex =
|
||||
profileIndex === index ? Math.max(index - 1, 0) : this.profileIndex;
|
||||
await storage.set({ profiles, profileIndex: newIndex });
|
||||
}
|
||||
}
|
||||
|
||||
export async function clearData() {
|
||||
let ignoreInstallHook = await storage.get({ ignoreInstallHook: false });
|
||||
await storage.clear();
|
||||
await storage.set(ignoreInstallHook);
|
||||
}
|
||||
|
||||
async function generatePrivateKey() {
|
||||
return await browser.runtime.sendMessage({ kind: 'generatePrivateKey' });
|
||||
}
|
||||
|
||||
export async function generateProfile(name = 'Default') {
|
||||
return {
|
||||
name,
|
||||
privKey: await generatePrivateKey(),
|
||||
hosts: [],
|
||||
relays: [],
|
||||
};
|
||||
}
|
||||
|
||||
async function getOrSetDefault(key, def) {
|
||||
let val = (await storage.get(key))[key];
|
||||
if (val == null || val == undefined) {
|
||||
await storage.set({ [key]: def });
|
||||
return def;
|
||||
}
|
||||
|
||||
return val;
|
||||
}
|
||||
|
||||
export async function saveProfileName(index, profileName) {
|
||||
let profiles = await getProfiles();
|
||||
profiles[index].name = profileName;
|
||||
await storage.set({ profiles });
|
||||
}
|
||||
|
||||
export async function savePrivateKey(index, privateKey) {
|
||||
await browser.runtime.sendMessage({
|
||||
kind: 'savePrivateKey',
|
||||
payload: [index, privateKey],
|
||||
});
|
||||
}
|
||||
|
||||
export async function newProfile() {
|
||||
let profiles = await getProfiles();
|
||||
const newProfile = await generateProfile('New Profile');
|
||||
profiles.push(newProfile);
|
||||
await storage.set({ profiles });
|
||||
return profiles.length - 1;
|
||||
}
|
||||
|
||||
export async function getRelays(profileIndex) {
|
||||
let profile = await getProfile(profileIndex);
|
||||
return profile.relays || [];
|
||||
}
|
||||
|
||||
export async function saveRelays(profileIndex, relays) {
|
||||
// Having an Alpine proxy object as a sub-object does not serialize correctly in storage,
|
||||
// so we are pre-serializing here before assigning it to the profile, so the proxy
|
||||
// obj doesn't bug out.
|
||||
let fixedRelays = JSON.parse(JSON.stringify(relays));
|
||||
let profiles = await getProfiles();
|
||||
let profile = profiles[profileIndex];
|
||||
profile.relays = fixedRelays;
|
||||
await storage.set({ profiles });
|
||||
}
|
||||
|
||||
export async function get(item) {
|
||||
return (await storage.get(item))[item];
|
||||
}
|
||||
43
build.js
43
build.js
@@ -1,21 +1,28 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
let watch = process.argv[2] === 'watch' ? {
|
||||
onRebuild(error, result) {
|
||||
if (error) console.error('watch rebuild failed: ', error)
|
||||
else console.log('watch rebuild succeeded: ', result)
|
||||
}
|
||||
} : false;
|
||||
let watch =
|
||||
process.argv[2] === 'watch'
|
||||
? {
|
||||
onRebuild(error, result) {
|
||||
if (error) console.error('watch rebuild failed: ', error);
|
||||
else console.log('watch rebuild succeeded: ', result);
|
||||
},
|
||||
}
|
||||
: false;
|
||||
|
||||
require('esbuild').build({
|
||||
entryPoints: {
|
||||
'background.build': './Shared (Extension)/Resources/background.js',
|
||||
'content.build': './Shared (Extension)/Resources/content.js',
|
||||
'nostr.build': './Shared (Extension)/Resources/nostr.js',
|
||||
'popup.build': './Shared (Extension)/Resources/popup.js',
|
||||
},
|
||||
outdir: './Shared (Extension)/Resources',
|
||||
sourcemap: 'inline',
|
||||
bundle: true,
|
||||
watch
|
||||
}).catch(() => process.exit(1))
|
||||
require('esbuild')
|
||||
.build({
|
||||
entryPoints: {
|
||||
'background.build': './Shared (Extension)/Resources/background.js',
|
||||
'content.build': './Shared (Extension)/Resources/content.js',
|
||||
'nostr.build': './Shared (Extension)/Resources/nostr.js',
|
||||
'popup.build': './Shared (Extension)/Resources/popup.js',
|
||||
'options.build': './Shared (Extension)/Resources/options.js',
|
||||
},
|
||||
outdir: './Shared (Extension)/Resources',
|
||||
sourcemap: 'inline',
|
||||
bundle: true,
|
||||
// minify: true,
|
||||
watch,
|
||||
})
|
||||
.catch(() => process.exit(1));
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
|
||||
839
package-lock.json
generated
839
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -11,16 +11,19 @@
|
||||
"scripts": {
|
||||
"build": "./build.js",
|
||||
"watch": "./build.js watch",
|
||||
"watch-tailwind": "tailwindcss -i './Shared (Extension)/Resources/options.css' -o './Shared (Extension)/Resources/options.build.css' --watch",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"alpinejs": "^3.10.5",
|
||||
"esbuild": "^0.16.17",
|
||||
"nostr-tools": "^1.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "^2.8.3"
|
||||
"esbuild": "^0.16.17",
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
"prettier": "^2.8.3",
|
||||
"tailwindcss": "^3.2.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
8
tailwind.config.js
Normal file
8
tailwind.config.js
Normal file
@@ -0,0 +1,8 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ['./Shared (Extension)/**/*.{html,js}'],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [require('@tailwindcss/forms')],
|
||||
};
|
||||
Reference in New Issue
Block a user