feat: mentions
This commit is contained in:
parent
fa3793362c
commit
23388c11b4
@ -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",
|
||||
|
@ -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;
|
||||
|
@ -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
96
src/element/Textarea.css
Normal 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
52
src/element/Textarea.js
Normal 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}`
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
@ -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 = "+") => {
|
||||
|
18
yarn.lock
18
yarn.lock
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user