Skip to content

Commit

Permalink
Cleanup Navigation
Browse files Browse the repository at this point in the history
  • Loading branch information
fallaciousreasoning committed Oct 16, 2024
1 parent 0780661 commit 4bd17aa
Show file tree
Hide file tree
Showing 3 changed files with 152 additions and 103 deletions.
5 changes: 3 additions & 2 deletions components/ai_chat/resources/page/chat_ui.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ import {
ConversationContextProvider
} from './state/conversation_context'
import FullScreen from './components/full_screen'
import { NavigationContext } from './components/navigation_context'
import { NavigationContext } from '$web-common/navigation/Context'
import { Routes } from './components/navigation_context'

setIconBasePath('chrome-untrusted://resources/brave-icons')

Expand All @@ -31,6 +32,7 @@ function App() {

return (
<NavigationContext>
<Routes />
<AIChatContextProvider>
<ConversationContextProvider>
<BraveCoreThemeProvider>
Expand All @@ -45,7 +47,6 @@ function App() {
function Content() {
const aiChatContext = useAIChat()

console.log(aiChatContext.isStandalone)
if (aiChatContext.isStandalone === undefined) {
return <div>loading...</div>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,119 +3,35 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.

import { useEffect } from "react";
import { AIChatContext, useAIChat } from "../../state/ai_chat_context";
import * as React from "react";
import { useNavigation, useParams } from '$web-common/navigation/Context'
import { useEffect } from 'react'

type Params = { [key: string]: string | undefined }

export interface NavigationContext {
params: Params
}

const Context = React.createContext<NavigationContext>({
params: {}
})

const routes: { [key: string]: (context: AIChatContext, params: Params) => Promise<void> } = {
['/']: async (context) => {
// context.onNewConversation()
},
['/{chatId}']: async (context, params: { chatId: string }) => {
// if (context.visibleConversations.find(c => c.uuid === params.chatId)) {
// context.onSelectConversationUuid(params.chatId)
// } else {
// location.href = "/"
// }
}
}

const getParamName = (part: string) => {
if (part.startsWith("{") && part.endsWith("}")) {
return part.substring(1, part.length - 1)
}
return null
}

const findMatchingRoute = (url: URL) => {
const path = url.pathname
const pathParts = path.split('/')


for (const [path, handler] of Object.entries(routes)) {
const routeParts = path.split('/')

if (routeParts.length !== pathParts.length) continue

let isMatch = true
const params: { [key: string]: string } = {}
for (let i = 0; i < pathParts.length; ++i) {
const routePart = routeParts[i]
const pathPart = pathParts[i]

const paramName = getParamName(routePart)
// If its a param, store the value
if (paramName) {
params[paramName] = pathPart
} else if (routePart !== pathPart) {
// The path doesn't match, so bail
isMatch = false
break
}
}

if (isMatch) {
return [handler, params] as const
}
}

// Couldn't find a handler
return [null, {}] as const
export function useSelectedConversation() {
return useParams().chatId
}

export function selectConversation(conversationId: string | null) {
window.location.href = conversationId ? `/${conversationId}` : "/"
}

export function useParams<T extends Params = Params>() {
return React.useContext(Context).params as T
}

export function useSelectedConversation() {
return useParams().chatId
}

export function NavigationContext(props: React.PropsWithChildren) {
const chat = useAIChat()
const [params, setParams] = React.useState<Params>(findMatchingRoute(new URL(window.location.href))[1])
const routes = [
'/',
'/{chatId}'
]

export function Routes() {
const { addRoute, removeRoute } = useNavigation()
useEffect(() => {
const handler = (e?: NavigateEvent) => {
const url = e?.destination.url ? new URL(e.destination.url) : new URL(window.location.href)
const [handler, params] = findMatchingRoute(url)

if (!handler) {
setParams({})
return
}

setParams(params)
if (e) {
e.intercept({
handler: () => handler(chat, params)
})
} else {
handler(chat, params)
}
for (const route of routes) {
addRoute(route)
}

window.navigation.addEventListener('navigate', handler)
return () => {
window.navigation.removeEventListener('navigate', handler)
for (const route of routes) {
removeRoute(route)
}
}
}, [chat.visibleConversations])
}, [])

return <Context.Provider value={{ params }} >
{props.children}
</Context.Provider>
return null
}
132 changes: 132 additions & 0 deletions components/common/navigation/Context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// Copyright (c) 2024 The Brave Authors. All rights reserved.
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.

import { useEffect } from "react";
import * as React from "react";

export type NavigationParams = { [key: string]: string | undefined }
export type Routes = Map<string, (undefined | ((params: NavigationParams) => void))[]>;

export interface NavigationContext {
params: NavigationParams
addRoute: (route: string, callback?: (params: any) => void) => void
removeRoute: (route: string, callback?: (params: any) => void) => void
}

const Context = React.createContext<NavigationContext>({
params: {},
addRoute: () => { },
removeRoute: () => { }
})

const getParamName = (part: string) => {
if (part.startsWith("{") && part.endsWith("}")) {
return part.substring(1, part.length - 1)
}
return null
}

const findMatchingRoute = (url: string, routes: Routes) => {
const path = new URL(url).pathname
const pathParts = path.split('/')


for (const [path, handler] of routes.entries()) {
const routeParts = path.split('/')

if (routeParts.length !== pathParts.length) continue

let isMatch = true
const params: { [key: string]: string } = {}
for (let i = 0; i < pathParts.length; ++i) {
const routePart = routeParts[i]
const pathPart = pathParts[i]

const paramName = getParamName(routePart)
// If its a param, store the value
if (paramName) {
params[paramName] = pathPart
} else if (routePart !== pathPart) {
// The path doesn't match, so bail
isMatch = false
break
}
}

if (isMatch) {
return [handler, params] as const
}
}

// Couldn't find a handler
return [null, {}] as const
}

export function useParams<T extends NavigationParams = NavigationParams>() {
return React.useContext(Context).params as T
}

export function useNavigation<T extends NavigationParams = NavigationParams>() {
return React.useContext(Context) as NavigationContext & { params: T }
}


export function NavigationContext(props: React.PropsWithChildren) {
const routes = React.useRef<Routes>(new Map())
const [params, setParams] = React.useState<NavigationParams>({})

useEffect(() => {
const handler = (e: NavigateEvent) => {
const [handler, params] = findMatchingRoute(e.destination.url, routes.current)

if (!handler) {
setParams({})
return
}

setParams(params)
e.intercept({
handler: async () => {
for (const callback of handler) {
callback?.(params)
}
}
})
}

window.navigation.addEventListener('navigate', handler)
return () => {
window.navigation.removeEventListener('navigate', handler)
}
}, [])

const addRoute = React.useCallback((route: string, callback?: (params: NavigationParams) => void) => {
if (!routes.current.has(route)) {
routes.current.set(route, [])
}

routes.current.get(route)!.push(callback)

setParams(findMatchingRoute(location.href, routes.current)[1])
}, [])

const removeRoute = React.useCallback((route: string, callback?: (params: NavigationParams) => void) => {
const callbacks = routes.current.get(route) ?? []
const index = callbacks.indexOf(callback)
if (index !== -1) {
callbacks.splice(index, 1)
}

if (callbacks.length === 0) {
routes.current.delete(route)
}

setParams(findMatchingRoute(location.href, routes.current)[1])
}, [])

return <Context.Provider value={{ params, addRoute, removeRoute }} >
{props.children}
</Context.Provider>
}

0 comments on commit 4bd17aa

Please sign in to comment.