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
1 change: 1 addition & 0 deletions client/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<meta name="viewport" content="width=device-width, initial-scale=1.0,maximum-scale=1.0" />
<script src="https://accounts.google.com/gsi/client" async></script>
<link rel="manifest" href="/tapestries.webmanifest" />
</head>
<body>
<div id="root"></div>
Expand Down
4 changes: 4 additions & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"scripts": {
"start": "vite",
"build": "tsc -b && vite build",
"build:watch": "tsc -b && vite build --watch",
"lint-ts": "eslint src --report-unused-disable-directives --max-warnings 0",
"lint-css": "stylelint src/**/*.css",
"prettier": "prettier ./src --check",
Expand All @@ -23,6 +24,8 @@
"color": "^5.0.0",
"date-fns": "^4.1.0",
"dompurify": "^3.2.6",
"file-type": "^21.0.0",
"idb": "^8.0.3",
"immer": "^10.1.1",
"lodash-es": "^4.17.21",
"marked": "^15.0.12",
Expand All @@ -41,6 +44,7 @@
"tapestry-core-client": "^1.0.0",
"tapestry-shared": "^1.0.0",
"video.js": "^8.23.4",
"vite-plugin-pwa": "^1.1.0",
"wavesurfer.js": "^7.9.5",
"zod": "^3.25.76"
},
Expand Down
Binary file added client/public/screenshot-narrow.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added client/public/screenshot-wide.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
37 changes: 37 additions & 0 deletions client/public/tapestries.webmanifest
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"short_name": "Tapestries",
"name": "Tapestries",
"icons": [
{
"src": "favicon.png",
"sizes": "487x487",
"type": "image/png"
}
],
"start_url": ".",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#f9703e",
"screenshots": [
{
"src": "screenshot-wide.png",
"sizes": "1920x968",
"type": "image/png",
"form_factor": "wide"
},
{
"src": "screenshot-narrow.png",
"sizes": "415x790",
"type": "image/png",
"form_factor": "narrow"
}
],
"file_handlers": [
{
"action": "/",
"accept": {
"application/zip": [".zip"]
}
}
]
}
7 changes: 5 additions & 2 deletions client/src/auth/google/login-button.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import { useEffect, useRef } from 'react'
import styles from './styles.module.css'
import { Breakpoint, useResponsive } from '../../providers/responsive-provider'
import { useOnline } from '../../services/api'

export function GoogleLoginButton() {
const button = useRef<HTMLDivElement>(null)

const type = useResponsive() <= Breakpoint.SM ? 'icon' : 'standard'

const online = useOnline() === 'online'

useEffect(() => {
if (button.current) {
if (button.current && online) {
window.google.accounts.id.renderButton(button.current, {
type,
shape: 'rectangular',
Expand All @@ -18,7 +21,7 @@ export function GoogleLoginButton() {
logo_alignment: 'left',
})
}
}, [type])
}, [type, online])

return <div ref={button} className={styles.root} />
}
4 changes: 3 additions & 1 deletion client/src/components/lazy-list/lazy-list-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { debounce, DebouncedFunc } from 'lodash-es'
import { Draft } from 'immer'
import { Observable } from 'tapestry-core-client/src/lib/events/observable'
import { CanceledError } from 'axios'
import { NoConnectionError } from '../../errors'

export interface LazyListLoaderConfig {
/**
Expand Down Expand Up @@ -134,7 +135,8 @@ export class LazyListLoader<T> extends Observable<ListResponseDto<T> & { state:
} catch (error) {
if (
!(error instanceof CanceledError) &&
!(error instanceof DOMException && error.name === 'AbortError')
!(error instanceof DOMException && error.name === 'AbortError') &&
!(error instanceof NoConnectionError)
) {
console.error('Error while loading lazy list items', error)
}
Expand Down
4 changes: 1 addition & 3 deletions client/src/components/leave-tapestry-dialog/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@ export function LeaveTapestryDialog() {
const tapestryViewPath = useTapestryPath('view')
const pendingRequests = useTapestryData('pendingRequests')

const pendingUpload = useObservable(itemUpload).some(
(i) => i.state === 'pending' || i.state === 'uploading',
)
const pendingUpload = useObservable(itemUpload).some((i) => i.state === 'uploading')

return (
<BlockNavigationDialog
Expand Down
43 changes: 22 additions & 21 deletions client/src/components/offline-indicator/index.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,39 @@
import clsx from 'clsx'
import styles from './styles.module.css'
import { useEffect, useState } from 'react'
import { useDispatch } from '../../pages/tapestry/tapestry-providers'
import { setSnackbar } from '../../pages/tapestry/view-model/store-commands/tapestry'
import { Icon } from 'tapestry-core-client/src/components/lib/icon/index'
import { useOnline } from '../../services/api'
import { usePrevious } from 'tapestry-core-client/src/components/lib/hooks/use-previous'
import { useEffect } from 'react'

interface OfflineIndicatorProps {
className?: string
}

export function OfflineIndicator({ className }: OfflineIndicatorProps) {
const [offline, setOffline] = useState(!navigator.onLine)
const online = useOnline()
const dispatch = useDispatch()

useEffect(() => {
const onConnectionChanged = () => {
setOffline(!navigator.onLine)
dispatch(
setSnackbar(
navigator.onLine
? { text: 'You are back online', variant: 'success' }
: { text: 'Connection lost', variant: 'error' },
),
)
}
const wasOnline = usePrevious(online)

window.addEventListener('online', onConnectionChanged)
window.addEventListener('offline', onConnectionChanged)

return () => {
window.removeEventListener('online', onConnectionChanged)
window.removeEventListener('offline', onConnectionChanged)
useEffect(() => {
if (online === 'online' && wasOnline === 'online') {
return
}
}, [dispatch])
dispatch(
setSnackbar(
online === 'online'
? { text: 'You are back online', variant: 'success' }
: {
text: wasOnline === 'online' ? 'Connection lost' : 'No connection',
variant: 'error',
},
),
)
}, [dispatch, online, wasOnline])

return offline ? <Icon icon="wifi_off" className={clsx(styles.root, className)} /> : null
return online !== 'offline' ? null : (
<Icon icon="wifi_off" className={clsx(styles.root, className)} />
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,16 +41,14 @@ export function TapestryItem({ id, children, halo }: TapestryItemProps) {

const isContentInteractive = id === interactiveElement?.modelId

const item = useObservable(itemUpload).find((i) => i.itemId === dto.id)

const viewportRect = new Rectangle(
positionAtViewport(viewport, ORIGIN),
scaleSize(viewport.size, 1 / viewport.transform.scale),
)

const isVisible = viewportRect.intersects(new Rectangle(dto))

// @ts-expect-error TS wants us to check for a media item
const item = useObservable(itemUpload).find((i) => i.objectUrl === dto.source)

return (
<BaseTapestryItem
id={id}
Expand Down
6 changes: 6 additions & 0 deletions client/src/errors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ export class APIError extends Error {
}
}

export class NoConnectionError extends Error {
constructor(public cause?: unknown) {
super('No connection error')
}
}

export class UnknownError extends Error {
constructor(public cause?: unknown) {
super('Unknown error')
Expand Down
23 changes: 23 additions & 0 deletions client/src/hooks/use-launch-queue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useEffect } from 'react'
import { usePropRef } from 'tapestry-core-client/src/components/lib/hooks/use-prop-ref'

export function useLaunchQueue(callback: (files: File[]) => unknown) {
const callbackRef = usePropRef(callback)

useEffect(() => {
if (!window.launchQueue) {
return
}
window.launchQueue.setConsumer(async ({ files: fileHandles }) => {
const files = await Promise.all(
fileHandles.reduce<Promise<File>[]>((acc, f) => {
if (f instanceof FileSystemFileHandle) {
acc.push(f.getFile())
}
return acc
}, []),
)
callbackRef.current(files)
})
}, [callbackRef])
}
6 changes: 3 additions & 3 deletions client/src/lib/media.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { pdfjs } from 'react-pdf'
import { urlToBlob } from 'tapestry-core-client/src/lib/file'
import { urlToBuffer } from 'tapestry-core-client/src/lib/file'
import { aspectRatio, clampSize, innerFit, Size } from 'tapestry-core/src/lib/geometry'
import { WEB_SOURCE_PARSERS } from 'tapestry-core/src/web-sources'

Expand All @@ -23,8 +23,8 @@ export function loadImageFromBlob(file: Blob) {
})
}

export function mediaSourceToBlob(source: MediaItemSource) {
return source instanceof File ? source : urlToBlob(source)
async function mediaSourceToBlob(source: MediaItemSource) {
return source instanceof File ? source : new Blob([await urlToBuffer(source)])
}

export const MIN_ITEM_SIZE: Size = {
Expand Down
6 changes: 6 additions & 0 deletions client/src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/// <reference types="vite-plugin-pwa/client" />
import { enableMapSet, enablePatches } from 'immer'
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
Expand All @@ -12,6 +13,7 @@ import {

import { useResponsiveClass } from 'tapestry-core-client/src/components/lib/hooks/use-responsive-class'
import { GoogleFonts } from 'tapestry-core-client/src/components/lib/icon/index'
import { registerSW } from 'virtual:pwa-register'

import './index.css'
import { SessionLayout } from './layouts/session/index'
Expand Down Expand Up @@ -67,3 +69,7 @@ createRoot(document.getElementById('root')!).render(
<RouterProvider router={router} />
</StrictMode>,
)

if (import.meta.env.PROD) {
registerSW()
}
19 changes: 7 additions & 12 deletions client/src/model/data/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,8 @@ import { resource } from '../../services/rest-resources'
import { isFunction } from 'lodash-es'
import mime from 'mime'
import axios, { AxiosProgressEvent } from 'axios'
import { arrayToIdMap, fileExtension, isMediaItem } from 'tapestry-core/src/utils'
import { arrayToIdMap, fileExtension, isBlobURL, isMediaItem } from 'tapestry-core/src/utils'
import { userSettings } from '../../services/user-settings'
import { itemUpload } from '../../services/item-upload'
import { PublicUserProfileDto } from 'tapestry-shared/src/data-transfer/resources/dtos/user'
import { RelDto, RelUpdateDto } from 'tapestry-shared/src/data-transfer/resources/dtos/rel'
import { CommentThreadsDto } from 'tapestry-shared/src/data-transfer/resources/dtos/comment-threads'
Expand All @@ -52,7 +51,7 @@ import {
PresentationStepUpdateDto,
} from 'tapestry-shared/src/data-transfer/resources/dtos/presentation-step'
import { FetchContentTypeProxyDto } from 'tapestry-shared/src/data-transfer/resources/dtos/proxy'
import { ItemType, MediaItemType } from 'tapestry-core/src/data-format/schemas/item'
import { ItemType, MediaItem, MediaItemType } from 'tapestry-core/src/data-format/schemas/item'
import { viewModelFromTapestry } from 'tapestry-core-client/src/view-model/utils'

export const EDITABLE_TAPESTRY_PROPS = [
Expand Down Expand Up @@ -270,14 +269,6 @@ export async function uploadAsset(
return key
}

function prepareMediaSource(source: MediaItemSource): string {
if (typeof source === 'string') {
return source
}

return itemUpload.prepare(source)
}

export async function getItemSize(item: ItemDto): Promise<Size> {
if (isMediaItem(item)) {
return getMediaItemSize(item.type, item.source)
Expand All @@ -299,7 +290,7 @@ export async function createMediaItem<T extends MediaItemType>(
return {
type,
size,
source: prepareMediaSource(source),
source: typeof source === 'string' ? source : URL.createObjectURL(source),
title: '',
dropShadow: true,
position: ORIGIN,
Expand Down Expand Up @@ -367,3 +358,7 @@ export function userAccess(
export function fullName({ givenName, familyName }: PublicUserProfileDto) {
return `${givenName} ${familyName}`
}

export function isLocalMediaItem(item: unknown): item is MediaItem {
return isMediaItem(item) && isBlobURL(item.source)
}
3 changes: 3 additions & 0 deletions client/src/pages/dashboard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { DragArea } from 'tapestry-core-client/src/components/lib/drag-area'
import { Breakpoint, useResponsive } from '../../providers/responsive-provider'
import { DashboardHeader } from './header'
import { TapestryListControls } from './tapestry-list-controls'
import { useLaunchQueue } from '../../hooks/use-launch-queue'
import { useThemeCss } from 'tapestry-core-client/src/components/lib/hooks/use-theme-css'
import { dashboardPath } from '../../utils/paths'
import { CreateTapestryDialog } from '../../components/create-tapestry-dialog'
Expand Down Expand Up @@ -140,6 +141,8 @@ export function Dashboard() {
reset: resetImports,
} = useTapestryImport(() => tapestryLoader?.reload())

useLaunchQueue(importTapestries)

const smOrLess = useResponsive() <= Breakpoint.SM

if (
Expand Down
Loading