Compare commits

..

14 Commits

Author SHA1 Message Date
nayan9617 3dd5a0915f Stop tracking .next artifacts 2026-04-08 16:42:54 +00:00
nayan9617 55497b7949 Keep follow sync trickling 2026-04-08 16:42:54 +00:00
nayan9617 2144bffc41 Align sync as per the review 2026-04-08 16:42:54 +00:00
nayan9617 35c27090c1 Guard fallback pulls after abort 2026-04-08 16:42:54 +00:00
nayan9617 e710fc2561 refactor: revert socket reset workaround for adapter layer investigation 2026-04-08 16:42:54 +00:00
nayan9617 a5506689f7 fix: reset relay sockets on visibility change to prevent auth hangs on sleep/wake
When browser tab is hidden (sleep), tear down all relay socket connections
completely. This forces fresh socket creation and clean auth handshake on wake,
preventing stuck AuthStatus.PendingResponse states.

- Add documentVisibility store tracking document.visibilityState
- Call Pool.get().remove(url) for relay sockets during hidden teardown
- Integrate socket reset in syncUserData, syncSpaces, syncDMs
- Add explanatory comments for visibility guards and race conditions

Fixes: App stuck in "Authenticating" after Mac sleep/wake cycle
Tested with: wss://news.utxo.one/
2026-04-08 16:42:54 +00:00
priyanshu_bharti 4c19ee823b 73-video-thumbnails (#142)
This PR implements video thumbnails for `.mov`, `.webm`, and `.mp4` files in the `ContentLinkBlock` component.

Changes:
- Added the `poster` attribute to the `<video>` tag.
- Set the poster source to `{url}#t=1` to capture a clear preview frame at the 1-second mark.
- Verified locally that thumbnails are now correctly displayed instead of a black/empty box.

Closes #73

Reviewed-on: #142
Co-authored-by: Priyanshubhartistm <bhartipriyanshustm@gmail.com>
Co-committed-by: Priyanshubhartistm <bhartipriyanshustm@gmail.com>
2026-04-08 16:07:11 +00:00
Jon Staab 8e2dd8b278 Upgrade daisyui/tailwind 2026-04-07 15:31:35 -07:00
Jon Staab 8d35b3aad2 Chat tweaks 2026-04-07 10:40:45 -07:00
Prat_09 613cad31c0 add start chat FAB (#152)
Co-authored-by: Pratyush Mohanty <prat_09@noreply.coracle.social>
Co-committed-by: Pratyush Mohanty <prat_09@noreply.coracle.social>
2026-04-07 17:02:40 +00:00
Jon Staab 3779a90f26 Tweak to chat item menu buttons 2026-04-07 09:51:25 -07:00
theAnuragMishra 7470f28f31 fix spacing around messages (#159)
Co-authored-by: theAnuragMishra <theanuragmishra@noreply.coracle.social>
Co-committed-by: theAnuragMishra <theanuragmishra@noreply.coracle.social>
2026-04-07 16:50:53 +00:00
hodlbod 17fb4e780b Clean up drafts implementation (#164) 2026-04-07 13:06:29 +00:00
userAdityaa 30c2a6ef79 persist drafts in memory (#155)
Co-authored-by: userAdityaa <aditya.chaudhary1558@gmail.com>
Co-committed-by: userAdityaa <aditya.chaudhary1558@gmail.com>
2026-04-07 12:06:29 +00:00
80 changed files with 1196 additions and 873 deletions
+1
View File
@@ -19,5 +19,6 @@ VITE_DEFAULT_SEARCH_RELAYS=relay.ditto.pub,antiprimal.net,relay.vertexlab.io
VITE_DEFAULT_MESSAGING_RELAYS=auth.nostr1.com,relay.keychat.io,relay.ditto.pub
VITE_SIGNER_RELAYS=relay.nsec.app,ephemeral.snowflare.cc,bucket.coracle.social
VITE_VAPID_PUBLIC_KEY=BIt2D4BdgdbCowD_0d3Np6GbrIGHxd7aIEUeZNe3hQuRlHz02OhzvDaai0XSFoJYVzSzdMjdyW-QhvW9_yq8j4Y
VITE_THUMBNAIL_URL=
VITE_GLITCHTIP_API_KEY=
GLITCHTIP_AUTH_TOKEN=
+1
View File
@@ -74,3 +74,4 @@ GoogleService-Info.plist
# OS generated
.DS_Store
Thumbs.db
package-lock.json
+4 -3
View File
@@ -22,6 +22,7 @@
"@eslint/js": "^9.39.2",
"@sveltejs/kit": "^2.50.1",
"@sveltejs/vite-plugin-svelte": "^4.0.4",
"@tailwindcss/postcss": "^4.2.2",
"@tauri-apps/cli": "^2.9.6",
"@types/eslint": "^9.6.1",
"autoprefixer": "^10.4.23",
@@ -35,7 +36,7 @@
"prettier-plugin-svelte": "^3.4.1",
"svelte": "^5.48.0",
"svelte-check": "^4.3.5",
"tailwindcss": "^3.4.19",
"tailwindcss": "^4.2.2",
"typescript": "^5.9.3",
"typescript-eslint": "^8.53.1",
"vite": "^5.4.21"
@@ -77,7 +78,7 @@
"@welshman/store": "^0.8.12",
"@welshman/util": "^0.8.12",
"compressorjs-next": "^1.1.2",
"daisyui": "^4.12.24",
"daisyui": "^5.5.19",
"date-picker-svelte": "^2.17.0",
"dotenv": "^16.6.1",
"emoji-picker-element": "^1.28.1",
@@ -87,7 +88,7 @@
"livekit-client": "^2.17.2",
"nostr-signer-capacitor-plugin": "github:coracle-social/nostr-signer-capacitor-plugin#main",
"nostr-tools": "^2.19.4",
"prettier-plugin-tailwindcss": "^0.6.14",
"prettier-plugin-tailwindcss": "^0.7.2",
"qr-scanner": "^1.4.2",
"qrcode": "^1.5.4",
"throttle-debounce": "^5.0.2",
+388 -369
View File
File diff suppressed because it is too large Load Diff
+1 -2
View File
@@ -1,6 +1,5 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
"@tailwindcss/postcss": {},
},
}
+246 -248
View File
@@ -1,149 +1,245 @@
@import "@welshman/editor/index.css";
@import 'tailwindcss';
@tailwind base;
@tailwind components;
@tailwind utilities;
@config "../tailwind.config.js";
/* Fonts */
@font-face {
font-family: "Satoshis";
font-style: normal;
font-weight: 400;
src:
local(""),
url("/fonts/Satoshi Symbol.ttf") format("truetype");
@utility pt-sai {
padding-top: var(--sait);
}
@font-face {
font-family: "Lato";
font-style: normal;
font-weight: 400;
src:
local(""),
url("/fonts/Lato-Regular.ttf") format("truetype");
@utility pr-sai {
padding-right: var(--sair);
}
@font-face {
font-family: "Lato";
font-style: bold;
font-weight: 600;
src:
local(""),
url("/fonts/Lato-Bold.ttf") format("truetype");
@utility pb-sai {
padding-bottom: var(--saib);
}
@font-face {
font-family: "Lato";
font-style: italic;
font-weight: 400;
src:
local(""),
url("/fonts/Italic.ttf") format("truetype");
@utility pl-sai {
padding-left: var(--sail);
}
/* root */
:root {
font-family: Lato;
--sait: var(--safe-area-inset-top, env(safe-area-inset-top));
--saib: var(--safe-area-inset-bottom, env(safe-area-inset-bottom));
--sail: var(--safe-area-inset-left, env(safe-area-inset-left));
--sair: var(--safe-area-inset-right, env(safe-area-inset-right));
@utility px-sai {
@apply pl-sai pr-sai;
}
[data-theme] {
@apply bg-base-300;
--base-100: oklch(var(--b1));
--base-200: oklch(var(--b2));
--base-300: oklch(var(--b3));
--base-content: oklch(var(--bc));
--primary: oklch(var(--p));
--primary-content: oklch(var(--pc));
--secondary: oklch(var(--s));
--secondary-content: oklch(var(--sc));
--neutral: oklch(var(--n));
--neutral-content: oklch(var(--nc));
@utility py-sai {
@apply pt-sai pb-sai;
}
.mobile [data-tip]::before {
display: none !important;
@utility p-sai {
@apply py-sai px-sai;
}
/* safe area insets */
@utility mt-sai {
margin-top: var(--sait);
}
@layer components {
.pt-sai {
padding-top: var(--sait);
@utility mr-sai {
margin-right: var(--sair);
}
@utility mb-sai {
margin-bottom: var(--saib);
}
@utility ml-sai {
margin-left: var(--sail);
}
@utility mx-sai {
@apply ml-sai mr-sai;
}
@utility my-sai {
@apply mt-sai mb-sai;
}
@utility m-sai {
@apply my-sai mx-sai;
}
@utility top-sai {
top: var(--sait);
}
@utility right-sai {
right: var(--sair);
}
@utility bottom-sai {
bottom: var(--saib);
}
@utility left-sai {
left: var(--sail);
}
@utility card2 {
@apply rounded-box text-base-content p-4 sm:p-6;
}
@utility column {
@apply flex flex-col;
}
@utility center {
@apply flex items-center justify-center;
}
@utility row-2 {
@apply flex items-center gap-2;
}
@utility row-3 {
@apply flex items-center gap-3;
}
@utility row-4 {
@apply flex items-center gap-4;
}
@utility col-2 {
@apply flex flex-col gap-2;
}
@utility col-3 {
@apply flex flex-col gap-3;
}
@utility col-4 {
@apply flex flex-col gap-4;
}
@utility col-8 {
@apply flex flex-col gap-8;
}
@utility ellipsize {
@apply overflow-hidden text-ellipsis;
}
@utility content-padding-x {
@apply px-4 sm:px-8 md:px-12;
}
@utility content-padding-t {
@apply pt-4 sm:pt-8 md:pt-12;
}
@utility content-padding-b {
@apply pb-4 sm:pb-8 md:pb-12;
}
@utility content-padding-y {
@apply pt-4 sm:pt-8 md:pt-12 pb-4 sm:pb-8 md:pb-12;
}
@utility content-sizing {
@apply m-auto w-full max-w-3xl;
}
@utility content {
@apply m-auto w-full max-w-3xl px-4 sm:px-8 md:px-12 pt-4 sm:pt-8 md:pt-12 pb-4 sm:pb-8 md:pb-12;
}
@utility heading {
@apply text-center text-2xl;
}
@utility subheading {
@apply text-center text-xl;
}
@utility superheading {
@apply text-center text-4xl;
}
@utility link {
@apply text-primary cursor-pointer underline;
}
/* content visibility */
@utility cv {
content-visibility: auto;
}
/*
The default border color has changed to `currentcolor` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentcolor);
}
}
@layer utilities {
/* Fonts */
@font-face {
font-family: "Satoshis";
font-style: normal;
font-weight: 400;
src:
local(""),
url("/fonts/Satoshi Symbol.ttf") format("truetype");
}
.pr-sai {
padding-right: var(--sair);
@font-face {
font-family: "Lato";
font-style: normal;
font-weight: 400;
src:
local(""),
url("/fonts/Lato-Regular.ttf") format("truetype");
}
.pb-sai {
padding-bottom: var(--saib);
@font-face {
font-family: "Lato";
font-style: bold;
font-weight: 600;
src:
local(""),
url("/fonts/Lato-Bold.ttf") format("truetype");
}
.pl-sai {
padding-left: var(--sail);
@font-face {
font-family: "Lato";
font-style: italic;
font-weight: 400;
src:
local(""),
url("/fonts/Italic.ttf") format("truetype");
}
.px-sai {
@apply pl-sai pr-sai;
/* root */
:root {
font-family: Lato;
--sait: var(--safe-area-inset-top, env(safe-area-inset-top));
--saib: var(--safe-area-inset-bottom, env(safe-area-inset-bottom));
--sail: var(--safe-area-inset-left, env(safe-area-inset-left));
--sair: var(--safe-area-inset-right, env(safe-area-inset-right));
}
.py-sai {
@apply pt-sai pb-sai;
[data-theme] {
@apply bg-base-300;
}
.p-sai {
@apply py-sai px-sai;
.mobile [data-tip]::before {
display: none !important;
}
.mt-sai {
margin-top: var(--sait);
}
.mr-sai {
margin-right: var(--sair);
}
.mb-sai {
margin-bottom: var(--saib);
}
.ml-sai {
margin-left: var(--sail);
}
.mx-sai {
@apply ml-sai mr-sai;
}
.my-sai {
@apply mt-sai mb-sai;
}
.m-sai {
@apply my-sai mx-sai;
}
.top-sai {
top: var(--sait);
}
.right-sai {
right: var(--sair);
}
.bottom-sai {
bottom: var(--saib);
}
.left-sai {
left: var(--sail);
}
/* safe area insets */
}
/* utilities */
@@ -165,110 +261,18 @@
@apply bg-base-300 text-base-content transition-colors;
}
.card2 {
@apply rounded-box p-4 text-base-content sm:p-6;
}
.card2.card2-sm {
@apply p-2 text-base-content sm:p-4;
}
.column {
@apply flex flex-col;
}
.center {
@apply flex items-center justify-center;
}
.row-2 {
@apply flex items-center gap-2;
}
.row-3 {
@apply flex items-center gap-3;
}
.row-4 {
@apply flex items-center gap-4;
}
.col-2 {
@apply flex flex-col gap-2;
}
.col-3 {
@apply flex flex-col gap-3;
}
.col-4 {
@apply flex flex-col gap-4;
}
.col-8 {
@apply flex flex-col gap-8;
}
.badge {
@apply justify-start overflow-hidden text-ellipsis whitespace-nowrap;
}
.ellipsize {
@apply overflow-hidden text-ellipsis;
@apply text-base-content p-2 sm:p-4;
}
[data-tip]::before {
@apply ellipsize;
}
.content-padding-x {
@apply px-4 sm:px-8 md:px-12;
}
.content-padding-t {
@apply pt-4 sm:pt-8 md:pt-12;
}
.content-padding-b {
@apply pb-4 sm:pb-8 md:pb-12;
}
.content-padding-y {
@apply content-padding-t content-padding-b;
}
.content-sizing {
@apply m-auto w-full max-w-3xl;
}
.content {
@apply content-sizing content-padding-x content-padding-y;
}
.heading {
@apply text-center text-2xl;
}
.subheading {
@apply text-center text-xl;
}
.superheading {
@apply text-center text-4xl;
}
.link {
@apply cursor-pointer text-primary underline;
@apply overflow-hidden text-ellipsis;
}
.input input::placeholder {
opacity: 0.5;
}
.shadow-top-xl {
@apply shadow-[0_20px_25px_-5px_rgb(0,0,0,0.1)_0_8px_10px_-6px_rgb(0,0,0,0.1)];
}
/* tiptap */
.input-editor,
@@ -278,21 +282,21 @@
}
.tiptap {
--tiptap-object-bg: var(--neutral);
--tiptap-object-fg: var(--neutral-content);
--tiptap-active-bg: var(--primary);
--tiptap-active-fg: var(--primary-content);
--tiptap-object-bg: var(--color-neutral);
--tiptap-object-fg: var(--color-neutral-content);
--tiptap-active-bg: var(--color-primary);
--tiptap-active-fg: var(--color-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-object-bg: var(--color-base-100);
--tiptap-object-fg: var(--color-base-content);
--tiptap-active-bg: var(--color-base-300);
--tiptap-active-fg: var(--color-base-content);
}
.tiptap-suggestions__item {
@apply border-l-2 border-solid border-base-100;
@apply border-base-100 border-l-2 border-solid;
}
.tiptap-suggestions__selected {
@@ -312,13 +316,13 @@
}
.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-object-bg: var(--color-base-200);
@apply input rounded-box h-auto min-h-32 p-[.65rem] pb-6;
}
.input-editor .tiptap {
--tiptap-object-bg: var(--base-200);
@apply input input-bordered h-auto p-[.65rem];
--tiptap-object-bg: var(--color-base-200);
@apply input h-auto p-[.65rem];
}
/* link-content, based on tiptap */
@@ -330,8 +334,8 @@
white-space: nowrap;
border-radius: 3px;
padding: 0 0.25rem;
background-color: var(--base-100);
color: var(--base-content);
background-color: var(--color-base-100);
color: var(--color-base-content);
}
/* content rendered by welshman/content */
@@ -347,25 +351,25 @@
/* date input */
.picker {
--date-picker-foreground: var(--base-content);
--date-picker-background: var(--base-300);
--date-picker-highlight-border: var(--primary);
--date-picker-selected-color: var(--primary-content);
--date-picker-selected-background: var(--primary);
--date-picker-foreground: var(--color-base-content);
--date-picker-background: var(--color-base-300);
--date-picker-highlight-border: var(--color-primary);
--date-picker-selected-color: var(--color-primary-content);
--date-picker-selected-background: var(--color-primary);
}
.date-time-field {
@apply input input-bordered rounded-lg px-0;
@apply input rounded-lg px-0;
}
.date-time-field input {
@apply !h-full !w-full !rounded-lg !border-none !bg-inherit !px-4 !text-inherit;
@apply h-full! w-full! rounded-lg! border-none! bg-inherit! px-4! text-inherit!;
}
/* tippy popover */
.tippy-target {
@apply pointer-events-none fixed inset-0 z-tooltip;
@apply z-tooltip pointer-events-none fixed inset-0;
}
.tippy-target > * {
@@ -379,15 +383,15 @@
/* emoji picker */
emoji-picker {
--background: var(--base-100);
--border-color: var(--base-100);
--background: var(--color-base-100);
--border-color: var(--color-base-100);
--border-radius: var(--rounded-box);
--button-active-background: var(--base-content);
--button-hover-background: var(--base-content);
--indicator-color: var(--base-content);
--input-border-color: var(--base-100);
--input-font-color: var(--base-content);
--outline-color: var(--base-100);
--button-active-background: var(--color-base-content);
--button-hover-background: var(--color-base-content);
--indicator-color: var(--color-base-content);
--input-border-color: var(--color-base-100);
--input-font-color: var(--color-base-content);
--outline-color: var(--color-base-100);
}
/* progress */
@@ -411,7 +415,7 @@ body.keyboard-open .hide-on-keyboard {
/* chat view */
.chat__compose {
@apply relative z-compose mb-14 flex-grow md:mb-0;
@apply z-compose relative mb-14 grow md:mb-0;
}
.chat__compose .chat__compose-inner {
@@ -419,11 +423,5 @@ body.keyboard-open .hide-on-keyboard {
}
.chat__scroll-down {
@apply pb-sai fixed bottom-28 right-4 z-feature md:bottom-16;
}
/* content visibility */
.cv {
content-visibility: auto;
@apply pb-sai z-feature fixed right-4 bottom-28 md:bottom-16;
}
@@ -38,7 +38,7 @@
publishReaction({...template, event, relays: [url], protect: await shouldProtect})
</script>
<div class="flex flex-grow flex-wrap justify-end gap-2">
<div class="flex grow flex-wrap justify-end gap-2">
{#if h && showRoom}
<Link href={makeSpacePath(url, h)} class="btn btn-neutral btn-xs rounded-full">
Posted in #<RoomName {h} {url} />
+37 -18
View File
@@ -20,24 +20,33 @@
import EditorContent from "@app/editor/EditorContent.svelte"
import {PROTECTED} from "@app/core/state"
import {makeEditor} from "@app/editor"
import {DraftKey} from "@app/util/drafts"
import {pushToast} from "@app/util/toast"
import {canEnforceNip70} from "@app/core/commands"
type Values = {
d: string
title: string
content: string | object
location: string
start?: number
end?: number
}
type Props = {
url: string
h?: string
header: Snippet
initialValues?: {
d: string
title: string
content: string
location: string
start: number
end: number
}
initialValues?: Values
}
const {url, h, header, initialValues}: Props = $props()
let {url, h, header, initialValues}: Props = $props()
const draftKey = new DraftKey<Values>(`calendar:${url}:${h ?? ""}`)
if (!initialValues) {
initialValues = draftKey.get()
}
const shouldProtect = canEnforceNip70(url)
@@ -74,9 +83,9 @@
const ed = await editor
const content = ed.getText({blockSeparator: "\n"}).trim()
const tags = [
["d", initialValues?.d || randomId()],
["d", d],
["title", title],
["location", location || ""],
["location", location],
["start", start.toString()],
["end", end.toString()],
...daysBetween(start, end).map(D => ["D", D.toString()]),
@@ -95,17 +104,27 @@
pushToast({message: "Your event has been saved!"})
publishThunk({event, relays: [url]})
draftKey.clear()
history.back()
}
const content = initialValues?.content || ""
const editor = makeEditor({url, submit, uploading, content})
let title = $state(initialValues?.title || "")
let location = $state(initialValues?.location || "")
const d = $state(initialValues?.d ?? randomId())
let title = $state(initialValues?.title ?? "")
let location = $state(initialValues?.location ?? "")
let start: number | undefined = $state(initialValues?.start)
let end: number | undefined = $state(initialValues?.end)
let endDirty = Boolean(initialValues?.end)
let endDirty = $state(Boolean(initialValues?.end))
let content = $state(initialValues?.content ?? "")
const onChange = (json: object) => {
content = json
}
const editor = makeEditor({url, submit, uploading, onChange, content})
$effect(() => {
draftKey.set({d, title, location, start, end, content})
})
$effect(() => {
if (!endDirty && start) {
@@ -136,7 +155,7 @@
{#snippet input()}
<div
class="relative z-feature flex gap-2 border-t border-solid border-base-100 bg-base-100">
<div class="input-editor flex-grow overflow-hidden">
<div class="input-editor grow overflow-hidden">
<EditorContent {editor} />
</div>
<Button data-tip="Add an image" class="center btn tooltip" onclick={selectFiles}>
@@ -19,7 +19,7 @@
const end = $derived(parseInt(meta.end))
</script>
<div class="flex flex-grow flex-wrap justify-between gap-2">
<div class="flex grow flex-wrap justify-between gap-2">
<p class="text-xl">{meta.title || meta.name}</p>
{#if !isNaN(start) && !isNaN(end)}
{@const startDateDisplay = formatTimestampAsDate(start)}
+1 -1
View File
@@ -23,7 +23,7 @@
{#if meta.location}
<span class="flex items-start gap-1">
<Icon icon={MapPoint} class="mt-[2px]" size={4} />
<span class="break-words">{meta.location}</span>
<span class="wrap-break-word">{meta.location}</span>
</span>
{/if}
</div>
+4 -1
View File
@@ -55,6 +55,7 @@
import ThunkToast from "@app/components/ThunkToast.svelte"
import {userSettingsValues, deriveChat} from "@app/core/state"
import {pushModal} from "@app/util/modal"
import {DraftKey} from "@app/util/drafts"
import {makeDelete, prependParent} from "@app/core/commands"
import {pushToast} from "@app/util/toast"
@@ -66,6 +67,7 @@
const {pubkeys, info}: Props = $props()
const chat = deriveChat(pubkeys)
const draftKey = new DraftKey<{content?: string | object}>(`dm:${$chat?.id}`)
const others = remove($pubkey!, pubkeys)
const missingRelayLists = $derived(others.filter(pk => !$messagingRelayListsByPubkey.has(pk)))
@@ -336,7 +338,8 @@
{onSubmit}
{onEscape}
{onEditPrevious}
content={eventToEdit?.content}
initialValues={eventToEdit}
draftKey={eventToEdit ? undefined : draftKey}
disabled={Boolean(missingRelayLists.length)} />
{/key}
</div>
+33 -5
View File
@@ -10,23 +10,40 @@
import Button from "@lib/components/Button.svelte"
import EditorContent from "@app/editor/EditorContent.svelte"
import {makeEditor} from "@app/editor"
import {type DraftKey} from "@app/util/drafts"
type Values = {
content?: string | object
}
type Props = {
content?: string
disabled?: boolean
draftKey?: DraftKey<Values>
onEscape?: () => void
onEditPrevious?: () => void
onSubmit: (event: EventContent) => void
initialValues?: Values
}
const {content, disabled = false, onEscape, onEditPrevious, onSubmit}: Props = $props()
let {
initialValues,
disabled = false,
draftKey,
onEscape,
onEditPrevious,
onSubmit,
}: Props = $props()
if (!initialValues) {
initialValues = draftKey?.get()
}
const autofocus = !isMobile && !disabled
const uploading = writable(false)
const editorClass = $derived(
cx("chat-editor flex-grow overflow-hidden", {
cx("chat-editor grow overflow-hidden", {
"pointer-events-none opacity-50": disabled,
}),
)
@@ -59,18 +76,29 @@
onSubmit({content, tags})
draftKey?.clear()
ed.chain().clearContent().run()
}
let content = $state(initialValues?.content ?? "")
const onChange = (json: object) => {
content = json
}
const editor = makeEditor({
content,
autofocus,
submit,
uploading,
onChange,
aggressive: true,
encryptFiles: true,
})
$effect(() => {
draftKey?.set({content})
})
onMount(async () => {
const ed = await editor
ed.view.dom.addEventListener("keydown", handleKeyDown)
@@ -95,7 +123,7 @@
{/if}
</Button>
<div class={editorClass} aria-disabled={disabled}>
<EditorContent {editor} />
<EditorContent {autofocus} {editor} />
</div>
<Button
data-tip="{window.navigator.platform.includes('Mac') ? 'cmd' : 'ctrl'}+enter to send"
+1 -1
View File
@@ -35,7 +35,7 @@
<Button class="flex flex-col justify-start gap-1 w-full" onclick={openChat}>
<div
class="cursor-pointer border-t border-solid border-base-100 px-6 py-2 transition-colors hover:bg-base-100 {props.class}"
class="cursor-pointer border-t border-solid border-base-100 px-3 py-2 transition-colors hover:bg-base-100 {props.class}"
class:bg-base-100={active}>
<div class="flex flex-col justify-start gap-1">
<div class="flex items-center justify-between gap-2">
-9
View File
@@ -1,6 +1,5 @@
<script lang="ts">
import {assoc} from "@welshman/lib"
import ChatSquare from "@assets/icons/chat-square.svg?dataurl"
import Check from "@assets/icons/check.svg?dataurl"
import Bell from "@assets/icons/bell.svg?dataurl"
import BellOff from "@assets/icons/bell-off.svg?dataurl"
@@ -8,13 +7,9 @@
import Button from "@lib/components/Button.svelte"
import Modal from "@lib/components/Modal.svelte"
import ModalBody from "@lib/components/ModalBody.svelte"
import ChatStart from "@app/components/ChatStart.svelte"
import {setChecked} from "@app/util/notifications"
import {pushModal} from "@app/util/modal"
import {notificationSettings} from "@app/core/state"
const startChat = () => pushModal(ChatStart, {}, {replaceState: true})
const markAsRead = () => {
setChecked("/chat/*")
history.back()
@@ -28,10 +23,6 @@
<Modal>
<ModalBody>
<div class="flex flex-col gap-2">
<Button class="btn btn-primary" onclick={startChat}>
<Icon size={5} icon={ChatSquare} />
Start chat
</Button>
<Button class="btn btn-neutral" onclick={markAsRead}>
<Icon size={5} icon={Check} />
Mark all read
+1 -1
View File
@@ -42,7 +42,7 @@
publishReaction({...template, event, relays: [url], protect: await shouldProtect})
</script>
<div class="flex flex-grow flex-wrap justify-end gap-2">
<div class="flex grow flex-wrap justify-end gap-2">
{#if h && showRoom}
<Link href={makeSpacePath(url, h)} class="btn btn-neutral btn-xs rounded-full">
Posted in #<RoomName {h} {url} />
+41 -22
View File
@@ -20,25 +20,34 @@
import {pushToast} from "@app/util/toast"
import {PROTECTED} from "@app/core/state"
import {makeEditor} from "@app/editor"
import {DraftKey} from "@app/util/drafts"
import {canEnforceNip70, uploadFile} from "@app/core/commands"
type Values = {
d: string
title: string
content: string | object
price: number
currency: string
images: (string | File)[]
status: string
topics: string[]
}
type Props = {
url: string
h?: string
header: Snippet
initialValues?: {
d?: string
title?: string
content?: string
price?: number
currency?: string
images?: string[]
status?: string
topics?: string[]
}
initialValues?: Values
}
const {url, h, header, initialValues}: Props = $props()
let {url, h, header, initialValues}: Props = $props()
const draftKey = new DraftKey<Values>(`classified:${url}:${h ?? ""}`)
if (!initialValues) {
initialValues = draftKey.get()
}
const shouldProtect = canEnforceNip70(url)
@@ -66,7 +75,7 @@
}
const tags = [
["d", initialValues?.d || randomId()],
["d", d],
["title", title],
["summary", content],
["price", String(price), currency],
@@ -110,22 +119,32 @@
event: makeEvent(CLASSIFIED, {content, tags}),
})
draftKey.clear()
history.back()
} finally {
loading = false
}
}
const content = initialValues?.content || ""
const editor = makeEditor({url, submit, content})
let loading = $state(false)
let title = $state(initialValues?.title || "")
let status = $state(initialValues?.status || "active")
let price = $state(Number(initialValues?.price || 0))
let currency = $state(initialValues?.currency || "SAT")
let images = $state<(string | File)[]>(initialValues?.images || [])
let topics = $state(uniq(removeUndefined((initialValues?.topics || []).map(normalizeTopic))))
const d = $state(initialValues?.d ?? randomId())
let title = $state(initialValues?.title ?? "")
let status = $state(initialValues?.status ?? "active")
let price = $state(initialValues?.price ?? 0)
let currency = $state(initialValues?.currency ?? "SAT")
let images = $state(initialValues?.images ?? [])
let topics = $state(uniq(removeUndefined(initialValues?.topics?.map(normalizeTopic) ?? [])))
let content = $state(initialValues?.content ?? "")
const onChange = (json: object) => {
content = json
}
const editor = makeEditor({url, submit, onChange, content})
$effect(() => {
draftKey.set({d, title, status, price, currency, images, topics, content})
})
</script>
<Modal tag="form" onsubmit={preventDefault(submit)}>
@@ -153,7 +172,7 @@
<p>Description*</p>
{/snippet}
{#snippet input()}
<div class="note-editor flex-grow overflow-hidden">
<div class="note-editor grow overflow-hidden">
<EditorContent {editor} />
</div>
{/snippet}
+1 -1
View File
@@ -28,7 +28,7 @@
</script>
<div class="flex flex-wrap items-center justify-between gap-2">
<div class="flex flex-grow flex-wrap justify-end gap-2">
<div class="flex grow flex-wrap justify-end gap-2">
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tooltip-left" />
<ThunkStatusOrDeleted {event} />
{#if showActivity}
+1 -1
View File
@@ -150,7 +150,7 @@
</div>
{:else}
<div
class="overflow-hidden text-ellipsis break-words"
class="overflow-hidden text-ellipsis wrap-break-word"
style={expandBlock ? "mask-image: linear-gradient(0deg, transparent 0px, black 100px)" : ""}>
{#each shortContent as parsed, i}
{#if isNewline(parsed) && !isBlock(i - 1)}
+22 -2
View File
@@ -1,12 +1,19 @@
<script lang="ts">
import {call, ellipsize, displayUrl, postJson} from "@welshman/lib"
import {isRelayUrl, getTagValue} from "@welshman/util"
import {Capacitor} from "@capacitor/core"
import {preventDefault, stopPropagation} from "@lib/html"
import Link from "@lib/components/Link.svelte"
import ContentLinkDetail from "@app/components/ContentLinkDetail.svelte"
import ContentLinkBlockImage from "@app/components/ContentLinkBlockImage.svelte"
import {pushModal} from "@app/util/modal"
import {dufflepud, PLATFORM_URL, IMAGE_CONTENT_TYPES, VIDEO_CONTENT_TYPES} from "@app/core/state"
import {
dufflepud,
PLATFORM_URL,
IMAGE_CONTENT_TYPES,
VIDEO_CONTENT_TYPES,
THUMBNAIL_URL,
} from "@app/core/state"
import {makeSpacePath} from "@app/util/routes"
const {value, event} = $props()
@@ -22,6 +29,14 @@
return [url, true]
})
const getVideoPoster = (videoUrl: string): string | undefined => {
if (Capacitor.getPlatform() === "android" && THUMBNAIL_URL) {
return `${THUMBNAIL_URL}/thumbnail?url=${encodeURIComponent(videoUrl)}`
}
return undefined
}
const loadPreview = async () => {
const json = await postJson(dufflepud("link/preview"), {url})
@@ -42,7 +57,12 @@
<Link {external} {href} class="my-2 block">
<div class="overflow-hidden rounded-box">
{#if url.match(/\.(mov|webm|mp4)$/) || VIDEO_CONTENT_TYPES.includes(fileType)}
<video controls src={url} class="max-h-96 rounded-box object-contain object-center">
<video
controls
src={url}
poster={getVideoPoster(url)}
preload="metadata"
class="max-h-96 rounded-box object-contain object-center">
<track kind="captions" />
</video>
{:else if url.match(/\.(jpe?g|png|gif|webp)$/) || IMAGE_CONTENT_TYPES.includes(fileType)}
+1 -1
View File
@@ -101,7 +101,7 @@
</p>
</div>
{:else}
<div class="overflow-hidden text-ellipsis break-words">
<div class="overflow-hidden text-ellipsis wrap-break-word">
{#each shortContent as parsed, i}
{#if isNewline(parsed)}
<ContentNewline value={parsed.value} />
+1 -1
View File
@@ -45,7 +45,7 @@
{#if $quote.kind === MESSAGE}
<div
class="border-l-2 border-solid border-l-primary py-1 pl-2 opacity-90"
style="background-color: color-mix(in srgb, var(--primary) 10%, var(--base-300) 90%);">
style="background-color: color-mix(in srgb, var(--color-primary) 10%, var(--color-base-300) 90%);">
<NoteContentMinimal trimParent {url} event={$quote} />
</div>
{:else}
+2 -2
View File
@@ -101,7 +101,7 @@
{/if}
<div class="relative">
<pre class="card2 card2-sm bg-alt overflow-auto text-xs"><code>{json}</code></pre>
<p class="absolute right-2 top-2 flex flex-grow items-center justify-between">
<p class="absolute right-2 top-2 flex grow items-center justify-between">
<Button onclick={copyJson} class="btn btn-neutral btn-sm flex items-center">
<Icon icon={Copy} /> Copy
</Button>
@@ -109,6 +109,6 @@
</div>
</ModalBody>
<ModalFooter>
<Button class="btn btn-primary flex-grow" onclick={() => history.back()}>Got it</Button>
<Button class="btn btn-primary grow" onclick={() => history.back()}>Got it</Button>
</ModalFooter>
</Modal>
+22 -6
View File
@@ -10,13 +10,19 @@
import {publishComment, canEnforceNip70} from "@app/core/commands"
import {PROTECTED} from "@app/core/state"
import {makeEditor} from "@app/editor"
import {DraftKey} from "@app/util/drafts"
import {pushToast} from "@app/util/toast"
type Values = {
content?: string | object
}
const {url, event, onClose, onSubmit} = $props()
const draftKey = new DraftKey<Values>(`reply:${event.id}`)
const initialValues = draftKey.get()
const shouldProtect = canEnforceNip70(url)
const uploading = writable(false)
const autofocus = !isMobile
const selectFiles = () => editor.then(ed => ed.commands.selectFiles())
@@ -38,13 +44,23 @@
})
}
draftKey.clear()
onSubmit(publishComment({event, content, tags, relays: [url]}))
}
const editor = makeEditor({url, submit, uploading, autofocus: !isMobile})
let form: HTMLElement
let spacer: HTMLElement
let content = $state(initialValues?.content ?? "")
const onChange = (json: object) => {
content = json
}
const editor = makeEditor({url, submit, uploading, content, onChange})
$effect(() => {
draftKey.set({content})
})
onMount(() => {
setTimeout(() => {
@@ -71,8 +87,8 @@
class="left-content bottom-sai right-sai ml-2 pl-2 fixed z-feature">
<div class="card2 mx-2 my-2 bg-alt shadow-md">
<div class="relative">
<div class="note-editor flex-grow overflow-hidden">
<EditorContent {editor} />
<div class="note-editor grow overflow-hidden">
<EditorContent {autofocus} {editor} />
</div>
<Button
data-tip="Add an image"
+1 -1
View File
@@ -30,7 +30,7 @@
publishReaction({...template, event, relays: [url], protect: await shouldProtect})
</script>
<div class="flex flex-grow flex-wrap justify-end gap-2">
<div class="flex grow flex-wrap justify-end gap-2">
{#if h && showRoom}
<Link href={makeSpacePath(url, h)} class="btn btn-neutral btn-xs rounded-full">
Posted in #<RoomName {h} {url} />
+43 -12
View File
@@ -20,14 +20,28 @@
import {pushToast} from "@app/util/toast"
import {PROTECTED} from "@app/core/state"
import {makeEditor} from "@app/editor"
import {DraftKey} from "@app/util/drafts"
import {canEnforceNip70} from "@app/core/commands"
type Values = {
title: string
content: string | object
amount: number
}
type Props = {
url: string
h?: string
initialValues?: Values
}
const {url, h}: Props = $props()
let {url, h, initialValues}: Props = $props()
const draftKey = new DraftKey<Values>(`goal:${url}:${h ?? ""}`)
if (!initialValues) {
initialValues = draftKey.get()
}
const shouldProtect = canEnforceNip70(url)
@@ -40,7 +54,7 @@
const submit = async () => {
if ($uploading) return
if (!content) {
if (!title) {
return pushToast({
theme: "error",
message: "Please provide a title for your funding goal.",
@@ -48,9 +62,9 @@
}
const ed = await editor
const summary = ed.getText({blockSeparator: "\n"}).trim()
const content = ed.getText({blockSeparator: "\n"}).trim()
if (!summary.trim()) {
if (!content.trim()) {
return pushToast({
theme: "error",
message: "Please provide details about your funding goal.",
@@ -59,7 +73,7 @@
const tags = [
...ed.storage.nostr.getEditorTags(),
["summary", summary],
["summary", content],
["amount", String(amount)],
["relays", url],
]
@@ -74,16 +88,33 @@
publishThunk({
relays: [url],
event: makeEvent(ZAP_GOAL, {content, tags}),
event: makeEvent(ZAP_GOAL, {content: title, tags}),
})
draftKey.clear()
history.back()
}
const editor = makeEditor({url, submit, uploading, placeholder: "What's on your mind?"})
let title = $state(initialValues?.title ?? "")
let amount = $state(initialValues?.amount ?? 1000)
let content = $state(initialValues?.content ?? "")
let content = $state("")
let amount = $state(1000)
const onChange = (json: object) => {
content = json
}
const editor = makeEditor({
url,
submit,
uploading,
onChange,
placeholder: "What's on your mind?",
content,
})
$effect(() => {
draftKey.update({title, content, amount})
})
</script>
<Modal tag="form" onsubmit={preventDefault(submit)}>
@@ -102,7 +133,7 @@
<!-- svelte-ignore a11y_autofocus -->
<input
autofocus={!isMobile}
bind:value={content}
bind:value={title}
class="grow"
type="text"
placeholder="What do funds go towards?" />
@@ -115,7 +146,7 @@
<p>Details*</p>
{/snippet}
{#snippet input()}
<div class="note-editor flex-grow overflow-hidden">
<div class="note-editor grow overflow-hidden">
<EditorContent {editor} />
</div>
{/snippet}
@@ -137,7 +168,7 @@
Goal Amount (sats)*
{/snippet}
{#snippet input()}
<div class="flex flex-grow justify-end">
<div class="flex grow justify-end">
<label class="input input-bordered flex items-center gap-2">
<Icon icon={Bolt} />
<input bind:value={amount} type="number" class="w-28" />
+1 -1
View File
@@ -23,7 +23,7 @@
<ModalTitle>Unable to Zap</ModalTitle>
</ModalHeader>
<p>
Zapping <ProfileLink {pubkey} class="!text-primary" /> isn't possible right now because
Zapping <ProfileLink {pubkey} class="text-primary!" /> isn't possible right now because
{#if $zapper}
their zap receiver isn't correctly set up.
{:else}
@@ -97,10 +97,10 @@
tabindex="-1"
onmousedown={stopPropagation(onClear)}
ontouchstart={stopPropagation(onClear)}>
<Icon icon={CloseCircle} class="scale-150 !bg-base-300" />
<Icon icon={CloseCircle} class="scale-150 bg-base-300!" />
</span>
{:else}
<Icon icon={AddCircle} class="scale-150 !bg-base-300" />
<Icon icon={AddCircle} class="scale-150 bg-base-300!" />
{/if}
</div>
{#if !url}
@@ -9,10 +9,10 @@
<div class="flex items-start gap-4">
<CalendarEventDate event={props.event} />
<div class="flex flex-grow flex-col">
<div class="flex grow flex-col">
<CalendarEventHeader event={props.event} />
<div class="flex py-2 opacity-50">
<div class="h-px flex-grow bg-base-content opacity-25"></div>
<div class="h-px grow bg-base-content opacity-25"></div>
</div>
<Content {...props} />
</div>
@@ -17,7 +17,7 @@
</script>
<div class="flex flex-col">
<div class="flex flex-grow flex-wrap justify-between gap-2">
<div class="flex grow flex-wrap justify-between gap-2">
<p class="text-sm">{meta.title || meta.name}</p>
{#if !isNaN(start) && !isNaN(end)}
{@const startDateDisplay = formatTimestampAsDate(start)}
+29 -12
View File
@@ -22,22 +22,32 @@
import {pushToast} from "@app/util/toast"
import {PROTECTED} from "@app/core/state"
import {canEnforceNip70} from "@app/core/commands"
import {DraftKey} from "@app/util/drafts"
import type {PollType} from "@app/util/polls"
type Option = {
id: string
value: string
}
type Values = {
title: string
pollType: PollType
endsAt?: number
options: Option[]
}
type Props = {
url: string
h?: string
}
const {url, h}: Props = $props()
const draftKey = new DraftKey<Values>(`poll:${url}:${h ?? ""}`)
const initialValues = draftKey.get()
const shouldProtect = canEnforceNip70(url)
type DraftOption = {
id: string
value: string
}
const back = () => history.back()
const addOption = () => {
@@ -129,17 +139,24 @@
event: makeEvent(Poll, {content: title.trim(), tags}),
})
draftKey.clear()
history.back()
}
let title = $state("")
let pollType = $state<PollType>("singlechoice")
let endsAt = $state<number | undefined>()
let options = $state<DraftOption[]>([
{id: randomId(), value: "Yes"},
{id: randomId(), value: "No"},
])
let draggedOptionId = $state<string | undefined>()
let title = $state(initialValues?.title ?? "")
let pollType = $state<PollType>(initialValues?.pollType ?? "singlechoice")
let endsAt = $state<number | undefined>(initialValues?.endsAt)
let options = $state<Option[]>(
initialValues?.options ?? [
{id: randomId(), value: "Yes"},
{id: randomId(), value: "No"},
],
)
$effect(() => {
draftKey.set({title, pollType, endsAt, options})
})
</script>
<Modal tag="form" onsubmit={preventDefault(submit)}>
+1 -1
View File
@@ -43,7 +43,7 @@
<div class="flex flex-col gap-2 card2 card2-sm bg-alt">
<div class="flex items-center justify-between gap-2">
<label class="flex min-w-0 flex-grow items-center gap-2">
<label class="flex min-w-0 grow items-center gap-2">
{#if !closed}
{#if pollType === "singlechoice"}
<input
+2 -2
View File
@@ -32,7 +32,7 @@
</script>
<div
class="ml-sai mt-sai mb-sai relative z-popover isolate hidden w-14 flex-shrink-0 bg-base-200 pt-2 md:block">
class="ml-sai mt-sai mb-sai relative z-popover isolate hidden w-14 shrink-0 bg-base-200 pt-2 md:block">
<div class="flex h-full flex-col" class:justify-between={PLATFORM_RELAYS.length === 0}>
<PrimaryNavSpaces />
{#if PLATFORM_RELAYS.length > 0}
@@ -63,7 +63,7 @@
<!-- a little extra something for ios -->
<div
class="hide-on-keyboard fixed bottom-0 left-0 right-0 z-nav h-[var(--saib)] bg-base-100 md:hidden">
class="hide-on-keyboard fixed bottom-0 left-0 right-0 z-nav h-(--saib) bg-base-100 md:hidden">
</div>
<div
class="hide-on-keyboard border-top bottom-sai fixed left-0 right-0 z-nav h-14 border border-base-200 bg-base-100 md:hidden">
+2 -2
View File
@@ -25,10 +25,10 @@
<div class="flex flex-col gap-2">
{#each spaceUrls as url (url)}
<div class="card2 bg-alt flex flex-row items-center gap-2">
<div class="flex-shrink-0">
<div class="shrink-0">
<RelayIcon {url} size={12} />
</div>
<div class="flex flex-grow flex-col">
<div class="flex grow flex-col">
<RelayName {url} />
<div class="text-sm opacity-75">
{url}
+1 -1
View File
@@ -121,6 +121,6 @@
</div>
</ModalBody>
<ModalFooter>
<Button class="btn btn-primary flex-grow" onclick={back}>Done</Button>
<Button class="btn btn-primary grow" onclick={back}>Done</Button>
</ModalFooter>
</Modal>
+2 -2
View File
@@ -26,8 +26,8 @@
type="button"
class="btn font-normal flex h-[unset] w-full flex-nowrap py-4 text-left items-start justify-between"
{onclick}>
<div class="flex flex-grow flex-row items-start gap-4">
<div class="flex h-7 w-7 flex-shrink-0 items-center justify-center">
<div class="flex grow flex-row items-start gap-4">
<div class="flex h-7 w-7 shrink-0 items-center justify-center">
<Icon {icon} />
</div>
<div class="flex flex-col gap-1">
+1 -1
View File
@@ -23,7 +23,7 @@
<div class="relative">
<div class="avatar relative">
<div
class="center !flex h-12 w-12 min-w-12 rounded-full border-2 border-solid border-base-300 bg-base-300">
class="center flex! h-12 w-12 min-w-12 rounded-full border-2 border-solid border-base-300 bg-base-300">
<RelayIcon {url} />
</div>
</div>
+34 -6
View File
@@ -12,18 +12,29 @@
import ComposeMenu from "@app/components/ComposeMenu.svelte"
import EditorContent from "@app/editor/EditorContent.svelte"
import {makeEditor} from "@app/editor"
import {DraftKey} from "@app/util/drafts"
import {onDestroy, onMount} from "svelte"
type Values = {
content?: string | object
}
type Props = {
url?: string
h?: string
content?: string
onEscape?: () => void
onEditPrevious?: () => void
onSubmit: (event: EventContent) => void
initialValues?: Values
}
const {url, h, content, onEscape, onEditPrevious, onSubmit}: Props = $props()
let {url, h, initialValues, onEscape, onEditPrevious, onSubmit}: Props = $props()
const draftKey = url || h ? new DraftKey<Values>(`room:${url ?? ""}:${h ?? ""}`) : undefined
if (!initialValues) {
initialValues = draftKey?.get()
}
const autofocus = !isMobile
@@ -61,12 +72,29 @@
onSubmit({content, tags})
draftKey?.clear()
ed.chain().clearContent().run()
}
const editor = makeEditor({url, content, autofocus, submit, uploading, aggressive: true})
let popover: Instance | undefined = $state()
let content = $state(initialValues?.content ?? "")
const onChange = (json: object) => {
content = json
}
const editor = makeEditor({
url,
content,
submit,
uploading,
onChange,
aggressive: true,
})
$effect(() => {
draftKey?.set({content})
})
onMount(async () => {
const ed = await editor
@@ -104,8 +132,8 @@
</Button>
</Tippy>
</div>
<div class="chat-editor flex-grow overflow-hidden">
<EditorContent {editor} />
<div class="chat-editor grow overflow-hidden">
<EditorContent {autofocus} {editor} />
</div>
<Button
data-tip="{window.navigator.platform.includes('Mac') ? 'cmd' : 'ctrl'}+enter to send"
+1 -1
View File
@@ -131,7 +131,7 @@
<p>Icon</p>
{/snippet}
{#snippet input()}
<div class="flex flex-grow items-center justify-between gap-4">
<div class="flex grow items-center justify-between gap-4">
{#if imagePreview}
<div class="flex items-center gap-2">
<span class="text-sm opacity-75">Selected:</span>
+10 -5
View File
@@ -37,6 +37,7 @@
event: TrustedEvent
replyTo?: (event: TrustedEvent) => void
showPubkey?: boolean
addSpaceBelow?: boolean
inert?: boolean
canEdit: (event: TrustedEvent) => boolean
onEdit: (event: TrustedEvent) => void
@@ -47,6 +48,7 @@
event,
replyTo = undefined,
showPubkey = false,
addSpaceBelow = false,
inert = false,
canEdit,
onEdit,
@@ -77,19 +79,22 @@
<TapTarget
data-event={event.id}
onTap={inert ? null : onTap}
class="group relative flex w-full cursor-default flex-col p-2 pb-3 text-left hover:bg-base-100/50">
class={cx(
"group relative flex w-full cursor-default flex-col px-2 py-0.5 text-left hover:bg-base-100/50",
{"mt-1.5": showPubkey, "mb-1.5": addSpaceBelow},
)}>
<div class="flex w-full gap-3 overflow-auto">
{#if showPubkey}
<Button onclick={openProfile} class="flex items-start">
<Button onclick={openProfile} class="flex items-start pt-1.5 justify-center w-8 shrink-0">
<ProfileCircle
pubkey={event.pubkey}
class="border border-solid border-base-content"
size={8} />
</Button>
{:else}
<div class="w-8 min-w-8 max-w-8"></div>
<div class="w-8 shrink-0"></div>
{/if}
<div class="min-w-0 flex-grow pr-1">
<div class="min-w-0 grow pr-1">
{#if showPubkey}
<div class="flex items-center gap-2">
<Button onclick={openProfile} class="text-sm font-bold" style="color: {colorValue}">
@@ -140,7 +145,7 @@
</div>
{#if !isMobile}
<button
class="join absolute right-1 top-1 border border-solid border-neutral text-xs opacity-0 transition-all"
class="join absolute right-2 top-0.5 border border-solid border-neutral text-xs opacity-0 transition-all pr-2"
class:group-hover:opacity-100={!isMobile}>
{#if ENABLE_ZAPS}
<RoomItemZapButton {url} {event} />
+1 -1
View File
@@ -11,7 +11,7 @@
const {url, h, ...props}: Props = $props()
</script>
<div class="flex flex-grow items-center justify-between gap-4 {props.class}">
<div class="flex grow items-center justify-between gap-4 {props.class}">
<div class="flex items-center gap-3">
<RoomImage {url} {h} />
<div class="min-w-0 overflow-hidden text-ellipsis">
+1 -1
View File
@@ -27,7 +27,7 @@
<Button onclick={back} class="place-self-start pr-3 md:hidden">
<Icon icon={ArrowLeft} size={7} />
</Button>
<div class="ellipsize whitespace-nowrap flex flex-grow items-center justify-between gap-4">
<div class="ellipsize whitespace-nowrap flex grow items-center justify-between gap-4">
<div class="flex flex-col">
<div class="flex gap-2 items-center">
{@render title?.()}
+1 -1
View File
@@ -42,7 +42,7 @@
<div class="relative">
<div class="avatar relative">
<div
class="center !flex h-16 w-16 min-w-16 rounded-full border-2 border-solid border-base-300 bg-base-300">
class="center flex! h-16 w-16 min-w-16 rounded-full border-2 border-solid border-base-300 bg-base-300">
<RelayIcon {url} size={10} />
</div>
</div>
+1 -1
View File
@@ -134,7 +134,7 @@
<p>Icon</p>
{/snippet}
{#snippet input()}
<div class="flex items-center gap-4 justify-between flex-grow">
<div class="flex items-center gap-4 justify-between grow">
{#if imagePreview}
<div class="flex items-center gap-2">
<span class="text-sm opacity-75">Selected:</span>
+1 -1
View File
@@ -100,6 +100,6 @@
</div>
</ModalBody>
<ModalFooter>
<Button class="btn btn-primary flex-grow" onclick={back}>Done</Button>
<Button class="btn btn-primary grow" onclick={back}>Done</Button>
</ModalFooter>
</Modal>
+6 -6
View File
@@ -140,7 +140,7 @@
<div bind:this={element} class="flex min-h-0 flex-1 flex-col">
<SecondaryNavSection class="min-h-0 flex-1 flex flex-col overflow-hidden pb-0">
<div class="flex-shrink-0">
<div class="shrink-0">
<Button
class="relative flex w-full flex-col rounded-xl p-3 transition-all hover:bg-base-100"
onclick={openMenu}>
@@ -270,14 +270,14 @@
{/if}
{#if hasNip29($relay)}
{#if $userRooms.length > 0}
<div class="h-2 flex-shrink-0"></div>
<div class="h-2 shrink-0"></div>
<SecondaryNavHeader>Your Rooms</SecondaryNavHeader>
{/if}
{#each $userRooms as h (h)}
<SpaceMenuRoomItem {url} {h} />
{/each}
{#if $otherRooms.length > 0}
<div class="h-2 flex-shrink-0"></div>
<div class="h-2 shrink-0"></div>
<SecondaryNavHeader>
{#if $userRooms.length > 0}
Other Rooms
@@ -296,7 +296,7 @@
<SpaceMenuRoomItem {url} {h} />
{/each}
{#if $otherVoiceRooms.length > 0}
<div class="h-2 flex-shrink-0"></div>
<div class="h-2 shrink-0"></div>
<SecondaryNavHeader>Voice Rooms</SecondaryNavHeader>
{#each $otherVoiceRooms as h (h)}
<SpaceMenuRoomItem {url} {h} />
@@ -309,11 +309,11 @@
</SecondaryNavItem>
{/if}
{/if}
<div class="h-5 flex-shrink-0"></div>
<div class="h-5 shrink-0"></div>
</div>
</SecondaryNavSection>
<div
class="flex flex-shrink-0 flex-col gap-2 p-2 pt-0 -mt-4 pb-[calc(var(--saib)+0.25rem)] md:pb-2 z-nav">
class="flex shrink-0 flex-col gap-2 p-2 pt-0 -mt-4 pb-[calc(var(--saib)+0.25rem)] md:pb-2 z-nav">
<VoiceWidget />
<Button class="btn btn-neutral btn-sm h-10" onclick={showDetail}>
<SocketStatusIndicator {url} />
+1 -1
View File
@@ -30,7 +30,7 @@
publishReaction({...template, event, relays: [url], protect: await shouldProtect})
</script>
<div class="flex flex-grow flex-wrap justify-end gap-2">
<div class="flex grow flex-wrap justify-end gap-2">
{#if h && showRoom}
<Link href={makeSpacePath(url, h)} class="btn btn-neutral btn-xs rounded-full">
Posted in #<RoomName {h} {url} />
+28 -4
View File
@@ -18,15 +18,22 @@
import {pushToast} from "@app/util/toast"
import {PROTECTED} from "@app/core/state"
import {makeEditor} from "@app/editor"
import {DraftKey} from "@app/util/drafts"
import {canEnforceNip70} from "@app/core/commands"
type Values = {
content?: string | object
title?: string
}
type Props = {
url: string
h?: string
}
const {url, h}: Props = $props()
const draftKey = new DraftKey<Values>(`thread:${url}:${h ?? ""}`)
const initialValues = draftKey.get()
const shouldProtect = canEnforceNip70(url)
const uploading = writable(false)
@@ -70,12 +77,29 @@
event: makeEvent(THREAD, {content, tags}),
})
draftKey.clear()
history.back()
}
const editor = makeEditor({url, submit, uploading, placeholder: "What's on your mind?"})
let title = $state(initialValues?.title ?? "")
let content = $state(initialValues?.content ?? "")
let title: string = $state("")
const onChange = (json: object) => {
content = json
}
const editor = makeEditor({
url,
submit,
uploading,
onChange,
placeholder: "What's on your mind?",
content,
})
$effect(() => {
draftKey.update({title, content})
})
</script>
<Modal tag="form" onsubmit={preventDefault(submit)}>
@@ -106,7 +130,7 @@
<p>Message*</p>
{/snippet}
{#snippet input()}
<div class="note-editor flex-grow overflow-hidden">
<div class="note-editor grow overflow-hidden">
<EditorContent {editor} />
</div>
{/snippet}
+1 -1
View File
@@ -64,7 +64,7 @@
{replaceState}
{notification}
onclick={handleClick}
class={cx("!items-start", isActive && "!bg-base-100 !text-base-content")}>
class={cx("items-start!", isActive && "bg-base-100! text-base-content!")}>
<div class="flex w-full min-w-0 flex-col gap-2">
<div class="flex gap-2 items-center">
{#if isJoining}
+1 -1
View File
@@ -70,7 +70,7 @@
Amount (satoshis)
{/snippet}
{#snippet input()}
<div class="flex flex-grow justify-end">
<div class="flex grow justify-end">
<label class="input input-bordered flex items-center gap-2">
<Icon icon={Bolt} />
<input
+1 -1
View File
@@ -80,7 +80,7 @@
Amount (satoshis)
{/snippet}
{#snippet input()}
<div class="flex flex-grow justify-end">
<div class="flex grow justify-end">
<label class="input input-bordered flex items-center gap-2">
<Icon icon={Bolt} />
<input bind:value={sats} type="number" class="w-14" placeholder="0" />
+2 -2
View File
@@ -1,7 +1,7 @@
<style>
.wot-background {
fill: transparent;
stroke: var(--base-content);
stroke: var(--color-base-content);
opacity: 30%;
}
@@ -32,7 +32,7 @@
const normalizedScore = $derived(clamp([0, max], $score) / max)
const dashOffset = $derived(100 - 44 * normalizedScore)
const style = $derived(`transform: rotate(${135 - normalizedScore * 180}deg)`)
const stroke = $derived(active ? "var(--primary)" : "var(--base-content)")
const stroke = $derived(active ? "var(--color-primary)" : "var(--color-base-content)")
</script>
<div class="relative h-[14px] w-[14px]">
+5 -5
View File
@@ -118,26 +118,26 @@
<ModalBody>
<ModalHeader>
<ModalTitle>Send a Zap</ModalTitle>
<ModalSubtitle>To <ProfileLink {pubkey} class="!text-primary" /></ModalSubtitle>
<ModalSubtitle>To <ProfileLink {pubkey} class="text-primary!" /></ModalSubtitle>
</ModalHeader>
<FieldInline class="!grid-cols-3">
<FieldInline class="grid-cols-3!">
{#snippet label()}
Emoji Reaction
{/snippet}
{#snippet input()}
<div class="flex flex-grow items-center justify-end gap-4">
<div class="flex grow items-center justify-end gap-4">
<EmojiButton {onEmoji} class="btn btn-neutral">
{content}
</EmojiButton>
</div>
{/snippet}
</FieldInline>
<FieldInline class="!grid-cols-3">
<FieldInline class="grid-cols-3!">
{#snippet label()}
Amount
{/snippet}
{#snippet input()}
<div class="flex flex-grow justify-end">
<div class="flex grow justify-end">
<label class="input input-bordered flex items-center gap-2">
<Icon icon={Bolt} />
<input bind:value={amount} type="number" class="w-24" />
+6 -6
View File
@@ -147,7 +147,7 @@
<ModalBody>
<ModalHeader>
<ModalTitle>Send a Zap</ModalTitle>
<ModalSubtitle>To <ProfileLink {pubkey} class="!text-primary" /></ModalSubtitle>
<ModalSubtitle>To <ProfileLink {pubkey} class="text-primary!" /></ModalSubtitle>
</ModalHeader>
{#if invoice}
@@ -158,30 +158,30 @@
</p>
</div>
<label class="input input-bordered flex w-full items-center justify-between gap-2">
<input readonly class="ellipsize flex-grow" value={invoice} />
<input readonly class="ellipsize grow" value={invoice} />
<Button class="flex items-center" onclick={copyInvoice}>
<Icon icon={Copy} />
</Button>
</label>
{:else}
<FieldInline class="!grid-cols-3">
<FieldInline class="grid-cols-3!">
{#snippet label()}
Emoji Reaction
{/snippet}
{#snippet input()}
<div class="flex flex-grow items-center justify-end gap-4">
<div class="flex grow items-center justify-end gap-4">
<EmojiButton {onEmoji} class="btn btn-neutral">
{content}
</EmojiButton>
</div>
{/snippet}
</FieldInline>
<FieldInline class="!grid-cols-3">
<FieldInline class="grid-cols-3!">
{#snippet label()}
Amount
{/snippet}
{#snippet input()}
<div class="flex flex-grow justify-end">
<div class="flex grow justify-end">
<label class="input input-bordered flex items-center gap-2">
<Icon icon={Bolt} />
<input bind:value={amount} type="number" class="w-24" />
+2
View File
@@ -210,6 +210,8 @@ export const DEFAULT_PUBKEYS = import.meta.env.VITE_DEFAULT_PUBKEYS
export const DUFFLEPUD_URL = "https://dufflepud.onrender.com"
export const THUMBNAIL_URL = import.meta.env.VITE_THUMBNAIL_URL
export const NIP46_PERMS =
"nip44_encrypt,nip44_decrypt," +
[
+11 -6
View File
@@ -4,20 +4,25 @@
type Props = {
editor: Promise<Editor>
autofocus?: boolean
}
const {editor}: Props = $props()
const {editor, autofocus}: Props = $props()
let element: HTMLElement
onMount(() => {
editor.then(({options}) => {
if (options.element) {
element?.append(options.element)
editor.then(ed => {
if (ed.options.element) {
element?.append(ed.options.element)
}
if (options.autofocus) {
;(element?.querySelector("[contenteditable]") as HTMLElement)?.focus()
if (autofocus) {
const hasContent = ed.getText().trim().length > 0
requestAnimationFrame(() => {
ed.commands.focus(hasContent ? "end" : "start")
})
}
})
})
+8 -6
View File
@@ -25,9 +25,9 @@ import {pushToast} from "@app/util/toast"
export const makeEditor = async ({
encryptFiles = false,
aggressive = false,
autofocus = false,
charCount,
content = "",
onChange,
placeholder = "",
url,
submit,
@@ -36,9 +36,9 @@ export const makeEditor = async ({
}: {
encryptFiles?: boolean
aggressive?: boolean
autofocus?: boolean
charCount?: Writable<number>
content?: string
content?: string | object
onChange?: (json: object) => void
placeholder?: string
url?: string
submit: () => void
@@ -82,9 +82,8 @@ export const makeEditor = async ({
},
)
return new Editor({
content: escapeHtml(content),
autofocus,
const ed = new Editor({
content: typeof content === "string" ? escapeHtml(content) : content,
editorProps,
element: document.createElement("div"),
extensions: [
@@ -142,6 +141,9 @@ export const makeEditor = async ({
onUpdate({editor}) {
wordCount?.set(editor.storage.wordCount.words)
charCount?.set(editor.storage.wordCount.chars)
onChange?.(editor.getJSON())
},
})
return ed
}
+21
View File
@@ -0,0 +1,21 @@
const store = new Map<string, unknown>()
export class DraftKey<T> {
constructor(private key: string) {}
get(): T | undefined {
return store.get(this.key) as T | undefined
}
set(value: T): void {
store.set(this.key, value)
}
update(value: Partial<T>): void {
this.set({...this.get(), ...value} as T)
}
clear(): void {
store.delete(this.key)
}
}
+4
View File
@@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.0867 21.3879L13.7321 21.77L13.0867 21.3879ZM13.6288 20.4721L12.9833 20.0901L13.6288 20.4721ZM10.3712 20.4721L9.72579 20.8541H9.72579L10.3712 20.4721ZM10.9133 21.3879L11.5587 21.0059L10.9133 21.3879ZM2.3806 15.9137L3.07351 15.6266V15.6266L2.3806 15.9137ZM7.78958 18.9917L7.77666 19.7416L7.78958 18.9917ZM5.08658 18.6196L4.79957 19.3126H4.79957L5.08658 18.6196ZM21.6194 15.9137L22.3123 16.2007V16.2007L21.6194 15.9137ZM16.2104 18.9917L16.1975 18.2418L16.2104 18.9917ZM18.9134 18.6196L19.2004 19.3126H19.2004L18.9134 18.6196ZM19.6125 2.73704L19.2206 3.37652L19.6125 2.73704ZM21.2632 4.38775L21.9027 3.99588V3.99588L21.2632 4.38775ZM4.38751 2.73704L3.99563 2.09756V2.09756L4.38751 2.73704ZM2.7368 4.38775L2.09732 3.99588H2.09732L2.7368 4.38775ZM9.40279 19.2101L9.77986 18.5618L9.77986 18.5618L9.40279 19.2101ZM13.7321 21.77L14.2742 20.8541L12.9833 20.0901L12.4412 21.0059L13.7321 21.77ZM9.72579 20.8541L10.2679 21.77L11.5587 21.0059L11.0166 20.0901L9.72579 20.8541ZM12.4412 21.0059C12.2485 21.3316 11.7515 21.3316 11.5587 21.0059L10.2679 21.77C11.0415 23.0769 12.9585 23.0769 13.7321 21.77L12.4412 21.0059ZM10.5 2.75024H13.5V1.25024H10.5V2.75024ZM21.25 10.5002V11.5002H22.75V10.5002H21.25ZM2.75 11.5002V10.5002H1.25V11.5002H2.75ZM1.25 11.5002C1.25 12.6548 1.24959 13.5583 1.29931 14.2871C1.3495 15.0225 1.45323 15.6346 1.68769 16.2007L3.07351 15.6266C2.92737 15.2738 2.84081 14.8441 2.79584 14.185C2.75041 13.5191 2.75 12.6754 2.75 11.5002H1.25ZM7.8025 18.2418C6.54706 18.2202 5.88923 18.1403 5.37359 17.9267L4.79957 19.3126C5.60454 19.646 6.52138 19.72 7.77666 19.7416L7.8025 18.2418ZM1.68769 16.2007C2.27128 17.6096 3.39066 18.729 4.79957 19.3126L5.3736 17.9267C4.33223 17.4954 3.50486 16.668 3.07351 15.6266L1.68769 16.2007ZM21.25 11.5002C21.25 12.6754 21.2496 13.5191 21.2042 14.185C21.1592 14.8441 21.0726 15.2738 20.9265 15.6266L22.3123 16.2007C22.5468 15.6346 22.6505 15.0225 22.7007 14.2871C22.7504 13.5583 22.75 12.6548 22.75 11.5002H21.25ZM16.2233 19.7416C17.4786 19.72 18.3955 19.646 19.2004 19.3126L18.6264 17.9267C18.1108 18.1403 17.4529 18.2202 16.1975 18.2418L16.2233 19.7416ZM20.9265 15.6266C20.4951 16.668 19.6678 17.4954 18.6264 17.9267L19.2004 19.3126C20.6093 18.729 21.7287 17.6096 22.3123 16.2007L20.9265 15.6266ZM13.5 2.75024C15.1512 2.75024 16.337 2.75104 17.2619 2.83898C18.1757 2.92586 18.7571 3.09247 19.2206 3.37652L20.0044 2.09756C19.2655 1.64481 18.4274 1.44303 17.4039 1.34571C16.3915 1.24945 15.1222 1.25024 13.5 1.25024V2.75024ZM22.75 10.5002C22.75 8.87805 22.7508 7.60874 22.6545 6.59635C22.5572 5.5728 22.3554 4.7347 21.9027 3.99588L20.6237 4.77962C20.9078 5.24315 21.0744 5.82458 21.1613 6.73833C21.2492 7.66325 21.25 8.84901 21.25 10.5002H22.75ZM19.2206 3.37652C19.7925 3.72696 20.2733 4.20776 20.6237 4.77963L21.9027 3.99588C21.4286 3.22218 20.7781 2.57168 20.0044 2.09756L19.2206 3.37652ZM10.5 1.25024C8.87781 1.25024 7.6085 1.24945 6.59611 1.34571C5.57256 1.44303 4.73445 1.64481 3.99563 2.09756L4.77938 3.37652C5.24291 3.09247 5.82434 2.92586 6.73809 2.83898C7.663 2.75104 8.84876 2.75024 10.5 2.75024V1.25024ZM2.75 10.5002C2.75 8.84901 2.75079 7.66325 2.83873 6.73833C2.92561 5.82458 3.09223 5.24315 3.37628 4.77963L2.09732 3.99588C1.64457 4.7347 1.44279 5.5728 1.34547 6.59635C1.24921 7.60874 1.25 8.87805 1.25 10.5002H2.75ZM3.99563 2.09756C3.22194 2.57168 2.57144 3.22218 2.09732 3.99588L3.37628 4.77963C3.72672 4.20776 4.20752 3.72696 4.77938 3.37652L3.99563 2.09756ZM11.0166 20.0901C10.8136 19.747 10.6354 19.4444 10.4621 19.2066C10.2795 18.9562 10.0702 18.7306 9.77986 18.5618L9.02572 19.8584C9.07313 19.886 9.13772 19.9362 9.24985 20.0901C9.37122 20.2566 9.50835 20.4867 9.72579 20.8541L11.0166 20.0901ZM7.77666 19.7416C8.21575 19.7492 8.49387 19.7547 8.70588 19.7782C8.90399 19.8001 8.98078 19.8323 9.02572 19.8584L9.77986 18.5618C9.4871 18.3915 9.18246 18.3218 8.87097 18.2873C8.57339 18.2543 8.21375 18.2489 7.8025 18.2418L7.77666 19.7416ZM14.2742 20.8541C14.4916 20.4867 14.6287 20.2566 14.7501 20.0901C14.8622 19.9362 14.9268 19.886 14.9742 19.8584L14.2201 18.5618C13.9298 18.7306 13.7204 18.9562 13.5379 19.2066C13.3646 19.4444 13.1864 19.747 12.9833 20.0901L14.2742 20.8541ZM16.1975 18.2418C15.7862 18.2489 15.4266 18.2543 15.129 18.2873C14.8175 18.3218 14.5129 18.3915 14.2201 18.5618L14.9742 19.8584C15.0192 19.8323 15.096 19.8001 15.2941 19.7782C15.5061 19.7547 15.7842 19.7492 16.2233 19.7416L16.1975 18.2418Z" fill="#000000"/>
<path d="M12 7.5V14.5M8.5 11H15.5" stroke="#000000" stroke-width="1.5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

+2 -2
View File
@@ -12,8 +12,8 @@
</script>
<div class="btn flex h-[unset] w-full flex-nowrap py-4 text-left {props.class}">
<div class="flex flex-grow flex-row items-start gap-4">
<div class="flex h-14 w-12 flex-shrink-0 items-center justify-center">
<div class="flex grow flex-row items-start gap-4">
<div class="flex h-14 w-12 shrink-0 items-center justify-center">
{@render props.icon?.()}
</div>
<div class="flex flex-col gap-1">
+1 -1
View File
@@ -29,7 +29,7 @@
const innerClass = $derived(
cx(
"relative text-base-content text-base-content flex-grow pointer-events-auto",
"relative text-base-content text-base-content grow pointer-events-auto",
"rounded-t-box sm:rounded-box",
{
"bg-alt shadow-m max-h-[90vh] flex flex-col max-w-full pb-sai sm:pb-0": !fullscreen,
+2 -2
View File
@@ -9,9 +9,9 @@
</script>
<div class="flex items-center gap-2 p-2 text-xs uppercase opacity-50">
<div class="h-px flex-grow bg-base-content opacity-25"></div>
<div class="h-px grow bg-base-content opacity-25"></div>
{#if children}
<p>{@render children?.()}</p>
<div class="h-px flex-grow bg-base-content opacity-25"></div>
<div class="h-px grow bg-base-content opacity-25"></div>
{/if}
</div>
+25
View File
@@ -0,0 +1,25 @@
<script lang="ts">
import type {Snippet} from "svelte"
import cx from "classnames"
import Button from "@lib/components/Button.svelte"
const {
onclick = () => {},
className = "",
children,
}: {
onclick?: () => void
className?: string
children?: Snippet
} = $props()
</script>
<div class={cx("fixed bottom-20 right-4 z-nav hide-on-keyboard md:hidden", className)}>
<Button
class="btn btn-primary border-none shadow-xl hover:opacity-90 transition-all size-[50px] rounded-xl p-0"
{onclick}>
<div class="flex items-center justify-center">
{@render children?.()}
</div>
</Button>
</div>
+2 -2
View File
@@ -8,9 +8,9 @@
const {children}: Props = $props()
</script>
<div class="h-20 flex-shrink-0"></div>
<div class="h-20 shrink-0"></div>
<div class="flex absolute bottom-sai left-0 right-0 p-6 py-4 rounded-b-box bg-base-200">
<div class="flex flex-grow gap-4 items-center justify-between">
<div class="flex grow gap-4 items-center justify-between">
{@render children?.()}
</div>
</div>
+1 -1
View File
@@ -9,6 +9,6 @@
<div
data-component="Page"
class="relative flex-grow flex flex-col min-w-0 ml-sai mb-sai mt-sai mr-sai bg-base-200 md:ml-0 md:mb-0 {props.class}">
class="relative grow flex flex-col min-w-0 ml-sai mb-sai mt-sai mr-sai bg-base-200 md:ml-0 md:mb-0 {props.class}">
{@render props.children?.()}
</div>
+1 -1
View File
@@ -12,7 +12,7 @@
<div
class={cx(
"mt-sai mb-sai max-h-screen w-60 min-h-0 flex-shrink-0 flex-col gap-1 bg-base-300 z-nav hidden md:flex",
"mt-sai mb-sai max-h-screen w-60 min-h-0 shrink-0 flex-col gap-1 bg-base-300 z-nav hidden md:flex",
props.class,
)}>
{@render children?.()}
+1 -1
View File
@@ -6,6 +6,6 @@
const {children}: Props = $props()
</script>
<div class="flex items-center justify-between px-4 py-2 text-sm font-bold uppercase">
<div class="flex items-center justify-between px-1 py-2 text-sm font-bold uppercase">
{@render children?.()}
</div>
+1 -1
View File
@@ -36,7 +36,7 @@
const active = $derived($page.url.pathname === href)
const wrapperClass = $derived(
cx(restProps.class, "relative flex flex-shrink-0 items-center gap-3 text-left transition-all", {
cx(restProps.class, "relative flex shrink-0 items-center gap-3 text-left transition-all", {
"hover:bg-base-100 hover:text-base-content": true,
"text-base-content bg-base-100": active,
"tooltip tooltip-right": title,
@@ -7,6 +7,6 @@
const {...props}: Props = $props()
</script>
<div class="flex flex-col gap-1 px-2 py-2 {props.class}">
<div class="flex flex-col gap-3 px-2 py-2 {props.class}">
{@render props.children?.()}
</div>
+6 -6
View File
@@ -1,7 +1,7 @@
<style>
:global(.tippy-box[data-theme~="tooltip"]) {
background-color: var(--neutral);
color: var(--neutral-content);
background-color: var(--color-neutral);
color: var(--color-neutral-content);
border-radius: 0.5rem;
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
@@ -11,19 +11,19 @@
}
:global(.tippy-box[data-theme~="tooltip"][data-placement^="top"] > .tippy-arrow::before) {
border-top-color: var(--neutral);
border-top-color: var(--color-neutral);
}
:global(.tippy-box[data-theme~="tooltip"][data-placement^="bottom"] > .tippy-arrow::before) {
border-bottom-color: var(--neutral);
border-bottom-color: var(--color-neutral);
}
:global(.tippy-box[data-theme~="tooltip"][data-placement^="left"] > .tippy-arrow::before) {
border-left-color: var(--neutral);
border-left-color: var(--color-neutral);
}
:global(.tippy-box[data-theme~="tooltip"][data-placement^="right"] > .tippy-arrow::before) {
border-right-color: var(--neutral);
border-right-color: var(--color-neutral);
}
</style>
+1
View File
@@ -1,5 +1,6 @@
<script lang="ts">
import "@src/app.css"
import "@welshman/editor/index.css"
import "@capacitor-community/safe-area"
import * as nip19 from "nostr-tools/nip19"
import type {Unsubscriber} from "svelte/store"
+20 -5
View File
@@ -5,15 +5,18 @@
import {sleep} from "@welshman/lib"
import {shouldUnwrap} from "@welshman/app"
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
import ChatSquarePlus from "@assets/icons/chat-square-plus.svg?dataurl"
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Page from "@lib/components/Page.svelte"
import Button from "@lib/components/Button.svelte"
import Spinner from "@lib/components/Spinner.svelte"
import FAB from "@lib/components/FAB.svelte"
import SecondaryNav from "@lib/components/SecondaryNav.svelte"
import SecondaryNavHeader from "@lib/components/SecondaryNavHeader.svelte"
import SecondaryNavSection from "@lib/components/SecondaryNavSection.svelte"
import ChatMenu from "@app/components/ChatMenu.svelte"
import ChatStart from "@app/components/ChatStart.svelte"
import ChatItem from "@app/components/ChatItem.svelte"
import {chatSearch} from "@app/core/state"
import {pushModal} from "@app/util/modal"
@@ -26,6 +29,8 @@
const openMenu = () => pushModal(ChatMenu)
const startChat = () => pushModal(ChatStart)
let term = $state("")
const chats = $derived($chatSearch.searchOptions(term))
@@ -37,7 +42,7 @@
})
</script>
<SecondaryNav>
<SecondaryNav class="relative">
<SecondaryNavSection>
<SecondaryNavHeader>
Chats
@@ -45,11 +50,15 @@
<Icon icon={MenuDots} />
</Button>
</SecondaryNavHeader>
<Button class="btn btn-primary w-full btn-sm" onclick={startChat}>
<Icon icon={ChatSquarePlus} />
Start New Chat
</Button>
<label class="input input-sm input-bordered flex items-center gap-2">
<Icon icon={Magnifier} />
<input bind:value={term} class="grow" type="text" />
</label>
</SecondaryNavSection>
<label class="input input-sm input-bordered mx-6 -mt-4 mb-2 flex items-center gap-2">
<Icon icon={Magnifier} />
<input bind:value={term} class="grow" type="text" />
</label>
<div class="overflow-auto">
{#each chats as { id, pubkeys, messages } (id)}
<ChatItem {id} {pubkeys} {messages} />
@@ -66,3 +75,9 @@
{@render children?.()}
{/key}
</Page>
{#if !$page.params.chat}
<FAB onclick={startChat}>
<Icon icon={ChatSquarePlus} size={7} />
</FAB>
{/if}
+3 -3
View File
@@ -37,8 +37,8 @@
<ContentSearch class="md:hidden">
{#snippet input()}
<div class="row-2 min-w-0 flex-grow items-center">
<label class="input input-bordered flex flex-grow items-center gap-2">
<div class="row-2 min-w-0 grow items-center">
<label class="input input-bordered flex grow items-center gap-2">
<Icon icon={Magnifier} />
<input
bind:value={term}
@@ -46,7 +46,7 @@
type="text"
placeholder="Search for conversations..." />
</label>
<Button class="btn btn-primary" onclick={openMenu}>
<Button class="btn btn-neutral" onclick={openMenu}>
<Icon icon={MenuDots} />
</Button>
</div>
+5 -2
View File
@@ -12,6 +12,7 @@
import Bell from "@assets/icons/bell.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Page from "@lib/components/Page.svelte"
import PageContent from "@lib/components/PageContent.svelte"
import SecondaryNav from "@lib/components/SecondaryNav.svelte"
import SecondaryNavItem from "@lib/components/SecondaryNavItem.svelte"
import SecondaryNavSection from "@lib/components/SecondaryNavSection.svelte"
@@ -32,7 +33,7 @@
<SecondaryNav>
<SecondaryNavSection>
<SecondaryNavItem class="w-full !justify-between">
<SecondaryNavItem class="w-full justify-between!">
<strong class="ellipsize flex items-center gap-3"> Your Settings </strong>
</SecondaryNavItem>
<SecondaryNavItem href="/settings/profile">
@@ -68,5 +69,7 @@
</SecondaryNav>
<Page>
{@render children?.()}
<PageContent>
{@render children?.()}
</PageContent>
</Page>
+3 -3
View File
@@ -67,7 +67,7 @@
</script>
<div class="content column gap-4">
<div class="card2 bg-alt shadow-md">
<div class="card2 bg-alt shadow-md col-2">
<div class="flex justify-between gap-2">
<div class="flex max-w-full gap-3">
<div class="py-1">
@@ -116,9 +116,9 @@
{/snippet}
{#snippet input()}
<label class="input input-bordered flex w-full items-center justify-between gap-2">
<div class="row-2 flex-grow items-center">
<div class="row-2 grow items-center">
<Icon icon={LinkRound} />
<input readonly class="ellipsize flex-grow" value={npub} />
<input readonly class="ellipsize grow" value={npub} />
</div>
<Button class="flex items-center" onclick={copyNpub}>
<Icon icon={Copy} />
+2 -2
View File
@@ -22,10 +22,10 @@
<svelte:window bind:innerWidth={width} />
{#if width <= md}
<div class="ml-sai mt-sai mb-sai relative z-nav w-14 flex-shrink-0 bg-base-200 pt-2">
<div class="ml-sai mt-sai mb-sai relative z-nav w-14 shrink-0 bg-base-200 pt-2">
<PrimaryNavSpaces />
</div>
<SecondaryNav class="!flex !w-auto flex-grow pb-16">
<SecondaryNav class="flex! w-auto! grow pb-16">
<SpaceMenu {url} />
</SecondaryNav>
{/if}
+21 -10
View File
@@ -272,14 +272,21 @@
elements.push({type: "date", value: date, id: date, showPubkey: false})
}
const showPubkey =
previousPubkey !== event.pubkey ||
event.created_at - previousCreatedAt > int(3, MINUTE) ||
previousKind === ROOM_ADD_MEMBER
if (showPubkey && elements.length > 0) {
elements[elements.length - 1].addSpaceBelow = true
}
elements.push({
id: event.id,
type: "note",
value: event,
showPubkey:
previousPubkey !== event.pubkey ||
event.created_at - previousCreatedAt > int(3, MINUTE) ||
previousKind === ROOM_ADD_MEMBER,
showPubkey,
addSpaceBelow: false,
})
previousDate = date
@@ -288,6 +295,9 @@
previousCreatedAt = event.created_at
seen.add(event.id)
}
if (elements.length > 0) {
elements[elements.length - 1].addSpaceBelow = true
}
}
elements.reverse()
@@ -382,15 +392,15 @@
<Spinner loading={loadingForward}>Looking for messages...</Spinner>
</p>
{/if}
{#each elements as { type, id, value, showPubkey } (id)}
{#each elements as { type, id, value, showPubkey, addSpaceBelow } (id)}
{#if type === "new-messages"}
<div
{id}
class="flex items-center py-2 text-xs transition-colors"
class:opacity-0={showFixedNewMessages}>
<div class="h-px flex-grow bg-primary"></div>
<div class="h-px grow bg-primary"></div>
<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>
<div class="h-px grow bg-primary"></div>
</div>
{:else if type === "date"}
<Divider>{value}</Divider>
@@ -399,12 +409,13 @@
{#if event.kind === ROOM_ADD_MEMBER}
<RoomItemAddMember {url} {event} />
{:else}
<div in:slide class="cv" class:-mt-1={!showPubkey}>
<div in:slide class="cv">
<RoomItem
{url}
{event}
{replyTo}
{showPubkey}
{addSpaceBelow}
canEdit={canEditEvent}
onEdit={onEditEvent} />
</div>
@@ -464,13 +475,13 @@
{onSubmit}
{onEscape}
{onEditPrevious}
content={eventToEdit?.content}
initialValues={eventToEdit}
bind:this={compose} />
{/key}
{/if}
</div>
{#if isVoiceRoom || $voiceState === VoiceState.Joining || $voiceState === VoiceState.Connected}
<div class="hide-on-keyboard flex-shrink-0 p-2 md:hidden">
<div class="hide-on-keyboard shrink-0 p-2 md:hidden">
<VoiceWidget />
</div>
{/if}
@@ -128,9 +128,9 @@
<div class={"calendar-event-" + event.id}>
{#if isFirstFutureEvent}
<div class="flex items-center gap-2 p-2">
<div class="h-px flex-grow bg-primary"></div>
<div class="h-px grow bg-primary"></div>
<p class="text-xs uppercase text-primary">Today</p>
<div class="h-px flex-grow bg-primary"></div>
<div class="h-px grow bg-primary"></div>
</div>
{/if}
{#if dateDisplay}
@@ -69,11 +69,11 @@
<div class="card2 bg-alt col-3 z-feature">
<div class="flex items-start gap-4">
<CalendarEventDate event={$event} />
<div class="flex min-w-0 flex-grow flex-col gap-1">
<div class="flex min-w-0 grow flex-col gap-1">
<CalendarEventHeader event={$event} />
<CalendarEventMeta event={$event} {url} />
<div class="flex py-2 opacity-50">
<div class="h-px flex-grow bg-base-content opacity-25"></div>
<div class="h-px grow bg-base-content opacity-25"></div>
</div>
<Content showEntire event={$event} {url} />
</div>
+21 -10
View File
@@ -210,14 +210,21 @@
elements.push({type: "date", value: date, id: date, showPubkey: false})
}
const showPubkey =
previousPubkey !== event.pubkey ||
event.created_at - previousCreatedAt > int(3, MINUTE) ||
previousKind === RELAY_ADD_MEMBER
if (showPubkey && elements.length > 0) {
elements[elements.length - 1].addSpaceBelow = true
}
elements.push({
id: event.id,
type: "note",
value: event,
showPubkey:
previousPubkey !== event.pubkey ||
event.created_at - previousCreatedAt > int(3, MINUTE) ||
previousKind === RELAY_ADD_MEMBER,
showPubkey,
addSpaceBelow: false,
})
previousDate = date
@@ -226,6 +233,9 @@
previousCreatedAt = event.created_at
seen.add(event.id)
}
if (elements.length > 0) {
elements[elements.length - 1].addSpaceBelow = true
}
}
elements.reverse()
@@ -295,15 +305,15 @@
<Spinner loading={loadingForward}>Looking for messages...</Spinner>
</p>
{/if}
{#each elements as { type, id, value, showPubkey } (id)}
{#each elements as { type, id, value, showPubkey, addSpaceBelow } (id)}
{#if type === "new-messages"}
<div
{id}
class="flex items-center py-2 text-xs transition-colors"
class:opacity-0={showFixedNewMessages}>
<div class="h-px flex-grow bg-primary"></div>
<div class="h-px grow bg-primary"></div>
<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>
<div class="h-px grow bg-primary"></div>
</div>
{:else if type === "date"}
<Divider>{value}</Divider>
@@ -312,14 +322,15 @@
{#if event.kind === RELAY_ADD_MEMBER}
<RoomItemAddMember {url} {event} />
{:else}
<div class:-mt-1={!showPubkey}>
<div>
<RoomItem
{url}
{event}
{replyTo}
{showPubkey}
canEdit={canEditEvent}
onEdit={onEditEvent} />
onEdit={onEditEvent}
{addSpaceBelow} />
</div>
{/if}
{/if}
@@ -352,7 +363,7 @@
{onSubmit}
{onEscape}
{onEditPrevious}
content={eventToEdit?.content}
initialValues={eventToEdit}
bind:this={compose} />
{/key}
</div>
+27 -24
View File
@@ -1,6 +1,7 @@
import {config} from "dotenv"
import daisyui from "daisyui"
import themes from "daisyui/src/theming/themes"
import daisyTheme from "daisyui/theme"
import themes from "daisyui/theme/object"
config({path: ".env.local"})
config({path: ".env"})
@@ -25,27 +26,29 @@ export default {
toast: 9,
},
},
plugins: [daisyui],
daisyui: {
themes: [
{
dark: {
...themes["dark"],
primary: process.env.VITE_PLATFORM_ACCENT,
"primary-content": process.env.VITE_PLATFORM_ACCENT_CONTENT || "#EAE7FF",
secondary: process.env.VITE_PLATFORM_SECONDARY,
"secondary-content": process.env.VITE_PLATFORM_SECONDARY_CONTENT || "#EAE7FF",
},
light: {
...themes["winter"],
neutral: "#F2F7FF",
warning: "#FD8D0B",
primary: process.env.VITE_PLATFORM_ACCENT,
"primary-content": process.env.VITE_PLATFORM_ACCENT_CONTENT || "#EAE7FF",
secondary: process.env.VITE_PLATFORM_SECONDARY,
"secondary-content": process.env.VITE_PLATFORM_SECONDARY_CONTENT || "#EAE7FF",
},
},
],
},
plugins: [
daisyui({
themes: ["light --default", "dark --prefersdark"],
}),
daisyTheme({
name: "dark",
...themes["night"],
"--color-base-content": "oklch(75% 0.029 256.847)",
"--color-primary": process.env.VITE_PLATFORM_ACCENT,
"--color-primary-content": process.env.VITE_PLATFORM_ACCENT_CONTENT || "#EAE7FF",
"--color-secondary": process.env.VITE_PLATFORM_SECONDARY,
"--color-secondary-content": process.env.VITE_PLATFORM_SECONDARY_CONTENT || "#EAE7FF",
}),
daisyTheme({
name: "light",
...themes["winter"],
"--color-neutral": "#F2F7FF",
"--color-neutral-content": "var(--color-base-content)",
"--color-warning": "#FD8D0B",
"--color-primary": process.env.VITE_PLATFORM_ACCENT,
"--color-primary-content": process.env.VITE_PLATFORM_ACCENT_CONTENT || "#EAE7FF",
"--color-secondary": process.env.VITE_PLATFORM_SECONDARY,
"--color-secondary-content": process.env.VITE_PLATFORM_SECONDARY_CONTENT || "#EAE7FF",
}),
],
}