Add user invite codes (#393)

* Add mobile UIs for invite codes

* Update invite code UIs for web

* Finish implementing invite code behaviors (including notifications of invited users)

* Bump deps

* Update web right nav to use real data; also fix lint
This commit is contained in:
Paul Frazee 2023-04-05 18:56:02 -05:00 committed by GitHub
parent 8e28d3c6be
commit ea04c2bd33
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 932 additions and 246 deletions

View file

@ -167,7 +167,9 @@ export const BottomBar = observer(({navigation}: BottomTabBarProps) => {
)
}
onPress={onPressNotifications}
notificationCount={store.me.notifications.unreadCount}
notificationCount={
store.me.notifications.unreadCount + store.invitedUsers.numNotifs
}
/>
<Btn
testID="bottomBarProfileBtn"

View file

@ -103,63 +103,19 @@ export const DrawerContent = observer(() => {
store.shell.closeDrawer()
}, [navigation, track, store.shell])
const onPressFeedback = () => {
const onPressFeedback = React.useCallback(() => {
track('Menu:FeedbackClicked')
Linking.openURL(FEEDBACK_FORM_URL)
}
}, [track])
const onDarkmodePress = React.useCallback(() => {
track('Menu:ItemClicked', {url: '#darkmode'})
store.shell.setDarkMode(!store.shell.darkMode)
}, [track, store])
// rendering
// =
const MenuItem = ({
icon,
label,
count,
bold,
onPress,
}: {
icon: JSX.Element
label: string
count?: number
bold?: boolean
onPress: () => void
}) => (
<TouchableOpacity
testID={`menuItemButton-${label}`}
style={styles.menuItem}
onPress={onPress}>
<View style={[styles.menuItemIconWrapper]}>
{icon}
{count ? (
<View
style={[
styles.menuItemCount,
count > 99
? styles.menuItemCountHundreds
: count > 9
? styles.menuItemCountTens
: undefined,
]}>
<Text style={styles.menuItemCountLabel} numberOfLines={1}>
{count > 999 ? `${Math.round(count / 1000)}k` : count}
</Text>
</View>
) : undefined}
</View>
<Text
type={bold ? '2xl-bold' : '2xl'}
style={[pal.text, s.flex1]}
numberOfLines={1}>
{label}
</Text>
</TouchableOpacity>
)
const onDarkmodePress = () => {
track('Menu:ItemClicked', {url: '/darkmode'})
store.shell.setDarkMode(!store.shell.darkMode)
}
return (
<View
testID="drawer"
@ -168,29 +124,34 @@ export const DrawerContent = observer(() => {
theme.colorScheme === 'light' ? pal.view : styles.viewDarkMode,
]}>
<SafeAreaView style={s.flex1}>
<TouchableOpacity testID="profileCardButton" onPress={onPressProfile}>
<UserAvatar size={80} avatar={store.me.avatar} />
<Text
type="title-lg"
style={[pal.text, s.bold, styles.profileCardDisplayName]}>
{store.me.displayName || store.me.handle}
</Text>
<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={styles.main}>
<TouchableOpacity testID="profileCardButton" onPress={onPressProfile}>
<UserAvatar size={80} avatar={store.me.avatar} />
<Text
type="title-lg"
style={[pal.text, s.bold, styles.profileCardDisplayName]}>
{store.me.displayName || store.me.handle}
</Text>
<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>
<InviteCodes />
<View style={s.flex1} />
<View>
<View style={styles.main}>
<MenuItem
icon={
isAtSearch ? (
@ -248,7 +209,9 @@ export const DrawerContent = observer(() => {
)
}
label="Notifications"
count={store.me.notifications.unreadCount}
count={
store.me.notifications.unreadCount + store.invitedUsers.numNotifs
}
bold={isAtNotifications}
onPress={onPressNotifications}
/>
@ -315,16 +278,97 @@ export const DrawerContent = observer(() => {
)
})
function MenuItem({
icon,
label,
count,
bold,
onPress,
}: {
icon: JSX.Element
label: string
count?: number
bold?: boolean
onPress: () => void
}) {
const pal = usePalette('default')
return (
<TouchableOpacity
testID={`menuItemButton-${label}`}
style={styles.menuItem}
onPress={onPress}>
<View style={[styles.menuItemIconWrapper]}>
{icon}
{count ? (
<View
style={[
styles.menuItemCount,
count > 99
? styles.menuItemCountHundreds
: count > 9
? styles.menuItemCountTens
: undefined,
]}>
<Text style={styles.menuItemCountLabel} numberOfLines={1}>
{count > 999 ? `${Math.round(count / 1000)}k` : count}
</Text>
</View>
) : undefined}
</View>
<Text
type={bold ? '2xl-bold' : '2xl'}
style={[pal.text, s.flex1]}
numberOfLines={1}>
{label}
</Text>
</TouchableOpacity>
)
}
const InviteCodes = observer(() => {
const {track} = useAnalytics()
const store = useStores()
const pal = usePalette('default')
const onPress = React.useCallback(() => {
track('Menu:ItemClicked', {url: '#invite-codes'})
store.shell.closeDrawer()
store.shell.openModal({name: 'invite-codes'})
}, [store, track])
return (
<TouchableOpacity
testID="menuItemInviteCodes"
style={[styles.inviteCodes]}
onPress={onPress}>
<FontAwesomeIcon
icon="ticket"
style={[
styles.inviteCodesIcon,
store.me.invitesAvailable > 0 ? pal.link : pal.textLight,
]}
size={18}
/>
<Text
type="lg-medium"
style={store.me.invitesAvailable > 0 ? pal.link : pal.textLight}>
{store.me.invitesAvailable} invite{' '}
{pluralize(store.me.invitesAvailable, 'code')}
</Text>
</TouchableOpacity>
)
})
const styles = StyleSheet.create({
view: {
flex: 1,
paddingTop: 20,
paddingBottom: 50,
paddingLeft: 20,
},
viewDarkMode: {
backgroundColor: '#1B1919',
},
main: {
paddingLeft: 20,
},
profileCardDisplayName: {
marginTop: 20,
@ -336,7 +380,7 @@ const styles = StyleSheet.create({
},
profileCardFollowers: {
marginTop: 16,
paddingRight: 30,
paddingRight: 10,
},
menuItem: {
@ -376,11 +420,22 @@ const styles = StyleSheet.create({
color: colors.white,
},
inviteCodes: {
paddingLeft: 22,
paddingVertical: 8,
flexDirection: 'row',
alignItems: 'center',
},
inviteCodesIcon: {
marginRight: 6,
},
footer: {
flexDirection: 'row',
justifyContent: 'space-between',
paddingRight: 30,
paddingTop: 80,
paddingTop: 20,
paddingLeft: 20,
},
footerBtn: {
flexDirection: 'row',

View file

@ -157,7 +157,9 @@ export const DesktopLeftNav = observer(function DesktopLeftNav() {
/>
<NavItem
href="/notifications"
count={store.me.notifications.unreadCount}
count={
store.me.notifications.unreadCount + store.invitedUsers.numNotifs
}
icon={<BellIcon strokeWidth={2} size={24} style={pal.text} />}
iconFilled={
<BellIconSolid strokeWidth={1.5} size={24} style={pal.text} />

View file

@ -1,6 +1,7 @@
import React from 'react'
import {observer} from 'mobx-react-lite'
import {StyleSheet, View} from 'react-native'
import {StyleSheet, TouchableOpacity, View} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {usePalette} from 'lib/hooks/usePalette'
import {DesktopSearch} from './Search'
import {Text} from 'view/com/util/text/Text'
@ -8,6 +9,7 @@ import {TextLink} from 'view/com/util/Link'
import {FEEDBACK_FORM_URL} from 'lib/constants'
import {s} from 'lib/styles'
import {useStores} from 'state/index'
import {pluralize} from 'lib/strings/helpers'
export const DesktopRightNav = observer(function DesktopRightNav() {
const store = useStores()
@ -38,10 +40,40 @@ export const DesktopRightNav = observer(function DesktopRightNav() {
/>
</View>
</View>
<InviteCodes />
</View>
)
})
function InviteCodes() {
const store = useStores()
const pal = usePalette('default')
const onPress = React.useCallback(() => {
store.shell.openModal({name: 'invite-codes'})
}, [store])
return (
<TouchableOpacity
style={[styles.inviteCodes, pal.border]}
onPress={onPress}>
<FontAwesomeIcon
icon="ticket"
style={[
styles.inviteCodesIcon,
store.me.invitesAvailable > 0 ? pal.link : pal.textLight,
]}
size={16}
/>
<Text
type="md-medium"
style={store.me.invitesAvailable > 0 ? pal.link : pal.textLight}>
{store.me.invitesAvailable} invite{' '}
{pluralize(store.me.invitesAvailable, 'code')} available
</Text>
</TouchableOpacity>
)
}
const styles = StyleSheet.create({
rightNav: {
position: 'absolute',
@ -57,4 +89,16 @@ const styles = StyleSheet.create({
messageLine: {
marginBottom: 10,
},
inviteCodes: {
marginTop: 12,
borderTopWidth: 1,
paddingHorizontal: 16,
paddingVertical: 12,
flexDirection: 'row',
alignItems: 'center',
},
inviteCodesIcon: {
marginRight: 6,
},
})