Big batch of UI updates (#276)

* Replace react-native-root-toast with a custom toast that fits the visual style

* Tune dark mode colors

* Tune colors a bit more

* Move the reply prompt to a fixed position in the footer

* Fully hide muted posts but give a control to show anyway (close #270)

* Improve thread rendering (better clarity on reply lines)

* Add follower/following counts to side menu

* Fix issues with display name overflows
zio/stable
Paul Frazee 2023-03-07 15:52:24 -06:00 committed by GitHub
parent 2f3fc4fe4e
commit e74f94bc72
19 changed files with 381 additions and 249 deletions

View File

@ -70,7 +70,6 @@
"react-native-progress": "^5.0.0",
"react-native-reanimated": "^2.9.1",
"react-native-root-siblings": "^4.1.1",
"react-native-root-toast": "^3.4.0",
"react-native-safe-area-context": "^4.4.1",
"react-native-screens": "^3.13.1",
"react-native-splash-screen": "^3.3.0",

View File

@ -15,7 +15,7 @@ export const colors = {
gray5: '#545664',
gray6: '#373942',
gray7: '#26272D',
gray8: '#101013',
gray8: '#141417',
blue0: '#bfe1ff',
blue1: '#8bc7fd',
@ -24,6 +24,7 @@ export const colors = {
blue4: '#0062bd',
blue5: '#034581',
blue6: '#012561',
blue7: '#001040',
red1: '#ffe6f2',
red2: '#fba2ce',
@ -64,6 +65,7 @@ export const s = StyleSheet.create({
// helpers
footerSpacer: {height: 100},
contentContainer: {paddingBottom: 200},
contentContainerExtra: {paddingBottom: 300},
border1: {borderWidth: 1},
borderTop1: {borderTopWidth: 1},
borderRight1: {borderRightWidth: 1},

View File

@ -21,6 +21,7 @@ export const defaultTheme: Theme = {
replyLine: colors.gray2,
replyLineDot: colors.gray3,
unreadNotifBg: '#ebf6ff',
unreadNotifBorder: colors.blue1,
postCtrl: '#71768A',
brandText: '#0066FF',
emptyStateIcon: '#B6B6C9',
@ -296,15 +297,16 @@ export const darkTheme: Theme = {
textLight: colors.gray3,
textInverted: colors.black,
link: colors.blue3,
border: colors.gray6,
borderDark: colors.gray5,
border: colors.black,
borderDark: colors.gray6,
icon: colors.gray4,
// non-standard
textVeryLight: colors.gray4,
replyLine: colors.gray5,
replyLineDot: colors.gray6,
unreadNotifBg: colors.blue5,
unreadNotifBg: colors.blue7,
unreadNotifBorder: colors.blue6,
postCtrl: '#61657A',
brandText: '#0085ff',
emptyStateIcon: colors.gray4,

View File

@ -11,6 +11,8 @@ export class MeModel {
displayName: string = ''
description: string = ''
avatar: string = ''
followsCount: number | undefined
followersCount: number | undefined
mainFeed: FeedModel
notifications: NotificationsViewModel
follows: MyFollowsModel
@ -90,10 +92,14 @@ export class MeModel {
this.displayName = profile.data.displayName || ''
this.description = profile.data.description || ''
this.avatar = profile.data.avatar || ''
this.followsCount = profile.data.followsCount
this.followersCount = profile.data.followersCount
} else {
this.displayName = ''
this.description = ''
this.avatar = ''
this.followsCount = profile.data.followsCount
this.followersCount = undefined
}
})
this.mainFeed.clear()

View File

@ -21,6 +21,8 @@ export class PostThreadViewPostModel {
_reactKey: string = ''
_depth = 0
_isHighlightedPost = false
_showParentReplyLine = false
_showChildReplyLine = false
_hasMore = false
// data
@ -30,6 +32,14 @@ export class PostThreadViewPostModel {
replies?: (PostThreadViewPostModel | GetPostThread.NotFoundPost)[]
richText?: RichText
get uri() {
return this.post.uri
}
get parentUri() {
return this.postRecord?.reply?.parent.uri
}
constructor(
public rootStore: RootStoreModel,
reactKey: string,
@ -65,6 +75,7 @@ export class PostThreadViewPostModel {
assignTreeModels(
keyGen: Generator<string>,
v: GetPostThread.ThreadViewPost,
higlightedPostUri: string,
includeParent = true,
includeChildren = true,
) {
@ -77,8 +88,16 @@ export class PostThreadViewPostModel {
v.parent,
)
parentModel._depth = this._depth - 1
parentModel._showChildReplyLine = true
if (v.parent.parent) {
parentModel.assignTreeModels(keyGen, v.parent, true, false)
parentModel._showParentReplyLine = true //parentModel.uri !== higlightedPostUri
parentModel.assignTreeModels(
keyGen,
v.parent,
higlightedPostUri,
true,
false,
)
}
this.parent = parentModel
} else if (GetPostThread.isNotFoundPost(v.parent)) {
@ -96,8 +115,17 @@ export class PostThreadViewPostModel {
item,
)
itemModel._depth = this._depth + 1
if (item.replies) {
itemModel.assignTreeModels(keyGen, item, false, true)
itemModel._showParentReplyLine =
itemModel.parentUri !== higlightedPostUri
if (item.replies?.length) {
itemModel._showChildReplyLine = true
itemModel.assignTreeModels(
keyGen,
item,
higlightedPostUri,
false,
true,
)
}
replies.push(itemModel)
} else if (GetPostThread.isNotFoundPost(item)) {
@ -333,6 +361,7 @@ export class PostThreadViewModel {
thread.assignTreeModels(
keyGen,
res.data.thread as GetPostThread.ThreadViewPost,
thread.uri,
)
this.thread = thread
}

View File

@ -1,91 +1,45 @@
import React from 'react'
import {StyleSheet, TouchableOpacity, View} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {StyleSheet, TouchableOpacity} from 'react-native'
import {UserAvatar} from '../util/UserAvatar'
import {Text} from '../util/text/Text'
import {usePalette} from 'lib/hooks/usePalette'
import {useStores} from 'state/index'
export function ComposePrompt({
text = "What's up?",
btn = 'Post',
isReply = false,
onPressCompose,
}: {
text?: string
btn?: string
isReply?: boolean
onPressCompose: (imagesOpen?: boolean) => void
}) {
const store = useStores()
const pal = usePalette('default')
return (
<TouchableOpacity
testID="composePromptButton"
style={[
pal.view,
pal.border,
styles.container,
isReply ? styles.containerReply : undefined,
]}
testID="replyPromptBtn"
style={[pal.view, pal.border, styles.prompt]}
onPress={() => onPressCompose()}>
{!isReply && (
<FontAwesomeIcon
icon={['fas', 'pen-nib']}
size={18}
style={[pal.textLight, styles.iconLeft]}
/>
)}
<View style={styles.textContainer}>
<Text type={isReply ? 'lg' : 'lg-medium'} style={pal.textLight}>
{text}
</Text>
</View>
{isReply ? (
<View
style={[styles.btn, {backgroundColor: pal.colors.backgroundLight}]}>
<Text type="button" style={pal.textLight}>
{btn}
</Text>
</View>
) : (
<TouchableOpacity onPress={() => onPressCompose(true)}>
<FontAwesomeIcon
icon={['far', 'image']}
size={18}
style={[pal.textLight, styles.iconRight]}
/>
</TouchableOpacity>
)}
<UserAvatar
handle={store.me.handle}
avatar={store.me.avatar}
displayName={store.me.displayName}
size={38}
/>
<Text type="xl" style={[pal.text, styles.label]}>
Write your reply
</Text>
</TouchableOpacity>
)
}
const styles = StyleSheet.create({
iconLeft: {
marginLeft: 22,
marginRight: 2,
},
iconRight: {
marginRight: 20,
},
container: {
paddingVertical: 16,
prompt: {
paddingHorizontal: 20,
paddingTop: 10,
paddingBottom: 10,
flexDirection: 'row',
alignItems: 'center',
borderTopWidth: 1,
},
containerReply: {
paddingVertical: 14,
paddingHorizontal: 10,
},
avatar: {
width: 50,
},
textContainer: {
marginLeft: 10,
flex: 1,
},
btn: {
paddingVertical: 6,
paddingHorizontal: 14,
borderRadius: 30,
label: {
paddingLeft: 12,
},
})

View File

@ -90,10 +90,10 @@ export const FeedItem = observer(function FeedItem({
style={
item.isRead
? undefined
: [
styles.outerUnread,
{backgroundColor: pal.colors.unreadNotifBg},
]
: {
backgroundColor: pal.colors.unreadNotifBg,
borderColor: pal.colors.unreadNotifBorder,
}
}
/>
</Link>
@ -152,7 +152,10 @@ export const FeedItem = observer(function FeedItem({
pal.border,
item.isRead
? undefined
: [styles.outerUnread, {backgroundColor: pal.colors.unreadNotifBg}],
: {
backgroundColor: pal.colors.unreadNotifBg,
borderColor: pal.colors.unreadNotifBorder,
},
]}
href={itemHref}
title={itemTitle}
@ -391,9 +394,6 @@ const styles = StyleSheet.create({
paddingRight: 15,
borderTopWidth: 1,
},
outerUnread: {
borderColor: colors.blue1,
},
layout: {
flexDirection: 'row',
},

View File

@ -96,7 +96,7 @@ export const PostThread = observer(function PostThread({
onLayout={onLayout}
onScrollToIndexFailed={onScrollToIndexFailed}
style={s.hContentRegion}
contentContainerStyle={s.contentContainer}
contentContainerStyle={s.contentContainerExtra}
/>
)
})

View File

@ -21,8 +21,8 @@ import {useStores} from 'state/index'
import {PostMeta} from '../util/PostMeta'
import {PostEmbeds} from '../util/PostEmbeds'
import {PostCtrls} from '../util/PostCtrls'
import {PostMutedWrapper} from '../util/PostMuted'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {ComposePrompt} from '../composer/Prompt'
import {usePalette} from 'lib/hooks/usePalette'
const PARENT_REPLY_LINE_LENGTH = 8
@ -271,23 +271,17 @@ export const PostThreadItem = observer(function PostThreadItem({
</View>
</View>
</View>
<ComposePrompt
isReply
text="Write your reply"
btn="Reply"
onPressCompose={onPressReply}
/>
</>
)
} else {
return (
<>
<PostMutedWrapper isMuted={item.post.author.viewer?.muted === true}>
<Link
style={[styles.outer, {borderTopColor: pal.colors.border}, pal.view]}
href={itemHref}
title={itemTitle}
noFeedback>
{record.reply && (
{item._showParentReplyLine && (
<View
style={[
styles.parentReplyLine,
@ -295,7 +289,7 @@ export const PostThreadItem = observer(function PostThreadItem({
]}
/>
)}
{item.replies?.length !== 0 && (
{item._showChildReplyLine && (
<View
style={[
styles.childReplyLine,
@ -322,12 +316,7 @@ export const PostThreadItem = observer(function PostThreadItem({
did={item.post.author.did}
declarationCid={item.post.author.declaration.cid}
/>
{item.post.author.viewer?.muted ? (
<View style={[styles.mutedWarning, pal.btn]}>
<FontAwesomeIcon icon={['far', 'eye-slash']} style={s.mr2} />
<Text type="sm">This post is by a muted account.</Text>
</View>
) : item.richText?.text ? (
{item.richText?.text ? (
<View style={styles.postTextContainer}>
<RichText
type="post-text"
@ -384,7 +373,7 @@ export const PostThreadItem = observer(function PostThreadItem({
/>
</Link>
) : undefined}
</>
</PostMutedWrapper>
)
}
})
@ -441,14 +430,6 @@ const styles = StyleSheet.create({
paddingRight: 5,
maxWidth: 240,
},
mutedWarning: {
flexDirection: 'row',
alignItems: 'center',
padding: 10,
marginTop: 2,
marginBottom: 6,
borderRadius: 2,
},
postTextContainer: {
flexDirection: 'row',
alignItems: 'center',

View File

@ -17,6 +17,7 @@ import {UserInfoText} from '../util/UserInfoText'
import {PostMeta} from '../util/PostMeta'
import {PostEmbeds} from '../util/PostEmbeds'
import {PostCtrls} from '../util/PostCtrls'
import {PostMutedWrapper} from '../util/PostMuted'
import {Text} from '../util/text/Text'
import {RichText} from '../util/text/RichText'
import * as Toast from '../util/Toast'
@ -140,92 +141,89 @@ export const Post = observer(function Post({
}
return (
<Link
style={[styles.outer, pal.view, pal.border, style]}
href={itemHref}
title={itemTitle}
noFeedback>
{showReplyLine && <View style={styles.replyLine} />}
<View style={styles.layout}>
<View style={styles.layoutAvi}>
<Link href={authorHref} title={authorTitle}>
<UserAvatar
size={52}
displayName={item.post.author.displayName}
handle={item.post.author.handle}
avatar={item.post.author.avatar}
<PostMutedWrapper isMuted={item.post.author.viewer?.muted === true}>
<Link
style={[styles.outer, pal.view, pal.border, style]}
href={itemHref}
title={itemTitle}
noFeedback>
{showReplyLine && <View style={styles.replyLine} />}
<View style={styles.layout}>
<View style={styles.layoutAvi}>
<Link href={authorHref} title={authorTitle}>
<UserAvatar
size={52}
displayName={item.post.author.displayName}
handle={item.post.author.handle}
avatar={item.post.author.avatar}
/>
</Link>
</View>
<View style={styles.layoutContent}>
<PostMeta
authorHandle={item.post.author.handle}
authorDisplayName={item.post.author.displayName}
timestamp={item.post.indexedAt}
did={item.post.author.did}
declarationCid={item.post.author.declaration.cid}
/>
</Link>
{replyAuthorDid !== '' && (
<View style={[s.flexRow, s.mb2, s.alignCenter]}>
<FontAwesomeIcon
icon="reply"
size={9}
style={[pal.textLight, s.mr5]}
/>
<Text type="sm" style={[pal.textLight, s.mr2]} lineHeight={1.2}>
Reply to
</Text>
<UserInfoText
type="sm"
did={replyAuthorDid}
attr="displayName"
style={[pal.textLight]}
/>
</View>
)}
{item.richText?.text ? (
<View style={styles.postTextContainer}>
<RichText
type="post-text"
richText={item.richText}
lineHeight={1.3}
/>
</View>
) : undefined}
<PostEmbeds embed={item.post.embed} style={s.mb10} />
<PostCtrls
itemUri={itemUri}
itemCid={itemCid}
itemHref={itemHref}
itemTitle={itemTitle}
author={{
avatar: item.post.author.avatar!,
handle: item.post.author.handle,
displayName: item.post.author.displayName!,
}}
indexedAt={item.post.indexedAt}
text={item.richText?.text || record.text}
isAuthor={item.post.author.did === store.me.did}
replyCount={item.post.replyCount}
repostCount={item.post.repostCount}
upvoteCount={item.post.upvoteCount}
isReposted={!!item.post.viewer.repost}
isUpvoted={!!item.post.viewer.upvote}
onPressReply={onPressReply}
onPressToggleRepost={onPressToggleRepost}
onPressToggleUpvote={onPressToggleUpvote}
onCopyPostText={onCopyPostText}
onOpenTranslate={onOpenTranslate}
onDeletePost={onDeletePost}
/>
</View>
</View>
<View style={styles.layoutContent}>
<PostMeta
authorHandle={item.post.author.handle}
authorDisplayName={item.post.author.displayName}
timestamp={item.post.indexedAt}
did={item.post.author.did}
declarationCid={item.post.author.declaration.cid}
/>
{replyAuthorDid !== '' && (
<View style={[s.flexRow, s.mb2, s.alignCenter]}>
<FontAwesomeIcon
icon="reply"
size={9}
style={[pal.textLight, s.mr5]}
/>
<Text type="sm" style={[pal.textLight, s.mr2]} lineHeight={1.2}>
Reply to
</Text>
<UserInfoText
type="sm"
did={replyAuthorDid}
attr="displayName"
style={[pal.textLight]}
/>
</View>
)}
{item.post.author.viewer?.muted ? (
<View style={[styles.mutedWarning, pal.btn]}>
<FontAwesomeIcon icon={['far', 'eye-slash']} style={s.mr2} />
<Text type="sm">This post is by a muted account.</Text>
</View>
) : item.richText?.text ? (
<View style={styles.postTextContainer}>
<RichText
type="post-text"
richText={item.richText}
lineHeight={1.3}
/>
</View>
) : undefined}
<PostEmbeds embed={item.post.embed} style={s.mb10} />
<PostCtrls
itemUri={itemUri}
itemCid={itemCid}
itemHref={itemHref}
itemTitle={itemTitle}
author={{
avatar: item.post.author.avatar!,
handle: item.post.author.handle,
displayName: item.post.author.displayName!,
}}
indexedAt={item.post.indexedAt}
text={item.richText?.text || record.text}
isAuthor={item.post.author.did === store.me.did}
replyCount={item.post.replyCount}
repostCount={item.post.repostCount}
upvoteCount={item.post.upvoteCount}
isReposted={!!item.post.viewer.repost}
isUpvoted={!!item.post.viewer.upvote}
onPressReply={onPressReply}
onPressToggleRepost={onPressToggleRepost}
onPressToggleUpvote={onPressToggleUpvote}
onCopyPostText={onCopyPostText}
onOpenTranslate={onOpenTranslate}
onDeletePost={onDeletePost}
/>
</View>
</View>
</Link>
</Link>
</PostMutedWrapper>
)
})
@ -245,14 +243,6 @@ const styles = StyleSheet.create({
layoutContent: {
flex: 1,
},
mutedWarning: {
flexDirection: 'row',
alignItems: 'center',
padding: 10,
marginTop: 2,
marginBottom: 6,
borderRadius: 2,
},
postTextContainer: {
flexDirection: 'row',
alignItems: 'center',

View File

@ -15,6 +15,7 @@ import {UserInfoText} from '../util/UserInfoText'
import {PostMeta} from '../util/PostMeta'
import {PostCtrls} from '../util/PostCtrls'
import {PostEmbeds} from '../util/PostEmbeds'
import {PostMutedWrapper} from '../util/PostMuted'
import {RichText} from '../util/text/RichText'
import * as Toast from '../util/Toast'
import {UserAvatar} from '../util/UserAvatar'
@ -113,6 +114,8 @@ export const FeedItem = observer(function ({
item._isThreadChild || (!item.reason && !item._hideParent && item.reply)
const isSmallTop = isChild && item._isThreadChild
const isNoTop = isChild && !item._isThreadChild
const isMuted =
item.post.author.viewer?.muted && ignoreMuteFor !== item.post.author.did
const outerStyles = [
styles.outer,
pal.view,
@ -123,7 +126,7 @@ export const FeedItem = observer(function ({
]
return (
<>
<PostMutedWrapper isMuted={isMuted}>
{isChild && !item._isThreadChild && item.replyParent ? (
<FeedItem
item={item.replyParent}
@ -160,7 +163,11 @@ export const FeedItem = observer(function ({
{color: pal.colors.textLight} as FontAwesomeIconStyle,
]}
/>
<Text type="sm-bold" style={pal.textLight} lineHeight={1.2}>
<Text
type="sm-bold"
style={pal.textLight}
lineHeight={1.2}
numberOfLines={1}>
Reposted by{' '}
{item.reasonRepost.by.displayName || item.reasonRepost.by.handle}
</Text>
@ -207,13 +214,7 @@ export const FeedItem = observer(function ({
/>
</View>
)}
{item.post.author.viewer?.muted &&
ignoreMuteFor !== item.post.author.did ? (
<View style={[styles.mutedWarning, pal.btn]}>
<FontAwesomeIcon icon={['far', 'eye-slash']} style={s.mr2} />
<Text type="sm">This post is by a muted account.</Text>
</View>
) : item.richText?.text ? (
{item.richText?.text ? (
<View style={styles.postTextContainer}>
<RichText
type="post-text"
@ -222,9 +223,7 @@ export const FeedItem = observer(function ({
/>
</View>
) : undefined}
{item.post.embed ? (
<PostEmbeds embed={item.post.embed} style={styles.embed} />
) : null}
<PostEmbeds embed={item.post.embed} style={styles.embed} />
<PostCtrls
style={styles.ctrls}
itemUri={itemUri}
@ -280,7 +279,7 @@ export const FeedItem = observer(function ({
</Text>
</Link>
) : undefined}
</>
</PostMutedWrapper>
)
})
@ -319,6 +318,7 @@ const styles = StyleSheet.create({
includeReason: {
flexDirection: 'row',
paddingLeft: 50,
paddingRight: 20,
marginTop: 2,
marginBottom: 2,
},
@ -336,14 +336,6 @@ const styles = StyleSheet.create({
layoutContent: {
flex: 1,
},
mutedWarning: {
flexDirection: 'row',
alignItems: 'center',
padding: 10,
marginTop: 2,
marginBottom: 6,
borderRadius: 2,
},
postTextContainer: {
flexDirection: 'row',
alignItems: 'center',

View File

@ -0,0 +1,50 @@
import React from 'react'
import {StyleSheet, TouchableOpacity, View} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {usePalette} from 'lib/hooks/usePalette'
import {Text} from './text/Text'
export function PostMutedWrapper({
isMuted,
children,
}: React.PropsWithChildren<{isMuted: boolean}>) {
const pal = usePalette('default')
const [override, setOverride] = React.useState(false)
if (!isMuted || override) {
return <>{children}</>
}
return (
<View style={[styles.container, pal.view, pal.border]}>
<FontAwesomeIcon
icon={['far', 'eye-slash']}
style={[styles.icon, pal.text]}
/>
<Text type="md" style={pal.textLight}>
Post from an account you muted.
</Text>
<TouchableOpacity
style={styles.showBtn}
onPress={() => setOverride(true)}>
<Text type="md" style={pal.link}>
Show post
</Text>
</TouchableOpacity>
</View>
)
}
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 14,
paddingHorizontal: 18,
borderTopWidth: 1,
},
icon: {
marginRight: 10,
},
showBtn: {
marginLeft: 'auto',
},
})

View File

@ -1,11 +1,81 @@
import Toast from 'react-native-root-toast'
import RootSiblings from 'react-native-root-siblings'
import React from 'react'
import {Animated, StyleSheet, View} from 'react-native'
import {Text} from './text/Text'
import {colors} from 'lib/styles'
import {useTheme} from 'lib/ThemeContext'
import {usePalette} from 'lib/hooks/usePalette'
import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
const TIMEOUT = 4e3
export function show(message: string) {
Toast.show(message, {
duration: Toast.durations.LONG,
position: 50,
shadow: true,
animation: true,
hideOnPress: true,
})
const item = new RootSiblings(<Toast message={message} />)
setTimeout(() => {
item.destroy()
}, TIMEOUT)
}
function Toast({message}: {message: string}) {
const theme = useTheme()
const pal = usePalette('default')
const interp = useAnimatedValue(0)
React.useEffect(() => {
Animated.sequence([
Animated.timing(interp, {
toValue: 1,
duration: 150,
useNativeDriver: true,
}),
Animated.delay(3700),
Animated.timing(interp, {
toValue: 0,
duration: 150,
useNativeDriver: true,
}),
]).start()
})
const opacityStyle = {opacity: interp}
return (
<View style={styles.container}>
<Animated.View
style={[
pal.view,
pal.border,
styles.toast,
theme.colorScheme === 'dark' && styles.toastDark,
opacityStyle,
]}>
<Text type="lg-medium" style={pal.text}>
{message}
</Text>
</Animated.View>
</View>
)
}
const styles = StyleSheet.create({
container: {
position: 'absolute',
top: 60,
left: 0,
right: 0,
alignItems: 'center',
},
toast: {
paddingHorizontal: 18,
paddingVertical: 10,
borderRadius: 24,
borderWidth: 1,
shadowColor: '#000',
shadowOpacity: 0.1,
shadowOffset: {width: 0, height: 4},
marginHorizontal: 6,
},
toastDark: {
backgroundColor: colors.gray6,
shadowOpacity: 0.5,
},
})

View File

@ -58,15 +58,15 @@ export function UserInfoText({
let inner
if (didFail) {
inner = (
<Text type={type} style={style}>
<Text type={type} style={style} numberOfLines={1}>
{failed}
</Text>
)
} else if (profile) {
inner = (
<Text type={type} style={style} lineHeight={1.2}>{`${prefix || ''}${
profile[attr] || profile.handle
}`}</Text>
<Text type={type} style={style} lineHeight={1.2} numberOfLines={1}>{`${
prefix || ''
}${profile[attr] || profile.handle}`}</Text>
)
} else {
inner = (

View File

@ -5,6 +5,7 @@ import {ThemeProvider, PaletteColorName} from 'lib/ThemeContext'
import {usePalette} from 'lib/hooks/usePalette'
import {s} from 'lib/styles'
import {displayNotification} from 'lib/notifee'
import * as Toast from 'view/com/util/Toast'
import {Text} from '../com/util/text/Text'
import {ViewSelector} from '../com/util/ViewSelector'
@ -171,16 +172,24 @@ function ErrorView() {
}
function NotifsView() {
const trigger = () => {
const triggerPush = () => {
displayNotification(
'Paul Frazee liked your post',
"Hello world! This is a test of the notifications card. The text is long to see how that's handled.",
)
}
const triggerToast = () => {
Toast.show('The task has been completed')
}
const triggerToast2 = () => {
Toast.show('The task has been completed successfully and with no problems')
}
return (
<View style={s.p10}>
<View style={s.flexRow}>
<Button onPress={trigger} label="Trigger" />
<Button onPress={triggerPush} label="Trigger Push" />
<Button onPress={triggerToast} label="Trigger Toast" />
<Button onPress={triggerToast2} label="Trigger Toast 2" />
</View>
</View>
)

View File

@ -1,15 +1,21 @@
import React, {useEffect, useMemo} from 'react'
import {View} from 'react-native'
import {StyleSheet, View} from 'react-native'
import {makeRecordUri} from 'lib/strings/url-helpers'
import {ViewHeader} from '../com/util/ViewHeader'
import {PostThread as PostThreadComponent} from '../com/post-thread/PostThread'
import {ComposePrompt} from 'view/com/composer/Prompt'
import {PostThreadViewModel} from 'state/models/post-thread-view'
import {ScreenParams} from '../routes'
import {useStores} from 'state/index'
import {s} from 'lib/styles'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {clamp} from 'lodash'
const SHELL_FOOTER_HEIGHT = 44
export const PostThread = ({navIdx, visible, params}: ScreenParams) => {
const store = useStores()
const safeAreaInsets = useSafeAreaInsets()
const {name, rkey} = params
const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
const view = useMemo<PostThreadViewModel>(
@ -48,12 +54,46 @@ export const PostThread = ({navIdx, visible, params}: ScreenParams) => {
}
}, [visible, store.nav, store.log, store.shell, name, navIdx, view])
const onPressReply = React.useCallback(() => {
if (!view.thread) {
return
}
store.shell.openComposer({
replyTo: {
uri: view.thread.post.uri,
cid: view.thread.post.cid,
text: view.thread.postRecord?.text as string,
author: {
handle: view.thread.post.author.handle,
displayName: view.thread.post.author.displayName,
avatar: view.thread.post.author.avatar,
},
},
onPost: () => view.refresh(),
})
}, [view, store])
return (
<View style={s.hContentRegion}>
<ViewHeader title="Post" />
<View style={s.hContentRegion}>
<PostThreadComponent uri={uri} view={view} />
</View>
<View
style={[
styles.prompt,
{bottom: SHELL_FOOTER_HEIGHT + clamp(safeAreaInsets.bottom, 15, 30)},
]}>
<ComposePrompt onPressCompose={onPressReply} />
</View>
</View>
)
}
const styles = StyleSheet.create({
prompt: {
position: 'absolute',
left: 0,
right: 0,
},
})

View File

@ -32,6 +32,7 @@ import {Text} from '../../com/util/text/Text'
import {useTheme} from 'lib/ThemeContext'
import {usePalette} from 'lib/hooks/usePalette'
import {useAnalytics} from 'lib/analytics'
import {pluralize} from 'lib/strings/helpers'
export const Menu = observer(({onClose}: {onClose: () => void}) => {
const theme = useTheme()
@ -138,6 +139,16 @@ export const Menu = observer(({onClose}: {onClose: () => void}) => {
<Text type="2xl" style={[pal.textLight, styles.profileCardHandle]}>
@{store.me.handle}
</Text>
<Text type="xl" style={[pal.textLight, styles.profileCardFollowers]}>
<Text type="xl-medium" style={pal.text}>
{store.me.followersCount || 0}
</Text>{' '}
{pluralize(store.me.followersCount || 0, 'follower')} &middot;{' '}
<Text type="xl-medium" style={pal.text}>
{store.me.followsCount || 0}
</Text>{' '}
following
</Text>
</TouchableOpacity>
<View style={s.flex1} />
<View>
@ -267,12 +278,12 @@ export const Menu = observer(({onClose}: {onClose: () => void}) => {
const styles = StyleSheet.create({
view: {
flex: 1,
paddingTop: 10,
paddingTop: 20,
paddingBottom: 50,
paddingLeft: 30,
},
viewDarkMode: {
backgroundColor: '#202023',
backgroundColor: '#1B1919',
},
profileCardDisplayName: {
@ -283,6 +294,10 @@ const styles = StyleSheet.create({
marginTop: 4,
paddingRight: 20,
},
profileCardFollowers: {
marginTop: 16,
paddingRight: 20,
},
menuItem: {
flexDirection: 'row',
@ -316,7 +331,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
justifyContent: 'space-between',
paddingRight: 30,
paddingTop: 20,
paddingTop: 80,
},
footerBtn: {
flexDirection: 'row',

View File

@ -157,7 +157,7 @@ export const MobileShell: React.FC = observer(() => {
}
const screenBg = {
backgroundColor: theme.colorScheme === 'dark' ? colors.gray7 : colors.gray1,
backgroundColor: theme.colorScheme === 'dark' ? colors.black : colors.gray1,
}
return (
<View testID="mobileShellView" style={[styles.outerContainer, pal.view]}>
@ -202,13 +202,7 @@ export const MobileShell: React.FC = observer(() => {
style={[
s.h100pct,
screenBg,
current
? [
swipeTransform,
// tabMenuTransform, TODO
// isRunningNewTabAnim ? newTabTransform : undefined, TODO
]
: undefined,
current ? [swipeTransform] : undefined,
]}>
<ErrorBoundary>
<Com

View File

@ -9,7 +9,6 @@ const uncompiled_deps = [
'@bam.tech/react-native-image-resizer',
'react-native-fs',
'rn-fetch-blob',
'react-native-root-toast',
'react-native-root-siblings',
'react-native-linear-gradient',
]