Fix personal profile failing to load if no storage sources are available

This commit is contained in:
Nutcake 2023-05-17 08:10:52 +02:00
parent b805157f89
commit 839bc541d4
6 changed files with 126 additions and 90 deletions

View file

@ -47,17 +47,11 @@ class Aux {
static String neosDbToHttp(String? neosdb) { static String neosDbToHttp(String? neosdb) {
if (neosdb == null || neosdb.isEmpty) return ""; if (neosdb == null || neosdb.isEmpty) return "";
if (neosdb.startsWith("http")) return neosdb; if (neosdb.startsWith("http")) return neosdb;
final fullUri = neosdb.replaceFirst("neosdb:///", Config.neosCdnUrl); final filename = p.basenameWithoutExtension(neosdb);
final lastPeriodIndex = fullUri.lastIndexOf("."); return "${Config.neosCdnUrl}$filename";
if (lastPeriodIndex != -1 && fullUri.length - lastPeriodIndex < 8) {
// I feel like 8 is a good maximum for file extension length? Can neosdb Uris even come without file extensions?
return fullUri.substring(0, lastPeriodIndex);
}
return fullUri;
} }
} }
extension Unique<E, Id> on List<E> { extension Unique<E, Id> on List<E> {
List<E> unique([Id Function(E element)? id, bool inplace = true]) { List<E> unique([Id Function(E element)? id, bool inplace = true]) {
final ids = <Id>{}; final ids = <Id>{};
@ -67,7 +61,7 @@ extension Unique<E, Id> on List<E> {
} }
} }
extension Strip on String { extension StringX on String {
String stripHtml() { String stripHtml() {
final document = htmlparser.parse(this); final document = htmlparser.parse(this);
return htmlparser.parse(document.body?.text).documentElement?.text ?? ""; return htmlparser.parse(document.body?.text).documentElement?.text ?? "";
@ -76,6 +70,8 @@ extension Strip on String {
// This won't be accurate since userIds can't contain certain characters that usernames can // This won't be accurate since userIds can't contain certain characters that usernames can
// but it's fine for just having a name to display // but it's fine for just having a name to display
String stripUid() => startsWith("U-") ? substring(2) : this; String stripUid() => startsWith("U-") ? substring(2) : this;
String? get asNullable => isEmpty ? null : this;
} }
extension Format on Duration { extension Format on Duration {

View file

@ -118,19 +118,20 @@ class ApiClient {
} }
static void checkResponse(http.Response response) { static void checkResponse(http.Response response) {
final error = "(${response.statusCode}${kDebugMode ? "|${response.body}" : ""})";
if (response.statusCode == 429) { if (response.statusCode == 429) {
throw "Sorry, you are being rate limited"; throw "Sorry, you are being rate limited. $error";
} }
if (response.statusCode == 403) { if (response.statusCode == 403) {
tryCachedLogin(); tryCachedLogin();
// TODO: Show the login screen again if cached login was unsuccessful. // TODO: Show the login screen again if cached login was unsuccessful.
throw "You are not authorized to do that"; throw "You are not authorized to do that. $error";
} }
if (response.statusCode == 500) { if (response.statusCode == 500) {
throw "Internal server error"; throw "Internal server error. $error";
} }
if (response.statusCode != 200) { if (response.statusCode >= 300) {
throw "Unknown Error: ${response.statusCode}${kDebugMode ? "|${response.body}" : ""}"; throw "Unknown Error. $error";
} }
} }

View file

@ -10,6 +10,7 @@ class PersonalProfile {
final String? publicBanType; final String? publicBanType;
final List<StorageQuotas> storageQuotas; final List<StorageQuotas> storageQuotas;
final Map<String, int> quotaBytesSource; final Map<String, int> quotaBytesSource;
final int quotaBytes;
final int usedBytes; final int usedBytes;
final bool twoFactor; final bool twoFactor;
final bool isPatreonSupporter; final bool isPatreonSupporter;
@ -17,8 +18,8 @@ class PersonalProfile {
PersonalProfile({ PersonalProfile({
required this.id, required this.username, required this.email, required this.publicBanExpiration, required this.id, required this.username, required this.email, required this.publicBanExpiration,
required this.publicBanType, required this.storageQuotas, required this.quotaBytesSource, required this.usedBytes, required this.publicBanType, required this.storageQuotas, required this.quotaBytesSource, required this.quotaBytes,
required this.twoFactor, required this.isPatreonSupporter, required this.userProfile, required this.usedBytes, required this.twoFactor, required this.isPatreonSupporter, required this.userProfile,
}); });
factory PersonalProfile.fromMap(Map map) { factory PersonalProfile.fromMap(Map map) {
@ -28,17 +29,15 @@ class PersonalProfile {
email: map["email"] ?? "", email: map["email"] ?? "",
publicBanExpiration: DateTime.tryParse(map["publicBanExpiration"] ?? ""), publicBanExpiration: DateTime.tryParse(map["publicBanExpiration"] ?? ""),
publicBanType: map["publicBanType"], publicBanType: map["publicBanType"],
storageQuotas: (map["storageQuotas"] as List).map((e) => StorageQuotas.fromMap(e)).toList(), storageQuotas: (map["storageQuotas"] as List? ?? []).map((e) => StorageQuotas.fromMap(e)).toList(),
quotaBytesSource: (map["quotaBytesSources"] as Map).map((key, value) => MapEntry(key, value as int)), quotaBytesSource: (map["quotaBytesSources"] as Map? ?? {}).map((key, value) => MapEntry(key, value as int)),
quotaBytes: map["quotaBytes"] ?? 0,
usedBytes: map["usedBytes"] ?? 0, usedBytes: map["usedBytes"] ?? 0,
twoFactor: map["2fa_login"] ?? false, twoFactor: map["2fa_login"] ?? false,
isPatreonSupporter: map["patreonData"]?["isPatreonSupporter"] ?? false, isPatreonSupporter: map["patreonData"]?["isPatreonSupporter"] ?? false,
userProfile: UserProfile.fromMap(map["profile"]), userProfile: UserProfile.fromMap(map["profile"]),
); );
} }
int get maxBytes => (quotaBytesSource.values.maxOrNull ?? 0)
+ (storageQuotas.isEmpty ? 0 : storageQuotas.map((e) => e.bytes).reduce((value, element) => value + element));
} }
class StorageQuotas { class StorageQuotas {

View file

@ -11,7 +11,7 @@ class DefaultErrorWidget extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 64, vertical: 128,), padding: const EdgeInsets.all(64),
child: Center( child: Center(
child: Column( child: Column(
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,

View file

@ -32,7 +32,6 @@ class FriendsList extends StatefulWidget {
} }
class _FriendsListState extends State<FriendsList> { class _FriendsListState extends State<FriendsList> {
Future<PersonalProfile>? _userProfileFuture;
Future<UserStatus>? _userStatusFuture; Future<UserStatus>? _userStatusFuture;
ClientHolder? _clientHolder; ClientHolder? _clientHolder;
String _searchFilter = ""; String _searchFilter = "";
@ -44,7 +43,6 @@ class _FriendsListState extends State<FriendsList> {
if (_clientHolder != clientHolder) { if (_clientHolder != clientHolder) {
_clientHolder = clientHolder; _clientHolder = clientHolder;
final apiClient = _clientHolder!.apiClient; final apiClient = _clientHolder!.apiClient;
_userProfileFuture = UserApi.getPersonalProfile(apiClient);
_refreshUserStatus(); _refreshUserStatus();
} }
} }
@ -137,7 +135,6 @@ class _FriendsListState extends State<FriendsList> {
_userStatusFuture = UserApi.getUserStatus(clientHolder.apiClient, userId: clientHolder.apiClient _userStatusFuture = UserApi.getUserStatus(clientHolder.apiClient, userId: clientHolder.apiClient
.userId); .userId);
}); });
}, },
icon: const Icon(Icons.warning), icon: const Icon(Icons.warning),
label: const Text("Retry"), label: const Text("Retry"),
@ -207,28 +204,7 @@ class _FriendsListState extends State<FriendsList> {
await showDialog( await showDialog(
context: context, context: context,
builder: (context) { builder: (context) {
return FutureBuilder( return const MyProfileDialog();
future: _userProfileFuture,
builder: (context, snapshot) {
if (snapshot.hasData) {
final profile = snapshot.data as PersonalProfile;
return MyProfileDialog(profile: profile);
} else if (snapshot.hasError) {
return DefaultErrorWidget(
title: "Failed to load personal profile.",
onRetry: () {
setState(() {
_userProfileFuture = UserApi.getPersonalProfile(ClientHolder
.of(context)
.apiClient);
});
},
);
} else {
return const Center(child: CircularProgressIndicator(),);
}
}
);
}, },
); );
}, },

View file

@ -1,20 +1,48 @@
import 'package:contacts_plus_plus/apis/user_api.dart';
import 'package:contacts_plus_plus/auxiliary.dart'; import 'package:contacts_plus_plus/auxiliary.dart';
import 'package:contacts_plus_plus/client_holder.dart';
import 'package:contacts_plus_plus/models/personal_profile.dart'; import 'package:contacts_plus_plus/models/personal_profile.dart';
import 'package:contacts_plus_plus/widgets/default_error_widget.dart';
import 'package:contacts_plus_plus/widgets/generic_avatar.dart'; import 'package:contacts_plus_plus/widgets/generic_avatar.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
class MyProfileDialog extends StatelessWidget { class MyProfileDialog extends StatefulWidget {
const MyProfileDialog({required this.profile, super.key}); const MyProfileDialog({super.key});
final PersonalProfile profile; @override
State<MyProfileDialog> createState() => _MyProfileDialogState();
}
class _MyProfileDialogState extends State<MyProfileDialog> {
ClientHolder? _clientHolder;
Future<PersonalProfile>? _personalProfileFuture;
@override
void didChangeDependencies() async {
super.didChangeDependencies();
final clientHolder = ClientHolder.of(context);
if (_clientHolder != clientHolder) {
_clientHolder = clientHolder;
final apiClient = _clientHolder!.apiClient;
_personalProfileFuture = UserApi.getPersonalProfile(apiClient);
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final tt = Theme.of(context).textTheme; final tt = Theme
.of(context)
.textTheme;
DateFormat dateFormat = DateFormat.yMd(); DateFormat dateFormat = DateFormat.yMd();
return Dialog( return Dialog(
child: Padding( child: FutureBuilder(
future: _personalProfileFuture,
builder: (context, snapshot) {
if (snapshot.hasData) {
final profile = snapshot.data as PersonalProfile;
return Padding(
padding: const EdgeInsets.all(24), padding: const EdgeInsets.all(24),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -41,11 +69,17 @@ class MyProfileDialog extends StatelessWidget {
), ),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [Text("2FA: ", style: tt.labelLarge,), Text(profile.twoFactor ? "Enabled" : "Disabled")], children: [
Text("2FA: ", style: tt.labelLarge,),
Text(profile.twoFactor ? "Enabled" : "Disabled")
],
), ),
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [Text("Patreon Supporter: ", style: tt.labelLarge,), Text(profile.isPatreonSupporter ? "Yes" : "No")], children: [
Text("Patreon Supporter: ", style: tt.labelLarge,),
Text(profile.isPatreonSupporter ? "Yes" : "No")
],
), ),
if (profile.publicBanExpiration?.isAfter(DateTime.now()) ?? false) if (profile.publicBanExpiration?.isAfter(DateTime.now()) ?? false)
Row( Row(
@ -53,9 +87,39 @@ class MyProfileDialog extends StatelessWidget {
children: [Text("Ban Expiration: ", style: tt.labelLarge,), children: [Text("Ban Expiration: ", style: tt.labelLarge,),
Text(dateFormat.format(profile.publicBanExpiration!))], Text(dateFormat.format(profile.publicBanExpiration!))],
), ),
StorageIndicator(usedBytes: profile.usedBytes, maxBytes: profile.maxBytes,), StorageIndicator(usedBytes: profile.usedBytes, maxBytes: profile.quotaBytes,),
], ],
), ),
);
}
else if (snapshot.hasError) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
DefaultErrorWidget(
message: snapshot.error.toString(),
onRetry: () {
setState(() {
_personalProfileFuture = UserApi.getPersonalProfile(ClientHolder
.of(context)
.apiClient);
});
},
),
],
);
} else {
return const Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: EdgeInsets.symmetric(vertical: 96, horizontal: 64),
child: CircularProgressIndicator(),
),
],
);
}
},
), ),
); );
} }