Remember which rooms the user has joined

This commit is contained in:
Jonathan Staab 2023-04-20 17:21:58 -05:00
parent 7916ed501c
commit 612a0d18df
8 changed files with 86 additions and 29 deletions

View File

@ -15,6 +15,7 @@ export const loki = new Loki("agent.db", {
autoload: true, autoload: true,
autosave: true, autosave: true,
autosaveInterval: 4000, autosaveInterval: 4000,
throttledSaves: true,
adapter: new Adapter(), adapter: new Adapter(),
autoloadCallback: () => { autoloadCallback: () => {
for (const table of Object.values(registry)) { for (const table of Object.values(registry)) {

View File

@ -1,4 +1,4 @@
import {uniq, prop, reject, nth, uniqBy, objOf, pick, identity} from "ramda" import {is, uniq, prop, reject, nth, uniqBy, objOf, pick, identity} from "ramda"
import {nip05} from "nostr-tools" import {nip05} from "nostr-tools"
import {noop, ensurePlural, chunk} from "hurdak/lib/hurdak" import {noop, ensurePlural, chunk} from "hurdak/lib/hurdak"
import { import {
@ -242,16 +242,30 @@ addHandler(
30078, 30078,
profileHandler("settings", async (e, p) => { profileHandler("settings", async (e, p) => {
if (Tags.from(e).getMeta("d") === "coracle/settings/v1") { if (Tags.from(e).getMeta("d") === "coracle/settings/v1") {
return keys.decryptJson(e.content) return {...p.settings, ...(await keys.decryptJson(e.content))}
} }
}) })
) )
addHandler( addHandler(
30078, 30078,
profileHandler("lastChecked", async (e, p) => { profileHandler("last_checked", async (e, p) => {
if (Tags.from(e).getMeta("d") === "coracle/last_checked/v1") { if (Tags.from(e).getMeta("d") === "coracle/last_checked/v1") {
return {...p.lastChecked, ...(await keys.decryptJson(e.content))} return {...p.last_checked, ...(await keys.decryptJson(e.content))}
}
})
)
addHandler(
30078,
profileHandler("rooms_joined", async (e, p) => {
if (Tags.from(e).getMeta("d") === "coracle/rooms_joined/v1") {
const roomsJoined = await keys.decryptJson(e.content)
// Just a bug from when I was building the feature, remove someday
if (is(Array, roomsJoined)) {
return roomsJoined
}
} }
}) })
) )

View File

@ -3,6 +3,7 @@ import type {Readable} from "svelte/store"
import { import {
slice, slice,
uniqBy, uniqBy,
without,
reject, reject,
prop, prop,
find, find,
@ -34,7 +35,8 @@ const profile = synced("agent/user/profile", {
dufflepudUrl: import.meta.env.VITE_DUFFLEPUD_URL, dufflepudUrl: import.meta.env.VITE_DUFFLEPUD_URL,
multiplextrUrl: import.meta.env.VITE_MULTIPLEXTR_URL, multiplextrUrl: import.meta.env.VITE_MULTIPLEXTR_URL,
}, },
lastChecked: {}, rooms_joined: [],
last_checked: {},
petnames: [], petnames: [],
relays: [], relays: [],
mutes: [], mutes: [],
@ -42,7 +44,8 @@ const profile = synced("agent/user/profile", {
}) })
const settings = derived(profile, prop("settings")) const settings = derived(profile, prop("settings"))
const lastChecked = derived(profile, prop("lastChecked")) as Readable<Record<string, number>> const roomsJoined = derived(profile, prop("rooms_joined")) as Readable<string>
const lastChecked = derived(profile, prop("last_checked")) as Readable<Record<string, number>>
const petnames = derived(profile, prop("petnames")) as Readable<Array<Array<string>>> const petnames = derived(profile, prop("petnames")) as Readable<Array<Array<string>>>
const relays = derived(profile, prop("relays")) as Readable<Array<Relay>> const relays = derived(profile, prop("relays")) as Readable<Array<Relay>>
const mutes = derived(profile, prop("mutes")) as Readable<Array<[string, string]>> const mutes = derived(profile, prop("mutes")) as Readable<Array<[string, string]>>
@ -93,6 +96,7 @@ export default {
// App data // App data
lastChecked, lastChecked,
roomsJoined,
async setAppData(key, content) { async setAppData(key, content) {
if (keys.canSign()) { if (keys.canSign()) {
const d = `coracle/${key}` const d = `coracle/${key}`
@ -103,11 +107,29 @@ export default {
}, },
setLastChecked(k, v) { setLastChecked(k, v) {
profile.update($profile => { profile.update($profile => {
const lastChecked = {...$profile.lastChecked, [k]: v} const lastChecked = {...$profile.last_checked, [k]: v}
this.setAppData("last_checked/v1", lastChecked) this.setAppData("last_checked/v1", lastChecked)
return {...$profile, lastChecked} return {...$profile, last_checked: lastChecked}
})
},
joinRoom(id) {
profile.update($profile => {
const roomsJoined = $profile.rooms_joined.concat(id)
this.setAppData("rooms_joined/v1", roomsJoined)
return {...$profile, rooms_joined: roomsJoined}
})
},
leaveRoom(id) {
profile.update($profile => {
const roomsJoined = without([id], $profile.rooms_joined)
this.setAppData("rooms_joined/v1", roomsJoined)
return {...$profile, rooms_joined: roomsJoined}
}) })
}, },

View File

@ -102,7 +102,7 @@ export const newNotifications = derived(
) )
export const hasNewMessages = ({lastReceived, lastSent}, lastChecked) => export const hasNewMessages = ({lastReceived, lastSent}, lastChecked) =>
lastReceived > Math.max(lastSent, lastChecked || 0) lastReceived > Math.max(lastSent || lastReceived, lastChecked || 0)
export const newDirectMessages = derived( export const newDirectMessages = derived(
[watch("contacts", t => t.all()), user.lastChecked], [watch("contacts", t => t.all()), user.lastChecked],
@ -111,9 +111,14 @@ export const newDirectMessages = derived(
) )
export const newChatMessages = derived( export const newChatMessages = derived(
[watch("rooms", t => t.all()), user.lastChecked], [watch("rooms", t => t.all()), user.lastChecked, user.roomsJoined],
([rooms, $lastChecked]) => ([rooms, $lastChecked, $roomsJoined]) =>
Boolean(find(r => hasNewMessages(r, $lastChecked[`chat/${r.id}`]), rooms)) Boolean(
find(
r => $roomsJoined.includes(r.id) && hasNewMessages(r, $lastChecked[`chat/${r.id}`]),
rooms
)
)
) )
// Synchronization from events to state // Synchronization from events to state
@ -159,10 +164,10 @@ const processChats = async (pubkey, events) => {
} }
} }
export const listen = async pubkey => { export const listen = async () => {
// Include an offset so we don't miss notifications on one relay but not another const pubkey = user.getPubkey()
const {roomsJoined} = user.getProfile()
const since = now() - timedelta(30, "days") const since = now() - timedelta(30, "days")
const roomIds = pluck("id", rooms.all({joined: true}))
const eventIds = doPipe(userEvents.all({kind: 1, created_at: {$gt: since}}), [ const eventIds = doPipe(userEvents.all({kind: 1, created_at: {$gt: since}}), [
sortBy(e => -e.created_at), sortBy(e => -e.created_at),
slice(0, 256), slice(0, 256),
@ -177,7 +182,7 @@ export const listen = async pubkey => {
{kinds: [1, 4], authors: [pubkey], since}, {kinds: [1, 4], authors: [pubkey], since},
{kinds: [1, 7, 4, 9735], "#p": [pubkey], since}, {kinds: [1, 7, 4, 9735], "#p": [pubkey], since},
{kinds: [1, 7, 4, 9735], "#e": eventIds, since}, {kinds: [1, 7, 4, 9735], "#e": eventIds, since},
{kinds: [42], "#e": roomIds, since}, {kinds: [42], "#e": roomsJoined, since},
], ],
onChunk: async events => { onChunk: async events => {
events = user.applyMutes(events) events = user.applyMutes(events)
@ -212,7 +217,7 @@ setInterval(() => {
export const loadAppData = async pubkey => { export const loadAppData = async pubkey => {
if (getUserReadRelays().length > 0) { if (getUserReadRelays().length > 0) {
// Start our listener, but don't wait for it // Start our listener, but don't wait for it
listen(pubkey) listen()
// Make sure the user and their network is loaded // Make sure the user and their network is loaded
await network.loadPeople([pubkey], {force: true, kinds: userKinds}) await network.loadPeople([pubkey], {force: true, kinds: userKinds})

View File

@ -9,7 +9,7 @@
import Button from "src/partials/Button.svelte" import Button from "src/partials/Button.svelte"
import {toast, modal} from "src/partials/state" import {toast, modal} from "src/partials/state"
import {getUserWriteRelays} from "src/agent/relays" import {getUserWriteRelays} from "src/agent/relays"
import {rooms} from "src/agent/db" import user from "src/agent/user"
import cmd from "src/agent/cmd" import cmd from "src/agent/cmd"
import {publishWithToast} from "src/app/state" import {publishWithToast} from "src/app/state"
@ -45,7 +45,7 @@
const [event] = await publishWithToast(relays, cmd.createRoom(room)) const [event] = await publishWithToast(relays, cmd.createRoom(room))
// Auto join the room the user just created // Auto join the room the user just created
rooms.patch({id: event.id, joined: true}) user.joinRoom(event.id)
} }
modal.pop() modal.pop()

View File

@ -1,11 +1,14 @@
<script> <script>
import {onMount} from "svelte" import {onMount} from "svelte"
import {derived} from "svelte/store"
import {partition} from "ramda"
import {fuzzy} from "src/util/misc" import {fuzzy} from "src/util/misc"
import Input from "src/partials/Input.svelte" import Input from "src/partials/Input.svelte"
import Content from "src/partials/Content.svelte" import Content from "src/partials/Content.svelte"
import Anchor from "src/partials/Anchor.svelte" import Anchor from "src/partials/Anchor.svelte"
import ChatListItem from "src/app/views/ChatListItem.svelte" import ChatListItem from "src/app/views/ChatListItem.svelte"
import {watch} from "src/agent/db" import {watch} from "src/agent/db"
import user from "src/agent/user"
import network from "src/agent/network" import network from "src/agent/network"
import {getUserReadRelays} from "src/agent/relays" import {getUserReadRelays} from "src/agent/relays"
import {modal} from "src/partials/state" import {modal} from "src/partials/state"
@ -14,10 +17,15 @@
let search let search
let results = [] let results = []
const userRooms = watch("rooms", t => t.all({joined: true})) const {roomsJoined} = user
const otherRooms = watch("rooms", t => t.all({joined: {$ne: true}})) const rooms = derived([watch("rooms", t => t.all()), roomsJoined], ([_rooms, _joined]) => {
const ids = new Set(_joined)
const [joined, other] = partition(r => ids.has(r.id), _rooms)
$: search = fuzzy($otherRooms, {keys: ["name", "about"]}) return {joined, other}
})
$: search = fuzzy($rooms.other, {keys: ["name", "about"]})
$: results = search(q).slice(0, 50) $: results = search(q).slice(0, 50)
document.title = "Chat" document.title = "Chat"
@ -44,7 +52,7 @@
<i class="fa-solid fa-plus" /> Create Room <i class="fa-solid fa-plus" /> Create Room
</Anchor> </Anchor>
</div> </div>
{#each $userRooms as room (room.id)} {#each $rooms.joined as room (room.id)}
<ChatListItem {room} /> <ChatListItem {room} />
{:else} {:else}
<p class="text-center py-8">You haven't yet joined any rooms.</p> <p class="text-center py-8">You haven't yet joined any rooms.</p>
@ -64,7 +72,7 @@
<ChatListItem {room} /> <ChatListItem {room} />
{/each} {/each}
<small class="text-center"> <small class="text-center">
Showing {Math.min(50, $otherRooms.length)} of {$otherRooms.length} known rooms Showing {Math.min(50, $rooms.other.length)} of {$rooms.other.length} known rooms
</small> </small>
{:else} {:else}
<small class="text-center"> No matching rooms found </small> <small class="text-center"> No matching rooms found </small>

View File

@ -4,13 +4,16 @@
import {fly} from "svelte/transition" import {fly} from "svelte/transition"
import {ellipsize} from "hurdak/lib/hurdak" import {ellipsize} from "hurdak/lib/hurdak"
import Anchor from "src/partials/Anchor.svelte" import Anchor from "src/partials/Anchor.svelte"
import {rooms} from "src/agent/db" import user from "src/agent/user"
export let room export let room
const {roomsJoined} = user
const enter = () => navigate(`/chat/${nip19.noteEncode(room.id)}`) const enter = () => navigate(`/chat/${nip19.noteEncode(room.id)}`)
const join = () => rooms.patch({id: room.id, joined: true}) const join = () => user.joinRoom(room.id)
const leave = () => rooms.patch({id: room.id, joined: false}) const leave = () => user.leaveRoom(room.id)
$: joined = $roomsJoined.includes(room.id)
</script> </script>
<button <button
@ -26,7 +29,7 @@
<i class="fa fa-lock-open text-gray-1" /> <i class="fa fa-lock-open text-gray-1" />
<h2 class="text-lg">{room.name || ""}</h2> <h2 class="text-lg">{room.name || ""}</h2>
</div> </div>
{#if room.joined} {#if joined}
<Anchor type="button" preventDefault class="flex items-center gap-2" on:click={leave}> <Anchor type="button" preventDefault class="flex items-center gap-2" on:click={leave}>
<i class="fa fa-right-from-bracket" /> <i class="fa fa-right-from-bracket" />
<span>Leave</span> <span>Leave</span>

View File

@ -7,7 +7,11 @@ import {invoiceAmount} from "src/util/lightning"
export const personKinds = [0, 2, 3, 10001, 10002] export const personKinds = [0, 2, 3, 10001, 10002]
export const userKinds = personKinds.concat([10000, 30001, 30078]) export const userKinds = personKinds.concat([10000, 30001, 30078])
export const appDataKeys = ["coracle/settings/v1", "coracle/last_checked/v1"] export const appDataKeys = [
"coracle/settings/v1",
"coracle/last_checked/v1",
"coracle/rooms_joined/v1",
]
export class Tags { export class Tags {
tags: Array<any> tags: Array<any>