Skip to content

Commit fcf39d0

Browse files
authored
feat(communities): Re-implement community overview (#22293)
* Fix page-nav `community` variant not aligned * Add a worklet wrapper to use kebab-case keywords * Add tests for sub :communities/flatten-channels-and-categories * Throw an exception for non-existing worklets
1 parent d1f80f0 commit fcf39d0

File tree

14 files changed

+1201
-351
lines changed

14 files changed

+1201
-351
lines changed

src/js/worklets/communities.js

+295
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
import {
2+
useDerivedValue,
3+
useAnimatedStyle,
4+
interpolate,
5+
scrollTo,
6+
withTiming,
7+
cancelAnimation,
8+
withDecay,
9+
} from 'react-native-reanimated';
10+
11+
import { Platform } from 'react-native';
12+
13+
export function useLogoStyles({
14+
scrollAmount,
15+
expandHeaderThreshold,
16+
sheetDisplacementThreshold,
17+
textMovementThreshold,
18+
}) {
19+
return useAnimatedStyle(() => {
20+
const firstDisplacement = scrollAmount.value < expandHeaderThreshold;
21+
if (firstDisplacement) {
22+
return {
23+
transform: [
24+
{ translateX: 20 },
25+
{ translateY: interpolate(scrollAmount.value, [0, expandHeaderThreshold], [0, -42.5], 'clamp') },
26+
{ scale: interpolate(scrollAmount.value, [0, textMovementThreshold], [1, 0.4], 'clamp') },
27+
],
28+
};
29+
} else {
30+
return {
31+
transform: [
32+
{ translateX: 20 },
33+
{
34+
translateY: interpolate(
35+
scrollAmount.value,
36+
[expandHeaderThreshold, sheetDisplacementThreshold],
37+
[-42.5, -50.5],
38+
'clamp',
39+
),
40+
},
41+
{ scale: 0.4 },
42+
],
43+
};
44+
}
45+
});
46+
}
47+
48+
export function useSheetStyles({ scrollAmount, expandHeaderThreshold, sheetDisplacementThreshold }) {
49+
return useAnimatedStyle(() => {
50+
const firstDisplacement = scrollAmount.value < expandHeaderThreshold;
51+
if (firstDisplacement) {
52+
return {
53+
transform: [{ translateY: interpolate(scrollAmount.value, [0, expandHeaderThreshold], [40, 0], 'clamp') }],
54+
borderTopLeftRadius: 20,
55+
borderTopRightRadius: 20,
56+
};
57+
} else {
58+
const radius = interpolate(
59+
scrollAmount.value,
60+
[expandHeaderThreshold, sheetDisplacementThreshold],
61+
[20, 0],
62+
'clamp',
63+
);
64+
return {
65+
transform: [
66+
{
67+
translateY: interpolate(
68+
scrollAmount.value,
69+
[expandHeaderThreshold, sheetDisplacementThreshold],
70+
[0, -8],
71+
'clamp',
72+
),
73+
},
74+
],
75+
borderTopLeftRadius: radius,
76+
borderTopRightRadius: radius,
77+
};
78+
}
79+
});
80+
}
81+
82+
export function useNameStyles({ scrollAmount, expandHeaderThreshold, textMovementThreshold }) {
83+
return useAnimatedStyle(() => {
84+
const animationProgress = interpolate(
85+
scrollAmount.value,
86+
[textMovementThreshold, expandHeaderThreshold],
87+
[0, 40],
88+
'clamp',
89+
);
90+
return {
91+
marginRight: animationProgress,
92+
transform: [
93+
{ translateX: animationProgress },
94+
{
95+
translateY: interpolate(
96+
scrollAmount.value,
97+
[textMovementThreshold, expandHeaderThreshold],
98+
[0, -44.5],
99+
'clamp',
100+
),
101+
},
102+
],
103+
};
104+
});
105+
}
106+
107+
export function useInfoStyles({ scrollAmount, infoOpacityThreshold }) {
108+
return useAnimatedStyle(() => {
109+
return {
110+
opacity: interpolate(scrollAmount.value, [0, infoOpacityThreshold], [1, 0.2], 'extend'),
111+
};
112+
});
113+
}
114+
115+
export function useChannelsStyles({
116+
scrollAmount,
117+
headerHeight,
118+
expandHeaderThreshold,
119+
sheetDisplacementThreshold,
120+
expandHeaderLimit,
121+
}) {
122+
return useAnimatedStyle(() => {
123+
const headerDisplacement = (headerHeight.value - 55.5) * -1;
124+
const firstDisplacement = scrollAmount.value < expandHeaderThreshold;
125+
const secondDisplacement = scrollAmount.value > sheetDisplacementThreshold;
126+
if (firstDisplacement) {
127+
return {
128+
transform: [
129+
{
130+
translateY: interpolate(scrollAmount.value, [0, expandHeaderThreshold], [39, headerDisplacement], 'clamp'),
131+
},
132+
],
133+
};
134+
} else if (secondDisplacement) {
135+
return {
136+
transform: [
137+
{
138+
translateY: interpolate(
139+
scrollAmount.value,
140+
[sheetDisplacementThreshold, expandHeaderLimit],
141+
[headerDisplacement - 8, headerDisplacement - 64],
142+
'clamp',
143+
),
144+
},
145+
],
146+
};
147+
} else {
148+
return {
149+
transform: [
150+
{
151+
translateY: interpolate(
152+
scrollAmount.value,
153+
[expandHeaderThreshold, sheetDisplacementThreshold],
154+
[headerDisplacement, headerDisplacement - 8],
155+
'clamp',
156+
),
157+
},
158+
],
159+
};
160+
}
161+
}, [headerHeight.value]);
162+
}
163+
164+
export function useScrollTo({ animatedRef, scrollAmount, expandHeaderLimit }) {
165+
const isAndroid = Platform.OS === 'android';
166+
return useDerivedValue(() => {
167+
scrollTo(animatedRef, 0, scrollAmount.value - expandHeaderLimit, isAndroid);
168+
});
169+
}
170+
171+
export function useHeaderOpacity({ scrollAmount, expandHeaderThreshold, sheetDisplacementThreshold }) {
172+
return useDerivedValue(() => {
173+
return interpolate(scrollAmount.value, [expandHeaderThreshold, sheetDisplacementThreshold], [0, 1], 'clamp');
174+
});
175+
}
176+
177+
export function useOppositeHeaderOpacity(headerOpacity) {
178+
return useDerivedValue(() => {
179+
return 1 - headerOpacity.value;
180+
});
181+
}
182+
183+
export function useNavContentOpacity({ scrollAmount, sheetDisplacementThreshold, expandHeaderLimit }) {
184+
return useDerivedValue(() => {
185+
return interpolate(scrollAmount.value, [sheetDisplacementThreshold, expandHeaderLimit], [0, 1], 'clamp');
186+
});
187+
}
188+
189+
export function onScrollAnimationEnd(
190+
scrollAmount,
191+
scrollStart,
192+
expandHeaderThreshold,
193+
snapHeaderThreshold,
194+
expandHeaderLimit,
195+
animationDuration,
196+
) {
197+
'worklet';
198+
return function () {
199+
'worklet';
200+
const duration = { duration: animationDuration };
201+
if (scrollAmount.value > snapHeaderThreshold && scrollAmount.value <= expandHeaderThreshold) {
202+
scrollStart.value = withTiming(-expandHeaderThreshold, duration);
203+
scrollAmount.value = withTiming(expandHeaderThreshold, duration);
204+
}
205+
206+
if (scrollAmount.value > expandHeaderThreshold) {
207+
scrollStart.value = -scrollAmount.value;
208+
}
209+
210+
if (scrollAmount.value <= snapHeaderThreshold) {
211+
scrollStart.value = withTiming(0, duration);
212+
scrollAmount.value = withTiming(0, duration);
213+
}
214+
215+
if (scrollAmount.value > expandHeaderThreshold && scrollAmount.value < expandHeaderLimit) {
216+
if (scrollAmount.value >= (expandHeaderLimit - expandHeaderThreshold) * 0.65 + expandHeaderThreshold) {
217+
scrollAmount.value = withTiming(expandHeaderLimit, duration);
218+
scrollStart.value = withTiming(-expandHeaderLimit, duration);
219+
} else {
220+
scrollAmount.value = withTiming(expandHeaderThreshold, duration);
221+
scrollStart.value = withTiming(-expandHeaderThreshold, duration);
222+
}
223+
}
224+
};
225+
}
226+
227+
export function onPanStart(scrollStart, scrollAmount) {
228+
return function () {
229+
'worklet';
230+
cancelAnimation(scrollStart);
231+
cancelAnimation(scrollAmount);
232+
scrollStart.value = -1 * scrollAmount.value;
233+
};
234+
}
235+
236+
export function onPanUpdate({ scrollStart, scrollAmount, maxScroll, expandHeaderLimit }) {
237+
return function (event) {
238+
'worklet';
239+
const newScrollAmount = -1 * (event.translationY + scrollStart.value);
240+
if (newScrollAmount <= 0) {
241+
scrollAmount.value = 0;
242+
} else {
243+
const limit = expandHeaderLimit + maxScroll.value;
244+
scrollAmount.value = newScrollAmount <= limit ? newScrollAmount : limit;
245+
}
246+
};
247+
}
248+
249+
export function onPanEnd({
250+
scrollStart,
251+
scrollAmount,
252+
maxScroll,
253+
expandHeaderLimit,
254+
expandHeaderThreshold,
255+
snapHeaderThreshold,
256+
animationDuration,
257+
}) {
258+
const isIOS = Platform.OS === 'ios';
259+
return function (event) {
260+
'worklet';
261+
scrollStart.value = -scrollAmount.value;
262+
const endAnimation = onScrollAnimationEnd(
263+
scrollAmount,
264+
scrollStart,
265+
expandHeaderThreshold,
266+
snapHeaderThreshold,
267+
expandHeaderLimit,
268+
animationDuration,
269+
);
270+
if (scrollAmount.value < expandHeaderLimit) {
271+
endAnimation();
272+
} else {
273+
const maxValue = maxScroll.value + expandHeaderLimit;
274+
const decelerationRate = isIOS ? { deceleration: 0.9997 } : { deceleration: 0.999 };
275+
276+
scrollStart.value = withDecay({
277+
...{
278+
velocity: event.velocityY,
279+
clamp: [-1 * maxValue, 0],
280+
},
281+
...decelerationRate,
282+
});
283+
scrollAmount.value = withDecay(
284+
{
285+
...{
286+
velocity: -1 * event.velocityY,
287+
clamp: [0, maxValue],
288+
},
289+
...decelerationRate,
290+
},
291+
endAnimation,
292+
);
293+
}
294+
};
295+
}

src/mocks/js_dependencies.cljs

+12
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,18 @@
429429
(slurp
430430
"./resources/data/emojis/en.json"))
431431
"../src/js/worklets/core.js" worklet-factory
432+
"../src/js/worklets/communities.js" #js {"useLogoStyles" #js {}
433+
"useSheetStyles" #js {}
434+
"useNameStyles" #js {}
435+
"useInfoStyles" #js {}
436+
"useChannelsStyles" #js {}
437+
"useScrollTo" #js {}
438+
"useHeaderOpacity" #js {}
439+
"useOppositeHeaderOpacity" #js {}
440+
"useNavContentOpacity" #js {}
441+
"onPanStart" #js {}
442+
"onPanUpdate" #js {}
443+
"onPanEnd" #js {}}
432444
"../src/js/worklets/shell/bottom_tabs.js" #js {}
433445
"../src/js/worklets/shell/home_stack.js" #js {}
434446
"../src/js/worklets/bottom_sheet.js" #js {}

src/quo/components/navigation/page_nav/style.cljs

+5-6
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,11 @@
3232
{:width 12})
3333

3434
(defn right-content
35-
[min-size?]
36-
(merge
37-
{:flex-grow 1
38-
:flex-basis 1}
39-
(when min-size?
40-
{:min-height 32})))
35+
[min-size? centered-content?]
36+
(cond-> {}
37+
centered-content? (assoc :flex-grow 1
38+
:flex-basis 1)
39+
min-size? (assoc :min-height 32)))
4140

4241
(def token-logo
4342
{:width 16 :height 16})

src/quo/components/navigation/page_nav/view.cljs

+12-10
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,10 @@
8787

8888
(defn- right-content
8989
[{:keys [background content max-actions min-size? support-account-switcher?
90-
behind-overlay?]
91-
:or {support-account-switcher? true}}]
92-
[rn/view (style/right-content min-size?)
90+
behind-overlay? centered-content?]
91+
:or {support-account-switcher? true
92+
centered-content? true}}]
93+
[rn/view (style/right-content min-size? centered-content?)
9394
(when (coll? content)
9495
(into [rn/view {:style style/right-actions-container}]
9596
(add-right-buttons-xf max-actions background behind-overlay? support-account-switcher?)
@@ -260,15 +261,15 @@
260261
right-side :none
261262
background :white}
262263
:as props}]
263-
(let [center-content-container-style (reanimated/apply-animations-to-style
264+
(let [centered-content? (case type
265+
:title (= text-align :center)
266+
(:dropdown :wallet-networks) true
267+
false)
268+
center-content-container-style (reanimated/apply-animations-to-style
264269
(if center-opacity
265270
{:opacity center-opacity}
266271
nil)
267-
(style/center-content-container
268-
(case type
269-
:title (= text-align :center)
270-
(:dropdown :wallet-networks) true
271-
false)))
272+
(style/center-content-container centered-content?))
272273
props-with-style (assoc props
273274
:center-content-container-style
274275
center-content-container-style)]
@@ -342,6 +343,7 @@
342343
{:background background
343344
:content right-side
344345
:max-actions 3
345-
:support-account-switcher? false}]]
346+
:support-account-switcher? false
347+
:centered-content? centered-content?}]]
346348

347349
nil)))

0 commit comments

Comments
 (0)