Compare commits
10 Commits
e746109f5c
...
e0e0a857b0
Author | SHA1 | Date | |
---|---|---|---|
e0e0a857b0 | |||
c5e534a730 | |||
2099eddebc | |||
d42d26fc20 | |||
006cad49bb | |||
e652cc7703 | |||
c23856daf4 | |||
3c017f89be | |||
e2ab1b4e3f | |||
d0bc8df6a1 |
13
README.md
13
README.md
@ -65,6 +65,19 @@ To build the application and system packages, use
|
||||
$ yarn build
|
||||
```
|
||||
|
||||
Tauri desktop application:
|
||||
|
||||
```
|
||||
# install dependencies
|
||||
yarn
|
||||
|
||||
# develop
|
||||
yarn tauri dev
|
||||
|
||||
# build
|
||||
yarn tauri build
|
||||
```
|
||||
|
||||
### Translations
|
||||
|
||||
[![Crowdin](https://badges.crowdin.net/snort/localized.svg)](https://crowdin.com/project/snort)
|
||||
|
@ -22,7 +22,7 @@ export default function NoteFooter(props: NoteFooterProps) {
|
||||
const [showReactions, setShowReactions] = useState(false);
|
||||
|
||||
const related = useReactions("reactions", ids, undefined, false);
|
||||
const { reactions, zaps, reposts } = useEventReactions(link, related);
|
||||
const { replies, reactions, zaps, reposts } = useEventReactions(link, related);
|
||||
const { positive } = reactions;
|
||||
|
||||
const { preferences: prefs, readonly } = useLogin(s => ({
|
||||
@ -33,7 +33,7 @@ export default function NoteFooter(props: NoteFooterProps) {
|
||||
|
||||
return (
|
||||
<div className="flex flex-row gap-4 overflow-hidden max-w-full h-6 items-center">
|
||||
<ReplyButton ev={ev} replyCount={props.replyCount} readonly={readonly} />
|
||||
<ReplyButton ev={ev} replyCount={props.replyCount ?? replies.length} readonly={readonly} />
|
||||
<RepostButton ev={ev} reposts={reposts} />
|
||||
{prefs.enableReactions && <LikeButton ev={ev} positiveReactions={positive} />}
|
||||
{CONFIG.showPowIcon && <PowIcon ev={ev} />}
|
||||
|
@ -27,6 +27,7 @@
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.avatar .icons {
|
||||
|
@ -1,35 +1,62 @@
|
||||
import { NostrEvent, parseZap } from "@snort/system";
|
||||
import debug from "debug";
|
||||
|
||||
import { Relay } from "@/Cache";
|
||||
|
||||
const log = debug("getForYouFeed");
|
||||
|
||||
export async function getForYouFeed(pubkey: string): Promise<NostrEvent[]> {
|
||||
console.time("For You feed generation time");
|
||||
|
||||
console.log("pubkey", pubkey);
|
||||
log("pubkey", pubkey);
|
||||
|
||||
// Get events reacted to by me
|
||||
const myReactedEvents = await getMyReactedEventIds(pubkey);
|
||||
console.log("my reacted events", myReactedEvents);
|
||||
const myReactedEventIds = await getMyReactedEvents(pubkey);
|
||||
log("my reacted events", myReactedEventIds);
|
||||
|
||||
const myReactedAuthors = await getMyReactedAuthors(myReactedEventIds, pubkey);
|
||||
log("my reacted authors", myReactedAuthors);
|
||||
|
||||
// Get others who reacted to the same events as me
|
||||
const othersWhoReacted = await getOthersWhoReacted(myReactedEvents, pubkey);
|
||||
const othersWhoReacted = await getOthersWhoReacted(myReactedEventIds, pubkey);
|
||||
// this tends to be small when the user has just logged in, we should maybe subscribe for more from relays
|
||||
console.log("others who reacted", othersWhoReacted);
|
||||
log("others who reacted", othersWhoReacted);
|
||||
|
||||
// Get event ids reacted to by those others
|
||||
const reactedByOthers = await getEventIdsReactedByOthers(othersWhoReacted, myReactedEvents, pubkey);
|
||||
console.log("reacted by others", reactedByOthers);
|
||||
const reactedByOthers = await getEventIdsReactedByOthers(othersWhoReacted, myReactedEventIds, pubkey);
|
||||
log("reacted by others", reactedByOthers);
|
||||
|
||||
// Get full events in sorted order
|
||||
const feed = await getFeedEvents(reactedByOthers);
|
||||
console.log("feed.length", feed.length);
|
||||
const feed = await getFeedEvents(reactedByOthers, myReactedAuthors);
|
||||
log("feed.length", feed.length);
|
||||
|
||||
console.timeEnd("For You feed generation time");
|
||||
return feed;
|
||||
}
|
||||
|
||||
async function getMyReactedEventIds(pubkey: string) {
|
||||
async function getMyReactedAuthors(myReactedEventIds: Set<string>, myPubkey: string) {
|
||||
const myReactedAuthors = new Map<string, number>();
|
||||
|
||||
const myReactions = await Relay.query([
|
||||
"REQ",
|
||||
"getMyReactedAuthors",
|
||||
{
|
||||
"#e": Array.from(myReactedEventIds),
|
||||
},
|
||||
]);
|
||||
|
||||
myReactions.forEach(reaction => {
|
||||
if (reaction.pubkey !== myPubkey) {
|
||||
myReactedAuthors.set(reaction.pubkey, (myReactedAuthors.get(reaction.pubkey) || 0) + 1);
|
||||
}
|
||||
});
|
||||
|
||||
return myReactedAuthors;
|
||||
}
|
||||
|
||||
async function getMyReactedEvents(pubkey: string) {
|
||||
const myReactedEventIds = new Set<string>();
|
||||
|
||||
const myEvents = await Relay.query([
|
||||
"REQ",
|
||||
"getMyReactedEventIds",
|
||||
@ -100,17 +127,40 @@ async function getEventIdsReactedByOthers(
|
||||
return eventIdsReactedByOthers;
|
||||
}
|
||||
|
||||
async function getFeedEvents(reactedToIds: Map<string, number>) {
|
||||
async function getFeedEvents(reactedToIds: Map<string, number>, reactedToAuthors: Map<string, number>) {
|
||||
const events = await Relay.query([
|
||||
"REQ",
|
||||
"getFeedEvents",
|
||||
{
|
||||
ids: Array.from(reactedToIds.keys()),
|
||||
kinds: [1],
|
||||
// max 24h old
|
||||
since: Math.floor(Date.now() / 1000) - 60 * 60 * 24 * 7,
|
||||
},
|
||||
]);
|
||||
const seen = new Set<string>(events.map(ev => ev.id));
|
||||
|
||||
log("reactedToAuthors", reactedToAuthors);
|
||||
|
||||
const favoriteAuthors = Array.from(reactedToAuthors.keys())
|
||||
.sort((a, b) => reactedToAuthors.get(b)! - reactedToAuthors.get(a)!)
|
||||
.slice(20);
|
||||
|
||||
const eventsByFavoriteAuthors = await Relay.query([
|
||||
"REQ",
|
||||
"getFeedEvents",
|
||||
{
|
||||
authors: favoriteAuthors,
|
||||
kinds: [1],
|
||||
since: Math.floor(Date.now() / 1000) - 60 * 60 * 24,
|
||||
limit: 100,
|
||||
},
|
||||
]);
|
||||
|
||||
eventsByFavoriteAuthors.forEach(ev => {
|
||||
if (!seen.has(ev.id)) {
|
||||
events.push(ev);
|
||||
}
|
||||
});
|
||||
|
||||
// Filter out replies
|
||||
const filteredEvents = events.filter(ev => !ev.tags.some(tag => tag[0] === "e"));
|
||||
@ -137,6 +187,9 @@ async function getFeedEvents(reactedToIds: Map<string, number>) {
|
||||
|
||||
const normalize = (value: number, min: number, max: number) => (value - min) / (max - min);
|
||||
|
||||
const maxFavoriteness = Math.max(...Array.from(reactedToAuthors.values()));
|
||||
const favoritenessWeight = 0.5;
|
||||
|
||||
// Normalize and sort events by calculated score
|
||||
filteredEvents.sort((a, b) => {
|
||||
const aReactions = normalize(reactedToIds.get(a.id) || 0, minReactions, maxReactions);
|
||||
@ -145,10 +198,13 @@ async function getFeedEvents(reactedToIds: Map<string, number>) {
|
||||
const aAge = normalize(currentTime - new Date(a.created_at).getTime(), minAge, maxAge);
|
||||
const bAge = normalize(currentTime - new Date(b.created_at).getTime(), minAge, maxAge);
|
||||
|
||||
const aFavoriteness = normalize(reactedToAuthors.get(a.pubkey) || 0, 0, maxFavoriteness);
|
||||
const bFavoriteness = normalize(reactedToAuthors.get(b.pubkey) || 0, 0, maxFavoriteness);
|
||||
|
||||
// randomly big or small weight for recentness
|
||||
const recentnessWeight = Math.random() > 0.5 ? -0.1 : -10;
|
||||
const aScore = aReactions + recentnessWeight * aAge;
|
||||
const bScore = bReactions + recentnessWeight * bAge;
|
||||
const recentnessWeight = Math.random() > 0.75 ? -0.1 : -10;
|
||||
const aScore = aReactions + recentnessWeight * aAge + aFavoriteness * favoritenessWeight;
|
||||
const bScore = bReactions + recentnessWeight * bAge + bFavoriteness * favoritenessWeight;
|
||||
|
||||
// Sort by descending score
|
||||
return bScore - aScore;
|
||||
|
@ -86,7 +86,7 @@ export default function NavSidebar({ narrow = false }: { narrow: boolean }) {
|
||||
|
||||
const className = classNames(
|
||||
{ "xl:w-56 xl:gap-2 xl:items-start": !narrow },
|
||||
"overflow-y-auto hide-scrollbar sticky items-center border-r border-border-color top-0 z-20 h-screen max-h-screen hidden md:flex flex-col px-2 py-4 flex-shrink-0 gap-1",
|
||||
"select-none overflow-y-auto hide-scrollbar sticky items-center border-r border-border-color top-0 z-20 h-screen max-h-screen hidden md:flex flex-col px-2 py-4 flex-shrink-0 gap-1",
|
||||
);
|
||||
|
||||
const readOnlyIcon = readonly && (
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {EventKind, TaggedNostrEvent} from "@snort/system";
|
||||
import { EventKind, TaggedNostrEvent } from "@snort/system";
|
||||
import { memo, useEffect, useMemo, useState } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { Link } from "react-router-dom";
|
||||
@ -7,10 +7,10 @@ import { DisplayAs, DisplayAsSelector } from "@/Components/Feed/DisplayAsSelecto
|
||||
import { TimelineRenderer } from "@/Components/Feed/TimelineRenderer";
|
||||
import { TaskList } from "@/Components/Tasks/TaskList";
|
||||
import { getForYouFeed } from "@/Db/getForYouFeed";
|
||||
import useTimelineFeed, { TimelineFeedOptions, TimelineSubject } from "@/Feed/TimelineFeed";
|
||||
import useLogin from "@/Hooks/useLogin";
|
||||
import messages from "@/Pages/messages";
|
||||
import { System } from "@/system";
|
||||
import useTimelineFeed, {TimelineFeedOptions, TimelineSubject} from "@/Feed/TimelineFeed";
|
||||
|
||||
const FollowsHint = () => {
|
||||
const { publicKey: pubKey, follows } = useLogin();
|
||||
@ -60,8 +60,12 @@ export const ForYouTab = memo(function ForYouTab() {
|
||||
}) as TimelineSubject,
|
||||
[login.follows.item, login.tags.item],
|
||||
);
|
||||
// also get "follows" feed so data is loaded
|
||||
// also get "follows" feed so data is loaded from relays and there's a fallback if "for you" feed is empty
|
||||
const latestFeed = useTimelineFeed(subject, { method: "TIME_RANGE" } as TimelineFeedOptions);
|
||||
const filteredLatestFeed = useMemo(() => {
|
||||
// no replies
|
||||
return latestFeed.main?.filter((ev: TaggedNostrEvent) => !ev.tags.some((tag: string[]) => tag[0] === "e")) ?? [];
|
||||
}, [latestFeed.main]);
|
||||
|
||||
const getFeed = () => {
|
||||
if (!publicKey) {
|
||||
@ -71,11 +75,10 @@ export const ForYouTab = memo(function ForYouTab() {
|
||||
getForYouFeedPromise = getForYouFeed(publicKey);
|
||||
}
|
||||
getForYouFeedPromise!.then(notes => {
|
||||
console.log("for you feed", notes);
|
||||
getForYouFeedPromise = null;
|
||||
if (notes.length < 10) {
|
||||
setTimeout(() => {
|
||||
getForYouFeedPromise = null;
|
||||
getForYouFeed();
|
||||
getForYouFeed(publicKey);
|
||||
}, 1000);
|
||||
}
|
||||
forYouFeed = {
|
||||
@ -92,19 +95,29 @@ export const ForYouTab = memo(function ForYouTab() {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (forYouFeed.events.length < 10 || Date.now() - forYouFeed.created_at > 1000 * 60 * 1) {
|
||||
if (forYouFeed.events.length < 10 || Date.now() - forYouFeed.created_at > 1000 * 60 * 2) {
|
||||
getFeed();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const combinedFeed = useMemo(() => {
|
||||
// combine feeds: intermittently pick from both feeds
|
||||
const seen = new Set<string>();
|
||||
const combined = [];
|
||||
let i = 0;
|
||||
let j = 0;
|
||||
while (i < notes.length || j < latestFeed.main?.length) {
|
||||
if (i < notes.length) {
|
||||
let i = 0; // Index for `notes`
|
||||
let j = 0; // Index for `latestFeed.main`
|
||||
let count = 0; // Combined feed count to decide when to insert from `latestFeed`
|
||||
|
||||
while (i < notes.length || j < (filteredLatestFeed.length ?? 0)) {
|
||||
// Insert approximately 1 event from `latestFeed` for every 4 events from `notes`
|
||||
if (count % 5 === 0 && j < (filteredLatestFeed.length ?? 0)) {
|
||||
const ev = filteredLatestFeed[j];
|
||||
if (!seen.has(ev.id) && !ev.tags.some((a: string[]) => a[0] === "e")) {
|
||||
seen.add(ev.id);
|
||||
combined.push(ev);
|
||||
}
|
||||
j++;
|
||||
} else if (i < notes.length) {
|
||||
// Add from `notes` otherwise
|
||||
const ev = notes[i];
|
||||
if (!seen.has(ev.id)) {
|
||||
seen.add(ev.id);
|
||||
@ -112,17 +125,10 @@ export const ForYouTab = memo(function ForYouTab() {
|
||||
}
|
||||
i++;
|
||||
}
|
||||
if (j < latestFeed.main?.length) {
|
||||
const ev = latestFeed.main[j];
|
||||
if (!seen.has(ev.id) && !ev.tags?.some((tag: string[]) => tag[0] === "e")) {
|
||||
seen.add(ev.id);
|
||||
combined.push(ev);
|
||||
}
|
||||
j++;
|
||||
}
|
||||
count++;
|
||||
}
|
||||
return combined;
|
||||
}, [notes, latestFeed.main]);
|
||||
}, [notes, filteredLatestFeed]);
|
||||
|
||||
const frags = useMemo(() => {
|
||||
return [
|
||||
@ -138,7 +144,7 @@ export const ForYouTab = memo(function ForYouTab() {
|
||||
<DisplayAsSelector activeSelection={displayAs} onSelect={a => setDisplayAs(a)} />
|
||||
<FollowsHint />
|
||||
<TaskList />
|
||||
<TimelineRenderer frags={frags} latest={[]} displayAs={displayAs} />
|
||||
<TimelineRenderer frags={frags} latest={[]} displayAs={displayAs} loadMore={() => latestFeed.loadMore()} />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
@ -563,7 +563,7 @@
|
||||
"wtLjP6": "ID kopieren",
|
||||
"x/Fx2P": "Unterstütze die von dir genutzten Dienste, indem du einen Teil deiner Zaps in einen Spendenpool einzahlst!",
|
||||
"x82IOl": "Stummschalten",
|
||||
"xEjBS7": "For you",
|
||||
"xEjBS7": "Für dich",
|
||||
"xIcAOU": "Stimmen nach {type}",
|
||||
"xIoGG9": "Gehe zu",
|
||||
"xQtL3v": "Entsperren",
|
||||
|
@ -563,7 +563,7 @@
|
||||
"wtLjP6": "Egyedi azonosító",
|
||||
"x/Fx2P": "Finanszírozza az Ön által használt szolgáltatásokat úgy, hogy az összes zaps-ek egy részét egy támogatói gyűjtőbe osztja fel!",
|
||||
"x82IOl": "Némítás",
|
||||
"xEjBS7": "For you",
|
||||
"xEjBS7": "Számodra",
|
||||
"xIcAOU": "Szavazatok {type} által",
|
||||
"xIoGG9": "Menj ide",
|
||||
"xQtL3v": "Feloldás",
|
||||
|
@ -47,6 +47,7 @@ export function useEventReactions(link: NostrLink, related: ReadonlyArray<Tagged
|
||||
positive: groupReactions[Reaction.Positive] ?? [],
|
||||
negative: groupReactions[Reaction.Negative] ?? [],
|
||||
},
|
||||
replies: reactionKinds[String(EventKind.TextNote)] ?? [],
|
||||
reposts,
|
||||
zaps,
|
||||
others: Object.fromEntries(
|
||||
|
@ -23,7 +23,9 @@ export function useReactions(
|
||||
);
|
||||
|
||||
for (const [, v] of Object.entries(grouped)) {
|
||||
rb.withFilter().kinds([EventKind.Reaction, EventKind.Repost, EventKind.ZapReceipt]).replyToLink(v);
|
||||
rb.withFilter()
|
||||
.kinds([EventKind.TextNote, EventKind.Reaction, EventKind.Repost, EventKind.ZapReceipt])
|
||||
.replyToLink(v);
|
||||
}
|
||||
}
|
||||
others?.(rb);
|
||||
|
@ -3,7 +3,7 @@
|
||||
"build": {
|
||||
"beforeBuildCommand": "yarn build",
|
||||
"beforeDevCommand": "yarn start",
|
||||
"devPath": "http://localhost:3000",
|
||||
"devPath": "http://localhost:5173",
|
||||
"distDir": "../packages/app/build"
|
||||
},
|
||||
"package": {
|
||||
|
Loading…
Reference in New Issue
Block a user