Skip to content

Commit

Permalink
T236317: Allow users to provide feedback from within the app (#216)
Browse files Browse the repository at this point in the history
* Introduce feedback component

* Fix tests

* Adding sendFeedback util

* Try to send feedback

* Add placeholder and onKeyBackspace functionality

* Add input validation

* Add hyperlinks to feedback explanation text

* Update landscape CSS

* Update softkey labels

* Update qqq.json

* Update to new sucess flow

* Polish landscape view and clean up CSS

* Update CSS to fit content in banana phone screen

* Formatting

* Adjust landscape view

* Trim feedback message to avoid blank strings

* Add CancelConfirmationPopup

* Blur textarea before showing confirmation popup

* Update feedback explanation language

* Add offline handling

* Show cancel confirmation dialog even when offline to ensure that text will not be lost accidentally

* CSS update: textarea highlight and line height

* Rename SearchOfflinePanel to OfflinePanel

* Update qqq.json, feedback-explanation

Co-authored-by: Stephane Bisson <[email protected]>
  • Loading branch information
medied and Stephane Bisson authored Jun 9, 2020
1 parent 1c2a2ad commit a59ef09
Show file tree
Hide file tree
Showing 16 changed files with 332 additions and 33 deletions.
2 changes: 2 additions & 0 deletions cypress/integration/settings-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@ const aboutWikipediaPage = new AboutWikipediaPage()
const settingsMenuListEnglishText = [enJson['settings-language'],
enJson['settings-textsize'],
enJson['settings-about-wikipedia'],
enJson['settings-help-feedback'],
enJson['settings-about-app'],
enJson['settings-privacy-terms']]
const languageSettingsPopupEnglishText = enJson['language-setting-message']
const languageChangeDutchText = nlJson['language-change']
const settingsMenuListDutchText = [nlJson['settings-language'],
nlJson['settings-textsize'],
nlJson['settings-about-wikipedia'],
nlJson['settings-help-feedback'],
nlJson['settings-about-app'],
enJson['settings-privacy-terms']] // TODO: update to nlJson when translations is available

Expand Down
16 changes: 15 additions & 1 deletion i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
"settings-privacy": "Privacy policy",
"settings-term": "Terms of use",
"settings-rate": "Rate the app",
"settings-help-feedback": "Help and feedback",
"settings-help-feedback": "Send feedback",
"settings-about-app": "About the app",
"textsize-increase": "Press 6 (Increase size)",
"textsize-decrease": "Press 4 (Decrease size)",
Expand All @@ -78,6 +78,20 @@
"view-in-browser": "View article in browser",
"about-app-message": "Made by the Wikimedia Foundation with the help of volunteers like you.",
"softkey-read-more": "Read More",
"feedback-header": "Send feedback",
"feedback-explanation": "We will use your feedback to help improve this app. Your response will be kept for 90 days, subject to our $1 and $2",
"feedback-placeholder": "Start writing here...",
"feedback-success": "Thank you for your feedback",
"feedback-success-header": "Feedback sent",
"feedback-terms-of-survey": "Terms of survey statement",
"feedback-privacy-policy": "Privacy policy",
"feedback-terms-of-use": "Terms of use",
"softkey-send": "Send",
"softkey-cancel": "Cancel",
"feedback-cancel": "Are you sure you want to leave? All your feedback will be lost",
"feedback-cancel-header": "Discard",
"softkey-yes": "Yes",
"softkey-no": "No",
"settings-privacy-terms": "Privacy and terms",
"privacy-terms-header": "Privacy and terms",
"softkey-consent-agree": "Agree",
Expand Down
14 changes: 14 additions & 0 deletions i18n/qqq.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,22 @@
"view-in-browser": "Label for the view in browser link. Shown in the footer",
"about-app-message": "Main message in About App page",
"softkey-read-more": "Label of the read more softkey",
"feedback-header": "Header title shown in Feedback component, selected from Settings menu",
"feedback-explanation": "Message displained in Feedback component that explains how feedback information will be used. \n* $1 - Privacy policy hyperlink. \n* $2 - Terms of use hyperlink",
"feedback-placeholder": "Placeholder message shown in textarea of Feedback component",
"feedback-success": "Message shown upon successfully sending feedback message in Feedback component",
"feedback-success-header": "Header for message shown upon successfully sending feedback message in Feedback component",
"feedback-terms-of-survey": "Text for Terms of survey statement in feedback explanation",
"feedback-privacy-policy": "Text for Privacy policy in feedback explanation",
"feedback-terms-of-use": "Text for Terms of use in feedback explanation",
"softkey-send": "Label of the send softkey, shown in Feedback component",
"softkey-cancel": "Label of the cancel softkey, shown in Feedback component",
"settings-privacy-terms": "Label of the Privacy and Terms item list. Shown in the Settings menu.",
"privacy-terms-header": "The header title shown in the Privacy and Terms page",
"feedback-cancel": "Message shown upon attempting to cancel the sending of feedback in Feedback component",
"feedback-cancel-header": "Message shown upon attempting to cancel the sending of feedback in Feedback component",
"softkey-yes": "Label for the yes key",
"softkey-no": "Label for the no key",
"softkey-consent-agree": "Option to agree to the personal data and usage consent.",
"softkey-consent-terms": "Option to go to the application's terms of service.",
"softkey-consent-policy": "Option to go to the application's privacy policy."
Expand Down
File renamed without changes
138 changes: 138 additions & 0 deletions src/components/Feedback.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { h } from 'preact'
import { useState, useRef, useEffect } from 'preact/hooks'
import { useI18n, useSoftkey, useNavigation, usePopup, useOnlineStatus } from 'hooks'
import { sendFeedback } from 'utils'
import { OfflinePanel } from 'components'

export const Feedback = ({ close }) => {
const containerRef = useRef()
const i18n = useI18n()
const [message, setMessage] = useState()
const [showSuccessConfirmation] = usePopup(SuccessConfirmationPopup, { stack: true })
const [showCancelConfirmation] = usePopup(CancelConfirmationPopup, { stack: true })
const [, setNavigation, getCurrent] = useNavigation('Feedback', containerRef, containerRef, 'y')
const isOnline = useOnlineStatus()

const items = [
{ text: `<a data-selectable>${i18n('feedback-privacy-policy')}</a>`, link: 'https://foundation.m.wikimedia.org/wiki/Privacy_policy' },
{ text: `<a data-selectable>${i18n('feedback-terms-of-use')}</a>`, link: 'https://foundation.m.wikimedia.org/wiki/Terms_of_Use/en' }
]
const hyperlinks = items.map(i => i.text)

const onKeyRight = () => {
const userMessage = message.trim()
if (isOnline && userMessage) {
sendFeedback(userMessage)
if (getCurrent().type === 'TEXTAREA') {
blurTextarea()
}
showSuccessConfirmation()
}
}

const onKeyBackspace = () => {
if (!isOnline && message) {
showCancelConfirmation()
} else if (message && getCurrent().type === 'TEXTAREA') {
setMessage(message.slice(0, -1))
} else {
close()
}
}

const onKeyCenter = () => {
const { index } = getCurrent()
if (index > 0) {
const item = items[index - 1]
window.open(item.link)
}
}

const onKeyLeft = () => {
if (message) {
if (isOnline && getCurrent().type === 'TEXTAREA') {
blurTextarea()
}
showCancelConfirmation()
} else {
close()
}
}

const blurTextarea = () => {
const element = containerRef.current.querySelector('textarea')
element.blur()
}

useSoftkey('Feedback', {
right: isOnline && message && message.trim() ? i18n('softkey-send') : '',
onKeyRight,
left: i18n('softkey-cancel'),
onKeyLeft,
onKeyBackspace,
onKeyCenter
}, [message, isOnline])

useEffect(() => {
setNavigation(0)
}, [isOnline])

return (
<div class='feedback' ref={containerRef}>
<div class='header'>
{i18n('feedback-header')}
</div>
<div class='body'>
{ isOnline
? <div>
<div class='textarea-box'>
<form>
<textarea value={message} placeholder={i18n('feedback-placeholder')} onChange={e => setMessage(e.target.value)} data-selectable />
</form>
</div>
<div class='explanation-box'>
<p dangerouslySetInnerHTML={{ __html: i18n('feedback-explanation', ...hyperlinks) }}> </p>
</div>
</div>
: <OfflinePanel />
}
</div>
</div>
)
}

const SuccessConfirmationPopup = ({ closeAll }) => {
const i18n = useI18n()

useSoftkey('FeedbackSuccessMessage', {
center: i18n('softkey-ok'),
onKeyCenter: closeAll,
onKeyBackspace: closeAll
}, [])

return (
<div class='confirmation-popup'>
<div class='header'>{i18n('feedback-success-header')}</div>
<p class='preview-text success'>{i18n('feedback-success')}</p>
</div>
)
}

const CancelConfirmationPopup = ({ close, closeAll }) => {
const i18n = useI18n()

useSoftkey('FeedbackCancelMessage', {
right: i18n('softkey-yes'),
onKeyRight: closeAll,
left: i18n('softkey-no'),
onKeyLeft: close,
onKeyBackspace: close
}, [])

return (
<div class='confirmation-popup'>
<div class='header'>{i18n('feedback-cancel-header')}</div>
<p class='preview-text cancel'>{i18n('feedback-cancel')}</p>
</div>
)
}
14 changes: 14 additions & 0 deletions src/components/OfflinePanel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { h } from 'preact'
import { useI18n } from 'hooks'

export const OfflinePanel = () => {
const i18n = useI18n()
return (
<div class='offline-panel'>
<div class='offline-content'>
<img src='images/offline.svg' />
<div class='message'>{i18n('offline-message')}</div>
</div>
</div>
)
}
16 changes: 2 additions & 14 deletions src/components/Search.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { h } from 'preact'
import { useRef, useEffect } from 'preact/hooks'
import { ListView } from 'components'
import { ListView, OfflinePanel } from 'components'
import {
useNavigation, useSearch, useI18n, useSoftkey,
useOnlineStatus, useTracking
Expand All @@ -11,18 +11,6 @@ import {
} from 'utils'
import { getRandomArticleTitle } from 'api'

const SearchOfflinePanel = () => {
const i18n = useI18n()
return (
<div class='search-offline-panel'>
<div class='search-offline-content'>
<img src='images/search-offline.svg' />
<div class='message'>{i18n('offline-message')}</div>
</div>
</div>
)
}

export const Search = () => {
const containerRef = useRef()
const inputRef = useRef()
Expand Down Expand Up @@ -77,7 +65,7 @@ export const Search = () => {
<img class='double-u' src='images/w.svg' style={{ display: ((searchResults || !isOnline) ? 'none' : 'block') }} />
<input ref={inputRef} type='text' placeholder={i18n('search-placeholder')} value={query} onInput={onInput} data-selectable />
{ (isOnline && searchResults) && <ListView header={i18n('header-search')} items={searchResults} containerRef={listRef} empty={i18n('no-result-found')} /> }
{ !isOnline && <SearchOfflinePanel /> }
{ !isOnline && <OfflinePanel /> }
</div>
)
}
9 changes: 7 additions & 2 deletions src/components/Settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { h } from 'preact'
import { route } from 'preact-router'
import { useRef, useEffect } from 'preact/hooks'
import { useNavigation, useI18n, useSoftkey, usePopup } from 'hooks'
import { ListView, TextSize, AboutApp, AboutWikipedia, PrivacyTerms } from 'components'
import { ListView, TextSize, AboutApp, AboutWikipedia, PrivacyTerms, Feedback } from 'components'

export const Settings = () => {
const containerRef = useRef()
Expand All @@ -11,6 +11,7 @@ export const Settings = () => {
const [showTextSize] = usePopup(TextSize)
const [showAboutApp] = usePopup(AboutApp, { mode: 'fullscreen' })
const [showAboutWikipedia] = usePopup(AboutWikipedia, { mode: 'fullscreen' })
const [showFeedback] = usePopup(Feedback, { mode: 'fullscreen' })
const [showPrivacyTerms] = usePopup(PrivacyTerms, { mode: 'fullscreen' })

const onKeyCenter = () => {
Expand Down Expand Up @@ -39,6 +40,10 @@ export const Settings = () => {
showAboutWikipedia()
}

const onFeedbackSelected = () => {
showFeedback()
}

const onPrivacyTermsSelected = () => {
showPrivacyTerms()
}
Expand All @@ -63,7 +68,7 @@ export const Settings = () => {
{ title: i18n('settings-about-wikipedia'), action: onAboutWikipediaSelected },
// @todo will have this soon and don't delete it from the language json
// { title: i18n('settings-rate') },
// { title: i18n('settings-help-feedback') },
{ title: i18n('settings-help-feedback'), action: onFeedbackSelected },
{ title: i18n('settings-about-app'), action: onAboutAppSelected },
{ title: i18n('settings-privacy-terms'), action: onPrivacyTermsSelected }
]
Expand Down
2 changes: 2 additions & 0 deletions src/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ export * from './ArticleToc'
export * from './ConfirmDialog'
export * from './Consent'
export * from './Error'
export * from './Feedback'
export * from './Gallery'
export * from './Language'
export * from './ListView'
export * from './Loading'
export * from './OfflineIndicator'
export * from './OfflinePanel'
export * from './Onboarding'
export * from './PopupContainer'
export * from './PrivacyTerms'
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/useNavigation.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export const useNavigation = (origin, containerRef, listRef, axis, elementsSelec
const selectThisElement = element === selectElement
element.setAttribute('nav-selected', selectThisElement)
element.setAttribute('nav-index', index)
if (element.nodeName === 'INPUT') {
if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') {
if (selectThisElement) {
element.focus()
element.selectionStart = element.selectionEnd = element.value.length
Expand Down
1 change: 1 addition & 0 deletions src/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ export * from './normalizeTitle'
export * from './onboarding'
export * from './sendErrorLog'
export * from './sendEvent'
export * from './sendFeedback'
export * from './viewport'
13 changes: 13 additions & 0 deletions src/utils/sendFeedback.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { appVersion, getAppLanguage, sendEvent } from 'utils'

export const sendFeedback = feedback => {
sendEvent(
'KaiOSAppFeedback',
20044947,
getAppLanguage(),
{
version: appVersion(),
feedback
}
)
}
Loading

0 comments on commit a59ef09

Please sign in to comment.