Skip to content

Commit f78d7e7

Browse files
authored
feat(mobile): support anonymous timeline reading (#4911)
1 parent 565793f commit f78d7e7

File tree

14 files changed

+177
-128
lines changed

14 files changed

+177
-128
lines changed

apps/mobile/e2e/flows/shared/open-auth.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@ appId: is.follow
1111
commands:
1212
- tapOn:
1313
id: no-login-timeline
14+
- runFlow:
15+
when:
16+
visible:
17+
id: home-avatar-trigger
18+
commands:
19+
- tapOn:
20+
id: home-avatar-trigger
1421
- extendedWaitUntil:
1522
visible:
1623
id: login-screen

apps/mobile/src/components/ui/image/ImageContextMenu.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { useIsEntryStarred } from "@follow/store/collection/hooks"
33
import { collectionSyncService } from "@follow/store/collection/store"
44
import { useEntry } from "@follow/store/entry/hooks"
55
import { unreadSyncService } from "@follow/store/unread/store"
6+
import { useIsLoggedIn } from "@follow/store/user/hooks"
67
import { requireNativeModule } from "expo"
78
import type { PropsWithChildren } from "react"
89
import { useRef } from "react"
@@ -35,6 +36,7 @@ const getIOSNativeImageActions = () => {
3536

3637
export const ImageContextMenu = ({ imageUrl, entryId, children, view }: ImageContextMenuProps) => {
3738
const { t } = useTranslation()
39+
const isLoggedIn = useIsLoggedIn()
3840
const entry = useEntry(entryId, (state) => ({
3941
read: state.read,
4042
feedId: state.feedId,
@@ -58,7 +60,7 @@ export const ImageContextMenu = ({ imageUrl, entryId, children, view }: ImageCon
5860
</ContextMenu.Trigger>
5961

6062
<ContextMenu.Content>
61-
{entryId && feedId && view !== undefined && (
63+
{isLoggedIn && entryId && feedId && view !== undefined && (
6264
<>
6365
<ContextMenu.Item
6466
key="MarkAsRead"

apps/mobile/src/modules/context-menu/entry.tsx

Lines changed: 78 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { collectionSyncService } from "@follow/store/collection/store"
44
import { getEntry } from "@follow/store/entry/getter"
55
import { useEntry } from "@follow/store/entry/hooks"
66
import { unreadSyncService } from "@follow/store/unread/store"
7+
import { useIsLoggedIn } from "@follow/store/user/hooks"
78
import { PortalProvider } from "@gorhom/portal"
89
import type { PropsWithChildren } from "react"
910
import { useCallback } from "react"
@@ -32,6 +33,7 @@ export const EntryItemContextMenu = ({
3233
const { t } = useTranslation()
3334
const selectedView = useSelectedView()
3435
const selectedFeed = useSelectedFeed()
36+
const isLoggedIn = useIsLoggedIn()
3537
const entry = useEntry(id, (state) => ({
3638
read: state.read,
3739
feedId: state.feedId,
@@ -73,81 +75,85 @@ export const EntryItemContextMenu = ({
7375
)}
7476
</ContextMenu.Preview>
7577

76-
<ContextMenu.Item
77-
key="MarkAsReadAbove"
78-
onSelect={() => {
79-
const payload = getFetchEntryPayload(selectedFeed, selectedView)
80-
const { publishedAt } = entry
81-
unreadSyncService.markBatchAsRead({
82-
view: selectedView,
83-
filter: payload,
84-
time: {
85-
startTime: new Date(publishedAt).getTime() + 1,
86-
endTime: Date.now(),
87-
},
88-
excludePrivate: getHideAllReadSubscriptions(),
89-
})
90-
}}
91-
>
92-
<ContextMenu.ItemIcon
93-
ios={{
94-
name: "arrow.up",
95-
}}
96-
/>
97-
<ContextMenu.ItemTitle>
98-
{t("operation.mark_all_as_read_which", {
99-
which: t("operation.mark_all_as_read_which_above"),
100-
})}
101-
</ContextMenu.ItemTitle>
102-
</ContextMenu.Item>
78+
{isLoggedIn && (
79+
<>
80+
<ContextMenu.Item
81+
key="MarkAsReadAbove"
82+
onSelect={() => {
83+
const payload = getFetchEntryPayload(selectedFeed, selectedView)
84+
const { publishedAt } = entry
85+
unreadSyncService.markBatchAsRead({
86+
view: selectedView,
87+
filter: payload,
88+
time: {
89+
startTime: new Date(publishedAt).getTime() + 1,
90+
endTime: Date.now(),
91+
},
92+
excludePrivate: getHideAllReadSubscriptions(),
93+
})
94+
}}
95+
>
96+
<ContextMenu.ItemIcon
97+
ios={{
98+
name: "arrow.up",
99+
}}
100+
/>
101+
<ContextMenu.ItemTitle>
102+
{t("operation.mark_all_as_read_which", {
103+
which: t("operation.mark_all_as_read_which_above"),
104+
})}
105+
</ContextMenu.ItemTitle>
106+
</ContextMenu.Item>
103107

104-
<ContextMenu.Item
105-
key="MarkAsRead"
106-
onSelect={() => {
107-
entry.read
108-
? unreadSyncService.markEntryAsUnread(id)
109-
: unreadSyncService.markEntryAsRead(id)
110-
}}
111-
>
112-
<ContextMenu.ItemTitle>
113-
{entry.read ? t("operation.mark_as_unread") : t("operation.mark_as_read")}
114-
</ContextMenu.ItemTitle>
115-
<ContextMenu.ItemIcon
116-
ios={{
117-
name: entry.read ? "circle.fill" : "checkmark.circle",
118-
}}
119-
/>
120-
</ContextMenu.Item>
108+
<ContextMenu.Item
109+
key="MarkAsRead"
110+
onSelect={() => {
111+
entry.read
112+
? unreadSyncService.markEntryAsUnread(id)
113+
: unreadSyncService.markEntryAsRead(id)
114+
}}
115+
>
116+
<ContextMenu.ItemTitle>
117+
{entry.read ? t("operation.mark_as_unread") : t("operation.mark_as_read")}
118+
</ContextMenu.ItemTitle>
119+
<ContextMenu.ItemIcon
120+
ios={{
121+
name: entry.read ? "circle.fill" : "checkmark.circle",
122+
}}
123+
/>
124+
</ContextMenu.Item>
121125

122-
<ContextMenu.Item
123-
key="MarkAsReadBelow"
124-
onSelect={() => {
125-
const payload = getFetchEntryPayload(selectedFeed, selectedView)
126-
const { publishedAt } = entry
127-
unreadSyncService.markBatchAsRead({
128-
view: selectedView,
129-
filter: payload,
130-
time: {
131-
startTime: 1,
132-
endTime: new Date(publishedAt).getTime() - 1,
133-
},
134-
excludePrivate: getHideAllReadSubscriptions(),
135-
})
136-
}}
137-
>
138-
<ContextMenu.ItemIcon
139-
ios={{
140-
name: "arrow.down",
141-
}}
142-
/>
143-
<ContextMenu.ItemTitle>
144-
{t("operation.mark_all_as_read_which", {
145-
which: t("operation.mark_all_as_read_which_below"),
146-
})}
147-
</ContextMenu.ItemTitle>
148-
</ContextMenu.Item>
126+
<ContextMenu.Item
127+
key="MarkAsReadBelow"
128+
onSelect={() => {
129+
const payload = getFetchEntryPayload(selectedFeed, selectedView)
130+
const { publishedAt } = entry
131+
unreadSyncService.markBatchAsRead({
132+
view: selectedView,
133+
filter: payload,
134+
time: {
135+
startTime: 1,
136+
endTime: new Date(publishedAt).getTime() - 1,
137+
},
138+
excludePrivate: getHideAllReadSubscriptions(),
139+
})
140+
}}
141+
>
142+
<ContextMenu.ItemIcon
143+
ios={{
144+
name: "arrow.down",
145+
}}
146+
/>
147+
<ContextMenu.ItemTitle>
148+
{t("operation.mark_all_as_read_which", {
149+
which: t("operation.mark_all_as_read_which_below"),
150+
})}
151+
</ContextMenu.ItemTitle>
152+
</ContextMenu.Item>
153+
</>
154+
)}
149155

150-
{feedId && view !== undefined && (
156+
{isLoggedIn && feedId && view !== undefined && (
151157
<ContextMenu.Item
152158
key="Star"
153159
onSelect={() => {

apps/mobile/src/modules/context-menu/video.tsx

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { useIsEntryStarred } from "@follow/store/collection/hooks"
33
import { collectionSyncService } from "@follow/store/collection/store"
44
import { useEntry } from "@follow/store/entry/hooks"
55
import { unreadSyncService } from "@follow/store/unread/store"
6+
import { useIsLoggedIn } from "@follow/store/user/hooks"
67
import type { PropsWithChildren } from "react"
78
import { useTranslation } from "react-i18next"
89
import { Share } from "react-native"
@@ -16,6 +17,7 @@ type VideoContextMenuProps = PropsWithChildren<{
1617

1718
export const VideoContextMenu = ({ entryId, children }: VideoContextMenuProps) => {
1819
const { t } = useTranslation()
20+
const isLoggedIn = useIsLoggedIn()
1921
const entry = useEntry(entryId, (state) => ({
2022
read: state.read,
2123
feedId: state.feedId,
@@ -35,24 +37,26 @@ export const VideoContextMenu = ({ entryId, children }: VideoContextMenuProps) =
3537
<ContextMenu.Trigger>{children}</ContextMenu.Trigger>
3638

3739
<ContextMenu.Content>
38-
<ContextMenu.Item
39-
key="MarkAsRead"
40-
onSelect={() => {
41-
entry.read
42-
? unreadSyncService.markEntryAsUnread(entryId)
43-
: unreadSyncService.markEntryAsRead(entryId)
44-
}}
45-
>
46-
<ContextMenu.ItemTitle>
47-
{entry.read ? t("operation.mark_as_unread") : t("operation.mark_as_read")}
48-
</ContextMenu.ItemTitle>
49-
<ContextMenu.ItemIcon
50-
ios={{
51-
name: entry.read ? "circle.fill" : "checkmark.circle",
40+
{isLoggedIn && (
41+
<ContextMenu.Item
42+
key="MarkAsRead"
43+
onSelect={() => {
44+
entry.read
45+
? unreadSyncService.markEntryAsUnread(entryId)
46+
: unreadSyncService.markEntryAsRead(entryId)
5247
}}
53-
/>
54-
</ContextMenu.Item>
55-
{feedId && (
48+
>
49+
<ContextMenu.ItemTitle>
50+
{entry.read ? t("operation.mark_as_unread") : t("operation.mark_as_read")}
51+
</ContextMenu.ItemTitle>
52+
<ContextMenu.ItemIcon
53+
ios={{
54+
name: entry.read ? "circle.fill" : "checkmark.circle",
55+
}}
56+
/>
57+
</ContextMenu.Item>
58+
)}
59+
{isLoggedIn && feedId && (
5660
<ContextMenu.Item
5761
key="Star"
5862
onSelect={() => {

apps/mobile/src/modules/entry-content/EntryContentHeaderRightActions.tsx

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { entrySyncServices } from "@follow/store/entry/store"
55
import { useFeedById } from "@follow/store/feed/hooks"
66
import { useSubscriptionById } from "@follow/store/subscription/hooks"
77
import { translationSyncService } from "@follow/store/translation/store"
8+
import { useIsLoggedIn } from "@follow/store/user/hooks"
89
import { setStringAsync } from "expo-clipboard"
910
import { useAtom } from "jotai"
1011
import { useCallback, useEffect, useState } from "react"
@@ -58,6 +59,7 @@ const HeaderRightActionsImpl = ({
5859
}: HeaderRightActionsProps) => {
5960
const { t } = useTranslation()
6061
const labelColor = useColor("label")
62+
const isLoggedIn = useIsLoggedIn()
6163
const isStarred = useIsEntryStarred(entryId)
6264
const [extraActionContainerWidth, setExtraActionContainerWidth] = useState(0)
6365

@@ -136,18 +138,19 @@ const HeaderRightActionsImpl = ({
136138

137139
// Define action items for reuse
138140
const actionItems = [
139-
subscription && {
140-
key: "Star",
141-
title: isStarred ? t("operation.unstar") : t("operation.star"),
142-
icon: isStarred ? <StarCuteFiIcon /> : <StarCuteReIcon />,
143-
iconIOS: {
144-
name: isStarred ? "star.fill" : "star",
145-
paletteColors: isStarred ? ["#facc15"] : undefined,
141+
isLoggedIn &&
142+
subscription && {
143+
key: "Star",
144+
title: isStarred ? t("operation.unstar") : t("operation.star"),
145+
icon: isStarred ? <StarCuteFiIcon /> : <StarCuteReIcon />,
146+
iconIOS: {
147+
name: isStarred ? "star.fill" : "star",
148+
paletteColors: isStarred ? ["#facc15"] : undefined,
149+
},
150+
onPress: handleToggleStar,
151+
active: isStarred,
152+
iconColor: isStarred ? "#facc15" : undefined,
146153
},
147-
onPress: handleToggleStar,
148-
active: isStarred,
149-
iconColor: isStarred ? "#facc15" : undefined,
150-
},
151154
!showReadabilitySetting && {
152155
key: "ShowReadability",
153156
title: "Show Readability",
@@ -158,16 +161,17 @@ const HeaderRightActionsImpl = ({
158161
isCheckbox: true,
159162
// inMenu: true,
160163
},
161-
!showAITranslationSetting && {
162-
key: "ShowTranslation",
163-
title: "Show Translation",
164-
icon: <Translate2CuteReIcon />,
165-
iconIOS: { name: "globe" },
166-
onPress: toggleAITranslation,
167-
active: showTranslation,
168-
isCheckbox: true,
169-
inMenu: true,
170-
},
164+
isLoggedIn &&
165+
!showAITranslationSetting && {
166+
key: "ShowTranslation",
167+
title: "Show Translation",
168+
icon: <Translate2CuteReIcon />,
169+
iconIOS: { name: "globe" },
170+
onPress: toggleAITranslation,
171+
active: showTranslation,
172+
isCheckbox: true,
173+
inMenu: true,
174+
},
171175
{
172176
key: "Share",
173177
title: t("operation.share"),

apps/mobile/src/modules/entry-content/EntryReadHistory.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useEntryReadHistory } from "@follow/store/entry/hooks"
2+
import { useIsLoggedIn } from "@follow/store/user/hooks"
23
import { View } from "react-native"
34

45
import { UserAvatar } from "@/src/components/ui/avatar/UserAvatar"
@@ -7,9 +8,10 @@ import { useNavigation } from "@/src/lib/navigation/hooks"
78
import { ProfileScreen } from "@/src/screens/(modal)/ProfileScreen"
89

910
export const EntryReadHistory = ({ entryId }: { entryId: string }) => {
10-
const data = useEntryReadHistory(entryId, 6)
11+
const isLoggedIn = useIsLoggedIn()
12+
const data = useEntryReadHistory(entryId, 6, isLoggedIn)
1113
const navigation = useNavigation()
12-
if (!data?.entryReadHistories) return null
14+
if (!isLoggedIn || !data?.entryReadHistories) return null
1315
return (
1416
<View className="flex-row items-center justify-center">
1517
{data?.entryReadHistories.userIds.map((userId, index) => {

apps/mobile/src/modules/entry-list/EntryListSelector.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,20 @@ import { useNavigation } from "@/src/lib/navigation/hooks"
1313
import { EntryListContentPicture } from "@/src/modules/entry-list/EntryListContentPicture"
1414
import { EntryDetailScreen } from "@/src/screens/(stack)/entries/[entryId]/EntryDetailScreen"
1515

16-
import { useEntries } from "../screen/atoms"
16+
import { useEntries, useEntryListContext } from "../screen/atoms"
1717
import { EntryListContentArticle } from "./EntryListContentArticle"
1818
import { EntryListContentSocial } from "./EntryListContentSocial"
1919
import { EntryListContentVideo } from "./EntryListContentVideo"
2020

2121
const NoLoginGuard = ({ children }: { children: React.ReactNode }) => {
2222
const whoami = useWhoami()
23-
return whoami ? children : <NoLoginInfo target="timeline" />
23+
const screenType = useEntryListContext().type
24+
25+
if (whoami || screenType !== "subscriptions") {
26+
return children
27+
}
28+
29+
return <NoLoginInfo target="subscriptions" />
2430
}
2531

2632
type EntryListSelectorProps = {

0 commit comments

Comments
 (0)