feat: mentions

This commit is contained in:
Alejandro Gomez 2023-01-14 11:07:49 +01:00
parent fa3793362c
commit 23388c11b4
No known key found for this signature in database
GPG Key ID: 4DF39E566658C817
7 changed files with 202 additions and 14 deletions

View File

@ -13,6 +13,7 @@
"@types/node": "^18.11.18",
"@types/react": "^18.0.26",
"@types/react-dom": "^18.0.10",
"@webscopeio/react-textarea-autocomplete": "^4.9.2",
"bech32": "^2.0.0",
"light-bolt11-decoder": "^2.1.0",
"qr-code-styling": "^1.6.0-rc.1",

View File

@ -6,10 +6,8 @@
}
.note-creator textarea {
resize: none;
outline: none;
min-height: 40px;
max-height: 300px;
border-radius: 10px 10px 0 0;
max-width: -webkit-fill-available;
max-width: -moz-available;

View File

@ -1,11 +1,15 @@
import "./NoteCreator.css";
import { useState } from "react";
import useEventPublisher from "../feed/EventPublisher";
import { useState, Component } from "react";
import { useSelector } from "react-redux";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faPaperclip } from "@fortawesome/free-solid-svg-icons";
import "./NoteCreator.css";
import useEventPublisher from "../feed/EventPublisher";
import { openFile } from "../Util";
import VoidUpload from "../feed/VoidUpload";
import { FileExtensionRegex } from "../Const";
import Textarea from "../element/Textarea";
export function NoteCreator(props) {
const replyTo = props.replyTo;
@ -15,14 +19,12 @@ export function NoteCreator(props) {
const [note, setNote] = useState("");
const [error, setError] = useState("");
const [active, setActive] = useState(false);
const users = useSelector((state) => state.users.users)
async function sendNote() {
let ev = replyTo ?
await publisher.reply(replyTo, note)
: await publisher.note(note);
let ev = replyTo ? await publisher.reply(replyTo, note) : await publisher.note(note);
console.debug("Sending note: ", ev);
publisher.broadcast(ev);
publisher.broadcast(ev)
setNote("");
if (typeof onSend === "function") {
onSend();
@ -49,8 +51,8 @@ export function NoteCreator(props) {
<>
{replyTo ? <small>{`Reply to: ${replyTo.Id.substring(0, 8)}`}</small> : null}
<div className="flex note-creator" onClick={() => setActive(true)}>
<div className="textarea flex f-col mr10 f-grow">
<textarea className="textarea w-max" placeholder="Say something!" value={note} onChange={(e) => setNote(e.target.value)} />
<div className="flex f-col mr10 f-grow">
<Textarea className="textarea" users={users} onChange={(ev) => setNote(ev.target.value)} />
{active ? <div className="actions flex f-row">
<div className="attachment flex f-row">
{error.length > 0 ? <b className="error">{error}</b> : null}

96
src/element/Textarea.css Normal file
View File

@ -0,0 +1,96 @@
.rta {
position: relative;
font-size: 18px;
width: 100%;
height: 100%;
min-width: 100%;
min-width: -webkit-fill-available;
min-width: -moz-available;
min-width: fill-available;
}
.rta__loader.rta__loader--empty-suggestion-data {
border-radius: 3px;
padding: 5px;
}
.rta--loading .rta__loader.rta__loader--suggestion-data {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.rta--loading .rta__loader.rta__loader--suggestion-data > * {
position: relative;
top: 50%;
}
.rta__textarea {
width: 100%;
height: 100%;
font-size: 1em;
}
.rta__autocomplete {
position: absolute;
display: block;
margin-top: 1em;
border: 1px solid var(--gray-tertiary);
}
.rta__autocomplete--top {
margin-top: 0;
margin-bottom: 1em;
}
.rta__list {
margin: 0;
padding: 0;
border-radius: 3px;
list-style: none;
max-height: 120px;
overflow: scroll;
}
.rta__entity {
background: var(--gray-secondary);
color: var(--font-color);
width: 100%;
text-align: left;
outline: none;
min-width: 300px;
}
.rta__entity:hover {
cursor: pointer;
}
.rta__item:not(:last-child) {
border-bottom: 1px solid var(--gray);
}
.rta__entity > * {
padding: 4px;
}
.rta__entity--selected {
color: var(--font-color);
text-decoration: none;
background: var(--gray-tertiary);
}
.user-item {
display: flex;
flex-direction: row;
align-items: center;
font-size: 14px;
}
.user-item .picture {
width: 30px;
height: 30px;
border-radius: 100%;
}
.user-picture {
display: flex;
align-items: center;
justify-content: center;
margin-right: 8px;
}
.user-details {
display: flex;
flex-direction: column;
align-items: flex-start;
}

52
src/element/Textarea.js Normal file
View File

@ -0,0 +1,52 @@
import { Component } from "react";
import ReactTextareaAutocomplete from "@webscopeio/react-textarea-autocomplete";
import Nip05, { useIsVerified } from "./Nip05";
import "./Textarea.css";
import Nostrich from "../nostrich.jpg";
function searchUsers(query, users) {
return Object.values(users).filter(({ name, display_name }) => {
return name.toLowerCase().includes(query.toLowerCase()) || display_name?.includes(query.toLowerCase())
})
}
const UserItem = ({ pubkey, display_name, picture, nip05 }) => {
const { isVerified, couldNotVerify, name, domain } = useIsVerified(nip05, pubkey)
return (
<div className="user-item">
<div className="user-picture">
{picture && <img src={picture ? picture : Nostrich} className="picture" />}
</div>
<div className="user-details">
<strong>{display_name ? display_name : name !== '_' ? name : domain}</strong>
<Nip05 name={name} domain={domain} isVerified={isVerified} couldNotVerify={couldNotVerify} />
</div>
</div>
)
}
export default class Textarea extends Component {
render() {
const { users, onChange, ...rest } = this.props
return (
<ReactTextareaAutocomplete
{...rest}
loadingComponent={() => <span>Loading....</span>}
placeholder="Say something!"
ref={rta => {
this.rta = rta;
}}
onChange={onChange}
trigger={{
"@": {
dataProvider: token => searchUsers(token, users),
component: ({ entity }) => <UserItem {...entity} />,
output: (item, trigger) => `@${item.pubkey}`
}
}}
/>
)
}
}

View File

@ -1,8 +1,10 @@
import { useSelector } from "react-redux";
import { System } from "..";
import Event from "../nostr/Event";
import EventKind from "../nostr/EventKind";
import Tag from "../nostr/Tag";
import { bech32ToHex } from "../Util"
export default function useEventPublisher() {
const pubKey = useSelector(s => s.login.publicKey);
@ -28,6 +30,25 @@ export default function useEventPublisher() {
return ev;
}
function processMentions(ev, msg) {
const replaceHexKey = (match) => {
const idx = ev.Tags.length;
ev.Tags.push(new Tag(["p", match.slice(1)], idx));
return `#[${idx}]`
}
const replaceNpub = (match) => {
const npub = match.slice(1);
const hex = bech32ToHex(npub);
const idx = ev.Tags.length;
ev.Tags.push(new Tag(["p", hex], idx));
return `#[${idx}]`
}
let content = msg.replace(/@[0-9A-Fa-f]{64}/g, replaceHexKey)
.replace(/@npub[a-z0-9]+/g, replaceNpub)
ev.Content = content;
}
return {
broadcast: (ev) => {
console.debug("Sending event: ", ev);
@ -45,7 +66,7 @@ export default function useEventPublisher() {
}
let ev = Event.ForPubKey(pubKey);
ev.Kind = EventKind.TextNote;
ev.Content = msg;
processMentions(ev, msg);
return await signEvent(ev);
},
/**
@ -60,7 +81,6 @@ export default function useEventPublisher() {
}
let ev = Event.ForPubKey(pubKey);
ev.Kind = EventKind.TextNote;
ev.Content = msg;
let thread = replyTo.Thread;
if (thread) {
@ -81,6 +101,7 @@ export default function useEventPublisher() {
ev.Tags.push(new Tag(["e", replyTo.Id, "", "reply"], 0));
ev.Tags.push(new Tag(["p", replyTo.PubKey], 1));
}
processMentions(ev, msg);
return await signEvent(ev);
},
react: async (evRef, content = "+") => {

View File

@ -2358,6 +2358,14 @@
"@webassemblyjs/ast" "1.11.1"
"@xtuc/long" "4.2.2"
"@webscopeio/react-textarea-autocomplete@^4.9.2":
version "4.9.2"
resolved "https://registry.yarnpkg.com/@webscopeio/react-textarea-autocomplete/-/react-textarea-autocomplete-4.9.2.tgz#b39e57d8048ad2e8790d70073afe63eafa877345"
integrity sha512-9l5lbyA709d5HHvI/COflSnblBJeYGxB2/0ghP3m3YViLzXRMzJwaXqnqz6oA96y7QdR3pQWYtVmkUKA0AUVAA==
dependencies:
custom-event "^1.0.1"
textarea-caret "3.0.2"
"@xtuc/ieee754@^1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790"
@ -3522,6 +3530,11 @@ csstype@^3.0.2:
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.1.tgz#841b532c45c758ee546a11d5bd7b7b473c8c30b9"
integrity sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==
custom-event@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425"
integrity sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==
damerau-levenshtein@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7"
@ -8919,6 +8932,11 @@ text-table@^0.2.0:
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==
textarea-caret@3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/textarea-caret/-/textarea-caret-3.0.2.tgz#f360c48699aa1abf718680a43a31a850665c2caf"
integrity sha512-gRzeti2YS4did7UJnPQ47wrjD+vp+CJIe9zbsu0bJ987d8QVLvLNG9757rqiQTIy4hGIeFauTTJt5Xkn51UkXg==
throat@^6.0.1:
version "6.0.2"
resolved "https://registry.yarnpkg.com/throat/-/throat-6.0.2.tgz#51a3fbb5e11ae72e2cf74861ed5c8020f89f29fe"