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 overflowszio/stable
parent
2f3fc4fe4e
commit
e74f94bc72
|
@ -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",
|
||||
|
|
|
@ -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},
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
})
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
|
|
|
@ -96,7 +96,7 @@ export const PostThread = observer(function PostThread({
|
|||
onLayout={onLayout}
|
||||
onScrollToIndexFailed={onScrollToIndexFailed}
|
||||
style={s.hContentRegion}
|
||||
contentContainerStyle={s.contentContainer}
|
||||
contentContainerStyle={s.contentContainerExtra}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
})
|
|
@ -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,
|
||||
},
|
||||
})
|
||||
|
|
|
@ -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 = (
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
})
|
||||
|
|
|
@ -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')} ·{' '}
|
||||
<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',
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
]
|
||||
|
|
Loading…
Reference in New Issue