Skip to content
Draft
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
9 changes: 7 additions & 2 deletions apps/tlon-web/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,11 @@ function AppRoutes() {
}, []);

const documentTitleFormatterMobile = useCallback(
(_options: any, route: Route<string>) => {
(options: any, route: Route<string>) => {
// Honor any explicit title set via navigation.setOptions on the
// focused screen — that way screens like AppViewer / AppLauncher can
// manage their own title without us hardcoding cases here.
if (options?.title) return options.title;
if (!route?.name) return 'Tlon';

if (route.name === 'GroupChannels') {
Expand Down Expand Up @@ -265,7 +269,8 @@ function AppRoutes() {
);

const documentTitleFormatterDesktop = useCallback(
(_options: any, route: Route<string>) => {
(options: any, route: Route<string>) => {
if (options?.title) return options.title;
if (!route?.name) return 'Tlon';

// For channel routes
Expand Down
4 changes: 2 additions & 2 deletions desk/desk.docket-0
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
info+'Start, host, and cultivate communities. Own your communications, organize your resources, and share documents. Tlon is a decentralized platform that offers a full, communal suite of tools for messaging, writing and sharing media with others.'
color+0xde.dede
image+'https://bootstrap.urbit.org/tlon.svg?v=1'
glob-http+['https://bootstrap.urbit.org/glob-0v4c002.8315i.9dijh.eqbnl.3n737.glob' 0v4c002.8315i.9dijh.eqbnl.3n737]
glob-http+['https://bootstrap.urbit.org/glob-0v5.pilpm.p2v4i.6o0f3.vpi93.vnctl.glob' 0v5.pilpm.p2v4i.6o0f3.vpi93.vnctl]
base+'groups'
version+[11 2 2]
version+[11 2 1]
website+'https://tlon.io'
license+'MIT'
==
1 change: 1 addition & 0 deletions packages/api/src/client/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { udToDate } from './apiUtils';
export { normalizeUrbitColor } from './utils';
export * from './channelContentConfig';
export * from './channelsApi';
export * from './chatApi';
Expand Down
10 changes: 10 additions & 0 deletions packages/api/src/client/settingsApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,16 @@ export interface Pikes {
[desk: string]: Pike;
}

// Returns all installed apps known to %docket, keyed by desk name. Each entry
// includes title, color, image, href and chad (install status).
export async function getCharges(): Promise<{ [desk: string]: Charge }> {
const res = await scry<ChargeUpdateInitial>({
app: 'docket',
path: '/charges',
});
return res.initial ?? {};
}

export async function getAppInfo(): Promise<db.AppInfo> {
const pikes = await scry<Pikes>({
app: 'hood',
Expand Down
52 changes: 52 additions & 0 deletions packages/app/features/apps/AppLauncherScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { NavigationProp, useNavigation } from '@react-navigation/native';
import * as store from '@tloncorp/shared/store';
import { useCallback, useEffect } from 'react';

import { useCurrentUserId } from '../../hooks/useCurrentUser';
import { useOpenApp } from '../../hooks/useOpenApps';
import type { RootStackParamList } from '../../navigation/types';
import { AppLauncherView, NavBarView, View } from '../../ui';

export function AppLauncherScreen() {
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const currentUserId = useCurrentUserId();
const { data: apps = [], isLoading } = store.useInstalledApps();
const openApp = useOpenApp();
useEffect(() => {
navigation.setOptions({ title: 'Apps' });
}, [navigation]);

const handleSelectApp = useCallback(
(app: store.InstalledApp) => {
openApp(app.desk);
navigation.navigate('AppViewer', { desk: app.desk });
},
[navigation, openApp]
);

return (
<View flex={1} backgroundColor="$secondaryBackground">
<AppLauncherView
apps={apps}
isLoading={isLoading}
onSelectApp={handleSelectApp}
/>
<NavBarView
navigateToContacts={() => {
navigation.navigate('Contacts');
}}
navigateToHome={() => {
navigation.navigate('ChatList');
}}
navigateToNotifications={() => {
navigation.navigate('Activity');
}}
navigateToApps={() => {
navigation.navigate('AppLauncher');
}}
currentRoute="AppLauncher"
currentUserId={currentUserId}
/>
</View>
);
}
106 changes: 106 additions & 0 deletions packages/app/features/apps/AppViewerScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import {
RouteProp,
useFocusEffect,
useIsFocused,
useNavigation,
useRoute,
} from '@react-navigation/native';
import * as db from '@tloncorp/shared/db';
import * as store from '@tloncorp/shared/store';
import { useIsWindowNarrow } from '@tloncorp/ui';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { Platform } from 'react-native';

import { useSetFocusedDesk } from '../../hooks/useOpenApps';
import {
AppWebView,
ScreenHeader,
View,
} from '../../ui';

type AppViewerRouteParams = {
AppViewer: { desk: string };
};

export function AppViewerScreen() {
const route = useRoute<RouteProp<AppViewerRouteParams, 'AppViewer'>>();
const navigation = useNavigation();
const isWindowNarrow = useIsWindowNarrow();
const isFocused = useIsFocused();
const { data: apps = [] } = store.useInstalledApps();
const shipInfo = db.shipInfo.useValue();

const desk = route.params?.desk;
const charge = useMemo(
() => apps.find((app) => app.desk === desk),
[apps, desk]
);

const setFocusedDesk = useSetFocusedDesk();
useFocusEffect(
useCallback(() => {
if (!desk) return;
store.recordVisit({ kind: 'app', id: desk });
setFocusedDesk(desk);
return () => setFocusedDesk(null);
}, [desk, setFocusedDesk])
);

// Title falls back to the charge title; once the iframe loads we override
// with the embedded document's title (e.g., the page title set by the
// app's router). Pushed via `setOptions` so the documentTitle formatter
// (in app.tsx) honors it instead of defaulting to the screen name.
const [iframeTitle, setIframeTitle] = useState<string | null>(null);
useEffect(() => {
setIframeTitle(null);
}, [desk]);
const screenTitle = iframeTitle || charge?.title || desk || null;
useEffect(() => {
if (screenTitle) navigation.setOptions({ title: screenTitle });
}, [navigation, screenTitle]);

// Native needs an absolute URL; web uses a relative path served by the same
// ship that hosts Tlon.
const shipUrl =
Platform.OS === 'web' ? undefined : shipInfo?.shipUrl ?? undefined;

// Site charges are served from an arbitrary path; glob charges live under
// /apps/{base}/ where base usually matches the desk name.
const path = useMemo(() => {
if (charge && 'site' in charge.href) return charge.href.site;
if (charge && 'glob' in charge.href) {
const base = charge.href.glob.base || desk;
return `/apps/${base}/`;
}
return desk ? `/apps/${desk}/` : null;
}, [charge, desk]);

if (!desk || !path) {
return (
<View flex={1} backgroundColor="$background">
<ScreenHeader
title="App"
backAction={() => navigation.goBack()}
/>
</View>
);
}

return (
<View flex={1} backgroundColor="$background">
{isWindowNarrow && (
<ScreenHeader
title={charge?.title ?? desk}
backAction={() => navigation.goBack()}
/>
)}
<AppWebView
shipUrl={shipUrl}
path={path}
cacheKey={`app:${desk}`}
paused={!isFocused}
onTitleChange={setIframeTitle}
/>
</View>
);
}
Loading