Skip to content

Commit addf95d

Browse files
authoredAug 13, 2023
Fix performance issues on navigation changes from post feeds (aeharding#649)
1 parent eb78292 commit addf95d

21 files changed

+615
-562
lines changed
 

‎src/App.tsx

+8-5
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { getDeviceMode } from "./features/settings/settingsSlice";
3030
import { SafeArea, SafeAreaInsets } from "capacitor-plugin-safe-area";
3131
import { StatusBar } from "@capacitor/status-bar";
3232
import { Keyboard } from "@capacitor/keyboard";
33+
import { TabContextProvider } from "./TabContext";
3334

3435
setupIonicReact({
3536
rippleEffect: false,
@@ -74,11 +75,13 @@ export default function App() {
7475
<BeforeInstallPromptProvider>
7576
<UpdateContextProvider>
7677
<Router>
77-
<IonApp>
78-
<Auth>
79-
<TabbedRoutes />
80-
</Auth>
81-
</IonApp>
78+
<TabContextProvider>
79+
<IonApp>
80+
<Auth>
81+
<TabbedRoutes />
82+
</Auth>
83+
</IonApp>
84+
</TabContextProvider>
8285
</Router>
8386
</UpdateContextProvider>
8487
</BeforeInstallPromptProvider>

‎src/TabContext.tsx

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import React, { createContext, useMemo } from "react";
2+
import { useLocation } from "react-router";
3+
4+
interface ITabContext {
5+
tab: string;
6+
}
7+
8+
export const TabContext = createContext<ITabContext>({
9+
tab: "",
10+
});
11+
12+
/**
13+
* The reason for this, instead of useLocation() in components directly to get tab name,
14+
* is that it does not trigger a rerender on navigation changes.
15+
*/
16+
export function TabContextProvider({
17+
children,
18+
}: {
19+
children: React.ReactNode;
20+
}) {
21+
const location = useLocation();
22+
23+
const tab = location.pathname.split("/")[1];
24+
25+
const memoized = useMemo(
26+
() => (
27+
<TabContextProviderInternals tab={tab}>
28+
{children}
29+
</TabContextProviderInternals>
30+
),
31+
[tab, children]
32+
);
33+
34+
return memoized;
35+
}
36+
37+
function TabContextProviderInternals({
38+
tab,
39+
children,
40+
}: {
41+
tab: string;
42+
children: React.ReactNode;
43+
}) {
44+
return <TabContext.Provider value={{ tab }}>{children}</TabContext.Provider>;
45+
}

‎src/TabbedRoutes.tsx

+154-144
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ const ProfileLabel = styled(IonLabel)`
8080
`;
8181

8282
export default function TabbedRoutes() {
83-
const { activePage } = useContext(AppContext);
83+
const { activePageRef } = useContext(AppContext);
8484
const location = useLocation();
8585
const router = useIonRouter();
8686
const jwt = useAppSelector(jwtSelector);
@@ -145,7 +145,7 @@ export default function TabbedRoutes() {
145145
async function onPostsClick() {
146146
if (!isPostsButtonDisabled) return;
147147

148-
if (await scrollUpIfNeeded(activePage)) return;
148+
if (await scrollUpIfNeeded(activePageRef?.current)) return;
149149

150150
if (location.pathname.endsWith(jwt ? "/home" : "/all")) {
151151
router.push(`/posts/${actor ?? iss ?? getDefaultServer()}`, "back");
@@ -172,15 +172,15 @@ export default function TabbedRoutes() {
172172
async function onInboxClick() {
173173
if (!isInboxButtonDisabled) return;
174174

175-
if (await scrollUpIfNeeded(activePage)) return;
175+
if (await scrollUpIfNeeded(activePageRef?.current)) return;
176176

177177
router.push(`/inbox`, "back");
178178
}
179179

180180
async function onProfileClick() {
181181
if (!isProfileButtonDisabled) return;
182182

183-
if (await scrollUpIfNeeded(activePage)) return;
183+
if (await scrollUpIfNeeded(activePageRef?.current)) return;
184184

185185
router.push("/profile", "back");
186186
}
@@ -191,7 +191,7 @@ export default function TabbedRoutes() {
191191
// if the search page is already open, focus the search bar
192192
focusSearchBar();
193193

194-
if (await scrollUpIfNeeded(activePage)) return;
194+
if (await scrollUpIfNeeded(activePageRef?.current)) return;
195195

196196
router.push(`/search`, "back");
197197
}
@@ -270,153 +270,163 @@ export default function TabbedRoutes() {
270270
];
271271
}
272272

273+
const pageContextValue = useMemo(() => ({ pageRef }), []);
274+
275+
// Ideally this would be a separate component,
276+
// but that is not currently supported in Ionic React
277+
// So memoize instead (since this is expensive to build)
278+
const routes = useMemo(
279+
() => (
280+
<IonRouterOutlet ref={pageRef}>
281+
<Route exact path="/">
282+
<Redirect
283+
to={`/posts/${iss ?? getDefaultServer()}/${iss ? "home" : "all"}`}
284+
push={false}
285+
/>
286+
</Route>
287+
<Route exact path="/posts/:actor/home">
288+
<ActorRedirect>
289+
<SpecialFeedPage type="Subscribed" />
290+
</ActorRedirect>
291+
</Route>
292+
<Route exact path="/posts/:actor/all">
293+
<ActorRedirect>
294+
<SpecialFeedPage type="All" />
295+
</ActorRedirect>
296+
</Route>
297+
<Route exact path="/posts/:actor/local">
298+
<ActorRedirect>
299+
<SpecialFeedPage type="Local" />
300+
</ActorRedirect>
301+
</Route>
302+
<Route exact path="/posts/:actor">
303+
<ActorRedirect>
304+
<CommunitiesPage />
305+
</ActorRedirect>
306+
</Route>
307+
{...buildGeneralBrowseRoutes("posts")}
308+
309+
<Route exact path="/inbox">
310+
<BoxesPage />
311+
</Route>
312+
<Route exact path="/inbox/all">
313+
<InboxAuthRequired>
314+
<InboxPage showRead />
315+
</InboxAuthRequired>
316+
</Route>
317+
<Route exact path="/inbox/unread">
318+
<InboxAuthRequired>
319+
<InboxPage />
320+
</InboxAuthRequired>
321+
</Route>
322+
<Route exact path="/inbox/mentions">
323+
<InboxAuthRequired>
324+
<MentionsPage />
325+
</InboxAuthRequired>
326+
</Route>
327+
<Route exact path="/inbox/comment-replies">
328+
<InboxAuthRequired>
329+
<RepliesPage type="Comment" />
330+
</InboxAuthRequired>
331+
</Route>
332+
<Route exact path="/inbox/post-replies">
333+
<InboxAuthRequired>
334+
<RepliesPage type="Post" />
335+
</InboxAuthRequired>
336+
</Route>
337+
<Route exact path="/inbox/messages">
338+
<InboxAuthRequired>
339+
<MessagesPage />
340+
</InboxAuthRequired>
341+
</Route>
342+
<Route exact path="/inbox/messages/:handle">
343+
<InboxAuthRequired>
344+
<ConversationPage />
345+
</InboxAuthRequired>
346+
</Route>
347+
{...buildGeneralBrowseRoutes("inbox")}
348+
349+
<Route exact path="/profile">
350+
<ProfilePage />
351+
</Route>
352+
{...buildGeneralBrowseRoutes("profile")}
353+
<Route exact path="/profile/:actor">
354+
<Redirect to="/profile" push={false} />
355+
</Route>
356+
357+
<Route exact path="/search">
358+
<SearchPage />
359+
</Route>
360+
<Route exact path="/search/posts/:search">
361+
<SearchPostsResultsPage type="Posts" />
362+
</Route>
363+
<Route exact path="/search/comments/:search">
364+
<SearchPostsResultsPage type="Comments" />
365+
</Route>
366+
<Route exact path="/search/communities/:search">
367+
<SearchCommunitiesPage />
368+
</Route>
369+
{...buildGeneralBrowseRoutes("search")}
370+
<Route exact path="/search/:actor">
371+
<Redirect to="/search" push={false} />
372+
</Route>
373+
374+
<Route exact path="/settings">
375+
<SettingsPage />
376+
</Route>
377+
<Route exact path="/settings/terms">
378+
<TermsPage />
379+
</Route>
380+
<Route exact path="/settings/install">
381+
<InstallAppPage />
382+
</Route>
383+
<Route exact path="/settings/update">
384+
<UpdateAppPage />
385+
</Route>
386+
<Route exact path="/settings/general">
387+
<GeneralPage />
388+
</Route>
389+
<Route exact path="/settings/general/hiding">
390+
<HidingSettingsPage />
391+
</Route>
392+
<Route exact path="/settings/appearance">
393+
<AppearancePage />
394+
</Route>
395+
<Route exact path="/settings/appearance/theme">
396+
<AppearanceThemePage />
397+
</Route>
398+
<Route exact path="/settings/appearance/theme/mode">
399+
<DeviceModeSettingsPage />
400+
</Route>
401+
<Route exact path="/settings/gestures">
402+
<GesturesPage />
403+
</Route>
404+
<Route exact path="/settings/blocks">
405+
<BlocksSettingsPage />
406+
</Route>
407+
<Route exact path="/settings/reddit-migrate">
408+
<RedditMigratePage />
409+
</Route>
410+
<Route exact path="/settings/reddit-migrate/:search">
411+
<SearchCommunitiesPage />
412+
</Route>
413+
{/* general routes for settings is only for reddit-migrate */}
414+
{...buildGeneralBrowseRoutes("settings")}
415+
</IonRouterOutlet>
416+
),
417+
[iss]
418+
);
419+
273420
if (!ready) return;
274421

275422
return (
276-
<PageContextProvider value={{ page: pageRef.current as HTMLElement }}>
423+
<PageContextProvider value={pageContextValue}>
277424
<GalleryProvider>
278425
{/* TODO key={} resets the tab route stack whenever your instance changes. */}
279426
{/* In the future, it would be really cool if we could resolve object urls to pick up where you left off */}
280427
{/* But this isn't trivial with needing to rewrite URLs... */}
281428
<IonTabs key={iss ?? getDefaultServer()}>
282-
<IonRouterOutlet ref={pageRef}>
283-
<Route exact path="/">
284-
<Redirect
285-
to={`/posts/${iss ?? getDefaultServer()}/${
286-
iss ? "home" : "all"
287-
}`}
288-
push={false}
289-
/>
290-
</Route>
291-
<Route exact path="/posts/:actor/home">
292-
<ActorRedirect>
293-
<SpecialFeedPage type="Subscribed" />
294-
</ActorRedirect>
295-
</Route>
296-
<Route exact path="/posts/:actor/all">
297-
<ActorRedirect>
298-
<SpecialFeedPage type="All" />
299-
</ActorRedirect>
300-
</Route>
301-
<Route exact path="/posts/:actor/local">
302-
<ActorRedirect>
303-
<SpecialFeedPage type="Local" />
304-
</ActorRedirect>
305-
</Route>
306-
<Route exact path="/posts/:actor">
307-
<ActorRedirect>
308-
<CommunitiesPage />
309-
</ActorRedirect>
310-
</Route>
311-
{...buildGeneralBrowseRoutes("posts")}
312-
313-
<Route exact path="/inbox">
314-
<BoxesPage />
315-
</Route>
316-
<Route exact path="/inbox/all">
317-
<InboxAuthRequired>
318-
<InboxPage showRead />
319-
</InboxAuthRequired>
320-
</Route>
321-
<Route exact path="/inbox/unread">
322-
<InboxAuthRequired>
323-
<InboxPage />
324-
</InboxAuthRequired>
325-
</Route>
326-
<Route exact path="/inbox/mentions">
327-
<InboxAuthRequired>
328-
<MentionsPage />
329-
</InboxAuthRequired>
330-
</Route>
331-
<Route exact path="/inbox/comment-replies">
332-
<InboxAuthRequired>
333-
<RepliesPage type="Comment" />
334-
</InboxAuthRequired>
335-
</Route>
336-
<Route exact path="/inbox/post-replies">
337-
<InboxAuthRequired>
338-
<RepliesPage type="Post" />
339-
</InboxAuthRequired>
340-
</Route>
341-
<Route exact path="/inbox/messages">
342-
<InboxAuthRequired>
343-
<MessagesPage />
344-
</InboxAuthRequired>
345-
</Route>
346-
<Route exact path="/inbox/messages/:handle">
347-
<InboxAuthRequired>
348-
<ConversationPage />
349-
</InboxAuthRequired>
350-
</Route>
351-
{...buildGeneralBrowseRoutes("inbox")}
352-
353-
<Route exact path="/profile">
354-
<ProfilePage />
355-
</Route>
356-
{...buildGeneralBrowseRoutes("profile")}
357-
<Route exact path="/profile/:actor">
358-
<Redirect to="/profile" push={false} />
359-
</Route>
360-
361-
<Route exact path="/search">
362-
<SearchPage />
363-
</Route>
364-
<Route exact path="/search/posts/:search">
365-
<SearchPostsResultsPage type="Posts" />
366-
</Route>
367-
<Route exact path="/search/comments/:search">
368-
<SearchPostsResultsPage type="Comments" />
369-
</Route>
370-
<Route exact path="/search/communities/:search">
371-
<SearchCommunitiesPage />
372-
</Route>
373-
{...buildGeneralBrowseRoutes("search")}
374-
<Route exact path="/search/:actor">
375-
<Redirect to="/search" push={false} />
376-
</Route>
377-
378-
<Route exact path="/settings">
379-
<SettingsPage />
380-
</Route>
381-
<Route exact path="/settings/terms">
382-
<TermsPage />
383-
</Route>
384-
<Route exact path="/settings/install">
385-
<InstallAppPage />
386-
</Route>
387-
<Route exact path="/settings/update">
388-
<UpdateAppPage />
389-
</Route>
390-
<Route exact path="/settings/general">
391-
<GeneralPage />
392-
</Route>
393-
<Route exact path="/settings/general/hiding">
394-
<HidingSettingsPage />
395-
</Route>
396-
<Route exact path="/settings/appearance">
397-
<AppearancePage />
398-
</Route>
399-
<Route exact path="/settings/appearance/theme">
400-
<AppearanceThemePage />
401-
</Route>
402-
<Route exact path="/settings/appearance/theme/mode">
403-
<DeviceModeSettingsPage />
404-
</Route>
405-
<Route exact path="/settings/gestures">
406-
<GesturesPage />
407-
</Route>
408-
<Route exact path="/settings/blocks">
409-
<BlocksSettingsPage />
410-
</Route>
411-
<Route exact path="/settings/reddit-migrate">
412-
<RedditMigratePage />
413-
</Route>
414-
<Route exact path="/settings/reddit-migrate/:search">
415-
<SearchCommunitiesPage />
416-
</Route>
417-
{/* general routes for settings is only for reddit-migrate */}
418-
{...buildGeneralBrowseRoutes("settings")}
419-
</IonRouterOutlet>
429+
{routes}
420430
<IonTabBar slot="bottom">
421431
<IonTabButton
422432
disabled={isPostsButtonDisabled}

‎src/features/auth/AccountSwitcher.tsx

+8-4
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,17 @@ import { add } from "ionicons/icons";
1515
import { useAppDispatch, useAppSelector } from "../../store";
1616
import { changeAccount } from "./authSlice";
1717
import Login from "./Login";
18-
import { useEffect, useState } from "react";
18+
import { RefObject, useEffect, useState } from "react";
1919
import Account from "./Account";
2020

2121
interface AccountSwitcherProps {
2222
onDismiss: (data?: string, role?: string) => void;
23-
page: HTMLElement | undefined;
23+
pageRef: RefObject<HTMLElement | undefined>;
2424
}
2525

2626
export default function AccountSwitcher({
2727
onDismiss,
28-
page,
28+
pageRef,
2929
}: AccountSwitcherProps) {
3030
const dispatch = useAppDispatch();
3131
const accounts = useAppSelector((state) => state.auth.accountData?.accounts);
@@ -52,7 +52,11 @@ export default function AccountSwitcher({
5252
<IonToolbar>
5353
<IonButtons slot="start">
5454
{editing ? (
55-
<IonButton onClick={() => login({ presentingElement: page })}>
55+
<IonButton
56+
onClick={() =>
57+
login({ presentingElement: pageRef.current ?? undefined })
58+
}
59+
>
5660
<IonIcon icon={add} />
5761
</IonButton>
5862
) : (

‎src/features/auth/AppContext.tsx

+17-10
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,21 @@ import React, {
44
createContext,
55
useContext,
66
useEffect,
7-
useState,
7+
useMemo,
8+
useRef,
89
} from "react";
910
import { VirtuosoHandle } from "react-virtuoso";
1011

1112
export type Page = RefObject<VirtuosoHandle | HTMLElement>;
1213

1314
interface IAppContext {
1415
// used for determining whether page needs to be scrolled up first
15-
activePage: Page | undefined;
16+
activePageRef: RefObject<Page | undefined> | undefined;
1617
setActivePage: (activePage: Page) => void;
1718
}
1819

1920
export const AppContext = createContext<IAppContext>({
20-
activePage: undefined,
21+
activePageRef: undefined,
2122
setActivePage: () => {},
2223
});
2324

@@ -26,17 +27,23 @@ export function AppContextProvider({
2627
}: {
2728
children: React.ReactNode;
2829
}) {
29-
const [activePage, setActivePage] = useState<Page | undefined>();
30+
const activePageRef = useRef<Page>();
31+
32+
const currentValue = useMemo(
33+
() => ({
34+
activePageRef,
35+
setActivePage: (page: Page) => (activePageRef.current = page),
36+
}),
37+
[]
38+
);
3039

3140
return (
32-
<AppContext.Provider value={{ activePage, setActivePage }}>
33-
{children}
34-
</AppContext.Provider>
41+
<AppContext.Provider value={currentValue}>{children}</AppContext.Provider>
3542
);
3643
}
3744

3845
export function useSetActivePage(page?: Page, enabled = true) {
39-
const { activePage, setActivePage } = useContext(AppContext);
46+
const { activePageRef, setActivePage } = useContext(AppContext);
4047

4148
useEffect(() => {
4249
if (!enabled) return;
@@ -55,12 +62,12 @@ export function useSetActivePage(page?: Page, enabled = true) {
5562
if (!enabled) return;
5663
if (!page) return;
5764

58-
if (!activePage) {
65+
if (!activePageRef?.current) {
5966
setActivePage(page);
6067
return;
6168
}
6269

63-
const current = activePage.current;
70+
const current = activePageRef.current?.current;
6471

6572
if (current && "querySelector" in current) {
6673
if (current.classList.contains("ion-page-hidden")) {

‎src/features/auth/PageContext.tsx

+31-18
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { useIonModal } from "@ionic/react";
22
import React, {
3+
RefObject,
34
createContext,
45
useCallback,
56
useEffect,
7+
useMemo,
68
useRef,
79
useState,
810
} from "react";
@@ -19,7 +21,7 @@ import SelectTextModal from "../../pages/shared/SelectTextModal";
1921

2022
interface IPageContext {
2123
// used for ion presentingElement
22-
page: HTMLElement | undefined;
24+
pageRef: RefObject<HTMLElement | undefined> | undefined;
2325

2426
/**
2527
* @returns true if login dialog was presented
@@ -51,7 +53,7 @@ interface IPageContext {
5153
}
5254

5355
export const PageContext = createContext<IPageContext>({
54-
page: undefined,
56+
pageRef: undefined,
5557
presentLoginIfNeeded: () => false,
5658
presentCommentReply: async () => undefined,
5759
presentCommentEdit: () => false,
@@ -61,7 +63,7 @@ export const PageContext = createContext<IPageContext>({
6163
});
6264

6365
interface PageContextProvider {
64-
value: Pick<IPageContext, "page">;
66+
value: Pick<IPageContext, "pageRef">;
6567
children: React.ReactNode;
6668
}
6769

@@ -75,9 +77,9 @@ export function PageContextProvider({ value, children }: PageContextProvider) {
7577
const presentLoginIfNeeded = useCallback(() => {
7678
if (jwt) return false;
7779

78-
presentLogin({ presentingElement: value.page });
80+
presentLogin({ presentingElement: value.pageRef?.current ?? undefined });
7981
return true;
80-
}, [jwt, presentLogin, value.page]);
82+
}, [jwt, presentLogin, value.pageRef]);
8183

8284
// Comment reply start
8385
const commentReplyItem = useRef<CommentReplyItem>();
@@ -135,22 +137,33 @@ export function PageContextProvider({ value, children }: PageContextProvider) {
135137
}, []);
136138
// Select text end
137139

138-
const presentReport = (item: ReportableItem) => {
140+
const presentReport = useCallback((item: ReportableItem) => {
139141
reportRef.current?.present(item);
140-
};
142+
}, []);
143+
144+
const currentValue = useMemo(
145+
() => ({
146+
...value,
147+
presentLoginIfNeeded,
148+
presentCommentReply,
149+
presentCommentEdit,
150+
presentReport,
151+
presentPostEditor,
152+
presentSelectText,
153+
}),
154+
[
155+
presentCommentEdit,
156+
presentCommentReply,
157+
presentLoginIfNeeded,
158+
presentPostEditor,
159+
presentReport,
160+
presentSelectText,
161+
value,
162+
]
163+
);
141164

142165
return (
143-
<PageContext.Provider
144-
value={{
145-
...value,
146-
presentLoginIfNeeded,
147-
presentCommentReply,
148-
presentCommentEdit,
149-
presentReport,
150-
presentPostEditor,
151-
presentSelectText,
152-
}}
153-
>
166+
<PageContext.Provider value={currentValue}>
154167
{children}
155168

156169
<CommentReplyModal
+151-166
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import styled from "@emotion/styled";
22
import {
3-
IonActionSheet,
43
IonIcon,
54
useIonActionSheet,
65
useIonRouter,
@@ -21,7 +20,7 @@ import {
2120
trashOutline,
2221
} from "ionicons/icons";
2322
import { CommentView } from "lemmy-js-client";
24-
import { useContext, useState } from "react";
23+
import { useContext } from "react";
2524
import { notEmpty } from "../../helpers/array";
2625
import {
2726
getHandle,
@@ -56,7 +55,6 @@ export default function MoreActions({
5655
}: MoreActionsProps) {
5756
const buildGeneralBrowseLink = useBuildGeneralBrowseLink();
5857
const dispatch = useAppDispatch();
59-
const [open, setOpen] = useState(false);
6058
const { prependComments } = useContext(CommentsContext);
6159
const myHandle = useAppSelector(handleSelector);
6260
const [present] = useIonToast();
@@ -92,195 +90,182 @@ export default function MoreActions({
9290
const isMyComment = getRemoteHandle(commentView.creator) === myHandle;
9391
const commentExists = !comment.deleted && !comment.removed;
9492

95-
return (
96-
<>
97-
<StyledIonIcon
98-
icon={ellipsisHorizontal}
99-
onClick={(e) => {
100-
setOpen(true);
101-
e.stopPropagation();
102-
}}
103-
/>
104-
105-
<IonActionSheet
106-
cssClass="left-align-buttons"
107-
onClick={(e) => e.stopPropagation()}
108-
isOpen={open}
109-
buttons={[
110-
{
111-
text: myVote !== 1 ? "Upvote" : "Undo Upvote",
112-
role: "upvote",
113-
icon: arrowUpOutline,
114-
},
115-
downvoteAllowed
116-
? {
117-
text: myVote !== -1 ? "Downvote" : "Undo Downvote",
118-
role: "downvote",
119-
icon: arrowDownOutline,
120-
}
121-
: undefined,
122-
{
123-
text: !mySaved ? "Save" : "Unsave",
124-
role: "save",
125-
icon: bookmarkOutline,
126-
},
127-
isMyComment && isCommentMutable(comment)
128-
? {
129-
text: "Edit",
130-
role: "edit",
131-
icon: pencilOutline,
132-
}
133-
: undefined,
134-
isMyComment && isCommentMutable(comment)
135-
? {
136-
text: "Delete",
137-
role: "delete",
138-
icon: trashOutline,
139-
}
140-
: undefined,
141-
{
142-
text: "Reply",
143-
role: "reply",
144-
icon: arrowUndoOutline,
145-
},
146-
commentExists && comment.content
147-
? {
148-
text: "Select Text",
149-
role: "select-text",
150-
icon: textOutline,
151-
}
152-
: undefined,
153-
{
154-
text: getHandle(commentView.creator),
155-
role: "person",
156-
icon: personOutline,
157-
},
158-
{
159-
text: "Share",
160-
role: "share",
161-
icon: shareOutline,
162-
},
163-
rootIndex !== undefined
164-
? {
165-
text: "Collapse to Top",
166-
role: "collapse",
167-
icon: chevronCollapseOutline,
168-
}
169-
: undefined,
170-
{
171-
text: "Report",
172-
role: "report",
173-
icon: flagOutline,
174-
},
175-
{
176-
text: "Cancel",
177-
role: "cancel",
178-
},
179-
].filter(notEmpty)}
180-
onDidDismiss={() => setOpen(false)}
181-
onWillDismiss={async (e) => {
182-
switch (e.detail.role) {
183-
case "upvote":
93+
function onClick() {
94+
presentActionSheet({
95+
cssClass: "left-align-buttons",
96+
buttons: [
97+
{
98+
text: myVote !== 1 ? "Upvote" : "Undo Upvote",
99+
icon: arrowUpOutline,
100+
handler: () => {
101+
(async () => {
184102
if (presentLoginIfNeeded()) return;
185103

186104
try {
187105
await dispatch(voteOnComment(comment.id, myVote === 1 ? 0 : 1));
188106
} catch (error) {
189107
present(voteError);
190108
}
109+
})();
110+
},
111+
},
112+
downvoteAllowed
113+
? {
114+
text: myVote !== -1 ? "Downvote" : "Undo Downvote",
115+
icon: arrowDownOutline,
116+
handler: () => {
117+
(async () => {
118+
if (presentLoginIfNeeded()) return;
191119

192-
break;
193-
case "downvote":
194-
if (presentLoginIfNeeded()) return;
195-
196-
try {
197-
await dispatch(
198-
voteOnComment(comment.id, myVote === -1 ? 0 : -1)
199-
);
200-
} catch (error) {
201-
present(voteError);
202-
}
203-
204-
break;
205-
case "save":
120+
try {
121+
await dispatch(
122+
voteOnComment(comment.id, myVote === -1 ? 0 : -1)
123+
);
124+
} catch (error) {
125+
present(voteError);
126+
}
127+
})();
128+
},
129+
}
130+
: undefined,
131+
{
132+
text: !mySaved ? "Save" : "Unsave",
133+
icon: bookmarkOutline,
134+
handler: () => {
135+
(async () => {
206136
if (presentLoginIfNeeded()) return;
207137

208138
try {
209139
await dispatch(saveComment(comment.id, !mySaved));
210140
} catch (error) {
211141
present(saveError);
212142
}
213-
break;
214-
case "edit": {
215-
presentCommentEdit(comment);
216-
break;
143+
})();
144+
},
145+
},
146+
isMyComment && isCommentMutable(comment)
147+
? {
148+
text: "Edit",
149+
icon: pencilOutline,
150+
handler: () => {
151+
presentCommentEdit(comment);
152+
},
217153
}
218-
case "delete":
219-
presentActionSheet({
220-
buttons: [
221-
{
222-
text: "Delete Comment",
223-
role: "destructive",
224-
handler: () => {
225-
(async () => {
226-
try {
227-
await dispatch(deleteComment(comment.id));
228-
} catch (error) {
154+
: undefined,
155+
isMyComment && isCommentMutable(comment)
156+
? {
157+
text: "Delete",
158+
icon: trashOutline,
159+
handler: () => {
160+
presentActionSheet({
161+
buttons: [
162+
{
163+
text: "Delete Comment",
164+
role: "destructive",
165+
handler: () => {
166+
(async () => {
167+
try {
168+
await dispatch(deleteComment(comment.id));
169+
} catch (error) {
170+
present({
171+
message:
172+
"Problem deleting comment. Please try again.",
173+
duration: 3500,
174+
position: "bottom",
175+
color: "danger",
176+
});
177+
178+
throw error;
179+
}
180+
229181
present({
230-
message:
231-
"Problem deleting comment. Please try again.",
182+
message: "Comment deleted!",
232183
duration: 3500,
233184
position: "bottom",
234-
color: "danger",
185+
color: "primary",
235186
});
236-
237-
throw error;
238-
}
239-
240-
present({
241-
message: "Comment deleted!",
242-
duration: 3500,
243-
position: "bottom",
244-
color: "primary",
245-
});
246-
})();
187+
})();
188+
},
247189
},
248-
},
249-
{
250-
text: "Cancel",
251-
role: "cancel",
252-
},
253-
],
254-
});
255-
256-
break;
257-
case "reply": {
190+
{
191+
text: "Cancel",
192+
role: "cancel",
193+
},
194+
],
195+
});
196+
},
197+
}
198+
: undefined,
199+
{
200+
text: "Reply",
201+
icon: arrowUndoOutline,
202+
handler: () => {
203+
(async () => {
258204
if (presentLoginIfNeeded()) return;
259205

260206
const reply = await presentCommentReply(commentView);
261207

262208
if (reply) prependComments([reply]);
263-
break;
209+
})();
210+
},
211+
},
212+
commentExists && comment.content
213+
? {
214+
text: "Select Text",
215+
icon: textOutline,
216+
handler: () => {
217+
presentSelectText(comment.content);
218+
},
264219
}
265-
case "select-text":
266-
return presentSelectText(comment.content);
267-
case "person":
268-
router.push(
269-
buildGeneralBrowseLink(`/u/${getHandle(commentView.creator)}`)
270-
);
271-
break;
272-
case "share":
273-
share(comment);
274-
break;
275-
case "collapse":
276-
collapseRootComment();
277-
break;
278-
case "report":
279-
presentReport(commentView);
280-
break;
281-
}
282-
}}
283-
/>
284-
</>
220+
: undefined,
221+
{
222+
text: getHandle(commentView.creator),
223+
icon: personOutline,
224+
handler: () => {
225+
router.push(
226+
buildGeneralBrowseLink(`/u/${getHandle(commentView.creator)}`)
227+
);
228+
},
229+
},
230+
{
231+
text: "Share",
232+
icon: shareOutline,
233+
handler: () => {
234+
share(comment);
235+
},
236+
},
237+
rootIndex !== undefined
238+
? {
239+
text: "Collapse to Top",
240+
icon: chevronCollapseOutline,
241+
handler: () => {
242+
collapseRootComment();
243+
},
244+
}
245+
: undefined,
246+
{
247+
text: "Report",
248+
role: "report",
249+
icon: flagOutline,
250+
handler: () => {
251+
presentReport(commentView);
252+
},
253+
},
254+
{
255+
text: "Cancel",
256+
role: "cancel",
257+
},
258+
].filter(notEmpty),
259+
});
260+
}
261+
262+
return (
263+
<StyledIonIcon
264+
icon={ellipsisHorizontal}
265+
onClick={(e) => {
266+
onClick();
267+
e.stopPropagation();
268+
}}
269+
/>
285270
);
286271
}

‎src/features/comment/CommentSort.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ interface CommentSortProps {
3535

3636
export default function CommentSort({ sort, setSort }: CommentSortProps) {
3737
const [open, setOpen] = useState(false);
38-
const { activePage } = useContext(AppContext);
38+
const { activePageRef } = useContext(AppContext);
3939

4040
return (
4141
<>
@@ -53,7 +53,7 @@ export default function CommentSort({ sort, setSort }: CommentSortProps) {
5353
setSort(e.detail.data);
5454
}
5555

56-
scrollUpIfNeeded(activePage, 1, "auto");
56+
scrollUpIfNeeded(activePageRef?.current, 1, "auto");
5757
}}
5858
header="Sort by..."
5959
buttons={BUTTONS.map((b) => ({

‎src/features/comment/useCollapseRootComment.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export default function useCollapseRootComment(
99
rootIndex: number | undefined
1010
) {
1111
const dispatch = useAppDispatch();
12-
const { activePage } = useContext(AppContext);
12+
const { activePageRef } = useContext(AppContext);
1313

1414
return useCallback(() => {
1515
if (!item || !rootIndex) return;
@@ -18,12 +18,12 @@ export default function useCollapseRootComment(
1818

1919
dispatch(toggleCommentCollapseState(rootCommentId));
2020

21-
const currentActivePage = activePage?.current;
21+
const currentActivePage = activePageRef?.current?.current;
2222
if (!currentActivePage || !("scrollToIndex" in currentActivePage)) return;
2323

2424
currentActivePage.scrollToIndex({
2525
index: rootIndex,
2626
behavior: "smooth",
2727
});
28-
}, [activePage, dispatch, item, rootIndex]);
28+
}, [activePageRef, dispatch, item, rootIndex]);
2929
}

‎src/features/feed/Feed.tsx

+16-14
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import React, {
66
useRef,
77
useState,
88
} from "react";
9-
import { Virtuoso, VirtuosoHandle, VirtuosoProps } from "react-virtuoso";
9+
import { Virtuoso, VirtuosoHandle } from "react-virtuoso";
1010
import {
1111
IonRefresher,
1212
IonRefresherContent,
@@ -159,13 +159,19 @@ export default function Feed<I>({
159159
}
160160
}
161161

162-
// TODO looks like a Virtuoso bug where virtuoso checks if computeItemKey exists,
163-
// not if it's not undefined (needs report)
164-
const computeProp: Partial<VirtuosoProps<unknown, unknown>> = getIndex
165-
? {
166-
computeItemKey: (index) => getIndex(filteredItems[index]),
167-
}
168-
: {};
162+
const itemContent = useCallback(
163+
(index: number) => {
164+
const item = filteredItems[index];
165+
166+
return renderItemContent(item);
167+
},
168+
[filteredItems, renderItemContent]
169+
);
170+
171+
const computeItemKey = useCallback(
172+
(index: number) => (getIndex ? getIndex(filteredItems[index]) : index),
173+
[filteredItems, getIndex]
174+
);
169175

170176
if ((loading && !filteredItems.length) || loading === undefined)
171177
return <CenteredSpinner />;
@@ -187,13 +193,9 @@ export default function Feed<I>({
187193
ref={virtuosoRef}
188194
style={{ height: "100%" }}
189195
atTopStateChange={setIsListAtTop}
190-
{...computeProp}
196+
computeItemKey={computeItemKey}
191197
totalCount={filteredItems.length}
192-
itemContent={(index) => {
193-
const item = filteredItems[index];
194-
195-
return renderItemContent(item);
196-
}}
198+
itemContent={itemContent}
197199
components={{ Header: header, Footer: footer }}
198200
onScroll={onScroll}
199201
increaseViewportBy={

‎src/features/feed/PostCommentFeed.tsx

+7-5
Original file line numberDiff line numberDiff line change
@@ -111,15 +111,17 @@ export default function PostCommentFeed({
111111
[postHiddenById]
112112
);
113113

114+
const getIndex = useCallback(
115+
(item: PostCommentItem) =>
116+
"comment" in item ? `comment-${item.comment.id}` : `post-${item.post.id}`,
117+
[]
118+
);
119+
114120
return (
115121
<Feed
116122
fetchFn={fetchFn}
117123
filterFn={filterHiddenPosts ? filterFn : undefined}
118-
getIndex={(item) =>
119-
"comment" in item
120-
? `comment-${item.comment.id}`
121-
: `post-${item.post.id}`
122-
}
124+
getIndex={getIndex}
123125
renderItemContent={renderItemContent}
124126
{...rest}
125127
itemsRef={itemsRef}

‎src/features/feed/PostSort.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ export default function PostSort() {
7676
const sort = useAppSelector((state) => state.post.sort);
7777
const [open, setOpen] = useState(false);
7878
const [topOpen, setTopOpen] = useState(false);
79-
const { activePage } = useContext(AppContext);
79+
const { activePageRef } = useContext(AppContext);
8080

8181
return (
8282
<>
@@ -98,7 +98,7 @@ export default function PostSort() {
9898
dispatch(updateSortType(e.detail.data));
9999
}
100100

101-
scrollUpIfNeeded(activePage, 0, "auto");
101+
scrollUpIfNeeded(activePageRef?.current, 0, "auto");
102102
}}
103103
header="Sort by..."
104104
buttons={BUTTONS.map((b) => ({
@@ -123,7 +123,7 @@ export default function PostSort() {
123123
) => {
124124
if (e.detail.data) {
125125
dispatch(updateSortType(e.detail.data));
126-
scrollUpIfNeeded(activePage, 0, "auto");
126+
scrollUpIfNeeded(activePageRef?.current, 0, "auto");
127127
}
128128
}}
129129
header="Sort by Top for..."

‎src/features/feed/useHidePosts.tsx

+5-5
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@ import { hidePosts } from "../post/postSlice";
77
export default function useHidePosts() {
88
const dispatch = useAppDispatch();
99
const { itemsRef } = useContext(FeedContext);
10-
const { activePage } = useContext(AppContext);
10+
const { activePageRef } = useContext(AppContext);
1111
const postReadById = useAppSelector((state) => state.post.postReadById);
1212

1313
const onHide = useCallback(async () => {
14-
if (!activePage?.current) return;
15-
if ("querySelector" in activePage.current) return;
14+
if (!activePageRef?.current?.current) return;
15+
if ("querySelector" in activePageRef.current.current) return;
1616

1717
const postIds: number[] | undefined = itemsRef?.current?.map(
1818
(item) => item.post.id
@@ -24,8 +24,8 @@ export default function useHidePosts() {
2424

2525
await dispatch(hidePosts(toHide));
2626

27-
activePage.current.scrollToIndex({ index: 0, behavior: "auto" });
28-
}, [activePage, dispatch, itemsRef, postReadById]);
27+
activePageRef.current.current.scrollToIndex({ index: 0, behavior: "auto" });
28+
}, [activePageRef, dispatch, itemsRef, postReadById]);
2929

3030
return onHide;
3131
}
+112-158
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import {
2-
IonActionSheet,
32
IonButton,
43
IonIcon,
54
useIonActionSheet,
@@ -22,7 +21,7 @@ import {
2221
textOutline,
2322
trashOutline,
2423
} from "ionicons/icons";
25-
import { useContext, useMemo, useState } from "react";
24+
import { useContext } from "react";
2625
import { useAppDispatch, useAppSelector } from "../../../store";
2726
import { PostView } from "lemmy-js-client";
2827
import {
@@ -59,7 +58,6 @@ export default function MoreActions({
5958
const [present] = useIonToast();
6059
const buildGeneralBrowseLink = useBuildGeneralBrowseLink();
6160
const dispatch = useAppDispatch();
62-
const [open, setOpen] = useState(false);
6361
const isHidden = useAppSelector(postHiddenByIdSelector)[post.post.id];
6462
const myHandle = useAppSelector(handleSelector);
6563

@@ -82,96 +80,181 @@ export default function MoreActions({
8280
const isMyPost = getRemoteHandle(post.creator) === myHandle;
8381
const downvoteAllowed = useAppSelector(isDownvoteEnabledSelector);
8482

85-
const buttons = useMemo(
86-
() =>
87-
[
83+
function onClick() {
84+
presentActionSheet({
85+
cssClass: "left-align-buttons",
86+
buttons: [
8887
{
8988
text: myVote !== 1 ? "Upvote" : "Undo Upvote",
90-
data: "upvote",
9189
icon: arrowUpOutline,
90+
handler: () => {
91+
(async () => {
92+
if (presentLoginIfNeeded()) return;
93+
94+
try {
95+
await dispatch(voteOnPost(post.post.id, myVote === 1 ? 0 : 1));
96+
} catch (error) {
97+
present(voteError);
98+
99+
throw error;
100+
}
101+
})();
102+
},
92103
},
93104
downvoteAllowed
94105
? {
95106
text: myVote !== -1 ? "Downvote" : "Undo Downvote",
96-
data: "downvote",
97107
icon: arrowDownOutline,
108+
handler: () => {
109+
(async () => {
110+
if (presentLoginIfNeeded()) return;
111+
112+
try {
113+
await dispatch(
114+
voteOnPost(post.post.id, myVote === -1 ? 0 : -1)
115+
);
116+
} catch (error) {
117+
present(voteError);
118+
119+
throw error;
120+
}
121+
})();
122+
},
98123
}
99124
: undefined,
100125
{
101126
text: !mySaved ? "Save" : "Unsave",
102-
data: "save",
103127
icon: bookmarkOutline,
128+
handler: () => {
129+
(async () => {
130+
if (presentLoginIfNeeded()) return;
131+
132+
try {
133+
await dispatch(savePost(post.post.id, !mySaved));
134+
} catch (error) {
135+
present(saveError);
136+
137+
throw error;
138+
}
139+
})();
140+
},
104141
},
105142
isMyPost
106143
? {
107144
text: "Delete",
108-
data: "delete",
109145
icon: trashOutline,
146+
handler: () => {
147+
presentActionSheet({
148+
buttons: [
149+
{
150+
text: "Delete Post",
151+
role: "destructive",
152+
handler: () => {
153+
(async () => {
154+
await dispatch(deletePost(post.post.id));
155+
156+
present({
157+
message: "Post deleted",
158+
duration: 3500,
159+
position: "bottom",
160+
color: "success",
161+
});
162+
})();
163+
},
164+
},
165+
{
166+
text: "Cancel",
167+
role: "cancel",
168+
},
169+
],
170+
});
171+
},
110172
}
111173
: undefined,
112174
isMyPost
113175
? {
114176
text: "Edit",
115-
data: "edit",
116177
icon: pencilOutline,
178+
handler: () => {
179+
presentPostEditor(post);
180+
},
117181
}
118182
: undefined,
119183
{
120184
text: "Reply",
121-
data: "reply",
122185
icon: arrowUndoOutline,
186+
handler: () => {
187+
if (presentLoginIfNeeded()) return;
188+
189+
// Not viewing comments, so no feed update
190+
presentCommentReply(post);
191+
},
123192
},
124193
{
125194
text: getHandle(post.creator),
126-
data: "person",
127195
icon: personOutline,
196+
handler: () => {
197+
router.push(
198+
buildGeneralBrowseLink(`/u/${getHandle(post.creator)}`)
199+
);
200+
},
128201
},
129202
{
130203
text: getHandle(post.community),
131-
data: "community",
132204
icon: peopleOutline,
205+
handler: () => {
206+
router.push(
207+
buildGeneralBrowseLink(`/c/${getHandle(post.community)}`)
208+
);
209+
},
133210
},
134211
post.post.body
135212
? {
136213
text: "Select Text",
137-
data: "select",
138214
icon: textOutline,
215+
handler: () => {
216+
if (!post.post.body) return;
217+
218+
presentSelectText(post.post.body);
219+
},
139220
}
140221
: undefined,
141222
onFeed
142223
? {
143224
text: isHidden ? "Unhide" : "Hide",
144-
data: isHidden ? "unhide" : "hide",
145225
icon: isHidden ? eyeOutline : eyeOffOutline,
226+
handler: () => {
227+
if (presentLoginIfNeeded()) return;
228+
229+
const fn = isHidden ? unhidePost : hidePost;
230+
231+
dispatch(fn(post.post.id));
232+
},
146233
}
147234
: undefined,
148235
{
149236
text: "Share",
150237
data: "share",
151238
icon: shareOutline,
239+
handler: () => {
240+
share(post.post);
241+
},
152242
},
153243
{
154244
text: "Report",
155245
data: "report",
156246
icon: flagOutline,
247+
handler: () => {
248+
presentReport(post);
249+
},
157250
},
158251
{
159252
text: "Cancel",
160253
role: "cancel",
161254
},
162255
].filter(notEmpty),
163-
[
164-
downvoteAllowed,
165-
isHidden,
166-
isMyPost,
167-
mySaved,
168-
myVote,
169-
onFeed,
170-
post.community,
171-
post.creator,
172-
post.post.body,
173-
]
174-
);
256+
});
257+
}
175258

176259
const Button = onFeed ? ActionButton : IonButton;
177260

@@ -180,140 +263,11 @@ export default function MoreActions({
180263
<Button
181264
onClick={(e) => {
182265
e.stopPropagation();
183-
setOpen(true);
266+
onClick();
184267
}}
185268
>
186269
<IonIcon className={className} icon={ellipsisHorizontal} />
187270
</Button>
188-
<IonActionSheet
189-
cssClass="left-align-buttons"
190-
isOpen={open}
191-
buttons={buttons}
192-
onClick={(e) => e.stopPropagation()}
193-
onDidDismiss={() => setOpen(false)}
194-
onWillDismiss={async (e) => {
195-
switch (e.detail.data) {
196-
case "upvote": {
197-
if (presentLoginIfNeeded()) return;
198-
199-
try {
200-
await dispatch(voteOnPost(post.post.id, myVote === 1 ? 0 : 1));
201-
} catch (error) {
202-
present(voteError);
203-
204-
throw error;
205-
}
206-
break;
207-
}
208-
case "downvote": {
209-
if (presentLoginIfNeeded()) return;
210-
211-
try {
212-
await dispatch(
213-
voteOnPost(post.post.id, myVote === -1 ? 0 : -1)
214-
);
215-
} catch (error) {
216-
present(voteError);
217-
218-
throw error;
219-
}
220-
break;
221-
}
222-
case "save": {
223-
if (presentLoginIfNeeded()) return;
224-
225-
try {
226-
await dispatch(savePost(post.post.id, !mySaved));
227-
} catch (error) {
228-
present(saveError);
229-
230-
throw error;
231-
}
232-
break;
233-
}
234-
case "reply": {
235-
if (presentLoginIfNeeded()) return;
236-
237-
// Not viewing comments, so no feed update
238-
presentCommentReply(post);
239-
240-
break;
241-
}
242-
case "person": {
243-
router.push(
244-
buildGeneralBrowseLink(`/u/${getHandle(post.creator)}`)
245-
);
246-
247-
break;
248-
}
249-
case "community": {
250-
router.push(
251-
buildGeneralBrowseLink(`/c/${getHandle(post.community)}`)
252-
);
253-
254-
break;
255-
}
256-
case "select": {
257-
if (!post.post.body) break;
258-
259-
return presentSelectText(post.post.body);
260-
}
261-
case "hide": {
262-
if (presentLoginIfNeeded()) return;
263-
264-
dispatch(hidePost(post.post.id));
265-
266-
break;
267-
}
268-
case "unhide": {
269-
if (presentLoginIfNeeded()) return;
270-
271-
dispatch(unhidePost(post.post.id));
272-
273-
break;
274-
}
275-
case "share": {
276-
share(post.post);
277-
278-
break;
279-
}
280-
case "report": {
281-
presentReport(post);
282-
break;
283-
}
284-
case "delete": {
285-
presentActionSheet({
286-
buttons: [
287-
{
288-
text: "Delete Post",
289-
role: "destructive",
290-
handler: () => {
291-
(async () => {
292-
await dispatch(deletePost(post.post.id));
293-
294-
present({
295-
message: "Post deleted",
296-
duration: 3500,
297-
position: "bottom",
298-
color: "success",
299-
});
300-
})();
301-
},
302-
},
303-
{
304-
text: "Cancel",
305-
role: "cancel",
306-
},
307-
],
308-
});
309-
break;
310-
}
311-
case "edit": {
312-
presentPostEditor(post);
313-
}
314-
}
315-
}}
316-
/>
317271
</>
318272
);
319273
}

‎src/features/post/shared/SaveButton.tsx

+3-9
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import styled from "@emotion/styled";
2-
import { IonIcon, useIonModal, useIonToast } from "@ionic/react";
3-
import Login from "../../auth/Login";
2+
import { IonIcon, useIonToast } from "@ionic/react";
43
import { MouseEvent, useContext } from "react";
54
import { PageContext } from "../../auth/PageContext";
65
import { useAppDispatch, useAppSelector } from "../../../store";
@@ -9,7 +8,6 @@ import { css } from "@emotion/react";
98
import { bookmarkOutline } from "ionicons/icons";
109
import { ActionButton } from "../actions/ActionButton";
1110
import { saveError } from "../../../helpers/toastMessages";
12-
import { jwtSelector } from "../../auth/authSlice";
1311
import { ImpactStyle } from "@capacitor/haptics";
1412
import useHapticFeedback from "../../../helpers/useHapticFeedback";
1513

@@ -34,12 +32,8 @@ interface SaveButtonProps {
3432
export function SaveButton({ postId }: SaveButtonProps) {
3533
const [present] = useIonToast();
3634
const dispatch = useAppDispatch();
37-
const pageContext = useContext(PageContext);
35+
const { presentLoginIfNeeded } = useContext(PageContext);
3836
const vibrate = useHapticFeedback();
39-
const [login, onDismiss] = useIonModal(Login, {
40-
onDismiss: (data: string, role: string) => onDismiss(data, role),
41-
});
42-
const jwt = useAppSelector(jwtSelector);
4337

4438
const postSavedById = useAppSelector((state) => state.post.postSavedById);
4539
const mySaved = postSavedById[postId];
@@ -49,7 +43,7 @@ export function SaveButton({ postId }: SaveButtonProps) {
4943

5044
vibrate({ style: ImpactStyle.Light });
5145

52-
if (!jwt) return login({ presentingElement: pageContext.page });
46+
if (presentLoginIfNeeded()) return;
5347

5448
try {
5549
await dispatch(savePost(postId, !postSavedById[postId]));

‎src/features/shared/DynamicDismissableModal.tsx

+16-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import { useIonActionSheet } from "@ionic/react";
99
import { PageContext } from "../auth/PageContext";
1010
import { Prompt, useLocation } from "react-router";
1111
import IonModalAutosizedForOnScreenKeyboard from "./IonModalAutosizedForOnScreenKeyboard";
12+
import { useAppSelector } from "../../store";
13+
import { jwtIssSelector } from "../auth/authSlice";
1214

1315
export interface DismissableProps {
1416
dismiss: () => void;
@@ -29,12 +31,25 @@ export function DynamicDismissableModal({
2931
}: DynamicDismissableModalProps) {
3032
const pageContext = useContext(PageContext);
3133
const location = useLocation();
34+
const iss = useAppSelector(jwtIssSelector);
3235

3336
const [canDismiss, setCanDismiss] = useState(true);
3437
const canDismissRef = useRef(canDismiss);
3538

3639
const [presentActionSheet] = useIonActionSheet();
3740

41+
const [presentingElement, setPresentingElement] = useState<
42+
HTMLElement | undefined
43+
>();
44+
45+
useEffect(() => {
46+
setPresentingElement(pageContext.pageRef?.current ?? undefined);
47+
48+
// In <TabbedRoutes>, <IonRouterOutlet> rebuilds (see `key`) when iss changes,
49+
// so grab new IonRouterOutlet
50+
// eslint-disable-next-line react-hooks/exhaustive-deps
51+
}, [pageContext.pageRef, iss]);
52+
3853
const onDismissAttemptCb = useCallback(async () => {
3954
await presentActionSheet([
4055
{
@@ -87,7 +102,7 @@ export function DynamicDismissableModal({
87102
isOpen={isOpen}
88103
canDismiss={canDismiss ? canDismiss : onDismissAttemptCb}
89104
onDidDismiss={() => setIsOpen(false)}
90-
presentingElement={pageContext.page}
105+
presentingElement={presentingElement}
91106
>
92107
{renderModalContents({
93108
setCanDismiss,

‎src/helpers/routes.ts

+10-4
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
1-
import { useLocation } from "react-router";
21
import { useAppSelector } from "../store";
2+
import { useCallback, useContext } from "react";
3+
import { TabContext } from "../TabContext";
34

45
export function useBuildGeneralBrowseLink() {
5-
const location = useLocation();
6+
const { tab } = useContext(TabContext);
67
const connectedServer = useAppSelector(
78
(state) => state.auth.connectedInstance
89
);
910

10-
const tab = location.pathname.split("/")[1];
11+
const buildGeneralBrowseLink = useCallback(
12+
(path: string) => `/${tab}/${connectedServer}${path}`,
13+
// tab should never dynamically change for a rendered buildGeneralBrowseLink tab. So don't re-render
14+
// eslint-disable-next-line react-hooks/exhaustive-deps
15+
[connectedServer]
16+
);
1117

12-
return (path: string) => `/${tab}/${connectedServer}${path}`;
18+
return buildGeneralBrowseLink;
1319
}

‎src/helpers/scrollUpIfNeeded.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Page } from "../features/auth/AppContext";
22

33
export async function scrollUpIfNeeded(
4-
activePage: Page | undefined,
4+
activePage: Page | null | undefined,
55
index: number | undefined = undefined,
66
behavior: "auto" | "smooth" = "smooth"
77
) {

‎src/helpers/useKeyboardOpen.ts

+9-2
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,15 @@ import BooleanWatcher from "./BooleanWatcher";
55

66
const keyboardWatcher = new BooleanWatcher(false);
77

8-
Keyboard.addListener("keyboardWillShow", () => keyboardWatcher.setValue(true));
9-
Keyboard.addListener("keyboardWillShow", () => keyboardWatcher.setValue(false));
8+
if (isNative()) {
9+
Keyboard.addListener("keyboardWillShow", () =>
10+
keyboardWatcher.setValue(true)
11+
);
12+
13+
Keyboard.addListener("keyboardWillShow", () =>
14+
keyboardWatcher.setValue(false)
15+
);
16+
}
1017

1118
export default function useKeyboardOpen() {
1219
const [keyboardOpen, setKeyboardOpen] = useState(

‎src/pages/inbox/ConversationPage.tsx

+12-6
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,20 @@ import {
1212
useIonViewWillEnter,
1313
} from "@ionic/react";
1414
import { useAppDispatch, useAppSelector } from "../../store";
15-
import { KeyboardEvent, useEffect, useMemo, useRef, useState } from "react";
15+
import {
16+
KeyboardEvent,
17+
useContext,
18+
useEffect,
19+
useMemo,
20+
useRef,
21+
useState,
22+
} from "react";
1623
import { jwtPayloadSelector, jwtSelector } from "../../features/auth/authSlice";
1724
import {
1825
receivedMessages,
1926
syncMessages,
2027
} from "../../features/inbox/inboxSlice";
21-
import { useLocation, useParams } from "react-router";
28+
import { useParams } from "react-router";
2229
import { getHandle } from "../../helpers/lemmy";
2330
import Message from "../../features/inbox/messages/Message";
2431
import styled from "@emotion/styled";
@@ -36,6 +43,7 @@ import { PageContentIonSpinner } from "../../features/user/AsyncProfile";
3643
import { StyledLink } from "../../features/labels/links/shared";
3744
import { useBuildGeneralBrowseLink } from "../../helpers/routes";
3845
import ConversationsMoreActions from "../../features/feed/ConversationsMoreActions";
46+
import { TabContext } from "../../TabContext";
3947

4048
const MaxSizeContainer = styled(MaxWidthContainer)`
4149
height: 100%;
@@ -118,7 +126,7 @@ export default function ConversationPage() {
118126
const dispatch = useAppDispatch();
119127
const allMessages = useAppSelector((state) => state.inbox.messages);
120128
const jwtPayload = useAppSelector(jwtPayloadSelector);
121-
const location = useLocation();
129+
const { tab } = useContext(TabContext);
122130
const jwt = useAppSelector(jwtSelector);
123131
const myUserId = useAppSelector(
124132
(state) => state.auth.site?.my_user?.local_user_view?.local_user?.person_id
@@ -215,9 +223,7 @@ export default function ConversationPage() {
215223
<IonButtons slot="start">
216224
<IonBackButton
217225
defaultHref="/inbox/messages"
218-
text={
219-
location.pathname.startsWith("/inbox") ? "Messages" : "Back"
220-
}
226+
text={tab === "inbox" ? "Messages" : "Back"}
221227
/>
222228
</IonButtons>
223229

‎src/pages/profile/ProfilePage.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,14 @@ import FeedContent from "../shared/FeedContent";
1919

2020
export default function ProfilePage() {
2121
const handle = useAppSelector(handleSelector);
22-
const { page, presentLoginIfNeeded } = useContext(PageContext);
22+
const { pageRef, presentLoginIfNeeded } = useContext(PageContext);
2323

2424
const [presentAccountSwitcher, onDismissAccountSwitcher] = useIonModal(
2525
AccountSwitcher,
2626
{
2727
onDismiss: (data: string, role: string) =>
2828
onDismissAccountSwitcher(data, role),
29-
page,
29+
pageRef,
3030
}
3131
);
3232

0 commit comments

Comments
 (0)
Please sign in to comment.