Handle errors loading data in the main window

This commit is contained in:
Samuel Elliott 2022-06-30 15:57:12 +01:00
parent e5c54613d0
commit bb606cfa7b
No known key found for this signature in database
GPG Key ID: 8420C7CDE43DC4D6
9 changed files with 116 additions and 21 deletions

View File

@ -0,0 +1,7 @@
import React from 'react';
export const svg_styles: React.CSSProperties = {
height: '1em',
width: 'auto',
fill: 'currentColor',
};

View File

@ -0,0 +1,9 @@
import React from 'react';
import { Platform, Text } from 'react-native';
import { svg_styles } from './util.js';
const IconWeb = React.memo(() => <Text>
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512" style={svg_styles}><title>ionicons-v5-r</title><path d="M449.07,399.08,278.64,82.58c-12.08-22.44-44.26-22.44-56.35,0L51.87,399.08A32,32,0,0,0,80,446.25H420.89A32,32,0,0,0,449.07,399.08Zm-198.6-1.83a20,20,0,1,1,20-20A20,20,0,0,1,250.47,397.25ZM272.19,196.1l-5.74,122a16,16,0,0,1-32,0l-5.74-121.95v0a21.73,21.73,0,0,1,21.5-22.69h.21a21.74,21.74,0,0,1,21.73,22.7Z"/></svg>
</Text>);
export default Platform.OS === 'web' ? IconWeb : React.memo(() => null);

View File

@ -11,6 +11,7 @@ export default function Event(props: {
user: User;
event: ActiveEvent;
loading?: boolean;
error?: Error;
}) {
const theme = useColourScheme() === 'light' ? light : dark;
const accent_colour = useAccentColour();
@ -18,7 +19,7 @@ export default function Event(props: {
const event_members = props.event.members.filter(m => m.isPlaying).length;
const voip_members = props.event.members.filter(m => m.isJoinedVoip).length;
return <Section title="Voice chat" loading={props.loading}>
return <Section title="Voice chat" loading={props.loading} error={props.error}>
<View style={styles.content}>
<Image source={{uri: props.event.imageUri, width: 100, height: 100}} style={styles.image} />

View File

@ -10,6 +10,7 @@ export default function Friends(props: {
user: User;
friends: Friend[];
loading?: boolean;
error?: Error;
}) {
const theme = useColourScheme() === 'light' ? light : dark;
@ -23,7 +24,7 @@ export default function Friends(props: {
onContextMenu={onFriendCodeContextMenu}
>SW-{props.user.nso!.nsoAccount.user.links.friendCode.id}</Text>;
return <Section title="Friends" loading={props.loading}>
return <Section title="Friends" loading={props.loading} error={props.error}>
{props.friends.length ? <ScrollView horizontal>
<View style={styles.content}>
{props.friends.map(f => <Friend key={f.nsaId} friend={f} user={props.user} />)}

View File

@ -1,5 +1,5 @@
import React, { useCallback, useEffect } from 'react';
import { ActivityIndicator, StyleSheet, Text, View } from 'react-native';
import { ActivityIndicator, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import ipc, { events } from '../ipc.js';
import { RequestState, useAccentColour, useAsync, useColourScheme, useEventListener, User } from '../util.js';
import Friends from './friends.js';
@ -9,20 +9,22 @@ import Section from './section.js';
import { TEXT_COLOUR_DARK, TEXT_COLOUR_LIGHT } from '../constants.js';
import SetupDiscordPresence from './discord-setup.js';
import { Button } from '../components/index.js';
import { hrlist } from '../../../util/misc.js';
export default function Main(props: {
user: User;
autoRefresh?: number;
}) {
const theme = useColourScheme() === 'light' ? light : dark;
const accent_colour = useAccentColour();
const [announcements, , announcements_state] = useAsync(useCallback(() => props.user.nsotoken ?
const [announcements, announcements_error, announcements_state] = useAsync(useCallback(() => props.user.nsotoken ?
ipc.getNsoAnnouncements(props.user.nsotoken) : Promise.resolve(null), [ipc, props.user.nsotoken]));
const [friends, , friends_state, forceRefreshFriends] = useAsync(useCallback(() => props.user.nsotoken ?
const [friends, friends_error, friends_state, forceRefreshFriends] = useAsync(useCallback(() => props.user.nsotoken ?
ipc.getNsoFriends(props.user.nsotoken) : Promise.resolve(null), [ipc, props.user.nsotoken]));
const [webservices, , webservices_state, forceRefreshWebServices] = useAsync(useCallback(() => props.user.nsotoken ?
const [webservices, webservices_error, webservices_state, forceRefreshWebServices] = useAsync(useCallback(() => props.user.nsotoken ?
ipc.getNsoWebServices(props.user.nsotoken) : Promise.resolve(null), [ipc, props.user.nsotoken]));
const [active_event, , active_event_state, forceRefreshActiveEvent] = useAsync(useCallback(() => props.user.nsotoken ?
const [active_event, active_event_error, active_event_state, forceRefreshActiveEvent] = useAsync(useCallback(() => props.user.nsotoken ?
ipc.getNsoActiveEvent(props.user.nsotoken) : Promise.resolve(null), [ipc, props.user.nsotoken]));
const loading = announcements_state === RequestState.LOADING ||
@ -41,10 +43,37 @@ export default function Main(props: {
useEventListener(events, 'window:refresh', refresh, []);
if (loading && (!announcements || !friends || !webservices || !active_event)) {
return <View style={styles.loading}>
<ActivityIndicator size="large" color={'#' + accent_colour} />
</View>;
const showErrorDetails = useCallback(() => {
if (friends_error) alert(friends_error.stack ?? friends_error.message);
if (webservices_error) alert(webservices_error.stack ?? webservices_error.message);
if (active_event_error) alert(active_event_error.stack ?? active_event_error.message);
}, [friends_error, webservices_error, active_event_error]);
if (!announcements || !friends || !webservices || !active_event) {
if (loading) {
return <View style={styles.loading}>
<ActivityIndicator size="large" color={'#' + accent_colour} />
</View>;
}
if (friends_error || webservices_error || active_event_error) {
const errors = [];
if (friends_error) errors.push('friends');
if (webservices_error) errors.push('game-specific services');
if (active_event_error) errors.push('voice chat');
const errors_text = hrlist(errors);
return <View style={styles.error}>
<Text style={[styles.errorHeader, theme.text]}>Error loading data</Text>
<Text style={[styles.errorMessage, theme.text]}>An error occured while loading {errors_text} data.</Text>
<View style={styles.errorActions}>
<Button title="Retry" onPress={refresh} color={'#' + accent_colour} primary />
<TouchableOpacity onPress={showErrorDetails} style={styles.errorViewDetailsTouchable}>
<Text style={theme.text}>View details</Text>
</TouchableOpacity>
</View>
</View>;
}
}
return <View>
@ -52,11 +81,11 @@ export default function Main(props: {
{props.user.nso ? <SetupDiscordPresence user={props.user} friends={friends} /> : null}
{props.user.nso && friends ? <Friends user={props.user} friends={friends}
loading={friends_state === RequestState.LOADING} /> : null}
loading={friends_state === RequestState.LOADING} error={friends_error ?? undefined} /> : null}
{props.user.nso && webservices ? <WebServices user={props.user} webservices={webservices}
loading={webservices_state === RequestState.LOADING} /> : null}
loading={webservices_state === RequestState.LOADING} error={webservices_error ?? undefined} /> : null}
{props.user.nso && active_event && 'id' in active_event ? <Event user={props.user} event={active_event}
loading={active_event_state === RequestState.LOADING} /> : null}
loading={active_event_state === RequestState.LOADING} error={active_event_error ?? undefined} /> : null}
</View>;
}
@ -103,6 +132,28 @@ const styles = StyleSheet.create({
flexDirection: 'row',
justifyContent: 'center',
},
error: {
flex: 1,
paddingVertical: 16,
paddingHorizontal: 20,
justifyContent: 'center',
},
errorHeader: {
marginBottom: 16,
fontSize: 16,
textAlign: 'center',
},
errorMessage: {
marginBottom: 16,
textAlign: 'center',
},
errorActions: {
alignItems: 'center',
},
errorViewDetailsTouchable: {
marginTop: 10,
},
});
const light = StyleSheet.create({

View File

@ -1,27 +1,38 @@
import React from 'react';
import { ActivityIndicator, StyleSheet, Text, View } from 'react-native';
import React, { useCallback } from 'react';
import { ActivityIndicator, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { useAccentColour, useColourScheme } from '../util.js';
import { BORDER_COLOUR_LIGHT, BORDER_COLOUR_SECONDARY_DARK, TEXT_COLOUR_DARK, TEXT_COLOUR_LIGHT } from '../constants.js';
import ipc from '../ipc.js';
import Warning from '../components/icons/warning.js';
export default function Section(props: React.PropsWithChildren<{
title: string;
loading?: boolean;
error?: Error;
}>) {
const theme = useColourScheme() === 'light' ? light : dark;
const accent_colour = useAccentColour();
const showErrorDetails = useCallback(() => {
alert(props.error);
}, [props.error]);
return <View style={[styles.container, theme.container]}>
<View style={styles.header}>
<Text style={[styles.headerText, theme.text]}>{props.title}</Text>
{props.loading ? <ActivityIndicator style={styles.activityIndicator} size={14}
color={'#' + accent_colour} /> : null}
{props.loading ? <ActivityIndicator style={styles.activityIndicator} size={HEADER_SIZE}
color={'#' + accent_colour} /> :
props.error ? <TouchableOpacity onPress={showErrorDetails} style={styles.iconTouchable}>
<Text style={[styles.icon, {color: '#' + accent_colour}]}><Warning /></Text>
</TouchableOpacity> : null}
</View>
{props.children}
</View>;
}
const HEADER_SIZE = ipc.platform === 'win32' ? 24 : 14;
const styles = StyleSheet.create({
container: {
marginBottom: ipc.platform === 'win32' ? 10 : 0,
@ -34,11 +45,17 @@ const styles = StyleSheet.create({
},
headerText: {
flex: 1,
fontSize: ipc.platform === 'win32' ? 24 : 14,
fontSize: HEADER_SIZE,
},
activityIndicator: {
marginLeft: 10,
},
iconTouchable: {
marginLeft: 10,
},
icon: {
fontSize: HEADER_SIZE,
},
});
const light = StyleSheet.create({

View File

@ -10,10 +10,11 @@ export default function WebServices(props: {
user: User;
webservices: WebService[];
loading?: boolean;
error?: Error;
}) {
if (!props.webservices.length) return null;
return <Section title="Game-specific services" loading={props.loading}>
return <Section title="Game-specific services" loading={props.loading} error={props.error}>
<ScrollView horizontal>
<View style={styles.content}>
{props.webservices.map(g => <WebService key={g.id} webservice={g} token={props.user.nsotoken} />)}

View File

@ -6,7 +6,7 @@ import openWebService, { WebServiceIpc } from './webservices.js';
import { createWindow, getWindowConfiguration } from './windows.js';
import { DiscordPresenceConfiguration, DiscordPresenceSource, WindowType } from '../common/types.js';
import { CurrentUser, Friend, Game, PresenceState, WebService } from '../../api/coral-types.js';
import { addNsoAccount, addPctlAccount, askAddNsoAccount, askAddPctlAccount, AuthoriseError } from './na-auth.js';
import { askAddNsoAccount, askAddPctlAccount } from './na-auth.js';
import { App } from './index.js';
import { NintendoAccountUser } from '../../api/na.js';
import { hrduration } from '../../util/misc.js';

View File

@ -19,6 +19,14 @@ export function hrduration(duration: number, short = false) {
}
}
export function hrlist(items: string[]) {
if (!items.length) throw new Error('list must not be empty');
if (items.length === 1) return items[0];
const last = items[items.length - 1];
return items.slice(0, -1).join(', ') + ' and ' + last;
}
export function timeoutSignal(ms = 10 * 1000) {
const controller = new AbortController();