Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c3c65c3970 | |||
| a5b868cd56 | |||
| 8fcc56a408 | |||
| c8dfbc936b | |||
| f1e76a1ed1 | |||
| 6ecc3e6770 | |||
| b05c408977 | |||
| e484c3cb00 | |||
| 69d0e11ba4 |
@@ -1,4 +1,4 @@
|
|||||||
VITE_DEFAULT_PUBKEYS=fe7f6bc6f7338b76bbf80db402ade65953e20b2f23e66e898204b63cc42539a3,391819e2f2f13b90cac7209419eb574ef7c0d1f4e81867fc24c47a3ce5e8a248,84dee6e676e5bb67b4ad4e042cf70cbd8681155db535942fcc6a0533858a7240,dace63b00c42e6e017d00dd190a9328386002ff597b841eb5ef91de4f1ce8491,82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2,58c741aa630c2da35a56a77c1d05381908bd10504fdd2d8b43f725efa6d23196,eeb11961b25442b16389fe6c7ebea9adf0ac36dd596816ea7119e521b8821b9e,b676ded7c768d66a757aa3967b1243d90bf57afb09d1044d3219d8d424e4aea0,61066504617ee79387021e18c89fb79d1ddbc3e7bff19cf2298f40466f8715e9,3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24,6389be6491e7b693e9f368ece88fcd145f07c068d2c1bbae4247b9b5ef439d32,97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322
|
VITE_DEFAULT_PUBKEYS=06639a386c9c1014217622ccbcf40908c4f1a0c33e23f8d6d68f4abf655f8f71,266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5,391819e2f2f13b90cac7209419eb574ef7c0d1f4e81867fc24c47a3ce5e8a248,3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d,3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24,55f04590674f3648f4cdc9dc8ce32da2a282074cd0b020596ee033d12d385185,58c741aa630c2da35a56a77c1d05381908bd10504fdd2d8b43f725efa6d23196,61066504617ee79387021e18c89fb79d1ddbc3e7bff19cf2298f40466f8715e9,6389be6491e7b693e9f368ece88fcd145f07c068d2c1bbae4247b9b5ef439d32,63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed,6e75f7972397ca3295e0f4ca0fbc6eb9cc79be85bafdd56bd378220ca8eee74e,76c71aae3a491f1d9eec47cba17e229cda4113a0bbb6e6ae1776d7643e29cafa,7fa56f5d6962ab1e3cd424e758c3002b8665f7b0d8dcee9fe9e288d7751ac194,82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2,84dee6e676e5bb67b4ad4e042cf70cbd8681155db535942fcc6a0533858a7240,97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322,b676ded7c768d66a757aa3967b1243d90bf57afb09d1044d3219d8d424e4aea0,dace63b00c42e6e017d00dd190a9328386002ff597b841eb5ef91de4f1ce8491,eeb11961b25442b16389fe6c7ebea9adf0ac36dd596816ea7119e521b8821b9e,fe7f6bc6f7338b76bbf80db402ade65953e20b2f23e66e898204b63cc42539a3
|
||||||
VITE_BURROW_URL=
|
VITE_BURROW_URL=
|
||||||
VITE_PLATFORM_URL=https://flotilla.social
|
VITE_PLATFORM_URL=https://flotilla.social
|
||||||
VITE_PLATFORM_TERMS=https://flotilla.social/terms
|
VITE_PLATFORM_TERMS=https://flotilla.social/terms
|
||||||
|
|||||||
@@ -1,5 +1,18 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
# 0.2.11
|
||||||
|
|
||||||
|
* Add in-app signup flow on ios
|
||||||
|
* Add profile deletion
|
||||||
|
|
||||||
|
# 0.2.10
|
||||||
|
|
||||||
|
* Improve space discovery
|
||||||
|
|
||||||
|
# 0.2.9
|
||||||
|
|
||||||
|
* Add NIP 01 signup flow on mobile
|
||||||
|
|
||||||
# 0.2.8
|
# 0.2.8
|
||||||
|
|
||||||
* Show spinner when joining a room
|
* Show spinner when joining a room
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ android {
|
|||||||
applicationId "social.flotilla"
|
applicationId "social.flotilla"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 9
|
versionCode 10
|
||||||
versionName "0.2.7"
|
versionName "0.2.11"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
aaptOptions {
|
aaptOptions {
|
||||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||||
|
|||||||
@@ -351,14 +351,14 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 2;
|
CURRENT_PROJECT_VERSION = 5;
|
||||||
DEVELOPMENT_TEAM = S26U9DYW3A;
|
DEVELOPMENT_TEAM = S26U9DYW3A;
|
||||||
INFOPLIST_FILE = App/Info.plist;
|
INFOPLIST_FILE = App/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
|
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||||
MARKETING_VERSION = 0.2.8;
|
MARKETING_VERSION = 0.2.11;
|
||||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
|
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
@@ -376,14 +376,14 @@
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 2;
|
CURRENT_PROJECT_VERSION = 5;
|
||||||
DEVELOPMENT_TEAM = S26U9DYW3A;
|
DEVELOPMENT_TEAM = S26U9DYW3A;
|
||||||
INFOPLIST_FILE = App/Info.plist;
|
INFOPLIST_FILE = App/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
|
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||||
MARKETING_VERSION = 0.2.8;
|
MARKETING_VERSION = 0.2.11;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
|
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "flotilla",
|
"name": "flotilla",
|
||||||
"version": "0.2.8",
|
"version": "0.2.11",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "flotilla",
|
"name": "flotilla",
|
||||||
"version": "0.2.8",
|
"version": "0.2.11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@capacitor/android": "^7.0.0",
|
"@capacitor/android": "^7.0.0",
|
||||||
"@capacitor/app": "^7.0.0",
|
"@capacitor/app": "^7.0.0",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "flotilla",
|
"name": "flotilla",
|
||||||
"version": "0.2.8",
|
"version": "0.2.11",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
|
|||||||
@@ -323,3 +323,9 @@ emoji-picker {
|
|||||||
--input-font-color: var(--base-content);
|
--input-font-color: var(--base-content);
|
||||||
--outline-color: var(--base-100);
|
--outline-color: var(--base-100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* progress */
|
||||||
|
|
||||||
|
progress[value]::-webkit-progress-value {
|
||||||
|
transition: width 0.5s;
|
||||||
|
}
|
||||||
|
|||||||
+3
-52
@@ -1,6 +1,6 @@
|
|||||||
import * as nip19 from "nostr-tools/nip19"
|
import * as nip19 from "nostr-tools/nip19"
|
||||||
import {get} from "svelte/store"
|
import {get} from "svelte/store"
|
||||||
import {ctx, sample, uniq, sleep, chunk, equals} from "@welshman/lib"
|
import {ctx, uniq, equals} from "@welshman/lib"
|
||||||
import {
|
import {
|
||||||
DELETE,
|
DELETE,
|
||||||
REPORT,
|
REPORT,
|
||||||
@@ -26,12 +26,10 @@ import {
|
|||||||
getTag,
|
getTag,
|
||||||
getListTags,
|
getListTags,
|
||||||
getRelayTags,
|
getRelayTags,
|
||||||
isShareableRelayUrl,
|
|
||||||
getRelayTagValues,
|
getRelayTagValues,
|
||||||
toNostrURI,
|
toNostrURI,
|
||||||
} from "@welshman/util"
|
} from "@welshman/util"
|
||||||
import type {TrustedEvent, EventContent, EventTemplate, List} from "@welshman/util"
|
import type {TrustedEvent, EventContent, EventTemplate} from "@welshman/util"
|
||||||
import type {SubscribeRequestWithHandlers} from "@welshman/net"
|
|
||||||
import {PublishStatus, AuthStatus, SocketStatus} from "@welshman/net"
|
import {PublishStatus, AuthStatus, SocketStatus} from "@welshman/net"
|
||||||
import {Nip59, makeSecret, stamp, Nip46Broker} from "@welshman/signer"
|
import {Nip59, makeSecret, stamp, Nip46Broker} from "@welshman/signer"
|
||||||
import {
|
import {
|
||||||
@@ -40,13 +38,9 @@ import {
|
|||||||
repository,
|
repository,
|
||||||
publishThunk,
|
publishThunk,
|
||||||
publishThunks,
|
publishThunks,
|
||||||
loadProfile,
|
|
||||||
loadInboxRelaySelections,
|
|
||||||
profilesByPubkey,
|
profilesByPubkey,
|
||||||
relaySelectionsByPubkey,
|
relaySelectionsByPubkey,
|
||||||
getWriteRelayUrls,
|
getWriteRelayUrls,
|
||||||
loadFollows,
|
|
||||||
loadMutes,
|
|
||||||
tagEvent,
|
tagEvent,
|
||||||
tagEventForReaction,
|
tagEventForReaction,
|
||||||
getRelayUrls,
|
getRelayUrls,
|
||||||
@@ -67,11 +61,9 @@ import {
|
|||||||
userMembership,
|
userMembership,
|
||||||
INDEXER_RELAYS,
|
INDEXER_RELAYS,
|
||||||
NIP46_PERMS,
|
NIP46_PERMS,
|
||||||
loadMembership,
|
|
||||||
loadSettings,
|
|
||||||
getDefaultPubkeys,
|
|
||||||
userRoomsByUrl,
|
userRoomsByUrl,
|
||||||
} from "@app/state"
|
} from "@app/state"
|
||||||
|
import {loadUserData} from "@app/requests"
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
|
|
||||||
@@ -161,47 +153,6 @@ export const logout = async () => {
|
|||||||
localStorage.clear()
|
localStorage.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loaders
|
|
||||||
|
|
||||||
export const loadUserData = (
|
|
||||||
pubkey: string,
|
|
||||||
request: Partial<SubscribeRequestWithHandlers> = {},
|
|
||||||
) => {
|
|
||||||
const promise = Promise.race([
|
|
||||||
sleep(3000),
|
|
||||||
Promise.all([
|
|
||||||
loadInboxRelaySelections(pubkey, request),
|
|
||||||
loadMembership(pubkey, request),
|
|
||||||
loadSettings(pubkey, request),
|
|
||||||
loadProfile(pubkey, request),
|
|
||||||
loadFollows(pubkey, request),
|
|
||||||
loadMutes(pubkey, request),
|
|
||||||
]),
|
|
||||||
])
|
|
||||||
|
|
||||||
// Load followed profiles slowly in the background without clogging other stuff up. Only use a single
|
|
||||||
// indexer relay to avoid too many redundant validations, which slow things down and eat bandwidth
|
|
||||||
promise.then(async () => {
|
|
||||||
for (const pubkeys of chunk(50, getDefaultPubkeys())) {
|
|
||||||
const relays = sample(1, INDEXER_RELAYS)
|
|
||||||
|
|
||||||
await sleep(1000)
|
|
||||||
|
|
||||||
for (const pubkey of pubkeys) {
|
|
||||||
loadMembership(pubkey, {relays})
|
|
||||||
loadProfile(pubkey, {relays})
|
|
||||||
loadFollows(pubkey, {relays})
|
|
||||||
loadMutes(pubkey, {relays})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return promise
|
|
||||||
}
|
|
||||||
|
|
||||||
export const discoverRelays = (lists: List[]) =>
|
|
||||||
Promise.all(uniq(lists.flatMap(getRelayUrls)).filter(isShareableRelayUrl).map(loadRelay))
|
|
||||||
|
|
||||||
// Synchronization
|
// Synchronization
|
||||||
|
|
||||||
export const broadcastUserData = async (relays: string[]) => {
|
export const broadcastUserData = async (relays: string[]) => {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
import {pushModal, clearModals} from "@app/modal"
|
import {pushModal, clearModals} from "@app/modal"
|
||||||
import {PLATFORM_NAME, BURROW_URL} from "@app/state"
|
import {PLATFORM_NAME, BURROW_URL} from "@app/state"
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/toast"
|
||||||
import {loadUserData} from "@app/commands"
|
import {loadUserData} from "@app/requests"
|
||||||
import {setChecked} from "@app/notifications"
|
import {setChecked} from "@app/notifications"
|
||||||
|
|
||||||
let signers: any[] = $state([])
|
let signers: any[] = $state([])
|
||||||
|
|||||||
@@ -12,7 +12,8 @@
|
|||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import QRCode from "@app/components/QRCode.svelte"
|
import QRCode from "@app/components/QRCode.svelte"
|
||||||
import InfoBunker from "@app/components/InfoBunker.svelte"
|
import InfoBunker from "@app/components/InfoBunker.svelte"
|
||||||
import {loginWithNip46, loadUserData} from "@app/commands"
|
import {loginWithNip46} from "@app/commands"
|
||||||
|
import {loadUserData} from "@app/requests"
|
||||||
import {pushModal, clearModals} from "@app/modal"
|
import {pushModal, clearModals} from "@app/modal"
|
||||||
import {setChecked} from "@app/notifications"
|
import {setChecked} from "@app/notifications"
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/toast"
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import PasswordResetRequest from "@app/components/PasswordResetRequest.svelte"
|
import PasswordResetRequest from "@app/components/PasswordResetRequest.svelte"
|
||||||
import {loadUserData} from "@app/commands"
|
import {loadUserData} from "@app/requests"
|
||||||
import {clearModals, pushModal} from "@app/modal"
|
import {clearModals, pushModal} from "@app/modal"
|
||||||
import {setChecked} from "@app/notifications"
|
import {setChecked} from "@app/notifications"
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/toast"
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {chunk, sleep, uniq} from "@welshman/lib"
|
||||||
|
import {
|
||||||
|
createEvent,
|
||||||
|
createProfile,
|
||||||
|
PROFILE,
|
||||||
|
DELETE,
|
||||||
|
isReplaceable,
|
||||||
|
getAddress,
|
||||||
|
} from "@welshman/util"
|
||||||
|
import {pubkey, userRelaySelections, publishThunk, getRelayUrls, repository} from "@welshman/app"
|
||||||
|
import {preventDefault} from "@lib/html"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
|
import {pushToast} from "@app/toast"
|
||||||
|
import {logout} from "@app/commands"
|
||||||
|
import {INDEXER_RELAYS, userMembership, getMembershipUrls} from "@app/state"
|
||||||
|
|
||||||
|
let progress: number | undefined = $state(undefined)
|
||||||
|
let confirmText = $state("")
|
||||||
|
|
||||||
|
const CONFIRM_TEXT = "permanently delete my nostr account"
|
||||||
|
const confirmOk = $derived(confirmText.toLowerCase().trim() === CONFIRM_TEXT)
|
||||||
|
const showProgress = $derived(progress !== undefined)
|
||||||
|
|
||||||
|
const deleteProfile = async () => {
|
||||||
|
if (!confirmOk) {
|
||||||
|
return pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: "Please type your confirmation into the text box.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const chunks = chunk(500, repository.query([{authors: [$pubkey!]}]))
|
||||||
|
const profileEvent = createEvent(PROFILE, createProfile({name: "[deleted]"}))
|
||||||
|
const vanishEvent = createEvent(62, {tags: [["relay", "ALL_RELAYS"]]})
|
||||||
|
const denominator = chunks.length + 2
|
||||||
|
const relays = uniq([
|
||||||
|
...INDEXER_RELAYS,
|
||||||
|
...getRelayUrls($userRelaySelections),
|
||||||
|
...getMembershipUrls($userMembership),
|
||||||
|
])
|
||||||
|
|
||||||
|
let step = 0
|
||||||
|
|
||||||
|
const incrementProgress = async () => {
|
||||||
|
progress = ++step / denominator
|
||||||
|
|
||||||
|
return sleep(800)
|
||||||
|
}
|
||||||
|
|
||||||
|
// First, blank out their profile in case relays don't support deletion by address
|
||||||
|
await publishThunk({relays, event: profileEvent})
|
||||||
|
|
||||||
|
await incrementProgress()
|
||||||
|
|
||||||
|
// Next, send a "right to vanish" event to all relays
|
||||||
|
await publishThunk({relays, event: vanishEvent})
|
||||||
|
|
||||||
|
await incrementProgress()
|
||||||
|
|
||||||
|
// Finally, send deletion requests for all known events in case relays don't support right to vanish
|
||||||
|
for (const events of chunks) {
|
||||||
|
const tags: string[][] = []
|
||||||
|
|
||||||
|
for (const event of events) {
|
||||||
|
tags.push(["e", event.id])
|
||||||
|
|
||||||
|
if (isReplaceable(event)) {
|
||||||
|
tags.push(["a", getAddress(event)])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await publishThunk({relays, event: createEvent(DELETE, {tags})})
|
||||||
|
|
||||||
|
await incrementProgress()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let them see that progress is complete
|
||||||
|
await sleep(2000)
|
||||||
|
|
||||||
|
// Goodbye forever!
|
||||||
|
await logout()
|
||||||
|
|
||||||
|
window.location.href = "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirm = async () => {
|
||||||
|
progress = 0
|
||||||
|
|
||||||
|
try {
|
||||||
|
await deleteProfile()
|
||||||
|
} catch (e) {
|
||||||
|
progress = undefined
|
||||||
|
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const back = () => history.back()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form class="column gap-4" onsubmit={preventDefault(confirm)}>
|
||||||
|
<ModalHeader>
|
||||||
|
{#snippet title()}
|
||||||
|
Delete your account
|
||||||
|
{/snippet}
|
||||||
|
{#snippet info()}
|
||||||
|
From the Nostr network
|
||||||
|
{/snippet}
|
||||||
|
</ModalHeader>
|
||||||
|
{#if showProgress}
|
||||||
|
<p>
|
||||||
|
We are currently sending deletion requests to your relay selections and space hosts. Please
|
||||||
|
wait while we complete this process. Once we're done, you'll be automatically logged out.
|
||||||
|
</p>
|
||||||
|
<progress class="progress progress-primary w-full" value={progress! * 100} max="100"></progress>
|
||||||
|
{:else}
|
||||||
|
<p>
|
||||||
|
Are you sure? To confirm, please type "{CONFIRM_TEXT}" into the text box below. This action
|
||||||
|
can't be undone.
|
||||||
|
</p>
|
||||||
|
<label class="input input-bordered flex w-full items-center gap-2">
|
||||||
|
<input bind:value={confirmText} class="grow" type="text" />
|
||||||
|
</label>
|
||||||
|
<p>
|
||||||
|
<strong>Note:</strong> not all relays may honor your request for deletion. If you find that your
|
||||||
|
content continues to be available, please contact the offending relays directly.
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
<ModalFooter>
|
||||||
|
<Button class="btn btn-link" onclick={back}>
|
||||||
|
<Icon icon="alt-arrow-left" />
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" class="btn btn-error" disabled={showProgress || !confirmOk}>
|
||||||
|
<Spinner loading={progress !== undefined}>Confirm</Spinner>
|
||||||
|
<Icon icon="alt-arrow-right" />
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</form>
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {ctx} from "@welshman/lib"
|
import {ctx} from "@welshman/lib"
|
||||||
|
import type {Profile} from "@welshman/util"
|
||||||
import {
|
import {
|
||||||
createEvent,
|
createEvent,
|
||||||
makeProfile,
|
makeProfile,
|
||||||
@@ -8,76 +9,31 @@
|
|||||||
isPublishedProfile,
|
isPublishedProfile,
|
||||||
} from "@welshman/util"
|
} from "@welshman/util"
|
||||||
import {pubkey, profilesByPubkey, publishThunk} from "@welshman/app"
|
import {pubkey, profilesByPubkey, publishThunk} from "@welshman/app"
|
||||||
import {preventDefault} from "@lib/html"
|
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
|
||||||
import Field from "@lib/components/Field.svelte"
|
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import InputProfilePicture from "@lib/components/InputProfilePicture.svelte"
|
import ProfileEditForm from "@app/components/ProfileEditForm.svelte"
|
||||||
import InfoHandle from "@app/components/InfoHandle.svelte"
|
import {clearModals} from "@app/modal"
|
||||||
import {pushModal, clearModals} from "@app/modal"
|
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/toast"
|
||||||
|
|
||||||
const values = $state({...($profilesByPubkey.get($pubkey!) || makeProfile())})
|
const initialValues = {...($profilesByPubkey.get($pubkey!) || makeProfile())}
|
||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
const saveEdit = () => {
|
const onsubmit = (profile: Profile) => {
|
||||||
const relays = ctx.app.router.FromUser().getUrls()
|
const relays = ctx.app.router.FromUser().getUrls()
|
||||||
const template = isPublishedProfile(values)
|
const template = isPublishedProfile(profile) ? editProfile(profile) : createProfile(profile)
|
||||||
? editProfile($state.snapshot(values))
|
|
||||||
: createProfile($state.snapshot(values))
|
|
||||||
const event = createEvent(template.kind, template)
|
const event = createEvent(template.kind, template)
|
||||||
|
|
||||||
publishThunk({event, relays})
|
publishThunk({event, relays})
|
||||||
pushToast({message: "Your profile has been updated!"})
|
pushToast({message: "Your profile has been updated!"})
|
||||||
clearModals()
|
clearModals()
|
||||||
}
|
}
|
||||||
|
|
||||||
let file: File | undefined = $state()
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form class="col-4" onsubmit={preventDefault(saveEdit)}>
|
<ProfileEditForm {initialValues} {onsubmit}>
|
||||||
<div class="flex justify-center py-2">
|
{#snippet footer()}
|
||||||
<InputProfilePicture bind:file bind:url={values.picture} />
|
<div class="mt-4 flex flex-row items-center justify-between gap-4">
|
||||||
</div>
|
<Button class="btn btn-neutral" onclick={back}>Discard Changes</Button>
|
||||||
<Field>
|
<Button type="submit" class="btn btn-primary">Save Changes</Button>
|
||||||
{#snippet label()}
|
</div>
|
||||||
<p>Username</p>
|
{/snippet}
|
||||||
{/snippet}
|
</ProfileEditForm>
|
||||||
{#snippet input()}
|
|
||||||
<label class="input input-bordered flex w-full items-center gap-2">
|
|
||||||
<Icon icon="user-circle" />
|
|
||||||
<input bind:value={values.name} class="grow" type="text" />
|
|
||||||
</label>
|
|
||||||
{/snippet}
|
|
||||||
</Field>
|
|
||||||
<Field>
|
|
||||||
{#snippet label()}
|
|
||||||
<p>About You</p>
|
|
||||||
{/snippet}
|
|
||||||
{#snippet input()}
|
|
||||||
<textarea class="textarea textarea-bordered leading-4" rows="3" bind:value={values.about}>
|
|
||||||
</textarea>
|
|
||||||
{/snippet}
|
|
||||||
</Field>
|
|
||||||
<Field>
|
|
||||||
{#snippet label()}
|
|
||||||
<p>Nostr Address</p>
|
|
||||||
{/snippet}
|
|
||||||
{#snippet input()}
|
|
||||||
<label class="input input-bordered flex w-full items-center gap-2">
|
|
||||||
<Icon icon="map-point" />
|
|
||||||
<input bind:value={values.nip05} class="grow" type="text" />
|
|
||||||
</label>
|
|
||||||
{/snippet}
|
|
||||||
{#snippet info()}
|
|
||||||
<p>
|
|
||||||
<Button class="link" onclick={() => pushModal(InfoHandle)}>What is a nostr address?</Button>
|
|
||||||
</p>
|
|
||||||
{/snippet}
|
|
||||||
</Field>
|
|
||||||
<div class="mt-4 flex flex-row items-center justify-between gap-4">
|
|
||||||
<Button class="btn btn-neutral" onclick={back}>Discard Changes</Button>
|
|
||||||
<Button type="submit" class="btn btn-primary">Save Changes</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type {Snippet} from "svelte"
|
||||||
|
import type {Profile} from "@welshman/util"
|
||||||
|
import {makeProfile} from "@welshman/util"
|
||||||
|
import {preventDefault} from "@lib/html"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Field from "@lib/components/Field.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import InputProfilePicture from "@lib/components/InputProfilePicture.svelte"
|
||||||
|
import InfoHandle from "@app/components/InfoHandle.svelte"
|
||||||
|
import {pushModal} from "@app/modal"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
initialValues?: Profile
|
||||||
|
onsubmit: (profile: Profile) => void
|
||||||
|
hideAddress?: boolean
|
||||||
|
footer: Snippet
|
||||||
|
}
|
||||||
|
|
||||||
|
const {initialValues = makeProfile(), hideAddress, onsubmit, footer}: Props = $props()
|
||||||
|
|
||||||
|
const values = $state(initialValues)
|
||||||
|
|
||||||
|
const submit = () => onsubmit($state.snapshot(values))
|
||||||
|
|
||||||
|
let file: File | undefined = $state()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form class="col-4" onsubmit={preventDefault(submit)}>
|
||||||
|
<div class="flex justify-center py-2">
|
||||||
|
<InputProfilePicture bind:file bind:url={values.picture} />
|
||||||
|
</div>
|
||||||
|
<Field>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>Username</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<label class="input input-bordered flex w-full items-center gap-2">
|
||||||
|
<Icon icon="user-circle" />
|
||||||
|
<input bind:value={values.name} class="grow" type="text" />
|
||||||
|
</label>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet info()}
|
||||||
|
What would you like people to call you?
|
||||||
|
{/snippet}
|
||||||
|
</Field>
|
||||||
|
<Field>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>About You</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<textarea class="textarea textarea-bordered leading-4" rows="3" bind:value={values.about}
|
||||||
|
></textarea>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet info()}
|
||||||
|
Give a brief introduction to why you're here.
|
||||||
|
{/snippet}
|
||||||
|
</Field>
|
||||||
|
{#if !hideAddress}
|
||||||
|
<Field>
|
||||||
|
{#snippet label()}
|
||||||
|
<p>Nostr Address</p>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<label class="input input-bordered flex w-full items-center gap-2">
|
||||||
|
<Icon icon="map-point" />
|
||||||
|
<input bind:value={values.nip05} class="grow" type="text" />
|
||||||
|
</label>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet info()}
|
||||||
|
<p>
|
||||||
|
<Button class="link" onclick={() => pushModal(InfoHandle)}
|
||||||
|
>What is a nostr address?</Button>
|
||||||
|
</p>
|
||||||
|
{/snippet}
|
||||||
|
</Field>
|
||||||
|
{/if}
|
||||||
|
{@render footer()}
|
||||||
|
</form>
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import {Capacitor} from "@capacitor/core"
|
||||||
import {postJson} from "@welshman/lib"
|
import {postJson} from "@welshman/lib"
|
||||||
import {isMobile, preventDefault} from "@lib/html"
|
import {isMobile, preventDefault} from "@lib/html"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
@@ -8,16 +9,22 @@
|
|||||||
import Spinner from "@lib/components/Spinner.svelte"
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
import LogIn from "@app/components/LogIn.svelte"
|
import LogIn from "@app/components/LogIn.svelte"
|
||||||
import InfoNostr from "@app/components/InfoNostr.svelte"
|
import InfoNostr from "@app/components/InfoNostr.svelte"
|
||||||
|
import SignUpKey from "@app/components/SignUpKey.svelte"
|
||||||
import SignUpSuccess from "@app/components/SignUpSuccess.svelte"
|
import SignUpSuccess from "@app/components/SignUpSuccess.svelte"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
import {BURROW_URL, PLATFORM_NAME} from "@app/state"
|
import {BURROW_URL, PLATFORM_NAME, PLATFORM_ACCENT} from "@app/state"
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/toast"
|
||||||
|
|
||||||
const ac = window.location.origin
|
const params = new URLSearchParams({
|
||||||
|
an: PLATFORM_NAME,
|
||||||
|
ac: window.location.origin,
|
||||||
|
at: isMobile ? "android" : "web",
|
||||||
|
aa: PLATFORM_ACCENT.slice(1),
|
||||||
|
am: "dark",
|
||||||
|
asf: "yes",
|
||||||
|
})
|
||||||
|
|
||||||
const at = isMobile ? "android" : "web"
|
const nstart = `https://start.njump.me/?${params.toString()}`
|
||||||
|
|
||||||
const nstart = `https://start.njump.me/?an=Flotilla&at=${at}&ac=${ac}`
|
|
||||||
|
|
||||||
const login = () => pushModal(LogIn)
|
const login = () => pushModal(LogIn)
|
||||||
|
|
||||||
@@ -37,18 +44,20 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const signup = () => {
|
const usePassword = () => {
|
||||||
if (BURROW_URL) {
|
if (BURROW_URL) {
|
||||||
signupPassword()
|
signupPassword()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const useKey = () => pushModal(SignUpKey)
|
||||||
|
|
||||||
let email = $state("")
|
let email = $state("")
|
||||||
let password = $state("")
|
let password = $state("")
|
||||||
let loading = $state(false)
|
let loading = $state(false)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form class="column gap-4" onsubmit={preventDefault(signup)}>
|
<form class="column gap-4" onsubmit={preventDefault(usePassword)}>
|
||||||
<h1 class="heading">Sign up with Nostr</h1>
|
<h1 class="heading">Sign up with Nostr</h1>
|
||||||
<p class="m-auto max-w-sm text-center">
|
<p class="m-auto max-w-sm text-center">
|
||||||
{PLATFORM_NAME} is built using the
|
{PLATFORM_NAME} is built using the
|
||||||
@@ -89,10 +98,17 @@
|
|||||||
</p>
|
</p>
|
||||||
<Divider>Or</Divider>
|
<Divider>Or</Divider>
|
||||||
{/if}
|
{/if}
|
||||||
<a href={nstart} class="btn {email || password ? 'btn-neutral' : 'btn-primary'}">
|
{#if Capacitor.isNativePlatform()}
|
||||||
<Icon icon="square-share-line" />
|
<Button onclick={useKey} class="btn {email || password ? 'btn-neutral' : 'btn-primary'}">
|
||||||
Get going on nstart
|
<Icon icon="key" />
|
||||||
</a>
|
Generate a key
|
||||||
|
</Button>
|
||||||
|
{:else}
|
||||||
|
<a href={nstart} class="btn {email || password ? 'btn-neutral' : 'btn-primary'}">
|
||||||
|
<Icon icon="square-share-line" />
|
||||||
|
Create an account on Nstart
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
Already have an account?
|
Already have an account?
|
||||||
<Button class="link" onclick={login}>Log in instead</Button>
|
<Button class="link" onclick={login}>Log in instead</Button>
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {encrypt} from "nostr-tools/nip49"
|
||||||
|
import {hexToBytes} from "@noble/hashes/utils"
|
||||||
|
import {makeSecret, getPubkey} from "@welshman/signer"
|
||||||
|
import {preventDefault, downloadText} from "@lib/html"
|
||||||
|
import Link from "@lib/components/Link.svelte"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Field from "@lib/components/Field.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
|
import SignUpKeyConfirm from "@app/components/SignUpKeyConfirm.svelte"
|
||||||
|
import {pushToast} from "@app/toast"
|
||||||
|
import {pushModal} from "@app/modal"
|
||||||
|
|
||||||
|
const secret = makeSecret()
|
||||||
|
|
||||||
|
const pubkey = getPubkey(secret)
|
||||||
|
|
||||||
|
const back = () => history.back()
|
||||||
|
|
||||||
|
const next = () => {
|
||||||
|
if (password.length < 12) {
|
||||||
|
return pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: "Passwords must be at least 12 characters long.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const ncryptsec = encrypt(hexToBytes(secret), password)
|
||||||
|
|
||||||
|
downloadText("Nostr Secret Key.txt", ncryptsec)
|
||||||
|
|
||||||
|
pushModal(SignUpKeyConfirm, {secret, pubkey, ncryptsec})
|
||||||
|
}
|
||||||
|
|
||||||
|
let password = ""
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form class="column gap-4" onsubmit={preventDefault(next)}>
|
||||||
|
<ModalHeader>
|
||||||
|
{#snippet title()}
|
||||||
|
<div>Welcome to Nostr!</div>
|
||||||
|
{/snippet}
|
||||||
|
</ModalHeader>
|
||||||
|
<p>
|
||||||
|
<Link external href="https://nostr.com/" class="link">Nostr</Link> is way to build social apps that
|
||||||
|
talk to each other. Users own their social identity instead of renting it from a tech company, and
|
||||||
|
can take it with them.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
This means that instead of using a password to log in, you generate a <strong
|
||||||
|
>secret key</strong>
|
||||||
|
which gives you full control over your account.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Keeping this key safe is very important, so we encourage you to download an encrypted copy. To
|
||||||
|
do this, go ahead and fill in the password you'd like to use to secure your key below.
|
||||||
|
</p>
|
||||||
|
<Field>
|
||||||
|
{#snippet label()}
|
||||||
|
Password*
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<label class="input input-bordered flex w-full items-center gap-2">
|
||||||
|
<Icon icon="key" />
|
||||||
|
<input bind:value={password} class="grow" type="password" />
|
||||||
|
</label>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet info()}
|
||||||
|
<p>Passwords should be at least 12 characters long. Write this down!</p>
|
||||||
|
{/snippet}
|
||||||
|
</Field>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button class="btn btn-link" onclick={back}>
|
||||||
|
<Icon icon="alt-arrow-left" />
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
<Button class="btn btn-primary" type="submit">
|
||||||
|
Download my key
|
||||||
|
<Icon icon="alt-arrow-right" />
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</form>
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {preventDefault, copyToClipboard} from "@lib/html"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Field from "@lib/components/Field.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
|
import SignUpProfile from "@app/components/SignUpProfile.svelte"
|
||||||
|
import {pushToast} from "@app/toast"
|
||||||
|
import {pushModal} from "@app/modal"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
secret: string
|
||||||
|
pubkey: string
|
||||||
|
ncryptsec: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const {secret, pubkey, ncryptsec}: Props = $props()
|
||||||
|
|
||||||
|
const back = () => history.back()
|
||||||
|
|
||||||
|
const copy = () => {
|
||||||
|
copyToClipboard(ncryptsec)
|
||||||
|
pushToast({message: "Your secret key has been copied to your clipboard!"})
|
||||||
|
}
|
||||||
|
|
||||||
|
const next = () => {
|
||||||
|
pushModal(SignUpProfile, {secret, pubkey})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form class="column gap-4" onsubmit={preventDefault(next)}>
|
||||||
|
<ModalHeader>
|
||||||
|
{#snippet title()}
|
||||||
|
<div>Download your key</div>
|
||||||
|
{/snippet}
|
||||||
|
</ModalHeader>
|
||||||
|
<p>
|
||||||
|
Great! We've encrypted your secret key and saved it to your device. If that didn't work, or if
|
||||||
|
you'd rather save your key somewhere else, you can find the encrypted version below:
|
||||||
|
</p>
|
||||||
|
<Field>
|
||||||
|
{#snippet label()}
|
||||||
|
Encrypted Secret Key
|
||||||
|
{/snippet}
|
||||||
|
{#snippet input()}
|
||||||
|
<label class="input input-bordered flex w-full items-center gap-2">
|
||||||
|
<Icon icon="key" />
|
||||||
|
<input value={ncryptsec} class="ellipsize grow" />
|
||||||
|
<Button onclick={copy} class="flex items-center">
|
||||||
|
<Icon icon="copy" />
|
||||||
|
</Button>
|
||||||
|
</label>
|
||||||
|
{/snippet}
|
||||||
|
</Field>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button class="btn btn-link" onclick={back}>
|
||||||
|
<Icon icon="alt-arrow-left" />
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
<Button class="btn btn-primary" type="submit">
|
||||||
|
Fill out your profile
|
||||||
|
<Icon icon="alt-arrow-right" />
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</form>
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type {Profile} from "@welshman/util"
|
||||||
|
import {PROFILE, createProfile, createEvent} from "@welshman/util"
|
||||||
|
import {addSession, publishThunk} from "@welshman/app"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import ProfileEditForm from "@app/components/ProfileEditForm.svelte"
|
||||||
|
import {INDEXER_RELAYS} from "@app/state"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
secret: string
|
||||||
|
pubkey: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const {secret, pubkey}: Props = $props()
|
||||||
|
|
||||||
|
const onsubmit = (profile: Profile) => {
|
||||||
|
const event = createEvent(PROFILE, createProfile(profile))
|
||||||
|
|
||||||
|
addSession({method: "nip01", secret, pubkey})
|
||||||
|
publishThunk({event, relays: INDEXER_RELAYS})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ProfileEditForm hideAddress {onsubmit}>
|
||||||
|
{#snippet footer()}
|
||||||
|
<Button type="submit" class="btn btn-primary">Create Account</Button>
|
||||||
|
{/snippet}
|
||||||
|
</ProfileEditForm>
|
||||||
+71
-4
@@ -1,5 +1,19 @@
|
|||||||
import {get, writable} from "svelte/store"
|
import {get, writable} from "svelte/store"
|
||||||
import {partition, shuffle, int, YEAR, MONTH, insert, sortBy, assoc, now} from "@welshman/lib"
|
import {
|
||||||
|
partition,
|
||||||
|
chunk,
|
||||||
|
sample,
|
||||||
|
sleep,
|
||||||
|
shuffle,
|
||||||
|
uniq,
|
||||||
|
int,
|
||||||
|
YEAR,
|
||||||
|
MONTH,
|
||||||
|
insert,
|
||||||
|
sortBy,
|
||||||
|
assoc,
|
||||||
|
now,
|
||||||
|
} from "@welshman/lib"
|
||||||
import {
|
import {
|
||||||
MESSAGE,
|
MESSAGE,
|
||||||
DELETE,
|
DELETE,
|
||||||
@@ -9,10 +23,11 @@ import {
|
|||||||
matchFilters,
|
matchFilters,
|
||||||
getTagValues,
|
getTagValues,
|
||||||
getTagValue,
|
getTagValue,
|
||||||
|
isShareableRelayUrl,
|
||||||
} from "@welshman/util"
|
} from "@welshman/util"
|
||||||
import type {TrustedEvent, Filter} from "@welshman/util"
|
import type {TrustedEvent, Filter, List} from "@welshman/util"
|
||||||
import {feedFromFilters, makeRelayFeed, makeIntersectionFeed} from "@welshman/feeds"
|
import {feedFromFilters, makeRelayFeed, makeIntersectionFeed} from "@welshman/feeds"
|
||||||
import type {Subscription} from "@welshman/net"
|
import type {Subscription, SubscribeRequestWithHandlers} from "@welshman/net"
|
||||||
import type {AppSyncOpts, Thunk} from "@welshman/app"
|
import type {AppSyncOpts, Thunk} from "@welshman/app"
|
||||||
import {
|
import {
|
||||||
subscribe,
|
subscribe,
|
||||||
@@ -22,10 +37,23 @@ import {
|
|||||||
hasNegentropy,
|
hasNegentropy,
|
||||||
thunkWorker,
|
thunkWorker,
|
||||||
createFeedController,
|
createFeedController,
|
||||||
|
loadRelay,
|
||||||
|
loadMutes,
|
||||||
|
loadFollows,
|
||||||
|
loadProfile,
|
||||||
|
loadInboxRelaySelections,
|
||||||
|
getRelayUrls,
|
||||||
} from "@welshman/app"
|
} from "@welshman/app"
|
||||||
import {createScroller} from "@lib/html"
|
import {createScroller} from "@lib/html"
|
||||||
import {daysBetween} from "@lib/util"
|
import {daysBetween} from "@lib/util"
|
||||||
import {userRoomsByUrl, getUrlsForEvent} from "@app/state"
|
import {
|
||||||
|
INDEXER_RELAYS,
|
||||||
|
getDefaultPubkeys,
|
||||||
|
userRoomsByUrl,
|
||||||
|
getUrlsForEvent,
|
||||||
|
loadMembership,
|
||||||
|
loadSettings,
|
||||||
|
} from "@app/state"
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
|
|
||||||
@@ -317,3 +345,42 @@ export const listenForNotifications = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const loadUserData = (
|
||||||
|
pubkey: string,
|
||||||
|
request: Partial<SubscribeRequestWithHandlers> = {},
|
||||||
|
) => {
|
||||||
|
const promise = Promise.race([
|
||||||
|
sleep(3000),
|
||||||
|
Promise.all([
|
||||||
|
loadInboxRelaySelections(pubkey, request),
|
||||||
|
loadMembership(pubkey, request),
|
||||||
|
loadSettings(pubkey, request),
|
||||||
|
loadProfile(pubkey, request),
|
||||||
|
loadFollows(pubkey, request),
|
||||||
|
loadMutes(pubkey, request),
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
|
||||||
|
// Load followed profiles slowly in the background without clogging other stuff up. Only use a single
|
||||||
|
// indexer relay to avoid too many redundant validations, which slow things down and eat bandwidth
|
||||||
|
promise.then(async () => {
|
||||||
|
for (const pubkeys of chunk(50, getDefaultPubkeys())) {
|
||||||
|
const relays = sample(1, INDEXER_RELAYS)
|
||||||
|
|
||||||
|
await sleep(1000)
|
||||||
|
|
||||||
|
for (const pubkey of pubkeys) {
|
||||||
|
loadMembership(pubkey, {relays})
|
||||||
|
loadProfile(pubkey, {relays})
|
||||||
|
loadFollows(pubkey, {relays})
|
||||||
|
loadMutes(pubkey, {relays})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return promise
|
||||||
|
}
|
||||||
|
|
||||||
|
export const discoverRelays = (lists: List[]) =>
|
||||||
|
Promise.all(uniq(lists.flatMap(getRelayUrls)).filter(isShareableRelayUrl).map(loadRelay))
|
||||||
|
|||||||
+5
-1
@@ -345,7 +345,11 @@ export const hasMembershipUrl = (list: List | undefined, url: string) =>
|
|||||||
export const getMembershipUrls = (list?: List) => {
|
export const getMembershipUrls = (list?: List) => {
|
||||||
const tags = getListTags(list)
|
const tags = getListTags(list)
|
||||||
|
|
||||||
return sort(uniq([...getRelayTagValues(tags), ...getGroupTags(tags).map(nth(2))]))
|
return sort(
|
||||||
|
uniq([...getRelayTagValues(tags), ...getGroupTags(tags).map(nth(2))]).map(url =>
|
||||||
|
normalizeRelayUrl(url),
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getMembershipRooms = (list?: List) =>
|
export const getMembershipRooms = (list?: List) =>
|
||||||
|
|||||||
@@ -76,3 +76,16 @@ export const createScroller = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const isMobile = "ontouchstart" in document.documentElement
|
export const isMobile = "ontouchstart" in document.documentElement
|
||||||
|
|
||||||
|
export const downloadText = (filename: string, text: string) => {
|
||||||
|
const blob = new Blob([text], {type: "text/plain"})
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
const a = document.createElement("a")
|
||||||
|
|
||||||
|
a.href = url
|
||||||
|
a.download = filename
|
||||||
|
document.body.appendChild(a)
|
||||||
|
a.click()
|
||||||
|
document.body.removeChild(a)
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
}
|
||||||
|
|||||||
@@ -67,8 +67,8 @@
|
|||||||
import {nsecDecode} from "@lib/util"
|
import {nsecDecode} from "@lib/util"
|
||||||
import {theme} from "@app/theme"
|
import {theme} from "@app/theme"
|
||||||
import {INDEXER_RELAYS, userMembership, ensureUnwrapped, canDecrypt} from "@app/state"
|
import {INDEXER_RELAYS, userMembership, ensureUnwrapped, canDecrypt} from "@app/state"
|
||||||
import {loadUserData, loginWithNip46} from "@app/commands"
|
import {loadUserData, listenForNotifications} from "@app/requests"
|
||||||
import {listenForNotifications} from "@app/requests"
|
import {loginWithNip46} from "@app/commands"
|
||||||
import * as commands from "@app/commands"
|
import * as commands from "@app/commands"
|
||||||
import * as requests from "@app/requests"
|
import * as requests from "@app/requests"
|
||||||
import * as notifications from "@app/notifications"
|
import * as notifications from "@app/notifications"
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import {addToMapKey, dec, gt} from "@welshman/lib"
|
import {addToMapKey, dec, gt} from "@welshman/lib"
|
||||||
import type {Relay} from "@welshman/app"
|
import type {Relay} from "@welshman/app"
|
||||||
import {relays, createSearch} from "@welshman/app"
|
import {relays, createSearch, loadRelay, loadRelaySelections} from "@welshman/app"
|
||||||
import {createScroller} from "@lib/html"
|
import {createScroller} from "@lib/html"
|
||||||
|
import {fly} from "@lib/transition"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Page from "@lib/components/Page.svelte"
|
import Page from "@lib/components/Page.svelte"
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
@@ -14,15 +15,26 @@
|
|||||||
import SpaceCheck from "@app/components/SpaceCheck.svelte"
|
import SpaceCheck from "@app/components/SpaceCheck.svelte"
|
||||||
import ProfileCircles from "@app/components/ProfileCircles.svelte"
|
import ProfileCircles from "@app/components/ProfileCircles.svelte"
|
||||||
import {
|
import {
|
||||||
memberships,
|
|
||||||
membershipByPubkey,
|
membershipByPubkey,
|
||||||
getMembershipUrls,
|
getMembershipUrls,
|
||||||
|
loadMembership,
|
||||||
userRoomsByUrl,
|
userRoomsByUrl,
|
||||||
getDefaultPubkeys,
|
getDefaultPubkeys,
|
||||||
} from "@app/state"
|
} from "@app/state"
|
||||||
import {discoverRelays} from "@app/commands"
|
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
|
|
||||||
|
const discoverRelays = () =>
|
||||||
|
Promise.all(
|
||||||
|
getDefaultPubkeys().map(async pubkey => {
|
||||||
|
await loadRelaySelections(pubkey)
|
||||||
|
|
||||||
|
const membership = await loadMembership(pubkey)
|
||||||
|
const urls = getMembershipUrls(membership)
|
||||||
|
|
||||||
|
await Promise.all(urls.map(url => loadRelay(url)))
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
const wotGraph = $derived.by(() => {
|
const wotGraph = $derived.by(() => {
|
||||||
const scores = new Map<string, Set<string>>()
|
const scores = new Map<string, Set<string>>()
|
||||||
|
|
||||||
@@ -36,20 +48,23 @@
|
|||||||
})
|
})
|
||||||
|
|
||||||
const relaySearch = $derived(
|
const relaySearch = $derived(
|
||||||
createSearch($relays, {
|
createSearch(
|
||||||
getValue: (relay: Relay) => relay.url,
|
$relays.filter(r => wotGraph.has(r.url)),
|
||||||
sortFn: ({score, item}) => {
|
{
|
||||||
if (score && score > 0.1) return -score!
|
getValue: (relay: Relay) => relay.url,
|
||||||
|
sortFn: ({score, item}) => {
|
||||||
|
if (score && score > 0.1) return -score!
|
||||||
|
|
||||||
const wotScore = wotGraph.get(item.url)?.size || 0
|
const wotScore = wotGraph.get(item.url)?.size || 0
|
||||||
|
|
||||||
return score ? dec(score) * wotScore : -wotScore
|
return score ? dec(score) * wotScore : -wotScore
|
||||||
|
},
|
||||||
|
fuseOptions: {
|
||||||
|
keys: ["url", "name", {name: "description", weight: 0.3}],
|
||||||
|
shouldSort: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
fuseOptions: {
|
),
|
||||||
keys: ["url", "name", {name: "description", weight: 0.3}],
|
|
||||||
shouldSort: false,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const openSpace = (url: string) => pushModal(SpaceCheck, {url})
|
const openSpace = (url: string) => pushModal(SpaceCheck, {url})
|
||||||
@@ -128,9 +143,9 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</Button>
|
</Button>
|
||||||
{/each}
|
{/each}
|
||||||
{#await discoverRelays($memberships)}
|
{#await discoverRelays()}
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center py-20" out:fly>
|
||||||
<Spinner loading>Loading more relays...</Spinner>
|
<Spinner loading>Looking for spaces...</Spinner>
|
||||||
</div>
|
</div>
|
||||||
{/await}
|
{/await}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
import Avatar from "@lib/components/Avatar.svelte"
|
import Avatar from "@lib/components/Avatar.svelte"
|
||||||
import Content from "@app/components/Content.svelte"
|
import Content from "@app/components/Content.svelte"
|
||||||
import ProfileEdit from "@app/components/ProfileEdit.svelte"
|
import ProfileEdit from "@app/components/ProfileEdit.svelte"
|
||||||
|
import ProfileDelete from "@app/components/ProfileDelete.svelte"
|
||||||
import InfoKeys from "@app/components/InfoKeys.svelte"
|
import InfoKeys from "@app/components/InfoKeys.svelte"
|
||||||
import {PLATFORM_NAME} from "@app/state"
|
import {PLATFORM_NAME} from "@app/state"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
@@ -25,6 +26,8 @@
|
|||||||
const startEdit = () => pushModal(ProfileEdit)
|
const startEdit = () => pushModal(ProfileEdit)
|
||||||
|
|
||||||
const startEject = () => pushModal(InfoKeys)
|
const startEject = () => pushModal(InfoKeys)
|
||||||
|
|
||||||
|
const startDelete = () => pushModal(ProfileDelete)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="content column gap-4">
|
<div class="content column gap-4">
|
||||||
@@ -117,4 +120,10 @@
|
|||||||
</FieldInline>
|
</FieldInline>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="card2 bg-alt col-4 shadow-xl">
|
||||||
|
<Button class="btn btn-outline btn-error" onclick={startDelete}>
|
||||||
|
<Icon icon="trash-bin-2" />
|
||||||
|
Delete your profile
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,7 +16,8 @@
|
|||||||
import RelayItem from "@app/components/RelayItem.svelte"
|
import RelayItem from "@app/components/RelayItem.svelte"
|
||||||
import RelayAdd from "@app/components/RelayAdd.svelte"
|
import RelayAdd from "@app/components/RelayAdd.svelte"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
import {setRelayPolicy, discoverRelays, setInboxRelayPolicy} from "@app/commands"
|
import {discoverRelays} from "@app/requests"
|
||||||
|
import {setRelayPolicy, setInboxRelayPolicy} from "@app/commands"
|
||||||
|
|
||||||
const readRelayUrls = derived(userRelaySelections, getReadRelayUrls)
|
const readRelayUrls = derived(userRelaySelections, getReadRelayUrls)
|
||||||
const writeRelayUrls = derived(userRelaySelections, getWriteRelayUrls)
|
const writeRelayUrls = derived(userRelaySelections, getWriteRelayUrls)
|
||||||
|
|||||||
Reference in New Issue
Block a user