[FIX] Jump to message stuck on loading animation (#4410)
This commit is contained in:
parent
e723990e82
commit
17be449d4e
|
@ -1,73 +0,0 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { Modal, StyleSheet, View, PixelRatio } from 'react-native';
|
||||
import Animated, {
|
||||
cancelAnimation,
|
||||
Extrapolate,
|
||||
interpolate,
|
||||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
withRepeat,
|
||||
withSequence,
|
||||
withTiming
|
||||
} from 'react-native-reanimated';
|
||||
|
||||
import { useTheme } from '../theme';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
image: {
|
||||
width: PixelRatio.get() * 40,
|
||||
height: PixelRatio.get() * 40,
|
||||
resizeMode: 'contain'
|
||||
}
|
||||
});
|
||||
|
||||
interface ILoadingProps {
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
const Loading = ({ visible }: ILoadingProps): React.ReactElement => {
|
||||
const opacity = useSharedValue(0);
|
||||
const scale = useSharedValue(1);
|
||||
const { colors } = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
opacity.value = withTiming(1, {
|
||||
duration: 200
|
||||
});
|
||||
scale.value = withRepeat(withSequence(withTiming(0, { duration: 1000 }), withTiming(1, { duration: 1000 })), -1);
|
||||
}
|
||||
return () => {
|
||||
cancelAnimation(scale);
|
||||
};
|
||||
}, [opacity, scale, visible]);
|
||||
|
||||
const animatedOpacity = useAnimatedStyle(() => ({
|
||||
opacity: interpolate(opacity.value, [0, 1], [0, colors.backdropOpacity], Extrapolate.CLAMP)
|
||||
}));
|
||||
const animatedScale = useAnimatedStyle(() => ({ transform: [{ scale: interpolate(scale.value, [0, 0.5, 1], [1, 1.1, 1]) }] }));
|
||||
|
||||
return (
|
||||
<Modal visible={visible} transparent onRequestClose={() => {}}>
|
||||
<View style={styles.container} testID='loading'>
|
||||
<Animated.View
|
||||
style={[
|
||||
{
|
||||
...StyleSheet.absoluteFillObject,
|
||||
backgroundColor: colors.backdropColor
|
||||
},
|
||||
animatedOpacity
|
||||
]}
|
||||
/>
|
||||
<Animated.Image source={require('../static/images/logo.png')} style={[styles.image, animatedScale]} />
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default Loading;
|
|
@ -0,0 +1,74 @@
|
|||
import { act, fireEvent, render, waitFor } from '@testing-library/react-native';
|
||||
import React from 'react';
|
||||
|
||||
import Loading, { sendLoadingEvent, LOADING_BUTTON_TEST_ID, LOADING_IMAGE_TEST_ID, LOADING_TEST_ID } from '.';
|
||||
|
||||
const Render = () => <Loading />;
|
||||
|
||||
const getByTestIdAndThrow = (fn: Function, testID: string) =>
|
||||
expect(() => fn(testID)).toThrow(`Unable to find an element with testID: ${testID}`);
|
||||
|
||||
describe('Loading', () => {
|
||||
it('starts invisible and shows/hides when event is received', async () => {
|
||||
const { getByTestId } = render(<Render />);
|
||||
getByTestIdAndThrow(getByTestId, LOADING_TEST_ID);
|
||||
// receive event and expect loading to be rendered
|
||||
act(() => sendLoadingEvent({ visible: true }));
|
||||
await waitFor(() => {
|
||||
expect(() => getByTestId(LOADING_TEST_ID));
|
||||
});
|
||||
expect(() => getByTestId(LOADING_IMAGE_TEST_ID));
|
||||
// receive event and expect loading not to be rendered
|
||||
act(() => sendLoadingEvent({ visible: false }));
|
||||
await waitFor(() => {
|
||||
getByTestIdAndThrow(getByTestId, LOADING_TEST_ID);
|
||||
});
|
||||
});
|
||||
|
||||
it('doesnt have onCancel and doesnt hide when pressed', async () => {
|
||||
const { getByTestId } = render(<Render />);
|
||||
getByTestIdAndThrow(getByTestId, LOADING_TEST_ID);
|
||||
act(() => sendLoadingEvent({ visible: true }));
|
||||
expect(() => getByTestId(LOADING_TEST_ID));
|
||||
fireEvent.press(getByTestId(LOADING_BUTTON_TEST_ID));
|
||||
await waitFor(() => {
|
||||
expect(() => getByTestId(LOADING_TEST_ID));
|
||||
});
|
||||
});
|
||||
|
||||
it('has onCancel and hides when pressed', async () => {
|
||||
const mockFn = jest.fn();
|
||||
const { getByTestId } = render(<Render />);
|
||||
getByTestIdAndThrow(getByTestId, LOADING_TEST_ID);
|
||||
act(() => sendLoadingEvent({ visible: true, onCancel: mockFn }));
|
||||
await waitFor(() => {
|
||||
expect(() => getByTestId(LOADING_TEST_ID));
|
||||
});
|
||||
fireEvent.press(getByTestId(LOADING_BUTTON_TEST_ID));
|
||||
await waitFor(() => {
|
||||
getByTestIdAndThrow(getByTestId, LOADING_TEST_ID);
|
||||
});
|
||||
expect(mockFn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('asserts onCancel return', async () => {
|
||||
const mockFn = jest.fn();
|
||||
const mockFn2 = jest.fn(() => 'test');
|
||||
const { getByTestId } = render(<Render />);
|
||||
getByTestIdAndThrow(getByTestId, LOADING_TEST_ID);
|
||||
act(() => sendLoadingEvent({ visible: true, onCancel: mockFn }));
|
||||
await waitFor(() => {
|
||||
expect(() => getByTestId(LOADING_TEST_ID));
|
||||
});
|
||||
act(() => sendLoadingEvent({ visible: true, onCancel: mockFn2 }));
|
||||
await waitFor(() => {
|
||||
expect(() => getByTestId(LOADING_TEST_ID));
|
||||
});
|
||||
fireEvent.press(getByTestId(LOADING_BUTTON_TEST_ID));
|
||||
await waitFor(() => {
|
||||
getByTestIdAndThrow(getByTestId, LOADING_TEST_ID);
|
||||
});
|
||||
expect(mockFn).not.toHaveBeenCalled();
|
||||
expect(mockFn2).toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,128 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { StyleSheet, View, PixelRatio, TouchableWithoutFeedback } from 'react-native';
|
||||
import Animated, {
|
||||
cancelAnimation,
|
||||
Extrapolate,
|
||||
interpolate,
|
||||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
withRepeat,
|
||||
withSequence,
|
||||
withTiming
|
||||
} from 'react-native-reanimated';
|
||||
|
||||
import { useTheme } from '../../theme';
|
||||
import EventEmitter from '../../lib/methods/helpers/events';
|
||||
|
||||
const LOADING_EVENT = 'LOADING_EVENT';
|
||||
export const LOADING_TEST_ID = 'loading';
|
||||
export const LOADING_BUTTON_TEST_ID = 'loading-button';
|
||||
export const LOADING_IMAGE_TEST_ID = 'loading-image';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
image: {
|
||||
width: PixelRatio.get() * 40,
|
||||
height: PixelRatio.get() * 40,
|
||||
resizeMode: 'contain'
|
||||
}
|
||||
});
|
||||
|
||||
interface ILoadingEvent {
|
||||
visible: boolean;
|
||||
onCancel?: null | Function;
|
||||
}
|
||||
|
||||
export const sendLoadingEvent = ({ visible, onCancel }: ILoadingEvent): void =>
|
||||
EventEmitter.emit(LOADING_EVENT, { visible, onCancel });
|
||||
|
||||
const Loading = (): React.ReactElement | null => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [onCancel, setOnCancel] = useState<null | Function>(null);
|
||||
const opacity = useSharedValue(0);
|
||||
const scale = useSharedValue(1);
|
||||
const { colors } = useTheme();
|
||||
|
||||
const onEventReceived = ({ visible: _visible, onCancel: _onCancel = null }: ILoadingEvent) => {
|
||||
if (_visible) {
|
||||
// if it's already visible, ignore it
|
||||
if (!visible) {
|
||||
setVisible(_visible);
|
||||
opacity.value = 0;
|
||||
scale.value = 1;
|
||||
opacity.value = withTiming(1, {
|
||||
// 300ms doens't work on expensive navigation animations, like jump to message
|
||||
duration: 500
|
||||
});
|
||||
scale.value = withRepeat(withSequence(withTiming(0, { duration: 1000 }), withTiming(1, { duration: 1000 })), -1);
|
||||
}
|
||||
|
||||
// allows to override the onCancel function
|
||||
if (_onCancel) {
|
||||
setOnCancel(() => () => _onCancel());
|
||||
}
|
||||
} else {
|
||||
setVisible(false);
|
||||
reset();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const listener = EventEmitter.addEventListener(LOADING_EVENT, onEventReceived);
|
||||
|
||||
return () => EventEmitter.removeListener(LOADING_EVENT, listener);
|
||||
}, [visible]);
|
||||
|
||||
const reset = () => {
|
||||
cancelAnimation(scale);
|
||||
cancelAnimation(opacity);
|
||||
setVisible(false);
|
||||
setOnCancel(null);
|
||||
};
|
||||
|
||||
const onCancelHandler = () => {
|
||||
if (!onCancel) {
|
||||
return;
|
||||
}
|
||||
onCancel();
|
||||
setVisible(false);
|
||||
reset();
|
||||
};
|
||||
|
||||
const animatedOpacity = useAnimatedStyle(() => ({
|
||||
opacity: interpolate(opacity.value, [0, 1], [0, colors.backdropOpacity], Extrapolate.CLAMP)
|
||||
}));
|
||||
const animatedScale = useAnimatedStyle(() => ({ transform: [{ scale: interpolate(scale.value, [0, 0.5, 1], [1, 1.1, 1]) }] }));
|
||||
|
||||
if (!visible) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<View style={StyleSheet.absoluteFill} testID={LOADING_TEST_ID}>
|
||||
<TouchableWithoutFeedback onPress={() => onCancelHandler()} testID={LOADING_BUTTON_TEST_ID}>
|
||||
<View style={styles.container}>
|
||||
<Animated.View
|
||||
style={[
|
||||
{
|
||||
...StyleSheet.absoluteFillObject,
|
||||
backgroundColor: colors.backdropColor
|
||||
},
|
||||
animatedOpacity
|
||||
]}
|
||||
/>
|
||||
<Animated.Image
|
||||
source={require('../../static/images/logo.png')}
|
||||
style={[styles.image, animatedScale]}
|
||||
testID={LOADING_IMAGE_TEST_ID}
|
||||
/>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default Loading;
|
|
@ -14,6 +14,7 @@ import { ActionSheetProvider } from './containers/ActionSheet';
|
|||
import InAppNotification from './containers/InAppNotification';
|
||||
import Toast from './containers/Toast';
|
||||
import TwoFactor from './containers/TwoFactor';
|
||||
import Loading from './containers/Loading';
|
||||
import { ICommand } from './definitions/ICommand';
|
||||
import { IThemePreference } from './definitions/ITheme';
|
||||
import { DimensionsContext } from './dimensions';
|
||||
|
@ -241,6 +242,7 @@ export default class Root extends React.Component<{}, IState> {
|
|||
<ChangePasscodeView />
|
||||
<InAppNotification />
|
||||
<Toast />
|
||||
<Loading />
|
||||
</ActionSheetProvider>
|
||||
</GestureHandlerRootView>
|
||||
</DimensionsContext.Provider>
|
||||
|
|
|
@ -10,6 +10,7 @@ type TEventEmitterEmmitArgs =
|
|||
| { invalid: boolean }
|
||||
| { force: boolean }
|
||||
| { hasBiometry: boolean }
|
||||
| { visible: boolean; onCancel?: null | Function }
|
||||
| { event: string | ICommand }
|
||||
| { cancel: () => void }
|
||||
| { submit: (param: string) => void }
|
||||
|
|
|
@ -25,6 +25,7 @@ import AuthLoadingView from './views/AuthLoadingView';
|
|||
import { DimensionsContext } from './dimensions';
|
||||
import { ShareInsideStackParamList, ShareOutsideStackParamList, ShareAppStackParamList } from './definitions/navigationTypes';
|
||||
import { colors, CURRENT_SERVER } from './lib/constants';
|
||||
import Loading from './containers/Loading';
|
||||
|
||||
initStore(store);
|
||||
|
||||
|
@ -131,6 +132,7 @@ const Root = (): React.ReactElement => {
|
|||
}}
|
||||
>
|
||||
<App root={root} />
|
||||
<Loading />
|
||||
</NavigationContainer>
|
||||
<ScreenLockedView />
|
||||
</DimensionsContext.Provider>
|
||||
|
|
|
@ -15,7 +15,7 @@ import StatusBar from '../containers/StatusBar';
|
|||
import { themes } from '../lib/constants';
|
||||
import { TSupportedThemes, withTheme } from '../theme';
|
||||
import SafeAreaView from '../containers/SafeAreaView';
|
||||
import Loading from '../containers/Loading';
|
||||
import { sendLoadingEvent } from '../containers/Loading';
|
||||
import { animateNextTransition } from '../lib/methods/helpers/layoutAnimation';
|
||||
import { goRoom } from '../lib/methods/helpers/goRoom';
|
||||
import { showErrorAlert } from '../lib/methods/helpers/info';
|
||||
|
@ -28,7 +28,6 @@ interface IAddExistingChannelViewState {
|
|||
search: TSubscriptionModel[];
|
||||
channels: TSubscriptionModel[];
|
||||
selected: string[];
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
interface IAddExistingChannelViewProps {
|
||||
|
@ -51,8 +50,7 @@ class AddExistingChannelView extends React.Component<IAddExistingChannelViewProp
|
|||
this.state = {
|
||||
search: [],
|
||||
channels: [],
|
||||
selected: [],
|
||||
loading: false
|
||||
selected: []
|
||||
};
|
||||
this.setHeader();
|
||||
}
|
||||
|
@ -130,12 +128,12 @@ class AddExistingChannelView extends React.Component<IAddExistingChannelViewProp
|
|||
const { selected } = this.state;
|
||||
const { isMasterDetail } = this.props;
|
||||
|
||||
this.setState({ loading: true });
|
||||
sendLoadingEvent({ visible: true });
|
||||
try {
|
||||
logEvent(events.CT_ADD_ROOM_TO_TEAM);
|
||||
const result = await Services.addRoomsToTeam({ rooms: selected, teamId: this.teamId });
|
||||
if (result.success) {
|
||||
this.setState({ loading: false });
|
||||
sendLoadingEvent({ visible: false });
|
||||
// @ts-ignore
|
||||
// TODO: Verify goRoom interface for return of call
|
||||
goRoom({ item: result, isMasterDetail });
|
||||
|
@ -143,7 +141,7 @@ class AddExistingChannelView extends React.Component<IAddExistingChannelViewProp
|
|||
} catch (e: any) {
|
||||
logEvent(events.CT_ADD_ROOM_TO_TEAM_F);
|
||||
showErrorAlert(I18n.t(e.data.error), I18n.t('Add_Existing_Channel'), () => {});
|
||||
this.setState({ loading: false });
|
||||
sendLoadingEvent({ visible: false });
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -209,13 +207,10 @@ class AddExistingChannelView extends React.Component<IAddExistingChannelViewProp
|
|||
};
|
||||
|
||||
render() {
|
||||
const { loading } = this.state;
|
||||
|
||||
return (
|
||||
<SafeAreaView testID='add-existing-channel-view'>
|
||||
<StatusBar />
|
||||
{this.renderList()}
|
||||
<Loading visible={loading} />
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import { dequal } from 'dequal';
|
|||
|
||||
import * as List from '../containers/List';
|
||||
import { TextInput } from '../containers/TextInput';
|
||||
import Loading from '../containers/Loading';
|
||||
import { sendLoadingEvent } from '../containers/Loading';
|
||||
import { createChannelRequest } from '../actions/createChannel';
|
||||
import { removeUser } from '../actions/selectedUsers';
|
||||
import KeyboardView from '../containers/KeyboardView';
|
||||
|
@ -169,13 +169,16 @@ class CreateChannelView extends React.Component<ICreateChannelViewProps, ICreate
|
|||
}
|
||||
|
||||
componentDidUpdate(prevProps: ICreateChannelViewProps) {
|
||||
const { createPublicChannelPermission, createPrivateChannelPermission } = this.props;
|
||||
const { createPublicChannelPermission, createPrivateChannelPermission, isFetching } = this.props;
|
||||
if (
|
||||
!dequal(createPublicChannelPermission, prevProps.createPublicChannelPermission) ||
|
||||
!dequal(createPrivateChannelPermission, prevProps.createPrivateChannelPermission)
|
||||
) {
|
||||
this.handleHasPermission();
|
||||
}
|
||||
if (isFetching !== prevProps.isFetching) {
|
||||
sendLoadingEvent({ visible: isFetching });
|
||||
}
|
||||
}
|
||||
|
||||
setHeader = () => {
|
||||
|
@ -368,7 +371,7 @@ class CreateChannelView extends React.Component<ICreateChannelViewProps, ICreate
|
|||
|
||||
render() {
|
||||
const { channelName, isTeam } = this.state;
|
||||
const { users, isFetching, theme } = this.props;
|
||||
const { users, theme } = this.props;
|
||||
const userCount = users.length;
|
||||
|
||||
return (
|
||||
|
@ -409,7 +412,6 @@ class CreateChannelView extends React.Component<ICreateChannelViewProps, ICreate
|
|||
</Text>
|
||||
</View>
|
||||
{this.renderInvitedList()}
|
||||
<Loading visible={isFetching} />
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
</KeyboardView>
|
||||
|
|
|
@ -3,7 +3,7 @@ import { connect } from 'react-redux';
|
|||
import { ScrollView, Switch, Text } from 'react-native';
|
||||
import { StackNavigationOptions } from '@react-navigation/stack';
|
||||
|
||||
import Loading from '../../containers/Loading';
|
||||
import { sendLoadingEvent } from '../../containers/Loading';
|
||||
import KeyboardView from '../../containers/KeyboardView';
|
||||
import scrollPersistTaps from '../../lib/methods/helpers/scrollPersistTaps';
|
||||
import I18n from '../../i18n';
|
||||
|
@ -52,8 +52,9 @@ class CreateChannelView extends React.Component<ICreateChannelViewProps, ICreate
|
|||
this.setHeader();
|
||||
}
|
||||
|
||||
if (!loading && loading !== prevProps.loading) {
|
||||
setTimeout(() => {
|
||||
if (loading !== prevProps.loading) {
|
||||
sendLoadingEvent({ visible: loading });
|
||||
if (!loading) {
|
||||
if (failure) {
|
||||
const msg = error.reason || I18n.t('There_was_an_error_while_action', { action: I18n.t('creating_discussion') });
|
||||
showErrorAlert(msg);
|
||||
|
@ -72,7 +73,7 @@ class CreateChannelView extends React.Component<ICreateChannelViewProps, ICreate
|
|||
};
|
||||
goRoom({ item, isMasterDetail });
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -146,7 +147,7 @@ class CreateChannelView extends React.Component<ICreateChannelViewProps, ICreate
|
|||
|
||||
render() {
|
||||
const { name, users, encrypted } = this.state;
|
||||
const { server, user, loading, blockUnauthenticatedAccess, theme, serverVersion } = this.props;
|
||||
const { server, user, blockUnauthenticatedAccess, theme, serverVersion } = this.props;
|
||||
return (
|
||||
<KeyboardView
|
||||
style={{ backgroundColor: themes[theme].auxiliaryBackground }}
|
||||
|
@ -189,7 +190,6 @@ class CreateChannelView extends React.Component<ICreateChannelViewProps, ICreate
|
|||
<Switch value={encrypted} onValueChange={this.onEncryptedChange} trackColor={SWITCH_TRACK_COLOR} />
|
||||
</>
|
||||
) : null}
|
||||
<Loading visible={loading} />
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
</KeyboardView>
|
||||
|
|
|
@ -11,7 +11,7 @@ import { Subscription } from 'rxjs';
|
|||
import { deleteRoom } from '../../actions/room';
|
||||
import { themes } from '../../lib/constants';
|
||||
import Avatar from '../../containers/Avatar';
|
||||
import Loading from '../../containers/Loading';
|
||||
import { sendLoadingEvent } from '../../containers/Loading';
|
||||
import SafeAreaView from '../../containers/SafeAreaView';
|
||||
import StatusBar from '../../containers/StatusBar';
|
||||
import { FormTextInput } from '../../containers/TextInput';
|
||||
|
@ -62,7 +62,6 @@ interface IRoomInfoEditViewState {
|
|||
announcement?: string;
|
||||
joinCode: string;
|
||||
nameError: any;
|
||||
saving: boolean;
|
||||
t: boolean;
|
||||
ro: boolean;
|
||||
reactWhenReadOnly?: boolean;
|
||||
|
@ -111,7 +110,6 @@ class RoomInfoEditView extends React.Component<IRoomInfoEditViewProps, IRoomInfo
|
|||
announcement: '',
|
||||
joinCode: '',
|
||||
nameError: {},
|
||||
saving: false,
|
||||
t: false,
|
||||
ro: false,
|
||||
reactWhenReadOnly: false,
|
||||
|
@ -269,7 +267,7 @@ class RoomInfoEditView extends React.Component<IRoomInfoEditViewProps, IRoomInfo
|
|||
avatar
|
||||
} = this.state;
|
||||
|
||||
this.setState({ saving: true });
|
||||
sendLoadingEvent({ visible: true });
|
||||
let error = false;
|
||||
|
||||
if (!this.formIsChanged()) {
|
||||
|
@ -339,7 +337,7 @@ class RoomInfoEditView extends React.Component<IRoomInfoEditViewProps, IRoomInfo
|
|||
log(e);
|
||||
}
|
||||
|
||||
await this.setState({ saving: false });
|
||||
sendLoadingEvent({ visible: false });
|
||||
setTimeout(() => {
|
||||
if (error) {
|
||||
logEvent(events.RI_EDIT_SAVE_F);
|
||||
|
@ -548,7 +546,6 @@ class RoomInfoEditView extends React.Component<IRoomInfoEditViewProps, IRoomInfo
|
|||
reactWhenReadOnly,
|
||||
room,
|
||||
joinCode,
|
||||
saving,
|
||||
permissions,
|
||||
archived,
|
||||
enableSysMes,
|
||||
|
@ -811,7 +808,6 @@ class RoomInfoEditView extends React.Component<IRoomInfoEditViewProps, IRoomInfo
|
|||
{I18n.t('DELETE')}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<Loading visible={saving} />
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
</KeyboardView>
|
||||
|
|
|
@ -62,6 +62,7 @@ class ListContainer extends React.Component<IListContainerProps, IListContainerS
|
|||
private mounted = false;
|
||||
private animated = false;
|
||||
private jumping = false;
|
||||
private cancelJump = false;
|
||||
private y = new Value(0);
|
||||
private onScroll = onScroll({ y: this.y });
|
||||
private unsubscribeFocus: () => void;
|
||||
|
@ -138,6 +139,7 @@ class ListContainer extends React.Component<IListContainerProps, IListContainerS
|
|||
console.countReset(`${this.constructor.name}.render calls`);
|
||||
}
|
||||
|
||||
// clears previous highlighted message timeout, if exists
|
||||
clearHighlightedMessageTimeout = () => {
|
||||
if (this.highlightedMessageTimeout) {
|
||||
clearTimeout(this.highlightedMessageTimeout);
|
||||
|
@ -276,40 +278,60 @@ class ListContainer extends React.Component<IListContainerProps, IListContainerS
|
|||
|
||||
jumpToMessage = (messageId: string) =>
|
||||
new Promise<void>(async resolve => {
|
||||
this.jumping = true;
|
||||
const { messages } = this.state;
|
||||
const { listRef } = this.props;
|
||||
|
||||
// if jump to message was cancelled, reset variables and stop
|
||||
if (this.cancelJump) {
|
||||
this.resetJumpToMessage();
|
||||
return resolve();
|
||||
}
|
||||
this.jumping = true;
|
||||
|
||||
// look for the message on the state
|
||||
const index = messages.findIndex(item => item.id === messageId);
|
||||
|
||||
// if found message, scroll to it
|
||||
if (index > -1) {
|
||||
listRef.current?.scrollToIndex({ index, viewPosition: 0.5, viewOffset: 100 });
|
||||
|
||||
// wait for scroll animation to finish
|
||||
await new Promise(res => setTimeout(res, 300));
|
||||
|
||||
// if message is not visible
|
||||
if (!this.viewableItems?.map(vi => vi.key).includes(messageId)) {
|
||||
if (!this.jumping) {
|
||||
return resolve();
|
||||
}
|
||||
await setTimeout(() => resolve(this.jumpToMessage(messageId)), 300);
|
||||
return;
|
||||
}
|
||||
// if message is visible, highlight it
|
||||
this.setState({ highlightedMessage: messageId });
|
||||
this.clearHighlightedMessageTimeout();
|
||||
// clears highlighted message after some time
|
||||
this.highlightedMessageTimeout = setTimeout(() => {
|
||||
this.setState({ highlightedMessage: null });
|
||||
}, 10000);
|
||||
await setTimeout(() => resolve(), 300);
|
||||
}, 5000);
|
||||
this.resetJumpToMessage();
|
||||
resolve();
|
||||
} else {
|
||||
listRef.current?.scrollToIndex({ index: messages.length - 1, animated: false });
|
||||
if (!this.jumping) {
|
||||
return resolve();
|
||||
}
|
||||
// if message not found, wait for scroll to top and then jump to message
|
||||
listRef.current?.scrollToIndex({ index: messages.length - 1, animated: true });
|
||||
await setTimeout(() => resolve(this.jumpToMessage(messageId)), 300);
|
||||
}
|
||||
});
|
||||
|
||||
// this.jumping is checked in between operations to make sure we're not stuck
|
||||
cancelJumpToMessage = () => {
|
||||
resetJumpToMessage = () => {
|
||||
this.cancelJump = false;
|
||||
this.jumping = false;
|
||||
};
|
||||
|
||||
cancelJumpToMessage = () => {
|
||||
if (this.jumping) {
|
||||
this.cancelJump = true;
|
||||
return;
|
||||
}
|
||||
this.resetJumpToMessage();
|
||||
};
|
||||
|
||||
jumpToBottom = () => {
|
||||
const { listRef } = this.props;
|
||||
listRef.current?.scrollToOffset({ offset: -100 });
|
||||
|
|
|
@ -42,7 +42,7 @@ import Navigation from '../../lib/navigation/appNavigation';
|
|||
import SafeAreaView from '../../containers/SafeAreaView';
|
||||
import { withDimensions } from '../../dimensions';
|
||||
import { takeInquiry, takeResume } from '../../ee/omnichannel/lib';
|
||||
import Loading from '../../containers/Loading';
|
||||
import { sendLoadingEvent } from '../../containers/Loading';
|
||||
import { goRoom, TGoRoomItem } from '../../lib/methods/helpers/goRoom';
|
||||
import getThreadName from '../../lib/methods/getThreadName';
|
||||
import getRoomInfo from '../../lib/methods/getRoomInfo';
|
||||
|
@ -115,7 +115,6 @@ const stateAttrsUpdate = [
|
|||
'reacting',
|
||||
'readOnly',
|
||||
'member',
|
||||
'showingBlockingLoader',
|
||||
'canForwardGuest',
|
||||
'canReturnQueue',
|
||||
'canViewCannedResponse'
|
||||
|
@ -198,7 +197,6 @@ interface IRoomViewState {
|
|||
selectedMessage?: TAnyMessageModel;
|
||||
canAutoTranslate: boolean;
|
||||
loading: boolean;
|
||||
showingBlockingLoader: boolean;
|
||||
editing: boolean;
|
||||
replying: boolean;
|
||||
replyWithMention: boolean;
|
||||
|
@ -273,7 +271,6 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
|
|||
selectedMessage,
|
||||
canAutoTranslate: false,
|
||||
loading: true,
|
||||
showingBlockingLoader: false,
|
||||
editing: false,
|
||||
replying: !!selectedMessage,
|
||||
replyWithMention: false,
|
||||
|
@ -940,25 +937,23 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
|
|||
return;
|
||||
}
|
||||
try {
|
||||
this.setState({ showingBlockingLoader: true });
|
||||
const parsedUrl = parse(messageUrl, true);
|
||||
const messageId = parsedUrl.query.msg;
|
||||
if (messageId) {
|
||||
await this.jumpToMessage(messageId);
|
||||
}
|
||||
this.setState({ showingBlockingLoader: false });
|
||||
} catch (e) {
|
||||
this.setState({ showingBlockingLoader: false });
|
||||
log(e);
|
||||
}
|
||||
};
|
||||
|
||||
jumpToMessage = async (messageId: string) => {
|
||||
try {
|
||||
this.setState({ showingBlockingLoader: true });
|
||||
sendLoadingEvent({ visible: true, onCancel: this.cancelJumpToMessage });
|
||||
const message = await RoomServices.getMessageInfo(messageId);
|
||||
|
||||
if (!message) {
|
||||
this.cancelJumpToMessage();
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -977,15 +972,19 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
|
|||
await loadSurroundingMessages({ messageId, rid: this.rid });
|
||||
}
|
||||
await Promise.race([this.list.current?.jumpToMessage(message.id), new Promise(res => setTimeout(res, 5000))]);
|
||||
this.list.current?.cancelJumpToMessage();
|
||||
this.cancelJumpToMessage();
|
||||
}
|
||||
} catch (e) {
|
||||
log(e);
|
||||
} finally {
|
||||
this.setState({ showingBlockingLoader: false });
|
||||
this.cancelJumpToMessage();
|
||||
}
|
||||
};
|
||||
|
||||
cancelJumpToMessage = () => {
|
||||
this.list.current?.cancelJumpToMessage();
|
||||
sendLoadingEvent({ visible: false });
|
||||
};
|
||||
|
||||
replyBroadcast = (message: IMessage) => {
|
||||
const { dispatch } = this.props;
|
||||
dispatch(replyBroadcast(message));
|
||||
|
@ -1137,10 +1136,12 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
|
|||
name = item.tmsg ?? '';
|
||||
jumpToMessageId = item.id;
|
||||
}
|
||||
sendLoadingEvent({ visible: true, onCancel: this.cancelJumpToMessage });
|
||||
if (!name) {
|
||||
const result = await this.getThreadName(item.tmid, jumpToMessageId);
|
||||
// test if there isn't a thread
|
||||
if (!result) {
|
||||
sendLoadingEvent({ visible: false });
|
||||
return;
|
||||
}
|
||||
name = result;
|
||||
|
@ -1484,7 +1485,7 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
|
|||
|
||||
render() {
|
||||
console.count(`${this.constructor.name}.render calls`);
|
||||
const { room, selectedMessage, loading, reacting, showingBlockingLoader } = this.state;
|
||||
const { room, selectedMessage, loading, reacting } = this.state;
|
||||
const { user, baseUrl, theme, navigation, Hide_System_Messages, width, height, serverVersion } = this.props;
|
||||
const { rid, t } = room;
|
||||
let sysMes;
|
||||
|
@ -1528,7 +1529,6 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
|
|||
/>
|
||||
<UploadProgress rid={rid} user={user} baseUrl={baseUrl} width={width} />
|
||||
<JoinCode ref={this.joinCode} onJoin={this.onJoin} rid={rid} t={t} theme={theme} />
|
||||
<Loading visible={showingBlockingLoader} />
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import { addUser, removeUser, reset } from '../actions/selectedUsers';
|
|||
import { themes } from '../lib/constants';
|
||||
import * as HeaderButton from '../containers/HeaderButton';
|
||||
import * as List from '../containers/List';
|
||||
import Loading from '../containers/Loading';
|
||||
import { sendLoadingEvent } from '../containers/Loading';
|
||||
import SafeAreaView from '../containers/SafeAreaView';
|
||||
import SearchBox from '../containers/SearchBox';
|
||||
import StatusBar from '../containers/StatusBar';
|
||||
|
@ -65,12 +65,15 @@ class SelectedUsersView extends React.Component<ISelectedUsersViewProps, ISelect
|
|||
}
|
||||
|
||||
componentDidUpdate(prevProps: ISelectedUsersViewProps) {
|
||||
const { users, loading } = this.props;
|
||||
if (this.isGroupChat()) {
|
||||
const { users } = this.props;
|
||||
if (prevProps.users.length !== users.length) {
|
||||
this.setHeader(users.length > 0);
|
||||
}
|
||||
}
|
||||
if (loading !== prevProps.loading) {
|
||||
sendLoadingEvent({ visible: loading });
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
@ -273,16 +276,14 @@ class SelectedUsersView extends React.Component<ISelectedUsersViewProps, ISelect
|
|||
);
|
||||
};
|
||||
|
||||
render = () => {
|
||||
const { loading } = this.props;
|
||||
render() {
|
||||
return (
|
||||
<SafeAreaView testID='select-users-view'>
|
||||
<StatusBar />
|
||||
{this.renderList()}
|
||||
<Loading visible={loading} />
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: IApplicationState) => ({
|
||||
|
|
|
@ -9,7 +9,7 @@ import { Q } from '@nozbe/watermelondb';
|
|||
import { InsideStackParamList } from '../../stacks/types';
|
||||
import { themes } from '../../lib/constants';
|
||||
import I18n from '../../i18n';
|
||||
import Loading from '../../containers/Loading';
|
||||
import { sendLoadingEvent } from '../../containers/Loading';
|
||||
import * as HeaderButton from '../../containers/HeaderButton';
|
||||
import { TSupportedThemes, withTheme } from '../../theme';
|
||||
import { FormTextInput } from '../../containers/TextInput';
|
||||
|
@ -207,6 +207,7 @@ class ShareView extends Component<IShareViewProps, IShareViewState> {
|
|||
// if it's share extension this should show loading
|
||||
if (this.isShareExtension) {
|
||||
this.setState({ loading: true });
|
||||
sendLoadingEvent({ visible: true });
|
||||
|
||||
// if it's not share extension this can close
|
||||
} else {
|
||||
|
@ -248,6 +249,7 @@ class ShareView extends Component<IShareViewProps, IShareViewState> {
|
|||
|
||||
// if it's share extension this should close
|
||||
if (this.isShareExtension) {
|
||||
sendLoadingEvent({ visible: false });
|
||||
ShareExtension.close();
|
||||
}
|
||||
};
|
||||
|
@ -346,7 +348,7 @@ class ShareView extends Component<IShareViewProps, IShareViewState> {
|
|||
|
||||
render() {
|
||||
console.count(`${this.constructor.name}.render calls`);
|
||||
const { readOnly, room, loading } = this.state;
|
||||
const { readOnly, room } = this.state;
|
||||
const { theme } = this.props;
|
||||
if (readOnly || isBlocked(room)) {
|
||||
return (
|
||||
|
@ -361,7 +363,6 @@ class ShareView extends Component<IShareViewProps, IShareViewState> {
|
|||
<SafeAreaView style={{ backgroundColor: themes[theme].backgroundColor }}>
|
||||
<StatusBar barStyle='light-content' backgroundColor={themes[theme].previewBackground} />
|
||||
{this.renderContent()}
|
||||
<Loading visible={loading} />
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import { useDispatch, useSelector } from 'react-redux';
|
|||
import { setUser } from '../../actions/login';
|
||||
import * as HeaderButton from '../../containers/HeaderButton';
|
||||
import * as List from '../../containers/List';
|
||||
import Loading from '../../containers/Loading';
|
||||
import { sendLoadingEvent } from '../../containers/Loading';
|
||||
import SafeAreaView from '../../containers/SafeAreaView';
|
||||
import StatusIcon from '../../containers/Status/Status';
|
||||
import { FormTextInput } from '../../containers/TextInput';
|
||||
|
@ -91,7 +91,6 @@ const StatusView = (): React.ReactElement => {
|
|||
);
|
||||
|
||||
const [statusText, setStatusText] = useState(user.statusText || '');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [status, setStatus] = useState(user.status);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
@ -120,7 +119,7 @@ const StatusView = (): React.ReactElement => {
|
|||
}, [statusText, status]);
|
||||
|
||||
const setCustomStatus = async (status: TUserStatus, statusText: string) => {
|
||||
setLoading(true);
|
||||
sendLoadingEvent({ visible: true });
|
||||
try {
|
||||
await Services.setUserStatus(status, statusText);
|
||||
dispatch(setUser({ statusText, status }));
|
||||
|
@ -135,7 +134,7 @@ const StatusView = (): React.ReactElement => {
|
|||
showErrorAlert(messageError);
|
||||
log(e);
|
||||
}
|
||||
setLoading(false);
|
||||
sendLoadingEvent({ visible: false });
|
||||
};
|
||||
|
||||
const statusType = Accounts_AllowInvisibleStatusOption ? STATUS : STATUS.filter(s => s.id !== 'offline');
|
||||
|
@ -163,7 +162,6 @@ const StatusView = (): React.ReactElement => {
|
|||
ListFooterComponent={List.Separator}
|
||||
ItemSeparatorComponent={List.Separator}
|
||||
/>
|
||||
<Loading visible={loading} />
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -49,10 +49,10 @@ async function waitForLoading() {
|
|||
await sleep(10000);
|
||||
return; // FIXME: Loading indicator doesn't animate properly on android
|
||||
}
|
||||
await waitFor(element(by.id('loading')))
|
||||
await waitFor(element(by.id('loading-image')))
|
||||
.toBeVisible()
|
||||
.withTimeout(5000);
|
||||
await waitFor(element(by.id('loading')))
|
||||
await waitFor(element(by.id('loading-image')))
|
||||
.toBeNotVisible()
|
||||
.withTimeout(10000);
|
||||
}
|
||||
|
@ -67,9 +67,10 @@ describe('Room', () => {
|
|||
|
||||
it('should jump to an old message and load its surroundings', async () => {
|
||||
await navigateToRoom('jumping');
|
||||
await waitFor(element(by[textMatcher]('300')))
|
||||
await waitFor(element(by[textMatcher]('295')))
|
||||
.toExist()
|
||||
.withTimeout(5000);
|
||||
await sleep(2000);
|
||||
await element(by[textMatcher]('1')).atIndex(0).tap();
|
||||
await waitForLoading();
|
||||
await waitFor(element(by[textMatcher]('1')).atIndex(0))
|
||||
|
@ -83,7 +84,7 @@ describe('Room', () => {
|
|||
.toExist()
|
||||
.withTimeout(15000);
|
||||
await element(by.id('nav-jump-to-bottom')).tap();
|
||||
await waitFor(element(by[textMatcher]('Quote first message')))
|
||||
await waitFor(element(by[textMatcher]("Go to jumping-thread's thread")))
|
||||
.toExist()
|
||||
.withTimeout(15000);
|
||||
await clearCache();
|
||||
|
@ -164,7 +165,7 @@ describe('Room', () => {
|
|||
await waitFor(element(by[textMatcher]('50')))
|
||||
.toExist()
|
||||
.withTimeout(5000);
|
||||
await element(by.id('room-view-messages')).atIndex(0).swipe('up', 'slow', 0.5);
|
||||
await element(by.id('room-view-messages')).atIndex(0).swipe('up', 'slow', 0.4);
|
||||
await waitFor(element(by[textMatcher]('Load Newer')))
|
||||
.toExist()
|
||||
.withTimeout(5000);
|
||||
|
@ -197,8 +198,9 @@ const expectThreadMessages = async message => {
|
|||
await waitFor(element(by.id('room-view-title-thread 1')))
|
||||
.toExist()
|
||||
.withTimeout(5000);
|
||||
await waitForLoading();
|
||||
await expect(element(by[textMatcher](message)).atIndex(0)).toExist();
|
||||
await waitFor(element(by[textMatcher](message)).atIndex(0))
|
||||
.toExist()
|
||||
.withTimeout(10000);
|
||||
await element(by[textMatcher](message)).atIndex(0).tap();
|
||||
};
|
||||
|
||||
|
|
|
@ -176,7 +176,7 @@
|
|||
"@typescript-eslint/eslint-plugin": "^4.28.3",
|
||||
"@typescript-eslint/parser": "^4.28.5",
|
||||
"axios": "0.27.2",
|
||||
"babel-jest": "^27.0.6",
|
||||
"babel-jest": "^28.1.3",
|
||||
"babel-plugin-transform-remove-console": "^6.9.4",
|
||||
"codecov": "^3.8.3",
|
||||
"detox": "19.7.0",
|
||||
|
@ -190,9 +190,9 @@
|
|||
"eslint-plugin-react-native": "4.0.0",
|
||||
"husky": "^6.0.0",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "^27.0.6",
|
||||
"jest-cli": "^27.0.6",
|
||||
"jest-expo": "^45.0.1",
|
||||
"jest": "^28.1.3",
|
||||
"jest-cli": "^28.1.3",
|
||||
"jest-expo": "^46.0.1",
|
||||
"metro-react-native-babel-preset": "^0.67.0",
|
||||
"mocha": "9.0.1",
|
||||
"otp.js": "1.2.0",
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
diff --git a/node_modules/react-native-reanimated/lib/reanimated2/jestUtils.js b/node_modules/react-native-reanimated/lib/reanimated2/jestUtils.js
|
||||
index 5ae42ec..273bbc0 100644
|
||||
--- a/node_modules/react-native-reanimated/lib/reanimated2/jestUtils.js
|
||||
+++ b/node_modules/react-native-reanimated/lib/reanimated2/jestUtils.js
|
||||
@@ -145,7 +145,8 @@ export const advanceAnimationByFrame = (count) => {
|
||||
jest.advanceTimersByTime(frameTime);
|
||||
};
|
||||
export const setUpTests = (userConfig = {}) => {
|
||||
- const expect = require('expect');
|
||||
+ // https://github.com/software-mansion/react-native-reanimated/issues/3215
|
||||
+ const { expect } = require('@jest/globals');
|
||||
require('setimmediate');
|
||||
frameTime = Math.round(1000 / config.fps);
|
||||
config = Object.assign(Object.assign({}, config), userConfig);
|
Loading…
Reference in New Issue