Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,18 @@
"@vitest/browser": "^4.0.16",
"@vlcn.io/crsqlite-wasm": "^0.16.0",
"@vlcn.io/xplat-api": "^0.15.0",
"arena-ts": "^1.0.2",
"arktype": "^2.1.29",
"convex": "^1.31.2",
"convex-helpers": "^0.1.108",
"convex-svelte": "^0.0.12",
"happy-dom": "^20.0.11",
"idb": "^8.0.3",
"micromark": "^4.0.2",
"openapi-fetch": "^0.15.0",
"openapi-typescript": "^7.10.1",
"prettier": "^3.7.4",
"prettier-plugin-svelte": "^3.4.1",
"runed": "^0.37.1",
"superstruct": "^2.0.2",
"svelte": "5.46.1",
"svelte-check": "^4.3.5",
Expand All @@ -61,4 +63,4 @@
"dependencies": {
"@tauri-apps/plugin-fs": "~2.4.4"
}
}
}
8 changes: 0 additions & 8 deletions app/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 1 addition & 5 deletions app/src/lib/components/BlockTypeCard.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,7 @@
return link
}
const source = !c.source ? null : c.type !== 'channel' ? c.source : makeLink()
let content = $state("")
// c.type === 'text' ? micromark() : null
c.content?.then(c => {
content = micromark(c)
})
const content = c.type === 'text' ? micromark(c.content ?? '') : null
const imgLoad = (e: Event) => {
const el = e.target as HTMLImageElement
el.classList.add('loaded')
Expand Down
4 changes: 2 additions & 2 deletions app/src/lib/components/omnibar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@
<a href="/" aria-label="home"> ~ </a>/
{#if page.params.channel}
{@render route(page)}
{:else if 'channel' in ($tree.at(-1)?.params ?? {})}
{@render route($tree.at(-1))}
{:else if 'channel' in (tree.at(-1)?.params ?? {})}
{@render route(tree.at(-1))}
{/if}
</div>
<div id="omninput" popover="auto" use:trigger>
Expand Down
2 changes: 1 addition & 1 deletion app/src/lib/components/views/GridView.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
</script>

<div class="grid" class:raw>
{#each content as c, i (c._id)}
{#each content as c, i (c.id)}
<BlockTypeCard {...c} />
{/each}
</div>
Expand Down
2 changes: 1 addition & 1 deletion app/src/lib/components/views/MillerView.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
<main id="miller">
<div class="pane left">
{#if previous && previous.length > 1}
{#each previous as p (p._id)}
{#each previous as p (p.key)}
{@render entry(p)}
{/each}
{:else}
Expand Down
84 changes: 56 additions & 28 deletions app/src/lib/data/block.svelte.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,51 @@
// SPDX-License-Identifier: MPL-2.0

import type { ArenaBlock } from "arena-ts"
import type { ArenaBlock } from "$lib/services/arena/types"
import { entries, channels, users, populateUser } from "./maps.svelte"
import type { Collectable } from "./types"
import { User } from "./user.svelte"

type BlockI = {
id: string,
author_slug: User['key'],
title: string,
description: string,
media?: string,
content?: string,
type: Block['type'],
created_at: number,
updated_at: number,
filename?: string,
provider_url?: string,
image?: string,
source?: string,
attachment?: string,
}

const ArenaTypes: Readonly<
Record<components['schemas']['Block']['type'], Block['type']>
> = Object.freeze({
Text: 'text',
Image: 'media',
Link: 'link',
Embed: 'link',
Attachment: 'attachment',
})

export class Block implements Collectable {
key: string
id: string
title: string = $state('')
description: string = $state('')
media?: string = $state('')
content?: string = $state('')
media?: string | undefined = $state('')
content?: string | undefined = $state('')
// Media and attachment may make sense to merge. One is Media represents file formats directly
// viewable in a browser, while attachements usually need an external renderer.
// building additional viwers should not require changing the data type
type: 'text' | 'media' | 'link' | 'attachment'
created_at: number
updated_at: number = $state()
filename: string
updated_at: number = $state(0)
filename?: string | undefined
provider_url: string
image: string
source: string
Expand All @@ -28,16 +59,13 @@ export class Block implements Collectable {
}
#connections = new Set<string>()
get connections() {
return [...this.#connections.values()].map(c => channels.get(c))
return [...this.#connections.values()].map(c => channels.get(c)).filter(c => c !== undefined)
}
addConnection(slug: string) {
this.#connections.add(slug)
}
rmConnection(slug: string) {
return false
}

constructor(b: Block) {
constructor(b: BlockI) {
this.type = b.type
this.created_at = b.created_at
this.updated_at = b.updated_at
Expand All @@ -56,10 +84,12 @@ export class Block implements Collectable {
this.#author = b.author_slug
entries.set(this.key, this)

populateUser(this.key, this.#author)
const user = users.get(this.#author)
if (user) user.addEntry(this.key, 'blocks')
}

write() {
return {
return JSON.stringify({
id: this.id,
type: this.type,
title: this.title,
Expand All @@ -74,29 +104,27 @@ export class Block implements Collectable {
author_slug: this.#author,
attachment: this.attachment,
connections: [...this.#connections.values()]
}
})
}
static fromArena(block: ArenaBlock): Block {
const data = {
id: block.id,
type: block.class.toLowerCase(),
static fromArena(block: ArenaBlock) {
const data: BlockI = {
id: block.id.toString(),
type: ArenaTypes[block.type],
title: block.title ?? '',
description: block.description ?? '',
description: block.description?.markdown ?? '',
created_at: new Date(block.created_at).valueOf(),
updated_at: new Date(block.updated_at).valueOf(),
content: block.content && block.content,
filename: block.attachment && block.attachment.content_type,
provider_url: block.source && block.source.provider.url,
image: block.image && block.image.original.url,
source: null,
content: block.type === 'Text' ? block.content.markdown : '',
filename: block.type === 'Attachment' ? block.attachment?.url : '',
provider_url: block.source?.provider ? block.source.provider.url : '',
image: block.type !== 'Text' && block.image ? block.image.large.src : undefined,
source: block.source?.url || '',
author_slug: block.user.slug,
attachment: block.attachment?.url
attachment: block.type === 'Attachment' ? block.attachment.url : undefined
}

if (block.class === 'Text')
data.source = block.source ? block.source.url : block.source
else
data.source = block.source && block.source.url
new User(block.user.slug, block.user.name, block.user.avatar ?? '')

// db.exec(`insert or ignore into Providers values (?,?);`, [
// data.source.provider.url,
// data.source.provider.name
Expand Down
69 changes: 41 additions & 28 deletions app/src/lib/data/channel.svelte.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
// SPDX-License-Identifier: MPL-2.0

import type { ArenaChannel, ArenaChannelContents, ArenaChannelWithDetails, ArenaUser } from "arena-ts"
import type { ArenaChannel, ArenaConnection, ArenaEntry } from "$lib/services/arena/types"
import { entries, channels, users, populateUser } from "./maps.svelte"
import type { Child, Collectable, Entry, Collection, User, ArenaConnectionEventData } from "./types"
import type { Collectable, Entry, Collection, User } from "./types"

type ConnectionI = {
key: string
parent_id: string
Expand All @@ -12,6 +13,7 @@ type ConnectionI = {
connected_at: number
connected_by: string
}

export class Connection {
key: string
parent_id: string
Expand All @@ -21,7 +23,6 @@ export class Connection {
connected_at: number
#connected_by: User['key']


constructor(obj: ConnectionI) {
this.parent_id = `${obj.parent_id}`
this.child_id = `${obj.child_id}`
Expand All @@ -40,20 +41,31 @@ export class Connection {
console.error(`Child not found for ${this.key} `);
return
}
return Object.assign(child, this) as Child
return Object.assign(child, this) as Entry & Connection
}
static fromArena(data: ArenaConnectionEventData): ConnectionI {
static fromArena(conn: ArenaConnection, child: ArenaEntry): ConnectionI {
return {
key: `${data.parent.id}:${data.child.id}`,
parent_id: data.parent.slug,
child_id: data.is_channel === true ? data.child.slug : data.child.id.toString(),
position: data.position,
pinned: data.selected ? true : false,
connected_at: new Date(data.connected_at).valueOf(),
connected_by: data.child.connected_by_user_slug
key: `${conn.id}:${child.id}`,
parent_id: conn.id.toString(),
child_id: child.type === 'Channel' ? child.slug : child.id.toString(),
position: conn.position,
pinned: conn.pinned ? true : false,
connected_at: new Date(conn.connected_at).valueOf(),
connected_by: conn.connected_by?.slug ?? ''
}
}
}
type ChannelI = {
slug: string,
id: string,
title: string,
description: string,
created_at: number,
updated_at: number,
status: string,
image?: string,
author: string,
}
export class Channel implements Collection, Collectable {
key: string
slug: string
Expand All @@ -63,28 +75,30 @@ export class Channel implements Collection, Collectable {
description: string
created_at: number
updated_at: number
status: string
status: 'private' | 'public' | 'closed'
image: string
#author: string
#keys = new Set<string>()
#blocks: Connection[] = $state([])
#connections = new Set<string>()

get author() {
return users.get(this.#author)
}
#blocks: Connection[] = $state([])
get entries() {
return this.#blocks.map(conn => conn.get()).sort((a, b) => a.position - b.position)
return this.#blocks.map(conn => conn.get()).filter(e => e !== undefined).sort((a, b) => a.position - b.position)
}
#connections = new Set<string>()
get connections() {
return [...this.#connections.values()].map(slug => channels.get(slug))
return [...this.#connections.values()].map(slug => channels.get(slug)).filter(e => e !== undefined)
}
addConnection(slug: string) {
this.#connections.add(slug)
}
rmConnection(slug: string) {
return false
}
constructor(obj: Channel) {
constructor(obj: ChannelI) {
// const existing = channels.get(`${obj.id}`)
this.id = `${obj.id}`
this.title = obj.title
this.slug = obj.slug
Expand All @@ -93,7 +107,7 @@ export class Channel implements Collection, Collectable {
this.description = obj.description
this.created_at = obj.created_at
this.updated_at = obj.updated_at
this.image = obj.image
this.image = obj.image ?? ''
this.#author = obj.author
channels.set(this.key, this)
entries.set(this.key, this)
Expand All @@ -111,22 +125,21 @@ export class Channel implements Collection, Collectable {
get length() {
return this.#blocks.length
}
static fromArena(c: ArenaChannel | ArenaChannelWithDetails): Channel {
const flags = [c.kind] as ChannelParsed['flags']
if (c.collaboration) flags.push('collaboration')
if (c.published) flags.push('published')
static fromArena(c: ArenaChannel): ChannelI {
// const flags = [c.kind] as ChannelParsed['flags']
// if (c.collaboration) flags.push('collaboration')
// if (c.published) flags.push('published')

return {
id: c.slug,
type: 'channel',
title: c.title,
slug: c.slug,
description: c.description?.markdown ?? '',
created_at: new Date(c.created_at).valueOf(),
updated_at: new Date(c.updated_at).valueOf(),
flags,
status: c.status,
source: 'arena',
author: c.user?.slug ?? c.user_id,
status: c.visibility,
// source: 'arena',
author: c.owner.slug,
}
}
}
Loading