init commit,

This commit is contained in:
louiscklaw
2025-05-28 09:55:51 +08:00
commit efe70ceb69
8042 changed files with 951668 additions and 0 deletions

View File

@@ -0,0 +1,26 @@
import { IonBadge, IonIcon, IonLabel } from "@ionic/react";
import { useAppSelector } from "../../../store";
import SharedTabButton, { TabButtonProps } from "./shared";
import { fileTray } from "ionicons/icons";
import { totalUnreadSelector } from "../../../features/inbox/inboxSlice";
function InboxTabButton(props: TabButtonProps) {
const totalUnread = useAppSelector(totalUnreadSelector);
return (
<SharedTabButton {...props}>
<IonIcon aria-hidden="true" icon={fileTray} />
<IonLabel>Inbox</IonLabel>
{totalUnread ? (
<IonBadge color="danger">{totalUnread}</IonBadge>
) : undefined}
</SharedTabButton>
);
}
/**
* Signal to Ionic that this is a tab bar button component
*/
InboxTabButton.isTabButton = true;
export default InboxTabButton;

View File

@@ -0,0 +1,78 @@
import { IonIcon, IonLabel } from "@ionic/react";
import { useAppSelector } from "../../../store";
import {
instanceSelector,
jwtSelector,
} from "../../../features/auth/authSelectors";
import { useOptimizedIonRouter } from "../../../helpers/useOptimizedIonRouter";
import SharedTabButton, { TabButtonProps } from "./shared";
import { getDefaultServer } from "../../../services/app";
import { telescope } from "ionicons/icons";
import { openTitleSearch } from "../../../features/community/titleSearch/TitleSearch";
import { useCallback } from "react";
function PostsTabButton(props: TabButtonProps) {
const router = useOptimizedIonRouter();
const selectedInstance = useAppSelector(instanceSelector);
const jwt = useAppSelector(jwtSelector);
const customBackAction = useCallback(() => {
const pathname = router.getRouteInfo()?.pathname;
if (!pathname) return;
const actor = pathname.split("/")[2];
if (pathname.endsWith(jwt ? "/home" : "/all")) {
router.push(
`/posts/${actor ?? selectedInstance ?? getDefaultServer()}`,
"back",
);
return;
}
const communitiesPath = `/posts/${
actor ?? selectedInstance ?? getDefaultServer()
}`;
if (pathname === communitiesPath || pathname === `${communitiesPath}/`)
return;
if (router.canGoBack()) {
router.goBack();
} else {
router.push(
`/posts/${actor ?? selectedInstance ?? getDefaultServer()}/${
jwt ? "home" : "all"
}`,
"back",
);
}
}, [jwt, router, selectedInstance]);
return (
<SharedTabButton
{...props}
customBackAction={customBackAction}
onLongPressExtraAction={onLongPressExtraAction}
>
<IonIcon aria-hidden="true" icon={telescope} />
<IonLabel>Posts</IonLabel>
</SharedTabButton>
);
}
function onLongPressExtraAction() {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
openTitleSearch();
});
});
});
}
/**
* Signal to Ionic that this is a tab bar button component
*/
PostsTabButton.isTabButton = true;
export default PostsTabButton;

View File

@@ -0,0 +1,79 @@
import { IonIcon, IonLabel } from "@ionic/react";
import { useCallback, useContext, useMemo } from "react";
import { useAppSelector } from "../../../store";
import SharedTabButton, { TabButtonProps } from "./shared";
import { personCircleOutline } from "ionicons/icons";
import { PageContext } from "../../../features/auth/PageContext";
import {
accountsListEmptySelector,
userHandleSelector,
} from "../../../features/auth/authSelectors";
import { getProfileTabLabel } from "../../../features/settings/general/other/ProfileTabLabel";
import { styled } from "@linaria/react";
import { useOptimizedIonRouter } from "../../../helpers/useOptimizedIonRouter";
const ProfileLabel = styled(IonLabel)`
max-width: 20vw;
`;
function ProfileTabButton(props: TabButtonProps) {
const router = useOptimizedIonRouter();
const { presentAccountSwitcher, presentLoginIfNeeded } =
useContext(PageContext);
const accountsListEmpty = useAppSelector(accountsListEmptySelector);
const connectedInstance = useAppSelector(
(state) => state.auth.connectedInstance,
);
const userHandle = useAppSelector(userHandleSelector);
const profileLabelType = useAppSelector(
(state) => state.settings.appearance.general.profileLabel,
);
const profileTabLabel = useMemo(
() => getProfileTabLabel(profileLabelType, userHandle, connectedInstance),
[profileLabelType, userHandle, connectedInstance],
);
const onBeforeBackAction = useCallback(() => {
const pathname = router.getRouteInfo()?.pathname;
if (!pathname) return;
// if the profile page is already open, show the account switcher
if (pathname === "/profile") {
if (!accountsListEmpty) {
presentAccountSwitcher();
} else {
presentLoginIfNeeded();
}
}
}, [accountsListEmpty, presentAccountSwitcher, presentLoginIfNeeded, router]);
const onLongPressOverride = useCallback(() => {
if (!accountsListEmpty) {
presentAccountSwitcher();
} else {
presentLoginIfNeeded();
}
}, [accountsListEmpty, presentAccountSwitcher, presentLoginIfNeeded]);
return (
<SharedTabButton
{...props}
onBeforeBackAction={onBeforeBackAction}
onLongPressOverride={onLongPressOverride}
>
<IonIcon aria-hidden="true" icon={personCircleOutline} />
<ProfileLabel>{profileTabLabel}</ProfileLabel>
</SharedTabButton>
);
}
/**
* Signal to Ionic that this is a tab bar button component
*/
ProfileTabButton.isTabButton = true;
export default ProfileTabButton;

View File

@@ -0,0 +1,34 @@
import { IonIcon, IonLabel } from "@ionic/react";
import SharedTabButton, { TabButtonProps } from "./shared";
import { search } from "ionicons/icons";
import { focusSearchBar } from "../../pages/search/SearchPage";
function SearchTabButton(props: TabButtonProps) {
return (
<SharedTabButton
{...props}
onBeforeBackAction={focusSearchBar}
onLongPressExtraAction={onLongPressExtraAction}
>
<IonIcon aria-hidden="true" icon={search} />
<IonLabel>Search</IonLabel>
</SharedTabButton>
);
}
function onLongPressExtraAction() {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
focusSearchBar();
});
});
});
}
/**
* Signal to Ionic that this is a tab bar button component
*/
SearchTabButton.isTabButton = true;
export default SearchTabButton;

View File

@@ -0,0 +1,39 @@
import { IonBadge, IonIcon, IonLabel } from "@ionic/react";
import { useContext } from "react";
import SharedTabButton, { TabButtonProps } from "./shared";
import { cog } from "ionicons/icons";
import { useAppSelector } from "../../../store";
import { UpdateContext } from "../../pages/settings/update/UpdateContext";
import useShouldInstall from "../../../features/pwa/useShouldInstall";
function SettingsTabButton(props: TabButtonProps) {
const databaseError = useAppSelector((state) => state.settings.databaseError);
const { status: updateStatus } = useContext(UpdateContext);
const shouldInstall = useShouldInstall();
const settingsNotificationCount =
(shouldInstall ? 1 : 0) + (updateStatus === "outdated" ? 1 : 0);
const settingsBadge = (() => {
if (databaseError) return <IonBadge color="danger">!</IonBadge>;
if (settingsNotificationCount)
return <IonBadge color="danger">{settingsNotificationCount}</IonBadge>;
})();
return (
<SharedTabButton {...props}>
<IonIcon aria-hidden="true" icon={cog} />
<IonLabel>Settings</IonLabel>
{settingsBadge}
</SharedTabButton>
);
}
/**
* Signal to Ionic that this is a tab bar button component
*/
SettingsTabButton.isTabButton = true;
export default SettingsTabButton;

View File

@@ -0,0 +1,141 @@
import { IonTabButton } from "@ionic/react";
import { useCallback, useContext, useMemo } from "react";
import { LongPressReactEvents, useLongPress } from "use-long-press";
import { useOptimizedIonRouter } from "../../../helpers/useOptimizedIonRouter";
import { scrollUpIfNeeded } from "../../../helpers/scrollUpIfNeeded";
import { AppContext } from "../../../features/auth/AppContext";
import { ImpactStyle } from "@capacitor/haptics";
import useHapticFeedback from "../../../helpers/useHapticFeedback";
import { styled } from "@linaria/react";
// reverts https://github.com/ionic-team/ionic-framework/pull/28754
const StyledIonTabButton = styled(IonTabButton)`
&.ios.tab-has-label {
ion-icon {
font-size: 30px;
}
}
`;
export interface TabButtonProps {
/**
* Used internally by Ionic. Pass down.
*/
tab: string;
/**
* Ionic will change. Pass down. Do not access/use directly.
*/
href: string;
/**
* When rendered inside TabBar with isTabButton, Ionic will setup this onClick function
*/
onClick?: (e: CustomEvent) => void;
children?: React.ReactNode;
longPressedRef: React.MutableRefObject<boolean>;
onLongPressOverride?: () => void;
onLongPressExtraAction?: () => void;
customBackAction?: () => void;
onBeforeBackAction?: () => void;
}
export default function SharedTabButton({
longPressedRef,
onClick,
children,
onLongPressExtraAction,
onLongPressOverride,
customBackAction,
onBeforeBackAction,
...rest
}: TabButtonProps) {
const vibrate = useHapticFeedback();
const router = useOptimizedIonRouter();
const { activePageRef } = useContext(AppContext);
const defaultHref = `/${rest.tab}`;
const onDefaultClick = useCallback(
(e: CustomEvent) => {
if (longPressedRef.current) {
return;
}
if (!router.getRouteInfo()?.pathname.startsWith(defaultHref)) {
onClick?.(e);
return;
}
if (scrollUpIfNeeded(activePageRef?.current)) return;
if (customBackAction) {
customBackAction();
return;
}
onBeforeBackAction?.();
router.push(defaultHref, "back");
},
[
activePageRef,
router,
longPressedRef,
onClick,
defaultHref,
customBackAction,
onBeforeBackAction,
],
);
const onLongPress = useCallback(
(e: LongPressReactEvents) => {
vibrate({ style: ImpactStyle.Light });
if (onLongPressOverride) {
onLongPressOverride();
return;
}
if (!router.getRouteInfo()?.pathname.startsWith(defaultHref)) {
if (e.target instanceof HTMLElement) e.target.click();
}
// order matters- set after target.click()
longPressedRef.current = true;
onLongPressExtraAction?.();
},
[
router,
vibrate,
longPressedRef,
onLongPressExtraAction,
defaultHref,
onLongPressOverride,
],
);
const tabLongPressSettings = useMemo(
() => ({
onFinish: () => {
setTimeout(() => {
longPressedRef.current = false;
}, 200);
},
}),
[longPressedRef],
);
const longPressBind = useLongPress(onLongPress, tabLongPressSettings);
return (
<StyledIonTabButton onClick={onDefaultClick} {...longPressBind()} {...rest}>
{children}
</StyledIonTabButton>
);
}

View File

@@ -0,0 +1,104 @@
/* eslint-disable react/jsx-key */
import Route from "../common/Route";
import SearchFeedResultsPage from "../pages/search/results/SearchFeedResultsPage";
import CommunityPage from "../pages/shared/CommunityPage";
import CommunitySidebarPage from "../pages/shared/CommunitySidebarPage";
import PostDetail from "../pages/posts/PostPage";
import CommunityCommentsPage from "../pages/shared/CommunityCommentsPage";
import ModlogPage from "../pages/shared/ModlogPage";
import ModqueuePage from "../pages/shared/ModqueuePage";
import CommentsPage from "../pages/shared/CommentsPage";
import UserPage from "../pages/profile/UserPage";
import ProfileFeedItemsPage from "../pages/profile/ProfileFeedItemsPage";
import ProfileFeedHiddenPostsPage from "../pages/profile/ProfileFeedHiddenPostsPage";
import ConversationPage from "../pages/inbox/ConversationPage";
import InstanceSidebarPage from "../pages/shared/InstanceSidebarPage";
import SpecialFeedPage from "../pages/shared/SpecialFeedPage";
export default [
<Route exact path="/:tab/:actor/c/:community">
<CommunityPage />
</Route>,
<Route exact path="/:tab/:actor/c/:community/search/posts/:search">
<SearchFeedResultsPage type="Posts" />
</Route>,
<Route exact path="/:tab/:actor/c/:community/search/comments/:search">
<SearchFeedResultsPage type="Comments" />
</Route>,
<Route exact path="/:tab/:actor/c/:community/sidebar">
<CommunitySidebarPage />
</Route>,
<Route exact path="/:tab/:actor/c/:community/comments/:id">
<PostDetail />
</Route>,
<Route
exact
path="/:tab/:actor/c/:community/comments/:id/thread/:threadCommentId"
>
<PostDetail />
</Route>,
<Route exact path="/:tab/:actor/c/:community/comments/:id/:commentPath">
<PostDetail />
</Route>,
<Route exact path="/:tab/:actor/c/:community/comments">
<CommunityCommentsPage />
</Route>,
<Route exact path="/:tab/:actor/c/:community/log">
<ModlogPage />
</Route>,
<Route exact path="/:tab/:actor/c/:community/modqueue">
<ModqueuePage />
</Route>,
<Route exact path="/:tab/:actor/home">
<SpecialFeedPage type="Subscribed" />
</Route>,
<Route exact path="/:tab/:actor/all">
<SpecialFeedPage type="All" />
</Route>,
<Route exact path="/:tab/:actor/local">
<SpecialFeedPage type="Local" />
</Route>,
<Route exact path="/:tab/:actor/mod">
<SpecialFeedPage type="ModeratorView" />
</Route>,
<Route exact path="/:tab/:actor/mod/comments">
<CommentsPage type="ModeratorView" />
</Route>,
<Route exact path="/:tab/:actor/mod/log">
<ModlogPage />
</Route>,
<Route exact path="/:tab/:actor/mod/modqueue">
<ModqueuePage />
</Route>,
<Route exact path="/:tab/:actor/u/:handle">
<UserPage />
</Route>,
<Route exact path="/:tab/:actor/u/:handle/posts">
<ProfileFeedItemsPage type="Posts" />
</Route>,
<Route exact path="/:tab/:actor/u/:handle/comments">
<ProfileFeedItemsPage type="Comments" />
</Route>,
<Route exact path="/:tab/:actor/u/:handle/saved">
<ProfileFeedItemsPage type="Saved" />
</Route>,
<Route exact path="/:tab/:actor/u/:handle/hidden">
<ProfileFeedHiddenPostsPage />
</Route>,
<Route exact path="/:tab/:actor/u/:handle/upvoted">
<ProfileFeedItemsPage type="Upvoted" />
</Route>,
<Route exact path="/:tab/:actor/u/:handle/downvoted">
<ProfileFeedItemsPage type="Downvoted" />
</Route>,
<Route exact path="/:tab/:actor/u/:handle/message">
<ConversationPage />
</Route>,
<Route exact path="/:tab/:actor/u/:handle/log">
<ModlogPage />
</Route>,
<Route exact path="/:tab/:actor/sidebar">
<InstanceSidebarPage />
</Route>,
];

View File

@@ -0,0 +1,51 @@
/* eslint-disable react/jsx-key */
import Route from "../common/Route";
import BoxesPage from "../pages/inbox/BoxesPage";
import ConversationPage from "../pages/inbox/ConversationPage";
import InboxAuthRequired from "../pages/inbox/InboxAuthRequired";
import InboxPage from "../pages/inbox/InboxPage";
import MentionsPage from "../pages/inbox/MentionsPage";
import MessagesPage from "../pages/inbox/MessagesPage";
import RepliesPage from "../pages/inbox/RepliesPage";
export default [
<Route exact path="/inbox">
<BoxesPage />
</Route>,
<Route exact path="/inbox/all">
<InboxAuthRequired>
<InboxPage showRead />
</InboxAuthRequired>
</Route>,
<Route exact path="/inbox/unread">
<InboxAuthRequired>
<InboxPage />
</InboxAuthRequired>
</Route>,
<Route exact path="/inbox/mentions">
<InboxAuthRequired>
<MentionsPage />
</InboxAuthRequired>
</Route>,
<Route exact path="/inbox/comment-replies">
<InboxAuthRequired>
<RepliesPage type="Comment" />
</InboxAuthRequired>
</Route>,
<Route exact path="/inbox/post-replies">
<InboxAuthRequired>
<RepliesPage type="Post" />
</InboxAuthRequired>
</Route>,
<Route exact path="/inbox/messages">
<InboxAuthRequired>
<MessagesPage />
</InboxAuthRequired>
</Route>,
<Route exact path="/inbox/messages/:handle">
<InboxAuthRequired>
<ConversationPage />
</InboxAuthRequired>
</Route>,
];

View File

@@ -0,0 +1,35 @@
/* eslint-disable react/jsx-key */
import { Redirect } from "react-router";
import Route from "../common/Route";
import { getDefaultServer } from "../../services/app";
import CommunitiesPage from "../pages/posts/CommunitiesPage";
import { DefaultFeedType } from "../../services/db";
interface Props {
defaultFeed: DefaultFeedType | undefined;
selectedInstance: string | undefined;
redirectRoute: string;
}
export default function buildPostsRoutes({
defaultFeed,
redirectRoute,
selectedInstance,
}: Props) {
return [
<Route exact path="/posts">
{defaultFeed ? (
<Redirect
to={`/posts/${selectedInstance ?? getDefaultServer()}${redirectRoute}`}
push={false}
/>
) : (
""
)}
</Route>,
<Route exact path="/posts/:actor">
<CommunitiesPage />
</Route>,
];
}

View File

@@ -0,0 +1,10 @@
/* eslint-disable react/jsx-key */
import Route from "../common/Route";
import ProfilePage from "../pages/profile/ProfilePage";
export default [
<Route exact path="/profile">
<ProfilePage />
</Route>,
];

View File

@@ -0,0 +1,29 @@
/* eslint-disable react/jsx-key */
import Route from "../common/Route";
import SearchPage from "../pages/search/SearchPage";
import SearchPostsResultsPage from "../pages/search/results/SearchFeedResultsPage";
import SearchCommunitiesPage from "../pages/search/results/SearchCommunitiesPage";
import RandomCommunityPage from "../pages/search/RandomCommunityPage";
import CommunitiesResultsPage from "../pages/search/CommunitiesResultsPage";
export default [
<Route exact path="/search">
<SearchPage />
</Route>,
<Route exact path="/search/random">
<RandomCommunityPage />
</Route>,
<Route exact path="/search/posts/:search">
<SearchPostsResultsPage type="Posts" />
</Route>,
<Route exact path="/search/comments/:search">
<SearchPostsResultsPage type="Comments" />
</Route>,
<Route exact path="/search/communities/:search">
<SearchCommunitiesPage />
</Route>,
<Route exact path="/search/explore">
<CommunitiesResultsPage />
</Route>,
];

View File

@@ -0,0 +1,74 @@
/* eslint-disable react/jsx-key */
import Route from "../common/Route";
import SettingsPage from "../pages/settings/SettingsPage";
import InstallAppPage from "../pages/settings/InstallAppPage";
import UpdateAppPage from "../pages/settings/UpdateAppPage";
import GeneralPage from "../pages/settings/GeneralPage";
import HidingSettingsPage from "../pages/settings/HidingSettingsPage";
import AppearancePage from "../pages/settings/AppearancePage";
import AppearanceThemePage from "../pages/settings/AppearanceThemePage";
import DeviceModeSettingsPage from "../pages/settings/DeviceModeSettingsPage";
import AppIconPage from "../pages/settings/AppIconPage";
import BiometricPage from "../pages/settings/BiometricPage";
import GesturesPage from "../pages/settings/GesturesPage";
import BlocksSettingsPage from "../pages/settings/BlocksSettingsPage";
import RedditMigratePage from "../pages/settings/RedditDataMigratePage";
import SearchCommunitiesPage from "../pages/search/results/SearchCommunitiesPage";
import AboutPage from "../pages/settings/about/AboutPage";
import AboutThanksPage from "../pages/settings/about/AboutThanksPage";
import RedditMigrateSubsListPage from "../pages/settings/RedditMigrateSubsListPage";
export default [
<Route exact path="/settings">
<SettingsPage />
</Route>,
<Route exact path="/settings/install">
<InstallAppPage />
</Route>,
<Route exact path="/settings/update">
<UpdateAppPage />
</Route>,
<Route exact path="/settings/general">
<GeneralPage />
</Route>,
<Route exact path="/settings/general/hiding">
<HidingSettingsPage />
</Route>,
<Route exact path="/settings/appearance">
<AppearancePage />
</Route>,
<Route exact path="/settings/appearance/theme">
<AppearanceThemePage />
</Route>,
<Route exact path="/settings/appearance/theme/mode">
<DeviceModeSettingsPage />
</Route>,
<Route exact path="/settings/app-icon">
<AppIconPage />
</Route>,
<Route exact path="/settings/biometric">
<BiometricPage />
</Route>,
<Route exact path="/settings/gestures">
<GesturesPage />
</Route>,
<Route exact path="/settings/blocks">
<BlocksSettingsPage />
</Route>,
<Route exact path="/settings/reddit-migrate">
<RedditMigratePage />
</Route>,
<Route exact path="/settings/reddit-migrate/:link">
<RedditMigrateSubsListPage />
</Route>,
<Route exact path="/settings/reddit-migrate/:link/:search">
<SearchCommunitiesPage />
</Route>,
<Route exact path="/settings/about">
<AboutPage />
</Route>,
<Route exact path="/settings/about/thanks">
<AboutThanksPage />
</Route>,
];