From eb94a239e4c701fc59271df239fd6296cfb8668f Mon Sep 17 00:00:00 2001 From: Kieran Date: Wed, 11 Jan 2023 14:31:58 +0000 Subject: [PATCH 1/9] Tweak notifications number --- src/element/Relay.css | 3 --- src/index.css | 2 +- src/nostr/Connection.js | 2 +- src/pages/Layout.css | 18 +++++++++++++----- src/pages/Layout.js | 8 +++----- 5 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/element/Relay.css b/src/element/Relay.css index a83c468c..76bf2a6b 100644 --- a/src/element/Relay.css +++ b/src/element/Relay.css @@ -12,9 +12,6 @@ padding: 5px; } -.relay > div:first-child { -} - .relay-extra { padding: 5px; margin: 0 5px; diff --git a/src/index.css b/src/index.css index 94dfaa54..ab527102 100644 --- a/src/index.css +++ b/src/index.css @@ -111,7 +111,7 @@ code { } .btn-rnd { - border-radius: 25px; + border-radius: 100%; } textarea { diff --git a/src/nostr/Connection.js b/src/nostr/Connection.js index efd1a5c1..40c001e3 100644 --- a/src/nostr/Connection.js +++ b/src/nostr/Connection.js @@ -211,7 +211,7 @@ export default class Connection { this.CurrentState.events.send = this.Stats.EventsSent; this.CurrentState.avgLatency = this.Stats.Latency.length > 0 ? (this.Stats.Latency.reduce((acc, v) => acc + v, 0) / this.Stats.Latency.length) : 0; this.CurrentState.disconnects = this.Stats.Disconnects; - this.Stats.Latency = this.Stats.Latency.slice(this.Stats.Latency.length - 20); // trim + this.Stats.Latency = this.Stats.Latency.slice(-20); // trim this.HasStateChange = true; this._NotifyState(); } diff --git a/src/pages/Layout.css b/src/pages/Layout.css index e4f7bfa9..9add2f2a 100644 --- a/src/pages/Layout.css +++ b/src/pages/Layout.css @@ -1,7 +1,15 @@ -.notifications { - margin-right: 10px; -} - .unread-count { - margin-left: .2em; + width: 20px; + height: 20px; + border: 1px solid; + border-radius: 100%; + position: relative; + padding: 3px; + line-height: 1.5em; + top: -10px; + left: -10px; + font-size: small; + background-color: var(--error); + font-weight: bold; + text-align: center; } diff --git a/src/pages/Layout.js b/src/pages/Layout.js index b0801992..d853941e 100644 --- a/src/pages/Layout.js +++ b/src/pages/Layout.js @@ -59,12 +59,10 @@ export default function Layout(props) { <>
goToNotifications(e)}> - {unreadNotifications !== 0 && ( - - {unreadNotifications} - - )}
+ + {unreadNotifications > 1000 ? "..." : unreadNotifications} + ) From 593c8a4fa93eb04e021675e61e836226b83b3ab9 Mon Sep 17 00:00:00 2001 From: Kieran Date: Thu, 12 Jan 2023 09:48:39 +0000 Subject: [PATCH 2/9] DMs --- package.json | 1 + src/Text.js | 17 ++++++++++ src/element/DM.css | 26 +++++++++++++++ src/element/DM.tsx | 41 ++++++++++++++++++++++++ src/element/Note.js | 18 ++++------- src/element/NoteTime.js | 3 +- src/element/ProfileImage.js | 10 +++--- src/feed/EventPublisher.js | 62 +++++++++++++++++++++++++++++++++-- src/feed/LoginFeed.js | 6 +++- src/index.js | 10 ++++++ src/nostr/Event.js | 45 ++++++++++++++++++++++++++ src/nostr/index.ts | 9 ++++++ src/pages/ChatPage.css | 32 +++++++++++++++++++ src/pages/ChatPage.tsx | 64 +++++++++++++++++++++++++++++++++++++ src/pages/Layout.css | 2 +- src/pages/Layout.js | 13 +++++--- src/pages/MessagesPage.tsx | 36 +++++++++++++++++++++ src/state/Login.js | 26 ++++++++++++++- yarn.lock | 5 +++ 19 files changed, 396 insertions(+), 30 deletions(-) create mode 100644 src/element/DM.css create mode 100644 src/element/DM.tsx create mode 100644 src/nostr/index.ts create mode 100644 src/pages/ChatPage.css create mode 100644 src/pages/ChatPage.tsx create mode 100644 src/pages/MessagesPage.tsx diff --git a/package.json b/package.json index bd74db73..22079ccc 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "@fortawesome/free-solid-svg-icons": "^6.2.1", "@fortawesome/react-fontawesome": "^0.2.0", "@noble/secp256k1": "^1.7.0", + "@protobufjs/base64": "^1.1.2", "@reduxjs/toolkit": "^1.9.1", "@types/jest": "^29.2.5", "@types/node": "^18.11.18", diff --git a/src/Text.js b/src/Text.js index 883d5cbd..3cd71e3a 100644 --- a/src/Text.js +++ b/src/Text.js @@ -6,6 +6,7 @@ import { UrlRegex, FileExtensionRegex, MentionRegex, InvoiceRegex, YoutubeUrlReg import { eventLink, hexToBech32, profileLink } from "./Util"; import LazyImage from "./element/LazyImage"; import Hashtag from "./element/Hashtag"; +import { useMemo } from "react"; function transformHttpLink(a) { try { @@ -135,3 +136,19 @@ export function extractHashtags(fragments) { return f; }).flat(); } + +export default function Text({ content, transforms }) { + const transformed = useMemo(() => { + let fragments = [content]; + transforms?.forEach(a => { + fragments = a(fragments); + }); + fragments = extractLinks(fragments); + fragments = extractInvoices(fragments); + fragments = extractHashtags(fragments); + + return fragments; + }, [content]); + + return transformed; +} \ No newline at end of file diff --git a/src/element/DM.css b/src/element/DM.css new file mode 100644 index 00000000..cb3e343f --- /dev/null +++ b/src/element/DM.css @@ -0,0 +1,26 @@ +.dm { + padding: 8px; + background-color: var(--gray); + margin-bottom: 5px; + border-radius: 5px; + width: fit-content; + min-width: 100px; + max-width: 90%; + overflow: hidden; +} + +.dm > div:first-child { + color: var(--gray-light); + font-size: small; + margin-bottom: 3px; +} + +.dm.me { + align-self: flex-end; + background-color: var(--gray-secondary); +} + +.dm img, .dm video, .dm iframe { + max-width: 100%; + max-height: 500px; +} \ No newline at end of file diff --git a/src/element/DM.tsx b/src/element/DM.tsx new file mode 100644 index 00000000..328d13ed --- /dev/null +++ b/src/element/DM.tsx @@ -0,0 +1,41 @@ +import "./DM.css"; +import { useEffect, useState } from "react"; +import { useSelector } from "react-redux"; + +// @ts-ignore +import useEventPublisher from "../feed/EventPublisher"; +// @ts-ignore +import Event from "../nostr/Event"; +// @ts-ignore +import NoteTime from "./NoteTime"; +// @ts-ignore +import Text from "../Text"; + +export type DMProps = { + data: any +} + +export default function DM(props: DMProps) { + const pubKey = useSelector(s => s.login.publicKey); + const publisher = useEventPublisher(); + const [content, setContent] = useState("Loading..."); + + async function decrypt() { + let e = Event.FromObject(props.data); + let decrypted = await publisher.decryptDm(e); + setContent(decrypted); + } + + useEffect(() => { + decrypt().catch(console.error); + }, [props.data]); + + return ( +
+
+
+ +
+
+ ) +} \ No newline at end of file diff --git a/src/element/Note.js b/src/element/Note.js index c24d79ed..f51fd502 100644 --- a/src/element/Note.js +++ b/src/element/Note.js @@ -5,7 +5,7 @@ import { useNavigate } from "react-router-dom"; import Event from "../nostr/Event"; import ProfileImage from "./ProfileImage"; -import { extractLinks, extractMentions, extractInvoices, extractHashtags } from "../Text"; +import Text, { extractMentions } from "../Text"; import { eventLink, hexToBech32 } from "../Util"; import NoteFooter from "./NoteFooter"; import NoteTime from "./NoteTime"; @@ -28,19 +28,13 @@ export default function Note(props) { const transformBody = useCallback(() => { let body = ev?.Content ?? ""; - - let fragments = extractLinks([body]); - fragments = extractMentions(fragments, ev.Tags, users); - fragments = extractInvoices(fragments); - fragments = extractHashtags(fragments); if (deletion?.length > 0) { - return ( - <> - Deleted - - ); + return (Deleted); } - return fragments; + const mentions = (fragments) => { + return extractMentions(fragments, ev.Tags, users); + } + return ; }, [data, dataEvent, reactions, deletion]); function goToEvent(e, id) { diff --git a/src/element/NoteTime.js b/src/element/NoteTime.js index 9b2ebc77..dcdc94d1 100644 --- a/src/element/NoteTime.js +++ b/src/element/NoteTime.js @@ -4,8 +4,7 @@ const MinuteInMs = 1_000 * 60; const HourInMs = MinuteInMs * 60; const DayInMs = HourInMs * 24; -export default function NoteTime(props) { - const from = props.from; +export default function NoteTime({ from }) { const [time, setTime] = useState(""); function calcTime() { diff --git a/src/element/ProfileImage.js b/src/element/ProfileImage.js index cdc35582..828c583c 100644 --- a/src/element/ProfileImage.js +++ b/src/element/ProfileImage.js @@ -7,7 +7,7 @@ import useProfile from "../feed/ProfileFeed"; import { hexToBech32, profileLink } from "../Util"; import LazyImage from "./LazyImage"; -export default function ProfileImage({ pubkey, subHeader, showUsername = true, className }) { +export default function ProfileImage({ pubkey, subHeader, showUsername = true, className, link }) { const navigate = useNavigate(); const user = useProfile(pubkey); @@ -23,12 +23,12 @@ export default function ProfileImage({ pubkey, subHeader, showUsername = true, c }, [user]); return ( -
- navigate(profileLink(pubkey))} /> +
+ navigate(link ?? profileLink(pubkey))} /> {showUsername && (
- {name} + {name} {subHeader ? <>{subHeader} : null} -
+
)}
) diff --git a/src/feed/EventPublisher.js b/src/feed/EventPublisher.js index 27edb674..1e51025b 100644 --- a/src/feed/EventPublisher.js +++ b/src/feed/EventPublisher.js @@ -20,7 +20,7 @@ export default function useEventPublisher() { async function signEvent(ev) { if (hasNip07 && !privKey) { ev.Id = await ev.CreateId(); - let tmpEv = await window.nostr.signEvent(ev.ToObject()); + let tmpEv = await barierNip07(() => window.nostr.signEvent(ev.ToObject())); return Event.FromObject(tmpEv); } else { await ev.Sign(privKey); @@ -72,7 +72,7 @@ export default function useEventPublisher() { ev.Tags.push(new Tag(["e", replyTo.Id, "", "reply"], ev.Tags.length)); ev.Tags.push(new Tag(["p", replyTo.PubKey], ev.Tags.length)); for (let pk of thread.PubKeys) { - if(pk === pubKey) { + if (pk === pubKey) { continue; // dont tag self in replies } ev.Tags.push(new Tag(["p", pk], ev.Tags.length)); @@ -152,6 +152,62 @@ export default function useEventPublisher() { ev.Tags.push(new Tag(["e", note.Id])); ev.Tags.push(new Tag(["p", note.PubKey])); return await signEvent(ev); + }, + decryptDm: async (note) => { + if (note.PubKey !== pubKey && !note.Tags.some(a => a.PubKey === pubKey)) { + return ""; + } + try { + let otherPubKey = note.PubKey === pubKey ? note.Tags.filter(a => a.Key === "p")[0].PubKey : note.PubKey; + if (hasNip07 && !privKey) { + return await barierNip07(() => window.nostr.nip04.decrypt(otherPubKey, note.Content)); + } else if(privKey) { + await note.DecryptDm(privKey, otherPubKey); + return note.Content; + } + } catch (e) { + console.error("Decyrption failed", e); + return ""; + } + return "test"; + }, + sendDm: async (content, to) => { + let ev = Event.ForPubKey(pubKey); + ev.Kind = EventKind.DirectMessage; + ev.Content = content; + ev.Tags.push(new Tag(["p", to])); + + try { + if (hasNip07 && !privKey) { + let ev = await barierNip07(() => window.nostr.nip04.encrypt(to, content)); + return await signEvent(ev); + } else if(privKey) { + await ev.EncryptDmForPubkey(to, privKey); + return await signEvent(ev); + } + } catch (e) { + console.error("Encryption failed", e); + } } } -} \ No newline at end of file +} + +let isNip07Busy = false; + +const delay = (t) => { + return new Promise((resolve, reject) => { + setTimeout(resolve, t); + }); +} + +const barierNip07 = async (then) => { + while (isNip07Busy) { + await delay(10); + } + isNip07Busy = true; + try { + return await then(); + } finally { + isNip07Busy = false; + } +}; \ No newline at end of file diff --git a/src/feed/LoginFeed.js b/src/feed/LoginFeed.js index 629b17f2..ab121cbc 100644 --- a/src/feed/LoginFeed.js +++ b/src/feed/LoginFeed.js @@ -2,7 +2,7 @@ import { useEffect, useMemo } from "react"; import { useDispatch, useSelector } from "react-redux"; import EventKind from "../nostr/EventKind"; import { Subscriptions } from "../nostr/Subscriptions"; -import { addNotifications, setFollows, setRelays } from "../state/Login"; +import { addDirectMessage, addNotifications, setFollows, setRelays } from "../state/Login"; import { setUserData } from "../state/Users"; import useSubscription from "./Subscription"; import { mapEventToProfile } from "./UsersFeed"; @@ -24,9 +24,11 @@ export default function useLoginFeed() { sub.Authors.add(pubKey); sub.Kinds.add(EventKind.ContactList); sub.Kinds.add(EventKind.SetMetadata); + sub.Kinds.add(EventKind.DirectMessage); let notifications = new Subscriptions(); notifications.Kinds.add(EventKind.TextNote); + notifications.Kinds.add(EventKind.DirectMessage); notifications.PTags.add(pubKey); notifications.Limit = 100; sub.AddSubscription(notifications); @@ -40,6 +42,7 @@ export default function useLoginFeed() { let contactList = notes.filter(a => a.kind === EventKind.ContactList); let notifications = notes.filter(a => a.kind === EventKind.TextNote); let metadata = notes.filter(a => a.kind === EventKind.SetMetadata).map(a => mapEventToProfile(a)); + let dms = notes.filter(a => a.kind === EventKind.DirectMessage); for (let cl of contactList) { if (cl.content !== "") { @@ -58,5 +61,6 @@ export default function useLoginFeed() { } dispatch(addNotifications(notifications)); dispatch(setUserData(metadata)); + dispatch(addDirectMessage(dms)); }, [notes]); } \ No newline at end of file diff --git a/src/index.js b/src/index.js index 10ec5f17..e834baa9 100644 --- a/src/index.js +++ b/src/index.js @@ -20,6 +20,8 @@ import NewUserPage from './pages/NewUserPage'; import SettingsPage from './pages/SettingsPage'; import ErrorPage from './pages/ErrorPage'; import VerificationPage from './pages/Verification'; +import MessagesPage from './pages/MessagesPage'; +import ChatPage from './pages/ChatPage'; /** * Nostr websocket managment system @@ -62,6 +64,14 @@ const router = createBrowserRouter([ { path: "/verification", element: + }, + { + path: "/messages", + element: + }, + { + path: "/messages/:id", + element: } ] } diff --git a/src/nostr/Event.js b/src/nostr/Event.js index 058b462e..88f07d47 100644 --- a/src/nostr/Event.js +++ b/src/nostr/Event.js @@ -1,4 +1,5 @@ import * as secp from '@noble/secp256k1'; +import base64 from "@protobufjs/base64" import EventKind from "./EventKind"; import Tag from './Tag'; import Thread from './Thread'; @@ -165,4 +166,48 @@ export default class Event { ev.PubKey = pubKey; return ev; } + + /** + * Encrypt the message content in place + * @param {string} pubkey + * @param {string} privkey + */ + async EncryptDmForPubkey(pubkey, privkey) { + let key = await this._GetDmSharedKey(pubkey, privkey); + let iv = window.crypto.getRandomValues(new Uint8Array(16)); + let data = new TextEncoder().encode(this.Content); + let result = await window.crypto.subtle.encrypt({ + name: "AES-CBC", + iv: iv + }, key, data); + let uData = new Uint8Array(result); + this.Content = `${base64.encode(uData, 0, result.byteLength)}?iv=${base64.encode(iv, 0, 16)}`; + } + + /** + * Decrypt the content of this message in place + * @param {string} privkey + * @param {string} pubkey + */ + async DecryptDm(privkey, pubkey) { + let key = await this._GetDmSharedKey(pubkey, privkey); + let cSplit = this.Content.split("?iv="); + let data = new Uint8Array(base64.length(cSplit[0])); + base64.decode(cSplit[0], data, 0); + + let iv = new Uint8Array(base64.length(cSplit[1])); + base64.decode(cSplit[1], iv, 0); + + let result = await window.crypto.subtle.decrypt({ + name: "AES-CBC", + iv: iv + }, key, data); + this.Content = new TextDecoder().decode(result); + } + + async _GetDmSharedKey(pubkey, privkey) { + let sharedPoint = secp.getSharedSecret(privkey, '02' + pubkey); + let sharedX = sharedPoint.slice(1, 33); + return await window.crypto.subtle.importKey("raw", sharedX, { name: "AES-CBC" }, false, ["encrypt", "decrypt"]) + } } \ No newline at end of file diff --git a/src/nostr/index.ts b/src/nostr/index.ts new file mode 100644 index 00000000..ce45564e --- /dev/null +++ b/src/nostr/index.ts @@ -0,0 +1,9 @@ +export interface RawEvent { + id: string, + pubkey: string, + created_at: number, + kind: number, + tags: string[][], + content: string, + sig: string +} \ No newline at end of file diff --git a/src/pages/ChatPage.css b/src/pages/ChatPage.css new file mode 100644 index 00000000..2e49d634 --- /dev/null +++ b/src/pages/ChatPage.css @@ -0,0 +1,32 @@ +.dm-list { + overflow-y: auto; + overflow-x: hidden; + height: calc(100vh - 66px - 50px - 70px); +} + +.dm-list > div { + display: flex; + flex-direction: column; + margin-bottom: 10px; +} + +.write-dm { + position: fixed; + bottom: 0; + background-color: var(--gray-light); + width: inherit; + border-radius: 5px 5px 0 0; +} + +.write-dm .inner { + display: flex; + align-items: center; + padding: 10px 5px; +} +.write-dm textarea { + resize: none; +} + +.write-dm-spacer { + margin-bottom: 80px; +} \ No newline at end of file diff --git a/src/pages/ChatPage.tsx b/src/pages/ChatPage.tsx new file mode 100644 index 00000000..4db43e63 --- /dev/null +++ b/src/pages/ChatPage.tsx @@ -0,0 +1,64 @@ +import "./ChatPage.css"; +import { useMemo, useState } from "react"; +import { useSelector } from "react-redux"; +import { useParams } from "react-router-dom"; + +// @ts-ignore +import ProfileImage from "../element/ProfileImage"; +// @ts-ignore +import { bech32ToHex } from "../Util"; +// @ts-ignore +import useEventPublisher from "../feed/EventPublisher"; + +import DM from "../element/DM"; +import { RawEvent } from "../nostr"; + +type RouterParams = { + id: string +} + +export default function ChatPage() { + const params = useParams(); + const publisher = useEventPublisher(); + const id = bech32ToHex(params.id); + const dms = useSelector(s => filterDms(s.login.dms, s.login.publicKey)); + const [content, setContent] = useState(); + + function filterDms(dms: RawEvent[], myPubkey: string) { + return dms.filter(a => { + if (a.pubkey === myPubkey && a.tags.some(b => b[0] === "p" && b[1] === id)) { + return true; + } else if (a.pubkey === id && a.tags.some(b => b[0] === "p" && b[1] === myPubkey)) { + return true; + } + return false; + }); + } + + const sortedDms = useMemo(() => { + return [...dms].sort((a, b) => a.created_at - b.created_at) + }, [dms]); + + async function sendDm() { + let ev = await publisher.sendDm(content, id); + console.debug(ev); + publisher.broadcast(ev); + } + + return ( + <> + +
+
+ {sortedDms.slice(-10).map(a => )} +
+
+
+
+ +
sendDm()}>Send
+
+
+ + ) +} \ No newline at end of file diff --git a/src/pages/Layout.css b/src/pages/Layout.css index 9add2f2a..7725e9b8 100644 --- a/src/pages/Layout.css +++ b/src/pages/Layout.css @@ -12,4 +12,4 @@ background-color: var(--error); font-weight: bold; text-align: center; -} +} \ No newline at end of file diff --git a/src/pages/Layout.js b/src/pages/Layout.js index d853941e..1a9a7137 100644 --- a/src/pages/Layout.js +++ b/src/pages/Layout.js @@ -2,7 +2,7 @@ import "./Layout.css"; import { useEffect } from "react" import { useDispatch, useSelector } from "react-redux"; import { Outlet, useNavigate } from "react-router-dom"; -import { faBell } from "@fortawesome/free-solid-svg-icons"; +import { faBell, faMessage } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { System } from ".." @@ -57,12 +57,15 @@ export default function Layout(props) { const unreadNotifications = notifications?.filter(a => (a.created_at * 1000) > readNotifications).length; return ( <> -
goToNotifications(e)}> +
navigate("/messages")}> + +
+
goToNotifications(e)}>
- - {unreadNotifications > 1000 ? "..." : unreadNotifications} - + {unreadNotifications > 0 && ( + {unreadNotifications > 100 ? ">99" : unreadNotifications} + )} ) diff --git a/src/pages/MessagesPage.tsx b/src/pages/MessagesPage.tsx new file mode 100644 index 00000000..d8785cdc --- /dev/null +++ b/src/pages/MessagesPage.tsx @@ -0,0 +1,36 @@ +import { useMemo } from "react"; +import { useSelector } from "react-redux" + +import { RawEvent } from "../nostr"; + +// @ts-ignore +import ProfileImage from "../element/ProfileImage"; +// @ts-ignore +import { hexToBech32 } from "../Util"; + +export default function MessagesPage() { + const pubKey = useSelector(s => s.login.publicKey); + const dms = useSelector(s => s.login.dms); + + const pubKeys = useMemo(() => { + return Array.from(new Set(dms.map(a => a.pubkey))); + }, [dms]); + + function person(pubkey: string) { + return ( +
+ + + {dms?.filter(a => a.pubkey === pubkey && a.pubkey !== pubKey).length} + +
+ ) + } + + return ( + <> +

Messages

+ {pubKeys.map(person)} + + ) +} \ No newline at end of file diff --git a/src/state/Login.js b/src/state/Login.js index 1c812686..c616cb11 100644 --- a/src/state/Login.js +++ b/src/state/Login.js @@ -43,6 +43,11 @@ const LoginSlice = createSlice({ * Timestamp of last read notification */ readNotifications: 0, + + /** + * Encrypted DM's + */ + dms: [] }, reducers: { init: (state) => { @@ -126,6 +131,25 @@ const LoginSlice = createSlice({ ]; } }, + addDirectMessage: (state, action) => { + let n = action.payload; + if (!Array.isArray(n)) { + n = [n]; + } + + let didChange = false; + for (let x of n) { + if (!state.dms.some(a => a.id === x.id)) { + state.dms.push(x); + didChange = true; + } + } + if (didChange) { + state.dms = [ + ...state.dms + ]; + } + }, logout: (state) => { window.localStorage.removeItem(PrivateKeyItem); window.localStorage.removeItem(PublicKeyItem); @@ -144,5 +168,5 @@ const LoginSlice = createSlice({ } }); -export const { init, setPrivateKey, setPublicKey, setRelays, removeRelay, setFollows, addNotifications, logout, markNotificationsRead } = LoginSlice.actions; +export const { init, setPrivateKey, setPublicKey, setRelays, removeRelay, setFollows, addNotifications, addDirectMessage, logout, markNotificationsRead } = LoginSlice.actions; export const reducer = LoginSlice.reducer; \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 114dfd8f..ea9ad38f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1594,6 +1594,11 @@ schema-utils "^3.0.0" source-map "^0.7.3" +"@protobufjs/base64@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735" + integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg== + "@reduxjs/toolkit@^1.9.1": version "1.9.1" resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.9.1.tgz#4c34dc4ddcec161535288c60da5c19c3ef15180e" From 5046ab73ff920a1d1139b66eee22e1b8d7225a17 Mon Sep 17 00:00:00 2001 From: Kieran Date: Sat, 14 Jan 2023 00:17:45 +0000 Subject: [PATCH 3/9] fix nip7 sendDm --- src/feed/EventPublisher.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/feed/EventPublisher.js b/src/feed/EventPublisher.js index 1e51025b..8be73dd9 100644 --- a/src/feed/EventPublisher.js +++ b/src/feed/EventPublisher.js @@ -179,7 +179,8 @@ export default function useEventPublisher() { try { if (hasNip07 && !privKey) { - let ev = await barierNip07(() => window.nostr.nip04.encrypt(to, content)); + let content = await barierNip07(() => window.nostr.nip04.encrypt(to, content)); + ev.Content = content; return await signEvent(ev); } else if(privKey) { await ev.EncryptDmForPubkey(to, privKey); From f4dca89c05924f0bd21bc8a7f72f0a266c4204c8 Mon Sep 17 00:00:00 2001 From: Kieran Date: Sat, 14 Jan 2023 00:21:05 +0000 Subject: [PATCH 4/9] Bug fix the bug fix --- src/feed/EventPublisher.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/feed/EventPublisher.js b/src/feed/EventPublisher.js index 8be73dd9..da8ac448 100644 --- a/src/feed/EventPublisher.js +++ b/src/feed/EventPublisher.js @@ -179,8 +179,8 @@ export default function useEventPublisher() { try { if (hasNip07 && !privKey) { - let content = await barierNip07(() => window.nostr.nip04.encrypt(to, content)); - ev.Content = content; + let cx = await barierNip07(() => window.nostr.nip04.encrypt(to, content)); + ev.Content = cx; return await signEvent(ev); } else if(privKey) { await ev.EncryptDmForPubkey(to, privKey); From b46520ebe906c3f59cb2ccd4bac544bcb54d8b19 Mon Sep 17 00:00:00 2001 From: Kieran Date: Sat, 14 Jan 2023 00:24:32 +0000 Subject: [PATCH 5/9] Show pub-key for dms with no response :( --- src/pages/MessagesPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/MessagesPage.tsx b/src/pages/MessagesPage.tsx index d8785cdc..6879f20d 100644 --- a/src/pages/MessagesPage.tsx +++ b/src/pages/MessagesPage.tsx @@ -13,7 +13,7 @@ export default function MessagesPage() { const dms = useSelector(s => s.login.dms); const pubKeys = useMemo(() => { - return Array.from(new Set(dms.map(a => a.pubkey))); + return Array.from(new Set(dms.map(a => [a.pubkey, ...a.tags.filter(b => b[0] === "p").map(b => b[1])]).flat())); }, [dms]); function person(pubkey: string) { From e9bec7b3fd5dd7a1ec1e412451894810e358f43f Mon Sep 17 00:00:00 2001 From: Kieran Date: Sat, 14 Jan 2023 00:25:28 +0000 Subject: [PATCH 6/9] Chat entry spacing --- src/pages/MessagesPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/MessagesPage.tsx b/src/pages/MessagesPage.tsx index 6879f20d..fc1b14a7 100644 --- a/src/pages/MessagesPage.tsx +++ b/src/pages/MessagesPage.tsx @@ -18,7 +18,7 @@ export default function MessagesPage() { function person(pubkey: string) { return ( -
+
{dms?.filter(a => a.pubkey === pubkey && a.pubkey !== pubKey).length} From 5116ecd8172acc8d6d28a8bdd269040e545f569c Mon Sep 17 00:00:00 2001 From: Kieran Date: Sat, 14 Jan 2023 00:29:54 +0000 Subject: [PATCH 7/9] Clear input when sending DM --- src/pages/ChatPage.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/ChatPage.tsx b/src/pages/ChatPage.tsx index 4db43e63..397e96ff 100644 --- a/src/pages/ChatPage.tsx +++ b/src/pages/ChatPage.tsx @@ -43,6 +43,7 @@ export default function ChatPage() { let ev = await publisher.sendDm(content, id); console.debug(ev); publisher.broadcast(ev); + setContent(undefined); } return ( From 1c0eaf48440028b7be0b05b5eb4341f7a0a8cd7b Mon Sep 17 00:00:00 2001 From: Kieran Date: Sat, 14 Jan 2023 11:50:06 +0000 Subject: [PATCH 8/9] DM improvements --- src/element/DM.css | 2 ++ src/element/DM.tsx | 12 +++++++++--- src/pages/ChatPage.css | 1 + src/pages/ChatPage.tsx | 27 ++++++++++++++++++++++----- src/state/Login.js | 1 + 5 files changed, 35 insertions(+), 8 deletions(-) diff --git a/src/element/DM.css b/src/element/DM.css index cb3e343f..bd883597 100644 --- a/src/element/DM.css +++ b/src/element/DM.css @@ -7,6 +7,8 @@ min-width: 100px; max-width: 90%; overflow: hidden; + min-height: 40px; + white-space: pre-wrap; } .dm > div:first-child { diff --git a/src/element/DM.tsx b/src/element/DM.tsx index 328d13ed..70fd4962 100644 --- a/src/element/DM.tsx +++ b/src/element/DM.tsx @@ -1,6 +1,7 @@ import "./DM.css"; import { useEffect, useState } from "react"; import { useSelector } from "react-redux"; +import { useInView } from 'react-intersection-observer'; // @ts-ignore import useEventPublisher from "../feed/EventPublisher"; @@ -19,6 +20,8 @@ export default function DM(props: DMProps) { const pubKey = useSelector(s => s.login.publicKey); const publisher = useEventPublisher(); const [content, setContent] = useState("Loading..."); + const [decrypted, setDecrypted] = useState(false); + const { ref, inView, entry } = useInView(); async function decrypt() { let e = Event.FromObject(props.data); @@ -27,11 +30,14 @@ export default function DM(props: DMProps) { } useEffect(() => { - decrypt().catch(console.error); - }, [props.data]); + if (!decrypted && inView) { + setDecrypted(true); + decrypt().catch(console.error); + } + }, [inView, props.data]); return ( -
+
diff --git a/src/pages/ChatPage.css b/src/pages/ChatPage.css index 2e49d634..f5ea6be0 100644 --- a/src/pages/ChatPage.css +++ b/src/pages/ChatPage.css @@ -8,6 +8,7 @@ display: flex; flex-direction: column; margin-bottom: 10px; + scroll-padding-bottom: 40px; } .write-dm { diff --git a/src/pages/ChatPage.tsx b/src/pages/ChatPage.tsx index 397e96ff..11067bda 100644 --- a/src/pages/ChatPage.tsx +++ b/src/pages/ChatPage.tsx @@ -1,7 +1,8 @@ import "./ChatPage.css"; -import { useMemo, useState } from "react"; +import { KeyboardEvent, useEffect, useMemo, useRef, useState } from "react"; import { useSelector } from "react-redux"; import { useParams } from "react-router-dom"; +import { useInView } from 'react-intersection-observer'; // @ts-ignore import ProfileImage from "../element/ProfileImage"; @@ -23,6 +24,8 @@ export default function ChatPage() { const id = bech32ToHex(params.id); const dms = useSelector(s => filterDms(s.login.dms, s.login.publicKey)); const [content, setContent] = useState(); + const { ref, inView, entry } = useInView(); + const dmListRef = useRef(null); function filterDms(dms: RawEvent[], myPubkey: string) { return dms.filter(a => { @@ -39,24 +42,38 @@ export default function ChatPage() { return [...dms].sort((a, b) => a.created_at - b.created_at) }, [dms]); + useEffect(() => { + if (inView && dmListRef.current) { + dmListRef.current.scroll(0, dmListRef.current.scrollHeight); + } + }, [inView, dmListRef, sortedDms]); + async function sendDm() { let ev = await publisher.sendDm(content, id); console.debug(ev); publisher.broadcast(ev); - setContent(undefined); + setContent(""); + } + + async function onEnter(e: KeyboardEvent) { + let isEnter = e.code === "Enter"; + if(isEnter && !e.shiftKey) { + await sendDm(); + } } return ( <> -
+
- {sortedDms.slice(-10).map(a => )} + {sortedDms.map(a => )} +
- +
sendDm()}>Send
diff --git a/src/state/Login.js b/src/state/Login.js index c616cb11..3640b705 100644 --- a/src/state/Login.js +++ b/src/state/Login.js @@ -160,6 +160,7 @@ const LoginSlice = createSlice({ state.notifications = []; state.loggedOut = true; state.readNotifications = 0; + state.dms = []; }, markNotificationsRead: (state) => { state.readNotifications = new Date().getTime(); From 1c987096fd3bc979eb7342c2fd4c6ff82be6bd61 Mon Sep 17 00:00:00 2001 From: Kieran Date: Sat, 14 Jan 2023 12:25:08 +0000 Subject: [PATCH 9/9] DM button on profile --- src/index.css | 4 ++++ src/pages/ProfilePage.css | 4 ---- src/pages/ProfilePage.js | 28 +++++++++++++++++++--------- 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/src/index.css b/src/index.css index ab527102..6232ab98 100644 --- a/src/index.css +++ b/src/index.css @@ -273,6 +273,10 @@ body.scroll-lock { margin-right: 10px; } +.mr5 { + margin-right: 5px; +} + .ml5 { margin-left: 5px; } diff --git a/src/pages/ProfilePage.css b/src/pages/ProfilePage.css index b2ca33eb..de62dd28 100644 --- a/src/pages/ProfilePage.css +++ b/src/pages/ProfilePage.css @@ -10,10 +10,6 @@ white-space: pre-wrap; } -.profile .name { - align-items: flex-start; -} - .profile .name h2 { margin: 0; } diff --git a/src/pages/ProfilePage.js b/src/pages/ProfilePage.js index 8bc303fe..fa8156e2 100644 --- a/src/pages/ProfilePage.js +++ b/src/pages/ProfilePage.js @@ -4,12 +4,12 @@ import Nostrich from "../nostrich.jpg"; import { useEffect, useMemo, useState } from "react"; import { useSelector } from "react-redux"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faQrcode, faGear } from "@fortawesome/free-solid-svg-icons"; +import { faQrcode, faGear, faEnvelope } from "@fortawesome/free-solid-svg-icons"; import { useNavigate, useParams } from "react-router-dom"; import useProfile from "../feed/ProfileFeed"; import FollowButton from "../element/FollowButton"; -import { extractLnAddress, parseId } from "../Util"; +import { extractLnAddress, parseId, hexToBech32 } from "../Util"; import Timeline from "../element/Timeline"; import { extractLinks, extractHashtags } from '../Text' import LNURLTip from "../element/LNURLTip"; @@ -50,20 +50,30 @@ export default function ProfilePage() { return ( <>
-
-

{user?.display_name || user?.name}

- - {user?.nip05 && } -
-
+
+

{user?.display_name || user?.name}

+
{isMe ? (
navigate("/settings")}>
- ) : + ) : <> +
navigate(`/messages/${hexToBech32("npub", id)}`)}> + +
+ + }
+
+
+ + + {user?.nip05 && } +
+ +

{about}

{user?.website && (