From 147c7502dccd15fe0a07a0715e48f1660c0e2db8 Mon Sep 17 00:00:00 2001 From: Kieran Date: Thu, 19 Jan 2023 18:00:56 +0000 Subject: [PATCH] Hashtags (#92) * feat: hashtags * Show tag in header --- src/element/Hashtag.tsx | 5 +++-- src/element/Text.tsx | 5 ++++- src/element/Timeline.tsx | 12 +++++------- src/feed/EventPublisher.ts | 16 ++++++++++++---- src/feed/TimelineFeed.ts | 34 +++++++++++++++++++++------------- src/index.tsx | 5 +++++ src/nostr/Subscriptions.ts | 8 ++++++++ src/nostr/Tag.ts | 8 ++++++++ src/nostr/index.ts | 1 + src/pages/HashTagsPage.tsx | 16 ++++++++++++++++ src/pages/ProfilePage.tsx | 8 ++++---- src/pages/Root.tsx | 5 ++++- 12 files changed, 91 insertions(+), 32 deletions(-) create mode 100644 src/pages/HashTagsPage.tsx diff --git a/src/element/Hashtag.tsx b/src/element/Hashtag.tsx index 8a6b7b32..78a5e5c3 100644 --- a/src/element/Hashtag.tsx +++ b/src/element/Hashtag.tsx @@ -1,9 +1,10 @@ +import { Link } from 'react-router-dom' import './Hashtag.css' -const Hashtag = ({ children }: any) => { +const Hashtag = ({ tag }: { tag: string }) => { return ( - {children} + e.stopPropagation()}>#{tag} ) } diff --git a/src/element/Text.tsx b/src/element/Text.tsx index 9aeddeca..762143aa 100644 --- a/src/element/Text.tsx +++ b/src/element/Text.tsx @@ -100,6 +100,9 @@ function extractMentions(fragments: Fragment[], tags: Tag[], users: Map e.stopPropagation()}>#{eText}; } + case "t": { + return + } } } return {matchTag[0]}?; @@ -132,7 +135,7 @@ function extractHashtags(fragments: Fragment[]) { if (typeof f === "string") { return f.split(HashtagRegex).map(i => { if (i.toLowerCase().startsWith("#")) { - return {i} + return } else { return i; } diff --git a/src/element/Timeline.tsx b/src/element/Timeline.tsx index 1d5cea10..184dec74 100644 --- a/src/element/Timeline.tsx +++ b/src/element/Timeline.tsx @@ -1,24 +1,22 @@ import { useMemo } from "react"; -import useTimelineFeed from "../feed/TimelineFeed"; -import { HexKey, TaggedRawEvent, u256 } from "../nostr"; +import useTimelineFeed, { TimelineSubject } from "../feed/TimelineFeed"; +import { TaggedRawEvent } from "../nostr"; import EventKind from "../nostr/EventKind"; import LoadMore from "./LoadMore"; import Note from "./Note"; import NoteReaction from "./NoteReaction"; export interface TimelineProps { - global: boolean, postsOnly: boolean, - pubkeys: HexKey[], + subject: TimelineSubject, method: "TIME_RANGE" | "LIMIT_UNTIL" } /** * A list of notes by pubkeys */ -export default function Timeline({ global, pubkeys, postsOnly = false, method }: TimelineProps) { - const { main, others, loadMore } = useTimelineFeed(pubkeys, { - global, +export default function Timeline({ subject, postsOnly = false, method }: TimelineProps) { + const { main, others, loadMore } = useTimelineFeed(subject, { method }); diff --git a/src/feed/EventPublisher.ts b/src/feed/EventPublisher.ts index 388dd553..5f4b4f94 100644 --- a/src/feed/EventPublisher.ts +++ b/src/feed/EventPublisher.ts @@ -6,6 +6,7 @@ import Tag from "../nostr/Tag"; import { RootState } from "../state/Store"; import { HexKey, RawEvent, u256, UserMetadata } from "../nostr"; import { bech32ToHex } from "../Util" +import { HashtagRegex } from "../Const"; declare global { interface Window { @@ -41,7 +42,7 @@ export default function useEventPublisher() { return ev; } - function processMentions(ev: NEvent, msg: string) { + function processContent(ev: NEvent, msg: string) { const replaceNpub = (match: string) => { const npub = match.slice(1); try { @@ -53,7 +54,14 @@ export default function useEventPublisher() { return match } } - let content = msg.replace(/@npub[a-z0-9]+/g, replaceNpub) + const replaceHashtag = (match: string) => { + const tag = match.slice(1); + const idx = ev.Tags.length; + ev.Tags.push(new Tag(["t", tag.toLowerCase()], idx)); + return `#[${idx}]`; + } + let content = msg.replace(/@npub[a-z0-9]+/g, replaceNpub); + content = content.replace(HashtagRegex, replaceHashtag); ev.Content = content; } @@ -76,7 +84,7 @@ export default function useEventPublisher() { if (pubKey) { let ev = NEvent.ForPubKey(pubKey); ev.Kind = EventKind.TextNote; - processMentions(ev, msg); + processContent(ev, msg); return await signEvent(ev); } }, @@ -113,7 +121,7 @@ export default function useEventPublisher() { ev.Tags.push(new Tag(["p", replyTo.PubKey], ev.Tags.length)); } } - processMentions(ev, msg); + processContent(ev, msg); return await signEvent(ev); } }, diff --git a/src/feed/TimelineFeed.ts b/src/feed/TimelineFeed.ts index 52b79ea9..a72c5d90 100644 --- a/src/feed/TimelineFeed.ts +++ b/src/feed/TimelineFeed.ts @@ -1,36 +1,44 @@ import { useEffect, useMemo, useState } from "react"; -import { HexKey, u256 } from "../nostr"; +import { u256 } from "../nostr"; import EventKind from "../nostr/EventKind"; import { Subscriptions } from "../nostr/Subscriptions"; import { unixNow } from "../Util"; import useSubscription from "./Subscription"; export interface TimelineFeedOptions { - global: boolean, method: "TIME_RANGE" | "LIMIT_UNTIL" } -export default function useTimelineFeed(pubKeys: HexKey | Array, options: TimelineFeedOptions) { +export interface TimelineSubject { + type: "pubkey" | "hashtag" | "global", + items: string[] +} + +export default function useTimelineFeed(subject: TimelineSubject, options: TimelineFeedOptions) { const now = unixNow(); const [window, setWindow] = useState(60 * 60); const [until, setUntil] = useState(now); const [since, setSince] = useState(now - window); const [trackingEvents, setTrackingEvent] = useState([]); - const subTab = options.global ? "global" : "follows"; const sub = useMemo(() => { - if (!Array.isArray(pubKeys)) { - pubKeys = [pubKeys]; - } - - if (!options.global && (!pubKeys || pubKeys.length === 0)) { + if (subject.type !== "global" && subject.items.length == 0) { return null; } let sub = new Subscriptions(); - sub.Id = `timeline:${subTab}`; - sub.Authors = options.global ? undefined : new Set(pubKeys); + sub.Id = `timeline:${subject.type}`; sub.Kinds = new Set([EventKind.TextNote, EventKind.Repost]); + switch (subject.type) { + case "pubkey": { + sub.Authors = new Set(subject.items); + break; + } + case "hashtag": { + sub.HashTags = new Set(subject.items); + break; + } + } if (options.method === "LIMIT_UNTIL") { sub.Until = until; sub.Limit = 10; @@ -43,14 +51,14 @@ export default function useTimelineFeed(pubKeys: HexKey | Array, options } return sub; - }, [pubKeys, until, since, window]); + }, [subject, until, since, window]); const main = useSubscription(sub, { leaveOpen: true }); const subNext = useMemo(() => { if (trackingEvents.length > 0) { let sub = new Subscriptions(); - sub.Id = `timeline-related:${subTab}`; + sub.Id = `timeline-related:${subject.type}`; sub.Kinds = new Set([EventKind.Reaction, EventKind.Deletion, EventKind.Repost]); sub.ETags = new Set(trackingEvents); return sub; diff --git a/src/index.tsx b/src/index.tsx index 37dceb95..32d73d05 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -25,6 +25,7 @@ import VerificationPage from './pages/Verification'; import MessagesPage from './pages/MessagesPage'; import ChatPage from './pages/ChatPage'; import DonatePage from './pages/DonatePage'; +import HashTagsPage from './pages/HashTagsPage'; /** * HTTP query provider @@ -81,6 +82,10 @@ const router = createBrowserRouter([ { path: "/donate", element: + }, + { + path: "/t/:tag", + element: } ] } diff --git a/src/nostr/Subscriptions.ts b/src/nostr/Subscriptions.ts index bf6fd9ba..676886c1 100644 --- a/src/nostr/Subscriptions.ts +++ b/src/nostr/Subscriptions.ts @@ -37,6 +37,11 @@ export class Subscriptions { */ PTags?: Set; + /** + * A list of "t" tags to search + */ + HashTags?: Set; + /** * a timestamp, events must be newer than this to pass */ @@ -125,6 +130,9 @@ export class Subscriptions { if (this.PTags) { ret["#p"] = Array.from(this.PTags); } + if(this.HashTags) { + ret["#t"] = Array.from(this.HashTags); + } if (this.Since !== null) { ret.since = this.Since; } diff --git a/src/nostr/Tag.ts b/src/nostr/Tag.ts index 2342914c..33e00417 100644 --- a/src/nostr/Tag.ts +++ b/src/nostr/Tag.ts @@ -7,6 +7,7 @@ export default class Tag { PubKey?: HexKey; Relay?: string; Marker?: string; + Hashtag?: string; Index: number; Invalid: boolean; @@ -35,6 +36,10 @@ export default class Tag { } break; } + case "t": { + this.Hashtag = tag[1]; + break; + } case "delegation": { this.PubKey = tag[1]; break; @@ -53,6 +58,9 @@ export default class Tag { case "p": { return this.PubKey ? ["p", this.PubKey] : null; } + case "t": { + return ["t", this.Hashtag!]; + } default: { return this.Original; } diff --git a/src/nostr/index.ts b/src/nostr/index.ts index 62da4517..cac6946e 100644 --- a/src/nostr/index.ts +++ b/src/nostr/index.ts @@ -34,6 +34,7 @@ export type RawReqFilter = { kinds?: number[], "#e"?: u256[], "#p"?: u256[], + "#t"?: string[], since?: number, until?: number, limit?: number diff --git a/src/pages/HashTagsPage.tsx b/src/pages/HashTagsPage.tsx new file mode 100644 index 00000000..0609e8e5 --- /dev/null +++ b/src/pages/HashTagsPage.tsx @@ -0,0 +1,16 @@ +import { useParams } from "react-router-dom"; +import Timeline from "../element/Timeline"; + +const HashTagsPage = () => { + const params = useParams(); + const tag = params.tag!.toLowerCase(); + + return ( + <> +

#{tag}

+ + + ) +} + +export default HashTagsPage; \ No newline at end of file diff --git a/src/pages/ProfilePage.tsx b/src/pages/ProfilePage.tsx index de29dbec..d697d63e 100644 --- a/src/pages/ProfilePage.tsx +++ b/src/pages/ProfilePage.tsx @@ -49,15 +49,15 @@ export default function ProfilePage() { return (

- {user?.display_name || user?.name || 'Nostrich'} - + {user?.display_name || user?.name || 'Nostrich'} +

{user?.nip05 && }
) } - + function bio() { const lnurl = extractLnAddress(user?.lud16 || user?.lud06 || ""); return ( @@ -88,7 +88,7 @@ export default function ProfilePage() { function tabContent() { switch (tab) { case ProfileTab.Notes: - return ; + return ; case ProfileTab.Follows: { if (isMe) { return ( diff --git a/src/pages/Root.tsx b/src/pages/Root.tsx index 4b496cc9..d5f8a12d 100644 --- a/src/pages/Root.tsx +++ b/src/pages/Root.tsx @@ -6,6 +6,7 @@ import Timeline from "../element/Timeline"; import { useState } from "react"; import { RootState } from "../state/Store"; import { HexKey } from "../nostr"; +import { TimelineSubject } from "../feed/TimelineFeed"; const RootTab = { Posts: 0, @@ -25,6 +26,8 @@ export default function RootPage() { } } + const isGlobal = loggedOut || tab === RootTab.Global; + const timelineSubect: TimelineSubject = isGlobal ? { type: "global", items: [] } : { type: "pubkey", items: follows }; return ( <> {pubKey ? <> @@ -41,7 +44,7 @@ export default function RootPage() { : null} {followHints()} - + ); } \ No newline at end of file