commit
c190634762
@ -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
|
||||||
*/
|
*/
|
||||||
|
@ -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;
|
||||||
|
@ -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
5
src/Nostr/Nips.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
enum Nips {
|
||||||
|
Search = 50
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Nips;
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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() });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
|
@ -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
51
src/Pages/SearchPage.tsx
Normal 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;
|
@ -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 />
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user