Compare commits

...

3 commits

Author SHA1 Message Date
thislight
4734a67f5a
Profile: fix page title and intro layout
All checks were successful
/ depoly (push) Successful in 1m13s
2024-10-27 13:57:09 +08:00
thislight
49e1731cdb
RegularToot: open profile 2024-10-27 13:43:34 +08:00
thislight
d4093aaf5c
README: update links for the app 2024-10-27 13:13:28 +08:00
4 changed files with 73 additions and 14 deletions

View file

@ -2,6 +2,8 @@
Tutu is a comfortable experience for tooting. Designed to work on any device - desktop, phone and tablet. Tutu is a comfortable experience for tooting. Designed to work on any device - desktop, phone and tablet.
[Launch Tutu](https://tutu.lightstands.xyz)
## Compatibility ## Compatibility
The code is built against those targets and Tutu must run on those platforms: The code is built against those targets and Tutu must run on those platforms:
@ -12,6 +14,12 @@ The code is built against those targets and Tutu must run on those platforms:
Tutu trys to push the Web technology to its limit. Some features might not be available on the platform does not meet the requirement. Tutu trys to push the Web technology to its limit. Some features might not be available on the platform does not meet the requirement.
## The "Next" Branch
The "next" branch of the app is built on every commit pushed into "master". You can tatse latest change but risks your data.
[Launch Tutu (Next)](https://next.tututheapp.pages.dev)
## Build & Depoly ## Build & Depoly
Tutu uses [bun](https://bun.sh) as the package manager. Run Tutu uses [bun](https://bun.sh) as the package manager. Run

View file

@ -49,7 +49,8 @@ export type Session = {
client: mastodon.rest.Client; client: mastodon.rest.Client;
}; };
const Context = /* @__PURE__ */ createContext<Accessor<readonly Readonly<Session>[]>>(); const Context =
/* @__PURE__ */ createContext<Accessor<readonly Readonly<Session>[]>>();
export const Provider = Context.Provider; export const Provider = Context.Provider;
@ -77,7 +78,9 @@ function useSessionsRaw() {
return store; return store;
} }
const DefaultSessionContext = /* @__PURE__ */ createContext<Accessor<number>>(() => 0) const DefaultSessionContext = /* @__PURE__ */ createContext<Accessor<number>>(
() => 0,
);
export const DefaultSessionProvider = DefaultSessionContext.Provider; export const DefaultSessionProvider = DefaultSessionContext.Provider;
@ -87,14 +90,14 @@ export const DefaultSessionProvider = DefaultSessionContext.Provider;
* This function may return `undefined`, but it will try to redirect the user to the sign in. * This function may return `undefined`, but it will try to redirect the user to the sign in.
*/ */
export function useDefaultSession() { export function useDefaultSession() {
const sessions = useSessions() const sessions = useSessions();
const sessionIndex = useContext(DefaultSessionContext) const sessionIndex = useContext(DefaultSessionContext);
return () => { return () => {
if (sessions().length > 0) { if (sessions().length > 0) {
return sessions()[sessionIndex()] return sessions()[sessionIndex()];
} }
} };
} }
/** /**
@ -114,7 +117,7 @@ export function useDefaultSession() {
* unauthorised client for the site. This client may not available for some operations. * unauthorised client for the site. This client may not available for some operations.
*/ */
export function useSessionForAcctStr(acct: Accessor<string>) { export function useSessionForAcctStr(acct: Accessor<string>) {
const allSessions = useSessions() const allSessions = useSessions();
return createMemo(() => { return createMemo(() => {
const [inputUsername, inputSite] = acct().split("@", 2); const [inputUsername, inputSite] = acct().split("@", 2);
@ -132,4 +135,6 @@ export function useSessionForAcctStr(acct: Accessor<string>) {
}); });
} }
export function makeAcctText(session: Session) {
return `${session.account.inf?.username}@${session.account.site}`;
}

View file

@ -116,10 +116,19 @@ const Profile: Component = () => {
} }
.acct-grp { .acct-grp {
display: grid; display: flex;
grid-template-columns: auto 1fr auto; flex-flow: row wrap;
gap: 16px; gap: 16px;
align-items: center; align-items: center;
& > :nth-child(2) {
flex-grow: 1;
}
& > :last-child {
flex-grow: 1;
text-align: right;
}
} }
.name-grp { .name-grp {
@ -128,11 +137,13 @@ const Profile: Component = () => {
} }
table.acct-fields { table.acct-fields {
word-break: break-all;
& td > :global(a) { & td > :global(a) {
display: inline-flex; display: inline-flex;
min-height: 44px;
align-items: center; align-items: center;
color: inherit; color: inherit;
min-height: 44px;
} }
& :global(a > .invisible) { & :global(a > .invisible) {
@ -146,6 +157,9 @@ const Profile: Component = () => {
.page-title { .page-title {
flex-grow: 1; flex-grow: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
} }
`; `;

View file

@ -39,6 +39,8 @@ import { FastAverageColor } from "fast-average-color";
import Color from "colorjs.io"; import Color from "colorjs.io";
import { useDateFnLocale } from "../platform/i18n"; import { useDateFnLocale } from "../platform/i18n";
import { canShare, share } from "../platform/share"; import { canShare, share } from "../platform/share";
import { makeAcctText, useDefaultSession } from "../masto/clients";
import { useNavigate } from "@solidjs/router";
type TootContentViewProps = { type TootContentViewProps = {
source?: string; source?: string;
@ -209,12 +211,16 @@ function TootActionGroup<T extends mastodon.v1.Status>(
); );
} }
function TootAuthorGroup(props: { status: mastodon.v1.Status; now: Date }) { function TootAuthorGroup(props: {
status: mastodon.v1.Status;
now: Date;
onClick?: JSX.EventHandlerUnion<HTMLDivElement, MouseEvent>;
}) {
const toot = () => props.status; const toot = () => props.status;
const dateFnLocale = useDateFnLocale(); const dateFnLocale = useDateFnLocale();
return ( return (
<div class={tootStyle.tootAuthorGrp}> <div class={tootStyle.tootAuthorGrp} onClick={props.onClick}>
<Img src={toot().account.avatar} class={tootStyle.tootAvatar} /> <Img src={toot().account.avatar} class={tootStyle.tootAvatar} />
<div class={tootStyle.tootAuthorNameGrp}> <div class={tootStyle.tootAuthorNameGrp}>
<Body2 <Body2
@ -309,6 +315,13 @@ export function TootPreviewCard(props: {
); );
} }
/**
* Component for a toot.
*
* If the session involved is not the first session, you must wrap
* this component under a `<DefaultSessionProvier />` with correct
* session.
*/
const RegularToot: Component<TootCardProps> = (props) => { const RegularToot: Component<TootCardProps> = (props) => {
let rootRef: HTMLElement; let rootRef: HTMLElement;
const [managed, managedActionGroup, rest] = splitProps( const [managed, managedActionGroup, rest] = splitProps(
@ -319,6 +332,25 @@ const RegularToot: Component<TootCardProps> = (props) => {
const now = useTimeSource(); const now = useTimeSource();
const status = () => managed.status; const status = () => managed.status;
const toot = () => status().reblog ?? status(); const toot = () => status().reblog ?? status();
const session = useDefaultSession();
const navigate = useNavigate();
const openProfile = (event: MouseEvent) => {
if (!managed.evaluated) return;
event.stopPropagation();
const s = session();
if (!s) {
console.warn("No session is provided");
return;
}
const acct = makeAcctText(s);
navigate(
`/${encodeURIComponent(acct)}/profile/${managed.status.account.id}`,
);
};
css` css`
.reply-sep { .reply-sep {
@ -396,7 +428,7 @@ const RegularToot: Component<TootCardProps> = (props) => {
</span> </span>
</div> </div>
</Show> </Show>
<TootAuthorGroup status={toot()} now={now()} /> <TootAuthorGroup status={toot()} now={now()} onClick={openProfile} />
<TootContentView <TootContentView
source={toot().content} source={toot().content}
emojis={toot().emojis} emojis={toot().emojis}