tangled
alpha
login
or
join now
robinwobin.dev
/
witchsky.app
forked from
jollywhoppers.com/witchsky.app
0
fork
atom
Bluesky app fork with some witchin' additions 💫
0
fork
atom
overview
issues
pulls
pipelines
Merge branch 'simplify' into main
Paul Frazee
3 years ago
e858bb52
b2dba9a1
+180
-232
20 changed files
expand all
collapse all
unified
split
src
build-flags.ts
state
index.ts
models
navigation.ts
view
com
composer
Prompt.tsx
modals
ServerInput.tsx
onboard
FeatureExplainer.tsx
post
Post.tsx
post-thread
PostThreadItem.tsx
posts
Feed.tsx
FeedItem.tsx
profile
ProfileHeader.tsx
util
DropdownBtn.tsx
LoadingPlaceholder.tsx
PostCtrls.tsx
ViewHeader.tsx
lib
icons.tsx
screens
Home.tsx
Notifications.tsx
Profile.tsx
shell
mobile
index.tsx
+2
src/build-flags.ts
···
1
1
+
export const LOGIN_INCLUDE_DEV_SERVERS = true
2
2
+
export const TABS_ENABLED = false
-1
src/state/index.ts
···
5
5
import * as libapi from './lib/api'
6
6
import * as storage from './lib/storage'
7
7
8
8
-
export const IS_PROD_BUILD = true
9
8
export const LOCAL_DEV_SERVICE = 'http://localhost:2583'
10
9
export const STAGING_SERVICE = 'https://pds.staging.bsky.dev'
11
10
export const PROD_SERVICE = 'https://bsky.social'
+10
-1
src/state/models/navigation.ts
···
1
1
import {makeAutoObservable} from 'mobx'
2
2
-
import {isObj, hasProp} from '../lib/type-guards'
2
2
+
import {TABS_ENABLED} from '../../build-flags'
3
3
4
4
let __id = 0
5
5
function genId() {
···
244
244
// =
245
245
246
246
newTab(url: string, title?: string) {
247
247
+
if (!TABS_ENABLED) {
248
248
+
return this.navigate(url)
249
249
+
}
247
250
const tab = new NavigationTabModel()
248
251
tab.navigate(url, title)
249
252
tab.isNewTab = true
···
252
255
}
253
256
254
257
setActiveTab(tabIndex: number) {
258
258
+
if (!TABS_ENABLED) {
259
259
+
return
260
260
+
}
255
261
this.tabIndex = Math.max(Math.min(tabIndex, this.tabs.length - 1), 0)
256
262
}
257
263
258
264
closeTab(tabIndex: number) {
265
265
+
if (!TABS_ENABLED) {
266
266
+
return
267
267
+
}
259
268
this.tabs = [
260
269
...this.tabs.slice(0, tabIndex),
261
270
...this.tabs.slice(tabIndex + 1),
+63
src/view/com/composer/Prompt.tsx
···
1
1
+
import React from 'react'
2
2
+
import {StyleSheet, Text, TouchableOpacity, View} from 'react-native'
3
3
+
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
4
4
+
import {colors} from '../../lib/styles'
5
5
+
import {useStores} from '../../../state'
6
6
+
import {UserAvatar} from '../util/UserAvatar'
7
7
+
8
8
+
export function ComposePrompt({onPressCompose}: {onPressCompose: () => void}) {
9
9
+
const store = useStores()
10
10
+
const onPressAvatar = () => {
11
11
+
store.nav.navigate(`/profile/${store.me.handle}`)
12
12
+
}
13
13
+
return (
14
14
+
<TouchableOpacity style={styles.container} onPress={onPressCompose}>
15
15
+
<TouchableOpacity style={styles.avatar} onPress={onPressAvatar}>
16
16
+
<UserAvatar
17
17
+
size={50}
18
18
+
handle={store.me.handle || ''}
19
19
+
displayName={store.me.displayName}
20
20
+
/>
21
21
+
</TouchableOpacity>
22
22
+
<View style={styles.textContainer}>
23
23
+
<Text style={styles.text}>What's happening?</Text>
24
24
+
</View>
25
25
+
<View style={styles.btn}>
26
26
+
<Text style={styles.btnText}>Post</Text>
27
27
+
</View>
28
28
+
</TouchableOpacity>
29
29
+
)
30
30
+
}
31
31
+
32
32
+
const styles = StyleSheet.create({
33
33
+
container: {
34
34
+
borderRadius: 6,
35
35
+
margin: 2,
36
36
+
marginBottom: 0,
37
37
+
paddingHorizontal: 10,
38
38
+
paddingVertical: 10,
39
39
+
flexDirection: 'row',
40
40
+
alignItems: 'center',
41
41
+
backgroundColor: colors.white,
42
42
+
},
43
43
+
avatar: {
44
44
+
width: 50,
45
45
+
},
46
46
+
textContainer: {
47
47
+
marginLeft: 10,
48
48
+
flex: 1,
49
49
+
},
50
50
+
text: {
51
51
+
color: colors.gray4,
52
52
+
fontSize: 17,
53
53
+
},
54
54
+
btn: {
55
55
+
backgroundColor: colors.gray1,
56
56
+
paddingVertical: 6,
57
57
+
paddingHorizontal: 14,
58
58
+
borderRadius: 30,
59
59
+
},
60
60
+
btnText: {
61
61
+
color: colors.gray5,
62
62
+
},
63
63
+
})
+2
-2
src/view/com/modals/ServerInput.tsx
···
5
5
import {useStores} from '../../../state'
6
6
import {s, colors} from '../../lib/styles'
7
7
import {
8
8
-
IS_PROD_BUILD,
9
8
LOCAL_DEV_SERVICE,
10
9
STAGING_SERVICE,
11
10
PROD_SERVICE,
12
11
} from '../../../state/index'
12
12
+
import {LOGIN_INCLUDE_DEV_SERVERS} from '../../../build-flags'
13
13
14
14
export const snapPoints = ['80%']
15
15
···
36
36
<Text style={[s.textCenter, s.bold, s.f18]}>Choose Service</Text>
37
37
<BottomSheetScrollView style={styles.inner}>
38
38
<View style={styles.group}>
39
39
-
{!IS_PROD_BUILD ? (
39
39
+
{LOGIN_INCLUDE_DEV_SERVERS ? (
40
40
<>
41
41
<TouchableOpacity
42
42
style={styles.btn}
+3
-2
src/view/com/onboard/FeatureExplainer.tsx
···
15
15
import {useStores} from '../../../state'
16
16
import {s} from '../../lib/styles'
17
17
import {SCENE_EXPLAINER, TABS_EXPLAINER} from '../../lib/assets'
18
18
+
import {TABS_ENABLED} from '../../../build-flags'
18
19
19
20
const Intro = () => (
20
21
<View style={styles.explainer}>
···
85
86
const routes = [
86
87
{key: 'intro', title: 'Intro'},
87
88
{key: 'scenes', title: 'Scenes'},
88
88
-
{key: 'tabs', title: 'Tabs'},
89
89
-
]
89
89
+
TABS_ENABLED ? {key: 'tabs', title: 'Tabs'} : undefined,
90
90
+
].filter(Boolean)
90
91
91
92
const onPressSkip = () => store.onboard.next()
92
93
const onPressNext = () => {
+1
-33
src/view/com/post-thread/PostThreadItem.tsx
···
29
29
const store = useStores()
30
30
const [deleted, setDeleted] = useState(false)
31
31
const record = item.record as unknown as PostType.Record
32
32
-
const hasEngagement =
33
33
-
item.upvoteCount || item.downvoteCount || item.repostCount
32
32
+
const hasEngagement = item.upvoteCount || item.repostCount
34
33
35
34
const itemHref = useMemo(() => {
36
35
const urip = new AtUri(item.uri)
···
44
43
return `/profile/${item.author.handle}/post/${urip.rkey}/upvoted-by`
45
44
}, [item.uri, item.author.handle])
46
45
const upvotesTitle = 'Upvotes on this post'
47
47
-
const downvotesHref = useMemo(() => {
48
48
-
const urip = new AtUri(item.uri)
49
49
-
return `/profile/${item.author.handle}/post/${urip.rkey}/downvoted-by`
50
50
-
}, [item.uri, item.author.handle])
51
51
-
const downvotesTitle = 'Downvotes on this post'
52
46
const repostsHref = useMemo(() => {
53
47
const urip = new AtUri(item.uri)
54
48
return `/profile/${item.author.handle}/post/${urip.rkey}/reposted-by`
···
70
64
item
71
65
.toggleUpvote()
72
66
.catch(e => console.error('Failed to toggle upvote', record, e))
73
73
-
}
74
74
-
const onPressToggleDownvote = () => {
75
75
-
item
76
76
-
.toggleDownvote()
77
77
-
.catch(e => console.error('Failed to toggle downvote', record, e))
78
67
}
79
68
const onDeletePost = () => {
80
69
item.delete().then(
···
186
175
) : (
187
176
<></>
188
177
)}
189
189
-
{item.downvoteCount ? (
190
190
-
<Link
191
191
-
style={styles.expandedInfoItem}
192
192
-
href={downvotesHref}
193
193
-
title={downvotesTitle}>
194
194
-
<Text style={[s.gray5, s.semiBold, s.f18]}>
195
195
-
<Text style={[s.bold, s.black, s.f18]}>
196
196
-
{item.downvoteCount}
197
197
-
</Text>{' '}
198
198
-
{pluralize(item.downvoteCount, 'downvote')}
199
199
-
</Text>
200
200
-
</Link>
201
201
-
) : (
202
202
-
<></>
203
203
-
)}
204
178
</View>
205
179
) : (
206
180
<></>
···
210
184
replyCount={item.replyCount}
211
185
repostCount={item.repostCount}
212
186
upvoteCount={item.upvoteCount}
213
213
-
downvoteCount={item.downvoteCount}
214
187
isReposted={!!item.myState.repost}
215
188
isUpvoted={!!item.myState.upvote}
216
216
-
isDownvoted={!!item.myState.downvote}
217
189
onPressReply={onPressReply}
218
190
onPressToggleRepost={onPressToggleRepost}
219
191
onPressToggleUpvote={onPressToggleUpvote}
220
220
-
onPressToggleDownvote={onPressToggleDownvote}
221
192
/>
222
193
</View>
223
194
</View>
···
299
270
replyCount={item.replyCount}
300
271
repostCount={item.repostCount}
301
272
upvoteCount={item.upvoteCount}
302
302
-
downvoteCount={item.downvoteCount}
303
273
isReposted={!!item.myState.repost}
304
274
isUpvoted={!!item.myState.upvote}
305
305
-
isDownvoted={!!item.myState.downvote}
306
275
onPressReply={onPressReply}
307
276
onPressToggleRepost={onPressToggleRepost}
308
277
onPressToggleUpvote={onPressToggleUpvote}
309
309
-
onPressToggleDownvote={onPressToggleDownvote}
310
278
/>
311
279
</View>
312
280
</View>
-8
src/view/com/post/Post.tsx
···
85
85
.toggleUpvote()
86
86
.catch(e => console.error('Failed to toggle upvote', record, e))
87
87
}
88
88
-
const onPressToggleDownvote = () => {
89
89
-
item
90
90
-
.toggleDownvote()
91
91
-
.catch(e => console.error('Failed to toggle downvote', record, e))
92
92
-
}
93
88
const onDeletePost = () => {
94
89
item.delete().then(
95
90
() => {
···
154
149
replyCount={item.replyCount}
155
150
repostCount={item.repostCount}
156
151
upvoteCount={item.upvoteCount}
157
157
-
downvoteCount={item.downvoteCount}
158
152
isReposted={!!item.myState.repost}
159
153
isUpvoted={!!item.myState.upvote}
160
160
-
isDownvoted={!!item.myState.downvote}
161
154
onPressReply={onPressReply}
162
155
onPressToggleRepost={onPressToggleRepost}
163
156
onPressToggleUpvote={onPressToggleUpvote}
164
164
-
onPressToggleDownvote={onPressToggleDownvote}
165
157
/>
166
158
</View>
167
159
</View>
+13
-2
src/view/com/posts/Feed.tsx
···
6
6
import {ErrorMessage} from '../util/ErrorMessage'
7
7
import {FeedModel, FeedItemModel} from '../../../state/models/feed-view'
8
8
import {FeedItem} from './FeedItem'
9
9
+
import {ComposePrompt} from '../composer/Prompt'
10
10
+
11
11
+
const COMPOSE_PROMPT_ITEM = {_reactKey: '__prompt__'}
9
12
10
13
export const Feed = observer(function Feed({
11
14
feed,
12
15
style,
13
16
scrollElRef,
17
17
+
onPressCompose,
14
18
onPressTryAgain,
15
19
}: {
16
20
feed: FeedModel
17
21
style?: StyleProp<ViewStyle>
18
22
scrollElRef?: MutableRefObject<FlatList<any> | null>
23
23
+
onPressCompose?: () => void
19
24
onPressTryAgain?: () => void
20
25
}) {
21
26
// TODO optimize renderItem or FeedItem, we're getting this notice from RN: -prf
22
27
// VirtualizedList: You have a large list that is slow to update - make sure your
23
28
// renderItem function renders components that follow React performance best practices
24
29
// like PureComponent, shouldComponentUpdate, etc
25
25
-
const renderItem = ({item}: {item: FeedItemModel}) => <FeedItem item={item} />
30
30
+
const renderItem = ({item}: {item: FeedItemModel}) => {
31
31
+
if (item === COMPOSE_PROMPT_ITEM) {
32
32
+
return <ComposePrompt onPressCompose={onPressCompose} />
33
33
+
} else {
34
34
+
return <FeedItem item={item} />
35
35
+
}
36
36
+
}
26
37
const onRefresh = () => {
27
38
feed.refresh().catch(err => console.error('Failed to refresh', err))
28
39
}
···
45
56
{feed.hasContent && (
46
57
<FlatList
47
58
ref={scrollElRef}
48
48
-
data={feed.feed.slice()}
59
59
+
data={[COMPOSE_PROMPT_ITEM].concat(feed.feed.slice())}
49
60
keyExtractor={item => item._reactKey}
50
61
renderItem={renderItem}
51
62
refreshing={feed.isRefreshing}
-8
src/view/com/posts/FeedItem.tsx
···
53
53
.toggleUpvote()
54
54
.catch(e => console.error('Failed to toggle upvote', record, e))
55
55
}
56
56
-
const onPressToggleDownvote = () => {
57
57
-
item
58
58
-
.toggleDownvote()
59
59
-
.catch(e => console.error('Failed to toggle downvote', record, e))
60
60
-
}
61
56
const onDeletePost = () => {
62
57
item.delete().then(
63
58
() => {
···
150
145
replyCount={item.replyCount}
151
146
repostCount={item.repostCount}
152
147
upvoteCount={item.upvoteCount}
153
153
-
downvoteCount={item.downvoteCount}
154
148
isReposted={!!item.myState.repost}
155
149
isUpvoted={!!item.myState.upvote}
156
156
-
isDownvoted={!!item.myState.downvote}
157
150
onPressReply={onPressReply}
158
151
onPressToggleRepost={onPressToggleRepost}
159
152
onPressToggleUpvote={onPressToggleUpvote}
160
160
-
onPressToggleDownvote={onPressToggleDownvote}
161
153
/>
162
154
</View>
163
155
</View>
+14
-24
src/view/com/profile/ProfileHeader.tsx
···
20
20
import {pluralize} from '../../lib/strings'
21
21
import {s, colors} from '../../lib/styles'
22
22
import {getGradient} from '../../lib/asset-gen'
23
23
+
import {MagnifyingGlassIcon} from '../../lib/icons'
23
24
import {DropdownBtn, DropdownItem} from '../util/DropdownBtn'
24
25
import Toast from '../util/Toast'
25
26
import {LoadingPlaceholder} from '../util/LoadingPlaceholder'
···
43
44
const onPressBack = () => {
44
45
store.nav.tab.goBack()
45
46
}
46
46
-
const onPressMyAvatar = () => {
47
47
-
if (store.me.handle) {
48
48
-
store.nav.navigate(`/profile/${store.me.handle}`)
49
49
-
}
47
47
+
const onPressSearch = () => {
48
48
+
store.nav.navigate(`/search`)
50
49
}
51
50
const onPressToggleFollow = () => {
52
51
view?.toggleFollowing().then(
···
117
116
/>
118
117
</TouchableOpacity>
119
118
) : undefined}
120
120
-
{store.me.did ? (
121
121
-
<TouchableOpacity style={styles.myAvatar} onPress={onPressMyAvatar}>
122
122
-
<UserAvatar
123
123
-
size={30}
124
124
-
handle={store.me.handle || ''}
125
125
-
displayName={store.me.displayName}
126
126
-
/>
127
127
-
</TouchableOpacity>
128
128
-
) : undefined}
119
119
+
<TouchableOpacity style={styles.searchBtn} onPress={onPressSearch}>
120
120
+
<MagnifyingGlassIcon size={19} style={styles.searchIcon} />
121
121
+
</TouchableOpacity>
129
122
<View style={styles.avi}>
130
123
<LoadingPlaceholder
131
124
width={80}
···
194
187
/>
195
188
</TouchableOpacity>
196
189
) : undefined}
197
197
-
{store.me.did ? (
198
198
-
<TouchableOpacity style={styles.myAvatar} onPress={onPressMyAvatar}>
199
199
-
<UserAvatar
200
200
-
size={30}
201
201
-
handle={store.me.handle || ''}
202
202
-
displayName={store.me.displayName}
203
203
-
/>
204
204
-
</TouchableOpacity>
205
205
-
) : undefined}
190
190
+
<TouchableOpacity style={styles.searchBtn} onPress={onPressSearch}>
191
191
+
<MagnifyingGlassIcon size={19} style={styles.searchIcon} />
192
192
+
</TouchableOpacity>
206
193
<View style={styles.avi}>
207
194
<UserAvatar
208
195
size={80}
···
375
362
height: 14,
376
363
color: colors.black,
377
364
},
378
378
-
myAvatar: {
365
365
+
searchBtn: {
379
366
position: 'absolute',
380
367
top: 10,
381
368
right: 12,
382
369
backgroundColor: '#ffff',
383
383
-
padding: 1,
370
370
+
padding: 5,
384
371
borderRadius: 30,
372
372
+
},
373
373
+
searchIcon: {
374
374
+
color: colors.black,
385
375
},
386
376
avi: {
387
377
position: 'absolute',
+10
-7
src/view/com/util/DropdownBtn.tsx
···
16
16
import {toShareUrl} from '../../lib/strings'
17
17
import {useStores} from '../../../state'
18
18
import {ConfirmModel} from '../../../state/models/shell-ui'
19
19
+
import {TABS_ENABLED} from '../../../build-flags'
19
20
20
21
export interface DropdownItem {
21
22
icon?: IconProp
···
84
85
const store = useStores()
85
86
86
87
const dropdownItems: DropdownItem[] = [
87
87
-
{
88
88
-
icon: ['far', 'clone'],
89
89
-
label: 'Open in new tab',
90
90
-
onPress() {
91
91
-
store.nav.newTab(itemHref)
92
92
-
},
93
93
-
},
88
88
+
TABS_ENABLED
89
89
+
? {
90
90
+
icon: ['far', 'clone'],
91
91
+
label: 'Open in new tab',
92
92
+
onPress() {
93
93
+
store.nav.newTab(itemHref)
94
94
+
},
95
95
+
}
96
96
+
: undefined,
94
97
{
95
98
icon: 'share',
96
99
label: 'Share...',
+5
-7
src/view/com/util/LoadingPlaceholder.tsx
···
9
9
} from 'react-native'
10
10
import LinearGradient from 'react-native-linear-gradient'
11
11
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
12
12
-
import {UpIcon, DownIcon} from '../../lib/icons'
12
12
+
import {UpIcon} from '../../lib/icons'
13
13
import {s, colors} from '../../lib/styles'
14
14
15
15
export function LoadingPlaceholder({
···
93
93
<FontAwesomeIcon
94
94
style={s.gray3}
95
95
icon={['far', 'comment']}
96
96
-
size={14}
96
96
+
size={16}
97
97
/>
98
98
</View>
99
99
<View style={s.flex1}>
100
100
-
<FontAwesomeIcon style={s.gray3} icon="retweet" size={18} />
101
101
-
</View>
102
102
-
<View style={s.flex1}>
103
103
-
<UpIcon style={s.gray3} size={18} />
100
100
+
<FontAwesomeIcon style={s.gray3} icon="retweet" size={20} />
104
101
</View>
105
102
<View style={s.flex1}>
106
106
-
<DownIcon style={s.gray3} size={18} />
103
103
+
<UpIcon style={s.gray3} size={19} strokeWidth={1.7} />
107
104
</View>
105
105
+
<View style={s.flex1}></View>
108
106
</View>
109
107
</View>
110
108
</View>
+11
-52
src/view/com/util/PostCtrls.tsx
···
8
8
interpolate,
9
9
} from 'react-native-reanimated'
10
10
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
11
11
-
import {UpIcon, UpIconSolid, DownIcon, DownIconSolid} from '../../lib/icons'
11
11
+
import {UpIcon, UpIconSolid} from '../../lib/icons'
12
12
import {s, colors} from '../../lib/styles'
13
13
14
14
interface PostCtrlsOpts {
15
15
replyCount: number
16
16
repostCount: number
17
17
upvoteCount: number
18
18
-
downvoteCount: number
19
18
isReposted: boolean
20
19
isUpvoted: boolean
21
21
-
isDownvoted: boolean
22
20
onPressReply: () => void
23
21
onPressToggleRepost: () => void
24
22
onPressToggleUpvote: () => void
25
25
-
onPressToggleDownvote: () => void
26
23
}
27
24
28
25
export function PostCtrls(opts: PostCtrlsOpts) {
29
26
const interp1 = useSharedValue<number>(0)
30
27
const interp2 = useSharedValue<number>(0)
31
31
-
const interp3 = useSharedValue<number>(0)
32
28
33
29
const anim1Style = useAnimatedStyle(() => ({
34
30
transform: [{scale: interpolate(interp1.value, [0, 1.0], [1.0, 3.0])}],
···
38
34
transform: [{scale: interpolate(interp2.value, [0, 1.0], [1.0, 3.0])}],
39
35
opacity: interpolate(interp2.value, [0, 1.0], [1.0, 0.0]),
40
36
}))
41
41
-
const anim3Style = useAnimatedStyle(() => ({
42
42
-
transform: [{scale: interpolate(interp3.value, [0, 1.0], [1.0, 3.0])}],
43
43
-
opacity: interpolate(interp3.value, [0, 1.0], [1.0, 0.0]),
44
44
-
}))
45
37
46
38
const onPressToggleRepostWrapper = () => {
47
39
if (!opts.isReposted) {
···
59
51
}
60
52
opts.onPressToggleUpvote()
61
53
}
62
62
-
const onPressToggleDownvoteWrapper = () => {
63
63
-
if (!opts.isDownvoted) {
64
64
-
interp3.value = withTiming(1, {duration: 300}, () => {
65
65
-
interp3.value = withDelay(100, withTiming(0, {duration: 20}))
66
66
-
})
67
67
-
}
68
68
-
opts.onPressToggleDownvote()
69
69
-
}
70
54
71
55
return (
72
56
<View style={styles.ctrls}>
···
75
59
<FontAwesomeIcon
76
60
style={styles.ctrlIcon}
77
61
icon={['far', 'comment']}
78
78
-
size={14}
62
62
+
size={16}
79
63
/>
80
80
-
<Text style={[s.gray5, s.ml5, s.f13]}>{opts.replyCount}</Text>
64
64
+
<Text style={[s.gray5, s.ml5, s.f17]}>{opts.replyCount}</Text>
81
65
</TouchableOpacity>
82
66
</View>
83
67
<View style={s.flex1}>
···
90
74
opts.isReposted ? styles.ctrlIconReposted : styles.ctrlIcon
91
75
}
92
76
icon="retweet"
93
93
-
size={18}
77
77
+
size={20}
94
78
/>
95
79
</Animated.View>
96
80
<Text
97
81
style={
98
82
opts.isReposted
99
99
-
? [s.bold, s.green3, s.f13, s.ml5]
100
100
-
: [s.gray5, s.f13, s.ml5]
83
83
+
? [s.bold, s.green3, s.f17, s.ml5]
84
84
+
: [s.gray5, s.f17, s.ml5]
101
85
}>
102
86
{opts.repostCount}
103
87
</Text>
···
109
93
onPress={onPressToggleUpvoteWrapper}>
110
94
<Animated.View style={anim2Style}>
111
95
{opts.isUpvoted ? (
112
112
-
<UpIconSolid style={styles.ctrlIconUpvoted} size={18} />
96
96
+
<UpIconSolid style={[styles.ctrlIconUpvoted]} size={19} />
113
97
) : (
114
114
-
<UpIcon style={styles.ctrlIcon} size={18} />
98
98
+
<UpIcon style={[styles.ctrlIcon]} size={20} strokeWidth={1.5} />
115
99
)}
116
100
</Animated.View>
117
101
<Text
118
102
style={
119
103
opts.isUpvoted
120
120
-
? [s.bold, s.red3, s.f13, s.ml5]
121
121
-
: [s.gray5, s.f13, s.ml5]
104
104
+
? [s.bold, s.red3, s.f17, s.ml5]
105
105
+
: [s.gray5, s.f17, s.ml5]
122
106
}>
123
107
{opts.upvoteCount}
124
108
</Text>
125
109
</TouchableOpacity>
126
110
</View>
127
127
-
<View style={s.flex1}>
128
128
-
<TouchableOpacity
129
129
-
style={styles.ctrl}
130
130
-
onPress={onPressToggleDownvoteWrapper}>
131
131
-
<Animated.View style={anim3Style}>
132
132
-
{opts.isDownvoted ? (
133
133
-
<DownIconSolid style={styles.ctrlIconDownvoted} size={18} />
134
134
-
) : (
135
135
-
<DownIcon style={styles.ctrlIcon} size={18} />
136
136
-
)}
137
137
-
</Animated.View>
138
138
-
<Text
139
139
-
style={
140
140
-
opts.isDownvoted
141
141
-
? [s.bold, s.blue3, s.f13, s.ml5]
142
142
-
: [s.gray5, s.f13, s.ml5]
143
143
-
}>
144
144
-
{opts.downvoteCount}
145
145
-
</Text>
146
146
-
</TouchableOpacity>
147
147
-
</View>
111
111
+
<View style={s.flex1}></View>
148
112
</View>
149
113
)
150
114
}
···
152
116
const styles = StyleSheet.create({
153
117
ctrls: {
154
118
flexDirection: 'row',
155
155
-
paddingRight: 20,
156
119
},
157
120
ctrl: {
158
121
flexDirection: 'row',
159
122
alignItems: 'center',
160
160
-
paddingLeft: 4,
161
123
paddingRight: 4,
162
124
},
163
125
ctrlIcon: {
···
168
130
},
169
131
ctrlIconUpvoted: {
170
132
color: colors.red3,
171
171
-
},
172
172
-
ctrlIconDownvoted: {
173
173
-
color: colors.blue3,
174
133
},
175
134
})
+24
-19
src/view/com/util/ViewHeader.tsx
···
3
3
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
4
4
import {UserAvatar} from './UserAvatar'
5
5
import {colors} from '../../lib/styles'
6
6
+
import {MagnifyingGlassIcon} from '../../lib/icons'
6
7
import {useStores} from '../../../state'
7
8
8
9
export function ViewHeader({
···
16
17
const onPressBack = () => {
17
18
store.nav.tab.goBack()
18
19
}
19
19
-
const onPressAvatar = () => {
20
20
-
if (store.me.handle) {
21
21
-
store.nav.navigate(`/profile/${store.me.handle}`)
22
22
-
}
20
20
+
const onPressSearch = () => {
21
21
+
store.nav.navigate(`/search`)
23
22
}
24
23
return (
25
24
<View style={styles.header}>
26
25
{store.nav.tab.canGoBack ? (
27
26
<TouchableOpacity onPress={onPressBack} style={styles.backIcon}>
28
28
-
<FontAwesomeIcon size={18} icon="angle-left" style={{marginTop: 3}} />
27
27
+
<FontAwesomeIcon size={18} icon="angle-left" style={{marginTop: 6}} />
29
28
</TouchableOpacity>
30
29
) : (
31
30
<View style={styles.cornerPlaceholder} />
···
38
37
</Text>
39
38
) : undefined}
40
39
</View>
41
41
-
{store.me.did ? (
42
42
-
<TouchableOpacity onPress={onPressAvatar}>
43
43
-
<UserAvatar
44
44
-
size={24}
45
45
-
handle={store.me.handle || ''}
46
46
-
displayName={store.me.displayName}
47
47
-
/>
48
48
-
</TouchableOpacity>
49
49
-
) : (
50
50
-
<View style={styles.cornerPlaceholder} />
51
51
-
)}
40
40
+
<TouchableOpacity onPress={onPressSearch} style={styles.searchBtn}>
41
41
+
<MagnifyingGlassIcon size={17} style={styles.searchBtnIcon} />
42
42
+
</TouchableOpacity>
52
43
</View>
53
44
)
54
45
}
···
83
74
},
84
75
85
76
cornerPlaceholder: {
86
86
-
width: 24,
87
87
-
height: 24,
77
77
+
width: 30,
78
78
+
height: 30,
88
79
},
89
89
-
backIcon: {width: 24, height: 24},
80
80
+
backIcon: {width: 30, height: 30},
81
81
+
searchBtn: {
82
82
+
flexDirection: 'row',
83
83
+
alignItems: 'center',
84
84
+
justifyContent: 'center',
85
85
+
backgroundColor: colors.gray1,
86
86
+
width: 30,
87
87
+
height: 30,
88
88
+
borderRadius: 15,
89
89
+
},
90
90
+
searchBtnIcon: {
91
91
+
color: colors.black,
92
92
+
position: 'relative',
93
93
+
top: -1,
94
94
+
},
90
95
})
+12
-29
src/view/lib/icons.tsx
···
91
91
92
92
// Copyright (c) 2020 Refactoring UI Inc.
93
93
// https://github.com/tailwindlabs/heroicons/blob/master/LICENSE
94
94
-
export function MangifyingGlassIcon({
94
94
+
export function MagnifyingGlassIcon({
95
95
style,
96
96
size,
97
97
}: {
···
116
116
)
117
117
}
118
118
119
119
-
// Copyright (c) 2020 Refactoring UI Inc.
120
120
-
// https://github.com/tailwindlabs/heroicons/blob/master/LICENSE
121
121
-
export function MangifyingGlassIconSolid({
122
122
-
style,
123
123
-
size,
124
124
-
}: {
125
125
-
style?: StyleProp<ViewStyle>
126
126
-
size?: string | number
127
127
-
}) {
128
128
-
return (
129
129
-
<Svg
130
130
-
fill="none"
131
131
-
viewBox="0 0 24 24"
132
132
-
strokeWidth={3}
133
133
-
stroke="currentColor"
134
134
-
width={size || 24}
135
135
-
height={size || 24}
136
136
-
style={style}>
137
137
-
<Path
138
138
-
strokeLinecap="round"
139
139
-
strokeLinejoin="round"
140
140
-
d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z"
141
141
-
/>
142
142
-
</Svg>
143
143
-
)
144
144
-
}
145
145
-
146
119
// https://github.com/Remix-Design/RemixIcon/blob/master/License
147
120
export function BellIcon({
148
121
style,
···
221
194
export function UpIcon({
222
195
style,
223
196
size,
197
197
+
strokeWidth = 1.3,
224
198
}: {
225
199
style?: StyleProp<ViewStyle>
226
200
size?: string | number
201
201
+
strokeWidth: number
227
202
}) {
228
203
return (
229
204
<Svg
···
232
207
height={size || 24}
233
208
style={style}>
234
209
<Path
235
235
-
strokeWidth={1.3}
210
210
+
strokeWidth={strokeWidth}
236
211
stroke="currentColor"
212
212
+
strokeLinecap="round"
213
213
+
strokeLinejoin="round"
237
214
d="M 7 3 L 2 8 L 4.5 8 L 4.5 11.5 L 9.5 11.5 L 9.5 8 L 12 8 L 7 3 Z"
238
215
/>
239
216
</Svg>
···
257
234
strokeWidth={1.3}
258
235
stroke="currentColor"
259
236
fill="currentColor"
237
237
+
strokeLinecap="round"
238
238
+
strokeLinejoin="round"
260
239
d="M 7 3 L 2 8 L 4.5 8 L 4.5 11.5 L 9.5 11.5 L 9.5 8 L 12 8 L 7 3 Z"
261
240
/>
262
241
</Svg>
···
279
258
<Path
280
259
strokeWidth={1.3}
281
260
stroke="currentColor"
261
261
+
strokeLinecap="round"
262
262
+
strokeLinejoin="round"
282
263
d="M 7 11.5 L 2 6.5 L 4.5 6.5 L 4.5 3 L 9.5 3 L 9.5 6.5 L 12 6.5 L 7 11.5 Z"
283
264
/>
284
265
</Svg>
···
302
283
strokeWidth={1.3}
303
284
stroke="currentColor"
304
285
fill="currentColor"
286
286
+
strokeLinecap="round"
287
287
+
strokeLinejoin="round"
305
288
d="M 7 11.5 L 2 6.5 L 4.5 6.5 L 4.5 3 L 9.5 3 L 9.5 6.5 L 12 6.5 L 7 11.5 Z"
306
289
/>
307
290
</Svg>
+2
-3
src/view/screens/Home.tsx
···
5
5
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
6
6
import {ViewHeader} from '../com/util/ViewHeader'
7
7
import {Feed} from '../com/posts/Feed'
8
8
-
import {FAB} from '../com/util/FloatingActionButton'
9
8
import {useStores} from '../../state'
10
9
import {FeedModel} from '../../state/models/feed-view'
11
10
import {ScreenParams} from '../routes'
···
65
64
}
66
65
}, [visible, store])
67
66
68
68
-
const onComposePress = () => {
67
67
+
const onPressCompose = () => {
69
68
store.shell.openComposer({onPost: onCreatePost})
70
69
}
71
70
const onCreatePost = () => {
···
87
86
feed={defaultFeedView}
88
87
scrollElRef={scrollElRef}
89
88
style={{flex: 1}}
89
89
+
onPressCompose={onPressCompose}
90
90
onPressTryAgain={onPressTryAgain}
91
91
/>
92
92
{defaultFeedView.hasNewLatest ? (
···
95
95
<Text style={styles.loadLatestText}>Load new posts</Text>
96
96
</TouchableOpacity>
97
97
) : undefined}
98
98
-
<FAB icon="pen-nib" onPress={onComposePress} />
99
98
</View>
100
99
)
101
100
})
-5
src/view/screens/Notifications.tsx
···
1
1
import React, {useState, useEffect} from 'react'
2
2
import {View} from 'react-native'
3
3
import {ViewHeader} from '../com/util/ViewHeader'
4
4
-
import {FAB} from '../com/util/FloatingActionButton'
5
4
import {Feed} from '../com/notifications/Feed'
6
5
import {useStores} from '../../state'
7
6
import {NotificationsViewModel} from '../../state/models/notifications-view'
···
37
36
}
38
37
}, [visible, store])
39
38
40
40
-
const onComposePress = () => {
41
41
-
store.shell.openComposer({})
42
42
-
}
43
39
const onPressTryAgain = () => {
44
40
notesView?.refresh()
45
41
}
···
48
44
<View style={{flex: 1}}>
49
45
<ViewHeader title="Notifications" />
50
46
{notesView && <Feed view={notesView} onPressTryAgain={onPressTryAgain} />}
51
51
-
<FAB icon="pen-nib" onPress={onComposePress} />
52
47
</View>
53
48
)
54
49
}
-5
src/view/screens/Profile.tsx
···
3
3
import {observer} from 'mobx-react-lite'
4
4
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
5
5
import {ViewSelector} from '../com/util/ViewSelector'
6
6
-
import {FAB} from '../com/util/FloatingActionButton'
7
6
import {ScreenParams} from '../routes'
8
7
import {ProfileUiModel, Sections} from '../../state/models/profile-ui'
9
8
import {MembershipItem} from '../../state/models/memberships-view'
···
85
84
},
86
85
),
87
86
)
88
88
-
}
89
89
-
const onComposePress = () => {
90
90
-
store.shell.openComposer({})
91
87
}
92
88
93
89
// rendering
···
241
237
) : (
242
238
renderHeader()
243
239
)}
244
244
-
<FAB icon="pen-nib" onPress={onComposePress} />
245
240
</View>
246
241
)
247
242
})
+8
-24
src/view/shell/mobile/index.tsx
···
26
26
} from 'react-native-reanimated'
27
27
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
28
28
import {IconProp} from '@fortawesome/fontawesome-svg-core'
29
29
+
import {TABS_ENABLED} from '../../../build-flags'
29
30
import {useStores} from '../../../state'
30
31
import {NavigationModel} from '../../../state/models/navigation'
31
32
import {match, MatchResult} from '../../routes'
···
41
42
GridIconSolid,
42
43
HomeIcon,
43
44
HomeIconSolid,
44
44
-
MangifyingGlassIcon,
45
45
-
MangifyingGlassIconSolid,
46
45
BellIcon,
47
46
BellIconSolid,
48
47
} from '../../lib/icons'
···
65
64
| 'home-solid'
66
65
| 'bell'
67
66
| 'bell-solid'
68
68
-
| 'search'
69
69
-
| 'search-solid'
70
67
notificationCount?: number
71
68
tabCount?: number
72
69
onPress?: (event: GestureResponderEvent) => void
···
85
82
} else if (icon === 'home-solid') {
86
83
IconEl = HomeIconSolid
87
84
size = 24
88
88
-
} else if (icon === 'search') {
89
89
-
IconEl = MangifyingGlassIcon
90
90
-
size = 24
91
91
-
addedStyles = {position: 'relative', top: -1} as ViewStyle
92
92
-
} else if (icon === 'search-solid') {
93
93
-
IconEl = MangifyingGlassIconSolid
94
94
-
size = 24
95
95
-
addedStyles = {position: 'relative', top: -1} as ViewStyle
96
85
} else if (icon === 'bell') {
97
86
IconEl = BellIcon
98
87
size = 24
···
147
136
store.nav.navigate('/')
148
137
}
149
138
}
150
150
-
const onPressSearch = () => store.nav.navigate('/search')
151
139
const onPressMenu = () => setMainMenuActive(true)
152
140
const onPressNotifications = () => store.nav.navigate('/notifications')
153
141
const onPressTabs = () => toggleTabsMenu(!isTabsSelectorActive)
···
261
249
}
262
250
263
251
const isAtHome = store.nav.tab.current.url === '/'
264
264
-
const isAtSearch = store.nav.tab.current.url === '/search'
265
252
const isAtNotifications = store.nav.tab.current.url === '/notifications'
266
253
return (
267
254
<View style={styles.outerContainer}>
···
326
313
onPress={onPressHome}
327
314
onLongPress={doNewTab('/')}
328
315
/>
329
329
-
<Btn
330
330
-
icon={isAtSearch ? 'search-solid' : 'search'}
331
331
-
onPress={onPressSearch}
332
332
-
onLongPress={doNewTab('/search')}
333
333
-
/>
334
334
-
<Btn
335
335
-
icon={isTabsSelectorActive ? 'clone' : ['far', 'clone']}
336
336
-
onPress={onPressTabs}
337
337
-
tabCount={store.nav.tabCount}
338
338
-
/>
316
316
+
{TABS_ENABLED ? (
317
317
+
<Btn
318
318
+
icon={isTabsSelectorActive ? 'clone' : ['far', 'clone']}
319
319
+
onPress={onPressTabs}
320
320
+
tabCount={store.nav.tabCount}
321
321
+
/>
322
322
+
) : undefined}
339
323
<Btn
340
324
icon={isAtNotifications ? 'bell-solid' : 'bell'}
341
325
onPress={onPressNotifications}