diff --git a/client/react-native/common/components/App.js b/client/react-native/common/components/App.js
index 93211b024d..777e525461 100644
--- a/client/react-native/common/components/App.js
+++ b/client/react-native/common/components/App.js
@@ -17,6 +17,7 @@ import { AppNavigator } from './Navigator/AppNavigator'
import { RelayContext } from '../relay'
import { UpdateContext } from '../update'
import * as KeyboardContext from '../helpers/KeyboardContext'
+import Mousetrap from 'mousetrap'
const { CoreModule } = NativeModules
@@ -160,6 +161,15 @@ export default class App extends PureComponent {
})
.catch(() => {})
+ if (Platform.OS === 'web') {
+ if (this._showQuickSwitch === undefined) {
+ this._showQuickSwitch = () => this.showQuickSwitch()
+ }
+
+ Mousetrap.prototype.stopCallback = () => {}
+ Mousetrap.bind(['command+k', 'ctrl+k', 'command+t', 'ctrl+t'], this._showQuickSwitch)
+ }
+
this.setState({ loading: false })
}
@@ -169,6 +179,15 @@ export default class App extends PureComponent {
if (this._handleOpenURL !== undefined) {
Linking.removeEventListener('url', this._handleOpenURL)
}
+
+ if (Platform.OS === 'web') {
+ Mousetrap.unbind(['command+k', 'ctrl+k'], this._showQuickSwitch)
+ }
+ }
+
+ showQuickSwitch () {
+ NavigationService.navigate('modal/chats/switcher')
+ return false
}
_onLanguageChange = ({ language } = {}) => {
diff --git a/client/react-native/common/components/Library/ModalScreen.js b/client/react-native/common/components/Library/ModalScreen.js
index 4d85aa3af1..28f58996a0 100644
--- a/client/react-native/common/components/Library/ModalScreen.js
+++ b/client/react-native/common/components/Library/ModalScreen.js
@@ -1,102 +1,127 @@
import { StackActions, withNavigation } from 'react-navigation'
-import { View } from 'react-native'
+import {
+ View,
+ Platform,
+ TouchableOpacity,
+} from 'react-native'
import React from 'react'
import Loader from './Loader'
import Button from './Button'
import colors from '../../constants/colors'
-const ModalScreen = props => {
- const {
- children,
- navigation,
- loading,
- showDismiss,
- width,
- footer,
- ...otherProps
- } = props
+class ModalScreen extends React.PureComponent {
+ componentDidMount () {
+ if (Platform.OS === 'web') {
+ if (this._keyboardListener === undefined) {
+ this._keyboardListener = (e) => this.keyboardListener(e)
+ }
+ window.addEventListener('keyup', this._keyboardListener)
+ }
+ }
- const beforeDismiss = navigation.getParam('beforeDismiss')
+ componentWillUnmount () {
+ window.removeEventListener('keyup', this._keyboardListener)
+ }
- return (
- <>
- {
+ const {
+ children,
+ loading,
+ showDismiss,
+ width,
+ footer,
+ ...otherProps
+ } = this.props
+
+ return (
+ <>
+
-
-
- {!loading ? (
-
+ }} onPress={() => (this.props.showDismiss || this.props.keyboardDismiss) && this.dismiss()}>
+
+ {!loading ? (
+
- {showDismiss ? (
+
+ {showDismiss ? (
+
+
+ ) : null}
-
- ) : null}
-
- {children}
+ {footer}
- {footer}
-
- ) : null}
- {loading && }
- >
- )
+ ) : null}
+ {loading && }
+ >
+ )
+ }
+
+ dismiss () {
+ const { navigation } = this.props
+ const beforeDismiss = navigation.getParam('beforeDismiss')
+
+ if (beforeDismiss !== undefined) {
+ beforeDismiss()
+ }
+
+ navigation.dispatch(
+ StackActions.pop({
+ n: 1,
+ })
+ )
+ }
}
export default withNavigation(ModalScreen)
diff --git a/client/react-native/common/components/Library/Text.js b/client/react-native/common/components/Library/Text.js
index 805d31f8ff..166ce40a7c 100644
--- a/client/react-native/common/components/Library/Text.js
+++ b/client/react-native/common/components/Library/Text.js
@@ -354,6 +354,14 @@ export class ForegroundText extends PureComponent {
return ForegroundText.styles[propsHash]
}
+ focus () {
+ this._input.focus()
+ }
+
+ blur () {
+ this._input.blur()
+ }
+
render () {
const { icon, input, children, multiline, onSubmit, onChangeText, value, flip } = this.props
const numberOfLines = typeof multiline === 'number' ? multiline : undefined
@@ -371,12 +379,14 @@ export class ForegroundText extends PureComponent {
{...(typeof input === 'object' ? input : {})}
style={styles.input}
placeholder={children || input.placeholder}
+ autoFocus={!!input.autoFocus}
placeholderTextColor={colors.subtleGrey}
onSubmitEditing={onSubmit}
onChangeText={onChangeText || (() => {})}
multiline={!!multiline}
numberOfLines={numberOfLines}
value={value}
+ ref={c => (this._input = c)}
/>
) : (
{
- props = reverse(props)
- return (
-
-
-
- )
+export class Text extends React.PureComponent {
+ focus () {
+ this._text.focus()
+ }
+
+ blur () {
+ this._text.blur()
+ }
+
+ render () {
+ const props = reverse(this.props)
+
+ return (
+
+ (this._text = c)} />
+
+ )
+ }
}
export default Text
diff --git a/client/react-native/common/components/Navigator/BottomNavigator.js b/client/react-native/common/components/Navigator/BottomNavigator.js
index 06dd61ced9..e27dedb5e3 100644
--- a/client/react-native/common/components/Navigator/BottomNavigator.js
+++ b/client/react-native/common/components/Navigator/BottomNavigator.js
@@ -240,7 +240,7 @@ export default createBottomTabNavigator(
export const SplitNavigator = createSplitNavigator({
'placeholder': Placeholder,
'contacts': SubviewsContactNavigator,
- 'chats': SubviewsChatNavigator,
+ 'chats/subviews': SubviewsChatNavigator,
}, {
'side/contacts': {
screen: SplitSideContactNavigator,
diff --git a/client/react-native/common/components/Navigator/MainNavigator.js b/client/react-native/common/components/Navigator/MainNavigator.js
index da088288b1..5cd913196c 100644
--- a/client/react-native/common/components/Navigator/MainNavigator.js
+++ b/client/react-native/common/components/Navigator/MainNavigator.js
@@ -4,6 +4,7 @@ import { Easing, Animated, Platform } from 'react-native'
import { EventListFilterModal } from '../Screens/Settings/Devtools/EventList'
import ContactCardModal from '../Screens/Contacts/ContactCardModal'
import { ViewExportComponent } from '../../helpers/saveViewToCamera'
+import ChatsSwitcherModal from '../Screens/Contacts/ChatsSwitcherModal'
export default createStackNavigator(
{
@@ -14,6 +15,9 @@ export default createStackNavigator(
'modal/contacts/card': {
screen: ContactCardModal,
},
+ 'modal/chats/switcher': {
+ screen: ChatsSwitcherModal,
+ },
'virtual/view-export': {
screen: ViewExportComponent,
},
diff --git a/client/react-native/common/components/Screens/Contacts/ChatsSwitcherModal.js b/client/react-native/common/components/Screens/Contacts/ChatsSwitcherModal.js
new file mode 100644
index 0000000000..0d559f0a3c
--- /dev/null
+++ b/client/react-native/common/components/Screens/Contacts/ChatsSwitcherModal.js
@@ -0,0 +1,315 @@
+import { FlatList, View } from 'react-native'
+import React from 'react'
+
+import { Avatar, Flex, ModalScreen } from '../../Library'
+import withRelayContext from '../../../helpers/withRelayContext'
+import Text from '../../Library/Text'
+import { colors } from '../../../constants'
+import { withNamespaces } from 'react-i18next'
+import { Pagination } from '../../../relay'
+import { fragments } from '../../../graphql'
+import { borderTop, marginLeft, padding } from '../../../styles'
+import { conversation as utils } from '../../../utils'
+import Mousetrap from 'mousetrap'
+import { withGoBack } from '../../Library/BackActionProvider'
+import { withNavigation, withNavigationFocus, StackActions, NavigationActions } from 'react-navigation'
+
+const modalWidth = 480
+
+const FilteredListContext = React.createContext()
+
+class SwitcherListComponentBase extends React.PureComponent {
+ constructor (props) {
+ super(props)
+
+ this.state = {
+ listProps: {
+ query: '',
+ focusedIndex: 0,
+ },
+ }
+
+ this.candidateRoute = null
+ this.matchedIndices = []
+ }
+
+ moveCursor (index, { scroll } = {}) {
+ try {
+ index = Math.min(index, this.props.data.length - 1)
+ index = Math.max(index, 0)
+ if (isNaN(index) || this.state.listProps.focusedIndex === index) {
+ return
+ }
+
+ if (scroll) {
+ const indexPos = this.matchedIndices.sort((a, b) => a - b).indexOf(index)
+ if (indexPos !== -1 && this._list) {
+ this._list.scrollToIndex({ index: indexPos, viewPosition: 1, animated: false })
+ } else {
+ console.warn('attempted to reach index ', index)
+ }
+ }
+
+ this.setState({
+ listProps: {
+ ...this.state.listProps,
+ focusedIndex: index,
+ },
+ })
+ } catch (e) {
+ console.warn(e)
+ }
+ }
+
+ enter () {
+ if (!this.props.isFocused) {
+ return
+ }
+
+ if (this.candidateRoute !== null) {
+ this.props.navigation.navigate(this.candidateRoute)
+ }
+ }
+
+ up (e) {
+ if (!this.props.isFocused) {
+ return
+ }
+
+ const index = this.matchedIndices.sort((a, b) => a - b).indexOf(this.state.listProps.focusedIndex)
+ if (index !== 0) {
+ this.moveCursor(this.matchedIndices[index - 1], { scroll: true })
+ }
+ e.preventDefault()
+ }
+
+ down (e) {
+ if (!this.props.isFocused) {
+ return
+ }
+
+ const index = this.matchedIndices.sort((a, b) => a - b).indexOf(this.state.listProps.focusedIndex)
+ if (index !== this.state.listProps.focusedIndex.length - 1) {
+ this.moveCursor(this.matchedIndices[index + 1], { scroll: true })
+ }
+ e.preventDefault()
+ }
+
+ setIndex (index) {
+ this.moveCursor(index, {})
+ }
+
+ setCandidateRoute (route) {
+ this.candidateRoute = route
+ }
+
+ componentDidMount () {
+ if (this._up === undefined || this._down === undefined || this._enter === undefined) {
+ this._up = (e) => this.up(e)
+ this._down = (e) => this.down(e)
+ this._enter = (e) => this.enter(e)
+ }
+
+ Mousetrap.prototype.stopCallback = () => {}
+ Mousetrap.bind(['up'], this._up)
+ Mousetrap.bind(['down'], this._down)
+ Mousetrap.bind(['enter'], this._enter)
+ }
+
+ componentWillUnmount () {
+ Mousetrap.unbind(['up'], this._up)
+ Mousetrap.unbind(['down'], this._down)
+ Mousetrap.unbind(['enter'], this._enter)
+ }
+
+ setQuery (query) {
+ this.setState({
+ listProps: {
+ query,
+ focusedIndex: 0,
+ },
+ })
+
+ this.matchedIndices = []
+ }
+
+ addMatched (index) {
+ if (this.matchedIndices.indexOf(index) !== -1) {
+ return
+ }
+
+ this.matchedIndices = [...this.matchedIndices, index]
+
+ if (this.matchedIndices.indexOf(this.state.listProps.focusedIndex) === -1) {
+ this.setIndex(index)
+ }
+ }
+
+ render () {
+ const { data, renderItem, t, navigation, ...props } = this.props
+
+ return
+
+ this.setQuery(query)} input={{
+ placeholder: t('contacts.quick-switch-placeholder'),
+ autoFocus: true,
+ }} big background={colors.white} color={colors.fakeBlack} left margin padding={{ top: 10 }} />
+
+ renderItem({
+ ...props,
+ listProps: this.state.listProps,
+ addMatched: (index) => this.addMatched(index),
+ setCandidateRoute: (route) => this.setCandidateRoute(route),
+ })}
+ getItemLayout={(data, index) => {
+ return {
+ length: 72,
+ index,
+ offset: 72 * index,
+ }
+ }}
+ ref={list => (this._list = list)}
+ {...props}
+ />
+
+ }
+}
+
+const SwitcherListComponent = withGoBack(withNavigation(withNavigationFocus(withNamespaces()(SwitcherListComponentBase))))
+
+const ItemBase = fragments.Conversation(withNavigation(
+ class ItemClass extends React.PureComponent {
+ constructor (props) {
+ super(props)
+
+ const item = this.props.data
+
+ this.forceIgnore = false
+
+ if (!item.members) {
+ this.forceIgnore = true
+ } else if (item.members.length === 2 &&
+ item.members.some(
+ m => m.contact == null || (m.contact.displayName === '' && m.contact.overrideDisplayName === '')
+ )) {
+ this.forceIgnore = true
+ } else {
+ this.title = utils.getTitle(item)
+ const membersName = item.members.filter(m => m.contact && m.contact.status !== 42).map(m => `${m.contact.displayName} ${m.contact.overrideDisplayName}`).join(' ')
+
+ this.testedName = (membersName + this.title).toLocaleLowerCase()
+ }
+ }
+
+ testItem () {
+ const { query } = this.props
+
+ return this.testedName.indexOf(query.toLocaleLowerCase()) !== -1
+ }
+
+ render () {
+ if (this.forceIgnore || !this.testItem()) {
+ return null
+ }
+
+ const { data, navigation, index, addMatched, focusedIndex, setCandidateRoute } = this.props
+
+ const route = {
+ routeName: 'chats/subviews',
+ action: StackActions.reset({
+ index: 0,
+ actions: [NavigationActions.navigate({ routeName: 'chats/detail', params: data })],
+ }),
+ }
+
+ if (index === focusedIndex) {
+ setCandidateRoute(route)
+ }
+
+ addMatched && addMatched(index)
+
+ const title = utils.getTitle(data)
+
+ return navigation.navigate(route)}
+ style={[{ height: 72 }, padding, borderTop, index === focusedIndex ? { backgroundColor: colors.blue } : null]}
+ >
+
+
+
+
+
+ {title}
+
+
+
+ }
+ }
+))
+
+class Item extends React.PureComponent {
+ render () {
+ return {({ listProps: { focusedIndex, query } }) => {
+ return
+ }}
+ }
+}
+
+class ContactCardModal extends React.Component {
+ render () {
+ const {
+ context,
+ context: { queries, fragments },
+ } = this.props
+
+ return (
+
+
+
+ (
+
+ )}
+ emptyItem={() => (
+ Nothing found
+ )}
+ />
+
+
+
+ )
+ }
+}
+
+export default withRelayContext(ContactCardModal)
diff --git a/client/react-native/common/constants/colors.js b/client/react-native/common/constants/colors.js
index cb67bc7e69..39feec073a 100644
--- a/client/react-native/common/constants/colors.js
+++ b/client/react-native/common/constants/colors.js
@@ -70,5 +70,5 @@ export default {
background: '#FAFAFE',
- transparentGrey: 'rgba(36, 49, 50, 0.8)', // based on grey 1
+ transparentGrey: 'rgba(161, 161, 178, 0.9)', // based on darkGrey
}
diff --git a/client/react-native/common/i18n/en/messages.json b/client/react-native/common/i18n/en/messages.json
index 57909a76d0..ccc55d73ce 100644
--- a/client/react-native/common/i18n/en/messages.json
+++ b/client/react-native/common/i18n/en/messages.json
@@ -189,7 +189,8 @@
"received": "Received",
"sent": "Sent",
"unknown": "Unknown",
- "info-updated": "Contact information has been updated"
+ "info-updated": "Contact information has been updated",
+ "quick-switch-placeholder": "Access..."
},
"setting-up": "Setting up Berty :)",
"qrcode": "QR Code",
diff --git a/client/react-native/common/i18n/fr/messages.json b/client/react-native/common/i18n/fr/messages.json
index 9964ee9a2b..630485f3a8 100644
--- a/client/react-native/common/i18n/fr/messages.json
+++ b/client/react-native/common/i18n/fr/messages.json
@@ -189,7 +189,8 @@
"received": "Invit. reçues",
"sent": "Invit. envoyées",
"unknown": "Inconnu",
- "info-updated": "Les informations de contact ont été mises à jour"
+ "info-updated": "Les informations de contact ont été mises à jour",
+ "quick-switch-placeholder": "Accéder à..."
},
"setting-up": "Démarrage de Berty :)",
"qrcode": "QR Code",
diff --git a/client/react-native/common/relay/Pagination.js b/client/react-native/common/relay/Pagination.js
index 3c7b738e72..5c58631689 100644
--- a/client/react-native/common/relay/Pagination.js
+++ b/client/react-native/common/relay/Pagination.js
@@ -50,7 +50,11 @@ class PaginationContainer extends Component {
keyExtractor = item => item.node.cursor + ':' + item.node.id
- renderItem = ({ item: { node } }) => this.props.renderItem({ data: node })
+ renderItem = ({ item: { node }, index, ...props }) => this.props.renderItem({ data: node, index, ...props })
+
+ scrollToIndex = (index) => {
+ index && this._list && this._list.scrollToIndex({ index })
+ }
render () {
const {
@@ -62,21 +66,24 @@ class PaginationContainer extends Component {
emptyItem,
cond,
condComponent,
+ ListComponent = FlatList,
} = this.props
if (!emptyItem || (data[alias] && data[alias].edges.length > 0)) {
return (
<>
- (this._list = list)}
/>
{cond != null && cond(data[alias]) && condComponent != null
? condComponent()
@@ -131,6 +138,10 @@ export default class Pagination extends PureComponent {
this.subscribers.forEach(s => s.unsubscribe())
}
+ scrollToIndex = (index) => {
+ this._container && this._container.scrollToIndex(index)
+ }
+
render () {
const { query, variables, noLoader } = this.props
@@ -150,7 +161,7 @@ export default class Pagination extends PureComponent {
)
case state.success:
- return
+ return (this._container = container)} />
case state.error:
return null
}
diff --git a/client/react-native/package.json b/client/react-native/package.json
index e4e9b684c7..f874499810 100644
--- a/client/react-native/package.json
+++ b/client/react-native/package.json
@@ -29,6 +29,7 @@
"isomorphic-fetch": "^2.2.1",
"lottie-react-native": "^2.5.11",
"moment": "^2.22.2",
+ "mousetrap": "^1.6.3",
"object-hash": "^1.3.0",
"react": "16.6.3",
"react-dom": "16.4.2",
@@ -45,6 +46,7 @@
"react-native-image-picker": "^0.27.1",
"react-native-keyboard-spacer": "^0.4.1",
"react-native-languages": "^3.0.1",
+ "react-native-markdown-renderer": "^3.2.8",
"react-native-network-info": "^4.0.0",
"react-native-permissions": "1.1.1",
"react-native-qrcode-scanner": "^1.1.0",