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-progress": "^5.0.0",
"react-native-reanimated": "^2.9.1", "react-native-reanimated": "^2.9.1",
"react-native-root-siblings": "^4.1.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-safe-area-context": "^4.4.1",
"react-native-screens": "^3.13.1", "react-native-screens": "^3.13.1",
"react-native-splash-screen": "^3.3.0", "react-native-splash-screen": "^3.3.0",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -17,6 +17,7 @@ import {UserInfoText} from '../util/UserInfoText'
import {PostMeta} from '../util/PostMeta' import {PostMeta} from '../util/PostMeta'
import {PostEmbeds} from '../util/PostEmbeds' import {PostEmbeds} from '../util/PostEmbeds'
import {PostCtrls} from '../util/PostCtrls' import {PostCtrls} from '../util/PostCtrls'
import {PostMutedWrapper} from '../util/PostMuted'
import {Text} from '../util/text/Text' import {Text} from '../util/text/Text'
import {RichText} from '../util/text/RichText' import {RichText} from '../util/text/RichText'
import * as Toast from '../util/Toast' import * as Toast from '../util/Toast'
@ -140,92 +141,89 @@ export const Post = observer(function Post({
} }
return ( return (
<Link <PostMutedWrapper isMuted={item.post.author.viewer?.muted === true}>
style={[styles.outer, pal.view, pal.border, style]} <Link
href={itemHref} style={[styles.outer, pal.view, pal.border, style]}
title={itemTitle} href={itemHref}
noFeedback> title={itemTitle}
{showReplyLine && <View style={styles.replyLine} />} noFeedback>
<View style={styles.layout}> {showReplyLine && <View style={styles.replyLine} />}
<View style={styles.layoutAvi}> <View style={styles.layout}>
<Link href={authorHref} title={authorTitle}> <View style={styles.layoutAvi}>
<UserAvatar <Link href={authorHref} title={authorTitle}>
size={52} <UserAvatar
displayName={item.post.author.displayName} size={52}
handle={item.post.author.handle} displayName={item.post.author.displayName}
avatar={item.post.author.avatar} 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>
<View style={styles.layoutContent}> </Link>
<PostMeta </PostMutedWrapper>
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>
) )
}) })
@ -245,14 +243,6 @@ const styles = StyleSheet.create({
layoutContent: { layoutContent: {
flex: 1, flex: 1,
}, },
mutedWarning: {
flexDirection: 'row',
alignItems: 'center',
padding: 10,
marginTop: 2,
marginBottom: 6,
borderRadius: 2,
},
postTextContainer: { postTextContainer: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',

View File

@ -15,6 +15,7 @@ import {UserInfoText} from '../util/UserInfoText'
import {PostMeta} from '../util/PostMeta' import {PostMeta} from '../util/PostMeta'
import {PostCtrls} from '../util/PostCtrls' import {PostCtrls} from '../util/PostCtrls'
import {PostEmbeds} from '../util/PostEmbeds' import {PostEmbeds} from '../util/PostEmbeds'
import {PostMutedWrapper} from '../util/PostMuted'
import {RichText} from '../util/text/RichText' import {RichText} from '../util/text/RichText'
import * as Toast from '../util/Toast' import * as Toast from '../util/Toast'
import {UserAvatar} from '../util/UserAvatar' import {UserAvatar} from '../util/UserAvatar'
@ -113,6 +114,8 @@ export const FeedItem = observer(function ({
item._isThreadChild || (!item.reason && !item._hideParent && item.reply) item._isThreadChild || (!item.reason && !item._hideParent && item.reply)
const isSmallTop = isChild && item._isThreadChild const isSmallTop = isChild && item._isThreadChild
const isNoTop = isChild && !item._isThreadChild const isNoTop = isChild && !item._isThreadChild
const isMuted =
item.post.author.viewer?.muted && ignoreMuteFor !== item.post.author.did
const outerStyles = [ const outerStyles = [
styles.outer, styles.outer,
pal.view, pal.view,
@ -123,7 +126,7 @@ export const FeedItem = observer(function ({
] ]
return ( return (
<> <PostMutedWrapper isMuted={isMuted}>
{isChild && !item._isThreadChild && item.replyParent ? ( {isChild && !item._isThreadChild && item.replyParent ? (
<FeedItem <FeedItem
item={item.replyParent} item={item.replyParent}
@ -160,7 +163,11 @@ export const FeedItem = observer(function ({
{color: pal.colors.textLight} as FontAwesomeIconStyle, {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{' '} Reposted by{' '}
{item.reasonRepost.by.displayName || item.reasonRepost.by.handle} {item.reasonRepost.by.displayName || item.reasonRepost.by.handle}
</Text> </Text>
@ -207,13 +214,7 @@ export const FeedItem = observer(function ({
/> />
</View> </View>
)} )}
{item.post.author.viewer?.muted && {item.richText?.text ? (
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 ? (
<View style={styles.postTextContainer}> <View style={styles.postTextContainer}>
<RichText <RichText
type="post-text" type="post-text"
@ -222,9 +223,7 @@ export const FeedItem = observer(function ({
/> />
</View> </View>
) : undefined} ) : undefined}
{item.post.embed ? ( <PostEmbeds embed={item.post.embed} style={styles.embed} />
<PostEmbeds embed={item.post.embed} style={styles.embed} />
) : null}
<PostCtrls <PostCtrls
style={styles.ctrls} style={styles.ctrls}
itemUri={itemUri} itemUri={itemUri}
@ -280,7 +279,7 @@ export const FeedItem = observer(function ({
</Text> </Text>
</Link> </Link>
) : undefined} ) : undefined}
</> </PostMutedWrapper>
) )
}) })
@ -319,6 +318,7 @@ const styles = StyleSheet.create({
includeReason: { includeReason: {
flexDirection: 'row', flexDirection: 'row',
paddingLeft: 50, paddingLeft: 50,
paddingRight: 20,
marginTop: 2, marginTop: 2,
marginBottom: 2, marginBottom: 2,
}, },
@ -336,14 +336,6 @@ const styles = StyleSheet.create({
layoutContent: { layoutContent: {
flex: 1, flex: 1,
}, },
mutedWarning: {
flexDirection: 'row',
alignItems: 'center',
padding: 10,
marginTop: 2,
marginBottom: 6,
borderRadius: 2,
},
postTextContainer: { postTextContainer: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', 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) { export function show(message: string) {
Toast.show(message, { const item = new RootSiblings(<Toast message={message} />)
duration: Toast.durations.LONG, setTimeout(() => {
position: 50, item.destroy()
shadow: true, }, TIMEOUT)
animation: true,
hideOnPress: true,
})
} }
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 let inner
if (didFail) { if (didFail) {
inner = ( inner = (
<Text type={type} style={style}> <Text type={type} style={style} numberOfLines={1}>
{failed} {failed}
</Text> </Text>
) )
} else if (profile) { } else if (profile) {
inner = ( inner = (
<Text type={type} style={style} lineHeight={1.2}>{`${prefix || ''}${ <Text type={type} style={style} lineHeight={1.2} numberOfLines={1}>{`${
profile[attr] || profile.handle prefix || ''
}`}</Text> }${profile[attr] || profile.handle}`}</Text>
) )
} else { } else {
inner = ( inner = (

View File

@ -5,6 +5,7 @@ import {ThemeProvider, PaletteColorName} from 'lib/ThemeContext'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {s} from 'lib/styles' import {s} from 'lib/styles'
import {displayNotification} from 'lib/notifee' import {displayNotification} from 'lib/notifee'
import * as Toast from 'view/com/util/Toast'
import {Text} from '../com/util/text/Text' import {Text} from '../com/util/text/Text'
import {ViewSelector} from '../com/util/ViewSelector' import {ViewSelector} from '../com/util/ViewSelector'
@ -171,16 +172,24 @@ function ErrorView() {
} }
function NotifsView() { function NotifsView() {
const trigger = () => { const triggerPush = () => {
displayNotification( displayNotification(
'Paul Frazee liked your post', '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.", "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 ( return (
<View style={s.p10}> <View style={s.p10}>
<View style={s.flexRow}> <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> </View>
) )

View File

@ -1,15 +1,21 @@
import React, {useEffect, useMemo} from 'react' 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 {makeRecordUri} from 'lib/strings/url-helpers'
import {ViewHeader} from '../com/util/ViewHeader' import {ViewHeader} from '../com/util/ViewHeader'
import {PostThread as PostThreadComponent} from '../com/post-thread/PostThread' 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 {PostThreadViewModel} from 'state/models/post-thread-view'
import {ScreenParams} from '../routes' import {ScreenParams} from '../routes'
import {useStores} from 'state/index' import {useStores} from 'state/index'
import {s} from 'lib/styles' 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) => { export const PostThread = ({navIdx, visible, params}: ScreenParams) => {
const store = useStores() const store = useStores()
const safeAreaInsets = useSafeAreaInsets()
const {name, rkey} = params const {name, rkey} = params
const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey) const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
const view = useMemo<PostThreadViewModel>( 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]) }, [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 ( return (
<View style={s.hContentRegion}> <View style={s.hContentRegion}>
<ViewHeader title="Post" /> <ViewHeader title="Post" />
<View style={s.hContentRegion}> <View style={s.hContentRegion}>
<PostThreadComponent uri={uri} view={view} /> <PostThreadComponent uri={uri} view={view} />
</View> </View>
<View
style={[
styles.prompt,
{bottom: SHELL_FOOTER_HEIGHT + clamp(safeAreaInsets.bottom, 15, 30)},
]}>
<ComposePrompt onPressCompose={onPressReply} />
</View>
</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 {useTheme} from 'lib/ThemeContext'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {useAnalytics} from 'lib/analytics' import {useAnalytics} from 'lib/analytics'
import {pluralize} from 'lib/strings/helpers'
export const Menu = observer(({onClose}: {onClose: () => void}) => { export const Menu = observer(({onClose}: {onClose: () => void}) => {
const theme = useTheme() const theme = useTheme()
@ -138,6 +139,16 @@ export const Menu = observer(({onClose}: {onClose: () => void}) => {
<Text type="2xl" style={[pal.textLight, styles.profileCardHandle]}> <Text type="2xl" style={[pal.textLight, styles.profileCardHandle]}>
@{store.me.handle} @{store.me.handle}
</Text> </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> </TouchableOpacity>
<View style={s.flex1} /> <View style={s.flex1} />
<View> <View>
@ -267,12 +278,12 @@ export const Menu = observer(({onClose}: {onClose: () => void}) => {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
view: { view: {
flex: 1, flex: 1,
paddingTop: 10, paddingTop: 20,
paddingBottom: 50, paddingBottom: 50,
paddingLeft: 30, paddingLeft: 30,
}, },
viewDarkMode: { viewDarkMode: {
backgroundColor: '#202023', backgroundColor: '#1B1919',
}, },
profileCardDisplayName: { profileCardDisplayName: {
@ -283,6 +294,10 @@ const styles = StyleSheet.create({
marginTop: 4, marginTop: 4,
paddingRight: 20, paddingRight: 20,
}, },
profileCardFollowers: {
marginTop: 16,
paddingRight: 20,
},
menuItem: { menuItem: {
flexDirection: 'row', flexDirection: 'row',
@ -316,7 +331,7 @@ const styles = StyleSheet.create({
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-between', justifyContent: 'space-between',
paddingRight: 30, paddingRight: 30,
paddingTop: 20, paddingTop: 80,
}, },
footerBtn: { footerBtn: {
flexDirection: 'row', flexDirection: 'row',

View File

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

View File

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