mirror of
https://github.com/coracle-social/coracle.git
synced 2024-09-30 00:41:12 +00:00
Work on note layout a big
This commit is contained in:
parent
b40515b133
commit
9a929ce78d
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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
35
src/partials/Note.svelte
Normal 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>
|
12
src/partials/UserBadge.svelte
Normal file
12
src/partials/UserBadge.svelte
Normal 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}
|
@ -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>
|
||||
|
@ -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}
|
||||
|
@ -21,8 +21,8 @@
|
||||
}
|
||||
|
||||
const logout = () => {
|
||||
user.set(null)
|
||||
navigate("/login")
|
||||
user.set(null)
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
|
6
src/routes/NotFound.svelte
Normal file
6
src/routes/NotFound.svelte
Normal 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
60
src/routes/Notes.svelte
Normal 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}
|
||||
|
@ -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}
|
||||
|
@ -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]: () => {
|
||||
onMount(async () => {
|
||||
await ensureAccount(pubkey)
|
||||
|
||||
const sub = nostr.sub({
|
||||
filter: {authors: [pubkey], kinds: [1]},
|
||||
cb: e => {
|
||||
notes = uniqBy(prop('id'), notes.concat(e))
|
||||
},
|
||||
default: () => null,
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
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>
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user