Work on note layout a big

This commit is contained in:
Jonathan Staab 2022-11-24 21:35:29 -08:00
parent b40515b133
commit 9a929ce78d
15 changed files with 206 additions and 166 deletions

View File

@ -1,13 +1,16 @@
Bugs
- [ ] Format text with line breaks - pre, or split/br
- [ ] Reduce concurrent subscriptions
- [ ] Remove dexie, or use it instead of localstorage for cached data
- [ ] Add redirect to /notes, ditch / route
- [ ] Remove hack for re-rendering rooms on url change
- [ ] Memoize room list, every time the user switches chat rooms it pulls the full list
- [ ] rename /user to /users
- [ ] Add fallback redirect to /notes, ditch / route
- [ ] Memoize room list, currently every time the user switches chat rooms it pulls the full list
- [ ] Fix toast, it gets in the way. Make it smaller and dismissable.
Features
- [ ] Threads/social
- [ ] Followers
- [ ] Server discovery
- [ ] Favorite chat rooms

View File

@ -8,7 +8,8 @@
import {Router, Route, links, navigate} from "svelte-routing"
import {store as toast} from "src/state/toast"
import {user} from 'src/state/user'
import Feed from "src/routes/Feed.svelte"
import NotFound from "src/routes/NotFound.svelte"
import Notes from "src/routes/Notes.svelte"
import Login from "src/routes/Login.svelte"
import Profile from "src/routes/Profile.svelte"
import Keys from "src/routes/Keys.svelte"
@ -49,8 +50,7 @@
<Router {url}>
<div use:links class="h-full">
<div class="pt-16 text-white h-full">
<Route path="/" component={Feed} />
<Route path="/notes" component={Feed} />
<Route path="/notes" component={Notes} />
<Route path="/notes/new" component={NoteCreate} />
<Route path="/chat" component={Chat} />
<Route path="/chat/new" component={ChatEdit} />
@ -65,6 +65,7 @@
<Route path="/settings/relays" component={RelayList} />
<Route path="/settings/profile" component={Profile} />
<Route path="/login" component={Login} />
<Route path="*" component={NotFound} />
</div>
<ul
@ -83,7 +84,7 @@
</li>
{/if}
<li class="cursor-pointer">
<a class="block px-4 py-2 hover:bg-accent transition-all" href="/">
<a class="block px-4 py-2 hover:bg-accent transition-all" href="/notes">
<i class="fa-solid fa-tag mr-2" /> Notes
</a>
</li>

View File

@ -16,6 +16,6 @@
)
</script>
<a on:click {...$$props} {href} class={className} target={external && '_blank noopener'}>
<a on:click {...$$props} {href} class={className} target={external ? '_blank noopener' : null}>
<slot />
</a>

35
src/partials/Note.svelte Normal file
View File

@ -0,0 +1,35 @@
<script>
import {fly} from 'svelte/transition'
import {navigate} from 'svelte-routing'
import {ellipsize} from 'hurdak/src/core'
import {hasParent} from 'src/util/html'
import {accounts} from "src/state/app"
import {formatTimestamp} from 'src/util/misc'
import UserBadge from "src/partials/UserBadge.svelte"
export let note
const onClick = e => {
if (!['I'].includes(e.target.tagName) && !hasParent('a', e.target)) {
navigate(`/notes/${note.id}`)
}
}
</script>
<li
in:fly={{y: 20}}
on:click={onClick}
class="py-2 px-3 chat-message flex flex-col gap-2 hover:bg-dark border border-solid border-black hover:border-medium transition-all cursor-pointer">
<div class="flex gap-4 items-center justify-between">
<UserBadge user={$accounts[note.pubkey]} />
<p class="text-sm text-light">{formatTimestamp(note.created_at)}</p>
</div>
<div class="ml-6 flex flex-col gap-2">
<p>{ellipsize(note.content, 240)}</p>
<div class="flex gap-6 text-light">
<i class="fa-solid fa-reply cursor-pointer" />
<i class="fa-solid fa-heart cursor-pointer" />
<i class="fa-solid fa-flag cursor-pointer" />
</div>
</div>
</li>

View File

@ -0,0 +1,12 @@
<script>
export let user
</script>
{#if user}
<a href={`/user/${user.pubkey}`} class="flex gap-2 items-center">
<div
class="overflow-hidden w-4 h-4 rounded-full bg-cover bg-center shrink-0 border border-solid border-white"
style="background-image: url({user.picture})" />
<span class="text-lg font-bold">{user.name}</span>
</a>
{/if}

View File

@ -4,8 +4,9 @@
import {navigate} from 'svelte-routing'
import {prop, last} from 'ramda'
import {switcherFn} from 'hurdak/src/core'
import UserBadge from 'src/partials/UserBadge.svelte'
import {nostr} from 'src/state/nostr'
import {rooms, accounts} from 'src/state/app'
import {rooms, accounts, ensureAccount} from 'src/state/app'
import {dispatch} from 'src/state/dispatch'
import {user} from 'src/state/user'
import RoomList from "src/partials/chat/RoomList.svelte"
@ -50,23 +51,11 @@
cb: e => {
switcherFn(e.kind, {
42: () => {
const $prevListItem = last(document.querySelectorAll('.chat-message'))
messages = messages.concat(e)
if (!$accounts[e.pubkey]) {
const accountSub = nostr.sub({
filter: {kinds: [0], authors: [e.pubkey]},
cb: e => {
$accounts[e.pubkey] = {
...$accounts[e.pubkey],
...JSON.parse(e.content),
}
ensureAccount(e)
accountSub.unsub()
},
})
}
const $prevListItem = last(document.querySelectorAll('.chat-message'))
if ($prevListItem && isVisible($prevListItem)) {
setTimeout(() => {
@ -116,12 +105,7 @@
{#each annotatedMessages as m}
<li in:fly={{y: 20}} class="py-1 chat-message">
{#if m.showAccount}
<div class="flex gap-2 items-center mt-2">
<div
class="overflow-hidden w-4 h-4 rounded-full bg-cover bg-center shrink-0 border border-solid border-white"
style="background-image: url({m.account.picture})" />
<span class="text-lg font-bold">{m.account.name}</span>
</div>
<UserBadge user={m.account} />
{/if}
<div class="ml-6">{m.content}</div>
</li>

View File

@ -1,102 +0,0 @@
<script>
import {onMount} from 'svelte'
import {fly} from 'svelte/transition'
import {navigate} from "svelte-routing"
import {prop, last} from 'ramda'
import Anchor from "src/partials/Anchor.svelte"
import {nostr, relays} from "src/state/nostr"
import {user} from "src/state/user"
import {accounts} from "src/state/app"
import {db} from "src/state/db"
let notes = []
let annotatedNotes = []
$: {
// Group notes so we're only showing the account once per chunk
annotatedNotes = notes.reduce(
(mx, m) => {
const account = $accounts[m.pubkey]
// If we don't have an account yet, don't show the message
if (!account) {
return mx
}
return mx.concat({
...m,
account,
showAccount: account !== prop('account', last(mx)),
})
},
[]
)
}
const createNote = () => {
navigate("/notes/new")
}
onMount(() => {
const sub = nostr.sub({
filter: {kinds: [1], since: new Date().valueOf() / 1000 - 7 * 24 * 60 * 60},
cb: e => {
notes = notes.concat(e)
if (!$accounts[e.pubkey]) {
const accountSub = nostr.sub({
filter: {kinds: [0], authors: [e.pubkey]},
cb: e => {
$accounts[e.pubkey] = {
pubkey: e.pubkey,
...$accounts[e.pubkey],
...JSON.parse(e.content),
}
accountSub.unsub()
},
})
}
},
})
return () => sub.unsub()
})
</script>
<ul class="p-2">
{#each annotatedNotes as n}
<li in:fly={{y: 20}} class="py-1 chat-message">
{#if n.showAccount}
<a href={`/user/${n.account.pubkey}`} class="flex gap-2 items-center mt-2">
<div
class="overflow-hidden w-4 h-4 rounded-full bg-cover bg-center shrink-0 border border-solid border-white"
style="background-image: url({n.account.picture})" />
<span class="text-lg font-bold">{n.account.name}</span>
</a>
{/if}
<div class="ml-6">{n.content}</div>
</li>
{/each}
</ul>
{#if $relays.length > 0}
<div class="fixed bottom-0 right-0 p-8">
<div
class="rounded-full bg-accent color-white w-16 h-16 flex justify-center
items-center border border-dark shadow-2xl cursor-pointer"
on:click={createNote}
>
<span class="fa-sold fa-plus fa-2xl" />
</div>
</div>
{:else}
<div class="flex w-full justify-center items-center py-16">
<div class="text-center max-w-md">
You aren't yet connected to any relays. Please click <Anchor href="/settings/relays"
>here</Anchor
> to get started.
</div>
</div>
{/if}

View File

@ -21,8 +21,8 @@
}
const logout = () => {
user.set(null)
navigate("/login")
user.set(null)
}
onMount(async () => {

View File

@ -0,0 +1,6 @@
<script>
import {onMount} from 'svelte'
import {navigate} from 'svelte-routing'
onMount(() => navigate('/notes'))
</script>

60
src/routes/Notes.svelte Normal file
View File

@ -0,0 +1,60 @@
<script>
import {onMount} from 'svelte'
import {get} from 'svelte/store'
import {fly} from 'svelte/transition'
import {navigate} from "svelte-routing"
import {prop, reverse, last} from 'ramda'
import {timedelta, now, formatTimestamp} from 'src/util/misc'
import Anchor from "src/partials/Anchor.svelte"
import Note from "src/partials/Note.svelte"
import {nostr, relays} from "src/state/nostr"
import {user} from "src/state/user"
import {accounts, ensureAccount} from "src/state/app"
import {db} from "src/state/db"
let notes = []
const createNote = () => {
navigate("/notes/new")
}
onMount(() => {
const sub = nostr.sub({
filter: {kinds: [1], since: new Date().valueOf() / 1000 - 7 * 24 * 60 * 60},
cb: e => {
notes = notes.concat(e)
ensureAccount(e.pubkey)
},
})
return () => sub.unsub()
})
</script>
<ul class="py-8 flex flex-col gap-4 max-w-xl m-auto">
{#each reverse(notes) as n}
<Note note={n} />
{/each}
</ul>
{#if $relays.length > 0}
<div class="fixed bottom-0 right-0 p-8">
<div
class="rounded-full bg-accent color-white w-16 h-16 flex justify-center
items-center border border-dark shadow-2xl cursor-pointer"
on:click={createNote}
>
<span class="fa-sold fa-plus fa-2xl" />
</div>
</div>
{:else}
<div class="flex w-full justify-center items-center py-16">
<div class="text-center max-w-md">
You aren't yet connected to any relays. Please click <Anchor href="/settings/relays"
>here</Anchor
> to get started.
</div>
</div>
{/if}

View File

@ -34,7 +34,7 @@
<Input bind:value={q} type="text" wrapperClass="flex-grow" placeholder="Type to search">
<i slot="before" class="fa-solid fa-search" />
</Input>
<Anchor type="button" href="/">Done</Anchor>
<Anchor type="button" href="/notes">Done</Anchor>
</div>
<div class="flex flex-col gap-6 overflow-auto flex-grow -mx-6 px-6">
{#each search(q) as relay}

View File

@ -3,47 +3,32 @@
import {reverse} from 'ramda'
import {fly} from 'svelte/transition'
import {uniqBy, prop} from 'ramda'
import {switcherFn} from 'hurdak/src/core'
import {ellipsize} from 'hurdak/src/core'
import {formatTimestamp} from 'src/util/misc'
import Note from "src/partials/Note.svelte"
import {nostr} from 'src/state/nostr'
import {user as currentUser} from 'src/state/user'
import {accounts} from 'src/state/app'
import {accounts, ensureAccount} from "src/state/app"
export let pubkey
let user
let notes = []
onMount(() => {
const sub = nostr.sub({
filter: {authors: [pubkey]},
cb: e => {
switcherFn(e.kind, {
[0]: () => {
user = JSON.parse(e.content)
$: user = $accounts[pubkey]
// Take this opportunity to sync account data. TODO this is a hack,
// we should by syncing and caching everywhere we grab accounts
$accounts[pubkey] = user
},
[1]: () => {
notes = uniqBy(prop('id'), notes.concat(e))
},
default: () => null,
})
onMount(async () => {
await ensureAccount(pubkey)
const sub = nostr.sub({
filter: {authors: [pubkey], kinds: [1]},
cb: e => {
notes = uniqBy(prop('id'), notes.concat(e))
},
})
return () => sub.unsub()
})
const formatTimestamp = ts => {
const formatter = new Intl.DateTimeFormat('en-US', {
dateStyle: 'medium',
timeStyle: 'short',
})
return formatter.format(new Date(ts * 1000))
}
</script>
{#if user}
@ -69,10 +54,7 @@
<div class="h-px bg-medium" in:fly={{y: 20, delay: 200}} />
<div class="flex flex-col gap-4" in:fly={{y: 20, delay: 400}}>
{#each reverse(notes) as note}
<div>
<small class="text-light">{formatTimestamp(note.created_at)}</small>
<p>{note.content}</p>
</div>
<Note note={note} />
{/each}
</div>
</div>

View File

@ -1,6 +1,8 @@
import {writable} from 'svelte/store'
import {getLocalJson, setLocalJson} from "src/util/misc"
import {prop} from 'ramda'
import {writable, get} from 'svelte/store'
import {getLocalJson, setLocalJson, now, timedelta} from "src/util/misc"
import {user} from 'src/state/user'
import {nostr} from 'src/state/nostr'
export const rooms = writable(getLocalJson("coracle/rooms") || {})
@ -19,3 +21,27 @@ user.subscribe($user => {
accounts.update($accounts => ({...$accounts, [$user.pubkey]: $user}))
}
})
export const ensureAccount = pubkey => {
let $account = prop(pubkey, get(accounts))
if (!$account || $account.lastRefreshed < now() - timedelta(10, 'minutes')) {
const accountSub = nostr.sub({
filter: {kinds: [0], authors: [pubkey]},
cb: e => {
$account = {
...$account,
...JSON.parse(e.content),
pubkey,
lastRefreshed: now(),
}
accounts.update($accounts => ({...$accounts, [pubkey]: $account}))
},
})
setTimeout(() => {
accountSub.unsub()
}, 1000)
}
}

View File

@ -44,3 +44,15 @@ export const stripExifData = async file => {
})
})
}
export const hasParent = (tag, e) => {
while (e.parentNode) {
if (e.parentNode.tagName === tag.toUpperCase()) {
return true
}
e = e.parentNode
}
return false
}

View File

@ -26,3 +26,24 @@ export const setLocalJson = (k, v) => {
// pass
}
}
export const now = () => new Date().valueOf() / 1000
export const timedelta = (n, unit = 'seconds') => {
switch (unit) {
case 'seconds': return n
case 'minutes': return n * 60
case 'hours': return n * 60 * 60
case 'days': return n * 60 * 60 * 24
default: throw new Error(`Invalid unit ${unit}`)
}
}
export const formatTimestamp = ts => {
const formatter = new Intl.DateTimeFormat('en-US', {
dateStyle: 'medium',
timeStyle: 'short',
})
return formatter.format(new Date(ts * 1000))
}