feat: note creator hashtags

This commit is contained in:
Kieran 2023-11-16 15:42:47 +00:00
parent 95b7cca4cb
commit 981ab5790a
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
9 changed files with 116 additions and 21 deletions

View File

@ -34,6 +34,7 @@
"react-intersection-observer": "^9.4.1",
"react-intl": "^6.4.4",
"react-router-dom": "^6.5.0",
"react-tag-input-component": "^2.0.2",
"react-textarea-autosize": "^8.4.0",
"react-twitter-embed": "^4.0.4",
"recharts": "^2.8.0",

View File

@ -95,3 +95,27 @@
linear-gradient(var(--gray-superdark), var(--gray-superdark)) padding-box,
linear-gradient(90deg, #ef9644, #fd7c49, #ff5e58, #ff3b70, #ff088e, #eb00b1, #c31ed5, #7b41f6) border-box;
}
.note-creator-modal .rti--container {
background-color: unset !important;
box-shadow: unset !important;
border: 2px solid var(--border-color) !important;
border-radius: 12px !important;
padding: 4px 8px !important;
}
.note-creator-modal .rti--tag {
color: black !important;
padding: 4px 10px !important;
border-radius: 12px !important;
display: unset !important;
}
.note-creator-modal .rti--input {
width: 100% !important;
border: unset !important;
}
.note-creator-modal .rti--tag button {
padding: 0 0 0 var(--rti-s);
}

View File

@ -2,17 +2,18 @@ import "./NoteCreator.css";
import { FormattedMessage, useIntl } from "react-intl";
import { EventKind, NostrPrefix, TaggedNostrEvent, EventBuilder, tryParseNostrLink, NostrLink } from "@snort/system";
import classNames from "classnames";
import { TagsInput } from "react-tag-input-component";
import Icon from "Icons/Icon";
import useEventPublisher from "Hooks/useEventPublisher";
import { openFile } from "SnortUtils";
import { appendDedupe, openFile } from "SnortUtils";
import Textarea from "Element/Textarea";
import Modal from "Element/Modal";
import ProfileImage from "Element/User/ProfileImage";
import useFileUpload from "Upload";
import Note from "Element/Event/Note";
import { ClipboardEventHandler, DragEvent } from "react";
import { ClipboardEventHandler, DragEvent, useEffect, useState } from "react";
import useLogin from "Hooks/useLogin";
import { GetPowWorker } from "index";
import AsyncButton from "Element/AsyncButton";
@ -23,6 +24,8 @@ import { useNoteCreator } from "State/NoteCreator";
import { NoteBroadcaster } from "./NoteBroadcaster";
import FileUploadProgress from "./FileUpload";
import { ToggleSwitch } from "Icons/Toggle";
import NostrBandApi from "External/NostrBand";
import { useLocale } from "IntlProvider";
export function NoteCreator() {
const { formatMessage } = useIntl();
@ -99,6 +102,10 @@ export function NoteCreator() {
extraTags ??= [];
extraTags.push(...note.pollOptions.map((a, i) => ["poll_option", i.toString(), a]));
}
if (note.hashTags.length > 0) {
extraTags ??= [];
extraTags.push(...note.hashTags.map(a => ["t", a.toLowerCase()]));
}
// add quote repost
if (note.quote) {
if (!note.note.endsWith("\n")) {
@ -307,18 +314,18 @@ export function NoteCreator() {
onChange={e => {
note.update(
v =>
(v.selectedCustomRelays =
// set false if all relays selected
e.target.checked &&
(v.selectedCustomRelays =
// set false if all relays selected
e.target.checked &&
note.selectedCustomRelays &&
note.selectedCustomRelays.length == a.length - 1
? undefined
: // otherwise return selectedCustomRelays with target relay added / removed
a.filter(el =>
el === r
? e.target.checked
: !note.selectedCustomRelays || note.selectedCustomRelays.includes(el),
)),
? undefined
: // otherwise return selectedCustomRelays with target relay added / removed
a.filter(el =>
el === r
? e.target.checked
: !note.selectedCustomRelays || note.selectedCustomRelays.includes(el),
)),
);
}}
/>
@ -387,9 +394,9 @@ export function NoteCreator() {
onChange={e =>
note.update(
v =>
(v.zapSplits = arr.map((vv, ii) =>
ii === i ? { ...vv, weight: Number(e.target.value) } : vv,
)),
(v.zapSplits = arr.map((vv, ii) =>
ii === i ? { ...vv, weight: Number(e.target.value) } : vv,
)),
)
}
/>
@ -565,7 +572,7 @@ export function NoteCreator() {
</>
)}
{note.preview && getPreviewNote()}
{!note.preview && (
{!note.preview && (<>
<div onPaste={handlePaste} className={classNames("note-creator", { poll: Boolean(note.pollOptions) })}>
<Textarea
onDragOver={handleDragOver}
@ -584,6 +591,16 @@ export function NoteCreator() {
/>
{renderPollOptions()}
</div>
<div className="flex flex-col g4">
<TagsInput value={note.hashTags} onChange={e => note.update(s => s.hashTags = e)} placeHolder={formatMessage({
defaultMessage: "Add up to 4 hashtags"
})} separators={["Enter", ","]} />
{note.hashTags.length > 4 && <small className="warning">
<FormattedMessage defaultMessage="Try to use less than 4 hashtags to stay on topic 🙏" />
</small>}
<TrendingHashTagsLine onClick={t => note.update(s => s.hashTags = appendDedupe(s.hashTags, [t]))} />
</div>
</>
)}
{uploader.progress.length > 0 && <FileUploadProgress progress={uploader.progress} />}
{noteCreatorFooter()}
@ -608,3 +625,30 @@ export function NoteCreator() {
</Modal>
);
}
function TrendingHashTagsLine(props: { onClick: (tag: string) => void }) {
const [hashtags, setHashtags] = useState<Array<{ hashtag: string, posts: number }>>();
const { lang } = useLocale();
async function loadTrendingHashtags() {
const api = new NostrBandApi();
const rsp = await api.trendingHashtags(lang);
setHashtags(rsp.hashtags);
}
useEffect(() => {
loadTrendingHashtags().catch(console.error);
}, []);
if (!hashtags || hashtags.length === 0) return;
return <div className="flex flex-col g4">
<small>
<FormattedMessage defaultMessage="Popular Hashtags" />
</small>
<div className="flex g4 flex-wrap">
{hashtags.slice(0, 5).map(a => <span className="px-2 py-1 bg-dark rounded-full pointer nowrap" onClick={() => props.onClick(a.hashtag)}>
#{a.hashtag}
</span>)}
</div>
</div>
}

View File

@ -13,16 +13,22 @@
.modal-body {
background-color: var(--gray-superdark);
padding: 24px;
padding: 24px 12px;
border-radius: 16px;
display: flex;
flex-direction: column;
width: 500px;
margin-top: auto;
margin-bottom: auto;
--border-color: var(--gray);
}
@media (min-width: 600px) {
.modal-body {
padding: 24px;
width: 600px;
}
}
.modal-body button.secondary:hover {
background-color: var(--gray);
}

View File

@ -7,7 +7,7 @@ import { HashTagHeader } from "Pages/HashTagsPage";
import { useLocale } from "IntlProvider";
export default function TrendingHashtags({ title }: { title?: ReactNode }) {
const [hashtags, setHashtags] = useState<string[]>();
const [hashtags, setHashtags] = useState<Array<{ hashtag: string, posts: number }>>();
const [error, setError] = useState<Error>();
const { lang } = useLocale();
@ -30,6 +30,6 @@ export default function TrendingHashtags({ title }: { title?: ReactNode }) {
return <>
{title}
{hashtags.map(a => <HashTagHeader tag={a} className="bb p" />)}
{hashtags.map(a => <HashTagHeader tag={a.hashtag} className="bb p" />)}
</>
}

View File

@ -19,7 +19,10 @@ export interface TrendingNoteResponse {
}
export interface TrendingHashtagsResponse {
hashtags: Array<string>
hashtags: Array<{
hashtag: string,
posts: number
}>
}
export interface SuggestedFollow {

View File

@ -1,3 +1,6 @@
svg#icon-toggle {
cursor: pointer;
}
svg#icon-toggle #bg {
fill: var(--gray);
transition: fill 0.5s;

View File

@ -21,6 +21,7 @@ interface NoteCreatorDataSnapshot {
extraTags?: Array<Array<string>>;
sending?: Array<NostrEvent>;
sendStarted: boolean;
hashTags: Array<string>;
reset: () => void;
update: (fn: (v: NoteCreatorDataSnapshot) => void) => void;
}
@ -37,6 +38,7 @@ class NoteCreatorStore extends ExternalStore<NoteCreatorDataSnapshot> {
active: false,
advanced: false,
sendStarted: false,
hashTags: [],
reset: () => {
this.#reset(this.#data);
this.notifyChange(this.#data);
@ -65,6 +67,7 @@ class NoteCreatorStore extends ExternalStore<NoteCreatorDataSnapshot> {
d.otherEvents = undefined;
d.sending = undefined;
d.extraTags = undefined;
d.hashTags = [];
}
takeSnapshot(): NoteCreatorDataSnapshot {

View File

@ -3171,6 +3171,7 @@ __metadata:
react-intersection-observer: ^9.4.1
react-intl: ^6.4.4
react-router-dom: ^6.5.0
react-tag-input-component: ^2.0.2
react-textarea-autosize: ^8.4.0
react-twitter-embed: ^4.0.4
recharts: ^2.8.0
@ -12841,6 +12842,16 @@ __metadata:
languageName: node
linkType: hard
"react-tag-input-component@npm:^2.0.2":
version: 2.0.2
resolution: "react-tag-input-component@npm:2.0.2"
peerDependencies:
react: ^16 || ^17 || ^18
react-dom: ^16 || ^17 || ^18
checksum: b8d5c588a3bfe4c4a82b8a8e34e3d11c37cd467bbd92b31aeb7e3fbd4e3a4e62228811d3ce61b01115c7efa8b6ccb06c7a2688710d03bb8ed91f9e2e690a1775
languageName: node
linkType: hard
"react-textarea-autosize@npm:^8.4.0":
version: 8.5.3
resolution: "react-textarea-autosize@npm:8.5.3"