Compare commits

..

2 Commits

Author SHA1 Message Date
triesap 275eb46565 Simplify page title logic per review 2026-02-17 20:19:14 +00:00
triesap 0b887d62f6 Move page title logic to util 2026-02-17 19:08:20 +00:00
17 changed files with 241 additions and 419 deletions
-7
View File
@@ -1,7 +0,0 @@
#!/usr/bin/env zsh
onchange src -ik -- npx svelte-kit sync &
onchange src -ik -- bash -c 'unbuffer npx svelte-check --tsconfig ./tsconfig.json | less -R' &
wait
+1 -1
View File
@@ -7,7 +7,7 @@
"build": "./build.sh",
"release:android": "./build.sh && cap build android --androidreleasetype APK --signing-type apksigner",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "./check.sh",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --check src && eslint src",
"format": "git diff head --name-only --diff-filter d | grep -E '(js|ts|svelte|css)$' | xargs -r prettier --write",
"format:all": "prettier --write src",
+2 -2
View File
@@ -274,7 +274,7 @@
.input-editor,
.chat-editor,
.note-editor {
@apply -m-1 p-1;
@apply -m-1 min-h-12 p-1 text-sm;
}
.tiptap {
@@ -300,7 +300,7 @@
}
.tiptap {
@apply max-h-[350px] min-h-10 overflow-y-auto p-2 px-4;
@apply max-h-[350px] overflow-y-auto p-2 px-4;
}
.tiptap p.is-editor-empty:first-child::before {
@@ -1,6 +1,6 @@
<script lang="ts">
import {onMount, onDestroy} from "svelte"
import {displayUrl, once} from "@welshman/lib"
import {displayUrl} from "@welshman/lib"
import {
getTags,
getBlob,
@@ -27,7 +27,7 @@
const nonce = getTagValue("decryption-nonce", meta)
const algorithm = getTagValue("encryption-algorithm", meta)
const onError = once(async () => {
const onError = async () => {
// If the image failed to load, try authenticating
if (hash && $signer) {
const server = new URL(url).origin
@@ -43,7 +43,7 @@
} else {
hasError = true
}
})
}
let hasError = $state(false)
let src = $state("")
+1 -1
View File
@@ -26,7 +26,7 @@
<Icon icon={Reply} />
<span>{$replies.length} {$replies.length === 1 ? "reply" : "replies"}</span>
</div>
<div class="btn btn-neutral btn-xs relative rounded-full">
<div class="btn btn-neutral btn-xs relative hidden rounded-full sm:flex">
{#if gt(lastActive, $checked)}
<div class="h-2 w-2 rounded-full bg-primary"></div>
{/if}
+15
View File
@@ -0,0 +1,15 @@
<script lang="ts">
import MenuSpacesItem from "@app/components/MenuSpacesItem.svelte"
type Props = {
urls: string[]
}
const {urls}: Props = $props()
</script>
<div class="column menu gap-2">
{#each urls as url (url)}
<MenuSpacesItem {url} />
{/each}
</div>
+1 -6
View File
@@ -1,6 +1,5 @@
<script lang="ts">
import {Capacitor} from "@capacitor/core"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
import Server from "@assets/icons/server.svg?dataurl"
import Moon from "@assets/icons/moon.svg?dataurl"
@@ -20,8 +19,8 @@
import {pushModal} from "@app/util/modal"
import {theme} from "@app/util/theme"
const back = () => history.back()
const logout = () => pushModal(LogOut)
const toggleTheme = () => theme.set($theme === "dark" ? "light" : "dark")
</script>
@@ -124,10 +123,6 @@
<Button onclick={logout} class="btn btn-neutral">
<Icon icon={Exit} /> Log Out
</Button>
<Button class="btn btn-link w-full md:hidden" onclick={back}>
<Icon icon={AltArrowLeft} />
Go back
</Button>
</div>
</ModalBody>
</Modal>
+1 -1
View File
@@ -13,7 +13,7 @@
</script>
<Link replaceState href={path}>
<CardButton class="btn-neutral shadow-md bg-alt rounded-box border-none">
<CardButton class="btn-neutral shadow-md bg-alt">
{#snippet icon()}
<RelayIcon {url} size={12} class="rounded-full" />
{/snippet}
+3 -2
View File
@@ -2,14 +2,15 @@
import cx from "classnames"
import type {Snippet} from "svelte"
import {formatTimestamp} from "@welshman/lib"
import {getListTags, getPubkeyTagValues} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util"
import {userMuteList} from "@welshman/app"
import Danger from "@assets/icons/danger-triangle.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import Profile from "@app/components/Profile.svelte"
import ProfileName from "@app/components/ProfileName.svelte"
import {goToEvent} from "@app/util/routes"
import {isEventMuted} from "@app/core/state"
const {
event,
@@ -31,7 +32,7 @@
muted = false
}
let muted = $state($isEventMuted(event))
let muted = $state(getPubkeyTagValues(getListTags($userMuteList)).includes(event.pubkey))
</script>
<div class="flex flex-col gap-2 shadow-md {restProps.class}">
+12 -7
View File
@@ -15,6 +15,7 @@
import Divider from "@lib/components/Divider.svelte"
import PrimaryNavItem from "@lib/components/PrimaryNavItem.svelte"
import ChatEnable from "@app/components/ChatEnable.svelte"
import MenuOtherSpaces from "@app/components/MenuOtherSpaces.svelte"
import MenuSettings from "@app/components/MenuSettings.svelte"
import PrimaryNavItemSpace from "@app/components/PrimaryNavItemSpace.svelte"
import {userSpaceUrls, PLATFORM_RELAYS, PLATFORM_LOGO} from "@app/core/state"
@@ -27,6 +28,8 @@
const {children}: Props = $props()
const showOtherSpacesMenu = () => pushModal(MenuOtherSpaces, {urls: secondarySpaceUrls})
const showSettingsMenu = () => pushModal(MenuSettings)
const openChat = () => ($shouldUnwrap ? goto("/chat") : pushModal(ChatEnable, {next: "/chat"}))
@@ -57,13 +60,15 @@
{#each primarySpaceUrls as url (url)}
<PrimaryNavItemSpace {url} />
{/each}
<PrimaryNavItem
href="/spaces"
title="All Spaces"
class="tooltip-right"
notification={otherSpaceNotifications}>
<ImageIcon alt="All Spaces" src={Widget} size={8} />
</PrimaryNavItem>
{#if secondarySpaceUrls.length > 0}
<PrimaryNavItem
title="Other Spaces"
class="tooltip-right"
onclick={showOtherSpacesMenu}
notification={otherSpaceNotifications}>
<ImageIcon alt="Other Spaces" src={Widget} size={8} />
</PrimaryNavItem>
{/if}
<PrimaryNavItem title="Add a Space" href="/discover" class="tooltip-right">
<ImageIcon alt="Add a Space" src={Compass} size={8} />
</PrimaryNavItem>
+6 -6
View File
@@ -152,18 +152,18 @@
transition:fly
class="bg-alt menu absolute right-0 z-popover w-48 gap-1 rounded-box p-2 shadow-md">
{#if $userIsAdmin}
<li>
<Button onclick={startEdit}>
<Icon icon={Pen} />
Edit Room
</Button>
</li>
<li>
<Button class="text-error" onclick={startDelete}>
<Icon icon={TrashBin2} />
Delete Room
</Button>
</li>
<li>
<Button onclick={startEdit}>
<Icon icon={Pen} />
Edit Room
</Button>
</li>
{:else if $membershipStatus === MembershipStatus.Initial}
<li>
<Button disabled={loading} onclick={join}>
-15
View File
@@ -37,7 +37,6 @@ import {
makeList,
addToListPublicly,
removeFromListByPredicate,
updateList,
getTag,
getListTags,
getRelayTagValues,
@@ -149,20 +148,6 @@ export const removeSpaceMembership = async (url: string) => {
return publishThunk({event, relays})
}
export const setSpaceMembershipOrder = async (urls: string[]) => {
const list = get(userGroupList) || makeList({kind: ROOMS})
const orderedUrls = uniq(urls.map(normalizeRelayUrl))
const relayTags = list.publicTags.filter(t => t[0] === "r")
const otherPublicTags = list.publicTags.filter(t => t[0] !== "r")
const relayTagByUrl = new Map(relayTags.map(t => [normalizeRelayUrl(t[1]), t]))
const orderedRelayTags = orderedUrls.map(url => relayTagByUrl.get(url) || ["r", url])
const publicTags = [...orderedRelayTags, ...otherPublicTags]
const event = await updateList(list, {publicTags}).reconcile(nip44EncryptToSelf)
const relays = uniq([...Router.get().FromUser().getUrls(), ...getRelayTagValues(event.tags)])
return publishThunk({event, relays})
}
export const addRoomMembership = async (url: string, h: string) => {
const list = get(userGroupList) || makeList({kind: ROOMS})
const newTags = [
+1 -131
View File
@@ -27,7 +27,6 @@ import {
randomId,
tryCatch,
fromPairs,
groupBy,
remove,
} from "@welshman/lib"
import type {Override} from "@welshman/lib"
@@ -49,7 +48,6 @@ import {
makeDeriveEvent,
makeLoadItem,
makeDeriveItem,
deriveItems,
deriveItemsByKey,
deriveDeduplicated,
deriveEventsById,
@@ -60,8 +58,6 @@ import {
deriveEventsDesc,
} from "@welshman/store"
import {
FEED,
FEEDS,
APP_DATA,
CLIENT_AUTH,
COMMENT,
@@ -94,8 +90,6 @@ import {
ZAP_GOAL,
ZAP_REQUEST,
ZAP_RESPONSE,
REPOST,
GENERIC_REPOST,
asDecryptedEvent,
getGroupTags,
getListTags,
@@ -110,29 +104,14 @@ import {
makeRoomMeta,
ManagementMethod,
sortEventsDesc,
getAddress,
Address,
getIdFilters,
getEventTagValues,
getAddressTagValues,
getParentIds,
getParentAddrs,
} from "@welshman/util"
import type {
TrustedEvent,
RelayProfile,
PublishedList,
PublishedRoomMeta,
List,
Filter,
} from "@welshman/util"
import type {TrustedEvent, RelayProfile, PublishedRoomMeta, List, Filter} from "@welshman/util"
import {routerContext, Router} from "@welshman/router"
import {
pubkey,
repository,
tracker,
createSearch,
userMuteList,
userFollowList,
ensurePlaintext,
makeOutboxLoader,
@@ -142,9 +121,7 @@ import {
makeUserLoader,
manageRelay,
displayProfileByPubkey,
getProfile,
} from "@welshman/app"
import {readFeed} from "@lib/feeds"
export const fromCsv = (s: string) => (s || "").split(",").filter(identity)
@@ -308,8 +285,6 @@ export const makeCommentFilter = (kinds: number[], extra: Filter = {}) => ({
...extra,
})
export const REPOST_KINDS = [REPOST, GENERIC_REPOST]
export const REACTION_KINDS = [REPORT, DELETE, REACTION]
if (ENABLE_ZAPS) {
@@ -940,111 +915,6 @@ export const deriveUserCanCreateRoom = (url: string) => {
)
}
// Feeds
export const feedsByAddress = deriveItemsByKey({
repository,
getKey: feed => getAddress(feed.event),
filters: [{kinds: [FEED]}],
eventToItem: readFeed,
})
export const getFeedsByAddress = getter(feedsByAddress)
export const feeds = deriveItems(feedsByAddress)
export const getFeeds = getter(feeds)
export const getFeed = (address: string) => getFeedsByAddress().get(address)
export const fetchFeed = (address: string) => {
const {pubkey} = Address.from(address)
return load({
relays: Router.get().FromPubkey(pubkey).getUrls(),
filters: getIdFilters([address]),
})
}
export const loadFeed = makeLoadItem(fetchFeed, getFeed)
export const deriveFeed = makeDeriveItem(feedsByAddress, loadFeed)
// Feeds by pubkey
export const feedsByPubkey = derived(feeds, $feeds => groupBy(f => f.event.pubkey, $feeds))
export const getFeedsByPubkey = getter(feedsByPubkey)
export const getFeedsForPubkey = (pubkey: string) => getFeedsByPubkey().get(pubkey)
export const loadFeedsForPubkey = makeLoadItem(makeOutboxLoader(FEED), getFeedsForPubkey)
export const userFeeds = makeUserData(feedsByPubkey, loadFeedsForPubkey)
export const loadUserFeeds = makeUserLoader(loadFeedsForPubkey)
// Feed favorites
export const feedFavoritesByPubkey = deriveItemsByKey<PublishedList>({
repository,
getKey: list => list.event.pubkey,
filters: [{kinds: [FEEDS]}],
eventToItem: async event =>
readList(
asDecryptedEvent(event, {
content: await ensurePlaintext(event),
}),
),
})
export const getFeedFavoritesByPubkey = getter(feedFavoritesByPubkey)
export const getFeedFavorites = (pubkey: string) => getFeedFavoritesByPubkey().get(pubkey)
export const loadFeedFavorites = makeLoadItem(makeOutboxLoader(FEEDS), getFeedFavorites)
export const userFeedFavorites = makeUserData(feedFavoritesByPubkey, loadFeedFavorites)
export const loadUserFeedFavorites = makeUserLoader(loadFeedFavorites)
// Mutes
export const isEventMuted = withGetter(
derived(userMuteList, $userMuteList => {
const pubkey = $userMuteList?.event.pubkey
const tags = getListTags($userMuteList)
const mutedEvents = new Set(getEventTagValues(tags))
const mutedPubkeys = new Set(getPubkeyTagValues(tags))
const mutedAddresses = new Set(getAddressTagValues(tags))
const mutedTopics = new Set(getTagValues("t", tags))
const mutedWords = getTagValues("word", tags)
const regex =
mutedWords.length > 0
? new RegExp(`\\b(${mutedWords.map(w => w.toLowerCase().trim()).join("|")})\\b`)
: null
return (e: TrustedEvent) => {
if (!pubkey) return false
if (pubkey === e.pubkey) return false
if (mutedPubkeys.has(e.pubkey)) return true
if (mutedEvents.has(e.id)) return true
if (mutedAddresses.has(getAddress(e))) return true
if (getParentIds(e).some(id => mutedEvents.has(id))) return true
if (getParentAddrs(e).some(address => mutedAddresses.has(address))) return true
if (getTagValues("t", e.tags).some(t => mutedTopics.has(t))) return true
if (regex) {
if (e.content?.toLowerCase().match(regex)) return true
if (displayProfileByPubkey(e.pubkey).toLowerCase().match(regex)) return true
if (tryCatch(() => getProfile(e.pubkey)?.nip05?.match(regex))) return true
}
return false
}
}),
)
// Other utils
export const encodeRelay = (url: string) =>
-2
View File
@@ -52,7 +52,6 @@ import {
getSpaceUrlsFromGroupList,
getSpaceRoomsFromGroupList,
makeCommentFilter,
loadFeedsForPubkey,
} from "@app/core/state"
import {hasBlossomSupport} from "@app/core/commands"
@@ -201,7 +200,6 @@ const syncUserData = () => {
loadMuteList($userRelayList.event.pubkey)
loadProfile($userRelayList.event.pubkey)
loadSettings($userRelayList.event.pubkey)
loadFeedsForPubkey($userRelayList.event.pubkey)
}
})
-78
View File
@@ -1,78 +0,0 @@
import {fromPairs, parseJson, randomId} from "@welshman/lib"
import {FEED, Address} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util"
import {
makeIntersectionFeed,
hasSubFeeds,
isTagFeed,
isAuthorFeed,
isScopeFeed,
} from "@welshman/feeds"
import type {Feed as IFeed} from "@welshman/feeds"
export type Feed = {
title: string
identifier: string
description: string
definition: IFeed
event?: TrustedEvent
}
export type PublishedFeed = Omit<Feed, "event"> & {
event: TrustedEvent
}
export const normalizeFeedDefinition = (feed: IFeed) =>
hasSubFeeds(feed) ? feed : makeIntersectionFeed(feed)
export const makeFeed = (feed: Partial<Feed> = {}): Feed => ({
title: "",
description: "",
identifier: randomId(),
definition: makeIntersectionFeed(),
...feed,
})
export const readFeed = (event: TrustedEvent) => {
const {d: identifier, title = "", description = "", feed = ""} = fromPairs(event.tags)
const definition = parseJson(feed) || makeIntersectionFeed()
return {title, identifier, description, definition, event} as PublishedFeed
}
export const createFeed = ({identifier, definition, title, description}: Feed) => ({
kind: FEED,
content: "",
tags: [
["d", identifier],
["alt", title],
["title", title],
["description", description],
["feed", JSON.stringify(definition)],
],
})
export const editFeed = (feed: PublishedFeed) => ({
kind: FEED,
content: feed.event.content,
tags: Object.entries({
...fromPairs(feed.event.tags),
title: feed.title,
alt: feed.title,
description: feed.description,
feed: JSON.stringify(feed.definition),
}),
})
export const displayFeed = (feed?: Feed) => feed?.title || "[no name]"
export const isTopicFeed = (f: IFeed) => isTagFeed(f) && f[1] === "#t"
export const isMentionFeed = (f: IFeed) => isTagFeed(f) && f[1] === "#p"
export const isAddressFeed = (f: IFeed) => isTagFeed(f) && f[1] === "#a"
export const isContextFeed = (f: IFeed) =>
isTagFeed(f) && f[1] === "#a" && f.slice(2).every(Address.isAddress)
export const isPeopleFeed = (f: IFeed) => isAuthorFeed(f) || isScopeFeed(f)
+193 -66
View File
@@ -1,77 +1,204 @@
<script lang="ts">
import {onMount} from "svelte"
import {goto} from "$app/navigation"
import {shouldUnwrap} from "@welshman/app"
import AddCircle from "@assets/icons/add-circle.svg?dataurl"
import Compass from "@assets/icons/compass.svg?dataurl"
import ChatRound from "@assets/icons/chat-round.svg?dataurl"
import {derived, writable} from "svelte/store"
import {batch, call, sortBy, uniqBy} from "@welshman/lib"
import {
NOTE,
MESSAGE,
THREAD,
CLASSIFIED,
ZAP_GOAL,
EVENT_TIME,
COMMENT,
getTagValue,
getTagValues,
getIdAndAddress,
} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util"
import {
makeKindFeed,
makeRelayFeed,
makeScopeFeed,
makeIntersectionFeed,
makeUnionFeed,
Scope,
} from "@welshman/feeds"
import {repository, tracker, makeFeedController, loadUserFollowList} from "@welshman/app"
import History from "@assets/icons/history.svg?dataurl"
import {createScroller} from "@lib/html"
import Icon from "@lib/components/Icon.svelte"
import Link from "@lib/components/Link.svelte"
import Button from "@lib/components/Button.svelte"
import CardButton from "@lib/components/CardButton.svelte"
import SpaceAdd from "@app/components/SpaceAdd.svelte"
import ChatEnable from "@app/components/ChatEnable.svelte"
import {pushModal} from "@app/util/modal"
import {goToSpace} from "@app/util/routes"
import {PLATFORM_NAME, PLATFORM_RELAYS} from "@app/core/state"
import PageBar from "@lib/components/PageBar.svelte"
import PageContent from "@lib/components/PageContent.svelte"
import NoteItem from "@app/components/NoteItem.svelte"
import ThreadItem from "@app/components/ThreadItem.svelte"
import ClassifiedItem from "@app/components/ClassifiedItem.svelte"
import GoalItem from "@app/components/GoalItem.svelte"
import CalendarEventItem from "@app/components/CalendarEventItem.svelte"
import RecentConversation from "@app/components/RecentConversation.svelte"
import {makeRoomId, userSpaceUrls, loadUserGroupList, CONTENT_KINDS} from "@app/core/state"
const addSpace = () => pushModal(SpaceAdd)
type Activity = {
type: "message" | "content"
event: TrustedEvent
timestamp: number
count: number
url: string
}
const openChat = () => ($shouldUnwrap ? goto("/chat") : pushModal(ChatEnable, {next: "/chat"}))
const controller = new AbortController()
const events = writable<TrustedEvent[]>([])
const limit = writable(0)
onMount(async () => {
if (PLATFORM_RELAYS.length > 0) {
goToSpace(PLATFORM_RELAYS[0])
const recentActivity = derived([events, limit], ([$events, $limit]) => {
const activity: Activity[] = []
const activityByRoom = new Map<string, Activity>()
const latestActivityByKey = new Map<string, number>()
for (const event of $events.slice(0, $limit)) {
if (event.kind === MESSAGE) {
const h = getTagValue("h", event.tags)
if (!h) continue
for (const url of tracker.getRelays(event.id)) {
const id = makeRoomId(url, h)
const item = activityByRoom.get(id)
if (!item) {
activityByRoom.set(id, {
type: "message",
event,
timestamp: event.created_at,
count: 1,
url,
})
} else if (item.timestamp < event.created_at) {
item.count++
item.event = event
item.timestamp = event.created_at
}
}
} else if (event.kind === COMMENT) {
for (const k of getTagValues(["E", "A"], event.tags)) {
latestActivityByKey.set(k, Math.max(latestActivityByKey.get(k) || 0, event.created_at))
}
} else {
for (const k of getIdAndAddress(event)) {
latestActivityByKey.set(k, Math.max(latestActivityByKey.get(k) || 0, event.created_at))
}
}
}
for (const item of activityByRoom.values()) {
activity.push(item)
}
for (const [address, timestamp] of latestActivityByKey.entries()) {
const event = repository.getEvent(address)
if (event) {
for (const url of tracker.getRelays(event.id)) {
activity.push({type: "content", event, timestamp, url, count: 1})
break
}
}
}
return sortBy(
a => -a.timestamp,
uniqBy(a => a.event.id, activity),
)
})
let loading = $state(true)
let element: Element | undefined = $state()
onMount(() => {
const promise = call(async () => {
await Promise.all([loadUserGroupList(), loadUserFollowList()])
const ctrl = makeFeedController({
useWindowing: true,
signal: controller.signal,
feed: makeUnionFeed(
makeIntersectionFeed(
makeRelayFeed(...$userSpaceUrls),
makeKindFeed(COMMENT, ...CONTENT_KINDS),
),
makeIntersectionFeed(makeScopeFeed(Scope.Follows), makeKindFeed(NOTE)),
),
onEvent: batch(100, (evts: TrustedEvent[]) => {
events.update($events => [...$events, ...evts])
}),
onExhausted: () => {
loading = false
},
})
const scroller = createScroller({
element: element!,
delay: 800,
threshold: 3000,
onScroll: async () => {
console.log("scroll")
limit.update($limit => {
if ($events.length - $limit < 50) {
ctrl.load(50)
}
return $limit + 10
})
},
})
return () => {
scroller.stop()
controller.abort()
}
})
return () => promise.then(call)
})
</script>
<div class="hero min-h-screen overflow-auto pb-8">
<div class="hero-content">
<div class="column content gap-4">
<h1 class="text-center text-5xl">Welcome to</h1>
<h1 class="mb-4 text-center text-5xl font-bold uppercase">{PLATFORM_NAME}</h1>
<div class="col-3">
<Button onclick={addSpace}>
<CardButton class="btn-neutral">
{#snippet icon()}
<Icon icon={AddCircle} size={7} />
{/snippet}
{#snippet title()}
<div>Add a space</div>
{/snippet}
{#snippet info()}
<div>Use an invite link, or create your own space.</div>
{/snippet}
</CardButton>
</Button>
<Link href="/discover">
<CardButton class="btn-neutral">
{#snippet icon()}
<Icon icon={Compass} size={7} />
{/snippet}
{#snippet title()}
<div>Browse the network</div>
{/snippet}
{#snippet info()}
<div>Find communities on the nostr network.</div>
{/snippet}
</CardButton>
</Link>
<Button onclick={openChat}>
<CardButton class="btn-neutral">
{#snippet icon()}
<Icon icon={ChatRound} size={7} />
{/snippet}
{#snippet title()}
<div>Start a conversation</div>
{/snippet}
{#snippet info()}
<div>Use nostr's encrypted group chats to stay in touch.</div>
{/snippet}
</CardButton>
</Button>
</div>
<PageBar>
{#snippet icon()}
<div class="center">
<Icon icon={History} />
</div>
</div>
</div>
{/snippet}
{#snippet title()}
<strong>Recent Activity</strong>
{/snippet}
{#snippet action()}
<div class="row-2"></div>
{/snippet}
</PageBar>
<PageContent class="flex flex-col gap-2 p-2 pt-4" bind:element>
{#each $recentActivity as { type, event, url, count } (event.id)}
{#if type === "message"}
<RecentConversation {url} {event} {count} />
{:else if event.kind === THREAD}
<ThreadItem {url} {event} />
{:else if event.kind === CLASSIFIED}
<ClassifiedItem {url} {event} />
{:else if event.kind === ZAP_GOAL}
<GoalItem {url} {event} />
{:else if event.kind === EVENT_TIME}
<CalendarEventItem {url} {event} />
{:else}
<NoteItem {url} {event} />
{/if}
{:else}
{#if loading}
<div class="flex justify-center items-center py-20">
<span class="loading loading-spinner mr-3"></span>
Loading recent activity...
</div>
{:else}
<p class="flex flex-col items-center py-20 text-center">No recent activity found!</p>
{/if}
{/each}
</PageContent>
+2 -91
View File
@@ -1,5 +1,4 @@
<script lang="ts">
import {insertAt, removeAt} from "@welshman/lib"
import SettingsMinimalistic from "@assets/icons/settings-minimalistic.svg?dataurl"
import AddCircle from "@assets/icons/add-circle.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
@@ -10,88 +9,9 @@
import MenuSpacesItem from "@app/components/MenuSpacesItem.svelte"
import SpaceAdd from "@app/components/SpaceAdd.svelte"
import {userSpaceUrls, loadUserGroupList, PLATFORM_RELAYS} from "@app/core/state"
import {setSpaceMembershipOrder} from "@app/core/commands"
import {pushModal} from "@app/util/modal"
const addSpace = () => pushModal(SpaceAdd)
const reconcileUrls = (currentUrls: string[], nextUrls: string[]) => {
const mergedUrls = currentUrls.filter(url => nextUrls.includes(url))
for (const url of nextUrls) {
if (!mergedUrls.includes(url)) {
mergedUrls.push(url)
}
}
return mergedUrls
}
const isSameOrder = (a: string[], b: string[]) =>
a.length === b.length && a.every((url, index) => url === b[index])
const reorderSpaceUrls = (targetUrl: string) => {
if (!draggedUrl) {
return
}
const sourceIndex = orderedSpaceUrls.indexOf(draggedUrl)
const targetIndex = orderedSpaceUrls.indexOf(targetUrl)
if (sourceIndex === -1 || targetIndex === -1 || sourceIndex === targetIndex) {
return
}
orderedSpaceUrls = insertAt(
targetIndex,
orderedSpaceUrls[sourceIndex],
removeAt(sourceIndex, orderedSpaceUrls),
)
}
const onDragStart = (e: DragEvent, url: string) => {
draggedUrl = url
dragStartOrder = [...orderedSpaceUrls]
if (e.dataTransfer) {
e.dataTransfer.effectAllowed = "move"
e.dataTransfer.setData("text/plain", url)
}
}
const onDragOver = (e: DragEvent, targetUrl: string) => {
e.preventDefault()
reorderSpaceUrls(targetUrl)
}
const onDrop = (e: DragEvent, targetUrl: string) => {
e.preventDefault()
reorderSpaceUrls(targetUrl)
draggedUrl = undefined
if (dragStartOrder && !isSameOrder(dragStartOrder, orderedSpaceUrls)) {
void setSpaceMembershipOrder(orderedSpaceUrls).catch(console.error)
}
dragStartOrder = undefined
}
const onDragEnd = () => {
draggedUrl = undefined
dragStartOrder = undefined
}
$effect(() => {
const nextUrls = reconcileUrls(orderedSpaceUrls, $userSpaceUrls)
if (!isSameOrder(nextUrls, orderedSpaceUrls)) {
orderedSpaceUrls = nextUrls
}
})
let orderedSpaceUrls = $state<string[]>([])
let draggedUrl = $state<string | undefined>()
let dragStartOrder = $state<string[] | undefined>()
</script>
<Page class="cw-full">
@@ -123,17 +43,8 @@
Loading your spaces...
</div>
{:then}
{#each orderedSpaceUrls as url (url)}
<div
class:opacity-60={draggedUrl === url}
draggable="true"
role="listitem"
ondragstart={e => onDragStart(e, url)}
ondragover={e => onDragOver(e, url)}
ondrop={e => onDrop(e, url)}
ondragend={onDragEnd}>
<MenuSpacesItem {url} />
</div>
{#each $userSpaceUrls as url (url)}
<MenuSpacesItem {url} />
{:else}
<div class="flex flex-col gap-8 items-center py-20">
<p>You haven't added any spaces yet!</p>