Merge pull request #143 from v0l/search

Search
This commit is contained in:
Kieran 2023-01-28 15:53:42 +00:00 committed by GitHub
commit c190634762
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 131 additions and 17 deletions

View File

@ -27,6 +27,13 @@ export const DefaultRelays = new Map<string, RelaySettings>([
["wss://relay.snort.social", { read: true, write: true }] ["wss://relay.snort.social", { read: true, write: true }]
]); ]);
/**
* Default search relays
*/
export const SearchRelays = new Map<string, RelaySettings>([
["wss://relay.nostr.band", { read: true, write: false }],
]);
/** /**
* List of recommended follows for new users * List of recommended follows for new users
*/ */

View File

@ -13,7 +13,7 @@ export interface TimelineFeedOptions {
} }
export interface TimelineSubject { export interface TimelineSubject {
type: "pubkey" | "hashtag" | "global" | "ptag", type: "pubkey" | "hashtag" | "global" | "ptag" | "keyword",
items: string[] items: string[]
} }
@ -47,6 +47,10 @@ export default function useTimelineFeed(subject: TimelineSubject, options: Timel
sub.PTags = new Set(subject.items); sub.PTags = new Set(subject.items);
break; break;
} }
case "keyword": {
sub.Search = subject.items[0];
break;
}
} }
return sub; return sub;
}, [subject.type, subject.items]); }, [subject.type, subject.items]);
@ -72,6 +76,7 @@ export default function useTimelineFeed(subject: TimelineSubject, options: Timel
latestSub.Authors = sub.Authors; latestSub.Authors = sub.Authors;
latestSub.HashTags = sub.HashTags; latestSub.HashTags = sub.HashTags;
latestSub.Kinds = sub.Kinds; latestSub.Kinds = sub.Kinds;
latestSub.Search = sub.Search;
latestSub.Limit = 1; latestSub.Limit = 1;
latestSub.Since = Math.floor(new Date().getTime() / 1000); latestSub.Since = Math.floor(new Date().getTime() / 1000);
sub.AddSubscription(latestSub); sub.AddSubscription(latestSub);
@ -123,7 +128,7 @@ export default function useTimelineFeed(subject: TimelineSubject, options: Timel
if (main.store.notes.length > 0) { if (main.store.notes.length > 0) {
setTrackingEvent(s => { setTrackingEvent(s => {
let ids = main.store.notes.map(a => a.id); let ids = main.store.notes.map(a => a.id);
if(ids.some(a => !s.includes(a))) { if (ids.some(a => !s.includes(a))) {
return Array.from(new Set([...s, ...ids])); return Array.from(new Set([...s, ...ids]));
} }
return s; return s;
@ -165,4 +170,4 @@ export default function useTimelineFeed(subject: TimelineSubject, options: Timel
latest.clear(); latest.clear();
} }
}; };
} }

View File

@ -7,6 +7,7 @@ import { DefaultConnectTimeout } from "Const";
import { ConnectionStats } from "Nostr/ConnectionStats"; import { ConnectionStats } from "Nostr/ConnectionStats";
import { RawEvent, TaggedRawEvent, u256 } from "Nostr"; import { RawEvent, TaggedRawEvent, u256 } from "Nostr";
import { RelayInfo } from "./RelayInfo"; import { RelayInfo } from "./RelayInfo";
import Nips from "./Nips";
export type CustomHook = (state: Readonly<StateSnapshot>) => void; export type CustomHook = (state: Readonly<StateSnapshot>) => void;
@ -238,6 +239,11 @@ export default class Connection {
return; return;
} }
// check relay supports search
if (sub.Search && !this.SupportsNip(Nips.Search)) {
return;
}
if (this.Subscriptions.has(sub.Id)) { if (this.Subscriptions.has(sub.Id)) {
return; return;
} }
@ -281,6 +287,13 @@ export default class Connection {
return this.LastState; return this.LastState;
} }
/**
* Using relay document to determine if this relay supports a feature
*/
SupportsNip(n: number) {
return this.Info?.supported_nips?.some(a => a === n) ?? false;
}
_UpdateState() { _UpdateState() {
this.CurrentState.connected = this.Socket?.readyState === WebSocket.OPEN; this.CurrentState.connected = this.Socket?.readyState === WebSocket.OPEN;
this.CurrentState.events.received = this.Stats.EventsReceived; this.CurrentState.events.received = this.Stats.EventsReceived;

5
src/Nostr/Nips.ts Normal file
View File

@ -0,0 +1,5 @@
enum Nips {
Search = 50
}
export default Nips;

View File

@ -42,6 +42,11 @@ export class Subscriptions {
*/ */
HashTags?: Set<string>; HashTags?: Set<string>;
/**
* A list of search terms
*/
Search?: string;
/** /**
* a timestamp, events must be newer than this to pass * a timestamp, events must be newer than this to pass
*/ */
@ -89,6 +94,7 @@ export class Subscriptions {
this.Kinds = sub?.kinds ? new Set(sub.kinds) : undefined; this.Kinds = sub?.kinds ? new Set(sub.kinds) : undefined;
this.ETags = sub?.["#e"] ? new Set(sub["#e"]) : undefined; this.ETags = sub?.["#e"] ? new Set(sub["#e"]) : undefined;
this.PTags = sub?.["#p"] ? new Set(sub["#p"]) : undefined; this.PTags = sub?.["#p"] ? new Set(sub["#p"]) : undefined;
this.Search = sub?.search ?? undefined;
this.Since = sub?.since ?? undefined; this.Since = sub?.since ?? undefined;
this.Until = sub?.until ?? undefined; this.Until = sub?.until ?? undefined;
this.Limit = sub?.limit ?? undefined; this.Limit = sub?.limit ?? undefined;
@ -133,6 +139,9 @@ export class Subscriptions {
if(this.HashTags) { if(this.HashTags) {
ret["#t"] = Array.from(this.HashTags); ret["#t"] = Array.from(this.HashTags);
} }
if (this.Search) {
ret.search = this.Search;
}
if (this.Since !== null) { if (this.Since !== null) {
ret.since = this.Since; ret.since = this.Since;
} }
@ -144,4 +153,4 @@ export class Subscriptions {
} }
return ret; return ret;
} }
} }

View File

@ -1,7 +1,7 @@
import { HexKey, TaggedRawEvent } from "Nostr"; import { HexKey, TaggedRawEvent } from "Nostr";
import { ProfileCacheExpire } from "Const";
import { mapEventToProfile, MetadataCache, } from "State/Users";
import { getDb } from "State/Users/Db"; import { getDb } from "State/Users/Db";
import { ProfileCacheExpire } from "Const";
import { mapEventToProfile, MetadataCache } from "State/Users";
import Connection, { RelaySettings } from "Nostr/Connection"; import Connection, { RelaySettings } from "Nostr/Connection";
import Event from "Nostr/Event"; import Event from "Nostr/Event";
import EventKind from "Nostr/EventKind"; import EventKind from "Nostr/EventKind";
@ -71,14 +71,14 @@ export class NostrSystem {
} }
AddSubscription(sub: Subscriptions) { AddSubscription(sub: Subscriptions) {
for (let [_, s] of this.Sockets) { for (let [a, s] of this.Sockets) {
s.AddSubscription(sub); s.AddSubscription(sub);
} }
this.Subscriptions.set(sub.Id, sub); this.Subscriptions.set(sub.Id, sub);
} }
RemoveSubscription(subId: string) { RemoveSubscription(subId: string) {
for (let [_, s] of this.Sockets) { for (let [a, s] of this.Sockets) {
s.RemoveSubscription(subId); s.RemoveSubscription(subId);
} }
this.Subscriptions.delete(subId); this.Subscriptions.delete(subId);
@ -192,9 +192,9 @@ export class NostrSystem {
let profile = mapEventToProfile(e); let profile = mapEventToProfile(e);
if (profile) { if (profile) {
let existing = await db.find(profile.pubkey); let existing = await db.find(profile.pubkey);
if((existing?.created ?? 0) < profile.created) { if ((existing?.created ?? 0) < profile.created) {
await db.put(profile); await db.put(profile);
} else if(existing) { } else if (existing) {
await db.update(profile.pubkey, { loaded: new Date().getTime() }); await db.update(profile.pubkey, { loaded: new Date().getTime() });
} }
} }

View File

@ -35,6 +35,7 @@ export type RawReqFilter = {
"#e"?: u256[], "#e"?: u256[],
"#p"?: u256[], "#p"?: u256[],
"#t"?: string[], "#t"?: string[],
search?: string,
since?: number, since?: number,
until?: number, until?: number,
limit?: number limit?: number
@ -53,4 +54,4 @@ export type UserMetadata = {
nip05?: string, nip05?: string,
lud06?: string, lud06?: string,
lud16?: string lud16?: string
} }

View File

@ -2,6 +2,18 @@
cursor: pointer; cursor: pointer;
} }
.search {
margin: 0 10px 0 10px;
}
.search input {
margin: 0 5px 0 5px;
}
.search .btn {
display: none;
}
.unread-count { .unread-count {
width: 20px; width: 20px;
height: 20px; height: 20px;

View File

@ -1,8 +1,8 @@
import "./Layout.css"; import "./Layout.css";
import { useEffect } from "react" import { useEffect, useState } from "react"
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { Outlet, useNavigate } from "react-router-dom"; import { Outlet, useNavigate } from "react-router-dom";
import { faBell, faMessage } from "@fortawesome/free-solid-svg-icons"; import { faBell, faMessage, faSearch } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { RootState } from "State/Store"; import { RootState } from "State/Store";
@ -13,6 +13,7 @@ import { System } from "Nostr/System"
import ProfileImage from "Element/ProfileImage"; import ProfileImage from "Element/ProfileImage";
import useLoginFeed from "Feed/LoginFeed"; import useLoginFeed from "Feed/LoginFeed";
import { totalUnread } from "Pages/MessagesPage"; import { totalUnread } from "Pages/MessagesPage";
import { SearchRelays } from 'Const';
export default function Layout() { export default function Layout() {
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -24,6 +25,9 @@ export default function Layout() {
const readNotifications = useSelector<RootState, number>(s => s.login.readNotifications); const readNotifications = useSelector<RootState, number>(s => s.login.readNotifications);
const dms = useSelector<RootState, RawEvent[]>(s => s.login.dms); const dms = useSelector<RootState, RawEvent[]>(s => s.login.dms);
const prefs = useSelector<RootState, UserPreferences>(s => s.login.preferences); const prefs = useSelector<RootState, UserPreferences>(s => s.login.preferences);
const [keyword, setKeyword] = useState<string>('');
useLoginFeed(); useLoginFeed();
useEffect(() => { useEffect(() => {
@ -32,7 +36,7 @@ export default function Layout() {
System.ConnectToRelay(k, v); System.ConnectToRelay(k, v);
} }
for (let [k, v] of System.Sockets) { for (let [k, v] of System.Sockets) {
if (!relays[k]) { if (!relays[k] && !SearchRelays.has(k)) {
System.DisconnectRelay(k); System.DisconnectRelay(k);
} }
} }
@ -105,12 +109,14 @@ export default function Layout() {
if (typeof isInit !== "boolean") { if (typeof isInit !== "boolean") {
return null; return null;
} }
return ( return (
<div className="page"> <div className="page">
<div className="header"> <div className="header">
<div className="logo" onClick={() => navigate("/")}>snort</div> <div className="logo" onClick={() => navigate("/")}>snort</div>
<div> <div>
<div className={`btn btn-rnd mr10`} onClick={(e) => navigate("/search")}>
<FontAwesomeIcon icon={faSearch} size="xl" />
</div>
{key ? accountHeader() : {key ? accountHeader() :
<div className="btn" onClick={() => navigate("/login")}>Login</div> <div className="btn" onClick={() => navigate("/login")}>Login</div>
} }

51
src/Pages/SearchPage.tsx Normal file
View File

@ -0,0 +1,51 @@
import { useParams } from "react-router-dom";
import Timeline from "Element/Timeline";
import { useEffect, useState } from "react";
import { debounce } from "Util";
import { router } from "index";
import { SearchRelays } from "Const";
import { System } from "Nostr/System";
const SearchPage = () => {
const params: any = useParams();
const [search, setSearch] = useState<string>();
const [keyword, setKeyword] = useState<string | undefined>(params.keyword);
useEffect(() => {
if (keyword) {
// "navigate" changing only url
router.navigate(`/search/${encodeURIComponent(keyword)}`)
}
}, [keyword]);
useEffect(() => {
return debounce(500, () => setKeyword(search));
}, [search]);
useEffect(() => {
let addedRelays: string[] = [];
for (let [k, v] of SearchRelays) {
if (!System.Sockets.has(k)) {
System.ConnectToRelay(k, v);
addedRelays.push(k);
}
}
return () => {
for (let r of addedRelays) {
System.DisconnectRelay(r);
}
}
}, []);
return (
<>
<h2>Search</h2>
<div className="flex mb10">
<input type="text" className="f-grow mr10" placeholder="Search.." value={search} onChange={e => setSearch(e.target.value)} />
</div>
{keyword && <Timeline key={keyword} subject={{ type: "keyword", items: [keyword] }} postsOnly={false} method={"LIMIT_UNTIL"} />}
</>
)
}
export default SearchPage;

View File

@ -302,4 +302,4 @@ export const {
markNotificationsRead, markNotificationsRead,
setPreferences setPreferences
} = LoginSlice.actions; } = LoginSlice.actions;
export const reducer = LoginSlice.reducer; export const reducer = LoginSlice.reducer;

View File

@ -27,6 +27,7 @@ import MessagesPage from 'Pages/MessagesPage';
import ChatPage from 'Pages/ChatPage'; import ChatPage from 'Pages/ChatPage';
import DonatePage from 'Pages/DonatePage'; import DonatePage from 'Pages/DonatePage';
import HashTagsPage from 'Pages/HashTagsPage'; import HashTagsPage from 'Pages/HashTagsPage';
import SearchPage from 'Pages/SearchPage';
/** /**
* HTTP query provider * HTTP query provider
@ -35,7 +36,7 @@ const HTTP = new QueryClient()
serviceWorkerRegistration.register(); serviceWorkerRegistration.register();
const router = createBrowserRouter([ export const router = createBrowserRouter([
{ {
element: <Layout />, element: <Layout />,
errorElement: <ErrorPage />, errorElement: <ErrorPage />,
@ -88,6 +89,10 @@ const router = createBrowserRouter([
{ {
path: "/t/:tag", path: "/t/:tag",
element: <HashTagsPage /> element: <HashTagsPage />
},
{
path: "/search/:keyword?",
element: <SearchPage />
} }
] ]
} }