forked from Kieran/snort
parent
39cd6fc3a8
commit
147c7502dc
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
});
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
},
|
||||
|
@ -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;
|
||||
|
@ -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 />
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -34,6 +34,7 @@ export type RawReqFilter = {
|
||||
kinds?: number[],
|
||||
"#e"?: u256[],
|
||||
"#p"?: u256[],
|
||||
"#t"?: string[],
|
||||
since?: number,
|
||||
until?: number,
|
||||
limit?: number
|
||||
|
16
src/pages/HashTagsPage.tsx
Normal file
16
src/pages/HashTagsPage.tsx
Normal 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;
|
@ -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 (
|
||||
|
@ -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"} />
|
||||
</>
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue
Block a user