mirror of
https://github.com/coracle-social/coracle.git
synced 2024-09-28 16:00:52 +00:00
Compare commits
12 Commits
88cd8cee2b
...
a23f71d307
Author | SHA1 | Date | |
---|---|---|---|
|
a23f71d307 | ||
|
1dccb3e95c | ||
|
c15453b996 | ||
|
a94fabdffb | ||
|
0d8d832e87 | ||
|
d87e8ca363 | ||
|
223a1550f1 | ||
|
e9b24d5f1d | ||
|
b132867967 | ||
|
dadf27d058 | ||
|
2f0a5def6f | ||
|
5946ece673 |
@ -5,6 +5,11 @@
|
||||
- [x] Show toast when offline
|
||||
- [x] Use new indexeddb wrapper
|
||||
- [x] Add `k` tag to deletions
|
||||
- [x] Allow users to choose where to publish their profile when using a white-labeled instance
|
||||
- [x] Add "open with" link populated by nip 89 handlers
|
||||
- [x] Fix several community and calendar related bugs
|
||||
- [x] Add reports using tagr-bot
|
||||
- [x] Open links to coracle in same tab
|
||||
|
||||
# 0.4.6
|
||||
|
||||
|
@ -72,7 +72,7 @@ Coracle is intended to be fully white-labeled by groups of various kinds. The fo
|
||||
- `VITE_DUFFLEPUD_URL` is a [Dufflepud](https://github.com/coracle-social/dufflepud) instance url, which helps Coracle with things like link previews and image uploads.
|
||||
- `VITE_PLATFORM_ZAP_SPLIT` is a decimal between 0 and 1 defining the default zap split percent.
|
||||
- `VITE_PLATFORM_PUBKEY` is the pubkey of the platform owner. This gets zapped when using the platform zap split.
|
||||
- `VITE_FORCE_GROUP` is an optional `kind:34550` or `kind:35834` address. If provided, the home page of Coracle will be the home page for the group, and most views will be filtered down to the group's scope.
|
||||
- `VITE_FORCE_GROUP` is an optional `kind:34550` or `kind:35834` address. If provided, the home page of Coracle will be the home page for the group, and most views will be filtered down to the group's scope. For user privacy, `VITE_PLATFORM_RELAYS` should also be set when using `VITE_FORCE_GROUP`.
|
||||
- `VITE_PLATFORM_RELAYS` is an optional comma-separated list of relay urls to use for feeds. If provided, most UI components related to relay selection will be hidden from the user.
|
||||
- `VITE_ENABLE_ZAPS` can be set to `false` to disable zaps.
|
||||
- `VITE_APP_NAME` is the app's name.
|
||||
|
BIN
package-lock.json
generated
BIN
package-lock.json
generated
Binary file not shown.
@ -56,10 +56,11 @@
|
||||
"@getalby/bitcoin-connect": "^3.2.2",
|
||||
"@scure/base": "^1.1.6",
|
||||
"@welshman/content": "^0.0.5",
|
||||
"@welshman/feeds": "^0.0.10",
|
||||
"@welshman/feeds": "^0.0.11",
|
||||
"@welshman/lib": "^0.0.9",
|
||||
"@welshman/net": "^0.0.12",
|
||||
"@welshman/util": "^0.0.13",
|
||||
"@welshman/net": "^0.0.13",
|
||||
"@welshman/util": "^0.0.14",
|
||||
"bowser": "^2.11.0",
|
||||
"classnames": "^2.5.1",
|
||||
"compressorjs": "^1.2.1",
|
||||
"date-picker-svelte": "^2.12.0",
|
||||
|
@ -221,6 +221,11 @@
|
||||
entity: asNote,
|
||||
},
|
||||
})
|
||||
router.register("/notes/:entity/report", import("src/app/views/ReportCreate.svelte"), {
|
||||
serializers: {
|
||||
entity: asNote,
|
||||
},
|
||||
})
|
||||
router.register("/notes/:entity/thread", import("src/app/views/ThreadDetail.svelte"), {
|
||||
serializers: {
|
||||
entity: asNote,
|
||||
@ -424,7 +429,11 @@
|
||||
|
||||
// App data boostrap and relay meta fetching
|
||||
|
||||
storage.ready.then(() => {
|
||||
storage.ready.then(async () => {
|
||||
// Our stores are throttled by 300, so wait until they're populated
|
||||
// before loading app data
|
||||
await lib.sleep(350)
|
||||
|
||||
loadAppData()
|
||||
|
||||
if ($session) {
|
||||
|
@ -79,7 +79,7 @@
|
||||
</Anchor>
|
||||
<MenuDesktopItem path="/notes" isActive={isFeedPage || isListPage}>Feeds</MenuDesktopItem>
|
||||
{#if !$env.FORCE_GROUP && $env.PLATFORM_RELAYS.length === 0}
|
||||
<MenuDesktopItem path="/settings/relays" isActive={$page.path.startsWith("/settings/relays")}>
|
||||
<MenuDesktopItem path="/settings/relays" isActive={$page?.path.startsWith("/settings/relays")}>
|
||||
<div class="relative inline-block">
|
||||
Relays
|
||||
{#if $slowConnections.length > 0}
|
||||
@ -91,7 +91,7 @@
|
||||
<MenuDesktopItem
|
||||
path="/notifications"
|
||||
disabled={!$canSign}
|
||||
isActive={$page.path.startsWith("/notifications")}>
|
||||
isActive={$page?.path.startsWith("/notifications")}>
|
||||
<div class="relative inline-block">
|
||||
Notifications
|
||||
{#if $hasNewNotifications}
|
||||
@ -102,7 +102,7 @@
|
||||
<MenuDesktopItem
|
||||
path="/channels"
|
||||
disabled={!$canSign}
|
||||
isActive={$page.path.startsWith("/channels")}>
|
||||
isActive={$page?.path.startsWith("/channels")}>
|
||||
<div class="relative inline-block">
|
||||
Messages
|
||||
{#if $hasNewMessages}
|
||||
@ -110,14 +110,14 @@
|
||||
{/if}
|
||||
</div>
|
||||
</MenuDesktopItem>
|
||||
<MenuDesktopItem path="/events" isActive={$page.path.startsWith("/events")}
|
||||
<MenuDesktopItem path="/events" isActive={$page?.path.startsWith("/events")}
|
||||
>Calendar</MenuDesktopItem>
|
||||
{#if $env.ENABLE_MARKET}
|
||||
<MenuDesktopItem path="/listings" isActive={$page.path.startsWith("/listings")}
|
||||
<MenuDesktopItem path="/listings" isActive={$page?.path.startsWith("/listings")}
|
||||
>Market</MenuDesktopItem>
|
||||
{/if}
|
||||
{#if !$env.FORCE_GROUP}
|
||||
<MenuDesktopItem path="/groups" isActive={$page.path.startsWith("/groups")}
|
||||
<MenuDesktopItem path="/groups" isActive={$page?.path.startsWith("/groups")}
|
||||
>Groups</MenuDesktopItem>
|
||||
{/if}
|
||||
<FlexColumn small class="absolute bottom-0 w-72">
|
||||
@ -125,7 +125,7 @@
|
||||
class="staatliches px-8 text-tinted-400 hover:text-tinted-100"
|
||||
on:click={() => setSubMenu("settings")}>Settings</Anchor>
|
||||
<div class="staatliches block flex h-8 gap-2 px-8 text-tinted-500">
|
||||
<Anchor external class="hover:text-tinted-100" href="/about">About</Anchor> /
|
||||
<Anchor class="hover:text-tinted-100" href="/about">About</Anchor> /
|
||||
<Anchor external class="hover:text-tinted-100" href="/terms.html">Terms</Anchor> /
|
||||
<Anchor external class="hover:text-tinted-100" href="/privacy.html">Privacy</Anchor>
|
||||
</div>
|
||||
|
@ -1,31 +1,41 @@
|
||||
<script lang="ts">
|
||||
import {fromPairs} from "ramda"
|
||||
import {batch} from "hurdak"
|
||||
import {onMount, onDestroy} from "svelte"
|
||||
import {writable} from "@welshman/lib"
|
||||
import {onMount} from "svelte"
|
||||
import {fromPairs} from "@welshman/lib"
|
||||
import {getAddress, getReplyFilters} from "@welshman/util"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {feedFromFilter} from "@welshman/feeds"
|
||||
import Calendar from "@event-calendar/core"
|
||||
import DayGrid from "@event-calendar/day-grid"
|
||||
import Interaction from "@event-calendar/interaction"
|
||||
import {secondsToDate} from "src/util/misc"
|
||||
import {themeColors} from "src/partials/state"
|
||||
import Anchor from "src/partials/Anchor.svelte"
|
||||
import {
|
||||
hints,
|
||||
load,
|
||||
pubkey,
|
||||
canSign,
|
||||
repository,
|
||||
subscribe,
|
||||
feedLoader,
|
||||
getFilterSelections,
|
||||
} from "src/engine"
|
||||
import {hints, load, pubkey, canSign, loadAll, deriveEventsMapped} from "src/engine"
|
||||
import {router} from "src/app/util/router"
|
||||
|
||||
export let feed
|
||||
export let filter
|
||||
export let group = null
|
||||
|
||||
const calendarEvents = deriveEventsMapped({
|
||||
filters: [filter],
|
||||
itemToEvent: (item: any) => item.event,
|
||||
eventToItem: (event: TrustedEvent) => {
|
||||
const meta = fromPairs(event.tags)
|
||||
const isOwn = event.pubkey === $pubkey
|
||||
|
||||
return {
|
||||
event,
|
||||
editable: isOwn,
|
||||
id: getAddress(event),
|
||||
title: meta.title || meta.name, // Backwards compat with a bug
|
||||
start: secondsToDate(meta.start),
|
||||
end: secondsToDate(meta.end),
|
||||
backgroundColor: $themeColors[isOwn ? "accent" : "neutral-100"],
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const createEvent = () => router.at("notes/create").qp({type: "calendar_event", group}).open()
|
||||
|
||||
const getEventContent = ({event}) => event.title
|
||||
@ -45,55 +55,17 @@
|
||||
|
||||
const onEventClick = ({event: calendarEvent}) => router.at("events").of(calendarEvent.id).open()
|
||||
|
||||
const events = writable(new Map())
|
||||
|
||||
const onEvent = batch(300, (chunk: TrustedEvent[]) => {
|
||||
events.update($events => {
|
||||
for (const e of chunk) {
|
||||
const addr = getAddress(e)
|
||||
const dup = $events.get(addr)
|
||||
|
||||
// Make sure we have the latest version of every event
|
||||
$events.set(addr, dup?.created_at > e.created_at ? dup : e)
|
||||
}
|
||||
|
||||
return $events
|
||||
})
|
||||
|
||||
// Load deletes for these events
|
||||
load({
|
||||
relays: hints.merge(chunk.map(e => hints.EventChildren(e))).getUrls(),
|
||||
filters: getReplyFilters(chunk, {kinds: [5]}),
|
||||
onMount(() => {
|
||||
loadAll(feedFromFilter(filter), {
|
||||
// Load deletes for these events
|
||||
onEvent: batch(300, (chunk: TrustedEvent[]) => {
|
||||
load({
|
||||
relays: hints.merge(chunk.map(e => hints.EventChildren(e))).getUrls(),
|
||||
filters: getReplyFilters(chunk, {kinds: [5]}),
|
||||
})
|
||||
}),
|
||||
})
|
||||
})
|
||||
|
||||
let subs = []
|
||||
|
||||
onMount(async () => {
|
||||
const [{filters}] = await feedLoader.compiler.compile(feed)
|
||||
|
||||
subs = getFilterSelections(filters).map(({relay, filters}) =>
|
||||
subscribe({relays: [relay], filters, onEvent}),
|
||||
)
|
||||
})
|
||||
|
||||
onDestroy(() => subs.map(sub => sub.close()))
|
||||
|
||||
$: calendarEvents = Array.from($events.values())
|
||||
.filter(e => !repository.isDeleted(e))
|
||||
.map(e => {
|
||||
const meta = fromPairs(e.tags)
|
||||
const isOwn = e.pubkey === $pubkey
|
||||
|
||||
return {
|
||||
editable: isOwn,
|
||||
id: getAddress(e),
|
||||
title: meta.title || meta.name, // Backwards compat with a bug
|
||||
start: secondsToDate(meta.start),
|
||||
end: secondsToDate(meta.end),
|
||||
backgroundColor: $themeColors[isOwn ? "accent" : "neutral-100"],
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if $canSign}
|
||||
@ -110,7 +82,7 @@
|
||||
plugins={[Interaction, DayGrid]}
|
||||
options={{
|
||||
view: "dayGridMonth",
|
||||
events: calendarEvents,
|
||||
events: $calendarEvents,
|
||||
dateClick: onDateClick,
|
||||
eventClick: onEventClick,
|
||||
eventContent: getEventContent,
|
||||
|
@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import {debounce} from "throttle-debounce"
|
||||
import {equals} from "ramda"
|
||||
import {isSearchFeed, makeSearchFeed, makeScopeFeed, Scope, getFeedArgs} from "@welshman/feeds"
|
||||
import {toSpliced} from "src/util/misc"
|
||||
import {boolCtrl} from "src/partials/utils"
|
||||
@ -11,6 +12,7 @@
|
||||
import MenuItem from "src/partials/MenuItem.svelte"
|
||||
import FeedForm from "src/app/shared/FeedForm.svelte"
|
||||
import {router} from "src/app/util"
|
||||
import {globalFeed} from "src/app/state"
|
||||
import {normalizeFeedDefinition, displayList, readFeed, makeFeed, displayFeed} from "src/domain"
|
||||
import {userListFeeds, canSign, deleteEvent, userFeeds} from "src/engine"
|
||||
|
||||
@ -118,15 +120,27 @@
|
||||
<Anchor modal href="/feeds/create"><i class="fa fa-plus" /></Anchor>
|
||||
</MenuItem>
|
||||
<div class="max-h-80 overflow-auto">
|
||||
<MenuItem on:click={() => setFeed(followsFeed)}>Follows</MenuItem>
|
||||
<MenuItem on:click={() => setFeed(networkFeed)}>Network</MenuItem>
|
||||
<MenuItem
|
||||
active={equals(followsFeed.definition, $globalFeed.definition)}
|
||||
on:click={() => setFeed(followsFeed)}>
|
||||
Follows
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
active={equals(networkFeed.definition, $globalFeed.definition)}
|
||||
on:click={() => setFeed(networkFeed)}>
|
||||
Network
|
||||
</MenuItem>
|
||||
{#each $userFeeds as feed}
|
||||
<MenuItem on:click={() => setFeed(feed)}>
|
||||
<MenuItem
|
||||
active={equals(feed.definition, $globalFeed.definition)}
|
||||
on:click={() => setFeed(feed)}>
|
||||
{displayFeed(feed)}
|
||||
</MenuItem>
|
||||
{/each}
|
||||
{#each $userListFeeds as feed}
|
||||
<MenuItem on:click={() => setFeed(feed)}>
|
||||
<MenuItem
|
||||
active={equals(feed.definition, $globalFeed.definition)}
|
||||
on:click={() => setFeed(feed)}>
|
||||
{displayList(feed.list)}
|
||||
</MenuItem>
|
||||
{/each}
|
||||
|
@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import {feedFromFilter} from "@welshman/feeds"
|
||||
import {EVENT_TIME} from "@welshman/util"
|
||||
import Calendar from "src/app/shared/Calendar.svelte"
|
||||
|
||||
export let address
|
||||
</script>
|
||||
|
||||
<Calendar group={address} feed={feedFromFilter({kinds: [31923], "#a": [address]})} />
|
||||
<Calendar group={address} filter={{kinds: [EVENT_TIME], "#a": [address]}} />
|
||||
|
@ -1,17 +1,25 @@
|
||||
<script>
|
||||
import {ellipsize} from "hurdak"
|
||||
import {derived} from "svelte/store"
|
||||
import {remove, intersection} from "@welshman/lib"
|
||||
import Chip from "src/partials/Chip.svelte"
|
||||
import Card from "src/partials/Card.svelte"
|
||||
import GroupCircle from "src/app/shared/GroupCircle.svelte"
|
||||
import PersonCircles from "src/app/shared/PersonCircles.svelte"
|
||||
import {router} from "src/app/util/router"
|
||||
import {displayGroup, deriveGroup, getWotGroupMembers} from "src/engine"
|
||||
import {displayGroup, deriveGroup, userFollows, communityListsByAddress, pubkey} from "src/engine"
|
||||
|
||||
export let address
|
||||
export let modal = false
|
||||
|
||||
const group = deriveGroup(address)
|
||||
const members = $getWotGroupMembers(address)
|
||||
const members = derived(communityListsByAddress, $m => {
|
||||
const allMembers = $m.get(address)?.map(l => l.event.pubkey) || []
|
||||
const otherMembers = remove($pubkey, allMembers)
|
||||
const followMembers = intersection(otherMembers, Array.from($userFollows))
|
||||
|
||||
return followMembers
|
||||
})
|
||||
|
||||
const enter = () => {
|
||||
const route = router.at("groups").of(address).at("notes")
|
||||
@ -45,9 +53,10 @@
|
||||
{ellipsize($group.meta.about, 300)}
|
||||
</p>
|
||||
{/if}
|
||||
{#if members.length > 0}
|
||||
<p class="mt-4 text-lg text-neutral-300">Members:</p>
|
||||
<PersonCircles pubkeys={members.slice(0, 20)} />
|
||||
{#if $members.length > 0}
|
||||
<div class="pt-1">
|
||||
<PersonCircles class="h-6 w-6" pubkeys={$members.slice(0, 20)} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Card>
|
||||
|
@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import {isGroupAddress} from "@welshman/util"
|
||||
import Card from "src/partials/Card.svelte"
|
||||
import Anchor from "src/partials/Anchor.svelte"
|
||||
import PersonSummary from "src/app/shared/PersonSummary.svelte"
|
||||
@ -24,7 +25,7 @@
|
||||
<Card interactive on:click={() => openPerson(pubkey)}>
|
||||
<PersonSummary inert {pubkey}>
|
||||
<div slot="actions" on:click|stopPropagation>
|
||||
{#if $adminKey && pubkey !== $session.pubkey}
|
||||
{#if $adminKey && pubkey !== $session.pubkey && isGroupAddress(address)}
|
||||
<Anchor on:click={remove} button accent>Remove</Anchor>
|
||||
{/if}
|
||||
</div>
|
||||
|
@ -1,15 +1,26 @@
|
||||
<script lang="ts">
|
||||
import {COMMUNITIES} from "@welshman/util"
|
||||
import FlexColumn from "src/partials/FlexColumn.svelte"
|
||||
import GroupMember from "src/app/shared/GroupMember.svelte"
|
||||
import {deriveGroup} from 'src/engine'
|
||||
import {load, hints, deriveGroup, communityListsByAddress} from "src/engine"
|
||||
|
||||
export let address
|
||||
|
||||
const group = deriveGroup(address)
|
||||
const filters = [{kinds: [COMMUNITIES], "#a": [address]}]
|
||||
|
||||
$: members =
|
||||
$group.members || $communityListsByAddress.get(address)?.map(l => l.event.pubkey) || []
|
||||
|
||||
load({
|
||||
filters,
|
||||
skipCache: true,
|
||||
relays: hints.merge([hints.WithinContext(address), hints.User()]).getUrls(),
|
||||
})
|
||||
</script>
|
||||
|
||||
<FlexColumn>
|
||||
{#each $group.members || [] as pubkey (pubkey)}
|
||||
{#each members as pubkey (pubkey)}
|
||||
<GroupMember {address} {pubkey} />
|
||||
{:else}
|
||||
<p class="text-center py-12">No members found.</p>
|
||||
|
@ -53,6 +53,7 @@
|
||||
export let showLoading = false
|
||||
export let showMuted = false
|
||||
export let showGroup = false
|
||||
export let showMedia = getSetting("show_media")
|
||||
export let contextAddress = null
|
||||
|
||||
let zapper, unsubZapper
|
||||
@ -63,7 +64,7 @@
|
||||
let showMutedReplies = false
|
||||
let actions = null
|
||||
let collapsed = depth === 0
|
||||
let context = repository.query([{'#e': [event.id]}]).filter(e => isChildOf(e, event))
|
||||
let context = repository.query([{"#e": [event.id]}]).filter(e => isChildOf(e, event))
|
||||
let showHiddenReplies = anchor === getIdOrAddress(event)
|
||||
|
||||
const showEntire = showHiddenReplies
|
||||
@ -296,7 +297,7 @@
|
||||
}}>Show</Anchor>
|
||||
</p>
|
||||
{:else}
|
||||
<NoteContent note={event} {showEntire} />
|
||||
<NoteContent note={event} {showEntire} {showMedia} />
|
||||
{/if}
|
||||
<div class="cy-note-click-target h-[2px]" />
|
||||
<NoteActions
|
||||
|
@ -3,6 +3,7 @@
|
||||
import {nip19} from "nostr-tools"
|
||||
import {onMount} from "svelte"
|
||||
import {derived} from "svelte/store"
|
||||
import {last, sortBy} from "@welshman/lib"
|
||||
import type {TrustedEvent, SignedEvent} from "@welshman/util"
|
||||
import {
|
||||
LOCAL_RELAY_URL,
|
||||
@ -18,9 +19,15 @@
|
||||
import {fly} from "src/util/transition"
|
||||
import {formatSats} from "src/util/misc"
|
||||
import {quantify, pluralize} from "hurdak"
|
||||
import {browser} from "src/partials/state"
|
||||
import {showInfo} from "src/partials/Toast.svelte"
|
||||
import Icon from "src/partials/Icon.svelte"
|
||||
import Anchor from "src/partials/Anchor.svelte"
|
||||
import WotScore from "src/partials/WotScore.svelte"
|
||||
import Popover from "src/partials/Popover.svelte"
|
||||
import ImageCircle from "src/partials/ImageCircle.svelte"
|
||||
import Menu from "src/partials/Menu.svelte"
|
||||
import MenuItem from "src/partials/MenuItem.svelte"
|
||||
import FlexColumn from "src/partials/FlexColumn.svelte"
|
||||
import Card from "src/partials/Card.svelte"
|
||||
import Heading from "src/partials/Heading.svelte"
|
||||
@ -70,12 +77,12 @@
|
||||
const addresses = [address].filter(identity)
|
||||
const nevent = nip19.neventEncode({id: note.id, relays: hints.Event(note).getUrls()})
|
||||
const muted = derived(isEventMuted, $isEventMuted => $isEventMuted(note, true))
|
||||
const kindHandlers = deriveHandlersForKind(note.kind)
|
||||
const interpolate = (a, b) => t => a + Math.round((b - a) * t)
|
||||
const mentions = tags.values("p").valueOf()
|
||||
const likesCount = tweened(0, {interpolate})
|
||||
const zapsTotal = tweened(0, {interpolate})
|
||||
const repliesCount = tweened(0, {interpolate})
|
||||
const kindHandlers = deriveHandlersForKind(note.kind)
|
||||
const handlerId = tags.get("client")?.nth(2)
|
||||
const handlerEvent = handlerId ? repository.getEvent(handlerId) : null
|
||||
const seenOn = tracker.data.derived(m =>
|
||||
@ -94,10 +101,14 @@
|
||||
handlersShown = false
|
||||
}
|
||||
|
||||
const os = browser.os.name.toLowerCase()
|
||||
|
||||
const createLabel = () => router.at("notes").of(note.id).at("label").open()
|
||||
|
||||
const quote = () => router.at("notes/create").cx({quote: note}).open()
|
||||
|
||||
const report = () => router.at("notes").of(note.id).at("report").open()
|
||||
|
||||
const react = async content => {
|
||||
if (isSignedEvent(note)) {
|
||||
publish({event: note, relays: hints.PublishEvent(note).getUrls()})
|
||||
@ -160,6 +171,26 @@
|
||||
showInfo("Note has been re-published!")
|
||||
}
|
||||
|
||||
const openWithHandler = handler => {
|
||||
const [templateTag] = sortBy((t: string[]) => {
|
||||
if (t[0] === "web" && last(t) === "nevent") return -6
|
||||
if (t[0] === "web" && last(t) === "note") return -5
|
||||
if (t[0] === "web" && t.length === 2) return -4
|
||||
if (t[0] === os && last(t) === "nevent") return -3
|
||||
if (t[0] === os && last(t) === "note") return -2
|
||||
if (t[0] === os && t.length === 2) return -1
|
||||
|
||||
return 0
|
||||
}, handler.event.tags)
|
||||
|
||||
const entity =
|
||||
last(templateTag) === "note"
|
||||
? nip19.noteEncode(note.id)
|
||||
: nip19.neventEncode({id: note.id, relays: hints.Event(note).getUrls()})
|
||||
|
||||
window.open(templateTag[1].replace("<bech32>", entity))
|
||||
}
|
||||
|
||||
const groupOptions = session.derived($session => {
|
||||
const options = []
|
||||
|
||||
@ -190,6 +221,13 @@
|
||||
$: canZap = zapper && note.pubkey !== $session?.pubkey
|
||||
$: reply = replies.find(e => e.pubkey === $session?.pubkey)
|
||||
$: $repliesCount = replies.length
|
||||
$: handlers = $kindHandlers.filter(
|
||||
h =>
|
||||
h.name.toLowerCase() !== "coracle" &&
|
||||
h.event.tags.some(
|
||||
t => ["web", os].includes(t[0]) && (t.length == 2 || ["note", "nevent"].includes(last(t))),
|
||||
),
|
||||
)
|
||||
|
||||
$: {
|
||||
actions = []
|
||||
@ -208,6 +246,8 @@
|
||||
} else {
|
||||
actions.push({label: "Mute", icon: "microphone-slash", onClick: muteNote})
|
||||
}
|
||||
|
||||
actions.push({label: "Report", icon: "triangle-exclamation", onClick: report})
|
||||
}
|
||||
|
||||
if (!$env.FORCE_GROUP && $env.PLATFORM_RELAYS.length === 0 && isSignedEvent(note)) {
|
||||
@ -268,6 +308,33 @@
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
{#if handlers.length > 0}
|
||||
<Popover theme="transparent" opts={{hideOnClick: true}}>
|
||||
<button
|
||||
slot="trigger"
|
||||
class="relative flex h-6 items-center gap-1 pt-1 transition-all hover:pb-1 hover:pt-0">
|
||||
<i class="fa fa-up-right-from-square fa-sm" />
|
||||
</button>
|
||||
<div slot="tooltip" class="max-h-[300px] min-w-[180px] overflow-auto">
|
||||
<Menu>
|
||||
<MenuItem inert class="bg-neutral-900">Open with:</MenuItem>
|
||||
{#each handlers as handler}
|
||||
<MenuItem
|
||||
class="flex h-12 items-center justify-between gap-2"
|
||||
on:click={() => openWithHandler(handler)}>
|
||||
<div class="flex gap-2">
|
||||
<ImageCircle class="h-5 w-5" src={handler.image} />
|
||||
{handler.name}
|
||||
</div>
|
||||
{#if handler.recommendations.length > 0}
|
||||
<WotScore accent score={handler.recommendations.length} />
|
||||
{/if}
|
||||
</MenuItem>
|
||||
{/each}
|
||||
</Menu>
|
||||
</div>
|
||||
</Popover>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex scale-90 items-center gap-2">
|
||||
{#if note.wrap}
|
||||
@ -330,7 +397,7 @@
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{#if $kindHandlers.length > 0 || handlerEvent}
|
||||
{#if handlers.length > 0 || handlerEvent}
|
||||
<h1 class="staatliches text-2xl">Apps</h1>
|
||||
{#if handlerEvent}
|
||||
{@const [handler] = readHandlers(handlerEvent)}
|
||||
@ -339,13 +406,10 @@
|
||||
<HandlerCard {handler} />
|
||||
{/if}
|
||||
{/if}
|
||||
{#if $kindHandlers.length > 0}
|
||||
{#if handlers.length > 0}
|
||||
<div class="flex justify-between">
|
||||
<p>
|
||||
This note can also be viewed using {quantify(
|
||||
$kindHandlers.length,
|
||||
"other nostr app",
|
||||
)}.
|
||||
This note can also be viewed using {quantify(handlers.length, "other nostr app")}.
|
||||
</p>
|
||||
{#if handlersShown}
|
||||
<Anchor underline on:click={hideHandlers}>Hide apps</Anchor>
|
||||
@ -356,7 +420,7 @@
|
||||
{#if handlersShown}
|
||||
<div in:fly={{y: 20}}>
|
||||
<FlexColumn>
|
||||
{#each $kindHandlers as handler (getHandlerKey(handler))}
|
||||
{#each handlers as handler (getHandlerKey(handler))}
|
||||
<HandlerCard {handler} />
|
||||
{/each}
|
||||
</FlexColumn>
|
||||
|
@ -21,7 +21,7 @@
|
||||
const tags = Tags.fromEvent(note)
|
||||
const images = tags.values("image").valueOf()
|
||||
const {title, summary, location, status} = tags.asObject()
|
||||
const [price = 0, code = "SAT"] = tags.get("price")?.drop(1).valueOf() || []
|
||||
const [price, code = "SAT"] = tags.get("price")?.drop(1).valueOf() || []
|
||||
const address = Address.fromEvent(note, hints.Event(note).redundancy(3).getUrls())
|
||||
const editLink = router.at("listings").of(address.toString()).at("edit").toString()
|
||||
const deleteLink = router.at("listings").of(address.toString()).at("delete").toString()
|
||||
@ -58,7 +58,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
<span class="whitespace-nowrap">
|
||||
<CurrencySymbol {code} />{commaFormat(price)}
|
||||
<CurrencySymbol {code} />{commaFormat(price || 0)}
|
||||
{code}
|
||||
</span>
|
||||
</div>
|
||||
|
@ -17,7 +17,15 @@
|
||||
let hidden = false
|
||||
</script>
|
||||
|
||||
{#if showMedia && value.isMedia && !hidden}
|
||||
{#if url.includes('coracle.social/')}
|
||||
<Anchor
|
||||
modal
|
||||
stopPropagation
|
||||
class="overflow-hidden text-ellipsis whitespace-nowrap underline"
|
||||
href={url.replace(/(https?:\/\/)?(app\.)?coracle.social/, '')}>
|
||||
{displayUrl(url)}
|
||||
</Anchor>
|
||||
{:else if showMedia && value.isMedia && !hidden}
|
||||
<div class="py-2">
|
||||
<Media url={url} onClose={close} />
|
||||
</div>
|
||||
|
@ -97,7 +97,7 @@
|
||||
<form on:submit|preventDefault={() => onSubmit()}>
|
||||
<AltColor background class="z-feature flex gap-4 overflow-hidden rounded p-3 text-neutral-100">
|
||||
<PersonCircle class="h-10 w-10" pubkey={$pubkey} />
|
||||
<div class="w-full">
|
||||
<div class="w-full min-w-0">
|
||||
<Compose placeholder="What's up?" bind:this={compose} {onSubmit} style="min-height: 3em;" />
|
||||
<div class="p-2">
|
||||
<NoteImages bind:this={images} bind:compose includeInContent />
|
||||
|
@ -15,9 +15,9 @@
|
||||
<script lang="ts">
|
||||
import cx from "classnames"
|
||||
import {derived} from "svelte/store"
|
||||
import {themeColors} from "src/partials/state"
|
||||
import Popover from "src/partials/Popover.svelte"
|
||||
import Anchor from "src/partials/Anchor.svelte"
|
||||
import WotScore from "src/partials/WotScore.svelte"
|
||||
import {displayPubkey} from "src/domain"
|
||||
import {userFollows, deriveProfileDisplay, session, maxWot, getWotScore} from "src/engine"
|
||||
|
||||
@ -27,11 +27,7 @@
|
||||
const wotScore = getWotScore($session?.pubkey, pubkey)
|
||||
const npubDisplay = displayPubkey(pubkey)
|
||||
const profileDisplay = deriveProfileDisplay(pubkey)
|
||||
|
||||
$: superMaxWot = $maxWot * 1.5
|
||||
$: dashOffset = 100 - (Math.max(superMaxWot / 20, wotScore) / superMaxWot) * 100
|
||||
$: style = `transform: rotate(${dashOffset * 1.8 - 50}deg)`
|
||||
$: stroke = $themeColors[$following || pubkey === $session?.pubkey ? "accent" : "neutral-200"]
|
||||
const accent = $following || pubkey === $session?.pubkey
|
||||
</script>
|
||||
|
||||
<div class={cx("flex gap-1", $$props.class)}>
|
||||
@ -44,22 +40,9 @@
|
||||
{#if $session}
|
||||
<div class="flex gap-1 font-normal">
|
||||
<Popover triggerType="mouseenter">
|
||||
<span
|
||||
slot="trigger"
|
||||
class="relative flex h-10 w-10 items-center justify-center whitespace-nowrap px-4 text-xs">
|
||||
<svg height="32" width="32" class="absolute">
|
||||
<circle class="wot-background" cx="16" cy="16" r="15" />
|
||||
<circle
|
||||
cx="16"
|
||||
cy="16"
|
||||
r="15"
|
||||
class="wot-highlight"
|
||||
stroke-dashoffset={dashOffset}
|
||||
{style}
|
||||
{stroke} />
|
||||
</svg>
|
||||
{wotScore}
|
||||
</span>
|
||||
<div slot="trigger">
|
||||
<WotScore score={wotScore} max={$maxWot} {accent} />
|
||||
</div>
|
||||
<Anchor modal slot="tooltip" class="flex items-center gap-1" href="/help/web-of-trust">
|
||||
<i class="fa fa-info-circle" />
|
||||
WoT Score
|
||||
|
@ -44,101 +44,103 @@
|
||||
{#await promise}
|
||||
<!-- pass -->
|
||||
{:then event}
|
||||
<div in:fly|local={{y: 20}}>
|
||||
<Card>
|
||||
<FlexColumn>
|
||||
<div class="flex justify-between">
|
||||
<span>Kind {event.kind}, published {formatTimestamp(pub.created_at)}</span>
|
||||
<Anchor underline modal class="text-sm" on:click={() => open(event)}>View Note</Anchor>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<div class="hidden gap-4 sm:flex">
|
||||
<span class="flex items-center gap-2">
|
||||
<i class="fa fa-check" />
|
||||
{success.length} succeeded
|
||||
</span>
|
||||
{#if pending.length > 0}
|
||||
{#if event}
|
||||
<div in:fly|local={{y: 20}}>
|
||||
<Card>
|
||||
<FlexColumn>
|
||||
<div class="flex justify-between">
|
||||
<span>Kind {event.kind}, published {formatTimestamp(pub.created_at)}</span>
|
||||
<Anchor underline modal class="text-sm" on:click={() => open(event)}>View Note</Anchor>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<div class="hidden gap-4 sm:flex">
|
||||
<span class="flex items-center gap-2">
|
||||
<i class="fa fa-circle-notch fa-spin" />
|
||||
{pending.length} pending
|
||||
</span>
|
||||
{/if}
|
||||
{#if failure.length > 0}
|
||||
<span class="flex items-center gap-2">
|
||||
<i class="fa fa-triangle-exclamation" />
|
||||
{failure.length} failed
|
||||
</span>
|
||||
{/if}
|
||||
{#if timeout.length > 0}
|
||||
<span class="flex items-center gap-2">
|
||||
<i class="fa fa-stopwatch" />
|
||||
{timeout.length} timed out
|
||||
<i class="fa fa-check" />
|
||||
{success.length} succeeded
|
||||
</span>
|
||||
{#if pending.length > 0}
|
||||
<span class="flex items-center gap-2">
|
||||
<i class="fa fa-circle-notch fa-spin" />
|
||||
{pending.length} pending
|
||||
</span>
|
||||
{/if}
|
||||
{#if failure.length > 0}
|
||||
<span class="flex items-center gap-2">
|
||||
<i class="fa fa-triangle-exclamation" />
|
||||
{failure.length} failed
|
||||
</span>
|
||||
{/if}
|
||||
{#if timeout.length > 0}
|
||||
<span class="flex items-center gap-2">
|
||||
<i class="fa fa-stopwatch" />
|
||||
{timeout.length} timed out
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
{#if expanded}
|
||||
<Anchor class="flex items-center gap-2" on:click={collapse}>
|
||||
<i class="fa fa-caret-up" />
|
||||
<span class="text-underline">Hide Details</span>
|
||||
</Anchor>
|
||||
{:else}
|
||||
<Anchor class="flex items-center gap-2" on:click={expand}>
|
||||
<i class="fa fa-caret-down" />
|
||||
<span class="text-underline">Show Details</span>
|
||||
</Anchor>
|
||||
{/if}
|
||||
</div>
|
||||
{#if expanded}
|
||||
<Anchor class="flex items-center gap-2" on:click={collapse}>
|
||||
<i class="fa fa-caret-up" />
|
||||
<span class="text-underline">Hide Details</span>
|
||||
</Anchor>
|
||||
{:else}
|
||||
<Anchor class="flex items-center gap-2" on:click={expand}>
|
||||
<i class="fa fa-caret-down" />
|
||||
<span class="text-underline">Show Details</span>
|
||||
</Anchor>
|
||||
<div transition:slide|local>
|
||||
<FlexColumn>
|
||||
{#if pending.length > 0}
|
||||
<p class="mt-4 text-lg">The following relays are still pending:</p>
|
||||
<div class="grid gap-2 sm:grid-cols-2">
|
||||
{#each pending as url}
|
||||
<RelayCard hideActions {url} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{#if success.length > 0}
|
||||
<p class="mt-4 text-lg">The following relays accepted your note:</p>
|
||||
<div class="grid gap-2 sm:grid-cols-2">
|
||||
{#each success as url}
|
||||
<RelayCard hideActions {url} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{#if failure.length > 0}
|
||||
<p class="mt-4 text-lg">The following relays rejected your note:</p>
|
||||
{#each failure as url}
|
||||
<RelayCard {url}>
|
||||
<div slot="actions">
|
||||
<Anchor
|
||||
on:click={() => retry(url, event)}
|
||||
class="flex items-center gap-2 text-sm">
|
||||
<i class="fa fa-rotate" /> Retry
|
||||
</Anchor>
|
||||
</div>
|
||||
</RelayCard>
|
||||
{/each}
|
||||
{/if}
|
||||
{#if timeout.length > 0}
|
||||
<p class="mt-4 text-lg">The following relays did not respond:</p>
|
||||
{#each timeout as url}
|
||||
<RelayCard {url}>
|
||||
<div slot="actions">
|
||||
<Anchor
|
||||
on:click={() => retry(url, event)}
|
||||
class="flex items-center gap-2 text-sm">
|
||||
<i class="fa fa-rotate" /> Retry
|
||||
</Anchor>
|
||||
</div>
|
||||
</RelayCard>
|
||||
{/each}
|
||||
{/if}
|
||||
</FlexColumn>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if expanded}
|
||||
<div transition:slide|local>
|
||||
<FlexColumn>
|
||||
{#if pending.length > 0}
|
||||
<p class="mt-4 text-lg">The following relays are still pending:</p>
|
||||
<div class="grid gap-2 sm:grid-cols-2">
|
||||
{#each pending as url}
|
||||
<RelayCard hideActions {url} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{#if success.length > 0}
|
||||
<p class="mt-4 text-lg">The following relays accepted your note:</p>
|
||||
<div class="grid gap-2 sm:grid-cols-2">
|
||||
{#each success as url}
|
||||
<RelayCard hideActions {url} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{#if failure.length > 0}
|
||||
<p class="mt-4 text-lg">The following relays rejected your note:</p>
|
||||
{#each failure as url}
|
||||
<RelayCard {url}>
|
||||
<div slot="actions">
|
||||
<Anchor
|
||||
on:click={() => retry(url, event)}
|
||||
class="flex items-center gap-2 text-sm">
|
||||
<i class="fa fa-rotate" /> Retry
|
||||
</Anchor>
|
||||
</div>
|
||||
</RelayCard>
|
||||
{/each}
|
||||
{/if}
|
||||
{#if timeout.length > 0}
|
||||
<p class="mt-4 text-lg">The following relays did not respond:</p>
|
||||
{#each timeout as url}
|
||||
<RelayCard {url}>
|
||||
<div slot="actions">
|
||||
<Anchor
|
||||
on:click={() => retry(url, event)}
|
||||
class="flex items-center gap-2 text-sm">
|
||||
<i class="fa fa-rotate" /> Retry
|
||||
</Anchor>
|
||||
</div>
|
||||
</RelayCard>
|
||||
{/each}
|
||||
{/if}
|
||||
</FlexColumn>
|
||||
</div>
|
||||
{/if}
|
||||
</FlexColumn>
|
||||
</Card>
|
||||
</div>
|
||||
</FlexColumn>
|
||||
</Card>
|
||||
</div>
|
||||
{/if}
|
||||
{/await}
|
||||
|
1
src/app/shared/WotScore.svelte
Normal file
1
src/app/shared/WotScore.svelte
Normal file
@ -0,0 +1 @@
|
||||
|
@ -22,8 +22,6 @@ import {
|
||||
getSetting,
|
||||
} from "src/engine"
|
||||
|
||||
// Global state
|
||||
|
||||
export const drafts = new Map<string, string>()
|
||||
|
||||
export const menuIsOpen = writable(false)
|
||||
|
@ -1,7 +1,7 @@
|
||||
import {partition, prop, uniqBy} from "ramda"
|
||||
import {batch, tryFunc, seconds} from "hurdak"
|
||||
import {writable, derived} from "svelte/store"
|
||||
import {inc, pushToMapKey, sortBy, now} from "@welshman/lib"
|
||||
import {inc, assoc, pushToMapKey, sortBy, now} from "@welshman/lib"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {
|
||||
Tags,
|
||||
@ -14,7 +14,7 @@ import {
|
||||
REACTION,
|
||||
} from "@welshman/util"
|
||||
import {Tracker} from "@welshman/net"
|
||||
import type {Feed} from "@welshman/feeds"
|
||||
import type {Feed, RequestItem} from "@welshman/feeds"
|
||||
import {walkFeed, FeedLoader as CoreFeedLoader} from "@welshman/feeds"
|
||||
import {noteKinds, isLike, reactionKinds, repostKinds} from "src/util/nostr"
|
||||
import {withGetter} from "src/util/misc"
|
||||
@ -59,7 +59,7 @@ const prepFilters = (filters, opts: FeedOpts) => {
|
||||
return filters
|
||||
}
|
||||
|
||||
function* getRequestItems({relays, filters}, opts: FeedOpts) {
|
||||
function* getRequestItems({relays, filters}: RequestItem, opts: FeedOpts) {
|
||||
filters = prepFilters(filters, opts)
|
||||
|
||||
// Use relays specified in feeds
|
||||
@ -127,14 +127,15 @@ export const createFeed = (opts: FeedOpts) => {
|
||||
if (reqs && opts.shouldListen) {
|
||||
const tracker = new Tracker()
|
||||
|
||||
for (const {relays, filters} of reqs) {
|
||||
for (const request of Array.from(getRequestItems({relays, filters}, opts))) {
|
||||
for (const request of reqs) {
|
||||
for (const {relays, filters} of Array.from(getRequestItems(request, opts))) {
|
||||
subscribe({
|
||||
...request,
|
||||
relays,
|
||||
tracker,
|
||||
skipCache: true,
|
||||
onEvent: prependEvent,
|
||||
signal: controller.signal,
|
||||
filters: filters.map(assoc("since", now())),
|
||||
forcePlatform: opts.forcePlatform && (relays?.length || 0) === 0,
|
||||
})
|
||||
}
|
||||
|
@ -1,21 +1,15 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
Scope,
|
||||
feedFromFilter,
|
||||
makeIntersectionFeed,
|
||||
makeKindFeed,
|
||||
makeScopeFeed,
|
||||
} from "@welshman/feeds"
|
||||
import {identity} from "ramda"
|
||||
import Calendar from "src/app/shared/Calendar.svelte"
|
||||
import {env, loadGroupMessages} from "src/engine"
|
||||
import {env, loadGroupMessages, pubkey, userFollows} from "src/engine"
|
||||
|
||||
const feed = $env.FORCE_GROUP
|
||||
? feedFromFilter({kinds: [31923], "#a": [$env.FORCE_GROUP]})
|
||||
: makeIntersectionFeed(makeKindFeed(31923), makeScopeFeed(Scope.Self, Scope.Follows))
|
||||
const filter = $env.FORCE_GROUP
|
||||
? {kinds: [31923], "#a": [$env.FORCE_GROUP]}
|
||||
: {kinds: [31923], authors: [$pubkey, ...$userFollows].filter(identity)}
|
||||
|
||||
if ($env.FORCE_GROUP) {
|
||||
loadGroupMessages([$env.FORCE_GROUP])
|
||||
}
|
||||
</script>
|
||||
|
||||
<Calendar {feed} />
|
||||
<Calendar {filter} />
|
||||
|
@ -71,7 +71,7 @@
|
||||
tabs.push("market")
|
||||
}
|
||||
|
||||
if ($sharedKey) {
|
||||
if ($sharedKey || address.startsWith("34550")) {
|
||||
tabs.push("members")
|
||||
} else if (activeTab === "members") {
|
||||
activeTab = "notes"
|
||||
|
@ -1,7 +1,8 @@
|
||||
<script>
|
||||
import {onMount} from "svelte"
|
||||
import {filter, assoc} from "ramda"
|
||||
import {now} from "@welshman/lib"
|
||||
import {now, shuffle} from "@welshman/lib"
|
||||
import {GROUP, COMMUNITY, getIdFilters} from "@welshman/util"
|
||||
import {createScroller} from "src/util/misc"
|
||||
import Anchor from "src/partials/Anchor.svelte"
|
||||
import FlexColumn from "src/partials/FlexColumn.svelte"
|
||||
@ -12,11 +13,11 @@
|
||||
hints,
|
||||
groups,
|
||||
repository,
|
||||
getGroupReqInfo,
|
||||
loadGiftWraps,
|
||||
loadGroupMessages,
|
||||
deriveIsGroupMember,
|
||||
updateCurrentSession,
|
||||
communityListsByAddress,
|
||||
searchGroups,
|
||||
} from "src/engine"
|
||||
|
||||
@ -41,9 +42,10 @@
|
||||
document.title = "Groups"
|
||||
|
||||
onMount(() => {
|
||||
const {admins} = getGroupReqInfo()
|
||||
const scroller = createScroller(loadMore, {element})
|
||||
const loader = loadGiftWraps()
|
||||
const scroller = createScroller(loadMore, {element})
|
||||
const communityAddrs = Array.from($communityListsByAddress.keys())
|
||||
.filter(a => !groups.key(a).get()?.meta)
|
||||
|
||||
updateCurrentSession(assoc("groups_last_synced", now()))
|
||||
|
||||
@ -51,15 +53,15 @@
|
||||
|
||||
load({
|
||||
skipCache: true,
|
||||
forcePlatform: false,
|
||||
relays: hints.User().getUrls(),
|
||||
filters: [{kinds: [35834, 34550], authors: admins}],
|
||||
filters: [{kinds: [GROUP, COMMUNITY], limit: 1000 - communityAddrs.length}],
|
||||
})
|
||||
|
||||
load({
|
||||
skipCache: true,
|
||||
forcePlatform: false,
|
||||
relays: hints.User().getUrls(),
|
||||
filters: [{kinds: [35834, 34550], limit: 500}],
|
||||
filters: getIdFilters(shuffle(communityAddrs).slice(0, 1000)),
|
||||
})
|
||||
|
||||
return () => {
|
||||
|
@ -2,7 +2,7 @@
|
||||
import cx from "classnames"
|
||||
import {onMount} from "svelte"
|
||||
import {last, prop, objOf} from "ramda"
|
||||
import {Handlerinformation, NostrConnect} from "nostr-tools/kinds"
|
||||
import {HANDLER_INFORMATION, NOSTR_CONNECT} from "@welshman/util"
|
||||
import {tryJson} from "src/util/misc"
|
||||
import {showWarning} from "src/partials/Toast.svelte"
|
||||
import Anchor from "src/partials/Anchor.svelte"
|
||||
@ -53,7 +53,6 @@
|
||||
loading = true
|
||||
|
||||
try {
|
||||
|
||||
// Fill in pubkey and relays if they entered a custom doain
|
||||
if (!handler.pubkey) {
|
||||
const handle = await loadHandle(`_@${handler.domain}`)
|
||||
@ -105,8 +104,8 @@
|
||||
relays: hints.ReadRelays().getUrls(),
|
||||
filters: [
|
||||
{
|
||||
kinds: [Handlerinformation],
|
||||
"#k": [NostrConnect.toString()],
|
||||
kinds: [HANDLER_INFORMATION],
|
||||
"#k": [NOSTR_CONNECT.toString()],
|
||||
},
|
||||
],
|
||||
onEvent: async e => {
|
||||
|
@ -1,60 +1,67 @@
|
||||
<script lang="ts">
|
||||
import {identity} from "ramda"
|
||||
import {seconds} from "hurdak"
|
||||
import {now} from "@welshman/lib"
|
||||
import {fuzzy} from "src/util/misc"
|
||||
import {asSignedEvent, createEvent} from "@welshman/util"
|
||||
import type {SignedEvent} from "@welshman/util"
|
||||
import {generatePrivateKey} from "src/util/nostr"
|
||||
import {showInfo} from "src/partials/Toast.svelte"
|
||||
import Heading from "src/partials/Heading.svelte"
|
||||
import FlexColumn from "src/partials/FlexColumn.svelte"
|
||||
import Anchor from "src/partials/Anchor.svelte"
|
||||
import Field from "src/partials/Field.svelte"
|
||||
import SearchSelect from "src/partials/SearchSelect.svelte"
|
||||
import Textarea from "src/partials/Textarea.svelte"
|
||||
import PersonLink from "src/app/shared/PersonLink.svelte"
|
||||
import Note from "src/app/shared/Note.svelte"
|
||||
import {router} from "src/app/util/router"
|
||||
import {createAndPublish, hints} from "src/engine"
|
||||
import {repository, nip59, loadPubkeys, publish, hints} from "src/engine"
|
||||
|
||||
export let eid
|
||||
export let pubkey
|
||||
|
||||
const searchContentWarnings = fuzzy(["nudity", "profanity", "illegal", "spam", "impersonation"])
|
||||
const event = repository.getEvent(eid)
|
||||
|
||||
const submit = () => {
|
||||
const tags = [
|
||||
["p", pubkey],
|
||||
["expiration", String(now() + seconds(7, "day"))],
|
||||
]
|
||||
const tagr = "56d4b3d6310fadb7294b7f041aab469c5ffc8991b1b1b331981b96a246f6ae65"
|
||||
|
||||
if (flags.length > 0) {
|
||||
for (const flag of flags) {
|
||||
tags.push(["e", eid, flag])
|
||||
}
|
||||
}
|
||||
const submit = async () => {
|
||||
const content = JSON.stringify({
|
||||
reporterText: message,
|
||||
reportedEvent: asSignedEvent(event as SignedEvent),
|
||||
})
|
||||
|
||||
const template = createEvent(14, {content})
|
||||
const rumor = await $nip59.wrap(template, {
|
||||
author: generatePrivateKey(),
|
||||
wrap: {
|
||||
author: generatePrivateKey(),
|
||||
recipient: tagr,
|
||||
},
|
||||
})
|
||||
|
||||
publish({
|
||||
event: rumor.wrap,
|
||||
relays: hints
|
||||
.merge([hints.fromRelays(["wss://relay.nos.social"]), hints.PublishMessage(tagr)])
|
||||
.getUrls(),
|
||||
forcePlatform: false,
|
||||
})
|
||||
|
||||
createAndPublish({kind: 1984, tags, relays: hints.WriteRelays().getUrls()})
|
||||
showInfo("Your report has been sent!")
|
||||
router.pop()
|
||||
}
|
||||
|
||||
let flags = []
|
||||
let message = ""
|
||||
|
||||
loadPubkeys([tagr])
|
||||
</script>
|
||||
|
||||
<form on:submit|preventDefault={submit}>
|
||||
<FlexColumn>
|
||||
<Heading class="text-center">File a Report</Heading>
|
||||
<div class="flex w-full flex-col gap-8">
|
||||
<Field label="Content Warnings">
|
||||
<SearchSelect
|
||||
multiple
|
||||
autofocus
|
||||
search={searchContentWarnings}
|
||||
bind:value={flags}
|
||||
termToItem={identity}>
|
||||
<div slot="item" let:item>
|
||||
<strong>{item}</strong>
|
||||
</div>
|
||||
</SearchSelect>
|
||||
<div slot="info">Flag this content as sensitive so other people can avoid it.</div>
|
||||
</Field>
|
||||
<Anchor button tag="button" type="submit">Save</Anchor>
|
||||
</div>
|
||||
<Field label="Why are you reporting this content?">
|
||||
<Textarea bind:value={message} />
|
||||
<div slot="info">
|
||||
Reports are sent to <PersonLink pubkey={tagr} /> for review. No identifying information is included
|
||||
with the report.
|
||||
</div>
|
||||
</Field>
|
||||
<Note note={event} showMedia={false} />
|
||||
<Anchor button tag="button" type="submit">Save</Anchor>
|
||||
</FlexColumn>
|
||||
</form>
|
||||
|
@ -1,12 +1,15 @@
|
||||
<script lang="ts">
|
||||
import Input from "src/partials/Input.svelte"
|
||||
import FlexColumn from "src/partials/FlexColumn.svelte"
|
||||
import Card from "src/partials/Card.svelte"
|
||||
import ImageInput from "src/partials/ImageInput.svelte"
|
||||
import Textarea from "src/partials/Textarea.svelte"
|
||||
import Anchor from "src/partials/Anchor.svelte"
|
||||
import Footer from "src/partials/Footer.svelte"
|
||||
import Heading from "src/partials/Heading.svelte"
|
||||
import Modal from "src/partials/Modal.svelte"
|
||||
import Field from "src/partials/Field.svelte"
|
||||
import {pubkey, getProfile, publishProfile} from "src/engine"
|
||||
import {env, pubkey, getProfile, publishProfile} from "src/engine"
|
||||
import {router} from "src/app/util/router"
|
||||
|
||||
const nip05Url = "https://github.com/nostr-protocol/nips/blob/master/05.md"
|
||||
@ -14,13 +17,34 @@
|
||||
const pseudUrl =
|
||||
"https://www.coindesk.com/markets/2020/06/29/many-bitcoin-developers-are-choosing-to-use-pseudonyms-for-good-reason/"
|
||||
|
||||
const submit = () => {
|
||||
publishProfile(values)
|
||||
const closeModal = () => {
|
||||
modal = null
|
||||
}
|
||||
|
||||
const publishToPlatform = () => {
|
||||
publishProfile(values, {forcePlatform: true})
|
||||
router.pop()
|
||||
router.pop()
|
||||
}
|
||||
|
||||
const publishToNetwork = () => {
|
||||
publishProfile(values, {forcePlatform: false})
|
||||
router.pop()
|
||||
router.pop()
|
||||
}
|
||||
|
||||
const submit = () => {
|
||||
if ($env.PLATFORM_RELAYS.length === 0) {
|
||||
publishToNetwork()
|
||||
} else {
|
||||
modal = 'select-scope'
|
||||
}
|
||||
}
|
||||
|
||||
const values = {...getProfile($pubkey)}
|
||||
|
||||
let modal
|
||||
|
||||
document.title = "Profile"
|
||||
</script>
|
||||
|
||||
@ -85,3 +109,42 @@
|
||||
<Anchor grow button tag="button" type="submit">Save</Anchor>
|
||||
</Footer>
|
||||
</form>
|
||||
|
||||
{#if modal === 'select-scope'}
|
||||
<Modal onEscape={closeModal}>
|
||||
<div class="mb-4 flex flex-col items-center justify-center">
|
||||
<Heading>Update Profile</Heading>
|
||||
<p>Where would you like to publish your profile?</p>
|
||||
</div>
|
||||
<Card interactive on:click={publishToNetwork}>
|
||||
<FlexColumn>
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="flex items-center gap-4 text-xl">
|
||||
<i class="fa fa-share-nodes text-neutral-600" />
|
||||
<strong>The wider nostr network</strong>
|
||||
</p>
|
||||
<i class="fa fa-arrow-right" />
|
||||
</div>
|
||||
<p>
|
||||
Publishing your profile to the wider nostr network will allow anyone to see it.
|
||||
Use this if you plan to use other clients or relay selections.
|
||||
</p>
|
||||
</FlexColumn>
|
||||
</Card>
|
||||
<Card interactive on:click={publishToPlatform}>
|
||||
<FlexColumn>
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="flex items-center gap-4 text-xl">
|
||||
<i class="fa fa-thumbtack text-neutral-600" />
|
||||
<strong>Just this instance</strong>
|
||||
</p>
|
||||
<i class="fa fa-arrow-right" />
|
||||
</div>
|
||||
<p>
|
||||
Publish your profile just to the relays configured on this instance if you prefer.
|
||||
Be aware that how private this is depends on how the instance operator has set things up.
|
||||
</p>
|
||||
</FlexColumn>
|
||||
</Card>
|
||||
</Modal>
|
||||
{/if}
|
||||
|
@ -66,7 +66,6 @@ import {
|
||||
nip04,
|
||||
nip44,
|
||||
nip59,
|
||||
people,
|
||||
pubkey,
|
||||
publish,
|
||||
session,
|
||||
@ -662,13 +661,13 @@ export const deleteEventByAddress = address =>
|
||||
|
||||
// Profile
|
||||
|
||||
export const publishProfile = profile =>
|
||||
export const publishProfile = (profile, {forcePlatform = false} = {}) =>
|
||||
createAndPublish({
|
||||
kind: 0,
|
||||
tags: getClientTags(),
|
||||
content: JSON.stringify(profile),
|
||||
relays: withIndexers(hints.WriteRelays().getUrls()),
|
||||
forcePlatform: false,
|
||||
forcePlatform,
|
||||
})
|
||||
|
||||
// Singletons
|
||||
@ -910,7 +909,6 @@ export const markChannelRead = (pubkey: string) => {
|
||||
|
||||
const addSession = (s: Session) => {
|
||||
sessions.update(assoc(s.pubkey, s))
|
||||
people.key(s.pubkey).update($p => ({...$p, pubkey: s.pubkey}))
|
||||
pubkey.set(s.pubkey)
|
||||
}
|
||||
|
||||
|
@ -73,12 +73,6 @@ export type Zapper = WelshmanZapper & {
|
||||
pubkey: string
|
||||
}
|
||||
|
||||
export type Person = {
|
||||
pubkey: string
|
||||
communities_updated_at?: number
|
||||
communities?: string[][]
|
||||
}
|
||||
|
||||
export type PublishInfo = Omit<Publish, "emitter" | "result">
|
||||
|
||||
export type Notification = {
|
||||
|
@ -11,6 +11,7 @@ import {
|
||||
MUTES,
|
||||
FOLLOWS,
|
||||
RELAYS,
|
||||
COMMUNITIES,
|
||||
} from "@welshman/util"
|
||||
import {tryJson} from "src/util/misc"
|
||||
import {appDataKeys, giftWrapKinds, getPublicKey} from "src/util/nostr"
|
||||
@ -33,11 +34,9 @@ import {
|
||||
groups,
|
||||
load,
|
||||
nip04,
|
||||
people,
|
||||
projections,
|
||||
sessions,
|
||||
hints,
|
||||
ensureMessagePlaintext,
|
||||
ensurePlaintext,
|
||||
} from "src/engine/state"
|
||||
import {
|
||||
@ -197,9 +196,9 @@ projections.addHandler(27, (e: TrustedEvent) => {
|
||||
}
|
||||
})
|
||||
|
||||
// Membership access/exit requests
|
||||
// Membership
|
||||
|
||||
projections.addHandler(10004, (e: TrustedEvent) => {
|
||||
projections.addHandler(COMMUNITIES, (e: TrustedEvent) => {
|
||||
let session = getSession(e.pubkey)
|
||||
|
||||
if (!session) {
|
||||
@ -250,12 +249,6 @@ projections.addHandler(0, e => {
|
||||
})
|
||||
})
|
||||
|
||||
projections.addHandler(10004, e => {
|
||||
updateStore(people.key(e.pubkey), e.created_at, {
|
||||
communities: Tags.fromEvent(e).whereKey("a").unwrap(),
|
||||
})
|
||||
})
|
||||
|
||||
// Relays
|
||||
|
||||
projections.addHandler(RELAYS, (e: TrustedEvent) => {
|
||||
@ -377,7 +370,6 @@ projections.addHandler(14, handleChannelMessage)
|
||||
|
||||
// Decrypt encrypted events eagerly
|
||||
|
||||
projections.addHandler(4, ensureMessagePlaintext)
|
||||
projections.addHandler(FOLLOWS, ensurePlaintext)
|
||||
projections.addHandler(MUTES, ensurePlaintext)
|
||||
|
||||
|
@ -52,7 +52,6 @@ import {
|
||||
loadOne,
|
||||
maxWot,
|
||||
getNetwork,
|
||||
people,
|
||||
primeWotCaches,
|
||||
pubkey,
|
||||
publish,
|
||||
@ -318,11 +317,11 @@ export const feedLoader = new FeedLoader<TrustedEvent>({
|
||||
|
||||
primeWotCaches($pubkey)
|
||||
|
||||
for (const person of people.get()) {
|
||||
const score = getWotScore($pubkey, person.pubkey)
|
||||
for (const tpk of repository.eventsByAuthor.keys()) {
|
||||
const score = getWotScore($pubkey, tpk)
|
||||
|
||||
if (score >= thresholdMin && score <= thresholdMax) {
|
||||
pubkeys.push(person.pubkey)
|
||||
pubkeys.push(tpk)
|
||||
}
|
||||
}
|
||||
|
||||
@ -538,7 +537,7 @@ export const loadHandlers = () =>
|
||||
load({
|
||||
skipCache: true,
|
||||
forcePlatform: false,
|
||||
relays: hints.ReadRelays().getUrls(),
|
||||
relays: hints.ReadRelays().getUrls().concat("wss://relay.nostr.band/"),
|
||||
filters: [
|
||||
addSinceToFilter({
|
||||
kinds: [HANDLER_RECOMMENDATION],
|
||||
|
@ -6,7 +6,9 @@ import {
|
||||
PROFILE,
|
||||
HANDLER_INFORMATION,
|
||||
NAMED_BOOKMARKS,
|
||||
COMMUNITIES,
|
||||
FEED,
|
||||
MUTES,
|
||||
FOLLOWS,
|
||||
APP_DATA,
|
||||
} from "@welshman/util"
|
||||
@ -50,10 +52,10 @@ const getFiltersForKey = (key: string, authors: string[]) => {
|
||||
case "pubkey/relays":
|
||||
return [{authors, kinds: [RELAYS]}]
|
||||
case "pubkey/profile":
|
||||
return [{authors, kinds: [PROFILE, FOLLOWS, HANDLER_INFORMATION]}]
|
||||
return [{authors, kinds: [PROFILE, FOLLOWS, HANDLER_INFORMATION, COMMUNITIES]}]
|
||||
case "pubkey/user":
|
||||
return [
|
||||
{authors, kinds: [PROFILE, RELAYS, FOLLOWS, APP_DATA]},
|
||||
{authors, kinds: [PROFILE, RELAYS, MUTES, FOLLOWS, COMMUNITIES, APP_DATA]},
|
||||
{authors, kinds: [APP_DATA], "#d": Object.values(appDataKeys)},
|
||||
]
|
||||
}
|
||||
|
@ -35,13 +35,16 @@ import {
|
||||
uniq,
|
||||
uniqBy,
|
||||
now,
|
||||
intersection,
|
||||
sort,
|
||||
groupBy,
|
||||
indexBy,
|
||||
pushToMapKey,
|
||||
} from "@welshman/lib"
|
||||
import {
|
||||
WRAP,
|
||||
WRAP_NIP04,
|
||||
COMMUNITIES,
|
||||
READ_RECEIPT,
|
||||
NAMED_BOOKMARKS,
|
||||
HANDLER_RECOMMENDATION,
|
||||
@ -59,6 +62,7 @@ import {
|
||||
getFilterId,
|
||||
isContextAddress,
|
||||
unionFilters,
|
||||
getAddress,
|
||||
getIdAndAddress,
|
||||
getIdOrAddress,
|
||||
getIdFilters,
|
||||
@ -134,7 +138,6 @@ import type {
|
||||
GroupKey,
|
||||
GroupRequest,
|
||||
GroupStatus,
|
||||
Person,
|
||||
PublishInfo,
|
||||
Session,
|
||||
Topic,
|
||||
@ -182,7 +185,6 @@ export const groupAdminKeys = new CollectionStore<GroupKey>("pubkey")
|
||||
export const groupSharedKeys = new CollectionStore<GroupKey>("pubkey")
|
||||
export const groupRequests = new CollectionStore<GroupRequest>("id")
|
||||
export const groupAlerts = new CollectionStore<GroupAlert>("id")
|
||||
export const people = new CollectionStore<Person>("pubkey")
|
||||
export const publishes = new CollectionStore<PublishInfo>("id", 1000)
|
||||
export const topics = new CollectionStore<Topic>("name")
|
||||
export const channels = new CollectionStore<Channel>("id")
|
||||
@ -296,6 +298,10 @@ export const ensureMessagePlaintext = async (e: TrustedEvent) => {
|
||||
return getPlaintext(e)
|
||||
}
|
||||
|
||||
export const canUnwrap = (event: TrustedEvent) =>
|
||||
isGiftWrap(event) &&
|
||||
(getSession(Tags.fromEvent(event).get("p")?.value()) || getRecipientKey(event))
|
||||
|
||||
export const ensureUnwrapped = async (event: TrustedEvent) => {
|
||||
if (!isGiftWrap(event)) {
|
||||
return event
|
||||
@ -669,6 +675,48 @@ export const unreadChannels = channels.derived(filter(channelHasNewMessages))
|
||||
|
||||
export const hasNewMessages = unreadChannels.derived(any((c: Channel) => Boolean(c.last_sent)))
|
||||
|
||||
// Communities
|
||||
|
||||
export const communityLists = withGetter(
|
||||
deriveEventsMapped<PublishedSingleton>({
|
||||
filters: [{kinds: [COMMUNITIES]}],
|
||||
itemToEvent: prop("event"),
|
||||
eventToItem: event =>
|
||||
readSingleton(
|
||||
asDecryptedEvent(event, {
|
||||
content: getPlaintext(event),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
)
|
||||
|
||||
export const communityListsByPubkey = withGetter(
|
||||
derived(communityLists, $ls => indexBy($l => $l.event.pubkey, $ls)),
|
||||
)
|
||||
|
||||
export const communityListsByAddress = derived(communityLists, $communityLists => {
|
||||
const m = new Map<string, PublishedSingleton[]>()
|
||||
|
||||
for (const list of $communityLists) {
|
||||
for (const a of getSingletonValues("a", list)) {
|
||||
pushToMapKey(m, a, list)
|
||||
}
|
||||
}
|
||||
|
||||
return m
|
||||
})
|
||||
|
||||
export const getCommunityList = (pk: string) =>
|
||||
communityListsByPubkey.get().get(pk) as PublishedSingleton | undefined
|
||||
|
||||
export const deriveCommunityList = (pk: string) =>
|
||||
derived(communityListsByPubkey, m => m.get(pk) as PublishedSingleton | undefined)
|
||||
|
||||
export const getCommunities = (pk: string) => getSingletonValues("a", getCommunityList(pk))
|
||||
|
||||
export const deriveCommunities = (pk: string) =>
|
||||
derived(communityListsByPubkey, m => getSingletonValues("a", m.get(pk)))
|
||||
|
||||
// Groups
|
||||
|
||||
export const deriveGroup = address => {
|
||||
@ -677,41 +725,39 @@ export const deriveGroup = address => {
|
||||
return groups.key(address).derived(defaultTo({id, pubkey, address}))
|
||||
}
|
||||
|
||||
export const getWotGroupMembers = withGetter(
|
||||
derived(
|
||||
[userFollows, people.mapStore],
|
||||
([$userFollows, $people]) =>
|
||||
address =>
|
||||
Array.from($userFollows).filter(pk =>
|
||||
$people.get(pk)?.communities?.some(t => t[1] === address),
|
||||
),
|
||||
),
|
||||
)
|
||||
export const searchGroups = derived(
|
||||
[groups.throttle(300), communityListsByAddress, userFollows],
|
||||
([$groups, $communityListsByAddress, $userFollows]) => {
|
||||
const options = $groups
|
||||
.filter(group => !repository.deletes.has(group.address))
|
||||
.map(group => {
|
||||
const lists = $communityListsByAddress.get(group.address) || []
|
||||
const members = lists.map(l => l.event.pubkey)
|
||||
const followedMembers = intersection(members, $userFollows)
|
||||
|
||||
export const searchGroups = groups.throttle(300).derived($groups => {
|
||||
const options = $groups
|
||||
.filter(group => !repository.deletes.has(group.address))
|
||||
.map(group => ({group, score: getWotGroupMembers.get()(group.address).length}))
|
||||
return {group, score: followedMembers.length}
|
||||
})
|
||||
|
||||
const fuse = new Fuse(options, {
|
||||
keys: [{name: "group.id", weight: 0.2}, "group.meta.name", "group.meta.about"],
|
||||
threshold: 0.3,
|
||||
shouldSort: false,
|
||||
includeScore: true,
|
||||
})
|
||||
const fuse = new Fuse(options, {
|
||||
keys: [{name: "group.id", weight: 0.2}, "group.meta.name", "group.meta.about"],
|
||||
threshold: 0.3,
|
||||
shouldSort: false,
|
||||
includeScore: true,
|
||||
})
|
||||
|
||||
return (term: string) => {
|
||||
if (!term) {
|
||||
return sortBy(item => -item.score, options).map(item => item.group)
|
||||
return (term: string) => {
|
||||
if (!term) {
|
||||
return sortBy(item => -item.score, options).map(item => item.group)
|
||||
}
|
||||
|
||||
return doPipe(fuse.search(term), [
|
||||
$results =>
|
||||
sortBy((r: any) => r.score - Math.pow(Math.max(0, r.item.score), 1 / 100), $results),
|
||||
$results => $results.map((r: any) => r.item.group),
|
||||
])
|
||||
}
|
||||
|
||||
return doPipe(fuse.search(term), [
|
||||
$results =>
|
||||
sortBy((r: any) => r.score - Math.pow(Math.max(0, r.item.score), 1 / 100), $results),
|
||||
$results => $results.map((r: any) => r.item.group),
|
||||
])
|
||||
}
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
export const getRecipientKey = wrap => {
|
||||
const pubkey = Tags.fromEvent(wrap).values("p").first()
|
||||
@ -1258,7 +1304,14 @@ export const recommendationsByHandlerAddress = derived(recommendations, $events
|
||||
)
|
||||
|
||||
export const deriveHandlersForKind = simpleCache(([kind]: [number]) =>
|
||||
derived(handlers, $handlers => $handlers.filter(h => h.kind === kind)),
|
||||
derived([handlers, recommendationsByHandlerAddress], ([$handlers, $recs]) =>
|
||||
sortBy(
|
||||
h => -h.recommendations.length,
|
||||
$handlers
|
||||
.filter(h => h.kind === kind)
|
||||
.map(h => ({...h, recommendations: $recs.get(getAddress(h.event)) || []})),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
// Collections
|
||||
@ -1426,7 +1479,6 @@ export const onAuth = async (url, challenge) => {
|
||||
|
||||
seenChallenges.add(challenge)
|
||||
|
||||
console.log(url, challenge)
|
||||
const event = await signer.get().signAsUser(
|
||||
createEvent(22242, {
|
||||
tags: [
|
||||
@ -1559,10 +1611,14 @@ export const publish = ({forcePlatform = true, ...request}: MyPublishRequest) =>
|
||||
const pub = basePublish(request)
|
||||
|
||||
// Add the event to projections
|
||||
ensureUnwrapped(request.event).then(projections.push)
|
||||
if (canUnwrap(request.event)) {
|
||||
ensureUnwrapped(request.event).then(projections.push)
|
||||
} else {
|
||||
projections.push(request.event)
|
||||
}
|
||||
|
||||
// Listen to updates and update our publish queue
|
||||
if (isGiftWrap(request.event) || request.event.pubkey === pubkey.get()) {
|
||||
if (canUnwrap(request.event) || request.event.pubkey === pubkey.get()) {
|
||||
const pubInfo = omit(["emitter", "result"], pub)
|
||||
|
||||
pub.emitter.on("*", t => publishes.key(pubInfo.id).set(pubInfo))
|
||||
@ -1571,7 +1627,7 @@ export const publish = ({forcePlatform = true, ...request}: MyPublishRequest) =>
|
||||
return pub
|
||||
}
|
||||
|
||||
export const sign = (template, opts: {anonymous?: boolean; sk?: string}) => {
|
||||
export const sign = (template, opts: {anonymous?: boolean; sk?: string} = {}) => {
|
||||
if (opts.anonymous) {
|
||||
return signer.get().signWithKey(template, generatePrivateKey())
|
||||
}
|
||||
@ -2006,9 +2062,7 @@ class Storage {
|
||||
},
|
||||
})
|
||||
|
||||
await Promise.all(
|
||||
this.adapters.map(adapter => adapter.initialize(this))
|
||||
)
|
||||
await Promise.all(this.adapters.map(adapter => adapter.initialize(this)))
|
||||
}
|
||||
|
||||
this.ready.resolve()
|
||||
@ -2070,10 +2124,6 @@ export const storage = new Storage(15, [
|
||||
objectAdapter("plaintext", "key", plaintext, {limit: 100000}),
|
||||
collectionAdapter("publishes", "id", publishes, {sort: sortBy(prop("created_at"))}),
|
||||
collectionAdapter("topics", "name", topics, {limit: 1000, sort: sortBy(prop("last_seen"))}),
|
||||
collectionAdapter("people", "pubkey", people, {
|
||||
limit: 100000,
|
||||
sort: sortBy(prop("last_fetched")),
|
||||
}),
|
||||
collectionAdapter("relays", "url", relays, {limit: 1000, sort: sortBy(prop("count"))}),
|
||||
collectionAdapter("channels", "id", channels, {limit: 1000, sort: sortBy(prop("last_checked"))}),
|
||||
collectionAdapter("groups", "address", groups, {limit: 1000, sort: sortBy(prop("count"))}),
|
||||
|
@ -1,7 +1,6 @@
|
||||
import {switcherFn} from "hurdak"
|
||||
import type {EventTemplate, UnsignedEvent} from "nostr-tools"
|
||||
import {getEventHash} from "nostr-tools"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import type {EventTemplate, OwnedEvent, HashedEvent} from "@welshman/util"
|
||||
import {getPublicKey, getSignature} from "src/util/nostr"
|
||||
import type {Session} from "src/engine/model"
|
||||
import type {Connect} from "./connect"
|
||||
@ -20,10 +19,10 @@ export class Signer {
|
||||
prepWithKey(event: EventTemplate, sk: string) {
|
||||
// Copy the event since we're mutating it
|
||||
event = {...event}
|
||||
;(event as UnsignedEvent).pubkey = getPublicKey(sk)
|
||||
;(event as TrustedEvent).id = getEventHash(event as UnsignedEvent)
|
||||
;(event as OwnedEvent).pubkey = getPublicKey(sk)
|
||||
;(event as HashedEvent).id = getEventHash(event as OwnedEvent)
|
||||
|
||||
return event as TrustedEvent
|
||||
return event as HashedEvent
|
||||
}
|
||||
|
||||
prepAsUser(event: EventTemplate) {
|
||||
@ -31,10 +30,10 @@ export class Signer {
|
||||
|
||||
// Copy the event since we're mutating it
|
||||
event = {...event}
|
||||
;(event as UnsignedEvent).pubkey = pubkey
|
||||
;(event as TrustedEvent).id = getEventHash(event as UnsignedEvent)
|
||||
;(event as OwnedEvent).pubkey = pubkey
|
||||
;(event as HashedEvent).id = getEventHash(event as OwnedEvent)
|
||||
|
||||
return event as TrustedEvent
|
||||
return event as HashedEvent
|
||||
}
|
||||
|
||||
signWithKey(template: EventTemplate, sk: string) {
|
||||
|
@ -7,7 +7,8 @@
|
||||
|
||||
<div class="text-sm">
|
||||
{#each items as item, i}
|
||||
<Chip pad onRemove={() => remove?.(i)}>
|
||||
{@const onRemove = remove ? () => remove(i) : null}
|
||||
<Chip pad {onRemove}>
|
||||
<slot name="item" context="value" {item}>
|
||||
{item}
|
||||
</slot>
|
||||
|
@ -90,7 +90,7 @@
|
||||
</div>
|
||||
<div
|
||||
style={$$props.style || "min-height: 6rem"}
|
||||
class={cx($$props.class, "w-full min-w-0 outline-0")}
|
||||
class={cx($$props.class, "w-full min-w-0 whitespace-pre-line outline-0")}
|
||||
{autofocus}
|
||||
contenteditable
|
||||
bind:this={input}
|
||||
|
@ -3,12 +3,14 @@
|
||||
import Anchor from "src/partials/Anchor.svelte"
|
||||
|
||||
export let inert = false
|
||||
export let active = false
|
||||
</script>
|
||||
|
||||
<Anchor
|
||||
{...$$props}
|
||||
class={cx($$props.class, "block p-3 px-4", {
|
||||
"transition-all hover:bg-accent hover:text-white": !inert,
|
||||
"bg-accent text-neutral-100": active,
|
||||
"transition-all hover:bg-accent hover:text-neutral-100": !inert,
|
||||
})}
|
||||
on:click>
|
||||
<slot />
|
||||
|
41
src/partials/WotScore.svelte
Normal file
41
src/partials/WotScore.svelte
Normal file
@ -0,0 +1,41 @@
|
||||
<style>
|
||||
.wot-background {
|
||||
fill: transparent;
|
||||
stroke: var(--neutral-600);
|
||||
}
|
||||
|
||||
.wot-highlight {
|
||||
fill: transparent;
|
||||
stroke-width: 1.5;
|
||||
stroke-dasharray: 100 100;
|
||||
transform-origin: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import {themeColors} from "src/partials/state"
|
||||
|
||||
export let score
|
||||
export let max = 100
|
||||
export let accent = false
|
||||
|
||||
$: superMaxWot = max * 1.5
|
||||
$: dashOffset = 100 - (Math.max(superMaxWot / 20, score) / superMaxWot) * 100
|
||||
$: style = `transform: rotate(${dashOffset * 1.8 - 50}deg)`
|
||||
$: stroke = $themeColors[accent ? 'accent' : 'neutral-200']
|
||||
</script>
|
||||
|
||||
<span class="relative flex h-10 w-10 items-center justify-center whitespace-nowrap px-4 text-xs">
|
||||
<svg height="32" width="32" class="absolute">
|
||||
<circle class="wot-background" cx="16" cy="16" r="15" />
|
||||
<circle
|
||||
cx="16"
|
||||
cy="16"
|
||||
r="15"
|
||||
class="wot-highlight"
|
||||
stroke-dashoffset={dashOffset}
|
||||
{style}
|
||||
{stroke} />
|
||||
</svg>
|
||||
{score}
|
||||
</span>
|
@ -1,9 +1,14 @@
|
||||
import Bowser from "bowser"
|
||||
import {fromPairs} from "ramda"
|
||||
import {derived} from "svelte/store"
|
||||
import {writable} from "@welshman/lib"
|
||||
import {parseHex} from "src/util/html"
|
||||
import {synced} from "src/util/misc"
|
||||
|
||||
// Browser
|
||||
|
||||
export const browser = Bowser.parse(window.navigator.userAgent)
|
||||
|
||||
// Settings
|
||||
|
||||
export const appName = import.meta.env.VITE_APP_NAME
|
||||
|
@ -157,6 +157,13 @@ export const parseAnythingSync = entity => {
|
||||
}
|
||||
}
|
||||
|
||||
export const parsePubkey = async entity => {
|
||||
const result = await parseAnything(entity)
|
||||
|
||||
if (result.type === 'npub') return result.data
|
||||
if (result.type === 'nprofile') return result.data.pubkey
|
||||
}
|
||||
|
||||
export const getTags =
|
||||
(types: string[], testValue = null) =>
|
||||
(tags: string[][]) =>
|
||||
|
@ -1,24 +1,24 @@
|
||||
import fs from 'fs'
|
||||
import dotenv from 'dotenv'
|
||||
import fs from "fs"
|
||||
import dotenv from "dotenv"
|
||||
import * as path from "path"
|
||||
import {defineConfig} from "vite"
|
||||
import {VitePWA} from "vite-plugin-pwa"
|
||||
import mkcert from "vite-plugin-mkcert"
|
||||
import {favicons} from 'favicons'
|
||||
import {favicons} from "favicons"
|
||||
import htmlPlugin from "vite-plugin-html-config"
|
||||
import sveltePreprocess from "svelte-preprocess"
|
||||
import {svelte} from "@sveltejs/vite-plugin-svelte"
|
||||
import {nodePolyfills} from "vite-plugin-node-polyfills"
|
||||
|
||||
dotenv.config({path: '.env.local'})
|
||||
dotenv.config({path: '.env'})
|
||||
dotenv.config({path: ".env.local"})
|
||||
dotenv.config({path: ".env"})
|
||||
|
||||
const accentColor = process.env.VITE_LIGHT_THEME.match(/accent:(#\w+)/)[1]
|
||||
|
||||
export default defineConfig(async () => {
|
||||
const icons = await favicons('public' + process.env.VITE_APP_LOGO)
|
||||
const icons = await favicons("public" + process.env.VITE_APP_LOGO)
|
||||
|
||||
if (!fs.existsSync('public/icons')) fs.mkdirSync('public/icons')
|
||||
if (!fs.existsSync("public/icons")) fs.mkdirSync("public/icons")
|
||||
|
||||
for (const {name, contents} of icons.images) {
|
||||
fs.writeFileSync(`public/icons/${name}`, contents, "binary")
|
||||
@ -72,19 +72,49 @@ export default defineConfig(async () => {
|
||||
{rel: "apple-touch-icon", sizes: "144x144", href: "/icons/apple-touch-icon-144x144.png"},
|
||||
{rel: "apple-touch-icon", sizes: "152x152", href: "/icons/apple-touch-icon-152x152.png"},
|
||||
{rel: "apple-touch-icon", sizes: "180x180", href: "/icons/apple-touch-icon-180x180.png"},
|
||||
{rel: "icon", type: "image/png", sizes: "192x192", href: "/icons/android-icon-192x192.png"},
|
||||
{
|
||||
rel: "icon",
|
||||
type: "image/png",
|
||||
sizes: "192x192",
|
||||
href: "/icons/android-icon-192x192.png",
|
||||
},
|
||||
{rel: "icon", type: "image/png", sizes: "32x32", href: "/icons/favicon-32x32.png"},
|
||||
{rel: "icon", type: "image/png", sizes: "96x96", href: "/icons/favicon-96x96.png"},
|
||||
{rel: "icon", type: "image/png", sizes: "16x16", href: "/icons/favicon-16x16.png"},
|
||||
{rel: "mask-icon", href: "/images/logo.svg", color: "#FFFFFF"},
|
||||
|
||||
{rel: "icon", type: "image/png", sizes: "144x144", href: "/icons/android-chrome-144x144.png"},
|
||||
{rel: "icon", type: "image/png", sizes: "192x192", href: "/icons/android-chrome-192x192.png"},
|
||||
{rel: "icon", type: "image/png", sizes: "256x256", href: "/icons/android-chrome-256x256.png"},
|
||||
{
|
||||
rel: "icon",
|
||||
type: "image/png",
|
||||
sizes: "144x144",
|
||||
href: "/icons/android-chrome-144x144.png",
|
||||
},
|
||||
{
|
||||
rel: "icon",
|
||||
type: "image/png",
|
||||
sizes: "192x192",
|
||||
href: "/icons/android-chrome-192x192.png",
|
||||
},
|
||||
{
|
||||
rel: "icon",
|
||||
type: "image/png",
|
||||
sizes: "256x256",
|
||||
href: "/icons/android-chrome-256x256.png",
|
||||
},
|
||||
{rel: "icon", type: "image/png", sizes: "36x36", href: "/icons/android-chrome-36x36.png"},
|
||||
{rel: "icon", type: "image/png", sizes: "384x384", href: "/icons/android-chrome-384x384.png"},
|
||||
{
|
||||
rel: "icon",
|
||||
type: "image/png",
|
||||
sizes: "384x384",
|
||||
href: "/icons/android-chrome-384x384.png",
|
||||
},
|
||||
{rel: "icon", type: "image/png", sizes: "48x48", href: "/icons/android-chrome-48x48.png"},
|
||||
{rel: "icon", type: "image/png", sizes: "512x512", href: "/icons/android-chrome-512x512.png"},
|
||||
{
|
||||
rel: "icon",
|
||||
type: "image/png",
|
||||
sizes: "512x512",
|
||||
href: "/icons/android-chrome-512x512.png",
|
||||
},
|
||||
{rel: "icon", type: "image/png", sizes: "72x72", href: "/icons/android-chrome-72x72.png"},
|
||||
{rel: "icon", type: "image/png", sizes: "96x96", href: "/icons/android-chrome-96x96.png"},
|
||||
{rel: "apple-touch-icon", sizes: "1024x1024", href: "apple-touch-icon-1024x1024.png"},
|
||||
@ -109,11 +139,17 @@ export default defineConfig(async () => {
|
||||
description: process.env.VITE_APP_DESCRIPTION,
|
||||
theme_color: accentColor,
|
||||
protocol_handlers: [{protocol: "web+nostr", url: "/%s"}],
|
||||
permissions: ["clipboardRead", "clipboardWrite", "unlimitedStorage"],
|
||||
icons: [
|
||||
{src: "images/pwa-64x64.png", sizes: "64x64", type: "image/png"},
|
||||
{src: "images/pwa-192x192.png", sizes: "192x192", type: "image/png"},
|
||||
{src: "images/pwa-512x512.png", sizes: "512x512", type: "image/png", purpose: "any"},
|
||||
{src: "images/maskable-icon-512x512.png", sizes: "512x512", type: "image/png", purpose: "maskable"},
|
||||
{
|
||||
src: "images/maskable-icon-512x512.png",
|
||||
sizes: "512x512",
|
||||
type: "image/png",
|
||||
purpose: "maskable",
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
|
Loading…
Reference in New Issue
Block a user