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) {
if (neosdb == null || neosdb.isEmpty) return "";
if (neosdb.startsWith("http")) return neosdb;
final fullUri = neosdb.replaceFirst("neosdb:///", Config.neosCdnUrl);
final lastPeriodIndex = fullUri.lastIndexOf(".");
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;
final filename = p.basenameWithoutExtension(neosdb);
return "${Config.neosCdnUrl}$filename";
}
}
extension Unique<E, Id> on List<E> {
List<E> unique([Id Function(E element)? id, bool inplace = true]) {
final ids = <Id>{};
@ -67,7 +61,7 @@ extension Unique<E, Id> on List<E> {
}
}
extension Strip on String {
extension StringX on String {
String stripHtml() {
final document = htmlparser.parse(this);
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
// but it's fine for just having a name to display
String stripUid() => startsWith("U-") ? substring(2) : this;
String? get asNullable => isEmpty ? null : this;
}
extension Format on Duration {

View file

@ -118,19 +118,20 @@ class ApiClient {
}
static void checkResponse(http.Response response) {
final error = "(${response.statusCode}${kDebugMode ? "|${response.body}" : ""})";
if (response.statusCode == 429) {
throw "Sorry, you are being rate limited";
throw "Sorry, you are being rate limited. $error";
}
if (response.statusCode == 403) {
tryCachedLogin();
// 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) {
throw "Internal server error";
throw "Internal server error. $error";
}
if (response.statusCode != 200) {
throw "Unknown Error: ${response.statusCode}${kDebugMode ? "|${response.body}" : ""}";
if (response.statusCode >= 300) {
throw "Unknown Error. $error";
}
}

View file

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

View file

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

View file

@ -32,7 +32,6 @@ class FriendsList extends StatefulWidget {
}
class _FriendsListState extends State<FriendsList> {
Future<PersonalProfile>? _userProfileFuture;
Future<UserStatus>? _userStatusFuture;
ClientHolder? _clientHolder;
String _searchFilter = "";
@ -44,7 +43,6 @@ class _FriendsListState extends State<FriendsList> {
if (_clientHolder != clientHolder) {
_clientHolder = clientHolder;
final apiClient = _clientHolder!.apiClient;
_userProfileFuture = UserApi.getPersonalProfile(apiClient);
_refreshUserStatus();
}
}
@ -137,7 +135,6 @@ class _FriendsListState extends State<FriendsList> {
_userStatusFuture = UserApi.getUserStatus(clientHolder.apiClient, userId: clientHolder.apiClient
.userId);
});
},
icon: const Icon(Icons.warning),
label: const Text("Retry"),
@ -207,28 +204,7 @@ class _FriendsListState extends State<FriendsList> {
await showDialog(
context: context,
builder: (context) {
return FutureBuilder(
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(),);
}
}
);
return const MyProfileDialog();
},
);
},

View file

@ -1,61 +1,125 @@
import 'package:contacts_plus_plus/apis/user_api.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/widgets/default_error_widget.dart';
import 'package:contacts_plus_plus/widgets/generic_avatar.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
class MyProfileDialog extends StatelessWidget {
const MyProfileDialog({required this.profile, super.key});
class MyProfileDialog extends StatefulWidget {
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
Widget build(BuildContext context) {
final tt = Theme.of(context).textTheme;
final tt = Theme
.of(context)
.textTheme;
DateFormat dateFormat = DateFormat.yMd();
return Dialog(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.end,
child: FutureBuilder(
future: _personalProfileFuture,
builder: (context, snapshot) {
if (snapshot.hasData) {
final profile = snapshot.data as PersonalProfile;
return Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(profile.username, style: tt.titleLarge),
Text(profile.email, style: tt.labelMedium?.copyWith(color: Colors.white54),)
],
),
GenericAvatar(imageUri: Aux.neosDbToHttp(profile.userProfile.iconUrl), radius: 24,)
],
),
const SizedBox(height: 16,),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [Text("User ID: ", style: tt.labelLarge,), Text(profile.id)],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("2FA: ", style: tt.labelLarge,),
Text(profile.twoFactor ? "Enabled" : "Disabled")
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("Patreon Supporter: ", style: tt.labelLarge,),
Text(profile.isPatreonSupporter ? "Yes" : "No")
],
),
if (profile.publicBanExpiration?.isAfter(DateTime.now()) ?? false)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [Text("Ban Expiration: ", style: tt.labelLarge,),
Text(dateFormat.format(profile.publicBanExpiration!))],
),
StorageIndicator(usedBytes: profile.usedBytes, maxBytes: profile.quotaBytes,),
],
),
);
}
else if (snapshot.hasError) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(profile.username, style: tt.titleLarge),
Text(profile.email, style: tt.labelMedium?.copyWith(color: Colors.white54),)
],
DefaultErrorWidget(
message: snapshot.error.toString(),
onRetry: () {
setState(() {
_personalProfileFuture = UserApi.getPersonalProfile(ClientHolder
.of(context)
.apiClient);
});
},
),
GenericAvatar(imageUri: Aux.neosDbToHttp(profile.userProfile.iconUrl), radius: 24,)
],
),
const SizedBox(height: 16,),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [Text("User ID: ", style: tt.labelLarge,), Text(profile.id)],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [Text("2FA: ", style: tt.labelLarge,), Text(profile.twoFactor ? "Enabled" : "Disabled")],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [Text("Patreon Supporter: ", style: tt.labelLarge,), Text(profile.isPatreonSupporter ? "Yes" : "No")],
),
if (profile.publicBanExpiration?.isAfter(DateTime.now()) ?? false)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [Text("Ban Expiration: ", style: tt.labelLarge,),
Text(dateFormat.format(profile.publicBanExpiration!))],
),
StorageIndicator(usedBytes: profile.usedBytes, maxBytes: profile.maxBytes,),
],
),
);
} else {
return const Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: EdgeInsets.symmetric(vertical: 96, horizontal: 64),
child: CircularProgressIndicator(),
),
],
);
}
},
),
);
}