diff --git a/package.json b/package.json index b2f96b44..cb8bf8ed 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "@fortawesome/fontawesome-svg-core": "^6.2.1", "@fortawesome/free-solid-svg-icons": "^6.2.1", "@fortawesome/react-fontawesome": "^0.2.0", + "@jukben/emoji-search": "^2.0.1", "@noble/secp256k1": "^1.7.0", "@protobufjs/base64": "^1.1.2", "@reduxjs/toolkit": "^1.9.1", @@ -13,9 +14,9 @@ "@types/node": "^18.11.18", "@types/react": "^18.0.26", "@types/react-dom": "^18.0.10", + "@types/uuid": "^9.0.0", "@types/webscopeio__react-textarea-autocomplete": "^4.7.2", "@webscopeio/react-textarea-autocomplete": "^4.9.2", - "@types/uuid": "^9.0.0", "bech32": "^2.0.0", "dexie": "^3.2.2", "dexie-react-hooks": "^1.1.1", diff --git a/src/db/User.ts b/src/db/User.ts index a8fc7de4..a8aec51f 100644 --- a/src/db/User.ts +++ b/src/db/User.ts @@ -1,4 +1,5 @@ import { HexKey, TaggedRawEvent, UserMetadata } from "../nostr"; +import { hexToBech32 } from "../Util"; export interface MetadataCache extends UserMetadata { /** @@ -22,6 +23,7 @@ export function mapEventToProfile(ev: TaggedRawEvent) { let data: UserMetadata = JSON.parse(ev.content); return { pubkey: ev.pubkey, + npub: hexToBech32("npub", ev.pubkey), created: ev.created_at, loaded: new Date().getTime(), ...data diff --git a/src/db/index.ts b/src/db/index.ts index d5734ccb..8af9d9a8 100644 --- a/src/db/index.ts +++ b/src/db/index.ts @@ -1,5 +1,6 @@ -import Dexie, { Table } from 'dexie'; -import { MetadataCache } from './User'; +import Dexie, { Table } from "dexie"; +import { MetadataCache } from "./User"; +import { hexToBech32 } from "../Util"; export class SnortDB extends Dexie { @@ -7,8 +8,12 @@ export class SnortDB extends Dexie { constructor() { super('snortDB'); - this.version(1).stores({ - users: '++pubkey, name, display_name, picture, nip05' // Primary key and indexed props + this.version(2).stores({ + users: '++pubkey, name, display_name, picture, nip05, npub' + }).upgrade(tx => { + return tx.table("users").toCollection().modify(user => { + user.npub = hexToBech32("npub", user.pubkey) + }) }); } } diff --git a/src/element/DM.tsx b/src/element/DM.tsx index f80ea471..96d45ee6 100644 --- a/src/element/DM.tsx +++ b/src/element/DM.tsx @@ -1,38 +1,33 @@ import "./DM.css"; import { useEffect, useState } from "react"; -import { useDispatch, useSelector } from "react-redux"; +import { useSelector } from "react-redux"; import { useInView } from 'react-intersection-observer'; import useEventPublisher from "../feed/EventPublisher"; import Event from "../nostr/Event"; import NoteTime from "./NoteTime"; import Text from "./Text"; -import { setLastReadDm } from "../pages/MessagesPage"; -import { RootState } from "../state/Store"; -import { HexKey, TaggedRawEvent } from "../nostr"; -import { incDmInteraction } from "../state/Login"; +import { lastReadDm, setLastReadDm } from "../pages/MessagesPage"; export type DMProps = { - data: TaggedRawEvent + data: any } export default function DM(props: DMProps) { - const dispatch = useDispatch(); - const pubKey = useSelector(s => s.login.publicKey); + const pubKey = useSelector(s => s.login.publicKey); const publisher = useEventPublisher(); const [content, setContent] = useState("Loading..."); const [decrypted, setDecrypted] = useState(false); - const { ref, inView } = useInView(); + const { ref, inView, entry } = useInView(); const isMe = props.data.pubkey === pubKey; async function decrypt() { let e = new Event(props.data); - let decrypted = await publisher.decryptDm(e); - setContent(decrypted || ""); if (!isMe) { setLastReadDm(e.PubKey); - dispatch(incDmInteraction()); } + let decrypted = await publisher.decryptDm(e); + setContent(decrypted || ""); } useEffect(() => { diff --git a/src/element/Note.css b/src/element/Note.css index 85c87c43..5b9e5f30 100644 --- a/src/element/Note.css +++ b/src/element/Note.css @@ -2,8 +2,8 @@ margin-bottom: 10px; border-radius: 10px; background-color: var(--note-bg); - padding: 10px 10px 8px 10px; - min-height: 110px; + padding: 20px; + min-height: 120px; } .note.thread { diff --git a/src/element/Textarea.css b/src/element/Textarea.css index fece461f..a3ccd992 100644 --- a/src/element/Textarea.css +++ b/src/element/Textarea.css @@ -1,4 +1,4 @@ -.user-item { +.user-item, .emoji-item { background: var(--gray); display: flex; flex-direction: row; @@ -7,7 +7,7 @@ padding: 10px; } -.user-item:hover { +.user-item:hover, .emoji-item:hover { background: var(--gray-tertiary); } @@ -39,3 +39,16 @@ .nip05 { font-size: 12px; } + +.emoji-item { + font-size: 12px; +} + +.emoji-item .emoji { + margin-right: .2em; + min-width: 20px; +} + +.emoji-item .emoji-name { + font-weight: bold; +} diff --git a/src/element/Textarea.tsx b/src/element/Textarea.tsx index 21ac610d..94c783d3 100644 --- a/src/element/Textarea.tsx +++ b/src/element/Textarea.tsx @@ -4,6 +4,7 @@ import "./Textarea.css"; import { useState } from "react"; import { useLiveQuery } from "dexie-react-hooks"; import ReactTextareaAutocomplete from "@webscopeio/react-textarea-autocomplete"; +import emoji from "@jukben/emoji-search"; import TextareaAutosize from "react-textarea-autosize"; import Avatar from "./Avatar"; @@ -12,6 +13,20 @@ import { hexToBech32 } from "../Util"; import { db } from "../db"; import { MetadataCache } from "../db/User"; +interface EmojiItemProps { + name: string + char: string +} + +const EmojiItem = ({ entity: { name, char } }: { entity: EmojiItemProps }) => { + return ( +
+
{char}
+
{name}
+
+ ) +} + const UserItem = (metadata: MetadataCache) => { const { pubkey, display_name, picture, nip05, ...rest } = metadata return ( @@ -32,7 +47,8 @@ const Textarea = ({ users, onChange, ...rest }: any) => { const allUsers = useLiveQuery( () => db.users - .where("name").startsWithIgnoreCase(query) + .where("npub").startsWithIgnoreCase(query) + .or("name").startsWithIgnoreCase(query) .or("display_name").startsWithIgnoreCase(query) .or("nip05").startsWithIgnoreCase(query) .limit(5) @@ -45,6 +61,12 @@ const Textarea = ({ users, onChange, ...rest }: any) => { return allUsers } + const emojiDataProvider = (token: string) => { + return emoji(token) + .slice(0, 10) + .map(({ name, char }) => ({ name, char })); + } + return ( { onChange={onChange} textAreaComponent={TextareaAutosize} trigger={{ + ":": { + dataProvider: emojiDataProvider, + component: EmojiItem, + output: (item: EmojiItemProps, trigger) => item.char + }, "@": { afterWhitespace: true, dataProvider: userDataProvider, diff --git a/src/index.css b/src/index.css index e3c8a075..a8f9ecbd 100644 --- a/src/index.css +++ b/src/index.css @@ -349,7 +349,8 @@ body.scroll-lock { @media(max-width: 720px) { .page { - width: calc(100vw - 8px); + width: calc(100vw - 20px); + margin: 0 10px; } div.form-group { flex-direction: column; diff --git a/src/pages/MessagesPage.tsx b/src/pages/MessagesPage.tsx index 1be9daa0..39608e00 100644 --- a/src/pages/MessagesPage.tsx +++ b/src/pages/MessagesPage.tsx @@ -1,11 +1,9 @@ import { useMemo } from "react"; -import { useDispatch, useSelector } from "react-redux" +import { useSelector } from "react-redux" import { HexKey, RawEvent } from "../nostr"; import ProfileImage from "../element/ProfileImage"; import { hexToBech32 } from "../Util"; -import { incDmInteraction } from "../state/Login"; -import { RootState } from "../state/Store"; type DmChat = { pubkey: HexKey, @@ -14,14 +12,12 @@ type DmChat = { } export default function MessagesPage() { - const dispatch = useDispatch(); - const myPubKey = useSelector(s => s.login.publicKey); - const dms = useSelector(s => s.login.dms); - const dmInteraction = useSelector(s => s.login.dmInteraction); + const myPubKey = useSelector(s => s.login.publicKey); + const dms = useSelector(s => s.login.dms); const chats = useMemo(() => { - return extractChats(dms, myPubKey!); - }, [dms, myPubKey, dmInteraction]); + return extractChats(dms, myPubKey); + }, [dms]); function person(chat: DmChat) { return ( @@ -34,19 +30,9 @@ export default function MessagesPage() { ) } - function markAllRead() { - for (let c of chats) { - setLastReadDm(c.pubkey); - } - dispatch(incDmInteraction()); - } - return ( <> -
-

Messages

-
markAllRead()}>Mark All Read
-
+

Messages

{chats.sort((a, b) => b.newestMessage - a.newestMessage).map(person)} ) @@ -95,6 +81,7 @@ function newestMessage(dms: RawEvent[], myPubKey: HexKey, pk: HexKey) { return dmsInChat(dms, pk).reduce((acc, v) => acc = v.created_at > acc ? v.created_at : acc, 0); } + export function extractChats(dms: RawEvent[], myPubKey: HexKey) { const keys = dms.map(a => [a.pubkey, dmTo(a)]).flat(); const filteredKeys = Array.from(new Set(keys)); diff --git a/src/state/Login.ts b/src/state/Login.ts index c06ad0b5..df88cc01 100644 --- a/src/state/Login.ts +++ b/src/state/Login.ts @@ -52,27 +52,9 @@ interface LoginStore { /** * Encrypted DM's */ - dms: TaggedRawEvent[], - - /** - * Counter to trigger refresh of unread dms - */ - dmInteraction: 0 + dms: TaggedRawEvent[] }; -const InitState = { - loggedOut: undefined, - publicKey: undefined, - privateKey: undefined, - relays: {}, - latestRelays: 0, - follows: [], - notifications: [], - readNotifications: 0, - dms: [], - dmInteraction: 0 -} as LoginStore; - export interface SetRelaysPayload { relays: Record, createdAt: number @@ -80,7 +62,14 @@ export interface SetRelaysPayload { const LoginSlice = createSlice({ name: "Login", - initialState: InitState, + initialState: { + relays: {}, + latestRelays: 0, + follows: [], + notifications: [], + readNotifications: 0, + dms: [] + }, reducers: { init: (state) => { state.privateKey = window.localStorage.getItem(PrivateKeyItem) ?? undefined; @@ -193,14 +182,17 @@ const LoginSlice = createSlice({ ]; } }, - incDmInteraction: (state) => { - state.dmInteraction += 1; - }, logout: (state) => { - window.localStorage.clear(); - Object.assign(state, InitState); + window.localStorage.removeItem(PrivateKeyItem); + window.localStorage.removeItem(PublicKeyItem); + window.localStorage.removeItem(NotificationsReadItem); + state.privateKey = undefined; + state.publicKey = undefined; + state.follows = []; + state.notifications = []; state.loggedOut = true; - state.relays = Object.fromEntries(DefaultRelays.entries()); + state.readNotifications = 0; + state.dms = []; }, markNotificationsRead: (state) => { state.readNotifications = new Date().getTime(); @@ -218,7 +210,6 @@ export const { setFollows, addNotifications, addDirectMessage, - incDmInteraction, logout, markNotificationsRead } = LoginSlice.actions; diff --git a/yarn.lock b/yarn.lock index 29bdfd81..2e1c57c1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1024,6 +1024,13 @@ "@babel/helper-validator-option" "^7.18.6" "@babel/plugin-transform-typescript" "^7.18.6" +"@babel/runtime@7.5.5": + version "7.5.5" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.5.tgz#74fba56d35efbeca444091c7850ccd494fd2f132" + integrity sha512-28QvEGyQyNkB0/m2B4FU7IEZGK2NUrcMtT6BZEFALTguLk+AUT6ofsHtPk5QyjAdUkpMJ+/Em+quwz4HOt30AQ== + dependencies: + regenerator-runtime "^0.13.2" + "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.16.3", "@babel/runtime@^7.20.7", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": version "7.20.7" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.7.tgz#fcb41a5a70550e04a7b708037c7c32f7f356d8fd" @@ -1541,6 +1548,15 @@ "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" +"@jukben/emoji-search@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@jukben/emoji-search/-/emoji-search-2.0.1.tgz#128ff80e3efaf00430cf1e235e5af81445f6f991" + integrity sha512-jXVcJGTBl+uOsGld+6J+hcHlRt3Vhm9ffvkrb1IeSVXuFCuyklY2XPI2wvSHG1uMGXfgmKbuUe1MCh1ZV3CXNg== + dependencies: + "@babel/runtime" "7.5.5" + emojilib "2.4.0" + match-sorter "4.0.0" + "@leichtgewicht/ip-codec@^2.0.1": version "2.0.4" resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b" @@ -2121,16 +2137,17 @@ resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz#b6725d5f4af24ace33b36fafd295136e75509f43" integrity sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA== +"@types/uuid@^9.0.0": + version "9.0.0" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.0.tgz#53ef263e5239728b56096b0a869595135b7952d2" + integrity sha512-kr90f+ERiQtKWMz5rP32ltJ/BtULDI5RVO0uavn1HQUOwjx0R1h0rnDYNL0CepF1zL5bSY6FISAfd9tOdDhU5Q== + "@types/webscopeio__react-textarea-autocomplete@^4.7.2": version "4.7.2" resolved "https://registry.yarnpkg.com/@types/webscopeio__react-textarea-autocomplete/-/webscopeio__react-textarea-autocomplete-4.7.2.tgz#605e8a6b4194fb4b6e55df8a19bc8fcd56319cfa" integrity sha512-e1DZGD+eH19BnllTWCGXAdrMa2kI53wEMuhn/d+wUmnu8//ZI6BiuK/EPdw07fI4+tlyo5qdPZdXdpkoXHJVOw== dependencies: "@types/react" "*" -"@types/uuid@^9.0.0": - version "9.0.0" - resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.0.tgz#53ef263e5239728b56096b0a869595135b7952d2" - integrity sha512-kr90f+ERiQtKWMz5rP32ltJ/BtULDI5RVO0uavn1HQUOwjx0R1h0rnDYNL0CepF1zL5bSY6FISAfd9tOdDhU5Q== "@types/ws@^8.5.1": version "8.5.4" @@ -3920,6 +3937,11 @@ emoji-regex@^9.2.2: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== +emojilib@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/emojilib/-/emojilib-2.4.0.tgz#ac518a8bb0d5f76dda57289ccb2fdf9d39ae721e" + integrity sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw== + emojis-list@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" @@ -6326,6 +6348,13 @@ makeerror@1.0.12: dependencies: tmpl "1.0.5" +match-sorter@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/match-sorter/-/match-sorter-4.0.0.tgz#16f1a52ce51b01e462df3e8a9c16b8c1efac2584" + integrity sha512-E4DWje5l7+VvDUlqnACXy1iecuD6ZNiqUFw/DUYdFQljRIskZVHoT+76lLv5zz2BQOTxF2CUEgP1/Xu9Xn3MyQ== + dependencies: + remove-accents "0.4.2" + match-sorter@^6.0.2: version "6.3.1" resolved "https://registry.yarnpkg.com/match-sorter/-/match-sorter-6.3.1.tgz#98cc37fda756093424ddf3cbc62bfe9c75b92bda" @@ -8142,7 +8171,7 @@ regenerate@^1.4.2: resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== -regenerator-runtime@^0.13.11, regenerator-runtime@^0.13.9: +regenerator-runtime@^0.13.11, regenerator-runtime@^0.13.2, regenerator-runtime@^0.13.9: version "0.13.11" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==