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
# 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
* Improve room and data loading
+2 -2
View File
@@ -7,8 +7,8 @@ android {
applicationId "social.flotilla"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 5
versionName "0.2.5"
versionCode 6
versionName "0.2.6"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// 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
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
curl $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/sveltekit": "^0.6.6",
"@welshman/app": "~0.0.41",
"@welshman/content": "~0.0.15",
"@welshman/content": "~0.0.16",
"@welshman/dvm": "~0.0.14",
"@welshman/editor": "~0.0.8",
"@welshman/editor": "~0.0.10",
"@welshman/feeds": "~0.0.30",
"@welshman/lib": "~0.0.38",
"@welshman/net": "~0.0.46",
@@ -4508,13 +4508,13 @@
}
},
"node_modules/@welshman/content": {
"version": "0.0.15",
"resolved": "https://registry.npmjs.org/@welshman/content/-/content-0.0.15.tgz",
"integrity": "sha512-y0f0iLIaHUqEJJ0ziRWbGw13mg0tOLTKpHQNgIXJ03PD3xGHBaQ5xPWiOI8XeUt35KgrayvQZHsaqfAsOWkwag==",
"version": "0.0.16",
"resolved": "https://registry.npmjs.org/@welshman/content/-/content-0.0.16.tgz",
"integrity": "sha512-042LG1rgyzDvwPzEISliY7wG0Hb8s+efhRR15DBtFBnbzZy5xRLihOiw/N6S2bMN+BoY6iBnilantwr0yfV5Cg==",
"license": "MIT",
"dependencies": {
"@braintree/sanitize-url": "^7.0.2",
"@welshman/lib": "~0.0.34",
"@welshman/lib": "~0.0.37",
"nostr-tools": "^2.7.2"
},
"engines": {
@@ -4535,9 +4535,9 @@
}
},
"node_modules/@welshman/editor": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/@welshman/editor/-/editor-0.0.8.tgz",
"integrity": "sha512-9zQy1MjbGhTARTPH3QEgyhEGNKy239VSD5Jo4llsiUUhHQVNQj1KpdSVQnTLOwUBbfSLeYPIfMRUm2Gq5Cn/Ew==",
"version": "0.0.10",
"resolved": "https://registry.npmjs.org/@welshman/editor/-/editor-0.0.10.tgz",
"integrity": "sha512-685+KUYrGHzSgz2V6hRRltyp3LMhdHKvYBqkc9DyDNZoz3qqn4pUsOF+SMGSBnisiXBEZrKj2d52Jv46FT81iQ==",
"peerDependencies": {
"@tiptap/core": "^2.9.1",
"@tiptap/extension-code": "^2.9.1",
@@ -4554,7 +4554,7 @@
"@tiptap/suggestion": "^2.9.1",
"@welshman/lib": "~0.0.36",
"@welshman/util": "~0.0.53",
"nostr-editor": "^0.0.4-pre.6",
"nostr-editor": "^0.0.4-pre.7",
"nostr-tools": "^2.8.1",
"svelte": "^4.0.0",
"svelte-tiptap": "^1.0.0"
@@ -9880,9 +9880,9 @@
}
},
"node_modules/nostr-editor": {
"version": "0.0.4-pre.6",
"resolved": "https://registry.npmjs.org/nostr-editor/-/nostr-editor-0.0.4-pre.6.tgz",
"integrity": "sha512-njOWThC8tfvyw59rcCeJjwTycZ0QdwBYURpMJAzPVoWFjxZmzmTeBNNznf00g3U7jwXde0VuI9OElzAaTdkz8w==",
"version": "0.0.4-pre.7",
"resolved": "https://registry.npmjs.org/nostr-editor/-/nostr-editor-0.0.4-pre.7.tgz",
"integrity": "sha512-Xg+562ywJLLUa2nZ+XoJDarqRjx7fdc2/oKNuuxoAitp95LFTXGyupDBigJs8Q9o9GVpGLsfCbXTeIGT/b9G8g==",
"license": "MIT",
"peer": true,
"dependencies": {
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "flotilla",
"version": "0.2.5",
"version": "0.2.6",
"private": true,
"scripts": {
"dev": "vite dev",
@@ -53,9 +53,9 @@
"@vite-pwa/assets-generator": "^0.2.6",
"@vite-pwa/sveltekit": "^0.6.6",
"@welshman/app": "~0.0.41",
"@welshman/content": "~0.0.15",
"@welshman/content": "~0.0.16",
"@welshman/dvm": "~0.0.14",
"@welshman/editor": "~0.0.8",
"@welshman/editor": "~0.0.10",
"@welshman/feeds": "~0.0.30",
"@welshman/lib": "~0.0.38",
"@welshman/net": "~0.0.46",
+34 -42
View File
@@ -185,45 +185,53 @@
@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;
}
.chat-editor .tiptap[contenteditable="true"] {
@apply rounded-box bg-base-300;
.tiptap p.is-editor-empty:first-child::before {
opacity: 40%;
}
.input-editor .tiptap[contenteditable="true"] {
@apply input input-bordered h-auto p-[.65rem];
.chat-editor .tiptap {
@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;
}
.tiptap pre code {
@apply link-content block w-full;
.input-editor .tiptap {
--tiptap-object-bg: var(--base-200);
@apply input input-bordered h-auto p-[.65rem];
}
.tiptap p code {
@apply link-content;
}
/* link-content, based on tiptap */
.link-content,
.tiptap [tag] {
@apply max-w-full overflow-hidden text-ellipsis whitespace-nowrap rounded bg-neutral px-1 text-neutral-content no-underline;
}
.link-content.link-content-selected {
@apply bg-primary text-primary-content;
}
.tiptap p.is-editor-empty:first-child::before {
content: attr(data-placeholder);
float: left;
height: 0;
pointer-events: none;
opacity: 50%;
.link-content {
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
border-radius: 3px;
padding: 0 0.25rem;
background-color: var(--base-100);
color: var(--base-content);
}
/* date input */
@@ -248,19 +256,3 @@ emoji-picker {
--input-font-color: var(--base-content);
--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>
<p class="text-primary">Replying to @{displayProfileByPubkey(event.pubkey)}</p>
{#key event.id}
<Content {event} minLength={100} maxLength={300} expandMode="disabled" />
<Content {event} hideMedia minLength={100} maxLength={300} expandMode="disabled" />
{/key}
<Button class="absolute right-2 top-2 cursor-pointer" on:click={clear}>
<Icon icon="close-circle" />
+1 -1
View File
@@ -40,7 +40,7 @@
const reply = () => replyTo(event)
const onLongPress = () => pushModal(ChannelMessageMenuMobile, {url, event})
const onLongPress = () => pushModal(ChannelMessageMenuMobile, {url, event, reply})
const openProfile = () => pushModal(ProfileDetail, {pubkey: event.pubkey})
@@ -11,6 +11,7 @@
export let url
export let event
export let reply
const onEmoji = (emoji: NativeEmoji) => {
history.back()
@@ -19,6 +20,11 @@
const showEmojiPicker = () => pushModal(EmojiPicker, {onClick: onEmoji}, {replaceState: true})
const sendReply = () => {
history.back()
reply()
}
const showInfo = () => pushModal(EventInfo, {event}, {replaceState: true})
const showDelete = () => pushModal(ConfirmDelete, {url, event})
@@ -29,6 +35,10 @@
<Icon size={4} icon="smile-circle" />
Send Reaction
</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}>
<Icon size={4} icon="code-2" />
Message Details
+1 -1
View File
@@ -30,7 +30,7 @@
</video>
{:else if url.match(/\.(jpe?g|png|gif|webp)$/)}
<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>
{:else}
{#await loadPreview()}
+23 -97
View File
@@ -1,107 +1,50 @@
<script lang="ts">
import {postJson, assoc} from "@welshman/lib"
import {makeSecret, Nip46Broker} from "@welshman/signer"
import {pubkey, loadHandle, updateSession} from "@welshman/app"
import {postJson} from "@welshman/lib"
import {isMobile} from "@lib/html"
import Icon from "@lib/components/Icon.svelte"
import Field from "@lib/components/Field.svelte"
import FieldInline from "@lib/components/FieldInline.svelte"
import Link from "@lib/components/Link.svelte"
import Button from "@lib/components/Button.svelte"
import Divider from "@lib/components/Divider.svelte"
import Spinner from "@lib/components/Spinner.svelte"
import LogIn from "@app/components/LogIn.svelte"
import InfoNostr from "@app/components/InfoNostr.svelte"
import SignUpSuccess from "@app/components/SignUpSuccess.svelte"
import {pushModal, clearModals} from "@app/modal"
import {setChecked} from "@app/notifications"
import {BURROW_URL, PLATFORM_NAME, NIP46_PERMS} from "@app/state"
import {pushModal} from "@app/modal"
import {BURROW_URL, PLATFORM_NAME} from "@app/state"
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 withLoading =
(cb: (...args: any[]) => any) =>
async (...args: any[]) => {
loading = true
const signupPassword = async () => {
loading = true
try {
await cb(...args)
} finally {
loading = false
try {
const res = await postJson(BURROW_URL + "/user", {email, password})
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 = () => {
if (BURROW_URL) {
signupPassword()
} else {
signupNsecApp()
}
}
let email = ""
let password = ""
let username = ""
let loading = false
</script>
@@ -136,29 +79,12 @@
on other nostr applications, you can create a nostr key yourself, or export your key from {PLATFORM_NAME}
later.
</p>
{:else}
<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>
<Divider>Or</Divider>
{/if}
<Divider>Or</Divider>
<Link
external
href="https://nosta.me"
class="btn {username || email || password ? 'btn-neutral' : 'btn-primary'}">
<a href={nstart} class="btn {email || password ? 'btn-neutral' : 'btn-primary'}">
<Icon icon="square-share-line" />
Get started on Nosta.me
</Link>
Get started on njump
</a>
<div class="text-sm">
Already have an account?
<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"
export let value: string
export let options: string[]
export let options: string[] = []
export let allowCreate = false
let input: Element
@@ -20,7 +20,7 @@
createSearch(options, {
getValue: identity,
fuseOptions: {keys: [""]},
}),
}).searchValues,
)
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") => {
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)
}
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">
import "@src/app.css"
import {onMount} from "svelte"
import {nip19} from "nostr-tools"
import * as nip19 from "nostr-tools/nip19"
import {get, derived} from "svelte/store"
import {App} from "@capacitor/app"
import {dev} from "$app/environment"
import {goto} from "$app/navigation"
import {bytesToHex, hexToBytes} from "@noble/hashes/utils"
import {identity, sleep, take, sortBy, ago, now, HOUR, WEEK, MONTH, Worker} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util"
@@ -21,6 +22,7 @@
getPubkeyTagValues,
getListTags,
} from "@welshman/util"
import {Nip46Broker, getPubkey, makeSecret} from "@welshman/signer"
import {
relays,
handles,
@@ -39,6 +41,7 @@
getRelayUrls,
subscribe,
userInboxRelaySelections,
addSession,
} from "@welshman/app"
import * as lib from "@welshman/lib"
import * as util from "@welshman/util"
@@ -49,9 +52,10 @@
import ModalContainer from "@app/components/ModalContainer.svelte"
import {setupTracking} from "@app/tracking"
import {setupAnalytics} from "@app/analytics"
import {nsecDecode} from "@lib/util"
import {theme} from "@app/theme"
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 * as commands from "@app/commands"
import * as requests from "@app/requests"
@@ -82,6 +86,34 @@
...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) {
setupTracking()
setupAnalytics()
+19
View File
@@ -4,6 +4,7 @@
import {pubkey, signer, userMutes, tagPubkey, publishThunk} from "@welshman/app"
import Field from "@lib/components/Field.svelte"
import FieldInline from "@lib/components/FieldInline.svelte"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import ProfileMultiSelect from "@app/components/ProfileMultiSelect.svelte"
import {pushToast} from "@app/toast"
@@ -79,6 +80,24 @@
{settings.send_delay === 1000 ? "second" : "seconds"}.
</p>
</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">
<Button class="btn btn-neutral" on:click={reset}>Discard 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 {pushModal} from "@app/modal"
const hash = import.meta.env.VITE_BUILD_HASH
const pubkey = "97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322"
const openProfile = () => pushModal(ProfileDetail, {pubkey})
@@ -48,6 +50,9 @@
class="link"
href="https://www.figma.com/community/file/1166831539721848736">480 Design</Link>
</p>
{#if hash}
<p class="text-xs">Running build {hash.slice(0, 8)}</p>
{/if}
</div>
<div class="flex justify-center gap-4">
<div class="tooltip" data-tip="Source Code">
+59 -7
View File
@@ -5,8 +5,8 @@
import {now} from "@welshman/lib"
import type {TrustedEvent, EventContent} from "@welshman/util"
import {createEvent, MESSAGE, DELETE, REACTION} from "@welshman/util"
import {formatTimestampAsDate, publishThunk, deriveRelay, repository} from "@welshman/app"
import {slide, fade} from "@lib/transition"
import {formatTimestampAsDate, pubkey, publishThunk, deriveRelay, repository} from "@welshman/app"
import {slide, fade, fly} from "@lib/transition"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import Spinner from "@lib/components/Spinner.svelte"
@@ -26,7 +26,7 @@
displayChannel,
getEventsForUrl,
} from "@app/state"
import {setChecked} from "@app/notifications"
import {setChecked, checked} from "@app/notifications"
import {
nip29,
addRoomMembership,
@@ -40,6 +40,7 @@
import {pushToast} from "@app/toast"
const {room = GENERAL} = $page.params
const lastChecked = $checked[$page.url.pathname]
const content = popKey<string>("content") || ""
const url = decodeRelay($page.params.relay)
const filter = {kinds: [MESSAGE], "#h": [room]}
@@ -89,11 +90,33 @@
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"})
let parent: TrustedEvent | undefined
let loading = true
let element: HTMLElement
let newMessages: HTMLElement
let newMessagesSeen = false
let showFixedNewMessages = false
let showScrollButton = false
let cleanup: () => void
let events: Readable<TrustedEvent[]>
@@ -107,6 +130,7 @@
let previousDate
let previousPubkey
let newMessagesSeen = false
if (events) {
for (const event of $events.toReversed()) {
@@ -118,6 +142,16 @@
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) {
elements.push({type: "date", value: date, id: date, showPubkey: false})
}
@@ -136,13 +170,12 @@
}
elements.reverse()
setTimeout(onScroll, 100)
}
$: {
if (element) {
element.addEventListener("scroll", () => {
showScrollButton = Math.abs(element.scrollTop) > 1500
})
;({events, cleanup} = makeFeed({
element,
relays: [url],
@@ -191,9 +224,19 @@
</PageBar>
<div
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}>
{#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>
{:else}
<div in:slide class:-mt-1={!showPubkey}>
@@ -209,6 +252,15 @@
{/if}
</p>
</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>
{#if parent}
<ChannelComposeParent event={parent} clear={clearParent} />