Compare commits

..

1 Commits

23 changed files with 195 additions and 375 deletions
-1
View File
@@ -85,7 +85,6 @@
"date-picker-svelte": "^2.17.0", "date-picker-svelte": "^2.17.0",
"dotenv": "^16.6.1", "dotenv": "^16.6.1",
"emoji-picker-element": "^1.28.1", "emoji-picker-element": "^1.28.1",
"emoji-picker-element-data": "^1.8.0",
"fuse.js": "^7.1.0", "fuse.js": "^7.1.0",
"husky": "^9.1.7", "husky": "^9.1.7",
"idb": "^8.0.3", "idb": "^8.0.3",
-8
View File
@@ -137,9 +137,6 @@ importers:
emoji-picker-element: emoji-picker-element:
specifier: ^1.28.1 specifier: ^1.28.1
version: 1.28.1 version: 1.28.1
emoji-picker-element-data:
specifier: ^1.8.0
version: 1.8.0
fuse.js: fuse.js:
specifier: ^7.1.0 specifier: ^7.1.0
version: 7.1.0 version: 7.1.0
@@ -2827,9 +2824,6 @@ packages:
resolution: {integrity: sha512-wkgGT6kugeQk/P6VZ/f4T+4HB41BVgNBq5CDIZVbQ02nvTVqAiVTbskxxu3eA/X96lMlfYOwnLQpN2v5E1zDEg==} resolution: {integrity: sha512-wkgGT6kugeQk/P6VZ/f4T+4HB41BVgNBq5CDIZVbQ02nvTVqAiVTbskxxu3eA/X96lMlfYOwnLQpN2v5E1zDEg==}
engines: {node: '>= 0.4.0'} engines: {node: '>= 0.4.0'}
emoji-picker-element-data@1.8.0:
resolution: {integrity: sha512-VfRuRJNEDLS1JKlNS4olaqhjX5S1nnZ+ZHG73b/dV8QeZyi0yPruTPEE72EmF6XO3k/9hj3lybMIYMOYXb/57A==}
emoji-picker-element@1.28.1: emoji-picker-element@1.28.1:
resolution: {integrity: sha512-8c64IPish2PWoV9oYCo2pvuPHwIv+uK9bO0dfpPyMupDAvaWL9ZvYhWNTAR+2sx7BhfRjciImqP6CIUgNX+DMg==} resolution: {integrity: sha512-8c64IPish2PWoV9oYCo2pvuPHwIv+uK9bO0dfpPyMupDAvaWL9ZvYhWNTAR+2sx7BhfRjciImqP6CIUgNX+DMg==}
@@ -8034,8 +8028,6 @@ snapshots:
dependencies: dependencies:
sax: 1.1.4 sax: 1.1.4
emoji-picker-element-data@1.8.0: {}
emoji-picker-element@1.28.1: {} emoji-picker-element@1.28.1: {}
emoji-regex@8.0.0: {} emoji-regex@8.0.0: {}
+1 -7
View File
@@ -15,7 +15,6 @@
import ModalHeader from "@lib/components/ModalHeader.svelte" import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalTitle from "@lib/components/ModalTitle.svelte" import ModalTitle from "@lib/components/ModalTitle.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte" import ModalFooter from "@lib/components/ModalFooter.svelte"
import ProgressBar from "@app/components/ProgressBar.svelte"
import {pushToast} from "@app/util/toast" import {pushToast} from "@app/util/toast"
import {PLATFORM_NAME} from "@app/core/state" import {PLATFORM_NAME} from "@app/core/state"
@@ -23,11 +22,9 @@
secret: string secret: string
next: () => unknown next: () => unknown
submitText?: string submitText?: string
step?: number
totalSteps?: number
} }
const {secret, next, submitText = "Continue", step, totalSteps}: Props = $props() const {secret, next, submitText = "Continue"}: Props = $props()
const back = () => history.back() const back = () => history.back()
@@ -153,9 +150,6 @@
</Button> </Button>
</div> </div>
</ModalBody> </ModalBody>
{#if step && totalSteps}
<ProgressBar current={step} total={totalSteps} />
{/if}
<ModalFooter> <ModalFooter>
<Button class="btn btn-link" onclick={back}> <Button class="btn btn-link" onclick={back}>
<Icon icon={AltArrowLeft} /> <Icon icon={AltArrowLeft} />
+1 -5
View File
@@ -23,10 +23,9 @@
onsubmit: (values: Values) => void onsubmit: (values: Values) => void
isSignup?: boolean isSignup?: boolean
footer: Snippet footer: Snippet
progressBar?: Snippet
} }
const {initialValues, isSignup, onsubmit, footer, progressBar}: Props = $props() const {initialValues, isSignup, onsubmit, footer}: Props = $props()
const values = $state(initialValues) const values = $state(initialValues)
@@ -104,9 +103,6 @@
</Field> </Field>
{/if} {/if}
</ModalBody> </ModalBody>
{#if progressBar}
{@render progressBar()}
{/if}
<ModalFooter> <ModalFooter>
{@render footer()} {@render footer()}
</ModalFooter> </ModalFooter>
-9
View File
@@ -1,9 +0,0 @@
<script lang="ts">
const {current, total}: {current: number; total: number} = $props()
</script>
<div class="flex w-full">
{#each Array(total) as _, i}
<div class="h-1 flex-1 transition-colors {i < current ? 'bg-primary' : 'bg-base-300'}"></div>
{/each}
</div>
+6 -8
View File
@@ -62,10 +62,9 @@
const flows = { const flows = {
email: { email: {
start: () => pushModal(SignUpEmail, {next: flows.email.profile, step: 1, totalSteps: 3}), start: () => pushModal(SignUpEmail, {next: flows.email.profile}),
profile: () => pushModal(SignUpProfile, {next: flows.email.complete, step: 2, totalSteps: 3}), profile: () => pushModal(SignUpProfile, {next: flows.email.complete}),
complete: () => complete: () => pushModal(SignUpComplete, {next: flows.email.finalize}),
pushModal(SignUpComplete, {next: flows.email.finalize, step: 3, totalSteps: 3}),
finalize: () => { finalize: () => {
const email = getKey<string>("signup.email")! const email = getKey<string>("signup.email")!
const clientOptions = getKey<ClientOptions>("signup.clientOptions")! const clientOptions = getKey<ClientOptions>("signup.clientOptions")!
@@ -75,10 +74,9 @@
}, },
}, },
nostr: { nostr: {
start: () => pushModal(SignUpProfile, {next: flows.nostr.key, step: 1, totalSteps: 3}), start: () => pushModal(SignUpProfile, {next: flows.nostr.key}),
key: () => pushModal(SignUpKey, {next: flows.nostr.complete, step: 2, totalSteps: 3}), key: () => pushModal(SignUpKey, {next: flows.nostr.complete}),
complete: () => complete: () => pushModal(SignUpComplete, {next: flows.nostr.finalize}),
pushModal(SignUpComplete, {next: flows.nostr.finalize, step: 3, totalSteps: 3}),
finalize: () => { finalize: () => {
const secret = getKey<string>("signup.secret")! const secret = getKey<string>("signup.secret")!
+1 -7
View File
@@ -9,15 +9,12 @@
import ModalHeader from "@lib/components/ModalHeader.svelte" import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalTitle from "@lib/components/ModalTitle.svelte" import ModalTitle from "@lib/components/ModalTitle.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte" import ModalFooter from "@lib/components/ModalFooter.svelte"
import ProgressBar from "@app/components/ProgressBar.svelte"
type Props = { type Props = {
next: () => void next: () => void
step?: number
totalSteps?: number
} }
const {next, step, totalSteps}: Props = $props() const {next}: Props = $props()
const back = () => history.back() const back = () => history.back()
</script> </script>
@@ -36,9 +33,6 @@
on groups you've already joined. Click below to get started! on groups you've already joined. Click below to get started!
</p> </p>
</ModalBody> </ModalBody>
{#if step && totalSteps}
<ProgressBar current={step} total={totalSteps} />
{/if}
<ModalFooter> <ModalFooter>
<Button class="btn btn-link" onclick={back}> <Button class="btn btn-link" onclick={back}>
<Icon icon={AltArrowLeft} /> <Icon icon={AltArrowLeft} />
+2 -8
View File
@@ -18,17 +18,14 @@
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte" import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte" import ModalFooter from "@lib/components/ModalFooter.svelte"
import SignUpEmailConfirm from "@app/components/SignUpEmailConfirm.svelte" import SignUpEmailConfirm from "@app/components/SignUpEmailConfirm.svelte"
import ProgressBar from "@app/components/ProgressBar.svelte"
import {pushToast, popToast} from "@app/util/toast" import {pushToast, popToast} from "@app/util/toast"
import {pushModal} from "@app/util/modal" import {pushModal} from "@app/util/modal"
type Props = { type Props = {
next: () => void next: () => void
step?: number
totalSteps?: number
} }
const {next, step, totalSteps}: Props = $props() const {next}: Props = $props()
const back = () => history.back() const back = () => history.back()
@@ -84,7 +81,7 @@
setKey("signup.clientOptions", clientOptions) setKey("signup.clientOptions", clientOptions)
popToast(toastId) popToast(toastId)
pushModal(SignUpEmailConfirm, {next, step, totalSteps}) pushModal(SignUpEmailConfirm, {next})
} catch (e) { } catch (e) {
console.error(e) console.error(e)
@@ -142,9 +139,6 @@
{/snippet} {/snippet}
</FieldInline> </FieldInline>
</ModalBody> </ModalBody>
{#if step && totalSteps}
<ProgressBar current={step} total={totalSteps} />
{/if}
<ModalFooter> <ModalFooter>
<Button class="btn btn-link" onclick={back}> <Button class="btn btn-link" onclick={back}>
<Icon icon={AltArrowLeft} /> <Icon icon={AltArrowLeft} />
+1 -7
View File
@@ -15,15 +15,12 @@
import ModalTitle from "@lib/components/ModalTitle.svelte" import ModalTitle from "@lib/components/ModalTitle.svelte"
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte" import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte" import ModalFooter from "@lib/components/ModalFooter.svelte"
import ProgressBar from "@app/components/ProgressBar.svelte"
type Props = { type Props = {
next: () => void next: () => void
step?: number
totalSteps?: number
} }
const {next, step, totalSteps}: Props = $props() const {next}: Props = $props()
const email = getKey<string>("signup.email") const email = getKey<string>("signup.email")
@@ -64,9 +61,6 @@
above. above.
</p> </p>
</ModalBody> </ModalBody>
{#if step && totalSteps}
<ProgressBar current={step} total={totalSteps} />
{/if}
<ModalFooter> <ModalFooter>
<Button class="btn btn-link" onclick={back} disabled={loading}> <Button class="btn btn-link" onclick={back} disabled={loading}>
<Icon icon={AltArrowLeft} /> <Icon icon={AltArrowLeft} />
+2 -4
View File
@@ -4,13 +4,11 @@
type Props = { type Props = {
next: () => void next: () => void
step?: number
totalSteps?: number
} }
const {next, step, totalSteps}: Props = $props() const {next}: Props = $props()
const secret = getKey<string>("signup.secret")! const secret = getKey<string>("signup.secret")!
</script> </script>
<KeyDownload {secret} {next} {step} {totalSteps} /> <KeyDownload {secret} {next} />
+19 -21
View File
@@ -5,16 +5,15 @@
import {getKey, setKey} from "@lib/implicit" import {getKey, setKey} from "@lib/implicit"
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 Modal from "@lib/components/Modal.svelte"
import ModalBody from "@lib/components/ModalBody.svelte"
import ProfileEditForm from "@app/components/ProfileEditForm.svelte" import ProfileEditForm from "@app/components/ProfileEditForm.svelte"
import ProgressBar from "@app/components/ProgressBar.svelte"
type Props = { type Props = {
next: () => void next: () => void
step?: number
totalSteps?: number
} }
const {next, step, totalSteps}: Props = $props() const {next}: Props = $props()
const profile = getKey<Profile>("signup.profile")! const profile = getKey<Profile>("signup.profile")!
@@ -28,20 +27,19 @@
} }
</script> </script>
<ProfileEditForm isSignup {initialValues} {onsubmit}> <Modal>
{#snippet footer()} <ModalBody>
<Button class="btn btn-link" onclick={back}> <ProfileEditForm isSignup {initialValues} {onsubmit}>
<Icon icon={AltArrowLeft} /> {#snippet footer()}
Go back <Button class="btn btn-link" onclick={back}>
</Button> <Icon icon={AltArrowLeft} />
<Button class="btn btn-primary" type="submit"> Go back
Create Account </Button>
<Icon icon={AltArrowRight} /> <Button class="btn btn-primary" type="submit">
</Button> Create Account
{/snippet} <Icon icon={AltArrowRight} />
{#snippet progressBar()} </Button>
{#if step && totalSteps} {/snippet}
<ProgressBar current={step} total={totalSteps} /> </ProfileEditForm>
{/if} </ModalBody>
{/snippet} </Modal>
</ProfileEditForm>
+7 -16
View File
@@ -2,10 +2,9 @@
import {tick} from "svelte" import {tick} from "svelte"
import {debounce} from "throttle-debounce" import {debounce} from "throttle-debounce"
import {request} from "@welshman/net" import {request} from "@welshman/net"
import {repository, tracker} from "@welshman/app" import {formatTimestampAsDate, groupBy, now, MINUTE, HOUR, DAY, WEEK} from "@welshman/lib"
import {formatTimestampAsDate, groupBy, uniqBy, now, MINUTE, HOUR, DAY, WEEK} from "@welshman/lib"
import type {TrustedEvent, Filter} from "@welshman/util" import type {TrustedEvent, Filter} from "@welshman/util"
import {MESSAGE, sortEventsDesc} from "@welshman/util" import {sortEventsDesc} from "@welshman/util"
import CloseCircle from "@assets/icons/close-circle.svg?dataurl" import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
import Magnifier from "@assets/icons/magnifier.svg?dataurl" import Magnifier from "@assets/icons/magnifier.svg?dataurl"
import {fly} from "@lib/transition" import {fly} from "@lib/transition"
@@ -54,11 +53,8 @@
const getFilter = (searchTerm: string): Filter => const getFilter = (searchTerm: string): Filter =>
h h
? {kinds: [MESSAGE, ...CONTENT_KINDS], "#h": [h], search: searchTerm} ? {kinds: CONTENT_KINDS, "#h": [h], search: searchTerm}
: {kinds: [MESSAGE, ...CONTENT_KINDS], search: searchTerm} : {kinds: CONTENT_KINDS, search: searchTerm}
const getLocalResults = (filter: Filter) =>
repository.query([filter]).filter(event => tracker.getRelays(event.id).has(url))
const search = debounce(300, async (searchTerm: string) => { const search = debounce(300, async (searchTerm: string) => {
controller?.abort() controller?.abort()
@@ -72,23 +68,18 @@
controller = new AbortController() controller = new AbortController()
loading = true loading = true
const filter = getFilter(searchTerm.trim())
const localResults = getLocalResults(filter)
results = sortEventsDesc(localResults)
try { try {
const events = await request({ const events = await request({
relays: getRelayUrls(), relays: getRelayUrls(),
autoClose: true, autoClose: true,
signal: controller.signal, signal: controller.signal,
filters: [filter], filters: [getFilter(searchTerm.trim())],
}) })
results = sortEventsDesc(uniqBy((e: TrustedEvent) => e.id, [...events, ...localResults])) results = sortEventsDesc(events)
} catch (error) { } catch (error) {
if (!(error instanceof DOMException && error.name === "AbortError")) { if (!(error instanceof DOMException && error.name === "AbortError")) {
results = sortEventsDesc(localResults) results = []
} }
} finally { } finally {
loading = false loading = false
+2 -12
View File
@@ -28,7 +28,7 @@ import {
import type {TrustedEvent, Filter, List} from "@welshman/util" import type {TrustedEvent, Filter, List} from "@welshman/util"
import {load, request, mergeRepositoryUpdates} from "@welshman/net" import {load, request, mergeRepositoryUpdates} from "@welshman/net"
import type {RepositoryUpdate} from "@welshman/net" import type {RepositoryUpdate} from "@welshman/net"
import {pubkey, repository, loadRelay, tracker} from "@welshman/app" import {repository, loadRelay, tracker} from "@welshman/app"
import {createScroller} from "@lib/html" import {createScroller} from "@lib/html"
import {daysBetween} from "@lib/util" import {daysBetween} from "@lib/util"
import {getEventsForUrl} from "@app/core/state" import {getEventsForUrl} from "@app/core/state"
@@ -41,7 +41,6 @@ export const makeFeed = ({
element, element,
onBackwardExhausted, onBackwardExhausted,
onForwardExhausted, onForwardExhausted,
allowOptimisticSelfEvents = false,
at = now(), at = now(),
}: { }: {
url: string url: string
@@ -49,7 +48,6 @@ export const makeFeed = ({
element: HTMLElement element: HTMLElement
onBackwardExhausted?: () => void onBackwardExhausted?: () => void
onForwardExhausted?: () => void onForwardExhausted?: () => void
allowOptimisticSelfEvents?: boolean
at?: number at?: number
}) => { }) => {
const controller = new AbortController() const controller = new AbortController()
@@ -115,15 +113,7 @@ export const makeFeed = ({
} }
const matching = added.filter( const matching = added.filter(
event => event => matchFilters(filters, event) && tracker.getRelays(event.id).has(url),
matchFilters(filters, event) &&
(tracker.getRelays(event.id).has(url) ||
// In Safari, relay confirmation can lag behind local repository updates.
// Only enable this for chat-like feeds that explicitly opt in.
(allowOptimisticSelfEvents &&
event.pubkey === pubkey.get() &&
tracker.getRelays(event.id).size === 0 &&
event.created_at >= now() - 60)),
) )
if (matching.length > 0) { if (matching.length > 0) {
+1 -2
View File
@@ -10,7 +10,6 @@
<script lang="ts"> <script lang="ts">
import "emoji-picker-element" import "emoji-picker-element"
import emojiDataUrl from "emoji-picker-element-data/en/emojibase/data.json?url"
import type {Emoji} from "emoji-picker-element/shared" import type {Emoji} from "emoji-picker-element/shared"
import {onMount} from "svelte" import {onMount} from "svelte"
@@ -27,4 +26,4 @@
}) })
</script> </script>
<emoji-picker bind:this={element} data-source={emojiDataUrl} class="m-auto"></emoji-picker> <emoji-picker bind:this={element} class="m-auto"></emoji-picker>
+11 -17
View File
@@ -9,22 +9,16 @@
const {...props}: Props = $props() const {...props}: Props = $props()
</script> </script>
<div class="flex flex-col gap-2 {props.class}"> <div class="grid grid-cols-1 gap-2 lg:gap-6 lg:grid-cols-3 {props.class}">
<div class="flex items-center justify-between w-full gap-2"> <label class="flex items-center gap-2 font-bold">
{#if props.label} {@render props.label?.()}
<label class="flex items-center gap-2 min-w-[30%] max-w-[80%] md:max-w-none"> </label>
{@render props.label()} <div class="col-span-2 flex items-center gap-2">
</label> {@render props.input?.()}
{/if}
<div class="flex items-center gap-2 justify-end grow">
{#if props.input}
{@render props.input()}
{/if}
</div>
</div> </div>
{#if props.info} <p class="flex-end text-sm opacity-50 lg:col-span-3">
<p class="text-sm opacity-50"> {#if props.info}
{@render props.info()} {@render props.info?.()}
</p> {/if}
{/if} </p>
</div> </div>
+9 -16
View File
@@ -13,12 +13,7 @@
placeholder?: string placeholder?: string
} }
let { let {value = $bindable(), addLabel, placeholder = "Enter text..."}: Props = $props()
value = $bindable(),
addLabel,
placeholder = "Enter text...",
allowAdd = true,
}: Props & {allowAdd?: boolean} = $props()
let draggedIndex: number | null = $state(null) let draggedIndex: number | null = $state(null)
const onChange = (newValue: string[]) => { const onChange = (newValue: string[]) => {
@@ -77,14 +72,12 @@
</div> </div>
</div> </div>
{/each} {/each}
{#if allowAdd} <Button onclick={addItem} class="btn btn-link w-fit px-0">
<Button onclick={addItem} class="btn btn-link w-fit px-0"> <Icon icon={AddCircle} size={5} />
<Icon icon={AddCircle} size={5} /> {#if addLabel}
{#if addLabel} {@render addLabel?.()}
{@render addLabel?.()} {:else}
{:else} Add Item
Add Item {/if}
{/if} </Button>
</Button>
{/if}
</div> </div>
+1 -1
View File
@@ -13,7 +13,7 @@
const className = $derived( const className = $derived(
cx( cx(
props.class, props.class,
"scroll-container z-feature flex min-h-0 w-full min-w-0 flex-col overflow-y-auto overflow-x-hidden", "scroll-container z-feature flex min-h-0 w-full min-w-0 flex-1 flex-col overflow-y-auto overflow-x-hidden",
), ),
) )
</script> </script>
+24 -49
View File
@@ -5,7 +5,6 @@
import {Badge} from "@capawesome/capacitor-badge" import {Badge} from "@capawesome/capacitor-badge"
import Bell from "@assets/icons/bell.svg?dataurl" import Bell from "@assets/icons/bell.svg?dataurl"
import {preventDefault} from "@lib/html" import {preventDefault} from "@lib/html"
import FieldInline from "@lib/components/FieldInline.svelte"
import Spinner from "@lib/components/Spinner.svelte" import Spinner from "@lib/components/Spinner.svelte"
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"
@@ -64,64 +63,40 @@
<!-- pass --> <!-- pass -->
{:then { isSupported }} {:then { isSupported }}
{#if isSupported} {#if isSupported}
<FieldInline> <div class="flex justify-between">
{#snippet label()} <p>Show badge for unread alerts</p>
<p>Show badge for unread alerts</p> <input type="checkbox" class="toggle toggle-primary" bind:checked={settings.badge} />
{/snippet} </div>
{#snippet input()}
<input type="checkbox" class="toggle toggle-primary" bind:checked={settings.badge} />
{/snippet}
</FieldInline>
{/if} {/if}
{/await} {/await}
{#if !Capacitor.isNativePlatform()} {#if !Capacitor.isNativePlatform()}
<FieldInline> <div class="flex justify-between">
{#snippet label()} <p>Play sound for new activity</p>
<p>Play sound for new activity</p> <input type="checkbox" class="toggle toggle-primary" bind:checked={settings.sound} />
{/snippet} </div>
{#snippet input()}
<input type="checkbox" class="toggle toggle-primary" bind:checked={settings.sound} />
{/snippet}
</FieldInline>
{/if} {/if}
<FieldInline> <div class="flex justify-between">
{#snippet label()} <p>Enable push notifications</p>
<p>Enable push notifications</p> <input type="checkbox" class="toggle toggle-primary" bind:checked={settings.push} />
{/snippet} </div>
{#snippet input()}
<input type="checkbox" class="toggle toggle-primary" bind:checked={settings.push} />
{/snippet}
</FieldInline>
</div> </div>
<div <div
class={cx("card2 bg-alt col-4 shadow-md", { class={cx("card2 bg-alt col-4 shadow-md", {
"pointer-events-none opacity-50": !settings.badge && !settings.sound && !settings.push, "pointer-events-none opacity-50": !settings.badge && !settings.sound && !settings.push,
})}> })}>
<strong class="text-lg">Alert Types</strong> <strong class="text-lg">Alert Types</strong>
<FieldInline> <div class="flex justify-between">
{#snippet label()} <p>Notify me about new activity</p>
<p>Notify me about new activity</p> <input type="checkbox" class="toggle toggle-primary" bind:checked={settings.spaces} />
{/snippet} </div>
{#snippet input()} <div class="flex justify-between">
<input type="checkbox" class="toggle toggle-primary" bind:checked={settings.spaces} /> <p>Always notify me when mentioned</p>
{/snippet} <input type="checkbox" class="toggle toggle-primary" checked={settings.mentions} />
</FieldInline> </div>
<FieldInline> <div class="flex justify-between">
{#snippet label()} <p>Notify me about new messages</p>
<p>Always notify me when mentioned</p> <input type="checkbox" class="toggle toggle-primary" bind:checked={settings.messages} />
{/snippet} </div>
{#snippet input()}
<input type="checkbox" class="toggle toggle-primary" checked={settings.mentions} />
{/snippet}
</FieldInline>
<FieldInline>
{#snippet label()}
<p>Notify me about new messages</p>
{/snippet}
{#snippet input()}
<input type="checkbox" class="toggle toggle-primary" bind:checked={settings.messages} />
{/snippet}
</FieldInline>
</div> </div>
<div <div
class="card2 bg-alt sticky -bottom-3 shadow-md flex flex-row items-center justify-between gap-4"> class="card2 bg-alt sticky -bottom-3 shadow-md flex flex-row items-center justify-between gap-4">
+9 -16
View File
@@ -11,7 +11,6 @@
import {Router} from "@welshman/router" import {Router} from "@welshman/router"
import {userMuteList, tagPubkey, publishThunk, userBlossomServerList} from "@welshman/app" import {userMuteList, tagPubkey, publishThunk, userBlossomServerList} from "@welshman/app"
import NotesMinimalistic from "@assets/icons/notes-minimalistic.svg?dataurl" import NotesMinimalistic from "@assets/icons/notes-minimalistic.svg?dataurl"
import AddCircle from "@assets/icons/add-circle.svg?dataurl"
import {preventDefault} from "@lib/html" import {preventDefault} from "@lib/html"
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"
@@ -29,10 +28,6 @@
blossomServers = getTagValues("server", getListTags($userBlossomServerList)) blossomServers = getTagValues("server", getListTags($userBlossomServerList))
} }
const addServer = () => {
blossomServers = [...blossomServers, ""]
}
const onsubmit = preventDefault(async () => { const onsubmit = preventDefault(async () => {
await publishSettings($state.snapshot(settings)) await publishSettings($state.snapshot(settings))
@@ -109,7 +104,7 @@
{/snippet} {/snippet}
{#snippet input()} {#snippet input()}
<input <input
class="range range-primary w-full" class="range range-primary"
type="range" type="range"
min="0.8" min="0.8"
max="1.3" max="1.3"
@@ -120,13 +115,13 @@
</div> </div>
<div class="card2 bg-alt col-4 shadow-md"> <div class="card2 bg-alt col-4 shadow-md">
<strong class="text-lg">Editor Settings</strong> <strong class="text-lg">Editor Settings</strong>
<Field> <FieldInline>
{#snippet label()} {#snippet label()}
<p>Send Delay</p> <p>Send Delay</p>
{/snippet} {/snippet}
{#snippet input()} {#snippet input()}
<input <input
class="range range-primary w-full" class="range range-primary"
type="range" type="range"
min="0" min="0"
max="10000" max="10000"
@@ -139,19 +134,17 @@
{settings.send_delay === 1000 ? "second" : "seconds"}. {settings.send_delay === 1000 ? "second" : "seconds"}.
</p> </p>
{/snippet} {/snippet}
</Field> </FieldInline>
<Field> <Field>
{#snippet label()} {#snippet label()}
<p>Media Server</p> <p>Media Server</p>
{/snippet} {/snippet}
{#snippet secondary()}
<Button class="link text-sm underline flex items-center gap-1" onclick={addServer}>
<Icon icon={AddCircle} size={4} />
Add Server
</Button>
{/snippet}
{#snippet input()} {#snippet input()}
<InputList allowAdd={false} bind:value={blossomServers} /> <InputList bind:value={blossomServers}>
{#snippet addLabel()}
Add Server
{/snippet}
</InputList>
{/snippet} {/snippet}
{#snippet info()} {#snippet info()}
<p>Choose a media server type and url for files you upload to {PLATFORM_NAME}.</p> <p>Choose a media server type and url for files you upload to {PLATFORM_NAME}.</p>
+27 -45
View File
@@ -1,7 +1,6 @@
<script lang="ts"> <script lang="ts">
import ShieldMinimalistic from "@assets/icons/shield-minimalistic.svg?dataurl" import ShieldMinimalistic from "@assets/icons/shield-minimalistic.svg?dataurl"
import {preventDefault} from "@lib/html" import {preventDefault} from "@lib/html"
import FieldInline from "@lib/components/FieldInline.svelte"
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 {pushToast} from "@app/util/toast" import {pushToast} from "@app/util/toast"
@@ -12,10 +11,8 @@
settings = {...$userSettingsValues} settings = {...$userSettingsValues}
} }
const onAuthModeChange = (e: Event) => { const onAuthModeChange = (e: any) => {
const target = e.currentTarget as HTMLInputElement settings.auth_mode = e.target.checked ? RelayAuthMode.Aggressive : RelayAuthMode.Conservative
settings.relay_auth = target.checked ? RelayAuthMode.Aggressive : RelayAuthMode.Conservative
} }
const onsubmit = preventDefault(async () => { const onsubmit = preventDefault(async () => {
@@ -33,46 +30,31 @@
<Icon icon={ShieldMinimalistic} /> <Icon icon={ShieldMinimalistic} />
Privacy Settings Privacy Settings
</strong> </strong>
<FieldInline> <div class="grid grid-cols-2 gap-2">
{#snippet label()} <p>Authenticate with unknown relays?</p>
<p>Authenticate with unknown relays?</p> <input
{/snippet} type="checkbox"
{#snippet input()} class="toggle toggle-primary"
<input onchange={onAuthModeChange}
type="checkbox" checked={settings.auth_mode === RelayAuthMode.Aggressive} />
class="toggle toggle-primary" <p class="col-span-2 text-sm opacity-70">
onchange={onAuthModeChange} Controls whether {PLATFORM_NAME} will identify you to relays not in your lists.
checked={settings.relay_auth === RelayAuthMode.Aggressive} /> </p>
{/snippet} </div>
{#snippet info()} <div class="grid grid-cols-2 gap-2">
<p>Controls whether {PLATFORM_NAME} will identify you to relays not in your lists.</p> <p>Report errors?</p>
{/snippet} <input type="checkbox" class="toggle toggle-primary" bind:checked={settings.report_errors} />
</FieldInline> <p class="col-span-2 text-sm opacity-70">
<FieldInline> Allow {PLATFORM_NAME} to send error reports to help improve the app.
{#snippet label()} </p>
<p>Report errors?</p> </div>
{/snippet} <div class="grid grid-cols-2 gap-2">
{#snippet input()} <p>Report usage?</p>
<input <input type="checkbox" class="toggle toggle-primary" bind:checked={settings.report_usage} />
type="checkbox" <p class="col-span-2 text-sm opacity-70">
class="toggle toggle-primary" Allow {PLATFORM_NAME} to collect anonymous usage data.
bind:checked={settings.report_errors} /> </p>
{/snippet} </div>
{#snippet info()}
<p>Allow {PLATFORM_NAME} to send error reports to help improve the app.</p>
{/snippet}
</FieldInline>
<FieldInline>
{#snippet label()}
<p>Report usage?</p>
{/snippet}
{#snippet input()}
<input type="checkbox" class="toggle toggle-primary" bind:checked={settings.report_usage} />
{/snippet}
{#snippet info()}
<p>Allow {PLATFORM_NAME} to collect anonymous usage data.</p>
{/snippet}
</FieldInline>
</div> </div>
<div <div
class="card2 bg-alt sticky -bottom-3 shadow-md flex flex-row items-center justify-between gap-4"> class="card2 bg-alt sticky -bottom-3 shadow-md flex flex-row items-center justify-between gap-4">
+5 -20
View File
@@ -15,7 +15,7 @@
import InfoCircle from "@assets/icons/info-circle.svg?dataurl" import InfoCircle from "@assets/icons/info-circle.svg?dataurl"
import Login2 from "@assets/icons/login-3.svg?dataurl" import Login2 from "@assets/icons/login-3.svg?dataurl"
import cx from "classnames" import cx from "classnames"
import {fade, fly} from "@lib/transition" import {slide, fade, fly} from "@lib/transition"
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 Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
@@ -246,7 +246,6 @@
if (!isProgrammaticScroll) { if (!isProgrammaticScroll) {
userHasScrolled = true userHasScrolled = true
isUserScrolling = true isUserScrolling = true
wasAtBottom = !element || Math.abs(element.scrollTop) < 100
clearIsUserScrolling() clearIsUserScrolling()
manageScrollPosition() manageScrollPosition()
} }
@@ -282,7 +281,6 @@
let events: Readable<TrustedEvent[]> = $state(readable([])) let events: Readable<TrustedEvent[]> = $state(readable([]))
let compose: RoomCompose | undefined = $state() let compose: RoomCompose | undefined = $state()
let eventToEdit: TrustedEvent | undefined = $state() let eventToEdit: TrustedEvent | undefined = $state()
let wasAtBottom = true
const clearIsUserScrolling = debounce(150, () => { const clearIsUserScrolling = debounce(150, () => {
isUserScrolling = false isUserScrolling = false
@@ -361,20 +359,8 @@
}) })
$effect(() => { $effect(() => {
if (elements.length > 0) { if (elements.length > 0 && !isUserScrolling) {
requestAnimationFrame(() => { requestAnimationFrame(manageScrollPosition)
// Safari does not implement CSS scroll anchoring for flex-col-reverse.
// When a new message is inserted, Safari shifts scrollTop upward to preserve
// visual position rather than keeping it pinned at 0 (visual bottom).
// Snap back to 0 whenever the user was at the bottom before the update.
if (element && isNaN(at) && wasAtBottom) {
isProgrammaticScroll = true
element.scrollTop = 0
}
if (!isUserScrolling) {
manageScrollPosition()
}
})
} }
}) })
@@ -385,7 +371,6 @@
url, url,
at: at || now(), at: at || now(),
element: element!, element: element!,
allowOptimisticSelfEvents: true,
filters: [{kinds: [MESSAGE, ROOM_ADD_MEMBER], "#h": [h]}], filters: [{kinds: [MESSAGE, ROOM_ADD_MEMBER], "#h": [h]}],
onBackwardExhausted: () => { onBackwardExhausted: () => {
loadingBackward = false loadingBackward = false
@@ -419,7 +404,7 @@
onMount(() => { onMount(() => {
start() start()
return () => cleanup?.() return cleanup
}) })
</script> </script>
@@ -511,7 +496,7 @@
{#if event.kind === ROOM_ADD_MEMBER} {#if event.kind === ROOM_ADD_MEMBER}
<RoomItemAddMember {url} {event} /> <RoomItemAddMember {url} {event} />
{:else} {:else}
<div> <div in:slide class="cv">
<RoomItem <RoomItem
{url} {url}
{event} {event}
+3 -18
View File
@@ -141,7 +141,6 @@
if (!isProgrammaticScroll) { if (!isProgrammaticScroll) {
userHasScrolled = true userHasScrolled = true
isUserScrolling = true isUserScrolling = true
wasAtBottom = !element || Math.abs(element.scrollTop) < 100
clearIsUserScrolling() clearIsUserScrolling()
manageScrollPosition() manageScrollPosition()
} }
@@ -175,7 +174,6 @@
let events: Readable<TrustedEvent[]> = $state(readable([])) let events: Readable<TrustedEvent[]> = $state(readable([]))
let compose: RoomCompose | undefined = $state() let compose: RoomCompose | undefined = $state()
let eventToEdit: TrustedEvent | undefined = $state() let eventToEdit: TrustedEvent | undefined = $state()
let wasAtBottom = true
const clearIsUserScrolling = debounce(150, () => { const clearIsUserScrolling = debounce(150, () => {
isUserScrolling = false isUserScrolling = false
@@ -254,20 +252,8 @@
}) })
$effect(() => { $effect(() => {
if (elements.length > 0) { if (elements.length > 0 && !isUserScrolling) {
requestAnimationFrame(() => { requestAnimationFrame(manageScrollPosition)
// Safari does not implement CSS scroll anchoring for flex-col-reverse.
// When a new message is inserted, Safari shifts scrollTop upward to preserve
// visual position rather than keeping it pinned at 0 (visual bottom).
// Snap back to 0 whenever the user was at the bottom before the update.
if (element && isNaN(at) && wasAtBottom) {
isProgrammaticScroll = true
element.scrollTop = 0
}
if (!isUserScrolling) {
manageScrollPosition()
}
})
} }
}) })
@@ -278,7 +264,6 @@
url, url,
at: at || now(), at: at || now(),
element: element!, element: element!,
allowOptimisticSelfEvents: true,
filters: [{kinds: [MESSAGE, RELAY_ADD_MEMBER]}], filters: [{kinds: [MESSAGE, RELAY_ADD_MEMBER]}],
onBackwardExhausted: () => { onBackwardExhausted: () => {
loadingBackward = false loadingBackward = false
@@ -312,7 +297,7 @@
onMount(() => { onMount(() => {
start() start()
return () => cleanup?.() return cleanup
}) })
</script> </script>
+63 -78
View File
@@ -1,7 +1,5 @@
<script lang="ts"> <script lang="ts">
import Server from "@assets/icons/server.svg?dataurl" import Server from "@assets/icons/server.svg?dataurl"
import CloudCheck from "@assets/icons/cloud-check.svg?dataurl"
import CheckCircle from "@assets/icons/check-circle.svg?dataurl"
import ArrowRight from "@assets/icons/arrow-right.svg?dataurl" import ArrowRight from "@assets/icons/arrow-right.svg?dataurl"
import Link from "@lib/components/Link.svelte" import Link from "@lib/components/Link.svelte"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
@@ -14,91 +12,78 @@
<PageContent class="flex flex-col items-center gap-2 p-2 pt-4"> <PageContent class="flex flex-col items-center gap-2 p-2 pt-4">
<PageHeader> <PageHeader>
{#snippet title()} {#snippet title()}
<div>Choose your Hosting Plan</div> <div>Create your own Space</div>
{/snippet} {/snippet}
{#snippet info()} {#snippet info()}
<p> <p>Get started with one of our trusted partners, or learn how to host your own space.</p>
Select how you want to deploy and manage your new Space. You can always migrate later.
</p>
{/snippet} {/snippet}
</PageHeader> </PageHeader>
<div class="flex w-full max-w-lg flex-col gap-4 lg:max-w-4xl"> <div class="grid w-full max-w-lg grid-cols-1 gap-2 lg:max-w-4xl lg:grid-cols-2">
<div class="grid grid-cols-1 gap-2 lg:grid-cols-2"> <div class="card2 bg-alt flex flex-col gap-4">
<div class="card2 bg-alt flex flex-col gap-5"> <div class="flex flex-col gap-4">
<div class="flex flex-col gap-3"> <div class="flex items-center justify-between">
<div class="bg-primary/20 flex h-10 w-10 items-center justify-center rounded-md"> <div class="flex items-center gap-3">
<Icon icon={CloudCheck} class="text-primary" /> <Icon icon={Server} />
<h3 class="text-lg font-bold">Self-Host your Space</h3>
</div> </div>
<div class="flex flex-col gap-1"> <div class="badge badge-neutral">Recommended</div>
<h3 class="text-lg font-bold">Community</h3>
<div class="text-xs font-semibold tracking-wider opacity-60">SELF-HOSTED</div>
</div>
<p class="text-sm opacity-70">
For technical users who want full control. Deploy on your own infrastructure and
manage your own updates and scaling.
</p>
</div> </div>
<ul class="flex flex-col gap-2 text-sm"> <ul class="flex list-inside list-disc flex-col gap-1 text-sm opacity-70">
<li class="flex items-center gap-2"> <li>Unlimited customization and control</li>
<Icon icon={CheckCircle} class="opacity-60" /> <li>Free and open source software</li>
Open source core <li>Full-featured admin dashboards available</li>
</li> <li>Requires some technical skills</li>
<li class="flex items-center gap-2">
<Icon icon={CheckCircle} class="opacity-60" />
Community support
</li>
<li class="flex items-center gap-2">
<Icon icon={CheckCircle} class="opacity-60" />
Bring your own infra
</li>
</ul> </ul>
<Link
external
class="btn btn-neutral mt-auto"
href="https://gitea.coracle.social/coracle/zooid">
Get started
<Icon icon={ArrowRight} />
</Link>
</div>
<div class="card2 bg-alt border-primary flex flex-col gap-5 border">
<div class="flex flex-col gap-3">
<div class="flex items-start justify-between">
<img alt="Coracle Logo" src="/coracle.png" class="h-10 w-10" />
<div class="badge badge-primary">Recommended</div>
</div>
<div class="flex flex-col gap-1">
<h3 class="text-lg font-bold">Coracle Hosting</h3>
<div class="text-xs font-semibold tracking-wider opacity-60">FULLY MANAGED</div>
</div>
<p class="text-sm opacity-70">
The premium experience. We handle the infrastructure, security updates, and scaling so
you can focus on your community.
</p>
</div>
<ul class="flex flex-col gap-2 text-sm">
<li class="flex items-center gap-2">
<Icon icon={CheckCircle} class="text-primary" />
One-click deployment
</li>
<li class="flex items-center gap-2">
<Icon icon={CheckCircle} class="text-primary" />
Automated backups & scaling
</li>
<li class="flex items-center gap-2">
<Icon icon={CheckCircle} class="text-primary" />
Priority support
</li>
</ul>
<Link external class="btn btn-primary mt-auto" href="https://hosting.coracle.social">
Start for free
<Icon icon={ArrowRight} />
</Link>
</div> </div>
<Link external class="btn btn-primary" href="https://github.com/coracle-social/zooid">
Get Started
<Icon icon={ArrowRight} />
</Link>
</div> </div>
<div class="flex flex-col items-center justify-center gap-2 py-2 text-sm opacity-70"> <div class="card2 bg-alt flex flex-col gap-4">
<span>Want to host on other servers?</span> <div class="flex flex-col gap-4">
<Link external class="link center gap-1" href="https://relay.tools/signup"> <div class="flex items-center justify-between">
Other hosting options <div class="flex items-center gap-2">
<img alt="Coracle Logo" src="/coracle.png" class="h-7 w-7" />
<h3 class="text-lg font-bold">Coracle Hosting</h3>
</div>
<div class="badge badge-neutral">Recommended</div>
</div>
<ul class="flex list-inside list-disc flex-col gap-1 text-sm opacity-70">
<li>Simple setup, support included</li>
<li>Free and open source software — no vendor lock-in</li>
<li>Advanced access controls and relay policies</li>
<li>Full-featured admin dashboard</li>
</ul>
</div>
<Link external class="btn btn-neutral" href="https://hosting.coracle.social">
Get Started
<Icon icon={ArrowRight} />
</Link>
</div>
<div class="card2 bg-alt flex flex-col gap-4">
<div class="flex flex-col gap-4">
<div class="self-start">
<img
alt="Relay Tools"
src="https://relay.tools/17.svg"
class="-my-20 -ml-2 hidden h-48 dark:block"
style="filter: contrast(50%)" />
<img
alt="Relay Tools"
src="https://relay.tools/19.svg"
class="-my-20 -ml-2 h-48 dark:hidden"
style="filter: contrast(50%)" />
</div>
<ul class="flex list-inside list-disc flex-col gap-1 text-sm opacity-70">
<li>Independently run</li>
<li>Customizable relay policies</li>
<li>Simple management dashboard</li>
<li>Support available</li>
</ul>
</div>
<Link external class="btn btn-neutral" href="https://relay.tools/signup">
Get Started
<Icon icon={ArrowRight} /> <Icon icon={ArrowRight} />
</Link> </Link>
</div> </div>