Added basic settings support.

You can edit relays, embed options, and sign out. This moves the signout
button from the nav to the settings area.
This commit is contained in:
Thomas Mathews 2022-12-29 21:56:06 -08:00
parent 9a3361750e
commit e3b31af127
12 changed files with 198 additions and 40 deletions

View file

@ -20,6 +20,27 @@ a {
a:visited { a:visited {
color: var(--clrLinkVisited); color: var(--clrLinkVisited);
} }
table {
width: 100%;
border-collapse: collapse;
}
thead {
font-weight: bold;
}
th, td {
padding: 5px 0;
font-size: var(--fsNormal);
}
.row {
margin: 15px 0;
}
.mr-some {
margin-right: 15px;
}
.align-right {
text-align: right;
}
#gsticker { #gsticker {
position: absolute; position: absolute;
@ -163,14 +184,14 @@ button.nav > img.icon {
width: 750px; width: 750px;
min-height: 100vh; min-height: 100vh;
} }
#view header { #view > div > header {
position: sticky; position: sticky;
top: 0; top: 0;
z-index: var(--zHeader); z-index: var(--zHeader);
backdrop-filter: blur(20px); backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
} }
#view header > label { #view > div > header > label {
padding: 15px; padding: 15px;
font-size: 22px; font-size: 22px;
font-weight: 800; font-weight: 800;
@ -484,6 +505,16 @@ label[role="profile-nip5"] {
margin: 15px 0; margin: 15px 0;
} }
/* Settings */
#settings section {
margin: 15px;
}
#settings header > label {
font-weight: bold;
font-size: var(--fsEnlarged);
}
/* Inputs */ /* Inputs */
.block { .block {
@ -493,6 +524,11 @@ label[role="profile-nip5"] {
width: 100%; width: 100%;
} }
/* Prevent events from inside button sub elements */
button > * {
pointer-events: none;
}
input[type="text"] { input[type="text"] {
background: transparent; background: transparent;
border: none; border: none;

1
icon/add-relay.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M0 256C0 114.6 114.6 0 256 0C397.4 0 512 114.6 512 256C512 397.4 397.4 512 256 512C114.6 512 0 397.4 0 256zM256 368C269.3 368 280 357.3 280 344V280H344C357.3 280 368 269.3 368 256C368 242.7 357.3 232 344 232H280V168C280 154.7 269.3 144 256 144C242.7 144 232 154.7 232 168V232H168C154.7 232 144 242.7 144 256C144 269.3 154.7 280 168 280H232V344C232 357.3 242.7 368 256 368z"/></svg>

After

Width:  |  Height:  |  Size: 620 B

1
icon/settings-active.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M502.6 182.6l-45.25-45.25C451.4 131.4 443.3 128 434.8 128H384V80C384 53.5 362.5 32 336 32h-160C149.5 32 128 53.5 128 80V128H77.25c-8.5 0-16.62 3.375-22.62 9.375L9.375 182.6C3.375 188.6 0 196.8 0 205.3V304h128v-32C128 263.1 135.1 256 144 256h32C184.9 256 192 263.1 192 272v32h128v-32C320 263.1 327.1 256 336 256h32C376.9 256 384 263.1 384 272v32h128V205.3C512 196.8 508.6 188.6 502.6 182.6zM336 128h-160V80h160V128zM384 368c0 8.875-7.125 16-16 16h-32c-8.875 0-16-7.125-16-16v-32H192v32C192 376.9 184.9 384 176 384h-32C135.1 384 128 376.9 128 368v-32H0V448c0 17.62 14.38 32 32 32h448c17.62 0 32-14.38 32-32v-112h-128V368z"/></svg>

After

Width:  |  Height:  |  Size: 867 B

1
icon/settings.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M502.6 214.6l-77.25-77.25C419.4 131.4 411.2 128 402.7 128H384V88C384 57.13 358.9 32 328 32h-144C153.1 32 128 57.13 128 88V128H109.3C100.8 128 92.63 131.4 86.63 137.4L9.373 214.6C3.371 220.6 0 228.8 0 237.3V416c0 35.35 28.65 64 64 64h384c35.35 0 64-28.65 64-64V237.3C512 228.8 508.6 220.6 502.6 214.6zM176 88c0-4.406 3.594-8 8-8h144c4.406 0 8 3.594 8 8V128h-160V88zM115.9 176h280.2L464 243.9V296h-88V279.1C376 266.7 365.3 256 352 256s-24 10.75-24 23.1V296h-144V279.1C184 266.7 173.3 256 160 256C146.7 256 136 266.7 136 279.1V296H48V243.9L115.9 176zM448 432H64c-8.837 0-16-7.163-16-16v-72h88v16C136 373.3 146.7 384 159.1 384C173.3 384 184 373.3 184 360V344h144v16C328 373.3 338.7 384 352 384s24-10.75 24-23.1V344h88V416C464 424.8 456.8 432 448 432z"/></svg>

After

Width:  |  Height:  |  Size: 994 B

View file

@ -17,6 +17,7 @@
<script defer src="js/ui/render.js?v=15"></script> <script defer src="js/ui/render.js?v=15"></script>
<script defer src="js/ui/state.js?v=1"></script> <script defer src="js/ui/state.js?v=1"></script>
<script defer src="js/ui/fmt.js?v=1"></script> <script defer src="js/ui/fmt.js?v=1"></script>
<script defer src="js/ui/settings.js?v=1"></script>
<script defer src="js/noble-secp256k1.js?v=1"></script> <script defer src="js/noble-secp256k1.js?v=1"></script>
<script defer src="js/bech32.js?v=1"></script> <script defer src="js/bech32.js?v=1"></script>
<script defer src="js/nostr.js?v=7"></script> <script defer src="js/nostr.js?v=7"></script>
@ -69,11 +70,10 @@
<img class="icon svg invert" src="icon/notifications.svg"/> <img class="icon svg invert" src="icon/notifications.svg"/>
<div class="new-notifications hide"></div> <div class="new-notifications hide"></div>
</button> </button>
<button class="icon" role="sign-out" title="Sign Out" onclick="press_logout()"> <button class="icon" role="settings" title="Settings" onclick="switch_view('settings')">
<img class="icon svg invert" src="icon/sign-out.svg"/> <img class="icon svg invert" src="icon/settings.svg"/>
</button> </button>
</nav> </nav>
<div id="container"> <div id="container">
<div class="flex-fill vertical-hide"></div> <div class="flex-fill vertical-hide"></div>
<div id="nav" class="flex-noshrink vertical-hide"> <div id="nav" class="flex-noshrink vertical-hide">
@ -97,11 +97,14 @@
<img class="icon svg active" src="icon/notifications-active.svg"/> <img class="icon svg active" src="icon/notifications-active.svg"/>
<div class="new-notifications hide"></div> <div class="new-notifications hide"></div>
</button> </button>
<button title="Sign Out" class="nav icon" onclick="press_logout()"> <button role="settings" title="Sign Out" class="nav icon"
<img class="icon svg" src="icon/sign-out.svg"/> onclick="switch_view('settings')">
<img class="icon svg inactive" src="icon/settings.svg"/>
<img class="icon svg active" src="icon/settings-active.svg"/>
</button> </button>
</div> </div>
</div> </div>
<div id="view"> <div id="view">
<div> <div>
<header> <header>
@ -161,6 +164,42 @@
Show New (<span role="count">0</span>)</button> Show New (<span role="count">0</span>)</button>
</div> </div>
<div id="timeline" class="events"></div> <div id="timeline" class="events"></div>
<div id="settings" class="hide">
<section>
<header>
<label>Relays</label>
<button id="add-relay" class="btn-text">
<img class="svg icon small" src="icon/add-relay.svg"/>
</button>
</header>
<table id="relay-list" class="row">
<thead>
<tr>
<td>Address</td>
<td>Option</td>
</tr>
</thead>
<tbody>
</tbody>
</table>
</section>
<section>
<header><label>Miscellanious</label></header>
<div class="row">
<label>
Show Embeds From Everyone
<input type="checkbox" name="show_embeds"/>
<label>
</div>
<div class="row">
<button class="action mr-some" role='about-nostr'>Read About Nostr</button>
<button class="action" role="sign-out">
Sign Out
<img class="icon svg small invert" src="icon/sign-out.svg"/>
</button>
</row>
</section>
</div>
</div> </div>
</div> </div>
<div class="flex-fill vertical-hide"></div> <div class="flex-fill vertical-hide"></div>

View file

@ -1,11 +1,5 @@
let DAMUS = new_model(); let DAMUS = new_model();
const BOOTSTRAP_RELAYS = [
"wss://relay.damus.io",
"wss://nostr-relay.wlvs.space",
"wss://nostr-pub.wellorder.net",
]
// TODO autogenerate these constants with a bash script // TODO autogenerate these constants with a bash script
const IMG_EVENT_LIKED = "icon/event-liked.svg"; const IMG_EVENT_LIKED = "icon/event-liked.svg";
const IMG_EVENT_LIKE = "icon/event-like.svg"; const IMG_EVENT_LIKE = "icon/event-like.svg";
@ -87,8 +81,10 @@ async function webapp_init() {
window.alert("Unable to load contacts."); window.alert("Unable to load contacts.");
} }
init_settings(model);
// Create our pool so that event processing functions can work // Create our pool so that event processing functions can work
const pool = nostrjs.RelayPool(BOOTSTRAP_RELAYS); const pool = nostrjs.RelayPool(model.relays);
model.pool = pool model.pool = pool
pool.on("open", on_pool_open); pool.on("open", on_pool_open);
pool.on("event", on_pool_event); pool.on("event", on_pool_event);

View file

@ -290,6 +290,8 @@ async function model_save_settings(model) {
store.put({ store.put({
pubkey: model.pubkey, pubkey: model.pubkey,
notifications_last_viewed: model.notifications.last_viewed, notifications_last_viewed: model.notifications.last_viewed,
relays: Array.from(model.relays),
embeds: model.embeds,
}); });
}; };
} }
@ -306,6 +308,9 @@ async function model_load_settings(model) {
const settings = ev.target.result; const settings = ev.target.result;
if (settings) { if (settings) {
model.notifications.last_viewed = settings.notifications_last_viewed; model.notifications.last_viewed = settings.notifications_last_viewed;
if (settings.length)
model.relays = new Set(settings.relays);
model.embeds = settings.embeds ? settings.embeds : "friends";
} }
db.close(); db.close();
resolve(); resolve();
@ -377,16 +382,6 @@ function new_model() {
last_viewed: 0, // time since last looking at notifications last_viewed: 0, // time since last looking at notifications
count: 0, // the number not seen since last looking count: 0, // the number not seen since last looking
}, },
max_depth: 2,
reactions_to: {},
unknown_ids: {},
unknown_pks: {},
but_wait_theres_more: 0,
deletions: {},
deleted: {},
pow: 0, // pow difficulty target
profiles: new Map(), // pubkey => profile data profiles: new Map(), // pubkey => profile data
contacts: { contacts: {
event: null, event: null,
@ -396,5 +391,17 @@ function new_model() {
invalidated: [], // event ids which should be added/removed invalidated: [], // event ids which should be added/removed
elements: {}, // map of evid > rendered element elements: {}, // map of evid > rendered element
relay_que: new Map(), relay_que: new Map(),
relays: new Set([
"wss://relay.damus.io",
"wss://nostr-relay.wlvs.space",
"wss://nostr-pub.wellorder.net",
]),
embeds: "friends", // friends, everyone
max_depth: 2,
reactions_to: {},
deletions: {},
deleted: {},
pow: 0, // pow difficulty target
}; };
} }

View file

@ -55,7 +55,7 @@ RelayPool.prototype.remove = function relayPoolRemove(url) {
for (const relay of this.relays) { for (const relay of this.relays) {
if (relay.url === url) { if (relay.url === url) {
relay.ws && relay.ws.close() relay.ws && relay.ws.close()
this.relays = this.replays.splice(i, 1) this.relays.splice(i, 1)
return true return true
} }

View file

@ -102,8 +102,12 @@ function render_event_body(model, ev, opts) {
const can_delete = model.pubkey === ev.pubkey || const can_delete = model.pubkey === ev.pubkey ||
(opts.shared && model.pubkey == opts.shared.pubkey); (opts.shared && model.pubkey == opts.shared.pubkey);
// Only show media for content that is by friends. // Only show media for content that is by friends.
const show_media = !opts.is_composing && let show_media = true;
model.contacts.friends.has(ev.pubkey); if (opts.is_composing) {
show_media = false;
} else if (model.embeds == "friends") {
show_media = model.contacts.friends.has(ev.pubkey);
}
let str = "<div>"; let str = "<div>";
str += shared ? render_shared_by(ev, opts) : render_replying_to(model, ev); str += shared ? render_shared_by(ev, opts) : render_replying_to(model, ev);
str += `</div><p> str += `</div><p>

71
js/ui/settings.js Normal file
View file

@ -0,0 +1,71 @@
function init_settings(model) {
const el = find_node("#settings");
find_node("#add-relay", el).addEventListener("click", on_click_add_relay);
const embeds_el = find_node("input[name='show_embeds']", el);
embeds_el.addEventListener("click", on_click_toggle_embeds);
embeds_el.checked = model.embeds != "friends";
find_node("[role='sign-out']", el).addEventListener("click", on_click_sign_out);
find_node("[role='about-nostr']", el).addEventListener("click", open_faqs);
const rlist = find_node("#relay-list tbody", el);
model.relays.forEach((str) => {
rlist.appendChild(new_relay_item(str));
});
}
function new_relay_item(str) {
const tr = document.createElement('tr');
tr.innerHTML = `<td>${str}</td>
<td>
<button class="remove-relay btn-text"
data-address="${str}"
role="remove-relay">
<img class="icon svg small" src="icon/event-delete.svg"/>
</button>
</td>`;
find_node("button", tr).addEventListener("click", on_click_remove_relay);
return tr;
}
function on_click_add_relay(ev) {
const model = DAMUS;
const address = prompt("Please provide a websocket address:", "wss://");
log_debug("got address", address);
// TODO add relay validation
if (!model.pool.add(address))
return;
model.relays.add(address);
find_node("#relay-list tbody").appendChild(new_relay_item(address));
model_save_settings(model);
}
function on_click_toggle_embeds(ev) {
const model = DAMUS;
model.embeds = ev.target.checked ? "everyone" : "friends";
window.alert("You will need to refresh to see changes.");
model_save_settings(model);
}
async function on_click_sign_out(ev) {
if (confirm("Are you sure you want to sign out?")) {
localStorage.clear();
await dbclear();
window.location.reload();
}
}
function on_click_remove_relay(ev) {
const model = DAMUS;
const address = ev.target.dataset.address;
if (!model.pool.remove(address))
return;
model.relays.delete(address);
let parent = ev.target;
while (parent) {
if (parent.matches("tr")) {
parent.parentElement.removeChild(parent);
break;
}
parent = parent.parentElement;
}
model_save_settings(model);
}

View file

@ -1,8 +1,9 @@
const VM_FRIENDS = "friends"; // mine + only events that are from my contacts const VM_FRIENDS = "friends"; // mine + only events that are from my contacts
const VM_EXPLORE = "explore"; // all events const VM_EXPLORE = "explore"; // all events
const VM_NOTIFICATIONS = "notifications"; // reactions & replys const VM_NOTIFICATIONS = "notifications"; // reactions & replys
const VM_THREAD = "thread"; // all events in response to target event const VM_THREAD = "thread"; // all events in response to target event
const VM_USER = "user"; // all events by pubkey const VM_USER = "user"; // all events by pubkey
const VM_SETTINGS = "settings";
function view_get_timeline_el() { function view_get_timeline_el() {
return find_node("#timeline"); return find_node("#timeline");
@ -51,16 +52,25 @@ function view_timeline_apply_mode(model, mode, opts={}) {
names[VM_NOTIFICATIONS] = "Notifications"; names[VM_NOTIFICATIONS] = "Notifications";
names[VM_USER] = "Profile"; names[VM_USER] = "Profile";
names[VM_THREAD] = "Thread"; names[VM_THREAD] = "Thread";
names[VM_SETTINGS] = "Settings";
// Do some visual updates // Do some visual updates
find_node("#view header > label").innerText = names[mode]; find_node("#view header > label").innerText = names[mode];
find_node("#nav > div[data-active]").dataset.active = names[mode].toLowerCase(); find_node("#nav > div[data-active]").dataset.active = names[mode].toLowerCase();
find_node("#view [role='profile-info']").classList.toggle("hide", mode != VM_USER); find_node("#view [role='profile-info']").classList.toggle("hide", mode != VM_USER);
find_node("#newpost").classList.toggle("hide", mode != VM_FRIENDS); find_node("#newpost").classList.toggle("hide", mode != VM_FRIENDS);
find_node("#timeline").classList.toggle("reverse", mode == VM_THREAD); const timeline_el = find_node("#timeline");
timeline_el.classList.toggle("reverse", mode == VM_THREAD);
timeline_el.classList.toggle("hide", mode == VM_SETTINGS);
find_node("#settings").classList.toggle("hide", mode != VM_SETTINGS);
view_timeline_refresh(model, mode, opts); view_timeline_refresh(model, mode, opts);
if (mode == VM_SETTINGS) {
view_show_spinner(false);
view_set_show_count(0, true, true);
}
return mode; return mode;
} }

View file

@ -163,14 +163,6 @@ function close_reply() {
modal.classList.add("closed"); modal.classList.add("closed");
} }
async function press_logout() {
if (confirm("Are you sure you want to sign out?")) {
localStorage.clear();
await dbclear();
window.location.reload();
}
}
function delete_post_confirm(evid) { function delete_post_confirm(evid) {
if (!confirm("Are you sure you want to delete this post?")) if (!confirm("Are you sure you want to delete this post?"))
return; return;