mirror of
https://github.com/coracle-social/coracle.git
synced 2024-09-30 00:41:12 +00:00
Improve chat
This commit is contained in:
parent
995904a2b2
commit
e5a4f86474
@ -1,6 +1,6 @@
|
||||
Bugs
|
||||
|
||||
- [ ] Use cursor for chat rooms
|
||||
- [ ] Pin joined relays at the top
|
||||
- [ ] Load/publish user preferred relays
|
||||
- [ ] Optimistically load events the user publishes (e.g. to reduce reflow for reactions/replies). Essentially, we can pretend to be our own in-memory relay.
|
||||
|
||||
|
@ -15,19 +15,15 @@
|
||||
|
||||
onMount(() => {
|
||||
cursor = new Cursor({ids: [note.id]}, note.created_at)
|
||||
|
||||
// Can't use async/await since we need to return unsubscribe functions
|
||||
notesListener(notes, [
|
||||
listener = notesListener(notes, [
|
||||
{kinds: [1, 5, 7], '#e': [note.id]},
|
||||
// We can't target reaction deletes by e tag, so get them
|
||||
// all so we can support toggling like/flags for our user
|
||||
{kinds: [5], authors: $user ? [$user.pubkey] : []}
|
||||
]).then(_listener => {
|
||||
listener = _listener
|
||||
])
|
||||
|
||||
// Populate our initial empty space
|
||||
listener.start()
|
||||
})
|
||||
|
||||
const unsubNotes = notes.subscribe($notes => {
|
||||
note = find(propEq('id', note.id), $notes)
|
||||
|
@ -6,7 +6,6 @@
|
||||
import Input from "src/partials/Input.svelte"
|
||||
import Textarea from "src/partials/Textarea.svelte"
|
||||
import Button from "src/partials/Button.svelte"
|
||||
import RoomList from "src/partials/chat/RoomList.svelte"
|
||||
import {dispatch} from "src/state/dispatch"
|
||||
import {channels} from "src/state/nostr"
|
||||
import toast from "src/state/toast"
|
||||
@ -51,9 +50,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex gap-4 h-full">
|
||||
<div class="sm:ml-56 w-full">
|
||||
<form on:submit={submit} class="flex justify-center py-8 px-4" in:fly={{y: 20}}>
|
||||
<form on:submit={submit} class="flex justify-center py-12" in:fly={{y: 20}}>
|
||||
<div class="flex flex-col gap-4 max-w-2xl">
|
||||
<div class="flex justify-center items-center flex-col mb-4">
|
||||
<h1 class="staatliches text-6xl">Name your room</h1>
|
||||
@ -86,9 +83,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="hidden sm:block">
|
||||
<RoomList />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -2,20 +2,21 @@
|
||||
import {onMount, onDestroy} from 'svelte'
|
||||
import {fly} from 'svelte/transition'
|
||||
import {navigate} from 'svelte-routing'
|
||||
import {prop, uniqBy, sortBy, last} from 'ramda'
|
||||
import {switcherFn} from 'hurdak/src/core'
|
||||
import {prop, uniq, pluck, reverse, uniqBy, sortBy, last} from 'ramda'
|
||||
import {formatTimestamp} from 'src/util/misc'
|
||||
import {toHtml} from 'src/util/html'
|
||||
import UserBadge from 'src/partials/UserBadge.svelte'
|
||||
import {Listener, epoch} from 'src/state/nostr'
|
||||
import {accounts, ensureAccounts} from 'src/state/app'
|
||||
import {Listener, Cursor, epoch} from 'src/state/nostr'
|
||||
import {accounts, scroller, ensureAccounts} from 'src/state/app'
|
||||
import {dispatch} from 'src/state/dispatch'
|
||||
import {user} from 'src/state/user'
|
||||
import RoomList from "src/partials/chat/RoomList.svelte"
|
||||
|
||||
export let room
|
||||
|
||||
let cursor
|
||||
let listener
|
||||
let scroll
|
||||
let textarea
|
||||
let messages = []
|
||||
let annotatedMessages = []
|
||||
@ -23,7 +24,7 @@
|
||||
|
||||
$: {
|
||||
// Group messages so we're only showing the account once per chunk
|
||||
annotatedMessages = sortBy(prop('created_at'), uniqBy(prop('id'), messages)).reduce(
|
||||
annotatedMessages = reverse(sortBy(prop('created_at'), uniqBy(prop('id'), messages)).reduce(
|
||||
(mx, m) => {
|
||||
const account = $accounts[m.pubkey]
|
||||
|
||||
@ -39,7 +40,7 @@
|
||||
})
|
||||
},
|
||||
[]
|
||||
)
|
||||
))
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
@ -47,52 +48,65 @@
|
||||
return navigate('/login')
|
||||
}
|
||||
|
||||
// flex-col means the first is the last
|
||||
const getLastListItem = () => document.querySelector('ul[name=messages] li')
|
||||
|
||||
const stickToBottom = async (behavior, cb) => {
|
||||
const shouldStick = window.scrollY + window.innerHeight > document.body.scrollHeight - 200
|
||||
const $li = getLastListItem()
|
||||
|
||||
await cb()
|
||||
|
||||
if ($li && shouldStick) {
|
||||
$li.scrollIntoView({behavior})
|
||||
}
|
||||
}
|
||||
|
||||
cursor = new Cursor({kinds: [42], '#e': [room]})
|
||||
scroll = scroller(
|
||||
cursor,
|
||||
chunk => {
|
||||
stickToBottom('auto', async () => {
|
||||
for (const e of chunk) {
|
||||
messages = messages.concat(e)
|
||||
}
|
||||
|
||||
if (chunk.length > 0) {
|
||||
await ensureAccounts(uniq(pluck('pubkey', chunk)))
|
||||
}
|
||||
})
|
||||
},
|
||||
{reverse: true}
|
||||
)
|
||||
|
||||
listener = new Listener(
|
||||
[{kinds: [40, 41], ids: [room], since: epoch},
|
||||
{kinds: [42, 43, 44], '#e': [room], since: epoch}],
|
||||
{kinds: [42], '#e': [room]}],
|
||||
e => {
|
||||
const {pubkey, kind, content} = e
|
||||
|
||||
if ([40, 41].includes(kind)) {
|
||||
roomData = {pubkey, ...roomData, ...JSON.parse(content)}
|
||||
} else {
|
||||
switcherFn(kind, {
|
||||
42: () => {
|
||||
stickToBottom('smooth', async () => {
|
||||
messages = messages.concat(e)
|
||||
|
||||
ensureAccounts([pubkey])
|
||||
|
||||
const $prevListItem = last(document.querySelectorAll('.chat-message'))
|
||||
|
||||
if ($prevListItem && isVisible($prevListItem)) {
|
||||
setTimeout(() => {
|
||||
const $li = last(document.querySelectorAll('.chat-message'))
|
||||
|
||||
$li.scrollIntoView({behavior: "smooth"})
|
||||
}, 100)
|
||||
}
|
||||
},
|
||||
43: () => null,
|
||||
44: () => null,
|
||||
await ensureAccounts([e.pubkey])
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
scroll()
|
||||
|
||||
listener.start()
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
cursor?.stop()
|
||||
listener?.stop()
|
||||
})
|
||||
|
||||
const isVisible = $el => {
|
||||
const bodyRect = document.body.getBoundingClientRect()
|
||||
const {top, height} = $el.getBoundingClientRect()
|
||||
|
||||
return top + height < bodyRect.height
|
||||
}
|
||||
|
||||
const edit = () => {
|
||||
navigate(`/chat/${room}/edit`)
|
||||
}
|
||||
@ -115,14 +129,15 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:scroll={scroll} />
|
||||
|
||||
<div class="flex gap-4 h-full">
|
||||
<div class="sm:ml-56 w-full">
|
||||
<div class="relative">
|
||||
<div class="flex flex-col pt-20 pb-32">
|
||||
<ul class="p-4 max-h-full flex-grow">
|
||||
{#each annotatedMessages as m}
|
||||
<li in:fly={{y: 20}} class="py-1 chat-message">
|
||||
<ul class="p-4 max-h-full flex-grow flex flex-col-reverse" name="messages">
|
||||
{#each annotatedMessages as m (m.id)}
|
||||
<li in:fly={{y: 20}} class="py-1">
|
||||
{#if m.showAccount}
|
||||
<div class="flex gap-4 items-center justify-between">
|
||||
<UserBadge user={m.account} />
|
||||
|
@ -1,6 +1,7 @@
|
||||
<script>
|
||||
import {onMount, onDestroy} from 'svelte'
|
||||
import {writable} from 'svelte/store'
|
||||
import {fly} from 'svelte/transition'
|
||||
import {navigate} from "svelte-routing"
|
||||
import {uniqBy, prop} from 'ramda'
|
||||
import Anchor from "src/partials/Anchor.svelte"
|
||||
@ -60,8 +61,12 @@
|
||||
{/each}
|
||||
</li>
|
||||
{/each}
|
||||
<li class="flex justify-center py-12" in:fly={{y: 20}}>
|
||||
<p>Loading notes...</p>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
|
||||
{#if $relays.length > 0}
|
||||
<div class="fixed bottom-0 right-0 p-8">
|
||||
<div
|
||||
|
@ -116,7 +116,7 @@ export const annotateNotes = async (chunk, {showParents = false} = {}) => {
|
||||
return reverse(sortBy(prop('created'), chunk.map(annotate)))
|
||||
}
|
||||
|
||||
export const notesListener = async (notes, filter) => {
|
||||
export const notesListener = (notes, filter) => {
|
||||
const updateNote = (id, f) =>
|
||||
notes.update($notes =>
|
||||
$notes
|
||||
@ -170,21 +170,29 @@ export const notesListener = async (notes, filter) => {
|
||||
|
||||
// UI
|
||||
|
||||
export const scroller = (cursor, cb, {isInModal = false, since = epoch} = {}) => {
|
||||
export const scroller = (cursor, cb, {isInModal = false, since = epoch, reverse = false} = {}) => {
|
||||
const startingDelta = cursor.delta
|
||||
|
||||
return debounce(1000, async () => {
|
||||
/* eslint no-constant-condition: 0 */
|
||||
while (true) {
|
||||
// If a modal opened up, wait for them to close it
|
||||
// If a modal opened up, wait for them to close it. Otherwise, throttle a tad
|
||||
if (!isInModal && get(modal)) {
|
||||
await sleep(1000)
|
||||
|
||||
continue
|
||||
} else {
|
||||
await sleep(100)
|
||||
}
|
||||
|
||||
// While we have empty space, fill it
|
||||
if (window.scrollY + window.innerHeight * 3 < document.body.scrollHeight) {
|
||||
const {scrollY, innerHeight} = window
|
||||
const {scrollHeight} = document.body
|
||||
|
||||
if (
|
||||
(reverse && scrollY > innerHeight * 3)
|
||||
|| (!reverse && scrollY + innerHeight * 3 < scrollHeight)
|
||||
) {
|
||||
break
|
||||
}
|
||||
|
||||
@ -197,7 +205,9 @@ export const scroller = (cursor, cb, {isInModal = false, since = epoch} = {}) =>
|
||||
const chunk = await cursor.chunk()
|
||||
|
||||
// Notify the caller
|
||||
if (chunk.length > 0) {
|
||||
await cb(chunk)
|
||||
}
|
||||
|
||||
// If we have an empty chunk, increase our step size so we can get back to where
|
||||
// we might have old events. Once we get a chunk, knock it down to the default again
|
||||
|
@ -67,7 +67,14 @@ export class Channel {
|
||||
}
|
||||
|
||||
let resolve
|
||||
const sub = nostr.sub({filter, cb}, this.name, onEose)
|
||||
const eoseRelays = []
|
||||
const sub = nostr.sub({filter, cb}, this.name, r => {
|
||||
eoseRelays.push(r)
|
||||
|
||||
if (eoseRelays.length === get(relays).length) {
|
||||
onEose()
|
||||
}
|
||||
})
|
||||
|
||||
this.p = new Promise(r => {
|
||||
resolve = r
|
||||
@ -132,13 +139,15 @@ export class Cursor {
|
||||
this.sub = null
|
||||
}
|
||||
}
|
||||
restart() {
|
||||
async restart() {
|
||||
this.stop()
|
||||
this.start()
|
||||
|
||||
await this.start()
|
||||
}
|
||||
step() {
|
||||
async step() {
|
||||
this.since -= this.delta
|
||||
this.restart()
|
||||
|
||||
await this.restart()
|
||||
}
|
||||
onEvent(e) {
|
||||
this.until = e.created_at - 1
|
||||
@ -148,7 +157,7 @@ export class Cursor {
|
||||
this.stop()
|
||||
}
|
||||
async chunk() {
|
||||
this.step()
|
||||
await this.step()
|
||||
|
||||
/* eslint no-constant-condition: 0 */
|
||||
while (true) {
|
||||
|
Loading…
Reference in New Issue
Block a user