tangled
alpha
login
or
join now
leaflet.pub
/
leaflet
289
fork
atom
a tool for shared writing and social publishing
289
fork
atom
overview
issues
27
pulls
pipelines
extract out page ui state helpers
awarm.space
1 week ago
e89e279a
42795c92
+161
-153
7 changed files
expand all
collapse all
unified
split
app
lish
[did]
[publication]
[rkey]
Blocks
PublishedPageBlock.tsx
BlueskyQuotesPage.tsx
BskyPostContent.tsx
Interactions
Quotes.tsx
PostLinks.tsx
PostPages.tsx
postPageState.ts
+2
-5
app/lish/[did]/[publication]/[rkey]/Blocks/PublishedPageBlock.tsx
···
16
16
import { AppBskyFeedDefs } from "@atproto/api";
17
17
import { TextBlock } from "./TextBlock";
18
18
import { useDocument } from "contexts/DocumentContext";
19
19
-
import { openPage, useOpenPages } from "../PostPages";
19
19
+
import { openPage, useOpenPages } from "../postPageState";
20
20
import {
21
21
openInteractionDrawer,
22
22
setInteractionState,
···
38
38
isCanvas?: boolean;
39
39
pages?: (PubLeafletPagesLinearDocument.Main | PubLeafletPagesCanvas.Main)[];
40
40
}) {
41
41
-
//switch to use actually state
42
41
let openPages = useOpenPages();
43
42
let isOpen = openPages.some((p) => p.type === "doc" && p.id === props.pageId);
44
43
return (
···
209
208
let comments = allComments.filter(
210
209
(c) => (c.record as PubLeafletComment.Record)?.onPage === props.pageId,
211
210
).length;
212
212
-
let quotes = mentions.filter((q) =>
213
213
-
q.link.includes(props.pageId),
214
214
-
).length;
211
211
+
let quotes = mentions.filter((q) => q.link.includes(props.pageId)).length;
215
212
216
213
let { drawerOpen, drawer, pageId } = useInteractionState(document_uri);
217
214
+1
-1
app/lish/[did]/[publication]/[rkey]/BlueskyQuotesPage.tsx
···
5
5
import { useDrawerOpen } from "./Interactions/InteractionDrawer";
6
6
import { DotLoader } from "components/utils/DotLoader";
7
7
import { QuoteTiny } from "components/Icons/QuoteTiny";
8
8
-
import { openPage } from "./PostPages";
8
8
+
import { openPage } from "./postPageState";
9
9
import { BskyPostContent } from "./BskyPostContent";
10
10
import {
11
11
QuotesLink,
+1
-1
app/lish/[did]/[publication]/[rkey]/BskyPostContent.tsx
···
8
8
import { Separator } from "components/Layout";
9
9
import { useLocalizedDate } from "src/hooks/useLocalizedDate";
10
10
import { useHasPageLoaded } from "components/InitialPageLoadProvider";
11
11
-
import { OpenPage, openPage } from "./PostPages";
11
11
+
import { OpenPage, openPage } from "./postPageState";
12
12
import { ThreadLink, QuotesLink } from "./PostLinks";
13
13
import { BlueskyLinkTiny } from "components/Icons/BlueskyLinkTiny";
14
14
import { Avatar } from "components/Avatar";
+1
-1
app/lish/[did]/[publication]/[rkey]/Interactions/Quotes.tsx
···
18
18
import { PostContent } from "../PostContent";
19
19
import { ProfileViewBasic } from "@atproto/api/dist/client/types/app/bsky/actor/defs";
20
20
import { flushSync } from "react-dom";
21
21
-
import { openPage } from "../PostPages";
21
21
+
import { openPage } from "../postPageState";
22
22
import useSWR, { mutate } from "swr";
23
23
import { DotLoader } from "components/utils/DotLoader";
24
24
import { CommentTiny } from "components/Icons/CommentTiny";
+1
-1
app/lish/[did]/[publication]/[rkey]/PostLinks.tsx
···
1
1
"use client";
2
2
import { AppBskyFeedDefs } from "@atproto/api";
3
3
import { preload } from "swr";
4
4
-
import { openPage, OpenPage } from "./PostPages";
4
4
+
import { openPage, OpenPage } from "./postPageState";
5
5
6
6
type ThreadViewPost = AppBskyFeedDefs.ThreadViewPost;
7
7
type NotFoundPost = AppBskyFeedDefs.NotFoundPost;
+16
-144
app/lish/[did]/[publication]/[rkey]/PostPages.tsx
···
10
10
import { PostPageData } from "./getPostPageData";
11
11
import { ProfileViewDetailed } from "@atproto/api/dist/client/types/app/bsky/actor/defs";
12
12
import { AppBskyFeedDefs } from "@atproto/api";
13
13
-
import { create } from "zustand/react";
14
13
import {
15
14
InteractionDrawer,
16
15
useDrawerOpen,
···
18
17
import { BookendSpacer, SandwichSpacer } from "components/LeafletLayout";
19
18
import { PageOptionButton } from "components/Pages/PageOptions";
20
19
import { CloseTiny } from "components/Icons/CloseTiny";
21
21
-
import { Fragment, useEffect } from "react";
22
22
-
import { flushSync } from "react-dom";
23
23
-
import { scrollIntoView } from "src/utils/scrollIntoView";
24
24
-
import { useParams, useSearchParams } from "next/navigation";
25
25
-
import { decodeQuotePosition } from "./quotePosition";
20
20
+
import { Fragment } from "react";
26
21
import { PollData } from "./fetchPollData";
27
22
import { LinearDocumentPage } from "./LinearDocumentPage";
28
23
import { CanvasPage } from "./CanvasPage";
29
24
import { ThreadPage as ThreadPageComponent } from "./ThreadPage";
30
25
import { BlueskyQuotesPage } from "./BlueskyQuotesPage";
31
26
import { useCardBorderHidden } from "components/Pages/useCardBorderHidden";
32
32
-
33
33
-
// Page types
34
34
-
export type DocPage = { type: "doc"; id: string };
35
35
-
export type ThreadPage = { type: "thread"; uri: string };
36
36
-
export type QuotesPage = { type: "quotes"; uri: string };
37
37
-
export type OpenPage = DocPage | ThreadPage | QuotesPage;
38
38
-
39
39
-
// Get a stable key for a page
40
40
-
const getPageKey = (page: OpenPage): string => {
41
41
-
if (page.type === "doc") return page.id;
42
42
-
if (page.type === "quotes") return `quotes:${page.uri}`;
43
43
-
return `thread:${page.uri}`;
44
44
-
};
45
45
-
46
46
-
const usePostPageUIState = create(() => ({
47
47
-
pages: [] as OpenPage[],
48
48
-
initialized: false,
49
49
-
}));
50
50
-
51
51
-
export const useOpenPages = (): OpenPage[] => {
52
52
-
const { quote } = useParams();
53
53
-
const state = usePostPageUIState((s) => s);
54
54
-
const searchParams = useSearchParams();
55
55
-
const pageParam = searchParams.get("page");
56
56
-
57
57
-
if (!state.initialized) {
58
58
-
// Check for page search param first (for comment links)
59
59
-
if (pageParam) {
60
60
-
return [{ type: "doc", id: pageParam }];
61
61
-
}
62
62
-
// Then check for quote param
63
63
-
if (quote) {
64
64
-
const decodedQuote = decodeQuotePosition(quote as string);
65
65
-
if (decodedQuote?.pageId) {
66
66
-
return [{ type: "doc", id: decodedQuote.pageId }];
67
67
-
}
68
68
-
}
69
69
-
}
70
70
-
71
71
-
return state.pages;
72
72
-
};
73
73
-
74
74
-
export const useInitializeOpenPages = () => {
75
75
-
const { quote } = useParams();
76
76
-
const searchParams = useSearchParams();
77
77
-
const pageParam = searchParams.get("page");
78
78
-
79
79
-
useEffect(() => {
80
80
-
const state = usePostPageUIState.getState();
81
81
-
if (!state.initialized) {
82
82
-
// Check for page search param first (for comment links)
83
83
-
if (pageParam) {
84
84
-
usePostPageUIState.setState({
85
85
-
pages: [{ type: "doc", id: pageParam }],
86
86
-
initialized: true,
87
87
-
});
88
88
-
return;
89
89
-
}
90
90
-
// Then check for quote param
91
91
-
if (quote) {
92
92
-
const decodedQuote = decodeQuotePosition(quote as string);
93
93
-
if (decodedQuote?.pageId) {
94
94
-
usePostPageUIState.setState({
95
95
-
pages: [{ type: "doc", id: decodedQuote.pageId }],
96
96
-
initialized: true,
97
97
-
});
98
98
-
return;
99
99
-
}
100
100
-
}
101
101
-
// Mark as initialized even if no pageId found
102
102
-
usePostPageUIState.setState({ initialized: true });
103
103
-
}
104
104
-
}, [quote, pageParam]);
105
105
-
};
106
106
-
107
107
-
export const openPage = (
108
108
-
parent: OpenPage | undefined,
109
109
-
page: OpenPage,
110
110
-
options?: { scrollIntoView?: boolean },
111
111
-
) => {
112
112
-
const pageKey = getPageKey(page);
113
113
-
const parentKey = parent ? getPageKey(parent) : undefined;
114
114
-
115
115
-
// Check if the page is already open
116
116
-
const currentState = usePostPageUIState.getState();
117
117
-
const existingPageIndex = currentState.pages.findIndex(
118
118
-
(p) => getPageKey(p) === pageKey,
119
119
-
);
120
120
-
121
121
-
// If page is already open, just scroll to it
122
122
-
if (existingPageIndex !== -1) {
123
123
-
if (options?.scrollIntoView !== false) {
124
124
-
scrollIntoView(`post-page-${pageKey}`);
125
125
-
}
126
126
-
return;
127
127
-
}
128
128
-
129
129
-
flushSync(() => {
130
130
-
usePostPageUIState.setState((state) => {
131
131
-
let parentPosition = state.pages.findIndex(
132
132
-
(s) => getPageKey(s) === parentKey,
133
133
-
);
134
134
-
// Close any pages after the parent and add the new page
135
135
-
return {
136
136
-
pages:
137
137
-
parentPosition === -1
138
138
-
? [page]
139
139
-
: [...state.pages.slice(0, parentPosition + 1), page],
140
140
-
initialized: true,
141
141
-
};
142
142
-
});
143
143
-
});
144
144
-
145
145
-
if (options?.scrollIntoView !== false) {
146
146
-
// Use requestAnimationFrame to ensure the DOM has been painted before scrolling
147
147
-
requestAnimationFrame(() => {
148
148
-
scrollIntoView(`post-page-${pageKey}`);
149
149
-
});
150
150
-
}
151
151
-
};
27
27
+
import {
28
28
+
type OpenPage,
29
29
+
type DocPage,
30
30
+
type ThreadPage,
31
31
+
type QuotesPage,
32
32
+
getPageKey,
33
33
+
useOpenPages,
34
34
+
useInitializeOpenPages,
35
35
+
openPage,
36
36
+
closePage,
37
37
+
} from "./postPageState";
152
38
153
153
-
export const closePage = (page: OpenPage) => {
154
154
-
const pageKey = getPageKey(page);
155
155
-
usePostPageUIState.setState((state) => {
156
156
-
let parentPosition = state.pages.findIndex(
157
157
-
(s) => getPageKey(s) === pageKey,
158
158
-
);
159
159
-
return {
160
160
-
pages: state.pages.slice(0, parentPosition),
161
161
-
initialized: true,
162
162
-
};
163
163
-
});
164
164
-
};
39
39
+
export type { DocPage, ThreadPage, QuotesPage, OpenPage };
40
40
+
export { getPageKey, useOpenPages, useInitializeOpenPages, openPage, closePage };
165
41
166
42
// Shared props type for both page components
167
43
export type SharedPageProps = {
···
305
181
: document.comments_on_documents
306
182
}
307
183
quotesAndMentions={
308
308
-
preferences.showMentions === false
309
309
-
? []
310
310
-
: quotesAndMentions
184
184
+
preferences.showMentions === false ? [] : quotesAndMentions
311
185
}
312
186
did={did}
313
187
/>
···
401
275
: document.comments_on_documents
402
276
}
403
277
quotesAndMentions={
404
404
-
preferences.showMentions === false
405
405
-
? []
406
406
-
: quotesAndMentions
278
278
+
preferences.showMentions === false ? [] : quotesAndMentions
407
279
}
408
280
did={did}
409
281
/>
+139
app/lish/[did]/[publication]/[rkey]/postPageState.ts
···
1
1
+
import { create } from "zustand";
2
2
+
import { flushSync } from "react-dom";
3
3
+
import { scrollIntoView } from "src/utils/scrollIntoView";
4
4
+
import { useParams, useSearchParams } from "next/navigation";
5
5
+
import { decodeQuotePosition } from "./quotePosition";
6
6
+
import { useEffect } from "react";
7
7
+
8
8
+
// Page types
9
9
+
export type DocPage = { type: "doc"; id: string };
10
10
+
export type ThreadPage = { type: "thread"; uri: string };
11
11
+
export type QuotesPage = { type: "quotes"; uri: string };
12
12
+
export type OpenPage = DocPage | ThreadPage | QuotesPage;
13
13
+
14
14
+
// Get a stable key for a page
15
15
+
export const getPageKey = (page: OpenPage): string => {
16
16
+
if (page.type === "doc") return page.id;
17
17
+
if (page.type === "quotes") return `quotes:${page.uri}`;
18
18
+
return `thread:${page.uri}`;
19
19
+
};
20
20
+
21
21
+
const usePostPageUIState = create(() => ({
22
22
+
pages: [] as OpenPage[],
23
23
+
initialized: false,
24
24
+
}));
25
25
+
26
26
+
export const useOpenPages = (): OpenPage[] => {
27
27
+
const { quote } = useParams();
28
28
+
const state = usePostPageUIState((s) => s);
29
29
+
const searchParams = useSearchParams();
30
30
+
const pageParam = searchParams.get("page");
31
31
+
32
32
+
if (!state.initialized) {
33
33
+
// Check for page search param first (for comment links)
34
34
+
if (pageParam) {
35
35
+
return [{ type: "doc", id: pageParam }];
36
36
+
}
37
37
+
// Then check for quote param
38
38
+
if (quote) {
39
39
+
const decodedQuote = decodeQuotePosition(quote as string);
40
40
+
if (decodedQuote?.pageId) {
41
41
+
return [{ type: "doc", id: decodedQuote.pageId }];
42
42
+
}
43
43
+
}
44
44
+
}
45
45
+
46
46
+
return state.pages;
47
47
+
};
48
48
+
49
49
+
export const useInitializeOpenPages = () => {
50
50
+
const { quote } = useParams();
51
51
+
const searchParams = useSearchParams();
52
52
+
const pageParam = searchParams.get("page");
53
53
+
54
54
+
useEffect(() => {
55
55
+
const state = usePostPageUIState.getState();
56
56
+
if (!state.initialized) {
57
57
+
// Check for page search param first (for comment links)
58
58
+
if (pageParam) {
59
59
+
usePostPageUIState.setState({
60
60
+
pages: [{ type: "doc", id: pageParam }],
61
61
+
initialized: true,
62
62
+
});
63
63
+
return;
64
64
+
}
65
65
+
// Then check for quote param
66
66
+
if (quote) {
67
67
+
const decodedQuote = decodeQuotePosition(quote as string);
68
68
+
if (decodedQuote?.pageId) {
69
69
+
usePostPageUIState.setState({
70
70
+
pages: [{ type: "doc", id: decodedQuote.pageId }],
71
71
+
initialized: true,
72
72
+
});
73
73
+
return;
74
74
+
}
75
75
+
}
76
76
+
// Mark as initialized even if no pageId found
77
77
+
usePostPageUIState.setState({ initialized: true });
78
78
+
}
79
79
+
}, [quote, pageParam]);
80
80
+
};
81
81
+
82
82
+
export const openPage = (
83
83
+
parent: OpenPage | undefined,
84
84
+
page: OpenPage,
85
85
+
options?: { scrollIntoView?: boolean },
86
86
+
) => {
87
87
+
const pageKey = getPageKey(page);
88
88
+
const parentKey = parent ? getPageKey(parent) : undefined;
89
89
+
90
90
+
// Check if the page is already open
91
91
+
const currentState = usePostPageUIState.getState();
92
92
+
const existingPageIndex = currentState.pages.findIndex(
93
93
+
(p) => getPageKey(p) === pageKey,
94
94
+
);
95
95
+
96
96
+
// If page is already open, just scroll to it
97
97
+
if (existingPageIndex !== -1) {
98
98
+
if (options?.scrollIntoView !== false) {
99
99
+
scrollIntoView(`post-page-${pageKey}`);
100
100
+
}
101
101
+
return;
102
102
+
}
103
103
+
104
104
+
flushSync(() => {
105
105
+
usePostPageUIState.setState((state) => {
106
106
+
let parentPosition = state.pages.findIndex(
107
107
+
(s) => getPageKey(s) === parentKey,
108
108
+
);
109
109
+
// Close any pages after the parent and add the new page
110
110
+
return {
111
111
+
pages:
112
112
+
parentPosition === -1
113
113
+
? [page]
114
114
+
: [...state.pages.slice(0, parentPosition + 1), page],
115
115
+
initialized: true,
116
116
+
};
117
117
+
});
118
118
+
});
119
119
+
120
120
+
if (options?.scrollIntoView !== false) {
121
121
+
// Use requestAnimationFrame to ensure the DOM has been painted before scrolling
122
122
+
requestAnimationFrame(() => {
123
123
+
scrollIntoView(`post-page-${pageKey}`);
124
124
+
});
125
125
+
}
126
126
+
};
127
127
+
128
128
+
export const closePage = (page: OpenPage) => {
129
129
+
const pageKey = getPageKey(page);
130
130
+
usePostPageUIState.setState((state) => {
131
131
+
let parentPosition = state.pages.findIndex(
132
132
+
(s) => getPageKey(s) === pageKey,
133
133
+
);
134
134
+
return {
135
135
+
pages: state.pages.slice(0, parentPosition),
136
136
+
initialized: true,
137
137
+
};
138
138
+
});
139
139
+
};