forked from coracle/flotilla
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cee6c3c164 | |||
| 06d0ae2798 | |||
| b129ef4242 | |||
| 48a45f3a3a | |||
| ce1fb396e3 | |||
| e95c57bcb7 | |||
| 414f5a5ace | |||
| a331d24bb1 | |||
| fb53e53411 |
@@ -1,5 +1,14 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
# 0.2.6
|
||||||
|
|
||||||
|
* Add reply to long-press menu
|
||||||
|
* Fix @-mentions
|
||||||
|
* Replace nsec.app signup with njump.me
|
||||||
|
* Add new messages button in rooms
|
||||||
|
* Add media server settings
|
||||||
|
* Add build hash to about page
|
||||||
|
|
||||||
# 0.2.5
|
# 0.2.5
|
||||||
|
|
||||||
* Improve room and data loading
|
* Improve room and data loading
|
||||||
|
|||||||
@@ -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 5
|
versionCode 6
|
||||||
versionName "0.2.5"
|
versionName "0.2.6"
|
||||||
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.
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ fi
|
|||||||
# https://stackoverflow.com/a/69127685/1467342
|
# https://stackoverflow.com/a/69127685/1467342
|
||||||
eval "$temp_env"
|
eval "$temp_env"
|
||||||
|
|
||||||
|
if [[ -z $VITE_BUILD_HASH ]]; then
|
||||||
|
export VITE_BUILD_HASH=$(git rev-parse --short HEAD)
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ $VITE_PLATFORM_LOGO =~ ^https://* ]]; then
|
if [[ $VITE_PLATFORM_LOGO =~ ^https://* ]]; then
|
||||||
curl $VITE_PLATFORM_LOGO > static/logo.png
|
curl $VITE_PLATFORM_LOGO > static/logo.png
|
||||||
export VITE_PLATFORM_LOGO=static/logo.png
|
export VITE_PLATFORM_LOGO=static/logo.png
|
||||||
|
|||||||
Generated
+13
-13
@@ -21,9 +21,9 @@
|
|||||||
"@vite-pwa/assets-generator": "^0.2.6",
|
"@vite-pwa/assets-generator": "^0.2.6",
|
||||||
"@vite-pwa/sveltekit": "^0.6.6",
|
"@vite-pwa/sveltekit": "^0.6.6",
|
||||||
"@welshman/app": "~0.0.41",
|
"@welshman/app": "~0.0.41",
|
||||||
"@welshman/content": "~0.0.15",
|
"@welshman/content": "~0.0.16",
|
||||||
"@welshman/dvm": "~0.0.14",
|
"@welshman/dvm": "~0.0.14",
|
||||||
"@welshman/editor": "~0.0.8",
|
"@welshman/editor": "~0.0.10",
|
||||||
"@welshman/feeds": "~0.0.30",
|
"@welshman/feeds": "~0.0.30",
|
||||||
"@welshman/lib": "~0.0.38",
|
"@welshman/lib": "~0.0.38",
|
||||||
"@welshman/net": "~0.0.46",
|
"@welshman/net": "~0.0.46",
|
||||||
@@ -4508,13 +4508,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@welshman/content": {
|
"node_modules/@welshman/content": {
|
||||||
"version": "0.0.15",
|
"version": "0.0.16",
|
||||||
"resolved": "https://registry.npmjs.org/@welshman/content/-/content-0.0.15.tgz",
|
"resolved": "https://registry.npmjs.org/@welshman/content/-/content-0.0.16.tgz",
|
||||||
"integrity": "sha512-y0f0iLIaHUqEJJ0ziRWbGw13mg0tOLTKpHQNgIXJ03PD3xGHBaQ5xPWiOI8XeUt35KgrayvQZHsaqfAsOWkwag==",
|
"integrity": "sha512-042LG1rgyzDvwPzEISliY7wG0Hb8s+efhRR15DBtFBnbzZy5xRLihOiw/N6S2bMN+BoY6iBnilantwr0yfV5Cg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@braintree/sanitize-url": "^7.0.2",
|
"@braintree/sanitize-url": "^7.0.2",
|
||||||
"@welshman/lib": "~0.0.34",
|
"@welshman/lib": "~0.0.37",
|
||||||
"nostr-tools": "^2.7.2"
|
"nostr-tools": "^2.7.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -4535,9 +4535,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@welshman/editor": {
|
"node_modules/@welshman/editor": {
|
||||||
"version": "0.0.8",
|
"version": "0.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/@welshman/editor/-/editor-0.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@welshman/editor/-/editor-0.0.10.tgz",
|
||||||
"integrity": "sha512-9zQy1MjbGhTARTPH3QEgyhEGNKy239VSD5Jo4llsiUUhHQVNQj1KpdSVQnTLOwUBbfSLeYPIfMRUm2Gq5Cn/Ew==",
|
"integrity": "sha512-685+KUYrGHzSgz2V6hRRltyp3LMhdHKvYBqkc9DyDNZoz3qqn4pUsOF+SMGSBnisiXBEZrKj2d52Jv46FT81iQ==",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@tiptap/core": "^2.9.1",
|
"@tiptap/core": "^2.9.1",
|
||||||
"@tiptap/extension-code": "^2.9.1",
|
"@tiptap/extension-code": "^2.9.1",
|
||||||
@@ -4554,7 +4554,7 @@
|
|||||||
"@tiptap/suggestion": "^2.9.1",
|
"@tiptap/suggestion": "^2.9.1",
|
||||||
"@welshman/lib": "~0.0.36",
|
"@welshman/lib": "~0.0.36",
|
||||||
"@welshman/util": "~0.0.53",
|
"@welshman/util": "~0.0.53",
|
||||||
"nostr-editor": "^0.0.4-pre.6",
|
"nostr-editor": "^0.0.4-pre.7",
|
||||||
"nostr-tools": "^2.8.1",
|
"nostr-tools": "^2.8.1",
|
||||||
"svelte": "^4.0.0",
|
"svelte": "^4.0.0",
|
||||||
"svelte-tiptap": "^1.0.0"
|
"svelte-tiptap": "^1.0.0"
|
||||||
@@ -9880,9 +9880,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/nostr-editor": {
|
"node_modules/nostr-editor": {
|
||||||
"version": "0.0.4-pre.6",
|
"version": "0.0.4-pre.7",
|
||||||
"resolved": "https://registry.npmjs.org/nostr-editor/-/nostr-editor-0.0.4-pre.6.tgz",
|
"resolved": "https://registry.npmjs.org/nostr-editor/-/nostr-editor-0.0.4-pre.7.tgz",
|
||||||
"integrity": "sha512-njOWThC8tfvyw59rcCeJjwTycZ0QdwBYURpMJAzPVoWFjxZmzmTeBNNznf00g3U7jwXde0VuI9OElzAaTdkz8w==",
|
"integrity": "sha512-Xg+562ywJLLUa2nZ+XoJDarqRjx7fdc2/oKNuuxoAitp95LFTXGyupDBigJs8Q9o9GVpGLsfCbXTeIGT/b9G8g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
+3
-3
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "flotilla",
|
"name": "flotilla",
|
||||||
"version": "0.2.5",
|
"version": "0.2.6",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
@@ -53,9 +53,9 @@
|
|||||||
"@vite-pwa/assets-generator": "^0.2.6",
|
"@vite-pwa/assets-generator": "^0.2.6",
|
||||||
"@vite-pwa/sveltekit": "^0.6.6",
|
"@vite-pwa/sveltekit": "^0.6.6",
|
||||||
"@welshman/app": "~0.0.41",
|
"@welshman/app": "~0.0.41",
|
||||||
"@welshman/content": "~0.0.15",
|
"@welshman/content": "~0.0.16",
|
||||||
"@welshman/dvm": "~0.0.14",
|
"@welshman/dvm": "~0.0.14",
|
||||||
"@welshman/editor": "~0.0.8",
|
"@welshman/editor": "~0.0.10",
|
||||||
"@welshman/feeds": "~0.0.30",
|
"@welshman/feeds": "~0.0.30",
|
||||||
"@welshman/lib": "~0.0.38",
|
"@welshman/lib": "~0.0.38",
|
||||||
"@welshman/net": "~0.0.46",
|
"@welshman/net": "~0.0.46",
|
||||||
|
|||||||
+34
-42
@@ -185,45 +185,53 @@
|
|||||||
@apply -m-1 min-h-12 p-1;
|
@apply -m-1 min-h-12 p-1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tiptap[contenteditable="true"] {
|
.tiptap {
|
||||||
|
--tiptap-object-bg: var(--base-100);
|
||||||
|
--tiptap-object-fg: var(--base-content);
|
||||||
|
--tiptap-active-bg: var(--primary);
|
||||||
|
--tiptap-active-fg: var(--primary-content);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tiptap-suggestions {
|
||||||
|
--tiptap-object-bg: var(--base-100);
|
||||||
|
--tiptap-object-fg: var(--base-content);
|
||||||
|
--tiptap-active-bg: var(--base-300);
|
||||||
|
--tiptap-active-fg: var(--base-content);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tiptap {
|
||||||
@apply max-h-[350px] overflow-y-auto p-2 px-4;
|
@apply max-h-[350px] overflow-y-auto p-2 px-4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-editor .tiptap[contenteditable="true"] {
|
.tiptap p.is-editor-empty:first-child::before {
|
||||||
@apply rounded-box bg-base-300;
|
opacity: 40%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-editor .tiptap[contenteditable="true"] {
|
.chat-editor .tiptap {
|
||||||
@apply input input-bordered h-auto p-[.65rem];
|
@apply rounded-box bg-base-300 pr-12;
|
||||||
}
|
}
|
||||||
|
|
||||||
.note-editor .tiptap[contenteditable="true"] {
|
.note-editor .tiptap {
|
||||||
|
--tiptap-object-bg: var(--base-200);
|
||||||
@apply input input-bordered h-auto min-h-32 rounded-box p-[.65rem] pb-6;
|
@apply input input-bordered h-auto min-h-32 rounded-box p-[.65rem] pb-6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tiptap pre code {
|
.input-editor .tiptap {
|
||||||
@apply link-content block w-full;
|
--tiptap-object-bg: var(--base-200);
|
||||||
|
@apply input input-bordered h-auto p-[.65rem];
|
||||||
}
|
}
|
||||||
|
|
||||||
.tiptap p code {
|
/* link-content, based on tiptap */
|
||||||
@apply link-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
.link-content,
|
.link-content {
|
||||||
.tiptap [tag] {
|
max-width: 100%;
|
||||||
@apply max-w-full overflow-hidden text-ellipsis whitespace-nowrap rounded bg-neutral px-1 text-neutral-content no-underline;
|
overflow: hidden;
|
||||||
}
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
.link-content.link-content-selected {
|
border-radius: 3px;
|
||||||
@apply bg-primary text-primary-content;
|
padding: 0 0.25rem;
|
||||||
}
|
background-color: var(--base-100);
|
||||||
|
color: var(--base-content);
|
||||||
.tiptap p.is-editor-empty:first-child::before {
|
|
||||||
content: attr(data-placeholder);
|
|
||||||
float: left;
|
|
||||||
height: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
opacity: 50%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* date input */
|
/* date input */
|
||||||
@@ -248,19 +256,3 @@ emoji-picker {
|
|||||||
--input-font-color: var(--base-content);
|
--input-font-color: var(--base-content);
|
||||||
--outline-color: var(--base-100);
|
--outline-color: var(--base-100);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* tiptap */
|
|
||||||
|
|
||||||
.tiptap {
|
|
||||||
--tiptap-object-bg: var(--base-100);
|
|
||||||
--tiptap-object-fg: var(--base-content);
|
|
||||||
--tiptap-active-bg: var(--primary);
|
|
||||||
--tiptap-active-fg: var(--primary-content);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tiptap-suggestions {
|
|
||||||
--tiptap-object-bg: var(--base-100);
|
|
||||||
--tiptap-object-fg: var(--base-content);
|
|
||||||
--tiptap-active-bg: var(--base-300);
|
|
||||||
--tiptap-active-fg: var(--base-content);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
transition:slide>
|
transition:slide>
|
||||||
<p class="text-primary">Replying to @{displayProfileByPubkey(event.pubkey)}</p>
|
<p class="text-primary">Replying to @{displayProfileByPubkey(event.pubkey)}</p>
|
||||||
{#key event.id}
|
{#key event.id}
|
||||||
<Content {event} minLength={100} maxLength={300} expandMode="disabled" />
|
<Content {event} hideMedia minLength={100} maxLength={300} expandMode="disabled" />
|
||||||
{/key}
|
{/key}
|
||||||
<Button class="absolute right-2 top-2 cursor-pointer" on:click={clear}>
|
<Button class="absolute right-2 top-2 cursor-pointer" on:click={clear}>
|
||||||
<Icon icon="close-circle" />
|
<Icon icon="close-circle" />
|
||||||
|
|||||||
@@ -40,7 +40,7 @@
|
|||||||
|
|
||||||
const reply = () => replyTo(event)
|
const reply = () => replyTo(event)
|
||||||
|
|
||||||
const onLongPress = () => pushModal(ChannelMessageMenuMobile, {url, event})
|
const onLongPress = () => pushModal(ChannelMessageMenuMobile, {url, event, reply})
|
||||||
|
|
||||||
const openProfile = () => pushModal(ProfileDetail, {pubkey: event.pubkey})
|
const openProfile = () => pushModal(ProfileDetail, {pubkey: event.pubkey})
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
export let url
|
export let url
|
||||||
export let event
|
export let event
|
||||||
|
export let reply
|
||||||
|
|
||||||
const onEmoji = (emoji: NativeEmoji) => {
|
const onEmoji = (emoji: NativeEmoji) => {
|
||||||
history.back()
|
history.back()
|
||||||
@@ -19,6 +20,11 @@
|
|||||||
|
|
||||||
const showEmojiPicker = () => pushModal(EmojiPicker, {onClick: onEmoji}, {replaceState: true})
|
const showEmojiPicker = () => pushModal(EmojiPicker, {onClick: onEmoji}, {replaceState: true})
|
||||||
|
|
||||||
|
const sendReply = () => {
|
||||||
|
history.back()
|
||||||
|
reply()
|
||||||
|
}
|
||||||
|
|
||||||
const showInfo = () => pushModal(EventInfo, {event}, {replaceState: true})
|
const showInfo = () => pushModal(EventInfo, {event}, {replaceState: true})
|
||||||
|
|
||||||
const showDelete = () => pushModal(ConfirmDelete, {url, event})
|
const showDelete = () => pushModal(ConfirmDelete, {url, event})
|
||||||
@@ -29,6 +35,10 @@
|
|||||||
<Icon size={4} icon="smile-circle" />
|
<Icon size={4} icon="smile-circle" />
|
||||||
Send Reaction
|
Send Reaction
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button class="btn btn-neutral w-full" on:click={sendReply}>
|
||||||
|
<Icon size={4} icon="reply" />
|
||||||
|
Send Reply
|
||||||
|
</Button>
|
||||||
<Button class="btn btn-neutral" on:click={showInfo}>
|
<Button class="btn btn-neutral" on:click={showInfo}>
|
||||||
<Icon size={4} icon="code-2" />
|
<Icon size={4} icon="code-2" />
|
||||||
Message Details
|
Message Details
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
</video>
|
</video>
|
||||||
{:else if url.match(/\.(jpe?g|png|gif|webp)$/)}
|
{:else if url.match(/\.(jpe?g|png|gif|webp)$/)}
|
||||||
<button type="button" on:click|stopPropagation|preventDefault={expand}>
|
<button type="button" on:click|stopPropagation|preventDefault={expand}>
|
||||||
<img alt="Link preview" src={imgproxy(url)} class="m-auto max-h-96" />
|
<img alt="Link preview" src={imgproxy(url)} class="m-auto max-h-96 rounded-box" />
|
||||||
</button>
|
</button>
|
||||||
{:else}
|
{:else}
|
||||||
{#await loadPreview()}
|
{#await loadPreview()}
|
||||||
|
|||||||
@@ -1,107 +1,50 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {postJson, assoc} from "@welshman/lib"
|
import {postJson} from "@welshman/lib"
|
||||||
import {makeSecret, Nip46Broker} from "@welshman/signer"
|
import {isMobile} from "@lib/html"
|
||||||
import {pubkey, loadHandle, updateSession} from "@welshman/app"
|
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Field from "@lib/components/Field.svelte"
|
|
||||||
import FieldInline from "@lib/components/FieldInline.svelte"
|
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||||
import Link from "@lib/components/Link.svelte"
|
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Divider from "@lib/components/Divider.svelte"
|
import Divider from "@lib/components/Divider.svelte"
|
||||||
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 SignUpSuccess from "@app/components/SignUpSuccess.svelte"
|
import SignUpSuccess from "@app/components/SignUpSuccess.svelte"
|
||||||
import {pushModal, clearModals} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
import {setChecked} from "@app/notifications"
|
import {BURROW_URL, PLATFORM_NAME} from "@app/state"
|
||||||
import {BURROW_URL, PLATFORM_NAME, NIP46_PERMS} from "@app/state"
|
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/toast"
|
||||||
import {loginWithNip46} from "@app/commands"
|
|
||||||
|
|
||||||
const relays = ["wss://relay.nsec.app"]
|
const ac = window.location.origin
|
||||||
|
|
||||||
const signerDomain = "nsec.app"
|
const at = isMobile ? "android" : "web"
|
||||||
|
|
||||||
const signerPubkey = "e24a86943d37a91ab485d6f9a7c66097c25ddd67e8bd1b75ed252a3c266cf9bb"
|
const nstart = `https://start.njump.me/?an=Flotilla&at=${at}&ac=${ac}`
|
||||||
|
|
||||||
const login = () => pushModal(LogIn)
|
const login = () => pushModal(LogIn)
|
||||||
|
|
||||||
const withLoading =
|
const signupPassword = async () => {
|
||||||
(cb: (...args: any[]) => any) =>
|
loading = true
|
||||||
async (...args: any[]) => {
|
|
||||||
loading = true
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await cb(...args)
|
const res = await postJson(BURROW_URL + "/user", {email, password})
|
||||||
} finally {
|
|
||||||
loading = false
|
if (res.error) {
|
||||||
|
pushToast({message: res.error, theme: "error"})
|
||||||
|
} else {
|
||||||
|
pushModal(SignUpSuccess, {email}, {replaceState: true})
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
loading = false
|
||||||
}
|
}
|
||||||
|
}
|
||||||
const signupPassword = withLoading(async () => {
|
|
||||||
const res = await postJson(BURROW_URL + "/user", {email, password})
|
|
||||||
|
|
||||||
if (res.error) {
|
|
||||||
return pushToast({message: res.error, theme: "error"})
|
|
||||||
}
|
|
||||||
|
|
||||||
pushModal(SignUpSuccess, {email}, {replaceState: true})
|
|
||||||
})
|
|
||||||
|
|
||||||
const signupNsecApp = withLoading(async () => {
|
|
||||||
const handle = await loadHandle(`${username}@${signerDomain}`)
|
|
||||||
|
|
||||||
if (handle?.pubkey) {
|
|
||||||
return pushToast({
|
|
||||||
theme: "error",
|
|
||||||
message: "Sorry, it looks like that account already exists. Try logging in instead.",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const clientSecret = makeSecret()
|
|
||||||
const broker = Nip46Broker.get({
|
|
||||||
relays,
|
|
||||||
clientSecret,
|
|
||||||
signerPubkey,
|
|
||||||
algorithm: "nip04",
|
|
||||||
})
|
|
||||||
|
|
||||||
const userPubkey = await broker.createAccount(username, signerDomain, NIP46_PERMS)
|
|
||||||
|
|
||||||
if (!userPubkey) {
|
|
||||||
return pushToast({
|
|
||||||
theme: "error",
|
|
||||||
message: "Sorry, it looks like something went wrong. Please try again.",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now we can log in. Use the user's pubkey for the handler (legacy stuff)
|
|
||||||
const success = await loginWithNip46({relays, signerPubkey: userPubkey, clientSecret})
|
|
||||||
|
|
||||||
if (!success) {
|
|
||||||
return pushToast({
|
|
||||||
theme: "error",
|
|
||||||
message: "Sorry, it looks like something went wrong. Please try again.",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
updateSession($pubkey!, assoc("email", email))
|
|
||||||
pushToast({message: "Successfully logged in!"})
|
|
||||||
setChecked("*")
|
|
||||||
clearModals()
|
|
||||||
})
|
|
||||||
|
|
||||||
const signup = () => {
|
const signup = () => {
|
||||||
if (BURROW_URL) {
|
if (BURROW_URL) {
|
||||||
signupPassword()
|
signupPassword()
|
||||||
} else {
|
|
||||||
signupNsecApp()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let email = ""
|
let email = ""
|
||||||
let password = ""
|
let password = ""
|
||||||
let username = ""
|
|
||||||
let loading = false
|
let loading = false
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -136,29 +79,12 @@
|
|||||||
on other nostr applications, you can create a nostr key yourself, or export your key from {PLATFORM_NAME}
|
on other nostr applications, you can create a nostr key yourself, or export your key from {PLATFORM_NAME}
|
||||||
later.
|
later.
|
||||||
</p>
|
</p>
|
||||||
{:else}
|
<Divider>Or</Divider>
|
||||||
<Field>
|
|
||||||
<div class="flex items-center gap-2" slot="input">
|
|
||||||
<label class="input input-bordered flex w-full items-center gap-2">
|
|
||||||
<Icon icon="user-rounded" />
|
|
||||||
<input bind:value={username} class="grow" type="text" placeholder="username" />
|
|
||||||
</label>
|
|
||||||
@{signerDomain}
|
|
||||||
</div>
|
|
||||||
</Field>
|
|
||||||
<Button type="submit" class="btn btn-primary" disabled={loading || !username}>
|
|
||||||
<Spinner {loading}>Sign Up</Spinner>
|
|
||||||
<Icon icon="alt-arrow-right" />
|
|
||||||
</Button>
|
|
||||||
{/if}
|
{/if}
|
||||||
<Divider>Or</Divider>
|
<a href={nstart} class="btn {email || password ? 'btn-neutral' : 'btn-primary'}">
|
||||||
<Link
|
|
||||||
external
|
|
||||||
href="https://nosta.me"
|
|
||||||
class="btn {username || email || password ? 'btn-neutral' : 'btn-primary'}">
|
|
||||||
<Icon icon="square-share-line" />
|
<Icon icon="square-share-line" />
|
||||||
Get started on Nosta.me
|
Get started on njump
|
||||||
</Link>
|
</a>
|
||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
Already have an account?
|
Already have an account?
|
||||||
<Button class="link" on:click={login}>Log in instead</Button>
|
<Button class="link" on:click={login}>Log in instead</Button>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
import Tippy from "@lib/components/Tippy.svelte"
|
import Tippy from "@lib/components/Tippy.svelte"
|
||||||
|
|
||||||
export let value: string
|
export let value: string
|
||||||
export let options: string[]
|
export let options: string[] = []
|
||||||
export let allowCreate = false
|
export let allowCreate = false
|
||||||
|
|
||||||
let input: Element
|
let input: Element
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
createSearch(options, {
|
createSearch(options, {
|
||||||
getValue: identity,
|
getValue: identity,
|
||||||
fuseOptions: {keys: [""]},
|
fuseOptions: {keys: [""]},
|
||||||
}),
|
}).searchValues,
|
||||||
)
|
)
|
||||||
|
|
||||||
const select = (newValue: string) => {
|
const select = (newValue: string) => {
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import {hexToBytes, bytesToHex} from "@noble/hashes/utils"
|
||||||
|
import * as nip19 from "nostr-tools/nip19"
|
||||||
|
|
||||||
export const displayList = <T>(xs: T[], conj = "and", n = 6, locale = "en-US") => {
|
export const displayList = <T>(xs: T[], conj = "and", n = 6, locale = "en-US") => {
|
||||||
const stringItems = xs.map(String)
|
const stringItems = xs.map(String)
|
||||||
|
|
||||||
@@ -11,3 +14,13 @@ export const displayList = <T>(xs: T[], conj = "and", n = 6, locale = "en-US") =
|
|||||||
|
|
||||||
return new Intl.ListFormat(locale, {style: "long", type: "conjunction"}).format(stringItems)
|
return new Intl.ListFormat(locale, {style: "long", type: "conjunction"}).format(stringItems)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const nsecEncode = (secret: string) => nip19.nsecEncode(hexToBytes(secret))
|
||||||
|
|
||||||
|
export const nsecDecode = (nsec: string) => {
|
||||||
|
const {type, data} = nip19.decode(nsec)
|
||||||
|
|
||||||
|
if (type !== "nsec") throw new Error(`Invalid nsec: ${nsec}`)
|
||||||
|
|
||||||
|
return bytesToHex(data)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import "@src/app.css"
|
import "@src/app.css"
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import {nip19} from "nostr-tools"
|
import * as nip19 from "nostr-tools/nip19"
|
||||||
import {get, derived} from "svelte/store"
|
import {get, derived} from "svelte/store"
|
||||||
import {App} from "@capacitor/app"
|
import {App} from "@capacitor/app"
|
||||||
import {dev} from "$app/environment"
|
import {dev} from "$app/environment"
|
||||||
|
import {goto} from "$app/navigation"
|
||||||
import {bytesToHex, hexToBytes} from "@noble/hashes/utils"
|
import {bytesToHex, hexToBytes} from "@noble/hashes/utils"
|
||||||
import {identity, sleep, take, sortBy, ago, now, HOUR, WEEK, MONTH, Worker} from "@welshman/lib"
|
import {identity, sleep, take, sortBy, ago, now, HOUR, WEEK, MONTH, Worker} from "@welshman/lib"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
@@ -21,6 +22,7 @@
|
|||||||
getPubkeyTagValues,
|
getPubkeyTagValues,
|
||||||
getListTags,
|
getListTags,
|
||||||
} from "@welshman/util"
|
} from "@welshman/util"
|
||||||
|
import {Nip46Broker, getPubkey, makeSecret} from "@welshman/signer"
|
||||||
import {
|
import {
|
||||||
relays,
|
relays,
|
||||||
handles,
|
handles,
|
||||||
@@ -39,6 +41,7 @@
|
|||||||
getRelayUrls,
|
getRelayUrls,
|
||||||
subscribe,
|
subscribe,
|
||||||
userInboxRelaySelections,
|
userInboxRelaySelections,
|
||||||
|
addSession,
|
||||||
} from "@welshman/app"
|
} from "@welshman/app"
|
||||||
import * as lib from "@welshman/lib"
|
import * as lib from "@welshman/lib"
|
||||||
import * as util from "@welshman/util"
|
import * as util from "@welshman/util"
|
||||||
@@ -49,9 +52,10 @@
|
|||||||
import ModalContainer from "@app/components/ModalContainer.svelte"
|
import ModalContainer from "@app/components/ModalContainer.svelte"
|
||||||
import {setupTracking} from "@app/tracking"
|
import {setupTracking} from "@app/tracking"
|
||||||
import {setupAnalytics} from "@app/analytics"
|
import {setupAnalytics} from "@app/analytics"
|
||||||
|
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} from "@app/commands"
|
import {loadUserData, loginWithNip46} from "@app/commands"
|
||||||
import {listenForNotifications} from "@app/requests"
|
import {listenForNotifications} from "@app/requests"
|
||||||
import * as commands from "@app/commands"
|
import * as commands from "@app/commands"
|
||||||
import * as requests from "@app/requests"
|
import * as requests from "@app/requests"
|
||||||
@@ -82,6 +86,34 @@
|
|||||||
...notifications,
|
...notifications,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Nstart login
|
||||||
|
if (window.location.hash?.startsWith("#nostr-login")) {
|
||||||
|
const params = new URLSearchParams(window.location.hash.slice(1))
|
||||||
|
const login = params.get("nostr-login")
|
||||||
|
|
||||||
|
let success = false
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (login?.startsWith("bunker://")) {
|
||||||
|
success = await loginWithNip46({
|
||||||
|
clientSecret: makeSecret(),
|
||||||
|
...Nip46Broker.parseBunkerUrl(login),
|
||||||
|
})
|
||||||
|
} else if (login) {
|
||||||
|
const secret = nsecDecode(login)
|
||||||
|
|
||||||
|
addSession({method: "nip01", secret, pubkey: getPubkey(secret)})
|
||||||
|
success = true
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
goto("/home")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!db) {
|
if (!db) {
|
||||||
setupTracking()
|
setupTracking()
|
||||||
setupAnalytics()
|
setupAnalytics()
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
import {pubkey, signer, userMutes, tagPubkey, publishThunk} from "@welshman/app"
|
import {pubkey, signer, userMutes, tagPubkey, publishThunk} from "@welshman/app"
|
||||||
import Field from "@lib/components/Field.svelte"
|
import Field from "@lib/components/Field.svelte"
|
||||||
import FieldInline from "@lib/components/FieldInline.svelte"
|
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import ProfileMultiSelect from "@app/components/ProfileMultiSelect.svelte"
|
import ProfileMultiSelect from "@app/components/ProfileMultiSelect.svelte"
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/toast"
|
||||||
@@ -79,6 +80,24 @@
|
|||||||
{settings.send_delay === 1000 ? "second" : "seconds"}.
|
{settings.send_delay === 1000 ? "second" : "seconds"}.
|
||||||
</p>
|
</p>
|
||||||
</FieldInline>
|
</FieldInline>
|
||||||
|
<Field>
|
||||||
|
<p slot="label">Media Server</p>
|
||||||
|
<div slot="input" class="flex gap-2">
|
||||||
|
<select bind:value={settings.upload_type} class="select select-bordered">
|
||||||
|
<option value="nip96">NIP 96 (default)</option>
|
||||||
|
<option value="blossom">Blossom</option>
|
||||||
|
</select>
|
||||||
|
<label class="input input-bordered flex flex-grow items-center gap-2">
|
||||||
|
<Icon icon="link-round" />
|
||||||
|
{#if settings.upload_type === "nip96"}
|
||||||
|
<input class="grow" bind:value={settings.nip96_urls[0]} />
|
||||||
|
{:else}
|
||||||
|
<input class="grow" bind:value={settings.blossom_urls[0]} />
|
||||||
|
{/if}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<p slot="info">Choose a media server type and url for files you upload to flotilla.</p>
|
||||||
|
</Field>
|
||||||
<div class="mt-4 flex flex-row items-center justify-between gap-4">
|
<div class="mt-4 flex flex-row items-center justify-between gap-4">
|
||||||
<Button class="btn btn-neutral" on:click={reset}>Discard Changes</Button>
|
<Button class="btn btn-neutral" on:click={reset}>Discard Changes</Button>
|
||||||
<Button type="submit" class="btn btn-primary">Save Changes</Button>
|
<Button type="submit" class="btn btn-primary">Save Changes</Button>
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
import {PLATFORM_NAME} from "@app/state"
|
import {PLATFORM_NAME} from "@app/state"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
|
|
||||||
|
const hash = import.meta.env.VITE_BUILD_HASH
|
||||||
|
|
||||||
const pubkey = "97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322"
|
const pubkey = "97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322"
|
||||||
|
|
||||||
const openProfile = () => pushModal(ProfileDetail, {pubkey})
|
const openProfile = () => pushModal(ProfileDetail, {pubkey})
|
||||||
@@ -48,6 +50,9 @@
|
|||||||
class="link"
|
class="link"
|
||||||
href="https://www.figma.com/community/file/1166831539721848736">480 Design</Link>
|
href="https://www.figma.com/community/file/1166831539721848736">480 Design</Link>
|
||||||
</p>
|
</p>
|
||||||
|
{#if hash}
|
||||||
|
<p class="text-xs">Running build {hash.slice(0, 8)}</p>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-center gap-4">
|
<div class="flex justify-center gap-4">
|
||||||
<div class="tooltip" data-tip="Source Code">
|
<div class="tooltip" data-tip="Source Code">
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
import {now} from "@welshman/lib"
|
import {now} from "@welshman/lib"
|
||||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||||
import {createEvent, MESSAGE, DELETE, REACTION} from "@welshman/util"
|
import {createEvent, MESSAGE, DELETE, REACTION} from "@welshman/util"
|
||||||
import {formatTimestampAsDate, publishThunk, deriveRelay, repository} from "@welshman/app"
|
import {formatTimestampAsDate, pubkey, publishThunk, deriveRelay, repository} from "@welshman/app"
|
||||||
import {slide, fade} from "@lib/transition"
|
import {slide, fade, fly} from "@lib/transition"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
displayChannel,
|
displayChannel,
|
||||||
getEventsForUrl,
|
getEventsForUrl,
|
||||||
} from "@app/state"
|
} from "@app/state"
|
||||||
import {setChecked} from "@app/notifications"
|
import {setChecked, checked} from "@app/notifications"
|
||||||
import {
|
import {
|
||||||
nip29,
|
nip29,
|
||||||
addRoomMembership,
|
addRoomMembership,
|
||||||
@@ -40,6 +40,7 @@
|
|||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/toast"
|
||||||
|
|
||||||
const {room = GENERAL} = $page.params
|
const {room = GENERAL} = $page.params
|
||||||
|
const lastChecked = $checked[$page.url.pathname]
|
||||||
const content = popKey<string>("content") || ""
|
const content = popKey<string>("content") || ""
|
||||||
const url = decodeRelay($page.params.relay)
|
const url = decodeRelay($page.params.relay)
|
||||||
const filter = {kinds: [MESSAGE], "#h": [room]}
|
const filter = {kinds: [MESSAGE], "#h": [room]}
|
||||||
@@ -89,11 +90,33 @@
|
|||||||
clearParent()
|
clearParent()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onScroll = () => {
|
||||||
|
showScrollButton = Math.abs(element?.scrollTop || 0) > 1500
|
||||||
|
|
||||||
|
if (!newMessages || newMessagesSeen) {
|
||||||
|
showFixedNewMessages = false
|
||||||
|
} else {
|
||||||
|
const {y} = newMessages.getBoundingClientRect()
|
||||||
|
|
||||||
|
if (y > 300) {
|
||||||
|
newMessagesSeen = true
|
||||||
|
} else {
|
||||||
|
showFixedNewMessages = y < 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const scrollToNewMessages = () =>
|
||||||
|
newMessages.scrollIntoView({behavior: "smooth", block: "center"})
|
||||||
|
|
||||||
const scrollToBottom = () => element.scrollTo({top: 0, behavior: "smooth"})
|
const scrollToBottom = () => element.scrollTo({top: 0, behavior: "smooth"})
|
||||||
|
|
||||||
let parent: TrustedEvent | undefined
|
let parent: TrustedEvent | undefined
|
||||||
let loading = true
|
let loading = true
|
||||||
let element: HTMLElement
|
let element: HTMLElement
|
||||||
|
let newMessages: HTMLElement
|
||||||
|
let newMessagesSeen = false
|
||||||
|
let showFixedNewMessages = false
|
||||||
let showScrollButton = false
|
let showScrollButton = false
|
||||||
let cleanup: () => void
|
let cleanup: () => void
|
||||||
let events: Readable<TrustedEvent[]>
|
let events: Readable<TrustedEvent[]>
|
||||||
@@ -107,6 +130,7 @@
|
|||||||
|
|
||||||
let previousDate
|
let previousDate
|
||||||
let previousPubkey
|
let previousPubkey
|
||||||
|
let newMessagesSeen = false
|
||||||
|
|
||||||
if (events) {
|
if (events) {
|
||||||
for (const event of $events.toReversed()) {
|
for (const event of $events.toReversed()) {
|
||||||
@@ -118,6 +142,16 @@
|
|||||||
|
|
||||||
const date = formatTimestampAsDate(created_at)
|
const date = formatTimestampAsDate(created_at)
|
||||||
|
|
||||||
|
if (
|
||||||
|
!newMessagesSeen &&
|
||||||
|
event.pubkey !== $pubkey &&
|
||||||
|
lastChecked &&
|
||||||
|
created_at > lastChecked
|
||||||
|
) {
|
||||||
|
elements.push({type: "new-messages", id: "new-messages"})
|
||||||
|
newMessagesSeen = true
|
||||||
|
}
|
||||||
|
|
||||||
if (date !== previousDate) {
|
if (date !== previousDate) {
|
||||||
elements.push({type: "date", value: date, id: date, showPubkey: false})
|
elements.push({type: "date", value: date, id: date, showPubkey: false})
|
||||||
}
|
}
|
||||||
@@ -136,13 +170,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
elements.reverse()
|
elements.reverse()
|
||||||
|
|
||||||
|
setTimeout(onScroll, 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
if (element) {
|
if (element) {
|
||||||
element.addEventListener("scroll", () => {
|
|
||||||
showScrollButton = Math.abs(element.scrollTop) > 1500
|
|
||||||
})
|
|
||||||
;({events, cleanup} = makeFeed({
|
;({events, cleanup} = makeFeed({
|
||||||
element,
|
element,
|
||||||
relays: [url],
|
relays: [url],
|
||||||
@@ -191,9 +224,19 @@
|
|||||||
</PageBar>
|
</PageBar>
|
||||||
<div
|
<div
|
||||||
class="scroll-container -mt-2 flex flex-grow flex-col-reverse overflow-y-auto overflow-x-hidden py-2"
|
class="scroll-container -mt-2 flex flex-grow flex-col-reverse overflow-y-auto overflow-x-hidden py-2"
|
||||||
|
on:scroll={onScroll}
|
||||||
bind:this={element}>
|
bind:this={element}>
|
||||||
{#each elements as { type, id, value, showPubkey } (id)}
|
{#each elements as { type, id, value, showPubkey } (id)}
|
||||||
{#if type === "date"}
|
{#if type === "new-messages"}
|
||||||
|
<div
|
||||||
|
bind:this={newMessages}
|
||||||
|
class="flex items-center py-2 text-xs transition-colors"
|
||||||
|
class:opacity-0={showFixedNewMessages}>
|
||||||
|
<div class="h-px flex-grow bg-primary" />
|
||||||
|
<p class="rounded-full bg-primary px-2 py-1 text-primary-content">New Messages</p>
|
||||||
|
<div class="h-px flex-grow bg-primary" />
|
||||||
|
</div>
|
||||||
|
{:else if type === "date"}
|
||||||
<Divider>{value}</Divider>
|
<Divider>{value}</Divider>
|
||||||
{:else}
|
{:else}
|
||||||
<div in:slide class:-mt-1={!showPubkey}>
|
<div in:slide class:-mt-1={!showPubkey}>
|
||||||
@@ -209,6 +252,15 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
{#if showFixedNewMessages}
|
||||||
|
<div class="relative z-feature flex justify-center">
|
||||||
|
<div transition:fly={{duration: 200}} class="fixed top-12">
|
||||||
|
<Button class="btn btn-primary btn-xs rounded-full" on:click={scrollToNewMessages}>
|
||||||
|
New Messages
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
<div>
|
<div>
|
||||||
{#if parent}
|
{#if parent}
|
||||||
<ChannelComposeParent event={parent} clear={clearParent} />
|
<ChannelComposeParent event={parent} clear={clearParent} />
|
||||||
|
|||||||
Reference in New Issue
Block a user