forked from Kieran/snort
feat: show latest
This commit is contained in:
parent
abeb2f5a6c
commit
d66f9ab18d
@ -12,7 +12,7 @@ export default function FollowersList({ pubkey }: FollowersListProps) {
|
||||
const feed = useFollowersFeed(pubkey);
|
||||
|
||||
const pubkeys = useMemo(() => {
|
||||
let contactLists = feed?.notes.filter(a => a.kind === EventKind.ContactList && a.tags.some(b => b[0] === "p" && b[1] === pubkey));
|
||||
let contactLists = feed?.store.notes.filter(a => a.kind === EventKind.ContactList && a.tags.some(b => b[0] === "p" && b[1] === pubkey));
|
||||
return [...new Set(contactLists?.map(a => a.pubkey))];
|
||||
}, [feed]);
|
||||
|
||||
|
@ -12,7 +12,7 @@ export default function FollowsList({ pubkey }: FollowsListProps) {
|
||||
const feed = useFollowsFeed(pubkey);
|
||||
|
||||
const pubkeys = useMemo(() => {
|
||||
return getFollowers(feed, pubkey);
|
||||
return getFollowers(feed.store, pubkey);
|
||||
}, [feed]);
|
||||
|
||||
return <FollowListBase pubkeys={pubkeys} title={`Following ${pubkeys?.length}`} />
|
||||
|
@ -15,7 +15,7 @@ export default function FollowsYou({ pubkey }: FollowsYouProps ) {
|
||||
const loginPubKey = useSelector<RootState, HexKey | undefined>(s => s.login.publicKey);
|
||||
|
||||
const pubkeys = useMemo(() => {
|
||||
return getFollowers(feed, pubkey);
|
||||
return getFollowers(feed.store, pubkey);
|
||||
}, [feed]);
|
||||
|
||||
const followsMe = pubkeys.includes(loginPubKey!) ?? false ;
|
||||
|
5
src/Element/Timeline.css
Normal file
5
src/Element/Timeline.css
Normal file
@ -0,0 +1,5 @@
|
||||
.latest-notes {
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
user-select: none;
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
import "./Timeline.css";
|
||||
import { useMemo } from "react";
|
||||
import useTimelineFeed, { TimelineSubject } from "Feed/TimelineFeed";
|
||||
import { TaggedRawEvent } from "Nostr";
|
||||
@ -5,6 +6,8 @@ import EventKind from "Nostr/EventKind";
|
||||
import LoadMore from "Element/LoadMore";
|
||||
import Note from "Element/Note";
|
||||
import NoteReaction from "Element/NoteReaction";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faFastForward, faForward } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
export interface TimelineProps {
|
||||
postsOnly: boolean,
|
||||
@ -16,18 +19,26 @@ export interface TimelineProps {
|
||||
* A list of notes by pubkeys
|
||||
*/
|
||||
export default function Timeline({ subject, postsOnly = false, method }: TimelineProps) {
|
||||
const { main, others, loadMore } = useTimelineFeed(subject, {
|
||||
const { main, related, latest, loadMore, showLatest } = useTimelineFeed(subject, {
|
||||
method
|
||||
});
|
||||
|
||||
const filterPosts = (notes: TaggedRawEvent[]) => {
|
||||
return [...notes].sort((a, b) => b.created_at - a.created_at)?.filter(a => postsOnly ? !a.tags.some(b => b[0] === "e") : true);
|
||||
}
|
||||
|
||||
const mainFeed = useMemo(() => {
|
||||
return main?.sort((a, b) => b.created_at - a.created_at)?.filter(a => postsOnly ? !a.tags.some(b => b[0] === "e") : true);
|
||||
return filterPosts(main.notes);
|
||||
}, [main]);
|
||||
|
||||
const latestFeed = useMemo(() => {
|
||||
return filterPosts(latest.notes);
|
||||
}, [latest]);
|
||||
|
||||
function eventElement(e: TaggedRawEvent) {
|
||||
switch (e.kind) {
|
||||
case EventKind.TextNote: {
|
||||
return <Note key={e.id} data={e} related={others} />
|
||||
return <Note key={e.id} data={e} related={related.notes} />
|
||||
}
|
||||
case EventKind.Reaction:
|
||||
case EventKind.Repost: {
|
||||
@ -38,6 +49,11 @@ export default function Timeline({ subject, postsOnly = false, method }: Timelin
|
||||
|
||||
return (
|
||||
<>
|
||||
{latestFeed.length > 0 && (<div className="card latest-notes pointer" onClick={() => showLatest()}>
|
||||
<FontAwesomeIcon icon={faForward} size="xl"/>
|
||||
|
||||
Show latest {latestFeed.length} notes
|
||||
</div>)}
|
||||
{mainFeed.map(eventElement)}
|
||||
{mainFeed.length > 0 ? <LoadMore onLoadMore={loadMore} /> : null}
|
||||
</>
|
||||
|
@ -47,13 +47,13 @@ export default function useLoginFeed() {
|
||||
const main = useSubscription(sub, { leaveOpen: true });
|
||||
|
||||
useEffect(() => {
|
||||
let contactList = main.notes.filter(a => a.kind === EventKind.ContactList);
|
||||
let notifications = main.notes.filter(a => a.kind === EventKind.TextNote);
|
||||
let metadata = main.notes.filter(a => a.kind === EventKind.SetMetadata);
|
||||
let contactList = main.store.notes.filter(a => a.kind === EventKind.ContactList);
|
||||
let notifications = main.store.notes.filter(a => a.kind === EventKind.TextNote);
|
||||
let metadata = main.store.notes.filter(a => a.kind === EventKind.SetMetadata);
|
||||
let profiles = metadata.map(a => mapEventToProfile(a))
|
||||
.filter(a => a !== undefined)
|
||||
.map(a => a!);
|
||||
let dms = main.notes.filter(a => a.kind === EventKind.DirectMessage);
|
||||
let dms = main.store.notes.filter(a => a.kind === EventKind.DirectMessage);
|
||||
|
||||
for (let cl of contactList) {
|
||||
if (cl.content !== "") {
|
||||
@ -87,7 +87,7 @@ export default function useLoginFeed() {
|
||||
}
|
||||
}
|
||||
})().catch(console.warn);
|
||||
}, [main]);
|
||||
}, [main.store]);
|
||||
}
|
||||
|
||||
async function makeNotification(ev: TaggedRawEvent) {
|
||||
|
@ -13,27 +13,38 @@ export type UseSubscriptionOptions = {
|
||||
}
|
||||
|
||||
interface ReducerArg {
|
||||
type: "END" | "EVENT"
|
||||
ev?: TaggedRawEvent,
|
||||
type: "END" | "EVENT" | "CLEAR",
|
||||
ev?: TaggedRawEvent | Array<TaggedRawEvent>,
|
||||
end?: boolean
|
||||
}
|
||||
|
||||
function notesReducer(state: NoteStore, arg: ReducerArg) {
|
||||
if (arg.type === "END") {
|
||||
state.end = arg.end!;
|
||||
return state;
|
||||
return {
|
||||
notes: state.notes,
|
||||
end: arg.end!
|
||||
} as NoteStore;
|
||||
}
|
||||
|
||||
let ev = arg.ev!;
|
||||
if (state.notes.some(a => a.id === ev.id)) {
|
||||
//state.notes.find(a => a.id == ev.id)?.relays?.push(ev.relays[0]);
|
||||
return state;
|
||||
if (arg.type === "CLEAR") {
|
||||
return {
|
||||
notes: [],
|
||||
end: state.end,
|
||||
} as NoteStore;
|
||||
}
|
||||
|
||||
let evs = arg.ev!;
|
||||
if (!Array.isArray(evs)) {
|
||||
evs = [evs];
|
||||
}
|
||||
evs = evs.filter(a => !state.notes.some(b => b.id === a.id));
|
||||
if (evs.length === 0) {
|
||||
return state;
|
||||
}
|
||||
return {
|
||||
notes: [
|
||||
...state.notes,
|
||||
ev
|
||||
...evs
|
||||
]
|
||||
} as NoteStore;
|
||||
}
|
||||
@ -43,13 +54,19 @@ const initStore: NoteStore = {
|
||||
end: false
|
||||
};
|
||||
|
||||
export interface UseSubscriptionState {
|
||||
store: NoteStore,
|
||||
clear: () => void,
|
||||
append: (notes: TaggedRawEvent[]) => void
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Subscriptions} sub
|
||||
* @param {any} opt
|
||||
* @returns
|
||||
*/
|
||||
export default function useSubscription(sub: Subscriptions | null, options?: UseSubscriptionOptions) {
|
||||
export default function useSubscription(sub: Subscriptions | null, options?: UseSubscriptionOptions): UseSubscriptionState {
|
||||
const [state, dispatch] = useReducer(notesReducer, initStore);
|
||||
const [debounce, setDebounce] = useState<number>(0);
|
||||
|
||||
@ -91,5 +108,17 @@ export default function useSubscription(sub: Subscriptions | null, options?: Use
|
||||
return () => clearTimeout(t);
|
||||
}, [state]);
|
||||
|
||||
return useMemo(() => state, [debounce]);
|
||||
const stateDebounced = useMemo(() => state, [debounce]);
|
||||
return {
|
||||
store: stateDebounced,
|
||||
clear: () => {
|
||||
dispatch({ type: "CLEAR" });
|
||||
},
|
||||
append: (n: TaggedRawEvent[]) => {
|
||||
dispatch({
|
||||
type: "EVENT",
|
||||
ev: n
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -37,13 +37,13 @@ export default function useThreadFeed(id: u256) {
|
||||
useEffect(() => {
|
||||
// debounce
|
||||
let t = setTimeout(() => {
|
||||
let eTags = main.notes.map(a => a.tags.filter(b => b[0] === "e").map(b => b[1])).flat();
|
||||
let ids = main.notes.map(a => a.id);
|
||||
let eTags = main.store.notes.map(a => a.tags.filter(b => b[0] === "e").map(b => b[1])).flat();
|
||||
let ids = main.store.notes.map(a => a.id);
|
||||
let allEvents = new Set([...eTags, ...ids]);
|
||||
addId(Array.from(allEvents));
|
||||
}, 200);
|
||||
return () => clearTimeout(t);
|
||||
}, [main.notes]);
|
||||
}, [main.store]);
|
||||
|
||||
return main;
|
||||
}
|
@ -19,13 +19,13 @@ export interface TimelineSubject {
|
||||
|
||||
export default function useTimelineFeed(subject: TimelineSubject, options: TimelineFeedOptions) {
|
||||
const now = unixNow();
|
||||
const [window, setWindow] = useState<number>(60 * 60);
|
||||
const [window, setWindow] = useState<number>(60 * 10);
|
||||
const [until, setUntil] = useState<number>(now);
|
||||
const [since, setSince] = useState<number>(now - window);
|
||||
const [trackingEvents, setTrackingEvent] = useState<u256[]>([]);
|
||||
const pref = useSelector<RootState, UserPreferences>(s => s.login.preferences);
|
||||
|
||||
const sub = useMemo(() => {
|
||||
function createSub() {
|
||||
if (subject.type !== "global" && subject.items.length == 0) {
|
||||
return null;
|
||||
}
|
||||
@ -43,6 +43,12 @@ export default function useTimelineFeed(subject: TimelineSubject, options: Timel
|
||||
break;
|
||||
}
|
||||
}
|
||||
return sub;
|
||||
}
|
||||
|
||||
const sub = useMemo(() => {
|
||||
let sub = createSub();
|
||||
if (sub) {
|
||||
if (options.method === "LIMIT_UNTIL") {
|
||||
sub.Until = until;
|
||||
sub.Limit = 10;
|
||||
@ -54,11 +60,32 @@ export default function useTimelineFeed(subject: TimelineSubject, options: Timel
|
||||
}
|
||||
}
|
||||
|
||||
if (pref.autoShowLatest) {
|
||||
// copy properties of main sub but with limit 0
|
||||
// this will put latest directly into main feed
|
||||
let latestSub = new Subscriptions();
|
||||
latestSub.Ids = sub.Ids;
|
||||
latestSub.Kinds = sub.Kinds;
|
||||
latestSub.Limit = 0;
|
||||
sub.AddSubscription(latestSub);
|
||||
}
|
||||
}
|
||||
return sub;
|
||||
}, [subject.type, subject.items, until, since, window]);
|
||||
|
||||
const main = useSubscription(sub, { leaveOpen: true });
|
||||
|
||||
const subRealtime = useMemo(() => {
|
||||
let subLatest = createSub();
|
||||
if (subLatest && !pref.autoShowLatest) {
|
||||
subLatest.Id = `${subLatest.Id}:latest`;
|
||||
subLatest.Limit = 0;
|
||||
}
|
||||
return subLatest;
|
||||
}, [subject.type, subject.items]);
|
||||
|
||||
const latest = useSubscription(subRealtime, { leaveOpen: true });
|
||||
|
||||
const subNext = useMemo(() => {
|
||||
if (trackingEvents.length > 0 && pref.enableReactions) {
|
||||
let sub = new Subscriptions();
|
||||
@ -73,26 +100,32 @@ export default function useTimelineFeed(subject: TimelineSubject, options: Timel
|
||||
const others = useSubscription(subNext, { leaveOpen: true });
|
||||
|
||||
useEffect(() => {
|
||||
if (main.notes.length > 0) {
|
||||
if (main.store.notes.length > 0) {
|
||||
setTrackingEvent(s => {
|
||||
let ids = main.notes.map(a => a.id);
|
||||
let ids = main.store.notes.map(a => a.id);
|
||||
let temp = new Set([...s, ...ids]);
|
||||
return Array.from(temp);
|
||||
});
|
||||
}
|
||||
}, [main.notes]);
|
||||
}, [main.store]);
|
||||
|
||||
return {
|
||||
main: main.notes,
|
||||
others: others.notes,
|
||||
main: main.store,
|
||||
related: others.store,
|
||||
latest: latest.store,
|
||||
loadMore: () => {
|
||||
console.debug("Timeline load more!")
|
||||
if (options.method === "LIMIT_UNTIL") {
|
||||
let oldest = main.notes.reduce((acc, v) => acc = v.created_at < acc ? v.created_at : acc, unixNow());
|
||||
let oldest = main.store.notes.reduce((acc, v) => acc = v.created_at < acc ? v.created_at : acc, unixNow());
|
||||
setUntil(oldest);
|
||||
} else {
|
||||
setUntil(s => s - window);
|
||||
setSince(s => s - window);
|
||||
}
|
||||
},
|
||||
showLatest: () => {
|
||||
main.append(latest.store.notes);
|
||||
latest.clear();
|
||||
}
|
||||
};
|
||||
}
|
@ -8,5 +8,5 @@ export default function EventPage() {
|
||||
const id = parseId(params.id!);
|
||||
const thread = useThreadFeed(id);
|
||||
|
||||
return <Thread notes={thread.notes} this={id} />;
|
||||
return <Thread notes={thread.store.notes} this={id} />;
|
||||
}
|
@ -51,11 +51,11 @@ export default function NotificationsPage() {
|
||||
<>
|
||||
{sorted?.map(a => {
|
||||
if (a.kind === EventKind.TextNote) {
|
||||
return <Note data={a} key={a.id} related={otherNotes?.notes ?? []} />
|
||||
return <Note data={a} key={a.id} related={otherNotes?.store.notes ?? []} />
|
||||
} else if (a.kind === EventKind.Reaction) {
|
||||
let ev = new Event(a);
|
||||
let reactedTo = ev.Thread?.ReplyTo?.Event ?? ev.Thread?.Root?.Event;
|
||||
let reactedNote = otherNotes?.notes?.find(c => c.id === reactedTo);
|
||||
let reactedNote = otherNotes?.store.notes?.find(c => c.id === reactedTo);
|
||||
return <NoteReaction data={a} key={a.id} root={reactedNote} />
|
||||
}
|
||||
return null;
|
||||
|
@ -50,6 +50,15 @@ const PreferencesPage = () => {
|
||||
<input type="checkbox" checked={perf.confirmReposts} onChange={e => dispatch(setPreferences({ ...perf, confirmReposts: e.target.checked }))} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="card flex">
|
||||
<div className="flex f-col f-grow">
|
||||
<div>Automatically show latest notes</div>
|
||||
<small>Notes will stream in real time into global and posts tab</small>
|
||||
</div>
|
||||
<div>
|
||||
<input type="checkbox" checked={perf.autoShowLatest} onChange={e => dispatch(setPreferences({ ...perf, autoShowLatest: e.target.checked }))} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="card flex">
|
||||
<div className="flex f-col f-grow">
|
||||
<div>Debug Menus</div>
|
||||
|
@ -31,6 +31,11 @@ export interface UserPreferences {
|
||||
*/
|
||||
confirmReposts: boolean,
|
||||
|
||||
/**
|
||||
* Automatically show the latests notes
|
||||
*/
|
||||
autoShowLatest: boolean,
|
||||
|
||||
/**
|
||||
* Show debugging menus to help diagnose issues
|
||||
*/
|
||||
@ -110,7 +115,8 @@ const InitState = {
|
||||
autoLoadMedia: true,
|
||||
theme: "system",
|
||||
confirmReposts: false,
|
||||
showDebugMenus: false
|
||||
showDebugMenus: false,
|
||||
autoShowLatest: false
|
||||
}
|
||||
} as LoginStore;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user