Auto-cleanup
This commit is contained in:
parent
4c21de57e2
commit
5ecb811b24
35 changed files with 230 additions and 295 deletions
|
|
@ -1,105 +0,0 @@
|
|||
# Suspected app bugs
|
||||
|
||||
Places where the app source contradicts a story in `USER_STORIES.md`. The spec for each asserts
|
||||
the story, so it fails until the bug is fixed.
|
||||
|
||||
## Rooms & membership
|
||||
|
||||
- **US-019 / US-020 — joined and created rooms never land under "Your Rooms".** `Rooms.joinRoom`
|
||||
publishes only kind-9021 and `RoomForm.submit` publishes create/edit/join — none of them write
|
||||
the user's kind-10009 room list, which is what `deriveUserRooms` reads. Only the Favorite toggle
|
||||
and the delete path touch the list.
|
||||
- **US-019 — leaving a room's member list is silent on success.** `RoomDetailMenu.handleLoading`
|
||||
only toasts when `waitForError()` returns a message.
|
||||
- **US-021 — private-room card says "Join Room" where the story (and the restricted-room card)
|
||||
say "Ask to Join"** (`RoomChat.svelte` isPrivate vs isRestricted branches).
|
||||
- **US-028 — no Share action exists on any message.** `shareEvent` is wired only into EventMenu
|
||||
("Share to Chat"), BoardMenu and PinMenu; the cross-space Share picker is reachable only from
|
||||
native share intents or `/share`.
|
||||
|
||||
## Spaces & access
|
||||
|
||||
- **US-011 — the "Request Access" state is unreachable on a `public_join = false` relay.**
|
||||
welshman's `RelayJoinWriter.setClaim("")` always writes a `claim` tag, so zooid answers
|
||||
"failed to validate invite code" instead of "no claim tag"; `attemptRelayAccess` swallows that
|
||||
error when no claim was supplied, so `SpaceJoin` shows "Join Space" and toasts "Welcome to the
|
||||
space!" for a space that refused the join.
|
||||
- **US-009 — "Browse Spaces" section renders as "More Spaces"** whenever the user has joined
|
||||
spaces, so the story's two named sections never appear together.
|
||||
|
||||
## DMs & composer
|
||||
|
||||
- **US-035 — up-arrow in an empty DM composer edits the oldest editable message, not the newest.**
|
||||
`Chat.svelte` does `messages.toReversed().find(canEditEvent)` but `chat.messages` is already
|
||||
newest-first; RoomChat uses the same expression over an ascending list, so the DM copy inverted
|
||||
the ordering.
|
||||
- **US-059 — a cancelled edit clobbers the room draft.** RoomChat re-keys RoomCompose on
|
||||
`initialValues` while passing the same `draftKey` during editing, so the draft store is
|
||||
overwritten with the edited message's text. Chat.svelte avoids this by passing
|
||||
`draftKey={eventToEdit ? undefined : draftKey}`.
|
||||
- **US-056 — mentions render with no avatar** (composer nodeview is a bare text span;
|
||||
`ContentMention` renders `@name` with no image).
|
||||
- **US-056 — `~room` references render as plain text once sent.** `parseRoom` pre-empts
|
||||
`parseLink` and `Content.svelte` has no `isRoom` branch, so the intended `#RoomName` link path
|
||||
in `ContentLinkUrl` is unreachable.
|
||||
- **US-057 — an unsupported file type is rejected silently.** `Uploader.addFile` returns false
|
||||
before any upload on a mime mismatch, so `onUploadError` (the only toast in the path) never runs.
|
||||
|
||||
## Delivery & deletion
|
||||
|
||||
- **US-072 — the "Deleted" pill never replaces reactions/menus.** In Article/Comment/Thread/Poll/
|
||||
Goal/CalendarEvent Actions, `ThunkStatusOrDeleted` is a sibling of `ReactionSummary`/
|
||||
`EventActions` instead of wrapping them; `ClassifiedActions` is the only one wired as intended.
|
||||
- **US-072 — a deleted event vanishes from lists and threads instead of staying with the pill.**
|
||||
`Repository` emits the target in `removed` and `makeFeed`/`deriveEventsById` prune on it; only
|
||||
`deriveEvent` (`includeDeleted: true`) retains it, so the pill is only reachable on detail pages.
|
||||
- **US-071 — the publish-status popover's timeout reason reads "timed out." not "request timed
|
||||
out".** `publishOne` always sets `detail: "timed out"`, making `ThunkStatusDetail`'s fallback
|
||||
branch unreachable.
|
||||
|
||||
## People & profiles
|
||||
|
||||
- **US-079 — profile notes show an absolute date+clock** (`NoteCard` uses `formatTimestamp`)
|
||||
where the story asks for a relative timestamp (`formatTimestampRelative` exists and is used for
|
||||
"Last active").
|
||||
- **US-079 — a note published live never appears in another user's open profile view without a
|
||||
reload.** `syncSpace`'s CONTENT_KINDS contains no NOTE kind and `makeFeed`'s forward scroller
|
||||
stops on its first tick.
|
||||
- **US-008 — deleting your profile can delete the blanked kind-0 it just published.** The kind-5
|
||||
requests carry an `a` tag (`0:<pubkey>`) newer than the blanked profile; zooid has no NIP-62
|
||||
handling, and in a fresh session no kind-5 may be sent at all.
|
||||
|
||||
## Settings
|
||||
|
||||
- **US-091 — the zero/negative zap-amount error toast is unreachable.** The inputs carry
|
||||
`min="1"`, so native constraint validation blocks submit before `onZapAmountsSubmit` runs.
|
||||
- **US-084 — blocked relays still appear in relay suggestions.** `RelayAdd` filters on
|
||||
matchRelay/already-selected/isIPAddress only; `BlockedRelayLists` is consumed solely by
|
||||
`policies.ts` to refuse sockets.
|
||||
|
||||
## Content rendering
|
||||
|
||||
- **US-067 — a bare `lnbc1…` invoice renders as plain prose.** `parseInvoice` only matches behind
|
||||
a `lightning:` scheme and `parseLink` skips it too, so the copyable chip appears only for the
|
||||
prefixed form.
|
||||
- **US-046 — the calendar "Today" divider keys off `created_at` instead of the event's start tag**
|
||||
(`isFuture = todayDateDisplay === newDateDisplay || event.created_at > now()`), so it lands on
|
||||
the wrong item for past-published events. (Not covered by the suite; the scroll effect uses the
|
||||
correct `getStart`.)
|
||||
|
||||
## Hosting & admin
|
||||
|
||||
- **US-102 — payment history is not sorted client-side.** The hosting settings page renders
|
||||
`listTenantInvoices` order verbatim, so "most recent first" depends on the backend.
|
||||
- **US-041 — the article list card lacks the "Posted in #room" badge** the story names for both
|
||||
surfaces; `ArticleItem` never passes `showRoom` and renders the room inline in the byline
|
||||
instead.
|
||||
- **US-092 — `deriveUserIsSpaceAdmin` gates on `supportedMethods().length > 0`.** zooid's migration writes `member_methods = ["listclaims", "createclaim"]` — if applied, every member reports as an admin.
|
||||
|
||||
## Known coverage gaps (not bugs)
|
||||
|
||||
- **US-070 bullet 3** (article/comment retry succeeds and clears the indicator) cannot be
|
||||
exercised: no UI path makes an article publish fail then succeed in one session without
|
||||
discarding `$thunks.history`.
|
||||
- **US-065's "briefly shows a loading state"** depends on timing. Sync may already hold the
|
||||
quoted event when the assertion runs.
|
||||
|
|
@ -406,14 +406,12 @@ Acceptance:
|
|||
|
||||
### US-028 — Share a message somewhere else
|
||||
|
||||
As alice, I want to forward a message into another room or conversation, so that
|
||||
I can pass it along without retyping it.
|
||||
As alice, I want to forward a message into another room in the same space, so
|
||||
that I can pass it along without retyping it.
|
||||
|
||||
Acceptance:
|
||||
|
||||
- "Share" on a message inside a space opens a picker of that space's rooms; from
|
||||
a context with no fixed space, it offers a search across all her rooms and
|
||||
conversations.
|
||||
- "Share" on a message inside a space opens a picker of that space's rooms.
|
||||
- Choosing a destination navigates there with the composer pre-filled with a
|
||||
quote of the shared message.
|
||||
- Sending posts the quoted message in the destination, visible to bob there.
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import {DAY, HOUR, MINUTE, WEEK} from "@welshman/lib"
|
||||
import {DAY, HOUR, WEEK} from "@welshman/lib"
|
||||
import {MessagingRelayList, RelayList, displayPubkey} from "@welshman/domain"
|
||||
import type {Locator, Page} from "@playwright/test"
|
||||
import {expect, makeTestUser, roomPath, spacePath, test, users} from "../harness"
|
||||
import type {SeededRumor, SeededSpace, TestUser} from "../harness"
|
||||
import type {SeededSpace, TestUser} from "../harness"
|
||||
|
||||
// A handle to a seeded event. SeededEvent isn't exported from the harness, and only its id and
|
||||
// timestamp are ever read back here.
|
||||
|
|
@ -709,8 +709,6 @@ test("US-027 find a past message and jump to it", async ({seed, as}) => {
|
|||
})
|
||||
|
||||
test("US-028 share a message somewhere else", async ({seed, as}) => {
|
||||
let dm!: SeededRumor
|
||||
|
||||
const scenario = await seed(({relay, user, at}) => {
|
||||
const space = relay("space")
|
||||
|
||||
|
|
@ -724,8 +722,6 @@ test("US-028 share a message somewhere else", async ({seed, as}) => {
|
|||
|
||||
seedChatter(space, user.alice)
|
||||
seedChatter(space, user.bob)
|
||||
|
||||
dm = space.dm(user.bob, [user.alice], "are you around?", at(20, MINUTE))
|
||||
})
|
||||
|
||||
const {url} = scenario.space("space")
|
||||
|
|
@ -754,18 +750,4 @@ test("US-028 share a message somewhere else", async ({seed, as}) => {
|
|||
|
||||
await expect(message(alice, "heads up")).toContainText("the dock is closed on sunday")
|
||||
await expect(message(bob, "heads up")).toContainText("the dock is closed on sunday")
|
||||
|
||||
// A conversation has no space of its own, so sharing from one searches everywhere she can post
|
||||
await alice.goto(`/chat/${users.bob.pubkey}`)
|
||||
|
||||
const received = alice.locator(`[data-event="${dm.id}"]`)
|
||||
|
||||
await expect(received).toContainText("are you around?")
|
||||
|
||||
await received.hover()
|
||||
await received.getByRole("button").first().click()
|
||||
await alice.getByRole("button", {name: "Share"}).click()
|
||||
|
||||
await expect(alice.getByText("Where would you like to share this?")).toBeVisible()
|
||||
await expect(alice.getByPlaceholder("Search rooms and conversations...")).toBeVisible()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import {
|
|||
RELAYS,
|
||||
type ManagementResponse,
|
||||
} from "@welshman/util"
|
||||
import {RelayJoin, RelayLeave, RoomJoin} from "@welshman/domain"
|
||||
import {RelayJoin, RelayLeave, RoomJoin, RoomLeave} from "@welshman/domain"
|
||||
import {Sync, User, publish} from "@welshman/app"
|
||||
import {stripPrefix} from "@lib/util"
|
||||
import {app, command, relayManagement, roomLists, thunks, writer} from "@app/core"
|
||||
|
|
@ -100,20 +100,52 @@ export const deriveRelayAuthError = (url: string) =>
|
|||
}
|
||||
})
|
||||
|
||||
export const publishJoinRequest = (url: string, claim = "") =>
|
||||
command(writer(RelayJoin).forceRelays(url).setClaim(claim)).then(publish)
|
||||
export const publishJoinRequest = (url: string, claim?: string) => {
|
||||
const eventWriter = writer(RelayJoin).forceRelays(url)
|
||||
|
||||
if (claim) {
|
||||
eventWriter.setClaim(claim)
|
||||
}
|
||||
|
||||
return command(eventWriter).then(publish)
|
||||
}
|
||||
|
||||
export const publishLeaveRequest = (url: string) =>
|
||||
command(writer(RelayLeave).forceRelays(url)).then(publish)
|
||||
|
||||
export const publishRoomJoinRequest = (url: string, h: string, code?: string) => {
|
||||
// A relay answers a re-sent request with "duplicate:" and a membership it already has with
|
||||
// "already a member" — both leave us where we wanted to be, so only anything else is a refusal.
|
||||
const isMembershipRefusal = (error: string) =>
|
||||
Boolean(error) && !error.startsWith("duplicate:") && !error.includes("already")
|
||||
|
||||
// Joining a room takes two publishes: a NIP-29 request the relay can refuse, and the user's
|
||||
// own room list, which is what puts the room in their sidebar. `code` is a room invite code.
|
||||
export const joinRoom = async (url: string, h: string, code?: string) => {
|
||||
const eventWriter = writer(RoomJoin).setRoom(url, h)
|
||||
|
||||
if (code) {
|
||||
eventWriter.setClaim(code)
|
||||
}
|
||||
|
||||
return command(eventWriter).then(publish)
|
||||
const thunk = await command(eventWriter).then(publish)
|
||||
const error = await thunk.waitForError()
|
||||
|
||||
if (isMembershipRefusal(error)) {
|
||||
return error
|
||||
}
|
||||
|
||||
await roomLists.get().addRoom(h, url).then(publish)
|
||||
}
|
||||
|
||||
export const leaveRoom = async (url: string, h: string) => {
|
||||
const thunk = await command(writer(RoomLeave).setRoom(url, h)).then(publish)
|
||||
const error = await thunk.waitForError()
|
||||
|
||||
if (isMembershipRefusal(error)) {
|
||||
return error
|
||||
}
|
||||
|
||||
await roomLists.get().removeRoom(h, url).then(publish)
|
||||
}
|
||||
|
||||
export const publishRoomInvite = async (url: string, h: string) => {
|
||||
|
|
@ -198,12 +230,13 @@ export const attemptRelayAccess = async (url: string, claim = "") => {
|
|||
|
||||
if (shouldIgnoreError(error)) return
|
||||
|
||||
if (claim) {
|
||||
if (error.includes("invite code")) {
|
||||
return "join request rejected"
|
||||
}
|
||||
} else if (error.includes("invite code")) {
|
||||
return
|
||||
if (error.includes("invite code")) {
|
||||
return "join request rejected"
|
||||
}
|
||||
|
||||
// A space that isn't open to the public refuses a join carrying no claim at all
|
||||
if (error.includes("claim")) {
|
||||
return "This space requires an invite code"
|
||||
}
|
||||
|
||||
return stripPrefix(error)
|
||||
|
|
@ -316,17 +349,6 @@ export class Access {
|
|||
}
|
||||
}
|
||||
|
||||
async joinRoom(h: string, code?: string) {
|
||||
if (!code) return
|
||||
|
||||
const thunk = await publishRoomJoinRequest(this.url, h, code)
|
||||
const message = await thunk.waitForError()
|
||||
|
||||
if (message && !message.startsWith("duplicate:")) {
|
||||
return message
|
||||
}
|
||||
}
|
||||
|
||||
async acceptInvite(data: InviteData, notifications: boolean) {
|
||||
const spaceUrls = roomLists.get().urls(User.require(app.get()).pubkey).get()
|
||||
const error = await this.joinSpace({
|
||||
|
|
@ -340,7 +362,7 @@ export class Access {
|
|||
}
|
||||
|
||||
if (data.h && data.code) {
|
||||
return this.joinRoom(data.h, data.code)
|
||||
return joinRoom(this.url, data.h, data.code)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -46,10 +46,11 @@
|
|||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tip-left" />
|
||||
<ThunkStatusOrDeleted {event} />
|
||||
{#if showActivity}
|
||||
<EventActivity {url} {path} {event} />
|
||||
{/if}
|
||||
<EventActions {url} {event} noun="Article" />
|
||||
<ThunkStatusOrDeleted {event}>
|
||||
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tip-left" />
|
||||
{#if showActivity}
|
||||
<EventActivity {url} {path} {event} />
|
||||
{/if}
|
||||
<EventActions {url} {event} noun="Article" />
|
||||
</ThunkStatusOrDeleted>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
import Content from "@app/components/Content.svelte"
|
||||
import ProfileLink from "@app/components/ProfileLink.svelte"
|
||||
import ArticleActions from "@app/components/ArticleActions.svelte"
|
||||
import RoomLink from "@app/components/RoomLink.svelte"
|
||||
import {reader} from "@app/core"
|
||||
import {makeArticlePath} from "@app/routes"
|
||||
|
||||
|
|
@ -23,7 +22,6 @@
|
|||
const title = article.title()
|
||||
const summary = article.summary()
|
||||
const image = article.image()
|
||||
const h = article.room()
|
||||
</script>
|
||||
|
||||
<Link
|
||||
|
|
@ -48,10 +46,7 @@
|
|||
<span class="whitespace-nowrap py-1 text-sm opacity-75">
|
||||
Written by
|
||||
<ProfileLink pubkey={event.pubkey} {url} />
|
||||
{#if h}
|
||||
in <RoomLink {url} {h} />
|
||||
{/if}
|
||||
</span>
|
||||
<ArticleActions showActivity {url} {event} />
|
||||
<ArticleActions showRoom showActivity {url} {event} />
|
||||
</div>
|
||||
</Link>
|
||||
|
|
|
|||
|
|
@ -41,21 +41,22 @@
|
|||
Posted in #<RoomName {h} {url} />
|
||||
</Link>
|
||||
{/if}
|
||||
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tip tip-left" />
|
||||
<ThunkStatusOrDeleted {event} />
|
||||
{#if showActivity}
|
||||
<EventActivity {url} {path} {event} />
|
||||
{/if}
|
||||
<EventActions {url} {event} noun="Event">
|
||||
{#snippet customActions()}
|
||||
{#if event.pubkey === $user.pubkey}
|
||||
<li>
|
||||
<Button onclick={editEvent}>
|
||||
<Icon size={4} icon={Pen2} />
|
||||
Edit Event
|
||||
</Button>
|
||||
</li>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</EventActions>
|
||||
<ThunkStatusOrDeleted {event}>
|
||||
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tip tip-left" />
|
||||
{#if showActivity}
|
||||
<EventActivity {url} {path} {event} />
|
||||
{/if}
|
||||
<EventActions {url} {event} noun="Event">
|
||||
{#snippet customActions()}
|
||||
{#if event.pubkey === $user.pubkey}
|
||||
<li>
|
||||
<Button onclick={editEvent}>
|
||||
<Icon size={4} icon={Pen2} />
|
||||
Edit Event
|
||||
</Button>
|
||||
</li>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</EventActions>
|
||||
</ThunkStatusOrDeleted>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -179,7 +179,7 @@
|
|||
eventToEdit = event
|
||||
}
|
||||
|
||||
const onEditPrevious = () => ifLet($chat?.messages.toReversed().find(canEditEvent), onEditEvent)
|
||||
const onEditPrevious = () => ifLet($chat?.messages.find(canEditEvent), onEditEvent)
|
||||
|
||||
let loading = $state(true)
|
||||
let compose: ChatCompose | undefined = $state()
|
||||
|
|
@ -300,7 +300,7 @@
|
|||
{#each missingRelayLists as pubkey (pubkey)}
|
||||
<ProfileLink {pubkey} />
|
||||
{/each}
|
||||
to enable direct messaging by opening this conversation in their $app.
|
||||
to enable direct messaging by opening this conversation in their client.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -55,23 +55,25 @@
|
|||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tip-left" />
|
||||
<ThunkStatusOrDeleted {event}>
|
||||
<ClassifiedStatus {event} />
|
||||
</ThunkStatusOrDeleted>
|
||||
{#if showActivity}
|
||||
<EventActivity {url} {path} {event} />
|
||||
{/if}
|
||||
<EventActions {url} {event} noun="Listing">
|
||||
{#snippet customActions()}
|
||||
{#if event.pubkey === $user.pubkey}
|
||||
<li>
|
||||
<Button onclick={editClassified}>
|
||||
<Icon size={4} icon={Pen2} />
|
||||
Edit Listing
|
||||
</Button>
|
||||
</li>
|
||||
{/if}
|
||||
{#snippet status()}
|
||||
<ClassifiedStatus {event} />
|
||||
{/snippet}
|
||||
</EventActions>
|
||||
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tip-left" />
|
||||
{#if showActivity}
|
||||
<EventActivity {url} {path} {event} />
|
||||
{/if}
|
||||
<EventActions {url} {event} noun="Listing">
|
||||
{#snippet customActions()}
|
||||
{#if event.pubkey === $user.pubkey}
|
||||
<li>
|
||||
<Button onclick={editClassified}>
|
||||
<Icon size={4} icon={Pen2} />
|
||||
Edit Listing
|
||||
</Button>
|
||||
</li>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</EventActions>
|
||||
</ThunkStatusOrDeleted>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -28,11 +28,12 @@
|
|||
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<div class="flex grow flex-wrap justify-end gap-2">
|
||||
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tip-left" />
|
||||
<ThunkStatusOrDeleted {event} />
|
||||
{#if showActivity && path}
|
||||
<EventActivity {url} {path} {event} />
|
||||
{/if}
|
||||
<EventActions {url} {event} noun="Comment" />
|
||||
<ThunkStatusOrDeleted {event}>
|
||||
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tip-left" />
|
||||
{#if showActivity && path}
|
||||
<EventActivity {url} {path} {event} />
|
||||
{/if}
|
||||
<EventActions {url} {event} noun="Comment" />
|
||||
</ThunkStatusOrDeleted>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
isInvoice,
|
||||
isLink,
|
||||
isProfile,
|
||||
isRoom,
|
||||
isEvent,
|
||||
isEllipsis,
|
||||
isAddress,
|
||||
|
|
@ -35,7 +36,9 @@
|
|||
import ContentQuote from "@app/components/ContentQuote.svelte"
|
||||
import ContentTopic from "@app/components/ContentTopic.svelte"
|
||||
import ContentMention from "@app/components/ContentMention.svelte"
|
||||
import RoomName from "@app/components/RoomName.svelte"
|
||||
import {entityLink} from "@app/env"
|
||||
import {makeRoomPath} from "@app/routes"
|
||||
import {userSettingsValues} from "@app/settings"
|
||||
|
||||
type Props = {
|
||||
|
|
@ -182,6 +185,10 @@
|
|||
{/if}
|
||||
{:else if isProfile(parsed)}
|
||||
<ContentMention value={parsed.value} {url} />
|
||||
{:else if isRoom(parsed)}
|
||||
<Link href={makeRoomPath(parsed.value.url, parsed.value.room)} class="link-content">
|
||||
#<RoomName url={parsed.value.url} h={parsed.value.room} />
|
||||
</Link>
|
||||
{:else if isQuote(parsed)}
|
||||
{#if isBlock(i)}
|
||||
<ContentQuote {url} value={parsed.value} {event} />
|
||||
|
|
|
|||
|
|
@ -36,10 +36,11 @@
|
|||
Posted in #<RoomName {h} {url} />
|
||||
</Link>
|
||||
{/if}
|
||||
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tip-left" />
|
||||
<ThunkStatusOrDeleted {event} />
|
||||
{#if showActivity}
|
||||
<EventActivity {url} {path} {event} />
|
||||
{/if}
|
||||
<EventActions {url} {event} hideZap noun="Goal" />
|
||||
<ThunkStatusOrDeleted {event}>
|
||||
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tip-left" />
|
||||
{#if showActivity}
|
||||
<EventActivity {url} {path} {event} />
|
||||
{/if}
|
||||
<EventActions {url} {event} hideZap noun="Goal" />
|
||||
</ThunkStatusOrDeleted>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -33,10 +33,11 @@
|
|||
Posted in #<RoomName {h} {url} />
|
||||
</Link>
|
||||
{/if}
|
||||
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tip-left" />
|
||||
<ThunkStatusOrDeleted {event} />
|
||||
{#if showActivity}
|
||||
<EventActivity {url} {path} {event} />
|
||||
{/if}
|
||||
<EventActions {url} {event} noun="Poll" />
|
||||
<ThunkStatusOrDeleted {event}>
|
||||
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tip-left" />
|
||||
{#if showActivity}
|
||||
<EventActivity {url} {path} {event} />
|
||||
{/if}
|
||||
<EventActions {url} {event} noun="Poll" />
|
||||
</ThunkStatusOrDeleted>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -63,7 +63,8 @@
|
|||
<ProfileCircles pubkeys={$followers} limit={5} />
|
||||
<p class="text-sm opacity-75">
|
||||
{$followers.length}
|
||||
{$followers.length === 1 ? "person" : "people"} you follow also follow
|
||||
{$followers.length === 1 ? "person" : "people"} you follow also
|
||||
{$followers.length === 1 ? "follows" : "follow"}
|
||||
{$profileDisplay}.
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import RelayItem from "@app/components/RelayItem.svelte"
|
||||
import {pushToast} from "@app/toast"
|
||||
import {app} from "@app/core"
|
||||
import {app, blockedRelayLists, user} from "@app/core"
|
||||
|
||||
interface Props {
|
||||
relays: Readable<string[]>
|
||||
|
|
@ -56,6 +56,7 @@
|
|||
|
||||
const loading = $state(new SvelteSet<string>())
|
||||
const relaySearch = $app.use(Relays).relaySearch
|
||||
const blockedRelayUrls = $blockedRelayLists.urls($user.pubkey).$
|
||||
|
||||
const searchResults = $derived(
|
||||
$relaySearch
|
||||
|
|
@ -64,6 +65,7 @@
|
|||
if (matchRelay?.(url) === false) return false
|
||||
if ($relays.includes(url)) return false
|
||||
if (isIPAddress(url)) return false
|
||||
if ($blockedRelayUrls.includes(url)) return false
|
||||
|
||||
return true
|
||||
})
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@
|
|||
if (error) {
|
||||
pushToast({theme: "error", message: error})
|
||||
} else {
|
||||
pushToast({message: "Content has successfully been deleted!"})
|
||||
pushToast({message: "Report has successfully been dismissed!"})
|
||||
$app.repository.removeEvent(event.id)
|
||||
onResolved?.()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
import type {Maybe} from "@welshman/lib"
|
||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||
import {makeEvent, MESSAGE, RELAY_ADD_MEMBER, ROOM_ADD_MEMBER} from "@welshman/util"
|
||||
import {MembershipStatus, publish} from "@welshman/app"
|
||||
import {MembershipStatus} from "@welshman/app"
|
||||
import AltArrowDown from "@assets/icons/alt-arrow-down.svg?dataurl"
|
||||
import ClockCircle from "@assets/icons/clock-circle.svg?dataurl"
|
||||
import Login2 from "@assets/icons/login-3.svg?dataurl"
|
||||
|
|
@ -31,7 +31,7 @@
|
|||
import VideoCallContent from "@app/components/VideoCallContent.svelte"
|
||||
import CallControlBar from "@app/components/CallControlBar.svelte"
|
||||
import {deletes, relays, rooms, thunks, user} from "@app/core"
|
||||
import {publishRoomJoinRequest} from "@app/access"
|
||||
import {joinRoom, leaveRoom} from "@app/access"
|
||||
import {CallState, callTargetRoom, callState, VideoCallLayout, videoCallLayout} from "@app/call"
|
||||
import {
|
||||
PROTECTED,
|
||||
|
|
@ -130,18 +130,14 @@
|
|||
joining = true
|
||||
|
||||
try {
|
||||
const thunk = await (inviteCode
|
||||
? publishRoomJoinRequest(url, h, inviteCode)
|
||||
: $rooms.joinRoom(url, {h}).then(publish))
|
||||
const message = await joinRoom(url, h, inviteCode)
|
||||
|
||||
const message = await thunk.waitForError()
|
||||
|
||||
if (message && !message.startsWith("duplicate:")) {
|
||||
return pushToast({theme: "error", message})
|
||||
if (message) {
|
||||
pushToast({theme: "error", message})
|
||||
} else {
|
||||
// Restart the feed now that we're a member
|
||||
start()
|
||||
}
|
||||
|
||||
// Restart the feed now that we're a member
|
||||
start()
|
||||
} finally {
|
||||
joining = false
|
||||
}
|
||||
|
|
@ -151,12 +147,14 @@
|
|||
const leave = async () => {
|
||||
if (h) {
|
||||
leaving = true
|
||||
try {
|
||||
const thunk = await $rooms.leaveRoom(url, {h}).then(publish)
|
||||
const message = await thunk.waitForError()
|
||||
|
||||
if (message && !message.startsWith("duplicate:")) {
|
||||
try {
|
||||
const message = await leaveRoom(url, h)
|
||||
|
||||
if (message) {
|
||||
pushToast({theme: "error", message})
|
||||
} else {
|
||||
pushToast({message: "You have left the room."})
|
||||
}
|
||||
} finally {
|
||||
leaving = false
|
||||
|
|
|
|||
|
|
@ -28,7 +28,8 @@
|
|||
|
||||
const {url, h, initialValues, onEscape, onEditPrevious, onSubmit}: Props = $props()
|
||||
|
||||
const draftKey = url || h ? new DraftKey<Draft>(`room:${url ?? ""}:${h ?? ""}`) : undefined
|
||||
const draftKey =
|
||||
(url || h) && !initialValues ? new DraftKey<Draft>(`room:${url ?? ""}:${h ?? ""}`) : undefined
|
||||
|
||||
const autofocus = !isMobile
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
import {onMount} from "svelte"
|
||||
import {goto} from "$app/navigation"
|
||||
import type {Command} from "@welshman/app"
|
||||
import type {Maybe} from "@welshman/lib"
|
||||
import {MembershipStatus, publish} from "@welshman/app"
|
||||
import Pen from "@assets/icons/pen.svg?dataurl"
|
||||
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
|
||||
|
|
@ -13,6 +13,7 @@
|
|||
import Confirm from "@lib/components/Confirm.svelte"
|
||||
import RoomEdit from "@app/components/RoomEdit.svelte"
|
||||
import {app, roomLists, rooms} from "@app/core"
|
||||
import {joinRoom, leaveRoom} from "@app/access"
|
||||
import {deriveUserIsRoomAdmin, deriveUserRoomMembershipStatus} from "@app/rooms"
|
||||
import {makeSpacePath} from "@app/routes"
|
||||
import {pushModal} from "@app/modal"
|
||||
|
|
@ -31,24 +32,21 @@
|
|||
|
||||
const startEdit = () => pushModal(RoomEdit, {url, h})
|
||||
|
||||
const handleLoading = async (buildCommand: () => Promise<Command>) => {
|
||||
const handleLoading = async (action: () => Promise<Maybe<string>>, successMessage: string) => {
|
||||
loading = true
|
||||
|
||||
try {
|
||||
const command = await buildCommand()
|
||||
const message = await command.publish().waitForError()
|
||||
const message = await action()
|
||||
|
||||
if (message && !message.startsWith("duplicate:")) {
|
||||
pushToast({theme: "error", message})
|
||||
}
|
||||
pushToast(message ? {theme: "error", message} : {message: successMessage})
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
|
||||
const join = () => handleLoading(() => $rooms.joinRoom(url, {h}))
|
||||
const join = () => handleLoading(() => joinRoom(url, h), "You have joined the room.")
|
||||
|
||||
const leave = () => handleLoading(() => $rooms.leaveRoom(url, {h}))
|
||||
const leave = () => handleLoading(() => leaveRoom(url, h), "You have left the room.")
|
||||
|
||||
const startDelete = () =>
|
||||
pushModal(Confirm, {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
import Modal from "@lib/components/Modal.svelte"
|
||||
import ModalBody from "@lib/components/ModalBody.svelte"
|
||||
import {rooms} from "@app/core"
|
||||
import {joinRoom} from "@app/access"
|
||||
import {pushToast} from "@app/toast"
|
||||
import {compressFileForUpload, uploadFileOrFallback} from "@app/uploads"
|
||||
import {deriveHasLivekit} from "@app/relays"
|
||||
|
|
@ -67,10 +68,9 @@
|
|||
return pushToast({theme: "error", message: editMessage})
|
||||
}
|
||||
|
||||
const joinCommand = await $rooms.joinRoom(url, room)
|
||||
const joinMessage = await joinCommand.publish().waitForError()
|
||||
const joinMessage = await joinRoom(url, room.h)
|
||||
|
||||
if (joinMessage && !joinMessage.includes("already")) {
|
||||
if (joinMessage) {
|
||||
return pushToast({theme: "error", message: joinMessage})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
import {tagSpec, tagValue, toNostrURI} from "@welshman/util"
|
||||
import Code2 from "@assets/icons/code-2.svg?dataurl"
|
||||
import GalleryWide from "@assets/icons/gallery-wide.svg?dataurl"
|
||||
import ShareCircle from "@assets/icons/share-circle.svg?dataurl"
|
||||
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
|
||||
import Danger from "@assets/icons/danger.svg?dataurl"
|
||||
import Pin from "@assets/icons/pin.svg?dataurl"
|
||||
|
|
@ -19,6 +20,7 @@
|
|||
import {app, relayManagement, roomPinLists, user} from "@app/core"
|
||||
import {deriveUserIsSpaceAdmin} from "@app/management"
|
||||
import {ROOM, deriveUserIsRoomAdmin} from "@app/rooms"
|
||||
import {shareEvent} from "@app/share"
|
||||
import {pushModal} from "@app/modal"
|
||||
import {pushToast} from "@app/toast"
|
||||
|
||||
|
|
@ -36,6 +38,11 @@
|
|||
const userIsRoomAdmin = deriveUserIsRoomAdmin(url, h)
|
||||
const isPinned = $derived($pinIds.includes(event.id))
|
||||
|
||||
const share = () => {
|
||||
onClick()
|
||||
shareEvent(url, "Message", event)
|
||||
}
|
||||
|
||||
const addToLibrary = () => {
|
||||
onClick()
|
||||
pushModal(PinboardSelect, {url, event})
|
||||
|
|
@ -106,6 +113,12 @@
|
|||
Message Details
|
||||
</Button>
|
||||
</li>
|
||||
<li>
|
||||
<Button onclick={share}>
|
||||
<Icon size={4} icon={ShareCircle} />
|
||||
Share
|
||||
</Button>
|
||||
</li>
|
||||
<li>
|
||||
<Button onclick={addToLibrary}>
|
||||
<Icon size={4} icon={GalleryWide} />
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
import {getIdOrAddress, tagSpec, tagValue, toNostrURI} from "@welshman/util"
|
||||
import Bolt from "@assets/icons/bolt.svg?dataurl"
|
||||
import Reply from "@assets/icons/reply-2.svg?dataurl"
|
||||
import ShareCircle from "@assets/icons/share-circle.svg?dataurl"
|
||||
import Code2 from "@assets/icons/code-2.svg?dataurl"
|
||||
import TrashBin2 from "@assets/icons/trash-bin-2.svg?dataurl"
|
||||
import SmileCircle from "@assets/icons/smile-circle.svg?dataurl"
|
||||
|
|
@ -25,6 +26,7 @@
|
|||
import {ROOM, deriveUserIsRoomAdmin} from "@app/rooms"
|
||||
import {ENABLE_ZAPS} from "@app/env"
|
||||
import {makeContentPath} from "@app/routes"
|
||||
import {shareEvent} from "@app/share"
|
||||
import {pushModal} from "@app/modal"
|
||||
import {pushToast} from "@app/toast"
|
||||
|
||||
|
|
@ -65,6 +67,11 @@
|
|||
reply()
|
||||
}
|
||||
|
||||
const share = () => {
|
||||
history.back()
|
||||
shareEvent(url, "Message", event)
|
||||
}
|
||||
|
||||
const showInfo = () => pushModal(EventInfo, {url, event}, {replaceState: true})
|
||||
|
||||
const showDelete = () => pushModal(EventDeleteConfirm, {url, event})
|
||||
|
|
@ -99,6 +106,10 @@
|
|||
<Icon size={4} icon={Code2} />
|
||||
Message Info
|
||||
</Button>
|
||||
<Button class="button button-neutral" onclick={share}>
|
||||
<Icon size={4} icon={ShareCircle} />
|
||||
Share
|
||||
</Button>
|
||||
{#if path}
|
||||
<Link class="button button-neutral" href={path}>
|
||||
<Icon size={4} icon={MenuDots} />
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@
|
|||
|
||||
const {url, initialValues = {}}: Props = $props()
|
||||
|
||||
const values = $state(initialValues)
|
||||
const values = $state({...initialValues})
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
|
||||
const {url, pubkey, roles = []}: Props = $props()
|
||||
|
||||
const profileDisplay = $profiles.display(pubkey).$
|
||||
const supportedMethods = deriveSpaceSupportedMethods(url)
|
||||
const canUnallow = $derived($supportedMethods.includes("unallowpubkey"))
|
||||
const canBan = $derived($supportedMethods.includes("banpubkey"))
|
||||
|
|
@ -31,7 +32,7 @@
|
|||
<button
|
||||
type="button"
|
||||
class="absolute inset-0 cursor-pointer rounded-2xl"
|
||||
aria-label="View {$profiles.display(pubkey).get()}'s profile"
|
||||
aria-label="View {$profileDisplay}'s profile"
|
||||
onclick={openProfile}>
|
||||
</button>
|
||||
<div class="pointer-events-none relative flex items-start justify-between gap-2">
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
import InfoSignatures from "@app/components/InfoSignatures.svelte"
|
||||
import {relaysPendingTrust} from "@app/policies"
|
||||
import {addTrustedRelay, removeTrustedRelay} from "@app/settings"
|
||||
import {pushModal} from "@app/modal"
|
||||
import {popModal, pushModal} from "@app/modal"
|
||||
import {roomLists} from "@app/core"
|
||||
|
||||
type Props = {
|
||||
|
|
@ -48,6 +48,7 @@
|
|||
await addTrustedRelay(url)
|
||||
|
||||
relaysPendingTrust.update($r => remove(url, $r))
|
||||
popModal()
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,10 +32,11 @@
|
|||
Posted in #<RoomName {h} {url} />
|
||||
</Link>
|
||||
{/if}
|
||||
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tip-left" />
|
||||
<ThunkStatusOrDeleted {event} />
|
||||
{#if showActivity}
|
||||
<EventActivity {url} {path} {event} />
|
||||
{/if}
|
||||
<EventActions {url} {event} noun="Thread" />
|
||||
<ThunkStatusOrDeleted {event}>
|
||||
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tip-left" />
|
||||
{#if showActivity}
|
||||
<EventActivity {url} {path} {event} />
|
||||
{/if}
|
||||
<EventActions {url} {event} noun="Thread" />
|
||||
</ThunkStatusOrDeleted>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -26,14 +26,14 @@
|
|||
)
|
||||
|
||||
const relayMessage = (status: PublishStatus | undefined, detail: string | undefined) => {
|
||||
if (detail) {
|
||||
return detail
|
||||
}
|
||||
|
||||
if (status === PublishStatus.Timeout) {
|
||||
return "request timed out"
|
||||
}
|
||||
|
||||
if (detail) {
|
||||
return detail
|
||||
}
|
||||
|
||||
return "no details received"
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -8,10 +8,11 @@
|
|||
|
||||
type Props = {
|
||||
event: TrustedEvent
|
||||
status?: Snippet
|
||||
children?: Snippet
|
||||
}
|
||||
|
||||
const {event, children}: Props = $props()
|
||||
const {event, status, children}: Props = $props()
|
||||
|
||||
const deleted = deriveIsDeleted(event)
|
||||
const history = $thunks.history
|
||||
|
|
@ -20,8 +21,11 @@
|
|||
|
||||
{#if $deleted}
|
||||
<div class="button button-error button-xs rounded-full">Deleted</div>
|
||||
{:else if thunk.thunks.length > 0 && !thunk.hasStatus(PublishStatus.Success)}
|
||||
<ThunkStatus {thunk} />
|
||||
{:else if children}
|
||||
{:else}
|
||||
{#if thunk.thunks.length > 0 && !thunk.hasStatus(PublishStatus.Success)}
|
||||
<ThunkStatus {thunk} />
|
||||
{:else}
|
||||
{@render status?.()}
|
||||
{/if}
|
||||
{@render children?.()}
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -43,6 +43,8 @@ export const deriveSpaceSupportedMethods = simpleCache(([url]: [string | undefin
|
|||
|
||||
// User
|
||||
|
||||
// A relay answers supportedmethods with everything it implements rather than with what the
|
||||
// caller may use, so all this can tell us is that the call wasn't refused outright.
|
||||
export const deriveUserIsSpaceAdmin = (url?: string) =>
|
||||
derived(deriveSpaceSupportedMethods(url), $methods => $methods.length > 0)
|
||||
|
||||
|
|
|
|||
|
|
@ -122,12 +122,12 @@ const trustPolicy = (socket: Socket) => {
|
|||
|
||||
const mostlyRestrictedPolicy = (socket: Socket) => {
|
||||
let total = 0
|
||||
let restricted = 0
|
||||
let refused = 0
|
||||
|
||||
const pending = new Set<string>()
|
||||
|
||||
const updateStatus = (error?: string) => {
|
||||
if (total > 5 && restricted > total / 2) {
|
||||
if (total > 5 && refused > total / 2) {
|
||||
if (error) {
|
||||
return relaysMostlyRestricted.update(assoc(socket.url, error))
|
||||
}
|
||||
|
|
@ -136,6 +136,19 @@ const mostlyRestrictedPolicy = (socket: Socket) => {
|
|||
}
|
||||
}
|
||||
|
||||
// NIP-01 reserves "blocked: " for a ban and "restricted: " for lacking permission.
|
||||
const countDetails = (details: string) => {
|
||||
if (details.startsWith("auth-required: ")) {
|
||||
total--
|
||||
updateStatus()
|
||||
}
|
||||
|
||||
if (details.startsWith("restricted: ") || details.startsWith("blocked: ")) {
|
||||
refused++
|
||||
updateStatus(details)
|
||||
}
|
||||
}
|
||||
|
||||
const unsubscribers = [
|
||||
on(socket, SocketEvent.Receive, (message: RelayMessage) => {
|
||||
if (isRelayOk(message)) {
|
||||
|
|
@ -145,15 +158,7 @@ const mostlyRestrictedPolicy = (socket: Socket) => {
|
|||
pending.delete(id)
|
||||
|
||||
if (!ok) {
|
||||
if (details.startsWith("auth-required: ")) {
|
||||
total--
|
||||
updateStatus()
|
||||
}
|
||||
|
||||
if (details.startsWith("restricted: ")) {
|
||||
restricted++
|
||||
updateStatus(details)
|
||||
}
|
||||
countDetails(details)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -163,16 +168,7 @@ const mostlyRestrictedPolicy = (socket: Socket) => {
|
|||
|
||||
if (pending.has(id)) {
|
||||
pending.delete(id)
|
||||
|
||||
if (details.startsWith("auth-required: ")) {
|
||||
total--
|
||||
updateStatus()
|
||||
}
|
||||
|
||||
if (details.startsWith("restricted: ")) {
|
||||
restricted++
|
||||
updateStatus(details)
|
||||
}
|
||||
countDetails(details)
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import {onMount} from "svelte"
|
||||
import {App} from "@capacitor/app"
|
||||
import type {Maybe} from "@welshman/lib"
|
||||
import {indexBy, spec} from "@welshman/lib"
|
||||
import {indexBy, sortBy, spec} from "@welshman/lib"
|
||||
import Add from "@assets/icons/add.svg?dataurl"
|
||||
import Server from "@assets/icons/server.svg?dataurl"
|
||||
import Bolt from "@assets/icons/bolt.svg?dataurl"
|
||||
|
|
@ -107,7 +107,7 @@
|
|||
const [t, inv, rel, draft] = results
|
||||
|
||||
if (t.status === "fulfilled") tenant = t.value
|
||||
if (inv.status === "fulfilled") invoices = inv.value
|
||||
if (inv.status === "fulfilled") invoices = sortBy(invoice => -invoice.created_at, inv.value)
|
||||
if (rel.status === "fulfilled") relays = rel.value
|
||||
if (draft.status === "fulfilled") draftInvoice = draft.value
|
||||
|
||||
|
|
|
|||
|
|
@ -283,7 +283,6 @@
|
|||
<input
|
||||
type="number"
|
||||
class="grow"
|
||||
min="1"
|
||||
data-index={index}
|
||||
value={amount}
|
||||
oninput={onZapAmountInput} />
|
||||
|
|
|
|||
|
|
@ -300,7 +300,7 @@
|
|||
<p class="py-12 text-center">You haven't joined any spaces yet.</p>
|
||||
{/if}
|
||||
{#if otherSpaces.length > 0}
|
||||
<Divider>{filteredUserUrls.length > 0 ? "More Spaces" : "Browse Spaces"}</Divider>
|
||||
<Divider>Browse Spaces</Divider>
|
||||
{/if}
|
||||
{#each otherSpaces.slice(0, limit) as relay (relay.url)}
|
||||
<Button class="card card-interactive" onclick={() => openSpace(relay.url)}>
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@
|
|||
.map<Item>(event => {
|
||||
const newDateDisplay = formatTimestampAsDate(getStart(event))
|
||||
const dateDisplay = prevDateDisplay === newDateDisplay ? undefined : newDateDisplay
|
||||
const isFuture = todayDateDisplay === newDateDisplay || event.created_at > now()
|
||||
const isFuture = todayDateDisplay === newDateDisplay || getStart(event) > now()
|
||||
const isFirstFutureEvent = !haveISeenTheFuture && isFuture
|
||||
|
||||
prevDateDisplay = newDateDisplay
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
import NoteContent from "@app/components/NoteContent.svelte"
|
||||
import NoteCard from "@app/components/NoteCard.svelte"
|
||||
import CalendarEventActions from "@app/components/CalendarEventActions.svelte"
|
||||
import CommentActions from "@app/components/CommentActions.svelte"
|
||||
import CalendarEventHeader from "@app/components/CalendarEventHeader.svelte"
|
||||
import CalendarEventMeta from "@app/components/CalendarEventMeta.svelte"
|
||||
import CalendarEventDate from "@app/components/CalendarEventDate.svelte"
|
||||
|
|
@ -95,7 +96,7 @@
|
|||
<NoteCard event={reply} {url} class="card z-feature w-full">
|
||||
<div class="flex flex-col gap-3 ml-12">
|
||||
<NoteContent showEntire event={reply} {url} />
|
||||
<CalendarEventActions event={reply} {url} />
|
||||
<CommentActions segment="calendar" event={reply} {url} />
|
||||
</div>
|
||||
</NoteCard>
|
||||
{/each}
|
||||
|
|
|
|||
Loading…
Reference in a new issue