git ssb

1+

Daan Patchwork / manyverse



Commit 44790022cba3e30de2a6a516798072ceab04d31a

ux: feature: dynamic header in the Profile screen

Andre Staltz committed on 9/30/2020, 11:55:36 AM
Parent: 8416b522c955281f357c4930a74ccc5dd6662ec9

Files changed

src/frontend/components/Feed.tschanged
src/frontend/screens/profile/styles.tschanged
src/frontend/screens/profile/view.tschanged
src/frontend/components/Feed.tsView
@@ -78,12 +78,8 @@
7878 function Separator() {
7979 return h(View, {style: styles.itemSeparator});
8080 }
8181
82-function PlaceholderWithSeparator() {
83- return h(View, [h(PlaceholderThreadCard), h(Separator)]);
84-}
85-
8682 class InitialLoading extends PureComponent<any> {
8783 private loadingAnim = new Animated.Value(0);
8884 private indexesAnim = new Animated.Value(0);
8985
@@ -147,8 +143,9 @@
147143 scrollToTop$?: Stream<any> | null;
148144 selfFeedId: FeedId;
149145 lastSessionTimestamp: number;
150146 EmptyComponent?: ReactElement<any>;
147+ HeaderComponent?: ReactElement<any>;
151148 style?: ViewStyle;
152149 contentContainerStyle?: ViewStyle;
153150 progressViewOffset?: number;
154151 onInitialPullDone?: () => void;
@@ -228,8 +225,28 @@
228225 }),
229226 );
230227 }
231228
229+ public renderHeader() {
230+ const {showPlaceholder} = this.state;
231+ const {HeaderComponent} = this.props;
232+
233+ if (showPlaceholder && HeaderComponent) {
234+ return h(View, [
235+ HeaderComponent,
236+ h(Separator),
237+ h(PlaceholderThreadCard),
238+ h(Separator),
239+ ]);
240+ } else if (showPlaceholder) {
241+ return h(View, [h(PlaceholderThreadCard), h(Separator)]);
242+ } else if (HeaderComponent) {
243+ return h(View, [HeaderComponent, h(Separator)]);
244+ } else {
245+ return null;
246+ }
247+ }
248+
232249 public render() {
233250 const {
234251 onRefresh,
235252 onPressReactions,
@@ -247,9 +264,9 @@
247264 lastSessionTimestamp,
248265 selfFeedId,
249266 EmptyComponent,
250267 } = this.props;
251- const {showPlaceholder, initialLoading} = this.state;
268+ const {initialLoading} = this.state;
252269
253270 return h(PullFlatList2, {
254271 getScrollStream: getReadable,
255272 getPrefixStream: () => this.addedThreadsStream,
@@ -282,9 +299,9 @@
282299 .mapTo(void 0),
283300 refreshColors: [Palette.backgroundBrandWeak],
284301 keyExtractor: (thread: ThreadSummaryWithExtras, index: number) =>
285302 thread.root.key ?? String(index),
286- ListHeaderComponent: showPlaceholder ? PlaceholderWithSeparator : null,
303+ ListHeaderComponent: this.renderHeader(),
287304 ListFooterComponent: initialLoading
288305 ? InitialLoading
289306 : PlaceholderThreadCard,
290307 ListEmptyComponent: EmptyComponent,
src/frontend/screens/profile/styles.tsView
@@ -10,9 +10,12 @@
1010 import {Typography} from '../../global-styles/typography';
1111
1212 export const avatarSize = Dimensions.avatarSizeBig;
1313 const avatarSizeHalf = avatarSize * 0.5;
14+export const toolbarAvatarSize = Dimensions.avatarSizeSmall;
1415
16+export const coverHeight = avatarSizeHalf;
17+
1518 export const styles = StyleSheet.create({
1619 container: {
1720 flex: 1,
1821 alignSelf: 'stretch',
@@ -20,73 +23,54 @@
2023 },
2124
2225 cover: {
2326 backgroundColor: Palette.backgroundBrand,
24- height: avatarSizeHalf,
27+ height: coverHeight,
2528 zIndex: 10,
2629 },
2730
31+ avatarTouchable: {
32+ position: 'absolute',
33+ top: Dimensions.toolbarHeight + coverHeight - avatarSizeHalf,
34+ left: Dimensions.horizontalSpaceBig,
35+ width: avatarSize,
36+ height: avatarSize,
37+ zIndex: 19,
38+ },
39+
40+ avatar: {
41+ zIndex: 20,
42+ },
43+
2844 name: {
45+ position: 'absolute',
2946 color: 'white',
47+ top:
48+ Dimensions.toolbarHeight + coverHeight - Typography.fontSizeLarge * 1.75,
49+ left:
50+ Dimensions.horizontalSpaceBig +
51+ avatarSize +
52+ Dimensions.horizontalSpaceBig,
53+ right: Dimensions.horizontalSpaceBig + Dimensions.iconSizeNormal,
3054 fontSize: Typography.fontSizeLarge,
3155 lineHeight: Typography.lineHeightLarge,
3256 fontFamily: Typography.fontFamilyReadableText,
3357 fontWeight: 'bold',
34- maxWidth: 220,
35- top: Dimensions.verticalSpaceSmall,
36- left:
37- Dimensions.horizontalSpaceBig +
38- avatarSize +
39- Dimensions.horizontalSpaceBig,
58+ zIndex: 20,
4059 },
4160
42- descriptionArea: {
43- top: -avatarSize,
44- marginBottom: -avatarSize,
45- zIndex: 10,
46- justifyContent: 'flex-start',
47- flexDirection: 'row',
48- paddingTop: avatarSizeHalf + Dimensions.verticalSpaceNormal,
49- paddingBottom: Dimensions.verticalSpaceNormal,
50- paddingLeft: Dimensions.horizontalSpaceBig,
51- paddingRight: Dimensions.horizontalSpaceBig,
61+ header: {
5262 backgroundColor: Palette.backgroundText,
5363 },
5464
55- bioButton: {
56- minWidth: avatarSize,
57- },
58-
59- description: {
60- fontSize: Typography.fontSizeNormal,
61- lineHeight: Typography.lineHeightNormal,
62- color: Palette.text,
63- },
64-
65- feed: {
66- top: Dimensions.verticalSpaceNormal * 0.5,
67- bottom: 0,
68- backgroundColor: Palette.backgroundVoid,
69- alignSelf: 'stretch',
70- },
71-
72- feedWithHeader: {
73- top: Dimensions.verticalSpaceNormal,
74- bottom: 0,
75- backgroundColor: Palette.backgroundVoid,
76- alignSelf: 'stretch',
77- },
78-
7965 sub: {
80- position: 'absolute',
81- top:
82- Dimensions.toolbarHeight + avatarSizeHalf + Dimensions.verticalSpaceSmall,
83- left:
66+ marginTop: Dimensions.verticalSpaceSmall,
67+ marginLeft:
8468 Dimensions.horizontalSpaceBig + // left margin to the avatar
8569 avatarSize + // avatar
8670 Dimensions.horizontalSpaceBig - // right margin to the avatar
8771 Dimensions.horizontalSpaceSmall, // minus follows-you-text margin
88- right: Dimensions.horizontalSpaceBig,
72+ marginRight: Dimensions.horizontalSpaceBig,
8973 zIndex: 30,
9074 flexDirection: 'row',
9175 justifyContent: 'space-between',
9276 alignItems: 'center',
@@ -117,20 +101,29 @@
117101 follow: {
118102 marginLeft: Dimensions.horizontalSpaceNormal,
119103 },
120104
121- avatarTouchable: {
122- top: -avatarSizeHalf,
123- left: Dimensions.horizontalSpaceBig,
124- width: avatarSize,
125- height: avatarSize,
126- zIndex: 19,
105+ descriptionArea: {
106+ zIndex: 10,
107+ justifyContent: 'flex-start',
108+ flexDirection: 'row',
109+ paddingTop: Dimensions.verticalSpaceNormal,
110+ paddingBottom: Dimensions.verticalSpaceNormal,
111+ paddingLeft: Dimensions.horizontalSpaceBig,
112+ paddingRight: Dimensions.horizontalSpaceBig,
113+ backgroundColor: Palette.backgroundText,
127114 },
128115
129- avatar: {
130- zIndex: 20,
116+ bioButton: {
117+ minWidth: avatarSize,
131118 },
132119
120+ feed: {
121+ bottom: 0,
122+ backgroundColor: Palette.backgroundVoid,
123+ alignSelf: 'stretch',
124+ },
125+
133126 emptySection: {
134127 marginTop: Dimensions.verticalSpaceBig * 2,
135128 },
136129 });
src/frontend/screens/profile/view.tsView
@@ -11,8 +11,9 @@
1111 View,
1212 Text,
1313 TouchableOpacity,
1414 TouchableWithoutFeedback,
15+ Animated,
1516 } from 'react-native';
1617 import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
1718 import {FloatingAction} from 'react-native-floating-action';
1819 import {isRootPostMsg, isPublic} from 'ssb-typescript/utils';
@@ -20,18 +21,52 @@
2021 import {t} from '../../drivers/localization';
2122 import {displayName} from '../../ssb/utils/from-ssb';
2223 import {Palette} from '../../global-styles/palette';
2324 import {Dimensions} from '../../global-styles/dimens';
25+import {Typography} from '../../global-styles/typography';
2426 import Feed from '../../components/Feed';
2527 import Button from '../../components/Button';
2628 import ToggleButton from '../../components/ToggleButton';
2729 import EmptySection from '../../components/EmptySection';
2830 import Avatar from '../../components/Avatar';
2931 import TopBar from '../../components/TopBar';
30-import {styles, avatarSize} from './styles';
3132 import {State} from './model';
33+import {styles, avatarSize, toolbarAvatarSize, coverHeight} from './styles';
3234
33-function renderTopBar(isSelfProfile: boolean) {
35+function calcNameTransY(scrollY: Animated.Value): Animated.Animated {
36+ return scrollY.interpolate({
37+ inputRange: [0, coverHeight],
38+ outputRange: [0, -coverHeight - Typography.fontSizeLarge * 0.5],
39+ extrapolate: 'clamp',
40+ });
41+}
42+
43+function calcAvatarTransX(scrollY: Animated.Value): Animated.Animated {
44+ return scrollY.interpolate({
45+ inputRange: [0, coverHeight],
46+ outputRange: [0, Dimensions.iconSizeNormal],
47+ extrapolate: 'clamp',
48+ });
49+}
50+
51+function calcAvatarTransY(scrollY: Animated.Value): Animated.Animated {
52+ const margin = (Dimensions.toolbarHeight - toolbarAvatarSize) * 0.5;
53+ return scrollY.interpolate({
54+ inputRange: [0, coverHeight],
55+ outputRange: [0, -coverHeight - toolbarAvatarSize * 0.5 - margin],
56+ extrapolate: 'clamp',
57+ });
58+}
59+
60+function calcAvatarScale(scrollY: Animated.Value): Animated.Animated {
61+ return scrollY.interpolate({
62+ inputRange: [0, coverHeight],
63+ outputRange: [1, toolbarAvatarSize / avatarSize],
64+ extrapolate: 'clamp',
65+ });
66+}
67+
68+function ProfileTopBar({isSelfProfile}: {isSelfProfile: boolean}) {
3469 return h(TopBar, {sel: 'topbar'}, [
3570 isSelfProfile
3671 ? null
3772 : h(
@@ -54,26 +89,21 @@
5489 ),
5590 ]);
5691 }
5792
58-function renderCover(state: State) {
59- return h(View, {style: styles.cover}, [
60- h(
61- Text,
62- {
63- style: styles.name,
64- numberOfLines: 1,
65- ellipsizeMode: 'middle',
66- accessible: true,
67- accessibilityRole: 'text',
68- accessibilityLabel: t('profile.name.accessibility_label'),
69- },
70- displayName(state.about.name, state.about.id),
71- ),
72- ]);
73-}
93+function ProfileAvatar({
94+ state,
95+ translateX,
96+ translateY,
97+ scale,
98+}: {
99+ state: State;
100+ translateX: Animated.Animated;
101+ translateY: Animated.Animated;
102+ scale: Animated.Animated;
103+}) {
104+ const animStyle = {transform: [{translateX, translateY}, {scale}]};
74105
75-function renderAvatar(state: State) {
76106 return h(
77107 TouchableWithoutFeedback,
78108 {
79109 sel: 'avatar',
@@ -81,20 +111,122 @@
81111 accessibilityRole: 'image',
82112 accessibilityLabel: t('profile.picture.accessibility_label'),
83113 },
84114 [
85- h(View, {style: styles.avatarTouchable, pointerEvents: 'box-only'}, [
86- h(Avatar, {
87- size: avatarSize,
88- url: state.about.imageUrl,
89- style: styles.avatar,
90- }),
91- ]),
115+ h(
116+ Animated.View,
117+ {style: [styles.avatarTouchable, animStyle], pointerEvents: 'box-only'},
118+ [
119+ h(Avatar, {
120+ size: avatarSize,
121+ url: state.about.imageUrl,
122+ style: styles.avatar,
123+ }),
124+ ],
125+ ),
92126 ],
93127 );
94128 }
95129
130+function ProfileName({
131+ state,
132+ translateY,
133+}: {
134+ state: State;
135+ translateY: Animated.Animated;
136+}) {
137+ const animStyle = {transform: [{translateY}]};
138+
139+ return h(
140+ Animated.Text,
141+ {
142+ style: [styles.name, animStyle],
143+ numberOfLines: 1,
144+ ellipsizeMode: 'middle',
145+ accessible: true,
146+ accessibilityRole: 'text',
147+ accessibilityLabel: t('profile.name.accessibility_label'),
148+ },
149+ displayName(state.about.name, state.about.id),
150+ );
151+}
152+
153+function ProfileHeader({state}: {state: State}) {
154+ const followsYouTristate = state.about.followsYou;
155+ const isSelfProfile = state.displayFeedId === state.selfFeedId;
156+ const isBlocked = state.about.following === false;
157+
158+ return h(View, {style: styles.header}, [
159+ h(View, {style: styles.cover}),
160+
161+ h(View, {style: styles.sub}, [
162+ followsYouTristate === true
163+ ? h(View, {style: styles.followsYou}, [
164+ h(
165+ Text,
166+ {style: styles.followsYouText},
167+ t('profile.info.follows_you'),
168+ ),
169+ ])
170+ : followsYouTristate === false
171+ ? h(View, {style: styles.followsYou}, [
172+ h(
173+ Text,
174+ {style: styles.followsYouText},
175+ t('profile.info.blocks_you'),
176+ ),
177+ ])
178+ : null,
179+
180+ h(View, {style: styles.cta}, [
181+ isSelfProfile
182+ ? h(Button, {
183+ sel: 'editProfile',
184+ text: t('profile.call_to_action.edit_profile.label'),
185+ accessible: true,
186+ accessibilityLabel: t(
187+ 'profile.call_to_action.edit_profile.accessibility_label',
188+ ),
189+ })
190+ : isBlocked
191+ ? null
192+ : h(ToggleButton, {
193+ sel: 'follow',
194+ style: styles.follow,
195+ text:
196+ state.about.following === true
197+ ? t('profile.info.following')
198+ : t('profile.call_to_action.follow'),
199+ toggled: state.about.following === true,
200+ }),
201+ ]),
202+ ]),
203+
204+ h(View, {style: styles.descriptionArea}, [
205+ state.about.description
206+ ? h(Button, {
207+ sel: 'bio',
208+ text: t('profile.call_to_action.open_biography.label'),
209+ small: true,
210+ style: styles.bioButton,
211+ accessible: true,
212+ accessibilityLabel: t(
213+ 'profile.call_to_action.open_biography.accessibility_label',
214+ ),
215+ strong: false,
216+ })
217+ : null,
218+ ]),
219+ ]);
220+}
221+
96222 export default function view(state$: Stream<State>, ssbSource: SSBSource) {
223+ const scrollHeaderBy = new Animated.Value(0);
224+ const avatarScale = calcAvatarScale(scrollHeaderBy);
225+ const avatarTransX = calcAvatarTransX(scrollHeaderBy);
226+ const avatarTransY = calcAvatarTransY(scrollHeaderBy);
227+ const nameTransY = calcNameTransY(scrollHeaderBy);
228+
97229 return state$
98230 .compose(
99231 dropRepeatsByKeys([
100232 'displayFeedId',
@@ -107,76 +239,21 @@
107239 )
108240 .map((state) => {
109241 const isSelfProfile = state.displayFeedId === state.selfFeedId;
110242 const isBlocked = state.about.following === false;
111- const followsYouTristate = state.about.followsYou;
112243
113244 return h(View, {style: styles.container}, [
114- renderTopBar(isSelfProfile),
245+ h(ProfileTopBar, {isSelfProfile}),
115246
116- renderCover(state),
247+ h(ProfileAvatar, {
248+ state,
249+ translateX: avatarTransX,
250+ translateY: avatarTransY,
251+ scale: avatarScale,
252+ }),
117253
118- renderAvatar(state),
254+ h(ProfileName, {state, translateY: nameTransY}),
119255
120- h(View, {style: styles.sub}, [
121- followsYouTristate === true
122- ? h(View, {style: styles.followsYou}, [
123- h(
124- Text,
125- {style: styles.followsYouText},
126- t('profile.info.follows_you'),
127- ),
128- ])
129- : followsYouTristate === false
130- ? h(View, {style: styles.followsYou}, [
131- h(
132- Text,
133- {style: styles.followsYouText},
134- t('profile.info.blocks_you'),
135- ),
136- ])
137- : null,
138-
139- h(View, {style: styles.cta}, [
140- isSelfProfile
141- ? h(Button, {
142- sel: 'editProfile',
143- text: t('profile.call_to_action.edit_profile.label'),
144- accessible: true,
145- accessibilityLabel: t(
146- 'profile.call_to_action.edit_profile.accessibility_label',
147- ),
148- })
149- : isBlocked
150- ? null
151- : h(ToggleButton, {
152- sel: 'follow',
153- style: styles.follow,
154- text:
155- state.about.following === true
156- ? t('profile.info.following')
157- : t('profile.call_to_action.follow'),
158- toggled: state.about.following === true,
159- }),
160- ]),
161- ]),
162-
163- h(View, {style: styles.descriptionArea}, [
164- state.about.description
165- ? h(Button, {
166- sel: 'bio',
167- text: t('profile.call_to_action.open_biography.label'),
168- small: true,
169- style: styles.bioButton,
170- accessible: true,
171- accessibilityLabel: t(
172- 'profile.call_to_action.open_biography.accessibility_label',
173- ),
174- strong: false,
175- })
176- : null,
177- ]),
178-
179256 isBlocked
180257 ? h(EmptySection, {
181258 style: styles.emptySection,
182259 title: t('profile.empty.blocked.title'),
@@ -192,9 +269,11 @@
192269 ? ssbSource.publishHook$.filter(isPublic).filter(isRootPostMsg)
193270 : null,
194271 selfFeedId: state.selfFeedId,
195272 lastSessionTimestamp: state.lastSessionTimestamp,
196- style: isSelfProfile ? styles.feedWithHeader : styles.feed,
273+ yOffsetAnimVal: scrollHeaderBy,
274+ HeaderComponent: h(ProfileHeader, {state}),
275+ style: styles.feed,
197276 EmptyComponent: isSelfProfile
198277 ? h(EmptySection, {
199278 style: styles.emptySection,
200279 image: require('../../../../images/noun-plant.png'),

Built with git-ssb-web