New Feature: Direct Messages
This feature involved a lot of refactoring in order to get working correctly. I wanted to continue using the timeline view for chats thus I used alternative styling & structure for DM event kinds. This worked create since the elements map does not care. There is some queing that has to be done to decrypt message content thus I allow viewing messages even if they haven't been decrypted yet. I think this is good for transparency since you understand what is and is not decrypted. I do think that the UX could improve, because even tho it is fast, it's flashes on new messages. I did not implement saving of latest messages. I will do this later, but this feature is big enough to merge as is: an alpha state that works. I further abstracted profile & name updating to work in a more global manner. Additionally I rewrote code that had attribute scripts to use addEventListener instead. This is needed to happen anyways for security and made the codebase easier to manage.
This commit is contained in:
parent
9badc35bf3
commit
077bf49fdb
17 changed files with 798 additions and 292 deletions
13
TODO
Normal file
13
TODO
Normal file
|
@ -0,0 +1,13 @@
|
|||
Here lie goals:
|
||||
[ ] Fix UI/X issues on mobile (modal, reply, etc.)
|
||||
[ ] Direct messaging
|
||||
[ ] Store profiles of friends, friends of friends, & the user
|
||||
[ ] Export/import contacts list
|
||||
[ ] Render lighting invoices (BOLT11) Do this without a library
|
||||
[ ] Render tagged users e.g. #[0]
|
||||
[ ] Autocomplete for usernames in composition
|
||||
[ ] Redesign embeds
|
||||
[ ] Store latest 500 STANDARD_KINDS & their profiles kick out as needed
|
||||
[ ] Display contacts list
|
||||
[ ] Replace local storage usage
|
||||
|
106
css/styles.css
106
css/styles.css
|
@ -101,13 +101,15 @@ th, td {
|
|||
#nav > div[data-active="home"] [role="home"] img.inactive,
|
||||
#nav > div[data-active="explore"] [role="explore"] img.inactive,
|
||||
#nav > div[data-active="notifications"] [role="notifications"] img.inactive,
|
||||
#nav > div[data-active="settings"] [role="settings"] img.inactive {
|
||||
#nav > div[data-active="settings"] [role="settings"] img.inactive,
|
||||
#nav > div[data-active="messages"] [role="dm"] img.inactive {
|
||||
display: none;
|
||||
}
|
||||
#nav > div[data-active="home"] [role="home"] img.active,
|
||||
#nav > div[data-active="explore"] [role="explore"] img.active,
|
||||
#nav > div[data-active="notifications"] [role="notifications"] img.active,
|
||||
#nav > div[data-active="settings"] [role="settings"] img.active {
|
||||
#nav > div[data-active="settings"] [role="settings"] img.active,
|
||||
#nav > div[data-active="messages"] [role="dm"] img.active {
|
||||
display: block;
|
||||
}
|
||||
#app-icon-logo > img {
|
||||
|
@ -148,9 +150,12 @@ button.nav > img.icon {
|
|||
padding: 15px;
|
||||
}
|
||||
#gnav.open button[role="home"] {
|
||||
top: -300px;
|
||||
top: -375px;
|
||||
}
|
||||
#gnav.open button[role="explore"] {
|
||||
top: -300px;
|
||||
}
|
||||
#gnav.open button[role="dm"] {
|
||||
top: -225px;
|
||||
}
|
||||
#gnav.open button[role="notifications"] {
|
||||
|
@ -168,7 +173,7 @@ button.nav > img.icon {
|
|||
top: 10px;
|
||||
right: 13px;
|
||||
border-radius: 13px;
|
||||
background: #20ff00;
|
||||
background: var(--clrNotification);
|
||||
color: white;
|
||||
font-weight: 800;
|
||||
width: 5px;
|
||||
|
@ -199,6 +204,13 @@ button.nav > img.icon {
|
|||
font-weight: 800;
|
||||
display: block;
|
||||
}
|
||||
#view > div > header > .pfp {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: 15px;
|
||||
}
|
||||
|
||||
/* Events & Content */
|
||||
.events {
|
||||
|
@ -422,17 +434,8 @@ details.cw summary {
|
|||
/* Post & Reply */
|
||||
#newpost {
|
||||
padding: 0px 15px 15px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
border-bottom: solid 1px var(--clrBorder);
|
||||
}
|
||||
#newpost > :first-child {
|
||||
width: 64px;
|
||||
}
|
||||
#newpost > :last-child {
|
||||
padding-left: 15px;
|
||||
flex: 1;
|
||||
}
|
||||
textarea.post-input {
|
||||
display: block;
|
||||
width: 100%;
|
||||
|
@ -454,6 +457,7 @@ input[type="text"].cw {
|
|||
font-size: var(--fsReduced);
|
||||
background: transparent;
|
||||
color: var(--clrText);
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
/* Profile */
|
||||
|
@ -528,6 +532,82 @@ code {
|
|||
font-size: var(--fsEnlarged);
|
||||
}
|
||||
|
||||
/* Messaging */
|
||||
|
||||
#dms-not-available {
|
||||
background: #ffd559;
|
||||
display: inline-block;
|
||||
color: #090909;
|
||||
padding: 15px;
|
||||
}
|
||||
.dm-group {
|
||||
display: flex;
|
||||
padding: 15px;
|
||||
}
|
||||
.dm-group .content {
|
||||
position: relative;
|
||||
padding: 0 15px;
|
||||
flex: 1;
|
||||
}
|
||||
.dm-group .message {
|
||||
word-break: break-word;
|
||||
}
|
||||
.dm-group .time {
|
||||
font-size: var(--fsReduced);
|
||||
color: var(--clrTextLight);
|
||||
}
|
||||
.dm-group .count {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
background: var(--clrBorder);
|
||||
border-radius: 20px;
|
||||
padding: 1px 8px;
|
||||
font-size: var(--fsSmall);
|
||||
}
|
||||
.dm-group .count.active {
|
||||
border: var(--clrNotification) solid 2px;
|
||||
font-weight: bold;
|
||||
background: transparent;
|
||||
}
|
||||
.event.dm {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.event.dm:hover {
|
||||
background: transparent;
|
||||
}
|
||||
.event.dm:last-child {
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
.event.dm .wrap{
|
||||
border-radius: 20px;
|
||||
background: var(--clrPanel);
|
||||
padding: 10px;
|
||||
}
|
||||
.event.dm.mine .wrap {
|
||||
background: #0058ff;
|
||||
margin-left: auto;
|
||||
}
|
||||
.event.dm.mine .timestamp {
|
||||
color: white;
|
||||
display: block;
|
||||
text-align: right;
|
||||
}
|
||||
.event.dm .body p {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
}
|
||||
.event.dm .timestamp {
|
||||
display: block;
|
||||
margin: 0;
|
||||
}
|
||||
.event.dm .reactions {
|
||||
margin: 0;
|
||||
}
|
||||
.event.dm .body {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* Inputs */
|
||||
|
||||
.block {
|
||||
|
|
23
css/vars.css
23
css/vars.css
|
@ -2,17 +2,18 @@
|
|||
|
||||
:root {
|
||||
/* Colors */
|
||||
--clrBg: #fff;
|
||||
--clrPanel: #f9f9f9;
|
||||
--clrBorder: #f2f2f2;
|
||||
--clrBtn: #202020;
|
||||
--clrText: #202020;
|
||||
--clrTextLight: #868686;
|
||||
--clrTextLighter: #abb4ca;
|
||||
--clrHeart: #ff5050;
|
||||
--clrWarn: #fbc560;
|
||||
--clrLink: blue;
|
||||
--clrLinkVisited: purple;
|
||||
--clrBg: #fff;
|
||||
--clrPanel: #f9f9f9;
|
||||
--clrBorder: #f2f2f2;
|
||||
--clrBtn: #202020;
|
||||
--clrText: #202020;
|
||||
--clrTextLight: #868686;
|
||||
--clrTextLighter: #abb4ca;
|
||||
--clrHeart: #ff5050;
|
||||
--clrWarn: #fbc560;
|
||||
--clrLink: blue;
|
||||
--clrLinkVisited: purple;
|
||||
--clrNotification: #20ff00;
|
||||
|
||||
/* TODO I don't like these and simply did it for dark mode. To rename. */
|
||||
--clrEvMsg: #f4f4f4;
|
||||
|
|
1
icon/messages-active.svg
Normal file
1
icon/messages-active.svg
Normal 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="M256 352c-16.53 0-33.06-5.422-47.16-16.41L0 173.2V400C0 426.5 21.49 448 48 448h416c26.51 0 48-21.49 48-48V173.2l-208.8 162.5C289.1 346.6 272.5 352 256 352zM16.29 145.3l212.2 165.1c16.19 12.6 38.87 12.6 55.06 0l212.2-165.1C505.1 137.3 512 125 512 112C512 85.49 490.5 64 464 64h-416C21.49 64 0 85.49 0 112C0 125 6.01 137.3 16.29 145.3z"/></svg>
|
After Width: | Height: | Size: 581 B |
1
icon/messages.svg
Normal file
1
icon/messages.svg
Normal 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="M448 64H64C28.65 64 0 92.65 0 128v256c0 35.35 28.65 64 64 64h384c35.35 0 64-28.65 64-64V128C512 92.65 483.3 64 448 64zM64 112h384c8.822 0 16 7.178 16 16v22.16l-166.8 138.1c-23.19 19.28-59.34 19.27-82.47 .0156L48 150.2V128C48 119.2 55.18 112 64 112zM448 400H64c-8.822 0-16-7.178-16-16V212.7l136.1 113.4C204.3 342.8 229.8 352 256 352s51.75-9.188 71.97-25.98L464 212.7V384C464 392.8 456.8 400 448 400z"/></svg>
|
After Width: | Height: | Size: 646 B |
74
index.html
74
index.html
|
@ -20,7 +20,9 @@
|
|||
<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/fmt.js?v=1"></script>
|
||||
<script defer src="js/ui/profile.js?v=1"></script>
|
||||
<script defer src="js/ui/settings.js?v=1"></script>
|
||||
<script defer src="js/ui/dm.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/nostr.js?v=7"></script>
|
||||
|
@ -69,6 +71,9 @@
|
|||
<button class="icon" role="explore" title="Explore" onclick="switch_view('explore')">
|
||||
<img class="icon svg invert" src="icon/explore.svg"/>
|
||||
</button>
|
||||
<button class="icon" role="dm" title="Direct Messages" onclick="switch_view('dm')">
|
||||
<img class="icon svg invert" src="icon/messages.svg"/>
|
||||
</button>
|
||||
<button class="icon" role="notifications" title="Notifications" onclick="switch_view('notifications')">
|
||||
<img class="icon svg invert" src="icon/notifications.svg"/>
|
||||
<div class="new-notifications hide"></div>
|
||||
|
@ -90,10 +95,14 @@
|
|||
<img class="icon svg active" src="icon/home-active.svg"/>
|
||||
</button>
|
||||
<button role="explore" class="nav icon"
|
||||
title="Explore" onclick="switch_view('explore')">
|
||||
<img class="icon svg inactive" src="icon/explore.svg"/>
|
||||
title="Explore" onclick="switch_view('explore')"> <img class="icon svg inactive" src="icon/explore.svg"/>
|
||||
<img class="icon svg active" src="icon/explore-active.svg"/>
|
||||
</button>
|
||||
<button role="dm" class="nav icon"
|
||||
title="Direct Messages" onclick="switch_view('dm')">
|
||||
<img class="icon svg inactive" src="icon/messages.svg"/>
|
||||
<img class="icon svg active" src="icon/messages-active.svg"/>
|
||||
</button>
|
||||
<button role="notifications" class="nav icon"
|
||||
title="Notifications" onclick="switch_view('notifications')">
|
||||
<img class="icon svg inactive" src="icon/notifications.svg"/>
|
||||
|
@ -112,44 +121,42 @@
|
|||
<div>
|
||||
<header>
|
||||
<label>Home</label>
|
||||
<img class="pfp" role="their-pfp" data-pubkey=""
|
||||
src="icon/no-user.svg"/>
|
||||
<img class="pfp" role="my-pfp" data-pubkey=""
|
||||
src="icon/no-user.svg"/>
|
||||
</header>
|
||||
<div id="newpost">
|
||||
<div class="my-userpic vertical-hide">
|
||||
<!-- To be loaded dynamically. -->
|
||||
</div>
|
||||
<div>
|
||||
<textarea placeholder="What's up?"
|
||||
oninput="post_input_changed(this)"
|
||||
class="post-input" id="post-input"></textarea>
|
||||
<div class="post-tools">
|
||||
<input id="content-warning-input" class="cw hide" type="text" placeholder="Reason"/>
|
||||
<button title="Mark this message as sensitive."
|
||||
onclick="toggle_cw(this)" class="cw icon">
|
||||
<img class="icon svg small" src="icon/content-warning.svg"/>
|
||||
</button>
|
||||
<button onclick="send_post(this)" class="action"
|
||||
role="send" id="post-button" disabled>Send</button>
|
||||
</div>
|
||||
<textarea placeholder="What's up?"
|
||||
class="post-input" id="post-input"></textarea>
|
||||
<div class="post-tools">
|
||||
<input id="content-warning-input" class="cw hide"
|
||||
type="text" placeholder="Reason"/>
|
||||
<button class="cw icon" role="toggle-cw"
|
||||
title="Mark this message as sensitive.">
|
||||
<img class="icon svg small"
|
||||
src="icon/content-warning.svg"/>
|
||||
</button>
|
||||
<button class="action" role="send"
|
||||
id="post-button" disabled>Send</button>
|
||||
</div>
|
||||
</div>
|
||||
<div role="profile-info" class="bottom-border hide">
|
||||
<div id="profile-info" role="profile-info" class="bottom-border hide">
|
||||
<div class="flex">
|
||||
<img role="profile-image" class="pfp jumbo"
|
||||
src="./icon/no-user.svg"
|
||||
onerror="this.src='./icon/no-user.svg';"/>
|
||||
<img role="profile-image" class="pfp jumbo"/>
|
||||
<div class="profile-tools">
|
||||
<!--
|
||||
<button class="icon" title="Message User" role="message-user">
|
||||
<img class="icon" src="icon/message-user.svg"/></button>
|
||||
-->
|
||||
<button class="icon" role="message-user"
|
||||
title="Directly Message">
|
||||
<img class="icon svg" src="icon/message-user.svg"/>
|
||||
</button>
|
||||
<button class="icon hide" role="edit-profile"
|
||||
onclick="show_profile_editor()" title="Edit Profile">
|
||||
title="Edit Profile">
|
||||
<img class="icon svg" src="icon/edit-profile.svg"/></button>
|
||||
<button class="icon" role="copy-pk"
|
||||
data-pk="" onclick="click_copy_pk(this)" title="Copy Public Key">
|
||||
data-pk="" title="Copy Public Key">
|
||||
<img class="icon svg" src="icon/pubkey.svg"/></button>
|
||||
<button class="action" role="follow-user" data-pk=""
|
||||
onclick="click_toggle_follow_user(this)">Follow</button>
|
||||
<button class="action" role="follow-user"
|
||||
data-pk="">Follow</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
|
@ -166,6 +173,13 @@
|
|||
<button onclick="show_new()">
|
||||
Show New (<span role="count">0</span>)</button>
|
||||
</div>
|
||||
<div id="dms-not-available" class="hide">
|
||||
DMs could not be decrypted due to lack of nip04
|
||||
integration. Please use an extension such as nos2x or
|
||||
Alby.
|
||||
</div>
|
||||
<div id="dms" class="hide">
|
||||
</div>
|
||||
<div id="timeline" class="events"></div>
|
||||
<div id="settings" class="hide">
|
||||
<section>
|
||||
|
|
21
js/core.js
21
js/core.js
|
@ -15,6 +15,7 @@ const R_HEART = "❤️";
|
|||
|
||||
const STANDARD_KINDS = [
|
||||
KIND_NOTE,
|
||||
KIND_DM,
|
||||
KIND_DELETE,
|
||||
KIND_REACTION,
|
||||
KIND_SHARE,
|
||||
|
@ -126,26 +127,6 @@ async function sign_event(ev) {
|
|||
return ev
|
||||
}
|
||||
|
||||
async function send_post() {
|
||||
const input_el = document.querySelector("#post-input")
|
||||
const cw_el = document.querySelector("#content-warning-input")
|
||||
const cw = cw_el.value
|
||||
const content = input_el.value
|
||||
const created_at = new_creation_time()
|
||||
const kind = 1
|
||||
const tags = cw ? [["content-warning", cw]] : []
|
||||
const pubkey = await get_pubkey()
|
||||
|
||||
let post = { pubkey, tags, content, created_at, kind }
|
||||
post.id = await nostrjs.calculate_id(post)
|
||||
post = await sign_event(post)
|
||||
broadcast_event(post)
|
||||
|
||||
input_el.value = ""
|
||||
cw_el.value = ""
|
||||
post_input_changed(input_el)
|
||||
}
|
||||
|
||||
function new_reply_tags(ev) {
|
||||
const tags = [["e", ev.id, "", "reply"]];
|
||||
if (ev.refs.root) {
|
||||
|
|
13
js/event.js
13
js/event.js
|
@ -8,21 +8,21 @@ function event_refs_pubkey(ev, pubkey) {
|
|||
return false
|
||||
if (ev.pubkey === pubkey)
|
||||
return false
|
||||
for (const tag of ev.tags) {
|
||||
if (tag.length >= 2 && tag[0] === "p" && tag[1] === pubkey)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
return event_tags_pubkey(ev, pubkey)
|
||||
}
|
||||
|
||||
function event_contains_pubkey(ev, pubkey) {
|
||||
if (ev.pubkey == pubkey)
|
||||
return true;
|
||||
return event_tags_pubkey(ev, pubkey)
|
||||
}
|
||||
|
||||
function event_tags_pubkey(ev, pubkey) {
|
||||
for (const tag of ev.tags) {
|
||||
if (tag.length >= 2 && tag[0] == "p" && tag[1] == pubkey)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
|
||||
function event_get_pubkeys(ev) {
|
||||
|
@ -140,4 +140,3 @@ function event_parse_reaction(ev) {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
23
js/main.js
23
js/main.js
|
@ -8,6 +8,8 @@ const IMG_NO_USER = "icon/no-user.svg";
|
|||
const SID_META = "meta";
|
||||
const SID_HISTORY = "history";
|
||||
const SID_NOTIFICATIONS = "notifications";
|
||||
const SID_DMS_OUT = "dms_out";
|
||||
const SID_DMS_IN = "dms_in";
|
||||
const SID_EXPLORE = "explore";
|
||||
const SID_PROFILES = "profiles";
|
||||
const SID_THREAD = "thread";
|
||||
|
@ -70,8 +72,10 @@ async function webapp_init() {
|
|||
|
||||
// WARNING Order Matters!
|
||||
init_message_textareas();
|
||||
init_my_pfp(model);
|
||||
init_postbox(model);
|
||||
init_profile();
|
||||
view_show_spinner(true);
|
||||
redraw_my_pfp(model);
|
||||
|
||||
// Load data from storage
|
||||
await model_load_settings(model);
|
||||
|
@ -80,7 +84,6 @@ async function webapp_init() {
|
|||
if (err) {
|
||||
window.alert("Unable to load contacts.");
|
||||
}
|
||||
|
||||
init_settings(model);
|
||||
|
||||
// Create our pool so that event processing functions can work
|
||||
|
@ -135,6 +138,7 @@ function on_timer_save() {
|
|||
}
|
||||
|
||||
function on_timer_tick() {
|
||||
//return;
|
||||
setTimeout(() => {
|
||||
DAMUS.relay_que.forEach((que, relay) => {
|
||||
model_fetch_next_profile(DAMUS, relay);
|
||||
|
@ -154,13 +158,23 @@ function on_pool_open(relay) {
|
|||
// Get all our info & history, well close this after we get it
|
||||
fetch_profile(pubkey, model.pool, relay);
|
||||
|
||||
// Get our notifications. We will never close this.
|
||||
// Get our notifications
|
||||
relay.subscribe(SID_NOTIFICATIONS, [{
|
||||
kinds: STANDARD_KINDS,
|
||||
"#p": [pubkey],
|
||||
limit: 5000,
|
||||
}]);
|
||||
|
||||
// Get our dms. You have to do 2 separate queries: ours out and others in
|
||||
relay.subscribe(SID_DMS_IN, [{
|
||||
kinds: [KIND_DM],
|
||||
"#p": [pubkey],
|
||||
}]);
|
||||
relay.subscribe(SID_DMS_OUT, [{
|
||||
kinds: [KIND_DM],
|
||||
authors: [pubkey],
|
||||
}]);
|
||||
|
||||
// Subscribe to the world as it will serve our friends, notifications, and
|
||||
// explore views
|
||||
relay.subscribe(SID_EXPLORE, [{
|
||||
|
@ -185,7 +199,6 @@ async function on_pool_eose(relay, sub_id) {
|
|||
log_info(`EOSE(${relay.url}): ${sub_id}`);
|
||||
const model = DAMUS;
|
||||
const { pool } = model;
|
||||
|
||||
const index = sub_id.indexOf(":");
|
||||
const sid = sub_id.slice(0, index >= 0 ? index : sub_id.length);
|
||||
const identifier = sub_id.slice(index+1);
|
||||
|
@ -207,6 +220,8 @@ async function on_pool_eose(relay, sub_id) {
|
|||
}
|
||||
case SID_NOTIFICATIONS:
|
||||
case SID_PROFILES:
|
||||
case SID_DMS_OUT:
|
||||
case SID_DMS_IN:
|
||||
pool.unsubscribe(sub_id, relay);
|
||||
break;
|
||||
}
|
||||
|
|
81
js/model.js
81
js/model.js
|
@ -27,6 +27,9 @@ function model_process_event(model, relay, ev) {
|
|||
case KIND_REACTION:
|
||||
fn = model_process_event_reaction;
|
||||
break;
|
||||
case KIND_DM:
|
||||
fn = model_process_event_dm;
|
||||
break;
|
||||
}
|
||||
if (fn)
|
||||
fn(model, ev, !!relay);
|
||||
|
@ -45,9 +48,6 @@ function model_process_event(model, relay, ev) {
|
|||
model_que_profile(model, relay, pubkey);
|
||||
}
|
||||
});
|
||||
|
||||
// TODO fetch unknown referenced events & pubkeys from this event
|
||||
// TODO notify user of new events aimed at them!
|
||||
}
|
||||
|
||||
function model_get_relay_que(model, relay) {
|
||||
|
@ -123,9 +123,9 @@ function model_process_event_metadata(model, ev, update_view) {
|
|||
// If it's my pubkey let's redraw my pfp that is not located in the view
|
||||
// This has to happen regardless of update_view because of the it's not
|
||||
// related to events
|
||||
if (profile.pubkey == model.pubkey) {
|
||||
/*if (profile.pubkey == model.pubkey) {
|
||||
redraw_my_pfp(model);
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
function model_has_profile(model, pk) {
|
||||
|
@ -153,6 +153,76 @@ function model_process_event_following(model, ev, update_view) {
|
|||
// load_our_relays(model.pubkey, model.pool, ev)
|
||||
}
|
||||
|
||||
function event_is_dm(ev, mykey) {
|
||||
if (ev.kind != KIND_DM)
|
||||
return false;
|
||||
if (ev.pubkey != mykey && event_tags_pubkey(ev, mykey))
|
||||
return true;
|
||||
return ev.pubkey == mykey;
|
||||
}
|
||||
|
||||
/* model_process_event_dm updates the internal dms hash map based on dms
|
||||
* targeted at the user.
|
||||
*/
|
||||
function model_process_event_dm(model, ev, update_view) {
|
||||
if (!event_is_dm(ev, model.pubkey))
|
||||
return;
|
||||
// We have to identify who the target DM is for since we are also in the
|
||||
// chat. We simply use the first non-us key we find as the target. I am not
|
||||
// sure that multi-sig chats are possible at this time in the spec. If no
|
||||
// target, it's a bad DM.
|
||||
let target;
|
||||
const keys = event_get_pubkeys(ev);
|
||||
for (let key of keys) {
|
||||
target = key;
|
||||
if (key == model.pubkey)
|
||||
continue;
|
||||
break;
|
||||
}
|
||||
if (!target)
|
||||
return;
|
||||
let dm = model_get_dm(model, target);
|
||||
dm.needs_decryption = true;
|
||||
dm.needs_redraw = true;
|
||||
// It may be faster to not use binary search due to the newest always being
|
||||
// at the front - but I could be totally wrong. Potentially it COULD be
|
||||
// slower during history if history is imported ASCENDINGLY. But anything
|
||||
// after this will always be faster and is insurance (race conditions).
|
||||
let i = 0;
|
||||
for (; i < dm.events.length; i++) {
|
||||
const b = dm.events[i];
|
||||
if (ev.created_at > b.created_at)
|
||||
break;
|
||||
}
|
||||
dm.events.splice(i, 0, ev);
|
||||
|
||||
// Check if DM is new
|
||||
const b = model.all_events[dm.last_seen];
|
||||
if (!b || b.created_at < ev.created_at) {
|
||||
// TODO update notification UI
|
||||
dm.new_count++;
|
||||
}
|
||||
}
|
||||
|
||||
function model_get_dm(model, target) {
|
||||
if (!model.dms.has(target)) {
|
||||
// TODO think about using "pubkey:subject" so we have threads
|
||||
model.dms.set(target, {
|
||||
pubkey: target,
|
||||
// events is an ordered list (new to old) of events referenced from
|
||||
// all_events. It should not be a copy to reduce memory.
|
||||
events: [],
|
||||
// Last read event by the client/user
|
||||
last_seen: "",
|
||||
new_count: 0,
|
||||
// Notifies the renderer that this dm is out of date
|
||||
needs_redraw: false,
|
||||
needs_decryption: false,
|
||||
});
|
||||
}
|
||||
return model.dms.get(target);
|
||||
}
|
||||
|
||||
/* model_process_event_reaction updates the reactions dictionary
|
||||
*/
|
||||
function model_process_event_reaction(model, ev, update_view) {
|
||||
|
@ -388,6 +458,7 @@ function new_model() {
|
|||
friends: new Set(),
|
||||
friend_of_friends: new Set(),
|
||||
},
|
||||
dms: new Map(), // pubkey => event list
|
||||
invalidated: [], // event ids which should be added/removed
|
||||
elements: {}, // map of evid > rendered element
|
||||
relay_que: new Map(),
|
||||
|
|
81
js/ui/dm.js
Normal file
81
js/ui/dm.js
Normal file
|
@ -0,0 +1,81 @@
|
|||
function view_dm_update(model) {
|
||||
const el = find_node("#dms");
|
||||
const order = [];
|
||||
model.dms.forEach((dm, pubkey, m) => {
|
||||
if (!dm.events.length)
|
||||
return;
|
||||
const i = arr_bsearch_insert(order, dm, dm_cmp);
|
||||
order.splice(i, 0, dm);
|
||||
if (!dm.needs_redraw)
|
||||
return;
|
||||
let gel = find_node(`[data-pubkey='${pubkey}']`, el);
|
||||
if (!gel) {
|
||||
gel = new_el_dmgroup(model, dm);
|
||||
gel.addEventListener("click", onclick_dm);
|
||||
el.appendChild(gel);
|
||||
}
|
||||
update_el_dmgroup(model, dm, gel);
|
||||
dm.needs_redraw = false;
|
||||
});
|
||||
|
||||
// I'm not sure what is faster, doing a frag update all at once OR just
|
||||
// updating individual positions. If they all update it's slower, but the
|
||||
// chances of them all updating is is small and only garuenteed when it
|
||||
// draws the first time.
|
||||
//const frag = new DocumentFragment();
|
||||
for (let i = 0; i < order.length; i++) {
|
||||
let dm = order[i];
|
||||
let xel = el.children[i];
|
||||
if (dm.pubkey == xel.dataset.pubkey)
|
||||
continue;
|
||||
let gel = find_node(`[data-pubkey='${order[i].pubkey}']`, el);
|
||||
el.insertBefore(gel, xel);
|
||||
//frag.appendChild(gel);
|
||||
}
|
||||
//el.appendChild(frag);
|
||||
}
|
||||
|
||||
function dm_cmp(a, b) {
|
||||
const x = a.events[0].created_at;
|
||||
const y = b.events[0].created_at;
|
||||
if (x > y)
|
||||
return -1;
|
||||
if (x < y)
|
||||
return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
function update_el_dmgroup(model, dm, el) {
|
||||
const ev = dm.events[0];
|
||||
const profile = model_get_profile(model, dm.pubkey);
|
||||
const message = ev.decrypted || ev.content || "No Message.";
|
||||
const time = fmt_datetime(new Date(ev.created_at * 1000));
|
||||
const cel = find_node(".count", el)
|
||||
cel.innerText = dm.new_count;
|
||||
cel.classList.toggle("active", dm.new_count > 0);
|
||||
find_node(".time", el).innerText = time;
|
||||
find_node(".message", el).innerText = message;
|
||||
find_node(".username", el).innerText = fmt_name(profile);
|
||||
}
|
||||
|
||||
function new_el_dmgroup(model, dm) {
|
||||
const profile = model_get_profile(model, dm.pubkey);
|
||||
return html2el(`<div class="dm-group bottom-border clickable" data-pubkey="${dm.pubkey}">
|
||||
<div>${render_profile_img(profile, true)}</div>
|
||||
<div class="content">
|
||||
<div class="count"></div>
|
||||
<label class="username" data-pubkey="${dm.pubkey}"></label>
|
||||
<p class="message"></p>
|
||||
<label class="time"></label>
|
||||
</div>
|
||||
</div>`);
|
||||
}
|
||||
|
||||
function onclick_dm(ev) {
|
||||
const el = find_parent(ev.target, "[data-pubkey]");
|
||||
if (!el || !el.dataset.pubkey) {
|
||||
log_error("did not find dm pubkey");
|
||||
return;
|
||||
}
|
||||
view_timeline_apply_mode(DAMUS, VM_DM_THREAD, {pubkey: el.dataset.pubkey});
|
||||
}
|
26
js/ui/fmt.js
26
js/ui/fmt.js
|
@ -25,12 +25,13 @@ function linkify(text="", show_media=false) {
|
|||
}
|
||||
|
||||
function format_content(ev, show_media) {
|
||||
if (ev.kind === 7) {
|
||||
if (ev.kind === KIND_REACTION) {
|
||||
if (ev.content === "" || ev.content === "+")
|
||||
return "❤️"
|
||||
return html`${ev.content.trim()}`;
|
||||
}
|
||||
const content = ev.content.trim();
|
||||
const content = (ev.kind == KIND_DM ? ev.decrypted || ev.content : ev.content)
|
||||
.trim();
|
||||
const body = fmt_body(content, show_media);
|
||||
let cw = get_content_warning(ev.tags)
|
||||
if (cw !== null) {
|
||||
|
@ -72,8 +73,9 @@ function fmt_body(content, show_media) {
|
|||
}, "")
|
||||
}
|
||||
|
||||
/* format_profile_name takes in a profile and tries it's best to return a string
|
||||
* that is best suited for the profile.
|
||||
/* DEPRECATED: use fmt_name
|
||||
* format_profile_name takes in a profile and tries it's best to
|
||||
* return a string that is best suited for the profile.
|
||||
*/
|
||||
function fmt_profile_name(profile={}, fallback="Anonymous") {
|
||||
const name = profile.display_name || profile.user || profile.name ||
|
||||
|
@ -81,7 +83,23 @@ function fmt_profile_name(profile={}, fallback="Anonymous") {
|
|||
return html`${name}`;
|
||||
}
|
||||
|
||||
function fmt_name(profile={data:{}}) {
|
||||
const { data } = profile;
|
||||
const name = data.display_name || data.user || data.name ||
|
||||
fmt_pubkey(profile.pubkey);
|
||||
return html`${name}`;
|
||||
}
|
||||
|
||||
function fmt_pubkey(pk) {
|
||||
if (!pk)
|
||||
return "Unknown";
|
||||
return pk.slice(-8)
|
||||
}
|
||||
|
||||
function fmt_datetime(d) {
|
||||
return d.getFullYear() +
|
||||
"/" + ("0" + (d.getMonth()+1)).slice(-2) +
|
||||
"/" + ("0" + d.getDate()).slice(-2) +
|
||||
" " + ("0" + d.getHours()).slice(-2) +
|
||||
":" + ("0" + d.getMinutes()).slice(-2);
|
||||
}
|
||||
|
|
115
js/ui/profile.js
Normal file
115
js/ui/profile.js
Normal file
|
@ -0,0 +1,115 @@
|
|||
const PROFILE_FIELDS = [
|
||||
'name',
|
||||
'picture',
|
||||
'nip05',
|
||||
'about',
|
||||
];
|
||||
|
||||
function open_profile(pubkey) {
|
||||
view_timeline_apply_mode(DAMUS, VM_USER, { pubkey });
|
||||
view_update_profile(DAMUS, pubkey);
|
||||
}
|
||||
|
||||
function init_profile() {
|
||||
const el = find_node("#profile-info");
|
||||
const el_pfp = find_node("[role='profile-image']", el);
|
||||
el_pfp.addEventListener("error", onerror_pfp);
|
||||
el_pfp.src = IMG_NO_USER;
|
||||
find_node("[role='message-user']", el)
|
||||
.addEventListener("click", onclick_message_user);
|
||||
find_node("[role='edit-profile']", el)
|
||||
.addEventListener("click", onclick_edit_profile);
|
||||
find_node("[role='copy-pk']", el)
|
||||
.addEventListener("click", onclick_copy_pubkey);
|
||||
find_node("[role='follow-user']", el)
|
||||
.addEventListener("click", onclick_follow_user);
|
||||
}
|
||||
|
||||
function onclick_message_user(ev) {
|
||||
const pubkey = ev.target.dataset.pubkey;
|
||||
view_timeline_apply_mode(DAMUS, VM_DM_THREAD, { pubkey });
|
||||
}
|
||||
|
||||
/* onclick_copy_pubkey writes the element's dataset.pk field to the users OS'
|
||||
* clipboard. No we don't use fallback APIs, use a recent browser.
|
||||
*/
|
||||
function onclick_copy_pubkey(ev) {
|
||||
const el = ev.target;
|
||||
navigator.clipboard.writeText(el.dataset.pk);
|
||||
// TODO show toast
|
||||
}
|
||||
|
||||
/* onclick_follow_user sends the event to the relay to subscribe the active user
|
||||
* to the target public key of the element's dataset.pk field.
|
||||
*/
|
||||
function onclick_follow_user(ev) {
|
||||
const el = ev.target;
|
||||
const { contacts } = DAMUS;
|
||||
const pubkey = el.dataset.pk;
|
||||
const is_friend = contacts.friends.has(pubkey);
|
||||
if (is_friend) {
|
||||
contacts.friends.delete(pubkey);
|
||||
} else {
|
||||
contacts.friends.add(pubkey);
|
||||
}
|
||||
el.innerText = is_friend ? "Follow" : "Unfollow";
|
||||
contacts_save(contacts);
|
||||
if (window.confirm("Contacts are saved locally. Do you want to sync you contacts with all relays?")) {
|
||||
update_contacts();
|
||||
}
|
||||
}
|
||||
|
||||
function view_update_profile(model, pubkey) {
|
||||
const profile = model_get_profile(model, pubkey);
|
||||
const el = find_node("#profile-info");
|
||||
|
||||
const name = fmt_name(profile);
|
||||
find_node("[role='profile-image']", el).src = get_profile_pic(profile);
|
||||
find_nodes("[role='profile-name']", el).forEach(el => {
|
||||
el.innerText = name;
|
||||
});
|
||||
|
||||
const el_nip5 = find_node("[role='profile-nip5']", el)
|
||||
el_nip5.innerText = profile.data.nip05;
|
||||
el_nip5.classList.toggle("hide", !profile.data.nip05);
|
||||
|
||||
const el_desc = find_node("[role='profile-desc']", el)
|
||||
el_desc.innerHTML = newlines_to_br(linkify(profile.data.about));
|
||||
el_desc.classList.toggle("hide", !profile.data.about);
|
||||
|
||||
find_node("button[role='copy-pk']", el).dataset.pk = pubkey;
|
||||
find_node("button[role='edit-profile']", el)
|
||||
.classList.toggle("hide", pubkey != model.pubkey);
|
||||
|
||||
const btn_follow = find_node("button[role='follow-user']", el);
|
||||
btn_follow.dataset.pk = pubkey;
|
||||
btn_follow.innerText = contact_is_friend(model.contacts, pubkey) ? "Unfollow" : "Follow";
|
||||
btn_follow.classList.toggle("hide", pubkey == model.pubkey);
|
||||
|
||||
const btn_message = find_node("button[role='message-user']", el);
|
||||
btn_message.dataset.pubkey = pubkey;
|
||||
}
|
||||
|
||||
function onclick_edit_profile() {
|
||||
const p = model_get_profile(DAMUS, DAMUS.pubkey);
|
||||
const el = find_node("#profile-editor");
|
||||
el.classList.remove("closed");
|
||||
for (const key of PROFILE_FIELDS) {
|
||||
find_node(`[name='${key}']`, el).value = p.data[key];
|
||||
}
|
||||
}
|
||||
|
||||
function click_update_profile() {
|
||||
const el = find_node("#profile-editor");
|
||||
const btn = find_node("button.action", el);
|
||||
const p = Object.assign({}, model_get_profile(DAMUS, DAMUS.pubkey).data, {
|
||||
name: find_node("input[name='name']", el).value,
|
||||
picture: find_node("input[name='picture']", el).value,
|
||||
nip05: find_node("input[name='nip05']", el).value,
|
||||
about: find_node("textarea[name='about']", el).value,
|
||||
});
|
||||
update_profile(p);
|
||||
close_modal(el);
|
||||
// TODO show toast that say's "broadcasted!"
|
||||
}
|
||||
|
|
@ -54,18 +54,21 @@ function render_shared_by(ev, opts) {
|
|||
}
|
||||
|
||||
function render_event(model, ev, opts={}) {
|
||||
if (ev.kind == KIND_SHARE) {
|
||||
return render_share(model, ev, opts);
|
||||
switch(ev.kind) {
|
||||
case KIND_SHARE:
|
||||
return render_share(model, ev, opts);
|
||||
case KIND_DM:
|
||||
return render_dm(model, ev, opts);
|
||||
}
|
||||
|
||||
const profile = model_get_profile(model, ev.pubkey);
|
||||
const delta = fmt_since_str(new Date().getTime(), ev.created_at*1000)
|
||||
const border_bottom = opts.is_composing ? "" : "bottom-border";
|
||||
let thread_btn = "";
|
||||
return html`<div id="ev${ev.id}" class="event ${border_bottom}">
|
||||
let classes = "event"
|
||||
if (!opts.is_composing)
|
||||
classes += " bottom-border";
|
||||
return html`<div id="ev${ev.id}" class="${classes}">
|
||||
<div class="userpic">
|
||||
$${render_pfp(ev.pubkey, profile.data)}
|
||||
</div>
|
||||
$${render_profile_img(profile)}</div>
|
||||
<div class="event-content">
|
||||
<div class="info">
|
||||
$${render_name(ev.pubkey, profile.data)}
|
||||
|
@ -78,12 +81,43 @@ function render_event(model, ev, opts={}) {
|
|||
</div>`
|
||||
}
|
||||
|
||||
function render_dm(model, ev, opts) {
|
||||
let classes = "event"
|
||||
if (ev.kind == KIND_DM) {
|
||||
classes += " dm";
|
||||
if (ev.pubkey == model.pubkey)
|
||||
classes += " mine";
|
||||
}
|
||||
const profile = model_get_profile(model, ev.pubkey);
|
||||
const delta = fmt_since_str(new Date().getTime(), ev.created_at*1000)
|
||||
let show_media = event_shows_media(model, ev, model.embeds);
|
||||
return html`<div id="ev${ev.id}" class="${classes}">
|
||||
<div class="wrap">
|
||||
<div class="body">
|
||||
<p>$${format_content(ev, show_media)}</p>
|
||||
</div>
|
||||
<div class="timestamp" data-timestamp="${ev.created_at}">${delta}</div>
|
||||
</div>
|
||||
</div>`
|
||||
}
|
||||
|
||||
function event_shows_media(model, ev, mode) {
|
||||
if (mode == "friends")
|
||||
return model.contacts.friends.has(ev.pubkey);
|
||||
return true;
|
||||
}
|
||||
|
||||
function rerender_dm(model, ev, el) {
|
||||
let show_media = event_shows_media(model, ev, model.embeds);
|
||||
find_node(".body > p", el).innerHTML = format_content(ev, show_media);
|
||||
}
|
||||
|
||||
function render_event_nointeract(model, ev, opts={}) {
|
||||
const profile = model_get_profile(model, ev.pubkey);
|
||||
const delta = fmt_since_str(new Date().getTime(), ev.created_at*1000)
|
||||
return html`<div class="event border-bottom">
|
||||
<div class="userpic">
|
||||
$${render_pfp(ev.pubkey, profile.data)}
|
||||
$${render_profile_img(profile)}
|
||||
</div>
|
||||
<div class="event-content">
|
||||
<div class="info">
|
||||
|
@ -114,7 +148,7 @@ function render_event_body(model, ev, opts) {
|
|||
${format_content(ev, show_media)}
|
||||
</p>`;
|
||||
str += render_reactions(model, ev);
|
||||
str += opts.nobar ? "" :
|
||||
str += opts.nobar || ev.kind == KIND_DM ? "" :
|
||||
render_action_bar(model, ev, {can_delete, shared});
|
||||
return str;
|
||||
}
|
||||
|
@ -237,7 +271,20 @@ function render_name(pk, profile, prefix="") {
|
|||
// Beware of whitespace.
|
||||
return html`<span>${prefix}<span class="username clickable" data-pubkey="${pk}"
|
||||
onclick="open_profile('${pk}')"
|
||||
>${fmt_profile_name(profile, fmt_pubkey(pk))}</span></span>`
|
||||
> ${fmt_profile_name(profile, fmt_pubkey(pk))}</span></span>`
|
||||
}
|
||||
|
||||
function render_profile_img(profile, noclick=false) {
|
||||
const name = fmt_name(profile);
|
||||
let str = html`class="pfp clickable" onclick="open_profile('${profile.pubkey}')"`;
|
||||
if (noclick)
|
||||
str = "class='pfp'";
|
||||
return html`<img
|
||||
$${str}
|
||||
data-pubkey="${profile.pubkey}"
|
||||
title="${name}"
|
||||
onerror="this.onerror=null;this.src='${IMG_NO_USER}';"
|
||||
src="${get_profile_pic(profile)}"/>`
|
||||
}
|
||||
|
||||
function render_pfp(pk, profile, opts={}) {
|
||||
|
|
237
js/ui/state.js
237
js/ui/state.js
|
@ -1,6 +1,8 @@
|
|||
const VM_FRIENDS = "friends"; // mine + only events that are from my contacts
|
||||
const VM_EXPLORE = "explore"; // all events
|
||||
const VM_NOTIFICATIONS = "notifications"; // reactions & replys
|
||||
const VM_DM = "dm"; // all events of KIND_DM aimmed at user
|
||||
const VM_DM_THREAD = "dmthread"; // all events from a user of KIND_DM
|
||||
const VM_THREAD = "thread"; // all events in response to target event
|
||||
const VM_USER = "user"; // all events by pubkey
|
||||
const VM_SETTINGS = "settings";
|
||||
|
@ -27,6 +29,7 @@ function view_timeline_apply_mode(model, mode, opts={}, push_state=true) {
|
|||
// Don't do anything if we are already here
|
||||
if (el.dataset.mode == mode) {
|
||||
switch (mode) {
|
||||
case VM_DM_THREAD:
|
||||
case VM_USER:
|
||||
if (el.dataset.pubkey == opts.pubkey)
|
||||
return;
|
||||
|
@ -39,7 +42,7 @@ function view_timeline_apply_mode(model, mode, opts={}, push_state=true) {
|
|||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Push a new state to the browser history stack
|
||||
if (push_state)
|
||||
history.pushState({mode, opts}, '');
|
||||
|
@ -49,7 +52,7 @@ function view_timeline_apply_mode(model, mode, opts={}, push_state=true) {
|
|||
view_show_spinner(true);
|
||||
fetch_thread_history(thread_id, model.pool);
|
||||
}
|
||||
if (pubkey && pubkey != model.pubkey) {
|
||||
if (mode == VM_USER && pubkey && pubkey != model.pubkey) {
|
||||
view_show_spinner(true);
|
||||
fetch_profile(pubkey, model.pool);
|
||||
}
|
||||
|
@ -57,11 +60,26 @@ function view_timeline_apply_mode(model, mode, opts={}, push_state=true) {
|
|||
reset_notifications(model);
|
||||
}
|
||||
|
||||
const names = {};
|
||||
names[VM_FRIENDS] = "Home";
|
||||
names[VM_EXPLORE] = "Explore";
|
||||
names[VM_NOTIFICATIONS] = "Notifications";
|
||||
names[VM_DM] = "Messages (Alpha)";
|
||||
names[VM_DM_THREAD] = "Messages";
|
||||
names[VM_USER] = "Profile";
|
||||
names[VM_THREAD] = "Thread";
|
||||
names[VM_SETTINGS] = "Settings";
|
||||
let name = names[mode];
|
||||
let profile;
|
||||
|
||||
el.dataset.mode = mode;
|
||||
switch(mode) {
|
||||
case VM_THREAD:
|
||||
el.dataset.threadId = thread_id;
|
||||
case VM_USER:
|
||||
case VM_DM_THREAD:
|
||||
profile = model_get_profile(model, pubkey);
|
||||
name = fmt_name(profile);
|
||||
el.dataset.pubkey = pubkey;
|
||||
break;
|
||||
default:
|
||||
|
@ -70,29 +88,42 @@ function view_timeline_apply_mode(model, mode, opts={}, push_state=true) {
|
|||
break;
|
||||
}
|
||||
|
||||
const names = {};
|
||||
names[VM_FRIENDS] = "Home";
|
||||
names[VM_EXPLORE] = "Explore";
|
||||
names[VM_NOTIFICATIONS] = "Notifications";
|
||||
names[VM_USER] = "Profile";
|
||||
names[VM_THREAD] = "Thread";
|
||||
names[VM_SETTINGS] = "Settings";
|
||||
|
||||
// Do some visual updates
|
||||
find_node("#view header > label").innerText = names[mode];
|
||||
find_node("#view header > label").innerText = name;
|
||||
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("#newpost").classList.toggle("hide", mode != VM_FRIENDS);
|
||||
find_node("#newpost").classList.toggle("hide", mode != VM_FRIENDS && mode != VM_DM_THREAD);
|
||||
const timeline_el = find_node("#timeline");
|
||||
timeline_el.classList.toggle("reverse", mode == VM_THREAD);
|
||||
timeline_el.classList.toggle("hide", mode == VM_SETTINGS);
|
||||
timeline_el.classList.toggle("hide", mode == VM_SETTINGS || mode == VM_DM);
|
||||
find_node("#settings").classList.toggle("hide", mode != VM_SETTINGS);
|
||||
find_node("#dms").classList.toggle("hide", mode != VM_DM);
|
||||
find_node("#dms-not-available")
|
||||
.classList.toggle("hide", mode == VM_DM_THREAD || mode == VM_DM ?
|
||||
dms_available() : true);
|
||||
|
||||
// Show/hide different profile image in header
|
||||
el_their_pfp = find_node("#view header > img.pfp[role='their-pfp']");
|
||||
el_their_pfp.classList.toggle("hide", mode != VM_DM_THREAD);
|
||||
find_node("#view header > img.pfp[role='my-pfp']")
|
||||
.classList.toggle("hide", mode == VM_DM_THREAD);
|
||||
|
||||
view_timeline_refresh(model, mode, opts);
|
||||
|
||||
if (mode == VM_SETTINGS) {
|
||||
view_show_spinner(false);
|
||||
view_set_show_count(0, true, true);
|
||||
switch (mode) {
|
||||
case VM_DM_THREAD:
|
||||
decrypt_dms(model);
|
||||
el_their_pfp.src = get_profile_pic(profile);
|
||||
el_their_pfp.dataset.pubkey = pubkey;
|
||||
break;
|
||||
case VM_DM:
|
||||
decrypt_dms(model);
|
||||
view_dm_update(model);
|
||||
break;
|
||||
case VM_SETTINGS:
|
||||
view_show_spinner(false);
|
||||
view_set_show_count(0, true, true);
|
||||
break;
|
||||
}
|
||||
|
||||
return mode;
|
||||
|
@ -112,7 +143,9 @@ function view_timeline_refresh(model, mode, opts={}) {
|
|||
el.innerHTML = "";
|
||||
// Build DOM fragment and render it
|
||||
let count = 0;
|
||||
const evs = model_events_arr(model)
|
||||
const evs = mode == VM_DM_THREAD ?
|
||||
model_get_dm(model, opts.pubkey).events.concat().reverse() :
|
||||
model_events_arr(model);
|
||||
const fragment = new DocumentFragment();
|
||||
for (let i = evs.length - 1; i >= 0 && count < 1000; i--) {
|
||||
const ev = evs[i];
|
||||
|
@ -149,6 +182,7 @@ function view_timeline_update(model) {
|
|||
|
||||
let count = 0;
|
||||
let ncount = 0;
|
||||
let decrypted = false;
|
||||
const latest_ev = el.firstChild ?
|
||||
model.all_events[el.firstChild.id.slice(2)] : undefined;
|
||||
const left_overs = [];
|
||||
|
@ -165,9 +199,16 @@ function view_timeline_update(model) {
|
|||
continue;
|
||||
}
|
||||
|
||||
// Skip non-renderables and already created
|
||||
// Skip non-renderables
|
||||
var ev = model.all_events[evid];
|
||||
if (!event_is_renderable(ev) || model.elements[evid]) {
|
||||
if (!event_is_renderable(ev)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Re-render content of a decrypted dm
|
||||
if (ev.kind == KIND_DM && model.elements[evid]) {
|
||||
rerender_dm(model, ev, model.elements[evid]);
|
||||
decrypted = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -205,6 +246,10 @@ function view_timeline_update(model) {
|
|||
model.notifications.count += ncount;
|
||||
update_notifications(model);
|
||||
}
|
||||
// Update the dms list view
|
||||
if (decrypted) {
|
||||
view_dm_update(model);
|
||||
}
|
||||
}
|
||||
|
||||
function view_set_show_count(count, add=false, hide=false) {
|
||||
|
@ -251,6 +296,7 @@ function view_timeline_show_new(model) {
|
|||
}
|
||||
view_set_show_count(-count, true);
|
||||
view_timeline_update_timestamps();
|
||||
if (mode == VM_DM_THREAD) decrypt_dms(model);
|
||||
}
|
||||
|
||||
function view_render_event(model, ev, force=false) {
|
||||
|
@ -269,29 +315,42 @@ function view_render_event(model, ev, force=false) {
|
|||
}
|
||||
|
||||
function view_timeline_update_profiles(model, pubkey) {
|
||||
let xs, html;
|
||||
const el = view_get_timeline_el();
|
||||
const p = model_get_profile(model, pubkey);
|
||||
const name = fmt_profile_name(p.data, fmt_pubkey(pubkey));
|
||||
const pic = get_picture(pubkey, p.data)
|
||||
const name = fmt_name(p);
|
||||
const pic = get_profile_pic(p);
|
||||
for (const evid in model.elements) {
|
||||
if (!event_contains_pubkey(model.all_events[evid], pubkey))
|
||||
continue;
|
||||
const el = model.elements[evid];
|
||||
let xs;
|
||||
xs = find_nodes(`.username[data-pubkey='${pubkey}']`, el)
|
||||
xs.forEach((el)=> {
|
||||
el.innerText = name;
|
||||
});
|
||||
xs = find_nodes(`img[data-pubkey='${pubkey}']`, el)
|
||||
xs.forEach((el)=> {
|
||||
el.src = pic;
|
||||
});
|
||||
update_el_profile(model.elements[evid], pubkey, name, pic);
|
||||
}
|
||||
// Update the profile view if it's active
|
||||
if (el.dataset.mode == VM_USER && el.dataset.pubkey == pubkey) {
|
||||
view_update_profile(model, pubkey);
|
||||
if (el.dataset.pubkey == pubkey) {
|
||||
const mode = el.dataset.mode;
|
||||
switch (mode) {
|
||||
case VM_USER:
|
||||
view_update_profile(model, pubkey);
|
||||
case VM_DM_THREAD:
|
||||
find_node("#view header > label").innerText = name;
|
||||
}
|
||||
}
|
||||
// Update dm's section since they are not in our view, dm's themselves will
|
||||
// be caught by the process above.
|
||||
update_el_profile(find_node("#dms"), pubkey, name, pic);
|
||||
update_el_profile(find_node("#view header"), pubkey, name, pic);
|
||||
update_el_profile(find_node("#newpost"), pubkey, name, pic);
|
||||
}
|
||||
|
||||
function update_el_profile(el, pubkey, name, pic) {
|
||||
if (!el)
|
||||
return;
|
||||
find_nodes(`.username[data-pubkey='${pubkey}']`, el).forEach((el)=> {
|
||||
el.innerText = name;
|
||||
});
|
||||
find_nodes(`img[data-pubkey='${pubkey}']`, el).forEach((el)=> {
|
||||
el.src = pic;
|
||||
el.title = name;
|
||||
});
|
||||
}
|
||||
|
||||
function view_timeline_update_timestamps() {
|
||||
|
@ -336,6 +395,9 @@ function view_timeline_update_reaction(model, ev) {
|
|||
}
|
||||
|
||||
function view_mode_contains_event(model, ev, mode, opts={}) {
|
||||
if (mode != VM_DM_THREAD && ev.kind == KIND_DM) {
|
||||
return false;
|
||||
}
|
||||
switch(mode) {
|
||||
case VM_EXPLORE:
|
||||
return ev.kind != KIND_REACTION;
|
||||
|
@ -349,13 +411,19 @@ function view_mode_contains_event(model, ev, mode, opts={}) {
|
|||
ev.refs.root == opts.thread_id ||
|
||||
ev.refs.reply == opts.thread_id));
|
||||
case VM_NOTIFICATIONS:
|
||||
return event_refs_pubkey(ev, model.pubkey);
|
||||
return event_tags_pubkey(ev, model.pubkey);
|
||||
case VM_DM_THREAD:
|
||||
if (ev.kind != KIND_DM) return false;
|
||||
return (ev.pubkey == opts.pubkey &&
|
||||
event_tags_pubkey(ev, model.pubkey)) ||
|
||||
(ev.pubkey == model.pubkey &&
|
||||
event_tags_pubkey(ev, opts.pubkey));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function event_is_renderable(ev={}) {
|
||||
return ev.kind == KIND_NOTE || ev.kind == KIND_SHARE;
|
||||
return ev.kind == KIND_NOTE || ev.kind == KIND_SHARE || ev.kind == KIND_DM;
|
||||
}
|
||||
|
||||
function get_default_max_depth(damus, view) {
|
||||
|
@ -364,18 +432,17 @@ function get_default_max_depth(damus, view) {
|
|||
|
||||
function get_thread_max_depth(damus, view, root_id) {
|
||||
if (!view.depths[root_id])
|
||||
return get_default_max_depth(damus, view)
|
||||
|
||||
return view.depths[root_id]
|
||||
return get_default_max_depth(damus, view);
|
||||
return view.depths[root_id];
|
||||
}
|
||||
|
||||
function get_thread_root_id(damus, id) {
|
||||
const ev = damus.all_events[id]
|
||||
const ev = damus.all_events[id];
|
||||
if (!ev) {
|
||||
log_debug("expand_thread: no event found?", id)
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
return ev.refs && ev.refs.root
|
||||
return ev.refs && ev.refs.root;
|
||||
}
|
||||
|
||||
function switch_view(mode, opts) {
|
||||
|
@ -389,3 +456,89 @@ function reset_notifications(model) {
|
|||
update_notifications(model);
|
||||
}
|
||||
|
||||
function html2el(html) {
|
||||
const div = document.createElement("div");
|
||||
div.innerHTML = html;
|
||||
return div.firstChild;
|
||||
}
|
||||
|
||||
function init_my_pfp(model) {
|
||||
find_nodes(`img[role='my-pfp']`).forEach((el)=> {
|
||||
el.dataset.pubkey = model.pubkey;
|
||||
el.addEventListener("error", onerror_pfp);
|
||||
el.addEventListener("click", onclick_pfp);
|
||||
el.classList.add("clickable");
|
||||
});
|
||||
find_nodes(`img[role='their-pfp']`).forEach((el)=> {
|
||||
el.addEventListener("error", onerror_pfp);
|
||||
el.addEventListener("click", onclick_pfp);
|
||||
el.classList.add("clickable");
|
||||
});
|
||||
}
|
||||
|
||||
function init_postbox(model) {
|
||||
const el = find_node("#newpost");
|
||||
find_node("textarea", el).addEventListener("input", oninput_post);
|
||||
find_node("button[role='send']").addEventListener("click", onclick_send);
|
||||
find_node("button[role='toggle-cw']")
|
||||
.addEventListener("click", onclick_toggle_cw);
|
||||
}
|
||||
async function onclick_send(ev) {
|
||||
const el = view_get_timeline_el();
|
||||
const mode = el.dataset.mode;
|
||||
const pubkey = await get_pubkey();
|
||||
const el_input = document.querySelector("#post-input");
|
||||
const el_cw = document.querySelector("#content-warning-input");
|
||||
let post = {
|
||||
pubkey,
|
||||
kind: KIND_NOTE,
|
||||
created_at: new_creation_time(),
|
||||
content: el_input.value,
|
||||
tags: el_cw.value ? [["content-warning", el_cw.value]] : [],
|
||||
}
|
||||
|
||||
// Handle DM type post
|
||||
if (mode == VM_DM_THREAD) {
|
||||
if (!dms_available()) {
|
||||
window.alert("DMing not available.");
|
||||
return;
|
||||
}
|
||||
post.kind = KIND_DM;
|
||||
const target = el.dataset.pubkey;
|
||||
post.tags.splice(0, 0, ["p", target]);
|
||||
post.content = await window.nostr.nip04.encrypt(target, post.content);
|
||||
}
|
||||
|
||||
// Send it
|
||||
post.id = await nostrjs.calculate_id(post)
|
||||
post = await sign_event(post)
|
||||
broadcast_event(post);
|
||||
|
||||
// Reset UI
|
||||
el_input.value = "";
|
||||
el_cw.value = "";
|
||||
trigger_postbox_assess(el_input);
|
||||
}
|
||||
/* oninput_post checks the content of the textarea and updates the size
|
||||
* of it's element. Additionally I will toggle the enabled state of the sending
|
||||
* button.
|
||||
*/
|
||||
function oninput_post(ev) {
|
||||
trigger_postbox_assess(ev.target);
|
||||
}
|
||||
function trigger_postbox_assess(el) {
|
||||
el.style.height = `0px`;
|
||||
el.style.height = `${el.scrollHeight}px`;
|
||||
let btn = el.parentElement.querySelector("button[role=send]");
|
||||
if (btn) btn.disabled = el.value === "";
|
||||
}
|
||||
/* toggle_cw changes the active stage of the Content Warning for a post. It is
|
||||
* relative to the element that is pressed.
|
||||
*/
|
||||
function onclick_toggle_cw(ev) {
|
||||
const el = ev.target;
|
||||
el.classList.toggle("active");
|
||||
const isOn = el.classList.contains("active");
|
||||
const input = el.parentElement.querySelector("input.cw");
|
||||
input.classList.toggle("hide", !isOn);
|
||||
}
|
||||
|
|
155
js/ui/util.js
155
js/ui/util.js
|
@ -3,16 +3,6 @@
|
|||
* this file grows specific UI area code should be migrated to its own file.
|
||||
*/
|
||||
|
||||
/* toggle_cw changes the active stage of the Content Warning for a post. It is
|
||||
* relative to the element that is pressed.
|
||||
*/
|
||||
function toggle_cw(el) {
|
||||
el.classList.toggle("active");
|
||||
const isOn = el.classList.contains("active");
|
||||
const input = el.parentElement.querySelector("input.cw");
|
||||
input.classList.toggle("hide", !isOn);
|
||||
}
|
||||
|
||||
/* toggle_gnav hides or shows the global navigation's additional buttons based
|
||||
* on its opened state.
|
||||
*/
|
||||
|
@ -24,17 +14,6 @@ function close_gnav() {
|
|||
find_node("#gnav").classList.remove("open");
|
||||
}
|
||||
|
||||
/* post_input_changed checks the content of the textarea and updates the size
|
||||
* of it's element. Additionally I will toggle the enabled state of the sending
|
||||
* button.
|
||||
*/
|
||||
function post_input_changed(el) {
|
||||
el.style.height = `0px`;
|
||||
el.style.height = `${el.scrollHeight}px`;
|
||||
let btn = el.parentElement.querySelector("button[role=send]");
|
||||
if (btn) btn.disabled = el.value === "";
|
||||
}
|
||||
|
||||
/* init_message_textareas finds all message textareas and updates their initial
|
||||
* height based on their content (0). This is so there is no jaring affect when
|
||||
* the page loads.
|
||||
|
@ -42,7 +21,7 @@ function post_input_changed(el) {
|
|||
function init_message_textareas() {
|
||||
const els = document.querySelectorAll(".post-input");
|
||||
for (const el of els) {
|
||||
post_input_changed(el);
|
||||
trigger_postbox_assess(el);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -56,39 +35,6 @@ function update_notification_markers(active) {
|
|||
}
|
||||
}
|
||||
|
||||
/* show_profile updates the current view to the profile display and updates the
|
||||
* information to the relevant profile based on the public key passed.
|
||||
* TODO handle async waiting for relay not yet connected
|
||||
*/
|
||||
function show_profile(pk) {
|
||||
switch_view("profile");
|
||||
const model = DAMUS;
|
||||
const profile = model_get_profile(model, pk).data;
|
||||
const el = find_node("#profile-view");
|
||||
// TODO show loading indicator then render
|
||||
|
||||
find_node("[role='profile-image']", el).src = get_picture(pk, profile);
|
||||
find_nodes("[role='profile-name']", el).forEach(el => {
|
||||
el.innerText = fmt_profile_name(profile, fmt_pubkey(pk));
|
||||
});
|
||||
|
||||
const el_nip5 = find_node("[role='profile-nip5']", el)
|
||||
el_nip5.innerText = profile.nip05;
|
||||
el_nip5.classList.toggle("hide", !profile.nip05);
|
||||
|
||||
const el_desc = find_node("[role='profile-desc']", el)
|
||||
el_desc.innerHTML = newlines_to_br(profile.about);
|
||||
el_desc.classList.toggle("hide", !profile.about);
|
||||
|
||||
find_node("button[role='copy-pk']", el).dataset.pk = pk;
|
||||
|
||||
const btn_follow = find_node("button[role='follow-user']", el)
|
||||
btn_follow.dataset.pk = pk;
|
||||
// TODO check follow status
|
||||
btn_follow.innerText = 1 == 1 ? "Follow" : "Unfollow";
|
||||
btn_follow.classList.toggle("hide", pk == model.pubkey);
|
||||
}
|
||||
|
||||
/* newlines_to_br takes a string and converts all newlines to HTML 'br' tags.
|
||||
*/
|
||||
function newlines_to_br(str="") {
|
||||
|
@ -97,33 +43,6 @@ function newlines_to_br(str="") {
|
|||
}, "");
|
||||
}
|
||||
|
||||
/* click_copy_pk writes the element's dataset.pk field to the users OS'
|
||||
* clipboard. No we don't use fallback APIs, use a recent browser.
|
||||
*/
|
||||
function click_copy_pk(el) {
|
||||
// TODO show toast
|
||||
navigator.clipboard.writeText(el.dataset.pk);
|
||||
}
|
||||
|
||||
/* click_follow_user sends the event to the relay to subscribe the active user
|
||||
* to the target public key of the element's dataset.pk field.
|
||||
*/
|
||||
function click_toggle_follow_user(el) {
|
||||
const { contacts } = DAMUS;
|
||||
const pubkey = el.dataset.pk;
|
||||
const is_friend = contacts.friends.has(pubkey);
|
||||
if (is_friend) {
|
||||
contacts.friends.delete(pubkey);
|
||||
} else {
|
||||
contacts.friends.add(pubkey);
|
||||
}
|
||||
el.innerText = is_friend ? "Follow" : "Unfollow";
|
||||
contacts_save(contacts);
|
||||
if (window.confirm("Contacts are saved locally. Do you want to sync you contacts with all relays?")) {
|
||||
update_contacts();
|
||||
}
|
||||
}
|
||||
|
||||
function show_new() {
|
||||
view_timeline_show_new(DAMUS);
|
||||
}
|
||||
|
@ -207,12 +126,12 @@ function reply_all(evid) {
|
|||
reply(evid, true);
|
||||
}
|
||||
|
||||
function redraw_my_pfp(model) {
|
||||
/*function redraw_my_pfp(model) {
|
||||
const p = model_get_profile(model, model.pubkey).data;
|
||||
const html = render_pfp(model.pubkey, p || {});
|
||||
const el = document.querySelector(".my-userpic");
|
||||
el.innerHTML = html;
|
||||
}
|
||||
}*/
|
||||
|
||||
function update_favicon(path) {
|
||||
let link = document.querySelector("link[rel~='icon']");
|
||||
|
@ -279,11 +198,6 @@ function open_thread(thread_id) {
|
|||
view_timeline_apply_mode(DAMUS, VM_THREAD, { thread_id });
|
||||
}
|
||||
|
||||
function open_profile(pubkey) {
|
||||
view_timeline_apply_mode(DAMUS, VM_USER, { pubkey });
|
||||
view_update_profile(DAMUS, pubkey);
|
||||
}
|
||||
|
||||
function open_faqs() {
|
||||
find_node("#faqs").classList.remove("closed");
|
||||
}
|
||||
|
@ -298,61 +212,6 @@ function close_modal(el) {
|
|||
}
|
||||
}
|
||||
|
||||
function view_update_profile(model, pubkey) {
|
||||
const profile = model_get_profile(model, pubkey);
|
||||
const el = find_node("[role='profile-info']");
|
||||
|
||||
const name = fmt_profile_name(profile.data, fmt_pubkey(pubkey));
|
||||
find_node("#view header > label").innerText = name;
|
||||
find_node("[role='profile-image']", el).src = get_picture(pubkey, profile.data);
|
||||
find_nodes("[role='profile-name']", el).forEach(el => {
|
||||
el.innerText = name;
|
||||
});
|
||||
|
||||
const el_nip5 = find_node("[role='profile-nip5']", el)
|
||||
el_nip5.innerText = profile.data.nip05;
|
||||
el_nip5.classList.toggle("hide", !profile.data.nip05);
|
||||
|
||||
const el_desc = find_node("[role='profile-desc']", el)
|
||||
el_desc.innerHTML = newlines_to_br(linkify(profile.data.about));
|
||||
el_desc.classList.toggle("hide", !profile.data.about);
|
||||
|
||||
find_node("button[role='copy-pk']", el).dataset.pk = pubkey;
|
||||
find_node("button[role='edit-profile']", el)
|
||||
.classList.toggle("hide", pubkey != model.pubkey);
|
||||
|
||||
const btn_follow = find_node("button[role='follow-user']", el)
|
||||
btn_follow.dataset.pk = pubkey;
|
||||
// TODO check follow status
|
||||
btn_follow.innerText = contact_is_friend(model.contacts, pubkey) ? "Unfollow" : "Follow";
|
||||
btn_follow.classList.toggle("hide", pubkey == model.pubkey);
|
||||
}
|
||||
|
||||
const PROFILE_FIELDS = ['name', 'picture', 'nip05', 'about'];
|
||||
|
||||
function show_profile_editor() {
|
||||
const p = model_get_profile(DAMUS, DAMUS.pubkey);
|
||||
const el = find_node("#profile-editor");
|
||||
el.classList.remove("closed");
|
||||
for (const key of PROFILE_FIELDS) {
|
||||
find_node(`[name='${key}']`, el).value = p.data[key];
|
||||
}
|
||||
}
|
||||
|
||||
function click_update_profile() {
|
||||
const el = find_node("#profile-editor");
|
||||
const btn = find_node("button.action", el);
|
||||
const p = Object.assign({}, model_get_profile(DAMUS, DAMUS.pubkey).data, {
|
||||
name: find_node("input[name='name']", el).value,
|
||||
picture: find_node("input[name='picture']", el).value,
|
||||
nip05: find_node("input[name='nip05']", el).value,
|
||||
about: find_node("textarea[name='about']", el).value,
|
||||
});
|
||||
update_profile(p);
|
||||
close_modal(el);
|
||||
// TODO show toast that say's "broadcasted!"
|
||||
}
|
||||
|
||||
function on_click_show_event_details(evid) {
|
||||
const model = DAMUS;
|
||||
const ev = model.all_events[evid];
|
||||
|
@ -362,3 +221,11 @@ function on_click_show_event_details(evid) {
|
|||
el.classList.remove("closed");
|
||||
find_node("code", el).innerText = JSON.stringify(ev, null, "\t");
|
||||
}
|
||||
|
||||
function onclick_pfp(ev) {
|
||||
open_profile(ev.target.dataset.pubkey);
|
||||
}
|
||||
|
||||
function onerror_pfp(ev) {
|
||||
ev.target.src = IMG_NO_USER;
|
||||
}
|
||||
|
|
53
js/util.js
53
js/util.js
|
@ -56,7 +56,8 @@ function shuffle(arr) {
|
|||
}
|
||||
|
||||
/* arr_bsearch_insert finds the point in the array that an item should be
|
||||
* inserted at based on the 'cmp' function used.
|
||||
* inserted at based on the 'cmp' function used. cmp function is same as sort
|
||||
* cmp function.
|
||||
*/
|
||||
function arr_bsearch_insert(arr, item, cmp) {
|
||||
let start = 0;
|
||||
|
@ -113,6 +114,13 @@ function find_nodes(selector, parentEl) {
|
|||
return (parentEl || document).querySelectorAll(selector);
|
||||
}
|
||||
|
||||
function find_parent(el, selector) {
|
||||
while (el && !el.matches(selector)) {
|
||||
el = el.parentNode;
|
||||
}
|
||||
return el;
|
||||
}
|
||||
|
||||
/* uuidv4 returns a new uuid v4
|
||||
*/
|
||||
function uuidv4() {
|
||||
|
@ -225,7 +233,15 @@ function get_qs(loc=location.href) {
|
|||
return new URL(loc).searchParams
|
||||
}
|
||||
|
||||
function get_picture(pk, profile) {
|
||||
function get_profile_pic(profile) {
|
||||
if (profile && profile.data && profile.data.picture)
|
||||
return html`${profile.data.picture}`;
|
||||
return IMG_NO_USER;
|
||||
}
|
||||
|
||||
/* DEPRECATED use get_profile_picture
|
||||
*/
|
||||
function get_picture(profile) {
|
||||
if (!profile || !profile.picture)
|
||||
return IMG_NO_USER;
|
||||
return html`${profile.picture}`;
|
||||
|
@ -247,3 +263,36 @@ function process_json_content(ev) {
|
|||
ev.json_content = safe_parse_json(ev.content, "event json_content");
|
||||
}
|
||||
|
||||
function dms_available() {
|
||||
return window.nostr && window.nostr.nip04;
|
||||
}
|
||||
|
||||
async function decrypt_dms(model) {
|
||||
if (!dms_available()) {
|
||||
log_warn("could not decrypt messages because nostr.nip04 is not available");
|
||||
return false;
|
||||
}
|
||||
for (const item of model.dms) {
|
||||
let dm = item[1];
|
||||
if (!dm.needs_decryption)
|
||||
continue;
|
||||
for (const ev of dm.events) {
|
||||
if (ev.decrypted != undefined)
|
||||
continue;
|
||||
let str;
|
||||
try {
|
||||
str = await window.nostr.nip04.decrypt(dm.pubkey, ev.content);
|
||||
} catch (err) {
|
||||
log_error("unable to decrypt dm", ev.id, err);
|
||||
}
|
||||
if (!str)
|
||||
continue;
|
||||
ev.decrypted = str;
|
||||
model.invalidated.push(ev.id);
|
||||
}
|
||||
dm.needs_decryption = false;
|
||||
dm.needs_redraw = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue