feat: note creator hashtags
This commit is contained in:
parent
95b7cca4cb
commit
981ab5790a
@ -34,6 +34,7 @@
|
|||||||
"react-intersection-observer": "^9.4.1",
|
"react-intersection-observer": "^9.4.1",
|
||||||
"react-intl": "^6.4.4",
|
"react-intl": "^6.4.4",
|
||||||
"react-router-dom": "^6.5.0",
|
"react-router-dom": "^6.5.0",
|
||||||
|
"react-tag-input-component": "^2.0.2",
|
||||||
"react-textarea-autosize": "^8.4.0",
|
"react-textarea-autosize": "^8.4.0",
|
||||||
"react-twitter-embed": "^4.0.4",
|
"react-twitter-embed": "^4.0.4",
|
||||||
"recharts": "^2.8.0",
|
"recharts": "^2.8.0",
|
||||||
|
@ -95,3 +95,27 @@
|
|||||||
linear-gradient(var(--gray-superdark), var(--gray-superdark)) padding-box,
|
linear-gradient(var(--gray-superdark), var(--gray-superdark)) padding-box,
|
||||||
linear-gradient(90deg, #ef9644, #fd7c49, #ff5e58, #ff3b70, #ff088e, #eb00b1, #c31ed5, #7b41f6) border-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 { FormattedMessage, useIntl } from "react-intl";
|
||||||
import { EventKind, NostrPrefix, TaggedNostrEvent, EventBuilder, tryParseNostrLink, NostrLink } from "@snort/system";
|
import { EventKind, NostrPrefix, TaggedNostrEvent, EventBuilder, tryParseNostrLink, NostrLink } from "@snort/system";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
import { TagsInput } from "react-tag-input-component";
|
||||||
|
|
||||||
import Icon from "Icons/Icon";
|
import Icon from "Icons/Icon";
|
||||||
import useEventPublisher from "Hooks/useEventPublisher";
|
import useEventPublisher from "Hooks/useEventPublisher";
|
||||||
import { openFile } from "SnortUtils";
|
import { appendDedupe, openFile } from "SnortUtils";
|
||||||
import Textarea from "Element/Textarea";
|
import Textarea from "Element/Textarea";
|
||||||
import Modal from "Element/Modal";
|
import Modal from "Element/Modal";
|
||||||
import ProfileImage from "Element/User/ProfileImage";
|
import ProfileImage from "Element/User/ProfileImage";
|
||||||
import useFileUpload from "Upload";
|
import useFileUpload from "Upload";
|
||||||
import Note from "Element/Event/Note";
|
import Note from "Element/Event/Note";
|
||||||
|
|
||||||
import { ClipboardEventHandler, DragEvent } from "react";
|
import { ClipboardEventHandler, DragEvent, useEffect, useState } from "react";
|
||||||
import useLogin from "Hooks/useLogin";
|
import useLogin from "Hooks/useLogin";
|
||||||
import { GetPowWorker } from "index";
|
import { GetPowWorker } from "index";
|
||||||
import AsyncButton from "Element/AsyncButton";
|
import AsyncButton from "Element/AsyncButton";
|
||||||
@ -23,6 +24,8 @@ import { useNoteCreator } from "State/NoteCreator";
|
|||||||
import { NoteBroadcaster } from "./NoteBroadcaster";
|
import { NoteBroadcaster } from "./NoteBroadcaster";
|
||||||
import FileUploadProgress from "./FileUpload";
|
import FileUploadProgress from "./FileUpload";
|
||||||
import { ToggleSwitch } from "Icons/Toggle";
|
import { ToggleSwitch } from "Icons/Toggle";
|
||||||
|
import NostrBandApi from "External/NostrBand";
|
||||||
|
import { useLocale } from "IntlProvider";
|
||||||
|
|
||||||
export function NoteCreator() {
|
export function NoteCreator() {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
@ -99,6 +102,10 @@ export function NoteCreator() {
|
|||||||
extraTags ??= [];
|
extraTags ??= [];
|
||||||
extraTags.push(...note.pollOptions.map((a, i) => ["poll_option", i.toString(), a]));
|
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
|
// add quote repost
|
||||||
if (note.quote) {
|
if (note.quote) {
|
||||||
if (!note.note.endsWith("\n")) {
|
if (!note.note.endsWith("\n")) {
|
||||||
@ -307,18 +314,18 @@ export function NoteCreator() {
|
|||||||
onChange={e => {
|
onChange={e => {
|
||||||
note.update(
|
note.update(
|
||||||
v =>
|
v =>
|
||||||
(v.selectedCustomRelays =
|
(v.selectedCustomRelays =
|
||||||
// set false if all relays selected
|
// set false if all relays selected
|
||||||
e.target.checked &&
|
e.target.checked &&
|
||||||
note.selectedCustomRelays &&
|
note.selectedCustomRelays &&
|
||||||
note.selectedCustomRelays.length == a.length - 1
|
note.selectedCustomRelays.length == a.length - 1
|
||||||
? undefined
|
? undefined
|
||||||
: // otherwise return selectedCustomRelays with target relay added / removed
|
: // otherwise return selectedCustomRelays with target relay added / removed
|
||||||
a.filter(el =>
|
a.filter(el =>
|
||||||
el === r
|
el === r
|
||||||
? e.target.checked
|
? e.target.checked
|
||||||
: !note.selectedCustomRelays || note.selectedCustomRelays.includes(el),
|
: !note.selectedCustomRelays || note.selectedCustomRelays.includes(el),
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -387,9 +394,9 @@ export function NoteCreator() {
|
|||||||
onChange={e =>
|
onChange={e =>
|
||||||
note.update(
|
note.update(
|
||||||
v =>
|
v =>
|
||||||
(v.zapSplits = arr.map((vv, ii) =>
|
(v.zapSplits = arr.map((vv, ii) =>
|
||||||
ii === i ? { ...vv, weight: Number(e.target.value) } : vv,
|
ii === i ? { ...vv, weight: Number(e.target.value) } : vv,
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@ -565,7 +572,7 @@ export function NoteCreator() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{note.preview && getPreviewNote()}
|
{note.preview && getPreviewNote()}
|
||||||
{!note.preview && (
|
{!note.preview && (<>
|
||||||
<div onPaste={handlePaste} className={classNames("note-creator", { poll: Boolean(note.pollOptions) })}>
|
<div onPaste={handlePaste} className={classNames("note-creator", { poll: Boolean(note.pollOptions) })}>
|
||||||
<Textarea
|
<Textarea
|
||||||
onDragOver={handleDragOver}
|
onDragOver={handleDragOver}
|
||||||
@ -584,6 +591,16 @@ export function NoteCreator() {
|
|||||||
/>
|
/>
|
||||||
{renderPollOptions()}
|
{renderPollOptions()}
|
||||||
</div>
|
</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} />}
|
{uploader.progress.length > 0 && <FileUploadProgress progress={uploader.progress} />}
|
||||||
{noteCreatorFooter()}
|
{noteCreatorFooter()}
|
||||||
@ -608,3 +625,30 @@ export function NoteCreator() {
|
|||||||
</Modal>
|
</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 {
|
.modal-body {
|
||||||
background-color: var(--gray-superdark);
|
background-color: var(--gray-superdark);
|
||||||
padding: 24px;
|
padding: 24px 12px;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 500px;
|
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
margin-bottom: auto;
|
margin-bottom: auto;
|
||||||
--border-color: var(--gray);
|
--border-color: var(--gray);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (min-width: 600px) {
|
||||||
|
.modal-body {
|
||||||
|
padding: 24px;
|
||||||
|
width: 600px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.modal-body button.secondary:hover {
|
.modal-body button.secondary:hover {
|
||||||
background-color: var(--gray);
|
background-color: var(--gray);
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ import { HashTagHeader } from "Pages/HashTagsPage";
|
|||||||
import { useLocale } from "IntlProvider";
|
import { useLocale } from "IntlProvider";
|
||||||
|
|
||||||
export default function TrendingHashtags({ title }: { title?: ReactNode }) {
|
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 [error, setError] = useState<Error>();
|
||||||
const { lang } = useLocale();
|
const { lang } = useLocale();
|
||||||
|
|
||||||
@ -30,6 +30,6 @@ export default function TrendingHashtags({ title }: { title?: ReactNode }) {
|
|||||||
|
|
||||||
return <>
|
return <>
|
||||||
{title}
|
{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 {
|
export interface TrendingHashtagsResponse {
|
||||||
hashtags: Array<string>
|
hashtags: Array<{
|
||||||
|
hashtag: string,
|
||||||
|
posts: number
|
||||||
|
}>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SuggestedFollow {
|
export interface SuggestedFollow {
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
svg#icon-toggle {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
svg#icon-toggle #bg {
|
svg#icon-toggle #bg {
|
||||||
fill: var(--gray);
|
fill: var(--gray);
|
||||||
transition: fill 0.5s;
|
transition: fill 0.5s;
|
||||||
|
@ -21,6 +21,7 @@ interface NoteCreatorDataSnapshot {
|
|||||||
extraTags?: Array<Array<string>>;
|
extraTags?: Array<Array<string>>;
|
||||||
sending?: Array<NostrEvent>;
|
sending?: Array<NostrEvent>;
|
||||||
sendStarted: boolean;
|
sendStarted: boolean;
|
||||||
|
hashTags: Array<string>;
|
||||||
reset: () => void;
|
reset: () => void;
|
||||||
update: (fn: (v: NoteCreatorDataSnapshot) => void) => void;
|
update: (fn: (v: NoteCreatorDataSnapshot) => void) => void;
|
||||||
}
|
}
|
||||||
@ -37,6 +38,7 @@ class NoteCreatorStore extends ExternalStore<NoteCreatorDataSnapshot> {
|
|||||||
active: false,
|
active: false,
|
||||||
advanced: false,
|
advanced: false,
|
||||||
sendStarted: false,
|
sendStarted: false,
|
||||||
|
hashTags: [],
|
||||||
reset: () => {
|
reset: () => {
|
||||||
this.#reset(this.#data);
|
this.#reset(this.#data);
|
||||||
this.notifyChange(this.#data);
|
this.notifyChange(this.#data);
|
||||||
@ -65,6 +67,7 @@ class NoteCreatorStore extends ExternalStore<NoteCreatorDataSnapshot> {
|
|||||||
d.otherEvents = undefined;
|
d.otherEvents = undefined;
|
||||||
d.sending = undefined;
|
d.sending = undefined;
|
||||||
d.extraTags = undefined;
|
d.extraTags = undefined;
|
||||||
|
d.hashTags = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
takeSnapshot(): NoteCreatorDataSnapshot {
|
takeSnapshot(): NoteCreatorDataSnapshot {
|
||||||
|
11
yarn.lock
11
yarn.lock
@ -3171,6 +3171,7 @@ __metadata:
|
|||||||
react-intersection-observer: ^9.4.1
|
react-intersection-observer: ^9.4.1
|
||||||
react-intl: ^6.4.4
|
react-intl: ^6.4.4
|
||||||
react-router-dom: ^6.5.0
|
react-router-dom: ^6.5.0
|
||||||
|
react-tag-input-component: ^2.0.2
|
||||||
react-textarea-autosize: ^8.4.0
|
react-textarea-autosize: ^8.4.0
|
||||||
react-twitter-embed: ^4.0.4
|
react-twitter-embed: ^4.0.4
|
||||||
recharts: ^2.8.0
|
recharts: ^2.8.0
|
||||||
@ -12841,6 +12842,16 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"react-textarea-autosize@npm:^8.4.0":
|
||||||
version: 8.5.3
|
version: 8.5.3
|
||||||
resolution: "react-textarea-autosize@npm:8.5.3"
|
resolution: "react-textarea-autosize@npm:8.5.3"
|
||||||
|
Loading…
Reference in New Issue
Block a user