Skip to content

Commit

Permalink
Add account switcher (aeharding#8)
Browse files Browse the repository at this point in the history
* Add initial account switcher

* Fix account switching

* Add fixes for account switching edge cases when tabs open on posts
  • Loading branch information
aeharding authored Jun 25, 2023
1 parent f65cde0 commit e5407bd
Show file tree
Hide file tree
Showing 38 changed files with 451 additions and 95 deletions.
7 changes: 6 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<title>wefwef</title>
<title>wefwef for lemmy</title>

<meta
name="description"
content="wefwef is a beautiful mobile web client for lemmy. Enjoy a seamless experience browsing the fediverse."
/>

<base href="/" />

Expand Down
9 changes: 7 additions & 2 deletions src/ActorRedirect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Redirect, useLocation, useParams } from "react-router";
import { useAppSelector } from "./store";
import { jwtIssSelector } from "./features/auth/authSlice";
import React from "react";
import UseIonViewIsVisible from "./helpers/useIonViewIsVisible";

interface ActorRedirectProps {
children?: React.ReactNode;
Expand All @@ -11,11 +12,15 @@ export default function ActorRedirect({ children }: ActorRedirectProps) {
const { actor } = useParams<{ actor: string }>();
const iss = useAppSelector(jwtIssSelector);
const location = useLocation();
const ionViewIsVisible = UseIonViewIsVisible();

if (!ionViewIsVisible) return <>{children}</>;
if (!iss || !actor) return <>{children}</>;
if (iss === actor) return <>{children}</>;

const [first, second, _wrongActor, ...rest] = location.pathname.split("/");
const [first, second, _wrongActor, ...urlEnd] = location.pathname.split("/");

return <Redirect to={[first, second, iss, ...rest].join("/")} push={false} />;
return (
<Redirect to={[first, second, iss, ...urlEnd].join("/")} push={false} />
);
}
3 changes: 2 additions & 1 deletion src/Auth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useAppDispatch, useAppSelector } from "./store";
import {
getSite,
jwtIssSelector,
jwtSelector,
updateConnectedInstance,
} from "./features/auth/authSlice";
import { useLocation } from "react-router";
Expand All @@ -17,7 +18,7 @@ interface AuthProps {

export default function Auth({ children }: AuthProps) {
const dispatch = useAppDispatch();
const jwt = useAppSelector((state) => state.auth.jwt);
const jwt = useAppSelector(jwtSelector);
const iss = useAppSelector(jwtIssSelector);
const connectedInstance = useAppSelector(
(state) => state.auth.connectedInstance
Expand Down
9 changes: 6 additions & 3 deletions src/TabbedRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import PostDetail from "./features/post/detail/PostDetail";
import CommunitiesPage from "./pages/posts/CommunitiesPage";
import CommunityPage from "./pages/shared/CommunityPage";
import { useAppSelector } from "./store";
import { jwtIssSelector } from "./features/auth/authSlice";
import { jwtIssSelector, jwtSelector } from "./features/auth/authSlice";
import { POPULAR_SERVERS } from "./helpers/lemmy";
import ActorRedirect from "./ActorRedirect";
import SpecialFeedPage from "./pages/shared/SpecialFeedPage";
Expand Down Expand Up @@ -60,7 +60,7 @@ export default function TabbedRoutes() {
const { activePage } = useContext(AppContext);
const location = useLocation();
const router = useIonRouter();
const jwt = useAppSelector((state) => state.auth.jwt);
const jwt = useAppSelector(jwtSelector);
const totalUnread = useAppSelector(totalUnreadSelector);

const pageRef = useRef<IonRouterOutletCustomEvent<unknown>["target"]>(null);
Expand Down Expand Up @@ -188,7 +188,10 @@ export default function TabbedRoutes() {

return (
<PageContext.Provider value={{ page: pageRef.current as HTMLElement }}>
<IonTabs>
{/* TODO key={} resets the tab route stack whenever your instance changes. */}
{/* In the future, it would be really cool if we could resolve object urls to pick up where you left off */}
{/* But this isn't trivial with needing to rewrite URLs... */}
<IonTabs key={iss ?? DEFAULT_ACTOR}>
<IonRouterOutlet ref={pageRef}>
<Route exact path="/">
<Redirect
Expand Down
74 changes: 74 additions & 0 deletions src/features/auth/Account.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import {
IonButton,
IonIcon,
IonItem,
IonItemOption,
IonItemOptions,
IonItemSliding,
IonRadio,
ItemSlidingCustomEvent,
} from "@ionic/react";
import { removeCircle } from "ionicons/icons";
import { Credential, logoutAccount } from "./authSlice";
import { useAppDispatch } from "../../store";
import { useRef } from "react";
import styled from "@emotion/styled";

const RemoveIcon = styled(IonIcon)`
position: relative;
font-size: 1.5rem;
&:after {
z-index: -1;
content: "";
position: absolute;
inset: 5px;
border-radius: 50%;
background: white;
}
`;

interface AccountProps {
editing: boolean;
account: Credential;
}

export default function Account({ editing, account }: AccountProps) {
const dispatch = useAppDispatch();
const slidingRef = useRef<ItemSlidingCustomEvent["target"]>(null);

return (
<IonItemSliding ref={slidingRef}>
<IonItemOptions
side="end"
onIonSwipe={(e) => {
dispatch(logoutAccount(e.detail.value));
}}
>
<IonItemOption
color="danger"
expandable
onClick={() => {
dispatch(logoutAccount(account.handle));
}}
>
Log out
</IonItemOption>
</IonItemOptions>
<IonItem>
{editing && (
<IonButton
color="none"
slot="start"
onClick={() => {
slidingRef.current?.open("end");
}}
>
<RemoveIcon icon={removeCircle} color="danger" />
</IonButton>
)}
<IonRadio value={account.handle}>{account.handle}</IonRadio>
</IonItem>
</IonItemSliding>
);
}
91 changes: 91 additions & 0 deletions src/features/auth/AccountSwitcher.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import {
IonButton,
IonButtons,
IonContent,
IonHeader,
IonIcon,
IonList,
IonPage,
IonRadioGroup,
IonTitle,
IonToolbar,
useIonModal,
} from "@ionic/react";
import { add } from "ionicons/icons";
import { useAppDispatch, useAppSelector } from "../../store";
import { changeAccount } from "./authSlice";
import Login from "./Login";
import { useEffect, useState } from "react";
import Account from "./Account";

interface AccountSwitcherProps {
onDismiss: (data?: string, role?: string) => void;
page: HTMLElement | undefined;
}

export default function AccountSwitcher({
onDismiss,
page,
}: AccountSwitcherProps) {
const dispatch = useAppDispatch();
const accounts = useAppSelector((state) => state.auth.accountData?.accounts);
const activeHandle = useAppSelector(
(state) => state.auth.accountData?.activeHandle
);
const [editing, setEditing] = useState(false);

const [login, onDismissLogin] = useIonModal(Login, {
onDismiss: (data: string, role: string) => onDismissLogin(data, role),
});

useEffect(() => {
if (accounts?.length) return;

onDismiss();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [accounts]);

return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
{editing ? (
<IonButton onClick={() => login({ presentingElement: page })}>
<IonIcon icon={add} />
</IonButton>
) : (
<IonButton onClick={() => onDismiss()}>Cancel</IonButton>
)}
</IonButtons>
<IonTitle>Accounts</IonTitle>
<IonButtons slot="end">
{editing ? (
<IonButton onClick={() => setEditing(false)}>Done</IonButton>
) : (
<IonButton onClick={() => setEditing(true)}>Edit</IonButton>
)}
</IonButtons>
</IonToolbar>
</IonHeader>
<IonContent>
<IonRadioGroup
value={activeHandle}
onIonChange={(e) => {
dispatch(changeAccount(e.target.value));
}}
>
<IonList>
{accounts?.map((account) => (
<Account
key={account.handle}
account={account}
editing={editing}
/>
))}
</IonList>
</IonRadioGroup>
</IonContent>
</IonPage>
);
}
Loading

0 comments on commit e5407bd

Please sign in to comment.