Compare commits

...

9 Commits

Author SHA1 Message Date
Jon Staab cee6c3c164 Bump versions 2025-01-28 19:22:57 -08:00
Jon Staab 06d0ae2798 Trim tiptap css 2025-01-28 16:23:19 -08:00
Jon Staab b129ef4242 Add build hash 2025-01-28 14:51:33 -08:00
Jon Staab 48a45f3a3a Add media server settings 2025-01-28 14:44:43 -08:00
Jon Staab ce1fb396e3 Add button to scroll to new messages in channel 2025-01-28 14:19:46 -08:00
Jon Staab e95c57bcb7 Replace nsec.app signup with njump.me 2025-01-28 13:04:37 -08:00
Jon Staab 414f5a5ace Update changelog 2025-01-28 12:33:35 -08:00
Jon Staab a331d24bb1 Bump welshman 2025-01-28 12:28:26 -08:00
Jon Staab fb53e53411 Add reply to long press menu 2025-01-28 09:47:11 -08:00
17 changed files with 233 additions and 171 deletions
+9
View File
@@ -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
+2 -2
View File
@@ -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.
+4
View File
@@ -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
+13 -13
View File
@@ -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
View File
@@ -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
View File
@@ -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" />
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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()}
+23 -97
View File
@@ -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>
+2 -2
View File
@@ -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) => {
+13
View File
@@ -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)
}
+34 -2
View File
@@ -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()
+19
View File
@@ -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>
+5
View File
@@ -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">
+59 -7
View File
@@ -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} />