Compare commits
No commits in common. "bea1d6abfa47b1cbfbca3b8a02a9d18c9a470d70" and "5b72160cdb79f73e0c379430ff8024ec900a781b" have entirely different histories.
bea1d6abfa
...
5b72160cdb
9 changed files with 112 additions and 515 deletions
|
@ -24,7 +24,9 @@ import {
|
||||||
ResultDispatcher,
|
ResultDispatcher,
|
||||||
type JSONRPC,
|
type JSONRPC,
|
||||||
} from "./serviceworker/workerrpc.js";
|
} from "./serviceworker/workerrpc.js";
|
||||||
import { Service } from "./serviceworker/services.js";
|
import {
|
||||||
|
Service
|
||||||
|
} from "./serviceworker/services.js"
|
||||||
import { makeEventListener } from "@solid-primitives/event-listener";
|
import { makeEventListener } from "@solid-primitives/event-listener";
|
||||||
import { ServiceWorkerProvider } from "./platform/host.js";
|
import { ServiceWorkerProvider } from "./platform/host.js";
|
||||||
|
|
||||||
|
@ -39,7 +41,6 @@ const MotionSettings = lazy(() => import("./settings/Motions.js"));
|
||||||
const LanguageSettings = lazy(() => import("./settings/Language.js"));
|
const LanguageSettings = lazy(() => import("./settings/Language.js"));
|
||||||
const RegionSettings = lazy(() => import("./settings/Region.jsx"));
|
const RegionSettings = lazy(() => import("./settings/Region.jsx"));
|
||||||
const UnexpectedError = lazy(() => import("./UnexpectedError.js"));
|
const UnexpectedError = lazy(() => import("./UnexpectedError.js"));
|
||||||
const Profile = lazy(() => import("./profiles/Profile.js"));
|
|
||||||
|
|
||||||
const Routing: Component = () => {
|
const Routing: Component = () => {
|
||||||
return (
|
return (
|
||||||
|
@ -52,8 +53,7 @@ const Routing: Component = () => {
|
||||||
<Route path="/region" component={RegionSettings}></Route>
|
<Route path="/region" component={RegionSettings}></Route>
|
||||||
<Route path="/motions" component={MotionSettings}></Route>
|
<Route path="/motions" component={MotionSettings}></Route>
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/:acct/toot/:id" component={TootBottomSheet}></Route>
|
<Route path="/:acct/:id" component={TootBottomSheet}></Route>
|
||||||
<Route path="/:acct/profile/:id" component={Profile}></Route>
|
|
||||||
</Route>
|
</Route>
|
||||||
<Route path={"/accounts"}>
|
<Route path={"/accounts"}>
|
||||||
<Route path={"/sign-in"} component={AccountSignIn} />
|
<Route path={"/sign-in"} component={AccountSignIn} />
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import {
|
import {
|
||||||
Accessor,
|
Accessor,
|
||||||
createContext,
|
createContext,
|
||||||
createMemo,
|
|
||||||
createRenderEffect,
|
createRenderEffect,
|
||||||
createResource,
|
createResource,
|
||||||
|
Signal,
|
||||||
useContext,
|
useContext,
|
||||||
} from "solid-js";
|
} from "solid-js";
|
||||||
import { Account } from "../accounts/stores";
|
import { Account } from "../accounts/stores";
|
||||||
|
@ -76,60 +76,3 @@ function useSessionsRaw() {
|
||||||
}
|
}
|
||||||
return store;
|
return store;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DefaultSessionContext = /* @__PURE__ */ createContext<Accessor<number>>(() => 0)
|
|
||||||
|
|
||||||
export const DefaultSessionProvider = DefaultSessionContext.Provider;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the default session (the first session).
|
|
||||||
*
|
|
||||||
* This function may return `undefined`, but it will try to redirect the user to the sign in.
|
|
||||||
*/
|
|
||||||
export function useDefaultSession() {
|
|
||||||
const sessions = useSessions()
|
|
||||||
const sessionIndex = useContext(DefaultSessionContext)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (sessions().length > 0) {
|
|
||||||
return sessions()[sessionIndex()]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a session for the specific acct string.
|
|
||||||
*
|
|
||||||
* Acct string is a string in the pattern of `{username}@{site_with_protocol}`,
|
|
||||||
* like `@thislight@https://mastodon.social`, can be used to identify (tempoarily)
|
|
||||||
* an session on the tutu instance.
|
|
||||||
*
|
|
||||||
* The `site_with_protocol` is required.
|
|
||||||
*
|
|
||||||
* - If the username is present, the session matches the username and the site is returned; or,
|
|
||||||
* - If the username is not present, any session on the site is returned; or,
|
|
||||||
* - If no available session available for the pattern, an unauthorised session is returned.
|
|
||||||
*
|
|
||||||
* In an unauthorised session, the `.account` is `undefined` and the `client` is an
|
|
||||||
* unauthorised client for the site. This client may not available for some operations.
|
|
||||||
*/
|
|
||||||
export function useSessionForAcctStr(acct: Accessor<string>) {
|
|
||||||
const allSessions = useSessions()
|
|
||||||
|
|
||||||
return createMemo(() => {
|
|
||||||
const [inputUsername, inputSite] = acct().split("@", 2);
|
|
||||||
const authedSession = allSessions().find(
|
|
||||||
(x) =>
|
|
||||||
x.account.site === inputSite &&
|
|
||||||
x.account.inf?.username === inputUsername,
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
authedSession ?? {
|
|
||||||
client: createUnauthorizedClient(inputSite),
|
|
||||||
account: undefined,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -23,20 +23,9 @@
|
||||||
|
|
||||||
box-shadow: var(--tutu-shadow-e16);
|
box-shadow: var(--tutu-shadow-e16);
|
||||||
|
|
||||||
:global(.MuiToolbar-root) {
|
:global(.MuiToolbar-root) > :global(.MuiButtonBase-root):first-child {
|
||||||
> :global(.MuiButtonBase-root) {
|
margin-left: -0.5em;
|
||||||
|
margin-right: 24px;
|
||||||
&:first-child {
|
|
||||||
margin-left: -0.5em;
|
|
||||||
margin-right: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin-right: -0.5em;
|
|
||||||
margin-left: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 560px) {
|
@media (max-width: 560px) {
|
||||||
|
@ -54,6 +43,7 @@
|
||||||
|
|
||||||
&.animated {
|
&.animated {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
transform: translateY(-50%);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
will-change: width, height, top, left;
|
will-change: width, height, top, left;
|
||||||
|
|
||||||
|
@ -64,6 +54,12 @@
|
||||||
& * {
|
& * {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 560px) {
|
||||||
|
& {
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.bottom {
|
&.bottom {
|
||||||
|
@ -75,6 +71,7 @@
|
||||||
& {
|
& {
|
||||||
transform: none;
|
transform: none;
|
||||||
height: unset;
|
height: unset;
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -118,9 +118,12 @@ const BottomSheet: ParentComponent<BottomSheetProps> = (props) => {
|
||||||
|
|
||||||
animation = element.animate(
|
animation = element.animate(
|
||||||
{
|
{
|
||||||
|
top: [`${rect.top}px`, `${rect.top}px`],
|
||||||
left: reserve
|
left: reserve
|
||||||
? [`${rect.left}px`, `${window.innerWidth}px`]
|
? [`${rect.left}px`, `${window.innerWidth}px`]
|
||||||
: [`${window.innerWidth}px`, `${rect.left}px`],
|
: [`${window.innerWidth}px`, `${rect.left}px`],
|
||||||
|
width: [`${rect.width}px`, `${rect.width}px`],
|
||||||
|
height: [`${rect.height}px`, `${rect.height}px`],
|
||||||
},
|
},
|
||||||
{ easing, duration },
|
{ easing, duration },
|
||||||
);
|
);
|
||||||
|
@ -148,9 +151,12 @@ const BottomSheet: ParentComponent<BottomSheetProps> = (props) => {
|
||||||
|
|
||||||
animation = element.animate(
|
animation = element.animate(
|
||||||
{
|
{
|
||||||
|
left: [`${rect.left}px`, `${rect.left}px`],
|
||||||
top: reserve
|
top: reserve
|
||||||
? [`${rect.top}px`, `${window.innerHeight}px`]
|
? [`${rect.top}px`, `${window.innerHeight}px`]
|
||||||
: [`${window.innerHeight}px`, `${rect.top}px`],
|
: [`${window.innerHeight}px`, `${rect.top}px`],
|
||||||
|
width: [`${rect.width}px`, `${rect.width}px`],
|
||||||
|
height: [`${rect.height}px`, `${rect.height}px`],
|
||||||
},
|
},
|
||||||
{ easing, duration },
|
{ easing, duration },
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,252 +0,0 @@
|
||||||
import {
|
|
||||||
createRenderEffect,
|
|
||||||
createResource,
|
|
||||||
createSignal,
|
|
||||||
For,
|
|
||||||
onCleanup,
|
|
||||||
Show,
|
|
||||||
type Component,
|
|
||||||
} from "solid-js";
|
|
||||||
import Scaffold from "../material/Scaffold";
|
|
||||||
import { AppBar, Avatar, Button, IconButton, Toolbar } from "@suid/material";
|
|
||||||
import { Close, MoreVert, Verified } from "@suid/icons-material";
|
|
||||||
import { Title } from "../material/typography";
|
|
||||||
import { useNavigate, useParams } from "@solidjs/router";
|
|
||||||
import { useSessionForAcctStr } from "../masto/clients";
|
|
||||||
import { resolveCustomEmoji } from "../masto/toot";
|
|
||||||
import { FastAverageColor } from "fast-average-color";
|
|
||||||
import { useWindowSize } from "@solid-primitives/resize-observer";
|
|
||||||
import { css } from "solid-styled";
|
|
||||||
import { createTimeline } from "../masto/timelines";
|
|
||||||
import TootList from "../timelines/TootList";
|
|
||||||
import { createIntersectionObserver } from "@solid-primitives/intersection-observer";
|
|
||||||
import { createTimeSource, TimeSourceProvider } from "../platform/timesrc";
|
|
||||||
|
|
||||||
const Profile: Component = () => {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const params = useParams<{ acct: string; id: string }>();
|
|
||||||
const acctText = () => decodeURIComponent(params.acct);
|
|
||||||
const session = useSessionForAcctStr(acctText);
|
|
||||||
const [bannerSampledColors, setBannerSampledColors] = createSignal<{
|
|
||||||
average: string;
|
|
||||||
text: string;
|
|
||||||
}>();
|
|
||||||
const windowSize = useWindowSize();
|
|
||||||
const time = createTimeSource();
|
|
||||||
|
|
||||||
const [scrolledPastBanner, setScrolledPastBanner] = createSignal(false);
|
|
||||||
const obx = new IntersectionObserver(
|
|
||||||
(entries) => {
|
|
||||||
const ent = entries[0];
|
|
||||||
if (ent.intersectionRatio < 0.1) {
|
|
||||||
setScrolledPastBanner(true);
|
|
||||||
} else {
|
|
||||||
setScrolledPastBanner(false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
threshold: 0.1,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
onCleanup(() => obx.disconnect());
|
|
||||||
|
|
||||||
const [profile] = createResource(
|
|
||||||
() => [session().client, params.id] as const,
|
|
||||||
async ([client, id]) => {
|
|
||||||
return await client.v1.accounts.$select(id).fetch();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const [recentToots] = createTimeline(
|
|
||||||
() => session().client.v1.accounts.$select(params.id).statuses,
|
|
||||||
() => 20,
|
|
||||||
);
|
|
||||||
|
|
||||||
const bannerImg = () => profile()?.header;
|
|
||||||
const avatarImg = () => profile()?.avatar;
|
|
||||||
const displayName = () =>
|
|
||||||
resolveCustomEmoji(profile()?.displayName || "", profile()?.emojis ?? []);
|
|
||||||
const fullUsername = () => `@${profile()?.acct ?? "..."}`; // TODO: full user name
|
|
||||||
const description = () => profile()?.note;
|
|
||||||
|
|
||||||
css`
|
|
||||||
.intro {
|
|
||||||
background-color: var(--tutu-color-surface-d);
|
|
||||||
color: var(--tutu-color-on-surface);
|
|
||||||
padding: 16px 12px;
|
|
||||||
display: flex;
|
|
||||||
flex-flow: column nowrap;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.acct-grp {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: auto 1fr auto;
|
|
||||||
gap: 16px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.name-grp {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: column nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.acct-fields {
|
|
||||||
& td > :global(a) {
|
|
||||||
display: inline-flex;
|
|
||||||
min-height: 44px;
|
|
||||||
align-items: center;
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
& :global(a > .invisible) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
& :global(svg) {
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-title {
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Scaffold
|
|
||||||
topbar={
|
|
||||||
<AppBar
|
|
||||||
position="static"
|
|
||||||
color={scrolledPastBanner() ? "primary" : "transparent"}
|
|
||||||
elevation={scrolledPastBanner() ? undefined : 0}
|
|
||||||
>
|
|
||||||
<Toolbar
|
|
||||||
variant="dense"
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
color: bannerSampledColors()?.text,
|
|
||||||
paddingTop: "var(--safe-area-inset-top)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IconButton color="inherit" onClick={[navigate, -1]}>
|
|
||||||
<Close />
|
|
||||||
</IconButton>
|
|
||||||
<Title
|
|
||||||
use:solid-styled
|
|
||||||
class="page-title"
|
|
||||||
style={{
|
|
||||||
visibility: scrolledPastBanner() ? undefined : "hidden",
|
|
||||||
}}
|
|
||||||
ref={(e: HTMLElement) =>
|
|
||||||
createRenderEffect(() => (e.innerHTML = displayName()))
|
|
||||||
}
|
|
||||||
></Title>
|
|
||||||
<IconButton color="inherit">
|
|
||||||
<MoreVert />
|
|
||||||
</IconButton>
|
|
||||||
</Toolbar>
|
|
||||||
</AppBar>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
width: "100%",
|
|
||||||
height: `${268 * (Math.min(560, windowSize.width) / 560)}px`,
|
|
||||||
"margin-top":
|
|
||||||
"calc(-1 * (var(--scaffold-topbar-height) + var(--safe-area-inset-top)))",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
ref={(e) => obx.observe(e)}
|
|
||||||
src={bannerImg()}
|
|
||||||
style={{
|
|
||||||
"object-fit": "contain",
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
}}
|
|
||||||
crossOrigin="anonymous"
|
|
||||||
onLoad={async (event) => {
|
|
||||||
const ins = new FastAverageColor();
|
|
||||||
const colors = ins.getColor(event.currentTarget);
|
|
||||||
setBannerSampledColors({
|
|
||||||
average: colors.hex,
|
|
||||||
text: colors.isDark ? "white" : "black",
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
></img>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="intro"
|
|
||||||
style={{
|
|
||||||
"background-color": bannerSampledColors()?.average,
|
|
||||||
color: bannerSampledColors()?.text,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div class="acct-grp">
|
|
||||||
<Avatar
|
|
||||||
src={avatarImg()}
|
|
||||||
sx={{
|
|
||||||
marginTop: "calc(-16px - 72px / 2)",
|
|
||||||
width: "72px",
|
|
||||||
height: "72px",
|
|
||||||
}}
|
|
||||||
></Avatar>
|
|
||||||
<div class="name-grp">
|
|
||||||
<span
|
|
||||||
ref={(e) =>
|
|
||||||
createRenderEffect(() => (e.innerHTML = displayName()))
|
|
||||||
}
|
|
||||||
></span>
|
|
||||||
<span>{fullUsername()}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Button variant="contained" color="secondary">
|
|
||||||
Subscribe
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
ref={(e) =>
|
|
||||||
createRenderEffect(() => (e.innerHTML = description() || ""))
|
|
||||||
}
|
|
||||||
></div>
|
|
||||||
<table class="acct-fields">
|
|
||||||
<tbody>
|
|
||||||
<For each={profile()?.fields ?? []}>
|
|
||||||
{(item, index) => {
|
|
||||||
return (
|
|
||||||
<tr data-field-index={index()}>
|
|
||||||
<td>{item.name}</td>
|
|
||||||
<td>
|
|
||||||
<Show when={item.verifiedAt}>
|
|
||||||
<Verified />
|
|
||||||
</Show>
|
|
||||||
</td>
|
|
||||||
<td
|
|
||||||
ref={(e) => {
|
|
||||||
createRenderEffect(() => (e.innerHTML = item.value));
|
|
||||||
}}
|
|
||||||
></td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</For>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<TimeSourceProvider value={time}>
|
|
||||||
<TootList
|
|
||||||
threads={recentToots.list}
|
|
||||||
onUnknownThread={recentToots.getPath}
|
|
||||||
onChangeToot={recentToots.set}
|
|
||||||
/>
|
|
||||||
</TimeSourceProvider>
|
|
||||||
</Scaffold>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Profile;
|
|
|
@ -151,7 +151,7 @@ const Home: ParentComponent = (props) => {
|
||||||
);
|
);
|
||||||
const acct = `${inf.username}@${p.account.site}`;
|
const acct = `${inf.username}@${p.account.site}`;
|
||||||
setTootBottomSheetCache(acct, toot);
|
setTootBottomSheetCache(acct, toot);
|
||||||
navigate(`/${encodeURIComponent(acct)}/toot/${toot.id}`, {
|
navigate(`/${encodeURIComponent(acct)}/${toot.id}`, {
|
||||||
state: reply
|
state: reply
|
||||||
? {
|
? {
|
||||||
tootReply: true,
|
tootReply: true,
|
||||||
|
@ -213,7 +213,7 @@ const Home: ParentComponent = (props) => {
|
||||||
Public
|
Public
|
||||||
</Tab>
|
</Tab>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
<ProfileMenuButton profile={profiles()[0]}>
|
<ProfileMenuButton profile={profile()}>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={(e) =>
|
onClick={(e) =>
|
||||||
$settings.setKey(
|
$settings.setKey(
|
||||||
|
|
|
@ -24,10 +24,7 @@ import {
|
||||||
import { A } from "@solidjs/router";
|
import { A } from "@solidjs/router";
|
||||||
|
|
||||||
const ProfileMenuButton: ParentComponent<{
|
const ProfileMenuButton: ParentComponent<{
|
||||||
profile?: {
|
profile?: { displayName: string; avatar: string; username: string };
|
||||||
account: { site: string };
|
|
||||||
inf?: { displayName: string; avatar: string; username: string; id: string };
|
|
||||||
};
|
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
}> = (props) => {
|
}> = (props) => {
|
||||||
|
@ -51,83 +48,79 @@ const ProfileMenuButton: ParentComponent<{
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ButtonBase
|
<ButtonBase
|
||||||
aria-haspopup="true"
|
aria-haspopup="true"
|
||||||
sx={{ borderRadius: "50%" }}
|
sx={{ borderRadius: "50%" }}
|
||||||
id={buttonId}
|
id={buttonId}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
aria-controls={open() ? menuId : undefined}
|
aria-controls={open() ? menuId : undefined}
|
||||||
aria-expanded={open() ? "true" : undefined}
|
aria-expanded={open() ? "true" : undefined}
|
||||||
>
|
|
||||||
<Avatar
|
|
||||||
alt={`${props.profile?.inf?.displayName}'s avatar`}
|
|
||||||
src={props.profile?.inf?.avatar}
|
|
||||||
></Avatar>
|
|
||||||
</ButtonBase>
|
|
||||||
<Menu
|
|
||||||
id={menuId}
|
|
||||||
anchorEl={anchor()}
|
|
||||||
open={open()}
|
|
||||||
onClose={onClose}
|
|
||||||
MenuListProps={{
|
|
||||||
"aria-labelledby": buttonId,
|
|
||||||
sx: {
|
|
||||||
minWidth: "220px",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
anchorOrigin={{
|
|
||||||
vertical: "top",
|
|
||||||
horizontal: "right",
|
|
||||||
}}
|
|
||||||
transformOrigin={{
|
|
||||||
vertical: "top",
|
|
||||||
horizontal: "right",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<MenuItem
|
|
||||||
component={A}
|
|
||||||
href={`/${encodeURIComponent(`${props.profile?.inf?.username}@${props.profile?.account.site}`)}/profile/${props.profile?.inf?.id}`}
|
|
||||||
disabled={!props.profile}
|
|
||||||
>
|
>
|
||||||
<ListItemAvatar>
|
<Avatar
|
||||||
<Avatar src={props.profile?.inf?.avatar}></Avatar>
|
alt={`${props.profile?.displayName}'s avatar`}
|
||||||
</ListItemAvatar>
|
src={props.profile?.avatar}
|
||||||
<ListItemText
|
></Avatar>
|
||||||
primary={props.profile?.inf?.displayName}
|
</ButtonBase>
|
||||||
secondary={`@${props.profile?.inf?.username}`}
|
<Menu
|
||||||
></ListItemText>
|
id={menuId}
|
||||||
</MenuItem>
|
anchorEl={anchor()}
|
||||||
|
open={open()}
|
||||||
|
onClose={onClose}
|
||||||
|
MenuListProps={{
|
||||||
|
"aria-labelledby": buttonId,
|
||||||
|
sx: {
|
||||||
|
minWidth: "220px",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
anchorOrigin={{
|
||||||
|
vertical: "top",
|
||||||
|
horizontal: "right",
|
||||||
|
}}
|
||||||
|
transformOrigin={{
|
||||||
|
vertical: "top",
|
||||||
|
horizontal: "right",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MenuItem>
|
||||||
|
<ListItemAvatar>
|
||||||
|
<Avatar src={props.profile?.avatar}></Avatar>
|
||||||
|
</ListItemAvatar>
|
||||||
|
<ListItemText
|
||||||
|
primary={props.profile?.displayName}
|
||||||
|
secondary={`@${props.profile?.username}`}
|
||||||
|
></ListItemText>
|
||||||
|
</MenuItem>
|
||||||
|
|
||||||
<MenuItem>
|
<MenuItem>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<BookmarkIcon />
|
<BookmarkIcon />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>Bookmarks</ListItemText>
|
<ListItemText>Bookmarks</ListItemText>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem>
|
<MenuItem>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<LikeIcon />
|
<LikeIcon />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>Likes</ListItemText>
|
<ListItemText>Likes</ListItemText>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem>
|
<MenuItem>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<ListIcon />
|
<ListIcon />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>Lists</ListItemText>
|
<ListItemText>Lists</ListItemText>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<Divider />
|
|
||||||
<Show when={props.children}>
|
|
||||||
{props.children}
|
|
||||||
<Divider />
|
<Divider />
|
||||||
</Show>
|
<Show when={props.children}>
|
||||||
<MenuItem component={A} href="/settings" onClick={onClose}>
|
{props.children}
|
||||||
<ListItemIcon>
|
<Divider />
|
||||||
<SettingsIcon />
|
</Show>
|
||||||
</ListItemIcon>
|
<MenuItem component={A} href="/settings" onClick={onClose}>
|
||||||
<ListItemText>Settings</ListItemText>
|
<ListItemIcon>
|
||||||
</MenuItem>
|
<SettingsIcon />
|
||||||
</Menu>
|
</ListItemIcon>
|
||||||
|
<ListItemText>Settings</ListItemText>
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -15,7 +15,7 @@ import {
|
||||||
ArrowBack as BackIcon,
|
ArrowBack as BackIcon,
|
||||||
Close as CloseIcon,
|
Close as CloseIcon,
|
||||||
} from "@suid/icons-material";
|
} from "@suid/icons-material";
|
||||||
import { useSessionForAcctStr } from "../masto/clients";
|
import { createUnauthorizedClient, useSessions } from "../masto/clients";
|
||||||
import { resolveCustomEmoji } from "../masto/toot";
|
import { resolveCustomEmoji } from "../masto/toot";
|
||||||
import RegularToot from "./RegularToot";
|
import RegularToot from "./RegularToot";
|
||||||
import type { mastodon } from "masto";
|
import type { mastodon } from "masto";
|
||||||
|
@ -45,10 +45,24 @@ const TootBottomSheet: Component = (props) => {
|
||||||
tootReply?: boolean;
|
tootReply?: boolean;
|
||||||
}>();
|
}>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const allSession = useSessions();
|
||||||
const time = createTimeSource();
|
const time = createTimeSource();
|
||||||
const [isInTyping, setInTyping] = createSignal(false);
|
const [isInTyping, setInTyping] = createSignal(false);
|
||||||
const acctText = () => decodeURIComponent(params.acct);
|
const acctText = () => decodeURIComponent(params.acct);
|
||||||
const session = useSessionForAcctStr(acctText)
|
const session = () => {
|
||||||
|
const [inputUsername, inputSite] = acctText().split("@", 2);
|
||||||
|
const authedSession = allSession().find(
|
||||||
|
(x) =>
|
||||||
|
x.account.site === inputSite &&
|
||||||
|
x.account.inf?.username === inputUsername,
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
authedSession ?? {
|
||||||
|
client: createUnauthorizedClient(inputSite),
|
||||||
|
account: undefined,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const pushedCount = () => {
|
const pushedCount = () => {
|
||||||
return location.state?.tootBottomSheetPushedCount || 0;
|
return location.state?.tootBottomSheetPushedCount || 0;
|
||||||
|
@ -161,7 +175,7 @@ const TootBottomSheet: Component = (props) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setCache(params.acct, status);
|
setCache(params.acct, status);
|
||||||
navigate(`/${params.acct}/toot/${status.id}`, {
|
navigate(`/${params.acct}/${status.id}`, {
|
||||||
state: {
|
state: {
|
||||||
tootBottomSheetPushedCount: pushedCount() + 1,
|
tootBottomSheetPushedCount: pushedCount() + 1,
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,104 +0,0 @@
|
||||||
import {
|
|
||||||
Component,
|
|
||||||
For,
|
|
||||||
onCleanup,
|
|
||||||
createSignal,
|
|
||||||
Show,
|
|
||||||
untrack,
|
|
||||||
Match,
|
|
||||||
Switch as JsSwitch,
|
|
||||||
ErrorBoundary,
|
|
||||||
type Ref,
|
|
||||||
} from "solid-js";
|
|
||||||
import { type mastodon } from "masto";
|
|
||||||
import { Button, LinearProgress } from "@suid/material";
|
|
||||||
import { createTimeline } from "../masto/timelines";
|
|
||||||
import { vibrate } from "../platform/hardware";
|
|
||||||
import PullDownToRefresh from "./PullDownToRefresh";
|
|
||||||
import TootComposer from "./TootComposer";
|
|
||||||
import Thread from "./Thread.jsx";
|
|
||||||
import { useDefaultSession } from "../masto/clients";
|
|
||||||
|
|
||||||
const TootList: Component<{
|
|
||||||
ref?: Ref<HTMLDivElement>;
|
|
||||||
threads: string[];
|
|
||||||
onUnknownThread: (id: string) => { value: mastodon.v1.Status }[] | undefined;
|
|
||||||
onChangeToot: (id: string, value: mastodon.v1.Status) => void;
|
|
||||||
}> = (props) => {
|
|
||||||
const session = useDefaultSession();
|
|
||||||
const [expandedThreadId, setExpandedThreadId] = createSignal<string>();
|
|
||||||
|
|
||||||
const onBookmark = async (
|
|
||||||
client: mastodon.rest.Client,
|
|
||||||
status: mastodon.v1.Status,
|
|
||||||
) => {
|
|
||||||
const result = await (status.bookmarked
|
|
||||||
? client.v1.statuses.$select(status.id).unbookmark()
|
|
||||||
: client.v1.statuses.$select(status.id).bookmark());
|
|
||||||
props.onChangeToot(result.id, result);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onBoost = async (
|
|
||||||
client: mastodon.rest.Client,
|
|
||||||
status: mastodon.v1.Status,
|
|
||||||
) => {
|
|
||||||
vibrate(50);
|
|
||||||
const rootStatus = status.reblog ? status.reblog : status;
|
|
||||||
const reblogged = rootStatus.reblogged;
|
|
||||||
if (status.reblog) {
|
|
||||||
status.reblog = { ...status.reblog, reblogged: !reblogged };
|
|
||||||
props.onChangeToot(status.id, status);
|
|
||||||
} else {
|
|
||||||
props.onChangeToot(
|
|
||||||
status.id,
|
|
||||||
Object.assign(status, {
|
|
||||||
reblogged: !reblogged,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const result = reblogged
|
|
||||||
? await client.v1.statuses.$select(status.id).unreblog()
|
|
||||||
: (await client.v1.statuses.$select(status.id).reblog()).reblog!;
|
|
||||||
props.onChangeToot(
|
|
||||||
status.id,
|
|
||||||
Object.assign(status.reblog ?? status, result.reblog),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ErrorBoundary
|
|
||||||
fallback={(err, reset) => {
|
|
||||||
return <p>Oops: {String(err)}</p>;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div ref={props.ref}>
|
|
||||||
<For each={props.threads}>
|
|
||||||
{(itemId, index) => {
|
|
||||||
const path = props.onUnknownThread(itemId)!;
|
|
||||||
const toots = path.reverse().map((x) => x.value);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Thread
|
|
||||||
toots={toots}
|
|
||||||
onBoost={onBoost}
|
|
||||||
onBookmark={onBookmark}
|
|
||||||
onReply={({ status }, element) => {}}
|
|
||||||
client={session()?.client!}
|
|
||||||
isExpended={(status) => status.id === expandedThreadId()}
|
|
||||||
onItemClick={(status, event) => {
|
|
||||||
if (status.id !== expandedThreadId()) {
|
|
||||||
setExpandedThreadId((x) => (x ? undefined : status.id));
|
|
||||||
} else {
|
|
||||||
// TODO: open full-screen toot
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
</ErrorBoundary>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TootList;
|
|
Loading…
Reference in a new issue