Compare commits
3 commits
b25c2d5f45
...
4734a67f5a
Author | SHA1 | Date | |
---|---|---|---|
|
4734a67f5a | ||
|
49e1731cdb | ||
|
d4093aaf5c |
4 changed files with 73 additions and 14 deletions
|
@ -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
|
||||||
|
|
|
@ -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}`;
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
|
@ -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}
|
||||||
|
|
Loading…
Reference in a new issue