Skip to content

Commit 861ab68

Browse files
Merge upstream develop
2 parents 9fcc39e + d429bd2 commit 861ab68

17 files changed

Lines changed: 697 additions & 320 deletions

.gitlab/ci/build_app.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,9 @@ app-android-apk:
9999
app-android-aab:
100100
extends: .app-eas-build
101101
script:
102-
- npx eas-cli build --platform android --non-interactive --no-wait ${CI_COMMIT_TAG:+--auto-submit}
102+
- npx eas-cli build --platform android --non-interactive --no-wait --auto-submit
103103

104104
app-ios:
105105
extends: .app-eas-build
106106
script:
107-
- npx eas-cli build --platform ios --non-interactive --no-wait ${CI_COMMIT_TAG:+--auto-submit}
107+
- npx eas-cli build --platform ios --non-interactive --no-wait --auto-submit

apps/polycentric/app.config.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
1010
name: NAME,
1111
slug: 'polycentric',
1212
version: process.env.APP_VERSION ?? '0.0.1',
13-
orientation: 'portrait',
13+
orientation: 'default',
1414
icon: './src/common/assets/images/app-icons/android-icon-foreground.png',
1515
scheme: 'polycentric',
1616
web: {
@@ -23,10 +23,12 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
2323
light: './src/common/assets/images/app-icons/ios-icon-default.png',
2424
tinted: './src/common/assets/images/app-icons/ios-icon-monochrome.png',
2525
},
26+
requireFullScreen: true,
2627
supportsTablet: true,
2728
bundleIdentifier: ID,
2829
infoPlist: {
2930
NSCameraUsageDescription: '$(PRODUCT_NAME) needs access to your Camera.',
31+
ITSAppUsesNonExemptEncryption: false,
3032
},
3133
entitlements: {
3234
'aps-environment': 'production',
@@ -81,6 +83,12 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
8183
],
8284
'expo-image',
8385
'expo-notifications',
86+
[
87+
'expo-screen-orientation',
88+
{
89+
initialOrientation: 'DEFAULT',
90+
},
91+
],
8492
],
8593
experiments: {
8694
typedRoutes: true,

apps/polycentric/app/_layout.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@ function RootStack() {
3333
headerShown: false,
3434
fullScreenGestureEnabled: !isWeb,
3535
contentStyle: [theme.atoms.bg, Atoms.flex_1, Atoms.overflow_auto],
36-
...(isWeb ? { animation: 'none' as const } : {}),
36+
...(isWeb
37+
? { animation: 'none' as const }
38+
: { orientation: 'portrait_up' }),
3739
}}
3840
>
3941
<Stack.Screen name="(tabs)" />
@@ -55,6 +57,17 @@ function RootStack() {
5557
contentStyle: { backgroundColor: 'transparent' },
5658
}}
5759
/>
60+
<Stack.Screen
61+
name="image-viewer"
62+
options={{
63+
presentation: 'transparentModal',
64+
animation: 'fade',
65+
contentStyle: { backgroundColor: 'transparent' },
66+
// Let just this screen rotate to landscape; the rest of the
67+
// app stays portrait.
68+
...(isWeb ? {} : { orientation: 'all' as const }),
69+
}}
70+
/>
5871
</Stack>
5972
</>
6073
);
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { default } from '@/src/features/post/ImageViewer/ImageViewerScreen';

apps/polycentric/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"expo-linking": "~55.0.7",
5353
"expo-notifications": "~55.0.0",
5454
"expo-router": "~55.0.3",
55+
"expo-screen-orientation": "~55.0.16",
5556
"expo-server": "~55.0.8",
5657
"expo-splash-screen": "~55.0.10",
5758
"expo-sqlite": "~55.0.15",

apps/polycentric/src/common/components/List.tsx

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ import {
99
} from '@shopify/flash-list';
1010
import React, {
1111
cloneElement,
12+
forwardRef,
1213
isValidElement,
1314
useEffect,
15+
useImperativeHandle,
1416
useRef,
1517
useState,
1618
} from 'react';
@@ -43,22 +45,39 @@ export type ListProps<T> = FlashListProps<T> & {
4345
| undefined;
4446
};
4547

46-
export function List<T extends PostData = PostData>(props: ListProps<T>) {
48+
/** Imperative handle exposed by `List` (and `FeedList`). */
49+
export type ListRef = { scrollToTop: () => void };
50+
51+
export const List = forwardRef(function List<T extends PostData = PostData>(
52+
props: ListProps<T>,
53+
ref: React.Ref<ListRef>,
54+
) {
4755
if (isWeb) {
48-
return <WebFeedViewer<T> {...props} />;
56+
return <WebFeedViewer<T> {...props} listRef={ref} />;
4957
}
5058

51-
return <NativeList<T> {...props} />;
52-
}
59+
return <NativeList<T> {...props} listRef={ref} />;
60+
}) as <T extends PostData = PostData>(
61+
props: ListProps<T> & { ref?: React.Ref<ListRef> },
62+
) => React.ReactElement;
5363

5464
function NativeList<T>({
5565
HeaderComponent,
5666
contentContainerStyle,
5767
refreshControl,
5868
onScroll: _ignoredOnScroll,
69+
listRef,
5970
...rest
60-
}: ListProps<T>) {
71+
}: ListProps<T> & { listRef?: React.Ref<ListRef> }) {
6172
const ref = useRef<FlashListRef<T>>(null);
73+
useImperativeHandle(
74+
listRef,
75+
() => ({
76+
scrollToTop: () =>
77+
ref.current?.scrollToOffset({ offset: 0, animated: true }),
78+
}),
79+
[],
80+
);
6281
const lastScrollY = useSharedValue(0);
6382
const headerTranslate = useSharedValue(0);
6483
const headerHeightShared = useSharedValue(0);
@@ -139,6 +158,7 @@ function NativeList<T>({
139158
) : null}
140159

141160
<AnimatedFlashList
161+
ref={ref as React.Ref<FlashListRef<unknown>>}
142162
{...(rest as FlashListProps<unknown>)}
143163
refreshControl={adjustedRefreshControl}
144164
onScroll={onScroll}
@@ -166,8 +186,20 @@ function WebFeedViewer<T>({
166186
onEndReached,
167187
contentContainerStyle,
168188
stickyHeaderIndices,
169-
}: ListProps<T>) {
189+
listRef,
190+
}: ListProps<T> & { listRef?: React.Ref<ListRef> }) {
170191
const sentinelRef = useRef<View>(null);
192+
useImperativeHandle(
193+
listRef,
194+
() => ({
195+
scrollToTop: () => {
196+
if (typeof window !== 'undefined') {
197+
window.scrollTo({ top: 0, behavior: 'smooth' });
198+
}
199+
},
200+
}),
201+
[],
202+
);
171203
const items = (data as readonly T[] | null | undefined) ?? [];
172204
const [visibleCount, setVisibleCount] = useState(WEB_INITIAL_VISIBLE);
173205

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import * as ScreenOrientation from 'expo-screen-orientation';
2+
import { useEffect, useState } from 'react';
3+
import { Platform } from 'react-native';
4+
5+
/**
6+
* Tracks the device's current screen orientation
7+
*/
8+
export function useOrientation(): ScreenOrientation.Orientation {
9+
const [orientation, setOrientation] = useState<ScreenOrientation.Orientation>(
10+
ScreenOrientation.Orientation.PORTRAIT_UP,
11+
);
12+
13+
useEffect(() => {
14+
if (Platform.OS === 'web') return;
15+
16+
let active = true;
17+
void ScreenOrientation.getOrientationAsync().then((current) => {
18+
if (active) setOrientation(current);
19+
});
20+
21+
const subscription = ScreenOrientation.addOrientationChangeListener(
22+
(event) => {
23+
console.log(event);
24+
setOrientation(event.orientationInfo.orientation);
25+
},
26+
);
27+
28+
return () => {
29+
active = false;
30+
ScreenOrientation.removeOrientationChangeListener(subscription);
31+
};
32+
}, []);
33+
34+
return orientation;
35+
}

apps/polycentric/src/features/feed/ExploreScreen.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@ import {
1313
} from '@/src/common/theme';
1414
import { isIOS, isWeb } from '@/src/common/util/platform';
1515
import { useFocusEffect } from 'expo-router';
16-
import { useState } from 'react';
16+
import { useCallback, useRef, useState } from 'react';
1717
import { Alert, Pressable, View } from 'react-native';
18+
import type { ListRef } from '@/src/common/components/List';
1819
import { ComposerInput } from '../composer';
1920
import FeedList from './FeedList';
2021
import { useExploreFeed } from './hooks/useExploreFeed';
@@ -72,12 +73,20 @@ export default function ExploreScreen() {
7273

7374
const [enabled, setEnabled] = useState<boolean>(false);
7475
const feed = useExploreFeed({ enabled });
76+
const listRef = useRef<ListRef>(null);
7577

7678
useFocusEffect(() => {
7779
setEnabled(true);
7880
});
7981

80-
useFocusedRefresh(feed.refresh);
82+
// Re-tapping the active tab scrolls to the top and refreshes.
83+
const { refresh } = feed;
84+
useFocusedRefresh(
85+
useCallback(() => {
86+
listRef.current?.scrollToTop();
87+
refresh();
88+
}, [refresh]),
89+
);
8190

8291
if (feed.error) {
8392
return (
@@ -101,7 +110,7 @@ export default function ExploreScreen() {
101110
return (
102111
<Screen>
103112
<Screen.PrimaryColumn>
104-
<FeedList feed={feed} HeaderComponent={ListHeader} />
113+
<FeedList ref={listRef} feed={feed} HeaderComponent={ListHeader} />
105114
{showComposeFab ? (
106115
<Fab
107116
onPress={openCompose}

apps/polycentric/src/features/feed/FeedList.tsx

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import { forwardRef } from 'react';
12
import { ActivityIndicator, RefreshControl, View } from 'react-native';
23
import {
34
List,
45
type ListProps,
6+
type ListRef,
57
type ListRenderItem,
68
} from '../../common/components/List';
79
import type { FeedHookResult } from './hooks/types';
@@ -25,19 +27,23 @@ const defaultRenderItem: ListRenderItem<PostData> = ({ item }) => (
2527
<Post post={item} />
2628
);
2729

28-
export default function FeedList({
29-
feed,
30-
emptyMessage = 'No posts yet',
31-
renderItem = defaultRenderItem,
32-
keyExtractor = defaultKeyExtractor,
33-
...rest
34-
}: FeedListProps) {
30+
const FeedList = forwardRef<ListRef, FeedListProps>(function FeedList(
31+
{
32+
feed,
33+
emptyMessage = 'No posts yet',
34+
renderItem = defaultRenderItem,
35+
keyExtractor = defaultKeyExtractor,
36+
...rest
37+
},
38+
ref,
39+
) {
3540
const { theme } = useTheme();
3641

3742
const insets = useSafeAreaInsets();
3843

3944
return (
4045
<List<PostData>
46+
ref={ref}
4147
renderItem={renderItem}
4248
keyExtractor={keyExtractor}
4349
data={feed.items}
@@ -84,4 +90,6 @@ export default function FeedList({
8490
{...rest}
8591
/>
8692
);
87-
}
93+
});
94+
95+
export default FeedList;

apps/polycentric/src/features/feed/FeedScreen.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ import { Atoms } from '@/src/common/theme';
99
import { isIOS, isWeb } from '@/src/common/util/platform';
1010
import { ComposerInput } from '@/src/features/composer';
1111
import { useFocusEffect } from 'expo-router';
12-
import { useCallback, useState } from 'react';
12+
import { useCallback, useRef, useState } from 'react';
1313
import { View } from 'react-native';
14+
import type { ListRef } from '@/src/common/components/List';
1415
import FeedList from './FeedList';
1516
import { useFollowingFeed } from './hooks/useFollowingFeed';
1617

@@ -29,14 +30,22 @@ export default function FeedScreen() {
2930

3031
const [enabled, setEnabled] = useState<boolean>(false);
3132
const feed = useFollowingFeed({ enabled });
33+
const listRef = useRef<ListRef>(null);
3234

3335
useFocusEffect(
3436
useCallback(() => {
3537
setEnabled(true);
3638
}, [setEnabled]),
3739
);
3840

39-
useFocusedRefresh(feed.refresh);
41+
// Re-tapping the active tab scrolls to the top and refreshes.
42+
const { refresh } = feed;
43+
useFocusedRefresh(
44+
useCallback(() => {
45+
listRef.current?.scrollToTop();
46+
refresh();
47+
}, [refresh]),
48+
);
4049

4150
if (feed.error) {
4251
return (
@@ -60,7 +69,7 @@ export default function FeedScreen() {
6069
return (
6170
<Screen>
6271
<Screen.PrimaryColumn>
63-
<FeedList feed={feed} HeaderComponent={ListHeader} />
72+
<FeedList ref={listRef} feed={feed} HeaderComponent={ListHeader} />
6473
{showComposeFab ? (
6574
<Fab
6675
onPress={openCompose}

0 commit comments

Comments
 (0)