mirror of
https://github.com/coracle-social/coracle.git
synced 2024-09-30 00:41:12 +00:00
Add DMs
This commit is contained in:
parent
1a88bfa629
commit
1a9a31b45b
11
README.md
11
README.md
@ -17,7 +17,8 @@ If you like Coracle and want to support its development, you can donate sats via
|
|||||||
- [x] Mentions
|
- [x] Mentions
|
||||||
- [x] Persist and load relay list
|
- [x] Persist and load relay list
|
||||||
- [x] NIP 05
|
- [x] NIP 05
|
||||||
- [ ] Direct messages https://github.com/nbd-wtf/nostr-tools/blob/master/nip04.ts
|
- [ ] Direct messages using NIP 04
|
||||||
|
- [ ] Deploy coracle relay, set better defaults
|
||||||
- [ ] Image uploads
|
- [ ] Image uploads
|
||||||
- Use dufflepud. Default will charge via lightning and have a tos, others can self-host and skip that.
|
- Use dufflepud. Default will charge via lightning and have a tos, others can self-host and skip that.
|
||||||
- Add banner field to profile
|
- Add banner field to profile
|
||||||
@ -46,11 +47,13 @@ If you like Coracle and want to support its development, you can donate sats via
|
|||||||
## Current
|
## Current
|
||||||
|
|
||||||
- [ ] Figure out migrations from previous version
|
- [ ] Figure out migrations from previous version
|
||||||
- [ ] Fix search
|
- [ ] Fix notes search
|
||||||
- [ ] Deploy coracle relay, set better defaults
|
|
||||||
- [ ] Chat
|
- [ ] Chat
|
||||||
- [ ] Figure out which relays to use
|
- [ ] Figure out which relays to use
|
||||||
- [ ] Add petnames for channels - join/leave from list page?
|
- [ ] Add petnames for channels
|
||||||
|
- [ ] Add back button
|
||||||
|
- [ ] Create Room -> open modal, choose dm or public room
|
||||||
|
- [ ] Add DM button to profile pages
|
||||||
|
|
||||||
## 0.2.7
|
## 0.2.7
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
import "@fortawesome/fontawesome-free/css/fontawesome.css"
|
import "@fortawesome/fontawesome-free/css/fontawesome.css"
|
||||||
import "@fortawesome/fontawesome-free/css/solid.css"
|
import "@fortawesome/fontawesome-free/css/solid.css"
|
||||||
|
|
||||||
import {pluck} from 'ramda'
|
import {find, pluck} from 'ramda'
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import {writable, get} from "svelte/store"
|
import {writable, get} from "svelte/store"
|
||||||
import {fly, fade} from "svelte/transition"
|
import {fly, fade} from "svelte/transition"
|
||||||
@ -12,7 +12,7 @@
|
|||||||
import {displayPerson, isLike} from 'src/util/nostr'
|
import {displayPerson, isLike} from 'src/util/nostr'
|
||||||
import {timedelta, now} from 'src/util/misc'
|
import {timedelta, now} from 'src/util/misc'
|
||||||
import {keys, user, pool, getRelays} from 'src/agent'
|
import {keys, user, pool, getRelays} from 'src/agent'
|
||||||
import {modal, toast, settings, alerts} from "src/app"
|
import {modal, toast, settings, alerts, messages} from "src/app"
|
||||||
import {routes} from "src/app/ui"
|
import {routes} from "src/app/ui"
|
||||||
import Anchor from 'src/partials/Anchor.svelte'
|
import Anchor from 'src/partials/Anchor.svelte'
|
||||||
import Spinner from 'src/partials/Spinner.svelte'
|
import Spinner from 'src/partials/Spinner.svelte'
|
||||||
@ -39,6 +39,7 @@
|
|||||||
import Bech32Entity from "src/routes/Bech32Entity.svelte"
|
import Bech32Entity from "src/routes/Bech32Entity.svelte"
|
||||||
import Chat from "src/routes/Chat.svelte"
|
import Chat from "src/routes/Chat.svelte"
|
||||||
import ChatRoom from "src/routes/ChatRoom.svelte"
|
import ChatRoom from "src/routes/ChatRoom.svelte"
|
||||||
|
import Messages from "src/routes/Messages.svelte"
|
||||||
|
|
||||||
export let url = ""
|
export let url = ""
|
||||||
|
|
||||||
@ -53,18 +54,27 @@
|
|||||||
menuIsOpen.set(false)
|
menuIsOpen.set(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const {canSign} = keys
|
|
||||||
const {lastCheckedAlerts, mostRecentAlert} = alerts
|
const {lastCheckedAlerts, mostRecentAlert} = alerts
|
||||||
|
const {lastCheckedByPubkey, mostRecentByPubkey} = messages
|
||||||
|
|
||||||
let menuIcon
|
let menuIcon
|
||||||
let scrollY
|
let scrollY
|
||||||
let suspendedSubs = []
|
let suspendedSubs = []
|
||||||
let slowConnections = []
|
let slowConnections = []
|
||||||
|
let hasNewMessages = false
|
||||||
|
|
||||||
|
$: {
|
||||||
|
hasNewMessages = Boolean(find(
|
||||||
|
([k, t]) => ($lastCheckedByPubkey[k] || 0) < t,
|
||||||
|
Object.entries($mostRecentByPubkey)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if ($user) {
|
if ($user) {
|
||||||
alerts.load(getRelays(), $user.pubkey)
|
alerts.load(getRelays(), $user.pubkey)
|
||||||
alerts.listen(getRelays(), $user.pubkey)
|
alerts.listen(getRelays(), $user.pubkey)
|
||||||
|
messages.listen(getRelays(), $user.pubkey)
|
||||||
}
|
}
|
||||||
|
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
@ -130,11 +140,16 @@
|
|||||||
{/key}
|
{/key}
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/chat" component={Chat} />
|
<Route path="/chat" component={Chat} />
|
||||||
<Route path="/chat/:roomId" let:params>
|
<Route path="/chat/:entity" let:params>
|
||||||
{#key params.roomId}
|
{#key params.entity}
|
||||||
<ChatRoom {...params} />
|
<ChatRoom {...params} />
|
||||||
{/key}
|
{/key}
|
||||||
</Route>
|
</Route>
|
||||||
|
<Route path="/messages/:entity" let:params>
|
||||||
|
{#key params.entity}
|
||||||
|
<Messages {...params} />
|
||||||
|
{/key}
|
||||||
|
</Route>
|
||||||
<Route path="/keys" component={Keys} />
|
<Route path="/keys" component={Keys} />
|
||||||
<Route path="/relays" component={RelayList} />
|
<Route path="/relays" component={RelayList} />
|
||||||
<Route path="/profile" component={Profile} />
|
<Route path="/profile" component={Profile} />
|
||||||
@ -186,6 +201,9 @@
|
|||||||
<li class="cursor-pointer">
|
<li class="cursor-pointer">
|
||||||
<a class="block px-4 py-2 hover:bg-accent transition-all" href="/chat">
|
<a class="block px-4 py-2 hover:bg-accent transition-all" href="/chat">
|
||||||
<i class="fa-solid fa-message mr-2" /> Chat
|
<i class="fa-solid fa-message mr-2" /> Chat
|
||||||
|
{#if hasNewMessages}
|
||||||
|
<div class="w-2 h-2 rounded bg-accent absolute top-3 left-6" />
|
||||||
|
{/if}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="h-px mx-3 my-4 bg-medium" />
|
<li class="h-px mx-3 my-4 bg-medium" />
|
||||||
@ -232,12 +250,12 @@
|
|||||||
<img src="/images/favicon.png" class="w-8" />
|
<img src="/images/favicon.png" class="w-8" />
|
||||||
<h1 class="staatliches text-3xl">Coracle</h1>
|
<h1 class="staatliches text-3xl">Coracle</h1>
|
||||||
</Anchor>
|
</Anchor>
|
||||||
{#if $mostRecentAlert > $lastCheckedAlerts}
|
{#if $mostRecentAlert > $lastCheckedAlerts || hasNewMessages}
|
||||||
<div class="w-2 h-2 rounded bg-accent absolute top-4 left-12 lg:hidden" />
|
<div class="w-2 h-2 rounded bg-accent absolute top-4 left-12 lg:hidden" />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if $canSign}
|
{#if keys.canSign()}
|
||||||
<div class="fixed bottom-0 right-0 m-8">
|
<div class="fixed bottom-0 right-0 m-8">
|
||||||
<a
|
<a
|
||||||
class="rounded-full bg-accent color-white w-16 h-16 flex justify-center
|
class="rounded-full bg-accent color-white w-16 h-16 flex justify-center
|
||||||
|
@ -8,9 +8,10 @@ import {personKinds, Tags, roomAttrs} from 'src/util/nostr'
|
|||||||
|
|
||||||
export const db = new Dexie('agent/data/db')
|
export const db = new Dexie('agent/data/db')
|
||||||
|
|
||||||
db.version(11).stores({
|
db.version(12).stores({
|
||||||
relays: '++url, name',
|
relays: '++url, name',
|
||||||
alerts: '++id, created_at',
|
alerts: '++id, created_at',
|
||||||
|
messages: '++id, pubkey',
|
||||||
people: '++pubkey',
|
people: '++pubkey',
|
||||||
rooms: '++id, joined',
|
rooms: '++id, joined',
|
||||||
})
|
})
|
||||||
@ -51,6 +52,7 @@ export const processEvents = async events => {
|
|||||||
await Promise.all([
|
await Promise.all([
|
||||||
processProfileEvents(events),
|
processProfileEvents(events),
|
||||||
processRoomEvents(events),
|
processRoomEvents(events),
|
||||||
|
processMessages(events),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -154,6 +156,14 @@ const processRoomEvents = async events => {
|
|||||||
await db.rooms.bulkPut(Object.values(updates))
|
await db.rooms.bulkPut(Object.values(updates))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const processMessages = async events => {
|
||||||
|
const messages = ensurePlural(events).filter(e => e.kind === 4)
|
||||||
|
|
||||||
|
if (messages.length > 0) {
|
||||||
|
await db.messages.bulkPut(messages)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
|
|
||||||
const verifyNip05 = (pubkey, as) =>
|
const verifyNip05 = (pubkey, as) =>
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import {nip04} from 'nostr-tools'
|
||||||
import {getPublicKey, getEventHash, signEvent} from 'nostr-tools'
|
import {getPublicKey, getEventHash, signEvent} from 'nostr-tools'
|
||||||
import {get} from 'svelte/store'
|
import {get} from 'svelte/store'
|
||||||
import {synced} from 'src/util/misc'
|
import {synced} from 'src/util/misc'
|
||||||
@ -6,7 +7,8 @@ let signingFunction
|
|||||||
|
|
||||||
const pubkey = synced('agent/user/pubkey')
|
const pubkey = synced('agent/user/pubkey')
|
||||||
const privkey = synced('agent/user/privkey')
|
const privkey = synced('agent/user/privkey')
|
||||||
const canSign = synced('agent/user/canSign')
|
const hasExtension = () => Boolean(window.nostr)
|
||||||
|
const canSign = () => Boolean(hasExtension() || get(privkey))
|
||||||
|
|
||||||
const setPrivateKey = _privkey => {
|
const setPrivateKey = _privkey => {
|
||||||
privkey.set(_privkey)
|
privkey.set(_privkey)
|
||||||
@ -23,6 +25,11 @@ const setPublicKey = _pubkey => {
|
|||||||
pubkey.set(_pubkey)
|
pubkey.set(_pubkey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const clear = () => {
|
||||||
|
pubkey.set(null)
|
||||||
|
privkey.set(null)
|
||||||
|
}
|
||||||
|
|
||||||
const sign = async event => {
|
const sign = async event => {
|
||||||
event.pubkey = get(pubkey)
|
event.pubkey = get(pubkey)
|
||||||
event.id = getEventHash(event)
|
event.id = getEventHash(event)
|
||||||
@ -33,14 +40,35 @@ const sign = async event => {
|
|||||||
return event
|
return event
|
||||||
}
|
}
|
||||||
|
|
||||||
const clear = () => {
|
const getCrypt = () => {
|
||||||
pubkey.set(null)
|
const $privkey = get(privkey)
|
||||||
privkey.set(null)
|
|
||||||
canSign.set(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (!$privkey && !hasExtension()) {
|
||||||
|
throw new Error('No encryption method available.')
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
encrypt: (pubkey, message) => {
|
||||||
|
return $privkey
|
||||||
|
? nip04.encrypt($privkey, pubkey, message)
|
||||||
|
: window.nostr.nip04.encrypt(pubkey, message)
|
||||||
|
},
|
||||||
|
decrypt: async (pubkey, message) => {
|
||||||
|
try {
|
||||||
|
return $privkey
|
||||||
|
? nip04.decrypt($privkey, pubkey, message)
|
||||||
|
: await window.nostr.nip04.decrypt(pubkey, message)
|
||||||
|
} catch (e) {
|
||||||
|
return `<Failed to decrypt message: ${e}>`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Init signing function by re-setting pubkey
|
// Init signing function by re-setting pubkey
|
||||||
setPublicKey(get(pubkey))
|
setPublicKey(get(pubkey))
|
||||||
|
|
||||||
export default {pubkey, privkey, canSign, setPrivateKey, setPublicKey, sign, clear}
|
export default {
|
||||||
|
pubkey, privkey, hasExtension, canSign, setPrivateKey, setPublicKey, clear,
|
||||||
|
sign, getCrypt,
|
||||||
|
}
|
||||||
|
@ -9,6 +9,7 @@ let connections = []
|
|||||||
|
|
||||||
class Connection {
|
class Connection {
|
||||||
constructor(url) {
|
constructor(url) {
|
||||||
|
this.promise = null
|
||||||
this.nostr = this.init(url)
|
this.nostr = this.init(url)
|
||||||
this.status = 'new'
|
this.status = 'new'
|
||||||
this.url = url
|
this.url = url
|
||||||
@ -41,9 +42,12 @@ class Connection {
|
|||||||
|
|
||||||
if (shouldConnect) {
|
if (shouldConnect) {
|
||||||
this.status = 'pending'
|
this.status = 'pending'
|
||||||
|
this.promise = this.nostr.connect()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.status === 'pending') {
|
||||||
try {
|
try {
|
||||||
await this.nostr.connect()
|
await this.promise
|
||||||
this.status = 'ready'
|
this.status = 'ready'
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.status = 'error'
|
this.status = 'error'
|
||||||
@ -107,7 +111,6 @@ const subscribe = async (relays, filters) => {
|
|||||||
relays = uniqBy(prop('url'), relays.filter(r => isRelay(r.url)))
|
relays = uniqBy(prop('url'), relays.filter(r => isRelay(r.url)))
|
||||||
filters = ensurePlural(filters)
|
filters = ensurePlural(filters)
|
||||||
|
|
||||||
|
|
||||||
// Create a human readable subscription id for debugging
|
// Create a human readable subscription id for debugging
|
||||||
const id = [
|
const id = [
|
||||||
Math.random().toString().slice(2, 6),
|
Math.random().toString().slice(2, 6),
|
||||||
|
@ -22,9 +22,13 @@ const createRoom = (relays, room) =>
|
|||||||
const updateRoom = (relays, {id, ...room}) =>
|
const updateRoom = (relays, {id, ...room}) =>
|
||||||
publishEvent(relays, 41, {content: JSON.stringify(pick(roomAttrs, room)), tags: [["e", id]]})
|
publishEvent(relays, 41, {content: JSON.stringify(pick(roomAttrs, room)), tags: [["e", id]]})
|
||||||
|
|
||||||
const createMessage = (relays, roomId, content) =>
|
const createChatMessage = (relays, roomId, content) =>
|
||||||
publishEvent(relays, 42, {content, tags: [["e", roomId, prop('url', first(relays)), "root"]]})
|
publishEvent(relays, 42, {content, tags: [["e", roomId, prop('url', first(relays)), "root"]]})
|
||||||
|
|
||||||
|
const createDirectMessage = (relays, pubkey, content) =>
|
||||||
|
// todo, encrypt messages
|
||||||
|
publishEvent(relays, 4, {content, tags: [["p", pubkey]]})
|
||||||
|
|
||||||
const createNote = (relays, content, mentions = [], topics = []) => {
|
const createNote = (relays, content, mentions = [], topics = []) => {
|
||||||
mentions = mentions.map(p => ["p", p, prop('url', first(getRelays(p)))])
|
mentions = mentions.map(p => ["p", p, prop('url', first(getRelays(p)))])
|
||||||
topics = topics.map(t => ["t", t])
|
topics = topics.map(t => ["t", t])
|
||||||
@ -100,6 +104,7 @@ const publishEvent = (relays, kind, {content = '', tags = []} = {}) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
updateUser, setRelays, setPetnames, muffle, createRoom, updateRoom, createMessage, createNote,
|
updateUser, setRelays, setPetnames, muffle, createRoom, updateRoom,
|
||||||
createReaction, createReply, deleteEvent,
|
createChatMessage, createDirectMessage, createNote, createReaction,
|
||||||
|
createReply, deleteEvent,
|
||||||
}
|
}
|
||||||
|
@ -9,9 +9,10 @@ import defaults from 'src/agent/defaults'
|
|||||||
import {toast, routes, modal, settings} from 'src/app/ui'
|
import {toast, routes, modal, settings} from 'src/app/ui'
|
||||||
import cmd from 'src/app/cmd'
|
import cmd from 'src/app/cmd'
|
||||||
import alerts from 'src/app/alerts'
|
import alerts from 'src/app/alerts'
|
||||||
|
import messages from 'src/app/messages'
|
||||||
import loaders from 'src/app/loaders'
|
import loaders from 'src/app/loaders'
|
||||||
|
|
||||||
export {toast, modal, settings, alerts}
|
export {toast, modal, settings, alerts, messages}
|
||||||
|
|
||||||
export const login = async ({privkey, pubkey}, usingExtension = false) => {
|
export const login = async ({privkey, pubkey}, usingExtension = false) => {
|
||||||
if (privkey) {
|
if (privkey) {
|
||||||
@ -20,14 +21,11 @@ export const login = async ({privkey, pubkey}, usingExtension = false) => {
|
|||||||
keys.setPublicKey(pubkey)
|
keys.setPublicKey(pubkey)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (usingExtension || privkey) {
|
|
||||||
keys.canSign.set(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load network and start listening, but don't wait for it
|
// Load network and start listening, but don't wait for it
|
||||||
loaders.loadNetwork(getRelays(), pubkey),
|
loaders.loadNetwork(getRelays(), pubkey),
|
||||||
alerts.load(getRelays(), pubkey),
|
alerts.load(getRelays(), pubkey),
|
||||||
alerts.listen(getRelays(), pubkey),
|
alerts.listen(getRelays(), pubkey),
|
||||||
|
messages.listen(getRelays(), pubkey)
|
||||||
|
|
||||||
navigate('/notes/global')
|
navigate('/notes/global')
|
||||||
}
|
}
|
||||||
@ -49,6 +47,7 @@ export const addRelay = async relay => {
|
|||||||
loaders.loadNetwork(relays, person.pubkey),
|
loaders.loadNetwork(relays, person.pubkey),
|
||||||
alerts.load(relays, person.pubkey),
|
alerts.load(relays, person.pubkey),
|
||||||
alerts.listen(relays, person.pubkey),
|
alerts.listen(relays, person.pubkey),
|
||||||
|
messages.listen(getRelays(), person.pubkey)
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
35
src/app/messages.js
Normal file
35
src/app/messages.js
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import {pluck} from 'ramda'
|
||||||
|
import {synced, batch} from 'src/util/misc'
|
||||||
|
import {listen as _listen} from 'src/agent'
|
||||||
|
import loaders from 'src/app/loaders'
|
||||||
|
|
||||||
|
let listener
|
||||||
|
|
||||||
|
const mostRecentByPubkey = synced('app/messages/mostRecentByPubkey', {})
|
||||||
|
const lastCheckedByPubkey = synced('app/messages/lastCheckedByPubkey', {})
|
||||||
|
|
||||||
|
const listen = async (relays, pubkey) => {
|
||||||
|
if (listener) {
|
||||||
|
listener.unsub()
|
||||||
|
}
|
||||||
|
|
||||||
|
listener = await _listen(
|
||||||
|
relays,
|
||||||
|
{kinds: [4], '#p': [pubkey]},
|
||||||
|
batch(300, async events => {
|
||||||
|
if (events.length > 0) {
|
||||||
|
await loaders.loadPeople(relays, pluck('pubkey', events))
|
||||||
|
|
||||||
|
mostRecentByPubkey.update(o => {
|
||||||
|
for (const {pubkey, created_at} of events) {
|
||||||
|
o[pubkey] = Math.max(created_at, o[pubkey] || 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return o
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {listen, mostRecentByPubkey, lastCheckedByPubkey}
|
178
src/partials/Channel.svelte
Normal file
178
src/partials/Channel.svelte
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
<script>
|
||||||
|
import {onMount} from 'svelte'
|
||||||
|
import {fly} from 'svelte/transition'
|
||||||
|
import {navigate} from 'svelte-routing'
|
||||||
|
import {prop, path as getPath, reverse, uniqBy, sortBy, last} from 'ramda'
|
||||||
|
import {formatTimestamp, createScroller, Cursor} from 'src/util/misc'
|
||||||
|
import Badge from 'src/partials/Badge.svelte'
|
||||||
|
import Spinner from 'src/partials/Spinner.svelte'
|
||||||
|
import {user, getPerson} from 'src/agent'
|
||||||
|
import {render} from 'src/app'
|
||||||
|
|
||||||
|
export let name
|
||||||
|
export let about
|
||||||
|
export let picture
|
||||||
|
export let loadMessages
|
||||||
|
export let listenForMessages
|
||||||
|
export let sendMessage
|
||||||
|
export let editRoom = null
|
||||||
|
|
||||||
|
let textarea
|
||||||
|
let messages = []
|
||||||
|
let annotatedMessages = []
|
||||||
|
let showNewMessages = false
|
||||||
|
let cursor = new Cursor()
|
||||||
|
|
||||||
|
$: {
|
||||||
|
// Group messages so we're only showing the person once per chunk
|
||||||
|
annotatedMessages = reverse(sortBy(prop('created_at'), uniqBy(prop('id'), messages)).reduce(
|
||||||
|
(mx, m) => {
|
||||||
|
const person = getPerson(m.pubkey, true)
|
||||||
|
const showPerson = person.pubkey !== getPath(['person', 'pubkey'], last(mx))
|
||||||
|
|
||||||
|
return mx.concat({...m, person, showPerson})
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
|
await cb()
|
||||||
|
|
||||||
|
if (shouldStick) {
|
||||||
|
const $li = getLastListItem()
|
||||||
|
|
||||||
|
if ($li) {
|
||||||
|
$li.scrollIntoView({behavior})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showNewMessages = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
if (!$user) {
|
||||||
|
return navigate('/login')
|
||||||
|
}
|
||||||
|
|
||||||
|
const sub = listenForMessages(
|
||||||
|
newMessages => stickToBottom('smooth', () => {
|
||||||
|
messages = messages.concat(newMessages)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const scroller = createScroller(
|
||||||
|
async () => {
|
||||||
|
const events = await loadMessages(cursor)
|
||||||
|
|
||||||
|
if (events.length) {
|
||||||
|
cursor.onChunk(events)
|
||||||
|
|
||||||
|
stickToBottom('auto', () => {
|
||||||
|
messages = events.concat(messages)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{reverse: true}
|
||||||
|
)
|
||||||
|
|
||||||
|
return async () => {
|
||||||
|
const {unsub} = await sub
|
||||||
|
|
||||||
|
scroller.stop()
|
||||||
|
unsub()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const send = async () => {
|
||||||
|
const content = textarea.value.trim()
|
||||||
|
|
||||||
|
if (content) {
|
||||||
|
textarea.value = ''
|
||||||
|
|
||||||
|
const event = await sendMessage(content)
|
||||||
|
|
||||||
|
stickToBottom('smooth', () => {
|
||||||
|
messages = [event].concat(messages)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onKeyPress = e => {
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
e.preventDefault()
|
||||||
|
send()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:window on:scroll={() => { showNewMessages = false}} />
|
||||||
|
|
||||||
|
<div class="flex gap-4 h-full">
|
||||||
|
<div class="relative w-full">
|
||||||
|
<div class="flex flex-col py-32">
|
||||||
|
<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 flex flex-col gap-2">
|
||||||
|
{#if m.showPerson}
|
||||||
|
<div class="flex gap-4 items-center justify-between">
|
||||||
|
<Badge person={m.person} />
|
||||||
|
<p class="text-sm text-light">{formatTimestamp(m.created_at)}</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div class="ml-6 overflow-hidden text-ellipsis">
|
||||||
|
{@html render(m, {showEntire: true})}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
<Spinner>Looking for messages...</Spinner>
|
||||||
|
<div class="h-48" />
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="fixed z-10 top-0 pt-20 w-full p-4 border-b border-solid border-medium bg-dark flex gap-4">
|
||||||
|
<div
|
||||||
|
class="overflow-hidden w-12 h-12 rounded-full bg-cover bg-center shrink-0 border border-solid border-white"
|
||||||
|
style="background-image: url({picture})" />
|
||||||
|
<div class="w-full">
|
||||||
|
<div class="flex items-center justify-between w-full">
|
||||||
|
<div class="text-lg font-bold">{name || ''}</div>
|
||||||
|
{#if editRoom}
|
||||||
|
<small class="cursor-pointer" on:click={editRoom}>
|
||||||
|
<i class="fa-solid fa-edit" /> Edit
|
||||||
|
</small>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div>{about || ''}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="fixed z-10 bottom-0 w-full flex bg-medium border-medium border-t border-solid border-dark">
|
||||||
|
<textarea
|
||||||
|
rows="4"
|
||||||
|
autofocus
|
||||||
|
placeholder="Type something..."
|
||||||
|
bind:this={textarea}
|
||||||
|
on:keypress={onKeyPress}
|
||||||
|
class="w-full p-2 text-white bg-medium
|
||||||
|
placeholder:text-light outline-0 resize-none" />
|
||||||
|
<div
|
||||||
|
on:click={send}
|
||||||
|
class="flex flex-col py-8 p-4 justify-center gap-2 border-l border-solid border-dark
|
||||||
|
hover:bg-accent transition-all cursor-pointer text-white ">
|
||||||
|
<i class="fa-solid fa-paper-plane fa-xl" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if showNewMessages}
|
||||||
|
<div class="fixed w-full flex justify-center bottom-32" transition:fly|local={{y: 20}}>
|
||||||
|
<div class="rounded-full bg-accent text-white py-2 px-4">
|
||||||
|
New messages found
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
@ -1,6 +1,8 @@
|
|||||||
<script>
|
<script>
|
||||||
import {fly} from 'svelte/transition'
|
import {fly} from 'svelte/transition'
|
||||||
|
import {ellipsize} from 'hurdak/lib/hurdak'
|
||||||
import Anchor from 'src/partials/Anchor.svelte'
|
import Anchor from 'src/partials/Anchor.svelte'
|
||||||
|
import {displayPerson} from 'src/util/nostr'
|
||||||
|
|
||||||
export let joined = false
|
export let joined = false
|
||||||
export let room
|
export let room
|
||||||
@ -11,14 +13,23 @@
|
|||||||
|
|
||||||
<li
|
<li
|
||||||
class="flex gap-4 px-4 py-6 cursor-pointer hover:bg-medium transition-all rounded border border-solid border-medium bg-dark"
|
class="flex gap-4 px-4 py-6 cursor-pointer hover:bg-medium transition-all rounded border border-solid border-medium bg-dark"
|
||||||
on:click={() => setRoom(room.id)}
|
on:click={() => setRoom(room)}
|
||||||
in:fly={{y: 20}}>
|
in:fly={{y: 20}}>
|
||||||
<div
|
<div
|
||||||
class="overflow-hidden w-14 h-14 rounded-full bg-cover bg-center shrink-0 border border-solid border-white"
|
class="overflow-hidden w-14 h-14 rounded-full bg-cover bg-center shrink-0 border border-solid border-white"
|
||||||
style="background-image: url({room.picture})" />
|
style="background-image: url({room.picture})" />
|
||||||
<div class="flex flex-grow flex-col justify-start gap-2">
|
<div class="flex flex-grow flex-col justify-start gap-2">
|
||||||
<div class="flex flex-grow items-center justify-between gap-2">
|
<div class="flex flex-grow items-center justify-between gap-2">
|
||||||
|
<div class="flex gap-2 items-center">
|
||||||
|
{#if room.type === 'private'}
|
||||||
|
<i class="fa fa-lock text-light" />
|
||||||
|
<h2 class="text-lg">{displayPerson(room)}</h2>
|
||||||
|
{:else}
|
||||||
|
<i class="fa fa-lock-open text-light" />
|
||||||
<h2 class="text-lg">{room.name}</h2>
|
<h2 class="text-lg">{room.name}</h2>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#if room.type === 'public'}
|
||||||
{#if joined}
|
{#if joined}
|
||||||
<Anchor type="button" class="flex items-center gap-2" on:click={e => { e.stopPropagation(); leaveRoom(room.id) }}>
|
<Anchor type="button" class="flex items-center gap-2" on:click={e => { e.stopPropagation(); leaveRoom(room.id) }}>
|
||||||
<i class="fa fa-right-from-bracket" />
|
<i class="fa fa-right-from-bracket" />
|
||||||
@ -30,10 +41,11 @@
|
|||||||
<span>Join</span>
|
<span>Join</span>
|
||||||
</Anchor>
|
</Anchor>
|
||||||
{/if}
|
{/if}
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if room.about}
|
{#if room.about}
|
||||||
<p class="text-light whitespace-nowrap text-ellipsis overflow-hidden">
|
<p class="text-light">
|
||||||
{room.about}
|
{ellipsize(room.about, 300)}
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
<script>
|
<script>
|
||||||
import {reject} from 'ramda'
|
import {uniq, pluck} from 'ramda'
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
|
import {nip19} from 'nostr-tools'
|
||||||
import {navigate} from "svelte-routing"
|
import {navigate} from "svelte-routing"
|
||||||
import {liveQuery} from 'dexie'
|
import {liveQuery} from 'dexie'
|
||||||
import {fuzzy} from "src/util/misc"
|
import {fuzzy} from "src/util/misc"
|
||||||
import {getRelays, listen, db} from 'src/agent'
|
import {getRelays, getPerson, listen, db} from 'src/agent'
|
||||||
import {modal} from 'src/app'
|
import {modal} from 'src/app'
|
||||||
|
import loaders from 'src/app/loaders'
|
||||||
import Room from "src/partials/Room.svelte"
|
import Room from "src/partials/Room.svelte"
|
||||||
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"
|
||||||
@ -15,20 +17,37 @@
|
|||||||
let q = ""
|
let q = ""
|
||||||
let roomsCount = 0
|
let roomsCount = 0
|
||||||
|
|
||||||
const joined = liveQuery(() => db.rooms.where('joined').equals(1).toArray())
|
const rooms = liveQuery(async () => {
|
||||||
|
const [rooms, messages] = await Promise.all([
|
||||||
|
db.rooms.where('joined').equals(1).toArray(),
|
||||||
|
db.messages.toArray(),
|
||||||
|
])
|
||||||
|
|
||||||
|
const pubkeys = uniq(pluck('pubkey', messages))
|
||||||
|
await loaders.loadPeople(getRelays(), pubkeys)
|
||||||
|
|
||||||
|
return pubkeys
|
||||||
|
.map(k => ({type: 'npub', id: k, ...getPerson(k, true)}))
|
||||||
|
.concat(rooms.map(room => ({type: 'note', ...room})))
|
||||||
|
})
|
||||||
|
|
||||||
const search = liveQuery(async () => {
|
const search = liveQuery(async () => {
|
||||||
const rooms = await db.rooms.where('joined').equals(0).toArray()
|
const rooms = await db.rooms.where('joined').equals(0).toArray()
|
||||||
const nonTestRooms = reject(r => r.name.toLowerCase().includes('test'), rooms)
|
|
||||||
|
|
||||||
roomsCount = rooms.length
|
roomsCount = rooms.length
|
||||||
|
|
||||||
return fuzzy(rooms, {keys: ["name", "about"]})
|
return fuzzy(rooms, {keys: ["name", "about"]})
|
||||||
})
|
})
|
||||||
|
|
||||||
const createRoom = () => navigate(`/chat/new`)
|
const setRoom = ({type, id}) => {
|
||||||
|
if (type === 'npub') {
|
||||||
|
navigate(`/messages/${nip19.npubEncode(id)}`)
|
||||||
|
}
|
||||||
|
|
||||||
const setRoom = id => navigate(`/chat/${id}`)
|
if (type === 'note') {
|
||||||
|
navigate(`/chat/${nip19.noteEncode(id)}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const joinRoom = id => {
|
const joinRoom = id => {
|
||||||
db.rooms.where('id').equals(id).modify({joined: 1})
|
db.rooms.where('id').equals(id).modify({joined: 1})
|
||||||
@ -49,7 +68,7 @@
|
|||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $search}
|
{#if $rooms}
|
||||||
<Content>
|
<Content>
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<div class="flex gap-2 items-center">
|
<div class="flex gap-2 items-center">
|
||||||
@ -60,7 +79,7 @@
|
|||||||
<i class="fa-solid fa-plus" /> Create Room
|
<i class="fa-solid fa-plus" /> Create Room
|
||||||
</Anchor>
|
</Anchor>
|
||||||
</div>
|
</div>
|
||||||
{#each ($joined || []) as room (room.id)}
|
{#each ($rooms || []) as room (room.id)}
|
||||||
<Room joined {room} {setRoom} {joinRoom} {leaveRoom} />
|
<Room joined {room} {setRoom} {joinRoom} {leaveRoom} />
|
||||||
{: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>
|
||||||
|
@ -1,64 +1,20 @@
|
|||||||
<script>
|
<script>
|
||||||
import {onMount} from 'svelte'
|
|
||||||
import {liveQuery} from 'dexie'
|
import {liveQuery} from 'dexie'
|
||||||
import {fly} from 'svelte/transition'
|
import {pluck} from 'ramda'
|
||||||
import {navigate} from 'svelte-routing'
|
import {nip19} from 'nostr-tools'
|
||||||
import {prop, path as getPath, pluck, reverse, uniqBy, sortBy, last} from 'ramda'
|
import {now, batch} from 'src/util/misc'
|
||||||
import {formatTimestamp, now, createScroller, batch, Cursor} from 'src/util/misc'
|
import Channel from 'src/partials/Channel.svelte'
|
||||||
import Badge from 'src/partials/Badge.svelte'
|
import {getRelays, db, listen, load} from 'src/agent'
|
||||||
import Spinner from 'src/partials/Spinner.svelte'
|
import {modal} from 'src/app'
|
||||||
import {user, getPerson, getRelays, db, listen, load} from 'src/agent'
|
|
||||||
import {render, modal} from 'src/app'
|
|
||||||
import loaders from 'src/app/loaders'
|
import loaders from 'src/app/loaders'
|
||||||
import cmd from 'src/app/cmd'
|
import cmd from 'src/app/cmd'
|
||||||
|
|
||||||
export let roomId
|
export let entity
|
||||||
|
|
||||||
let textarea
|
let {data: roomId} = nip19.decode(entity)
|
||||||
let messages = []
|
|
||||||
let annotatedMessages = []
|
|
||||||
let showNewMessages = false
|
|
||||||
let room = liveQuery(() => db.rooms.where('id').equals(roomId).first())
|
let room = liveQuery(() => db.rooms.where('id').equals(roomId).first())
|
||||||
let cursor = new Cursor()
|
|
||||||
|
|
||||||
$: {
|
const listenForMessages = cb => listen(
|
||||||
// Group messages so we're only showing the person once per chunk
|
|
||||||
annotatedMessages = reverse(sortBy(prop('created_at'), uniqBy(prop('id'), messages)).reduce(
|
|
||||||
(mx, m) => {
|
|
||||||
const person = getPerson(m.pubkey, true)
|
|
||||||
const showPerson = person.pubkey !== getPath(['person', 'pubkey'], last(mx))
|
|
||||||
|
|
||||||
return mx.concat({...m, person, showPerson})
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
|
|
||||||
await cb()
|
|
||||||
|
|
||||||
if (shouldStick) {
|
|
||||||
const $li = getLastListItem()
|
|
||||||
|
|
||||||
if ($li) {
|
|
||||||
$li.scrollIntoView({behavior})
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
showNewMessages = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
if (!$user) {
|
|
||||||
return navigate('/login')
|
|
||||||
}
|
|
||||||
|
|
||||||
const sub = listen(
|
|
||||||
getRelays(),
|
getRelays(),
|
||||||
// Listen for updates to the room in case we didn't get them before
|
// Listen for updates to the room in case we didn't get them before
|
||||||
[{kinds: [40, 41], ids: [roomId]},
|
[{kinds: [40, 41], ids: [roomId]},
|
||||||
@ -68,126 +24,34 @@
|
|||||||
|
|
||||||
loaders.loadPeople(getRelays(), pluck('pubkey', events))
|
loaders.loadPeople(getRelays(), pluck('pubkey', events))
|
||||||
|
|
||||||
stickToBottom('smooth', () => {
|
cb(newMessages)
|
||||||
messages = messages.concat(newMessages)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
const scroller = createScroller(
|
const loadMessages = async ({until, limit}) => {
|
||||||
async () => {
|
|
||||||
const {until, limit} = cursor
|
|
||||||
const events = await load(getRelays(), {kinds: [42], '#e': [roomId], until, limit})
|
const events = await load(getRelays(), {kinds: [42], '#e': [roomId], until, limit})
|
||||||
|
|
||||||
if (events.length) {
|
if (events.length) {
|
||||||
cursor.onChunk(events)
|
|
||||||
|
|
||||||
await loaders.loadPeople(getRelays(), pluck('pubkey', events))
|
await loaders.loadPeople(getRelays(), pluck('pubkey', events))
|
||||||
|
|
||||||
stickToBottom('auto', () => {
|
|
||||||
messages = events.concat(messages)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{reverse: true}
|
|
||||||
)
|
|
||||||
|
|
||||||
return async () => {
|
return events
|
||||||
const {unsub} = await sub
|
|
||||||
|
|
||||||
scroller.stop()
|
|
||||||
unsub()
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
||||||
const edit = () => {
|
const editRoom = () => {
|
||||||
modal.set({form: 'room/edit', room: $room})
|
modal.set({form: 'room/edit', room: $room})
|
||||||
}
|
}
|
||||||
|
|
||||||
const sendMessage = async () => {
|
const sendMessage = content =>
|
||||||
const content = textarea.value.trim()
|
cmd.createChannelMessage(getRelays(), roomId, content)
|
||||||
|
|
||||||
if (content) {
|
|
||||||
textarea.value = ''
|
|
||||||
|
|
||||||
const event = await cmd.createMessage(getRelays(), roomId, content)
|
|
||||||
|
|
||||||
stickToBottom('smooth', () => {
|
|
||||||
messages = [event].concat(messages)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onKeyPress = e => {
|
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
|
||||||
e.preventDefault()
|
|
||||||
sendMessage()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window on:scroll={() => { showNewMessages = false}} />
|
<Channel
|
||||||
|
name={$room?.name}
|
||||||
<div class="flex gap-4 h-full">
|
about={$room?.about}
|
||||||
<div class="relative w-full">
|
picture={$room?.picture}
|
||||||
<div class="flex flex-col py-32">
|
{loadMessages}
|
||||||
<ul class="p-4 max-h-full flex-grow flex flex-col-reverse" name="messages">
|
{listenForMessages}
|
||||||
{#each annotatedMessages as m (m.id)}
|
{sendMessage}
|
||||||
<li in:fly={{y: 20}} class="py-1 flex flex-col gap-2">
|
{editRoom}
|
||||||
{#if m.showPerson}
|
/>
|
||||||
<div class="flex gap-4 items-center justify-between">
|
|
||||||
<Badge person={m.person} />
|
|
||||||
<p class="text-sm text-light">{formatTimestamp(m.created_at)}</p>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
<div class="ml-6 overflow-hidden text-ellipsis">
|
|
||||||
{@html render(m, {showEntire: true})}
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
{/each}
|
|
||||||
<Spinner>Looking for messages...</Spinner>
|
|
||||||
<div class="h-64" />
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="fixed z-10 top-0 pt-20 w-full p-4 border-b border-solid border-medium bg-dark flex gap-4">
|
|
||||||
<div
|
|
||||||
class="overflow-hidden w-12 h-12 rounded-full bg-cover bg-center shrink-0 border border-solid border-white"
|
|
||||||
style="background-image: url({$room?.picture})" />
|
|
||||||
<div class="w-full">
|
|
||||||
<div class="flex items-center justify-between w-full">
|
|
||||||
<div class="text-lg font-bold">{$room?.name || ''}</div>
|
|
||||||
{#if $room?.pubkey === $user?.pubkey}
|
|
||||||
<small class="cursor-pointer" on:click={edit}>
|
|
||||||
<i class="fa-solid fa-edit" /> Edit
|
|
||||||
</small>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
<div>{$room?.about || ''}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="fixed z-10 bottom-0 w-full flex bg-medium border-medium border-t border-solid border-dark">
|
|
||||||
<textarea
|
|
||||||
rows="4"
|
|
||||||
autofocus
|
|
||||||
placeholder="Type something..."
|
|
||||||
bind:this={textarea}
|
|
||||||
on:keypress={onKeyPress}
|
|
||||||
class="w-full p-2 text-white bg-medium
|
|
||||||
placeholder:text-light outline-0 resize-none" />
|
|
||||||
<div
|
|
||||||
on:click={sendMessage}
|
|
||||||
class="flex flex-col py-8 p-4 justify-center gap-2 border-l border-solid border-dark
|
|
||||||
hover:bg-accent transition-all cursor-pointer text-white ">
|
|
||||||
<i class="fa-solid fa-paper-plane fa-xl" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{#if showNewMessages}
|
|
||||||
<div class="fixed w-full flex justify-center bottom-32" transition:fly|local={{y: 20}}>
|
|
||||||
<div class="rounded-full bg-accent text-white py-2 px-4">
|
|
||||||
New messages found
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
57
src/routes/Messages.svelte
Normal file
57
src/routes/Messages.svelte
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
<script>
|
||||||
|
import {liveQuery} from 'dexie'
|
||||||
|
import {nip19} from 'nostr-tools'
|
||||||
|
import {assoc, propEq, mergeRight} from 'ramda'
|
||||||
|
import {now, batch} from 'src/util/misc'
|
||||||
|
import {personKinds} from 'src/util/nostr'
|
||||||
|
import Channel from 'src/partials/Channel.svelte'
|
||||||
|
import {getRelays, user, db, listen, load, keys} from 'src/agent'
|
||||||
|
import cmd from 'src/app/cmd'
|
||||||
|
|
||||||
|
export let entity
|
||||||
|
|
||||||
|
let crypt = keys.getCrypt()
|
||||||
|
let {data: pubkey} = nip19.decode(entity)
|
||||||
|
let person = liveQuery(() => db.people.get(pubkey))
|
||||||
|
let filters = [
|
||||||
|
{kinds: [4], authors: [$user.pubkey], '#p': [pubkey]},
|
||||||
|
{kinds: [4], authors: [pubkey], '#p': [$user.pubkey]}]
|
||||||
|
|
||||||
|
const decryptMessages = async messages => {
|
||||||
|
// Gotta do it in serial because of extension limitations
|
||||||
|
for (const message of messages) {
|
||||||
|
message.content = await crypt.decrypt(message.pubkey, message.content)
|
||||||
|
console.log(message.content)
|
||||||
|
}
|
||||||
|
|
||||||
|
return messages
|
||||||
|
}
|
||||||
|
|
||||||
|
const listenForMessages = cb => listen(
|
||||||
|
getRelays(),
|
||||||
|
[{kinds: personKinds, authors: [pubkey]},
|
||||||
|
...filters.map(assoc('since', now()))],
|
||||||
|
batch(300, events => {
|
||||||
|
return cb(decryptMessages(events.filter(propEq('kind', 4))))
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const loadMessages = async ({until, limit}) => {
|
||||||
|
const messages = await load(getRelays(), filters.map(mergeRight({until, limit})))
|
||||||
|
|
||||||
|
return await decryptMessages(messages)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const sendMessage = content =>
|
||||||
|
cmd.createDirectMessage(getRelays(), pubkey, content)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Channel
|
||||||
|
name={$person?.name}
|
||||||
|
about={$person?.about}
|
||||||
|
picture={$person?.picture}
|
||||||
|
{loadMessages}
|
||||||
|
{listenForMessages}
|
||||||
|
{sendMessage}
|
||||||
|
/>
|
@ -24,8 +24,6 @@
|
|||||||
export let activeTab
|
export let activeTab
|
||||||
export let relays = null
|
export let relays = null
|
||||||
|
|
||||||
const {canSign} = keys
|
|
||||||
|
|
||||||
let subs = []
|
let subs = []
|
||||||
let pubkey = nip19.decode(npub).data
|
let pubkey = nip19.decode(npub).data
|
||||||
let following = false
|
let following = false
|
||||||
@ -109,7 +107,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="whitespace-nowrap flex gap-4 items-center">
|
<div class="whitespace-nowrap flex gap-4 items-center">
|
||||||
{#if $user?.pubkey === pubkey && $canSign}
|
{#if $user?.pubkey === pubkey && keys.canSign()}
|
||||||
<a href="/profile" class="cursor-pointer text-sm">
|
<a href="/profile" class="cursor-pointer text-sm">
|
||||||
<i class="fa-solid fa-edit" /> Edit
|
<i class="fa-solid fa-edit" /> Edit
|
||||||
</a>
|
</a>
|
||||||
@ -117,7 +115,7 @@
|
|||||||
{#if $user && $user.pubkey !== pubkey}
|
{#if $user && $user.pubkey !== pubkey}
|
||||||
<i class="fa-solid fa-sliders cursor-pointer" on:click={openAdvanced} />
|
<i class="fa-solid fa-sliders cursor-pointer" on:click={openAdvanced} />
|
||||||
{/if}
|
{/if}
|
||||||
{#if $user?.petnames && $canSign}
|
{#if $user?.petnames && keys.canSign()}
|
||||||
<div class="flex flex-col items-end gap-2">
|
<div class="flex flex-col items-end gap-2">
|
||||||
{#if following}
|
{#if following}
|
||||||
<Button on:click={unfollow}>Unfollow</Button>
|
<Button on:click={unfollow}>Unfollow</Button>
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
import Content from "src/partials/Content.svelte"
|
import Content from "src/partials/Content.svelte"
|
||||||
import Textarea from "src/partials/Textarea.svelte"
|
import Textarea from "src/partials/Textarea.svelte"
|
||||||
import Button from "src/partials/Button.svelte"
|
import Button from "src/partials/Button.svelte"
|
||||||
import {getRelays, load} from 'src/agent'
|
import {getRelays, db} from 'src/agent'
|
||||||
import {toast} from "src/app"
|
import {toast} from "src/app"
|
||||||
import cmd from "src/app/cmd"
|
import cmd from "src/app/cmd"
|
||||||
|
|
||||||
@ -38,7 +38,6 @@
|
|||||||
? await cmd.updateRoom(getRelays(), room)
|
? await cmd.updateRoom(getRelays(), room)
|
||||||
: await cmd.createRoom(getRelays(), room)
|
: await cmd.createRoom(getRelays(), room)
|
||||||
|
|
||||||
console.log('here')
|
|
||||||
await db.rooms.where('id').equals(room.id).modify({joined: 1})
|
await db.rooms.where('id').equals(room.id).modify({joined: 1})
|
||||||
|
|
||||||
toast.show("info", `Your room has been ${room.id ? 'updated' : 'created'}!`)
|
toast.show("info", `Your room has been ${room.id ? 'updated' : 'created'}!`)
|
||||||
|
Loading…
Reference in New Issue
Block a user