feat: note creator hashtags
This commit is contained in:
parent
95b7cca4cb
commit
981ab5790a
@ -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",
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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>
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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" />)}
|
||||
</>
|
||||
}
|
||||
|
5
packages/app/src/External/NostrBand.ts
vendored
5
packages/app/src/External/NostrBand.ts
vendored
@ -19,7 +19,10 @@ export interface TrendingNoteResponse {
|
||||
}
|
||||
|
||||
export interface TrendingHashtagsResponse {
|
||||
hashtags: Array<string>
|
||||
hashtags: Array<{
|
||||
hashtag: string,
|
||||
posts: number
|
||||
}>
|
||||
}
|
||||
|
||||
export interface SuggestedFollow {
|
||||
|
@ -1,3 +1,6 @@
|
||||
svg#icon-toggle {
|
||||
cursor: pointer;
|
||||
}
|
||||
svg#icon-toggle #bg {
|
||||
fill: var(--gray);
|
||||
transition: fill 0.5s;
|
||||
|
@ -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 {
|
||||
|
11
yarn.lock
11
yarn.lock
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user