Skip to content

Commit 81a4f13

Browse files
committed
Quick Feedback input on all modals
1 parent da8f89a commit 81a4f13

11 files changed

Lines changed: 249 additions & 24 deletions

File tree

packages/components/src/components/banners/AppBannerMessage.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ export function AppBannerMessage() {
7979
<Spacer width={contentPadding / 2} />
8080

8181
<IconButton
82+
family="octicon"
8283
name="x"
8384
onPress={() => closeBannerMessage(bannerMessage.id)}
8485
size={18}

packages/components/src/components/cards/CardsSearchHeader.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,7 @@ export const CardsSearchHeader = React.memo((props: CardsSearchHeaderProps) => {
402402
active={forceShowTextInput}
403403
analyticsAction="toggle"
404404
analyticsLabel="column_search"
405+
family="octicon"
405406
name="search"
406407
onPress={() => {
407408
batch(() => {

packages/components/src/components/columns/ColumnFiltersButton.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export const ColumnFiltersButton = React.memo(
4141
key="column-filters-toggle-button"
4242
analyticsAction="toggle"
4343
analyticsLabel="column_filters"
44+
family="octicon"
4445
name="settings"
4546
onPress={onPress}
4647
style={style}

packages/components/src/components/columns/ColumnOptions.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,7 @@ export const ColumnOptions = React.memo(
348348
columnIndex === 0 ||
349349
!!(plan && columnIndex + 1 > plan.featureFlags.columnsLimit)
350350
}
351+
family="octicon"
351352
name="chevron-left"
352353
onPress={() =>
353354
dispatch(
@@ -373,6 +374,7 @@ export const ColumnOptions = React.memo(
373374
columnIndex + 1 >= constants.COLUMNS_LIMIT ||
374375
!!(plan && columnIndex + 1 > plan.featureFlags.columnsLimit - 1)
375376
}
377+
family="octicon"
376378
name="chevron-right"
377379
onPress={() =>
378380
dispatch(
@@ -398,6 +400,7 @@ export const ColumnOptions = React.memo(
398400
<IconButton
399401
key="column-options-button-remove-column"
400402
analyticsLabel="remove_column"
403+
family="octicon"
401404
name="trashcan"
402405
onPress={() =>
403406
dispatch(actions.deleteColumn({ columnId, columnIndex }))

packages/components/src/components/columns/ModalColumn.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import * as selectors from '../../redux/selectors'
1010
import { sharedStyles } from '../../styles/shared'
1111
import { contentPadding } from '../../styles/variables'
1212
import { findNode, tryFocus } from '../../utils/helpers/shared'
13+
import { QuickFeedbackRow } from '../common/QuickFeedbackRow'
1314
import { Spacer } from '../common/Spacer'
1415
import { keyboardShortcutsById } from '../modals/KeyboardShortcutsModal'
1516
import { Column } from './Column'
@@ -115,7 +116,17 @@ export const ModalColumn = React.memo((props: ModalColumnProps) => {
115116
}
116117
/>
117118

118-
{children}
119+
<View style={sharedStyles.flex}>{children}</View>
120+
121+
<View
122+
style={[
123+
sharedStyles.fullWidth,
124+
sharedStyles.horizontalAndVerticallyAligned,
125+
sharedStyles.padding,
126+
]}
127+
>
128+
<QuickFeedbackRow />
129+
</View>
119130
</Column>
120131
)
121132
})

packages/components/src/components/common/IconButton.tsx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,31 +4,31 @@ import { StyleSheet } from 'react-native'
44
import { ThemeColors } from '@devhub/core'
55
import { useDynamicRef } from '../../hooks/use-dynamic-ref'
66
import { useHover } from '../../hooks/use-hover'
7-
import { OcticonIconProps, Octicons } from '../../libs/vector-icons'
87
import { sharedStyles } from '../../styles/shared'
98
import { contentPadding } from '../../styles/variables'
109
import { getTheme } from '../context/ThemeContext'
1110
import { getThemeColorOrItself } from '../themed/helpers'
11+
import { ThemedIcon, ThemedIconProps } from '../themed/ThemedIcon'
1212
import { ThemedText } from '../themed/ThemedText'
1313
import { ThemedView } from '../themed/ThemedView'
1414
import {
1515
TouchableWithoutFeedback,
1616
TouchableWithoutFeedbackProps,
1717
} from './TouchableWithoutFeedback'
1818

19-
export interface IconButtonProps extends TouchableWithoutFeedbackProps {
19+
export type IconButtonProps = TouchableWithoutFeedbackProps & {
2020
active?: boolean
21-
name: OcticonIconProps['name']
2221
size?: number
2322
type?: 'primary' | 'neutral' | 'danger'
24-
}
23+
} & Pick<ThemedIconProps, 'family' | 'name'>
2524

2625
export const defaultIconButtonSize = 17
2726

2827
export function IconButton(props: IconButtonProps) {
2928
const {
3029
active,
3130
disabled: _disabled,
31+
family,
3232
name,
3333
onPressIn,
3434
onPressOut,
@@ -164,8 +164,9 @@ export function IconButton(props: IconButtonProps) {
164164
/>
165165

166166
<ThemedText ref={textRef} color={foregroundThemeColor}>
167-
<Octicons
168-
name={name}
167+
<ThemedIcon
168+
family={family as any}
169+
name={name as any}
169170
selectable={false}
170171
size={size}
171172
style={sharedStyles.textCenter}
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import { constants } from '@devhub/core'
2+
import axios from 'axios'
3+
import React, { useEffect, useRef, useState } from 'react'
4+
import { View } from 'react-native'
5+
import { batch } from 'react-redux'
6+
7+
import { useReduxState } from '../../hooks/use-redux-state'
8+
import { bugsnag } from '../../libs/bugsnag'
9+
import * as selectors from '../../redux/selectors'
10+
import { sharedStyles } from '../../styles/shared'
11+
import { getDefaultDevHubHeaders } from '../../utils/api'
12+
import { ThemedText } from '../themed/ThemedText'
13+
import { ThemedTextInput } from '../themed/ThemedTextInput'
14+
import { Link } from './Link'
15+
import { defaultTextInputHeight } from './TextInput'
16+
17+
export interface QuickFeedbackRowProps {}
18+
19+
export const quickFeedbackRowHeight = defaultTextInputHeight
20+
21+
export function QuickFeedbackRow(_props: QuickFeedbackRowProps) {
22+
const isMountedRef = useRef(true)
23+
24+
const [feedbackText, setFeedbackText] = useState('')
25+
const [message, setMessage] = useState('')
26+
const [placeholder, setPlaceholder] = useState('')
27+
28+
const appToken = useReduxState(selectors.appTokenSelector)
29+
const currentOpenedModal = useReduxState(selectors.currentOpenedModal)
30+
31+
useEffect(() => {
32+
isMountedRef.current = true
33+
34+
return () => {
35+
isMountedRef.current = false
36+
}
37+
}, [])
38+
39+
async function trySendFeedback() {
40+
const response = await axios.post(
41+
constants.GRAPHQL_ENDPOINT,
42+
{
43+
query: `
44+
mutation($input: SendFeedbackInput) {
45+
sendFeedback(input: $input)
46+
}`,
47+
variables: {
48+
input: {
49+
feedback: feedbackText,
50+
screeName: currentOpenedModal.name,
51+
},
52+
},
53+
},
54+
{ headers: getDefaultDevHubHeaders({ appToken }) },
55+
)
56+
57+
if (!isMountedRef.current) return
58+
59+
const { data, errors } = await response.data
60+
61+
if (errors && errors[0] && errors[0].message)
62+
throw new Error(errors[0].message)
63+
64+
if (!(data && data.sendFeedback)) {
65+
throw new Error('Failed to send feedback.')
66+
}
67+
}
68+
69+
async function handleSubmit() {
70+
if (!isMountedRef.current) return
71+
72+
if (!(feedbackText && feedbackText.trim())) return
73+
74+
try {
75+
setMessage('Sending...')
76+
77+
await trySendFeedback()
78+
if (!isMountedRef.current) return
79+
80+
if (feedbackText.toLowerCase().includes('expensive')) {
81+
setMessage('Got it...')
82+
83+
setTimeout(() => {
84+
setMessage('How much would you pay?')
85+
setTimeout(() => {
86+
batch(() => {
87+
setMessage('')
88+
setFeedbackText('')
89+
setPlaceholder('How much would you pay?')
90+
})
91+
}, 2000)
92+
}, 1000)
93+
94+
return
95+
}
96+
if (feedbackText.toLowerCase().includes(' caro')) {
97+
setMessage('Entendi...')
98+
setTimeout(() => {
99+
setMessage('Quanto você pagaria?')
100+
setTimeout(() => {
101+
batch(() => {
102+
setMessage('')
103+
setFeedbackText('')
104+
setPlaceholder('Quanto você pagaria?')
105+
})
106+
}, 2000)
107+
}, 1000)
108+
109+
return
110+
}
111+
112+
setTimeout(() => {
113+
setMessage('Thanks for your feedback!')
114+
115+
setTimeout(() => {
116+
setMessage('Join our Slack to get a response.')
117+
118+
setTimeout(() => {
119+
batch(() => {
120+
setMessage('')
121+
setFeedbackText('')
122+
setPlaceholder('')
123+
})
124+
}, 8000)
125+
}, 2000)
126+
}, 1000)
127+
} catch (error) {
128+
console.error(error)
129+
bugsnag.notify(error)
130+
131+
setMessage(`Failed to send feedback. Please contact us.`)
132+
133+
setTimeout(() => {
134+
batch(() => {
135+
setMessage('')
136+
setFeedbackText('')
137+
setPlaceholder('')
138+
})
139+
}, 2000)
140+
}
141+
}
142+
143+
return (
144+
<View
145+
style={[
146+
sharedStyles.fullWidth,
147+
sharedStyles.horizontalAndVerticallyAligned,
148+
{ height: quickFeedbackRowHeight },
149+
]}
150+
>
151+
{message ? (
152+
message.toLowerCase().includes('slack') ? (
153+
<Link
154+
analyticsLabel="join_slack_after_feedback"
155+
enableUnderlineHover
156+
href={constants.DEVHUB_LINKS.SLACK_INVITATION}
157+
openOnNewTab
158+
textProps={{
159+
color: 'foregroundColorMuted65',
160+
style: [sharedStyles.flex, sharedStyles.textCenter],
161+
}}
162+
style={sharedStyles.flex}
163+
>
164+
{message}
165+
</Link>
166+
) : (
167+
<ThemedText
168+
color="foregroundColorMuted65"
169+
style={[sharedStyles.flex, sharedStyles.textCenter]}
170+
>
171+
{message}
172+
</ThemedText>
173+
)
174+
) : (
175+
<ThemedTextInput
176+
textInputKey="quick-feedback-text-input"
177+
onChangeText={text => {
178+
setFeedbackText(text)
179+
}}
180+
onSubmitEditing={handleSubmit}
181+
placeholder={placeholder || 'Send quick feedback'}
182+
style={[sharedStyles.flex, sharedStyles.textCenter]}
183+
/>
184+
)}
185+
</View>
186+
)
187+
}

packages/components/src/components/modals/ModalRenderer.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,7 @@ export function ModalRenderer(props: ModalRendererProps) {
279279
<View
280280
collapsable={false}
281281
style={[
282+
sharedStyles.flex,
282283
sharedStyles.fullHeight,
283284
sharedStyles.overflowHidden,
284285
{

packages/components/src/components/themed/ThemedIcon.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export type ThemedIconProps = {
2424
} & Omit<MaterialIconProps, 'color' | 'style'>))
2525

2626
export const ThemedIcon = React.memo(
27-
React.forwardRef<Octicons, ThemedIconProps>((props, ref) => {
27+
React.forwardRef<Octicons | MaterialIcons, ThemedIconProps>((props, ref) => {
2828
const {
2929
color: _color,
3030
family = 'octicon',
@@ -47,4 +47,4 @@ export const ThemedIcon = React.memo(
4747

4848
ThemedIcon.displayName = 'ThemedIcon'
4949

50-
export type ThemedIcon = Octicons
50+
export type ThemedIcon = Octicons | MaterialIcons

packages/components/src/hooks/use-cards-props.tsx

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,16 @@ import {
3434
import { EmptyCards } from '../components/cards/EmptyCards'
3535
import { ColumnLoadingIndicator } from '../components/columns/ColumnLoadingIndicator'
3636
import { Button } from '../components/common/Button'
37+
import { QuickFeedbackRow } from '../components/common/QuickFeedbackRow'
3738
import { RefreshControl } from '../components/common/RefreshControl'
39+
import { Spacer } from '../components/common/Spacer'
3840
import { useAppLayout } from '../components/context/LayoutContext'
3941
import { OneListProps } from '../libs/one-list'
4042
import { useSafeArea } from '../libs/safe-area-view'
4143
import * as actions from '../redux/actions'
4244
import * as selectors from '../redux/selectors'
4345
import { sharedStyles } from '../styles/shared'
46+
import { contentPadding } from '../styles/variables'
4447
import { useColumn } from './use-column'
4548
import { useReduxState } from './use-redux-state'
4649

@@ -345,21 +348,27 @@ export function useCardsProps<ItemT extends EnhancedItem>({
345348
columnId={column.id}
346349
emoji="lock"
347350
errorButtonView={
348-
<Button
349-
analyticsCategory="plan_expired"
350-
analyticsLabel="select_a_plan_button"
351-
children="Select a plan"
352-
onPress={() => {
353-
dispatch(
354-
actions.pushModal({
355-
name: 'PRICING',
356-
params: {
357-
highlightFeature: 'columnsLimit',
358-
},
359-
}),
360-
)
361-
}}
362-
/>
351+
<View>
352+
<Button
353+
analyticsCategory="plan_expired"
354+
analyticsLabel="select_a_plan_button"
355+
children="Select a plan"
356+
onPress={() => {
357+
dispatch(
358+
actions.pushModal({
359+
name: 'PRICING',
360+
params: {
361+
highlightFeature: 'columnsLimit',
362+
},
363+
}),
364+
)
365+
}}
366+
/>
367+
368+
<Spacer height={contentPadding} />
369+
370+
<QuickFeedbackRow />
371+
</View>
363372
}
364373
errorMessage="You need a paid plan to keep using DevHub."
365374
errorTitle="Free trial expired"

0 commit comments

Comments
 (0)