Hashtags (#92)

* feat: hashtags

* Show tag in header
This commit is contained in:
Kieran 2023-01-19 18:00:56 +00:00 committed by GitHub
parent 39cd6fc3a8
commit 147c7502dc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 91 additions and 32 deletions

View File

@ -1,9 +1,10 @@
import { Link } from 'react-router-dom'
import './Hashtag.css'
const Hashtag = ({ children }: any) => {
const Hashtag = ({ tag }: { tag: string }) => {
return (
<span className="hashtag">
{children}
<Link to={`/t/${tag}`} onClick={(e) => e.stopPropagation()}>#{tag}</Link>
</span>
)
}

View File

@ -100,6 +100,9 @@ function extractMentions(fragments: Fragment[], tags: Tag[], users: Map<string,
let eText = hexToBech32("note", ref.Event!).substring(0, 12);
return <Link key={ref.Event} to={eventLink(ref.Event!)} onClick={(e) => e.stopPropagation()}>#{eText}</Link>;
}
case "t": {
return <Hashtag tag={ref.Hashtag!} />
}
}
}
return <b style={{ color: "var(--error)" }}>{matchTag[0]}?</b>;
@ -132,7 +135,7 @@ function extractHashtags(fragments: Fragment[]) {
if (typeof f === "string") {
return f.split(HashtagRegex).map(i => {
if (i.toLowerCase().startsWith("#")) {
return <Hashtag>{i}</Hashtag>
return <Hashtag tag={i.substring(1)} />
} else {
return i;
}

View File

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

View File

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

View File

@ -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<HexKey>, 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<number>(60 * 60);
const [until, setUntil] = useState<number>(now);
const [since, setSince] = useState<number>(now - window);
const [trackingEvents, setTrackingEvent] = useState<u256[]>([]);
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<HexKey>, 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;

View File

@ -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: <DonatePage />
},
{
path: "/t/:tag",
element: <HashTagsPage />
}
]
}

View File

@ -37,6 +37,11 @@ export class Subscriptions {
*/
PTags?: Set<u256>;
/**
* A list of "t" tags to search
*/
HashTags?: Set<string>;
/**
* 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;
}

View File

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

View File

@ -34,6 +34,7 @@ export type RawReqFilter = {
kinds?: number[],
"#e"?: u256[],
"#p"?: u256[],
"#t"?: string[],
since?: number,
until?: number,
limit?: number

View File

@ -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 (
<>
<h2>#{tag}</h2>
<Timeline subject={{ type: "hashtag", items: [tag] }} postsOnly={false} method={"TIME_RANGE"} />
</>
)
}
export default HashTagsPage;

View File

@ -49,15 +49,15 @@ export default function ProfilePage() {
return (
<div className="name">
<h2>
{user?.display_name || user?.name || 'Nostrich'}
<FollowsYou pubkey={id} />
{user?.display_name || user?.name || 'Nostrich'}
<FollowsYou pubkey={id} />
</h2>
<Copy text={params.id || ""} />
{user?.nip05 && <Nip05 nip05={user.nip05} pubkey={user.pubkey} />}
</div>
)
}
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 <Timeline key={id} pubkeys={[id]} global={false} postsOnly={false} method={"LIMIT_UNTIL"} />;
return <Timeline key={id} subject={{ type: "pubkey", items: [id] }} postsOnly={false} method={"LIMIT_UNTIL"} />;
case ProfileTab.Follows: {
if (isMe) {
return (

View File

@ -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() {
</div>
</div></> : null}
{followHints()}
<Timeline key={tab} pubkeys={follows} global={loggedOut || tab === RootTab.Global} postsOnly={tab === RootTab.Posts} method={"TIME_RANGE"} />
<Timeline key={tab} subject={timelineSubect} postsOnly={tab === RootTab.Posts} method={"TIME_RANGE"} />
</>
);
}