diff --git a/package.json b/package.json index 63fe582..8b98434 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "prettier": "^3.3.2", "typescript": "^5.5.2", "vite": "^5.3.2", + "vite-plugin-package-version": "^1.1.0", "vite-plugin-pwa": "^0.20.0", "vite-plugin-solid": "^2.10.2", "vite-plugin-solid-styled": "^0.11.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index acc4341..9651bfd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -69,6 +69,9 @@ importers: vite: specifier: ^5.3.2 version: 5.3.2(@types/node@20.14.10)(lightningcss@1.25.1)(terser@5.31.2) + vite-plugin-package-version: + specifier: ^1.1.0 + version: 1.1.0(vite@5.3.2(@types/node@20.14.10)(lightningcss@1.25.1)(terser@5.31.2)) vite-plugin-pwa: specifier: ^0.20.0 version: 0.20.0(vite@5.3.2(@types/node@20.14.10)(lightningcss@1.25.1)(terser@5.31.2))(workbox-build@7.1.1(@types/babel__core@7.20.5))(workbox-window@7.1.0) @@ -2489,6 +2492,11 @@ packages: validate-html-nesting@1.2.2: resolution: {integrity: sha512-hGdgQozCsQJMyfK5urgFcWEqsSSrK63Awe0t/IMR0bZ0QMtnuaiHzThW81guu3qx9abLi99NEuiaN6P9gVYsNg==} + vite-plugin-package-version@1.1.0: + resolution: {integrity: sha512-TPoFZXNanzcaKCIrC3e2L/TVRkkRLB6l4RPN/S7KbG7rWfyLcCEGsnXvxn6qR7fyZwXalnnSN/I9d6pSFjHpEA==} + peerDependencies: + vite: '>=2.0.0-beta.69' + vite-plugin-pwa@0.20.0: resolution: {integrity: sha512-/kDZyqF8KqoXRpMUQtR5Atri/7BWayW8Gp7Kz/4bfstsV6zSFTxjREbXZYL7zSuRL40HGA+o2hvUAFRmC+bL7g==} engines: {node: '>=16.0.0'} @@ -5280,6 +5288,10 @@ snapshots: validate-html-nesting@1.2.2: {} + vite-plugin-package-version@1.1.0(vite@5.3.2(@types/node@20.14.10)(lightningcss@1.25.1)(terser@5.31.2)): + dependencies: + vite: 5.3.2(@types/node@20.14.10)(lightningcss@1.25.1)(terser@5.31.2) + vite-plugin-pwa@0.20.0(vite@5.3.2(@types/node@20.14.10)(lightningcss@1.25.1)(terser@5.31.2))(workbox-build@7.1.1(@types/babel__core@7.20.5))(workbox-window@7.1.0): dependencies: debug: 4.3.5 diff --git a/src/App.css b/src/App.css index c04a937..26dd292 100644 --- a/src/App.css +++ b/src/App.css @@ -1,14 +1,4 @@ -html, -body { - overflow: hidden; - height: 100vh; - height: 100dvh; -} - -#root { - overflow: hidden hidden; - height: 100vh; - height: 100dvh; +:root { background-color: var(--tutu-color-surface, transparent); } diff --git a/src/App.tsx b/src/App.tsx index 4a36e23..8a711e1 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -22,11 +22,15 @@ const AccountMastodonOAuth2Callback = lazy( () => import("./accounts/MastodonOAuth2Callback.js"), ); const TimelineHome = lazy(() => import("./timelines/Home.js")); +const Settings = lazy(() => import("./settings/Settings.js")); const Routing: Component = () => { return ( - + + + + ("accounts", [], { @@ -75,6 +82,23 @@ export const acceptAccountViaAuthCode = action( }, ); +export const updateAcctInf = action( + $accounts, + "updateAcctInf", + async ($store, idx: number) => { + const o = $store.get(); + const client = createMastoClientFor(o[idx]); + const inf = await client.v1.accounts.verifyCredentials(); + o[idx].inf = inf; + $store.set(o); + return inf; + }, +); + +export const signOut = action($accounts, "signOut", ($store, predicate: (acct: Account) => boolean) => { + $store.set($store.get().filter(a => !predicate(a))); +}); + export type RegisteredApp = { site: string; clientId: string; diff --git a/src/masto/acct.ts b/src/masto/acct.ts index eafd297..b1a2a28 100644 --- a/src/masto/acct.ts +++ b/src/masto/acct.ts @@ -1,10 +1,35 @@ import { Accessor, createResource } from "solid-js"; import type { mastodon } from "masto"; +import { useSessions } from "./clients"; +import { updateAcctInf } from "../accounts/stores"; export function useAcctProfile(client: Accessor) { - return createResource(client, (client) => { - return client.v1.accounts.verifyCredentials() - }, { - name: "MastodonAccountProfile" - }) + return createResource( + client, + (client) => { + return client.v1.accounts.verifyCredentials(); + }, + { + name: "MastodonAccountProfile", + }, + ); +} + +export function useSignedInProfiles() { + const sessions = useSessions(); + const [accessor, tools] = createResource(sessions, async (all) => { + return Promise.all( + all.map(async (x, i) => ({ ...x, inf: await updateAcctInf(i) })), + ); + }); + return [ + () => { + if (accessor.loading) { + accessor(); + return sessions().map((x) => ({ ...x, inf: x.account.inf })); + } + return accessor(); + }, + tools, + ] as const; } diff --git a/src/masto/clients.ts b/src/masto/clients.ts index 9e71b8c..f100779 100644 --- a/src/masto/clients.ts +++ b/src/masto/clients.ts @@ -69,7 +69,7 @@ export function useSessions() { return sessions; } -export function useSessionsRw() { +function useSessionsRw() { const store = useContext(Context); if (!store) { throw new TypeError("sessions are not provided"); diff --git a/src/material/BottomSheet.module.css b/src/material/BottomSheet.module.css new file mode 100644 index 0000000..1a15849 --- /dev/null +++ b/src/material/BottomSheet.module.css @@ -0,0 +1,32 @@ +.bottomSheet { + composes: surface from 'material.module.css'; + border: none; + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + padding: 0; + width: 100%; + max-width: 560px; + border-radius: 2px; + overscroll-behavior: contain; + + box-shadow: var(--tutu-shadow-e16); + + :global(.MuiToolbar-root) > :global(.MuiButtonBase-root):first-child { + color: white; + margin-left: -0.5em; + margin-right: 24px; + } + + @media (max-width: 560px) { + & { + left: 0; + top: 0; + transform: none; + bottom: 0; + height: 100vh; + height: 100dvh; + } + } +} \ No newline at end of file diff --git a/src/material/BottomSheet.tsx b/src/material/BottomSheet.tsx new file mode 100644 index 0000000..112a17e --- /dev/null +++ b/src/material/BottomSheet.tsx @@ -0,0 +1,26 @@ +import { createEffect, type ParentComponent } from "solid-js"; +import styles from './BottomSheet.module.css' + +export type BottomSheetProps = { + open?: boolean; +}; + +const BottomSheet: ParentComponent = (props) => { + let element: HTMLDialogElement; + + createEffect(() => { + if (props.open) { + if (!element.open) { + element.showModal(); + } + } else { + if (element.open) { + element.close(); + } + } + }); + + return {props.children}; +}; + +export default BottomSheet; diff --git a/src/material/Scaffold.tsx b/src/material/Scaffold.tsx index d7a1344..f1f9d34 100644 --- a/src/material/Scaffold.tsx +++ b/src/material/Scaffold.tsx @@ -22,9 +22,6 @@ const Scaffold: ParentComponent = (props) => { css` .scaffold-content { --scaffold-topbar-height: ${(topbarSize.height?.toString() ?? 0) + "px"}; - - height: 100%; - width: 100%; } .topbar { diff --git a/src/overrides.d.ts b/src/overrides.d.ts new file mode 100644 index 0000000..fc32e50 --- /dev/null +++ b/src/overrides.d.ts @@ -0,0 +1,10 @@ +/// + +interface ImportMetaEnv { + readonly BUILT_AT: string; + readonly PACKAGE_VERSION: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx new file mode 100644 index 0000000..e93de43 --- /dev/null +++ b/src/settings/Settings.tsx @@ -0,0 +1,137 @@ +import { createResource, For, type ParentComponent } from "solid-js"; +import Scaffold from "../material/Scaffold.js"; +import { + AppBar, + Divider, + IconButton, + List, + ListItem, + ListItemButton, + ListItemSecondaryAction, + ListItemText, + ListSubheader, + NativeSelect, + Select, + Switch, + Toolbar, +} from "@suid/material"; +import { Close as CloseIcon } from "@suid/icons-material"; +import { useNavigate } from "@solidjs/router"; +import { Title } from "../material/typography.jsx"; +import { useSessions } from "../masto/clients.js"; +import { css } from "solid-styled"; +import { useSignedInProfiles } from "../masto/acct.js"; +import { signOut, type Account } from "../accounts/stores.js"; +import { intlFormat } from "date-fns"; + +const Settings: ParentComponent = () => { + const navigate = useNavigate(); + + const [profiles] = useSignedInProfiles(); + + const doSignOut = (acct: Account) => { + signOut((a) => a.site === acct.site && a.accessToken === acct.accessToken); + }; + + css` + ul { + padding: 0; + } + + .setting-list { + padding-bottom: calc(env(safe-area-inset-bottom, 0px) + 16px); + } + `; + return ( + + + + + + Settings + + + } + > + +
  • +
      + Accounts + + All Notifications + + + + + + Sign in... + +
    + + {({ account: acct, inf }) => ( +
      + {`@${inf?.username ?? "..."}@${new URL(acct.site).host}`} + + Notifications + + + + + + Sign out + +
    + )} +
    +
  • +
  • + Reading + + Fonts + + + + Prefetch Toots + + + + + +
  • +
  • + Controls + + Optimized UI + + + + + + + + + +
  • +
  • + This Application + + + About Tutu + + + + + No updates + + +
  • +
    +
    + ); +}; + +export default Settings; diff --git a/src/timelines/Home.tsx b/src/timelines/Home.tsx index 16fa2e1..0738a37 100644 --- a/src/timelines/Home.tsx +++ b/src/timelines/Home.tsx @@ -6,9 +6,11 @@ import { Show, untrack, onMount, + type ParentComponent, + children, } from "solid-js"; import { useDocumentTitle } from "../utils"; -import { useSessions } from "../masto/clients"; +import { useSessions } from "../masto/clients"; import { type mastodon } from "masto"; import Scaffold from "../material/Scaffold"; import { @@ -32,6 +34,7 @@ import Tab from "../material/Tab"; import { Create as CreateTootIcon } from "@suid/icons-material"; import { useTimeline } from "../masto/timelines"; import { makeEventListener } from "@solid-primitives/event-listener"; +import BottomSheet from "../material/BottomSheet"; const TimelinePanel: Component<{ client: mastodon.rest.Client; @@ -145,7 +148,7 @@ const TimelinePanel: Component<{ ); }; -const Home: Component = () => { +const Home: ParentComponent = (props) => { let panelList: HTMLDivElement; useDocumentTitle("Timelines"); const now = createTimeSource(); @@ -162,6 +165,8 @@ const Home: Component = () => { number, ]); + const child = children(() => props.children) + let scrollEventLockReleased = true; const recalculateTabIndicator = () => { @@ -221,7 +226,7 @@ const Home: Component = () => { const onTabClick = (idx: number) => { const items = panelList.querySelectorAll(".tab-panel"); if (items.length > idx) { - items.item(idx).scrollIntoView({ behavior: "smooth" }); + items.item(idx).scrollIntoView({ block: "nearest", behavior: "smooth" }); } }; @@ -235,10 +240,6 @@ const Home: Component = () => { max-height: calc(100dvh - var(--scaffold-topbar-height, 0px)); scroll-snap-align: center; - &:not(.active) { - overflow: hidden; - } - @media (max-width: 600px) { padding: 0; } @@ -261,71 +262,74 @@ const Home: Component = () => { `; return ( - - - - - Home - - - Trending - - - Public - - - - setPrefetching((x) => !x)}> - Prefetch Toots - - - - - - - - } - fab={ - - - - } - > - -
    -
    -
    - + <> + + + + + Home + + + Trending + + + Public + + + + setPrefetching((x) => !x)}> + Prefetch Toots + + + + + + + + } + fab={ + + + + } + > + +
    +
    +
    + +
    -
    -
    -
    - +
    +
    + +
    -
    -
    -
    - +
    +
    + +
    +
    -
    -
    - - + + {child()} + + ); }; diff --git a/src/timelines/ProfileMenuButton.tsx b/src/timelines/ProfileMenuButton.tsx index 1d61553..b997041 100644 --- a/src/timelines/ProfileMenuButton.tsx +++ b/src/timelines/ProfileMenuButton.tsx @@ -15,6 +15,7 @@ import { Star as LikeIcon, FeaturedPlayList as ListIcon, } from "@suid/icons-material"; +import { A } from "@solidjs/router"; const ProfileMenuButton: ParentComponent<{ profile?: { displayName: string; avatar: string; username: string }; @@ -107,7 +108,7 @@ const ProfileMenuButton: ParentComponent<{ {props.children} - + diff --git a/vite.config.ts b/vite.config.ts index 96cb481..26d01b6 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -3,6 +3,7 @@ import solid from "vite-plugin-solid"; import solidStyled from "vite-plugin-solid-styled"; import suid from "@suid/vite-plugin"; import { VitePWA } from "vite-plugin-pwa"; +import version from "vite-plugin-package-version"; export default defineConfig(({ mode }) => ({ plugins: [ @@ -17,7 +18,11 @@ export default defineConfig(({ mode }) => ({ VitePWA({ registerType: "autoUpdate", }), + version(), ], + define: { + "import.meta.env.BUILT_AT": `"${new Date().toISOString()}"`, + }, css: { devSourcemap: true, },