mirror of
https://github.com/coracle-social/coracle.git
synced 2024-09-30 00:41:12 +00:00
Add analytics
This commit is contained in:
parent
1a9a31b45b
commit
ef3b7f2d3d
14
README.md
14
README.md
@ -42,6 +42,11 @@ If you like Coracle and want to support its development, you can donate sats via
|
|||||||
- [ ] Add Labs tab with cards for non-standard features
|
- [ ] Add Labs tab with cards for non-standard features
|
||||||
- Time travel - see events as of a date/time
|
- Time travel - see events as of a date/time
|
||||||
|
|
||||||
|
|
||||||
|
# Bugs
|
||||||
|
|
||||||
|
- [ ] Sync mentions box and in-reply mentions
|
||||||
|
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## Current
|
## Current
|
||||||
@ -53,7 +58,14 @@ If you like Coracle and want to support its development, you can donate sats via
|
|||||||
- [ ] Add petnames for channels
|
- [ ] Add petnames for channels
|
||||||
- [ ] Add back button
|
- [ ] Add back button
|
||||||
- [ ] Create Room -> open modal, choose dm or public room
|
- [ ] Create Room -> open modal, choose dm or public room
|
||||||
- [ ] Add DM button to profile pages
|
- [x] Add DM button to profile pages
|
||||||
|
- [ ] Linkify bech32 entities
|
||||||
|
- [ ] linkify dm page header
|
||||||
|
- [ ] Add lock/unlock icon to channel header
|
||||||
|
- [ ] Add notification for dms
|
||||||
|
- [ ] Default to network/following
|
||||||
|
- [ ] Add analytics
|
||||||
|
- [ ] Allow disabling error reporting/analytics
|
||||||
|
|
||||||
## 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 {find, pluck} from 'ramda'
|
import {find, nthArg, 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, messages} from "src/app"
|
import {modal, toast, settings, logUsage, 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'
|
||||||
@ -105,9 +105,21 @@
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const unsubHistory = globalHistory.listen(({location}) => {
|
||||||
|
if (!location.hash) {
|
||||||
|
// Remove identifying information, e.g. pubkeys, event ids, etc
|
||||||
|
const name = location.pathname.slice(1)
|
||||||
|
.replace(/(npub|nprofile|note|nevent)[^\/]+/g, (_, m) => `<${m}>`)
|
||||||
|
|
||||||
|
logUsage(btoa(['page', name].join(':')))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const unsubModal = modal.subscribe($modal => {
|
const unsubModal = modal.subscribe($modal => {
|
||||||
// Keep scroll position on body, but don't allow scrolling
|
// Keep scroll position on body, but don't allow scrolling
|
||||||
if ($modal) {
|
if ($modal) {
|
||||||
|
logUsage(btoa(['modal', $modal.type].join(':')))
|
||||||
|
|
||||||
// This is not idempotent, so don't duplicate it
|
// This is not idempotent, so don't duplicate it
|
||||||
if (document.body.style.position !== 'fixed') {
|
if (document.body.style.position !== 'fixed') {
|
||||||
scrollY = window.scrollY
|
scrollY = window.scrollY
|
||||||
@ -123,6 +135,7 @@
|
|||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
clearInterval(interval)
|
clearInterval(interval)
|
||||||
|
unsubHistory()
|
||||||
unsubModal()
|
unsubModal()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -260,7 +273,7 @@
|
|||||||
<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
|
||||||
items-center border border-dark shadow-2xl cursor-pointer"
|
items-center border border-dark shadow-2xl cursor-pointer"
|
||||||
on:click={() => modal.set({form: 'note/create'})}>
|
on:click={() => modal.set({type: 'note/create'})}>
|
||||||
<span class="fa-sold fa-plus fa-2xl" />
|
<span class="fa-sold fa-plus fa-2xl" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@ -268,27 +281,24 @@
|
|||||||
|
|
||||||
{#if $modal}
|
{#if $modal}
|
||||||
<Modal onEscape={closeModal}>
|
<Modal onEscape={closeModal}>
|
||||||
{#if $modal.note}
|
{#if $modal.type === 'note/detail'}
|
||||||
{#key $modal.note.id}
|
{#key $modal.note.id}
|
||||||
<NoteDetail {...$modal} />
|
<NoteDetail {...$modal} />
|
||||||
{/key}
|
{/key}
|
||||||
{:else if $modal.form === 'note/create'}
|
{:else if $modal.type === 'note/create'}
|
||||||
<NoteCreate />
|
<NoteCreate />
|
||||||
{:else if $modal.form === 'relay'}
|
{:else if $modal.type === 'relay/add'}
|
||||||
<AddRelay />
|
<AddRelay />
|
||||||
{:else if $modal.form === 'signUp'}
|
{:else if $modal.type === 'signUp'}
|
||||||
<SignUp />
|
<SignUp />
|
||||||
{:else if $modal.form === 'room/edit'}
|
{:else if $modal.type === 'room/edit'}
|
||||||
<ChatEdit {...$modal} />
|
<ChatEdit {...$modal} />
|
||||||
{:else if $modal.form === 'privkeyLogin'}
|
{:else if $modal.type === 'login/privkey'}
|
||||||
<PrivKeyLogin />
|
<PrivKeyLogin />
|
||||||
{:else if $modal.form === 'pubkeyLogin'}
|
{:else if $modal.type === 'login/pubkey'}
|
||||||
<PubKeyLogin />
|
<PubKeyLogin />
|
||||||
{:else if $modal.form === 'person/settings'}
|
{:else if $modal.type === 'person/settings'}
|
||||||
<PersonSettings />
|
<PersonSettings />
|
||||||
{:else if $modal.message}
|
|
||||||
<p class="text-white text-center py-12 pb-8">{$modal.message}</p>
|
|
||||||
<Spinner />
|
|
||||||
{/if}
|
{/if}
|
||||||
</Modal>
|
</Modal>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -8,10 +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(12).stores({
|
db.version(13).stores({
|
||||||
relays: '++url, name',
|
relays: '++url, name',
|
||||||
alerts: '++id, created_at',
|
alerts: '++id, created_at',
|
||||||
messages: '++id, pubkey',
|
messages: '++id, pubkey, recipient',
|
||||||
people: '++pubkey',
|
people: '++pubkey',
|
||||||
rooms: '++id, joined',
|
rooms: '++id, joined',
|
||||||
})
|
})
|
||||||
@ -157,7 +157,9 @@ const processRoomEvents = async events => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const processMessages = async events => {
|
const processMessages = async events => {
|
||||||
const messages = ensurePlural(events).filter(e => e.kind === 4)
|
const messages = ensurePlural(events)
|
||||||
|
.filter(e => e.kind === 4)
|
||||||
|
.map(e => ({...e, recipient: Tags.from(e).type("p").values().first()}))
|
||||||
|
|
||||||
if (messages.length > 0) {
|
if (messages.length > 0) {
|
||||||
await db.messages.bulkPut(messages)
|
await db.messages.bulkPut(messages)
|
||||||
|
@ -16,10 +16,12 @@ const setPrivateKey = _privkey => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const setPublicKey = _pubkey => {
|
const setPublicKey = _pubkey => {
|
||||||
signingFunction = async event => {
|
if (window.nostr) {
|
||||||
const {sig} = await window.nostr.signEvent(event)
|
signingFunction = async event => {
|
||||||
|
const {sig} = await window.nostr.signEvent(event)
|
||||||
|
|
||||||
return sig
|
return sig
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pubkey.set(_pubkey)
|
pubkey.set(_pubkey)
|
||||||
@ -59,6 +61,7 @@ const getCrypt = () => {
|
|||||||
? nip04.decrypt($privkey, pubkey, message)
|
? nip04.decrypt($privkey, pubkey, message)
|
||||||
: await window.nostr.nip04.decrypt(pubkey, message)
|
: await window.nostr.nip04.decrypt(pubkey, message)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
return `<Failed to decrypt message: ${e}>`
|
return `<Failed to decrypt message: ${e}>`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -6,13 +6,13 @@ import {renderContent} from 'src/util/html'
|
|||||||
import {Tags, displayPerson, findReplyId} from 'src/util/nostr'
|
import {Tags, displayPerson, findReplyId} from 'src/util/nostr'
|
||||||
import {user, people, getPerson, getRelays, keys} from 'src/agent'
|
import {user, people, getPerson, getRelays, keys} from 'src/agent'
|
||||||
import defaults from 'src/agent/defaults'
|
import defaults from 'src/agent/defaults'
|
||||||
import {toast, routes, modal, settings} from 'src/app/ui'
|
import {toast, routes, modal, settings, logUsage} 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 messages from 'src/app/messages'
|
||||||
import loaders from 'src/app/loaders'
|
import loaders from 'src/app/loaders'
|
||||||
|
|
||||||
export {toast, modal, settings, alerts, messages}
|
export {toast, modal, settings, alerts, messages, logUsage}
|
||||||
|
|
||||||
export const login = async ({privkey, pubkey}, usingExtension = false) => {
|
export const login = async ({privkey, pubkey}, usingExtension = false) => {
|
||||||
if (privkey) {
|
if (privkey) {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import {prop} from "ramda"
|
import Bugsnag from "@bugsnag/js"
|
||||||
|
import {prop, nthArg} from "ramda"
|
||||||
import {uuid} from "hurdak/lib/hurdak"
|
import {uuid} from "hurdak/lib/hurdak"
|
||||||
import {navigate} from "svelte-routing"
|
import {navigate} from "svelte-routing"
|
||||||
import {nip19} from 'nostr-tools'
|
import {nip19} from 'nostr-tools'
|
||||||
@ -54,6 +55,32 @@ export const modal = {
|
|||||||
// Settings, alerts, etc
|
// Settings, alerts, etc
|
||||||
|
|
||||||
export const settings = synced("coracle/settings", {
|
export const settings = synced("coracle/settings", {
|
||||||
|
reportAnalytics: true,
|
||||||
showLinkPreviews: true,
|
showLinkPreviews: true,
|
||||||
dufflepudUrl: import.meta.env.VITE_DUFFLEPUD_URL,
|
dufflepudUrl: import.meta.env.VITE_DUFFLEPUD_URL,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Wait for bugsnag to be started in main
|
||||||
|
setTimeout(() => {
|
||||||
|
Bugsnag.addOnError(event => {
|
||||||
|
if (window.location.host.startswith('localhost')) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!get(settings).reportAnalytics) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const session = Math.random().toString().slice(2)
|
||||||
|
|
||||||
|
export const logUsage = name => {
|
||||||
|
const {dufflepudUrl, reportAnalytics} = get(settings)
|
||||||
|
|
||||||
|
if (reportAnalytics) {
|
||||||
|
fetch(`${dufflepudUrl}/usage/${session}/${name}`, {method: 'post' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
import 'src/app.css'
|
import 'src/app.css'
|
||||||
|
|
||||||
import Bugsnag from "@bugsnag/js"
|
import Bugsnag from "@bugsnag/js"
|
||||||
import App from 'src/App.svelte'
|
|
||||||
|
|
||||||
// Annoying global always fails silently. Figure out an eslint rule instead
|
|
||||||
window.find = null
|
|
||||||
|
|
||||||
Bugsnag.start({apiKey: "2ea412feabfe14dc9849c6f0b4fa7003"})
|
Bugsnag.start({apiKey: "2ea412feabfe14dc9849c6f0b4fa7003"})
|
||||||
|
|
||||||
|
import App from 'src/App.svelte'
|
||||||
|
|
||||||
|
// Annoying global always fails silently. TODO: figure out an eslint rule instead
|
||||||
|
window.find = null
|
||||||
|
|
||||||
const app = new App({
|
const app = new App({
|
||||||
target: document.getElementById('app')
|
target: document.getElementById('app')
|
||||||
})
|
})
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
<script>
|
<script>
|
||||||
|
import cx from 'classnames'
|
||||||
import {onMount} from 'svelte'
|
import {onMount} from 'svelte'
|
||||||
import {fly} from 'svelte/transition'
|
import {fly} from 'svelte/transition'
|
||||||
import {navigate} from 'svelte-routing'
|
import {navigate} from 'svelte-routing'
|
||||||
import {prop, path as getPath, reverse, uniqBy, sortBy, last} from 'ramda'
|
import {prop, path as getPath, reverse, uniqBy, sortBy, last} from 'ramda'
|
||||||
import {formatTimestamp, createScroller, Cursor} from 'src/util/misc'
|
import {formatTimestamp, sleep, createScroller, Cursor} from 'src/util/misc'
|
||||||
import Badge from 'src/partials/Badge.svelte'
|
import Badge from 'src/partials/Badge.svelte'
|
||||||
import Spinner from 'src/partials/Spinner.svelte'
|
import Spinner from 'src/partials/Spinner.svelte'
|
||||||
import {user, getPerson} from 'src/agent'
|
import {user, getPerson} from 'src/agent'
|
||||||
@ -16,9 +17,12 @@
|
|||||||
export let listenForMessages
|
export let listenForMessages
|
||||||
export let sendMessage
|
export let sendMessage
|
||||||
export let editRoom = null
|
export let editRoom = null
|
||||||
|
export let type
|
||||||
|
console.log(editRoom)
|
||||||
|
|
||||||
let textarea
|
let textarea
|
||||||
let messages = []
|
let messages = []
|
||||||
|
let loading = sleep(10_000)
|
||||||
let annotatedMessages = []
|
let annotatedMessages = []
|
||||||
let showNewMessages = false
|
let showNewMessages = false
|
||||||
let cursor = new Cursor()
|
let cursor = new Cursor()
|
||||||
@ -62,6 +66,7 @@
|
|||||||
|
|
||||||
const sub = listenForMessages(
|
const sub = listenForMessages(
|
||||||
newMessages => stickToBottom('smooth', () => {
|
newMessages => stickToBottom('smooth', () => {
|
||||||
|
loading = sleep(10_000)
|
||||||
messages = messages.concat(newMessages)
|
messages = messages.concat(newMessages)
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
@ -74,6 +79,7 @@
|
|||||||
cursor.onChunk(events)
|
cursor.onChunk(events)
|
||||||
|
|
||||||
stickToBottom('auto', () => {
|
stickToBottom('auto', () => {
|
||||||
|
loading = sleep(10_000)
|
||||||
messages = events.concat(messages)
|
messages = events.concat(messages)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -115,39 +121,47 @@
|
|||||||
|
|
||||||
<div class="flex gap-4 h-full">
|
<div class="flex gap-4 h-full">
|
||||||
<div class="relative w-full">
|
<div class="relative w-full">
|
||||||
<div class="flex flex-col py-32">
|
<div class="flex flex-col py-32 h-full">
|
||||||
<ul class="p-4 max-h-full flex-grow flex flex-col-reverse" name="messages">
|
<ul class="p-4 h-full flex-grow flex flex-col-reverse justify-start" name="messages">
|
||||||
{#each annotatedMessages as m (m.id)}
|
{#each annotatedMessages as m (m.id)}
|
||||||
<li in:fly={{y: 20}} class="py-1 flex flex-col gap-2">
|
<li in:fly={{y: 20}} class="py-1 flex flex-col gap-2">
|
||||||
{#if m.showPerson}
|
{#if type === 'chat' && m.showPerson}
|
||||||
<div class="flex gap-4 items-center justify-between">
|
<div class="flex gap-4 items-center justify-between">
|
||||||
<Badge person={m.person} />
|
<Badge person={m.person} />
|
||||||
<p class="text-sm text-light">{formatTimestamp(m.created_at)}</p>
|
<p class="text-sm text-light">{formatTimestamp(m.created_at)}</p>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="ml-6 overflow-hidden text-ellipsis">
|
<div class={cx("overflow-hidden text-ellipsis", {
|
||||||
|
'ml-6': type === 'chat',
|
||||||
|
'rounded-2xl py-2 px-4': type === 'dm',
|
||||||
|
'ml-12 bg-light text-black rounded-br-none': type === 'dm' && m.person.pubkey === $user.pubkey,
|
||||||
|
'mr-12 bg-dark rounded-bl-none': type === 'dm' && m.person.pubkey !== $user.pubkey,
|
||||||
|
})}>
|
||||||
{@html render(m, {showEntire: true})}
|
{@html render(m, {showEntire: true})}
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
|
{#await loading}
|
||||||
<Spinner>Looking for messages...</Spinner>
|
<Spinner>Looking for messages...</Spinner>
|
||||||
<div class="h-48" />
|
{/await}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</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="fixed z-10 top-16 w-full lg:-ml-56 lg:pl-56 border-b border-solid border-medium bg-dark">
|
||||||
<div
|
<div class="p-4 flex gap-4">
|
||||||
class="overflow-hidden w-12 h-12 rounded-full bg-cover bg-center shrink-0 border border-solid border-white"
|
<div
|
||||||
style="background-image: url({picture})" />
|
class="overflow-hidden w-12 h-12 rounded-full bg-cover bg-center shrink-0 border border-solid border-white"
|
||||||
<div class="w-full">
|
style="background-image: url({picture})" />
|
||||||
<div class="flex items-center justify-between w-full">
|
<div class="w-full">
|
||||||
<div class="text-lg font-bold">{name || ''}</div>
|
<div class="flex items-center justify-between w-full">
|
||||||
{#if editRoom}
|
<div class="text-lg font-bold">{name || ''}</div>
|
||||||
<small class="cursor-pointer" on:click={editRoom}>
|
{#if editRoom}
|
||||||
<i class="fa-solid fa-edit" /> Edit
|
<small class="cursor-pointer" on:click={editRoom}>
|
||||||
</small>
|
<i class="fa-solid fa-edit" /> Edit
|
||||||
{/if}
|
</small>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div>{about || ''}</div>
|
||||||
</div>
|
</div>
|
||||||
<div>{about || ''}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="fixed z-10 bottom-0 w-full flex bg-medium border-medium border-t border-solid border-dark">
|
<div class="fixed z-10 bottom-0 w-full flex bg-medium border-medium border-t border-solid border-dark">
|
||||||
|
@ -27,7 +27,7 @@
|
|||||||
<div
|
<div
|
||||||
class="py-2 px-3 flex flex-col gap-2 text-white cursor-pointer transition-all
|
class="py-2 px-3 flex flex-col gap-2 text-white cursor-pointer transition-all
|
||||||
border border-solid border-black hover:border-medium hover:bg-dark"
|
border border-solid border-black hover:border-medium hover:bg-dark"
|
||||||
on:click={() => modal.set({note})}>
|
on:click={() => modal.set({type: 'note', note})}>
|
||||||
<div class="flex gap-2 items-center justify-between relative">
|
<div class="flex gap-2 items-center justify-between relative">
|
||||||
<span class="cursor-pointer" on:click={openPopover}>
|
<span class="cursor-pointer" on:click={openPopover}>
|
||||||
{quantify(note.people.length, 'person', 'people')} liked your note.
|
{quantify(note.people.length, 'person', 'people')} liked your note.
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
import cx from 'classnames'
|
import cx from 'classnames'
|
||||||
import extractUrls from 'extract-urls'
|
import extractUrls from 'extract-urls'
|
||||||
import {nip19} from 'nostr-tools'
|
import {nip19} from 'nostr-tools'
|
||||||
import {whereEq, uniq, pluck, reject, propEq, find} from 'ramda'
|
import {whereEq, without, uniq, pluck, reject, propEq, find} from 'ramda'
|
||||||
import {slide} from 'svelte/transition'
|
import {slide} from 'svelte/transition'
|
||||||
import {navigate} from 'svelte-routing'
|
import {navigate} from 'svelte-routing'
|
||||||
import {quantify} from 'hurdak/lib/hurdak'
|
import {quantify} from 'hurdak/lib/hurdak'
|
||||||
@ -24,7 +24,7 @@
|
|||||||
export let invertColors = false
|
export let invertColors = false
|
||||||
|
|
||||||
let reply = null
|
let reply = null
|
||||||
let replyMentions = Tags.from(note).type("p").values().all()
|
let replyMentions = without([$user.pubkey], Tags.from(note).type("p").values().all())
|
||||||
|
|
||||||
const links = $settings.showLinkPreviews ? extractUrls(note.content) || [] : null
|
const links = $settings.showLinkPreviews ? extractUrls(note.content) || [] : null
|
||||||
const showEntire = anchorId === note.id
|
const showEntire = anchorId === note.id
|
||||||
@ -45,7 +45,7 @@
|
|||||||
|
|
||||||
const onClick = e => {
|
const onClick = e => {
|
||||||
if (!['I'].includes(e.target.tagName) && !e.target.closest('a')) {
|
if (!['I'].includes(e.target.tagName) && !e.target.closest('a')) {
|
||||||
modal.set({note, relays})
|
modal.set({type: 'note', note, relays})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,7 +53,7 @@
|
|||||||
const [id, url] = findReply(note).slice(1)
|
const [id, url] = findReply(note).slice(1)
|
||||||
const relays = getEventRelays(note).concat({url})
|
const relays = getEventRelays(note).concat({url})
|
||||||
|
|
||||||
modal.set({note: {id}, relays})
|
modal.set({type: 'note', note: {id}, relays})
|
||||||
}
|
}
|
||||||
|
|
||||||
const react = async content => {
|
const react = async content => {
|
||||||
@ -93,7 +93,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const removeMention = pubkey => {
|
const removeMention = pubkey => {
|
||||||
replyMentions = reject(p => p === pubkey, replyMentions)
|
replyMentions = without([pubkey], replyMentions)
|
||||||
}
|
}
|
||||||
|
|
||||||
const resetReply = () => {
|
const resetReply = () => {
|
||||||
@ -195,6 +195,9 @@
|
|||||||
</div>
|
</div>
|
||||||
{#if replyMentions.length > 0}
|
{#if replyMentions.length > 0}
|
||||||
<div class="text-white text-sm p-2 rounded-b border-t-0 border border-solid border-medium">
|
<div class="text-white text-sm p-2 rounded-b border-t-0 border border-solid border-medium">
|
||||||
|
<div class="inline-block border-r border-solid border-medium pl-1 pr-3 mr-2">
|
||||||
|
<i class="fa fa-at" />
|
||||||
|
</div>
|
||||||
{#each replyMentions as p}
|
{#each replyMentions as p}
|
||||||
<div class="inline-block py-1 px-2 mr-1 mb-2 rounded-full border border-solid border-light">
|
<div class="inline-block py-1 px-2 mr-1 mb-2 rounded-full border border-solid border-light">
|
||||||
<i class="fa fa-times cursor-pointer" on:click|stopPropagation={() => removeMention(p)} />
|
<i class="fa fa-times cursor-pointer" on:click|stopPropagation={() => removeMention(p)} />
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
<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">
|
<div class="flex gap-2 items-center">
|
||||||
{#if room.type === 'private'}
|
{#if room.type === 'npub'}
|
||||||
<i class="fa fa-lock text-light" />
|
<i class="fa fa-lock text-light" />
|
||||||
<h2 class="text-lg">{displayPerson(room)}</h2>
|
<h2 class="text-lg">{displayPerson(room)}</h2>
|
||||||
{:else}
|
{:else}
|
||||||
@ -29,7 +29,7 @@
|
|||||||
<h2 class="text-lg">{room.name}</h2>
|
<h2 class="text-lg">{room.name}</h2>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if room.type === 'public'}
|
{#if room.type === 'note'}
|
||||||
{#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" />
|
||||||
|
@ -75,7 +75,7 @@
|
|||||||
<i class="fa fa-server fa-lg" />
|
<i class="fa fa-server fa-lg" />
|
||||||
<h2 class="staatliches text-2xl">Your rooms</h2>
|
<h2 class="staatliches text-2xl">Your rooms</h2>
|
||||||
</div>
|
</div>
|
||||||
<Anchor type="button-accent" on:click={() => modal.set({form: 'room/edit'})}>
|
<Anchor type="button-accent" on:click={() => modal.set({type: 'room/edit'})}>
|
||||||
<i class="fa-solid fa-plus" /> Create Room
|
<i class="fa-solid fa-plus" /> Create Room
|
||||||
</Anchor>
|
</Anchor>
|
||||||
</div>
|
</div>
|
||||||
@ -103,4 +103,6 @@
|
|||||||
Showing {Math.min(50, roomsCount)} of {roomsCount} known rooms
|
Showing {Math.min(50, roomsCount)} of {roomsCount} known rooms
|
||||||
</small>
|
</small>
|
||||||
</Content>
|
</Content>
|
||||||
|
{:else}
|
||||||
|
<Spinner />
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -39,7 +39,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const editRoom = () => {
|
const editRoom = () => {
|
||||||
modal.set({form: 'room/edit', room: $room})
|
modal.set({type: 'room/edit', room: $room})
|
||||||
}
|
}
|
||||||
|
|
||||||
const sendMessage = content =>
|
const sendMessage = content =>
|
||||||
@ -47,6 +47,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Channel
|
<Channel
|
||||||
|
type="chat"
|
||||||
name={$room?.name}
|
name={$room?.name}
|
||||||
about={$room?.about}
|
about={$room?.about}
|
||||||
picture={$room?.picture}
|
picture={$room?.picture}
|
||||||
|
@ -11,16 +11,16 @@
|
|||||||
if (window.nostr) {
|
if (window.nostr) {
|
||||||
await login({pubkey: await window.nostr.getPublicKey()}, true)
|
await login({pubkey: await window.nostr.getPublicKey()}, true)
|
||||||
} else {
|
} else {
|
||||||
modal.set({form: 'privkeyLogin'})
|
modal.set({type: 'login/privkey'})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const signUp = () => {
|
const signUp = () => {
|
||||||
modal.set({form: 'signUp'})
|
modal.set({type: 'signUp'})
|
||||||
}
|
}
|
||||||
|
|
||||||
const pubkeyLogIn = () => {
|
const pubkeyLogIn = () => {
|
||||||
modal.set({form: 'pubkeyLogin'})
|
modal.set({type: 'login/pubkey'})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
<script>
|
<script>
|
||||||
import {liveQuery} from 'dexie'
|
import {liveQuery} from 'dexie'
|
||||||
import {nip19} from 'nostr-tools'
|
import {nip19} from 'nostr-tools'
|
||||||
import {assoc, propEq, mergeRight} from 'ramda'
|
import {sortBy, pluck} from 'ramda'
|
||||||
import {now, batch} from 'src/util/misc'
|
|
||||||
import {personKinds} from 'src/util/nostr'
|
import {personKinds} from 'src/util/nostr'
|
||||||
|
import {batch} from 'src/util/misc'
|
||||||
import Channel from 'src/partials/Channel.svelte'
|
import Channel from 'src/partials/Channel.svelte'
|
||||||
import {getRelays, user, db, listen, load, keys} from 'src/agent'
|
import {getRelays, user, db, listen, keys} from 'src/agent'
|
||||||
import cmd from 'src/app/cmd'
|
import cmd from 'src/app/cmd'
|
||||||
|
|
||||||
export let entity
|
export let entity
|
||||||
@ -13,41 +13,52 @@
|
|||||||
let crypt = keys.getCrypt()
|
let crypt = keys.getCrypt()
|
||||||
let {data: pubkey} = nip19.decode(entity)
|
let {data: pubkey} = nip19.decode(entity)
|
||||||
let person = liveQuery(() => db.people.get(pubkey))
|
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 => {
|
const decryptMessages = async events => {
|
||||||
// Gotta do it in serial because of extension limitations
|
// Gotta do it in serial because of extension limitations
|
||||||
for (const message of messages) {
|
for (const event of events) {
|
||||||
message.content = await crypt.decrypt(message.pubkey, message.content)
|
const key = event.pubkey === pubkey ? pubkey : event.recipient
|
||||||
console.log(message.content)
|
|
||||||
|
event.content = await crypt.decrypt(key, event.content)
|
||||||
}
|
}
|
||||||
|
|
||||||
return messages
|
return events
|
||||||
}
|
}
|
||||||
|
|
||||||
const listenForMessages = cb => listen(
|
const listenForMessages = cb => listen(
|
||||||
getRelays(),
|
getRelays(),
|
||||||
[{kinds: personKinds, authors: [pubkey]},
|
[{kinds: personKinds, authors: [pubkey]},
|
||||||
...filters.map(assoc('since', now()))],
|
{kinds: [4], authors: [$user.pubkey], '#p': [pubkey]},
|
||||||
batch(300, events => {
|
{kinds: [4], authors: [pubkey], '#p': [$user.pubkey]}],
|
||||||
return cb(decryptMessages(events.filter(propEq('kind', 4))))
|
batch(300, async events => {
|
||||||
|
// Reload from db since we annotate messages there
|
||||||
|
const messageIds = pluck('id', events.filter(e => e.kind === 4))
|
||||||
|
const messages = await db.messages.where('id').anyOf(messageIds).toArray()
|
||||||
|
|
||||||
|
cb(await decryptMessages(messages))
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
const loadMessages = async ({until, limit}) => {
|
const loadMessages = async ({until, limit}) => {
|
||||||
const messages = await load(getRelays(), filters.map(mergeRight({until, limit})))
|
const fromThem = await db.messages.where('pubkey').equals(pubkey).toArray()
|
||||||
|
const toThem = await db.messages.where('recipient').equals(pubkey).toArray()
|
||||||
|
const events = fromThem.concat(toThem).filter(e => e.created_at < until)
|
||||||
|
const messages = sortBy(e => -e.created_at, events).slice(0, limit)
|
||||||
|
|
||||||
return await decryptMessages(messages)
|
return await decryptMessages(messages)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sendMessage = async content => {
|
||||||
|
const cyphertext = await crypt.encrypt(pubkey, content)
|
||||||
|
const event = await cmd.createDirectMessage(getRelays(), pubkey, cyphertext)
|
||||||
|
|
||||||
const sendMessage = content =>
|
// Return unencrypted content so we can display it immediately
|
||||||
cmd.createDirectMessage(getRelays(), pubkey, content)
|
return {...event, content}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Channel
|
<Channel
|
||||||
|
type="dm"
|
||||||
name={$person?.name}
|
name={$person?.name}
|
||||||
about={$person?.about}
|
about={$person?.about}
|
||||||
picture={$person?.picture}
|
picture={$person?.picture}
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
import {displayPerson} from 'src/util/nostr'
|
import {displayPerson} from 'src/util/nostr'
|
||||||
import Tabs from "src/partials/Tabs.svelte"
|
import Tabs from "src/partials/Tabs.svelte"
|
||||||
import Content from "src/partials/Content.svelte"
|
import Content from "src/partials/Content.svelte"
|
||||||
|
import Anchor from "src/partials/Anchor.svelte"
|
||||||
import Button from "src/partials/Button.svelte"
|
import Button from "src/partials/Button.svelte"
|
||||||
import Spinner from "src/partials/Spinner.svelte"
|
import Spinner from "src/partials/Spinner.svelte"
|
||||||
import Notes from "src/views/person/Notes.svelte"
|
import Notes from "src/views/person/Notes.svelte"
|
||||||
@ -76,7 +77,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const openAdvanced = () => {
|
const openAdvanced = () => {
|
||||||
modal.set({form: 'person/settings', person})
|
modal.set({type: 'person/settings', person})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -93,8 +94,8 @@
|
|||||||
<div
|
<div
|
||||||
class="overflow-hidden w-32 h-32 rounded-full bg-cover bg-center shrink-0 border border-solid border-white"
|
class="overflow-hidden w-32 h-32 rounded-full bg-cover bg-center shrink-0 border border-solid border-white"
|
||||||
style="background-image: url({person.picture})" />
|
style="background-image: url({person.picture})" />
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4 flex-grow">
|
||||||
<div class="flex items-start gap-4">
|
<div class="flex justify-between items-center gap-4">
|
||||||
<div class="flex-grow flex flex-col gap-2">
|
<div class="flex-grow flex flex-col gap-2">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<h1 class="text-2xl">{displayPerson(person)}</h1>
|
<h1 class="text-2xl">{displayPerson(person)}</h1>
|
||||||
@ -108,21 +109,21 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="whitespace-nowrap flex gap-4 items-center">
|
<div class="whitespace-nowrap flex gap-4 items-center">
|
||||||
{#if $user?.pubkey === pubkey && keys.canSign()}
|
{#if $user?.pubkey === pubkey && keys.canSign()}
|
||||||
<a href="/profile" class="cursor-pointer text-sm">
|
<Anchor type="button" href="/profile">
|
||||||
<i class="fa-solid fa-edit" /> Edit
|
<i class="fa-solid fa-edit" /> Edit profile
|
||||||
</a>
|
</Anchor>
|
||||||
{/if}
|
{:else if $user && keys.canSign()}
|
||||||
{#if $user && $user.pubkey !== pubkey}
|
<Anchor type="button" on:click={openAdvanced}>
|
||||||
<i class="fa-solid fa-sliders cursor-pointer" on:click={openAdvanced} />
|
<i class="fa-solid fa-sliders" />
|
||||||
{/if}
|
</Anchor>
|
||||||
{#if $user?.petnames && keys.canSign()}
|
<Anchor type="button" href={`/messages/${npub}`}>
|
||||||
<div class="flex flex-col items-end gap-2">
|
<i class="fa-solid fa-envelope" />
|
||||||
|
</Anchor>
|
||||||
{#if following}
|
{#if following}
|
||||||
<Button on:click={unfollow}>Unfollow</Button>
|
<Button on:click={unfollow}>Unfollow</Button>
|
||||||
{:else}
|
{:else}
|
||||||
<Button on:click={follow}>Follow</Button>
|
<Button on:click={follow}>Follow</Button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -73,7 +73,7 @@
|
|||||||
<i class="fa fa-server fa-lg" />
|
<i class="fa fa-server fa-lg" />
|
||||||
<h2 class="staatliches text-2xl">Your relays</h2>
|
<h2 class="staatliches text-2xl">Your relays</h2>
|
||||||
</div>
|
</div>
|
||||||
<Anchor type="button" on:click={() => modal.set({form: 'relay', url: q})}>
|
<Anchor type="button" on:click={() => modal.set({type: 'relay/add', url: q})}>
|
||||||
<i class="fa-solid fa-plus" /> Add Relay
|
<i class="fa-solid fa-plus" /> Add Relay
|
||||||
</Anchor>
|
</Anchor>
|
||||||
</div>
|
</div>
|
||||||
|
@ -56,6 +56,16 @@
|
|||||||
hosting images and loading link previews.
|
hosting images and loading link previews.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<div class="flex gap-2 items-center">
|
||||||
|
<strong>Report Errors and Analytics</strong>
|
||||||
|
<Toggle bind:value={values.reportAnalytics} />
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-light">
|
||||||
|
Keep this enabled if you would like the Coracle developers to be able to
|
||||||
|
know what features are used, and to diagnose and fix bugs.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<Button type="submit" class="text-center">Save</Button>
|
<Button type="submit" class="text-center">Save</Button>
|
||||||
</div>
|
</div>
|
||||||
</Content>
|
</Content>
|
||||||
|
@ -1,14 +1,13 @@
|
|||||||
<script>
|
<script>
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import {fly} from 'svelte/transition'
|
import {fly} from 'svelte/transition'
|
||||||
import {navigate} from "svelte-routing"
|
|
||||||
import {stripExifData} from "src/util/html"
|
import {stripExifData} from "src/util/html"
|
||||||
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 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, db} from 'src/agent'
|
import {getRelays, db} from 'src/agent'
|
||||||
import {toast} from "src/app"
|
import {toast, modal} from "src/app"
|
||||||
import cmd from "src/app/cmd"
|
import cmd from "src/app/cmd"
|
||||||
|
|
||||||
export let room = {}
|
export let room = {}
|
||||||
@ -34,7 +33,7 @@
|
|||||||
if (!room.name) {
|
if (!room.name) {
|
||||||
toast.show("error", "Please enter a name for your room.")
|
toast.show("error", "Please enter a name for your room.")
|
||||||
} else {
|
} else {
|
||||||
const event = room.id
|
room.id
|
||||||
? await cmd.updateRoom(getRelays(), room)
|
? await cmd.updateRoom(getRelays(), room)
|
||||||
: await cmd.createRoom(getRelays(), room)
|
: await cmd.createRoom(getRelays(), room)
|
||||||
|
|
||||||
@ -42,7 +41,7 @@
|
|||||||
|
|
||||||
toast.show("info", `Your room has been ${room.id ? 'updated' : 'created'}!`)
|
toast.show("info", `Your room has been ${room.id ? 'updated' : 'created'}!`)
|
||||||
|
|
||||||
navigate(`/chat/${room.id || event.id}`)
|
modal.set(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -9,8 +9,9 @@
|
|||||||
import {modal} from 'src/app'
|
import {modal} from 'src/app'
|
||||||
import cmd from 'src/app/cmd'
|
import cmd from 'src/app/cmd'
|
||||||
|
|
||||||
|
const muffle = $user.muffle || []
|
||||||
const muffleOptions = ['Never', 'Sometimes', 'Often', 'Always']
|
const muffleOptions = ['Never', 'Sometimes', 'Often', 'Always']
|
||||||
const muffleValue = parseFloat(first($user.muffle.filter(t => t[1] === $modal.person.pubkey).map(last)) || 1)
|
const muffleValue = parseFloat(first(muffle.filter(t => t[1] === $modal.person.pubkey).map(last)) || 1)
|
||||||
|
|
||||||
const values = {
|
const values = {
|
||||||
// Scale up to integers for each choice we have
|
// Scale up to integers for each choice we have
|
||||||
@ -22,7 +23,7 @@
|
|||||||
|
|
||||||
// Scale back down to a decimal based on string value
|
// Scale back down to a decimal based on string value
|
||||||
const muffleValue = muffleOptions.indexOf(values.muffle) / 3
|
const muffleValue = muffleOptions.indexOf(values.muffle) / 3
|
||||||
const muffleTags = $user.muffle
|
const muffleTags = muffle
|
||||||
.filter(t => t[1] !== $modal.person.pubkey)
|
.filter(t => t[1] !== $modal.person.pubkey)
|
||||||
.concat([["p", $modal.person.pubkey, muffleValue.toString()]])
|
.concat([["p", $modal.person.pubkey, muffleValue.toString()]])
|
||||||
.filter(t => last(t) !== "1")
|
.filter(t => last(t) !== "1")
|
||||||
|
Loading…
Reference in New Issue
Block a user