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-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",

View File

@ -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);
}

View File

@ -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>
}

View File

@ -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);
} }

View File

@ -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" />)}
</> </>
} }

View File

@ -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 {

View File

@ -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;

View File

@ -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 {

View File

@ -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"