diff --git a/__libdef-tests__/bottom-tabs_v5.x.x-test.js b/__libdef-tests__/bottom-tabs_v5.x.x-test.js
new file mode 100644
index 00000000000..c34b14cad05
--- /dev/null
+++ b/__libdef-tests__/bottom-tabs_v5.x.x-test.js
@@ -0,0 +1,36 @@
+/* @flow strict-local */
+import { type NavigationProp } from '@react-navigation/bottom-tabs';
+
+/* eslint-disable no-unused-vars */
+
+function test_setParams() {
+ type NavProp
= NavigationProp<{| r: P |}, 'r'>;
+
+ function test_happy(navigation: NavProp<{| a: number |}>) {
+ navigation.setParams({ a: 1 });
+ }
+
+ function test_accepts_missing(navigation: NavProp<{| a: number, b: number |}>) {
+ navigation.setParams({ a: 1 });
+ }
+
+ function test_rejects_extra(navigation: NavProp<{| a: number |}>) {
+ // $FlowExpectedError[prop-missing]
+ navigation.setParams({ b: 1 });
+ }
+
+ function test_rejects_mismatch(navigation: NavProp<{| a: number |}>) {
+ // $FlowExpectedError[incompatible-call]
+ navigation.setParams({ a: 'a' });
+ }
+
+ function test_rejects_object_to_void(navigation: NavProp) {
+ // $FlowExpectedError[incompatible-call]
+ navigation.setParams({ a: 1 });
+ }
+
+ function test_rejects_void_to_object(navigation: NavProp<{| a: number |}>) {
+ // $FlowExpectedError[incompatible-call]
+ navigation.setParams();
+ }
+}
diff --git a/__libdef-tests__/drawer_v5.x.x-test.js b/__libdef-tests__/drawer_v5.x.x-test.js
new file mode 100644
index 00000000000..3c0a3a9d736
--- /dev/null
+++ b/__libdef-tests__/drawer_v5.x.x-test.js
@@ -0,0 +1,36 @@
+/* @flow strict-local */
+import { type NavigationProp } from '@react-navigation/drawer';
+
+/* eslint-disable no-unused-vars */
+
+function test_setParams() {
+ type NavProp = NavigationProp<{| r: P |}, 'r'>;
+
+ function test_happy(navigation: NavProp<{| a: number |}>) {
+ navigation.setParams({ a: 1 });
+ }
+
+ function test_accepts_missing(navigation: NavProp<{| a: number, b: number |}>) {
+ navigation.setParams({ a: 1 });
+ }
+
+ function test_rejects_extra(navigation: NavProp<{| a: number |}>) {
+ // $FlowExpectedError[prop-missing]
+ navigation.setParams({ b: 1 });
+ }
+
+ function test_rejects_mismatch(navigation: NavProp<{| a: number |}>) {
+ // $FlowExpectedError[incompatible-call]
+ navigation.setParams({ a: 'a' });
+ }
+
+ function test_rejects_object_to_void(navigation: NavProp) {
+ // $FlowExpectedError[incompatible-call]
+ navigation.setParams({ a: 1 });
+ }
+
+ function test_rejects_void_to_object(navigation: NavProp<{| a: number |}>) {
+ // $FlowExpectedError[incompatible-call]
+ navigation.setParams();
+ }
+}
diff --git a/__libdef-tests__/material-top-tabs_v5.x.x-test.js b/__libdef-tests__/material-top-tabs_v5.x.x-test.js
new file mode 100644
index 00000000000..5b5a0b4a03f
--- /dev/null
+++ b/__libdef-tests__/material-top-tabs_v5.x.x-test.js
@@ -0,0 +1,36 @@
+/* @flow strict-local */
+import { type NavigationProp } from '@react-navigation/material-top-tabs';
+
+/* eslint-disable no-unused-vars */
+
+function test_setParams() {
+ type NavProp = NavigationProp<{| r: P |}, 'r'>;
+
+ function test_happy(navigation: NavProp<{| a: number |}>) {
+ navigation.setParams({ a: 1 });
+ }
+
+ function test_accepts_missing(navigation: NavProp<{| a: number, b: number |}>) {
+ navigation.setParams({ a: 1 });
+ }
+
+ function test_rejects_extra(navigation: NavProp<{| a: number |}>) {
+ // $FlowExpectedError[prop-missing]
+ navigation.setParams({ b: 1 });
+ }
+
+ function test_rejects_mismatch(navigation: NavProp<{| a: number |}>) {
+ // $FlowExpectedError[incompatible-call]
+ navigation.setParams({ a: 'a' });
+ }
+
+ function test_rejects_object_to_void(navigation: NavProp) {
+ // $FlowExpectedError[incompatible-call]
+ navigation.setParams({ a: 1 });
+ }
+
+ function test_rejects_void_to_object(navigation: NavProp<{| a: number |}>) {
+ // $FlowExpectedError[incompatible-call]
+ navigation.setParams();
+ }
+}
diff --git a/__libdef-tests__/native_v5.x.x-test.js b/__libdef-tests__/native_v5.x.x-test.js
new file mode 100644
index 00000000000..6ff0aec1e99
--- /dev/null
+++ b/__libdef-tests__/native_v5.x.x-test.js
@@ -0,0 +1,36 @@
+/* @flow strict-local */
+import { type NavigationProp } from '@react-navigation/native';
+
+/* eslint-disable no-unused-vars */
+
+function test_setParams() {
+ type NavProp = NavigationProp<{| r: P |}, 'r'>;
+
+ function test_happy(navigation: NavProp<{| a: number |}>) {
+ navigation.setParams({ a: 1 });
+ }
+
+ function test_accepts_missing(navigation: NavProp<{| a: number, b: number |}>) {
+ navigation.setParams({ a: 1 });
+ }
+
+ function test_rejects_extra(navigation: NavProp<{| a: number |}>) {
+ // $FlowExpectedError[prop-missing]
+ navigation.setParams({ b: 1 });
+ }
+
+ function test_rejects_mismatch(navigation: NavProp<{| a: number |}>) {
+ // $FlowExpectedError[incompatible-call]
+ navigation.setParams({ a: 'a' });
+ }
+
+ function test_rejects_object_to_void(navigation: NavProp) {
+ // $FlowExpectedError[incompatible-call]
+ navigation.setParams({ a: 1 });
+ }
+
+ function test_rejects_void_to_object(navigation: NavProp<{| a: number |}>) {
+ // $FlowExpectedError[incompatible-call]
+ navigation.setParams();
+ }
+}
diff --git a/babel.config.js b/babel.config.js
index 0598b4692a2..a87c0f9c861 100644
--- a/babel.config.js
+++ b/babel.config.js
@@ -13,5 +13,6 @@ module.exports = {
// @babel/reset-env, but that doesn't get used as a base plugin; see a
// comment on that issue explaining why.
'@babel/plugin-proposal-numeric-separator',
+ 'react-native-reanimated/plugin',
],
};
diff --git a/flow-typed/@react-navigation/bottom-tabs_v5.x.x.js b/flow-typed/@react-navigation/bottom-tabs_v5.x.x.js
index eceffc47a96..6165a36d07e 100644
--- a/flow-typed/@react-navigation/bottom-tabs_v5.x.x.js
+++ b/flow-typed/@react-navigation/bottom-tabs_v5.x.x.js
@@ -840,11 +840,13 @@ declare module '@react-navigation/bottom-tabs' {
>>,
+setOptions: (options: $Shape) => void,
+setParams: (
- params: $If<
- $IsUndefined<$ElementType>,
- empty,
- $Shape<$NonMaybeType<$ElementType>>,
- >,
+ // We've edited this to be less complicated, so Flow in types-first
+ // mode can handle it.
+ //
+ // The complicated version appears to have been a workaround for the
+ // brokenness of $Shape: `$Shape` is `{ ... }`. `$Partial` is
+ // basically the fixed `$Shape`, and makes the complexity unneeded.
+ params: $Partial<$NonMaybeType<$ElementType>>,
) => void,
...
};
diff --git a/flow-typed/@react-navigation/drawer_v5.x.x.js b/flow-typed/@react-navigation/drawer_v5.x.x.js
index be7acac6e06..3b61cfd8340 100644
--- a/flow-typed/@react-navigation/drawer_v5.x.x.js
+++ b/flow-typed/@react-navigation/drawer_v5.x.x.js
@@ -840,11 +840,13 @@ declare module '@react-navigation/drawer' {
>>,
+setOptions: (options: $Shape) => void,
+setParams: (
- params: $If<
- $IsUndefined<$ElementType>,
- empty,
- $Shape<$NonMaybeType<$ElementType>>,
- >,
+ // We've edited this to be less complicated, so Flow in types-first
+ // mode can handle it.
+ //
+ // The complicated version appears to have been a workaround for the
+ // brokenness of $Shape: `$Shape` is `{ ... }`. `$Partial` is
+ // basically the fixed `$Shape`, and makes the complexity unneeded.
+ params: $Partial<$NonMaybeType<$ElementType>>,
) => void,
...
};
diff --git a/flow-typed/@react-navigation/material-top-tabs_v5.x.x.js b/flow-typed/@react-navigation/material-top-tabs_v5.x.x.js
index 1a7d2aeda0a..5dc1824ef40 100644
--- a/flow-typed/@react-navigation/material-top-tabs_v5.x.x.js
+++ b/flow-typed/@react-navigation/material-top-tabs_v5.x.x.js
@@ -840,11 +840,13 @@ declare module '@react-navigation/material-top-tabs' {
>>,
+setOptions: (options: $Shape) => void,
+setParams: (
- params: $If<
- $IsUndefined<$ElementType>,
- empty,
- $Shape<$NonMaybeType<$ElementType>>,
- >,
+ // We've edited this to be less complicated, so Flow in types-first
+ // mode can handle it.
+ //
+ // The complicated version appears to have been a workaround for the
+ // brokenness of $Shape: `$Shape` is `{ ... }`. `$Partial` is
+ // basically the fixed `$Shape`, and makes the complexity unneeded.
+ params: $Partial<$NonMaybeType<$ElementType>>,
) => void,
...
};
diff --git a/flow-typed/@react-navigation/native_v5.x.x.js b/flow-typed/@react-navigation/native_v5.x.x.js
index 2c4c3813968..7605b14ecd1 100644
--- a/flow-typed/@react-navigation/native_v5.x.x.js
+++ b/flow-typed/@react-navigation/native_v5.x.x.js
@@ -840,11 +840,13 @@ declare module '@react-navigation/native' {
>>,
+setOptions: (options: $Shape) => void,
+setParams: (
- params: $If<
- $IsUndefined<$ElementType>,
- empty,
- $Shape<$NonMaybeType<$ElementType>>,
- >,
+ // We've edited this to be less complicated, so Flow in types-first
+ // mode can handle it.
+ //
+ // The complicated version appears to have been a workaround for the
+ // brokenness of $Shape: `$Shape` is `{ ... }`. `$Partial` is
+ // basically the fixed `$Shape`, and makes the complexity unneeded.
+ params: $Partial<$NonMaybeType<$ElementType>>,
) => void,
...
};
diff --git a/index.js b/index.js
index cade378ab20..3df2c4db81d 100644
--- a/index.js
+++ b/index.js
@@ -1,4 +1,5 @@
/* @flow strict-local */
+import 'react-native-gesture-handler';
import { AppRegistry } from 'react-native';
import ZulipMobile from './src/ZulipMobile';
diff --git a/src/account-info/ProfileScreen.js b/src/account-info/ProfileScreen.js
index 3d570378934..e5da89ed835 100644
--- a/src/account-info/ProfileScreen.js
+++ b/src/account-info/ProfileScreen.js
@@ -2,6 +2,7 @@
import React, { useContext } from 'react';
import type { Node } from 'react';
import { ScrollView, View, Alert } from 'react-native';
+import { SafeAreaView } from 'react-native-safe-area-context';
import { TranslationContext } from '../boot/TranslationProvider';
import type { RouteProp } from '../react-navigation';
@@ -121,19 +122,21 @@ export default function ProfileScreen(props: Props): Node {
const ownUser = useSelector(getOwnUser);
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
);
}
diff --git a/src/main/HomeScreen.js b/src/main/HomeScreen.js
deleted file mode 100644
index cf545ddba5a..00000000000
--- a/src/main/HomeScreen.js
+++ /dev/null
@@ -1,75 +0,0 @@
-/* @flow strict-local */
-
-import React from 'react';
-import type { Node } from 'react';
-import { View } from 'react-native';
-
-import type { RouteProp } from '../react-navigation';
-import type { MainTabsNavigationProp } from './MainTabsScreen';
-import * as NavigationService from '../nav/NavigationService';
-import { useDispatch } from '../react-redux';
-import { HOME_NARROW, MENTIONED_NARROW, STARRED_NARROW } from '../utils/narrow';
-import { TopTabButton, TopTabButtonGeneral } from '../nav/TopTabButton';
-import UnreadCards from '../unread/UnreadCards';
-import { doNarrow, navigateToSearch } from '../actions';
-import IconUnreadMentions from '../nav/IconUnreadMentions';
-import { BRAND_COLOR, createStyleSheet } from '../styles';
-import { LoadingBanner } from '../common';
-import ServerCompatBanner from '../common/ServerCompatBanner';
-import ServerPushSetupBanner from '../common/ServerPushSetupBanner';
-
-const styles = createStyleSheet({
- wrapper: {
- flex: 1,
- flexDirection: 'column',
- },
- iconList: {
- justifyContent: 'space-between',
- flexDirection: 'row',
- },
-});
-
-type Props = $ReadOnly<{|
- navigation: MainTabsNavigationProp<'home'>,
- route: RouteProp<'home', void>,
-|}>;
-
-export default function HomeScreen(props: Props): Node {
- const dispatch = useDispatch();
-
- return (
-
-
- {
- dispatch(doNarrow(HOME_NARROW));
- }}
- />
- {
- dispatch(doNarrow(STARRED_NARROW));
- }}
- />
- {
- dispatch(doNarrow(MENTIONED_NARROW));
- }}
- >
-
-
- {
- NavigationService.dispatch(navigateToSearch());
- }}
- />
-
-
-
-
-
-
- );
-}
diff --git a/src/main/MainTabsScreen.js b/src/main/MainTabsScreen.js
index 22af3caa2f4..d13d788ca6d 100644
--- a/src/main/MainTabsScreen.js
+++ b/src/main/MainTabsScreen.js
@@ -1,12 +1,11 @@
/* @flow strict-local */
import React, { useContext } from 'react';
import type { Node } from 'react';
-import { Platform } from 'react-native';
+import { Platform, View } from 'react-native';
import {
createBottomTabNavigator,
type BottomTabNavigationProp,
} from '@react-navigation/bottom-tabs';
-import { SafeAreaView } from 'react-native-safe-area-context';
import type { RouteProp, RouteParamsOf } from '../react-navigation';
import { getUnreadHuddlesTotal, getUnreadPmsTotal } from '../selectors';
@@ -14,7 +13,7 @@ import { useSelector } from '../react-redux';
import type { AppNavigationProp } from '../nav/AppNavigator';
import type { GlobalParamList } from '../nav/globalTypes';
import { bottomTabNavigatorConfig } from '../styles/tabs';
-import HomeScreen from './HomeScreen';
+import HomeDrawerNavigator from './home/HomeDrawerNavigator';
import StreamTabsScreen from './StreamTabsScreen';
import PmConversationsScreen from '../pm-conversations/PmConversationsScreen';
import { IconInbox, IconStream, IconPeople } from '../common/Icons';
@@ -23,7 +22,7 @@ import ProfileScreen from '../account-info/ProfileScreen';
import styles, { BRAND_COLOR, ThemeContext } from '../styles';
export type MainTabsNavigatorParamList = {|
- home: RouteParamsOf,
+ home: RouteParamsOf,
'stream-tabs': RouteParamsOf,
'pm-conversations': RouteParamsOf,
profile: RouteParamsOf,
@@ -50,7 +49,7 @@ export default function MainTabsScreen(props: Props): Node {
const unreadPmsCount = useSelector(getUnreadHuddlesTotal) + useSelector(getUnreadPmsTotal);
return (
-
+
,
@@ -105,6 +104,6 @@ export default function MainTabsScreen(props: Props): Node {
}}
/>
-
+
);
}
diff --git a/src/main/StreamTabsScreen.js b/src/main/StreamTabsScreen.js
index bdc7f1ccc68..370dc1890bc 100644
--- a/src/main/StreamTabsScreen.js
+++ b/src/main/StreamTabsScreen.js
@@ -5,6 +5,7 @@ import {
createMaterialTopTabNavigator,
type MaterialTopTabNavigationProp,
} from '@react-navigation/material-top-tabs';
+import { SafeAreaView } from 'react-native-safe-area-context';
import { ZulipTextIntl } from '../common';
import { createStyleSheet } from '../styles';
@@ -44,31 +45,33 @@ type Props = $ReadOnly<{|
export default function StreamTabsScreen(props: Props): Node {
return (
-
- (
-
- ),
- }}
- />
- (
-
- ),
- }}
- />
-
+
+
+ (
+
+ ),
+ }}
+ />
+ (
+
+ ),
+ }}
+ />
+
+
);
}
diff --git a/src/main/home/AllNarrowScreen.js b/src/main/home/AllNarrowScreen.js
new file mode 100644
index 00000000000..68214aae20b
--- /dev/null
+++ b/src/main/home/AllNarrowScreen.js
@@ -0,0 +1,234 @@
+/* @flow strict-local */
+import React, { useCallback, useContext } from 'react';
+import type { Node } from 'react';
+import { useIsFocused } from '@react-navigation/native';
+
+import { useSelector, useDispatch } from '../../react-redux';
+import type { RouteProp } from '../../react-navigation';
+import type { HomeDrawerNavigationProp } from './HomeDrawerNavigator';
+import { ThemeContext, createStyleSheet } from '../../styles';
+import type { Narrow, EditMessage } from '../../types';
+import { KeyboardAvoider, OfflineNotice } from '../../common';
+import ChatNavBar from '../../nav/ChatNavBar';
+import MessageList from '../../webview/MessageList';
+import NoMessages from '../../message/NoMessages';
+import FetchError from '../../chat/FetchError';
+import InvalidNarrow from '../../chat/InvalidNarrow';
+import { fetchMessagesInNarrow } from '../../message/fetchActions';
+import ComposeBox from '../../compose/ComposeBox';
+import UnreadNotice from '../../chat/UnreadNotice';
+import {
+ showComposeBoxOnNarrow,
+ caseNarrowDefault,
+ keyFromNarrow,
+ HOME_NARROW,
+} from '../../utils/narrow';
+import { getLoading, getSession } from '../../directSelectors';
+import { getFetchingForNarrow } from '../../chat/fetchingSelectors';
+import {
+ getShownMessagesForNarrow,
+ isNarrowValid as getIsNarrowValid,
+} from '../../chat/narrowsSelectors';
+import { getFirstUnreadIdInNarrow } from '../../message/messageSelectors';
+import { getDraftForNarrow } from '../../drafts/draftsSelectors';
+import { addToOutbox } from '../../actions';
+import { getAuth } from '../../selectors';
+import { showErrorAlert } from '../../utils/info';
+import { TranslationContext } from '../../boot/TranslationProvider';
+import * as api from '../../api';
+
+type Props = $ReadOnly<{|
+ navigation: HomeDrawerNavigationProp<'all-narrow'>,
+ route: RouteProp<'all-narrow', {| editMessage: EditMessage | null |}>,
+|}>;
+
+const componentStyles = createStyleSheet({
+ screen: {
+ flex: 1,
+ flexDirection: 'column',
+ },
+});
+
+/**
+ * Fetch messages for this narrow and report an error, if any
+ *
+ * See `MessagesState` for background about the fetching, including
+ * why this is nearly the only place where additional data fetching
+ * is required. See `fetchMessagesInNarrow` and `fetchMessages` for
+ * more details, including how Redux is kept up-to-date during the
+ * whole process.
+ */
+const useMessagesWithFetch = args => {
+ const { narrow } = args;
+
+ const dispatch = useDispatch();
+ const isFocused = useIsFocused();
+
+ const eventQueueId = useSelector(state => getSession(state).eventQueueId);
+ const loading = useSelector(getLoading);
+ const fetching = useSelector(state => getFetchingForNarrow(state, narrow));
+ const isFetching = fetching.older || fetching.newer || loading;
+ const messages = useSelector(state => getShownMessagesForNarrow(state, narrow));
+ const firstUnreadIdInNarrow = useSelector(state => getFirstUnreadIdInNarrow(state, narrow));
+
+ // This could live in state, but then we'd risk pointless rerenders;
+ // we only use it in our `useEffect` callbacks. Using `useRef` is
+ // like using instance variables in class components:
+ // https://reactjs.org/docs/hooks-faq.html#is-there-something-like-instance-variables
+ const shouldFetchWhenNextFocused = React.useRef(false);
+
+ const [fetchError, setFetchError] = React.useState(null);
+
+ const fetch = React.useCallback(async () => {
+ shouldFetchWhenNextFocused.current = false;
+ try {
+ await dispatch(fetchMessagesInNarrow(narrow));
+ } catch (e) {
+ setFetchError(e);
+ }
+ }, [dispatch, narrow]);
+
+ // When the event queue changes, schedule a fetch. (Currently, we never
+ // set this to null from a non-null value, so this really does mean the
+ // event queue has changed; it can't mean that we had a queue ID and
+ // dropped it.)
+ React.useEffect(() => {
+ shouldFetchWhenNextFocused.current = true;
+ }, [eventQueueId]);
+
+ // On first mount, fetch. Also unset `shouldFetchWhenNextFocused.current`
+ // that was set in the previous `useEffect`, so the fetch below doesn't
+ // also fire.
+ React.useEffect(() => {
+ fetch();
+ }, [fetch]);
+
+ // When a fetch is scheduled and we're focused, fetch.
+ React.useEffect(() => {
+ if (shouldFetchWhenNextFocused.current && isFocused === true) {
+ fetch();
+ }
+ // `eventQueueId` needed here because it affects `shouldFetchWhenNextFocused`.
+ }, [isFocused, eventQueueId, fetch]);
+
+ return { fetchError, isFetching, messages, firstUnreadIdInNarrow };
+};
+
+export default function AllNarrowScreen(props: Props): Node {
+ const { route, navigation } = props;
+ const { backgroundColor } = React.useContext(ThemeContext);
+
+ const narrow = HOME_NARROW;
+ const { editMessage } = route.params;
+ const setEditMessage = useCallback(
+ (value: EditMessage | null) => navigation.setParams({ editMessage: value }),
+ [navigation],
+ );
+
+ const isNarrowValid = useSelector(state => getIsNarrowValid(state, narrow));
+ const draft = useSelector(state => getDraftForNarrow(state, narrow));
+
+ const { fetchError, isFetching, messages, firstUnreadIdInNarrow } = useMessagesWithFetch({
+ narrow,
+ });
+
+ const showMessagePlaceholders = messages.length === 0 && isFetching;
+ const sayNoMessages = messages.length === 0 && !isFetching;
+ const showComposeBox = showComposeBoxOnNarrow(narrow) && !showMessagePlaceholders;
+
+ const auth = useSelector(getAuth);
+ const dispatch = useDispatch();
+ const fetching = useSelector(state => getFetchingForNarrow(state, narrow));
+ const _ = useContext(TranslationContext);
+
+ const sendCallback = useCallback(
+ (message: string, destinationNarrow: Narrow) => {
+ if (editMessage) {
+ const content = editMessage.content !== message ? message : undefined;
+ const subject = caseNarrowDefault(
+ destinationNarrow,
+ { topic: (stream, topic) => (topic !== editMessage.topic ? topic : undefined) },
+ () => undefined,
+ );
+
+ if (
+ (content !== undefined && content !== '')
+ || (subject !== undefined && subject !== '')
+ ) {
+ api.updateMessage(auth, { content, subject }, editMessage.id).catch(error => {
+ showErrorAlert(_('Failed to edit message'), error.message);
+ });
+ }
+
+ setEditMessage(null);
+ } else {
+ if (fetching.newer) {
+ // If we're fetching, that means that (a) we're scrolled near the
+ // bottom, and likely are scrolled to the very bottom so that it
+ // looks like we're showing the latest messages, but (b) we don't
+ // actually have the latest messages. So the user may be misled
+ // and send a reply that doesn't make sense with the later context.
+ //
+ // Ideally in this condition we'd show a warning to make sure the
+ // user knows what they're getting into, and then let them send
+ // anyway. We'd also then need to take care with how the
+ // resulting message appears in the message list: see #3800 and
+ // https://chat.zulip.org/#narrow/stream/48-mobile/topic/Failed.20to.20send.20on.20Android/near/1158162
+ //
+ // For now, just refuse to send. After all, this condition will
+ // resolve itself when we complete the fetch, and if that doesn't
+ // happen soon then it's unlikely we could successfully send a
+ // message anyway.
+ showErrorAlert(_('Failed to send message'));
+ return;
+ }
+
+ dispatch(addToOutbox(destinationNarrow, message));
+ }
+ },
+ [_, auth, fetching.newer, dispatch, editMessage, setEditMessage],
+ );
+
+ return (
+
+
+
+
+ {(() => {
+ if (!isNarrowValid) {
+ return ;
+ } else if (fetchError !== null) {
+ return ;
+ } else if (sayNoMessages) {
+ return ;
+ } else {
+ return (
+ | void)?.id
+ ?? null
+ }
+ showMessagePlaceholders={showMessagePlaceholders}
+ startEditMessage={setEditMessage}
+ />
+ );
+ }
+ })()}
+ {showComposeBox && (
+
+ )}
+
+ );
+}
diff --git a/src/main/home/DefaultScreen.js b/src/main/home/DefaultScreen.js
new file mode 100644
index 00000000000..e52823079db
--- /dev/null
+++ b/src/main/home/DefaultScreen.js
@@ -0,0 +1,50 @@
+/* @flow strict-local */
+
+import React from 'react';
+import type { Node } from 'react';
+import { View } from 'react-native';
+import { SafeAreaView } from 'react-native-safe-area-context';
+
+import type { RouteProp } from '../../react-navigation';
+import type { HomeDrawerNavigationProp } from './HomeDrawerNavigator';
+import { TopTabButton } from '../../nav/TopTabButton';
+import UnreadCards from '../../unread/UnreadCards';
+import { createStyleSheet } from '../../styles';
+import { LoadingBanner } from '../../common';
+import ServerCompatBanner from '../../common/ServerCompatBanner';
+import ServerPushSetupBanner from '../../common/ServerPushSetupBanner';
+
+const styles = createStyleSheet({
+ wrapper: {
+ flex: 1,
+ flexDirection: 'column',
+ },
+ iconList: {
+ justifyContent: 'space-between',
+ flexDirection: 'row',
+ },
+});
+
+type Props = $ReadOnly<{|
+ navigation: HomeDrawerNavigationProp<'default'>,
+ route: RouteProp<'default', void>,
+|}>;
+
+export default function DefaultScreen(props: Props): Node {
+ return (
+
+
+ {
+ props.navigation.openDrawer();
+ }}
+ />
+
+
+
+
+
+
+ );
+}
diff --git a/src/main/home/HomeDrawerNavigator.js b/src/main/home/HomeDrawerNavigator.js
new file mode 100644
index 00000000000..341cd485707
--- /dev/null
+++ b/src/main/home/HomeDrawerNavigator.js
@@ -0,0 +1,85 @@
+/* @flow strict-local */
+import React from 'react';
+import type { Node } from 'react';
+import { createDrawerNavigator, type DrawerNavigationProp } from '@react-navigation/drawer';
+
+import type { RouteProp, RouteParamsOf } from '../../react-navigation';
+import type { MainTabsNavigationProp } from '../MainTabsScreen';
+import type { GlobalParamList } from '../../nav/globalTypes';
+import DefaultScreen from './DefaultScreen';
+import AllNarrowScreen from './AllNarrowScreen';
+import StarredNarrowScreen from './StarredNarrowScreen';
+import MentionedNarrowScreen from './MentionedNarrowScreen';
+import SearchInHomeDrawerScreen from './SearchInHomeDrawerScreen';
+
+export type HomeDrawerNavigatorParamList = {|
+ default: RouteParamsOf,
+ 'all-narrow': RouteParamsOf,
+ 'starred-narrow': RouteParamsOf,
+ 'mentioned-narrow': RouteParamsOf,
+ 'search-in-home-drawer': RouteParamsOf,
+|};
+
+export type HomeDrawerNavigationProp<
+ +RouteName: $Keys = $Keys,
+> = DrawerNavigationProp;
+
+const Tab = createDrawerNavigator<
+ GlobalParamList,
+ HomeDrawerNavigatorParamList,
+ HomeDrawerNavigationProp<>,
+>();
+
+type Props = $ReadOnly<{|
+ navigation: MainTabsNavigationProp<'home'>,
+ route: RouteProp<'home', void>,
+|}>;
+
+export default function HomeDrawerNavigator(props: Props): Node {
+ return (
+ CAUTION: Please note that Reanimated 2 doesn't support remote
+ // > debugging, only Flipper can be used for debugging.
+ gestureEnabled: true,
+ swipeEnabled: true,
+ }}
+ >
+
+
+
+
+
+
+ );
+}
diff --git a/src/main/home/MentionedNarrowScreen.js b/src/main/home/MentionedNarrowScreen.js
new file mode 100644
index 00000000000..ab2de232611
--- /dev/null
+++ b/src/main/home/MentionedNarrowScreen.js
@@ -0,0 +1,234 @@
+/* @flow strict-local */
+import React, { useCallback, useContext } from 'react';
+import type { Node } from 'react';
+import { useIsFocused } from '@react-navigation/native';
+
+import { useSelector, useDispatch } from '../../react-redux';
+import type { RouteProp } from '../../react-navigation';
+import type { HomeDrawerNavigationProp } from './HomeDrawerNavigator';
+import { ThemeContext, createStyleSheet } from '../../styles';
+import type { Narrow, EditMessage } from '../../types';
+import { KeyboardAvoider, OfflineNotice } from '../../common';
+import ChatNavBar from '../../nav/ChatNavBar';
+import MessageList from '../../webview/MessageList';
+import NoMessages from '../../message/NoMessages';
+import FetchError from '../../chat/FetchError';
+import InvalidNarrow from '../../chat/InvalidNarrow';
+import { fetchMessagesInNarrow } from '../../message/fetchActions';
+import ComposeBox from '../../compose/ComposeBox';
+import UnreadNotice from '../../chat/UnreadNotice';
+import {
+ showComposeBoxOnNarrow,
+ caseNarrowDefault,
+ keyFromNarrow,
+ MENTIONED_NARROW,
+} from '../../utils/narrow';
+import { getLoading, getSession } from '../../directSelectors';
+import { getFetchingForNarrow } from '../../chat/fetchingSelectors';
+import {
+ getShownMessagesForNarrow,
+ isNarrowValid as getIsNarrowValid,
+} from '../../chat/narrowsSelectors';
+import { getFirstUnreadIdInNarrow } from '../../message/messageSelectors';
+import { getDraftForNarrow } from '../../drafts/draftsSelectors';
+import { addToOutbox } from '../../actions';
+import { getAuth } from '../../selectors';
+import { showErrorAlert } from '../../utils/info';
+import { TranslationContext } from '../../boot/TranslationProvider';
+import * as api from '../../api';
+
+type Props = $ReadOnly<{|
+ navigation: HomeDrawerNavigationProp<'mentioned-narrow'>,
+ route: RouteProp<'mentioned-narrow', {| editMessage: EditMessage | null |}>,
+|}>;
+
+const componentStyles = createStyleSheet({
+ screen: {
+ flex: 1,
+ flexDirection: 'column',
+ },
+});
+
+/**
+ * Fetch messages for this narrow and report an error, if any
+ *
+ * See `MessagesState` for background about the fetching, including
+ * why this is nearly the only place where additional data fetching
+ * is required. See `fetchMessagesInNarrow` and `fetchMessages` for
+ * more details, including how Redux is kept up-to-date during the
+ * whole process.
+ */
+const useMessagesWithFetch = args => {
+ const { narrow } = args;
+
+ const dispatch = useDispatch();
+ const isFocused = useIsFocused();
+
+ const eventQueueId = useSelector(state => getSession(state).eventQueueId);
+ const loading = useSelector(getLoading);
+ const fetching = useSelector(state => getFetchingForNarrow(state, narrow));
+ const isFetching = fetching.older || fetching.newer || loading;
+ const messages = useSelector(state => getShownMessagesForNarrow(state, narrow));
+ const firstUnreadIdInNarrow = useSelector(state => getFirstUnreadIdInNarrow(state, narrow));
+
+ // This could live in state, but then we'd risk pointless rerenders;
+ // we only use it in our `useEffect` callbacks. Using `useRef` is
+ // like using instance variables in class components:
+ // https://reactjs.org/docs/hooks-faq.html#is-there-something-like-instance-variables
+ const shouldFetchWhenNextFocused = React.useRef(false);
+
+ const [fetchError, setFetchError] = React.useState(null);
+
+ const fetch = React.useCallback(async () => {
+ shouldFetchWhenNextFocused.current = false;
+ try {
+ await dispatch(fetchMessagesInNarrow(narrow));
+ } catch (e) {
+ setFetchError(e);
+ }
+ }, [dispatch, narrow]);
+
+ // When the event queue changes, schedule a fetch. (Currently, we never
+ // set this to null from a non-null value, so this really does mean the
+ // event queue has changed; it can't mean that we had a queue ID and
+ // dropped it.)
+ React.useEffect(() => {
+ shouldFetchWhenNextFocused.current = true;
+ }, [eventQueueId]);
+
+ // On first mount, fetch. Also unset `shouldFetchWhenNextFocused.current`
+ // that was set in the previous `useEffect`, so the fetch below doesn't
+ // also fire.
+ React.useEffect(() => {
+ fetch();
+ }, [fetch]);
+
+ // When a fetch is scheduled and we're focused, fetch.
+ React.useEffect(() => {
+ if (shouldFetchWhenNextFocused.current && isFocused === true) {
+ fetch();
+ }
+ // `eventQueueId` needed here because it affects `shouldFetchWhenNextFocused`.
+ }, [isFocused, eventQueueId, fetch]);
+
+ return { fetchError, isFetching, messages, firstUnreadIdInNarrow };
+};
+
+export default function MentionedNarrowScreen(props: Props): Node {
+ const { route, navigation } = props;
+ const { backgroundColor } = React.useContext(ThemeContext);
+
+ const narrow = MENTIONED_NARROW;
+ const { editMessage } = route.params;
+ const setEditMessage = useCallback(
+ (value: EditMessage | null) => navigation.setParams({ editMessage: value }),
+ [navigation],
+ );
+
+ const isNarrowValid = useSelector(state => getIsNarrowValid(state, narrow));
+ const draft = useSelector(state => getDraftForNarrow(state, narrow));
+
+ const { fetchError, isFetching, messages, firstUnreadIdInNarrow } = useMessagesWithFetch({
+ narrow,
+ });
+
+ const showMessagePlaceholders = messages.length === 0 && isFetching;
+ const sayNoMessages = messages.length === 0 && !isFetching;
+ const showComposeBox = showComposeBoxOnNarrow(narrow) && !showMessagePlaceholders;
+
+ const auth = useSelector(getAuth);
+ const dispatch = useDispatch();
+ const fetching = useSelector(state => getFetchingForNarrow(state, narrow));
+ const _ = useContext(TranslationContext);
+
+ const sendCallback = useCallback(
+ (message: string, destinationNarrow: Narrow) => {
+ if (editMessage) {
+ const content = editMessage.content !== message ? message : undefined;
+ const subject = caseNarrowDefault(
+ destinationNarrow,
+ { topic: (stream, topic) => (topic !== editMessage.topic ? topic : undefined) },
+ () => undefined,
+ );
+
+ if (
+ (content !== undefined && content !== '')
+ || (subject !== undefined && subject !== '')
+ ) {
+ api.updateMessage(auth, { content, subject }, editMessage.id).catch(error => {
+ showErrorAlert(_('Failed to edit message'), error.message);
+ });
+ }
+
+ setEditMessage(null);
+ } else {
+ if (fetching.newer) {
+ // If we're fetching, that means that (a) we're scrolled near the
+ // bottom, and likely are scrolled to the very bottom so that it
+ // looks like we're showing the latest messages, but (b) we don't
+ // actually have the latest messages. So the user may be misled
+ // and send a reply that doesn't make sense with the later context.
+ //
+ // Ideally in this condition we'd show a warning to make sure the
+ // user knows what they're getting into, and then let them send
+ // anyway. We'd also then need to take care with how the
+ // resulting message appears in the message list: see #3800 and
+ // https://chat.zulip.org/#narrow/stream/48-mobile/topic/Failed.20to.20send.20on.20Android/near/1158162
+ //
+ // For now, just refuse to send. After all, this condition will
+ // resolve itself when we complete the fetch, and if that doesn't
+ // happen soon then it's unlikely we could successfully send a
+ // message anyway.
+ showErrorAlert(_('Failed to send message'));
+ return;
+ }
+
+ dispatch(addToOutbox(destinationNarrow, message));
+ }
+ },
+ [_, auth, fetching.newer, dispatch, editMessage, setEditMessage],
+ );
+
+ return (
+
+
+
+
+ {(() => {
+ if (!isNarrowValid) {
+ return ;
+ } else if (fetchError !== null) {
+ return ;
+ } else if (sayNoMessages) {
+ return ;
+ } else {
+ return (
+ | void)?.id
+ ?? null
+ }
+ showMessagePlaceholders={showMessagePlaceholders}
+ startEditMessage={setEditMessage}
+ />
+ );
+ }
+ })()}
+ {showComposeBox && (
+
+ )}
+
+ );
+}
diff --git a/src/main/home/SearchInHomeDrawerScreen.js b/src/main/home/SearchInHomeDrawerScreen.js
new file mode 100644
index 00000000000..ff821c5dfb1
--- /dev/null
+++ b/src/main/home/SearchInHomeDrawerScreen.js
@@ -0,0 +1,158 @@
+/* @flow strict-local */
+import React, { PureComponent } from 'react';
+import type { ComponentType } from 'react';
+import type { EditingEvent } from 'react-native/Libraries/Components/TextInput/TextInput';
+
+import type { RouteProp } from '../../react-navigation';
+import type { HomeDrawerNavigationProp } from './HomeDrawerNavigator';
+import type { Auth, Dispatch, Message } from '../../types';
+import { Screen } from '../../common';
+import SearchMessagesCard from '../../search/SearchMessagesCard';
+import styles from '../../styles';
+import { SEARCH_NARROW } from '../../utils/narrow';
+import { LAST_MESSAGE_ANCHOR } from '../../anchor';
+import { connect } from '../../react-redux';
+import { getAuth } from '../../account/accountsSelectors';
+import { fetchMessages } from '../../message/fetchActions';
+
+type OuterProps = $ReadOnly<{|
+ // These should be passed from React Navigation
+ navigation: HomeDrawerNavigationProp<'search-in-home-drawer'>,
+ route: RouteProp<'search-in-home-drawer', void>,
+|}>;
+
+type SelectorProps = $ReadOnly<{|
+ auth: Auth,
+|}>;
+
+type Props = $ReadOnly<{|
+ ...OuterProps,
+
+ dispatch: Dispatch,
+ ...SelectorProps,
+ // Warning: do not add new props without considering their effect on the
+ // behavior of this component's non-React internal state. See comment below.
+|}>;
+
+type State = {|
+ /** The latest search query we have results for. */
+ query: string,
+
+ /**
+ * The list of messages found as results for `query`.
+ *
+ * This is `null` if `query` is empty, representing an empty search box
+ * and so effectively not a query to have results from at all.
+ */
+ messages: $ReadOnlyArray | null,
+
+ /** Whether there is currently an active valid network request. */
+ isFetching: boolean,
+|};
+
+class SearchInHomeDrawerScreenInner extends PureComponent {
+ state = {
+ query: '',
+ messages: null,
+ isFetching: false,
+ };
+
+ /**
+ * PRIVATE. Send search query to server, fetching message results.
+ *
+ * Stores the fetched messages in the Redux store. Does not read any
+ * of the component's data except `props.dispatch`.
+ */
+ fetchSearchMessages = async (query: string): Promise<$ReadOnlyArray> => {
+ const fetchArgs = {
+ narrow: SEARCH_NARROW(query),
+ anchor: LAST_MESSAGE_ANCHOR,
+ numBefore: 20,
+ numAfter: 0,
+ };
+
+ return this.props.dispatch(fetchMessages(fetchArgs));
+ };
+
+ // Non-React state. See comment following.
+ // Invariant: lastIdSuccess <= lastIdReceived <= lastIdSent.
+ lastIdSuccess: number = 1000;
+ lastIdReceived: number = 1000;
+ lastIdSent: number = 1000;
+
+ // This component is less pure than it should be. The correct behavior here is
+ // probably that, when props change, all outstanding asynchronous requests
+ // should be **synchronously** invalidated before the next render.
+ //
+ // As the only React prop this component has is `auth`, we ignore this for
+ // now: any updates to `auth` would involve this screen being torn down and
+ // reconstructed anyway. However, addition of any new props which need to
+ // invalidate outstanding requests on change will require more work.
+
+ handleQuerySubmit = async (e: EditingEvent) => {
+ const query = e.nativeEvent.text;
+ const id = ++this.lastIdSent;
+
+ if (query === '') {
+ // The empty query can be resolved without a network call.
+ this.lastIdReceived = id;
+ this.lastIdSuccess = id;
+ this.setState({ query, messages: null, isFetching: false });
+ return;
+ }
+
+ this.setState({ isFetching: true });
+ try {
+ const messages = await this.fetchSearchMessages(query);
+
+ // Update `state.messages` if this is our new latest result.
+ if (id > this.lastIdSuccess) {
+ this.lastIdSuccess = id;
+ this.setState({ query, messages });
+ }
+ } finally {
+ // Updating `isFetching` is the same for success or failure.
+ if (id > this.lastIdReceived) {
+ this.lastIdReceived = id;
+ if (this.lastIdReceived === this.lastIdSent) {
+ this.setState({ isFetching: false });
+ }
+
+ // TODO: if the request failed, should we arrange to display
+ // something to the user?
+ }
+ }
+ };
+
+ // The real work to be done on a query is async. This wrapper exists
+ // just to fire off `handleQuerySubmit` without waiting for it.
+ // TODO do we even need this wrapper?
+ handleQuerySubmitWrapper = (e: EditingEvent) => {
+ this.handleQuerySubmit(e);
+ };
+
+ render() {
+ const { messages, isFetching } = this.state;
+
+ return (
+
+
+
+ );
+ }
+}
+
+const SearchInHomeDrawerScreen: ComponentType = connect(state => ({
+ auth: getAuth(state),
+}))(SearchInHomeDrawerScreenInner);
+
+export default SearchInHomeDrawerScreen;
diff --git a/src/main/home/StarredNarrowScreen.js b/src/main/home/StarredNarrowScreen.js
new file mode 100644
index 00000000000..65f09fb2cd4
--- /dev/null
+++ b/src/main/home/StarredNarrowScreen.js
@@ -0,0 +1,234 @@
+/* @flow strict-local */
+import React, { useCallback, useContext } from 'react';
+import type { Node } from 'react';
+import { useIsFocused } from '@react-navigation/native';
+
+import { useSelector, useDispatch } from '../../react-redux';
+import type { RouteProp } from '../../react-navigation';
+import type { HomeDrawerNavigationProp } from './HomeDrawerNavigator';
+import { ThemeContext, createStyleSheet } from '../../styles';
+import type { Narrow, EditMessage } from '../../types';
+import { KeyboardAvoider, OfflineNotice } from '../../common';
+import ChatNavBar from '../../nav/ChatNavBar';
+import MessageList from '../../webview/MessageList';
+import NoMessages from '../../message/NoMessages';
+import FetchError from '../../chat/FetchError';
+import InvalidNarrow from '../../chat/InvalidNarrow';
+import { fetchMessagesInNarrow } from '../../message/fetchActions';
+import ComposeBox from '../../compose/ComposeBox';
+import UnreadNotice from '../../chat/UnreadNotice';
+import {
+ showComposeBoxOnNarrow,
+ caseNarrowDefault,
+ keyFromNarrow,
+ STARRED_NARROW,
+} from '../../utils/narrow';
+import { getLoading, getSession } from '../../directSelectors';
+import { getFetchingForNarrow } from '../../chat/fetchingSelectors';
+import {
+ getShownMessagesForNarrow,
+ isNarrowValid as getIsNarrowValid,
+} from '../../chat/narrowsSelectors';
+import { getFirstUnreadIdInNarrow } from '../../message/messageSelectors';
+import { getDraftForNarrow } from '../../drafts/draftsSelectors';
+import { addToOutbox } from '../../actions';
+import { getAuth } from '../../selectors';
+import { showErrorAlert } from '../../utils/info';
+import { TranslationContext } from '../../boot/TranslationProvider';
+import * as api from '../../api';
+
+type Props = $ReadOnly<{|
+ navigation: HomeDrawerNavigationProp<'starred-narrow'>,
+ route: RouteProp<'starred-narrow', {| editMessage: EditMessage | null |}>,
+|}>;
+
+const componentStyles = createStyleSheet({
+ screen: {
+ flex: 1,
+ flexDirection: 'column',
+ },
+});
+
+/**
+ * Fetch messages for this narrow and report an error, if any
+ *
+ * See `MessagesState` for background about the fetching, including
+ * why this is nearly the only place where additional data fetching
+ * is required. See `fetchMessagesInNarrow` and `fetchMessages` for
+ * more details, including how Redux is kept up-to-date during the
+ * whole process.
+ */
+const useMessagesWithFetch = args => {
+ const { narrow } = args;
+
+ const dispatch = useDispatch();
+ const isFocused = useIsFocused();
+
+ const eventQueueId = useSelector(state => getSession(state).eventQueueId);
+ const loading = useSelector(getLoading);
+ const fetching = useSelector(state => getFetchingForNarrow(state, narrow));
+ const isFetching = fetching.older || fetching.newer || loading;
+ const messages = useSelector(state => getShownMessagesForNarrow(state, narrow));
+ const firstUnreadIdInNarrow = useSelector(state => getFirstUnreadIdInNarrow(state, narrow));
+
+ // This could live in state, but then we'd risk pointless rerenders;
+ // we only use it in our `useEffect` callbacks. Using `useRef` is
+ // like using instance variables in class components:
+ // https://reactjs.org/docs/hooks-faq.html#is-there-something-like-instance-variables
+ const shouldFetchWhenNextFocused = React.useRef(false);
+
+ const [fetchError, setFetchError] = React.useState(null);
+
+ const fetch = React.useCallback(async () => {
+ shouldFetchWhenNextFocused.current = false;
+ try {
+ await dispatch(fetchMessagesInNarrow(narrow));
+ } catch (e) {
+ setFetchError(e);
+ }
+ }, [dispatch, narrow]);
+
+ // When the event queue changes, schedule a fetch. (Currently, we never
+ // set this to null from a non-null value, so this really does mean the
+ // event queue has changed; it can't mean that we had a queue ID and
+ // dropped it.)
+ React.useEffect(() => {
+ shouldFetchWhenNextFocused.current = true;
+ }, [eventQueueId]);
+
+ // On first mount, fetch. Also unset `shouldFetchWhenNextFocused.current`
+ // that was set in the previous `useEffect`, so the fetch below doesn't
+ // also fire.
+ React.useEffect(() => {
+ fetch();
+ }, [fetch]);
+
+ // When a fetch is scheduled and we're focused, fetch.
+ React.useEffect(() => {
+ if (shouldFetchWhenNextFocused.current && isFocused === true) {
+ fetch();
+ }
+ // `eventQueueId` needed here because it affects `shouldFetchWhenNextFocused`.
+ }, [isFocused, eventQueueId, fetch]);
+
+ return { fetchError, isFetching, messages, firstUnreadIdInNarrow };
+};
+
+export default function StarredNarrowScreen(props: Props): Node {
+ const { route, navigation } = props;
+ const { backgroundColor } = React.useContext(ThemeContext);
+
+ const narrow = STARRED_NARROW;
+ const { editMessage } = route.params;
+ const setEditMessage = useCallback(
+ (value: EditMessage | null) => navigation.setParams({ editMessage: value }),
+ [navigation],
+ );
+
+ const isNarrowValid = useSelector(state => getIsNarrowValid(state, narrow));
+ const draft = useSelector(state => getDraftForNarrow(state, narrow));
+
+ const { fetchError, isFetching, messages, firstUnreadIdInNarrow } = useMessagesWithFetch({
+ narrow,
+ });
+
+ const showMessagePlaceholders = messages.length === 0 && isFetching;
+ const sayNoMessages = messages.length === 0 && !isFetching;
+ const showComposeBox = showComposeBoxOnNarrow(narrow) && !showMessagePlaceholders;
+
+ const auth = useSelector(getAuth);
+ const dispatch = useDispatch();
+ const fetching = useSelector(state => getFetchingForNarrow(state, narrow));
+ const _ = useContext(TranslationContext);
+
+ const sendCallback = useCallback(
+ (message: string, destinationNarrow: Narrow) => {
+ if (editMessage) {
+ const content = editMessage.content !== message ? message : undefined;
+ const subject = caseNarrowDefault(
+ destinationNarrow,
+ { topic: (stream, topic) => (topic !== editMessage.topic ? topic : undefined) },
+ () => undefined,
+ );
+
+ if (
+ (content !== undefined && content !== '')
+ || (subject !== undefined && subject !== '')
+ ) {
+ api.updateMessage(auth, { content, subject }, editMessage.id).catch(error => {
+ showErrorAlert(_('Failed to edit message'), error.message);
+ });
+ }
+
+ setEditMessage(null);
+ } else {
+ if (fetching.newer) {
+ // If we're fetching, that means that (a) we're scrolled near the
+ // bottom, and likely are scrolled to the very bottom so that it
+ // looks like we're showing the latest messages, but (b) we don't
+ // actually have the latest messages. So the user may be misled
+ // and send a reply that doesn't make sense with the later context.
+ //
+ // Ideally in this condition we'd show a warning to make sure the
+ // user knows what they're getting into, and then let them send
+ // anyway. We'd also then need to take care with how the
+ // resulting message appears in the message list: see #3800 and
+ // https://chat.zulip.org/#narrow/stream/48-mobile/topic/Failed.20to.20send.20on.20Android/near/1158162
+ //
+ // For now, just refuse to send. After all, this condition will
+ // resolve itself when we complete the fetch, and if that doesn't
+ // happen soon then it's unlikely we could successfully send a
+ // message anyway.
+ showErrorAlert(_('Failed to send message'));
+ return;
+ }
+
+ dispatch(addToOutbox(destinationNarrow, message));
+ }
+ },
+ [_, auth, fetching.newer, dispatch, editMessage, setEditMessage],
+ );
+
+ return (
+
+
+
+
+ {(() => {
+ if (!isNarrowValid) {
+ return ;
+ } else if (fetchError !== null) {
+ return ;
+ } else if (sayNoMessages) {
+ return ;
+ } else {
+ return (
+ | void)?.id
+ ?? null
+ }
+ showMessagePlaceholders={showMessagePlaceholders}
+ startEditMessage={setEditMessage}
+ />
+ );
+ }
+ })()}
+ {showComposeBox && (
+
+ )}
+
+ );
+}
diff --git a/src/nav/NavBarBackButton.js b/src/nav/NavBarBackButton.js
index 8de9449443c..98250434fc7 100644
--- a/src/nav/NavBarBackButton.js
+++ b/src/nav/NavBarBackButton.js
@@ -3,9 +3,8 @@ import React from 'react';
import type { Node } from 'react';
import { Platform } from 'react-native';
-import { navigateBack } from '../actions';
+import { useNavigation } from '../react-navigation';
import NavButton from './NavButton';
-import * as NavigationService from './NavigationService';
/**
* The button for the start of the app bar, to return to previous screen.
@@ -26,13 +25,15 @@ export default function NavBarBackButton(props: {| +color?: string |}): Node {
const { color } = props;
const iconName = Platform.OS === 'android' ? 'arrow-left' : 'chevron-left';
+ const navigation = useNavigation();
+
return (
{
- NavigationService.dispatch(navigateBack());
+ navigation.goBack();
}}
/>
);
diff --git a/src/nav/globalTypes.js b/src/nav/globalTypes.js
index 756a2029d8f..f522edebe39 100644
--- a/src/nav/globalTypes.js
+++ b/src/nav/globalTypes.js
@@ -4,10 +4,12 @@ import type { AppNavigatorParamList } from './AppNavigator';
import type { SharingNavigatorParamList } from '../sharing/SharingScreen';
import type { StreamTabsNavigatorParamList } from '../main/StreamTabsScreen';
import type { MainTabsNavigatorParamList } from '../main/MainTabsScreen';
+import type { HomeDrawerNavigatorParamList } from '../main/home/HomeDrawerNavigator';
export type GlobalParamList = {|
...AppNavigatorParamList,
...SharingNavigatorParamList,
...StreamTabsNavigatorParamList,
...MainTabsNavigatorParamList,
+ ...HomeDrawerNavigatorParamList,
|};
diff --git a/src/pm-conversations/PmConversationsScreen.js b/src/pm-conversations/PmConversationsScreen.js
index 300a44a7b45..3543b0fbdd5 100644
--- a/src/pm-conversations/PmConversationsScreen.js
+++ b/src/pm-conversations/PmConversationsScreen.js
@@ -3,6 +3,7 @@
import React, { useContext } from 'react';
import type { Node } from 'react';
import { View } from 'react-native';
+import { SafeAreaView } from 'react-native-safe-area-context';
import type { RouteProp } from '../react-navigation';
import type { MainTabsNavigationProp } from '../main/MainTabsScreen';
@@ -47,7 +48,11 @@ export default function PmConversationsScreen(props: Props): Node {
const context = useContext(ThemeContext);
return (
-
+
)}
-
+
);
}