Merge branch 'main' into upload-image

This commit is contained in:
João Ferreiro 2022-11-28 16:56:05 +00:00
commit c5f3200d6b
27 changed files with 424 additions and 428 deletions

View file

@ -1,4 +1,4 @@
import React, {useEffect, useMemo, useState} from 'react'
import React, {useEffect, useMemo, useRef, useState} from 'react'
import {observer} from 'mobx-react-lite'
import {
ActivityIndicator,
@ -17,9 +17,10 @@ import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {UserAutocompleteViewModel} from '../../../state/models/user-autocomplete-view'
import {UserLocalPhotosModel} from '../../../state/models/user-local-photos'
import {Autocomplete} from './Autocomplete'
import Toast from '../util/Toast'
import * as Toast from '../util/Toast'
import ProgressCircle from '../util/ProgressCircle'
import {TextLink} from '../util/Link'
import {UserAvatar} from '../util/UserAvatar'
import {useStores} from '../../../state'
import * as apilib from '../../../state/lib/api'
import {ComposerOpts} from '../../../state/models/shell-ui'
@ -28,7 +29,6 @@ import {detectLinkables} from '../../../lib/strings'
import {openPicker, openCamera} from 'react-native-image-crop-picker'
const MAX_TEXT_LENGTH = 256
const WARNING_TEXT_LENGTH = 200
const DANGER_TEXT_LENGTH = MAX_TEXT_LENGTH
export const ComposePost = observer(function ComposePost({
@ -41,6 +41,7 @@ export const ComposePost = observer(function ComposePost({
onClose: () => void
}) {
const store = useStores()
const textInput = useRef<TextInput>(null)
const [isProcessing, setIsProcessing] = useState(false)
const [error, setError] = useState('')
const [text, setText] = useState('')
@ -57,6 +58,22 @@ export const ComposePost = observer(function ComposePost({
useEffect(() => {
autocompleteView.setup()
})
useEffect(() => {
// HACK
// wait a moment before focusing the input to resolve some layout bugs with the keyboard-avoiding-view
// -prf
let to: NodeJS.Timeout | undefined
if (textInput.current) {
to = setTimeout(() => {
textInput.current?.focus()
}, 250)
}
return () => {
if (to) {
clearTimeout(to)
}
}
}, [textInput.current])
useEffect(() => {
localPhotos.setup()
@ -90,7 +107,10 @@ export const ComposePost = observer(function ComposePost({
}
setIsProcessing(true)
try {
await apilib.post(store, text, replyTo, autocompleteView.knownHandles)
const replyRef = replyTo
? {uri: replyTo.uri, cid: replyTo.cid}
: undefined
await apilib.post(store, text, replyRef, autocompleteView.knownHandles)
} catch (e: any) {
console.error(`Failed to create post: ${e.toString()}`)
setError(
@ -101,13 +121,7 @@ export const ComposePost = observer(function ComposePost({
}
onPost?.()
onClose()
Toast.show(`Your ${replyTo ? 'reply' : 'post'} has been published`, {
duration: Toast.durations.LONG,
position: Toast.positions.TOP,
shadow: true,
animation: true,
hideOnPress: true,
})
Toast.show(`Your ${replyTo ? 'reply' : 'post'} has been published`)
}
const onSelectAutocompleteItem = (item: string) => {
setText(replaceTextAutocompletePrefix(text, item))
@ -115,12 +129,7 @@ export const ComposePost = observer(function ComposePost({
}
const canPost = text.length <= MAX_TEXT_LENGTH
const progressColor =
text.length > DANGER_TEXT_LENGTH
? '#e60000'
: text.length > WARNING_TEXT_LENGTH
? '#f7c600'
: undefined
const progressColor = text.length > DANGER_TEXT_LENGTH ? '#e60000' : undefined
const textDecorated = useMemo(() => {
let i = 0
@ -142,7 +151,7 @@ export const ComposePost = observer(function ComposePost({
<SafeAreaView style={s.flex1}>
<View style={styles.topbar}>
<TouchableOpacity onPress={onPressCancel}>
<Text style={[s.blue3, s.f16]}>Cancel</Text>
<Text style={[s.blue3, s.f18]}>Cancel</Text>
</TouchableOpacity>
<View style={s.flex1} />
{isProcessing ? (
@ -156,7 +165,9 @@ export const ComposePost = observer(function ComposePost({
start={{x: 0, y: 0}}
end={{x: 1, y: 1}}
style={styles.postBtn}>
<Text style={[s.white, s.f16, s.bold]}>Post</Text>
<Text style={[s.white, s.f16, s.bold]}>
{replyTo ? 'Reply' : 'Post'}
</Text>
</LinearGradient>
</TouchableOpacity>
) : (
@ -178,39 +189,46 @@ export const ComposePost = observer(function ComposePost({
</View>
)}
{replyTo ? (
<View>
<Text style={s.gray4}>
Replying to{' '}
<View style={styles.replyToLayout}>
<UserAvatar
handle={replyTo.author.handle}
displayName={replyTo.author.displayName}
size={50}
/>
<View style={styles.replyToPost}>
<TextLink
href={`/profile/${replyTo.author.handle}`}
text={'@' + replyTo.author.handle}
style={[s.bold, s.gray5]}
text={replyTo.author.displayName || replyTo.author.handle}
style={[s.f16, s.bold]}
/>
</Text>
<View style={styles.replyToPost}>
<Text style={s.gray5}>{replyTo.text}</Text>
<Text style={[s.f16, s['lh16-1.3']]} numberOfLines={6}>
{replyTo.text}
</Text>
</View>
</View>
) : undefined}
<TextInput
multiline
scrollEnabled
onChangeText={(text: string) => onChangeText(text)}
placeholder={
replyTo
? 'Write your reply'
: photoUris.length === 0
? "What's up?"
: 'Add a comment...'
}
style={styles.textInput}>
{textDecorated}
</TextInput>
<View style={styles.textInputLayout}>
<UserAvatar
handle={store.me.handle || ''}
displayName={store.me.displayName}
size={50}
/>
<TextInput
ref={textInput}
multiline
scrollEnabled
onChangeText={(text: string) => onChangeText(text)}
placeholder={replyTo ? 'Write your reply' : "What's up?"}
style={styles.textInput}>
{textDecorated}
</TextInput>
</View>
{photoUris.length !== 0 && (
<View style={styles.selectedImageContainer}>
{photoUris.length !== 0 &&
photoUris.map(item => (
photoUris.map((item, index) => (
<View
key={`selected-image-${index}`}
style={[
styles.selectedImage,
photoUris.length === 1
@ -264,8 +282,9 @@ export const ComposePost = observer(function ComposePost({
style={{color: colors.blue3}}
/>
</TouchableOpacity>
{localPhotos.photos.map(item => (
{localPhotos.photos.map((item, index) => (
<TouchableOpacity
key={`local-image-${index}`}
style={styles.photoButton}
onPress={() => {
setPhotoUris([item.node.image.uri, ...photoUris])
@ -343,9 +362,9 @@ const styles = StyleSheet.create({
flexDirection: 'row',
alignItems: 'center',
paddingTop: 10,
paddingBottom: 5,
paddingBottom: 10,
paddingHorizontal: 5,
height: 50,
height: 55,
},
postBtn: {
borderRadius: 20,
@ -371,19 +390,30 @@ const styles = StyleSheet.create({
justifyContent: 'center',
marginRight: 5,
},
textInputLayout: {
flexDirection: 'row',
flex: 1,
borderTopWidth: 1,
borderTopColor: colors.gray2,
paddingTop: 16,
},
textInput: {
flex: 1,
padding: 5,
fontSize: 21,
fontSize: 18,
marginLeft: 8,
},
replyToLayout: {
flexDirection: 'row',
borderTopWidth: 1,
borderTopColor: colors.gray2,
paddingTop: 16,
paddingBottom: 16,
},
replyToPost: {
paddingHorizontal: 8,
paddingVertical: 6,
borderWidth: 1,
borderColor: colors.gray2,
borderRadius: 6,
marginTop: 5,
marginBottom: 10,
flex: 1,
paddingLeft: 13,
paddingRight: 8,
},
contentCenter: {alignItems: 'center'},
selectedImageContainer: {

View file

@ -1,29 +1,42 @@
import React from 'react'
import {StyleSheet, Text, TouchableOpacity, View} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {colors} from '../../lib/styles'
import {useStores} from '../../../state'
import {UserAvatar} from '../util/UserAvatar'
export function ComposePrompt({onPressCompose}: {onPressCompose: () => void}) {
export function ComposePrompt({
noAvi = false,
text = "What's up?",
btn = 'Post',
onPressCompose,
}: {
noAvi?: boolean
text?: string
btn?: string
onPressCompose: () => void
}) {
const store = useStores()
const onPressAvatar = () => {
store.nav.navigate(`/profile/${store.me.handle}`)
}
return (
<TouchableOpacity style={styles.container} onPress={onPressCompose}>
<TouchableOpacity style={styles.avatar} onPress={onPressAvatar}>
<UserAvatar
size={50}
handle={store.me.handle || ''}
displayName={store.me.displayName}
/>
</TouchableOpacity>
<TouchableOpacity
style={[styles.container, noAvi ? styles.noAviContainer : undefined]}
onPress={onPressCompose}>
{!noAvi ? (
<TouchableOpacity style={styles.avatar} onPress={onPressAvatar}>
<UserAvatar
size={50}
handle={store.me.handle || ''}
displayName={store.me.displayName}
/>
</TouchableOpacity>
) : undefined}
<View style={styles.textContainer}>
<Text style={styles.text}>What's up?</Text>
<Text style={styles.text}>{text}</Text>
</View>
<View style={styles.btn}>
<Text style={styles.btnText}>Post</Text>
<Text style={styles.btnText}>{btn}</Text>
</View>
</TouchableOpacity>
)
@ -40,6 +53,9 @@ const styles = StyleSheet.create({
alignItems: 'center',
backgroundColor: colors.white,
},
noAviContainer: {
paddingVertical: 14,
},
avatar: {
width: 50,
},

View file

@ -14,7 +14,7 @@ import _omit from 'lodash.omit'
import {ErrorScreen} from '../util/ErrorScreen'
import {Link} from '../util/Link'
import {UserAvatar} from '../util/UserAvatar'
import Toast from '../util/Toast'
import * as Toast from '../util/Toast'
import {useStores} from '../../../state'
import * as apilib from '../../../state/lib/api'
import {
@ -63,10 +63,7 @@ export const SuggestedFollows = observer(
setFollows({[item.did]: res.uri, ...follows})
} catch (e) {
console.log(e)
Toast.show('An issue occurred, please try again.', {
duration: Toast.durations.LONG,
position: Toast.positions.TOP,
})
Toast.show('An issue occurred, please try again.')
}
}
const onPressUnfollow = async (item: SuggestedActor) => {
@ -75,10 +72,7 @@ export const SuggestedFollows = observer(
setFollows(_omit(follows, [item.did]))
} catch (e) {
console.log(e)
Toast.show('An issue occurred, please try again.', {
duration: Toast.durations.LONG,
position: Toast.positions.TOP,
})
Toast.show('An issue occurred, please try again.')
}
}

View file

@ -1,5 +1,5 @@
import React, {useState} from 'react'
import Toast from '../util/Toast'
import * as Toast from '../util/Toast'
import {
ActivityIndicator,
StyleSheet,
@ -71,9 +71,7 @@ export function Component({}: {}) {
},
)
.catch(e => console.error(e)) // an error here is not critical
Toast.show('Scene created', {
position: Toast.positions.TOP,
})
Toast.show('Scene created')
store.shell.closeModal()
store.nav.navigate(`/profile/${fullHandle}`)
} catch (e: any) {

View file

@ -1,5 +1,5 @@
import React, {useState} from 'react'
import Toast from '../util/Toast'
import * as Toast from '../util/Toast'
import {StyleSheet, Text, TouchableOpacity, View} from 'react-native'
import LinearGradient from 'react-native-linear-gradient'
import {BottomSheetScrollView, BottomSheetTextInput} from '@gorhom/bottom-sheet'
@ -52,9 +52,7 @@ export function Component({
}
},
)
Toast.show('Profile updated', {
position: Toast.positions.TOP,
})
Toast.show('Profile updated')
onUpdate?.()
store.shell.closeModal()
} catch (e: any) {

View file

@ -1,6 +1,6 @@
import React, {useState, useEffect, useMemo} from 'react'
import {observer} from 'mobx-react-lite'
import Toast from '../util/Toast'
import * as Toast from '../util/Toast'
import {
ActivityIndicator,
FlatList,
@ -83,10 +83,7 @@ export const Component = observer(function Component({
follow.declaration.cid,
)
setCreatedInvites({[follow.did]: assertionUri, ...createdInvites})
Toast.show('Invite sent', {
duration: Toast.durations.LONG,
position: Toast.positions.TOP,
})
Toast.show('Invite sent')
} catch (e) {
setError('There was an issue with the invite. Please try again.')
console.error(e)
@ -119,10 +116,7 @@ export const Component = observer(function Component({
[assertion.uri]: true,
...deletedPendingInvites,
})
Toast.show('Invite removed', {
duration: Toast.durations.LONG,
position: Toast.positions.TOP,
})
Toast.show('Invite removed')
} catch (e) {
setError('There was an issue with the invite. Please try again.')
console.error(e)

View file

@ -7,7 +7,7 @@ import {NotificationsViewItemModel} from '../../../state/models/notifications-vi
import {ConfirmModel} from '../../../state/models/shell-ui'
import {useStores} from '../../../state'
import {ProfileCard} from '../profile/ProfileCard'
import Toast from '../util/Toast'
import * as Toast from '../util/Toast'
import {s, colors, gradients} from '../../lib/styles'
export function InviteAccepter({item}: {item: NotificationsViewItemModel}) {
@ -46,10 +46,7 @@ export function InviteAccepter({item}: {item: NotificationsViewItemModel}) {
},
})
store.me.refreshMemberships()
Toast.show('Invite accepted', {
duration: Toast.durations.LONG,
position: Toast.positions.TOP,
})
Toast.show('Invite accepted')
setConfirmationUri(uri)
}
return (

View file

@ -8,7 +8,7 @@ import {PostThreadViewPostModel} from '../../../state/models/post-thread-view'
import {Link} from '../util/Link'
import {RichText} from '../util/RichText'
import {PostDropdownBtn} from '../util/DropdownBtn'
import Toast from '../util/Toast'
import * as Toast from '../util/Toast'
import {UserAvatar} from '../util/UserAvatar'
import {s, colors} from '../../lib/styles'
import {ago, pluralize} from '../../../lib/strings'
@ -16,6 +16,7 @@ import {useStores} from '../../../state'
import {PostMeta} from '../util/PostMeta'
import {PostEmbeds} from '../util/PostEmbeds'
import {PostCtrls} from '../util/PostCtrls'
import {ComposePrompt} from '../composer/Prompt'
const PARENT_REPLY_LINE_LENGTH = 8
const REPLYING_TO_LINE_LENGTH = 6
@ -78,131 +79,133 @@ export const PostThreadItem = observer(function PostThreadItem({
item.delete().then(
() => {
setDeleted(true)
Toast.show('Post deleted', {
position: Toast.positions.TOP,
})
Toast.show('Post deleted')
},
e => {
console.error(e)
Toast.show('Failed to delete post, please try again', {
position: Toast.positions.TOP,
})
Toast.show('Failed to delete post, please try again')
},
)
}
if (item._isHighlightedPost) {
return (
<View style={styles.outer}>
<View style={styles.layout}>
<View style={styles.layoutAvi}>
<Link href={authorHref} title={authorTitle}>
<UserAvatar
size={50}
displayName={item.author.displayName}
handle={item.author.handle}
/>
</Link>
</View>
<View style={styles.layoutContent}>
<View style={[styles.meta, {paddingTop: 5, paddingBottom: 0}]}>
<Link
style={styles.metaItem}
href={authorHref}
title={authorTitle}>
<Text style={[s.f16, s.bold]} numberOfLines={1}>
{item.author.displayName || item.author.handle}
</Text>
</Link>
<Text style={[styles.metaItem, s.f15, s.gray5]}>
&middot; {ago(item.indexedAt)}
</Text>
<View style={s.flex1} />
<PostDropdownBtn
style={styles.metaItem}
itemHref={itemHref}
itemTitle={itemTitle}
isAuthor={item.author.did === store.me.did}
onDeletePost={onDeletePost}>
<FontAwesomeIcon
icon="ellipsis-h"
size={14}
style={[s.mt2, s.mr5]}
<>
<View style={styles.outer}>
<View style={styles.layout}>
<View style={styles.layoutAvi}>
<Link href={authorHref} title={authorTitle}>
<UserAvatar
size={50}
displayName={item.author.displayName}
handle={item.author.handle}
/>
</PostDropdownBtn>
</View>
<View style={styles.meta}>
<Link
style={styles.metaItem}
href={authorHref}
title={authorTitle}>
<Text style={[s.f15, s.gray5]} numberOfLines={1}>
@{item.author.handle}
</Text>
</Link>
</View>
</View>
</View>
<View style={[s.pl10, s.pr10, s.pb10]}>
<View
style={[styles.postTextContainer, styles.postTextLargeContainer]}>
<RichText
text={record.text}
entities={record.entities}
style={[styles.postText, styles.postTextLarge]}
/>
</View>
<PostEmbeds entities={record.entities} />
{item._isHighlightedPost && hasEngagement ? (
<View style={styles.expandedInfo}>
{item.repostCount ? (
<View style={styles.layoutContent}>
<View style={[styles.meta, {paddingTop: 5, paddingBottom: 0}]}>
<Link
style={styles.expandedInfoItem}
href={repostsHref}
title={repostsTitle}>
<Text style={[s.gray5, s.semiBold, s.f18]}>
<Text style={[s.bold, s.black, s.f18]}>
{item.repostCount}
</Text>{' '}
{pluralize(item.repostCount, 'repost')}
style={styles.metaItem}
href={authorHref}
title={authorTitle}>
<Text style={[s.f16, s.bold]} numberOfLines={1}>
{item.author.displayName || item.author.handle}
</Text>
</Link>
) : (
<></>
)}
{item.upvoteCount ? (
<Text style={[styles.metaItem, s.f15, s.gray5]}>
&middot; {ago(item.indexedAt)}
</Text>
<View style={s.flex1} />
<PostDropdownBtn
style={styles.metaItem}
itemHref={itemHref}
itemTitle={itemTitle}
isAuthor={item.author.did === store.me.did}
onDeletePost={onDeletePost}>
<FontAwesomeIcon
icon="ellipsis-h"
size={14}
style={[s.mt2, s.mr5]}
/>
</PostDropdownBtn>
</View>
<View style={styles.meta}>
<Link
style={styles.expandedInfoItem}
href={upvotesHref}
title={upvotesTitle}>
<Text style={[s.gray5, s.semiBold, s.f18]}>
<Text style={[s.bold, s.black, s.f18]}>
{item.upvoteCount}
</Text>{' '}
{pluralize(item.upvoteCount, 'upvote')}
style={styles.metaItem}
href={authorHref}
title={authorTitle}>
<Text style={[s.f15, s.gray5]} numberOfLines={1}>
@{item.author.handle}
</Text>
</Link>
) : (
<></>
)}
</View>
</View>
</View>
<View style={[s.pl10, s.pr10, s.pb10]}>
<View
style={[styles.postTextContainer, styles.postTextLargeContainer]}>
<RichText
text={record.text}
entities={record.entities}
style={[styles.postText, styles.postTextLarge]}
/>
</View>
<PostEmbeds entities={record.entities} style={s.mb10} />
{item._isHighlightedPost && hasEngagement ? (
<View style={styles.expandedInfo}>
{item.repostCount ? (
<Link
style={styles.expandedInfoItem}
href={repostsHref}
title={repostsTitle}>
<Text style={[s.gray5, s.semiBold, s.f17]}>
<Text style={[s.bold, s.black, s.f17]}>
{item.repostCount}
</Text>{' '}
{pluralize(item.repostCount, 'repost')}
</Text>
</Link>
) : (
<></>
)}
{item.upvoteCount ? (
<Link
style={styles.expandedInfoItem}
href={upvotesHref}
title={upvotesTitle}>
<Text style={[s.gray5, s.semiBold, s.f17]}>
<Text style={[s.bold, s.black, s.f17]}>
{item.upvoteCount}
</Text>{' '}
{pluralize(item.upvoteCount, 'upvote')}
</Text>
</Link>
) : (
<></>
)}
</View>
) : (
<></>
)}
<View style={[s.pl10, s.pb5]}>
<PostCtrls
big
isReposted={!!item.myState.repost}
isUpvoted={!!item.myState.upvote}
onPressReply={onPressReply}
onPressToggleRepost={onPressToggleRepost}
onPressToggleUpvote={onPressToggleUpvote}
/>
</View>
) : (
<></>
)}
<View style={[s.pl10]}>
<PostCtrls
replyCount={item.replyCount}
repostCount={item.repostCount}
upvoteCount={item.upvoteCount}
isReposted={!!item.myState.repost}
isUpvoted={!!item.myState.upvote}
onPressReply={onPressReply}
onPressToggleRepost={onPressToggleRepost}
onPressToggleUpvote={onPressToggleUpvote}
/>
</View>
</View>
</View>
<ComposePrompt
noAvi
text="Write your reply"
btn="Reply"
onPressCompose={onPressReply}
/>
</>
)
} else {
return (
@ -345,8 +348,8 @@ const styles = StyleSheet.create({
},
postText: {
fontFamily: 'Helvetica Neue',
fontSize: 17,
lineHeight: 22.1, // 1.3 of 17px
fontSize: 16,
lineHeight: 20.8, // 1.3 of 16px
},
postTextContainer: {
flexDirection: 'row',
@ -371,7 +374,7 @@ const styles = StyleSheet.create({
borderTopWidth: 1,
borderBottomWidth: 1,
marginTop: 5,
marginBottom: 10,
marginBottom: 15,
},
expandedInfoItem: {
marginRight: 10,

View file

@ -10,7 +10,7 @@ import {UserInfoText} from '../util/UserInfoText'
import {PostMeta} from '../util/PostMeta'
import {PostCtrls} from '../util/PostCtrls'
import {RichText} from '../util/RichText'
import Toast from '../util/Toast'
import * as Toast from '../util/Toast'
import {UserAvatar} from '../util/UserAvatar'
import {useStores} from '../../../state'
import {s, colors} from '../../lib/styles'
@ -99,15 +99,11 @@ export const Post = observer(function Post({uri}: {uri: string}) {
item.delete().then(
() => {
setDeleted(true)
Toast.show('Post deleted', {
position: Toast.positions.TOP,
})
Toast.show('Post deleted')
},
e => {
console.error(e)
Toast.show('Failed to delete post, please try again', {
position: Toast.positions.TOP,
})
Toast.show('Failed to delete post, please try again')
},
)
}
@ -196,7 +192,7 @@ const styles = StyleSheet.create({
},
postText: {
fontFamily: 'Helvetica Neue',
fontSize: 17,
lineHeight: 22.1, // 1.3 of 17px
fontSize: 16,
lineHeight: 20.8, // 1.3 of 16px
},
})

View file

@ -11,7 +11,7 @@ import {PostMeta} from '../util/PostMeta'
import {PostCtrls} from '../util/PostCtrls'
import {PostEmbeds} from '../util/PostEmbeds'
import {RichText} from '../util/RichText'
import Toast from '../util/Toast'
import * as Toast from '../util/Toast'
import {UserAvatar} from '../util/UserAvatar'
import {s, colors} from '../../lib/styles'
import {useStores} from '../../../state'
@ -70,15 +70,11 @@ export const FeedItem = observer(function FeedItem({
item.delete().then(
() => {
setDeleted(true)
Toast.show('Post deleted', {
position: Toast.positions.TOP,
})
Toast.show('Post deleted')
},
e => {
console.error(e)
Toast.show('Failed to delete post, please try again', {
position: Toast.positions.TOP,
})
Toast.show('Failed to delete post, please try again')
},
)
}
@ -254,7 +250,7 @@ const styles = StyleSheet.create({
},
postText: {
fontFamily: 'Helvetica Neue',
fontSize: 17,
lineHeight: 22.1, // 1.3 of 17px
fontSize: 16,
lineHeight: 20.8, // 1.3 of 16px
},
})

View file

@ -1,12 +1,6 @@
import React, {useMemo} from 'react'
import {observer} from 'mobx-react-lite'
import {
ActivityIndicator,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native'
import {StyleSheet, Text, TouchableOpacity, View} from 'react-native'
import LinearGradient from 'react-native-linear-gradient'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {AtUri} from '../../../third-party/uri'
@ -20,9 +14,8 @@ import {
import {pluralize} from '../../../lib/strings'
import {s, colors} from '../../lib/styles'
import {getGradient} from '../../lib/asset-gen'
import {MagnifyingGlassIcon} from '../../lib/icons'
import {DropdownBtn, DropdownItem} from '../util/DropdownBtn'
import Toast from '../util/Toast'
import * as Toast from '../util/Toast'
import {LoadingPlaceholder} from '../util/LoadingPlaceholder'
import {RichText} from '../util/RichText'
import {UserAvatar} from '../util/UserAvatar'
@ -55,10 +48,6 @@ export const ProfileHeader = observer(function ProfileHeader({
`${view.myState.follow ? 'Following' : 'No longer following'} ${
view.displayName || view.handle
}`,
{
duration: Toast.durations.LONG,
position: Toast.positions.TOP,
},
)
},
err => console.error('Failed to toggle follow', err),
@ -94,10 +83,7 @@ export const ProfileHeader = observer(function ProfileHeader({
did: store.me.did || '',
rkey: new AtUri(view.myState.member).rkey,
})
Toast.show(`Scene left`, {
duration: Toast.durations.LONG,
position: Toast.positions.TOP,
})
Toast.show(`Scene left`)
}
onRefreshAll()
}
@ -108,18 +94,6 @@ export const ProfileHeader = observer(function ProfileHeader({
return (
<View style={styles.outer}>
<LoadingPlaceholder width="100%" height={120} />
{store.nav.tab.canGoBack ? (
<TouchableOpacity style={styles.backButton} onPress={onPressBack}>
<FontAwesomeIcon
size={18}
icon="angle-left"
style={styles.backIcon}
/>
</TouchableOpacity>
) : undefined}
<TouchableOpacity style={styles.searchBtn} onPress={onPressSearch}>
<MagnifyingGlassIcon size={19} style={styles.searchIcon} />
</TouchableOpacity>
<View style={styles.avi}>
<LoadingPlaceholder
width={80}
@ -179,18 +153,6 @@ export const ProfileHeader = observer(function ProfileHeader({
return (
<View style={styles.outer}>
<UserBanner handle={view.handle} />
{store.nav.tab.canGoBack ? (
<TouchableOpacity style={styles.backButton} onPress={onPressBack}>
<FontAwesomeIcon
size={18}
icon="angle-left"
style={styles.backIcon}
/>
</TouchableOpacity>
) : undefined}
<TouchableOpacity style={styles.searchBtn} onPress={onPressSearch}>
<MagnifyingGlassIcon size={19} style={styles.searchIcon} />
</TouchableOpacity>
<View style={styles.avi}>
<UserAvatar
size={80}
@ -353,30 +315,6 @@ const styles = StyleSheet.create({
width: '100%',
height: 120,
},
backButton: {
position: 'absolute',
top: 10,
left: 12,
backgroundColor: '#ffff',
padding: 6,
borderRadius: 30,
},
backIcon: {
width: 14,
height: 14,
color: colors.black,
},
searchBtn: {
position: 'absolute',
top: 10,
right: 12,
backgroundColor: '#ffff',
padding: 5,
borderRadius: 30,
},
searchIcon: {
color: colors.black,
},
avi: {
position: 'absolute',
top: 80,

View file

@ -12,9 +12,10 @@ import {UpIcon, UpIconSolid} from '../../lib/icons'
import {s, colors} from '../../lib/styles'
interface PostCtrlsOpts {
replyCount: number
repostCount: number
upvoteCount: number
big?: boolean
replyCount?: number
repostCount?: number
upvoteCount?: number
isReposted: boolean
isUpvoted: boolean
onPressReply: () => void
@ -30,17 +31,17 @@ export function PostCtrls(opts: PostCtrlsOpts) {
const interp2 = useSharedValue<number>(0)
const anim1Style = useAnimatedStyle(() => ({
transform: [{scale: interpolate(interp1.value, [0, 1.0], [1.0, 3.0])}],
transform: [{scale: interpolate(interp1.value, [0, 1.0], [1.0, 4.0])}],
opacity: interpolate(interp1.value, [0, 1.0], [1.0, 0.0]),
}))
const anim2Style = useAnimatedStyle(() => ({
transform: [{scale: interpolate(interp2.value, [0, 1.0], [1.0, 3.0])}],
transform: [{scale: interpolate(interp2.value, [0, 1.0], [1.0, 4.0])}],
opacity: interpolate(interp2.value, [0, 1.0], [1.0, 0.0]),
}))
const onPressToggleRepostWrapper = () => {
if (!opts.isReposted) {
interp1.value = withTiming(1, {duration: 300}, () => {
interp1.value = withTiming(1, {duration: 400}, () => {
interp1.value = withDelay(100, withTiming(0, {duration: 20}))
})
}
@ -48,7 +49,7 @@ export function PostCtrls(opts: PostCtrlsOpts) {
}
const onPressToggleUpvoteWrapper = () => {
if (!opts.isUpvoted) {
interp2.value = withTiming(1, {duration: 300}, () => {
interp2.value = withTiming(1, {duration: 400}, () => {
interp2.value = withDelay(100, withTiming(0, {duration: 20}))
})
}
@ -62,9 +63,11 @@ export function PostCtrls(opts: PostCtrlsOpts) {
<FontAwesomeIcon
style={styles.ctrlIcon}
icon={['far', 'comment']}
size={14}
size={opts.big ? 20 : 14}
/>
<Text style={[sRedgray, s.ml5, s.f16]}>{opts.replyCount}</Text>
{typeof opts.replyCount !== 'undefined' ? (
<Text style={[sRedgray, s.ml5, s.f16]}>{opts.replyCount}</Text>
) : undefined}
</TouchableOpacity>
</View>
<View style={s.flex1}>
@ -77,17 +80,19 @@ export function PostCtrls(opts: PostCtrlsOpts) {
opts.isReposted ? styles.ctrlIconReposted : styles.ctrlIcon
}
icon="retweet"
size={18}
size={opts.big ? 22 : 18}
/>
</Animated.View>
<Text
style={
opts.isReposted
? [s.bold, s.green3, s.f16, s.ml5]
: [sRedgray, s.f16, s.ml5]
}>
{opts.repostCount}
</Text>
{typeof opts.repostCount !== 'undefined' ? (
<Text
style={
opts.isReposted
? [s.bold, s.green3, s.f16, s.ml5]
: [sRedgray, s.f16, s.ml5]
}>
{opts.repostCount}
</Text>
) : undefined}
</TouchableOpacity>
</View>
<View style={s.flex1}>
@ -96,19 +101,28 @@ export function PostCtrls(opts: PostCtrlsOpts) {
onPress={onPressToggleUpvoteWrapper}>
<Animated.View style={anim2Style}>
{opts.isUpvoted ? (
<UpIconSolid style={[styles.ctrlIconUpvoted]} size={18} />
<UpIconSolid
style={[styles.ctrlIconUpvoted]}
size={opts.big ? 22 : 18}
/>
) : (
<UpIcon style={[styles.ctrlIcon]} size={18} strokeWidth={1.5} />
<UpIcon
style={[styles.ctrlIcon]}
size={opts.big ? 22 : 18}
strokeWidth={1.5}
/>
)}
</Animated.View>
<Text
style={
opts.isUpvoted
? [s.bold, s.red3, s.f16, s.ml5]
: [sRedgray, s.f16, s.ml5]
}>
{opts.upvoteCount}
</Text>
{typeof opts.upvoteCount !== 'undefined' ? (
<Text
style={
opts.isUpvoted
? [s.bold, s.red3, s.f16, s.ml5]
: [sRedgray, s.f16, s.ml5]
}>
{opts.upvoteCount}
</Text>
) : undefined}
</TouchableOpacity>
</View>
<View style={s.flex1}></View>

View file

@ -1,2 +0,0 @@
import Toast from 'react-native-root-toast'
export default Toast

View file

@ -1,62 +1,11 @@
/*
* Note: the dataSet properties are used to leverage custom CSS in public/index.html
*/
import Toast from 'react-native-root-toast'
import React, {useState, useEffect} from 'react'
// @ts-ignore no declarations available -prf
import {Text, View} from 'react-native-web'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
interface ActiveToast {
text: string
}
type GlobalSetActiveToast = (_activeToast: ActiveToast | undefined) => void
// globals
// =
let globalSetActiveToast: GlobalSetActiveToast | undefined
let toastTimeout: NodeJS.Timeout | undefined
// components
// =
type ToastContainerProps = {}
const ToastContainer: React.FC<ToastContainerProps> = ({}) => {
const [activeToast, setActiveToast] = useState<ActiveToast | undefined>()
useEffect(() => {
globalSetActiveToast = (t: ActiveToast | undefined) => {
setActiveToast(t)
}
export function show(message: string) {
Toast.show(message, {
duration: Toast.durations.LONG,
position: 50,
shadow: true,
animation: true,
hideOnPress: true,
})
return (
<>
{activeToast && (
<View dataSet={{'toast-container': 1}}>
<FontAwesomeIcon icon="check" size={24} />
<Text>{activeToast.text}</Text>
</View>
)}
</>
)
}
// exports
// =
export default {
show(text: string, _opts: any) {
console.log('TODO: toast', text)
if (toastTimeout) {
clearTimeout(toastTimeout)
}
globalSetActiveToast?.({text})
toastTimeout = setTimeout(() => {
globalSetActiveToast?.(undefined)
}, 2e3)
},
positions: {
TOP: 0,
},
durations: {
LONG: 0,
},
ToastContainer,
}

View file

@ -1,7 +1,6 @@
import React from 'react'
import {StyleSheet, Text, TouchableOpacity, View} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {UserAvatar} from './UserAvatar'
import {colors} from '../../lib/styles'
import {MagnifyingGlassIcon} from '../../lib/icons'
import {useStores} from '../../../state'
@ -9,14 +8,19 @@ import {useStores} from '../../../state'
export function ViewHeader({
title,
subtitle,
onPost,
}: {
title: string
subtitle?: string
onPost?: () => void
}) {
const store = useStores()
const onPressBack = () => {
store.nav.tab.goBack()
}
const onPressCompose = () => {
store.shell.openComposer({onPost})
}
const onPressSearch = () => {
store.nav.navigate(`/search`)
}
@ -26,9 +30,7 @@ export function ViewHeader({
<TouchableOpacity onPress={onPressBack} style={styles.backIcon}>
<FontAwesomeIcon size={18} icon="angle-left" style={{marginTop: 6}} />
</TouchableOpacity>
) : (
<View style={styles.cornerPlaceholder} />
)}
) : undefined}
<View style={styles.titleContainer}>
<Text style={styles.title}>{title}</Text>
{subtitle ? (
@ -37,8 +39,17 @@ export function ViewHeader({
</Text>
) : undefined}
</View>
<TouchableOpacity onPress={onPressSearch} style={styles.searchBtn}>
<MagnifyingGlassIcon size={17} style={styles.searchBtnIcon} />
<TouchableOpacity onPress={onPressCompose} style={styles.btn}>
<FontAwesomeIcon size={18} icon="plus" />
</TouchableOpacity>
<TouchableOpacity
onPress={onPressSearch}
style={[styles.btn, {marginLeft: 8}]}>
<MagnifyingGlassIcon
size={18}
strokeWidth={3}
style={styles.searchBtnIcon}
/>
</TouchableOpacity>
</View>
)
@ -59,33 +70,28 @@ const styles = StyleSheet.create({
titleContainer: {
flexDirection: 'row',
alignItems: 'baseline',
marginLeft: 'auto',
marginRight: 'auto',
},
title: {
fontSize: 16,
fontSize: 21,
fontWeight: '600',
},
subtitle: {
fontSize: 15,
marginLeft: 3,
fontSize: 18,
marginLeft: 6,
color: colors.gray4,
maxWidth: 200,
},
cornerPlaceholder: {
width: 30,
height: 30,
},
backIcon: {width: 30, height: 30},
searchBtn: {
btn: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: colors.gray1,
width: 30,
height: 30,
borderRadius: 15,
width: 36,
height: 36,
borderRadius: 20,
},
searchBtnIcon: {
color: colors.black,