1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
|||||||
node_modules/
|
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 */; };
|
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 */; };
|
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 */; };
|
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 */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
@@ -159,6 +171,12 @@
|
|||||||
941B042E2978CDF900CA291E /* Icon-32.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Icon-32.png"; sourceTree = "<group>"; };
|
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>"; };
|
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>"; };
|
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 */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@@ -240,6 +258,11 @@
|
|||||||
941B03A2296FA90400CA291E /* Resources */ = {
|
941B03A2296FA90400CA291E /* Resources */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
948C69E4297F8BA600FB3574 /* options.build.css */,
|
||||||
|
948C69E1297F891F00FB3574 /* options.build.js */,
|
||||||
|
948C69DB297F88A200FB3574 /* options.css */,
|
||||||
|
948C69DC297F88A200FB3574 /* options.js */,
|
||||||
|
948C69D8297F887600FB3574 /* options.html */,
|
||||||
941B04162971138F00CA291E /* content.build.js */,
|
941B04162971138F00CA291E /* content.build.js */,
|
||||||
941B04152971138F00CA291E /* nostr.build.js */,
|
941B04152971138F00CA291E /* nostr.build.js */,
|
||||||
941B04172971138F00CA291E /* popup.build.js */,
|
941B04172971138F00CA291E /* popup.build.js */,
|
||||||
@@ -253,6 +276,7 @@
|
|||||||
941B03A8296FA90400CA291E /* popup.html */,
|
941B03A8296FA90400CA291E /* popup.html */,
|
||||||
941B03A9296FA90400CA291E /* popup.css */,
|
941B03A9296FA90400CA291E /* popup.css */,
|
||||||
941B03AA296FA90400CA291E /* popup.js */,
|
941B03AA296FA90400CA291E /* popup.js */,
|
||||||
|
948C69E72982DFE900FB3574 /* background.html */,
|
||||||
);
|
);
|
||||||
path = Resources;
|
path = Resources;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -475,7 +499,11 @@
|
|||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
941B0413297110F100CA291E /* background.build.js in Resources */,
|
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 */,
|
941B03F2296FA90400CA291E /* background.js in Resources */,
|
||||||
|
948C69E2297F891F00FB3574 /* options.build.js in Resources */,
|
||||||
941B03F8296FA90400CA291E /* popup.css in Resources */,
|
941B03F8296FA90400CA291E /* popup.css in Resources */,
|
||||||
941B04292977A28700CA291E /* Icon-128.png in Resources */,
|
941B04292977A28700CA291E /* Icon-128.png in Resources */,
|
||||||
941B04182971138F00CA291E /* nostr.build.js in Resources */,
|
941B04182971138F00CA291E /* nostr.build.js in Resources */,
|
||||||
@@ -489,6 +517,8 @@
|
|||||||
941B041C2971139000CA291E /* popup.build.js in Resources */,
|
941B041C2971139000CA291E /* popup.build.js in Resources */,
|
||||||
941B03EC296FA90400CA291E /* _locales in Resources */,
|
941B03EC296FA90400CA291E /* _locales in Resources */,
|
||||||
941B04222977A25700CA291E /* Icon-512.png in Resources */,
|
941B04222977A25700CA291E /* Icon-512.png in Resources */,
|
||||||
|
948C69E5297F8BA600FB3574 /* options.build.css in Resources */,
|
||||||
|
948C69D9297F887600FB3574 /* options.html in Resources */,
|
||||||
941B03F4296FA90400CA291E /* content.js in Resources */,
|
941B03F4296FA90400CA291E /* content.js in Resources */,
|
||||||
941B04262977A25700CA291E /* Icon-1024.png in Resources */,
|
941B04262977A25700CA291E /* Icon-1024.png in Resources */,
|
||||||
941B04352978CDF900CA291E /* Icon-64.png in Resources */,
|
941B04352978CDF900CA291E /* Icon-64.png in Resources */,
|
||||||
@@ -503,7 +533,11 @@
|
|||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
941B0414297110F100CA291E /* background.build.js in Resources */,
|
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 */,
|
941B03F3296FA90400CA291E /* background.js in Resources */,
|
||||||
|
948C69E3297F891F00FB3574 /* options.build.js in Resources */,
|
||||||
941B03F9296FA90400CA291E /* popup.css in Resources */,
|
941B03F9296FA90400CA291E /* popup.css in Resources */,
|
||||||
941B042A2977A28700CA291E /* Icon-128.png in Resources */,
|
941B042A2977A28700CA291E /* Icon-128.png in Resources */,
|
||||||
941B04192971138F00CA291E /* nostr.build.js in Resources */,
|
941B04192971138F00CA291E /* nostr.build.js in Resources */,
|
||||||
@@ -517,6 +551,8 @@
|
|||||||
941B041D2971139000CA291E /* popup.build.js in Resources */,
|
941B041D2971139000CA291E /* popup.build.js in Resources */,
|
||||||
941B03ED296FA90400CA291E /* _locales in Resources */,
|
941B03ED296FA90400CA291E /* _locales in Resources */,
|
||||||
941B04232977A25700CA291E /* Icon-512.png in Resources */,
|
941B04232977A25700CA291E /* Icon-512.png in Resources */,
|
||||||
|
948C69E6297F8BA600FB3574 /* options.build.css in Resources */,
|
||||||
|
948C69DA297F887600FB3574 /* options.html in Resources */,
|
||||||
941B03F5296FA90400CA291E /* content.js in Resources */,
|
941B03F5296FA90400CA291E /* content.js in Resources */,
|
||||||
941B04272977A25700CA291E /* Icon-1024.png in Resources */,
|
941B04272977A25700CA291E /* Icon-1024.png in Resources */,
|
||||||
941B04362978CDF900CA291E /* Icon-64.png in Resources */,
|
941B04362978CDF900CA291E /* Icon-64.png in Resources */,
|
||||||
@@ -805,7 +841,7 @@
|
|||||||
INFOPLIST_KEY_UIMainStoryboardFile = Main;
|
INFOPLIST_KEY_UIMainStoryboardFile = Main;
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.4;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
@@ -847,7 +883,7 @@
|
|||||||
INFOPLIST_KEY_UIMainStoryboardFile = Main;
|
INFOPLIST_KEY_UIMainStoryboardFile = Main;
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 15.4;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@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.
|
2. Open project folder in terminal.
|
||||||
3. Run `npm install` to install the dependencies.
|
3. Run `npm install` to install the dependencies.
|
||||||
4. Run `npm run watch` to watch and build the necessary extension files.
|
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:
|
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,
|
nip19,
|
||||||
} from 'nostr-tools';
|
} from 'nostr-tools';
|
||||||
|
|
||||||
|
import { getProfileIndex, get, getProfile } from './utils';
|
||||||
|
|
||||||
const storage = browser.storage.local;
|
const storage = browser.storage.local;
|
||||||
|
const log = msg => console.log('Background: ', msg);
|
||||||
|
|
||||||
browser.runtime.onInstalled.addListener(async ({ reason }) => {
|
browser.runtime.onInstalled.addListener(async ({ reason }) => {
|
||||||
// I would like to be able to skip this for development purposes
|
// 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) {
|
if (ignoreHook === true) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -23,56 +27,36 @@ browser.runtime.onInstalled.addListener(async ({ reason }) => {
|
|||||||
|
|
||||||
browser.runtime.onMessage.addListener(
|
browser.runtime.onMessage.addListener(
|
||||||
async (message, _sender, sendResponse) => {
|
async (message, _sender, sendResponse) => {
|
||||||
console.log(message);
|
log(message);
|
||||||
|
|
||||||
switch (message.kind) {
|
switch (message.kind) {
|
||||||
case 'init':
|
// General
|
||||||
await initialize();
|
case 'log':
|
||||||
|
console.log(
|
||||||
|
message.payload.module ? `${module}: ` : '',
|
||||||
|
message.payload.msg
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case 'setProfileIndex':
|
case 'generatePrivateKey':
|
||||||
await setProfileIndex(message.payload);
|
sendResponse(generatePrivateKey());
|
||||||
break;
|
break;
|
||||||
case 'getProfileIndex':
|
case 'savePrivateKey':
|
||||||
let profileIndex = await getProfileIndex();
|
await savePrivateKey(message.payload);
|
||||||
sendResponse(profileIndex);
|
|
||||||
break;
|
break;
|
||||||
case 'getNsecKey':
|
case 'getNpub':
|
||||||
let nsecKey = await getNsecKey();
|
let npub = await getNpub(message.payload);
|
||||||
sendResponse(nsecKey);
|
sendResponse(npub);
|
||||||
break;
|
break;
|
||||||
case 'getNpubKey':
|
case 'getNsec':
|
||||||
let npubKey = await getNpubKey();
|
let nsec = await getNsec(message.payload);
|
||||||
sendResponse(npubKey);
|
sendResponse(nsec);
|
||||||
break;
|
break;
|
||||||
case 'getPubKey':
|
case 'getPubKey':
|
||||||
let pubKey = await getPubKey();
|
let pubKey = await getPubKey();
|
||||||
sendResponse(pubKey);
|
sendResponse(pubKey);
|
||||||
break;
|
break;
|
||||||
case 'getHosts':
|
|
||||||
let hosts = await getHosts();
|
// window.nostr
|
||||||
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;
|
|
||||||
case 'signEvent':
|
case 'signEvent':
|
||||||
let event = await signEvent_(message.payload);
|
let event = await signEvent_(message.payload);
|
||||||
sendResponse(event);
|
sendResponse(event);
|
||||||
@@ -86,38 +70,38 @@ browser.runtime.onMessage.addListener(
|
|||||||
sendResponse(plainText);
|
sendResponse(plainText);
|
||||||
break;
|
break;
|
||||||
case 'getRelays':
|
case 'getRelays':
|
||||||
sendResponse({});
|
let relays = await getRelays();
|
||||||
|
sendResponse(relays);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
async function get(item) {
|
// Options
|
||||||
return (await storage.get(item))[item];
|
async function savePrivateKey([index, privKey]) {
|
||||||
}
|
if (privKey.startsWith('nsec')) {
|
||||||
|
privKey = nip19.decode(privKey).data;
|
||||||
async function getOrSetDefault(key, def) {
|
|
||||||
let val = (await storage.get(key))[key];
|
|
||||||
if (val == null || val == undefined) {
|
|
||||||
await storage.set({ [key]: def });
|
|
||||||
return def;
|
|
||||||
}
|
}
|
||||||
|
let profiles = await get('profiles');
|
||||||
return val;
|
profiles[index].privKey = privKey;
|
||||||
|
await storage.set({ profiles });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function initialize() {
|
async function getNsec(index) {
|
||||||
await getOrSetDefault('profileIndex', 0);
|
let profile = await getProfile(index);
|
||||||
await getOrSetDefault('profiles', [
|
let nsec = nip19.nsecEncode(profile.privKey);
|
||||||
{ name: 'Default', privKey: generatePrivateKey(), hosts: [] },
|
return nsec;
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getNsecKey() {
|
async function getNpub(index) {
|
||||||
let profile = await currentProfile();
|
let profile = await getProfile(index);
|
||||||
return profile.nsecKey;
|
let pubKey = getPublicKey(profile.privKey);
|
||||||
|
let npub = nip19.npubEncode(pubKey);
|
||||||
|
return npub;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getPrivKey() {
|
async function getPrivKey() {
|
||||||
@@ -125,81 +109,18 @@ async function getPrivKey() {
|
|||||||
return profile.privKey;
|
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() {
|
async function getPubKey() {
|
||||||
let privKey = await getPrivKey();
|
let privKey = await getPrivKey();
|
||||||
let pubKey = getPublicKey(privKey);
|
let pubKey = getPublicKey(privKey);
|
||||||
return pubKey;
|
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() {
|
async function currentProfile() {
|
||||||
let index = await get('profileIndex');
|
let index = await getProfileIndex();
|
||||||
let profiles = await get('profiles');
|
let profiles = await get('profiles');
|
||||||
let currentProfile = profiles[index];
|
|
||||||
currentProfile.nsecKey = nip19.nsecEncode(currentProfile.privKey);
|
|
||||||
return profiles[index];
|
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) {
|
async function signEvent_(event) {
|
||||||
event = { ...event };
|
event = { ...event };
|
||||||
let privKey = await getPrivKey();
|
let privKey = await getPrivKey();
|
||||||
@@ -216,3 +137,15 @@ async function nip04Decrypt({ pubKey, cipherText }) {
|
|||||||
let privKey = await getPrivKey();
|
let privKey = await getPrivKey();
|
||||||
return nip04.decrypt(privKey, pubKey, cipherText);
|
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,
|
"manifest_version": 3,
|
||||||
"default_locale": "en",
|
"default_locale": "en",
|
||||||
|
|
||||||
"name": "__MSG_extension_name__",
|
"name": "__MSG_extension_name__",
|
||||||
"description": "__MSG_extension_description__",
|
"description": "__MSG_extension_description__",
|
||||||
"version": "1.0",
|
"version": "1.0",
|
||||||
|
|
||||||
"icons": {
|
"icons": {
|
||||||
"48": "images/icon-48.png",
|
"48": "images/icon-48.png",
|
||||||
"96": "images/icon-96.png",
|
"96": "images/icon-96.png",
|
||||||
@@ -13,17 +11,19 @@
|
|||||||
"256": "images/icon-256.png",
|
"256": "images/icon-256.png",
|
||||||
"512": "images/icon-512.png"
|
"512": "images/icon-512.png"
|
||||||
},
|
},
|
||||||
|
|
||||||
"background": {
|
"background": {
|
||||||
"service_worker": "background.build.js",
|
"page": "background.html"
|
||||||
"type": "module"
|
|
||||||
},
|
},
|
||||||
|
"content_scripts": [
|
||||||
"content_scripts": [{
|
{
|
||||||
"js": [ "content.build.js" ],
|
"js": [
|
||||||
"matches": [ "<all_urls>" ]
|
"content.build.js"
|
||||||
}],
|
],
|
||||||
|
"matches": [
|
||||||
|
"<all_urls>"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
"action": {
|
"action": {
|
||||||
"default_popup": "popup.html",
|
"default_popup": "popup.html",
|
||||||
"default_icon": {
|
"default_icon": {
|
||||||
@@ -35,17 +35,32 @@
|
|||||||
"72": "images/toolbar-72.png"
|
"72": "images/toolbar-72.png"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"options_ui": {
|
||||||
"permissions": [ "storage" ],
|
"page": "options.html"
|
||||||
|
},
|
||||||
"web_accessible_resources": [
|
"permissions": [
|
||||||
{
|
"storage"
|
||||||
"resources": ["nostr.build.js", "popup.build.js"],
|
],
|
||||||
"matches": ["<all_urls>"]
|
"web_accessible_resources": [
|
||||||
}
|
{
|
||||||
|
"resources": [
|
||||||
|
"nostr.build.js",
|
||||||
|
"popup.build.js",
|
||||||
|
"options.build.js",
|
||||||
|
"options.build.css",
|
||||||
|
"options.html"
|
||||||
|
],
|
||||||
|
"matches": [
|
||||||
|
"<all_urls>"
|
||||||
|
]
|
||||||
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
"content_security_policy": {
|
"content_security_policy": {
|
||||||
"extension_pages": "script-src 'self' 'unsafe-eval'"
|
"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();
|
let reqId = Math.random().toString();
|
||||||
return new Promise((resolve, _reject) => {
|
return new Promise((resolve, _reject) => {
|
||||||
this.requests[reqId] = resolve;
|
this.requests[reqId] = resolve;
|
||||||
console.log(`Event ${reqId}: ${kind}, payload: `, payload);
|
|
||||||
window.postMessage({ kind, reqId, payload }, '*');
|
window.postMessage({ kind, reqId, payload }, '*');
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -57,7 +56,6 @@ window.addEventListener('message', message => {
|
|||||||
|
|
||||||
if (!validEvents.includes(kind)) return;
|
if (!validEvents.includes(kind)) return;
|
||||||
|
|
||||||
console.log(`Event ${reqId}: Received payload:`, payload);
|
|
||||||
window.nostr.requests[reqId](payload);
|
window.nostr.requests[reqId](payload);
|
||||||
delete window.nostr.requests[reqId];
|
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 {
|
body {
|
||||||
width: 300px;
|
width: 300px;
|
||||||
padding: 10px;
|
padding: 15px;
|
||||||
|
|
||||||
font-family: system-ui;
|
font-family: system-ui;
|
||||||
}
|
}
|
||||||
|
|
||||||
label {
|
.relay {
|
||||||
display: inline-block;
|
margin-top: 10px;
|
||||||
width: 200px;
|
font-size: 80%;
|
||||||
}
|
color: darkred;
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.help {
|
.help {
|
||||||
margin-top: 15px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.disclaimer {
|
.disclaimer {
|
||||||
|
margin-top: 10px;
|
||||||
font-size: 50%;
|
font-size: 50%;
|
||||||
color: green;
|
color: green;
|
||||||
}
|
}
|
||||||
@@ -17,50 +17,20 @@
|
|||||||
<option x-text="prof" :value="index"></option>
|
<option x-text="prof" :value="index"></option>
|
||||||
</template>
|
</template>
|
||||||
</select>
|
</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>
|
</div>
|
||||||
|
|
||||||
<div class="profile-name">
|
<div class="relay" x-show="relayCount < 1">
|
||||||
<label for="profile-name">Profile Name</label>
|
<span>
|
||||||
<input type="text" id="profile-name" x-model="name">
|
You do not have any relays setup for this profile. Would you like to add some recommended
|
||||||
</div>
|
relays now?
|
||||||
|
</span>
|
||||||
<div class="key">
|
<br>
|
||||||
<label for="priv-key">Private Key</label>
|
<button @click="await addRelays()">Add Relays</button>
|
||||||
<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>
|
</div>
|
||||||
|
|
||||||
<div class="help">
|
<div class="help">
|
||||||
<button @click='window.open("https://ursus.camp/nostore", "_blank")'>Get Help</button>
|
<button @click="await openOptions()">Settings</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="disclaimer">
|
<div class="disclaimer">
|
||||||
|
|||||||
@@ -1,131 +1,76 @@
|
|||||||
|
import {
|
||||||
|
bglog,
|
||||||
|
getProfileNames,
|
||||||
|
setProfileIndex,
|
||||||
|
getProfileIndex,
|
||||||
|
getRelays,
|
||||||
|
RECOMMENDED_RELAYS,
|
||||||
|
saveRelays,
|
||||||
|
} from './utils';
|
||||||
import Alpine from 'alpinejs';
|
import Alpine from 'alpinejs';
|
||||||
window.Alpine = Alpine;
|
window.Alpine = Alpine;
|
||||||
|
|
||||||
|
const log = console.log;
|
||||||
|
|
||||||
Alpine.data('popup', () => ({
|
Alpine.data('popup', () => ({
|
||||||
privKey: '',
|
|
||||||
pubKey: '',
|
|
||||||
pristinePrivKey: '',
|
|
||||||
hosts: [],
|
|
||||||
name: '',
|
|
||||||
pristineName: '',
|
|
||||||
profileNames: ['Default'],
|
profileNames: ['Default'],
|
||||||
profileIndex: 0,
|
profileIndex: 0,
|
||||||
visibleKey: false,
|
relayCount: 0,
|
||||||
confirmClear: false,
|
|
||||||
confirmDelete: false,
|
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
console.log('Initializing backend.');
|
log('Initializing backend.');
|
||||||
await browser.runtime.sendMessage({ kind: 'init' });
|
await browser.runtime.sendMessage({ kind: 'init' });
|
||||||
|
|
||||||
this.$watch('profileIndex', async () => {
|
this.$watch('profileIndex', async () => {
|
||||||
|
await this.loadNames();
|
||||||
await this.setProfileIndex();
|
await this.setProfileIndex();
|
||||||
await this.refreshProfile();
|
await this.countRelays();
|
||||||
this.confirmClear = false;
|
|
||||||
this.confirmDelete = false;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 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
|
// 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,
|
// 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
|
// otherwise it generates a rendering error where it may not show the correct selected
|
||||||
// profile when first loading the popup.
|
// profile when first loading the popup.
|
||||||
await this.refreshProfile();
|
await this.loadNames();
|
||||||
await this.getProfileIndex();
|
await this.loadProfileIndex();
|
||||||
},
|
await this.countRelays();
|
||||||
|
|
||||||
async refreshProfile() {
|
|
||||||
await this.getNsecKey();
|
|
||||||
await this.getNpubKey();
|
|
||||||
await this.getHosts();
|
|
||||||
await this.getName();
|
|
||||||
await this.getProfileNames();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async setProfileIndex() {
|
async setProfileIndex() {
|
||||||
// Becauset the popup state resets every time it open, we use null as a guard. That way
|
// 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
|
// whenever the user opens the popup, it doesn't automatically reset the current profile
|
||||||
if (this.profileIndex !== null) {
|
if (this.profileIndex !== null) {
|
||||||
await browser.runtime.sendMessage({
|
await setProfileIndex(this.profileIndex);
|
||||||
kind: 'setProfileIndex',
|
|
||||||
payload: this.profileIndex,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async getNsecKey() {
|
async loadNames() {
|
||||||
this.privKey = await browser.runtime.sendMessage({
|
this.profileNames = await getProfileNames();
|
||||||
kind: 'getNsecKey',
|
|
||||||
});
|
|
||||||
this.pristinePrivKey = this.privKey;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async getNpubKey() {
|
async loadProfileIndex() {
|
||||||
this.pubKey = await browser.runtime.sendMessage({ kind: 'getNpubKey' });
|
this.profileIndex = await getProfileIndex();
|
||||||
},
|
},
|
||||||
|
|
||||||
async getHosts() {
|
async openOptions() {
|
||||||
this.hosts = await browser.runtime.sendMessage({ kind: 'getHosts' });
|
await browser.runtime.openOptionsPage();
|
||||||
|
window.close();
|
||||||
},
|
},
|
||||||
|
|
||||||
async getProfileNames() {
|
async countRelays() {
|
||||||
this.profileNames = await browser.runtime.sendMessage({
|
let relays = await getRelays(this.profileIndex);
|
||||||
kind: 'getProfileNames',
|
this.relayCount = relays.length;
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async getName() {
|
async addRelays() {
|
||||||
this.name = await browser.runtime.sendMessage({ kind: 'getName' });
|
let relays = RECOMMENDED_RELAYS.map(r => ({
|
||||||
this.pristineName = this.name;
|
url: r.href,
|
||||||
},
|
read: true,
|
||||||
|
write: true,
|
||||||
async getProfileIndex() {
|
}));
|
||||||
this.profileIndex = await browser.runtime.sendMessage({
|
await saveRelays(this.profileIndex, relays);
|
||||||
kind: 'getProfileIndex',
|
await this.countRelays();
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
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
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
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
|
#!/usr/bin/env node
|
||||||
|
|
||||||
let watch = process.argv[2] === 'watch' ? {
|
let watch =
|
||||||
onRebuild(error, result) {
|
process.argv[2] === 'watch'
|
||||||
if (error) console.error('watch rebuild failed: ', error)
|
? {
|
||||||
else console.log('watch rebuild succeeded: ', result)
|
onRebuild(error, result) {
|
||||||
}
|
if (error) console.error('watch rebuild failed: ', error);
|
||||||
} : false;
|
else console.log('watch rebuild succeeded: ', result);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: false;
|
||||||
|
|
||||||
require('esbuild').build({
|
require('esbuild')
|
||||||
entryPoints: {
|
.build({
|
||||||
'background.build': './Shared (Extension)/Resources/background.js',
|
entryPoints: {
|
||||||
'content.build': './Shared (Extension)/Resources/content.js',
|
'background.build': './Shared (Extension)/Resources/background.js',
|
||||||
'nostr.build': './Shared (Extension)/Resources/nostr.js',
|
'content.build': './Shared (Extension)/Resources/content.js',
|
||||||
'popup.build': './Shared (Extension)/Resources/popup.js',
|
'nostr.build': './Shared (Extension)/Resources/nostr.js',
|
||||||
},
|
'popup.build': './Shared (Extension)/Resources/popup.js',
|
||||||
outdir: './Shared (Extension)/Resources',
|
'options.build': './Shared (Extension)/Resources/options.js',
|
||||||
sourcemap: 'inline',
|
},
|
||||||
bundle: true,
|
outdir: './Shared (Extension)/Resources',
|
||||||
watch
|
sourcemap: 'inline',
|
||||||
}).catch(() => process.exit(1))
|
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">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
|
<false/>
|
||||||
<key>NSExtension</key>
|
<key>NSExtension</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSExtensionPointIdentifier</key>
|
<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">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
|
<false/>
|
||||||
<key>NSExtension</key>
|
<key>NSExtension</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSExtensionPointIdentifier</key>
|
<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": {
|
"scripts": {
|
||||||
"build": "./build.js",
|
"build": "./build.js",
|
||||||
"watch": "./build.js watch",
|
"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"
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
},
|
},
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"alpinejs": "^3.10.5",
|
"alpinejs": "^3.10.5",
|
||||||
"esbuild": "^0.16.17",
|
|
||||||
"nostr-tools": "^1.1.1"
|
"nostr-tools": "^1.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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