diff --git a/src/app/commands.ts b/src/app/commands.ts index 86c06f25..9d5d702b 100644 --- a/src/app/commands.ts +++ b/src/app/commands.ts @@ -8,6 +8,7 @@ import { makeThunk, publishThunk, loadProfile, + loadInboxRelaySelections, profilesByPubkey, relaySelectionsByPubkey, getWriteRelayUrls, @@ -59,6 +60,7 @@ export const loadUserData = ( request: Partial = {}, ) => { const promise = Promise.all([ + loadInboxRelaySelections(pubkey, request), loadProfile(pubkey, request), loadFollows(pubkey, request), loadMutes(pubkey, request), diff --git a/src/app/components/PrimaryNav.svelte b/src/app/components/PrimaryNav.svelte index c17b8bb1..767b5339 100644 --- a/src/app/components/PrimaryNav.svelte +++ b/src/app/components/PrimaryNav.svelte @@ -49,8 +49,7 @@ + class="!h-10 !w-10 border border-solid border-base-300" /> {#each $userMembership?.roomsByUrl.keys() || [] as url (url)} @@ -58,21 +57,15 @@ {/each} -
- -
+
-
- -
+
-
- -
+
diff --git a/src/app/components/ProfileCircle.svelte b/src/app/components/ProfileCircle.svelte new file mode 100644 index 00000000..d1ab10d0 --- /dev/null +++ b/src/app/components/ProfileCircle.svelte @@ -0,0 +1,10 @@ + + + diff --git a/src/app/components/ProfileCircles.svelte b/src/app/components/ProfileCircles.svelte new file mode 100644 index 00000000..4ddca5a5 --- /dev/null +++ b/src/app/components/ProfileCircles.svelte @@ -0,0 +1,14 @@ + + +
+ {#each pubkeys.slice(0, 15) as pubkey (pubkey)} +
+ +
+ {/each} +
diff --git a/src/app/components/SpaceAvatar.svelte b/src/app/components/SpaceAvatar.svelte index ea7222ee..04b8a79c 100644 --- a/src/app/components/SpaceAvatar.svelte +++ b/src/app/components/SpaceAvatar.svelte @@ -9,8 +9,7 @@ + src={$relay?.profile?.icon} /> diff --git a/src/app/state.ts b/src/app/state.ts index b0322f5e..8c9c244f 100644 --- a/src/app/state.ts +++ b/src/app/state.ts @@ -1,7 +1,7 @@ import {nip19} from "nostr-tools" import {get, derived} from "svelte/store" import type {Maybe} from "@welshman/lib" -import {setContext, sort, uniq, partition, nth, max, pushToMapKey, nthEq} from "@welshman/lib" +import {setContext, remove, assoc, sortBy, sort, uniq, partition, nth, max, pushToMapKey, nthEq} from "@welshman/lib" import { getIdFilters, NOTE, @@ -17,8 +17,11 @@ import { getAncestorTags, getAncestorTagValues, getPubkeyTagValues, + isHashedEvent, + displayProfile, } from "@welshman/util" -import type {TrustedEvent} from "@welshman/util" +import type {TrustedEvent, SignedEvent} from "@welshman/util" +import {Nip59} from "@welshman/signer" import { pubkey, repository, @@ -32,7 +35,15 @@ import { getDefaultNetContext, makeRouter, trackerStore, + tracker, + relay, + getSession, + getSigner, + hasNegentropy, + pull, + createSearch, } from "@welshman/app" +import type {AppSyncOpts} from "@welshman/app" import type {SubscribeRequestWithHandlers} from "@welshman/net" import {deriveEvents, deriveEventsMapped, withGetter} from "@welshman/store" @@ -78,6 +89,60 @@ export const entityLink = (entity: string) => `https://coracle.social/${entity}` export const tagRoom = (room: string, url: string) => [ROOM, room, url] +export const ensureUnwrapped = async (event: TrustedEvent) => { + if (event.kind !== WRAP) { + return event + } + + let rumor = repository.eventsByWrap.get(event.id) + + if (rumor) { + return rumor + } + + for (const recipient of getPubkeyTagValues(event.tags)) { + const session = getSession(recipient) + const signer = getSigner(session) + + if (signer) { + try { + rumor = await Nip59.fromSigner(signer).unwrap(event as SignedEvent) + break + } catch (e) { + // pass + } + } + } + + if (rumor && isHashedEvent(rumor)) { + tracker.copy(event.id, rumor.id) + relay.send("EVENT", rumor) + } + + return rumor +} + +export const pullConservatively = ({relays, filters}: AppSyncOpts) => { + const [smart, dumb] = partition(hasNegentropy, relays) + const promises = [pull({relays: smart, filters})] + + // Since pulling from relays without negentropy is expensive, only do it 30% of the time, + // unless we have very few matching events. If that's the case, either we haven't synced + // this filter yet, or there are few enough events that we don't really need to worry about + // downloading duplicates. Otherwise, add a reasonable since value to make sure we at + // least fetch recent events. + if (Math.random() > 0.7 || repository.query(filters).length < 100) { + promises.push(pull({relays: dumb, filters})) + } else { + const events = sortBy(e => -e.created_at, repository.query(filters)) + const since = events[50]!.created_at + + promises.push(pull({relays: dumb, filters: filters.map(assoc("since", since))})) + } + + return Promise.all(promises) +} + setContext({ net: getDefaultNetContext(), app: getDefaultAppContext({ @@ -88,6 +153,12 @@ setContext({ }), }) +repository.on('update', ({added}) => { + for (const event of added) { + ensureUnwrapped(event) + } +}) + export const deriveEvent = (idOrAddress: string, hints: string[] = []) => { let attempted = false @@ -231,13 +302,15 @@ export type Chat = { id: string pubkeys: string[] messages: TrustedEvent[] + last_activity: number + search_text: string } export const makeChatId = (pubkeys: string[]) => sort(uniq(pubkeys)).join(",") export const splitChatId = (id: string) => id.split(",") -export const chats = derived(chatMessages, $messages => { +export const chats = derived([pubkey, chatMessages, profilesByPubkey], ([$pubkey, $messages, $profilesByPubkey]) => { const messagesByChatId = new Map() for (const message of $messages) { @@ -246,11 +319,23 @@ export const chats = derived(chatMessages, $messages => { pushToMapKey(messagesByChatId, chatId, message) } - return Array.from(messagesByChatId.entries()).map(([id, messages]): Chat => { - const pubkeys = splitChatId(id) + return sortBy( + c => -c.last_activity, + Array.from(messagesByChatId.entries()).map(([id, events]): Chat => { + const pubkeys = splitChatId(id) + const messages = sortBy(e => -e.created_at, events) + const last_activity = messages[0].created_at + const search_text = remove($pubkey as string, pubkeys) + .map(pubkey => { + const profile = $profilesByPubkey.get(pubkey) - return {id, pubkeys, messages} - }) + return profile ? displayProfile(profile) : "" + }) + .join(' ') + + return {id, pubkeys, messages, last_activity, search_text} + }) + ) }) export const { @@ -274,6 +359,13 @@ export const { }, }) +export const chatSearch = derived(chats, $chats => + createSearch($chats, { + getValue: (chat: Chat) => chat.id, + fuseOptions: {keys: ["search_text"]}, + }), +) + // Calendar events export const events = deriveEvents(repository, {filters: [{kinds: [EVENT_DATE, EVENT_TIME]}]}) diff --git a/src/lib/components/Avatar.svelte b/src/lib/components/Avatar.svelte index 1989b1fa..d91ae9bc 100644 --- a/src/lib/components/Avatar.svelte +++ b/src/lib/components/Avatar.svelte @@ -2,7 +2,7 @@ import cx from "classnames" import Icon from "@lib/components/Icon.svelte" - export let src + export let src = "" export let size = 7 export let icon = "user-rounded" @@ -15,6 +15,6 @@
- +
{/if} diff --git a/src/lib/components/SecondaryNav.svelte b/src/lib/components/SecondaryNav.svelte index e05ccb41..e74d3ada 100644 --- a/src/lib/components/SecondaryNav.svelte +++ b/src/lib/components/SecondaryNav.svelte @@ -1,3 +1,3 @@ -
+
diff --git a/src/routes/home/+layout.svelte b/src/routes/home/+layout.svelte index 91ae78e5..e627bde2 100644 --- a/src/routes/home/+layout.svelte +++ b/src/routes/home/+layout.svelte @@ -1,17 +1,41 @@ @@ -39,14 +63,36 @@
- {#each $chats as {id, pubkeys}, i (id)} -
- - {id} - + + +
+ {#each chats as {id, pubkeys, messages}, i (id)} + {@const message = messages[0]} + {@const others = remove($pubkey, pubkeys)} +
+ +
+ {#if others.length === 1} + + + {:else} + +

+ + and {others.length - 1} {others.length > 2 ? 'others' : 'other'} +

+ {/if} +
+

+ {message.content} +

+
{/each} - +