Merge branch 'feature/chrome-options' into develop

This commit is contained in:
DEV Sam Hayes
2025-02-07 17:14:39 +01:00
24 changed files with 558 additions and 63 deletions

View File

@@ -18,5 +18,9 @@ module.exports = {
import: 'src/prompt.ts', import: 'src/prompt.ts',
runtime: false, runtime: false,
}, },
options: {
import: 'src/options.ts',
runtime: false,
},
}, },
} as Configuration; } as Configuration;

View File

@@ -2,8 +2,9 @@
"manifest_version": 3, "manifest_version": 3,
"name": "Gooti", "name": "Gooti",
"description": "Nostr Identity Manager & Signer", "description": "Nostr Identity Manager & Signer",
"version": "0.0.1", "version": "0.0.2",
"homepage_url": "https://getgooti.com", "homepage_url": "https://getgooti.com",
"options_page": "options.html",
"permissions": [ "permissions": [
"windows", "windows",
"storage" "storage"

View File

@@ -0,0 +1,173 @@
<!DOCTYPE html>
<html data-bs-theme="dark">
<head>
<title>Gooti - Options</title>
<link rel="stylesheet" type="text/css" href="styles.css" />
<script src="scripts.js"></script>
<style>
body {
background: var(--background);
height: 100vh;
width: 100vw;
color: #ffffff;
font-size: 16px;
box-sizing: border-box;
}
.page {
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
overflow-y: auto;
}
.container {
max-width: 1200px;
box-sizing: border-box;
width: 100%;
padding-left: 1rem;
padding-right: 1rem;
}
.logo {
height: 60px;
width: 60px;
border-radius: 100%;
border: 2px solid var(--primary);
img {
height: 100%;
width: 100%;
}
}
.brand-name {
font-weight: 700;
font-size: 1.5rem;
letter-spacing: 4px;
color: #b9d6ff;
}
.main-header {
padding-top: 50px;
font-size: 50px;
font-weight: 500;
}
.sub-header {
padding-top: 28px;
font-size: 20px;
max-width: 460px;
text-align: right;
line-height: 1.4;
.accent {
color: #d63384;
border: 1px solid #d63384;
border-radius: 4px;
padding: 2px 4px;
}
}
.middle {
margin-top: 68px;
width: 100%;
background: var(--background-light);
display: flex;
flex-direction: row;
justify-content: center;
padding-bottom: 24px;
}
.option-label {
font-size: 20px;
margin-bottom: 0px;
margin-top: 16px;
}
.option-text {
color: gray;
margin-top: 4px;
}
.snapshots-list {
margin-top: 8px;
}
</style>
</head>
<body>
<div class="page">
<div class="container sam-flex-row gap" style="margin-top: 16px">
<div class="logo">
<img src="gooti.svg" alt="" />
</div>
<span class="brand-name">Gooti</span>
<span>OPTIONS</span>
</div>
<div class="container sam-flex-column center">
<span class="main-header"> Nostr Identity Manager & Signer </span>
<span class="sub-header">
Manage and switch between
<span class="accent">multiple identities</span>
while interacting with Nostr apps
</span>
</div>
<div class="middle">
<div class="container sam-flex-column">
<!-- VAULT SNAPSHOTS -->
<span class="option-label">Vault Snapshots</span>
<span class="option-text">
Importing a previously exported vault snapshot is not
<b>directly</b>
possible in the extension's popup window. This is due to the
browser's limitation of automatically closing the popup when it
looses focus, making it impossible to drop or select a file there.
</span>
<span class="option-text">
To circumvent this limitation, you need to upload your snapshot here
and make it available for the extension to import in the popup.
</span>
<span class="option-text">
<b>
Uploading a snapshot here does NOT automatically start an import!
</b>
</span>
<span class="option-text">
<b>
The data remains inside this browser and is NOT uploaded to
any server!
</b>
</span>
<div class="sam-mt-h sam-flex-row gap">
<button id="uploadSnapshotsButton" class="btn btn-primary">
Upload Snapshots
</button>
<button id="deleteSnapshotsButton" class="btn btn-danger">
Delete Snapshots
</button>
</div>
<ul id="snapshotsList" class="snapshots-list">
<!-- will be filled by JS -->
</ul>
</div>
</div>
</div>
<input
id="uploadSnapshotInput"
type="file"
multiple
style="position: absolute; top: 0; display: none"
accept=".json"
/>
<script src="options.js"></script>
</body>
</html>

View File

@@ -15,6 +15,7 @@ import { HomeComponent as EditIdentityHomeComponent } from './components/edit-id
import { KeysComponent as EditIdentityKeysComponent } from './components/edit-identity/keys/keys.component'; import { KeysComponent as EditIdentityKeysComponent } from './components/edit-identity/keys/keys.component';
import { PermissionsComponent as EditIdentityPermissionsComponent } from './components/edit-identity/permissions/permissions.component'; import { PermissionsComponent as EditIdentityPermissionsComponent } from './components/edit-identity/permissions/permissions.component';
import { RelaysComponent as EditIdentityRelaysComponent } from './components/edit-identity/relays/relays.component'; import { RelaysComponent as EditIdentityRelaysComponent } from './components/edit-identity/relays/relays.component';
import { VaultImportComponent } from './components/vault-import/vault-import.component';
export const routes: Routes = [ export const routes: Routes = [
{ {
@@ -39,6 +40,10 @@ export const routes: Routes = [
}, },
], ],
}, },
{
path: 'vault-import',
component: VaultImportComponent,
},
{ {
path: 'home', path: 'home',
component: HomeComponent, component: HomeComponent,

View File

@@ -23,7 +23,15 @@ export class ChromeMetaHandler extends GootiMetaHandler {
await chrome.storage.local.set(data); await chrome.storage.local.set(data);
} }
async clearData(): Promise<void> { async clearData(keep: string[]): Promise<void> {
await chrome.storage.local.remove(this.metaProperties); const toBeRemovedProperties: string[] = [];
for (const property of this.metaProperties) {
if (!keep.includes(property)) {
toBeRemovedProperties.push(property);
}
}
await chrome.storage.local.remove(toBeRemovedProperties);
} }
} }

View File

@@ -8,15 +8,7 @@
Export Vault Export Vault
</button> </button>
<button <button class="btn btn-primary" (click)="navigate('/vault-import')">
class="btn btn-primary"
(click)="
confirm.show(
'Do you really want to import a vault? All existing data will be overwritten.',
onImportVault.bind(fileInput)
)
"
>
Import Vault Import Vault
</button> </button>
@@ -26,12 +18,12 @@
class="btn btn-danger" class="btn btn-danger"
(click)=" (click)="
confirm.show( confirm.show(
'Do you really want to delete your vault with all identities?', 'Do you really want to reset your extension? Every data will be lost.',
onDeleteVault.bind(this) onResetExtension.bind(this)
) )
" "
> >
Delete Vault Reset Extension
</button> </button>
<lib-confirm #confirm> </lib-confirm> <lib-confirm #confirm> </lib-confirm>

View File

@@ -4,6 +4,7 @@ import {
BrowserSyncFlow, BrowserSyncFlow,
ConfirmComponent, ConfirmComponent,
DateHelper, DateHelper,
NavComponent,
StartupService, StartupService,
StorageService, StorageService,
} from '@common'; } from '@common';
@@ -15,7 +16,7 @@ import { getNewStorageServiceConfig } from '../../../common/data/get-new-storage
templateUrl: './settings.component.html', templateUrl: './settings.component.html',
styleUrl: './settings.component.scss', styleUrl: './settings.component.scss',
}) })
export class SettingsComponent implements OnInit { export class SettingsComponent extends NavComponent implements OnInit {
syncFlow: string | undefined; syncFlow: string | undefined;
readonly #storage = inject(StorageService); readonly #storage = inject(StorageService);
@@ -41,9 +42,9 @@ export class SettingsComponent implements OnInit {
} }
} }
async onDeleteVault() { async onResetExtension() {
try { try {
await this.#storage.deleteVault(); await this.#storage.resetExtension();
this.#startup.startOver(getNewStorageServiceConfig()); this.#startup.startOver(getNewStorageServiceConfig());
} catch (error) { } catch (error) {
console.log(error); console.log(error);

View File

@@ -6,7 +6,7 @@
<div class="sam-flex-column center"> <div class="sam-flex-column center">
<div class="sam-flex-column gap" style="align-items: center"> <div class="sam-flex-column gap" style="align-items: center">
<div class="logo-frame"> <div class="logo-frame">
<img src="gooti.svg" height="120" width="120" alt=""/> <img src="gooti.svg" height="120" width="120" alt="" />
</div> </div>
<button <button
@@ -25,18 +25,10 @@
<button <button
type="button" type="button"
class="btn btn-secondary" class="btn btn-secondary"
(click)="fileInput.click()" (click)="router.navigateByUrl('/vault-import')"
> >
<span>Import a vault</span> <span>Import a vault</span>
</button> </button>
</div> </div>
</div> </div>
</div> </div>
<input
#fileInput
class="file-input"
type="file"
(change)="onImportFileChange($event)"
accept=".json"
/>

View File

@@ -1,7 +1,6 @@
import { Component, inject } from '@angular/core'; import { Component, inject } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { BrowserSyncData, StartupService, StorageService } from '@common'; import { NavComponent } from '@common';
import { getNewStorageServiceConfig } from '../../../common/data/get-new-storage-service-config';
@Component({ @Component({
selector: 'app-home', selector: 'app-home',
@@ -9,28 +8,6 @@ import { getNewStorageServiceConfig } from '../../../common/data/get-new-storage
templateUrl: './home.component.html', templateUrl: './home.component.html',
styleUrl: './home.component.scss', styleUrl: './home.component.scss',
}) })
export class HomeComponent { export class HomeComponent extends NavComponent {
readonly router = inject(Router); readonly router = inject(Router);
readonly #storage = inject(StorageService);
readonly #startup = inject(StartupService);
async onImportFileChange(event: Event) {
try {
const element = event.currentTarget as HTMLInputElement;
const file = element.files !== null ? element.files[0] : undefined;
if (!file) {
return;
}
const text = await file.text();
const vault = JSON.parse(text) as BrowserSyncData;
console.log(vault);
await this.#storage.importVault(vault);
this.#startup.startOver(getNewStorageServiceConfig());
} catch (error) {
console.log(error);
// TODO
}
}
} }

View File

@@ -0,0 +1,39 @@
<div class="custom-header">
<lib-icon-button
class="button"
icon="chevron-left"
(click)="navigateBack()"
></lib-icon-button>
<span class="text">Import Vault </span>
</div>
<div class="sam-pl sam-pr sam-flex-column gap">
<span class="sam-text-muted">
You can select any snapshot that you have previously uploaded on the
<a href="options.html" target="_blank">options page</a>.
</span>
<select class="form-select sam-text-sm" [(ngModel)]="selectedSnapshot">
@for(snapshot of snapshots; track snapshot) {
<option [ngValue]="snapshot">
<span>{{ snapshot.fileName }}</span>
</option>
}
</select>
<span class="sam-text-muted" style="font-size: 12px">
Please note that your data will be imported regarding your current SYNC
setting: <b>{{ syncText }}</b>
</span>
</div>
<div class="sam-flex-grow"></div>
<button
[disabled]="!selectedSnapshot"
class="import-button btn btn-primary"
(click)="onClickImport()"
>
Import
</button>

View File

@@ -0,0 +1,46 @@
:host {
height: 100%;
display: flex;
flex-direction: column;
overflow-y: auto;
overflow-x: hidden;
.custom-header {
padding-top: var(--size);
padding-bottom: var(--size);
display: grid;
grid-template-columns: 1fr;
grid-template-rows: auto;
align-items: center;
background: var(--background);
.button {
grid-column-start: 1;
grid-column-end: 2;
grid-row-start: 1;
grid-row-end: 2;
justify-self: start;
margin-left: 16px;
z-index: 1;
}
.text {
grid-column-start: 1;
grid-column-end: 2;
grid-row-start: 1;
grid-row-end: 2;
font-size: 20px;
font-weight: 500;
justify-self: center;
height: 32px;
overflow-x: hidden;
white-space: nowrap;
text-overflow: ellipsis;
max-width: 70%;
}
}
.import-button {
margin: var(--size);
}
}

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { VaultImportComponent } from './vault-import.component';
describe('VaultImportComponent', () => {
let component: VaultImportComponent;
let fixture: ComponentFixture<VaultImportComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [VaultImportComponent]
})
.compileComponents();
fixture = TestBed.createComponent(VaultImportComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,71 @@
import { Component, inject, OnInit } from '@angular/core';
import { FormsModule } from '@angular/forms';
import {
BrowserSyncFlow,
GootiMetaData_VaultSnapshot,
IconButtonComponent,
NavComponent,
StartupService,
StorageService,
} from '@common';
import browser from 'webextension-polyfill';
import { getNewStorageServiceConfig } from '../../common/data/get-new-storage-service-config';
@Component({
selector: 'app-vault-import',
imports: [IconButtonComponent, FormsModule],
templateUrl: './vault-import.component.html',
styleUrl: './vault-import.component.scss',
})
export class VaultImportComponent extends NavComponent implements OnInit {
snapshots: GootiMetaData_VaultSnapshot[] = [];
selectedSnapshot: GootiMetaData_VaultSnapshot | undefined;
syncText: string | undefined;
readonly #storage = inject(StorageService);
readonly #startup = inject(StartupService);
async openOptionsPage() {
await browser.runtime.openOptionsPage();
}
ngOnInit(): void {
this.#loadData();
}
async onClickImport() {
if (!this.selectedSnapshot) {
return;
}
try {
await this.#storage.deleteVault(true);
await this.#storage.importVault(this.selectedSnapshot.data);
this.#storage.isInitialized = false;
this.#startup.startOver(getNewStorageServiceConfig());
} catch (error) {
console.log(error);
// TODO
}
}
async #loadData() {
this.snapshots = (
this.#storage.getGootiMetaHandler().gootiMetaData?.vaultSnapshots ?? []
).sortBy((x) => x.fileName, 'desc');
const syncFlow =
this.#storage.getGootiMetaHandler().gootiMetaData?.syncFlow;
switch (syncFlow) {
case BrowserSyncFlow.BROWSER_SYNC:
this.syncText = 'GOOGLE CHROME';
break;
default:
case BrowserSyncFlow.NO_SYNC:
this.syncText = 'OFF';
break;
}
}
}

View File

@@ -5,7 +5,7 @@
<div class="content-login-vault"> <div class="content-login-vault">
<div class="sam-flex-column gap" style="align-items: center"> <div class="sam-flex-column gap" style="align-items: center">
<div class="logo-frame"> <div class="logo-frame">
<img src="gooti.svg" height="120" width="120" alt=""/> <img src="gooti.svg" height="120" width="120" alt="" />
</div> </div>
<div class="sam-mt-2 input-group"> <div class="sam-mt-2 input-group">
@@ -45,14 +45,14 @@
class="sam-mt" class="sam-mt"
(click)=" (click)="
confirm.show( confirm.show(
'Do you really want to delete your vault? All existing data will be lost.', 'Do you really want to reset the extension? All data will be lost.',
onClickDeleteVault.bind(this) onClickResetExtension.bind(this)
) )
" "
type="button" type="button"
class="btn btn-link" class="btn btn-link"
> >
Delete Vault Reset Extension
</button> </button>
</div> </div>
</div> </div>

View File

@@ -43,9 +43,9 @@ export class VaultLoginComponent {
} }
} }
async onClickDeleteVault() { async onClickResetExtension() {
try { try {
await this.#storage.deleteVault(); await this.#storage.resetExtension();
this.#startup.startOver(getNewStorageServiceConfig()); this.#startup.startOver(getNewStorageServiceConfig());
} catch (error) { } catch (error) {
console.log(error); console.log(error);

View File

@@ -0,0 +1,130 @@
import {
BrowserSyncData,
GOOTI_META_DATA_KEY,
GootiMetaData_VaultSnapshot,
} from '@common';
import './app/common/extensions/array';
import browser from 'webextension-polyfill';
//
// Functions
//
async function getGootiMetaDataVaultSnapshots(): Promise<
GootiMetaData_VaultSnapshot[]
> {
const data = (await browser.storage.local.get(
GOOTI_META_DATA_KEY.vaultSnapshots
)) as {
vaultSnapshots?: GootiMetaData_VaultSnapshot[];
};
return typeof data.vaultSnapshots === 'undefined'
? []
: data.vaultSnapshots.sortBy((x) => x.fileName, 'desc');
}
async function setGootiMetaDataVaultSnapshots(
vaultSnapshots: GootiMetaData_VaultSnapshot[]
): Promise<void> {
await browser.storage.local.set({
vaultSnapshots,
});
}
function rebuildSnapshotsList(snapshots: GootiMetaData_VaultSnapshot[]) {
const ul = document.getElementById('snapshotsList');
if (!ul) {
return;
}
// Clear the list
ul.innerHTML = '';
for (const snapshot of snapshots) {
const li = document.createElement('li');
const test =
'"' +
snapshot.fileName +
'"' +
' -> vault version: ' +
snapshot.data.version +
' -> identities: ' +
snapshot.data.identities.length +
' -> relays: ' +
snapshot.data.relays.length +
'';
li.innerText = test;
ul.appendChild(li);
}
}
//
// Main
//
document.addEventListener('DOMContentLoaded', async () => {
const uploadSnapshotsButton = document.getElementById(
'uploadSnapshotsButton'
);
const deleteSnapshotsButton = document.getElementById(
'deleteSnapshotsButton'
);
const uploadSnapshotInput = document.getElementById(
'uploadSnapshotInput'
) as HTMLInputElement;
deleteSnapshotsButton?.addEventListener('click', async () => {
await setGootiMetaDataVaultSnapshots([]);
rebuildSnapshotsList([]);
});
uploadSnapshotsButton?.addEventListener('click', async () => {
uploadSnapshotInput?.click();
});
uploadSnapshotInput?.addEventListener('change', async (event) => {
const files = (event.target as HTMLInputElement).files;
if (!files) {
return;
}
try {
const existingSnapshots = await getGootiMetaDataVaultSnapshots();
const newSnapshots: GootiMetaData_VaultSnapshot[] = [];
for (const file of files) {
const text = await file.text();
const vault = JSON.parse(text) as BrowserSyncData;
// Check, if the "new" file is already in the list (via fileName comparison)
if (existingSnapshots.some((x) => x.fileName === file.name)) {
continue;
}
newSnapshots.push({
fileName: file.name,
data: vault,
});
}
const snapshots = [...existingSnapshots, ...newSnapshots].sortBy(
(x) => x.fileName,
'desc'
);
// Persist the new snapshots to the local storage
await setGootiMetaDataVaultSnapshots(snapshots);
//
rebuildSnapshotsList(snapshots);
} catch (error) {
console.log(error);
}
});
const snapshots = await getGootiMetaDataVaultSnapshots();
rebuildSnapshotsList(snapshots);
});

View File

@@ -11,7 +11,8 @@
"src/background.ts", "src/background.ts",
"src/gooti-extension.ts", "src/gooti-extension.ts",
"src/gooti-content-script.ts", "src/gooti-content-script.ts",
"src/prompt.ts" "src/prompt.ts",
"src/options.ts"
], ],
"include": ["src/**/*.d.ts"] "include": ["src/**/*.d.ts"]
} }

View File

@@ -1,5 +1,14 @@
import { inject } from '@angular/core';
import { Router } from '@angular/router';
export class NavComponent { export class NavComponent {
readonly #router = inject(Router);
navigateBack() { navigateBack() {
window.history.back(); window.history.back();
} }
navigate(path: string) {
this.#router.navigate([path]);
}
} }

View File

@@ -8,7 +8,7 @@ export abstract class GootiMetaHandler {
#gootiMetaData?: GootiMetaData; #gootiMetaData?: GootiMetaData;
readonly metaProperties = ['syncFlow']; readonly metaProperties = ['syncFlow', 'vaultSnapshots'];
/** /**
* Load the full data from the storage. If the storage is used for storing * Load the full data from the storage. If the storage is used for storing
* other data (e.g. browser sync data when the user decided to NOT sync), * other data (e.g. browser sync data when the user decided to NOT sync),
@@ -39,5 +39,5 @@ export abstract class GootiMetaHandler {
await this.saveFullData(this.#gootiMetaData); await this.saveFullData(this.#gootiMetaData);
} }
abstract clearData(): Promise<void>; abstract clearData(keep: string[]): Promise<void>;
} }

View File

@@ -120,7 +120,6 @@ export const deleteVault = async function (
await this.getBrowserSyncHandler().clearData(); await this.getBrowserSyncHandler().clearData();
await this.getBrowserSessionHandler().clearData(); await this.getBrowserSessionHandler().clearData();
await this.getGootiMetaHandler().clearData();
if (!doNotSetIsInitializedToFalse) { if (!doNotSetIsInitializedToFalse) {
this.isInitialized = false; this.isInitialized = false;

View File

@@ -115,6 +115,14 @@ export class StorageService {
await deleteVault.call(this, doNotSetIsInitializedToFalse); await deleteVault.call(this, doNotSetIsInitializedToFalse);
} }
async resetExtension() {
this.assureIsInitialized();
await this.getBrowserSyncHandler().clearData();
await this.getBrowserSessionHandler().clearData();
await this.getGootiMetaHandler().clearData([]);
this.isInitialized = false;
}
async unlockVault(password: string): Promise<void> { async unlockVault(password: string): Promise<void> {
await unlockVault.call(this, password); await unlockVault.call(this, password);
} }

View File

@@ -79,6 +79,17 @@ export interface BrowserSessionData {
relays: Relay_DECRYPTED[]; relays: Relay_DECRYPTED[];
} }
export interface GootiMetaData_VaultSnapshot {
fileName: string;
data: BrowserSyncData;
}
export const GOOTI_META_DATA_KEY = {
vaultSnapshots: 'vaultSnapshots',
};
export interface GootiMetaData { export interface GootiMetaData {
syncFlow?: number; // 0 = no sync, 1 = browser sync, (future: 2 = Gooti sync, 3 = Custom sync (bring your own sync)) syncFlow?: number; // 0 = no sync, 1 = browser sync, (future: 2 = Gooti sync, 3 = Custom sync (bring your own sync))
vaultSnapshots?: GootiMetaData_VaultSnapshot[];
} }

View File

@@ -10,6 +10,10 @@
margin-top: var(--size-h); margin-top: var(--size-h);
} }
.sam-mt-hh {
margin-top: var(--size-hh);
}
.sam-mb { .sam-mb {
margin-bottom: var(--size); margin-bottom: var(--size);
} }

View File

@@ -8,6 +8,7 @@
--size-2: 32px; --size-2: 32px;
--size: 16px; --size: 16px;
--size-h: 8px; --size-h: 8px;
--size-hh: 4px;
--background: #161c26; --background: #161c26;
--background-light: #202733; --background-light: #202733;