forked from Ivasoft/mattermost-mobile
Add connection banner (#6798)
* Add connection banner * Switch icon depending on whether it is connected or not * Clean timeout and change text * Handle optimistic approach * Piggyback server item fix * Use toMilliseconds util function * Set the websocket as disconnected when we are manually closing it * Do not hide banner when app state changes Co-authored-by: Daniel Espino <danielespino@MacBook-Pro-de-Daniel.local>
This commit is contained in:
committed by
GitHub
parent
a3489d9674
commit
88835ce142
200
app/components/connection_banner/connection_banner.tsx
Normal file
200
app/components/connection_banner/connection_banner.tsx
Normal file
@@ -0,0 +1,200 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {useNetInfo} from '@react-native-community/netinfo';
|
||||
import React, {useCallback, useEffect, useRef, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {
|
||||
Text,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import {ANNOUNCEMENT_BAR_HEIGHT} from '@constants/view';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useAppState} from '@hooks/device';
|
||||
import useDidUpdate from '@hooks/did_update';
|
||||
import {toMilliseconds} from '@utils/datetime';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
type Props = {
|
||||
isConnected: boolean;
|
||||
}
|
||||
|
||||
const getStyle = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
const bannerContainer = {
|
||||
flex: 1,
|
||||
paddingHorizontal: 10,
|
||||
overflow: 'hidden',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginHorizontal: 8,
|
||||
borderRadius: 7,
|
||||
};
|
||||
return {
|
||||
background: {
|
||||
backgroundColor: theme.sidebarBg,
|
||||
},
|
||||
bannerContainerNotConnected: {
|
||||
...bannerContainer,
|
||||
backgroundColor: theme.centerChannelColor,
|
||||
},
|
||||
bannerContainerConnected: {
|
||||
...bannerContainer,
|
||||
backgroundColor: theme.onlineIndicator,
|
||||
},
|
||||
wrapper: {
|
||||
flexDirection: 'row',
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
bannerTextContainer: {
|
||||
flex: 1,
|
||||
flexGrow: 1,
|
||||
marginRight: 5,
|
||||
textAlign: 'center',
|
||||
color: theme.centerChannelBg,
|
||||
},
|
||||
bannerText: {
|
||||
...typography('Body', 100, 'SemiBold'),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const clearTimeoutRef = (ref: React.MutableRefObject<NodeJS.Timeout | null | undefined>) => {
|
||||
if (ref.current) {
|
||||
clearTimeout(ref.current);
|
||||
ref.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
const TIME_TO_OPEN = toMilliseconds({seconds: 3});
|
||||
const TIME_TO_CLOSE = toMilliseconds({seconds: 1});
|
||||
|
||||
const ConnectionBanner = ({
|
||||
isConnected,
|
||||
}: Props) => {
|
||||
const intl = useIntl();
|
||||
const closeTimeout = useRef<NodeJS.Timeout | null>();
|
||||
const openTimeout = useRef<NodeJS.Timeout | null>();
|
||||
const height = useSharedValue(0);
|
||||
const theme = useTheme();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const style = getStyle(theme);
|
||||
const appState = useAppState();
|
||||
const netInfo = useNetInfo();
|
||||
|
||||
const openCallback = useCallback(() => {
|
||||
setVisible(true);
|
||||
clearTimeoutRef(openTimeout);
|
||||
}, []);
|
||||
|
||||
const closeCallback = useCallback(() => {
|
||||
setVisible(false);
|
||||
clearTimeoutRef(closeTimeout);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isConnected) {
|
||||
openTimeout.current = setTimeout(openCallback, TIME_TO_OPEN);
|
||||
}
|
||||
return () => {
|
||||
clearTimeoutRef(openTimeout);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useDidUpdate(() => {
|
||||
if (isConnected) {
|
||||
if (visible) {
|
||||
if (!closeTimeout.current) {
|
||||
closeTimeout.current = setTimeout(closeCallback, TIME_TO_CLOSE);
|
||||
}
|
||||
} else {
|
||||
clearTimeoutRef(openTimeout);
|
||||
}
|
||||
} else if (visible) {
|
||||
clearTimeoutRef(closeTimeout);
|
||||
} else if (appState === 'active') {
|
||||
setVisible(true);
|
||||
}
|
||||
}, [isConnected]);
|
||||
|
||||
useEffect(() => {
|
||||
if (appState === 'active') {
|
||||
if (!isConnected && !visible) {
|
||||
if (!openTimeout.current) {
|
||||
openTimeout.current = setTimeout(openCallback, TIME_TO_OPEN);
|
||||
}
|
||||
}
|
||||
if (isConnected && visible) {
|
||||
if (!closeTimeout.current) {
|
||||
closeTimeout.current = setTimeout(closeCallback, TIME_TO_CLOSE);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
clearTimeoutRef(openTimeout);
|
||||
clearTimeoutRef(closeTimeout);
|
||||
}
|
||||
}, [appState]);
|
||||
|
||||
useEffect(() => {
|
||||
height.value = withTiming(visible ? ANNOUNCEMENT_BAR_HEIGHT : 0, {
|
||||
duration: 200,
|
||||
});
|
||||
}, [visible]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
clearTimeoutRef(closeTimeout);
|
||||
};
|
||||
});
|
||||
|
||||
const bannerStyle = useAnimatedStyle(() => ({
|
||||
height: height.value,
|
||||
}));
|
||||
|
||||
let text;
|
||||
if (isConnected) {
|
||||
text = intl.formatMessage({id: 'connection_banner.connected', defaultMessage: 'Connection restored'});
|
||||
} else if (netInfo.isInternetReachable) {
|
||||
text = intl.formatMessage({id: 'connection_banner.not_reachable', defaultMessage: 'The server is not reachable'});
|
||||
} else {
|
||||
text = intl.formatMessage({id: 'connection_banner.not_connected', defaultMessage: 'No internet connection'});
|
||||
}
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
style={[style.background, bannerStyle]}
|
||||
>
|
||||
<View
|
||||
style={isConnected ? style.bannerContainerConnected : style.bannerContainerNotConnected}
|
||||
>
|
||||
{visible &&
|
||||
<View
|
||||
style={style.wrapper}
|
||||
>
|
||||
<Text
|
||||
style={style.bannerTextContainer}
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
>
|
||||
<CompassIcon
|
||||
color={theme.centerChannelBg}
|
||||
name={isConnected ? 'check' : 'information-outline'}
|
||||
size={18}
|
||||
/>
|
||||
{' '}
|
||||
<Text style={style.bannerText}>
|
||||
{text}
|
||||
</Text>
|
||||
</Text>
|
||||
</View>
|
||||
}
|
||||
</View>
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConnectionBanner;
|
||||
15
app/components/connection_banner/index.ts
Normal file
15
app/components/connection_banner/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
|
||||
import {withServerUrl} from '@context/server';
|
||||
import websocket_manager from '@managers/websocket_manager';
|
||||
|
||||
import ConnectionBanner from './connection_banner';
|
||||
|
||||
const enhanced = withObservables(['serverUrl'], ({serverUrl}: {serverUrl: string}) => ({
|
||||
isConnected: websocket_manager.observeConnected(serverUrl),
|
||||
}));
|
||||
|
||||
export default withServerUrl(enhanced(ConnectionBanner));
|
||||
@@ -5,6 +5,8 @@ import NetInfo, {NetInfoState} from '@react-native-community/netinfo';
|
||||
import {debounce, DebouncedFunc} from 'lodash';
|
||||
import {AppState, AppStateStatus} from 'react-native';
|
||||
import BackgroundTimer from 'react-native-background-timer';
|
||||
import {BehaviorSubject} from 'rxjs';
|
||||
import {distinctUntilChanged} from 'rxjs/operators';
|
||||
|
||||
import {setCurrentUserStatusOffline} from '@actions/local/user';
|
||||
import {fetchStatusByIds} from '@actions/remote/user';
|
||||
@@ -22,6 +24,8 @@ const WAIT_TO_CLOSE = toMilliseconds({seconds: 15});
|
||||
const WAIT_UNTIL_NEXT = toMilliseconds({seconds: 20});
|
||||
|
||||
class WebsocketManager {
|
||||
private connectedSubjects: {[serverUrl: string]: BehaviorSubject<boolean>} = {};
|
||||
|
||||
private clients: Record<string, WebSocketClient> = {};
|
||||
private connectionTimerIDs: Record<string, DebouncedFunc<() => void>> = {};
|
||||
private isBackgroundTimerRunning = false;
|
||||
@@ -64,6 +68,9 @@ class WebsocketManager {
|
||||
this.connectionTimerIDs[serverUrl].cancel();
|
||||
}
|
||||
delete this.clients[serverUrl];
|
||||
|
||||
this.getConnectedSubject(serverUrl).next(false);
|
||||
delete this.connectedSubjects[serverUrl];
|
||||
};
|
||||
|
||||
public createClient = (serverUrl: string, bearerToken: string, storedLastDisconnect = 0) => {
|
||||
@@ -85,9 +92,11 @@ class WebsocketManager {
|
||||
};
|
||||
|
||||
public closeAll = () => {
|
||||
for (const client of Object.values(this.clients)) {
|
||||
for (const url of Object.keys(this.clients)) {
|
||||
const client = this.clients[url];
|
||||
if (client.isConnected()) {
|
||||
client.close(true);
|
||||
this.getConnectedSubject(url).next(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -109,6 +118,20 @@ class WebsocketManager {
|
||||
return this.clients[serverUrl]?.isConnected();
|
||||
};
|
||||
|
||||
public observeConnected = (serverUrl: string) => {
|
||||
return this.getConnectedSubject(serverUrl).asObservable().pipe(
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
};
|
||||
|
||||
private getConnectedSubject = (serverUrl: string) => {
|
||||
if (!this.connectedSubjects[serverUrl]) {
|
||||
this.connectedSubjects[serverUrl] = new BehaviorSubject(this.isConnected(serverUrl));
|
||||
}
|
||||
|
||||
return this.connectedSubjects[serverUrl];
|
||||
};
|
||||
|
||||
private cancelAllConnections = () => {
|
||||
for (const url in this.connectionTimerIDs) {
|
||||
if (this.connectionTimerIDs[url]) {
|
||||
@@ -130,11 +153,13 @@ class WebsocketManager {
|
||||
private onFirstConnect = (serverUrl: string) => {
|
||||
this.startPeriodicStatusUpdates(serverUrl);
|
||||
handleFirstConnect(serverUrl);
|
||||
this.getConnectedSubject(serverUrl).next(true);
|
||||
};
|
||||
|
||||
private onReconnect = (serverUrl: string) => {
|
||||
this.startPeriodicStatusUpdates(serverUrl);
|
||||
handleReconnect(serverUrl);
|
||||
this.getConnectedSubject(serverUrl).next(true);
|
||||
};
|
||||
|
||||
private onWebsocketClose = async (serverUrl: string, connectFailCount: number, lastDisconnect: number) => {
|
||||
@@ -143,6 +168,7 @@ class WebsocketManager {
|
||||
await handleClose(serverUrl, lastDisconnect);
|
||||
|
||||
this.stopPeriodicStatusUpdates(serverUrl);
|
||||
this.getConnectedSubject(serverUrl).next(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -277,7 +277,7 @@ export const getWebSocketLastDisconnected = async (serverDatabase: Database) =>
|
||||
}
|
||||
};
|
||||
|
||||
export const observeWebsocket = (database: Database) => {
|
||||
export const observeWebsocketLastDisconnected = (database: Database) => {
|
||||
return querySystemValue(database, SYSTEM_IDENTIFIERS.WEBSOCKET).observe().pipe(
|
||||
switchMap((result) => (result.length ? result[0].observe() : of$({value: '0'}))),
|
||||
switchMap((model) => of$(parseInt(model.value || 0, 10) || 0)),
|
||||
|
||||
@@ -10,6 +10,7 @@ import Animated, {useAnimatedStyle, withTiming} from 'react-native-reanimated';
|
||||
import {Edge, SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context';
|
||||
|
||||
import AnnouncementBanner from '@components/announcement_banner';
|
||||
import ConnectionBanner from '@components/connection_banner';
|
||||
import FreezeScreen from '@components/freeze_screen';
|
||||
import TeamSidebar from '@components/team_sidebar';
|
||||
import {Navigation as NavigationConstants, Screens} from '@constants';
|
||||
@@ -166,6 +167,7 @@ const ChannelListScreen = (props: ChannelProps) => {
|
||||
edges={edges}
|
||||
testID='channel_list.screen'
|
||||
>
|
||||
<ConnectionBanner/>
|
||||
{props.isLicensed &&
|
||||
<AnnouncementBanner/>
|
||||
}
|
||||
|
||||
@@ -157,7 +157,7 @@ const ServerItem = ({
|
||||
const viewRef = useRef<View>(null);
|
||||
const [showTutorial, setShowTutorial] = useState(false);
|
||||
const [itemBounds, setItemBounds] = useState<TutorialItemBounds>({startX: 0, startY: 0, endX: 0, endY: 0});
|
||||
const database = DatabaseManager.serverDatabases[server.url]?.database;
|
||||
|
||||
let displayName = server.displayName;
|
||||
|
||||
if (server.url === server.displayName) {
|
||||
@@ -451,9 +451,9 @@ const ServerItem = ({
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{Boolean(database) && server.lastActiveAt > 0 &&
|
||||
{server.lastActiveAt > 0 &&
|
||||
<WebSocket
|
||||
database={database!}
|
||||
serverUrl={server.url}
|
||||
/>
|
||||
}
|
||||
{showTutorial &&
|
||||
|
||||
@@ -2,19 +2,13 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import withObservables from '@nozbe/with-observables';
|
||||
import {of as of$} from 'rxjs';
|
||||
import {switchMap} from 'rxjs/operators';
|
||||
|
||||
import {observeWebsocket} from '@queries/servers/system';
|
||||
import WebsocketManager from '@managers/websocket_manager';
|
||||
|
||||
import WebSocket from './websocket';
|
||||
|
||||
import type {WithDatabaseArgs} from '@typings/database/database';
|
||||
|
||||
const enhanced = withObservables([], ({database}: WithDatabaseArgs) => ({
|
||||
isConnected: observeWebsocket(database).pipe(
|
||||
switchMap((value) => of$(value > 0)),
|
||||
),
|
||||
const enhanced = withObservables(['serverUrl'], ({serverUrl}: {serverUrl: string}) => ({
|
||||
isConnected: WebsocketManager.observeConnected(serverUrl),
|
||||
}));
|
||||
|
||||
export default enhanced(WebSocket);
|
||||
|
||||
@@ -31,7 +31,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
const WebSocket = ({isConnected}: Props) => {
|
||||
const theme = useTheme();
|
||||
|
||||
if (!isConnected) {
|
||||
if (isConnected) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -202,6 +202,9 @@
|
||||
"combined_system_message.removed_from_team.one_you": "You were **removed from the team**.",
|
||||
"combined_system_message.removed_from_team.two": "{firstUser} and {secondUser} were **removed from the team**.",
|
||||
"combined_system_message.you": "You",
|
||||
"connection_banner.connected": "Connection restored",
|
||||
"connection_banner.not_connected": "No internet connection",
|
||||
"connection_banner.not_reachable": "The server is not reachable",
|
||||
"create_direct_message.title": "Create Direct Message",
|
||||
"create_post.deactivated": "You are viewing an archived channel with a deactivated user.",
|
||||
"create_post.thread_reply": "Reply to this thread...",
|
||||
|
||||
Reference in New Issue
Block a user