mirror of
https://github.com/samuelthomas2774/nxapi.git
synced 2026-03-21 18:04:10 -05:00
Handle errors loading data in the main window
This commit is contained in:
parent
e5c54613d0
commit
bb606cfa7b
7
src/app/browser/components/icons/util.ts
Normal file
7
src/app/browser/components/icons/util.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import React from 'react';
|
||||
|
||||
export const svg_styles: React.CSSProperties = {
|
||||
height: '1em',
|
||||
width: 'auto',
|
||||
fill: 'currentColor',
|
||||
};
|
||||
9
src/app/browser/components/icons/warning.tsx
Normal file
9
src/app/browser/components/icons/warning.tsx
Normal 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);
|
||||
|
|
@ -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} />
|
||||
|
||||
|
|
|
|||
|
|
@ -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} />)}
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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} />)}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user