Native app facelift, extension UI tweaks, bug fixes.

Show/Hide Button for private key in Options page.

Move experimental page into separate sub-folder.

Move delegation wizard to sub-folder.

Move permission page into separate folder.

Basic functional SwiftUI look for the app.

Beginning to define the main app view.

NavigationStack and Privacy Policy

Show App Icon on main screen.

Getting Started: macOS

Getting Started: iPhone

Getting Started: iPad

Removing old default UIKit code.

Added "No Thanks" toggle to the relay reminder.

Clearly indicate in the Settings page when a profile is a delegated profile.

Changed recommended relays to all public relays.

Use x-cloak in all the places.

Fix bundle display name to use capital N.

Added copy button to pubkey in settings.

Window default size.

Updating event kind list.

Allow events to be copied by clicking on them in the event log.

Tweaking the colors for a more purple-ish look.

Added Tips and Tricks view to native app.

Move utilities modules into separate folder.

Rename event_log files to event_history to escape some content blockers.

Renamed Event Log to Event History in the UI as well.
This commit is contained in:
Ryan Breen
2023-02-16 21:48:47 -05:00
parent 75274aed27
commit 8322aca674
56 changed files with 849 additions and 481 deletions

View File

@@ -0,0 +1,142 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script defer src="event_history.build.js"></script>
<link rel="stylesheet" href="/options.build.css">
<title>Event History</title>
<style>
label {
display: block;
}
</style>
</head>
<body class="text-fuchsia-900 p-3.5 lg:p-32" x-data="eventLog">
<p>
<a href="/options.html" class="border-none hover:underline">← Back</a>
</p>
<h1 class="section-header">Event History</h1>
<div class="section">
<div class="section-header">Filters</div>
<div class="grid grid-cols-2 xl:grid-cols-4 gap-4">
<div>
<label for="view">View</label>
<select id="view" class="input" x-model="view" @change="reload">
<option value="created_at">created_at</option>
<option value="kind">kind</option>
<option value="host">host</option>
<option value="pubkey">pubkey</option>
</select>
</div>
<div>
<label for="sort">Order</label>
<select id="sort" x-model="sort" class="input" @change="reload">
<option value="asc">Ascending</option>
<option value="desc">Descending</option>
</select>
</div>
<div>
<label for="max">Max</label>
<input type="number" x-model.number="max" @input.debounce.750ms="reload" class="input" min="10">
</div>
<div></div>
<div x-show="view === 'created_at'" x-cloak>
<label for="fromCreatedAt">From</label>
<input type="date" id="fromCreatedAt" x-model="fromCreatedAt" class="input" @change="reload">
</div>
<div x-show="view === 'created_at'" x-cloak>
<label for="toCreatedAt">To</label>
<input type="date" id="toCreatedAt" x-model="toCreatedAt" class="input" @change="reload">
</div>
<div x-show="view === 'kind'" x-cloak>
<label for="kindShortcut">Quick Select</label>
<select id="kindShortcut" class="input" @change="quickKindSelect" x-model="quickKind">
<option></option>
<template x-for="k in kinds">
<option :value="k[0]" x-text="k[1]"></option>
</template>
</select>
</div>
<div x-show="view === 'kind'" x-cloak>
<label for="fromKind">From</label>
<input type="number" id="fromKind" x-model.number="fromKind" class="input" @change="reload">
</div>
<div x-show="view === 'kind'" x-cloak>
<label for="toKind">To</label>
<input type="number" id="toKind" x-model.number="toKind" class="input" @change="reload">
</div>
<div x-show="view === 'host'" x-cloak>
<label for="host">Host</label>
<select id="host" class="input" x-model="host" @change="reload">
<option value=""></option>
<template x-for="h in allHosts">
<option :value="h" x-text="h"></option>
</template>
</select>
</div>
<div x-show="view === 'pubkey'" x-cloak>
<label for="profiles">Profiles</label>
<select id="profiles" class="input" x-model="profile" @change="pkFromProfile">
<option value=""></option>
<template x-for="p in profileNames">
<option :value="p" x-text="p"></option>
</template>
</select>
</div>
<div x-show="view === 'pubkey'" x-cloak>
<label for="pubkey">Pubkey</label>
<input type="text" class="input" x-model="pubkey" @input.debounce="reload">
</div>
</div>
<div>
<button class="button mt-3" @click="saveAll">Save all</button>
<button class="button mt-3" @click="deleteAll">Delete all</button>
</div>
</div>
<div class="text-sm italic mt-1 text-right">Click/tap event body to copy the raw event.</div>
<template x-for="(event, index) in events">
<div class="mt-3 border-solid border border-fuchsia-700 rounded-lg">
<div class="select-none flex cursor-pointer text-sm md:text-xl"
@click="selected = selected === index ? null : index">
<div class="flex-none w-14 p-4 font-extrabold" x-text="selected === index ? '-' : '+'"></div>
<div class="flex-1 w-64 p-4" x-text="formatDate(event.metadata.signed_at)"></div>
<div class="flex-1 w-64 p-4" x-text="event.metadata.host"></div>
<div class="flex-1 w-64 p-4" x-text="formatKind(event.event.kind)"></div>
</div>
<div @click.prevent="copyEvent(index)" class="cursor-pointer">
<pre class="rounded-b-lg bg-slate-200 text-sm md:text-xl" x-html="highlight(event)" x-show="selected === index"
x-transition:enter.opacity.delay.75ms x-transition:leave.opacity x-cloak>
</pre>
</div>
</div>
</template>
<div class="fixed top-0 right-0 bg-fuchsia-800 rounded-md p-4 text-white m-8 drop-shadow-md" x-show="copied" x-cloack>
Event copied!
</div>
</body>
</html>

View File

@@ -0,0 +1,147 @@
import Alpine from 'alpinejs';
import { deleteDB } from 'idb';
import jsonFormatHighlight from 'json-format-highlight';
import { downloadAllContents, getHosts, sortByIndex } from '../utilities/db';
import { getProfiles, KINDS } from '../utilities/utils';
const TOMORROW = new Date();
TOMORROW.setDate(TOMORROW.getDate() + 1);
Alpine.data('eventLog', () => ({
kinds: KINDS,
events: [],
view: 'created_at',
max: 100,
sort: 'asc',
allHosts: [],
host: '',
allProfiles: [],
profile: '',
pubkey: '',
selected: null,
copied: false,
// date view
fromCreatedAt: '2008-10-31',
toCreatedAt: TOMORROW.toISOString().split('T')[0],
// kind view
quickKind: '',
fromKind: 0,
toKind: 50000,
async init() {
await this.reload();
},
async reload() {
let events = await sortByIndex(
this.view,
this.keyRange,
this.sort === 'asc',
this.max
);
this.events = events.map(e => ({ ...e, copied: false }));
getHosts().then(hosts => (this.allHosts = hosts));
const profiles = await getProfiles();
console.log(profiles);
this.allProfiles = await Promise.all(
profiles.map(async profile => ({
name: profile.name,
pubkey: await browser.runtime.sendMessage({
kind: 'calcPubKey',
payload: profile.privKey,
}),
}))
);
},
async saveAll() {
const file = await downloadAllContents();
browser.tabs.create({
url: URL.createObjectURL(file),
active: true,
});
},
async deleteAll() {
if (confirm('Are you sure you want to delete ALL events?')) {
await deleteDB('events');
await this.reload();
}
},
quickKindSelect() {
if (this.quickKind === '') return;
const i = parseInt(this.quickKind);
this.fromKind = i;
this.toKind = i;
this.reload();
},
pkFromProfile() {
this.pubkey = this.allProfiles.find(
({ name }) => name === this.profile
).pubkey;
this.reload();
},
highlight(event) {
return jsonFormatHighlight(event);
},
formatDate(epochSeconds) {
return new Date(epochSeconds * 1000).toUTCString();
},
formatKind(kind) {
const k = KINDS.find(([kNum, _]) => kNum === kind);
return k ? `${k[1]} (${kind})` : `Unknown (${kind})`;
},
async copyEvent(index) {
let event = JSON.stringify(this.events[index]);
this.copied = true;
setTimeout(() => (this.copied = false), 1000);
await navigator.clipboard.writeText(event);
},
// Properties
get fromTime() {
let dt = new Date(this.fromCreatedAt);
return Math.floor(dt.getTime() / 1000);
},
get toTime() {
let dt = new Date(this.toCreatedAt);
return Math.floor(dt.getTime() / 1000);
},
get profileNames() {
return this.allProfiles.map(p => p.name);
},
get keyRange() {
switch (this.view) {
case 'created_at':
return IDBKeyRange.bound(this.fromTime, this.toTime);
case 'kind':
return IDBKeyRange.bound(this.fromKind, this.toKind);
case 'host':
if (this.host.length === 0) return null;
return IDBKeyRange.only(this.host);
case 'pubkey':
if (this.pubkey.length === 0) return null;
return IDBKeyRange.only(this.pubkey);
default:
return null;
}
},
}));
Alpine.start();