Browse Source

SSL Check and Kind 3 Management (#219)

* Add relay.nostr.scot

Add relay.nostr.scot

* Add rsslay.nostr.moe (#210)

A custom fork of the rss-bridge relay of fiatjaf [piraces/rsslay](https://github.com/piraces/rsslay)

Co-authored-by: Sandwich <299465+dskvr@users.noreply.github.com>

* remove e.e.

* kind3 editing, ssl, various improvements

* working relay list editor

* remove debugging

* move buttons

* show/hide button for logged in/out

* bugfix

* fix column issue

* remove debugging

* reduce wait time

* disable edit button while relays are refreshing

* add some gates

* resets after save

Co-authored-by: Paul Rollo <kingrollo@gmail.com>
Co-authored-by: Raul Piraces Alastuey <raul.piraces@gmail.com>
Co-authored-by: dskvr <dskvr@users.noreply.github.com>
develop
Sandwich 2 years ago
committed by dskvr
parent
commit
98872dc4dd
  1. 8
      Dockerfile
  2. 4
      package.json
  3. 2
      relays.yaml
  4. 5
      src/components/partials/AboutNostrWatch.vue
  5. 2
      src/components/partials/DarkMode.vue
  6. 2
      src/components/relays/blocks/MapSummary.vue
  7. 139
      src/components/relays/blocks/RelaysResultTable.vue
  8. 3
      src/components/relays/nav/RelaysFindNav.vue
  9. 8
      src/components/relays/pages/RelaysFind.vue
  10. 57
      src/components/relays/pages/RelaysSingle.vue
  11. 191
      src/components/relays/partials/NostrSync.vue
  12. 53
      src/components/relays/partials/NostrSyncPopoverNag.vue
  13. 75
      src/components/relays/tasks/RefreshTask.vue
  14. 3
      src/components/relays/tasks/TasksManager.vue
  15. 125
      src/components/relays/tasks/UserRelayList.vue
  16. 28
      src/shared/relays-lib.js
  17. 2
      src/shared/user-lib.js
  18. 4
      src/store/layout.js
  19. 5
      src/store/prefs.js
  20. 54
      src/store/relays.js
  21. 68
      src/store/user.js
  22. 56
      src/styles/main.scss
  23. 1
      tailwind.config.js
  24. 5
      vue.config.js
  25. 2
      yarn.lock

8
Dockerfile

@ -6,9 +6,13 @@ COPY . /app/
RUN yarn && yarn build
#RUN yarn global add yaml2json
RUN yarn global add yaml2json yaml-doctor
#RUN yaml2json relays.yaml > dist/relays.json
RUN yaml-doctor relays.yaml --fix
RUN echo $(cat relays.yaml)
RUN yaml2json relays.yaml > dist/relays.json
FROM nginx:stable-alpine as nginx-nostr-relay-registry

4
package.json

@ -4,14 +4,13 @@
"private": true,
"scripts": {
"build": "vue-cli-service build",
"prebuild": "yarn get:relays && yarn get:geo",
"get:relays": "node ./scripts/relays.js",
"get:geo": "node ./scripts/geo.js",
"serve": "vue-cli-service serve --host localhost",
"lint": "vue-cli-service lint",
"docker": "yarn docker:build && yarn docker:tag && yarn docker:push",
"docker:up": "docker-compose up",
"docker:build": "docker build . -t nostr-relay-status",
"docker:build": "docker build . -t nostr-relay-status --progress=plain",
"docker:tag": "docker tag nostr-relay-status:latest registry.digitalocean.com/sandwich-farm/nostr-relay-status",
"docker:push": "docker push registry.digitalocean.com/sandwich-farm/nostr-relay-status"
},
@ -35,6 +34,7 @@
"nostr": "0.2.5",
"nostr-relay-inspector": "0.0.25",
"nostr-tools": "1.1.1",
"object-hash": "3.0.0",
"object-sizeof": "1.6.3",
"onion-regex": "2.0.8",
"pinia": "2.0.28",

2
relays.yaml

@ -210,4 +210,6 @@ relays:
- wss://nostr.kollider.xyz
- wss://relay.nostr.band
- wss://relay.nosphr.com
- wss://rsslay.nostr.moe
- wss://relay.nostr.scot
- wss://student.chadpolytechnic.com

5
src/components/partials/AboutNostrWatch.vue

@ -6,8 +6,9 @@
<div class="font-bold text-md pb-3">
I launched nostr.watch as a prototype for fun
and a few weeks later nostr took off. Built
with Vue3/Pinia, Tailwind and nostr-relay-inspector.
<span class="italic text-white/50">Thank you for giving me something positive to focus on.</span>
with Vue3/Pinia, Tailwind and nostr-tools,
nostr-js and nostr-relay-inspector. Fork of
nostr-relay-registry; rewrite.
</div>
<div class="border-t text-md pt-3 border-slate-700 ">
<img alt="sandwich" src="https://cloudflare-ipfs.com/ipfs/QmeCYiohwh8mKgYTaBNXHQ8etRJpVYpiBSqhdkVx5dWP4a" class="block m-auto h-14 w-14 rounded-full mb-2" />

2
src/components/partials/DarkMode.vue

@ -14,7 +14,7 @@ import { useDark, useToggle } from "@vueuse/core";
const isDark = useDark({
selector: "html", //element to add attribute to
attribute: "theme", // attribute name
// attribute: "theme", // attribute name
valueDark: "dark", // attribute value for dark mode
valueLight: "light", // attribute value for light mode
})

2
src/components/relays/blocks/MapSummary.vue

@ -41,7 +41,7 @@
<div class="mb-10 w-min">
<div class="text-slate-800 text-3xl block py-1 text-center">
<span @click="copy" class="py-1px-2">{{ relay }}</span>
<a href="#" @click="$router.push(`/relay/${cleanUrl(relay)}`)" class="block text-sm mb-3">Status Page</a>
<a href="#" @click="$router.push(`/relay/${getHostname(relay)}`)" class="block text-sm mb-3">Status Page</a>
<img class="inline-block mr-1" :src="badgeCheck(relay, 'connect')" />
<img class="inline-block mr-1" :src="badgeCheck(relay, 'read')" />
<img class="inline-block mr-1" :src="badgeCheck(relay, 'write')" />

139
src/components/relays/blocks/RelaysResultTable.vue

@ -1,5 +1,5 @@
<template>
<div class="pt-0 px-1 sm:px-6 lg:px-8">
<div class="pt-0 px-1 sm:px-6 lg:px-8 dark:bg-black/20 rounded-lg">
<div class="mt-8 flex flex-col">
<div class="overflow-x-auto">
<div class="inline-block min-w-full align-middle" v-if="subsectionRelays.length">
@ -18,8 +18,15 @@
<span
:class="getThemeBtnClass('spacious')"
@click="store.prefs.setRowTheme('spacious')">spacious</span>
<a
v-if="!store.layout.editorExpanded"
class="text-left inline-block underline text-xs ml-10"
@click="this.setRandomRelay"
target="_blank"
:href="`/relay/${relayClean(randomRelay)}`">
random relay
</a>
</span>
<NostrSyncPopoverNag v-if="subsection == 'favorite'" />
<span v-if="subsection != 'favorite' && store.relays.getFavorites.length" class="ml-6 text-slate-600">
<input type="checkbox" class=" cursor-pointer relative top-0.5 mr-1" id="relays-pin-favorites" v-model="store.prefs.pinFavorites" />
<label class="cursor-pointer font-thin text-xs" for="relays-pin-favorites">
@ -27,35 +34,44 @@
</label>
</span>
</th>
<th v-if="store.layout.editorIsExpanded && isLoggedIn" scope="col" class="hidden md:table-cell lg:table-cell xl:table-cell verified">
<!-- <span class="verified-shape-wrapper">
<span class="shape verified"></span>
</span> -->
<code class="text-xs block">Read</code>
</th>
<th v-if="store.layout.editorIsExpanded && isLoggedIn" scope="col" class="hidden md:table-cell lg:table-cell xl:table-cell verified">
<!-- <span class="verified-shape-wrapper">
<span class="shape verified"></span>
</span> -->
<code class="text-xs block">Write</code>
</th>
<!-- <th scope="col" class="relative py-3.5 pl-0 pr-0 sm:pr-0" v-if="isLoggedIn()">
<code class="text-xs block">Upvote</code>
</th> -->
<th scope="col" class="hidden md:table-cell lg:table-cell xl:table-cell verified">
<th v-if="!store.layout.editorIsExpanded || !isLoggedIn" scope="col" class="hidden md:table-cell lg:table-cell xl:table-cell verified">
<!-- <span class="verified-shape-wrapper">
<span class="shape verified"></span>
</span> -->
<code class="text-xs block">NIP-11</code>
</th>
<th scope="col" class="location text-center" v-tooltip:top.tooltip="'Detected location of Relay'">
<th v-if="!store.layout.editorIsExpanded || !isLoggedIn" scope="col" class="location text-center" v-tooltip:top.tooltip="'Detected location of Relay'">
<code class="text-xs block">Location</code>
<!-- 🌎 -->
</th>
<th scope="col" class="latency text-center" v-tooltip:top.tooltip="'Relay Latency on Read'">
<th v-if="!store.layout.editorIsExpanded || !isLoggedIn" scope="col" class="latency text-center" v-tooltip:top.tooltip="'Relay Latency on Read'">
<code class="text-xs block">Latency</code>
<!-- -->
</th>
<th scope="col" class="hidden md:table-cell lg:table-cell xl:table-cell connect text-center" v-tooltip:top.tooltip="'Relay connection status'">
<th v-if="!store.layout.editorIsExpanded || !isLoggedIn" scope="col" class="hidden md:table-cell lg:table-cell xl:table-cell connect text-center" v-tooltip:top.tooltip="'Relay connection status'">
<code class="text-xs block">Connect</code>
<!-- 🔌 -->
</th>
<th scope="col" class="hidden md:table-cell lg:table-cell xl:table-cell first-line:read text-center" v-tooltip:top.tooltip="'Relay read status'">
<th v-if="!store.layout.editorIsExpanded || !isLoggedIn" scope="col" class="hidden md:table-cell lg:table-cell xl:table-cell first-line:read text-center" v-tooltip:top.tooltip="'Relay read status'">
<code class="text-xs block">Read</code>
<!-- 👁🗨 -->
</th>
<th scope="col" class="hidden md:table-cell lg:table-cell xl:table-cell write text-center" v-tooltip:top.tooltip="'Relay write status'">
<th v-if="!store.layout.editorIsExpanded && isLoggedIn" scope="col" class="hidden md:table-cell lg:table-cell xl:table-cell write text-center" v-tooltip:top.tooltip="'Relay write status'">
<code class="text-xs block">Write</code>
<!-- -->
</th>
<th scope="col" class="relative py-3.5 pl-0 pr-0 sm:pr-0">
<code class="text-xs block">Favorite</code>
@ -86,40 +102,81 @@
</a>
</td> -->
<td class="w-12 verified text-center md:table-cell lg:table-cell xl:table-cell">
<td v-if="!store.layout.editorIsExpanded || !isLoggedIn" class="w-12 verified text-center md:table-cell lg:table-cell xl:table-cell">
<span v-if="this.results[relay]?.identities">
<span v-tooltip:top.tooltip="identityList(relay)"> <span class="verified-shape-wrapper cursor-pointer" v-if="Object.entries(results[relay]?.identities).length"><span class="shape verified"></span></span></span>
</span>
</td>
<td class="w-24 location text-center">{{ getFlag(relay) }}</td>
<td v-if="!store.layout.editorIsExpanded || !isLoggedIn" class="w-24 location text-center">
{{ getFlag(relay) }}
</td>
<td class="w-24 latency text-center">
<td v-if="!store.layout.editorIsExpanded || !isLoggedIn" class="w-24 latency text-center">
<span>{{ results[relay]?.latency?.final }}<span v-if="results[relay]?.check?.latency">ms</span></span>
</td>
<!-- .indicator {
display:block;
margin: 0 auto;
height: 14px;
width: 14px;
border-radius: 7px;
border-width:0px;
} -->
<td class="w-16 content-center text-center hidden md:table-cell lg:table-cell xl:table-cell" :key="generateKey(relay, 'check.connect')">
<td v-if="!store.layout.editorIsExpanded || !isLoggedIn" class="w-16 content-center text-center hidden md:table-cell lg:table-cell xl:table-cell" :key="generateKey(relay, 'check.connect')">
<span class="m-auto block" :class="getCheckIndicator(relay, 'connect')">&nbsp;</span>
</td>
<td class="w-16 content-center text-center hidden md:table-cell lg:table-cell xl:table-cell" :key="generateKey(relay, 'check.read')">
<td v-if="!store.layout.editorIsExpanded || !isLoggedIn" class="w-16 content-center text-center hidden md:table-cell lg:table-cell xl:table-cell" :key="generateKey(relay, 'check.read')">
<span class="m-auto block" :class="getCheckIndicator(relay, 'read')">&nbsp;</span>
</td>
<td class="w-16 content-center text-center hidden md:table-cell lg:table-cell xl:table-cell" :key="generateKey(relay, 'check.write')">
<td v-if="!store.layout.editorIsExpanded || !isLoggedIn" class="w-16 content-center text-center hidden md:table-cell lg:table-cell xl:table-cell" :key="generateKey(relay, 'check.write')">
<span class="m-auto block" :class="getCheckIndicator(relay, 'write')">&nbsp;</span>
</td>
<td class="w-16 fav text-center">
<!-- editor -->
<td v-if="store.tasks.getActiveSlug != 'user/relay/list'
&& store.layout.editorIsExpanded
&& typeof store.user.kind3?.[relay]?.read !== `undefined`
&& isLoggedIn"
class="text-center md:table-cell lg:table-cell xl:table-cell">
<Switch
v-model="store.user.kind3[relay].read"
class="group relative inline-flex h-5 w-10 flex-shrink-0 cursor-pointer items-center justify-center rounded-full focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2">
<span class="sr-only">Use setting</span>
<span aria-hidden="true" class="pointer-events-none absolute h-full w-full rounded-md bg-white dark:bg-black/10" />
<span aria-hidden="true" :class="[store.user.kind3[relay].read ? 'bg-indigo-600' : 'bg-gray-200 dark:bg-black', 'pointer-events-none absolute mx-auto h-4 w-9 rounded-full transition-colors duration-200 ease-in-out']" />
<span aria-hidden="true" :class="[store.user.kind3[relay].read ? 'translate-x-5' : 'translate-x-0', 'pointer-events-none absolute left-0 inline-block h-5 w-5 transform rounded-full border border-gray-200 bg-white shadow ring-0 transition-transform duration-200 ease-in-out']" />
</Switch>
</td>
<td v-if="store.tasks.getActiveSlug != 'user/relay/list'
&& store.layout.editorIsExpanded
&& typeof store.user.kind3?.[relay]?.write !== `undefined`
&& isLoggedIn"
class="text-center md:table-cell lg:table-cell xl:table-cell">
<Switch
v-model="store.user.kind3[relay].write"
class="group relative inline-flex h-5 w-10 flex-shrink-0 cursor-pointer items-center justify-center rounded-full
focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2">
<span class="sr-only">Use setting</span>
<span aria-hidden="true" class="pointer-events-none absolute h-full w-full rounded-md bg-white dark:bg-black/10" />
<span aria-hidden="true" :class="[store.user.kind3[relay].write ? 'bg-indigo-600' : 'bg-gray-200 dark:bg-black', 'pointer-events-none absolute mx-auto h-4 w-9 rounded-full transition-colors duration-200 ease-in-out']" />
<span aria-hidden="true" :class="[store.user.kind3[relay].write ? 'translate-x-5' : 'translate-x-0', 'pointer-events-none absolute left-0 inline-block h-5 w-5 transform rounded-full border border-gray-200 bg-white shadow ring-0 transition-transform duration-200 ease-in-out']" />
</Switch>
</td>
<td
colspan="2"
v-if="store.layout.editorExpanded && store.tasks.getActiveSlug == 'user/relay/list'"
class="w-auto text-center md:table-cell lg:table-cell xl:table-cell">
<svg class="animate-spin mr-1 -mt-0.5 h-4 w-5 text-white inline" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</td>
<td
colspan="2"
v-if="store.layout.editorExpanded && !store.relays.isFavorite(relay) && store.tasks.getActiveSlug != 'user/relay/list'"
class="w-auto text-center md:table-cell lg:table-cell xl:table-cell">
</td>
<td class="w-16 fav text-center">
<a
class="hover:opacity-100 cursor-pointer"
:class="store.relays.isFavorite(relay) ? 'opacity-100' : 'opacity-10'"
@ -146,9 +203,9 @@
import { countryCodeEmoji } from 'country-code-emoji';
import emoji from 'node-emoji';
import crypto from 'crypto'
import { Switch } from '@headlessui/vue'
// import SingleClearnet from '@/components/relays/SingleClearnet.vue'
import NostrSyncPopoverNag from '@/components/relays/partials/NostrSyncPopoverNag.vue'
import RelaysLib from '@/shared/relays-lib.js'
import UserLib from '@/shared/user-lib.js'
@ -159,6 +216,9 @@
import { setupStore } from '@/store'
const localMethods = {
setRandomRelay(){
this.randomRelay = this.store.relays.getShuffledPublic[0]
},
async likeRelay(relay){
const id = this.store.relays.getCanonical(relay)
const event = {
@ -187,7 +247,7 @@
name: 'ListClearnet',
components: {
// SingleClearnet,
NostrSyncPopoverNag
Switch,
},
setup(props){
const {subsectionProp: subsection} = toRefs(props)
@ -204,24 +264,13 @@
mounted(){
//console.log('navdata', this.navData, this.navData.filter( item => item.slug == this.subsection )[0], this.navData.filter( item => item.slug == this.subsection ))
this.activePageData = this.navData.filter( item => item.slug == this.subsection )[0]
// if(screenIs('sm')){
// this.user.prefs.setRowTheme('compact')
// }
// if(screenIs('md')){
// this.user.prefs.setRowTheme('comfortable')
// }
this.setRandomRelay()
},
updated(){
// //console.log('state, updated')
},
beforeUnmount(){
//console.log('relays list', 'beforeUnmount()', this.subsection)
},
unmounted(){
//console.log('relays list unmounted', this.subsection)
delete this.results
},
props: {
@ -250,7 +299,9 @@
relays: [],
timeout: null,
navData: this.store.layout.getNavGroup('relays/find'),
activePageData: {}
activePageData: {},
randomRelay: "",
inputDetected: false,
}
},
computed: {

3
src/components/relays/nav/RelaysFindNav.vue

@ -1,4 +1,5 @@
<template>
<div>
<Disclosure as="nav" id="subsection_nav" class="bg-white mb-5" v-slot="{ open }">
<div class="mx-auto max-w-7xl px-0">
<div class="flex h-12 justify-center md:justify-between">
@ -44,6 +45,7 @@
</div>
</DisclosurePanel>
</Disclosure>
</div>
</template>
<script>
@ -102,6 +104,7 @@ computed: {
return (slug) => {
// //console.log('active?', this.contentIsActive(slug), this.isActive(slug), this.store.layout.getActive('relays/find'), this.store.layout.getActiveItem == slug)
return {
// 'opacity-10' : this.store.layout.editorExpanded,
'py-1 px-2': this.store.prefs.getTheme === 'compact',
'text-lg py-2 px-3': this.store.prefs.getTheme === 'comfortable',
'text-xl py-3 px-4': this.store.prefs.getTheme === 'spacious',

8
src/components/relays/pages/RelaysFind.vue

@ -27,7 +27,9 @@
</p>
</div>
<div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
<NostrSync />
<button
v-if="!store.layout.editorExpanded"
@click="$router.push('/relays/add')"
type="button"
class="inline-flex items-center justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-m font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 sm:w-auto">
@ -62,15 +64,16 @@ import { setupStore } from '@/store'
import RelaysLib from '@/shared/relays-lib.js'
import SharedComputed from '@/shared/computed.js'
import { parseHash } from '@/shared/hash-router.js'
import NostrSync from '@/components/relays/partials/NostrSync.vue'
//components
// import RelaysNav from '@/components/relays/nav/RelaysNav.vue'
// import RelaysFindNav from '@/components/relays/nav/RelaysFindNav.vue'
// import RelaysResultTable from '@/components/relays/blocks/RelaysResultTable.vue'
// import MapSummary from '@/components/relays/blocks/MapSummary.vue'
import { relays } from '../../../../relays.yaml'
import { geo } from '../../../../cache/geo.yaml'
const localMethods = {}
const MapSummary = defineAsyncComponent(() =>
@ -97,6 +100,7 @@ export default defineComponent({
RelaysFindNav,
MapSummary,
RelaysResultTable,
NostrSync,
},
setup(){

57
src/components/relays/pages/RelaysSingle.vue

@ -18,38 +18,42 @@
<section v-if="result">
<div class="data-card overflow-hidden bg-slate-100 shadow sm:rounded-lg">
<div id="title_card" class="data-card overflow-hidden sm:rounded-lg mb-8 pt-5" style="background:transparent">
<div class="px-4 py-5 sm:px-6">
<h1>{{geo?.countryCode ? getFlag : ''}}<span @click="copy(relayFromUrl)">{{ relayFromUrl }}</span></h1>
<h1 class="font-light text-3xl md:text-4xl xl:text-7xl">{{geo?.countryCode ? getFlag : ''}} <span @click="copy(relayFromUrl)">{{ relayFromUrl }}</span></h1>
<p class="mt-1 w-auto text-xl text-gray-500" v-if="result?.info?.description">{{ result.info.description }}</p>
</div>
<a
target="_blank"
:href="`https://www.ssllabs.com/ssltest/analyze.html?d=${ getHostname(relay) }`"
class="inline-block py-2 px-3 bg-black/10 first-line:font-bold text-black dark:bg-white/50 dark:text-white ">
Check SSL
</a>
</div>
<!-- this.result?.check?.[which] ? 'green' : 'red' -->
<div class="flex">
<div id="status" class="flex mb-2 py-5"> <!--something is weird here with margin-->
<div v-for="key in ['connect', 'read', 'write']" :key="key" class="text-white text-lg md:text-xl lg:text-3xl flex-1 block py-6" :class="check(key)">
<span>{{key}}</span>
<span>{{key}}</span>
</div>
</div>
<div v-if="!result?.check?.connect">
<div class="data-card block mt-1 py-24 w-auto bg-white border border-gray-200 rounded-lg shadow-md dark:bg-gray-800 dark:border-gray-700" v-if="!result?.check?.connect">
<div id="did_not_connect" v-if="!result?.check?.connect" class="mb-8 py-8">
<div class="data-card block mt-8 py-24 w-auto bg-white border border-gray-200 rounded-lg shadow-md dark:bg-gray-800 dark:border-gray-700" v-if="!result?.check?.connect">
<h5 class="mb-2 text-2xl font-bold tracking-tight text-red-600 dark:text-red-300">This Relay Appears to be offline</h5>
</div>
<div class="flex bg-slate-50 shadow mt-12" v-if="Object.keys(this.result?.geo).length">
<div class="flex bg-slate-50 shadow mt-8" v-if="Object.keys(this.result?.geo).length">
<div class="text-slate-800 text-3xl flex-none w-full block py-1 text-center">
I did find this...
</div>
</div>
</div>
<div v-if="result?.info?.supported_nips" class="mb-10 overflow-hidden bg-slate-400 border-slate-200 shadow sm:rounded-lg">
<div id="nips" v-if="result?.info?.supported_nips" class="mb-8 py-1 overflow-hidden bg-slate-400 border-slate-200 shadow sm:rounded-lg dark:bg-slate-800">
<div class="px-1 py-2 sm:px-6">
<div class="flex">
<div class="flex-none">
<h3 class="text-lg md:text-lg lg:text-xl xl:text-3xl mb-2 px-2 align-middle mt-4 font-black">nips</h3>
<div class="lg:flex">
<div class="flex-none lg:flex-initial">
<h3 class="inline-block lg:block text-lg md:text-lg lg:text-xl xl:text-3xl mb-2 px-2 align-middle mt-4 font-black">nips</h3>
</div>
<a target="_blank" :href="nipLink(key)" v-for="key in result?.info?.supported_nips" :key="`nip-${key}`"
class="hover:bg-slate-300 hover:shadow pointer-cursor flex-initial gap-4 text-slate-800 text-1xl w-1/5 inline-block py-6 ">
@ -59,8 +63,8 @@
</div>
</div>
<div class="data-card flex sm:rounded-lg bg-slate-50 border-slate-200 mb-10" v-if="geo?.dns">
<div class="text-slate-800 text-3xl flex-none w-full block py-1 text-center">
<div class="data-card flex sm:rounded-lg bg-slate-50 border-slate-200 mb-8 py-8" v-if="geo?.dns">
<div class="text-slate-800 text-lg md:text-xl lg:text-3xl flex-none w-full block py-1 text-center">
<span>
The IP of <strong>{{ geo?.dns.name }}</strong> is <strong>{{ geo?.dns.data }}</strong> <br />
<em>{{ geo?.dns.data }}</em> appears to be in <strong>{{ geo?.city }} {{ geo?.country }}.</strong> <br />
@ -69,15 +73,15 @@
</div>
</div>
<div class="data-card flex sm:rounded-lg bg-slate-50 border-slate-200 border mb-10" v-if="this.result?.info?.software">
<div class="text-slate-800 text-3xl flex-none w-full block py-1 text-center">
<div class="data-card flex sm:rounded-lg bg-slate-50 border-slate-200 border mb-8 py-8" v-if="this.result?.info?.software">
<div class="text-slate-800 text-lg md:text-xl lg:text-3xl flex-none w-full block py-1 text-center">
<span>
The current date/time in <strong>{{ geo?.city }}</strong> is <strong>{{ getLocalTime }}</strong>
It's <strong>{{ getLocalTime }}</strong> in <strong>{{ geo?.city }}</strong>
</span>
</div>
</div>
<div class="data-card flex sm:rounded-lg bg-slate-50 border-slate-200 shadow" v-if="this.result?.info?.software">
<div class="data-card flex sm:rounded-lg bg-slate-50 border-slate-200 shadow mb-8 py-8" v-if="this.result?.info?.software">
<div class="text-clip overflow-ellipsis text-slate-800 text-lg md:text-xl lg:text-3xl flex-none w-full block py-1 text-center">
It's running <strong>{{ getSoftware }}:{{ result.info.version }}</strong>
</div>
@ -90,13 +94,13 @@
</div>
</div> -->
<div class="data-card flex bg-slate-50 border-slate-200 mt-12 shadow" v-if="this.result?.info?.pubkey">
<div class="data-card flex bg-slate-50 border-slate-200 shadow mb-8 py-5" v-if="this.result?.info?.pubkey">
<div class="text-slate-800 w-full text-sm md:text-lg lg:text-3xl overflow-ellipsis flex-none block py-1 text-center">
<code class="block">{{ this.result?.info.pubkey }}</code>
</div>
</div>
<div class="data-card flex bg-slate-50 border-slate-200 shadow mt-12" v-if="this.result?.info?.pubkey">
<div class="data-card flex bg-slate-50 border-slate-200 shadow mt-12 mb-8 py-5" v-if="this.result?.info?.pubkey">
<div class="text-slate-800 w-full flex-none block py-1 text-center">
Here's the details...
</div>
@ -582,9 +586,9 @@ export default defineComponent({
getLocalTime: function(){
let options = {
timeZone: this.geo?.timezone,
year: 'numeric',
month: 'numeric',
day: 'numeric',
// year: 'numeric',
// month: 'numeric',
// day: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
@ -595,7 +599,8 @@ export default defineComponent({
getSoftware: function(){
return this.result?.info?.software
},
cleanUrl: function(){
getHostname: function(){
return (relay) => relay.replace('wss://', '')
},
@ -701,5 +706,5 @@ body, .grid-Column { padding:0; margin:0; }
/* #relay-wrapper { margin: 50px 0 20px; padding: 20px 0} */
h1 {cursor:pointer;font-size:40pt; margin: 0px 0 15px; padding:0 0 10px; border-bottom:3px solid #e9e9e9}
/* h1 {cursor:pointer;font-size:40pt; margin: 0px 0 15px; padding:0 0 10px; border-bottom:3px solid #e9e9e9} */
</style>

191
src/components/relays/partials/NostrSync.vue

@ -0,0 +1,191 @@
<template>
<div class="inline" v-if="store.user.getPublicKey.length">
<div class="inline text-left">
<span v-if="savedSuccess" class="inline-block mr-3">
<svg class="h-4 w-4 inline-block" fill="none" stroke="#32CD32" stroke-width="1.5" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
Saved to <span>{{ savedSuccess }}</span>
</span>
<button
:title="this.store.tasks.getActiveSlug === 'relays/check' ? 'disabled while relays are checking' : ''"
ref="btnRef"
type="button"
v-on:click="this.store.tasks.getActiveSlug === 'relays/check' ? false : toggleEditor()"
:class="{
'cursor-not-allowed opacity-40': this.store.tasks.getActiveSlug === 'relays/check',
'cursor-pointer': this.store.tasks.getActiveSlug === 'user/relay/list'
}"
class="mr-3 inline-flex items-center justify-center rounded-md border border-transparent bg-white/20 px-4 py-2 text-m font-medium text-white shadow-sm hover:bg-white/40 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 sm:w-auto">
<span v-if="this.store.layout.editorExpanded">
Cancel
</span>
<span v-if="!this.store.layout.editorExpanded">
Edit Relay List
</span>
</button>
<button
:title="!changed ? 'nothing to save' : ''"
ref="btnRef"
type="button"
v-on:click="changed ? persistChanges() : false"
v-if="this.store.layout.editorExpanded && store.tasks.getActiveSlug !== 'user/relay/list'"
:class="{
'cursor-not-allowed opacity-40': !changed,
'cursor-pointer': changed
}"
class="mr-3 inline-flex items-center justify-center rounded-md border border-transparent bg-white/20 px-4 py-2 text-m font-medium text-white shadow-sm hover:bg-white/40 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 sm:w-auto">
Save
</button>
</div>
</div>
</template>
<script>
// import { createPopper } from "@popperjs/core";
import { defineComponent, toRefs } from 'vue'
import { setupStore } from '@/store/'
import safeStringify from 'fast-safe-stringify'
import { getEventHash, validateEvent, verifySignature } from 'nostr-tools'
import RelaysLib from '@/shared/relays-lib'
import { RelayPool } from 'nostr'
import objHash from 'object-hash'
export default defineComponent({
name: "NostrSyncPopoverNag",
setup(props){
const {editorProp: editor} = toRefs(props)
return {
store : setupStore(),
editor: editor
}
},
data() {
return {
changed: false,
hashCache: null,
hashOG: null,
savedTo: [],
savedSuccess: null,
interval: null,
// editor: false,
// popoverShow: false
}
},
mounted(){
this.hashOG = objHash(structuredClone(this.store.user.getKind3))
this.hashCache = structuredClone(this.hashOG)
this.store.layout.editorOff()
this.interval = setInterval( () => {
if(this.savedTo.length)
this.savedSuccess = this.savedTo.shift()
else
this.savedSuccess = null
const hashCurrent = objHash(this.store.user.getKind3),
hashCache = this.hashCache,
hashOG = this.hashOG
if(hashCache === hashCurrent)
return
if(hashOG === hashCurrent )
return this.changed = false
console.log('input cache did not match', hashCache)
console.log(
'changed?',
this.changed,
'ok..',
hashCache,
objHash(this.store.user.getKind3),
hashCache == objHash(this.store.user.getKind3)
)
this.hashCache = objHash(structuredClone(this.store.user.getKind3))
this.changed = true
}, 500)
},
unmounted(){
clearInterval(this.interval)
this.store.layout.editorOff()
},
methods: Object.assign(RelaysLib, {
toggleEditor: async function(){
this.store.layout.toggleEditor()
if(this.store.layout.editorExpanded)
this.queueJob(
'user/relay/list',
async () => {
await this.store.user.setKind3()
.then( () => {
Object.keys(this.store.user.kind3).forEach( key => {
this.store.relays.setFavorite(key)
})
this.store.tasks.completeJob()
})
.catch( err => {
console.error('error!', err)
this.store.tasks.completeJob()
})
},
true
)
},
persistChanges: async function(){
const event = {
created_at: Math.floor(Date.now()/1000),
kind: 3,
content: safeStringify(this.store.user.kind3),
tags: [...this.store.user.kind3Event.tags],
pubkey: this.store.user.getPublicKey,
}
event.id = getEventHash(event)
console.log('kind3 event', event)
console.log(window.nostr, typeof window.nostr.signEvent)
const signedEvent = await window.nostr.signEvent(structuredClone(event))
let ok = validateEvent(signedEvent)
let veryOk = verifySignature(signedEvent)
if(!ok || !veryOk)
return
console.log('valid event?', ok, veryOk)
const pool = new RelayPool( Object.keys(this.store.user.kind3) )
pool.on('open', relay=>{
relay.send(['EVENT', signedEvent])
})
pool.on('ok', relay => {
this.savedTo.push(relay.url)
})
this.hashOG = objHash(JSON.parse(event.content))
this.hashCache = this.hashOG
this.changed = false
},
togglePopover: function(){
// if(this.popoverShow){
// this.popoverShow = false;
// } else {
// this.popoverShow = true;
// createPopper(this.$refs.btnRef, this.$refs.popoverRef, {
// placement: "left"
// });
// }
},
})
})
</script>

53
src/components/relays/partials/NostrSyncPopoverNag.vue

@ -1,53 +0,0 @@
<template>
<div class="inline">
<div class="inline text-left">
<button
ref="btnRef"
type="button"
v-on:click="togglePopover()"
class="items-start cursor-not-allowed inline-flex items-center rounded border border-gray-300 bg-white px-2.5 py-1.5 text-xs font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2">
Sync Favs with Nostr
</button>
<div ref="popoverRef"
v-bind:class="{'hidden': !popoverShow, 'block': popoverShow}"
class="bg-pink-600 border-0 mr-3 z-50 font-normal leading-normal text-sm max-w-xs text-left no-underline break-words rounded-lg
">
<div>
<div class="bg-pink-600 text-white opacity-75 font-semibold p-3 mb-0 border-b border-solid border-slate-100 uppercase rounded-t-lg">
Coming soon
</div>
<div class="text-white p-3">
This feature depends on the finalization of
<a href="https://github.com/nostr-protocol/nips/pull/32" target="_blank">NIP-23 [link]</a>,
if you actively develop a nostr client, please provide input on the NIP to help make the
implementation of user relay lists easier for everyone.
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { createPopper } from "@popperjs/core";
export default {
name: "NostrSyncPopoverNag",
data() {
return {
popoverShow: false
}
},
methods: {
togglePopover: function(){
if(this.popoverShow){
this.popoverShow = false;
} else {
this.popoverShow = true;
createPopper(this.$refs.btnRef, this.$refs.popoverRef, {
placement: "left"
});
}
}
}
}
</script>

75
src/components/relays/tasks/RefreshTask.vue

@ -1,31 +1,31 @@
<template>
<div
v-if="(!store.tasks.isActive || store.tasks.getActiveSlug === this.taskSlug) && !this.isSingle"
v-if="(!store.tasks.isActive || store.tasks.getActiveSlug === this.slug) && !this.isSingle"
class="inline">
<span class="text-white lg:text-sm mr-2 ml-2 mt-1 text-xs">
<span v-if="!store.tasks.isProcessing(this.taskSlug)">Checked {{ sinceLast }} ago</span>
<span v-if="store.tasks.isProcessing(this.taskSlug)" class="italic lg:pr-9 text-white lg:text-sm mr-2 ml-2 block mt-1.5 md:pt-1.5 md:mt-0 text-xs">
<span v-if="!store.tasks.isProcessing(this.slug)">Checked {{ sinceLast }} ago</span>
<span v-if="store.tasks.isProcessing(this.slug)" class="italic lg:pr-9 text-white lg:text-sm mr-2 ml-2 block mt-1.5 md:pt-1.5 md:mt-0 text-xs">
<svg class="animate-spin mr-1 -mt-0.5 h-4 w-5 text-white inline" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
{{ this.store.tasks.getProcessed(this.taskSlug).length }}/{{ this.relays.length }} Relays Checked
{{ this.store.tasks.getProcessed(this.slug).length }}/{{ this.relays.length }} Relays Checked
</span>
</span>
<span class="text-white lg:text-sm mr-2 ml-2 text-xs" v-if="!store.tasks.isProcessing(this.taskSlug)">-</span>
<span class="text-white lg:text-sm mr-2 ml-2 text-xs" v-if="store.prefs.refresh && !store.tasks.isProcessing(this.taskSlug)">
<span class="text-white lg:text-sm mr-2 ml-2 text-xs" v-if="!store.tasks.isProcessing(this.slug)">-</span>
<span class="text-white lg:text-sm mr-2 ml-2 text-xs" v-if="store.prefs.refresh && !store.tasks.isProcessing(this.slug)">
Next check in: {{ untilNext }}
</span>
<button
v-if="!store.tasks.isProcessing(this.taskSlug)"
v-if="!store.tasks.isProcessing(this.slug)"
class="mr-8 my-1 py-1 px-3 text-xs rounded border-b-3 border-slate-700 bg-slate-500 font-bold text-white hover:border-slate-500 hover:bg-slate-400"
:disabled='store.tasks.isProcessing(this.taskSlug)'
:disabled='store.tasks.isProcessing(this.slug)'
@click="refreshNow()">
Check{{ relay ? ` ${relay}` : "" }} Now
</button>
</div>
<span
v-if="(store.tasks.getActiveSlug === this.taskSlug) && this.isSingle"
v-if="(store.tasks.getActiveSlug === this.slug) && this.isSingle"
class="text-white lg:text-sm mr-2 ml-2 mt-1.5 text-xs mr-11">
Loading {{ relayFromUrl }}
</span>
@ -45,22 +45,21 @@ import SharedComputed from '@/shared/computed.js'
import { Inspector } from 'nostr-relay-inspector'
import { relays } from '../../../../relays.yaml'
// import { relays } from '../../../../relays.yaml'
import { geo } from '../../../../cache/geo.yaml'
const localMethods = {
migrateLegacy(){
let hit = false
for(let i=0;i<relays.length;i++) {
const cache = localStorage.getItem(`nostrwatch_${relays[i]}`)
for(let i=0;i<this.relays.length;i++) {
const cache = localStorage.getItem(`nostrwatch_${this.relays[i]}`)
if(!cache)
continue
hit = true
break;
}
if(hit){
relays.forEach( relay => {
this.relays.forEach( relay => {
const oldKey = `nostrwatch_${relay}`
const oldCache = localStorage.getItem(oldKey)
if(oldCache instanceof Object)
@ -87,12 +86,12 @@ const localMethods = {
this.untilNext = this.timeUntilRefresh()
this.sinceLast = this.timeSinceRefresh()
if(this.store.tasks.getProcessed(this.taskSlug).length >= this.relays.length){
this.store.tasks.updateNow(this.taskSlug)
this.store.tasks.finishProcessing(this.taskSlug)
if(this.store.tasks.getProcessed(this.slug).length >= this.relays.length){
this.store.tasks.updateNow(this.slug)
this.store.tasks.finishProcessing(this.slug)
}
if(!this.store.tasks.isProcessing(this.taskSlug))
if(!this.store.tasks.isProcessing(this.slug))
this.invalidate()
}, 1000)
@ -111,17 +110,17 @@ const localMethods = {
},
invalidate: async function(force, single){
if( (!this.isExpired(this.taskSlug) && !force) )
if( (!this.isExpired(this.slug) && !force) )
return
if(!this.windowActive)
return
this.queueJob(this.taskSlug, async () => {
const relays = this.relays.filter( relay => !this.store.tasks.isProcessed(this.taskSlug, relay) )
this.queueJob(this.slug, async () => {
const relays = this.relays.filter( relay => !this.store.tasks.isProcessed(this.slug, relay) )
console.log('unprocessed relays',
this.relays.filter( relay => !this.store.tasks.getProcessed(this.taskSlug).includes(relay)))
this.relays.filter( relay => !this.store.tasks.getProcessed(this.slug).includes(relay)))
if(single) {
await this.check(single)
@ -142,23 +141,24 @@ const localMethods = {
},
completeRelay: function(relay, result){
if(this.store.tasks.isProcessed(this.taskSlug, relay))
if(this.store.tasks.isProcessed(this.slug, relay))
return
this.store.tasks.addProcessed(this.taskSlug, relay)
this.store.tasks.addProcessed(this.slug, relay)
if(result) {
console.log('whoops', result)
this.results[relay] = result
this.setCache(result)
}
if(this.isSingle)
this.completeAll()
else if(this.store.tasks.getProcessed(this.taskSlug).length >= this.relays.length)
else if(this.store.tasks.getProcessed(this.slug).length >= this.relays.length)
this.completeAll()
},
completeAll: function(){
this.store.tasks.completeJob(this.taskSlug)
this.store.tasks.completeJob(this.slug)
this.store.relays.setAggregateCache('public', Object.keys(this.results).filter( result => this.results[result].aggregate === 'public' ))
this.store.relays.setAggregateCache('restricted', Object.keys(this.results).filter( result => this.results[result].aggregate === 'restricted' ))
this.store.relays.setAggregateCache('offline', Object.keys(this.results).filter( result => this.results[result].aggregate === 'offline' ))
@ -168,10 +168,13 @@ const localMethods = {
check: async function(relay){
return new Promise( (resolve) => {
const opts = {
checkRead: true,
checkWrite: true,
checkLatency: true,
getInfo: true,
getIdentities: true,
// debug: true,
run: true,
debug: true,
connectTimeout: this.getDynamicTimeout,
readTimeout: this.getDynamicTimeout,
writeTimeout: this.getDynamicTimeout,
@ -190,7 +193,6 @@ const localMethods = {
instance.result.log = instance.log
resolve(instance.result)
})
.run()
})
},
@ -210,10 +212,10 @@ const localMethods = {
return Math.floor(parseFloat(sum/total));
},
timeUntilRefresh(){
return this.timeSince(Date.now()-(this.store.tasks.getLastUpdate(this.taskSlug)+this.store.prefs.duration-Date.now()))
return this.timeSince(Date.now()-(this.store.tasks.getLastUpdate(this.slug)+this.store.prefs.duration-Date.now()))
},
timeSinceRefresh(){
return this.timeSince(this.store.tasks.getLastUpdate(this.taskSlug)) || Date.now()
return this.timeSince(this.store.tasks.getLastUpdate(this.slug)) || Date.now()
},
}
@ -243,7 +245,7 @@ export default defineComponent({
windowActive: true,
averageLatency: 200,
pageOpen: 0,
taskSlug: 'relays/check'
slug: 'relays/check'
// history: null
}
},
@ -258,12 +260,12 @@ export default defineComponent({
},
beforeMount(){
this.lastUpdate = this.store.tasks.getLastUpdate(this.taskSlug)
this.lastUpdate = this.store.tasks.getLastUpdate(this.slug)
this.untilNext = this.timeUntilRefresh()
this.sinceLast = this.timeSinceRefresh()
this.relays = Array.from(new Set(relays))
this.store.relays.setRelays(relays)
this.relays = Array.from(new Set(this.store.relays.getShuffled))
this.store.relays.setRelays(this.relays)
this.store.relays.setGeo(geo)
for(let ri=0;ri-this.relays.length;ri++){
@ -275,12 +277,11 @@ export default defineComponent({
mounted(){
this.migrateLegacy()
if( this.isSingle ){
this.taskSlug = 'relays/single'
this.slug = 'relays/single'
this.invalidate(true, this.relayFromUrl)
} else {
if(this.store.tasks.isProcessing(this.taskSlug))
if(this.store.tasks.isProcessing(this.slug))
this.invalidate(true)
else
this.invalidate()

3
src/components/relays/tasks/TasksManager.vue

@ -1,6 +1,7 @@
<template>
<RefreshTask
v-bind:resultsProp="results" />
<UserRelayList />
<!-- <RelayCanonicalsTask
:resultsProp="results" />
<RelayOperatorTask
@ -15,6 +16,7 @@ import { setupStore } from '@/store'
import SharedComputed from '@/shared/computed.js'
import RefreshTask from './RefreshTask.vue'
import UserRelayList from './UserRelayList.vue'
// import RelayCanonicalsTask from './RelayCanonicalsTask.vue'
@ -24,6 +26,7 @@ export default defineComponent({
name: "TasksManager",
components: {
RefreshTask,
UserRelayList,
// RelayCanonicalsTask,
// RelayOperatorTask
},

125
src/components/relays/tasks/UserRelayList.vue

@ -0,0 +1,125 @@
<template>
<span
v-if="this.store.tasks.getActiveSlug === taskSlug && isLoggedIn"
class="text-white lg:text-sm mr-10 ml-2 mt-1.5 text-xs">
<span>Retrieving Relays List...</span>
</span>
</template>
<style scoped>
</style>
<script>
import crypto from 'crypto'
// import { RelayPool } from 'nostr'
import { defineComponent, toRefs } from 'vue'
import { setupStore } from '@/store'
import SharedMethods from '@/shared/relays-lib.js'
import UserMethods from '@/shared/user-lib.js'
import SharedComputed from '@/shared/computed.js'
import { relays } from '../../../../relays.yaml'
const localMethods = new Object()
localMethods.invalidate = function(force){
if( !this.isExpired(this.taskSlug) && !force )
return
if( !this.isLoggedIn )
return
if( !this.store.prefs.useKind3 )
return
console.log('wtf?', this.taskSlug, !this.isExpired(this.taskSlug) && !force)
this.queueKind3(this.taskSlug)
}
localMethods.timeUntilRefresh = function(){
return this.timeSince(Date.now()-(this.store.tasks.getLastUpdate(this.taskSlug)+this.store.prefs.duration-Date.now()))
}
localMethods.timeSinceRefresh = function(){
return this.timeSince(this.store.tasks.getLastUpdate(this.taskSlug)) || Date.now()
}
localMethods.hash = function(relay){
return crypto.createHash('md5').update(relay).digest('hex');
}
export default defineComponent({
name: 'TemplateTask',
components: {},
data() {
return {
taskSlug: 'user/relay/list',
kind3Remote: new Object(),
kind3Local: {}
}
},
setup(props){
const {resultsProp: results} = toRefs(props)
const {forceProp: force} = toRefs(props)
return {
store : setupStore(),
results: results,
force: force
}
},
created(){
clearInterval(this.interval)
},
unmounted(){
clearInterval(this.interval)
},
beforeMount(){
this.lastUpdate = this.store.tasks.getLastUpdate(this.taskSlug)
this.untilNext = this.timeUntilRefresh()
this.sinceLast = this.timeSinceRefresh()
this.relays = Array.from(new Set(relays))
},
mounted(){
console.log('task', this.taskSlug, 'is processing:', this.store.tasks.isProcessing(this.taskSlug))
if(this.store.tasks.isProcessing(this.taskSlug))
this.invalidate(true)
else
this.invalidate(this.force)
},
updated(){},
computed: Object.assign(SharedComputed, {
getDynamicTimeout: function(){
return this.averageLatency*this.relays.length
},
}),
methods: Object.assign(localMethods, UserMethods, SharedMethods),
props: {
resultsProp: {
type: Object,
default(){
return {}
}
},
forceProp: {
type: Boolean,
default(){
return false
}
},
},
})
</script>
<style scoped>
#refresh { font-size: 12pt; color:#666; margin-bottom:15px }
#refresh button { cursor: pointer; border-radius: 3px; border: 1px solid #a0a0a0; color:#333 }
#refresh button:hover {color:#000;}
#refresh button[disabled] {color:#999 !important; border-color:#e0e0e0}
</style>

28
src/shared/relays-lib.js

@ -2,6 +2,32 @@ import crypto from "crypto"
import {sort} from 'array-timsort'
export default {
queueKind3: async function(slug){
this.queueJob(
slug,
async () => {
await this.store.user.setKind3()
.then( () => {
Object.keys(this.store.user.kind3).forEach( key => {
this.store.relays.setFavorite(key)
})
this.store.tasks.completeJob()
})
.catch( err => {
console.error('error!', err)
this.store.tasks.completeJob()
})
},
true
)
},
queueJob: function(id, fn, unique){
this.store.tasks.addJob({
id: id,
handler: fn,
unique: unique
})
},
getRelays(relays){
relays = this.filterRelays(relays)
relays = this.sortRelays(relays)
@ -48,7 +74,7 @@ export default {
return this.$storage.getStorageSync(key)
},
cleanUrl: function(relay){
getHostname: function(relay){
return relay.replace('wss://', '')
},

2
src/shared/user-lib.js

@ -1,6 +1,6 @@
export default {
isLoggedIn: function(){
return this.store.user.getPublicKey
return this.store.user.getPublicKey ? true : false
},
signOut: function(){
this.store.user.$reset()

4
src/store/layout.js

@ -3,6 +3,7 @@ import { defineStore } from 'pinia'
export const useLayoutStore = defineStore('layout', {
state: () => ({
mapExpanded: false,
editorExpanded: false,
active: {},
nav: {},
activeTab: null,
@ -14,6 +15,7 @@ export const useLayoutStore = defineStore('layout', {
getNav: (state) => state.nav,
getNavGroup: (state) => (group) => state.nav[group],
mapIsExpanded: (state) => state.mapExpanded,
editorIsExpanded: state => state.editorExpanded,
},
actions: {
deactivateTab(tabId){
@ -24,5 +26,7 @@ export const useLayoutStore = defineStore('layout', {
setNavItems(section, items){ this.nav[section] = items },
setActive(section, slug){ this.active[section] = slug },
toggleMap(){ this.mapExpanded = !this.mapExpanded },
toggleEditor(){ this.editorExpanded = !this.editorExpanded },
editorOff(){ this.editorExpanded = false }
},
})

5
src/store/prefs.js

@ -8,6 +8,7 @@ export const usePrefsStore = defineStore('prefs', {
rowTheme: 'comfortable',
filters: [],
filterFn: [],
useKind3: true
}),
getters: {
doRefresh: (state) => state.refresh,
@ -21,7 +22,9 @@ export const usePrefsStore = defineStore('prefs', {
disable(){ this.refresh = false },
toggleRefresh(){ this.refresh = !this.refresh },
updateExpiration(dur) { this.duration = dur },
togglePinFavorites(){ this.pinFavorites = !this.pinFavorites },
togglePinFavorites(){
this.pinFavorites = !this.pinFavorites
},
setRowTheme(theme){ this.rowTheme = theme },
addFilter(key, fn){
if(this.filters.includes(key))

54
src/store/relays.js

@ -1,4 +1,5 @@
import { defineStore } from 'pinia'
import { useUserStore } from '@/store/user'
// import { relays } from '../../relays.yaml'
// import { geo } from '../../relays.yaml'
@ -19,6 +20,11 @@ export const useRelaysStore = defineStore('relays', {
}),
getters: {
getAll: (state) => state.urls,
getShuffled: state => shuffle(state.urls),
getShuffledPublic: state => {
console.log('aggregates are set',state.aggregatesAreSet )
return state.aggregatesAreSet ? shuffle(state.aggregates.public) : shuffle(state.urls)
},
getRelays: (state) => (aggregate, results) => {
if( 'all' == aggregate )
return state.urls.map(x=>x)
@ -26,13 +32,12 @@ export const useRelaysStore = defineStore('relays', {
return state.favorites
return state.urls.filter( (relay) => results?.[relay]?.aggregate == aggregate)
},
getByAggregate: state => aggregate => {
return state.urls
.filter( (relay) => state.results?.[relay]?.aggregate == aggregate)
const results = state.urls.filter( (relay) => state.results?.[relay]?.aggregate == aggregate)
this.setAggregate(aggregate, results)
return results
},
// getResults: (state) => state.results,
// getResult: (state) => (relayUrl) => state.results?.[relayUrl],
getGeo: state => relayUrl => state.geo[relayUrl],
@ -41,6 +46,7 @@ export const useRelaysStore = defineStore('relays', {
getCount: state => type => state.count[type],
getCounts: state => state.count,
getAggregates: state => state.aggregates,
getAggregate: state => which => state.aggregates[which],
areAggregatesSet: state => state.aggregatesAreSet,
@ -66,25 +72,39 @@ export const useRelaysStore = defineStore('relays', {
setGeo(geo){ this.geo = geo },
setStat(type, value){
this.count[type] = value
},
setAggregate(aggregate, arr){ this.aggregates[aggregate] = arr },
setAggregates(obj){
this.aggregatesAreSet = true
this.aggregates = obj
},
setFavorite(relayUrl){
if(this.favorites.includes(relayUrl))
return
this.favorites.push(relayUrl)
this.favorites = this.favorites.map( x => x )
const store = useUserStore()
if(store.kind3[relayUrl] instanceof Object)
return
store.kind3[relayUrl] = {
read: false,
write: false
}
},
unsetFavorite(relayUrl){
this.favorites = this.favorites.filter(item => item !== relayUrl)
const store = useUserStore()
console.log('before delete', typeof store.kind3[relayUrl])
delete store.kind3[relayUrl]
console.log('deleted?', typeof store.kind3[relayUrl])
},
toggleFavorite(relayUrl){
@ -106,4 +126,22 @@ export const useRelaysStore = defineStore('relays', {
this.canonicals = c
}
},
})
})
function shuffle(array) {
let currentIndex = array.length, randomIndex;
// While there remain elements to shuffle.
while (currentIndex != 0) {
// Pick a remaining element.
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex--;
// And swap it with the current element.
[array[currentIndex], array[randomIndex]] = [
array[randomIndex], array[currentIndex]];
}
return array;
}

68
src/store/user.js

@ -1,11 +1,17 @@
import { defineStore } from 'pinia'
import { RelayPool } from 'nostr'
import { useRelaysStore } from '@/store/relays'
import crypto from 'crypto'
// import { isJson } from '@/utils'
export const useUserStore = defineStore('user', {
state: () => ({
pubKey: "",
events: [],
profile: {},
testEvent: false
testEvent: false,
kind3: new Object(),
kind3Event: new Object(),
}),
getters: {
getPublicKey: (state) => state.pubKey,
@ -14,9 +20,67 @@ export const useUserStore = defineStore('user', {
getPicture: (state) => state.profile.picture,
getNip05: (state) => state.profile.nip05,
isProfile: (state) => Object.keys(state.profile).length ? true : false,
getTestEvent: (state) => state.testEvent
getTestEvent: (state) => state.testEvent,
getKind3: (state) => state.kind3,
getKind3Event: (state) => state.kind3Event,
},
actions: {
retrieveKind3: async function() {
return new Promise( (resolve) => {
// console.log('retrieveKind3 promise()', useRelaysStore())
const subid = crypto.randomBytes(40).toString('hex')
const store = useRelaysStore()
const relays = store.getFavorites.length ? store.getFavorites : ['wss://nostr.sandwich.farm']
const pool = new RelayPool(relays)
const ordered = []
pool
.on('open', r => {
console.log('subscribing', subid)
r.subscribe(subid, {
limit: 1,
kinds: [3],
authors:[ this.pubKey ]
})
})
.on('event', (relay, _subid, ev) => {
if(_subid == subid){
if(!ev.content.length)
return
console.log('the content', ev.content)
try {
ev.content = JSON.parse(ev.content)
}
catch(e){
ev.content = {}
}
ordered.push(ev)
}
ordered.push(ev)
})
.on('error', (relay, err) => {
relay
console.log('there was an error', err)
// reject(err)
})
setTimeout( () => {
ordered.sort( (a, b) => a.created_at - b.created_at )
if(!ordered.length)
return
this.kind3Event = ordered[0]
const result = this.kind3Event.content
console.log('final', result)
pool.unsubscribe(subid)
pool.close()
resolve(result)
},5000)
})
},
setKind3: async function(obj) {
if(obj instanceof Object && Object.keys(obj).length)
this.kind3 = obj
else
this.kind3 = Object.assign(this.kind3, await this.retrieveKind3())
},
setPublicKey: function(pubKey){ this.pubKey = pubKey },
setProfile: function(stringifiedEvContent){
this.profile = JSON.parse(stringifiedEvContent)

56
src/styles/main.scss

@ -18,7 +18,30 @@
//TODO: When building out the stylesheet, convert these to WS dark: conditional.
html[theme=dark] {
html {
main {
.data-card {
* {
@apply border-white/10
}
dl > div {
@apply border-none
}
&:first-child {
@apply bg-none
}
@apply bg-white/10 border-none
}
.data-card {
@apply bg-none
}
}
}
html.dark {
background: #16171d;
color: #fff;
@ -26,9 +49,9 @@ html[theme=dark] {
background-color: rgba(22, 23, 29, .5)
}
#relays_list_wrapper {
@apply bg-black/20 rounded-lg
}
// #relays_list_wrapper {
// @apply bg-black/20 rounded-lg
// }
div[role=menu] {
background: #16171d;
@ -45,7 +68,6 @@ html[theme=dark] {
#map_control button {
background: #16171d;
color: #fff;
}
table tbody {
@ -73,9 +95,15 @@ html[theme=dark] {
dl > div {
@apply border-none
}
&:first-child {
@apply bg-none
}
@apply bg-slate-50/10 border-none
}
.data-card {
@apply bg-none
}
}
.leaflet-tile {
@ -87,15 +115,6 @@ html[theme=dark] {
}
}
td ul { padding:0; margin:0; list-style: none; }
td ul li { list-style: none; }
@ -302,10 +321,11 @@ th .shape.verified:after {
// margin:0 auto;
// }
div.block {
display:block;
margin:40px 0;
}
// #wrapper section > {
// & > div {
// @apply block my-8
// }
// }
h1 {
position:relative;

1
tailwind.config.js

@ -1,4 +1,5 @@
module.exports = {
darkMode: 'class',
content: [
"./index.html",
"./src/**/*.{vue,js,ts,jsx,tsx}"

5
vue.config.js

@ -7,7 +7,8 @@ module.exports = defineConfig({
transpileDependencies: true,
devServer: {
port: 8080
port: 8080,
https: true
},
configureWebpack: {
// watch: true,
@ -35,7 +36,7 @@ module.exports = defineConfig({
"tls": false,
"net": false,
"utf-8-validate": false,
"bufferutil": false
"bufferutil": false,
}
},
},

2
yarn.lock

@ -6497,7 +6497,7 @@ object-assign@^4, object-assign@^4.0.1:
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
object-hash@^3.0.0:
object-hash@3.0.0, object-hash@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9"
integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==

Loading…
Cancel
Save