Skip to content

Commit

Permalink
Handle eph login failure state
Browse files Browse the repository at this point in the history
- .form-submit -> .button-type-primary
- Allow cancelling dialog open/close
- Actually handle load params types correctly
- Use the correct values for WorkEditForm
- Handle all of login in ViewContainer because it's incredibly complicated due to view transitions
  • Loading branch information
ChiriVulpes committed Nov 7, 2024
1 parent 0e3fba6 commit c2381cc
Show file tree
Hide file tree
Showing 28 changed files with 237 additions and 105 deletions.
11 changes: 10 additions & 1 deletion lang/en-nz.quilt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ home/label: home

# shared

## action
login-or-signup: Log In or Sign Up

## form
name/label: Name
name/hint: Your display name or penname — the name your comments and works are attributed to. This can be changed whenever you like, and doesn't have to be unique.
Expand Down Expand Up @@ -80,13 +83,19 @@ primary-nav/alt: primary

## user/profile
alt: profile
popover/login: Log In or Sign Up
popover/login=shared/action/login-or-signup
popover/profile: View Profile
popover/account: Account Settings

# view
container/alt: main content

## shared
### login-required
title: Log-in required
description: You must log in or sign up to view this content.
action=shared/action/login-or-signup

## error
title: Error {CODE}
description-404: Your adventure has come to a close, your questions left unanswered, your goals unfulfilled. What will you do?
Expand Down
15 changes: 0 additions & 15 deletions src/App.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import FocusListener from "ui/utility/FocusListener"
import HoverListener from "ui/utility/HoverListener"
import Mouse from "ui/utility/Mouse"
import Viewport from "ui/utility/Viewport"
import AccountView from "ui/view/AccountView"
import ViewContainer from "ui/ViewContainer"
import Async from "utility/Async"
import Env from "utility/Env"
Expand All @@ -19,7 +18,6 @@ import Time from "utility/Time"
interface AppExtensions {
navigate: Navigator
view: ViewContainer
logIn (): Promise<void>
}

interface App extends Component, AppExtensions { }
Expand Down Expand Up @@ -97,19 +95,6 @@ async function App (): Promise<App> {
.extend<AppExtensions>(app => ({
navigate: Navigator(app),
view,
logIn: async () => {
if (Session.Auth.author.value)
return

const accountView = await view.showEphemeral(AccountView, undefined)
if (accountView)
await Session.Auth.await(accountView)

if (accountView?.removed.value)
return

await view.hideEphemeral()
},
}))
.appendTo(document.body)

Expand Down
6 changes: 4 additions & 2 deletions src/navigation/Navigate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type App from "App"
import type { RoutePath } from "navigation/Routes"
import Routes from "navigation/Routes"
import ErrorView from "ui/view/ErrorView"
import type ViewContainer from "ui/ViewContainer"
import Env from "utility/Env"

declare global {
Expand All @@ -12,7 +13,7 @@ interface Navigator {
fromURL (): Promise<void>
toURL (route: RoutePath): Promise<void>
toRawURL (url: string): boolean
logIn (): Promise<void>
ephemeral: ViewContainer["showEphemeral"]
}

function Navigator (app: App): Navigator {
Expand Down Expand Up @@ -90,7 +91,8 @@ function Navigator (app: App): Navigator {
console.error(`Unsupported raw URL to navigate to: "${url}"`)
return false
},
logIn: () => app.logIn(),
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
ephemeral: (...args: any[]) => (app.view.showEphemeral as any)(...args),
}

// eslint-disable-next-line @typescript-eslint/no-misused-promises
Expand Down
14 changes: 7 additions & 7 deletions src/package-lock.json

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

2 changes: 1 addition & 1 deletion src/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"private": true,
"dependencies": {
"api.fluff4.me": "^1.0.110",
"api.fluff4.me": "^1.0.112",
"prosemirror-example-setup": "1.2.3",
"prosemirror-markdown": "1.13.1",
"prosemirror-state": "1.4.3",
Expand Down
157 changes: 118 additions & 39 deletions src/ui/ViewContainer.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,73 @@
import type { ErrorResponse } from "api.fluff4.me"
import Session from "model/Session"
import Component from "ui/Component"
import Button from "ui/component/core/Button"
import Dialog from "ui/component/core/Dialog"
import ViewTransition from "ui/view/component/ViewTransition"
import AccountView from "ui/view/AccountView"
import ErrorView from "ui/view/ErrorView"
import RequireLoginView from "ui/view/RequireLoginView"
import ViewTransition from "ui/view/shared/ext/ViewTransition"
import type View from "ui/view/View"
import type ViewDefinition from "ui/view/ViewDefinition"

interface ViewContainerExtensions {
view?: View
show<VIEW extends View, PARAMS extends object | undefined> (view: ViewDefinition<VIEW, PARAMS>, params: PARAMS): Promise<VIEW | undefined>
show<VIEW extends View, PARAMS extends object | undefined, LOAD_PARAMS extends object | undefined> (view: ViewDefinition<VIEW, PARAMS, LOAD_PARAMS>, params: PARAMS): Promise<VIEW | undefined>

ephemeral?: View
ephemeralDialog: Dialog
showEphemeral<VIEW extends View, PARAMS extends object | undefined> (view: ViewDefinition<VIEW, PARAMS>, params: PARAMS): Promise<VIEW | undefined>
showEphemeral<VIEW extends View, PARAMS extends object | undefined, LOAD_PARAMS extends object | undefined> (view: ViewDefinition<VIEW, PARAMS, LOAD_PARAMS>, params: PARAMS): Promise<VIEW | undefined>
hideEphemeral (): Promise<void>
}

interface ViewContainer extends Component, ViewContainerExtensions { }

let globalId = 0
const ViewContainer = (): ViewContainer => {

let cancelLogin: (() => void) | undefined

const container = Component()
.style("view-container")
.tabIndex("programmatic")
.ariaRole("main")
.ariaLabel.use("view/container/alt")
.extend<ViewContainerExtensions>(container => ({
show: async <VIEW extends View, PARAMS extends object | undefined> (definition: ViewDefinition<VIEW, PARAMS>, params: PARAMS) => {
show: async <VIEW extends View, PARAMS extends object | undefined, LOAD_PARAMS extends object | undefined> (definition: ViewDefinition<VIEW, PARAMS, LOAD_PARAMS>, params: PARAMS) => {
const showingId = ++globalId
if (definition.prepare)
await definition.prepare(params)

let view: VIEW | undefined
let loadParams: LOAD_PARAMS | undefined = undefined

const needsLogin = definition.requiresLogin && Session.Auth.state.value !== "logged-in"
if (needsLogin || definition.load) {
let loginPromise: Promise<boolean> | undefined
const transition = ViewTransition.perform("view", async () => {
swapRemove()
if (!needsLogin)
return

const login = logIn()
loginPromise = login?.authed
await login?.accountViewShown
})
await transition.updateCallbackDone
await loginPromise

if (needsLogin && Session.Auth.state.value !== "logged-in") {
ViewTransition.perform("view", async () => {
hideEphemeral()
await swapAdd(RequireLoginView)
})
return
}
}

loadParams = !definition.load ? undefined : await definition.load(params)

if (globalId !== showingId)
return

let view: VIEW | undefined

if (container.view || showingId > 1) {
const transition = ViewTransition.perform("view", swap)
await transition.updateCallbackDone
Expand All @@ -47,16 +78,21 @@ const ViewContainer = (): ViewContainer => {
return view

async function swap () {
container.view?.remove()
swapRemove()
await swapAdd()
}

container.ephemeralDialog.close()
container.ephemeral?.remove()
delete container.ephemeral
container.attributes.remove("inert")
function swapRemove () {
container.view?.remove()
hideEphemeral()
}

// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const shownView = await Promise.resolve(definition.create(params))
.then(v => view = v)
async function swapAdd (replacementDefinition: ViewDefinition<View, object | undefined, object | undefined> = definition) {
const shownView = await Promise.resolve(replacementDefinition.create(params, loadParams))
.then(v => {
view = replacementDefinition === definition ? v as VIEW : undefined
return v
})
.catch((error: Error & Partial<ErrorResponse>) => ErrorView.create({ code: error.code ?? 500, error }))
if (shownView) {
shownView.appendTo(container)
Expand All @@ -72,43 +108,86 @@ const ViewContainer = (): ViewContainer => {
.setNotModal()
.append(Button()
.style("view-container-ephemeral-close")
.event.subscribe("click", () => container.hideEphemeral()))
.event.subscribe("click", () => {
if (cancelLogin)
cancelLogin()
else
return container.hideEphemeral()
}))
.appendTo(document.body),

showEphemeral: async <VIEW extends View, PARAMS extends object | undefined> (definition: ViewDefinition<VIEW, PARAMS>, params: PARAMS) => {
showEphemeral: async <VIEW extends View, PARAMS extends object | undefined, LOAD_PARAMS extends object | undefined> (definition: ViewDefinition<VIEW, PARAMS, LOAD_PARAMS>, params: PARAMS) => {
let view: VIEW | undefined

const transition = document.startViewTransition(swap)
const transition = document.startViewTransition(async () =>
view = await showEphemeral(definition, params))
await transition.updateCallbackDone

return view

async function swap () {
container.ephemeral?.remove()
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const shownView = await Promise.resolve(definition.create(params))
.then(v => view = v)
.catch((error: Error & Partial<ErrorResponse>) => ErrorView.create({ code: error.code ?? 500, error }))
if (shownView) {
shownView.appendTo(container.ephemeralDialog)
container.ephemeral = shownView
container.ephemeralDialog.open()
container.attributes.add("inert")
}
}
},
hideEphemeral: async () => {
const transition = document.startViewTransition(() => {
container.ephemeralDialog.close()
container.ephemeral?.remove()
delete container.ephemeral
container.attributes.remove("inert")
})
const transition = document.startViewTransition(hideEphemeral)
await transition.updateCallbackDone
},
}))

return container

async function showEphemeral<VIEW extends View> (definition: ViewDefinition<VIEW, any, any>, params: any) {
container.ephemeral?.remove()

let view: VIEW | undefined

const shownView = await Promise.resolve(definition.create(params))
.then(v => view = v)
.catch((error: Error & Partial<ErrorResponse>) => ErrorView.create({ code: error.code ?? 500, error }))
if (shownView) {
shownView.appendTo(container.ephemeralDialog)
container.ephemeral = shownView
container.ephemeralDialog.open()
container.attributes.add("inert")
container.ephemeralDialog.opened.subscribe(shownView, opened => {
if (!opened) {
hideEphemeral()
}
})
}

return view
}

function hideEphemeral () {
container.ephemeralDialog.close()
container.ephemeral?.remove()
delete container.ephemeral
container.attributes.remove("inert")
}

function logIn () {
if (Session.Auth.author.value)
return

const accountViewShown = showEphemeral(AccountView, undefined)
const authPromise = accountViewShown.then(async view => {
if (!view)
return false

const loginCancelledPromise = new Promise<void>(resolve => cancelLogin = resolve)

await Promise.race([
Session.Auth.await(view),
loginCancelledPromise,
])

cancelLogin = undefined
return Session.Auth.state.value === "logged-in"
})

return {
accountViewShown,
authed: authPromise,
}
}
}

export default ViewContainer
2 changes: 1 addition & 1 deletion src/ui/component/core/Block.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Component from "ui/Component"
import Heading from "ui/component/core/Heading"
import Paragraph from "ui/component/core/Paragraph"
import ViewTransition from "ui/view/component/ViewTransition"
import ViewTransition from "ui/view/shared/ext/ViewTransition"

interface BlockExtensions {
readonly header: Component
Expand Down
Loading

0 comments on commit c2381cc

Please sign in to comment.