fluffychat merge - resolve conflicts

pull/1384/head
ggurdin 1 year ago
commit 5e347144aa
No known key found for this signature in database
GPG Key ID: A01CB41737CBB478

@ -2,6 +2,9 @@
FluffyChat v1.22.0 brings a new design for spaces, replaces the bottom navigation bar with filter chips and makes it finally possible to play ogg audio messages on iOS. A lot of other fixes and improvements have also been added to this release. FluffyChat v1.22.0 brings a new design for spaces, replaces the bottom navigation bar with filter chips and makes it finally possible to play ogg audio messages on iOS. A lot of other fixes and improvements have also been added to this release.
FluffyChat also now uses the new authenticated media endpoints if the server supports Matrix v1.11 or
mentions the msc with the key `org.matrix.msc3916.stable` in the `unstable_features`.
- build: (deps): bump docker/build-push-action from 5 to 6 (dependabot[bot]) - build: (deps): bump docker/build-push-action from 5 to 6 (dependabot[bot])
- build(deps): bump rexml from 3.2.8 to 3.3.3 in /ios (dependabot[bot]) - build(deps): bump rexml from 3.2.8 to 3.3.3 in /ios (dependabot[bot])
- build: Remove permissions for screensharing until it is fixed (Krille) - build: Remove permissions for screensharing until it is fixed (Krille)
@ -24,6 +27,7 @@ FluffyChat v1.22.0 brings a new design for spaces, replaces the bottom navigatio
- chore: Sligthly improve chat permissions page design (krille-chan) - chore: Sligthly improve chat permissions page design (krille-chan)
- design: Add snackbar with link to changelog on new version (Krille) - design: Add snackbar with link to changelog on new version (Krille)
- docs: Update privacy policy (krille-chan) - docs: Update privacy policy (krille-chan)
- feat: Support for matrix auth media endpoints
- feat: Convert opus to aac on iOS before playing (Krille) - feat: Convert opus to aac on iOS before playing (Krille)
- feat: New spaces and chat list design (krille-chan) - feat: New spaces and chat list design (krille-chan)
- feat: Record voice message with opus/ogg if supported (Krille) - feat: Record voice message with opus/ogg if supported (Krille)

@ -4190,5 +4190,9 @@
"version": {} "version": {}
} }
}, },
"changelog": "Changelog" "changelog": "Changelog",
"loginWithMatrixId": "Login with Matrix-ID",
"discoverHomeservers": "Discover homeservers",
"whatIsAHomeserver": "What is a homeserver?",
"homeserverDescription": "All your data is stored on the homeserver, just like an email provider. You can choose which homeserver you want to use, while you can still communicate with everyone. Learn more at at https://matrix.org."
} }

@ -76,7 +76,7 @@ abstract class AppRoutes {
pageBuilder: (context, state) => defaultPageBuilder( pageBuilder: (context, state) => defaultPageBuilder(
context, context,
state, state,
const HomeserverPicker(), const HomeserverPicker(addMultiAccount: false),
), ),
redirect: loggedInRedirect, redirect: loggedInRedirect,
routes: [ routes: [
@ -351,13 +351,15 @@ abstract class AppRoutes {
// redirect: loggedOutRedirect, // redirect: loggedOutRedirect,
// pageBuilder: (context, state) => defaultPageBuilder( // pageBuilder: (context, state) => defaultPageBuilder(
// context, // context,
// const HomeserverPicker(), // state,
// const HomeserverPicker(addMultiAccount: true),
// ), // ),
// routes: [ // routes: [
// GoRoute( // GoRoute(
// path: 'login', // path: 'login',
// pageBuilder: (context, state) => defaultPageBuilder( // pageBuilder: (context, state) => defaultPageBuilder(
// context, // context,
// state,
// const Login(), // const Login(),
// ), // ),
// redirect: loggedOutRedirect, // redirect: loggedOutRedirect,

@ -92,11 +92,10 @@ abstract class FluffyThemes {
), ),
inputDecorationTheme: InputDecorationTheme( inputDecorationTheme: InputDecorationTheme(
border: OutlineInputBorder( border: OutlineInputBorder(
borderSide: BorderSide.none, borderRadius: BorderRadius.circular(AppConfig.borderRadius),
borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2),
), ),
contentPadding: const EdgeInsets.all(12), contentPadding: const EdgeInsets.all(12),
filled: true, filled: false,
), ),
appBarTheme: AppBarTheme( appBarTheme: AppBarTheme(
toolbarHeight: FluffyThemes.isColumnMode(context) ? 72 : 56, toolbarHeight: FluffyThemes.isColumnMode(context) ? 72 : 56,
@ -115,13 +114,6 @@ abstract class FluffyThemes {
systemNavigationBarColor: colorScheme.surface, systemNavigationBarColor: colorScheme.surface,
), ),
), ),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2),
),
),
),
outlinedButtonTheme: OutlinedButtonThemeData( outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
side: BorderSide( side: BorderSide(
@ -146,9 +138,6 @@ abstract class FluffyThemes {
elevation: 0, elevation: 0,
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
textStyle: const TextStyle(fontSize: 16), textStyle: const TextStyle(fontSize: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
),
), ),
), ),
// #Pangea // #Pangea

@ -264,7 +264,9 @@ class BootstrapDialogState extends State<BootstrapDialog> {
hintStyle: TextStyle( hintStyle: TextStyle(
fontFamily: theme.textTheme.bodyLarge?.fontFamily, fontFamily: theme.textTheme.bodyLarge?.fontFamily,
), ),
hintText: L10n.of(context)!.recoveryKey, prefixIcon: const Icon(Icons.key_outlined),
labelText: L10n.of(context)!.recoveryKey,
hintText: 'Es** **** **** ****',
errorText: _recoveryKeyInputError, errorText: _recoveryKeyInputError,
errorMaxLines: 2, errorMaxLines: 2,
), ),

@ -347,13 +347,23 @@ class ChatController extends State<ChatPageWithRoom>
await loadTimelineFuture; await loadTimelineFuture;
if (initialEventId != null) scrollToEventId(initialEventId); if (initialEventId != null) scrollToEventId(initialEventId);
final readMarkerEventIndex = readMarkerEventId.isEmpty var readMarkerEventIndex = readMarkerEventId.isEmpty
? -1 ? -1
: timeline!.events : timeline!.events
.where((e) => e.isVisibleInGui) .where((e) => e.isVisibleInGui || e.eventId == readMarkerEventId)
.toList() .toList()
.indexWhere((e) => e.eventId == readMarkerEventId); .indexWhere((e) => e.eventId == readMarkerEventId);
// Read marker is existing but not found in first events. Try a single
// requestHistory call before opening timeline on event context:
if (readMarkerEventId.isNotEmpty && readMarkerEventIndex == -1) {
await timeline?.requestHistory(historyCount: _loadHistoryCount);
readMarkerEventIndex = timeline!.events
.where((e) => e.isVisibleInGui || e.eventId == readMarkerEventId)
.toList()
.indexWhere((e) => e.eventId == readMarkerEventId);
}
if (readMarkerEventIndex > 1) { if (readMarkerEventIndex > 1) {
Logs().v('Scroll up to visible event', readMarkerEventId); Logs().v('Scroll up to visible event', readMarkerEventId);
scrollToEventId(readMarkerEventId, highlightEvent: false); scrollToEventId(readMarkerEventId, highlightEvent: false);
@ -362,6 +372,9 @@ class ChatController extends State<ChatPageWithRoom>
showScrollUpMaterialBanner(readMarkerEventId); showScrollUpMaterialBanner(readMarkerEventId);
} }
// Mark room as read on first visit if requirements are fulfilled
setReadMarker();
if (!mounted) return; if (!mounted) return;
} catch (e, s) { } catch (e, s) {
ErrorReporter(context, 'Unable to load timeline').onErrorCallback(e, s); ErrorReporter(context, 'Unable to load timeline').onErrorCallback(e, s);

@ -1,5 +1,3 @@
import 'dart:io';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -33,6 +31,8 @@ class SendFileDialogState extends State<SendFileDialog> {
static const int minSizeToCompress = 20 * 1024; static const int minSizeToCompress = 20 * 1024;
Future<void> _send() async { Future<void> _send() async {
final scaffoldMessenger = ScaffoldMessenger.of(context);
final l10n = L10n.of(context)!;
for (var file in widget.files) { for (var file in widget.files) {
MatrixImageFile? thumbnail; MatrixImageFile? thumbnail;
if (file is MatrixVideoFile && file.bytes.length > minSizeToCompress) { if (file is MatrixVideoFile && file.bytes.length > minSizeToCompress) {
@ -44,20 +44,24 @@ class SendFileDialogState extends State<SendFileDialog> {
}, },
); );
} }
try { widget.room
await widget.room.sendFileEvent( .sendFileEvent(
file, file,
thumbnail: thumbnail, thumbnail: thumbnail,
shrinkImageMaxDimension: origImage ? null : 1600, shrinkImageMaxDimension: origImage ? null : 1600,
)
.catchError(
(e, s) {
if (e is FileTooBigMatrixException) {
scaffoldMessenger.showSnackBar(
SnackBar(content: Text(l10n.fileIsTooBigForServer)),
); );
} on IOException catch (_) { return null;
} on FileTooBigMatrixException catch (_) {
} catch (e, s) {
if (mounted) {
ErrorReporter(context, 'Unable to send file').onErrorCallback(e, s);
}
rethrow;
} }
ErrorReporter(context, 'Unable to send file').onErrorCallback(e, s);
return null;
},
);
} }
Navigator.of(context, rootNavigator: false).pop(); Navigator.of(context, rootNavigator: false).pop();

@ -53,6 +53,7 @@ class ChatListHeader extends StatelessWidget implements PreferredSizeWidget {
// globalSearch: globalSearch, // globalSearch: globalSearch,
// ), // ),
// decoration: InputDecoration( // decoration: InputDecoration(
// filled: true,
// fillColor: theme.colorScheme.secondaryContainer, // fillColor: theme.colorScheme.secondaryContainer,
// border: OutlineInputBorder( // border: OutlineInputBorder(
// borderSide: BorderSide.none, // borderSide: BorderSide.none,

@ -359,6 +359,7 @@ class _SpaceViewState extends State<SpaceView> {
onChanged: (_) => setState(() {}), onChanged: (_) => setState(() {}),
textInputAction: TextInputAction.search, textInputAction: TextInputAction.search,
decoration: InputDecoration( decoration: InputDecoration(
filled: true,
fillColor: theme.colorScheme.secondaryContainer, fillColor: theme.colorScheme.secondaryContainer,
border: OutlineInputBorder( border: OutlineInputBorder(
borderSide: BorderSide.none, borderSide: BorderSide.none,

@ -1,108 +0,0 @@
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pages/homeserver_picker/public_homeserver.dart';
import 'package:fluffychat/utils/localized_exception_extension.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:flutter_typeahead/flutter_typeahead.dart';
import 'homeserver_bottom_sheet.dart';
import 'homeserver_picker.dart';
class HomeserverAppBar extends StatelessWidget {
final HomeserverPickerController controller;
const HomeserverAppBar({super.key, required this.controller});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return TypeAheadField<PublicHomeserver>(
decorationBuilder: (context, child) => ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 256),
child: Material(
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
elevation: theme.appBarTheme.scrolledUnderElevation ?? 4,
shadowColor: theme.appBarTheme.shadowColor ?? Colors.black,
child: child,
),
),
emptyBuilder: (context) => ListTile(
leading: const Icon(Icons.search_outlined),
title: Text(L10n.of(context)!.nothingFound),
),
loadingBuilder: (context) => ListTile(
leading: const CircularProgressIndicator.adaptive(strokeWidth: 2),
title: Text(L10n.of(context)!.loadingPleaseWait),
),
errorBuilder: (context, error) => ListTile(
leading: const Icon(Icons.error_outlined),
title: Text(
error.toLocalizedString(context),
),
),
itemBuilder: (context, homeserver) => ListTile(
title: Text(homeserver.name),
subtitle: homeserver.description == null
? null
: Text(homeserver.description ?? ''),
trailing: IconButton(
icon: const Icon(Icons.info_outlined),
onPressed: () => showModalBottomSheet(
context: context,
builder: (_) => HomeserverBottomSheet(
homeserver: homeserver,
),
),
),
),
suggestionsCallback: (pattern) async {
pattern = pattern.toLowerCase().trim();
final homeservers = await controller.loadHomeserverList();
final matches = homeservers
.where(
(homeserver) =>
homeserver.name.toLowerCase().contains(pattern) ||
(homeserver.description?.toLowerCase().contains(pattern) ??
false),
)
.toList();
if (pattern.contains('.') &&
pattern.split('.').any((part) => part.isNotEmpty) &&
!matches.any((homeserver) => homeserver.name == pattern)) {
matches.add(PublicHomeserver(name: pattern));
}
return matches;
},
onSelected: (suggestion) {
controller.homeserverController.text = suggestion.name;
controller.checkHomeserverAction();
},
controller: controller.homeserverController,
builder: (context, textEditingController, focusNode) => TextField(
enabled: !controller.isLoggingIn,
controller: textEditingController,
focusNode: focusNode,
decoration: InputDecoration(
prefixIcon: Navigator.of(context).canPop()
? IconButton(
onPressed: Navigator.of(context).pop,
icon: const Icon(Icons.arrow_back),
)
: null,
fillColor: FluffyThemes.isColumnMode(context)
? theme.colorScheme.surface
// ignore: deprecated_member_use
: theme.colorScheme.surfaceVariant,
prefixText: '${L10n.of(context)!.homeserver}: ',
hintText: L10n.of(context)!.enterYourHomeserver,
suffixIcon: const Icon(Icons.search),
),
textInputAction: TextInputAction.search,
onSubmitted: controller.checkHomeserverAction,
autocorrect: false,
),
);
}
}

@ -1,53 +0,0 @@
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:fluffychat/pages/homeserver_picker/public_homeserver.dart';
class HomeserverBottomSheet extends StatelessWidget {
final PublicHomeserver homeserver;
const HomeserverBottomSheet({required this.homeserver, super.key});
@override
Widget build(BuildContext context) {
final description = homeserver.description;
final registration = homeserver.regLink;
final jurisdiction = homeserver.staffJur;
final homeserverSoftware = homeserver.software;
return Scaffold(
appBar: AppBar(
title: Text(homeserver.name),
),
body: ListView(
children: [
if (description != null && description.isNotEmpty)
ListTile(
leading: const Icon(Icons.info_outlined),
title: Text(description),
),
if (jurisdiction != null && jurisdiction.isNotEmpty)
ListTile(
leading: const Icon(Icons.location_city_outlined),
title: Text(jurisdiction),
),
if (homeserverSoftware != null && homeserverSoftware.isNotEmpty)
ListTile(
leading: const Icon(Icons.domain_outlined),
title: Text(homeserverSoftware),
),
ListTile(
onTap: () => launchUrlString(homeserver.name),
leading: const Icon(Icons.link_outlined),
title: Text(homeserver.name),
),
if (registration != null)
ListTile(
onTap: () => launchUrlString(registration),
leading: const Icon(Icons.person_add_outlined),
title: Text(registration),
),
],
),
);
}
}

@ -1,14 +1,11 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pages/homeserver_picker/homeserver_picker_view.dart'; import 'package:fluffychat/pages/homeserver_picker/homeserver_picker_view.dart';
import 'package:fluffychat/pages/homeserver_picker/public_homeserver.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:fluffychat/pangea/utils/firebase_analytics.dart';
import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/utils/tor_stub.dart' import 'package:fluffychat/utils/tor_stub.dart'
if (dart.library.html) 'package:tor_detector_web/tor_detector_web.dart'; if (dart.library.html) 'package:tor_detector_web/tor_detector_web.dart';
@ -27,7 +24,8 @@ import 'package:universal_html/html.dart' as html;
import '../../utils/localized_exception_extension.dart'; import '../../utils/localized_exception_extension.dart';
class HomeserverPicker extends StatefulWidget { class HomeserverPicker extends StatefulWidget {
const HomeserverPicker({super.key}); final bool addMultiAccount;
const HomeserverPicker({required this.addMultiAccount, super.key});
@override @override
HomeserverPickerController createState() => HomeserverPickerController(); HomeserverPickerController createState() => HomeserverPickerController();
@ -65,6 +63,22 @@ class HomeserverPickerController extends State<HomeserverPicker> {
String? _lastCheckedUrl; String? _lastCheckedUrl;
Timer? _checkHomeserverCooldown;
tryCheckHomeserverActionWithCooldown([_]) {
_checkHomeserverCooldown?.cancel();
_checkHomeserverCooldown = Timer(
const Duration(milliseconds: 500),
checkHomeserverAction,
);
}
tryCheckHomeserverActionWithoutCooldown([_]) {
_checkHomeserverCooldown?.cancel();
_lastCheckedUrl = null;
checkHomeserverAction();
}
/// Starts an analysis of the given homeserver. It uses the current domain and /// Starts an analysis of the given homeserver. It uses the current domain and
/// makes sure that it is prefixed with https. Then it searches for the /// makes sure that it is prefixed with https. Then it searches for the
/// well-known information and forwards to the login page depending on the /// well-known information and forwards to the login page depending on the
@ -75,7 +89,7 @@ class HomeserverPickerController extends State<HomeserverPicker> {
if (homeserverController.text == _lastCheckedUrl) return; if (homeserverController.text == _lastCheckedUrl) return;
_lastCheckedUrl = homeserverController.text; _lastCheckedUrl = homeserverController.text;
setState(() { setState(() {
error = _rawLoginTypes = loginFlows = null; error = loginFlows = null;
isLoading = true; isLoading = true;
}); });
@ -87,12 +101,6 @@ class HomeserverPickerController extends State<HomeserverPicker> {
final client = Matrix.of(context).getLoginClient(); final client = Matrix.of(context).getLoginClient();
final (_, _, loginFlows) = await client.checkHomeserver(homeserver); final (_, _, loginFlows) = await client.checkHomeserver(homeserver);
this.loginFlows = loginFlows; this.loginFlows = loginFlows;
if (supportsSso) {
_rawLoginTypes = await client.request(
RequestType.GET,
'/client/v3/login',
);
}
} catch (e) { } catch (e) {
setState(() => error = (e).toLocalizedString(context)); setState(() => error = (e).toLocalizedString(context));
} finally { } finally {
@ -114,12 +122,7 @@ class HomeserverPickerController extends State<HomeserverPicker> {
bool get supportsPasswordLogin => _supportsFlow('m.login.password'); bool get supportsPasswordLogin => _supportsFlow('m.login.password');
Map<String, dynamic>? _rawLoginTypes; void ssoLoginAction() async {
// #Pangea
// void ssoLoginAction(String id) async {
void ssoLoginAction(IdentityProvider provider) async {
//Pangea#
final redirectUrl = kIsWeb final redirectUrl = kIsWeb
? Uri.parse(html.window.location.href) ? Uri.parse(html.window.location.href)
.resolveUri( .resolveUri(
@ -131,17 +134,14 @@ class HomeserverPickerController extends State<HomeserverPicker> {
: 'http://localhost:3001//login'; : 'http://localhost:3001//login';
final url = Matrix.of(context).getLoginClient().homeserver!.replace( final url = Matrix.of(context).getLoginClient().homeserver!.replace(
// #Pangea path: '/_matrix/client/v3/login/sso/redirect',
// path: '/_matrix/client/v3/login/sso/redirect${id == null ? '' : '/$id'}',
path:
'/_matrix/client/v3/login/sso/redirect${provider.id == null ? '' : '/${provider.id}'}',
// Pangea#
queryParameters: {'redirectUrl': redirectUrl}, queryParameters: {'redirectUrl': redirectUrl},
); );
final urlScheme = isDefaultPlatform final urlScheme = isDefaultPlatform
? Uri.parse(redirectUrl).scheme ? Uri.parse(redirectUrl).scheme
: "http://localhost:3001"; : "http://localhost:3001";
// #Pangea // #Pangea
// final result = await FlutterWebAuth2.authenticate( // final result = await FlutterWebAuth2.authenticate(
// url: url.toString(), // url: url.toString(),
@ -182,7 +182,7 @@ class HomeserverPickerController extends State<HomeserverPicker> {
initialDeviceDisplayName: PlatformInfos.clientName, initialDeviceDisplayName: PlatformInfos.clientName,
); );
// #Pangea // #Pangea
GoogleAnalytics.login(provider.name!, loginRes.userId); // GoogleAnalytics.login(provider.name!, loginRes.userId);
// Pangea# // Pangea#
} catch (e) { } catch (e) {
setState(() { setState(() {
@ -200,39 +200,6 @@ class HomeserverPickerController extends State<HomeserverPicker> {
} }
} }
List<IdentityProvider>? get identityProviders {
final loginTypes = _rawLoginTypes;
if (loginTypes == null) return null;
final List? rawProviders =
loginTypes.tryGetList('flows')?.singleWhereOrNull(
(flow) => flow['type'] == AuthenticationTypes.sso,
)['identity_providers'] ??
[
{'id': null},
];
if (rawProviders == null) return null;
final list =
rawProviders.map((json) => IdentityProvider.fromJson(json)).toList();
if (PlatformInfos.isCupertinoStyle) {
list.sort((a, b) => a.brand == 'apple' ? -1 : 1);
}
return list;
}
List<PublicHomeserver>? cachedHomeservers;
Future<List<PublicHomeserver>> loadHomeserverList() async {
if (cachedHomeservers != null) return cachedHomeservers!;
final result = await Matrix.of(context)
.getLoginClient()
.httpClient
.get(AppConfig.homeserverList);
final resultJson = jsonDecode(result.body)['public_servers'] as List;
final homeserverList =
resultJson.map((json) => PublicHomeserver.fromJson(json)).toList();
return cachedHomeservers = homeserverList;
}
void login() => context.push( void login() => context.push(
'${GoRouter.of(context).routeInformationProvider.value.uri.path}/login', '${GoRouter.of(context).routeInformationProvider.value.uri.path}/login',
); );

@ -1,11 +1,9 @@
import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pangea/pages/connect/p_sso_button.dart';
import 'package:fluffychat/pangea/widgets/common/pangea_logo_svg.dart';
import 'package:fluffychat/pangea/widgets/signup/signup_buttons.dart';
import 'package:fluffychat/widgets/layouts/login_scaffold.dart'; import 'package:fluffychat/widgets/layouts/login_scaffold.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:url_launcher/url_launcher.dart';
import 'homeserver_picker.dart'; import 'homeserver_picker.dart';
@ -18,25 +16,15 @@ class HomeserverPickerView extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
final identityProviders = controller.identityProviders;
final errorText = controller.error;
// #Pangea
// final publicHomeserver = controller.cachedHomeservers?.singleWhereOrNull(
// (homeserver) =>
// homeserver.name ==
// controller.homeserverController.text.trim().toLowerCase(),
// );
// final regLink = publicHomeserver?.regLink;
// Pangea#
return LoginScaffold( return LoginScaffold(
// #Pangea // #Pangea
// enforceMobileMode: Matrix.of(context).client.isLogged(), // enforceMobileMode: Matrix.of(context).client.isLogged(),
// appBar: AppBar( // appBar: controller.widget.addMultiAccount
// titleSpacing,: 12, // ? AppBar(
// automaticallyImplyLeading: false, // centerTitle: true,
// surfaceTintColor: theme.colorScheme.surface, // title: Text(L10n.of(context)!.addAccount),
// title: HomeserverAppBar(controller: controller), // )
// ), // : null,
appBar: AppBar( appBar: AppBar(
centerTitle: true, centerTitle: true,
title: Text( title: Text(
@ -44,7 +32,7 @@ class HomeserverPickerView extends StatelessWidget {
), ),
), ),
// Pangea# // Pangea#
body: Column( body: ListView(
children: [ children: [
// display a prominent banner to import session for TOR browser // display a prominent banner to import session for TOR browser
// users. This feature is just some UX sugar as TOR users are // users. This feature is just some UX sugar as TOR users are
@ -70,217 +58,135 @@ class HomeserverPickerView extends StatelessWidget {
// ), // ),
// ), // ),
// ), // ),
// Image.asset(
// 'assets/banner_transparent.png',
// ),
// Pangea# // Pangea#
Expanded( Padding(
child: controller.isLoading padding: const EdgeInsets.only(
// #Pangea top: 16.0,
// ? const Center(child: CircularProgressIndicator.adaptive()) right: 8.0,
? const Center( left: 8.0,
child: CircularProgressIndicator( bottom: 16.0,
valueColor: AlwaysStoppedAnimation<Color>(Colors.black), ),
), child: Column(
) crossAxisAlignment: CrossAxisAlignment.stretch,
// Pangea# mainAxisSize: MainAxisSize.min,
: ListView(
children: [ children: [
if (errorText != null) ...[ Padding(
const SizedBox(height: 12), padding: const EdgeInsets.all(16.0),
const Center( child: TextField(
child: Icon( onChanged: controller.tryCheckHomeserverActionWithCooldown,
Icons.error_outline, onEditingComplete:
size: 48, controller.tryCheckHomeserverActionWithoutCooldown,
color: Colors.orange, onSubmitted:
controller.tryCheckHomeserverActionWithoutCooldown,
onTap: controller.tryCheckHomeserverActionWithCooldown,
controller: controller.homeserverController,
decoration: InputDecoration(
prefixIcon: controller.isLoading
? Container(
width: 16,
height: 16,
alignment: Alignment.center,
child: const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator.adaptive(
strokeWidth: 2,
), ),
), ),
const SizedBox(height: 12), )
Center( : const Icon(Icons.search_outlined),
filled: false,
border: OutlineInputBorder(
borderRadius:
BorderRadius.circular(AppConfig.borderRadius),
),
hintText: AppConfig.defaultHomeserver,
labelText: L10n.of(context)!.homeserver,
errorText: controller.error,
suffixIcon: IconButton(
onPressed: () {
showDialog(
context: context,
builder: (context) => AlertDialog.adaptive(
title: Text(L10n.of(context)!.whatIsAHomeserver),
content: Linkify(
text: L10n.of(context)!.homeserverDescription,
),
actions: [
TextButton(
onPressed: () => launchUrl(
Uri.https('servers.joinmatrix.org'),
),
child: Text( child: Text(
errorText, L10n.of(context)!.discoverHomeservers,
textAlign: TextAlign.center,
style: TextStyle(
color: theme.colorScheme.error,
fontSize: 18,
), ),
), ),
TextButton(
onPressed: Navigator.of(context).pop,
child: Text(L10n.of(context)!.close),
), ),
// #Pangea ],
// Center(
// child: Text(
// L10n.of(context)!
// .pleaseTryAgainLaterOrChooseDifferentServer,
// textAlign: TextAlign.center,
// style: TextStyle(
// color: theme.colorScheme.error,
// fontSize: 12,
// ),
// ),
// ),
// Pangea#
const SizedBox(height: 36),
] else
// #Pangea
const SignupButtons(),
// Padding(
// padding: const EdgeInsets.only(
// top: 0.0,
// right: 8.0,
// left: 8.0,
// bottom: 16.0,
// ),
// child: Image.asset(
// 'assets/banner_transparent.png',
// ),
// ),
// Pangea#
if (identityProviders != null) ...[
// #Pangea
...identityProviders.map(
(provider) => Padding(
padding: const EdgeInsets.all(12.0),
child: Hero(
tag: "ssobutton ${provider.id ?? provider.name}",
child: PangeaSsoButton(
identityProvider: provider,
onPressed: () =>
controller.ssoLoginAction(provider),
), ),
);
},
icon: const Icon(Icons.info_outlined),
), ),
), ),
), ),
// ...identityProviders.map( ),
// (provider) => _LoginButton( if (controller.supportsPasswordLogin || controller.supportsSso)
// icon: provider.icon == null
// ? const Icon(
// Icons.open_in_new_outlined,
// size: 16,
// )
// : Material(
// borderRadius: BorderRadius.circular(
// AppConfig.borderRadius,
// ),
// clipBehavior: Clip.hardEdge,
// child: MxcImage(
// placeholder: (_) => const Icon(
// Icons.open_in_new_outlined,
// size: 16,
// ),
// uri: Uri.parse(provider.icon!),
// width: 24,
// height: 24,
// isThumbnail: false,
// //isThumbnail: false,
// ),
// ),
// label: L10n.of(context)!.signInWith(
// provider.name ??
// provider.brand ??
// L10n.of(context)!.singlesignon,
// ),
// onPressed: () =>
// controller.ssoLoginAction(provider.id),
// ),
// ),
// ],
// Pangea#
if (controller.supportsPasswordLogin)
// #Pangea
Padding(
padding: const EdgeInsets.all(12.0),
child: Hero(
tag: 'signinButton',
child: ElevatedButton(
onPressed: controller.login,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const PangeaLogoSvg(width: 20),
Padding( Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 10, horizontal: 16.0,
), vertical: 8.0,
child: Text(
"${L10n.of(context)!.loginOrSignup} Pangea Chat",
),
), ),
], child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: theme.colorScheme.primary,
foregroundColor: theme.colorScheme.onPrimary,
), ),
onPressed: controller.isLoggingIn || controller.isLoading
? null
: controller.supportsSso
? controller.ssoLoginAction
: controller.login,
child: Text(L10n.of(context)!.connect),
), ),
), ),
if (controller.supportsPasswordLogin && controller.supportsSso)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: TextButton(
style: TextButton.styleFrom(
foregroundColor: theme.colorScheme.secondary,
textStyle: theme.textTheme.labelMedium,
), ),
// _LoginButton( onPressed: controller.isLoggingIn || controller.isLoading
// onPressed: controller.login, ? null
// label: L10n.of(context)!.signInWithPassword, : controller.login,
// icon: const Icon(Icons.lock_open_outlined, size: 16), child: Text(L10n.of(context)!.loginWithMatrixId),
// ),
// if (regLink != null)
// _LoginButton(
// onPressed: () => launchUrlString(regLink),
// icon: const Icon(
// Icons.open_in_new_outlined,
// size: 16,
// ),
// label: L10n.of(context)!.register,
// ),
// _LoginButton(
// onPressed: controller.restoreBackup,
// label: L10n.of(context)!.hydrate,
// withBorder: false,
// ),
// const SizedBox(height: 16),
// Pangea#
],
],
), ),
), ),
], Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: TextButton(
style: TextButton.styleFrom(
textStyle: theme.textTheme.labelMedium,
foregroundColor: theme.colorScheme.secondary,
), ),
); onPressed: controller.isLoggingIn || controller.isLoading
} ? null
} : controller.restoreBackup,
child: Text(L10n.of(context)!.hydrate),
class _LoginButton extends StatelessWidget {
final Widget? icon;
final String label;
final void Function() onPressed;
final bool withBorder;
const _LoginButton({
this.icon,
required this.label,
required this.onPressed,
this.withBorder = true,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final icon = this.icon;
return Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.symmetric(horizontal: 16),
alignment: Alignment.center,
child: SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
style: OutlinedButton.styleFrom(
side: FluffyThemes.isColumnMode(context)
? BorderSide.none
: BorderSide(
color: theme.colorScheme.outlineVariant,
width: 1,
), ),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(99),
), ),
foregroundColor: theme.colorScheme.onSurface, ],
backgroundColor:
withBorder ? theme.colorScheme.surface : Colors.transparent,
), ),
onPressed: onPressed,
label: Text(label),
icon: icon ?? const SizedBox.shrink(),
), ),
],
), ),
); );
} }

@ -1,73 +0,0 @@
class PublicHomeserver {
final String name;
final String? clientDomain;
final String? isp;
final String? staffJur;
final bool? usingVanillaReg;
final String? description;
final String? regMethod;
final String? regLink;
final String? software;
final String? version;
final bool? captcha;
final bool? email;
final List<String>? languages;
final List<Object>? features;
final int? onlineStatus;
final String? serverDomain;
final int? verStatus;
final int? roomDirectory;
final bool? slidingSync;
final bool? ipv6;
const PublicHomeserver({
required this.name,
this.clientDomain,
this.isp,
this.staffJur,
this.usingVanillaReg,
this.description,
this.regMethod,
this.regLink,
this.software,
this.version,
this.captcha,
this.email,
this.languages,
this.features,
this.onlineStatus,
this.serverDomain,
this.verStatus,
this.roomDirectory,
this.slidingSync,
this.ipv6,
});
factory PublicHomeserver.fromJson(Map<String, dynamic> json) =>
PublicHomeserver(
name: json['name'],
clientDomain: json['client_domain'],
isp: json['isp'],
staffJur: json['staff_jur'],
usingVanillaReg: json['using_vanilla_reg'],
description: json['description'],
regMethod: json['reg_method'],
regLink: json['reg_link'],
software: json['software'],
version: json['version'],
captcha: json['captcha'],
email: json['email'],
languages: json['languages'] == null
? null
: List<String>.from(json['languages']),
features: json['features'] == null
? null
: List<Object>.from(json['features']),
onlineStatus: json['online_status'],
serverDomain: json['server_domain'],
verStatus: json['ver_status'],
roomDirectory: json['room_directory'],
slidingSync: json['sliding_sync'],
ipv6: json['ipv6'],
);
}

@ -21,11 +21,6 @@ class LoginView extends StatelessWidget {
// .replaceFirst('https://', ''); // .replaceFirst('https://', '');
// final title = L10n.of(context)!.logInTo(homeserver); // final title = L10n.of(context)!.logInTo(homeserver);
// final titleParts = title.split(homeserver); // final titleParts = title.split(homeserver);
// final textFieldFillColor = FluffyThemes.isColumnMode(context)
// ? theme.colorScheme.surface
// // ignore: deprecated_member_use
// : theme.colorScheme.surfaceVariant;
// Pangea# // Pangea#
return LoginScaffold( return LoginScaffold(
@ -102,14 +97,14 @@ class LoginView extends StatelessWidget {
errorText: controller.usernameError, errorText: controller.usernameError,
// #Pangea // #Pangea
// errorStyle: const TextStyle(color: Colors.orange), // errorStyle: const TextStyle(color: Colors.orange),
// hintText: '@username:localpart',
errorStyle: TextStyle( errorStyle: TextStyle(
color: theme.textTheme.bodyMedium?.color, color: Theme.of(context).textTheme.bodyMedium?.color,
fontSize: 14, fontSize: 14,
), ),
// fillColor: textFieldFillColor,
fillColor: theme.colorScheme.surface.withOpacity(0.75),
// Pangea#
hintText: L10n.of(context)!.emailOrUsername, hintText: L10n.of(context)!.emailOrUsername,
// Pangea#
labelText: L10n.of(context)!.emailOrUsername,
), ),
), ),
), ),
@ -129,14 +124,10 @@ class LoginView extends StatelessWidget {
prefixIcon: const Icon(Icons.lock_outlined), prefixIcon: const Icon(Icons.lock_outlined),
errorText: controller.passwordError, errorText: controller.passwordError,
// #Pangea // #Pangea
// errorStyle: const TextStyle(color: Colors.orange),
errorStyle: TextStyle( errorStyle: TextStyle(
color: theme.textTheme.bodyMedium?.color, color: Theme.of(context).textTheme.bodyMedium?.color,
fontSize: 14, fontSize: 14,
), ),
// fillColor: textFieldFillColor,
fillColor: theme.colorScheme.surface.withOpacity(0.75),
// prevent enter key from clicking show password button
suffixIcon: MouseRegion( suffixIcon: MouseRegion(
cursor: SystemMouseCursors.click, cursor: SystemMouseCursors.click,
child: GestureDetector( child: GestureDetector(
@ -150,6 +141,7 @@ class LoginView extends StatelessWidget {
), ),
), ),
), ),
// errorStyle: const TextStyle(color: Colors.orange),
// suffixIcon: IconButton( // suffixIcon: IconButton(
// onPressed: controller.toggleShowPassword, // onPressed: controller.toggleShowPassword,
// icon: Icon( // icon: Icon(
@ -160,7 +152,8 @@ class LoginView extends StatelessWidget {
// ), // ),
// ), // ),
// Pangea# // Pangea#
hintText: L10n.of(context)!.password, hintText: '******',
labelText: L10n.of(context)!.password,
), ),
), ),
), ),
@ -175,14 +168,13 @@ class LoginView extends StatelessWidget {
? const LinearProgressIndicator() ? const LinearProgressIndicator()
: Text(L10n.of(context)!.login), : Text(L10n.of(context)!.login),
), ),
// child: ElevatedButton.icon( // child: ElevatedButton(
// style: ElevatedButton.styleFrom( // style: ElevatedButton.styleFrom(
// backgroundColor: theme.colorScheme.primary, // backgroundColor: theme.colorScheme.primary,
// foregroundColor: theme.colorScheme.onPrimary, // foregroundColor: theme.colorScheme.onPrimary,
// ), // ),
// onPressed: controller.loading ? null : controller.login, // onPressed: controller.loading ? null : controller.login,
// icon: const Icon(Icons.login_outlined), // child: controller.loading
// label: controller.loading
// ? const LinearProgressIndicator() // ? const LinearProgressIndicator()
// : Text(L10n.of(context)!.login), // : Text(L10n.of(context)!.login),
// ), // ),
@ -228,15 +220,14 @@ class LoginView extends StatelessWidget {
ElevatedButton.styleFrom(foregroundColor: Colors.red), ElevatedButton.styleFrom(foregroundColor: Colors.red),
child: Text(L10n.of(context)!.passwordForgotten), child: Text(L10n.of(context)!.passwordForgotten),
), ),
// child: TextButton.icon( // child: TextButton(
// onPressed: controller.loading // onPressed: controller.loading
// ? () {} // ? () {}
// : controller.passwordForgotten, // : controller.passwordForgotten,
// style: TextButton.styleFrom( // style: TextButton.styleFrom(
// foregroundColor: theme.colorScheme.error, // foregroundColor: theme.colorScheme.error,
// ), // ),
// icon: const Icon(Icons.safety_check_outlined), // child: Text(L10n.of(context)!.passwordForgotten),
// label: Text(L10n.of(context)!.passwordForgotten),
// ), // ),
// Pangea# // Pangea#
), ),

@ -1,27 +1,22 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:adaptive_dialog/adaptive_dialog.dart';
import 'package:archive/archive.dart'
if (dart.library.io) 'package:archive/archive_io.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:fluffychat/utils/client_manager.dart';
import 'package:fluffychat/widgets/app_lock.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:http/http.dart' hide Client;
import 'package:matrix/matrix.dart'; import 'package:matrix/matrix.dart';
import 'package:fluffychat/utils/client_manager.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_file_extension.dart';
import 'package:fluffychat/widgets/app_lock.dart';
import '../../widgets/matrix.dart'; import '../../widgets/matrix.dart';
import 'import_archive_dialog.dart'; import 'import_archive_dialog.dart';
import 'settings_emotes_view.dart'; import 'settings_emotes_view.dart';
import 'package:archive/archive.dart'
if (dart.library.io) 'package:archive/archive_io.dart';
class EmotesSettings extends StatefulWidget { class EmotesSettings extends StatefulWidget {
const EmotesSettings({super.key}); const EmotesSettings({super.key});
@ -320,38 +315,42 @@ class EmotesSettingsController extends State<EmotesSettings> {
} }
Future<void> exportAsZip() async { Future<void> exportAsZip() async {
final client = Matrix.of(context).client; // TODO update matrix SDK to support this
// final client = Matrix.of(context).client;
await showFutureLoadingDialog(
context: context, // await showFutureLoadingDialog(
future: () async { // context: context,
final pack = _getPack(); // future: () async {
final archive = Archive(); // final pack = _getPack();
for (final entry in pack.images.entries) { // final archive = Archive();
final emote = entry.value; // for (final entry in pack.images.entries) {
final name = entry.key; // final emote = entry.value;
final url = emote.url.getDownloadLink(client); // final name = entry.key;
final response = await get(url); // final url = await emote.url.getDownloadUri(client);
// final response = await get(
archive.addFile( // url,
ArchiveFile( // headers: {'authorization': 'Bearer ${client.accessToken}'},
name, // );
response.bodyBytes.length,
response.bodyBytes, // archive.addFile(
), // ArchiveFile(
); // name,
} // response.bodyBytes.length,
final fileName = // response.bodyBytes,
'${pack.pack.displayName ?? client.userID?.localpart ?? 'emotes'}.zip'; // ),
final output = ZipEncoder().encode(archive); // );
// }
if (output == null) return; // final fileName =
// '${pack.pack.displayName ?? client.userID?.localpart ?? 'emotes'}.zip';
MatrixFile( // final output = ZipEncoder().encode(archive);
name: fileName,
bytes: Uint8List.fromList(output), // if (output == null) return;
).save(context);
}, // MatrixFile(
); // name: fileName,
// bytes: Uint8List.fromList(output),
// ).save(context);
// },
// );
} }
} }

@ -0,0 +1,55 @@
import 'dart:typed_data';
import 'package:matrix/matrix.dart';
extension ClientDownloadContentExtension on Client {
Future<Uint8List> downloadMxcCached(
Uri mxc, {
num? width,
num? height,
bool isThumbnail = false,
bool? animated,
ThumbnailMethod? thumbnailMethod,
}) async {
// // To stay compatible with previous storeKeys:
// final cacheKey = isThumbnail
// // ignore: deprecated_member_use
// ? mxc.getThumbnail(
// this,
// width: width,
// height: height,
// animated: animated,
// method: thumbnailMethod!,
// )
// : mxc;
// final cachedData = await database?.getFile(cacheKey);
// if (cachedData != null) return cachedData;
// final httpUri = isThumbnail
// ? await mxc.getThumbnailUri(
// this,
// width: width,
// height: height,
// animated: animated,
// method: thumbnailMethod,
// )
// : await mxc.getDownloadUri(this);
// final response = await httpClient.get(
// httpUri,
// headers:
// accessToken == null ? null : {'authorization': 'Bearer $accessToken'},
// );
// if (response.statusCode != 200) {
// throw Exception();
// }
// final remoteData = response.bodyBytes;
// await database?.storeFile(cacheKey, remoteData, 0);
// return remoteData;
// TODO update matrix SDK to support this
return Uint8List.fromList([]);
}
}

@ -1,16 +1,15 @@
import 'dart:io';
import 'dart:ui'; import 'dart:ui';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:fluffychat/utils/client_download_content_extension.dart';
import 'package:fluffychat/utils/client_manager.dart'; import 'package:fluffychat/utils/client_manager.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/utils/voip/callkeep_manager.dart'; import 'package:fluffychat/utils/voip/callkeep_manager.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter_shortcuts/flutter_shortcuts.dart'; import 'package:flutter_shortcuts/flutter_shortcuts.dart';
@ -179,28 +178,25 @@ Future<void> _tryPushHelper(
); );
// The person object for the android message style notification // The person object for the android message style notification
final avatar = event.room.avatar final avatar = event.room.avatar;
?.getThumbnail(
client,
width: 256,
height: 256,
)
.toString();
final senderAvatar = event.room.isDirectChat final senderAvatar = event.room.isDirectChat
? avatar ? avatar
: event.senderFromMemoryOrFallback.avatarUrl : event.senderFromMemoryOrFallback.avatarUrl;
?.getThumbnail(
client,
width: 256,
height: 256,
)
.toString();
File? roomAvatarFile, senderAvatarFile; Uint8List? roomAvatarFile, senderAvatarFile;
try { try {
roomAvatarFile = avatar == null roomAvatarFile = avatar == null
? null ? null
: await DefaultCacheManager().getSingleFile(avatar); : await client
.downloadMxcCached(
avatar,
thumbnailMethod: ThumbnailMethod.scale,
width: 256,
height: 256,
animated: false,
isThumbnail: true,
)
.timeout(const Duration(seconds: 3));
} catch (e, s) { } catch (e, s) {
Logs().e('Unable to get avatar picture', e, s); Logs().e('Unable to get avatar picture', e, s);
// #Pangea // #Pangea
@ -212,7 +208,16 @@ Future<void> _tryPushHelper(
? roomAvatarFile ? roomAvatarFile
: senderAvatar == null : senderAvatar == null
? null ? null
: await DefaultCacheManager().getSingleFile(senderAvatar); : await client
.downloadMxcCached(
senderAvatar,
thumbnailMethod: ThumbnailMethod.scale,
width: 256,
height: 256,
animated: false,
isThumbnail: true,
)
.timeout(const Duration(seconds: 3));
} catch (e, s) { } catch (e, s) {
Logs().e('Unable to get avatar picture', e, s); Logs().e('Unable to get avatar picture', e, s);
} }
@ -230,7 +235,7 @@ Future<void> _tryPushHelper(
name: event.senderFromMemoryOrFallback.calcDisplayname(), name: event.senderFromMemoryOrFallback.calcDisplayname(),
icon: senderAvatarFile == null icon: senderAvatarFile == null
? null ? null
: BitmapFilePathAndroidIcon(senderAvatarFile.path), : ByteArrayAndroidIcon(senderAvatarFile),
), ),
); );
@ -277,7 +282,7 @@ Future<void> _tryPushHelper(
name: event.senderFromMemoryOrFallback.calcDisplayname(), name: event.senderFromMemoryOrFallback.calcDisplayname(),
icon: roomAvatarFile == null icon: roomAvatarFile == null
? null ? null
: BitmapFilePathAndroidIcon(roomAvatarFile.path), : ByteArrayAndroidIcon(roomAvatarFile),
key: event.roomId, key: event.roomId,
important: event.room.isFavourite, important: event.room.isFavourite,
), ),
@ -326,7 +331,7 @@ Future<void> _setShortcut(
Event event, Event event,
L10n l10n, L10n l10n,
String title, String title,
File? avatarFile, Uint8List? avatarFile,
) async { ) async {
final flutterShortcuts = FlutterShortcuts(); final flutterShortcuts = FlutterShortcuts();
await flutterShortcuts.initialize(debug: !kReleaseMode); await flutterShortcuts.initialize(debug: !kReleaseMode);
@ -338,8 +343,7 @@ Future<void> _setShortcut(
conversationShortcut: true, conversationShortcut: true,
icon: avatarFile == null icon: avatarFile == null
? null ? null
: ShortcutMemoryIcon(jpegImage: await avatarFile.readAsBytes()) : ShortcutMemoryIcon(jpegImage: avatarFile).toString(),
.toString(),
shortcutIconAsset: avatarFile == null shortcutIconAsset: avatarFile == null
? ShortcutIconAsset.androidAsset ? ShortcutIconAsset.androidAsset
: ShortcutIconAsset.memoryAsset, : ShortcutIconAsset.memoryAsset,

@ -19,12 +19,10 @@ class LoginScaffold extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// #Pangea final theme = Theme.of(context);
// final theme = Theme.of(context);
// final isMobileMode = final isMobileMode =
// enforceMobileMode || !FluffyThemes.isColumnMode(context); enforceMobileMode || !FluffyThemes.isColumnMode(context);
// Pangea#
final scaffold = Scaffold( final scaffold = Scaffold(
key: const Key('LoginScaffold'), key: const Key('LoginScaffold'),
// Pangea# // Pangea#
@ -99,7 +97,7 @@ class LoginScaffold extends StatelessWidget {
// child: ConstrainedBox( // child: ConstrainedBox(
// constraints: isMobileMode // constraints: isMobileMode
// ? const BoxConstraints() // ? const BoxConstraints()
// : const BoxConstraints(maxWidth: 480, maxHeight: 720), // : const BoxConstraints(maxWidth: 480, maxHeight: 640),
// child: BackdropFilter( // child: BackdropFilter(
// filter: ImageFilter.blur( // filter: ImageFilter.blur(
// sigmaX: 10.0, // sigmaX: 10.0,

@ -6,12 +6,11 @@ import 'package:flutter/material.dart';
import 'package:desktop_notifications/desktop_notifications.dart'; import 'package:desktop_notifications/desktop_notifications.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:http/http.dart' as http;
import 'package:matrix/matrix.dart'; import 'package:matrix/matrix.dart';
import 'package:path_provider/path_provider.dart';
import 'package:universal_html/html.dart' as html; import 'package:universal_html/html.dart' as html;
import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/utils/client_download_content_extension.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/matrix.dart';
@ -48,50 +47,38 @@ extension LocalNotificationsExtension on MatrixState {
hideEdit: true, hideEdit: true,
removeMarkdown: true, removeMarkdown: true,
); );
final icon = event.senderFromMemoryOrFallback.avatarUrl?.getThumbnail(
client, if (kIsWeb) {
width: 64, final avatarUrl = event.senderFromMemoryOrFallback.avatarUrl;
height: 64,
method: ThumbnailMethod.crop, final iconBytes = avatarUrl == null
) ?? ? null
room.avatar?.getThumbnail( : await client.downloadMxcCached(
client, avatarUrl,
width: 64, width: 64,
height: 64, height: 64,
method: ThumbnailMethod.crop, thumbnailMethod: ThumbnailMethod.crop,
isThumbnail: true,
animated: false,
); );
if (kIsWeb) {
_audioPlayer.play(); _audioPlayer.play();
html.Notification( html.Notification(
title, title,
body: body, body: body,
icon: icon.toString(), icon: iconBytes == null
? null
: html.Url.createObjectUrl(html.Blob(iconBytes)),
tag: event.room.id,
); );
} else if (Platform.isLinux) { } else if (Platform.isLinux) {
final appIconUrl = room.avatar?.getThumbnail(
room.client,
width: 56,
height: 56,
);
File? appIconFile;
if (appIconUrl != null) {
final tempDirectory = await getApplicationSupportDirectory();
final avatarDirectory =
await Directory('${tempDirectory.path}/notiavatars/').create();
appIconFile = File(
'${avatarDirectory.path}/${Uri.encodeComponent(appIconUrl.toString())}',
);
if (await appIconFile.exists() == false) {
final response = await http.get(appIconUrl);
await appIconFile.writeAsBytes(response.bodyBytes);
}
}
final notification = await linuxNotifications!.notify( final notification = await linuxNotifications!.notify(
title, title,
body: body, body: body,
replacesId: linuxNotificationIds[roomId] ?? 0, replacesId: linuxNotificationIds[roomId] ?? 0,
appName: AppConfig.applicationName, appName: AppConfig.applicationName,
appIcon: appIconFile?.path ?? '', appIcon: 'fluffychat',
actions: [ actions: [
NotificationAction( NotificationAction(
DesktopNotificationActions.openChat.name, DesktopNotificationActions.openChat.name,

@ -2,10 +2,10 @@ import 'dart:typed_data';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:matrix/matrix.dart'; import 'package:matrix/matrix.dart';
import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/utils/client_download_content_extension.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_file_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_file_extension.dart';
import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/matrix.dart';
@ -63,8 +63,6 @@ class _MxcImageState extends State<MxcImage> {
: _imageDataCache[cacheKey] = data; : _imageDataCache[cacheKey] = data;
} }
bool? _isCached;
Future<void> _load() async { Future<void> _load() async {
final client = widget.client ?? Matrix.of(context).client; final client = widget.client ?? Matrix.of(context).client;
final uri = widget.uri; final uri = widget.uri;
@ -77,45 +75,18 @@ class _MxcImageState extends State<MxcImage> {
final height = widget.height; final height = widget.height;
final realHeight = height == null ? null : height * devicePixelRatio; final realHeight = height == null ? null : height * devicePixelRatio;
final httpUri = widget.isThumbnail final remoteData = await client.downloadMxcCached(
? uri.getThumbnail( uri,
client,
width: realWidth, width: realWidth,
height: realHeight, height: realHeight,
thumbnailMethod: widget.thumbnailMethod,
isThumbnail: widget.isThumbnail,
animated: widget.animated, animated: widget.animated,
method: widget.thumbnailMethod, );
)
: uri.getDownloadLink(client);
final storeKey = widget.isThumbnail ? httpUri : uri;
if (_isCached == null) {
final cachedData = await client.database?.getFile(storeKey);
if (cachedData != null) {
if (!mounted) return;
setState(() {
_imageData = cachedData;
_isCached = true;
});
return;
}
_isCached = false;
}
final response = await http.get(httpUri);
if (response.statusCode != 200) {
if (response.statusCode == 404) {
return;
}
throw Exception();
}
final remoteData = response.bodyBytes;
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {
_imageData = remoteData; _imageData = remoteData;
}); });
await client.database?.storeFile(storeKey, remoteData, 0);
} }
if (event != null) { if (event != null) {
@ -179,7 +150,6 @@ class _MxcImageState extends State<MxcImage> {
filterQuality: filterQuality:
widget.isThumbnail ? FilterQuality.low : FilterQuality.medium, widget.isThumbnail ? FilterQuality.low : FilterQuality.medium,
errorBuilder: (context, __, ___) { errorBuilder: (context, __, ___) {
_isCached = false;
_imageData = null; _imageData = null;
WidgetsBinding.instance.addPostFrameCallback(_tryLoad); WidgetsBinding.instance.addPostFrameCallback(_tryLoad);
return placeholder(context); return placeholder(context);

@ -553,10 +553,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_cache_manager name: flutter_cache_manager
sha256: "8207f27539deb83732fdda03e259349046a39a4c767269285f449ade355d54ba" sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.3.1" version: "3.4.1"
flutter_dotenv: flutter_dotenv:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1043,10 +1043,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: http name: http
sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938" sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.1" version: "1.2.2"
http_multi_server: http_multi_server:
dependency: transitive dependency: transitive
description: description:
@ -1413,6 +1413,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.11.1" version: "0.11.1"
material_symbols_icons:
dependency: "direct main"
description:
name: material_symbols_icons
sha256: b72bf7566d024d51627dce81b1b98539830a0e3ffbb5784989aa3e97c8493160
url: "https://pub.dev"
source: hosted
version: "4.2784.0"
matrix: matrix:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1562,10 +1570,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: path_provider name: path_provider
sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.3" version: "2.1.4"
path_provider_android: path_provider_android:
dependency: transitive dependency: transitive
description: description:
@ -2599,10 +2607,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: vm_service name: vm_service
sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "14.2.4" version: "14.2.5"
wakelock_plus: wakelock_plus:
dependency: "direct main" dependency: "direct main"
description: description:
@ -2671,10 +2679,10 @@ packages:
dependency: "direct overridden" dependency: "direct overridden"
description: description:
name: win32 name: win32
sha256: "0eaf06e3446824099858367950a813472af675116bf63f008a4c2a75ae13e9cb" sha256: "015002c060f1ae9f41a818f2d5640389cc05283e368be19dc8d77cecb43c40c9"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.5.0" version: "5.5.3"
win32_registry: win32_registry:
dependency: transitive dependency: transitive
description: description:

@ -35,7 +35,7 @@ dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
flutter_app_badger: ^1.5.0 flutter_app_badger: ^1.5.0
flutter_cache_manager: ^3.3.1 flutter_cache_manager: ^3.4.1
flutter_foreground_task: ^6.1.3 flutter_foreground_task: ^6.1.3
flutter_highlighter: ^0.1.1 flutter_highlighter: ^0.1.1
flutter_html: ^3.0.0-beta.2 flutter_html: ^3.0.0-beta.2
@ -216,4 +216,4 @@ dependency_overrides:
git: git:
url: https://github.com/TheOneWithTheBraid/keyboard_shortcuts.git url: https://github.com/TheOneWithTheBraid/keyboard_shortcuts.git
ref: null-safety ref: null-safety
win32: 5.5.0 win32: 5.5.3

@ -1,9 +1,7 @@
#!/bin/sh -ve #!/bin/sh -ve
rm -r assets/js/package rm -r assets/js/package
mkdir -p assets/js/package
touch assets/js/package/.gitkeep
OLM_VERSION=$(cat pubspec.yaml | yq -r .dependencies.flutter_olm) OLM_VERSION=$(cat pubspec.yaml | yq .dependencies.flutter_olm)
DOWNLOAD_PATH="https://github.com/famedly/olm/releases/download/v$OLM_VERSION/olm.zip" DOWNLOAD_PATH="https://github.com/famedly/olm/releases/download/v$OLM_VERSION/olm.zip"
cd assets/js/ && curl -L $DOWNLOAD_PATH > olm.zip && cd ../../ cd assets/js/ && curl -L $DOWNLOAD_PATH > olm.zip && cd ../../

Loading…
Cancel
Save