Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0e41680fff |
@@ -74,7 +74,6 @@
|
|||||||
"@vite-pwa/sveltekit": "^1.1.0",
|
"@vite-pwa/sveltekit": "^1.1.0",
|
||||||
"@welshman/app": "^0.8.16",
|
"@welshman/app": "^0.8.16",
|
||||||
"@welshman/content": "^0.8.16",
|
"@welshman/content": "^0.8.16",
|
||||||
"@welshman/domain": "^0.8.16",
|
|
||||||
"@welshman/editor": "^0.8.16",
|
"@welshman/editor": "^0.8.16",
|
||||||
"@welshman/feeds": "^0.8.16",
|
"@welshman/feeds": "^0.8.16",
|
||||||
"@welshman/lib": "^0.8.16",
|
"@welshman/lib": "^0.8.16",
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 340 KiB |
+203
-12
@@ -2,10 +2,15 @@
|
|||||||
|
|
||||||
@config "../tailwind.config.js";
|
@config "../tailwind.config.js";
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
--font-sans: "Lato", ui-sans-serif, system-ui, sans-serif;
|
||||||
|
--font-display: "Baloo 2", "Lato", ui-rounded, system-ui, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
/* root */
|
/* root */
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
font-family: Lato;
|
font-family: var(--font-sans);
|
||||||
--sait: var(--safe-area-inset-top, env(safe-area-inset-top));
|
--sait: var(--safe-area-inset-top, env(safe-area-inset-top));
|
||||||
--saib: var(--safe-area-inset-bottom, env(safe-area-inset-bottom));
|
--saib: var(--safe-area-inset-bottom, env(safe-area-inset-bottom));
|
||||||
--sail: var(--safe-area-inset-left, env(safe-area-inset-left));
|
--sail: var(--safe-area-inset-left, env(safe-area-inset-left));
|
||||||
@@ -153,15 +158,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@utility heading {
|
@utility heading {
|
||||||
@apply text-center text-2xl;
|
@apply font-display text-center text-2xl font-bold tracking-tight;
|
||||||
}
|
}
|
||||||
|
|
||||||
@utility subheading {
|
@utility brand {
|
||||||
@apply text-center text-xl;
|
@apply font-display text-primary font-bold tracking-tight;
|
||||||
}
|
}
|
||||||
|
|
||||||
@utility superheading {
|
@utility label {
|
||||||
@apply text-center text-4xl;
|
@apply font-display text-sm font-semibold tracking-wider uppercase opacity-70;
|
||||||
}
|
}
|
||||||
|
|
||||||
@utility link {
|
@utility link {
|
||||||
@@ -215,8 +220,19 @@
|
|||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Lato";
|
font-family: "Lato";
|
||||||
font-style: bold;
|
font-style: normal;
|
||||||
font-weight: 600;
|
font-weight: 300;
|
||||||
|
src:
|
||||||
|
local(""),
|
||||||
|
url("/fonts/Lato-Light.ttf") format("truetype");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Lato ships Regular + Bold only; map 600 (semibold) and 700 (bold) to the
|
||||||
|
Bold file so the browser never synthesizes a faux-bold. */
|
||||||
|
@font-face {
|
||||||
|
font-family: "Lato";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600 700;
|
||||||
src:
|
src:
|
||||||
local(""),
|
local(""),
|
||||||
url("/fonts/Lato-Bold.ttf") format("truetype");
|
url("/fonts/Lato-Bold.ttf") format("truetype");
|
||||||
@@ -228,13 +244,38 @@
|
|||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
src:
|
src:
|
||||||
local(""),
|
local(""),
|
||||||
url("/fonts/Italic.ttf") format("truetype");
|
url("/fonts/Lato-Italic.ttf") format("truetype");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Baloo 2 — rounded, friendly display face (self-hosted, Latin subset). */
|
||||||
|
@font-face {
|
||||||
|
font-family: "Baloo 2";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
font-display: swap;
|
||||||
|
src: url("/fonts/Baloo2-Medium.woff2") format("woff2");
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Baloo 2";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
|
src: url("/fonts/Baloo2-SemiBold.woff2") format("woff2");
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Baloo 2";
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
src: url("/fonts/Baloo2-Bold.woff2") format("woff2");
|
||||||
}
|
}
|
||||||
|
|
||||||
/* root */
|
/* root */
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
font-family: Lato;
|
font-family: var(--font-sans);
|
||||||
text-size-adjust: 100%;
|
text-size-adjust: 100%;
|
||||||
--sait: var(--safe-area-inset-top, env(safe-area-inset-top));
|
--sait: var(--safe-area-inset-top, env(safe-area-inset-top));
|
||||||
--saib: var(--safe-area-inset-bottom, env(safe-area-inset-bottom));
|
--saib: var(--safe-area-inset-bottom, env(safe-area-inset-bottom));
|
||||||
@@ -284,7 +325,7 @@
|
|||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* tiptap */
|
/* editors */
|
||||||
|
|
||||||
.input-editor,
|
.input-editor,
|
||||||
.chat-editor,
|
.chat-editor,
|
||||||
@@ -323,7 +364,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.chat-editor .tiptap {
|
.chat-editor .tiptap {
|
||||||
@apply rounded-box bg-base-300 pr-12;
|
@apply bg-base-300 rounded-[1.5rem] pr-12 transition-shadow;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-editor:focus-within .tiptap {
|
||||||
|
box-shadow: 0 0 0 2px color-mix(in oklab, var(--color-primary), transparent 55%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.note-editor .tiptap {
|
.note-editor .tiptap {
|
||||||
@@ -448,3 +493,149 @@ body.keyboard-open .chat__compose {
|
|||||||
.chat__scroll-down {
|
.chat__scroll-down {
|
||||||
@apply pb-sai z-feature fixed right-4 bottom-28 md:bottom-16;
|
@apply pb-sai z-feature fixed right-4 bottom-28 md:bottom-16;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* shape, depth & motion */
|
||||||
|
|
||||||
|
/* Accessibility: neutralize all motion when the user asks for it. Decorative
|
||||||
|
motion is otherwise opt-in via `motion-safe:` and the guards below. */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
animation-duration: 0.01ms !important;
|
||||||
|
animation-iteration-count: 1 !important;
|
||||||
|
transition-duration: 0.01ms !important;
|
||||||
|
scroll-behavior: auto !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Soft, diffuse elevation — replaces ad-hoc hard `shadow-md` uses. */
|
||||||
|
@utility shadow-soft {
|
||||||
|
box-shadow:
|
||||||
|
0 4px 16px -4px oklch(0% 0 0 / 0.18),
|
||||||
|
0 1px 3px oklch(0% 0 0 / 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Organic "hand-drawn" avatar masks. The image (or gradient fallback) fills
|
||||||
|
the blob; three variants are chosen deterministically by pubkey hash so a
|
||||||
|
person's shape stays stable across the app. */
|
||||||
|
@utility avatar-blob {
|
||||||
|
border-radius: 42% 58% 54% 46% / 58% 46% 54% 42%;
|
||||||
|
}
|
||||||
|
@utility avatar-blob-2 {
|
||||||
|
border-radius: 60% 40% 46% 54% / 43% 57% 43% 57%;
|
||||||
|
}
|
||||||
|
@utility avatar-blob-3 {
|
||||||
|
border-radius: 47% 53% 62% 38% / 50% 62% 38% 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Friendly rounded-square for space / relay / room tiles. */
|
||||||
|
@utility squircle {
|
||||||
|
border-radius: 30%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Every DaisyUI button speaks in the rounded display voice and presses in. */
|
||||||
|
.btn {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
.btn {
|
||||||
|
transition:
|
||||||
|
transform 150ms ease,
|
||||||
|
box-shadow 150ms ease,
|
||||||
|
background-color 150ms ease,
|
||||||
|
border-color 150ms ease;
|
||||||
|
}
|
||||||
|
.btn:active {
|
||||||
|
transform: scale(0.96);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Motion vocabulary ---- */
|
||||||
|
@keyframes nav-button-pop {
|
||||||
|
0% {
|
||||||
|
transform: scale(0.9);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes button-pop {
|
||||||
|
0% {
|
||||||
|
transform: scale(0.97);
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes pop {
|
||||||
|
0% {
|
||||||
|
transform: scale(0);
|
||||||
|
}
|
||||||
|
70% {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes reaction-pop {
|
||||||
|
0% {
|
||||||
|
transform: scale(0.6);
|
||||||
|
}
|
||||||
|
60% {
|
||||||
|
transform: scale(1.15);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes float {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateY(-6px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes wiggle {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
25% {
|
||||||
|
transform: rotate(-4deg);
|
||||||
|
}
|
||||||
|
75% {
|
||||||
|
transform: rotate(4deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% {
|
||||||
|
background-position: -200% 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: 200% 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility animate-pop {
|
||||||
|
animation: pop 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
|
}
|
||||||
|
@utility animate-reaction-pop {
|
||||||
|
animation: reaction-pop 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
|
}
|
||||||
|
@utility animate-float {
|
||||||
|
animation: float 6s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
@utility animate-wiggle {
|
||||||
|
animation: wiggle 0.4s ease-in-out;
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import {derived, get} from "svelte/store"
|
|||||||
import {map, not, nthEq, reject, removeUndefined, uniqBy} from "@welshman/lib"
|
import {map, not, nthEq, reject, removeUndefined, uniqBy} from "@welshman/lib"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {makeHttpAuth, makeHttpAuthHeader, getTags} from "@welshman/util"
|
import {makeHttpAuth, makeHttpAuthHeader, getTags} from "@welshman/util"
|
||||||
import {signer} from "@app/welshman"
|
import {signer} from "@welshman/app"
|
||||||
import {load} from "@welshman/net"
|
import {load} from "@welshman/net"
|
||||||
import {getLivekitEndpoint} from "$lib/livekit"
|
import {getLivekitEndpoint} from "$lib/livekit"
|
||||||
import {AbortError, TimeoutError, whenAborted, whenTimeout} from "$lib/util"
|
import {AbortError, TimeoutError, whenAborted, whenTimeout} from "$lib/util"
|
||||||
|
|||||||
+1
-4
@@ -2,8 +2,7 @@ import {DELETE, PROFILE, getPubkeyTagValues} from "@welshman/util"
|
|||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {append, call, on, reject, remove, sort, sortBy, spec, uniq, uniqBy} from "@welshman/lib"
|
import {append, call, on, reject, remove, sort, sortBy, spec, uniq, uniqBy} from "@welshman/lib"
|
||||||
import type {Override} from "@welshman/lib"
|
import type {Override} from "@welshman/lib"
|
||||||
import {Profiles} from "@welshman/app"
|
import {createSearch, displayProfileByPubkey, pubkey, repository} from "@welshman/app"
|
||||||
import {app, createSearch, pubkey, repository} from "@app/welshman"
|
|
||||||
import {derived, readable} from "svelte/store"
|
import {derived, readable} from "svelte/store"
|
||||||
import {DM_KINDS} from "@app/content"
|
import {DM_KINDS} from "@app/content"
|
||||||
import type {RepositoryUpdate} from "@welshman/net"
|
import type {RepositoryUpdate} from "@welshman/net"
|
||||||
@@ -36,8 +35,6 @@ export const chatsById = call(() => {
|
|||||||
const chatsByPubkey = new Map<string, string[]>()
|
const chatsByPubkey = new Map<string, string[]>()
|
||||||
|
|
||||||
const addSearchText = (chat: Override<Chat, {search_text?: string}>) => {
|
const addSearchText = (chat: Override<Chat, {search_text?: string}>) => {
|
||||||
const displayProfileByPubkey = (pk: string) => app.use(Profiles).display(pk).get()
|
|
||||||
|
|
||||||
chat.search_text =
|
chat.search_text =
|
||||||
chat.pubkeys.length === 1
|
chat.pubkeys.length === 1
|
||||||
? displayProfileByPubkey(chat.pubkeys[0]) + " note to self"
|
? displayProfileByPubkey(chat.pubkeys[0]) + " note to self"
|
||||||
|
|||||||
+3
-4
@@ -1,7 +1,6 @@
|
|||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {COMMENT, makeEvent} from "@welshman/util"
|
import {COMMENT, makeEvent} from "@welshman/util"
|
||||||
import {Thunks, Tags} from "@welshman/app"
|
import {publishThunk, tagEventForComment} from "@welshman/app"
|
||||||
import {app} from "@app/welshman"
|
|
||||||
|
|
||||||
export type CommentParams = {
|
export type CommentParams = {
|
||||||
event: TrustedEvent
|
event: TrustedEvent
|
||||||
@@ -11,7 +10,7 @@ export type CommentParams = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const makeComment = ({url, event, content, tags = []}: CommentParams) =>
|
export const makeComment = ({url, event, content, tags = []}: CommentParams) =>
|
||||||
makeEvent(COMMENT, {content, tags: [...tags, ...app.use(Tags).tagEventForComment(event, url)]})
|
makeEvent(COMMENT, {content, tags: [...tags, ...tagEventForComment(event, url)]})
|
||||||
|
|
||||||
export const publishComment = ({relays, ...params}: CommentParams & {relays: string[]}) =>
|
export const publishComment = ({relays, ...params}: CommentParams & {relays: string[]}) =>
|
||||||
app.use(Thunks).publish({event: makeComment({url: relays[0], ...params}), relays})
|
publishThunk({event: makeComment({url: relays[0], ...params}), relays})
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type {Snippet} from "svelte"
|
import type {Snippet} from "svelte"
|
||||||
import {pubkey} from "@app/welshman"
|
import {pubkey} from "@welshman/app"
|
||||||
import Dialog from "@lib/components/Dialog.svelte"
|
import Dialog from "@lib/components/Dialog.svelte"
|
||||||
import Landing from "@app/components/Landing.svelte"
|
import Landing from "@app/components/Landing.svelte"
|
||||||
import Toast from "@app/components/Toast.svelte"
|
import Toast from "@app/components/Toast.svelte"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||||
import {getTagValue, getAddress} from "@welshman/util"
|
import {getTagValue, getAddress} from "@welshman/util"
|
||||||
import {pubkey} from "@app/welshman"
|
import {pubkey} from "@welshman/app"
|
||||||
import Pen2 from "@assets/icons/pen-2.svg?dataurl"
|
import Pen2 from "@assets/icons/pen-2.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
|||||||
@@ -3,8 +3,7 @@
|
|||||||
import {writable} from "svelte/store"
|
import {writable} from "svelte/store"
|
||||||
import {randomId, HOUR} from "@welshman/lib"
|
import {randomId, HOUR} from "@welshman/lib"
|
||||||
import {makeEvent, EVENT_TIME} from "@welshman/util"
|
import {makeEvent, EVENT_TIME} from "@welshman/util"
|
||||||
import {app} from "@app/welshman"
|
import {publishThunk, waitForThunkError} from "@welshman/app"
|
||||||
import {Thunks} from "@welshman/app"
|
|
||||||
import {preventDefault} from "@lib/html"
|
import {preventDefault} from "@lib/html"
|
||||||
import {daysBetween} from "@lib/util"
|
import {daysBetween} from "@lib/util"
|
||||||
import GallerySend from "@assets/icons/gallery-send.svg?dataurl"
|
import GallerySend from "@assets/icons/gallery-send.svg?dataurl"
|
||||||
@@ -108,8 +107,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const event = makeEvent(EVENT_TIME, {content, tags})
|
const event = makeEvent(EVENT_TIME, {content, tags})
|
||||||
const calendarThunk = app.use(Thunks).publish({event, relays: [url]})
|
const calendarThunk = publishThunk({event, relays: [url]})
|
||||||
const error = await calendarThunk.waitForError()
|
const error = await waitForThunkError(calendarThunk)
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return pushToast({theme: "error", message: error})
|
return pushToast({theme: "error", message: error})
|
||||||
|
|||||||
@@ -26,10 +26,14 @@
|
|||||||
DIRECT_MESSAGE,
|
DIRECT_MESSAGE,
|
||||||
DIRECT_MESSAGE_FILE,
|
DIRECT_MESSAGE_FILE,
|
||||||
} from "@welshman/util"
|
} from "@welshman/util"
|
||||||
import {app, pubkey} from "@app/welshman"
|
import {
|
||||||
import {Tags, Wraps, Thunks, MessagingRelayLists} from "@welshman/app"
|
pubkey,
|
||||||
|
tagPubkey,
|
||||||
const messagingRelayListsByPubkey = app.use(MessagingRelayLists).index.$
|
sendWrapped,
|
||||||
|
mergeThunks,
|
||||||
|
loadMessagingRelayList,
|
||||||
|
messagingRelayListsByPubkey,
|
||||||
|
} from "@welshman/app"
|
||||||
import Danger from "@assets/icons/danger-triangle.svg?dataurl"
|
import Danger from "@assets/icons/danger-triangle.svg?dataurl"
|
||||||
import ArrowLeft from "@assets/icons/arrow-left.svg?dataurl"
|
import ArrowLeft from "@assets/icons/arrow-left.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
@@ -92,7 +96,7 @@
|
|||||||
|
|
||||||
const onSubmit = async (params: EventContent) => {
|
const onSubmit = async (params: EventContent) => {
|
||||||
try {
|
try {
|
||||||
const ptags = remove($pubkey!, pubkeys).map(pk => app.use(Tags).tagPubkey(pk))
|
const ptags = remove($pubkey!, pubkeys).map(tagPubkey)
|
||||||
|
|
||||||
// Remove p tags since they result in forking the conversation
|
// Remove p tags since they result in forking the conversation
|
||||||
params.tags = params.tags.filter(nthNe(0, "p"))
|
params.tags = params.tags.filter(nthNe(0, "p"))
|
||||||
@@ -105,7 +109,7 @@
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await app.use(Wraps).publish({
|
await sendWrapped({
|
||||||
event: makeDelete({event: eventToEdit, protect: false}),
|
event: makeDelete({event: eventToEdit, protect: false}),
|
||||||
recipients: pubkeys,
|
recipients: pubkeys,
|
||||||
pow: 16,
|
pow: 16,
|
||||||
@@ -154,7 +158,7 @@
|
|||||||
// Sleep 1 second between each one to make sure timestamps are distinct
|
// Sleep 1 second between each one to make sure timestamps are distinct
|
||||||
const thunks = await Promise.all(
|
const thunks = await Promise.all(
|
||||||
Array.from(enumerate(templates)).map(([i, event]) =>
|
Array.from(enumerate(templates)).map(([i, event]) =>
|
||||||
app.use(Wraps).publish({
|
sendWrapped({
|
||||||
event,
|
event,
|
||||||
recipients: pubkeys,
|
recipients: pubkeys,
|
||||||
delay: $userSettingsValues.send_delay + ms(i),
|
delay: $userSettingsValues.send_delay + ms(i),
|
||||||
@@ -167,7 +171,7 @@
|
|||||||
timeout: 30_000,
|
timeout: 30_000,
|
||||||
children: {
|
children: {
|
||||||
component: ThunkToast,
|
component: ThunkToast,
|
||||||
props: {thunk: app.use(Thunks).merge(thunks)},
|
props: {thunk: mergeThunks(thunks)},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
} finally {
|
} finally {
|
||||||
@@ -230,7 +234,7 @@
|
|||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
for (const pubkey of others) {
|
for (const pubkey of others) {
|
||||||
app.use(MessagingRelayLists).load(pubkey)
|
loadMessagingRelayList(pubkey)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -127,7 +127,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
data-tip="{window.navigator.platform.includes('Mac') ? 'cmd' : 'ctrl'}+enter to send"
|
data-tip="{window.navigator.platform.includes('Mac') ? 'cmd' : 'ctrl'}+enter to send"
|
||||||
class="center tooltip tooltip-left absolute right-4 h-10 w-10 min-w-10 rounded-full"
|
class="center tooltip tooltip-left bg-primary text-primary-content absolute top-[7px] right-[7px] h-11 w-11 min-w-11 scale-90 rounded-full transition-transform motion-safe:hover:scale-100"
|
||||||
disabled={$uploading || disabled}
|
disabled={$uploading || disabled}
|
||||||
onclick={submit}>
|
onclick={submit}>
|
||||||
<Icon icon={Plane} />
|
<Icon icon={Plane} />
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {app} from "@app/welshman"
|
import {displayProfileByPubkey} from "@welshman/app"
|
||||||
import {Profiles} from "@welshman/app"
|
|
||||||
import {slide} from "@lib/transition"
|
import {slide} from "@lib/transition"
|
||||||
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
|
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
@@ -22,7 +21,7 @@
|
|||||||
<div
|
<div
|
||||||
class="relative border-l-2 border-solid border-primary bg-base-300 px-2 py-1 pr-8"
|
class="relative border-l-2 border-solid border-primary bg-base-300 px-2 py-1 pr-8"
|
||||||
transition:slide>
|
transition:slide>
|
||||||
<p class="text-xs text-primary">{verb} @{app.use(Profiles).display(event.pubkey).get()}</p>
|
<p class="text-xs text-primary">{verb} @{displayProfileByPubkey(event.pubkey)}</p>
|
||||||
{#key event.id}
|
{#key event.id}
|
||||||
<NoteContentMinimal trimParent {event} />
|
<NoteContentMinimal trimParent {event} />
|
||||||
{/key}
|
{/key}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {app, userRelayList} from "@app/welshman"
|
import {getRelaysFromList} from "@welshman/util"
|
||||||
import {RelayLists, MessagingRelayLists} from "@welshman/app"
|
import {waitForThunkError, setMessagingRelays, userRelayList, setRelays} from "@welshman/app"
|
||||||
import {preventDefault} from "@lib/html"
|
import {preventDefault} from "@lib/html"
|
||||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
||||||
@@ -29,10 +29,8 @@
|
|||||||
loading = true
|
loading = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (($userRelayList?.urls() ?? []).length === 0) {
|
if (getRelaysFromList($userRelayList).length === 0) {
|
||||||
const error = await (
|
const error = await waitForThunkError(await setRelays(DEFAULT_RELAYS.map(r => ["r", r])))
|
||||||
await app.use(RelayLists).setRelays(DEFAULT_RELAYS.map(r => ["r", r]))
|
|
||||||
).waitForError()
|
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
pushToast({theme: "error", message: error})
|
pushToast({theme: "error", message: error})
|
||||||
@@ -40,9 +38,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const error = await (
|
const error = await waitForThunkError(await setMessagingRelays(DEFAULT_MESSAGING_RELAYS))
|
||||||
await app.use(MessagingRelayLists).setUrls(DEFAULT_MESSAGING_RELAYS)
|
|
||||||
).waitForError()
|
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
pushToast({theme: "error", message: error})
|
pushToast({theme: "error", message: error})
|
||||||
|
|||||||
@@ -3,8 +3,7 @@
|
|||||||
import {page} from "$app/stores"
|
import {page} from "$app/stores"
|
||||||
import {remove, uniq, formatTimestamp} from "@welshman/lib"
|
import {remove, uniq, formatTimestamp} from "@welshman/lib"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {app, pubkey} from "@app/welshman"
|
import {pubkey, loadMessagingRelayList} from "@welshman/app"
|
||||||
import {MessagingRelayLists} from "@welshman/app"
|
|
||||||
import {fade} from "@lib/transition"
|
import {fade} from "@lib/transition"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import ProfileName from "@app/components/ProfileName.svelte"
|
import ProfileName from "@app/components/ProfileName.svelte"
|
||||||
@@ -29,7 +28,7 @@
|
|||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
for (const pk of others) {
|
for (const pk of others) {
|
||||||
app.use(MessagingRelayLists).load(pk)
|
loadMessagingRelayList(pk)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {type Instance} from "tippy.js"
|
import {type Instance} from "tippy.js"
|
||||||
import {hash, formatTimestampAsTime} from "@welshman/lib"
|
import {formatTimestampAsTime} from "@welshman/lib"
|
||||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||||
import {app, thunks, pubkey} from "@app/welshman"
|
import {thunks, mergeThunks, pubkey, deriveProfileDisplay, sendWrapped} from "@welshman/app"
|
||||||
import {Thunks, Profiles, Wraps} from "@welshman/app"
|
|
||||||
import {isMobile} from "@lib/html"
|
import {isMobile} from "@lib/html"
|
||||||
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
|
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
@@ -17,7 +16,7 @@
|
|||||||
import ProfileDetail from "@app/components/ProfileDetail.svelte"
|
import ProfileDetail from "@app/components/ProfileDetail.svelte"
|
||||||
import ChatMessageMenu from "@app/components/ChatMessageMenu.svelte"
|
import ChatMessageMenu from "@app/components/ChatMessageMenu.svelte"
|
||||||
import ChatMessageMenuMobile from "@app/components/ChatMessageMenuMobile.svelte"
|
import ChatMessageMenuMobile from "@app/components/ChatMessageMenuMobile.svelte"
|
||||||
import {colors} from "@app/theme"
|
import {getColor} from "@app/theme"
|
||||||
import {makeDelete} from "@app/deletes"
|
import {makeDelete} from "@app/deletes"
|
||||||
import {makeReaction} from "@app/reactions"
|
import {makeReaction} from "@app/reactions"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
@@ -34,18 +33,18 @@
|
|||||||
const {event, replyTo, canEdit, onEdit, pubkeys, showPubkey = false}: Props = $props()
|
const {event, replyTo, canEdit, onEdit, pubkeys, showPubkey = false}: Props = $props()
|
||||||
|
|
||||||
const isOwn = event.pubkey === $pubkey
|
const isOwn = event.pubkey === $pubkey
|
||||||
const profileDisplay = app.use(Profiles).display(event.pubkey).$
|
const profileDisplay = deriveProfileDisplay(event.pubkey)
|
||||||
const thunk = app.use(Thunks).merge($thunks.filter(t => t.event.id === event.id))
|
const thunk = mergeThunks($thunks.filter(t => t.event.id === event.id))
|
||||||
const [_, colorValue] = colors[hash(event.pubkey) % colors.length]
|
const colorValue = getColor(event.pubkey)
|
||||||
|
|
||||||
const reply = () => replyTo(event)
|
const reply = () => replyTo(event)
|
||||||
const edit = canEdit?.(event) ? () => onEdit?.(event) : undefined
|
const edit = canEdit?.(event) ? () => onEdit?.(event) : undefined
|
||||||
|
|
||||||
const deleteReaction = (event: TrustedEvent) =>
|
const deleteReaction = (event: TrustedEvent) =>
|
||||||
app.use(Wraps).publish({event: makeDelete({event, protect: false}), recipients: pubkeys, pow: 16})
|
sendWrapped({event: makeDelete({event, protect: false}), recipients: pubkeys, pow: 16})
|
||||||
|
|
||||||
const createReaction = (template: EventContent) =>
|
const createReaction = (template: EventContent) =>
|
||||||
app.use(Wraps).publish({
|
sendWrapped({
|
||||||
event: makeReaction({event, protect: false, ...template}),
|
event: makeReaction({event, protect: false, ...template}),
|
||||||
recipients: pubkeys,
|
recipients: pubkeys,
|
||||||
pow: 16,
|
pow: 16,
|
||||||
@@ -102,7 +101,9 @@
|
|||||||
{/if}
|
{/if}
|
||||||
<div class="flex min-w-0 flex-col" class:items-end={isOwn}>
|
<div class="flex min-w-0 flex-col" class:items-end={isOwn}>
|
||||||
<TapTarget
|
<TapTarget
|
||||||
class="bg-alt chat-bubble mx-1 mb-2 flex cursor-auto flex-col gap-1 text-left lg:max-w-2xl min-w-[100px]"
|
class="chat-bubble shadow-soft mx-1 mb-2 flex cursor-auto flex-col gap-1 text-left lg:max-w-2xl min-w-[100px] {isOwn
|
||||||
|
? 'bg-primary text-primary-content'
|
||||||
|
: 'bg-base-100'}"
|
||||||
onTap={showMobileMenu}>
|
onTap={showMobileMenu}>
|
||||||
{#if showPubkey}
|
{#if showPubkey}
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
@@ -110,7 +111,7 @@
|
|||||||
<Button onclick={openProfile} class="flex items-center gap-1">
|
<Button onclick={openProfile} class="flex items-center gap-1">
|
||||||
<ProfileCircle
|
<ProfileCircle
|
||||||
pubkey={event.pubkey}
|
pubkey={event.pubkey}
|
||||||
class="border border-solid border-base-content"
|
style="box-shadow: 0 0 0 1.5px {colorValue}"
|
||||||
size={4} />
|
size={4} />
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<Button onclick={openProfile} class="text-sm font-bold" style="color: {colorValue}">
|
<Button onclick={openProfile} class="text-sm font-bold" style="color: {colorValue}">
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type {NativeEmoji} from "emoji-picker-element/shared"
|
import type {NativeEmoji} from "emoji-picker-element/shared"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {app} from "@app/welshman"
|
import {sendWrapped} from "@welshman/app"
|
||||||
import {Wraps} from "@welshman/app"
|
|
||||||
import SmileCircle from "@assets/icons/smile-circle.svg?dataurl"
|
import SmileCircle from "@assets/icons/smile-circle.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import EmojiButton from "@lib/components/EmojiButton.svelte"
|
import EmojiButton from "@lib/components/EmojiButton.svelte"
|
||||||
@@ -16,7 +15,7 @@
|
|||||||
const {event, pubkeys}: Props = $props()
|
const {event, pubkeys}: Props = $props()
|
||||||
|
|
||||||
const onEmoji = (emoji: NativeEmoji) =>
|
const onEmoji = (emoji: NativeEmoji) =>
|
||||||
app.use(Wraps).publish({
|
sendWrapped({
|
||||||
event: makeReaction({event, content: emoji.unicode, protect: false}),
|
event: makeReaction({event, content: emoji.unicode, protect: false}),
|
||||||
recipients: pubkeys,
|
recipients: pubkeys,
|
||||||
pow: 16,
|
pow: 16,
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type {NativeEmoji} from "emoji-picker-element/shared"
|
import type {NativeEmoji} from "emoji-picker-element/shared"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {app} from "@app/welshman"
|
import {sendWrapped} from "@welshman/app"
|
||||||
import {Wraps} from "@welshman/app"
|
|
||||||
import SmileCircle from "@assets/icons/smile-circle.svg?dataurl"
|
import SmileCircle from "@assets/icons/smile-circle.svg?dataurl"
|
||||||
import Pen from "@assets/icons/pen.svg?dataurl"
|
import Pen from "@assets/icons/pen.svg?dataurl"
|
||||||
import Reply from "@assets/icons/reply-2.svg?dataurl"
|
import Reply from "@assets/icons/reply-2.svg?dataurl"
|
||||||
@@ -29,7 +28,7 @@
|
|||||||
|
|
||||||
const onEmoji = ((event: TrustedEvent, pubkeys: string[], emoji: NativeEmoji) => {
|
const onEmoji = ((event: TrustedEvent, pubkeys: string[], emoji: NativeEmoji) => {
|
||||||
history.back()
|
history.back()
|
||||||
app.use(Wraps).publish({
|
sendWrapped({
|
||||||
event: makeReaction({event, content: emoji.unicode, protect: false}),
|
event: makeReaction({event, content: emoji.unicode, protect: false}),
|
||||||
recipients: pubkeys,
|
recipients: pubkeys,
|
||||||
pow: 16,
|
pow: 16,
|
||||||
|
|||||||
@@ -4,8 +4,7 @@
|
|||||||
import {writable} from "svelte/store"
|
import {writable} from "svelte/store"
|
||||||
import {tryCatch, uniq} from "@welshman/lib"
|
import {tryCatch, uniq} from "@welshman/lib"
|
||||||
import {fromNostrURI} from "@welshman/util"
|
import {fromNostrURI} from "@welshman/util"
|
||||||
import {app} from "@app/welshman"
|
import {loadMessagingRelayList} from "@welshman/app"
|
||||||
import {MessagingRelayLists} from "@welshman/app"
|
|
||||||
import {preventDefault} from "@lib/html"
|
import {preventDefault} from "@lib/html"
|
||||||
import Field from "@lib/components/Field.svelte"
|
import Field from "@lib/components/Field.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
@@ -35,7 +34,7 @@
|
|||||||
let pubkeys: string[] = $state([])
|
let pubkeys: string[] = $state([])
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
pubkeys.forEach(pubkey => app.use(MessagingRelayLists).load(pubkey))
|
pubkeys.forEach(pubkey => loadMessagingRelayList(pubkey))
|
||||||
})
|
})
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import {uniq} from "@welshman/lib"
|
import {uniq} from "@welshman/lib"
|
||||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||||
import {getTagValue, getTagValues, getAddress} from "@welshman/util"
|
import {getTagValue, getTagValues, getAddress} from "@welshman/util"
|
||||||
import {pubkey} from "@app/welshman"
|
import {pubkey} from "@welshman/app"
|
||||||
import Pen2 from "@assets/icons/pen-2.svg?dataurl"
|
import Pen2 from "@assets/icons/pen-2.svg?dataurl"
|
||||||
import {normalizeTopic} from "@lib/util"
|
import {normalizeTopic} from "@lib/util"
|
||||||
import Link from "@lib/components/Link.svelte"
|
import Link from "@lib/components/Link.svelte"
|
||||||
|
|||||||
@@ -2,8 +2,7 @@
|
|||||||
import type {Snippet} from "svelte"
|
import type {Snippet} from "svelte"
|
||||||
import {removeUndefined, randomId, uniq} from "@welshman/lib"
|
import {removeUndefined, randomId, uniq} from "@welshman/lib"
|
||||||
import {makeEvent, CLASSIFIED} from "@welshman/util"
|
import {makeEvent, CLASSIFIED} from "@welshman/util"
|
||||||
import {app} from "@app/welshman"
|
import {publishThunk, waitForThunkError} from "@welshman/app"
|
||||||
import {Thunks} from "@welshman/app"
|
|
||||||
import {isMobile, preventDefault} from "@lib/html"
|
import {isMobile, preventDefault} from "@lib/html"
|
||||||
import {normalizeTopic} from "@lib/util"
|
import {normalizeTopic} from "@lib/util"
|
||||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
@@ -119,12 +118,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const classifiedThunk = app.use(Thunks).publish({
|
const classifiedThunk = publishThunk({
|
||||||
relays: [url],
|
relays: [url],
|
||||||
event: makeEvent(CLASSIFIED, {content, tags}),
|
event: makeEvent(CLASSIFIED, {content, tags}),
|
||||||
})
|
})
|
||||||
|
|
||||||
const error = await classifiedThunk.waitForError()
|
const error = await waitForThunkError(classifiedThunk)
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return pushToast({theme: "error", message: error})
|
return pushToast({theme: "error", message: error})
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
tagsFromIMeta,
|
tagsFromIMeta,
|
||||||
makeBlossomAuthEvent,
|
makeBlossomAuthEvent,
|
||||||
} from "@welshman/util"
|
} from "@welshman/util"
|
||||||
import {signer} from "@app/welshman"
|
import {signer} from "@welshman/app"
|
||||||
import LinkRound from "@assets/icons/link-round.svg?dataurl"
|
import LinkRound from "@assets/icons/link-round.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {removeUndefined} from "@welshman/lib"
|
import {removeUndefined} from "@welshman/lib"
|
||||||
import type {ProfilePointer} from "@welshman/content"
|
import type {ProfilePointer} from "@welshman/content"
|
||||||
import {app} from "@app/welshman"
|
import {deriveProfileDisplay} from "@welshman/app"
|
||||||
import {Profiles} from "@welshman/app"
|
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import ProfileDetail from "@app/components/ProfileDetail.svelte"
|
import ProfileDetail from "@app/components/ProfileDetail.svelte"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
@@ -14,7 +13,7 @@
|
|||||||
|
|
||||||
const {value, url}: Props = $props()
|
const {value, url}: Props = $props()
|
||||||
|
|
||||||
const display = app.use(Profiles).display(value.pubkey, removeUndefined([url])).$
|
const display = deriveProfileDisplay(value.pubkey, removeUndefined([url]))
|
||||||
|
|
||||||
const openProfile = () => pushModal(ProfileDetail, {pubkey: value.pubkey, url})
|
const openProfile = () => pushModal(ProfileDetail, {pubkey: value.pubkey, url})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
import NoteCard from "@app/components/NoteCard.svelte"
|
import NoteCard from "@app/components/NoteCard.svelte"
|
||||||
import NoteContent from "@app/components/NoteContent.svelte"
|
|
||||||
import NoteContentMinimal from "@app/components/NoteContentMinimal.svelte"
|
import NoteContentMinimal from "@app/components/NoteContentMinimal.svelte"
|
||||||
import {deriveEvent} from "@app/repository"
|
import {deriveEvent} from "@app/repository"
|
||||||
import {entityLink} from "@app/env"
|
import {entityLink} from "@app/env"
|
||||||
@@ -44,9 +43,7 @@
|
|||||||
|
|
||||||
<Button class="my-2 block w-full max-w-full text-left" {onclick}>
|
<Button class="my-2 block w-full max-w-full text-left" {onclick}>
|
||||||
{#if $quote}
|
{#if $quote}
|
||||||
{#if $quote.content.trim().match(/^(nostr:)?nevent1[a-z0-9]+$/)}
|
{#if $quote.kind === MESSAGE}
|
||||||
<NoteContent {url} event={$quote} />
|
|
||||||
{:else if $quote.kind === MESSAGE}
|
|
||||||
<div
|
<div
|
||||||
class="border-l-2 border-solid border-l-primary py-1 pl-2 opacity-90"
|
class="border-l-2 border-solid border-l-primary py-1 pl-2 opacity-90"
|
||||||
style="background-color: color-mix(in srgb, var(--color-primary) 10%, var(--color-base-300) 90%);">
|
style="background-color: color-mix(in srgb, var(--color-primary) 10%, var(--color-base-300) 90%);">
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
import type {Writable} from "svelte/store"
|
import type {Writable} from "svelte/store"
|
||||||
import type {Instance} from "tippy.js"
|
import type {Instance} from "tippy.js"
|
||||||
import {preventDefault} from "@lib/html"
|
import {preventDefault} from "@lib/html"
|
||||||
import {createSearch} from "@app/welshman"
|
import {createSearch} from "@welshman/app"
|
||||||
import {currencyOptions, displayCurrency} from "@lib/currency"
|
import {currencyOptions, displayCurrency} from "@lib/currency"
|
||||||
import Suggestions from "@lib/components/Suggestions.svelte"
|
import Suggestions from "@lib/components/Suggestions.svelte"
|
||||||
import CurrencySuggestion from "@app/components/CurrencySuggestion.svelte"
|
import CurrencySuggestion from "@app/components/CurrencySuggestion.svelte"
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type {Snippet} from "svelte"
|
||||||
|
import {fly} from "@lib/transition"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
icon?: string
|
||||||
|
title: string
|
||||||
|
children?: Snippet
|
||||||
|
action?: Snippet
|
||||||
|
}
|
||||||
|
|
||||||
|
const {icon, title, children, action}: Props = $props()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="m-auto flex max-w-sm flex-col items-center gap-3 px-4 py-12 text-center"
|
||||||
|
in:fly={{y: 16}}>
|
||||||
|
{#if icon}
|
||||||
|
<div class="bg-primary/10 text-primary center size-16 rounded-full motion-safe:animate-float">
|
||||||
|
<Icon {icon} size={8} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<h3 class="font-display text-xl font-bold tracking-tight">{title}</h3>
|
||||||
|
{#if children}
|
||||||
|
<p class="text-sm opacity-70">{@render children?.()}</p>
|
||||||
|
{/if}
|
||||||
|
{#if action}
|
||||||
|
<div class="mt-1">{@render action?.()}</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
import {load} from "@welshman/net"
|
import {load} from "@welshman/net"
|
||||||
import {deriveArray, deriveEventsById} from "@welshman/store"
|
import {deriveArray, deriveEventsById} from "@welshman/store"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {repository} from "@app/welshman"
|
import {repository} from "@welshman/app"
|
||||||
import {deriveChecked} from "@app/notifications"
|
import {deriveChecked} from "@app/notifications"
|
||||||
import Reply from "@assets/icons/reply-2.svg?dataurl"
|
import Reply from "@assets/icons/reply-2.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
import {LOCALE, secondsToDate} from "@welshman/lib"
|
import {LOCALE, secondsToDate} from "@welshman/lib"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {displayRelayUrl} from "@welshman/util"
|
import {displayRelayUrl} from "@welshman/util"
|
||||||
import {tracker} from "@app/welshman"
|
import {tracker} from "@welshman/app"
|
||||||
import FileText from "@assets/icons/file-text.svg?dataurl"
|
import FileText from "@assets/icons/file-text.svg?dataurl"
|
||||||
import Copy from "@assets/icons/copy.svg?dataurl"
|
import Copy from "@assets/icons/copy.svg?dataurl"
|
||||||
import UserCircle from "@assets/icons/user-circle.svg?dataurl"
|
import UserCircle from "@assets/icons/user-circle.svg?dataurl"
|
||||||
|
|||||||
@@ -4,8 +4,7 @@
|
|||||||
import {goto} from "$app/navigation"
|
import {goto} from "$app/navigation"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {COMMENT, ManagementMethod} from "@welshman/util"
|
import {COMMENT, ManagementMethod} from "@welshman/util"
|
||||||
import {app, pubkey, repository, relaysByUrl} from "@app/welshman"
|
import {pubkey, repository, relaysByUrl, manageRelay} from "@welshman/app"
|
||||||
import {RelayManagement} from "@welshman/app"
|
|
||||||
import ShareCircle from "@assets/icons/share-circle.svg?dataurl"
|
import ShareCircle from "@assets/icons/share-circle.svg?dataurl"
|
||||||
import Code2 from "@assets/icons/code-2.svg?dataurl"
|
import Code2 from "@assets/icons/code-2.svg?dataurl"
|
||||||
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
|
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
|
||||||
@@ -57,7 +56,7 @@
|
|||||||
title: `Delete ${noun}`,
|
title: `Delete ${noun}`,
|
||||||
message: `Are you sure you want to delete this ${noun.toLowerCase()} from the space?`,
|
message: `Are you sure you want to delete this ${noun.toLowerCase()} from the space?`,
|
||||||
confirm: async () => {
|
confirm: async () => {
|
||||||
const {error} = await app.use(RelayManagement).post(url, {
|
const {error} = await manageRelay(url, {
|
||||||
method: ManagementMethod.BanEvent,
|
method: ManagementMethod.BanEvent,
|
||||||
params: [event.id],
|
params: [event.id],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import {writable} from "svelte/store"
|
import {writable} from "svelte/store"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
|
||||||
import {isMobile, preventDefault} from "@lib/html"
|
import {isMobile, preventDefault} from "@lib/html"
|
||||||
import {fly} from "@lib/transition"
|
import {fly} from "@lib/transition"
|
||||||
import Paperclip from "@assets/icons/paperclip-2.svg?dataurl"
|
import Paperclip from "@assets/icons/paperclip-2.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import EditorContent from "@app/editor/EditorContent.svelte"
|
import EditorContent from "@app/editor/EditorContent.svelte"
|
||||||
import ChatComposeParent from "@app/components/ChatComposeParent.svelte"
|
|
||||||
import {publishComment} from "@app/comments"
|
import {publishComment} from "@app/comments"
|
||||||
import {canEnforceNip70} from "@app/relays"
|
import {canEnforceNip70} from "@app/relays"
|
||||||
import {PROTECTED, prependParent} from "@app/groups"
|
import {PROTECTED} from "@app/groups"
|
||||||
import {makeEditor} from "@app/editor"
|
import {makeEditor} from "@app/editor"
|
||||||
import {DraftKey} from "@app/drafts"
|
import {DraftKey} from "@app/drafts"
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/toast"
|
||||||
@@ -20,17 +18,8 @@
|
|||||||
content?: string | object
|
content?: string | object
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = {
|
const {url, event, onClose, onSubmit} = $props()
|
||||||
url: string
|
const draftKey = new DraftKey<Values>(`reply:${event.id}`)
|
||||||
event: TrustedEvent
|
|
||||||
parent?: TrustedEvent
|
|
||||||
onClose: () => void
|
|
||||||
onClearParent?: () => void
|
|
||||||
onSubmit: (thunk: unknown) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const {url, event, parent, onClose, onClearParent, onSubmit}: Props = $props()
|
|
||||||
const draftKey = new DraftKey<Values>(`reply:${event.id}:${parent?.id || ""}`)
|
|
||||||
const initialValues = draftKey.get()
|
const initialValues = draftKey.get()
|
||||||
const shouldProtect = canEnforceNip70(url)
|
const shouldProtect = canEnforceNip70(url)
|
||||||
const uploading = writable(false)
|
const uploading = writable(false)
|
||||||
@@ -42,8 +31,8 @@
|
|||||||
if ($uploading) return
|
if ($uploading) return
|
||||||
|
|
||||||
const ed = await editor
|
const ed = await editor
|
||||||
let content = ed.getText({blockSeparator: "\n"}).trim()
|
const content = ed.getText({blockSeparator: "\n"}).trim()
|
||||||
let tags = ed.storage.nostr.getEditorTags()
|
const tags = ed.storage.nostr.getEditorTags()
|
||||||
|
|
||||||
if (await shouldProtect) {
|
if (await shouldProtect) {
|
||||||
tags.push(PROTECTED)
|
tags.push(PROTECTED)
|
||||||
@@ -56,10 +45,6 @@
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parent) {
|
|
||||||
;({content, tags} = prependParent(parent, {content, tags}, url))
|
|
||||||
}
|
|
||||||
|
|
||||||
draftKey.clear()
|
draftKey.clear()
|
||||||
onSubmit(publishComment({event, content, tags, relays: [url]}))
|
onSubmit(publishComment({event, content, tags, relays: [url]}))
|
||||||
}
|
}
|
||||||
@@ -102,9 +87,6 @@
|
|||||||
onsubmit={preventDefault(submit)}
|
onsubmit={preventDefault(submit)}
|
||||||
class="left-content bottom-sai right-sai fixed z-feature mb-14 md:mb-0 w-full md:w-auto pr-2">
|
class="left-content bottom-sai right-sai fixed z-feature mb-14 md:mb-0 w-full md:w-auto pr-2">
|
||||||
<div class="card2 mx-2 my-2 bg-alt shadow-md">
|
<div class="card2 mx-2 my-2 bg-alt shadow-md">
|
||||||
{#if parent}
|
|
||||||
<ChatComposeParent event={parent} clear={() => onClearParent?.()} verb="Replying to" />
|
|
||||||
{/if}
|
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="note-editor grow overflow-hidden">
|
<div class="note-editor grow overflow-hidden">
|
||||||
<EditorContent {autofocus} {editor} />
|
<EditorContent {autofocus} {editor} />
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {writable} from "svelte/store"
|
import {writable} from "svelte/store"
|
||||||
import {makeEvent, ZAP_GOAL} from "@welshman/util"
|
import {makeEvent, ZAP_GOAL} from "@welshman/util"
|
||||||
import {app} from "@app/welshman"
|
import {publishThunk, waitForThunkError} from "@welshman/app"
|
||||||
import {Thunks} from "@welshman/app"
|
|
||||||
import {isMobile, preventDefault} from "@lib/html"
|
import {isMobile, preventDefault} from "@lib/html"
|
||||||
import Paperclip from "@assets/icons/paperclip-2.svg?dataurl"
|
import Paperclip from "@assets/icons/paperclip-2.svg?dataurl"
|
||||||
import Bolt from "@assets/icons/bolt.svg?dataurl"
|
import Bolt from "@assets/icons/bolt.svg?dataurl"
|
||||||
@@ -94,12 +93,12 @@
|
|||||||
tags.push(["h", h])
|
tags.push(["h", h])
|
||||||
}
|
}
|
||||||
|
|
||||||
const goalThunk = app.use(Thunks).publish({
|
const goalThunk = publishThunk({
|
||||||
relays: [url],
|
relays: [url],
|
||||||
event: makeEvent(ZAP_GOAL, {content: title, tags}),
|
event: makeEvent(ZAP_GOAL, {content: title, tags}),
|
||||||
})
|
})
|
||||||
|
|
||||||
const error = await goalThunk.waitForError()
|
const error = await waitForThunkError(goalThunk)
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return pushToast({theme: "error", message: error})
|
return pushToast({theme: "error", message: error})
|
||||||
|
|||||||
@@ -2,10 +2,8 @@
|
|||||||
import {now, DAY, uniq, sum} from "@welshman/lib"
|
import {now, DAY, uniq, sum} from "@welshman/lib"
|
||||||
import type {Zap, TrustedEvent} from "@welshman/util"
|
import type {Zap, TrustedEvent} from "@welshman/util"
|
||||||
import {getTagValue, fromMsats, ZAP_RESPONSE} from "@welshman/util"
|
import {getTagValue, fromMsats, ZAP_RESPONSE} from "@welshman/util"
|
||||||
import {derived} from "svelte/store"
|
import {deriveItemsByKey, deriveArray} from "@welshman/store"
|
||||||
import {deriveEvents} from "@welshman/store"
|
import {repository, getValidZap} from "@welshman/app"
|
||||||
import {app, repository} from "@app/welshman"
|
|
||||||
import {Zappers} from "@welshman/app"
|
|
||||||
import Bolt from "@assets/icons/bolt.svg?dataurl"
|
import Bolt from "@assets/icons/bolt.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import ZapButton from "@app/components/ZapButton.svelte"
|
import ZapButton from "@app/components/ZapButton.svelte"
|
||||||
@@ -18,15 +16,13 @@
|
|||||||
|
|
||||||
const {url, event, ...props}: Props = $props()
|
const {url, event, ...props}: Props = $props()
|
||||||
|
|
||||||
// Validated zaps for this goal. `validZapReceipts` is a reactive Projection
|
const zaps = deriveArray(
|
||||||
// (resolves recipient zappers from loaded profiles); we re-derive it whenever
|
deriveItemsByKey<Zap>({
|
||||||
// the set of ZAP_RESPONSE events in the repository changes.
|
repository,
|
||||||
const zapReceipts = deriveEvents({repository, filters: [{kinds: [ZAP_RESPONSE], "#e": [event.id]}]})
|
getKey: zap => zap.response.id,
|
||||||
const zaps = derived(
|
filters: [{kinds: [ZAP_RESPONSE], "#e": [event.id]}],
|
||||||
zapReceipts,
|
eventToItem: (response: TrustedEvent) => getValidZap(response, event),
|
||||||
($receipts: TrustedEvent[], set) =>
|
}),
|
||||||
app.use(Zappers).validZapReceipts($receipts, event).$.subscribe(set),
|
|
||||||
[] as Zap[],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const goalAmount = parseInt(getTagValue("amount", event.tags) || "0")
|
const goalAmount = parseInt(getTagValue("amount", event.tags) || "0")
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {createSearch} from "@app/welshman"
|
import {createSearch} from "@welshman/app"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
|
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {session} from "@app/welshman"
|
import {session} from "@welshman/app"
|
||||||
import Link from "@lib/components/Link.svelte"
|
import Link from "@lib/components/Link.svelte"
|
||||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
import CheckCircle from "@assets/icons/check-circle.svg?dataurl"
|
import CheckCircle from "@assets/icons/check-circle.svg?dataurl"
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {app} from "@app/welshman"
|
import {deriveZapperForPubkey} from "@welshman/app"
|
||||||
import {Zappers} from "@welshman/app"
|
|
||||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
@@ -13,7 +12,7 @@
|
|||||||
|
|
||||||
const {pubkey} = $props()
|
const {pubkey} = $props()
|
||||||
|
|
||||||
const zapper = app.use(Zappers).forPubkey(pubkey).$
|
const zapper = deriveZapperForPubkey(pubkey)
|
||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -80,8 +80,9 @@
|
|||||||
for={id}
|
for={id}
|
||||||
aria-label="Drag and drop files here."
|
aria-label="Drag and drop files here."
|
||||||
style="background-image: url({url});"
|
style="background-image: url({url});"
|
||||||
class="relative flex h-24 w-24 shrink-0 cursor-pointer items-center justify-center rounded-full border-2 border-solid border-base-content bg-base-300 bg-cover bg-center transition-all"
|
class="avatar-blob relative flex h-24 w-24 shrink-0 cursor-pointer items-center justify-center border-2 border-dashed border-primary/40 bg-base-300 bg-cover bg-center transition-all motion-safe:hover:rotate-1 motion-safe:hover:scale-[1.02]"
|
||||||
class:transparent={!url}
|
class:transparent={!url}
|
||||||
|
class:border-solid={url || active}
|
||||||
class:border-primary={active}
|
class:border-primary={active}
|
||||||
ondragenter={stopPropagation(preventDefault(onDragEnter))}
|
ondragenter={stopPropagation(preventDefault(onDragEnter))}
|
||||||
ondragover={stopPropagation(preventDefault(onDragOver))}
|
ondragover={stopPropagation(preventDefault(onDragOver))}
|
||||||
|
|||||||
@@ -1,14 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {Client} from "@pomade/core"
|
import {Client} from "@pomade/core"
|
||||||
import {getPubkey} from "@welshman/util"
|
import {getPubkey} from "@welshman/util"
|
||||||
import {session} from "@app/welshman"
|
import type {SessionPomade} from "@welshman/app"
|
||||||
// TODO(welshman-migration): `SessionPomade` was removed from @welshman/app; the
|
import {session} from "@welshman/app"
|
||||||
// new generic Session nests its descriptor under `.data` ({clientOptions, email}),
|
|
||||||
// whereas this code destructures `email`/`clientOptions` from the top level.
|
|
||||||
// Confirm whether flotilla's FlotillaSession still surfaces these at the top
|
|
||||||
// level (loginWithPomade stores them under `.data` via toSession) and adjust the
|
|
||||||
// destructuring accordingly. Local type kept loose to avoid a broken import.
|
|
||||||
type SessionPomade = {email: string; clientOptions: {secret: string; peers: any}}
|
|
||||||
import {preventDefault} from "@lib/html"
|
import {preventDefault} from "@lib/html"
|
||||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
||||||
|
|||||||
@@ -1,12 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {Client} from "@pomade/core"
|
import {Client} from "@pomade/core"
|
||||||
import {session} from "@app/welshman"
|
import type {SessionPomade} from "@welshman/app"
|
||||||
// TODO(welshman-migration): `SessionPomade` was removed from @welshman/app; the
|
import {session} from "@welshman/app"
|
||||||
// new generic Session nests its descriptor under `.data` ({clientOptions, email}),
|
|
||||||
// whereas this code destructures from the top level. Confirm whether flotilla's
|
|
||||||
// FlotillaSession still surfaces these at the top level and adjust accordingly.
|
|
||||||
// Local type kept loose to avoid a broken import.
|
|
||||||
type SessionPomade = {email: string; clientOptions: {peers: any}}
|
|
||||||
import {preventDefault} from "@lib/html"
|
import {preventDefault} from "@lib/html"
|
||||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
import CardButton from "@lib/components/CardButton.svelte"
|
import CardButton from "@lib/components/CardButton.svelte"
|
||||||
import LogIn from "@app/components/LogIn.svelte"
|
import LogIn from "@app/components/LogIn.svelte"
|
||||||
import SignUp from "@app/components/SignUp.svelte"
|
import SignUp from "@app/components/SignUp.svelte"
|
||||||
import {PLATFORM_TERMS, PLATFORM_PRIVACY, PLATFORM_NAME} from "@app/env"
|
import {PLATFORM_TERMS, PLATFORM_PRIVACY, PLATFORM_NAME, PLATFORM_LOGO} from "@app/env"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
|
|
||||||
const logIn = () => pushModal(LogIn)
|
const logIn = () => pushModal(LogIn)
|
||||||
@@ -19,9 +19,15 @@
|
|||||||
|
|
||||||
<Modal>
|
<Modal>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<div class="py-2">
|
<div class="flex flex-col items-center gap-3 py-2">
|
||||||
<h1 class="heading">Welcome to {PLATFORM_NAME}!</h1>
|
<img
|
||||||
<p class="text-center">The chat app built for self-hosted communities.</p>
|
src={PLATFORM_LOGO}
|
||||||
|
alt={PLATFORM_NAME}
|
||||||
|
class="shadow-soft ring-primary/20 size-16 rounded-2xl object-cover ring-4 motion-safe:animate-float" />
|
||||||
|
<h1 class="heading">Welcome to <span class="brand">{PLATFORM_NAME}</span>!</h1>
|
||||||
|
<p class="max-w-sm text-center opacity-80">
|
||||||
|
A cozy home for your community — chat, connect, and own your little corner of the internet.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button onclick={logIn}>
|
<Button onclick={logIn}>
|
||||||
<CardButton class="btn-primary">
|
<CardButton class="btn-primary">
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import {Capacitor} from "@capacitor/core"
|
import {Capacitor} from "@capacitor/core"
|
||||||
import {getNip07, getNip55, Nip55Signer} from "@welshman/signer"
|
import {getNip07, getNip55, Nip55Signer} from "@welshman/signer"
|
||||||
import {addSession, type FlotillaSession, makeNip07Session, makeNip55Session} from "@app/welshman"
|
import {addSession, type Session, makeNip07Session, makeNip55Session} from "@welshman/app"
|
||||||
import Widget from "@assets/icons/widget-4.svg?dataurl"
|
import Widget from "@assets/icons/widget-4.svg?dataurl"
|
||||||
import Letter from "@assets/icons/letter.svg?dataurl"
|
import Letter from "@assets/icons/letter.svg?dataurl"
|
||||||
import Cpu from "@assets/icons/cpu-bolt.svg?dataurl"
|
import Cpu from "@assets/icons/cpu-bolt.svg?dataurl"
|
||||||
@@ -32,8 +32,8 @@
|
|||||||
|
|
||||||
const signUp = () => pushModal(SignUp)
|
const signUp = () => pushModal(SignUp)
|
||||||
|
|
||||||
const onSuccess = async (session: FlotillaSession, pubkey: string) => {
|
const onSuccess = async (session: Session) => {
|
||||||
addSession({...session, pubkey})
|
addSession(session)
|
||||||
setChecked("*")
|
setChecked("*")
|
||||||
clearModals()
|
clearModals()
|
||||||
}
|
}
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
const pubkey = await getNip07()?.getPublicKey()
|
const pubkey = await getNip07()?.getPublicKey()
|
||||||
|
|
||||||
if (pubkey) {
|
if (pubkey) {
|
||||||
await onSuccess(makeNip07Session(pubkey), pubkey)
|
await onSuccess(makeNip07Session(pubkey))
|
||||||
} else {
|
} else {
|
||||||
pushToast({
|
pushToast({
|
||||||
theme: "error",
|
theme: "error",
|
||||||
@@ -65,7 +65,7 @@
|
|||||||
const pubkey = await signer.getPubkey()
|
const pubkey = await signer.getPubkey()
|
||||||
|
|
||||||
if (pubkey) {
|
if (pubkey) {
|
||||||
await onSuccess(makeNip55Session(pubkey, app.packageName), pubkey)
|
await onSuccess(makeNip55Session(pubkey, app.packageName))
|
||||||
} else {
|
} else {
|
||||||
pushToast({
|
pushToast({
|
||||||
theme: "error",
|
theme: "error",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
import type {Nip46ResponseWithResult} from "@welshman/signer"
|
import type {Nip46ResponseWithResult} from "@welshman/signer"
|
||||||
import {Nip46Broker} from "@welshman/signer"
|
import {Nip46Broker} from "@welshman/signer"
|
||||||
import {makeSecret} from "@welshman/util"
|
import {makeSecret} from "@welshman/util"
|
||||||
import {loginWithNip01, loginWithNip46} from "@app/welshman"
|
import {loginWithNip01, loginWithNip46} from "@welshman/app"
|
||||||
import {preventDefault} from "@lib/html"
|
import {preventDefault} from "@lib/html"
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {bytesToHex} from "@welshman/lib"
|
import {bytesToHex} from "@welshman/lib"
|
||||||
import {loginWithNip01} from "@app/welshman"
|
import {loginWithNip01} from "@welshman/app"
|
||||||
import {decrypt} from "nostr-tools/nip49"
|
import {decrypt} from "nostr-tools/nip49"
|
||||||
import {preventDefault} from "@lib/html"
|
import {preventDefault} from "@lib/html"
|
||||||
import {nsecDecode} from "@lib/util"
|
import {nsecDecode} from "@lib/util"
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
await loginWithNip01(secret)
|
loginWithNip01(secret)
|
||||||
setChecked("*")
|
setChecked("*")
|
||||||
clearModals()
|
clearModals()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -76,7 +76,7 @@
|
|||||||
onclick={() => selectAccount(option)}
|
onclick={() => selectAccount(option)}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
class="card2 bg-alt flex w-full items-center p-3 text-left">
|
class="card2 bg-alt flex w-full items-center p-3 text-left">
|
||||||
<Profile inert pubkey={option.pubkey} />
|
<Profile pubkey={option.pubkey} />
|
||||||
</Button>
|
</Button>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {Capacitor} from "@capacitor/core"
|
import {Capacitor} from "@capacitor/core"
|
||||||
import {pubkey} from "@app/welshman"
|
import {pubkey} from "@welshman/app"
|
||||||
import Server from "@assets/icons/server.svg?dataurl"
|
import Server from "@assets/icons/server.svg?dataurl"
|
||||||
import GalleryMinimalistic from "@assets/icons/gallery-minimalistic.svg?dataurl"
|
import GalleryMinimalistic from "@assets/icons/gallery-minimalistic.svg?dataurl"
|
||||||
import Shield from "@assets/icons/shield-minimalistic.svg?dataurl"
|
import Shield from "@assets/icons/shield-minimalistic.svg?dataurl"
|
||||||
|
|||||||
@@ -3,10 +3,8 @@
|
|||||||
import {sum} from "@welshman/lib"
|
import {sum} from "@welshman/lib"
|
||||||
import type {Zap, TrustedEvent} from "@welshman/util"
|
import type {Zap, TrustedEvent} from "@welshman/util"
|
||||||
import {getTagValue, fromMsats, ZAP_RESPONSE} from "@welshman/util"
|
import {getTagValue, fromMsats, ZAP_RESPONSE} from "@welshman/util"
|
||||||
import {derived} from "svelte/store"
|
import {deriveItemsByKey, deriveArray} from "@welshman/store"
|
||||||
import {deriveEvents} from "@welshman/store"
|
import {repository, getValidZap} from "@welshman/app"
|
||||||
import {app, repository} from "@app/welshman"
|
|
||||||
import {Zappers} from "@welshman/app"
|
|
||||||
import Bolt from "@assets/icons/bolt.svg?dataurl"
|
import Bolt from "@assets/icons/bolt.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import ContentMinimal from "@app/components/ContentMinimal.svelte"
|
import ContentMinimal from "@app/components/ContentMinimal.svelte"
|
||||||
@@ -16,14 +14,13 @@
|
|||||||
const content = getTagValue("summary", props.event.tags)
|
const content = getTagValue("summary", props.event.tags)
|
||||||
const fakeEvent = {content, tags: props.event.tags}
|
const fakeEvent = {content, tags: props.event.tags}
|
||||||
|
|
||||||
// Validated zaps for this goal (reactive Projection, re-derived as the set of
|
const zaps = deriveArray(
|
||||||
// ZAP_RESPONSE events in the repository changes).
|
deriveItemsByKey<Zap>({
|
||||||
const zapReceipts = deriveEvents({repository, filters: [{kinds: [ZAP_RESPONSE], "#e": [props.event.id]}]})
|
repository,
|
||||||
const zaps = derived(
|
getKey: zap => zap.response.id,
|
||||||
zapReceipts,
|
filters: [{kinds: [ZAP_RESPONSE], "#e": [props.event.id]}],
|
||||||
($receipts: TrustedEvent[], set) =>
|
eventToItem: (response: TrustedEvent) => getValidZap(response, props.event),
|
||||||
app.use(Zappers).validZapReceipts($receipts, props.event).$.subscribe(set),
|
}),
|
||||||
[] as Zap[],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const goalAmount = parseInt(getTagValue("amount", props.event.tags) || "0")
|
const goalAmount = parseInt(getTagValue("amount", props.event.tags) || "0")
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {Client} from "@pomade/core"
|
import {Client} from "@pomade/core"
|
||||||
import type {SessionPomade} from "@welshman/app"
|
import type {SessionPomade} from "@welshman/app"
|
||||||
import {session} from "@app/welshman"
|
import {session} from "@welshman/app"
|
||||||
import {preventDefault} from "@lib/html"
|
import {preventDefault} from "@lib/html"
|
||||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
||||||
|
|||||||
@@ -1,12 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {Client} from "@pomade/core"
|
import {Client} from "@pomade/core"
|
||||||
import {session} from "@app/welshman"
|
import {session} from "@welshman/app"
|
||||||
// TODO(welshman-migration): `SessionPomade` was removed from @welshman/app; the
|
import type {SessionPomade} from "@welshman/app"
|
||||||
// new generic Session nests its descriptor under `.data` ({clientOptions, email}),
|
|
||||||
// whereas this code destructures `email` from the top level. Confirm whether
|
|
||||||
// flotilla's FlotillaSession still surfaces `email` at the top level and adjust.
|
|
||||||
// Local type kept loose to avoid a broken import.
|
|
||||||
type SessionPomade = {email: string}
|
|
||||||
import {preventDefault} from "@lib/html"
|
import {preventDefault} from "@lib/html"
|
||||||
import Key from "@assets/icons/key.svg?dataurl"
|
import Key from "@assets/icons/key.svg?dataurl"
|
||||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {insertAt, now, randomId, removeAt, removeUndefined} from "@welshman/lib"
|
import {insertAt, now, randomId, removeAt, removeUndefined} from "@welshman/lib"
|
||||||
import {makeEvent, POLL} from "@welshman/util"
|
import {makeEvent, POLL} from "@welshman/util"
|
||||||
import {Thunks} from "@welshman/app"
|
import {publishThunk, waitForThunkError} from "@welshman/app"
|
||||||
import {app} from "@app/welshman"
|
|
||||||
import {isMobile, preventDefault} from "@lib/html"
|
import {isMobile, preventDefault} from "@lib/html"
|
||||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
import HamburgerMenu from "@assets/icons/hamburger-menu.svg?dataurl"
|
import HamburgerMenu from "@assets/icons/hamburger-menu.svg?dataurl"
|
||||||
@@ -143,12 +142,12 @@
|
|||||||
tags.push(PROTECTED)
|
tags.push(PROTECTED)
|
||||||
}
|
}
|
||||||
|
|
||||||
const pollThunk = app.use(Thunks).publish({
|
const pollThunk = publishThunk({
|
||||||
relays: [url],
|
relays: [url],
|
||||||
event: makeEvent(POLL, {content: title.trim(), tags}),
|
event: makeEvent(POLL, {content: title.trim(), tags}),
|
||||||
})
|
})
|
||||||
|
|
||||||
const error = await pollThunk.waitForError()
|
const error = await waitForThunkError(pollThunk)
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return pushToast({theme: "error", message: error})
|
return pushToast({theme: "error", message: error})
|
||||||
|
|||||||
@@ -2,9 +2,7 @@
|
|||||||
import {onDestroy} from "svelte"
|
import {onDestroy} from "svelte"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {POLL_RESPONSE} from "@welshman/util"
|
import {POLL_RESPONSE} from "@welshman/util"
|
||||||
import type {Thunk} from "@welshman/app"
|
import {pubkey, publishThunk, abortThunk} from "@welshman/app"
|
||||||
import {Thunks} from "@welshman/app"
|
|
||||||
import {pubkey, app} from "@app/welshman"
|
|
||||||
import {formatTimestampRelative} from "@welshman/lib"
|
import {formatTimestampRelative} from "@welshman/lib"
|
||||||
import {deriveEvents} from "@app/repository"
|
import {deriveEvents} from "@app/repository"
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/toast"
|
||||||
@@ -52,7 +50,7 @@
|
|||||||
|
|
||||||
const publishSelection = (selection: string[]) => {
|
const publishSelection = (selection: string[]) => {
|
||||||
if (activeThunk) {
|
if (activeThunk) {
|
||||||
activeThunk.abort()
|
abortThunk(activeThunk)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selection.length === 0) {
|
if (selection.length === 0) {
|
||||||
@@ -60,7 +58,7 @@
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
activeThunk = app.use(Thunks).publish({
|
activeThunk = publishThunk({
|
||||||
relays: [url],
|
relays: [url],
|
||||||
event: makePollResponse({event, selectedIds: selection}),
|
event: makePollResponse({event, selectedIds: selection}),
|
||||||
delay: publishDelay,
|
delay: publishDelay,
|
||||||
@@ -94,7 +92,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
let selectedIds = $state<string[]>([])
|
let selectedIds = $state<string[]>([])
|
||||||
let activeThunk: Thunk | undefined
|
let activeThunk: ReturnType<typeof publishThunk> | undefined
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (ownResponse) {
|
if (ownResponse) {
|
||||||
@@ -104,7 +102,7 @@
|
|||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
if (activeThunk) {
|
if (activeThunk) {
|
||||||
activeThunk.abort()
|
abortThunk(activeThunk)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import {Client} from "@pomade/core"
|
import {Client} from "@pomade/core"
|
||||||
import {session, isPomadeSession} from "@app/welshman"
|
import {session, isPomadeSession} from "@welshman/app"
|
||||||
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
|
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
|
||||||
import {fly} from "@lib/transition"
|
import {fly} from "@lib/transition"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type {Snippet} from "svelte"
|
import type {Snippet} from "svelte"
|
||||||
import {userProfile} from "@app/welshman"
|
import {userProfile} from "@welshman/app"
|
||||||
import Letter from "@assets/icons/letter.svg?dataurl"
|
import Letter from "@assets/icons/letter.svg?dataurl"
|
||||||
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
|
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
|
||||||
import Widget from "@assets/icons/widget-4.svg?dataurl"
|
import Widget from "@assets/icons/widget-4.svg?dataurl"
|
||||||
@@ -41,8 +41,8 @@
|
|||||||
{/if}
|
{/if}
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<PrimaryNavItem title="Settings" href="/settings/profile" prefix="/settings">
|
<PrimaryNavItem title="Settings" href="/settings/profile" prefix="/settings">
|
||||||
{#if $userProfile?.picture()}
|
{#if $userProfile?.picture}
|
||||||
<ImageIcon alt="Settings" src={$userProfile?.picture()} class="rounded-full" size={10} />
|
<ImageIcon alt="Settings" src={$userProfile?.picture} class="rounded-full" size={10} />
|
||||||
{:else}
|
{:else}
|
||||||
<ImageIcon alt="Settings" src={UserRounded} class="rounded-full" size={8} />
|
<ImageIcon alt="Settings" src={UserRounded} class="rounded-full" size={8} />
|
||||||
{/if}
|
{/if}
|
||||||
@@ -86,8 +86,8 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<PrimaryNavItem title="Settings" onclick={showSettingsMenu}>
|
<PrimaryNavItem title="Settings" onclick={showSettingsMenu}>
|
||||||
{#if $userProfile?.picture()}
|
{#if $userProfile?.picture}
|
||||||
<ImageIcon alt="Settings" src={$userProfile?.picture()} size={10} class="rounded-full" />
|
<ImageIcon alt="Settings" src={$userProfile?.picture} size={10} class="rounded-full" />
|
||||||
{:else}
|
{:else}
|
||||||
<ImageIcon alt="Settings" src={Settings} size={8} class="rounded-full" />
|
<ImageIcon alt="Settings" src={Settings} size={8} class="rounded-full" />
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {Relays} from "@welshman/app"
|
import {deriveRelayDisplay} from "@welshman/app"
|
||||||
import {app} from "@app/welshman"
|
|
||||||
import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte"
|
import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte"
|
||||||
import RelayIcon from "@app/components/RelayIcon.svelte"
|
import RelayIcon from "@app/components/RelayIcon.svelte"
|
||||||
import {makeSpacePath, goToSpace} from "@app/routes"
|
import {makeSpacePath, goToSpace} from "@app/routes"
|
||||||
@@ -16,7 +15,7 @@
|
|||||||
|
|
||||||
const path = makeSpacePath(url)
|
const path = makeSpacePath(url)
|
||||||
|
|
||||||
const display = $derived(app.use(Relays).display(url).$)
|
const display = $derived(deriveRelayDisplay(url))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<PrimaryNavItem
|
<PrimaryNavItem
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as nip19 from "nostr-tools/nip19"
|
import * as nip19 from "nostr-tools/nip19"
|
||||||
import {removeUndefined} from "@welshman/lib"
|
import {removeUndefined} from "@welshman/lib"
|
||||||
import {displayPubkey, displayNip05} from "@welshman/util"
|
import {displayPubkey} from "@welshman/util"
|
||||||
import {Handles, Profiles} from "@welshman/app"
|
import {deriveHandleForPubkey, displayHandle, deriveProfileDisplay} from "@welshman/app"
|
||||||
import {app} from "@app/welshman"
|
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
||||||
@@ -24,8 +23,8 @@
|
|||||||
const {pubkey, url, showPubkey, inert, avatarSize = 10}: Props = $props()
|
const {pubkey, url, showPubkey, inert, avatarSize = 10}: Props = $props()
|
||||||
|
|
||||||
const relays = removeUndefined([url])
|
const relays = removeUndefined([url])
|
||||||
const profileDisplay = app.use(Profiles).display(pubkey, relays).$
|
const profileDisplay = deriveProfileDisplay(pubkey, relays)
|
||||||
const handle = app.use(Handles).forPubkey(pubkey).$
|
const handle = deriveHandleForPubkey(pubkey)
|
||||||
|
|
||||||
const openProfile = () => {
|
const openProfile = () => {
|
||||||
pushModal(ProfileDetail, {pubkey, url})
|
pushModal(ProfileDetail, {pubkey, url})
|
||||||
@@ -59,7 +58,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{#if $handle}
|
{#if $handle}
|
||||||
<div class="overflow-hidden text-ellipsis text-sm opacity-75">
|
<div class="overflow-hidden text-ellipsis text-sm opacity-75">
|
||||||
{displayNip05($handle?.nip05)}
|
{displayHandle($handle)}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if showPubkey}
|
{#if showPubkey}
|
||||||
|
|||||||
@@ -6,8 +6,7 @@
|
|||||||
import {deriveEventsDesc, deriveEventsById} from "@welshman/store"
|
import {deriveEventsDesc, deriveEventsById} from "@welshman/store"
|
||||||
import {formatTimestampRelative} from "@welshman/lib"
|
import {formatTimestampRelative} from "@welshman/lib"
|
||||||
import {NOTE, ROOMS, COMMENT, MESSAGE} from "@welshman/util"
|
import {NOTE, ROOMS, COMMENT, MESSAGE} from "@welshman/util"
|
||||||
import {RelayLists} from "@welshman/app"
|
import {repository, loadRelayList} from "@welshman/app"
|
||||||
import {repository, app} from "@app/welshman"
|
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import ProfileSpaces from "@app/components/ProfileSpaces.svelte"
|
import ProfileSpaces from "@app/components/ProfileSpaces.svelte"
|
||||||
import {deriveGroupList, getSpaceUrlsFromGroupList} from "@app/groups"
|
import {deriveGroupList, getSpaceUrlsFromGroupList} from "@app/groups"
|
||||||
@@ -31,7 +30,7 @@
|
|||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
// Make sure we have their relay selections before we load their posts
|
// Make sure we have their relay selections before we load their posts
|
||||||
await app.use(RelayLists).load(pubkey)
|
await loadRelayList(pubkey)
|
||||||
|
|
||||||
// Load groups and at least one note, regardless of time frame
|
// Load groups and at least one note, regardless of time frame
|
||||||
load({
|
load({
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import cx from "classnames"
|
import cx from "classnames"
|
||||||
import {removeUndefined} from "@welshman/lib"
|
import {removeUndefined} from "@welshman/lib"
|
||||||
import {Profiles} from "@welshman/app"
|
import {deriveProfile, deriveProfileDisplay} from "@welshman/app"
|
||||||
import {app} from "@app/welshman"
|
import {getColor, getBlobVariant} from "@app/theme"
|
||||||
import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
|
|
||||||
import ImageIcon from "@lib/components/ImageIcon.svelte"
|
import ImageIcon from "@lib/components/ImageIcon.svelte"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -11,15 +10,39 @@
|
|||||||
class?: string
|
class?: string
|
||||||
size?: number
|
size?: number
|
||||||
url?: string
|
url?: string
|
||||||
|
shape?: "blob" | "circle"
|
||||||
|
style?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const {pubkey, url, size = 7, ...props}: Props = $props()
|
const {pubkey, url, size = 7, shape = "blob", style = "", ...props}: Props = $props()
|
||||||
|
|
||||||
const profile = app.use(Profiles).one(pubkey, removeUndefined([url]))
|
const profile = deriveProfile(pubkey, removeUndefined([url]))
|
||||||
|
const display = deriveProfileDisplay(pubkey)
|
||||||
|
|
||||||
|
// Organic, hand-drawn-feeling mask. The variant is stable per pubkey so a
|
||||||
|
// person's silhouette never changes; `shape="circle"` opts back into a disc.
|
||||||
|
const shapeClass =
|
||||||
|
shape === "circle"
|
||||||
|
? "rounded-full"
|
||||||
|
: ["avatar-blob", "avatar-blob-2", "avatar-blob-3"][getBlobVariant(pubkey) - 1]
|
||||||
|
|
||||||
|
const color = getColor(pubkey)
|
||||||
|
const px = $derived(size * 4)
|
||||||
|
const initial = $derived([...($display || "")].find(c => c.trim()) || "?")
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ImageIcon
|
{#if $profile?.picture}
|
||||||
{size}
|
<ImageIcon {size} alt="" {style} class={cx(props.class, shapeClass)} src={$profile.picture} />
|
||||||
alt=""
|
{:else}
|
||||||
class={cx(props.class, "rounded-full")}
|
<!-- Fallback: a subtle gradient derived from the pubkey + the person's initial. -->
|
||||||
src={$profile?.picture() || UserRounded} />
|
<div
|
||||||
|
class={cx(
|
||||||
|
props.class,
|
||||||
|
shapeClass,
|
||||||
|
"font-display flex shrink-0 items-center justify-center font-bold text-white uppercase select-none",
|
||||||
|
)}
|
||||||
|
style="width:{px}px;height:{px}px;font-size:{px *
|
||||||
|
0.45}px;background-image:linear-gradient(135deg,{color},color-mix(in oklab,{color},#000 28%));{style}">
|
||||||
|
{initial}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import cx from "classnames"
|
import cx from "classnames"
|
||||||
import {Profiles} from "@welshman/app"
|
import {getProfile, loadProfile} from "@welshman/app"
|
||||||
import {app} from "@app/welshman"
|
|
||||||
import {isMobile} from "@lib/html"
|
import {isMobile} from "@lib/html"
|
||||||
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
||||||
|
|
||||||
@@ -15,20 +14,22 @@
|
|||||||
const {pubkeys, size = 7, limit, class: className}: Props = $props()
|
const {pubkeys, size = 7, limit, class: className}: Props = $props()
|
||||||
const effectiveLimit = $derived(limit ?? (isMobile ? 7 : 10))
|
const effectiveLimit = $derived(limit ?? (isMobile ? 7 : 10))
|
||||||
|
|
||||||
|
// circle is one step smaller than box so the bg-base-100 wrapper reads as a
|
||||||
|
// thin separating ring between overlapping avatars (Discord-style stack).
|
||||||
const dimensions = $derived(
|
const dimensions = $derived(
|
||||||
size <= 5
|
size <= 5
|
||||||
? {box: "h-5 w-5", overlap: "-mr-2", overflow: "text-[9px]"}
|
? {box: "h-5 w-5", circle: 4, overlap: "-mr-2", overflow: "text-[9px]"}
|
||||||
: size <= 6
|
: size <= 6
|
||||||
? {box: "h-6 w-6", overlap: "-mr-2.5", overflow: "text-[10px]"}
|
? {box: "h-6 w-6", circle: 5, overlap: "-mr-2.5", overflow: "text-[10px]"}
|
||||||
: {box: "h-8 w-8", overlap: "-mr-3", overflow: "text-xs"},
|
: {box: "h-8 w-8", circle: 7, overlap: "-mr-3", overflow: "text-xs"},
|
||||||
)
|
)
|
||||||
|
|
||||||
for (const pubkey of pubkeys) {
|
for (const pubkey of pubkeys) {
|
||||||
app.use(Profiles).load(pubkey)
|
loadProfile(pubkey)
|
||||||
}
|
}
|
||||||
|
|
||||||
const visiblePubkeys = $derived.by(() => {
|
const visiblePubkeys = $derived.by(() => {
|
||||||
const filtered = pubkeys.filter(pubkey => app.use(Profiles).get(pubkey)?.picture())
|
const filtered = pubkeys.filter(pubkey => getProfile(pubkey)?.picture)
|
||||||
|
|
||||||
return filtered.length > 0 ? filtered : pubkeys.slice(0, 1)
|
return filtered.length > 0 ? filtered : pubkeys.slice(0, 1)
|
||||||
})
|
})
|
||||||
@@ -38,20 +39,21 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class={cx("flex", size <= 5 ? "pr-2" : "pr-3", className)}>
|
<div class={cx("flex", size <= 5 ? "pr-2" : "pr-3", className)}>
|
||||||
{#each displayPubkeys as pubkey (pubkey)}
|
{#each displayPubkeys as pubkey, i (pubkey)}
|
||||||
<div
|
<div
|
||||||
class={cx(
|
class={cx(
|
||||||
"z-feature inline-block flex items-center justify-center rounded-full bg-base-100",
|
"z-feature inline-flex items-center justify-center rounded-full bg-base-100 transition-transform",
|
||||||
dimensions.box,
|
dimensions.box,
|
||||||
dimensions.overlap,
|
dimensions.overlap,
|
||||||
|
i % 2 === 0 ? "rotate-2" : "-rotate-2",
|
||||||
)}>
|
)}>
|
||||||
<ProfileCircle class={cx(dimensions.box, "bg-base-300")} {pubkey} {size} />
|
<ProfileCircle class="bg-base-300" shape="circle" {pubkey} size={dimensions.circle} />
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
{#if overflowCount > 0}
|
{#if overflowCount > 0}
|
||||||
<div
|
<div
|
||||||
class={cx(
|
class={cx(
|
||||||
"z-feature inline-flex items-center justify-center rounded-full bg-neutral font-medium text-neutral-content",
|
"z-feature bg-primary text-primary-content shadow-soft font-display inline-flex rotate-2 items-center justify-center rounded-full font-bold",
|
||||||
dimensions.box,
|
dimensions.box,
|
||||||
dimensions.overlap,
|
dimensions.overlap,
|
||||||
dimensions.overflow,
|
dimensions.overflow,
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {chunk, sleep, uniq} from "@welshman/lib"
|
import {chunk, sleep, uniq} from "@welshman/lib"
|
||||||
import {makeEvent, DELETE, isReplaceable, getAddress} from "@welshman/util"
|
import {
|
||||||
import {ProfileBuilder} from "@welshman/domain"
|
makeEvent,
|
||||||
import {Thunks, RelayLists} from "@welshman/app"
|
createProfile,
|
||||||
import {pubkey, repository, app} from "@app/welshman"
|
PROFILE,
|
||||||
|
DELETE,
|
||||||
|
isReplaceable,
|
||||||
|
getAddress,
|
||||||
|
RelayMode,
|
||||||
|
} from "@welshman/util"
|
||||||
|
import {pubkey, publishThunk, repository, derivePubkeyRelays} from "@welshman/app"
|
||||||
import {preventDefault} from "@lib/html"
|
import {preventDefault} from "@lib/html"
|
||||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
||||||
@@ -25,7 +31,7 @@
|
|||||||
let confirmText = $state("")
|
let confirmText = $state("")
|
||||||
|
|
||||||
const CONFIRM_TEXT = "permanently delete my nostr account"
|
const CONFIRM_TEXT = "permanently delete my nostr account"
|
||||||
const userWriteRelays = app.use(RelayLists).writeUrls($pubkey!).$
|
const userWriteRelays = derivePubkeyRelays($pubkey!, RelayMode.Write)
|
||||||
const confirmOk = $derived(confirmText.toLowerCase().trim() === CONFIRM_TEXT)
|
const confirmOk = $derived(confirmText.toLowerCase().trim() === CONFIRM_TEXT)
|
||||||
const showProgress = $derived(progress !== undefined)
|
const showProgress = $derived(progress !== undefined)
|
||||||
|
|
||||||
@@ -38,7 +44,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const chunks = chunk(500, repository.query([{authors: [$pubkey!]}]))
|
const chunks = chunk(500, repository.query([{authors: [$pubkey!]}]))
|
||||||
const profileEvent = await new ProfileBuilder().update({name: "[deleted]"}).toTemplate()
|
const profileEvent = makeEvent(PROFILE, createProfile({name: "[deleted]"}))
|
||||||
const vanishEvent = makeEvent(62, {tags: [["relay", "ALL_RELAYS"]]})
|
const vanishEvent = makeEvent(62, {tags: [["relay", "ALL_RELAYS"]]})
|
||||||
const denominator = chunks.length + 2
|
const denominator = chunks.length + 2
|
||||||
const relays = uniq([...INDEXER_RELAYS, ...$userWriteRelays, ...$userSpaceUrls])
|
const relays = uniq([...INDEXER_RELAYS, ...$userWriteRelays, ...$userSpaceUrls])
|
||||||
@@ -52,12 +58,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// First, blank out their profile in case relays don't support deletion by address
|
// First, blank out their profile in case relays don't support deletion by address
|
||||||
await app.use(Thunks).publish({relays, event: profileEvent})
|
await publishThunk({relays, event: profileEvent})
|
||||||
|
|
||||||
await incrementProgress()
|
await incrementProgress()
|
||||||
|
|
||||||
// Next, send a "right to vanish" event to all relays
|
// Next, send a "right to vanish" event to all relays
|
||||||
await app.use(Thunks).publish({relays, event: vanishEvent})
|
await publishThunk({relays, event: vanishEvent})
|
||||||
|
|
||||||
await incrementProgress()
|
await incrementProgress()
|
||||||
|
|
||||||
@@ -73,7 +79,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await app.use(Thunks).publish({relays, event: makeEvent(DELETE, {tags})})
|
await publishThunk({relays, event: makeEvent(DELETE, {tags})})
|
||||||
|
|
||||||
await incrementProgress()
|
await incrementProgress()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,12 @@
|
|||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import {removeUndefined} from "@welshman/lib"
|
import {removeUndefined} from "@welshman/lib"
|
||||||
import {ManagementMethod} from "@welshman/util"
|
import {ManagementMethod} from "@welshman/util"
|
||||||
import {RelayManagement, Profiles, MessagingRelayLists} from "@welshman/app"
|
import {
|
||||||
import {app} from "@app/welshman"
|
manageRelay,
|
||||||
|
deriveProfile,
|
||||||
|
displayProfileByPubkey,
|
||||||
|
loadMessagingRelayList,
|
||||||
|
} from "@welshman/app"
|
||||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
import Code2 from "@assets/icons/code-2.svg?dataurl"
|
import Code2 from "@assets/icons/code-2.svg?dataurl"
|
||||||
import Letter from "@assets/icons/letter-opened.svg?dataurl"
|
import Letter from "@assets/icons/letter-opened.svg?dataurl"
|
||||||
@@ -37,7 +41,7 @@
|
|||||||
|
|
||||||
const {pubkey, url}: Props = $props()
|
const {pubkey, url}: Props = $props()
|
||||||
|
|
||||||
const profile = app.use(Profiles).one(pubkey, removeUndefined([url]))
|
const profile = deriveProfile(pubkey, removeUndefined([url]))
|
||||||
|
|
||||||
const userIsAdmin = deriveUserIsSpaceAdmin(url)
|
const userIsAdmin = deriveUserIsSpaceAdmin(url)
|
||||||
|
|
||||||
@@ -62,9 +66,9 @@
|
|||||||
const banMember = () =>
|
const banMember = () =>
|
||||||
pushModal(Confirm, {
|
pushModal(Confirm, {
|
||||||
title: "Ban User",
|
title: "Ban User",
|
||||||
message: `Are you sure you want to ban @${app.use(Profiles).display(pubkey).get()} from the space?`,
|
message: `Are you sure you want to ban @${displayProfileByPubkey(pubkey)} from the space?`,
|
||||||
confirm: async () => {
|
confirm: async () => {
|
||||||
const {error} = await app.use(RelayManagement).post(url!, {
|
const {error} = await manageRelay(url!, {
|
||||||
method: ManagementMethod.BanPubkey,
|
method: ManagementMethod.BanPubkey,
|
||||||
params: [pubkey],
|
params: [pubkey],
|
||||||
})
|
})
|
||||||
@@ -92,7 +96,7 @@
|
|||||||
let showMenu = $state(false)
|
let showMenu = $state(false)
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
app.use(MessagingRelayLists).load(pubkey)
|
loadMessagingRelayList(pubkey)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {Profile, ProfileBuilder} from "@welshman/domain"
|
import type {Profile} from "@welshman/util"
|
||||||
import {pubkey, profilesByPubkey} from "@app/welshman"
|
import {makeProfile} from "@welshman/util"
|
||||||
|
import {pubkey, profilesByPubkey, waitForThunkError} from "@welshman/app"
|
||||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
import {errorMessage} from "@lib/util"
|
import {errorMessage} from "@lib/util"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
@@ -11,28 +12,16 @@
|
|||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/toast"
|
||||||
import {updateProfile} from "@app/profiles"
|
import {updateProfile} from "@app/profiles"
|
||||||
|
|
||||||
// The edit form binds to plain mutable fields (name/about/nip05/picture), so we
|
const profile = $profilesByPubkey.get($pubkey!) || makeProfile()
|
||||||
// hand it a plain values object rather than a Profile Reader. A Reader exposes its
|
|
||||||
// raw parsed content as `.values`; a fresh profile starts from an empty builder.
|
|
||||||
const reader = $profilesByPubkey.get($pubkey!)
|
|
||||||
const profile = reader instanceof Profile ? {...reader.values} : new ProfileBuilder().update({}).values
|
|
||||||
const initialValues = {profile}
|
const initialValues = {profile}
|
||||||
|
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
// TODO(welshman-migration): `profile` here is a plain values object (the form binds
|
|
||||||
// mutable fields), not a Profile Reader. It is typed Profile to match ProfileEditForm's
|
|
||||||
// Values type and updateProfile's signature, both of which still say Profile; updateProfile
|
|
||||||
// routes a non-Reader through `new ProfileBuilder().update(profile)`. Confirm the intended
|
|
||||||
// values vs Reader contract once the shared types settle.
|
|
||||||
const onsubmit = async ({profile}: {profile: Profile}) => {
|
const onsubmit = async ({profile}: {profile: Profile}) => {
|
||||||
loading = true
|
loading = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// TODO(welshman-migration): updateProfile is async (returns Promise<Thunk>); the old
|
const error = await waitForThunkError(updateProfile({profile}))
|
||||||
// waitForThunkError shim did not await its arg. Awaiting the thunk first before
|
|
||||||
// .waitForError() — confirm this matches intended behavior.
|
|
||||||
const error = await (await updateProfile({profile})).waitForError()
|
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
pushToast({
|
pushToast({
|
||||||
|
|||||||
@@ -4,8 +4,7 @@
|
|||||||
import {feedFromFilter, makeIntersectionFeed, makeRelayFeed} from "@welshman/feeds"
|
import {feedFromFilter, makeIntersectionFeed, makeRelayFeed} from "@welshman/feeds"
|
||||||
import {NOTE, getReplyTags} from "@welshman/util"
|
import {NOTE, getReplyTags} from "@welshman/util"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {Feeds} from "@welshman/app"
|
import {makeFeedController} from "@welshman/app"
|
||||||
import {app} from "@app/welshman"
|
|
||||||
import {createScroller} from "@lib/html"
|
import {createScroller} from "@lib/html"
|
||||||
import {fly} from "@lib/transition"
|
import {fly} from "@lib/transition"
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
@@ -20,7 +19,7 @@
|
|||||||
|
|
||||||
let {url, pubkey, events = $bindable([]), hideLoading = false}: Props = $props()
|
let {url, pubkey, events = $bindable([]), hideLoading = false}: Props = $props()
|
||||||
|
|
||||||
const ctrl = app.use(Feeds).makeFeedController({
|
const ctrl = makeFeedController({
|
||||||
useWindowing: true,
|
useWindowing: true,
|
||||||
feed: makeIntersectionFeed(
|
feed: makeIntersectionFeed(
|
||||||
makeRelayFeed(url),
|
makeRelayFeed(url),
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {removeUndefined} from "@welshman/lib"
|
import {removeUndefined} from "@welshman/lib"
|
||||||
import {Profiles} from "@welshman/app"
|
import {deriveProfile} from "@welshman/app"
|
||||||
import {app} from "@app/welshman"
|
|
||||||
import ContentMinimal from "@app/components/ContentMinimal.svelte"
|
import ContentMinimal from "@app/components/ContentMinimal.svelte"
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
@@ -11,9 +10,9 @@
|
|||||||
|
|
||||||
const {pubkey, url}: Props = $props()
|
const {pubkey, url}: Props = $props()
|
||||||
|
|
||||||
const profile = app.use(Profiles).one(pubkey, removeUndefined([url]))
|
const profile = deriveProfile(pubkey, removeUndefined([url]))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $profile}
|
{#if $profile}
|
||||||
<ContentMinimal event={{content: $profile.about() || "", tags: []}} />
|
<ContentMinimal event={{content: $profile.about || "", tags: []}} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
import type {Writable} from "svelte/store"
|
import type {Writable} from "svelte/store"
|
||||||
import {type Instance} from "tippy.js"
|
import {type Instance} from "tippy.js"
|
||||||
import {append, remove, uniq} from "@welshman/lib"
|
import {append, remove, uniq} from "@welshman/lib"
|
||||||
import {profileSearch} from "@app/welshman"
|
import {profileSearch} from "@welshman/app"
|
||||||
import Suggestions from "@lib/components/Suggestions.svelte"
|
import Suggestions from "@lib/components/Suggestions.svelte"
|
||||||
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
|
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
|
||||||
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
|
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {removeUndefined} from "@welshman/lib"
|
import {removeUndefined} from "@welshman/lib"
|
||||||
import {Profiles} from "@welshman/app"
|
import {deriveProfileDisplay} from "@welshman/app"
|
||||||
import {app} from "@app/welshman"
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
pubkey: string
|
pubkey: string
|
||||||
@@ -10,7 +9,7 @@
|
|||||||
|
|
||||||
const {pubkey, url}: Props = $props()
|
const {pubkey, url}: Props = $props()
|
||||||
|
|
||||||
const profileDisplay = app.use(Profiles).display(pubkey, removeUndefined([url])).$
|
const profileDisplay = deriveProfileDisplay(pubkey, removeUndefined([url]))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{$profileDisplay}
|
{$profileDisplay}
|
||||||
|
|||||||
@@ -2,8 +2,14 @@
|
|||||||
const {current, total}: {current: number; total: number} = $props()
|
const {current, total}: {current: number; total: number} = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex w-full">
|
<div class="flex w-full gap-1.5">
|
||||||
{#each Array(total) as _, i}
|
{#each Array(total) as _, i}
|
||||||
<div class="h-1 flex-1 transition-colors {i < current ? 'bg-primary' : 'bg-base-300'}"></div>
|
<div
|
||||||
|
class="h-2 flex-1 rounded-full transition-colors duration-300 {i < current
|
||||||
|
? 'bg-primary'
|
||||||
|
: i === current
|
||||||
|
? 'bg-primary/40 motion-safe:animate-pulse'
|
||||||
|
: 'bg-base-300'}">
|
||||||
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,8 +17,7 @@
|
|||||||
import type {TrustedEvent, EventContent, Zap} from "@welshman/util"
|
import type {TrustedEvent, EventContent, Zap} from "@welshman/util"
|
||||||
import {deriveArray, deriveEventsById, deriveItemsByKey} from "@welshman/store"
|
import {deriveArray, deriveEventsById, deriveItemsByKey} from "@welshman/store"
|
||||||
import {load} from "@welshman/net"
|
import {load} from "@welshman/net"
|
||||||
import {Zappers, Profiles} from "@welshman/app"
|
import {pubkey, repository, getValidZap, displayProfileByPubkey} from "@welshman/app"
|
||||||
import {pubkey, repository, app} from "@app/welshman"
|
|
||||||
import {isMobile, preventDefault, stopPropagation} from "@lib/html"
|
import {isMobile, preventDefault, stopPropagation} from "@lib/html"
|
||||||
import Danger from "@assets/icons/danger-triangle.svg?dataurl"
|
import Danger from "@assets/icons/danger-triangle.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
@@ -65,15 +64,15 @@
|
|||||||
repository,
|
repository,
|
||||||
getKey: zap => zap.response.id,
|
getKey: zap => zap.response.id,
|
||||||
filters: [{kinds: [ZAP_RESPONSE], "#e": eventIds}],
|
filters: [{kinds: [ZAP_RESPONSE], "#e": eventIds}],
|
||||||
eventToItem: async (response: TrustedEvent) => {
|
eventToItem: (response: TrustedEvent) => {
|
||||||
const zap = await app.use(Zappers).validateZapReceipt(response, event)
|
const zap = getValidZap(response, event)
|
||||||
|
|
||||||
if (zap) {
|
if (zap) {
|
||||||
return zap
|
return zap
|
||||||
}
|
}
|
||||||
|
|
||||||
if (innerEvent) {
|
if (innerEvent) {
|
||||||
return await app.use(Zappers).validateZapReceipt(response, innerEvent)
|
return getValidZap(response, innerEvent)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@@ -151,28 +150,28 @@
|
|||||||
{@const amount = fromMsats(sum(zaps.map(zap => zap.invoiceAmount)))}
|
{@const amount = fromMsats(sum(zaps.map(zap => zap.invoiceAmount)))}
|
||||||
{@const pubkeys = uniq(zaps.map(zap => zap.request.pubkey))}
|
{@const pubkeys = uniq(zaps.map(zap => zap.request.pubkey))}
|
||||||
{@const isOwn = $pubkey && pubkeys.includes($pubkey)}
|
{@const isOwn = $pubkey && pubkeys.includes($pubkey)}
|
||||||
{@const info = displayList(pubkeys.map(pubkey => app.use(Profiles).display(pubkey).get()))}
|
{@const info = displayList(pubkeys.map(pubkey => displayProfileByPubkey(pubkey)))}
|
||||||
{@const tooltip = `${info} zapped`}
|
{@const tooltip = `${info} zapped`}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
data-tip={tooltip}
|
data-tip={tooltip}
|
||||||
class={cx(
|
class={cx(
|
||||||
reactionClass,
|
reactionClass,
|
||||||
"flex-inline btn btn-outline btn-neutral btn-xs flex items-center gap-1 rounded-full text-xs font-normal bg-alt",
|
"flex-inline btn btn-xs flex items-center gap-1 rounded-full border text-xs font-normal transition-transform motion-safe:hover:scale-110 motion-safe:active:scale-95",
|
||||||
{
|
{
|
||||||
tooltip: !noTooltip && !isMobile,
|
tooltip: !noTooltip && !isMobile,
|
||||||
"border-neutral-content/20": !isOwn,
|
"bg-alt border-base-content/15": !isOwn,
|
||||||
"btn-primary": isOwn,
|
"border-primary/50 bg-primary/15 text-primary": isOwn,
|
||||||
},
|
},
|
||||||
)}>
|
)}>
|
||||||
<Reaction event={zaps[0].request} />
|
<Reaction event={zaps[0].request} />
|
||||||
<span>{amount}</span>
|
<span class="font-semibold">{amount}</span>
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
{#each groupedReactions.entries() as [key, events]}
|
{#each groupedReactions.entries() as [key, events]}
|
||||||
{@const pubkeys = events.map(e => e.pubkey)}
|
{@const pubkeys = events.map(e => e.pubkey)}
|
||||||
{@const isOwn = $pubkey && pubkeys.includes($pubkey)}
|
{@const isOwn = $pubkey && pubkeys.includes($pubkey)}
|
||||||
{@const info = displayList(pubkeys.map(pubkey => app.use(Profiles).display(pubkey).get()))}
|
{@const info = displayList(pubkeys.map(pubkey => displayProfileByPubkey(pubkey)))}
|
||||||
{@const tooltip = `${info} reacted`}
|
{@const tooltip = `${info} reacted`}
|
||||||
{@const onClick = () => onReactionClick(events)}
|
{@const onClick = () => onReactionClick(events)}
|
||||||
<button
|
<button
|
||||||
@@ -180,17 +179,17 @@
|
|||||||
data-tip={tooltip}
|
data-tip={tooltip}
|
||||||
class={cx(
|
class={cx(
|
||||||
reactionClass,
|
reactionClass,
|
||||||
"flex-inline btn btn-outline btn-neutral btn-xs gap-1 rounded-full font-normal bg-alt",
|
"flex-inline btn btn-xs gap-1 rounded-full border font-normal transition-transform motion-safe:hover:scale-110 motion-safe:active:scale-95",
|
||||||
{
|
{
|
||||||
tooltip: !noTooltip && !isMobile,
|
tooltip: !noTooltip && !isMobile,
|
||||||
"border-neutral-content/20": !isOwn,
|
"bg-alt border-base-content/15": !isOwn,
|
||||||
"btn-primary": isOwn,
|
"border-primary/50 bg-primary/15 text-primary": isOwn,
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
onclick={stopPropagation(preventDefault(onClick))}>
|
onclick={stopPropagation(preventDefault(onClick))}>
|
||||||
<Reaction event={events[0]} />
|
<Reaction event={events[0]} />
|
||||||
{#if events.length > 1}
|
{#if events.length > 1}
|
||||||
<span>{events.length}</span>
|
<span class="font-semibold">{events.length}</span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
import NoteContentMinimal from "@app/components/NoteContentMinimal.svelte"
|
import NoteContentMinimal from "@app/components/NoteContentMinimal.svelte"
|
||||||
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
||||||
import RoomNameWithImage from "@app/components/RoomNameWithImage.svelte"
|
import RoomNameWithImage from "@app/components/RoomNameWithImage.svelte"
|
||||||
|
import {getColor} from "@app/theme"
|
||||||
import {makeRoomPath, makeSpaceChatPath} from "@app/routes"
|
import {makeRoomPath, makeSpaceChatPath} from "@app/routes"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -25,7 +26,9 @@
|
|||||||
const onClick = () => goto(h ? makeRoomPath(url, h) : makeSpaceChatPath(url))
|
const onClick = () => goto(h ? makeRoomPath(url, h) : makeSpaceChatPath(url))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Button class="cv card2 bg-alt shadow-md" onclick={onClick}>
|
<Button
|
||||||
|
class="cv card2 bg-alt shadow-soft block w-full transition-all motion-safe:hover:-translate-y-0.5"
|
||||||
|
onclick={onClick}>
|
||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
<div class="flex items-center gap-2 text-sm">
|
<div class="flex items-center gap-2 text-sm">
|
||||||
{#if h}
|
{#if h}
|
||||||
@@ -39,7 +42,10 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-start gap-3">
|
<div class="flex items-start gap-3">
|
||||||
<ProfileCircle pubkey={event.pubkey} size={10} />
|
<ProfileCircle
|
||||||
|
pubkey={event.pubkey}
|
||||||
|
size={10}
|
||||||
|
style="box-shadow: 0 0 0 2px {getColor(event.pubkey)}" />
|
||||||
<div class="min-w-0 flex-1">
|
<div class="min-w-0 flex-1">
|
||||||
<NoteContentMinimal {event} />
|
<NoteContentMinimal {event} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
import {tryCatch} from "@welshman/lib"
|
import {tryCatch} from "@welshman/lib"
|
||||||
import {isShareableRelayUrl, isIPAddress, normalizeRelayUrl} from "@welshman/util"
|
import {isShareableRelayUrl, isIPAddress, normalizeRelayUrl} from "@welshman/util"
|
||||||
import type {Thunk} from "@welshman/app"
|
import type {Thunk} from "@welshman/app"
|
||||||
import {relaySearch} from "@app/welshman"
|
import {waitForThunkError, relaySearch} from "@welshman/app"
|
||||||
import {createScroller} from "@lib/html"
|
import {createScroller} from "@lib/html"
|
||||||
import {errorMessage} from "@lib/util"
|
import {errorMessage} from "@lib/util"
|
||||||
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
|
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
loading.add(url)
|
loading.add(url)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const error = await (await addRelay(url)).waitForError()
|
const error = await waitForThunkError(await addRelay(url))
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
pushToast({
|
pushToast({
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {Relays} from "@welshman/app"
|
import {deriveRelay} from "@welshman/app"
|
||||||
import {app} from "@app/welshman"
|
|
||||||
|
|
||||||
const {...props} = $props()
|
const {...props} = $props()
|
||||||
|
|
||||||
const relay = app.use(Relays).one(props.url)
|
const relay = deriveRelay(props.url)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $relay?.description}
|
{#if $relay?.description}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {Relays} from "@welshman/app"
|
import cx from "classnames"
|
||||||
import {app} from "@app/welshman"
|
import {deriveRelay} from "@welshman/app"
|
||||||
import RemoteControllerMinimalistic from "@assets/icons/remote-controller-minimalistic.svg?dataurl"
|
import {getColor} from "@app/theme"
|
||||||
import ImageIcon from "@lib/components/ImageIcon.svelte"
|
import ImageIcon from "@lib/components/ImageIcon.svelte"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -12,11 +12,23 @@
|
|||||||
|
|
||||||
const {url, size = 7, ...props}: Props = $props()
|
const {url, size = 7, ...props}: Props = $props()
|
||||||
|
|
||||||
const relay = app.use(Relays).one(url)
|
const relay = deriveRelay(url)
|
||||||
|
const px = size * 4
|
||||||
|
const color = getColor(url)
|
||||||
|
const letter = (url.replace(/^wss?:\/\//, "").replace(/^www\./, "")[0] || "?").toUpperCase()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $relay?.icon}
|
{#if $relay?.icon}
|
||||||
<ImageIcon {size} alt="" src={$relay?.icon} class={props.class} />
|
<ImageIcon {size} alt="" src={$relay.icon} class={cx(props.class, "squircle")} />
|
||||||
{:else}
|
{:else}
|
||||||
<ImageIcon size={size - 2} alt="" src={RemoteControllerMinimalistic} class={props.class} />
|
<!-- Lettered workspace tile (Slack/Discord-style) colored by the relay url. -->
|
||||||
|
<div
|
||||||
|
class={cx(
|
||||||
|
props.class,
|
||||||
|
"squircle font-display flex shrink-0 items-center justify-center font-bold text-white uppercase",
|
||||||
|
)}
|
||||||
|
style="width:{px}px;height:{px}px;font-size:{px *
|
||||||
|
0.42}px;background-image:linear-gradient(135deg,{color},color-mix(in oklab,{color},#000 28%))">
|
||||||
|
{letter}
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -4,13 +4,12 @@
|
|||||||
import Link from "@lib/components/Link.svelte"
|
import Link from "@lib/components/Link.svelte"
|
||||||
import {displayUrl} from "@welshman/lib"
|
import {displayUrl} from "@welshman/lib"
|
||||||
import {displayRelayUrl} from "@welshman/util"
|
import {displayRelayUrl} from "@welshman/util"
|
||||||
import {Relays, RelayStats} from "@welshman/app"
|
import {deriveRelay, deriveRelayStats} from "@welshman/app"
|
||||||
import {app} from "@app/welshman"
|
|
||||||
|
|
||||||
const {url, children} = $props()
|
const {url, children} = $props()
|
||||||
|
|
||||||
const relay = app.use(Relays).one(url)
|
const relay = deriveRelay(url)
|
||||||
const relayStats = app.use(RelayStats).one(url)
|
const relayStats = deriveRelayStats(url)
|
||||||
const connections = $derived($relayStats?.open_count || 0)
|
const connections = $derived($relayStats?.open_count || 0)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type {Readable} from "svelte/store"
|
import type {Readable} from "svelte/store"
|
||||||
import {SvelteSet} from "svelte/reactivity"
|
import {SvelteSet} from "svelte/reactivity"
|
||||||
|
import {waitForThunkError} from "@welshman/app"
|
||||||
import type {Thunk} from "@welshman/app"
|
import type {Thunk} from "@welshman/app"
|
||||||
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
|
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
|
||||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
@@ -36,7 +37,7 @@
|
|||||||
loading.add(url)
|
loading.add(url)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const error = await (await removeRelay(url)).waitForError()
|
const error = await waitForThunkError(await removeRelay(url))
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
pushToast({
|
pushToast({
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {Relays} from "@welshman/app"
|
import {deriveRelayDisplay} from "@welshman/app"
|
||||||
import {app} from "@app/welshman"
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
url: string
|
url: string
|
||||||
@@ -9,7 +8,7 @@
|
|||||||
|
|
||||||
const {url, ...props}: Props = $props()
|
const {url, ...props}: Props = $props()
|
||||||
|
|
||||||
const display = $derived(app.use(Relays).display(url).$)
|
const display = $derived(deriveRelayDisplay(url))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<span class={props.class}>
|
<span class={props.class}>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import {REPORT} from "@welshman/util"
|
import {REPORT} from "@welshman/util"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {deriveEventsById} from "@welshman/store"
|
import {deriveEventsById} from "@welshman/store"
|
||||||
import {repository} from "@app/welshman"
|
import {repository} from "@welshman/app"
|
||||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
||||||
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {getTag, ManagementMethod} from "@welshman/util"
|
import {getTag, ManagementMethod} from "@welshman/util"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {RelayManagement, Profiles} from "@welshman/app"
|
import {pubkey, manageRelay, repository, displayProfileByPubkey} from "@welshman/app"
|
||||||
import {pubkey, repository, app} from "@app/welshman"
|
|
||||||
import InboxOut from "@assets/icons/inbox-out.svg?dataurl"
|
import InboxOut from "@assets/icons/inbox-out.svg?dataurl"
|
||||||
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
|
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
|
||||||
import MinusCircle from "@assets/icons/minus-circle.svg?dataurl"
|
import MinusCircle from "@assets/icons/minus-circle.svg?dataurl"
|
||||||
@@ -46,7 +45,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const dismissReport = async () => {
|
const dismissReport = async () => {
|
||||||
const {error} = await app.use(RelayManagement).post(url, {
|
const {error} = await manageRelay(url, {
|
||||||
method: ManagementMethod.BanEvent,
|
method: ManagementMethod.BanEvent,
|
||||||
params: [event.id, "Dismissed by admin"],
|
params: [event.id, "Dismissed by admin"],
|
||||||
})
|
})
|
||||||
@@ -67,7 +66,7 @@
|
|||||||
title: `Remove Content`,
|
title: `Remove Content`,
|
||||||
message: `Are you sure you want to delete this content from the space?`,
|
message: `Are you sure you want to delete this content from the space?`,
|
||||||
confirm: async () => {
|
confirm: async () => {
|
||||||
const {error} = await app.use(RelayManagement).post(url, {
|
const {error} = await manageRelay(url, {
|
||||||
method: ManagementMethod.BanEvent,
|
method: ManagementMethod.BanEvent,
|
||||||
params: [id, reason],
|
params: [id, reason],
|
||||||
})
|
})
|
||||||
@@ -90,9 +89,9 @@
|
|||||||
|
|
||||||
pushModal(Confirm, {
|
pushModal(Confirm, {
|
||||||
title: "Ban User",
|
title: "Ban User",
|
||||||
message: `Are you sure you want to ban @${app.use(Profiles).display(pubkey).get()} from the space?`,
|
message: `Are you sure you want to ban @${displayProfileByPubkey(pubkey)} from the space?`,
|
||||||
confirm: async () => {
|
confirm: async () => {
|
||||||
const {error} = await app.use(RelayManagement).post(url, {
|
const {error} = await manageRelay(url, {
|
||||||
method: ManagementMethod.BanPubkey,
|
method: ManagementMethod.BanPubkey,
|
||||||
params: [pubkey, reason],
|
params: [pubkey, reason],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -135,7 +135,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
data-tip="{window.navigator.platform.includes('Mac') ? 'cmd' : 'ctrl'}+enter to send"
|
data-tip="{window.navigator.platform.includes('Mac') ? 'cmd' : 'ctrl'}+enter to send"
|
||||||
class="center tooltip tooltip-left absolute right-4 h-10 w-10 min-w-10 rounded-full"
|
class="center tooltip tooltip-left bg-primary text-primary-content absolute top-[7px] right-[7px] h-11 w-11 min-w-11 scale-90 rounded-full transition-transform motion-safe:hover:scale-100"
|
||||||
disabled={$uploading}
|
disabled={$uploading}
|
||||||
onclick={submit}>
|
onclick={submit}>
|
||||||
<Icon icon={Plane} />
|
<Icon icon={Plane} />
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {Profiles} from "@welshman/app"
|
import {displayProfileByPubkey} from "@welshman/app"
|
||||||
import {app} from "@app/welshman"
|
|
||||||
import {slide} from "@lib/transition"
|
import {slide} from "@lib/transition"
|
||||||
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
|
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
@@ -22,7 +21,7 @@
|
|||||||
<div
|
<div
|
||||||
class="relative border-l-2 border-solid border-primary bg-base-300 px-2 py-1 pr-8"
|
class="relative border-l-2 border-solid border-primary bg-base-300 px-2 py-1 pr-8"
|
||||||
transition:slide>
|
transition:slide>
|
||||||
<p class="text-xs text-primary">{verb} @{app.use(Profiles).display(event.pubkey).get()}</p>
|
<p class="text-xs text-primary">{verb} @{displayProfileByPubkey(event.pubkey)}</p>
|
||||||
{#key event.id}
|
{#key event.id}
|
||||||
<NoteContentMinimal trimParent {event} />
|
<NoteContentMinimal trimParent {event} />
|
||||||
{/key}
|
{/key}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {goto} from "$app/navigation"
|
import {goto} from "$app/navigation"
|
||||||
import {displayRelayUrl} from "@welshman/util"
|
import type {RoomMeta} from "@welshman/util"
|
||||||
|
import {displayRelayUrl, makeRoomMeta} from "@welshman/util"
|
||||||
import type {Thunk} from "@welshman/app"
|
import type {Thunk} from "@welshman/app"
|
||||||
import {Rooms} from "@welshman/app"
|
import {deleteRoom, waitForThunkError, repository, joinRoom, leaveRoom} from "@welshman/app"
|
||||||
import {repository, app} from "@app/welshman"
|
|
||||||
import Pen from "@assets/icons/pen.svg?dataurl"
|
import Pen from "@assets/icons/pen.svg?dataurl"
|
||||||
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
|
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
|
||||||
import Login3 from "@assets/icons/login-3.svg?dataurl"
|
import Login3 from "@assets/icons/login-3.svg?dataurl"
|
||||||
@@ -71,13 +71,11 @@
|
|||||||
|
|
||||||
const startEdit = () => pushModal(RoomEdit, {url, h})
|
const startEdit = () => pushModal(RoomEdit, {url, h})
|
||||||
|
|
||||||
const handleLoading = async (
|
const handleLoading = async (f: (url: string, room: RoomMeta) => Thunk) => {
|
||||||
f: (url: string, room: {h: string}) => Promise<Thunk>,
|
|
||||||
) => {
|
|
||||||
loading = true
|
loading = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const message = await (await f(url, {h})).waitForError()
|
const message = await waitForThunkError(f(url, makeRoomMeta({h})))
|
||||||
|
|
||||||
if (message && !message.startsWith("duplicate:")) {
|
if (message && !message.startsWith("duplicate:")) {
|
||||||
pushToast({theme: "error", message})
|
pushToast({theme: "error", message})
|
||||||
@@ -87,9 +85,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const join = () => handleLoading((url, room) => app.use(Rooms).join(url, room))
|
const join = () => handleLoading(joinRoom)
|
||||||
|
|
||||||
const leave = () => handleLoading((url, room) => app.use(Rooms).leave(url, room))
|
const leave = () => handleLoading(leaveRoom)
|
||||||
|
|
||||||
const showMembers = () => pushModal(RoomMembers, {url, h})
|
const showMembers = () => pushModal(RoomMembers, {url, h})
|
||||||
|
|
||||||
@@ -111,8 +109,8 @@
|
|||||||
message:
|
message:
|
||||||
"This room will no longer be accessible to space members, and all messages posted to it will be deleted.",
|
"This room will no longer be accessible to space members, and all messages posted to it will be deleted.",
|
||||||
confirm: async () => {
|
confirm: async () => {
|
||||||
const thunk = await app.use(Rooms).delete(url, $room)
|
const thunk = deleteRoom(url, $room)
|
||||||
const message = await thunk.waitForError()
|
const message = await waitForThunkError(thunk)
|
||||||
|
|
||||||
if (message) {
|
if (message) {
|
||||||
repository.removeEvent(thunk.event.id)
|
repository.removeEvent(thunk.event.id)
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type {Snippet} from "svelte"
|
import type {Snippet} from "svelte"
|
||||||
import {Rooms} from "@welshman/app"
|
import type {RoomMeta} from "@welshman/util"
|
||||||
import {app} from "@app/welshman"
|
import {makeRoomMeta} from "@welshman/util"
|
||||||
|
import {waitForThunkError, createRoom, editRoom, joinRoom} from "@welshman/app"
|
||||||
import StickerSmileSquare from "@assets/icons/sticker-smile-square.svg?dataurl"
|
import StickerSmileSquare from "@assets/icons/sticker-smile-square.svg?dataurl"
|
||||||
import Hashtag from "@assets/icons/hashtag.svg?dataurl"
|
import Hashtag from "@assets/icons/hashtag.svg?dataurl"
|
||||||
import Volume from "@assets/icons/volume.svg?dataurl"
|
import Volume from "@assets/icons/volume.svg?dataurl"
|
||||||
@@ -18,56 +19,6 @@
|
|||||||
import {deriveHasLivekit} from "@app/relays"
|
import {deriveHasLivekit} from "@app/relays"
|
||||||
import {getRoomType, RoomType} from "@app/groups"
|
import {getRoomType, RoomType} from "@app/groups"
|
||||||
|
|
||||||
// Plain mutable form object (the old @welshman/util `RoomMeta` plain-object
|
|
||||||
// type, removed in the migration). The new domain `RoomMeta` is an async
|
|
||||||
// method-accessor Reader, which doesn't fit a `$state` object bound to inputs,
|
|
||||||
// so we keep a plain object here and let `app.use(Rooms).create/edit/join`
|
|
||||||
// (which accept a plain object) build the events at submit time.
|
|
||||||
type RoomMeta = {
|
|
||||||
h: string
|
|
||||||
name?: string
|
|
||||||
about?: string
|
|
||||||
picture?: string
|
|
||||||
pictureMeta?: string[]
|
|
||||||
isClosed?: boolean
|
|
||||||
isHidden?: boolean
|
|
||||||
isPrivate?: boolean
|
|
||||||
isRestricted?: boolean
|
|
||||||
livekit?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(welshman-migration): reimplemented inline from the removed
|
|
||||||
// @welshman/util `generateH`/`makeRoomMeta` (room-id generator). Verify the
|
|
||||||
// generated id still matches the expected `^[a-z]+[1-9]$` shape and that no
|
|
||||||
// shared generator should be used instead.
|
|
||||||
const vowels = "a,e,i,o,u,ay,ey,oy,ou,ia,ea,ough,oo,ee,argh".split(",")
|
|
||||||
|
|
||||||
const consonants =
|
|
||||||
"p,b,t,d,k,g,ch,sh,th,f,v,s,z,l,r,m,n,pl,bl,cl,gl,pr,br,tr,dr,kr,gr,fl,sl,fr,thr,str,sk,sp,st".split(
|
|
||||||
",",
|
|
||||||
)
|
|
||||||
|
|
||||||
const generateH = () => {
|
|
||||||
const n = (6 + Math.random() * 2) | 0
|
|
||||||
const s = [consonants.slice(), vowels.slice()]
|
|
||||||
|
|
||||||
if (Math.random() < 0.5) {
|
|
||||||
s.reverse()
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
Array.from({length: n}, (_, i) =>
|
|
||||||
s[i % 2].splice((Math.random() * s[i % 2].length) | 0, 1),
|
|
||||||
).join("") +
|
|
||||||
(1 + Math.floor(Math.random() * 9))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const makeRoomMeta = (room: Partial<RoomMeta> = {}): RoomMeta => ({
|
|
||||||
h: room.h ?? generateH(),
|
|
||||||
...room,
|
|
||||||
})
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
url: string
|
url: string
|
||||||
header: Snippet
|
header: Snippet
|
||||||
@@ -107,22 +58,19 @@
|
|||||||
room.pictureMeta = result.tags
|
room.pictureMeta = result.tags
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(welshman-migration): app.use(Rooms).create/edit/join are async
|
const createMessage = await waitForThunkError(createRoom(url, room))
|
||||||
// (return Promise<Thunk>); the old code passed the un-awaited result to
|
|
||||||
// waitForThunkError. Awaiting the thunk first before .waitForError().
|
|
||||||
const createMessage = await (await app.use(Rooms).create(url, room)).waitForError()
|
|
||||||
|
|
||||||
if (createMessage && !createMessage.includes("already")) {
|
if (createMessage && !createMessage.includes("already")) {
|
||||||
return pushToast({theme: "error", message: createMessage})
|
return pushToast({theme: "error", message: createMessage})
|
||||||
}
|
}
|
||||||
|
|
||||||
const editMessage = await (await app.use(Rooms).edit(url, room)).waitForError()
|
const editMessage = await waitForThunkError(editRoom(url, room))
|
||||||
|
|
||||||
if (editMessage) {
|
if (editMessage) {
|
||||||
return pushToast({theme: "error", message: editMessage})
|
return pushToast({theme: "error", message: editMessage})
|
||||||
}
|
}
|
||||||
|
|
||||||
const joinMessage = await (await app.use(Rooms).join(url, room)).waitForError()
|
const joinMessage = await waitForThunkError(joinRoom(url, room))
|
||||||
|
|
||||||
if (joinMessage && !joinMessage.includes("already")) {
|
if (joinMessage && !joinMessage.includes("already")) {
|
||||||
return pushToast({theme: "error", message: joinMessage})
|
return pushToast({theme: "error", message: joinMessage})
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import Volume from "@assets/icons/volume.svg?dataurl"
|
import Volume from "@assets/icons/volume.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import ImageIcon from "@lib/components/ImageIcon.svelte"
|
import ImageIcon from "@lib/components/ImageIcon.svelte"
|
||||||
|
import {getColor} from "@app/theme"
|
||||||
import {deriveRoom} from "@app/groups"
|
import {deriveRoom} from "@app/groups"
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -16,18 +17,26 @@
|
|||||||
|
|
||||||
const room = deriveRoom(url, h)
|
const room = deriveRoom(url, h)
|
||||||
const isVoiceRoom = $derived($room.livekit)
|
const isVoiceRoom = $derived($room.livekit)
|
||||||
|
const px = size * 4
|
||||||
|
// Voice rooms read warm/orange; text rooms get a per-room identity color.
|
||||||
|
const color = $derived(isVoiceRoom ? "var(--color-secondary)" : getColor(h))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if isVoiceRoom}
|
{#if isVoiceRoom}
|
||||||
<div class="flex shrink-0 items-center gap-1.5">
|
<div class="flex shrink-0 items-center gap-1.5">
|
||||||
<Icon size={size + 1} icon={Volume} />
|
<Icon size={size + 1} icon={Volume} class="text-secondary" />
|
||||||
{#if $room.picture}
|
{#if $room.picture}
|
||||||
<span class="text-base">/</span>
|
<span class="text-base">/</span>
|
||||||
<ImageIcon src={$room.picture} {size} alt="" class="rounded-lg" />
|
<ImageIcon src={$room.picture} {size} alt="" class="squircle shadow-sm" />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{:else if $room.picture}
|
{:else if $room.picture}
|
||||||
<ImageIcon src={$room.picture} {size} alt="" class="rounded-lg" />
|
<ImageIcon src={$room.picture} {size} alt="" class="squircle shadow-sm" />
|
||||||
{:else}
|
{:else}
|
||||||
<Icon icon={fallbackIcon} {size} />
|
<!-- Colored room tile with the type glyph in white. -->
|
||||||
|
<div
|
||||||
|
class="squircle flex shrink-0 items-center justify-center text-white"
|
||||||
|
style="width:{px}px;height:{px}px;background-image:linear-gradient(135deg,{color},color-mix(in oklab,{color},#000 28%))">
|
||||||
|
<Icon icon={fallbackIcon} size={Math.max(3, size - 1)} />
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -1,18 +1,16 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import cx from "classnames"
|
import cx from "classnames"
|
||||||
import {readable} from "svelte/store"
|
import {readable} from "svelte/store"
|
||||||
import {
|
import {gte, now, displayList, formatTimestampAsTime, formatTimestampAsDate} from "@welshman/lib"
|
||||||
hash,
|
|
||||||
gte,
|
|
||||||
now,
|
|
||||||
displayList,
|
|
||||||
formatTimestampAsTime,
|
|
||||||
formatTimestampAsDate,
|
|
||||||
} from "@welshman/lib"
|
|
||||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||||
import {MESSAGE, COMMENT, getTag} from "@welshman/util"
|
import {MESSAGE, COMMENT, getTag} from "@welshman/util"
|
||||||
import {Thunks, Profiles} from "@welshman/app"
|
import {
|
||||||
import {thunks, pubkey, app} from "@app/welshman"
|
thunks,
|
||||||
|
pubkey,
|
||||||
|
mergeThunks,
|
||||||
|
deriveProfileDisplay,
|
||||||
|
displayProfileByPubkey,
|
||||||
|
} from "@welshman/app"
|
||||||
import {isMobile} from "@lib/html"
|
import {isMobile} from "@lib/html"
|
||||||
import Pen from "@assets/icons/pen.svg?dataurl"
|
import Pen from "@assets/icons/pen.svg?dataurl"
|
||||||
import Reply from "@assets/icons/reply-2.svg?dataurl"
|
import Reply from "@assets/icons/reply-2.svg?dataurl"
|
||||||
@@ -30,7 +28,7 @@
|
|||||||
import RoomItemMenuButton from "@app/components/RoomItemMenuButton.svelte"
|
import RoomItemMenuButton from "@app/components/RoomItemMenuButton.svelte"
|
||||||
import RoomItemMenuMobile from "@app/components/RoomItemMenuMobile.svelte"
|
import RoomItemMenuMobile from "@app/components/RoomItemMenuMobile.svelte"
|
||||||
import RoomItemContent from "@app/components/RoomItemContent.svelte"
|
import RoomItemContent from "@app/components/RoomItemContent.svelte"
|
||||||
import {colors} from "@app/theme"
|
import {getColor} from "@app/theme"
|
||||||
import {ENABLE_ZAPS} from "@app/env"
|
import {ENABLE_ZAPS} from "@app/env"
|
||||||
import {deriveEventsForUrl, deriveEvent} from "@app/repository"
|
import {deriveEventsForUrl, deriveEvent} from "@app/repository"
|
||||||
import {publishDelete} from "@app/deletes"
|
import {publishDelete} from "@app/deletes"
|
||||||
@@ -53,9 +51,9 @@
|
|||||||
const path = getRoomItemPath(url, event)
|
const path = getRoomItemPath(url, event)
|
||||||
const shouldProtect = canEnforceNip70(url)
|
const shouldProtect = canEnforceNip70(url)
|
||||||
const today = formatTimestampAsDate(now())
|
const today = formatTimestampAsDate(now())
|
||||||
const profileDisplay = app.use(Profiles).display(event.pubkey, [url]).$
|
const profileDisplay = deriveProfileDisplay(event.pubkey, [url])
|
||||||
const thunk = app.use(Thunks).merge($thunks.filter(t => t.event.id === event.id))
|
const thunk = mergeThunks($thunks.filter(t => t.event.id === event.id))
|
||||||
const [_, colorValue] = colors[hash(event.pubkey) % colors.length]
|
const colorValue = getColor(event.pubkey)
|
||||||
|
|
||||||
const qTag = getTag("q", event.tags)
|
const qTag = getTag("q", event.tags)
|
||||||
const isQuoteOnly = Boolean(
|
const isQuoteOnly = Boolean(
|
||||||
@@ -90,10 +88,7 @@
|
|||||||
<div class="flex w-full gap-3 overflow-auto">
|
<div class="flex w-full gap-3 overflow-auto">
|
||||||
{#if showPubkey}
|
{#if showPubkey}
|
||||||
<Button onclick={openProfile} class="flex items-start pt-1.5 justify-center w-8 shrink-0">
|
<Button onclick={openProfile} class="flex items-start pt-1.5 justify-center w-8 shrink-0">
|
||||||
<ProfileCircle
|
<ProfileCircle pubkey={event.pubkey} style="box-shadow: 0 0 0 2px {colorValue}" size={8} />
|
||||||
pubkey={event.pubkey}
|
|
||||||
class="border border-solid border-base-content"
|
|
||||||
size={8} />
|
|
||||||
</Button>
|
</Button>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="w-8 shrink-0"></div>
|
<div class="w-8 shrink-0"></div>
|
||||||
@@ -133,7 +128,7 @@
|
|||||||
{#if path && $innerComments.length > 0}
|
{#if path && $innerComments.length > 0}
|
||||||
{@const pubkeys = $innerComments.map(e => e.pubkey)}
|
{@const pubkeys = $innerComments.map(e => e.pubkey)}
|
||||||
{@const isOwn = $pubkey && pubkeys.includes($pubkey)}
|
{@const isOwn = $pubkey && pubkeys.includes($pubkey)}
|
||||||
{@const info = displayList(pubkeys.map(pubkey => app.use(Profiles).display(pubkey).get()))}
|
{@const info = displayList(pubkeys.map(pubkey => displayProfileByPubkey(pubkey)))}
|
||||||
{@const tooltip = `${info} commented`}
|
{@const tooltip = `${info} commented`}
|
||||||
<div data-tip={tooltip} class="tooltip tooltip-right flex">
|
<div data-tip={tooltip} class="tooltip tooltip-right flex">
|
||||||
<Link
|
<Link
|
||||||
@@ -150,8 +145,9 @@
|
|||||||
</div>
|
</div>
|
||||||
{#if !isMobile}
|
{#if !isMobile}
|
||||||
<button
|
<button
|
||||||
class="join absolute right-2 top-0.5 border border-solid border-neutral text-xs opacity-0 transition-all pr-2"
|
class="join bg-base-100 shadow-soft absolute right-2 top-0.5 translate-y-1 rounded-full p-0.5 text-xs opacity-0 transition-all"
|
||||||
class:group-hover:opacity-100={!isMobile}>
|
class:group-hover:opacity-100={!isMobile}
|
||||||
|
class:group-hover:translate-y-0={!isMobile}>
|
||||||
{#if ENABLE_ZAPS}
|
{#if ENABLE_ZAPS}
|
||||||
<RoomItemZapButton {url} {event} />
|
<RoomItemZapButton {url} {event} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {ManagementMethod} from "@welshman/util"
|
import {ManagementMethod} from "@welshman/util"
|
||||||
import {RelayManagement} from "@welshman/app"
|
import {pubkey, manageRelay, repository} from "@welshman/app"
|
||||||
import {pubkey, repository, app} from "@app/welshman"
|
|
||||||
import Code2 from "@assets/icons/code-2.svg?dataurl"
|
import Code2 from "@assets/icons/code-2.svg?dataurl"
|
||||||
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
|
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
|
||||||
import Danger from "@assets/icons/danger.svg?dataurl"
|
import Danger from "@assets/icons/danger.svg?dataurl"
|
||||||
@@ -46,7 +45,7 @@
|
|||||||
title: `Delete Message`,
|
title: `Delete Message`,
|
||||||
message: `Are you sure you want to delete this message from the space?`,
|
message: `Are you sure you want to delete this message from the space?`,
|
||||||
confirm: async () => {
|
confirm: async () => {
|
||||||
const {error} = await app.use(RelayManagement).post(url, {
|
const {error} = await manageRelay(url, {
|
||||||
method: ManagementMethod.BanEvent,
|
method: ManagementMethod.BanEvent,
|
||||||
params: [event.id],
|
params: [event.id],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type {NativeEmoji} from "emoji-picker-element/shared"
|
import type {NativeEmoji} from "emoji-picker-element/shared"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {pubkey} from "@app/welshman"
|
import {pubkey} from "@welshman/app"
|
||||||
import Bolt from "@assets/icons/bolt.svg?dataurl"
|
import Bolt from "@assets/icons/bolt.svg?dataurl"
|
||||||
import Reply from "@assets/icons/reply-2.svg?dataurl"
|
import Reply from "@assets/icons/reply-2.svg?dataurl"
|
||||||
import Code2 from "@assets/icons/code-2.svg?dataurl"
|
import Code2 from "@assets/icons/code-2.svg?dataurl"
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {getTagValue, ManagementMethod} from "@welshman/util"
|
import {getTagValue, ManagementMethod} from "@welshman/util"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent, PublishedRoomMeta} from "@welshman/util"
|
||||||
import {RelayManagement} from "@welshman/app"
|
import {repository, manageRelay} from "@welshman/app"
|
||||||
import {app, repository} from "@app/welshman"
|
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import ProfileName from "@app/components/ProfileName.svelte"
|
import ProfileName from "@app/components/ProfileName.svelte"
|
||||||
import ProfileDetail from "@app/components/ProfileDetail.svelte"
|
import ProfileDetail from "@app/components/ProfileDetail.svelte"
|
||||||
@@ -29,7 +28,7 @@
|
|||||||
loading = true
|
loading = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const {error} = await app.use(RelayManagement).post(url, {
|
const {error} = await manageRelay(url, {
|
||||||
method: ManagementMethod.BanEvent,
|
method: ManagementMethod.BanEvent,
|
||||||
params: [event.id, "Join request dismissed"],
|
params: [event.id, "Join request dismissed"],
|
||||||
})
|
})
|
||||||
@@ -50,11 +49,7 @@
|
|||||||
loading = true
|
loading = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// TODO(welshman-migration): addRoomMembers now expects a RoomMeta domain
|
const error = await addRoomMembers(url, $room as PublishedRoomMeta, [event.pubkey])
|
||||||
// Reader, but deriveRoom yields the plain `Room` object. Passing it through
|
|
||||||
// as `any` preserves prior runtime behavior; verify Rooms.addMember accepts
|
|
||||||
// the plain Room shape (url/h/...) or resolve a real RoomMeta reader here.
|
|
||||||
const error = await addRoomMembers(url, $room as any, [event.pubkey])
|
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
pushToast({theme: "error", message: error})
|
pushToast({theme: "error", message: error})
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {Rooms} from "@welshman/app"
|
import {waitForThunkError, removeRoomMember} from "@welshman/app"
|
||||||
import {app} from "@app/welshman"
|
|
||||||
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
|
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
|
||||||
import MinusCircle from "@assets/icons/minus-circle.svg?dataurl"
|
import MinusCircle from "@assets/icons/minus-circle.svg?dataurl"
|
||||||
import AddCircle from "@assets/icons/add-circle.svg?dataurl"
|
import AddCircle from "@assets/icons/add-circle.svg?dataurl"
|
||||||
@@ -52,8 +51,7 @@
|
|||||||
title: "Remove Member",
|
title: "Remove Member",
|
||||||
message: "Are you sure you want to remove this user from the room?",
|
message: "Are you sure you want to remove this user from the room?",
|
||||||
confirm: async () => {
|
confirm: async () => {
|
||||||
const thunk = await app.use(Rooms).removeMember(url, $room, pubkey)
|
const error = await waitForThunkError(removeRoomMember(url, $room, pubkey))
|
||||||
const error = await thunk.waitForError()
|
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
pushToast({theme: "error", message: error})
|
pushToast({theme: "error", message: error})
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import {setKey, popKey} from "@lib/implicit"
|
import {setKey, popKey} from "@lib/implicit"
|
||||||
import {sleep} from "@welshman/lib"
|
import {sleep} from "@welshman/lib"
|
||||||
import {Profiles} from "@welshman/app"
|
import {displayProfileByPubkey} from "@welshman/app"
|
||||||
import {app} from "@app/welshman"
|
import type {PublishedRoomMeta} from "@welshman/util"
|
||||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
@@ -42,11 +42,7 @@
|
|||||||
// Show loading for auto submit callback
|
// Show loading for auto submit callback
|
||||||
await sleep(500)
|
await sleep(500)
|
||||||
|
|
||||||
// TODO(welshman-migration): addRoomMembers now expects a RoomMeta domain
|
const error = await addRoomMembers(url, $room as PublishedRoomMeta, pubkeys)
|
||||||
// Reader, but deriveRoom yields the plain `Room` object. Passing it through
|
|
||||||
// as `any` preserves prior runtime behavior; verify Rooms.addMember accepts
|
|
||||||
// the plain Room shape (url/h/...) or resolve a real RoomMeta reader here.
|
|
||||||
const error = await addRoomMembers(url, $room as any, pubkeys)
|
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
pushToast({theme: "error", message: error})
|
pushToast({theme: "error", message: error})
|
||||||
@@ -76,7 +72,7 @@
|
|||||||
subtitle: "Automatically add members to space",
|
subtitle: "Automatically add members to space",
|
||||||
message:
|
message:
|
||||||
nonSpaceMembers.length === 1
|
nonSpaceMembers.length === 1
|
||||||
? `${app.use(Profiles).display(nonSpaceMembers[0]).get()} is not a member of this space. Add them?`
|
? `${displayProfileByPubkey(nonSpaceMembers[0])} is not a member of this space. Add them?`
|
||||||
: `${nonSpaceMembers.length} people are not members of this space. Add them?`,
|
: `${nonSpaceMembers.length} people are not members of this space. Add them?`,
|
||||||
confirm: async () => {
|
confirm: async () => {
|
||||||
setKey("RoomMembersAdd.confirm", true)
|
setKey("RoomMembersAdd.confirm", true)
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type {ClientOptions} from "@pomade/core"
|
import type {ClientOptions} from "@pomade/core"
|
||||||
import type {Profile} from "@welshman/domain"
|
import type {Profile} from "@welshman/util"
|
||||||
import {makeSecret, RELAYS, MESSAGING_RELAYS, makeEvent} from "@welshman/util"
|
import {makeProfile, makeSecret, RELAYS, MESSAGING_RELAYS, makeEvent} from "@welshman/util"
|
||||||
import {ProfileBuilder} from "@welshman/domain"
|
import {loginWithNip01, publishThunk} from "@welshman/app"
|
||||||
import {Thunks} from "@welshman/app"
|
|
||||||
import {app, loginWithNip01} from "@app/welshman"
|
|
||||||
import Key from "@assets/icons/key-minimalistic.svg?dataurl"
|
import Key from "@assets/icons/key-minimalistic.svg?dataurl"
|
||||||
import Letter from "@assets/icons/letter.svg?dataurl"
|
import Letter from "@assets/icons/letter.svg?dataurl"
|
||||||
import {getKey, setKey} from "@lib/implicit"
|
import {getKey, setKey} from "@lib/implicit"
|
||||||
@@ -33,7 +31,7 @@
|
|||||||
|
|
||||||
setKey("signup.email", "")
|
setKey("signup.email", "")
|
||||||
setKey("signup.secret", makeSecret())
|
setKey("signup.secret", makeSecret())
|
||||||
setKey("signup.profile", new ProfileBuilder().values)
|
setKey("signup.profile", makeProfile())
|
||||||
setKey("signup.clientOptions", undefined)
|
setKey("signup.clientOptions", undefined)
|
||||||
|
|
||||||
const hasPomade = POMADE_SIGNERS.length >= 3
|
const hasPomade = POMADE_SIGNERS.length >= 3
|
||||||
@@ -42,13 +40,13 @@
|
|||||||
|
|
||||||
const completeSignup = () => {
|
const completeSignup = () => {
|
||||||
// Add default outbox/inbox relays
|
// Add default outbox/inbox relays
|
||||||
app.use(Thunks).publish({
|
publishThunk({
|
||||||
event: makeEvent(RELAYS, {tags: DEFAULT_RELAYS.map(url => ["r", url])}),
|
event: makeEvent(RELAYS, {tags: DEFAULT_RELAYS.map(url => ["r", url])}),
|
||||||
relays: [...INDEXER_RELAYS, ...DEFAULT_RELAYS],
|
relays: [...INDEXER_RELAYS, ...DEFAULT_RELAYS],
|
||||||
})
|
})
|
||||||
|
|
||||||
// Add default messaging relays
|
// Add default messaging relays
|
||||||
app.use(Thunks).publish({
|
publishThunk({
|
||||||
event: makeEvent(MESSAGING_RELAYS, {tags: DEFAULT_MESSAGING_RELAYS.map(url => ["r", url])}),
|
event: makeEvent(MESSAGING_RELAYS, {tags: DEFAULT_MESSAGING_RELAYS.map(url => ["r", url])}),
|
||||||
relays: DEFAULT_RELAYS,
|
relays: DEFAULT_RELAYS,
|
||||||
})
|
})
|
||||||
@@ -85,10 +83,10 @@
|
|||||||
key: () => pushModal(SignUpKey, {next: flows.nostr.complete, step: 2, totalSteps: 3}),
|
key: () => pushModal(SignUpKey, {next: flows.nostr.complete, step: 2, totalSteps: 3}),
|
||||||
complete: () =>
|
complete: () =>
|
||||||
pushModal(SignUpComplete, {next: flows.nostr.finalize, step: 3, totalSteps: 3}),
|
pushModal(SignUpComplete, {next: flows.nostr.finalize, step: 3, totalSteps: 3}),
|
||||||
finalize: async () => {
|
finalize: () => {
|
||||||
const secret = getKey<string>("signup.secret")!
|
const secret = getKey<string>("signup.secret")!
|
||||||
|
|
||||||
await loginWithNip01(secret)
|
loginWithNip01(secret)
|
||||||
completeSignup()
|
completeSignup()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -97,17 +95,19 @@
|
|||||||
|
|
||||||
<Modal>
|
<Modal>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<h1 class="heading">Join {PLATFORM_NAME}</h1>
|
<h1 class="heading">Join <span class="brand">{PLATFORM_NAME}</span></h1>
|
||||||
<p class="m-auto max-w-sm text-center">
|
<p class="m-auto max-w-sm text-center">
|
||||||
Censorship resistant digital spaces for communities. Meet new people, own your identity.
|
Censorship resistant digital spaces for communities. Meet new people, own your identity.
|
||||||
</p>
|
</p>
|
||||||
{#if hasPomade}
|
{#if hasPomade}
|
||||||
<Button onclick={flows.email.start} class="btn btn-primary">
|
<Button onclick={flows.email.start} class="btn btn-primary rounded-full">
|
||||||
<Icon icon={Letter} />
|
<Icon icon={Letter} />
|
||||||
Sign up with email
|
Sign up with email
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
<Button onclick={flows.nostr.start} class="btn {hasPomade ? 'btn-neutral' : 'btn-primary'}">
|
<Button
|
||||||
|
onclick={flows.nostr.start}
|
||||||
|
class="btn rounded-full {hasPomade ? 'btn-neutral' : 'btn-primary'}">
|
||||||
<Icon icon={Key} />
|
<Icon icon={Key} />
|
||||||
Generate a key
|
Generate a key
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -2,12 +2,11 @@
|
|||||||
import {preventDefault} from "@lib/html"
|
import {preventDefault} from "@lib/html"
|
||||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
import HomeSmile from "@assets/icons/home-smile.svg?dataurl"
|
import HomeSmile from "@assets/icons/home-smile.svg?dataurl"
|
||||||
|
import CheckCircle from "@assets/icons/check-circle.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Modal from "@lib/components/Modal.svelte"
|
import Modal from "@lib/components/Modal.svelte"
|
||||||
import ModalBody from "@lib/components/ModalBody.svelte"
|
import ModalBody from "@lib/components/ModalBody.svelte"
|
||||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
|
||||||
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import ProgressBar from "@app/components/ProgressBar.svelte"
|
import ProgressBar from "@app/components/ProgressBar.svelte"
|
||||||
|
|
||||||
@@ -24,9 +23,12 @@
|
|||||||
|
|
||||||
<Modal tag="form" onsubmit={preventDefault(next)}>
|
<Modal tag="form" onsubmit={preventDefault(next)}>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<ModalHeader>
|
<div class="flex flex-col items-center gap-3">
|
||||||
<ModalTitle>You're all set!</ModalTitle>
|
<div class="center bg-primary/15 text-primary size-16 rounded-full motion-safe:animate-pop">
|
||||||
</ModalHeader>
|
<Icon icon={CheckCircle} size={9} />
|
||||||
|
</div>
|
||||||
|
<h1 class="heading">You're all set!</h1>
|
||||||
|
</div>
|
||||||
<p>
|
<p>
|
||||||
You've created your profile, saved your keys, and now you're ready to start chatting — all
|
You've created your profile, saved your keys, and now you're ready to start chatting — all
|
||||||
without asking permission!
|
without asking permission!
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {spec, avg} from "@welshman/lib"
|
import {spec, avg} from "@welshman/lib"
|
||||||
import {session, SessionMethod, signerLog} from "@app/welshman"
|
import {session, SessionMethod, signerLog} from "@welshman/app"
|
||||||
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
|
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
|
||||||
import Danger from "@assets/icons/danger-triangle.svg?dataurl"
|
import Danger from "@assets/icons/danger-triangle.svg?dataurl"
|
||||||
import ClockCircle from "@assets/icons/clock-circle.svg?dataurl"
|
import ClockCircle from "@assets/icons/clock-circle.svg?dataurl"
|
||||||
|
|||||||
@@ -29,22 +29,20 @@
|
|||||||
<Button onclick={back} class="place-self-start pr-3 md:hidden">
|
<Button onclick={back} class="place-self-start pr-3 md:hidden">
|
||||||
<Icon icon={ArrowLeft} size={7} />
|
<Icon icon={ArrowLeft} size={7} />
|
||||||
</Button>
|
</Button>
|
||||||
<div class="flex grow items-center justify-between gap-4">
|
<div class="ellipsize whitespace-nowrap flex grow items-center justify-between gap-4">
|
||||||
<div class="flex min-w-0 flex-col">
|
<div class="flex flex-col">
|
||||||
<div class="flex min-w-0 items-start gap-2">
|
<div class="flex gap-2 items-center">
|
||||||
<RelayIcon {url} size={5} class="shrink-0 rounded-full md:hidden" />
|
<RelayIcon {url} size={5} class="rounded-full md:hidden" />
|
||||||
<div class="hidden shrink-0 md:flex md:items-center">
|
<div class="hidden md:contents">
|
||||||
{@render leading?.()}
|
{@render leading?.()}
|
||||||
</div>
|
</div>
|
||||||
<div class="min-w-0">
|
{@render title?.()}
|
||||||
{@render title?.()}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs text-primary pl-7 md:hidden">
|
<div class="text-xs text-primary md:hidden">
|
||||||
{displayRelayUrl(url)}
|
{displayRelayUrl(url)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex shrink-0 items-center gap-2">
|
<div class="flex gap-2 items-start">
|
||||||
{@render action?.()}
|
{@render action?.()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {displayRelayUrl} from "@welshman/util"
|
import {displayRelayUrl} from "@welshman/util"
|
||||||
import {Relays} from "@welshman/app"
|
import {deriveRelay} from "@welshman/app"
|
||||||
import {app} from "@app/welshman"
|
|
||||||
import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
|
import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
|
||||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
import Pen from "@assets/icons/pen.svg?dataurl"
|
import Pen from "@assets/icons/pen.svg?dataurl"
|
||||||
@@ -27,7 +26,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const {url}: Props = $props()
|
const {url}: Props = $props()
|
||||||
const relay = app.use(Relays).one(url)
|
const relay = deriveRelay(url)
|
||||||
const owner = $derived($relay?.pubkey)
|
const owner = $derived($relay?.pubkey)
|
||||||
const userIsAdmin = deriveUserIsSpaceAdmin(url)
|
const userIsAdmin = deriveUserIsSpaceAdmin(url)
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type {RelayProfile} from "@welshman/util"
|
import type {RelayProfile} from "@welshman/util"
|
||||||
import {displayRelayUrl, ManagementMethod} from "@welshman/util"
|
import {displayRelayUrl, ManagementMethod} from "@welshman/util"
|
||||||
import {RelayManagement, Relays} from "@welshman/app"
|
import {manageRelay, forceLoadRelay} from "@welshman/app"
|
||||||
import {app} from "@app/welshman"
|
|
||||||
import StickerSmileSquare from "@assets/icons/sticker-smile-square.svg?dataurl"
|
import StickerSmileSquare from "@assets/icons/sticker-smile-square.svg?dataurl"
|
||||||
import Widget from "@assets/icons/widget-4.svg?dataurl"
|
import Widget from "@assets/icons/widget-4.svg?dataurl"
|
||||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
@@ -37,7 +36,7 @@
|
|||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
if (values.name != initialValues.name) {
|
if (values.name != initialValues.name) {
|
||||||
const res = await app.use(RelayManagement).post(url, {
|
const res = await manageRelay(url, {
|
||||||
method: ManagementMethod.ChangeRelayName,
|
method: ManagementMethod.ChangeRelayName,
|
||||||
params: [values.name || ""],
|
params: [values.name || ""],
|
||||||
})
|
})
|
||||||
@@ -48,7 +47,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (values.description != initialValues.description) {
|
if (values.description != initialValues.description) {
|
||||||
const res = await app.use(RelayManagement).post(url, {
|
const res = await manageRelay(url, {
|
||||||
method: ManagementMethod.ChangeRelayDescription,
|
method: ManagementMethod.ChangeRelayDescription,
|
||||||
params: [values.description || ""],
|
params: [values.description || ""],
|
||||||
})
|
})
|
||||||
@@ -65,7 +64,7 @@
|
|||||||
return pushToast({theme: "error", message: error})
|
return pushToast({theme: "error", message: error})
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await app.use(RelayManagement).post(url, {
|
const res = await manageRelay(url, {
|
||||||
method: ManagementMethod.ChangeRelayIcon,
|
method: ManagementMethod.ChangeRelayIcon,
|
||||||
params: [result.url],
|
params: [result.url],
|
||||||
})
|
})
|
||||||
@@ -76,7 +75,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
pushToast({message: "Your changes have been saved!"})
|
pushToast({message: "Your changes have been saved!"})
|
||||||
app.use(Relays).forceLoad(url)
|
forceLoadRelay(url)
|
||||||
clearModals()
|
clearModals()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {debounce} from "throttle-debounce"
|
import {debounce} from "throttle-debounce"
|
||||||
import {dissoc, maybe} from "@welshman/lib"
|
import {dissoc, maybe} from "@welshman/lib"
|
||||||
|
import {goto} from "$app/navigation"
|
||||||
import {preventDefault} from "@lib/html"
|
import {preventDefault} from "@lib/html"
|
||||||
import {slideAndFade} from "@lib/transition"
|
import {slideAndFade} from "@lib/transition"
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
@@ -21,7 +22,7 @@
|
|||||||
import RelaySummary from "@app/components/RelaySummary.svelte"
|
import RelaySummary from "@app/components/RelaySummary.svelte"
|
||||||
import SpaceJoinSettings from "@app/components/SpaceJoinSettings.svelte"
|
import SpaceJoinSettings from "@app/components/SpaceJoinSettings.svelte"
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/toast"
|
||||||
import {goToSpace} from "@app/routes"
|
import {makeSpacePath} from "@app/routes"
|
||||||
import {relaysMostlyRestricted} from "@app/policies"
|
import {relaysMostlyRestricted} from "@app/policies"
|
||||||
import {notificationSettings, setSpaceNotifications} from "@app/settings"
|
import {notificationSettings, setSpaceNotifications} from "@app/settings"
|
||||||
import {parseInviteLink} from "@app/invites"
|
import {parseInviteLink} from "@app/invites"
|
||||||
@@ -67,7 +68,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
await addSpace(url)
|
await addSpace(url)
|
||||||
await goToSpace(url)
|
await goto(makeSpacePath(url), {replaceState: true})
|
||||||
|
|
||||||
broadcastUserData([url])
|
broadcastUserData([url])
|
||||||
relaysMostlyRestricted.update(dissoc(url))
|
relaysMostlyRestricted.update(dissoc(url))
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
|
import {goto} from "$app/navigation"
|
||||||
import {dissoc, maybe} from "@welshman/lib"
|
import {dissoc, maybe} from "@welshman/lib"
|
||||||
import {preventDefault} from "@lib/html"
|
import {preventDefault} from "@lib/html"
|
||||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
@@ -21,7 +22,7 @@
|
|||||||
import {notificationSettings} from "@app/settings"
|
import {notificationSettings} from "@app/settings"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/toast"
|
||||||
import {goToSpace} from "@app/routes"
|
import {makeSpacePath} from "@app/routes"
|
||||||
import {Push} from "@app/push"
|
import {Push} from "@app/push"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -55,7 +56,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
await addSpace(url)
|
await addSpace(url)
|
||||||
await goToSpace(url)
|
await goto(makeSpacePath(url), {replaceState: true})
|
||||||
|
|
||||||
broadcastUserData([url])
|
broadcastUserData([url])
|
||||||
relaysMostlyRestricted.update(dissoc(url))
|
relaysMostlyRestricted.update(dissoc(url))
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {ManagementMethod} from "@welshman/util"
|
import {ManagementMethod} from "@welshman/util"
|
||||||
import {RelayManagement, Profiles} from "@welshman/app"
|
import {manageRelay, displayProfileByPubkey} from "@welshman/app"
|
||||||
import {app} from "@app/welshman"
|
|
||||||
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
|
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
|
||||||
import UserMinus from "@assets/icons/user-minus.svg?dataurl"
|
import UserMinus from "@assets/icons/user-minus.svg?dataurl"
|
||||||
import MinusCircle from "@assets/icons/minus-circle.svg?dataurl"
|
import MinusCircle from "@assets/icons/minus-circle.svg?dataurl"
|
||||||
@@ -61,9 +60,9 @@
|
|||||||
const unallowMember = (pubkey: string) =>
|
const unallowMember = (pubkey: string) =>
|
||||||
pushModal(Confirm, {
|
pushModal(Confirm, {
|
||||||
title: "Remove User",
|
title: "Remove User",
|
||||||
message: `Are you sure you want to remove @${app.use(Profiles).display(pubkey).get()} from the space?`,
|
message: `Are you sure you want to remove @${displayProfileByPubkey(pubkey)} from the space?`,
|
||||||
confirm: async () => {
|
confirm: async () => {
|
||||||
const {error} = await app.use(RelayManagement).post(url, {
|
const {error} = await manageRelay(url, {
|
||||||
method: ManagementMethod.UnallowPubkey,
|
method: ManagementMethod.UnallowPubkey,
|
||||||
params: [pubkey],
|
params: [pubkey],
|
||||||
})
|
})
|
||||||
@@ -80,9 +79,9 @@
|
|||||||
const banMember = (pubkey: string) =>
|
const banMember = (pubkey: string) =>
|
||||||
pushModal(Confirm, {
|
pushModal(Confirm, {
|
||||||
title: "Ban User",
|
title: "Ban User",
|
||||||
message: `Are you sure you want to ban @${app.use(Profiles).display(pubkey).get()} from the space?`,
|
message: `Are you sure you want to ban @${displayProfileByPubkey(pubkey)} from the space?`,
|
||||||
confirm: async () => {
|
confirm: async () => {
|
||||||
const {error} = await app.use(RelayManagement).post(url, {
|
const {error} = await manageRelay(url, {
|
||||||
method: ManagementMethod.BanPubkey,
|
method: ManagementMethod.BanPubkey,
|
||||||
params: [pubkey],
|
params: [pubkey],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {displayRelayUrl, ManagementMethod} from "@welshman/util"
|
import {displayRelayUrl, ManagementMethod} from "@welshman/util"
|
||||||
import {RelayManagement} from "@welshman/app"
|
import {manageRelay} from "@welshman/app"
|
||||||
import {app} from "@app/welshman"
|
|
||||||
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
|
import MenuDots from "@assets/icons/menu-dots.svg?dataurl"
|
||||||
import Restart from "@assets/icons/restart.svg?dataurl"
|
import Restart from "@assets/icons/restart.svg?dataurl"
|
||||||
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
|
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
|
||||||
@@ -43,7 +42,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const unbanMember = async (pubkey: string) => {
|
const unbanMember = async (pubkey: string) => {
|
||||||
const {error} = await app.use(RelayManagement).post(url, {
|
const {error} = await manageRelay(url, {
|
||||||
method: ManagementMethod.UnbanPubkey,
|
method: ManagementMethod.UnbanPubkey,
|
||||||
params: [pubkey],
|
params: [pubkey],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {derived} from "svelte/store"
|
import {derived} from "svelte/store"
|
||||||
import {displayRelayUrl, EVENT_TIME, ZAP_GOAL, THREAD, CLASSIFIED, POLL} from "@welshman/util"
|
import {displayRelayUrl, EVENT_TIME, ZAP_GOAL, THREAD, CLASSIFIED, POLL} from "@welshman/util"
|
||||||
import {Relays} from "@welshman/app"
|
import {deriveRelay, createSearch, pubkey} from "@welshman/app"
|
||||||
import {app, createSearch, pubkey} from "@app/welshman"
|
|
||||||
import {fly} from "@lib/transition"
|
import {fly} from "@lib/transition"
|
||||||
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
|
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
|
||||||
import AltArrowDown from "@assets/icons/alt-arrow-down.svg?dataurl"
|
import AltArrowDown from "@assets/icons/alt-arrow-down.svg?dataurl"
|
||||||
@@ -61,7 +60,7 @@
|
|||||||
|
|
||||||
const {url} = $props()
|
const {url} = $props()
|
||||||
|
|
||||||
const relay = app.use(Relays).one(url)
|
const relay = deriveRelay(url)
|
||||||
const chatPath = makeSpacePath(url, "chat")
|
const chatPath = makeSpacePath(url, "chat")
|
||||||
const goalsPath = makeSpacePath(url, "goals")
|
const goalsPath = makeSpacePath(url, "goals")
|
||||||
const threadsPath = makeSpacePath(url, "threads")
|
const threadsPath = makeSpacePath(url, "threads")
|
||||||
@@ -140,7 +139,7 @@
|
|||||||
class="relative flex w-full flex-col rounded-xl p-3 transition-all hover:bg-base-100"
|
class="relative flex w-full flex-col rounded-xl p-3 transition-all hover:bg-base-100"
|
||||||
onclick={openMenu}>
|
onclick={openMenu}>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<strong class="flex items-center gap-1 relative">
|
<strong class="font-display relative flex items-center gap-1">
|
||||||
<RelayName {url} class="ellipsize" />
|
<RelayName {url} class="ellipsize" />
|
||||||
<div
|
<div
|
||||||
class="absolute -right-3 top-0 h-2 w-2 rounded-full bg-primary transition-all opacity-0"
|
class="absolute -right-3 top-0 h-2 w-2 rounded-full bg-primary transition-all opacity-0"
|
||||||
@@ -312,7 +311,7 @@
|
|||||||
<div
|
<div
|
||||||
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">
|
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 />
|
<VoiceWidget />
|
||||||
<Button class="btn btn-neutral btn-sm h-10" onclick={showDetail}>
|
<Button class="btn btn-ghost btn-sm bg-base-100 h-10 rounded-full" onclick={showDetail}>
|
||||||
<SocketStatusIndicator {url} />
|
<SocketStatusIndicator {url} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {Relays} from "@welshman/app"
|
import {deriveRelay} from "@welshman/app"
|
||||||
import {app} from "@app/welshman"
|
|
||||||
import Server from "@assets/icons/server.svg?dataurl"
|
import Server from "@assets/icons/server.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import ProfileLink from "@app/components/ProfileLink.svelte"
|
import ProfileLink from "@app/components/ProfileLink.svelte"
|
||||||
@@ -12,7 +11,7 @@
|
|||||||
|
|
||||||
const {url}: Props = $props()
|
const {url}: Props = $props()
|
||||||
|
|
||||||
const relay = app.use(Relays).one(url)
|
const relay = deriveRelay(url)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="card2 bg-alt flex flex-col gap-4">
|
<div class="card2 bg-alt flex flex-col gap-4">
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import {tick} from "svelte"
|
import {tick} from "svelte"
|
||||||
import {debounce} from "throttle-debounce"
|
import {debounce} from "throttle-debounce"
|
||||||
import {request} from "@welshman/net"
|
import {request} from "@welshman/net"
|
||||||
import {repository, tracker} from "@app/welshman"
|
import {repository, tracker} from "@welshman/app"
|
||||||
import {formatTimestampAsDate, groupBy, uniqBy, now, MINUTE, HOUR, DAY, WEEK} from "@welshman/lib"
|
import {formatTimestampAsDate, groupBy, uniqBy, now, MINUTE, HOUR, DAY, WEEK} from "@welshman/lib"
|
||||||
import type {TrustedEvent, Filter} from "@welshman/util"
|
import type {TrustedEvent, Filter} from "@welshman/util"
|
||||||
import {MESSAGE, sortEventsDesc} from "@welshman/util"
|
import {MESSAGE, sortEventsDesc} from "@welshman/util"
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
|
||||||
import RoomName from "@app/components/RoomName.svelte"
|
|
||||||
import ThreadBoardItem from "@app/components/ThreadBoardItem.svelte"
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
url: string
|
|
||||||
h: string
|
|
||||||
threads: TrustedEvent[]
|
|
||||||
}
|
|
||||||
|
|
||||||
const {url, h, threads}: Props = $props()
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<section class="overflow-hidden rounded-box border border-base-content/15 bg-base-100 shadow-sm">
|
|
||||||
<header
|
|
||||||
class="flex items-center justify-between gap-2 border-b border-base-content/15 bg-base-200/70 px-4 py-2.5">
|
|
||||||
<h2 class="text-sm font-bold sm:text-base">
|
|
||||||
{#if h}
|
|
||||||
#<RoomName {url} {h} />
|
|
||||||
{:else}
|
|
||||||
General
|
|
||||||
{/if}
|
|
||||||
</h2>
|
|
||||||
<span class="text-xs opacity-60">
|
|
||||||
{threads.length}
|
|
||||||
{threads.length === 1 ? "topic" : "topics"}
|
|
||||||
</span>
|
|
||||||
</header>
|
|
||||||
<div
|
|
||||||
class="hidden border-b border-base-content/10 bg-base-200/40 px-4 py-2 text-xs font-bold uppercase tracking-wide opacity-60 sm:grid sm:grid-cols-[1fr_8rem_5rem_8rem] sm:gap-x-4">
|
|
||||||
<span>Topic</span>
|
|
||||||
<span>Author</span>
|
|
||||||
<span class="text-center">Replies</span>
|
|
||||||
<span class="text-right">Last post</span>
|
|
||||||
</div>
|
|
||||||
{#each threads as event (event.id)}
|
|
||||||
<ThreadBoardItem {url} {event} />
|
|
||||||
{/each}
|
|
||||||
</section>
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import {formatTimestamp, max} from "@welshman/lib"
|
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
|
||||||
import {COMMENT, getTagValue} from "@welshman/util"
|
|
||||||
import Link from "@lib/components/Link.svelte"
|
|
||||||
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
|
||||||
import ProfileName from "@app/components/ProfileName.svelte"
|
|
||||||
import {deriveEventsForUrl} from "@app/repository"
|
|
||||||
import {makeThreadPath} from "@app/routes"
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
url: string
|
|
||||||
event: TrustedEvent
|
|
||||||
}
|
|
||||||
|
|
||||||
const {url, event}: Props = $props()
|
|
||||||
|
|
||||||
const filters = [{kinds: [COMMENT], "#E": [event.id]}]
|
|
||||||
const replies = deriveEventsForUrl(url, filters)
|
|
||||||
const replyCount = $derived($replies.length)
|
|
||||||
const lastActive = $derived(max([...$replies, event].map(e => e.created_at)))
|
|
||||||
const title = getTagValue("title", event.tags)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Link
|
|
||||||
href={makeThreadPath(url, event.id)}
|
|
||||||
class="grid grid-cols-[1fr_auto] gap-x-3 gap-y-1 border-b border-base-content/10 px-3 py-3 transition-colors hover:bg-base-200/50 sm:grid-cols-[1fr_8rem_5rem_8rem] sm:items-center sm:gap-x-4 sm:px-4">
|
|
||||||
<div class="col-span-2 min-w-0 sm:col-span-1">
|
|
||||||
<p class="ellipsize text-sm font-bold sm:text-base">{title || "Untitled thread"}</p>
|
|
||||||
<p class="ellipsize mt-0.5 text-xs opacity-60 sm:hidden">
|
|
||||||
by <ProfileName pubkey={event.pubkey} {url} />
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="hidden items-center gap-2 sm:flex">
|
|
||||||
<ProfileCircle pubkey={event.pubkey} {url} size={6} />
|
|
||||||
<span class="ellipsize text-sm">
|
|
||||||
<ProfileName pubkey={event.pubkey} {url} />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<p class="text-right text-xs opacity-75 sm:text-center sm:text-sm">
|
|
||||||
<span class="opacity-60 sm:hidden">Replies · </span>
|
|
||||||
{replyCount}
|
|
||||||
</p>
|
|
||||||
<p class="text-right text-xs opacity-75 sm:text-sm">
|
|
||||||
<span class="opacity-60 sm:hidden">Last · </span>
|
|
||||||
{formatTimestamp(lastActive)}
|
|
||||||
</p>
|
|
||||||
</Link>
|
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {writable} from "svelte/store"
|
import {writable} from "svelte/store"
|
||||||
import {makeEvent, THREAD} from "@welshman/util"
|
import {makeEvent, THREAD} from "@welshman/util"
|
||||||
import {Thunks} from "@welshman/app"
|
import {publishThunk, waitForThunkError} from "@welshman/app"
|
||||||
import {app} from "@app/welshman"
|
|
||||||
import {isMobile, preventDefault} from "@lib/html"
|
import {isMobile, preventDefault} from "@lib/html"
|
||||||
import Paperclip from "@assets/icons/paperclip-2.svg?dataurl"
|
import Paperclip from "@assets/icons/paperclip-2.svg?dataurl"
|
||||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
@@ -80,12 +79,12 @@
|
|||||||
tags.push(["h", h])
|
tags.push(["h", h])
|
||||||
}
|
}
|
||||||
|
|
||||||
const threadThunk = app.use(Thunks).publish({
|
const threadThunk = publishThunk({
|
||||||
relays: [url],
|
relays: [url],
|
||||||
event: makeEvent(THREAD, {content, tags}),
|
event: makeEvent(THREAD, {content, tags}),
|
||||||
})
|
})
|
||||||
|
|
||||||
const error = await threadThunk.waitForError()
|
const error = await waitForThunkError(threadThunk)
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return pushToast({theme: "error", message: error})
|
return pushToast({theme: "error", message: error})
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user